vendor: 11,165 bytes, lines 1-388
1/*global self, document, DOMException */ 2 3/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ 4 5// Full polyfill for browsers with no classList support 6if (!("classList" in document.createElement("_"))) { 7 (function (view) { 8 9 "use strict"; 10 11 if (!('Element' in view)) return; 12 13 var 14 classListProp = "classList" 15 , protoProp = "prototype" 16 , elemCtrProto = view.Element[protoProp] 17 , objCtr = Object 18 , strTrim = String[protoProp].trim || function () { 19 return this.replace(/^\s+|\s+$/g, ""); 20 } 21 , arrIndexOf = Array[protoProp].indexOf || function (item) { 22 var 23 i = 0 24 , len = this.length 25 ; 26 for (; i < len; i++) { 27 if (i in this && this[i] === item) { 28 return i; 29 } 30 } 31 return -1; 32 } 33 // Vendors: please allow content code to instantiate DOMExceptions 34 , DOMEx = function (type, message) { 35 this.name = type; 36 this.code = DOMException[type]; 37 this.message = message; 38 } 39 , checkTokenAndGetIndex = function (classList, token) { 40 if (token === "") { 41 throw new DOMEx( 42 "SYNTAX_ERR" 43 , "An invalid or illegal string was specified" 44 ); 45 } 46 if (/\s/.test(token)) { 47 throw new DOMEx( 48 "INVALID_CHARACTER_ERR" 49 , "String contains an invalid character" 50 ); 51 } 52 return arrIndexOf.call(classList, token); 53 } 54 , ClassList = function (elem) { 55 var 56 trimmedClasses = strTrim.call(elem.getAttribute("class") || "") 57 , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] 58 , i = 0 59 , len = classes.length 60 ; 61 for (; i < len; i++) { 62 this.push(classes[i]); 63 } 64 this._updateClassName = function () { 65 elem.setAttribute("class", this.toString()); 66 }; 67 } 68 , classListProto = ClassList[protoProp] = [] 69 , classListGetter = function () { 70 return new ClassList(this); 71 } 72 ; 73 // Most DOMException implementations don't allow calling DOMException's toString() 74 // on non-DOMExceptions. Error's toString() is sufficient here. 75 DOMEx[protoProp] = Error[protoProp]; 76 classListProto.item = function (i) { 77 return this[i] || null; 78 }; 79 classListProto.contains = function (token) { 80 token += ""; 81 return checkTokenAndGetIndex(this, token) !== -1; 82 }; 83 classListProto.add = function () { 84 var 85 tokens = arguments 86 , i = 0 87 , l = tokens.length 88 , token 89 , updated = false 90 ; 91 do { 92 token = tokens[i] + ""; 93 if (checkTokenAndGetIndex(this, token) === -1) { 94 this.push(token); 95 updated = true; 96 } 97 } 98 while (++i < l); 99 100 if (updated) { 101 this._updateClassName(); 102 } 103 }; 104 classListProto.remove = function () { 105 var 106 tokens = arguments 107 , i = 0 108 , l = tokens.length 109 , token 110 , updated = false 111 , index 112 ; 113 do { 114 token = tokens[i] + ""; 115 index = checkTokenAndGetIndex(this, token); 116 while (index !== -1) { 117 this.splice(index, 1); 118 updated = true; 119 index = checkTokenAndGetIndex(this, token); 120 } 121 } 122 while (++i < l); 123 124 if (updated) { 125 this._updateClassName(); 126 } 127 }; 128 classListProto.toggle = function (token, force) { 129 token += ""; 130 131 var 132 result = this.contains(token) 133 , method = result ? 134 force !== true && "remove" 135 : 136 force !== false && "add" 137 ; 138 139 if (method) { 140 this[method](token); 141 } 142 143 if (force === true || force === false) { 144 return force; 145 } else { 146 return !result; 147 } 148 }; 149 classListProto.toString = function () { 150 return this.join(" "); 151 }; 152 153 if (objCtr.defineProperty) { 154 var classListPropDesc = { 155 get: classListGetter 156 , enumerable: true 157 , configurable: true 158 }; 159 try { 160 objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 161 } catch (ex) { // IE 8 doesn't support enumerable:true 162 if (ex.number === -0x7FF5EC54) { 163 classListPropDesc.enumerable = false; 164 objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 165 } 166 } 167 } else if (objCtr[protoProp].__defineGetter__) { 168 elemCtrProto.__defineGetter__(classListProp, classListGetter); 169 } 170 171 }(self)); 172} 173 174/* Blob.js 175 * A Blob implementation. 176 * 2014-07-24 177 * 178 * By Eli Grey, http://eligrey.com 179 * By Devin Samarin, https://github.com/dsamarin 180 * License: X11/MIT 181 * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md 182 */ 183 184/*global self, unescape */ 185/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 186 plusplus: true */ 187 188/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ 189 190(function (view) { 191 "use strict"; 192 193 view.URL = view.URL || view.webkitURL; 194 195 if (view.Blob && view.URL) { 196 try { 197 new Blob; 198 return; 199 } catch (e) {} 200 } 201 202 // Internally we use a BlobBuilder implementation to base Blob off of 203 // in order to support older browsers that only have BlobBuilder 204 var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { 205 var 206 get_class = function(object) { 207 return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; 208 } 209 , FakeBlobBuilder = function BlobBuilder() { 210 this.data = []; 211 } 212 , FakeBlob = function Blob(data, type, encoding) { 213 this.data = data; 214 this.size = data.length; 215 this.type = type; 216 this.encoding = encoding; 217 } 218 , FBB_proto = FakeBlobBuilder.prototype 219 , FB_proto = FakeBlob.prototype 220 , FileReaderSync = view.FileReaderSync 221 , FileException = function(type) { 222 this.code = this[this.name = type]; 223 } 224 , file_ex_codes = ( 225 "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " 226 + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" 227 ).split(" ") 228 , file_ex_code = file_ex_codes.length 229 , real_URL = view.URL || view.webkitURL || view 230 , real_create_object_URL = real_URL.createObjectURL 231 , real_revoke_object_URL = real_URL.revokeObjectURL 232 , URL = real_URL 233 , btoa = view.btoa 234 , atob = view.atob 235 236 , ArrayBuffer = view.ArrayBuffer 237 , Uint8Array = view.Uint8Array 238 239 , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ 240 ; 241 FakeBlob.fake = FB_proto.fake = true; 242 while (file_ex_code--) { 243 FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; 244 } 245 // Polyfill URL 246 if (!real_URL.createObjectURL) { 247 URL = view.URL = function(uri) { 248 var 249 uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") 250 , uri_origin 251 ; 252 uri_info.href = uri; 253 if (!("origin" in uri_info)) { 254 if (uri_info.protocol.toLowerCase() === "data:") { 255 uri_info.origin = null; 256 } else { 257 uri_origin = uri.match(origin); 258 uri_info.origin = uri_origin && uri_origin[1]; 259 } 260 } 261 return uri_info; 262 }; 263 } 264 URL.createObjectURL = function(blob) { 265 var 266 type = blob.type 267 , data_URI_header 268 ; 269 if (type === null) { 270 type = "application/octet-stream"; 271 } 272 if (blob instanceof FakeBlob) { 273 data_URI_header = "data:" + type; 274 if (blob.encoding === "base64") { 275 return data_URI_header + ";base64," + blob.data; 276 } else if (blob.encoding === "URI") { 277 return data_URI_header + "," + decodeURIComponent(blob.data); 278 } if (btoa) { 279 return data_URI_header + ";base64," + btoa(blob.data); 280 } else { 281 return data_URI_header + "," + encodeURIComponent(blob.data); 282 } 283 } else if (real_create_object_URL) { 284 return real_create_object_URL.call(real_URL, blob); 285 } 286 }; 287 URL.revokeObjectURL = function(object_URL) { 288 if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { 289 real_revoke_object_URL.call(real_URL, object_URL); 290 } 291 }; 292 FBB_proto.append = function(data/*, endings*/) { 293 var bb = this.data; 294 // decode data to a binary string 295 if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { 296 var 297 str = "" 298 , buf = new Uint8Array(data) 299 , i = 0 300 , buf_len = buf.length 301 ; 302 for (; i < buf_len; i++) { 303 str += String.fromCharCode(buf[i]); 304 } 305 bb.push(str); 306 } else if (get_class(data) === "Blob" || get_class(data) === "File") { 307 if (FileReaderSync) { 308 var fr = new FileReaderSync; 309 bb.push(fr.readAsBinaryString(data)); 310 } else { 311 // async FileReader won't work as BlobBuilder is sync 312 throw new FileException("NOT_READABLE_ERR"); 313 } 314 } else if (data instanceof FakeBlob) { 315 if (data.encoding === "base64" && atob) { 316 bb.push(atob(data.data)); 317 } else if (data.encoding === "URI") { 318 bb.push(decodeURIComponent(data.data)); 319 } else if (data.encoding === "raw") { 320 bb.push(data.data); 321 } 322 } else { 323 if (typeof data !== "string") { 324 data += ""; // convert unsupported types to strings 325 } 326 // decode UTF-16 to binary string 327 bb.push(unescape(encodeURIComponent(data))); 328 } 329 }; 330 FBB_proto.getBlob = function(type) { 331 if (!arguments.length) { 332 type = null; 333 } 334 return new FakeBlob(this.data.join(""), type, "raw"); 335 }; 336 FBB_proto.toString = function() { 337 return "[object BlobBuilder]"; 338 }; 339 FB_proto.slice = function(start, end, type) { 340 var args = arguments.length; 341 if (args < 3) { 342 type = null; 343 } 344 return new FakeBlob( 345 this.data.slice(start, args > 1 ? end : this.data.length) 346 , type 347 , this.encoding 348 ); 349 }; 350 FB_proto.toString = function() { 351 return "[object Blob]"; 352 }; 353 FB_proto.close = function() { 354 this.size = 0; 355 delete this.data; 356 }; 357 return FakeBlobBuilder; 358 }(view)); 359 360 view.Blob = function(blobParts, options) { 361 var type = options ? (options.type || "") : ""; 362 var builder = new BlobBuilder(); 363 if (blobParts) { 364 for (var i = 0, len = blobParts.length; i < len; i++) { 365 if (Uint8Array && blobParts[i] instanceof Uint8Array) { 366 builder.append(blobParts[i].buffer); 367 } 368 else { 369 builder.append(blobParts[i]); 370 } 371 } 372 } 373 var blob = builder.getBlob(type); 374 if (!blob.slice && blob.webkitSlice) { 375 blob.slice = blob.webkitSlice; 376 } 377 return blob; 378 }; 379 380 var getPrototypeOf = Object.getPrototypeOf || function(object) { 381 return object.__proto__; 382 }; 383 view.Blob.prototype = getPrototypeOf(new view.Blob()); 384}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); 385 386(function (root, factory) { 387 'use strict'; 388
vendor: 10,256 bytes, lines 388-638
388if (typeof module === 'object') { 389 module.exports = factory; 390 } else if (typeof define === 'function' && define.amd) { 391 define(function () { 392 return factory; 393 }); 394 } else { 395 root.MediumEditor = factory; 396 } 397}(this, function () { 398 399 'use strict'; 400 401function MediumEditor(elements, options) { 402 'use strict'; 403 return this.init(elements, options); 404} 405 406MediumEditor.extensions = {}; 407/*jshint unused: true */ 408(function (window) { 409 'use strict'; 410 411 function copyInto(overwrite, dest) { 412 var prop, 413 sources = Array.prototype.slice.call(arguments, 2); 414 dest = dest || {}; 415 for (var i = 0; i < sources.length; i++) { 416 var source = sources[i]; 417 if (source) { 418 for (prop in source) { 419 if (source.hasOwnProperty(prop) && 420 typeof source[prop] !== 'undefined' && 421 (overwrite || dest.hasOwnProperty(prop) === false)) { 422 dest[prop] = source[prop]; 423 } 424 } 425 } 426 } 427 return dest; 428 } 429 430 // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains 431 // Some browsers (including phantom) don't return true for Node.contains(child) 432 // if child is a text node. Detect these cases here and use a fallback 433 // for calls to Util.isDescendant() 434 var nodeContainsWorksWithTextNodes = false; 435 try { 436 var testParent = document.createElement('div'), 437 testText = document.createTextNode(' '); 438 testParent.appendChild(testText); 439 nodeContainsWorksWithTextNodes = testParent.contains(testText); 440 } catch (exc) {} 441 442 var Util = { 443 444 // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562 445 // by rg89 446 isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))), 447 448 // http://stackoverflow.com/a/11752084/569101 449 isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0), 450 451 // https://github.com/jashkenas/underscore 452 keyCode: { 453 BACKSPACE: 8, 454 TAB: 9, 455 ENTER: 13, 456 ESCAPE: 27, 457 SPACE: 32, 458 DELETE: 46, 459 K: 75, // K keycode, and not k 460 M: 77 461 }, 462 463 /** 464 * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac. 465 * See #591 466 */ 467 isMetaCtrlKey: function (event) { 468 if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) { 469 return true; 470 } 471 472 return false; 473 }, 474 475 /** 476 * Returns true if the key associated to the event is inside keys array 477 * 478 * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484 479 * @see : http://stackoverflow.com/q/4471582/569101 480 */ 481 isKey: function (event, keys) { 482 var keyCode = Util.getKeyCode(event); 483 484 // it's not an array let's just compare strings! 485 if (false === Array.isArray(keys)) { 486 return keyCode === keys; 487 } 488 489 if (-1 === keys.indexOf(keyCode)) { 490 return false; 491 } 492 493 return true; 494 }, 495 496 getKeyCode: function (event) { 497 var keyCode = event.which; 498 499 // getting the key code from event 500 if (null === keyCode) { 501 keyCode = event.charCode !== null ? event.charCode : event.keyCode; 502 } 503 504 return keyCode; 505 }, 506 507 blockContainerElementNames: [ 508 // elements our editor generates 509 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol', 510 // all other known block elements 511 'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset', 512 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav', 513 'noscript', 'output', 'section', 'video', 514 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td' 515 ], 516 517 emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'], 518 519 extend: function extend(/* dest, source1, source2, ...*/) { 520 var args = [true].concat(Array.prototype.slice.call(arguments)); 521 return copyInto.apply(this, args); 522 }, 523 524 defaults: function defaults(/*dest, source1, source2, ...*/) { 525 var args = [false].concat(Array.prototype.slice.call(arguments)); 526 return copyInto.apply(this, args); 527 }, 528 529 /* 530 * Create a link around the provided text nodes which must be adjacent to each other and all be 531 * descendants of the same closest block container. If the preconditions are not met, unexpected 532 * behavior will result. 533 */ 534 createLink: function (document, textNodes, href, target) { 535 var anchor = document.createElement('a'); 536 Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor); 537 anchor.setAttribute('href', href); 538 if (target) { 539 anchor.setAttribute('target', target); 540 } 541 return anchor; 542 }, 543 544 /* 545 * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the 546 * textContent of the provided element argument, modify the DOM inside element to ensure that the text 547 * identified by the provided match can be returned as text nodes that contain exactly that text, without 548 * any additional text at the beginning or end of the returned array of adjacent text nodes. 549 * 550 * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are 551 * not affected in any way. 552 */ 553 findOrCreateMatchingTextNodes: function (document, element, match) { 554 var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false), 555 matchedNodes = [], 556 currentTextIndex = 0, 557 startReached = false, 558 currentNode = null, 559 newNode = null; 560 561 while ((currentNode = treeWalker.nextNode()) !== null) { 562 if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) { 563 startReached = true; 564 newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex); 565 } 566 if (startReached) { 567 Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex); 568 } 569 if (startReached && currentTextIndex === match.end) { 570 break; // Found the node(s) corresponding to the link. Break out and move on to the next. 571 } else if (startReached && currentTextIndex > (match.end + 1)) { 572 throw new Error('PerformLinking overshot the target!'); // should never happen... 573 } 574 575 if (startReached) { 576 matchedNodes.push(newNode || currentNode); 577 } 578 579 currentTextIndex += currentNode.nodeValue.length; 580 if (newNode !== null) { 581 currentTextIndex += newNode.nodeValue.length; 582 // Skip the newNode as we'll already have pushed it to the matches 583 treeWalker.nextNode(); 584 } 585 newNode = null; 586 } 587 return matchedNodes; 588 }, 589 590 /* 591 * Given the provided text node and text coordinates, split the text node if needed to make it align 592 * precisely with the coordinates. 593 * 594 * This function is intended to be called from Util.findOrCreateMatchingTextNodes. 595 */ 596 splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) { 597 if (matchStartIndex !== currentTextIndex) { 598 return currentNode.splitText(matchStartIndex - currentTextIndex); 599 } 600 return null; 601 }, 602 603 /* 604 * Given the provided text node and text coordinates, split the text node if needed to make it align 605 * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded, 606 * if that function has been called on the same currentNode. 607 * 608 * This function is intended to be called from Util.findOrCreateMatchingTextNodes. 609 */ 610 splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) { 611 var textIndexOfEndOfFarthestNode, 612 endSplitPoint; 613 textIndexOfEndOfFarthestNode = currentTextIndex + (newNode || currentNode).nodeValue.length + 614 (newNode ? currentNode.nodeValue.length : 0) - 615 1; 616 endSplitPoint = (newNode || currentNode).nodeValue.length - 617 (textIndexOfEndOfFarthestNode + 1 - matchEndIndex); 618 if (textIndexOfEndOfFarthestNode >= matchEndIndex && 619 currentTextIndex !== textIndexOfEndOfFarthestNode && 620 endSplitPoint !== 0) { 621 (newNode || currentNode).splitText(endSplitPoint); 622 } 623 }, 624 625 /* 626 * Take an element, and break up all of its text content into unique pieces such that: 627 * 1) All text content of the elements are in separate blocks. No piece of text content should span 628 * span multiple blocks. This means no element return by this function should have 629 * any blocks as children. 630 * 2) The union of the textcontent of all of the elements returned here covers all 631 * of the text within the element. 632 * 633 * 634 * EXAMPLE: 635 * In the event that we have something like: 636 * 637 * <blockquote> 638 * <p>Some Text</p>
639 * <ol> 640 * <li>List Item 1</li> 641 * <li>List Item 2</li> 642 * </ol> 643 * </blockquote> 644 * 645 * This function would return these elements as an array: 646 * [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ] 647 * 648 * Since the <blockquote> and <ol> elements contain blocks within them they are not returned. 649 * Since the <p> and <li>'s don't contain block elements and cover all the text content of the 650 * <blockquote> container, they are the elements returned. 651 */ 652 splitByBlockElements: function (element) { 653 var toRet = [], 654 blockElementQuery = MediumEditor.util.blockContainerElementNames.join(','); 655 656 if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) { 657 return [element]; 658 } 659 660 for (var i = 0; i < element.childNodes.length; i++) { 661 var child = element.childNodes[i]; 662 if (child.nodeType === 3) { 663 toRet.push(child); 664 } else { 665 var blockElements = child.querySelectorAll(blockElementQuery); 666 if (blockElements.length === 0) { 667 toRet.push(child); 668 } else { 669 toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child)); 670 } 671 } 672 } 673 674 return toRet; 675 }, 676 677 // Find the next node in the DOM tree that represents any text that is being 678 // displayed directly next to the targetNode (passed as an argument) 679 // Text that appears directly next to the current node can be: 680 // - A sibling text node 681 // - A descendant of a sibling element 682 // - A sibling text node of an ancestor 683 // - A descendant of a sibling element of an ancestor 684 findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) { 685 var pastTarget = false, 686 nextNode, 687 nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false); 688 689 // Use a native NodeIterator to iterate over all the text nodes that are descendants 690 // of the rootNode. Once past the targetNode, choose the first non-empty text node 691 nextNode = nodeIterator.nextNode(); 692 while (nextNode) { 693 if (nextNode === targetNode) { 694 pastTarget = true; 695 } else if (pastTarget) { 696 if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) { 697 break; 698 } 699 } 700 nextNode = nodeIterator.nextNode(); 701 } 702 703 return nextNode; 704 }, 705 706 isDescendant: function isDescendant(parent, child, checkEquality) { 707 if (!parent || !child) { 708 return false; 709 } 710 if (parent === child) { 711 return !!checkEquality; 712 } 713 // If parent is not an element, it can't have any descendants 714 if (parent.nodeType !== 1) { 715 return false; 716 } 717 if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) { 718 return parent.contains(child); 719 } 720 var node = child.parentNode; 721 while (node !== null) { 722 if (node === parent) { 723 return true; 724 } 725 node = node.parentNode; 726 } 727 return false; 728 }, 729 730 // https://github.com/jashkenas/underscore 731 isElement: function isElement(obj) { 732 return !!(obj && obj.nodeType === 1); 733 }, 734 735 // https://github.com/jashkenas/underscore 736 throttle: function (func, wait) { 737 var THROTTLE_INTERVAL = 50, 738 context, 739 args, 740 result, 741 timeout = null, 742 previous = 0, 743 later = function () { 744 previous = Date.now(); 745 timeout = null;
vendor: 4,303 bytes, lines 746-847
746 result = func.apply(context, args); 747 if (!timeout) { 748 context = args = null; 749 } 750 }; 751 752 if (!wait && wait !== 0) { 753 wait = THROTTLE_INTERVAL; 754 } 755 756 return function () { 757 var now = Date.now(), 758 remaining = wait - (now - previous); 759 760 context = this; 761 args = arguments; 762 if (remaining <= 0 || remaining > wait) { 763 if (timeout) { 764 clearTimeout(timeout); 765 timeout = null; 766 } 767 previous = now; 768 result = func.apply(context, args); 769 if (!timeout) { 770 context = args = null; 771 } 772 } else if (!timeout) { 773 timeout = setTimeout(later, remaining); 774 } 775 return result; 776 }; 777 }, 778 779 traverseUp: function (current, testElementFunction) { 780 if (!current) { 781 return false; 782 } 783 784 do { 785 if (current.nodeType === 1) { 786 if (testElementFunction(current)) { 787 return current; 788 } 789 // do not traverse upwards past the nearest containing editor 790 if (Util.isMediumEditorElement(current)) { 791 return false; 792 } 793 } 794 795 current = current.parentNode; 796 } while (current); 797 798 return false; 799 }, 800 801 htmlEntities: function (str) { 802 // converts special characters (like <) into their escaped/encoded values (like <). 803 // This allows you to show to display the string without the browser reading it as HTML. 804 return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 805 }, 806 807 // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div 808 insertHTMLCommand: function (doc, html) { 809 var selection, range, el, fragment, node, lastNode, toReplace; 810 811 if (doc.queryCommandSupported('insertHTML')) { 812 try { 813 return doc.execCommand('insertHTML', false, html); 814 } catch (ignore) {} 815 } 816 817 selection = doc.getSelection(); 818 if (selection.rangeCount) { 819 range = selection.getRangeAt(0); 820 toReplace = range.commonAncestorContainer; 821 822 // https://github.com/yabwe/medium-editor/issues/748 823 // If the selection is an empty editor element, create a temporary text node inside of the editor 824 // and select it so that we don't delete the editor element 825 if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) { 826 range.selectNode(toReplace.appendChild(doc.createTextNode(''))); 827 } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) || 828 (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) { 829 // Ensure range covers maximum amount of nodes as possible 830 // By moving up the DOM and selecting ancestors whose only child is the range 831 while (!Util.isMediumEditorElement(toReplace) && 832 toReplace.parentNode && 833 toReplace.parentNode.childNodes.length === 1 && 834 !Util.isMediumEditorElement(toReplace.parentNode)) { 835 toReplace = toReplace.parentNode; 836 } 837 range.selectNode(toReplace); 838 } 839 range.deleteContents(); 840 841 el = doc.createElement('div'); 842 el.innerHTML = html; 843 fragment = doc.createDocumentFragment(); 844 while (el.firstChild) { 845 node = el.firstChild; 846 lastNode = fragment.appendChild(node); 847 }
848 range.insertNode(fragment); 849 850 // Preserve the selection: 851 if (lastNode) { 852 range = range.cloneRange(); 853 range.setStartAfter(lastNode); 854 range.collapse(true); 855 selection.removeAllRanges(); 856 selection.addRange(range); 857 } 858 } 859 }, 860 861 execFormatBlock: function (doc, tagName) { 862 // Get the top level block element that contains the selection 863 var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)); 864 865 // Special handling for blockquote 866 if (tagName === 'blockquote') { 867 if (blockContainer) { 868 var childNodes = Array.prototype.slice.call(blockContainer.childNodes); 869 // Check if the blockquote has a block element as a child (nested blocks) 870 if (childNodes.some(function (childNode) { 871 return Util.isBlockContainer(childNode); 872 })) { 873 // FF handles blockquote differently on formatBlock 874 // allowing nesting, we need to use outdent 875 // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla 876 return doc.execCommand('outdent', false, null); 877 } 878 } 879 880 // When IE blockquote needs to be called as indent 881 // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777 882 if (Util.isIE) { 883 return doc.execCommand('indent', false, tagName); 884 } 885 } 886 887 // If the blockContainer is already the element type being passed in 888 // treat it as 'undo' formatting and just convert it to a <p> 889 if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) { 890 tagName = 'p'; 891 } 892 893 // When IE we need to add <> to heading elements 894 // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie 895 if (Util.isIE) { 896 tagName = '<' + tagName + '>'; 897 } 898 return doc.execCommand('formatBlock', false, tagName); 899 }, 900 901 /** 902 * Set target to blank on the given el element 903 * 904 * TODO: not sure if this should be here 905 * 906 * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link 907 * instead of the created link itself (as it is for Chrome for example), so we retrieve all "a" children to grab the good one by 908 * using `anchorUrl` to ensure that we are adding target="_blank" on the good one. 909 * This isn't a bulletproof solution anyway .. 910 */ 911 setTargetBlank: function (el, anchorUrl) { 912 var i, url = anchorUrl || false; 913 if (el.nodeName.toLowerCase() === 'a') { 914 el.target = '_blank'; 915 } else { 916 el = el.getElementsByTagName('a'); 917 918 for (i = 0; i < el.length; i += 1) { 919 if (false === url || url === el[i].attributes.href.value) { 920 el[i].target = '_blank'; 921 } 922 } 923 } 924 }, 925 926 addClassToAnchors: function (el, buttonClass) { 927 var classes = buttonClass.split(' '), 928 i, 929 j; 930 if (el.nodeName.toLowerCase() === 'a') { 931 for (j = 0; j < classes.length; j += 1) { 932 el.classList.add(classes[j]); 933 } 934 } else { 935 el = el.getElementsByTagName('a'); 936 for (i = 0; i < el.length; i += 1) { 937 for (j = 0; j < classes.length; j += 1) { 938 el[i].classList.add(classes[j]); 939 } 940 } 941 } 942 }, 943 944 isListItem: function (node) { 945 if (!node) { 946 return false;
vendor: 50,775 bytes, lines 947-2260
947 } 948 if (node.nodeName.toLowerCase() === 'li') { 949 return true; 950 } 951 952 var parentNode = node.parentNode, 953 tagName = parentNode.nodeName.toLowerCase(); 954 while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) { 955 if (tagName === 'li') { 956 return true; 957 } 958 parentNode = parentNode.parentNode; 959 if (parentNode) { 960 tagName = parentNode.nodeName.toLowerCase(); 961 } else { 962 return false; 963 } 964 } 965 return false; 966 }, 967 968 cleanListDOM: function (ownerDocument, element) { 969 if (element.nodeName.toLowerCase() !== 'li') { 970 return; 971 } 972 973 var list = element.parentElement; 974 975 if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up 976 Util.unwrap(list.parentElement, ownerDocument); 977 978 // move cursor at the end of the text inside the list 979 // for some unknown reason, the cursor is moved to end of the "visual" line 980 MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length); 981 } 982 }, 983 984 /* splitDOMTree 985 * 986 * Given a root element some descendant element, split the root element 987 * into its own element containing the descendant element and all elements 988 * on the left or right side of the descendant ('right' is default) 989 * 990 * example: 991 * 992 * <div> 993 * / | \ 994 * <span> <span> <span> 995 * / \ / \ / \ 996 * 1 2 3 4 5 6 997 * 998 * If I wanted to split this tree given the <div> as the root and "4" as the leaf 999 * the result would be (the prime ' marks indicates nodes that are created as clones): 1000 * 1001 * SPLITTING OFF 'RIGHT' TREE SPLITTING OFF 'LEFT' TREE 1002 * 1003 * <div> <div>' <div>' <div> 1004 * / \ / \ / \ | 1005 * <span> <span> <span>' <span> <span> <span> <span> 1006 * / \ | | / \ /\ /\ /\ 1007 * 1 2 3 4 5 6 1 2 3 4 5 6 1008 * 1009 * The above example represents splitting off the 'right' or 'left' part of a tree, where 1010 * the <div>' would be returned as an element not appended to the DOM, and the <div> 1011 * would remain in place where it was 1012 * 1013 */ 1014 splitOffDOMTree: function (rootNode, leafNode, splitLeft) { 1015 var splitOnNode = leafNode, 1016 createdNode = null, 1017 splitRight = !splitLeft; 1018 1019 // loop until we hit the root 1020 while (splitOnNode !== rootNode) { 1021 var currParent = splitOnNode.parentNode, 1022 newParent = currParent.cloneNode(false), 1023 targetNode = (splitRight ? splitOnNode : currParent.firstChild), 1024 appendLast; 1025 1026 // Create a new parent element which is a clone of the current parent 1027 if (createdNode) { 1028 if (splitRight) { 1029 // If we're splitting right, add previous created element before siblings 1030 newParent.appendChild(createdNode); 1031 } else { 1032 // If we're splitting left, add previous created element last 1033 appendLast = createdNode; 1034 } 1035 } 1036 createdNode = newParent; 1037 1038 while (targetNode) { 1039 var sibling = targetNode.nextSibling; 1040 // Special handling for the 'splitNode' 1041 if (targetNode === splitOnNode) { 1042 if (!targetNode.hasChildNodes()) { 1043 targetNode.parentNode.removeChild(targetNode); 1044 } else { 1045 // For the node we're splitting on, if it has children, we need to clone it 1046 // and not just move it 1047 targetNode = targetNode.cloneNode(false); 1048 } 1049 // If the resulting split node has content, add it 1050 if (targetNode.textContent) { 1051 createdNode.appendChild(targetNode); 1052 } 1053 1054 targetNode = (splitRight ? sibling : null); 1055 } else { 1056 // For general case, just remove the element and only 1057 // add it to the split tree if it contains something 1058 targetNode.parentNode.removeChild(targetNode); 1059 if (targetNode.hasChildNodes() || targetNode.textContent) { 1060 createdNode.appendChild(targetNode); 1061 } 1062 1063 targetNode = sibling; 1064 } 1065 } 1066 1067 // If we had an element we wanted to append at the end, do that now 1068 if (appendLast) { 1069 createdNode.appendChild(appendLast); 1070 } 1071 1072 splitOnNode = currParent; 1073 } 1074 1075 return createdNode; 1076 }, 1077 1078 moveTextRangeIntoElement: function (startNode, endNode, newElement) { 1079 if (!startNode || !endNode) { 1080 return false; 1081 } 1082 1083 var rootNode = Util.findCommonRoot(startNode, endNode); 1084 if (!rootNode) { 1085 return false; 1086 } 1087 1088 if (endNode === startNode) { 1089 var temp = startNode.parentNode, 1090 sibling = startNode.nextSibling; 1091 temp.removeChild(startNode); 1092 newElement.appendChild(startNode); 1093 if (sibling) { 1094 temp.insertBefore(newElement, sibling); 1095 } else { 1096 temp.appendChild(newElement); 1097 } 1098 return newElement.hasChildNodes(); 1099 } 1100 1101 // create rootChildren array which includes all the children 1102 // we care about 1103 var rootChildren = [], 1104 firstChild, 1105 lastChild, 1106 nextNode; 1107 for (var i = 0; i < rootNode.childNodes.length; i++) { 1108 nextNode = rootNode.childNodes[i]; 1109 if (!firstChild) { 1110 if (Util.isDescendant(nextNode, startNode, true)) { 1111 firstChild = nextNode; 1112 } 1113 } else { 1114 if (Util.isDescendant(nextNode, endNode, true)) { 1115 lastChild = nextNode; 1116 break; 1117 } else { 1118 rootChildren.push(nextNode); 1119 } 1120 } 1121 } 1122 1123 var afterLast = lastChild.nextSibling, 1124 fragment = rootNode.ownerDocument.createDocumentFragment(); 1125 1126 // build up fragment on startNode side of tree 1127 if (firstChild === startNode) { 1128 firstChild.parentNode.removeChild(firstChild); 1129 fragment.appendChild(firstChild); 1130 } else { 1131 fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode)); 1132 } 1133 1134 // add any elements between firstChild & lastChild 1135 rootChildren.forEach(function (element) { 1136 element.parentNode.removeChild(element); 1137 fragment.appendChild(element); 1138 }); 1139 1140 // build up fragment on endNode side of the tree 1141 if (lastChild === endNode) { 1142 lastChild.parentNode.removeChild(lastChild); 1143 fragment.appendChild(lastChild); 1144 } else { 1145 fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true)); 1146 } 1147 1148 // Add fragment into passed in element 1149 newElement.appendChild(fragment); 1150 1151 if (lastChild.parentNode === rootNode) { 1152 // If last child is in the root, insert newElement in front of it 1153 rootNode.insertBefore(newElement, lastChild); 1154 } else if (afterLast) { 1155 // If last child was removed, but it had a sibling, insert in front of it 1156 rootNode.insertBefore(newElement, afterLast); 1157 } else { 1158 // lastChild was removed and was the last actual element just append 1159 rootNode.appendChild(newElement); 1160 } 1161 1162 return newElement.hasChildNodes(); 1163 }, 1164 1165 /* based on http://stackoverflow.com/a/6183069 */ 1166 depthOfNode: function (inNode) { 1167 var theDepth = 0, 1168 node = inNode; 1169 while (node.parentNode !== null) { 1170 node = node.parentNode; 1171 theDepth++; 1172 } 1173 return theDepth; 1174 }, 1175 1176 findCommonRoot: function (inNode1, inNode2) { 1177 var depth1 = Util.depthOfNode(inNode1), 1178 depth2 = Util.depthOfNode(inNode2), 1179 node1 = inNode1, 1180 node2 = inNode2; 1181 1182 while (depth1 !== depth2) { 1183 if (depth1 > depth2) { 1184 node1 = node1.parentNode; 1185 depth1 -= 1; 1186 } else { 1187 node2 = node2.parentNode; 1188 depth2 -= 1; 1189 } 1190 } 1191 1192 while (node1 !== node2) { 1193 node1 = node1.parentNode; 1194 node2 = node2.parentNode; 1195 } 1196 1197 return node1; 1198 }, 1199 /* END - based on http://stackoverflow.com/a/6183069 */ 1200 1201 isElementAtBeginningOfBlock: function (node) { 1202 var textVal, 1203 sibling; 1204 while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) { 1205 sibling = node; 1206 while (sibling = sibling.previousSibling) { 1207 textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent; 1208 if (textVal.length > 0) { 1209 return false; 1210 } 1211 } 1212 node = node.parentNode; 1213 } 1214 return true; 1215 }, 1216 1217 isMediumEditorElement: function (element) { 1218 return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element'); 1219 }, 1220 1221 getContainerEditorElement: function (element) { 1222 return Util.traverseUp(element, function (node) { 1223 return Util.isMediumEditorElement(node); 1224 }); 1225 }, 1226 1227 isBlockContainer: function (element) { 1228 return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; 1229 }, 1230 1231 getClosestBlockContainer: function (node) { 1232 return Util.traverseUp(node, function (node) { 1233 return Util.isBlockContainer(node); 1234 }); 1235 }, 1236 1237 getTopBlockContainer: function (element) { 1238 var topBlock = element; 1239 Util.traverseUp(element, function (el) { 1240 if (Util.isBlockContainer(el)) { 1241 topBlock = el; 1242 } 1243 return false; 1244 }); 1245 return topBlock; 1246 }, 1247 1248 getFirstSelectableLeafNode: function (element) { 1249 while (element && element.firstChild) { 1250 element = element.firstChild; 1251 } 1252 1253 // We don't want to set the selection to an element that can't have children, this messes up Gecko. 1254 element = Util.traverseUp(element, function (el) { 1255 return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1; 1256 }); 1257 // Selecting at the beginning of a table doesn't work in PhantomJS. 1258 if (element.nodeName.toLowerCase() === 'table') { 1259 var firstCell = element.querySelector('th, td'); 1260 if (firstCell) { 1261 element = firstCell; 1262 } 1263 } 1264 return element; 1265 }, 1266 1267 getFirstTextNode: function (element) { 1268 if (element.nodeType === 3) { 1269 return element; 1270 } 1271 1272 for (var i = 0; i < element.childNodes.length; i++) { 1273 var textNode = Util.getFirstTextNode(element.childNodes[i]); 1274 if (textNode !== null) { 1275 return textNode; 1276 } 1277 } 1278 return null; 1279 }, 1280 1281 ensureUrlHasProtocol: function (url) { 1282 if (url.indexOf('://') === -1) { 1283 return 'http://' + url; 1284 } 1285 return url; 1286 }, 1287 1288 warn: function () { 1289 if (window.console !== undefined && typeof window.console.warn === 'function') { 1290 window.console.warn.apply(window.console, arguments); 1291 } 1292 }, 1293 1294 deprecated: function (oldName, newName, version) { 1295 // simple deprecation warning mechanism. 1296 var m = oldName + ' is deprecated, please use ' + newName + ' instead.'; 1297 if (version) { 1298 m += ' Will be removed in ' + version; 1299 } 1300 Util.warn(m); 1301 }, 1302 1303 deprecatedMethod: function (oldName, newName, args, version) { 1304 // run the replacement and warn when someone calls a deprecated method 1305 Util.deprecated(oldName, newName, version); 1306 if (typeof this[newName] === 'function') { 1307 this[newName].apply(this, args); 1308 } 1309 }, 1310 1311 cleanupAttrs: function (el, attrs) { 1312 attrs.forEach(function (attr) { 1313 el.removeAttribute(attr); 1314 }); 1315 }, 1316 1317 cleanupTags: function (el, tags) { 1318 tags.forEach(function (tag) { 1319 if (el.nodeName.toLowerCase() === tag) { 1320 el.parentNode.removeChild(el); 1321 } 1322 }); 1323 }, 1324 1325 // get the closest parent 1326 getClosestTag: function (el, tag) { 1327 return Util.traverseUp(el, function (element) { 1328 return element.nodeName.toLowerCase() === tag.toLowerCase(); 1329 }); 1330 }, 1331 1332 unwrap: function (el, doc) { 1333 var fragment = doc.createDocumentFragment(), 1334 nodes = Array.prototype.slice.call(el.childNodes); 1335 1336 // cast nodeList to array since appending child 1337 // to a different node will alter length of el.childNodes 1338 for (var i = 0; i < nodes.length; i++) { 1339 fragment.appendChild(nodes[i]); 1340 } 1341 1342 if (fragment.childNodes.length) { 1343 el.parentNode.replaceChild(fragment, el); 1344 } else { 1345 el.parentNode.removeChild(el); 1346 } 1347 } 1348 }; 1349 1350 MediumEditor.util = Util; 1351}(window)); 1352 1353(function () { 1354 'use strict'; 1355 1356 var Extension = function (options) { 1357 MediumEditor.util.extend(this, options); 1358 }; 1359 1360 Extension.extend = function (protoProps) { 1361 // magic extender thinger. mostly borrowed from backbone/goog.inherits 1362 // place this function on some thing you want extend-able. 1363 // 1364 // example: 1365 // 1366 // function Thing(args){ 1367 // this.options = args; 1368 // } 1369 // 1370 // Thing.prototype = { foo: "bar" }; 1371 // Thing.extend = extenderify; 1372 // 1373 // var ThingTwo = Thing.extend({ foo: "baz" }); 1374 // 1375 // var thingOne = new Thing(); // foo === "bar" 1376 // var thingTwo = new ThingTwo(); // foo === "baz" 1377 // 1378 // which seems like some simply shallow copy nonsense 1379 // at first, but a lot more is going on there. 1380 // 1381 // passing a `constructor` to the extend props 1382 // will cause the instance to instantiate through that 1383 // instead of the parent's constructor. 1384 1385 var parent = this, 1386 child; 1387 1388 // The constructor function for the new subclass is either defined by you 1389 // (the "constructor" property in your `extend` definition), or defaulted 1390 // by us to simply call the parent's constructor. 1391 1392 if (protoProps && protoProps.hasOwnProperty('constructor')) { 1393 child = protoProps.constructor; 1394 } else { 1395 child = function () { 1396 return parent.apply(this, arguments); 1397 }; 1398 } 1399 1400 // das statics (.extend comes over, so your subclass can have subclasses too) 1401 MediumEditor.util.extend(child, parent); 1402 1403 // Set the prototype chain to inherit from `parent`, without calling 1404 // `parent`'s constructor function. 1405 var Surrogate = function () { 1406 this.constructor = child; 1407 }; 1408 Surrogate.prototype = parent.prototype; 1409 child.prototype = new Surrogate(); 1410 1411 if (protoProps) { 1412 MediumEditor.util.extend(child.prototype, protoProps); 1413 } 1414 1415 // todo: $super? 1416 1417 return child; 1418 }; 1419 1420 Extension.prototype = { 1421 /* init: [function] 1422 * 1423 * Called by MediumEditor during initialization. 1424 * The .base property will already have been set to 1425 * current instance of MediumEditor when this is called. 1426 * All helper methods will exist as well 1427 */ 1428 init: function () {}, 1429 1430 /* base: [MediumEditor instance] 1431 * 1432 * If not overriden, this will be set to the current instance 1433 * of MediumEditor, before the init method is called 1434 */ 1435 base: undefined, 1436 1437 /* name: [string] 1438 * 1439 * 'name' of the extension, used for retrieving the extension. 1440 * If not set, MediumEditor will set this to be the key 1441 * used when passing the extension into MediumEditor via the 1442 * 'extensions' option 1443 */ 1444 name: undefined, 1445 1446 /* checkState: [function (node)] 1447 * 1448 * If implemented, this function will be called one or more times 1449 * the state of the editor & toolbar are updated. 1450 * When the state is updated, the editor does the following: 1451 * 1452 * 1) Find the parent node containing the current selection 1453 * 2) Call checkState on the extension, passing the node as an argument 1454 * 3) Get the parent node of the previous node 1455 * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable 1456 */ 1457 checkState: undefined, 1458 1459 /* destroy: [function ()] 1460 * 1461 * This method should remove any created html, custom event handlers 1462 * or any other cleanup tasks that should be performed. 1463 * If implemented, this function will be called when MediumEditor's 1464 * destroy method has been called. 1465 */ 1466 destroy: undefined, 1467 1468 /* As alternatives to checkState, these functions provide a more structured 1469 * path to updating the state of an extension (usually a button) whenever 1470 * the state of the editor & toolbar are updated. 1471 */ 1472 1473 /* queryCommandState: [function ()] 1474 * 1475 * If implemented, this function will be called once on each extension 1476 * when the state of the editor/toolbar is being updated. 1477 * 1478 * If this function returns a non-null value, the extension will 1479 * be ignored as the code climbs the dom tree. 1480 * 1481 * If this function returns true, and the setActive() function is defined 1482 * setActive() will be called 1483 */ 1484 queryCommandState: undefined, 1485 1486 /* isActive: [function ()] 1487 * 1488 * If implemented, this function will be called when MediumEditor 1489 * has determined that this extension is 'active' for the current selection. 1490 * This may be called when the editor & toolbar are being updated, 1491 * but only if queryCommandState() or isAlreadyApplied() functions 1492 * are implemented, and when called, return true. 1493 */ 1494 isActive: undefined, 1495 1496 /* isAlreadyApplied: [function (node)] 1497 * 1498 * If implemented, this function is similar to checkState() in 1499 * that it will be called repeatedly as MediumEditor moves up 1500 * the DOM to update the editor & toolbar after a state change. 1501 * 1502 * NOTE: This function will NOT be called if checkState() has 1503 * been implemented. This function will NOT be called if 1504 * queryCommandState() is implemented and returns a non-null 1505 * value when called 1506 */ 1507 isAlreadyApplied: undefined, 1508 1509 /* setActive: [function ()] 1510 * 1511 * If implemented, this function is called when MediumEditor knows 1512 * that this extension is currently enabled. Currently, this 1513 * function is called when updating the editor & toolbar, and 1514 * only if queryCommandState() or isAlreadyApplied(node) return 1515 * true when called 1516 */ 1517 setActive: undefined, 1518 1519 /* setInactive: [function ()] 1520 * 1521 * If implemented, this function is called when MediumEditor knows 1522 * that this extension is currently disabled. Curently, this 1523 * is called at the beginning of each state change for 1524 * the editor & toolbar. After calling this, MediumEditor 1525 * will attempt to update the extension, either via checkState() 1526 * or the combination of queryCommandState(), isAlreadyApplied(node), 1527 * isActive(), and setActive() 1528 */ 1529 setInactive: undefined, 1530 1531 /************************ Helpers ************************ 1532 * The following are helpers that are either set by MediumEditor 1533 * during initialization, or are helper methods which either 1534 * route calls to the MediumEditor instance or provide common 1535 * functionality for all extensions 1536 *********************************************************/ 1537 1538 /* window: [Window] 1539 * 1540 * If not overriden, this will be set to the window object 1541 * to be used by MediumEditor and its extensions. This is 1542 * passed via the 'contentWindow' option to MediumEditor 1543 * and is the global 'window' object by default 1544 */ 1545 'window': undefined, 1546 1547 /* document: [Document] 1548 * 1549 * If not overriden, this will be set to the document object 1550 * to be used by MediumEditor and its extensions. This is 1551 * passed via the 'ownerDocument' optin to MediumEditor 1552 * and is the global 'document' object by default 1553 */ 1554 'document': undefined, 1555 1556 /* getEditorElements: [function ()] 1557 * 1558 * Helper function which returns an array containing 1559 * all the contenteditable elements for this instance 1560 * of MediumEditor 1561 */ 1562 getEditorElements: function () { 1563 return this.base.elements; 1564 }, 1565 1566 /* getEditorId: [function ()] 1567 * 1568 * Helper function which returns a unique identifier 1569 * for this instance of MediumEditor 1570 */ 1571 getEditorId: function () { 1572 return this.base.id; 1573 }, 1574 1575 /* getEditorOptions: [function (option)] 1576 * 1577 * Helper function which returns the value of an option 1578 * used to initialize this instance of MediumEditor 1579 */ 1580 getEditorOption: function (option) { 1581 return this.base.options[option]; 1582 } 1583 }; 1584 1585 /* List of method names to add to the prototype of Extension 1586 * Each of these methods will be defined as helpers that 1587 * just call directly into the MediumEditor instance. 1588 * 1589 * example for 'on' method: 1590 * Extension.prototype.on = function () { 1591 * return this.base.on.apply(this.base, arguments); 1592 * } 1593 */ 1594 [ 1595 // general helpers 1596 'execAction', 1597 1598 // event handling 1599 'on', 1600 'off', 1601 'subscribe', 1602 'trigger' 1603 1604 ].forEach(function (helper) { 1605 Extension.prototype[helper] = function () { 1606 return this.base[helper].apply(this.base, arguments); 1607 }; 1608 }); 1609 1610 MediumEditor.Extension = Extension; 1611})(); 1612 1613(function () { 1614 'use strict'; 1615 1616 function filterOnlyParentElements(node) { 1617 if (MediumEditor.util.isBlockContainer(node)) { 1618 return NodeFilter.FILTER_ACCEPT; 1619 } else { 1620 return NodeFilter.FILTER_SKIP; 1621 } 1622 } 1623 1624 var Selection = { 1625 findMatchingSelectionParent: function (testElementFunction, contentWindow) { 1626 var selection = contentWindow.getSelection(), 1627 range, 1628 current; 1629 1630 if (selection.rangeCount === 0) { 1631 return false; 1632 } 1633 1634 range = selection.getRangeAt(0); 1635 current = range.commonAncestorContainer; 1636 1637 return MediumEditor.util.traverseUp(current, testElementFunction); 1638 }, 1639 1640 getSelectionElement: function (contentWindow) { 1641 return this.findMatchingSelectionParent(function (el) { 1642 return MediumEditor.util.isMediumEditorElement(el); 1643 }, contentWindow); 1644 }, 1645 1646 // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html 1647 // Tim Down 1648 exportSelection: function (root, doc) { 1649 if (!root) { 1650 return null; 1651 } 1652 1653 var selectionState = null, 1654 selection = doc.getSelection(); 1655 1656 if (selection.rangeCount > 0) { 1657 var range = selection.getRangeAt(0), 1658 preSelectionRange = range.cloneRange(), 1659 start; 1660 1661 preSelectionRange.selectNodeContents(root); 1662 preSelectionRange.setEnd(range.startContainer, range.startOffset); 1663 start = preSelectionRange.toString().length; 1664 1665 selectionState = { 1666 start: start, 1667 end: start + range.toString().length 1668 }; 1669 // If start = 0 there may still be an empty paragraph before it, but we don't care. 1670 if (start !== 0) { 1671 var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset); 1672 if (emptyBlocksIndex !== -1) { 1673 selectionState.emptyBlocksIndex = emptyBlocksIndex; 1674 } 1675 } 1676 } 1677 1678 return selectionState; 1679 }, 1680 1681 // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html 1682 // Tim Down 1683 // 1684 // {object} selectionState - the selection to import 1685 // {DOMElement} root - the root element the selection is being restored inside of 1686 // {Document} doc - the document to use for managing selection 1687 // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately 1688 // subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the 1689 // anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior 1690 // in MS IE. 1691 importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) { 1692 if (!selectionState || !root) { 1693 return; 1694 } 1695 1696 var range = doc.createRange(); 1697 range.setStart(root, 0); 1698 range.collapse(true); 1699 1700 var node = root, 1701 nodeStack = [], 1702 charIndex = 0, 1703 foundStart = false, 1704 stop = false, 1705 nextCharIndex; 1706 1707 while (!stop && node) { 1708 if (node.nodeType === 3) { 1709 nextCharIndex = charIndex + node.length; 1710 if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { 1711 range.setStart(node, selectionState.start - charIndex); 1712 foundStart = true; 1713 } 1714 if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { 1715 range.setEnd(node, selectionState.end - charIndex); 1716 stop = true; 1717 } 1718 charIndex = nextCharIndex; 1719 } else { 1720 var i = node.childNodes.length - 1; 1721 while (i >= 0) { 1722 nodeStack.push(node.childNodes[i]); 1723 i -= 1; 1724 } 1725 } 1726 if (!stop) { 1727 node = nodeStack.pop(); 1728 } 1729 } 1730 1731 if (typeof selectionState.emptyBlocksIndex !== 'undefined') { 1732 range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range); 1733 } 1734 1735 // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside. 1736 if (favorLaterSelectionAnchor) { 1737 range = this.importSelectionMoveCursorPastAnchor(selectionState, range); 1738 } 1739 1740 var sel = doc.getSelection(); 1741 sel.removeAllRanges(); 1742 sel.addRange(range); 1743 }, 1744 1745 // Utility method called from importSelection only 1746 importSelectionMoveCursorPastAnchor: function (selectionState, range) { 1747 var nodeInsideAnchorTagFunction = function (node) { 1748 return node.nodeName.toLowerCase() === 'a'; 1749 }; 1750 if (selectionState.start === selectionState.end && 1751 range.startContainer.nodeType === 3 && 1752 range.startOffset === range.startContainer.nodeValue.length && 1753 MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) { 1754 var prevNode = range.startContainer, 1755 currentNode = range.startContainer.parentNode; 1756 while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') { 1757 if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) { 1758 currentNode = null; 1759 } else { 1760 prevNode = currentNode; 1761 currentNode = currentNode.parentNode; 1762 } 1763 } 1764 if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') { 1765 var currentNodeIndex = null; 1766 for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) { 1767 if (currentNode.parentNode.childNodes[i] === currentNode) { 1768 currentNodeIndex = i; 1769 } 1770 } 1771 range.setStart(currentNode.parentNode, currentNodeIndex + 1); 1772 range.collapse(true); 1773 } 1774 } 1775 return range; 1776 }, 1777 1778 // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks 1779 // to move the cursor back to the start of the correct paragraph 1780 importSelectionMoveCursorPastBlocks: function (doc, root, index, range) { 1781 var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), 1782 startContainer = range.startContainer, 1783 startBlock, 1784 targetNode, 1785 currIndex = 0; 1786 index = index || 1; // If index is 0, we still want to move to the next block 1787 1788 // Chrome counts newlines and spaces that separate block elements as actual elements. 1789 // If the selection is inside one of these text nodes, and it has a previous sibling 1790 // which is a block element, we want the treewalker to start at the previous sibling 1791 // and NOT at the parent of the textnode 1792 if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) { 1793 startBlock = startContainer.previousSibling; 1794 } else { 1795 startBlock = MediumEditor.util.getClosestBlockContainer(startContainer); 1796 } 1797 1798 // Skip over empty blocks until we hit the block we want the selection to be in 1799 while (treeWalker.nextNode()) { 1800 if (!targetNode) { 1801 // Loop through all blocks until we hit the starting block element 1802 if (startBlock === treeWalker.currentNode) { 1803 targetNode = treeWalker.currentNode; 1804 } 1805 } else { 1806 targetNode = treeWalker.currentNode; 1807 currIndex++; 1808 // We hit the target index, bail 1809 if (currIndex === index) { 1810 break; 1811 } 1812 // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here 1813 if (targetNode.textContent.length > 0) { 1814 break; 1815 } 1816 } 1817 } 1818 1819 // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest 1820 // element at the beginning of the block 1821 range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0); 1822 1823 return range; 1824 }, 1825 1826 // Returns -1 unless the cursor is at the beginning of a paragraph/block 1827 // If the paragraph/block is preceeded by empty paragraphs/block (with no text) 1828 // it will return the number of empty paragraphs before the cursor. 1829 // Otherwise, it will return 0, which indicates the cursor is at the beginning 1830 // of a paragraph/block, and not at the end of the paragraph/block before it 1831 getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) { 1832 // If there is text in front of the cursor, that means there isn't only empty blocks before it 1833 if (cursorContainer.textContent.length > 0 && cursorOffset > 0) { 1834 return -1; 1835 } 1836 1837 // Check if the block that contains the cursor has any other text in front of the cursor 1838 var node = cursorContainer; 1839 if (node.nodeType !== 3) { 1840 node = cursorContainer.childNodes[cursorOffset]; 1841 } 1842 if (node && !MediumEditor.util.isElementAtBeginningOfBlock(node)) { 1843 return -1; 1844 } 1845 1846 // Walk over block elements, counting number of empty blocks between last piece of text 1847 // and the block the cursor is in 1848 var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer), 1849 treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), 1850 emptyBlocksCount = 0; 1851 while (treeWalker.nextNode()) { 1852 var blockIsEmpty = treeWalker.currentNode.textContent === ''; 1853 if (blockIsEmpty || emptyBlocksCount > 0) { 1854 emptyBlocksCount += 1; 1855 } 1856 if (treeWalker.currentNode === closestBlock) { 1857 return emptyBlocksCount; 1858 } 1859 if (!blockIsEmpty) { 1860 emptyBlocksCount = 0; 1861 } 1862 } 1863 1864 return emptyBlocksCount; 1865 }, 1866 1867 selectionInContentEditableFalse: function (contentWindow) { 1868 // determine if the current selection is exclusively inside 1869 // a contenteditable="false", though treat the case of an 1870 // explicit contenteditable="true" inside a "false" as false. 1871 var sawtrue, 1872 sawfalse = this.findMatchingSelectionParent(function (el) { 1873 var ce = el && el.getAttribute('contenteditable'); 1874 if (ce === 'true') { 1875 sawtrue = true; 1876 } 1877 return el.nodeName !== '#text' && ce === 'false'; 1878 }, contentWindow); 1879 1880 return !sawtrue && sawfalse; 1881 }, 1882 1883 // http://stackoverflow.com/questions/4176923/html-of-selected-text 1884 // by Tim Down 1885 getSelectionHtml: function getSelectionHtml(doc) { 1886 var i, 1887 html = '', 1888 sel = doc.getSelection(), 1889 len, 1890 container; 1891 if (sel.rangeCount) { 1892 container = doc.createElement('div'); 1893 for (i = 0, len = sel.rangeCount; i < len; i += 1) { 1894 container.appendChild(sel.getRangeAt(i).cloneContents()); 1895 } 1896 html = container.innerHTML; 1897 } 1898 return html; 1899 }, 1900 1901 /** 1902 * Find the caret position within an element irrespective of any inline tags it may contain. 1903 * 1904 * @param {DOMElement} An element containing the cursor to find offsets relative to. 1905 * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed. 1906 * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element 1907 */ 1908 getCaretOffsets: function getCaretOffsets(element, range) { 1909 var preCaretRange, postCaretRange; 1910 1911 if (!range) { 1912 range = window.getSelection().getRangeAt(0); 1913 } 1914 1915 preCaretRange = range.cloneRange(); 1916 postCaretRange = range.cloneRange(); 1917 1918 preCaretRange.selectNodeContents(element); 1919 preCaretRange.setEnd(range.endContainer, range.endOffset); 1920 1921 postCaretRange.selectNodeContents(element); 1922 postCaretRange.setStart(range.endContainer, range.endOffset); 1923 1924 return { 1925 left: preCaretRange.toString().length, 1926 right: postCaretRange.toString().length 1927 }; 1928 }, 1929 1930 // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox 1931 rangeSelectsSingleNode: function (range) { 1932 var startNode = range.startContainer; 1933 return startNode === range.endContainer && 1934 startNode.hasChildNodes() && 1935 range.endOffset === range.startOffset + 1; 1936 }, 1937 1938 getSelectedParentElement: function (range) { 1939 if (!range) { 1940 return null; 1941 } 1942 1943 // Selection encompasses a single element 1944 if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) { 1945 return range.startContainer.childNodes[range.startOffset]; 1946 } 1947 1948 // Selection range starts inside a text node, so get its parent 1949 if (range.startContainer.nodeType === 3) { 1950 return range.startContainer.parentNode; 1951 } 1952 1953 // Selection starts inside an element 1954 return range.startContainer; 1955 }, 1956 1957 getSelectedElements: function (doc) { 1958 var selection = doc.getSelection(), 1959 range, 1960 toRet, 1961 currNode; 1962 1963 if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) { 1964 return []; 1965 } 1966 1967 range = selection.getRangeAt(0); 1968 1969 if (range.commonAncestorContainer.nodeType === 3) { 1970 toRet = []; 1971 currNode = range.commonAncestorContainer; 1972 while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) { 1973 toRet.push(currNode.parentNode); 1974 currNode = currNode.parentNode; 1975 } 1976 1977 return toRet; 1978 } 1979 1980 return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) { 1981 return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true; 1982 }); 1983 }, 1984 1985 selectNode: function (node, doc) { 1986 var range = doc.createRange(), 1987 sel = doc.getSelection(); 1988 1989 range.selectNodeContents(node); 1990 sel.removeAllRanges(); 1991 sel.addRange(range); 1992 }, 1993 1994 select: function (doc, startNode, startOffset, endNode, endOffset) { 1995 doc.getSelection().removeAllRanges(); 1996 var range = doc.createRange(); 1997 range.setStart(startNode, startOffset); 1998 if (endNode) { 1999 range.setEnd(endNode, endOffset); 2000 } else { 2001 range.collapse(true); 2002 } 2003 doc.getSelection().addRange(range); 2004 return range; 2005 }, 2006 2007 /** 2008 * Move cursor to the given node with the given offset. 2009 * 2010 * @param {DomDocument} doc Current document 2011 * @param {DomElement} node Element where to jump 2012 * @param {integer} offset Where in the element should we jump, 0 by default 2013 */ 2014 moveCursor: function (doc, node, offset) { 2015 this.select(doc, node, offset); 2016 }, 2017 2018 getSelectionRange: function (ownerDocument) { 2019 var selection = ownerDocument.getSelection(); 2020 if (selection.rangeCount === 0) { 2021 return null; 2022 } 2023 return selection.getRangeAt(0); 2024 }, 2025 2026 // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi 2027 // by You 2028 getSelectionStart: function (ownerDocument) { 2029 var node = ownerDocument.getSelection().anchorNode, 2030 startNode = (node && node.nodeType === 3 ? node.parentNode : node); 2031 2032 return startNode; 2033 } 2034 }; 2035 2036 MediumEditor.selection = Selection; 2037}()); 2038 2039(function () { 2040 'use strict'; 2041 2042 var Events = function (instance) { 2043 this.base = instance; 2044 this.options = this.base.options; 2045 this.events = []; 2046 this.disabledEvents = {}; 2047 this.customEvents = {}; 2048 this.listeners = {}; 2049 }; 2050 2051 Events.prototype = { 2052 InputEventOnContenteditableSupported: !MediumEditor.util.isIE, 2053 2054 // Helpers for event handling 2055 2056 attachDOMEvent: function (target, event, listener, useCapture) { 2057 target.addEventListener(event, listener, useCapture); 2058 this.events.push([target, event, listener, useCapture]); 2059 }, 2060 2061 detachDOMEvent: function (target, event, listener, useCapture) { 2062 var index = this.indexOfListener(target, event, listener, useCapture), 2063 e; 2064 if (index !== -1) { 2065 e = this.events.splice(index, 1)[0]; 2066 e[0].removeEventListener(e[1], e[2], e[3]); 2067 } 2068 }, 2069 2070 indexOfListener: function (target, event, listener, useCapture) { 2071 var i, n, item; 2072 for (i = 0, n = this.events.length; i < n; i = i + 1) { 2073 item = this.events[i]; 2074 if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) { 2075 return i; 2076 } 2077 } 2078 return -1; 2079 }, 2080 2081 detachAllDOMEvents: function () { 2082 var e = this.events.pop(); 2083 while (e) { 2084 e[0].removeEventListener(e[1], e[2], e[3]); 2085 e = this.events.pop(); 2086 } 2087 }, 2088 2089 enableCustomEvent: function (event) { 2090 if (this.disabledEvents[event] !== undefined) { 2091 delete this.disabledEvents[event]; 2092 } 2093 }, 2094 2095 disableCustomEvent: function (event) { 2096 this.disabledEvents[event] = true; 2097 }, 2098 2099 // custom events 2100 attachCustomEvent: function (event, listener) { 2101 this.setupListener(event); 2102 if (!this.customEvents[event]) { 2103 this.customEvents[event] = []; 2104 } 2105 this.customEvents[event].push(listener); 2106 }, 2107 2108 detachCustomEvent: function (event, listener) { 2109 var index = this.indexOfCustomListener(event, listener); 2110 if (index !== -1) { 2111 this.customEvents[event].splice(index, 1); 2112 // TODO: If array is empty, should detach internal listeners via destroyListener() 2113 } 2114 }, 2115 2116 indexOfCustomListener: function (event, listener) { 2117 if (!this.customEvents[event] || !this.customEvents[event].length) { 2118 return -1; 2119 } 2120 2121 return this.customEvents[event].indexOf(listener); 2122 }, 2123 2124 detachAllCustomEvents: function () { 2125 this.customEvents = {}; 2126 // TODO: Should detach internal listeners here via destroyListener() 2127 }, 2128 2129 triggerCustomEvent: function (name, data, editable) { 2130 if (this.customEvents[name] && !this.disabledEvents[name]) { 2131 this.customEvents[name].forEach(function (listener) { 2132 listener(data, editable); 2133 }); 2134 } 2135 }, 2136 2137 // Cleaning up 2138 2139 destroy: function () { 2140 this.detachAllDOMEvents(); 2141 this.detachAllCustomEvents(); 2142 this.detachExecCommand(); 2143 2144 if (this.base.elements) { 2145 this.base.elements.forEach(function (element) { 2146 element.removeAttribute('data-medium-focused'); 2147 }); 2148 } 2149 }, 2150 2151 // Listening to calls to document.execCommand 2152 2153 // Attach a listener to be notified when document.execCommand is called 2154 attachToExecCommand: function () { 2155 if (this.execCommandListener) { 2156 return; 2157 } 2158 2159 // Store an instance of the listener so: 2160 // 1) We only attach to execCommand once 2161 // 2) We can remove the listener later 2162 this.execCommandListener = function (execInfo) { 2163 this.handleDocumentExecCommand(execInfo); 2164 }.bind(this); 2165 2166 // Ensure that execCommand has been wrapped correctly 2167 this.wrapExecCommand(); 2168 2169 // Add listener to list of execCommand listeners 2170 this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener); 2171 }, 2172 2173 // Remove our listener for calls to document.execCommand 2174 detachExecCommand: function () { 2175 var doc = this.options.ownerDocument; 2176 if (!this.execCommandListener || !doc.execCommand.listeners) { 2177 return; 2178 } 2179 2180 // Find the index of this listener in the array of listeners so it can be removed 2181 var index = doc.execCommand.listeners.indexOf(this.execCommandListener); 2182 if (index !== -1) { 2183 doc.execCommand.listeners.splice(index, 1); 2184 } 2185 2186 // If the list of listeners is now empty, put execCommand back to its original state 2187 if (!doc.execCommand.listeners.length) { 2188 this.unwrapExecCommand(); 2189 } 2190 }, 2191 2192 // Wrap document.execCommand in a custom method so we can listen to calls to it 2193 wrapExecCommand: function () { 2194 var doc = this.options.ownerDocument; 2195 2196 // Ensure all instance of MediumEditor only wrap execCommand once 2197 if (doc.execCommand.listeners) { 2198 return; 2199 } 2200 2201 // Create a wrapper method for execCommand which will: 2202 // 1) Call document.execCommand with the correct arguments 2203 // 2) Loop through any listeners and notify them that execCommand was called 2204 // passing extra info on the call 2205 // 3) Return the result 2206 var wrapper = function (aCommandName, aShowDefaultUI, aValueArgument) { 2207 var result = doc.execCommand.orig.apply(this, arguments); 2208 2209 if (!doc.execCommand.listeners) { 2210 return result; 2211 } 2212 2213 var args = Array.prototype.slice.call(arguments); 2214 doc.execCommand.listeners.forEach(function (listener) { 2215 listener({ 2216 command: aCommandName, 2217 value: aValueArgument, 2218 args: args, 2219 result: result 2220 }); 2221 }); 2222 2223 return result; 2224 }; 2225 2226 // Store a reference to the original execCommand 2227 wrapper.orig = doc.execCommand; 2228 2229 // Attach an array for storing listeners 2230 wrapper.listeners = []; 2231 2232 // Overwrite execCommand 2233 doc.execCommand = wrapper; 2234 }, 2235 2236 // Revert document.execCommand back to its original self 2237 unwrapExecCommand: function () { 2238 var doc = this.options.ownerDocument; 2239 if (!doc.execCommand.orig) { 2240 return; 2241 } 2242 2243 // Use the reference to the original execCommand to revert back 2244 doc.execCommand = doc.execCommand.orig; 2245 }, 2246 2247 // Listening to browser events to emit events medium-editor cares about 2248 setupListener: function (name) { 2249 if (this.listeners[name]) { 2250 return; 2251 } 2252 2253 switch (name) { 2254 case 'externalInteraction': 2255 // Detecting when user has interacted with elements outside of MediumEditor 2256 this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true); 2257 this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true); 2258 this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true); 2259 break; 2260 case 'blur':
2261 // Detecting when focus is lost 2262 this.setupListener('externalInteraction'); 2263 break; 2264 case 'focus': 2265 // Detecting when focus moves into some part of MediumEditor 2266 this.setupListener('externalInteraction'); 2267 break; 2268 case 'editableInput': 2269 // setup cache for knowing when the content has changed 2270 this.contentCache = []; 2271 this.base.elements.forEach(function (element) { 2272 this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML; 2273 2274 // Attach to the 'oninput' event, handled correctly by most browsers 2275 if (this.InputEventOnContenteditableSupported) { 2276 this.attachDOMEvent(element, 'input', this.handleInput.bind(this)); 2277 } 2278 }.bind(this)); 2279 2280 // For browsers which don't support the input event on contenteditable (IE) 2281 // we'll attach to 'selectionchange' on the document and 'keypress' on the editables 2282 if (!this.InputEventOnContenteditableSupported) { 2283 this.setupListener('editableKeypress'); 2284 this.keypressUpdateInput = true; 2285 this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this)); 2286 // Listen to calls to execCommand 2287 this.attachToExecCommand(); 2288 } 2289 break; 2290 case 'editableClick': 2291 // Detecting click in the contenteditables 2292 this.attachToEachElement('click', this.handleClick); 2293 break; 2294 case 'editableBlur': 2295 // Detecting blur in the contenteditables 2296 this.attachToEachElement('blur', this.handleBlur); 2297 break; 2298 case 'editableKeypress': 2299 // Detecting keypress in the contenteditables 2300 this.attachToEachElement('keypress', this.handleKeypress); 2301 break; 2302 case 'editableKeyup': 2303 // Detecting keyup in the contenteditables 2304 this.attachToEachElement('keyup', this.handleKeyup); 2305 break; 2306 case 'editableKeydown': 2307 // Detecting keydown on the contenteditables 2308 this.attachToEachElement('keydown', this.handleKeydown); 2309 break; 2310 case 'editableKeydownEnter': 2311 // Detecting keydown for ENTER on the contenteditables 2312 this.setupListener('editableKeydown'); 2313 break; 2314 case 'editableKeydownTab': 2315 // Detecting keydown for TAB on the contenteditable 2316 this.setupListener('editableKeydown'); 2317 break; 2318 case 'editableKeydownDelete': 2319 // Detecting keydown for DELETE/BACKSPACE on the contenteditables 2320 this.setupListener('editableKeydown'); 2321 break; 2322 case 'editableMouseover': 2323 // Detecting mouseover on the contenteditables 2324 this.attachToEachElement('mouseover', this.handleMouseover); 2325 break; 2326 case 'editableDrag': 2327 // Detecting dragover and dragleave on the contenteditables 2328 this.attachToEachElement('dragover', this.handleDragging); 2329 this.attachToEachElement('dragleave', this.handleDragging); 2330 break; 2331 case 'editableDrop': 2332 // Detecting drop on the contenteditables 2333 this.attachToEachElement('drop', this.handleDrop); 2334 break; 2335 case 'editablePaste': 2336 // Detecting paste on the contenteditables 2337 this.attachToEachElement('paste', this.handlePaste); 2338 break; 2339 } 2340 this.listeners[name] = true; 2341 }, 2342 2343 attachToEachElement: function (name, handler) {
2344 this.base.elements.forEach(function (element) { 2345 this.attachDOMEvent(element, name, handler.bind(this)); 2346 }, this); 2347 }, 2348 2349 focusElement: function (element) { 2350 element.focus(); 2351 this.updateFocus(element, { target: element, type: 'focus' }); 2352 }, 2353 2354 updateFocus: function (target, eventObj) { 2355 var toolbar = this.base.getExtensionByName('toolbar'), 2356 toolbarEl = toolbar ? toolbar.getToolbarElement() : null, 2357 anchorPreview = this.base.getExtensionByName('anchor-preview'), 2358 previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null, 2359 hadFocus = this.base.getFocusedElement(), 2360 toFocus; 2361 2362 // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element. 2363 // If so, we don't want to focus another element 2364 if (hadFocus && 2365 eventObj.type === 'click' && 2366 this.lastMousedownTarget && 2367 (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) || 2368 MediumEditor.util.isDescendant(toolbarEl, this.lastMousedownTarget, true) || 2369 MediumEditor.util.isDescendant(previewEl, this.lastMousedownTarget, true))) { 2370 toFocus = hadFocus; 2371 } 2372 2373 if (!toFocus) { 2374 this.base.elements.some(function (element) { 2375 // If the target is part of an editor element, this is the element getting focus 2376 if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) { 2377 toFocus = element; 2378 } 2379 2380 // bail if we found an element that's getting focus 2381 return !!toFocus; 2382 }, this); 2383 } 2384 2385 // Check if the target is external (not part of the editor, toolbar, or anchorpreview) 2386 var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) && 2387 !MediumEditor.util.isDescendant(toolbarEl, target, true) && 2388 !MediumEditor.util.isDescendant(previewEl, target, true); 2389 2390 if (toFocus !== hadFocus) { 2391 // If element has focus, and focus is going outside of editor 2392 // Don't blur focused element if clicking on editor, toolbar, or anchorpreview 2393 if (hadFocus && externalEvent) { 2394 // Trigger blur on the editable that has lost focus 2395 hadFocus.removeAttribute('data-medium-focused'); 2396 this.triggerCustomEvent('blur', eventObj, hadFocus); 2397 } 2398 2399 // If focus is going into an editor element 2400 if (toFocus) { 2401 // Trigger focus on the editable that now has focus 2402 toFocus.setAttribute('data-medium-focused', true); 2403 this.triggerCustomEvent('focus', eventObj, toFocus); 2404 } 2405 } 2406 2407 if (externalEvent) { 2408 this.triggerCustomEvent('externalInteraction', eventObj); 2409 } 2410 }, 2411 2412 updateInput: function (target, eventObj) { 2413 if (!this.contentCache) { 2414 return; 2415 } 2416 // An event triggered which signifies that the user may have changed someting 2417 // Look in our cache of input for the contenteditables to see if something changed 2418 var index = target.getAttribute('medium-editor-index'); 2419 if (target.innerHTML !== this.contentCache[index]) { 2420 // The content has changed since the last time we checked, fire the event 2421 this.triggerCustomEvent('editableInput', eventObj, target); 2422 } 2423 this.contentCache[index] = target.innerHTML; 2424 }, 2425 2426 handleDocumentSelectionChange: function (event) { 2427 // When selectionchange fires, target and current target are set 2428 // to document, since this is where the event is handled 2429 // However, currentTarget will have an 'activeElement' property 2430 // which will point to whatever element has focus. 2431 if (event.currentTarget && event.currentTarget.activeElement) { 2432 var activeElement = event.currentTarget.activeElement, 2433 currentTarget; 2434 // We can look at the 'activeElement' to determine if the selectionchange has 2435 // happened within a contenteditable owned by this instance of MediumEditor 2436 this.base.elements.some(function (element) { 2437 if (MediumEditor.util.isDescendant(element, activeElement, true)) { 2438 currentTarget = element; 2439 return true; 2440 } 2441 return false;
vendor: 2,978 bytes, lines 2442-2522
2442 }, this); 2443 2444 // We know selectionchange fired within one of our contenteditables 2445 if (currentTarget) { 2446 this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget }); 2447 } 2448 } 2449 }, 2450 2451 handleDocumentExecCommand: function () { 2452 // document.execCommand has been called 2453 // If one of our contenteditables currently has focus, we should 2454 // attempt to trigger the 'editableInput' event 2455 var target = this.base.getFocusedElement(); 2456 if (target) { 2457 this.updateInput(target, { target: target, currentTarget: target }); 2458 } 2459 }, 2460 2461 handleBodyClick: function (event) { 2462 this.updateFocus(event.target, event); 2463 }, 2464 2465 handleBodyFocus: function (event) { 2466 this.updateFocus(event.target, event); 2467 }, 2468 2469 handleBodyMousedown: function (event) { 2470 this.lastMousedownTarget = event.target; 2471 }, 2472 2473 handleInput: function (event) { 2474 this.updateInput(event.currentTarget, event); 2475 }, 2476 2477 handleClick: function (event) { 2478 this.triggerCustomEvent('editableClick', event, event.currentTarget); 2479 }, 2480 2481 handleBlur: function (event) { 2482 this.triggerCustomEvent('editableBlur', event, event.currentTarget); 2483 }, 2484 2485 handleKeypress: function (event) { 2486 this.triggerCustomEvent('editableKeypress', event, event.currentTarget); 2487 2488 // If we're doing manual detection of the editableInput event we need 2489 // to check for input changes during 'keypress' 2490 if (this.keypressUpdateInput) { 2491 var eventObj = { target: event.target, currentTarget: event.currentTarget }; 2492 2493 // In IE, we need to let the rest of the event stack complete before we detect 2494 // changes to input, so using setTimeout here 2495 setTimeout(function () { 2496 this.updateInput(eventObj.currentTarget, eventObj); 2497 }.bind(this), 0); 2498 } 2499 }, 2500 2501 handleKeyup: function (event) { 2502 this.triggerCustomEvent('editableKeyup', event, event.currentTarget); 2503 }, 2504 2505 handleMouseover: function (event) { 2506 this.triggerCustomEvent('editableMouseover', event, event.currentTarget); 2507 }, 2508 2509 handleDragging: function (event) { 2510 this.triggerCustomEvent('editableDrag', event, event.currentTarget); 2511 }, 2512 2513 handleDrop: function (event) { 2514 this.triggerCustomEvent('editableDrop', event, event.currentTarget); 2515 }, 2516 2517 handlePaste: function (event) { 2518 this.triggerCustomEvent('editablePaste', event, event.currentTarget); 2519 }, 2520 2521 handleKeydown: function (event) { 2522 this.triggerCustomEvent('editableKeydown', event, event.currentTarget);
vendor: 12,596 bytes, lines 2522-2866
2522 2523 2524 if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) { 2525 return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget); 2526 } 2527 2528 if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) { 2529 return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget); 2530 } 2531 2532 if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) { 2533 return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget); 2534 } 2535 } 2536 }; 2537 2538 MediumEditor.Events = Events; 2539}()); 2540 2541(function () { 2542 'use strict'; 2543 2544 var Button = MediumEditor.Extension.extend({ 2545 2546 /* Button Options */ 2547 2548 /* action: [string] 2549 * The action argument to pass to MediumEditor.execAction() 2550 * when the button is clicked 2551 */ 2552 action: undefined, 2553 2554 /* aria: [string] 2555 * The value to add as the aria-label attribute of the button 2556 * element displayed in the toolbar. 2557 * This is also used as the tooltip for the button 2558 */ 2559 aria: undefined, 2560 2561 /* tagNames: [Array] 2562 * NOTE: This is not used if useQueryState is set to true. 2563 * 2564 * Array of element tag names that would indicate that this 2565 * button has already been applied. If this action has already 2566 * been applied, the button will be displayed as 'active' in the toolbar 2567 * 2568 * Example: 2569 * For 'bold', if the text is ever within a <b> or <strong> 2570 * tag that indicates the text is already bold. So the array 2571 * of tagNames for bold would be: ['b', 'strong'] 2572 */ 2573 tagNames: undefined, 2574 2575 /* style: [Object] 2576 * NOTE: This is not used if useQueryState is set to true. 2577 * 2578 * A pair of css property & value(s) that indicate that this 2579 * button has already been applied. If this action has already 2580 * been applied, the button will be displayed as 'active' in the toolbar 2581 * Properties of the object: 2582 * prop [String]: name of the css property 2583 * value [String]: value(s) of the css property 2584 * multiple values can be separated by a '|' 2585 * 2586 * Example: 2587 * For 'bold', if the text is ever within an element with a 'font-weight' 2588 * style property set to '700' or 'bold', that indicates the text 2589 * is already bold. So the style object for bold would be: 2590 * { prop: 'font-weight', value: '700|bold' } 2591 */ 2592 style: undefined, 2593 2594 /* useQueryState: [boolean] 2595 * Enables/disables whether this button should use the built-in 2596 * document.queryCommandState() method to determine whether 2597 * the action has already been applied. If the action has already 2598 * been applied, the button will be displayed as 'active' in the toolbar 2599 * 2600 * Example: 2601 * For 'bold', if this is set to true, the code will call: 2602 * document.queryCommandState('bold') which will return true if the 2603 * browser thinks the text is already bold, and false otherwise 2604 */ 2605 useQueryState: undefined, 2606 2607 /* contentDefault: [string] 2608 * Default innerHTML to put inside the button 2609 */ 2610 contentDefault: undefined, 2611 2612 /* contentFA: [string] 2613 * The innerHTML to use for the content of the button 2614 * if the `buttonLabels` option for MediumEditor is set to 'fontawesome' 2615 */ 2616 contentFA: undefined, 2617 2618 /* classList: [Array] 2619 * An array of classNames (strings) to be added to the button 2620 */ 2621 classList: undefined, 2622 2623 /* attrs: [object] 2624 * A set of key-value pairs to add to the button as custom attributes 2625 */ 2626 attrs: undefined, 2627 2628 // The button constructor can optionally accept the name of a built-in button 2629 // (ie 'bold', 'italic', etc.) 2630 // When the name of a button is passed, it will initialize itself with the 2631 // configuration for that button 2632 constructor: function (options) { 2633 if (Button.isBuiltInButton(options)) { 2634 MediumEditor.Extension.call(this, this.defaults[options]); 2635 } else { 2636 MediumEditor.Extension.call(this, options); 2637 } 2638 }, 2639 2640 init: function () { 2641 MediumEditor.Extension.prototype.init.apply(this, arguments); 2642 2643 this.button = this.createButton(); 2644 this.on(this.button, 'click', this.handleClick.bind(this)); 2645 }, 2646 2647 /* getButton: [function ()] 2648 * 2649 * If implemented, this function will be called when 2650 * the toolbar is being created. The DOM Element returned 2651 * by this function will be appended to the toolbar along 2652 * with any other buttons. 2653 */ 2654 getButton: function () { 2655 return this.button; 2656 }, 2657 2658 getAction: function () { 2659 return (typeof this.action === 'function') ? this.action(this.base.options) : this.action; 2660 }, 2661 2662 getAria: function () { 2663 return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria; 2664 }, 2665 2666 getTagNames: function () { 2667 return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames; 2668 }, 2669 2670 createButton: function () { 2671 var button = this.document.createElement('button'), 2672 content = this.contentDefault, 2673 ariaLabel = this.getAria(), 2674 buttonLabels = this.getEditorOption('buttonLabels'); 2675 // Add class names 2676 button.classList.add('medium-editor-action'); 2677 button.classList.add('medium-editor-action-' + this.name); 2678 if (this.classList) { 2679 this.classList.forEach(function (className) { 2680 button.classList.add(className); 2681 }); 2682 } 2683 2684 // Add attributes 2685 button.setAttribute('data-action', this.getAction()); 2686 if (ariaLabel) { 2687 button.setAttribute('title', ariaLabel); 2688 button.setAttribute('aria-label', ariaLabel); 2689 } 2690 if (this.attrs) { 2691 Object.keys(this.attrs).forEach(function (attr) { 2692 button.setAttribute(attr, this.attrs[attr]); 2693 }, this); 2694 } 2695 2696 if (buttonLabels === 'fontawesome' && this.contentFA) { 2697 content = this.contentFA; 2698 } 2699 button.innerHTML = content; 2700 return button; 2701 }, 2702 2703 handleClick: function (event) { 2704 event.preventDefault(); 2705 event.stopPropagation(); 2706 2707 var action = this.getAction(); 2708 2709 if (action) { 2710 this.execAction(action); 2711 } 2712 }, 2713 2714 isActive: function () { 2715 return this.button.classList.contains(this.getEditorOption('activeButtonClass')); 2716 }, 2717 2718 setInactive: function () { 2719 this.button.classList.remove(this.getEditorOption('activeButtonClass')); 2720 delete this.knownState; 2721 }, 2722 2723 setActive: function () { 2724 this.button.classList.add(this.getEditorOption('activeButtonClass')); 2725 delete this.knownState; 2726 }, 2727 2728 queryCommandState: function () { 2729 var queryState = null; 2730 if (this.useQueryState) { 2731 queryState = this.base.queryCommandState(this.getAction()); 2732 } 2733 return queryState; 2734 }, 2735 2736 isAlreadyApplied: function (node) { 2737 var isMatch = false, 2738 tagNames = this.getTagNames(), 2739 styleVals, 2740 computedStyle; 2741 2742 if (this.knownState === false || this.knownState === true) { 2743 return this.knownState; 2744 } 2745 2746 if (tagNames && tagNames.length > 0) { 2747 isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1; 2748 } 2749 2750 if (!isMatch && this.style) { 2751 styleVals = this.style.value.split('|'); 2752 computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop); 2753 styleVals.forEach(function (val) { 2754 if (!this.knownState) { 2755 isMatch = (computedStyle.indexOf(val) !== -1); 2756 // text-decoration is not inherited by default 2757 // so if the computed style for text-decoration doesn't match 2758 // don't write to knownState so we can fallback to other checks 2759 if (isMatch || this.style.prop !== 'text-decoration') { 2760 this.knownState = isMatch; 2761 } 2762 } 2763 }, this); 2764 } 2765 2766 return isMatch; 2767 } 2768 }); 2769 2770 Button.isBuiltInButton = function (name) { 2771 return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name); 2772 }; 2773 2774 MediumEditor.extensions.button = Button; 2775}()); 2776 2777(function () { 2778 'use strict'; 2779 2780 /* MediumEditor.extensions.button.defaults: [Object] 2781 * Set of default config options for all of the built-in MediumEditor buttons 2782 */ 2783 MediumEditor.extensions.button.prototype.defaults = { 2784 'bold': { 2785 name: 'bold', 2786 action: 'bold', 2787 aria: 'bold', 2788 tagNames: ['b', 'strong'], 2789 style: { 2790 prop: 'font-weight', 2791 value: '700|bold' 2792 }, 2793 useQueryState: true, 2794 contentDefault: '<b>B</b>', 2795 contentFA: '<i class="fa fa-bold"></i>' 2796 }, 2797 'italic': { 2798 name: 'italic', 2799 action: 'italic', 2800 aria: 'italic', 2801 tagNames: ['i', 'em'], 2802 style: { 2803 prop: 'font-style', 2804 value: 'italic' 2805 }, 2806 useQueryState: true, 2807 contentDefault: '<b><i>I</i></b>', 2808 contentFA: '<i class="fa fa-italic"></i>' 2809 }, 2810 'underline': { 2811 name: 'underline', 2812 action: 'underline', 2813 aria: 'underline', 2814 tagNames: ['u'], 2815 style: { 2816 prop: 'text-decoration', 2817 value: 'underline' 2818 }, 2819 useQueryState: true, 2820 contentDefault: '<b><u>U</u></b>', 2821 contentFA: '<i class="fa fa-underline"></i>' 2822 }, 2823 'strikethrough': { 2824 name: 'strikethrough', 2825 action: 'strikethrough', 2826 aria: 'strike through', 2827 tagNames: ['strike'], 2828 style: { 2829 prop: 'text-decoration', 2830 value: 'line-through' 2831 }, 2832 useQueryState: true, 2833 contentDefault: '<s>A</s>', 2834 contentFA: '<i class="fa fa-strikethrough"></i>' 2835 }, 2836 'superscript': { 2837 name: 'superscript', 2838 action: 'superscript', 2839 aria: 'superscript', 2840 tagNames: ['sup'], 2841 /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript 2842 https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */ 2843 // useQueryState: true 2844 contentDefault: '<b>x<sup>1</sup></b>', 2845 contentFA: '<i class="fa fa-superscript"></i>' 2846 }, 2847 'subscript': { 2848 name: 'subscript', 2849 action: 'subscript', 2850 aria: 'subscript', 2851 tagNames: ['sub'], 2852 /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript 2853 https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */ 2854 // useQueryState: true 2855 contentDefault: '<b>x<sub>1</sub></b>', 2856 contentFA: '<i class="fa fa-subscript"></i>' 2857 }, 2858 'image': { 2859 name: 'image', 2860 action: 'image', 2861 aria: 'image', 2862 tagNames: ['img'], 2863 contentDefault: '<b>image</b>', 2864 contentFA: '<i class="fa fa-picture-o"></i>' 2865 }, 2866 '
2866orderedlist': { 2867 name: 'orderedlist', 2868 action: 'insertorderedlist', 2869 aria: 'ordered list', 2870 tagNames: ['ol'], 2871 useQueryState: true, 2872 contentDefault: '<b>1.</b>', 2873 contentFA: '<i class="fa fa-list-ol"></i>' 2874 }, 2875 'unorderedlist': { 2876 name: 'unorderedlist', 2877 action: 'insertunorderedlist', 2878 aria: 'unordered list', 2879 tagNames: ['ul'], 2880 useQueryState: true, 2881 contentDefault: '<b>•</b>', 2882 contentFA: '<i class="fa fa-list-ul"></i>' 2883 }, 2884 'indent': { 2885 name: 'indent', 2886 action: 'indent', 2887 aria: 'indent', 2888 tagNames: [], 2889 contentDefault: '<b>→</b>', 2890 contentFA: '<i class="fa fa-indent"></i>' 2891 }, 2892 'outdent': { 2893 name: 'outdent', 2894 action: 'outdent', 2895 aria: 'outdent', 2896 tagNames: [], 2897 contentDefault: '<b>←</b>', 2898 contentFA: '<i class="fa fa-outdent"></i>' 2899 }, 2900 'justifyCenter': { 2901 name: 'justifyCenter', 2902 action: 'justifyCenter', 2903 aria: 'center justify', 2904 tagNames: [], 2905 style: { 2906 prop: 'text-align', 2907 value: 'center' 2908 }, 2909 contentDefault: '<b>C</b>', 2910 contentFA: '<i class="fa fa-align-center"></i>' 2911 }, 2912 'justifyFull': { 2913 name: 'justifyFull', 2914 action: 'justifyFull', 2915 aria: 'full justify', 2916 tagNames: [], 2917 style: { 2918 prop: 'text-align', 2919 value: 'justify' 2920 }, 2921 contentDefault: '<b>J</b>', 2922 contentFA: '<i class="fa fa-align-justify"></i>' 2923 }, 2924 'justifyLeft': { 2925 name: 'justifyLeft', 2926 action: 'justifyLeft', 2927 aria: 'left justify', 2928 tagNames: [], 2929 style: { 2930 prop: 'text-align', 2931 value: 'left' 2932 }, 2933 contentDefault: '<b>L</b>', 2934 contentFA: '<i class="fa fa-align-left"></i>' 2935 }, 2936 'justifyRight': { 2937 name: 'justifyRight', 2938 action: 'justifyRight', 2939 aria: 'right justify', 2940 tagNames: [], 2941 style: { 2942 prop: 'text-align', 2943 value: 'right' 2944 }, 2945 contentDefault: '<b>R</b>', 2946 contentFA: '<i class="fa fa-align-right"></i>' 2947 }, 2948 // Known inline elements that are not removed, or not removed consistantly across browsers: 2949 // <span>, <label>, <br> 2950 'removeFormat': { 2951 name: 'removeFormat', 2952 aria: 'remove formatting', 2953 action: 'removeFormat', 2954 contentDefault: '<b>X</b>', 2955 contentFA: '<i class="fa fa-eraser"></i>' 2956 }, 2957 2958 /***** Buttons for appending block elements (append-<element> action) *****/ 2959 2960 'quote': { 2961 name: 'quote', 2962 action: 'append-blockquote', 2963 aria: 'blockquote', 2964 tagNames: ['blockquote'], 2965 contentDefault: '<b>“</b>', 2966 contentFA: '<i class="fa fa-quote-right"></i>' 2967 }, 2968 'pre': { 2969 name: 'pre', 2970 action: 'append-pre', 2971 aria: 'preformatted text', 2972 tagNames: ['pre'], 2973 contentDefault: '<b>0101</b>', 2974 contentFA: '<i class="fa fa-code fa-lg"></i>' 2975 }, 2976 'h1': { 2977 name: 'h1', 2978 action: 'append-h1', 2979 aria: 'header type one', 2980 tagNames: ['h1'], 2981 contentDefault: '<b>H1</b>', 2982 contentFA: '<i class="fa fa-header"><sup>1</sup>' 2983 }, 2984 'h2': { 2985 name: 'h2', 2986 action: 'append-h2', 2987 aria: 'header type two', 2988 tagNames: ['h2'], 2989 contentDefault: '<b>H2</b>', 2990 contentFA: '<i class="fa fa-header"><sup>2</sup>' 2991 }, 2992 'h3': { 2993 name: 'h3', 2994 action: 'append-h3', 2995 aria: 'header type three', 2996 tagNames: ['h3'], 2997 contentDefault: '<b>H3</b>', 2998 contentFA: '<i class="fa fa-header"><sup>3</sup>' 2999 }, 3000 'h4': { 3001 name: 'h4', 3002 action: 'append-h4', 3003 aria: 'header type four', 3004 tagNames: ['h4'], 3005 contentDefault: '<b>H4</b>', 3006 contentFA: '<i class="fa fa-header"><sup>4</sup>' 3007 }, 3008 'h5': { 3009 name: 'h5', 3010 action: 'append-h5', 3011 aria: 'header type five', 3012 tagNames: ['h5'], 3013 contentDefault: '<b>H5</b>', 3014 contentFA: '<i class="fa fa-header"><sup>5</sup>' 3015 }, 3016 'h6': { 3017 name: 'h6', 3018 action: 'append-h6', 3019 aria: 'header type six', 3020 tagNames: ['h6'], 3021 contentDefault: '<b>H6</b>', 3022 contentFA: '<i class="fa fa-header"><sup>6</sup>' 3023 } 3024 }; 3025 3026})(); 3027(function () { 3028 'use strict'; 3029 3030 /* Base functionality for an extension which will display 3031 * a 'form' inside the toolbar 3032 */ 3033 var FormExtension = MediumEditor.extensions.button.extend({ 3034 3035 init: function () {
3036 MediumEditor.extensions.button.prototype.init.apply(this, arguments); 3037 }, 3038 3039 // default labels for the form buttons 3040 formSaveLabel: '✓', 3041 formCloseLabel: '×', 3042 3043 /* hasForm: [boolean] 3044 * 3045 * Setting this to true will cause getForm() to be called 3046 * when the toolbar is created, so the form can be appended 3047 * inside the toolbar container 3048 */ 3049 hasForm: true, 3050 3051 /* getForm: [function ()] 3052 * 3053 * When hasForm is true, this function must be implemented 3054 * and return a DOM Element which will be appended to 3055 * the toolbar container. The form should start hidden, and 3056 * the extension can choose when to hide/show it 3057 */ 3058 getForm: function () {}, 3059 3060 /* isDisplayed: [function ()] 3061 * 3062 * This function should return true/false reflecting 3063 * whether the form is currently displayed 3064 */ 3065 isDisplayed: function () {}, 3066 3067 /* hideForm: [function ()] 3068 * 3069 * This function should hide the form element inside 3070 * the toolbar container 3071 */ 3072 hideForm: function () {}, 3073 3074 /************************ Helpers ************************ 3075 * The following are helpers that are either set by MediumEditor 3076 * during initialization, or are helper methods which either 3077 * route calls to the MediumEditor instance or provide common 3078 * functionality for all form extensions 3079 *********************************************************/ 3080 3081 /* showToolbarDefaultActions: [function ()] 3082 * 3083 * Helper method which will turn back the toolbar after canceling 3084 * the customized form 3085 */ 3086 showToolbarDefaultActions: function () { 3087 var toolbar = this.base.getExtensionByName('toolbar'); 3088 if (toolbar) { 3089 toolbar.showToolbarDefaultActions(); 3090 } 3091 }, 3092 3093 /* hideToolbarDefaultActions: [function ()] 3094 * 3095 * Helper function which will hide the default contents of the 3096 * toolbar, but leave the toolbar container in the same state 3097 * to allow a form to display its custom contents inside the toolbar 3098 */ 3099 hideToolbarDefaultActions: function () { 3100 var toolbar = this.base.getExtensionByName('toolbar'); 3101 if (toolbar) { 3102 toolbar.hideToolbarDefaultActions(); 3103 } 3104 }, 3105 3106 /* setToolbarPosition: [function ()] 3107 * 3108 * Helper function which will update the size and position 3109 * of the toolbar based on the toolbar content and the current 3110 * position of the user's selection 3111 */ 3112 setToolbarPosition: function () { 3113 var toolbar = this.base.getExtensionByName('toolbar'); 3114 if (toolbar) { 3115 toolbar.setToolbarPosition(); 3116 } 3117 } 3118 }); 3119 3120 MediumEditor.extensions.form = FormExtension; 3121})(); 3122(function () { 3123 'use strict'; 3124 3125 var AnchorForm = MediumEditor.extensions.form.extend({ 3126 /* Anchor Form Options */ 3127 3128 /* customClassOption: [string] (previously options.anchorButton + options.anchorButtonClass) 3129 * Custom class name the user can optionally have added to their created links (ie 'button'). 3130 * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose 3131 * whether to have the class added to the created link or not. 3132 */ 3133 customClassOption: null, 3134 3135 /* customClassOptionText: [string] 3136 * text to be shown in the checkbox when the __customClassOption__ is being used. 3137 */ 3138 customClassOptionText: 'Button', 3139 3140 /* linkValidation: [boolean] (previously options.checkLinkFormat)
vendor: 11,000 bytes, lines 3141-3453
3141 * enables/disables check for common URL protocols on anchor links. 3142 */ 3143 linkValidation: false, 3144 3145 /* placeholderText: [string] (previously options.anchorInputPlaceholder) 3146 * text to be shown as placeholder of the anchor input. 3147 */ 3148 placeholderText: 'Paste or type a link', 3149 3150 /* targetCheckbox: [boolean] (previously options.anchorTarget) 3151 * enables/disables displaying a "Open in new window" checkbox, which when checked 3152 * changes the `target` attribute of the created link. 3153 */ 3154 targetCheckbox: false, 3155 3156 /* targetCheckboxText: [string] (previously options.anchorInputCheckboxLabel) 3157 * text to be shown in the checkbox enabled via the __targetCheckbox__ option. 3158 */ 3159 targetCheckboxText: 'Open in new window', 3160 3161 // Options for the Button base class 3162 name: 'anchor', 3163 action: 'createLink', 3164 aria: 'link', 3165 tagNames: ['a'], 3166 contentDefault: '<b>#</b>', 3167 contentFA: '<i class="fa fa-link"></i>', 3168 3169 init: function () { 3170 MediumEditor.extensions.form.prototype.init.apply(this, arguments); 3171 3172 this.subscribe('editableKeydown', this.handleKeydown.bind(this)); 3173 }, 3174 3175 // Called when the button the toolbar is clicked 3176 // Overrides ButtonExtension.handleClick 3177 handleClick: function (event) { 3178 event.preventDefault(); 3179 event.stopPropagation(); 3180 3181 var range = MediumEditor.selection.getSelectionRange(this.document); 3182 3183 if (range.startContainer.nodeName.toLowerCase() === 'a' || 3184 range.endContainer.nodeName.toLowerCase() === 'a' || 3185 MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) { 3186 return this.execAction('unlink'); 3187 } 3188 3189 if (!this.isDisplayed()) { 3190 this.showForm(); 3191 } 3192 3193 return false; 3194 }, 3195 3196 // Called when user hits the defined shortcut (CTRL / COMMAND + K) 3197 handleKeydown: function (event) { 3198 if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) { 3199 this.handleClick(event); 3200 } 3201 }, 3202 3203 // Called by medium-editor to append form to the toolbar 3204 getForm: function () { 3205 if (!this.form) { 3206 this.form = this.createForm(); 3207 } 3208 return this.form; 3209 }, 3210 3211 getTemplate: function () { 3212 var template = [ 3213 '<input type="text" class="medium-editor-toolbar-input" placeholder="', this.placeholderText, '">' 3214 ]; 3215 3216 template.push( 3217 '<a href="#" class="medium-editor-toolbar-save">', 3218 this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel, 3219 '</a>' 3220 ); 3221 3222 template.push('<a href="#" class="medium-editor-toolbar-close">', 3223 this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel, 3224 '</a>'); 3225 3226 // both of these options are slightly moot with the ability to 3227 // override the various form buildup/serialize functions. 3228 3229 if (this.targetCheckbox) { 3230 // fixme: ideally, this targetCheckboxText would be a formLabel too, 3231 // figure out how to deprecate? also consider `fa-` icon default implcations. 3232 template.push( 3233 '<div class="medium-editor-toolbar-form-row">', 3234 '<input type="checkbox" class="medium-editor-toolbar-anchor-target">', 3235 '<label>', 3236 this.targetCheckboxText, 3237 '</label>', 3238 '</div>' 3239 ); 3240 } 3241 3242 if (this.customClassOption) { 3243 // fixme: expose this `Button` text as a formLabel property, too 3244 // and provide similar access to a `fa-` icon default. 3245 template.push( 3246 '<div class="medium-editor-toolbar-form-row">', 3247 '<input type="checkbox" class="medium-editor-toolbar-anchor-button">', 3248 '<label>', 3249 this.customClassOptionText, 3250 '</label>', 3251 '</div>' 3252 ); 3253 } 3254 3255 return template.join(''); 3256 3257 }, 3258 3259 // Used by medium-editor when the default toolbar is to be displayed 3260 isDisplayed: function () { 3261 return this.getForm().style.display === 'block'; 3262 }, 3263 3264 hideForm: function () { 3265 this.getForm().style.display = 'none'; 3266 this.getInput().value = ''; 3267 }, 3268 3269 showForm: function (opts) { 3270 var input = this.getInput(), 3271 targetCheckbox = this.getAnchorTargetCheckbox(), 3272 buttonCheckbox = this.getAnchorButtonCheckbox(); 3273 3274 opts = opts || { url: '' }; 3275 // TODO: This is for backwards compatability 3276 // We don't need to support the 'string' argument in 6.0.0 3277 if (typeof opts === 'string') { 3278 opts = { 3279 url: opts 3280 }; 3281 } 3282 3283 this.base.saveSelection(); 3284 this.hideToolbarDefaultActions(); 3285 this.getForm().style.display = 'block'; 3286 this.setToolbarPosition(); 3287 3288 input.value = opts.url; 3289 input.focus(); 3290 3291 // If we have a target checkbox, we want it to be checked/unchecked 3292 // based on whether the existing link has target=_blank 3293 if (targetCheckbox) { 3294 targetCheckbox.checked = opts.target === '_blank'; 3295 } 3296 3297 // If we have a custom class checkbox, we want it to be checked/unchecked 3298 // based on whether an existing link already has the class 3299 if (buttonCheckbox) { 3300 var classList = opts.buttonClass ? opts.buttonClass.split(' ') : []; 3301 buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1); 3302 } 3303 }, 3304 3305 // Called by core when tearing down medium-editor (destroy) 3306 destroy: function () { 3307 if (!this.form) { 3308 return false; 3309 } 3310 3311 if (this.form.parentNode) { 3312 this.form.parentNode.removeChild(this.form); 3313 } 3314 3315 delete this.form; 3316 }, 3317 3318 // core methods 3319 3320 getFormOpts: function () { 3321 // no notion of private functions? wanted `_getFormOpts` 3322 var targetCheckbox = this.getAnchorTargetCheckbox(), 3323 buttonCheckbox = this.getAnchorButtonCheckbox(), 3324 opts = { 3325 url: this.getInput().value 3326 }; 3327 3328 if (this.linkValidation) { 3329 opts.url = this.checkLinkFormat(opts.url); 3330 } 3331 3332 opts.target = '_self'; 3333 if (targetCheckbox && targetCheckbox.checked) { 3334 opts.target = '_blank'; 3335 } 3336 3337 if (buttonCheckbox && buttonCheckbox.checked) { 3338 opts.buttonClass = this.customClassOption; 3339 } 3340 3341 return opts; 3342 }, 3343 3344 doFormSave: function () { 3345 var opts = this.getFormOpts(); 3346 this.completeFormSave(opts); 3347 }, 3348 3349 completeFormSave: function (opts) { 3350 this.base.restoreSelection(); 3351 this.execAction(this.action, opts); 3352 this.base.checkSelection(); 3353 }, 3354 3355 checkLinkFormat: function (value) { 3356 var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/; 3357 return (re.test(value) ? '' : 'http://') + value; 3358 }, 3359 3360 doFormCancel: function () { 3361 this.base.restoreSelection(); 3362 this.base.checkSelection(); 3363 }, 3364 3365 // form creation and event handling 3366 attachFormEvents: function (form) { 3367 var close = form.querySelector('.medium-editor-toolbar-close'), 3368 save = form.querySelector('.medium-editor-toolbar-save'), 3369 input = form.querySelector('.medium-editor-toolbar-input'); 3370 3371 // Handle clicks on the form itself 3372 this.on(form, 'click', this.handleFormClick.bind(this)); 3373 3374 // Handle typing in the textbox 3375 this.on(input, 'keyup', this.handleTextboxKeyup.bind(this)); 3376 3377 // Handle close button clicks 3378 this.on(close, 'click', this.handleCloseClick.bind(this)); 3379 3380 // Handle save button clicks (capture) 3381 this.on(save, 'click', this.handleSaveClick.bind(this), true); 3382 3383 }, 3384 3385 createForm: function () { 3386 var doc = this.document, 3387 form = doc.createElement('div'); 3388 3389 // Anchor Form (div) 3390 form.className = 'medium-editor-toolbar-form'; 3391 form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId(); 3392 form.innerHTML = this.getTemplate(); 3393 this.attachFormEvents(form); 3394 3395 return form; 3396 }, 3397 3398 getInput: function () { 3399 return this.getForm().querySelector('input.medium-editor-toolbar-input'); 3400 }, 3401 3402 getAnchorTargetCheckbox: function () { 3403 return this.getForm().querySelector('.medium-editor-toolbar-anchor-target'); 3404 }, 3405 3406 getAnchorButtonCheckbox: function () { 3407 return this.getForm().querySelector('.medium-editor-toolbar-anchor-button'); 3408 }, 3409 3410 handleTextboxKeyup: function (event) { 3411 // For ENTER -> create the anchor 3412 if (event.keyCode === MediumEditor.util.keyCode.ENTER) { 3413 event.preventDefault(); 3414 this.doFormSave(); 3415 return; 3416 } 3417 3418 // For ESCAPE -> close the form 3419 if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) { 3420 event.preventDefault(); 3421 this.doFormCancel(); 3422 } 3423 }, 3424 3425 handleFormClick: function (event) { 3426 // make sure not to hide form when clicking inside the form 3427 event.stopPropagation(); 3428 }, 3429 3430 handleSaveClick: function (event) { 3431 // Clicking Save -> create the anchor 3432 event.preventDefault(); 3433 this.doFormSave(); 3434 }, 3435 3436 handleCloseClick: function (event) { 3437 // Click Close -> close the form 3438 event.preventDefault(); 3439 this.doFormCancel(); 3440 } 3441 }); 3442 3443 MediumEditor.extensions.anchor = AnchorForm; 3444}()); 3445(function () { 3446 'use strict'; 3447 3448 var AnchorPreview = MediumEditor.Extension.extend({ 3449 name: 'anchor-preview', 3450 3451 // Anchor Preview Options 3452 3453 /* hideDelay: [number] (previously options.anchorPreviewHideDelay)
3454 * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag. 3455 */ 3456 hideDelay: 500, 3457 3458 /* previewValueSelector: [string] 3459 * the default selector to locate where to put the activeAnchor value in the preview 3460 */ 3461 previewValueSelector: 'a', 3462 3463 init: function () { 3464 this.anchorPreview = this.createPreview(); 3465 3466 this.getEditorOption('elementsContainer').appendChild(this.anchorPreview); 3467 3468 this.attachToEditables(); 3469 }, 3470 3471 getPreviewElement: function () { 3472 return this.anchorPreview; 3473 }, 3474 3475 createPreview: function () { 3476 var el = this.document.createElement('div'); 3477 3478 el.id = 'medium-editor-anchor-preview-' + this.getEditorId(); 3479 el.className = 'medium-editor-anchor-preview'; 3480 el.innerHTML = this.getTemplate(); 3481 3482 this.on(el, 'click', this.handleClick.bind(this)); 3483 3484 return el; 3485 }, 3486 3487 getTemplate: function () { 3488 return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' + 3489 ' <a class="medium-editor-toolbar-anchor-preview-inner"></a>' + 3490 '</div>'; 3491 }, 3492 3493 destroy: function () { 3494 if (this.anchorPreview) { 3495 if (this.anchorPreview.parentNode) { 3496 this.anchorPreview.parentNode.removeChild(this.anchorPreview); 3497 } 3498 delete this.anchorPreview; 3499 } 3500 }, 3501 3502 hidePreview: function () { 3503 this.anchorPreview.classList.remove('medium-editor-anchor-preview-active'); 3504 this.activeAnchor = null; 3505 }, 3506 3507 showPreview: function (anchorEl) { 3508 if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') || 3509 anchorEl.getAttribute('data-disable-preview')) { 3510 return true; 3511 } 3512 3513 if (this.previewValueSelector) { 3514 this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value; 3515 this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value; 3516 } 3517 3518 this.anchorPreview.classList.add('medium-toolbar-arrow-over'); 3519 this.anchorPreview.classList.remove('medium-toolbar-arrow-under'); 3520 3521 if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { 3522 this.anchorPreview.classList.add('medium-editor-anchor-preview-active'); 3523 } 3524 3525 this.activeAnchor = anchorEl; 3526 3527 this.positionPreview(); 3528 this.attachPreviewHandlers(); 3529 3530 return this; 3531 }, 3532 3533 positionPreview: function (activeAnchor) { 3534 activeAnchor = activeAnchor || this.activeAnchor; 3535 var buttonHeight = this.anchorPreview.offsetHeight, 3536 boundary = activeAnchor.getBoundingClientRect(), 3537 middleBoundary = (boundary.left + boundary.right) / 2, 3538 diffLeft = this.diffLeft, 3539 diffTop = this.diffTop, 3540 halfOffsetWidth, 3541 defaultLeft; 3542 3543 halfOffsetWidth = this.anchorPreview.offsetWidth / 2; 3544 var toolbarExtension = this.base.getExtensionByName('toolbar'); 3545 if (toolbarExtension) { 3546 diffLeft = toolbarExtension.diffLeft; 3547 diffTop = toolbarExtension.diffTop; 3548 } 3549 defaultLeft = diffLeft - halfOffsetWidth; 3550 3551 this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - diffTop + this.window.pageYOffset - this.anchorPreview.offsetHeight) + 'px'; 3552 if (middleBoundary < halfOffsetWidth) { 3553 this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px'; 3554 } else if ((this.window.innerWidth - middleBoundary) < halfOffsetWidth) { 3555 this.anchorPreview.style.left = this.window.innerWidth + defaultLeft - halfOffsetWidth + 'px'; 3556 } else { 3557 this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px'; 3558 } 3559 }, 3560 3561 attachToEditables: function () { 3562 this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this)); 3563 }, 3564 3565 handleClick: function (event) { 3566 var anchorExtension = this.base.getExtensionByName('anchor'), 3567 activeAnchor = this.activeAnchor; 3568 3569 if (anchorExtension && activeAnchor) { 3570 event.preventDefault(); 3571 3572 this.base.selectElement(this.activeAnchor); 3573 3574 // Using setTimeout + delay because:
vendor: 30,834 bytes, lines 3575-4329
3575 // We may actually be displaying the anchor form, which should be controlled by delay 3576 this.base.delay(function () { 3577 if (activeAnchor) { 3578 var opts = { 3579 url: activeAnchor.attributes.href.value, 3580 target: activeAnchor.getAttribute('target'), 3581 buttonClass: activeAnchor.getAttribute('class') 3582 }; 3583 anchorExtension.showForm(opts); 3584 activeAnchor = null; 3585 } 3586 }.bind(this)); 3587 } 3588 3589 this.hidePreview(); 3590 }, 3591 3592 handleAnchorMouseout: function () { 3593 this.anchorToPreview = null; 3594 this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout); 3595 this.instanceHandleAnchorMouseout = null; 3596 }, 3597 3598 handleEditableMouseover: function (event) { 3599 var target = MediumEditor.util.getClosestTag(event.target, 'a'); 3600 3601 if (false === target) { 3602 return; 3603 } 3604 3605 // Detect empty href attributes 3606 // The browser will make href="" or href="#top" 3607 // into absolute urls when accessed as event.target.href, so check the html 3608 if (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML)) { 3609 return true; 3610 } 3611 3612 // only show when toolbar is not present 3613 var toolbar = this.base.getExtensionByName('toolbar'); 3614 if (toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) { 3615 return true; 3616 } 3617 3618 // detach handler for other anchor in case we hovered multiple anchors quickly 3619 if (this.activeAnchor && this.activeAnchor !== target) { 3620 this.detachPreviewHandlers(); 3621 } 3622 3623 this.anchorToPreview = target; 3624 3625 this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this); 3626 this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout); 3627 // Using setTimeout + delay because: 3628 // - We're going to show the anchor preview according to the configured delay 3629 // if the mouse has not left the anchor tag in that time 3630 this.base.delay(function () { 3631 if (this.anchorToPreview) { 3632 this.showPreview(this.anchorToPreview); 3633 } 3634 }.bind(this)); 3635 }, 3636 3637 handlePreviewMouseover: function () { 3638 this.lastOver = (new Date()).getTime(); 3639 this.hovering = true; 3640 }, 3641 3642 handlePreviewMouseout: function (event) { 3643 if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) { 3644 this.hovering = false; 3645 } 3646 }, 3647 3648 updatePreview: function () { 3649 if (this.hovering) { 3650 return true; 3651 } 3652 var durr = (new Date()).getTime() - this.lastOver; 3653 if (durr > this.hideDelay) { 3654 // hide the preview 1/2 second after mouse leaves the link 3655 this.detachPreviewHandlers(); 3656 } 3657 }, 3658 3659 detachPreviewHandlers: function () { 3660 // cleanup 3661 clearInterval(this.intervalTimer); 3662 if (this.instanceHandlePreviewMouseover) { 3663 this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover); 3664 this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout); 3665 if (this.activeAnchor) { 3666 this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover); 3667 this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout); 3668 } 3669 } 3670 3671 this.hidePreview(); 3672 3673 this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null; 3674 }, 3675 3676 // TODO: break up method and extract out handlers 3677 attachPreviewHandlers: function () { 3678 this.lastOver = (new Date()).getTime(); 3679 this.hovering = true; 3680 3681 this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this); 3682 this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this); 3683 3684 this.intervalTimer = setInterval(this.updatePreview.bind(this), 200); 3685 3686 this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover); 3687 this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout); 3688 this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover); 3689 this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout); 3690 } 3691 }); 3692 3693 MediumEditor.extensions.anchorPreview = AnchorPreview; 3694}()); 3695 3696(function () { 3697 'use strict'; 3698 3699 var WHITESPACE_CHARS, 3700 KNOWN_TLDS_FRAGMENT, 3701 LINK_REGEXP_TEXT, 3702 KNOWN_TLDS_REGEXP; 3703 3704 WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003', 3705 '\u2028', '\u2029']; 3706 KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' + 3707 'xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|' + 3708 'bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|' + 3709 'fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|' + 3710 'is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|' + 3711 'mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|' + 3712 'pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|' + 3713 'tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw'; 3714 3715 LINK_REGEXP_TEXT = 3716 '(' + 3717 // Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640 3718 '((?:(https?://|ftps?://|nntp://)|www\\d{0,3}[.]|[a-z0-9.\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' + 3719 // Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix: 3720 ')|(([a-z0-9\\-]+\\.)?[a-z0-9\\-]+\\.(' + KNOWN_TLDS_FRAGMENT + '))'; 3721 3722 KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i'); 3723 3724 function nodeIsNotInsideAnchorTag(node) { 3725 return !MediumEditor.util.getClosestTag(node, 'a'); 3726 } 3727 3728 var AutoLink = MediumEditor.Extension.extend({ 3729 init: function () { 3730 MediumEditor.Extension.prototype.init.apply(this, arguments); 3731 3732 this.disableEventHandling = false; 3733 this.subscribe('editableKeypress', this.onKeypress.bind(this)); 3734 this.subscribe('editableBlur', this.onBlur.bind(this)); 3735 // MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent. 3736 this.document.execCommand('AutoUrlDetect', false, false); 3737 }, 3738 3739 destroy: function () { 3740 // Turn AutoUrlDetect back on 3741 if (this.document.queryCommandSupported('AutoUrlDetect')) { 3742 this.document.execCommand('AutoUrlDetect', false, true); 3743 } 3744 }, 3745 3746 onBlur: function (blurEvent, editable) { 3747 this.performLinking(editable); 3748 }, 3749 3750 onKeypress: function (keyPressEvent) { 3751 if (this.disableEventHandling) { 3752 return; 3753 } 3754 3755 if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) { 3756 clearTimeout(this.performLinkingTimeout); 3757 // Saving/restoring the selection in the middle of a keypress doesn't work well... 3758 this.performLinkingTimeout = setTimeout(function () { 3759 try { 3760 var sel = this.base.exportSelection(); 3761 if (this.performLinking(keyPressEvent.target)) { 3762 // pass true for favorLaterSelectionAnchor - this is needed for links at the end of a 3763 // paragraph in MS IE, or MS IE causes the link to be deleted right after adding it. 3764 this.base.importSelection(sel, true); 3765 } 3766 } catch (e) { 3767 if (window.console) { 3768 window.console.error('Failed to perform linking', e); 3769 } 3770 this.disableEventHandling = true; 3771 } 3772 }.bind(this), 0); 3773 } 3774 }, 3775 3776 performLinking: function (contenteditable) { 3777 // Perform linking on a paragraph level basis as otherwise the detection can wrongly find the end 3778 // of one paragraph and the beginning of another paragraph to constitute a link, such as a paragraph ending 3779 // "link." and the next paragraph beginning with "my" is interpreted into "link.my" and the code tries to create 3780 // a link across blockElements - which doesn't work and is terrible. 3781 // (Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing) 3782 var blockElements = MediumEditor.util.splitByBlockElements(contenteditable), 3783 documentModified = false; 3784 if (blockElements.length === 0) { 3785 blockElements = [contenteditable]; 3786 } 3787 for (var i = 0; i < blockElements.length; i++) { 3788 documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified; 3789 documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified; 3790 } 3791 return documentModified; 3792 }, 3793 3794 removeObsoleteAutoLinkSpans: function (element) { 3795 if (!element || element.nodeType === 3) { 3796 return false; 3797 } 3798 3799 var spans = element.querySelectorAll('span[data-auto-link="true"]'), 3800 documentModified = false; 3801 3802 for (var i = 0; i < spans.length; i++) { 3803 var textContent = spans[i].textContent; 3804 if (textContent.indexOf('://') === -1) { 3805 textContent = MediumEditor.util.ensureUrlHasProtocol(textContent); 3806 } 3807 if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) { 3808 documentModified = true; 3809 var trimmedTextContent = textContent.replace(/\s+$/, ''); 3810 if (spans[i].getAttribute('data-href') === trimmedTextContent) { 3811 var charactersTrimmed = textContent.length - trimmedTextContent.length, 3812 subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed)); 3813 spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling); 3814 } else { 3815 // Some editing has happened to the span, so just remove it entirely. The user can put it back 3816 // around just the href content if they need to prevent it from linking 3817 MediumEditor.util.unwrap(spans[i], this.document); 3818 } 3819 } 3820 } 3821 return documentModified; 3822 }, 3823 3824 splitTextBeforeEnd: function (element, characterCount) { 3825 var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false), 3826 lastChildNotExhausted = true; 3827 3828 // Start the tree walker at the last descendant of the span 3829 while (lastChildNotExhausted) { 3830 lastChildNotExhausted = treeWalker.lastChild() !== null; 3831 } 3832 3833 var currentNode, 3834 currentNodeValue, 3835 previousNode; 3836 while (characterCount > 0 && previousNode !== null) { 3837 currentNode = treeWalker.currentNode; 3838 currentNodeValue = currentNode.nodeValue; 3839 if (currentNodeValue.length > characterCount) { 3840 previousNode = currentNode.splitText(currentNodeValue.length - characterCount); 3841 characterCount = 0; 3842 } else { 3843 previousNode = treeWalker.previousNode(); 3844 characterCount -= currentNodeValue.length; 3845 } 3846 } 3847 return previousNode; 3848 }, 3849 3850 performLinkingWithinElement: function (element) { 3851 var matches = this.findLinkableText(element), 3852 linkCreated = false; 3853 3854 for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) { 3855 var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element, 3856 matches[matchIndex]); 3857 if (this.shouldNotLink(matchingTextNodes)) { 3858 continue; 3859 } 3860 this.createAutoLink(matchingTextNodes, matches[matchIndex].href); 3861 } 3862 return linkCreated; 3863 }, 3864 3865 shouldNotLink: function (textNodes) { 3866 var shouldNotLink = false; 3867 for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) { 3868 // Do not link if the text node is either inside an anchor or inside span[data-auto-link] 3869 shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function (node) { 3870 return node.nodeName.toLowerCase() === 'a' || 3871 (node.getAttribute && node.getAttribute('data-auto-link') === 'true'); 3872 }); 3873 } 3874 return shouldNotLink; 3875 }, 3876 3877 findLinkableText: function (contenteditable) { 3878 var linkRegExp = new RegExp(LINK_REGEXP_TEXT, 'gi'), 3879 textContent = contenteditable.textContent, 3880 match = null, 3881 matches = []; 3882 3883 while ((match = linkRegExp.exec(textContent)) !== null) { 3884 var matchOk = true, 3885 matchEnd = match.index + match[0].length; 3886 // If the regexp detected something as a link that has text immediately preceding/following it, bail out. 3887 matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) && 3888 (matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1); 3889 // If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out. 3890 matchOk = matchOk && (match[0].indexOf('/') !== -1 || 3891 KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift())); 3892 3893 if (matchOk) { 3894 matches.push({ 3895 href: match[0], 3896 start: match.index, 3897 end: matchEnd 3898 }); 3899 } 3900 } 3901 return matches; 3902 }, 3903 3904 createAutoLink: function (textNodes, href) { 3905 href = MediumEditor.util.ensureUrlHasProtocol(href); 3906 var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null), 3907 span = this.document.createElement('span'); 3908 span.setAttribute('data-auto-link', 'true'); 3909 span.setAttribute('data-href', href); 3910 anchor.insertBefore(span, anchor.firstChild); 3911 while (anchor.childNodes.length > 1) { 3912 span.appendChild(anchor.childNodes[1]); 3913 } 3914 } 3915 3916 }); 3917 3918 MediumEditor.extensions.autoLink = AutoLink; 3919}()); 3920(function () { 3921 'use strict'; 3922 3923 var CLASS_DRAG_OVER = 'medium-editor-dragover'; 3924 3925 function clearClassNames(element) { 3926 var editable = MediumEditor.util.getContainerEditorElement(element), 3927 existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER)); 3928 3929 existing.forEach(function (el) { 3930 el.classList.remove(CLASS_DRAG_OVER); 3931 }); 3932 } 3933 3934 var FileDragging = MediumEditor.Extension.extend({ 3935 name: 'fileDragging', 3936 3937 allowedTypes: ['image'], 3938 3939 init: function () { 3940 MediumEditor.Extension.prototype.init.apply(this, arguments); 3941 3942 this.subscribe('editableDrag', this.handleDrag.bind(this)); 3943 this.subscribe('editableDrop', this.handleDrop.bind(this)); 3944 }, 3945 3946 handleDrag: function (event) { 3947 event.preventDefault(); 3948 event.dataTransfer.dropEffect = 'copy'; 3949 3950 var target = event.target.classList ? event.target : event.target.parentElement; 3951 3952 // Ensure the class gets removed from anything that had it before 3953 clearClassNames(target); 3954 3955 if (event.type === 'dragover') { 3956 target.classList.add(CLASS_DRAG_OVER); 3957 } 3958 }, 3959 3960 handleDrop: function (event) { 3961 // Prevent file from opening in the current window 3962 event.preventDefault(); 3963 event.stopPropagation(); 3964 3965 // IE9 does not support the File API, so prevent file from opening in the window 3966 // but also don't try to actually get the file 3967 if (event.dataTransfer.files) { 3968 Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) { 3969 if (this.isAllowedFile(file)) { 3970 if (file.type.match('image')) { 3971 this.insertImageFile(file); 3972 } 3973 } 3974 }, this); 3975 } 3976 3977 // Make sure we remove our class from everything 3978 clearClassNames(event.target); 3979 }, 3980 3981 isAllowedFile: function (file) { 3982 return this.allowedTypes.some(function (fileType) { 3983 return !!file.type.match(fileType); 3984 }); 3985 }, 3986 3987 insertImageFile: function (file) { 3988 var fileReader = new FileReader(); 3989 fileReader.readAsDataURL(file); 3990 3991 var id = 'medium-img-' + (+new Date()); 3992 MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />'); 3993 3994 fileReader.onload = function () { 3995 var img = this.document.getElementById(id); 3996 if (img) { 3997 img.removeAttribute('id'); 3998 img.removeAttribute('class'); 3999 img.src = fileReader.result; 4000 } 4001 }.bind(this); 4002 } 4003 }); 4004 4005 MediumEditor.extensions.fileDragging = FileDragging; 4006}()); 4007 4008(function () { 4009 'use strict'; 4010 4011 var KeyboardCommands = MediumEditor.Extension.extend({ 4012 name: 'keyboard-commands', 4013 4014 /* KeyboardCommands Options */ 4015 4016 /* commands: [Array] 4017 * Array of objects describing each command and the combination of keys that will trigger it 4018 * Required for each object: 4019 * command [String] (argument passed to editor.execAction()) 4020 * key [String] (keyboard character that triggers this command) 4021 * meta [boolean] (whether the ctrl/meta key has to be active or inactive) 4022 * shift [boolean] (whether the shift key has to be active or inactive) 4023 * alt [boolean] (whether the alt key has to be active or inactive) 4024 */ 4025 commands: [ 4026 { 4027 command: 'bold', 4028 key: 'B', 4029 meta: true, 4030 shift: false, 4031 alt: false 4032 }, 4033 { 4034 command: 'italic', 4035 key: 'I', 4036 meta: true, 4037 shift: false, 4038 alt: false 4039 }, 4040 { 4041 command: 'underline', 4042 key: 'U', 4043 meta: true, 4044 shift: false, 4045 alt: false 4046 } 4047 ], 4048 4049 init: function () { 4050 MediumEditor.Extension.prototype.init.apply(this, arguments); 4051 4052 this.subscribe('editableKeydown', this.handleKeydown.bind(this)); 4053 this.keys = {}; 4054 this.commands.forEach(function (command) { 4055 var keyCode = command.key.charCodeAt(0); 4056 if (!this.keys[keyCode]) { 4057 this.keys[keyCode] = []; 4058 } 4059 this.keys[keyCode].push(command); 4060 }, this); 4061 }, 4062 4063 handleKeydown: function (event) { 4064 var keyCode = MediumEditor.util.getKeyCode(event); 4065 if (!this.keys[keyCode]) { 4066 return; 4067 } 4068 4069 var isMeta = MediumEditor.util.isMetaCtrlKey(event), 4070 isShift = !!event.shiftKey, 4071 isAlt = !!event.altKey; 4072 4073 this.keys[keyCode].forEach(function (data) { 4074 if (data.meta === isMeta && 4075 data.shift === isShift && 4076 (data.alt === isAlt || 4077 undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0 4078 event.preventDefault(); 4079 event.stopPropagation(); 4080 4081 // command can be false so the shortcut is just disabled 4082 if (false !== data.command) { 4083 this.execAction(data.command); 4084 } 4085 } 4086 }, this); 4087 } 4088 }); 4089 4090 MediumEditor.extensions.keyboardCommands = KeyboardCommands; 4091}()); 4092 4093(function () { 4094 'use strict'; 4095 4096 var FontSizeForm = MediumEditor.extensions.form.extend({ 4097 4098 name: 'fontsize', 4099 action: 'fontSize', 4100 aria: 'increase/decrease font size', 4101 contentDefault: '±', // ñ 4102 contentFA: '<i class="fa fa-text-height"></i>', 4103 4104 init: function () { 4105 MediumEditor.extensions.form.prototype.init.apply(this, arguments); 4106 }, 4107 4108 // Called when the button the toolbar is clicked 4109 // Overrides ButtonExtension.handleClick 4110 handleClick: function (event) { 4111 event.preventDefault(); 4112 event.stopPropagation(); 4113 4114 if (!this.isDisplayed()) { 4115 // Get fontsize of current selection (convert to string since IE returns this as number) 4116 var fontSize = this.document.queryCommandValue('fontSize') + ''; 4117 this.showForm(fontSize); 4118 } 4119 4120 return false; 4121 }, 4122 4123 // Called by medium-editor to append form to the toolbar 4124 getForm: function () { 4125 if (!this.form) { 4126 this.form = this.createForm(); 4127 } 4128 return this.form; 4129 }, 4130 4131 // Used by medium-editor when the default toolbar is to be displayed 4132 isDisplayed: function () { 4133 return this.getForm().style.display === 'block'; 4134 }, 4135 4136 hideForm: function () { 4137 this.getForm().style.display = 'none'; 4138 this.getInput().value = ''; 4139 }, 4140 4141 showForm: function (fontSize) { 4142 var input = this.getInput(); 4143 4144 this.base.saveSelection(); 4145 this.hideToolbarDefaultActions(); 4146 this.getForm().style.display = 'block'; 4147 this.setToolbarPosition(); 4148 4149 input.value = fontSize || ''; 4150 input.focus(); 4151 }, 4152 4153 // Called by core when tearing down medium-editor (destroy) 4154 destroy: function () { 4155 if (!this.form) { 4156 return false; 4157 } 4158 4159 if (this.form.parentNode) { 4160 this.form.parentNode.removeChild(this.form); 4161 } 4162 4163 delete this.form; 4164 }, 4165 4166 // core methods 4167 4168 doFormSave: function () { 4169 this.base.restoreSelection(); 4170 this.base.checkSelection(); 4171 }, 4172 4173 doFormCancel: function () { 4174 this.base.restoreSelection(); 4175 this.clearFontSize(); 4176 this.base.checkSelection(); 4177 }, 4178 4179 // form creation and event handling 4180 createForm: function () { 4181 var doc = this.document, 4182 form = doc.createElement('div'), 4183 input = doc.createElement('input'), 4184 close = doc.createElement('a'), 4185 save = doc.createElement('a'); 4186 4187 // Font Size Form (div) 4188 form.className = 'medium-editor-toolbar-form'; 4189 form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId(); 4190 4191 // Handle clicks on the form itself 4192 this.on(form, 'click', this.handleFormClick.bind(this)); 4193 4194 // Add font size slider 4195 input.setAttribute('type', 'range'); 4196 input.setAttribute('min', '1'); 4197 input.setAttribute('max', '7'); 4198 input.className = 'medium-editor-toolbar-input'; 4199 form.appendChild(input); 4200 4201 // Handle typing in the textbox 4202 this.on(input, 'change', this.handleSliderChange.bind(this)); 4203 4204 // Add save buton 4205 save.setAttribute('href', '#'); 4206 save.className = 'medium-editor-toobar-save'; 4207 save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ? 4208 '<i class="fa fa-check"></i>' : 4209 '✓'; 4210 form.appendChild(save); 4211 4212 // Handle save button clicks (capture) 4213 this.on(save, 'click', this.handleSaveClick.bind(this), true); 4214 4215 // Add close button 4216 close.setAttribute('href', '#'); 4217 close.className = 'medium-editor-toobar-close'; 4218 close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ? 4219 '<i class="fa fa-times"></i>' : 4220 '×'; 4221 form.appendChild(close); 4222 4223 // Handle close button clicks 4224 this.on(close, 'click', this.handleCloseClick.bind(this)); 4225 4226 return form; 4227 }, 4228 4229 getInput: function () { 4230 return this.getForm().querySelector('input.medium-editor-toolbar-input'); 4231 }, 4232 4233 clearFontSize: function () { 4234 MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) { 4235 if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) { 4236 el.removeAttribute('size'); 4237 } 4238 }); 4239 }, 4240 4241 handleSliderChange: function () { 4242 var size = this.getInput().value; 4243 if (size === '4') { 4244 this.clearFontSize(); 4245 } else { 4246 this.execAction('fontSize', { size: size }); 4247 } 4248 }, 4249 4250 handleFormClick: function (event) { 4251 // make sure not to hide form when clicking inside the form 4252 event.stopPropagation(); 4253 }, 4254 4255 handleSaveClick: function (event) { 4256 // Clicking Save -> create the font size 4257 event.preventDefault(); 4258 this.doFormSave(); 4259 }, 4260 4261 handleCloseClick: function (event) { 4262 // Click Close -> close the form 4263 event.preventDefault(); 4264 this.doFormCancel(); 4265 } 4266 }); 4267 4268 MediumEditor.extensions.fontSize = FontSizeForm; 4269}()); 4270(function () { 4271 'use strict'; 4272 /*jslint regexp: true*/ 4273 /* 4274 jslint does not allow character negation, because the negation 4275 will not match any unicode characters. In the regexes in this 4276 block, negation is used specifically to match the end of an html 4277 tag, and in fact unicode characters *should* be allowed. 4278 */ 4279 function createReplacements() { 4280 return [ 4281 // replace two bogus tags that begin pastes from google docs 4282 [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''], 4283 [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''], 4284 4285 // un-html spaces and newlines inserted by OS X 4286 [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '], 4287 [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'], 4288 4289 // replace google docs italics+bold with a span to be replaced once the html is inserted 4290 [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'], 4291 4292 // replace google docs italics with a span to be replaced once the html is inserted 4293 [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'], 4294 4295 //[replace google docs bolds with a span to be replaced once the html is inserted 4296 [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'], 4297 4298 // replace manually entered b/i/a tags with real ones 4299 [new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'], 4300 4301 // replace manually a tags with real ones, converting smart-quotes from google docs 4302 [new RegExp(/<a(?:(?!href).)+href=(?:"|”|“|"|ââ¬Å|ââ¬Â)(((?!"|”|“|"|ââ¬Å|ââ¬Â).)*)(?:"|”|“|"|ââ¬Å|ââ¬Â)(?:(?!>).)*>/gi), '<a href="$1">'], 4303 4304 // Newlines between paragraphs in html have no syntactic value, 4305 // but then have a tendency to accidentally become additional paragraphs down the line 4306 [new RegExp(/<\/p>\n+/gi), '</p>'], 4307 [new RegExp(/\n+<p/gi), '<p'], 4308 4309 // Microsoft Word makes these odd tags, like <o:p></o:p> 4310 [new RegExp(/<\/?o:[a-z]*>/gi), ''], 4311 4312 // cleanup comments added by Chrome when pasting html 4313 ['<!--EndFragment-->', ''], 4314 ['<!--StartFragment-->', ''] 4315 ]; 4316 } 4317 /*jslint regexp: false*/ 4318 4319 var PasteHandler = MediumEditor.Extension.extend({ 4320 /* Paste Options */ 4321 4322 /* forcePlainText: [boolean] 4323 * Forces pasting as plain text. 4324 */ 4325 forcePlainText: true, 4326 4327 /* cleanPastedHTML: [boolean] 4328 * cleans pasted content from different sources, like google docs etc. 4329 */
4330 cleanPastedHTML: false, 4331 4332 /* cleanReplacements: [Array] 4333 * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when 4334 * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method. 4335 */ 4336 cleanReplacements: [], 4337 4338 /* cleanAttrs:: [Array] 4339 * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when 4340 * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods. 4341 */ 4342 cleanAttrs: ['class', 'style', 'dir'], 4343 4344 /* cleanTags: [Array] 4345 * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when 4346 * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods. 4347 */ 4348 cleanTags: ['meta'], 4349 4350 init: function () { 4351 MediumEditor.Extension.prototype.init.apply(this, arguments); 4352 4353 if (this.forcePlainText || this.cleanPastedHTML) { 4354 this.subscribe('editablePaste', this.handlePaste.bind(this)); 4355 } 4356 }, 4357 4358 handlePaste: function (event, element) { 4359 var paragraphs, 4360 html = '', 4361 p, 4362 dataFormatHTML = 'text/html', 4363 dataFormatPlain = 'text/plain', 4364 pastedHTML, 4365 pastedPlain; 4366 4367 if (this.window.clipboardData && event.clipboardData === undefined) { 4368 event.clipboardData = this.window.clipboardData; 4369 // If window.clipboardData exists, but event.clipboardData doesn't exist, 4370 // we're probably in IE. IE only has two possibilities for clipboard 4371 // data format: 'Text' and 'URL'. 4372 // 4373 // Of the two, we want 'Text': 4374 dataFormatHTML = 'Text'; 4375 dataFormatPlain = 'Text'; 4376 } 4377 4378 if (event.clipboardData && 4379 event.clipboardData.getData && 4380 !event.defaultPrevented) { 4381 event.preventDefault(); 4382 4383 pastedHTML = event.clipboardData.getData(dataFormatHTML); 4384 pastedPlain = event.clipboardData.getData(dataFormatPlain); 4385 4386 if (this.cleanPastedHTML && pastedHTML) { 4387 return this.cleanPaste(pastedHTML); 4388 } 4389 4390 if (!(this.getEditorOption('disableReturn') || element.getAttribute('data-disable-return'))) { 4391 paragraphs = pastedPlain.split(/[\r\n]+/g); 4392 // If there are no \r\n in data, don't wrap in <p> 4393 if (paragraphs.length > 1) { 4394 for (p = 0; p < paragraphs.length; p += 1) { 4395 if (paragraphs[p] !== '') { 4396 html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>'; 4397 } 4398 } 4399 } else { 4400 html = MediumEditor.util.htmlEntities(paragraphs[0]); 4401 } 4402 } else { 4403 html = MediumEditor.util.htmlEntities(pastedPlain); 4404 } 4405 MediumEditor.util.insertHTMLCommand(this.document, html); 4406 } 4407 }, 4408 4409 cleanPaste: function (text) { 4410 var i, elList, tmp, workEl, 4411 multiline = /<p|<br|<div/.test(text), 4412 replacements = createReplacements().concat(this.cleanReplacements || []); 4413 4414 for (i = 0; i < replacements.length; i += 1) { 4415 text = text.replace(replacements[i][0], replacements[i][1]); 4416 } 4417 4418 if (!multiline) { 4419 return this.pasteHTML(text); 4420 } 4421 4422 // create a temporary div to cleanup block elements 4423 tmp = this.document.createElement('div'); 4424 4425 // double br's aren't converted to p tags, but we want paragraphs. 4426 tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>'; 4427 4428 // block element cleanup 4429 elList = tmp.querySelectorAll('a,p,div,br'); 4430 for (i = 0; i < elList.length; i += 1) { 4431 workEl = elList[i]; 4432 4433 // Microsoft Word replaces some spaces with newlines. 4434 // While newlines between block elements are meaningless, newlines within 4435 // elements are sometimes actually spaces. 4436 workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' '); 4437 4438 switch (workEl.nodeName.toLowerCase()) { 4439 case 'p': 4440 case 'div': 4441 this.filterCommonBlocks(workEl); 4442 break; 4443 case 'br': 4444 this.filterLineBreak(workEl); 4445 break; 4446 } 4447 } 4448 4449 this.pasteHTML(tmp.innerHTML); 4450 }, 4451 4452 pasteHTML: function (html, options) { 4453 options = MediumEditor.util.defaults({}, options, { 4454 cleanAttrs: this.cleanAttrs, 4455 cleanTags: this.cleanTags 4456 }); 4457 4458 var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment(); 4459 4460 pasteBlock.appendChild(this.document.createElement('body')); 4461 4462 fragmentBody = pasteBlock.querySelector('body'); 4463 fragmentBody.innerHTML = html; 4464 4465 this.cleanupSpans(fragmentBody); 4466 4467 elList = fragmentBody.querySelectorAll('*'); 4468 for (i = 0; i < elList.length; i += 1) { 4469 workEl = elList[i]; 4470 4471 if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) { 4472 MediumEditor.util.setTargetBlank(workEl); 4473 } 4474 4475 MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
vendor: 28,916 bytes, lines 4476-5212
4476 MediumEditor.util.cleanupTags(workEl, options.cleanTags); 4477 } 4478 4479 MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/ /g, ' ')); 4480 }, 4481 4482 isCommonBlock: function (el) { 4483 return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div')); 4484 }, 4485 4486 filterCommonBlocks: function (el) { 4487 if (/^\s*$/.test(el.textContent) && el.parentNode) { 4488 el.parentNode.removeChild(el); 4489 } 4490 }, 4491 4492 filterLineBreak: function (el) { 4493 if (this.isCommonBlock(el.previousElementSibling)) { 4494 // remove stray br's following common block elements 4495 this.removeWithParent(el); 4496 } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) { 4497 // remove br's just inside open or close tags of a div/p 4498 this.removeWithParent(el); 4499 } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') { 4500 // and br's that are the only child of elements other than div/p 4501 this.removeWithParent(el); 4502 } 4503 }, 4504 4505 // remove an element, including its parent, if it is the only element within its parent 4506 removeWithParent: function (el) { 4507 if (el && el.parentNode) { 4508 if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) { 4509 el.parentNode.parentNode.removeChild(el.parentNode); 4510 } else { 4511 el.parentNode.removeChild(el); 4512 } 4513 } 4514 }, 4515 4516 cleanupSpans: function (containerEl) { 4517 var i, 4518 el, 4519 newEl, 4520 spans = containerEl.querySelectorAll('.replace-with'), 4521 isCEF = function (el) { 4522 return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false'); 4523 }; 4524 4525 for (i = 0; i < spans.length; i += 1) { 4526 el = spans[i]; 4527 newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i'); 4528 4529 if (el.classList.contains('bold') && el.classList.contains('italic')) { 4530 // add an i tag as well if this has both italics and bold 4531 newEl.innerHTML = '<i>' + el.innerHTML + '</i>'; 4532 } else { 4533 newEl.innerHTML = el.innerHTML; 4534 } 4535 el.parentNode.replaceChild(newEl, el); 4536 } 4537 4538 spans = containerEl.querySelectorAll('span'); 4539 for (i = 0; i < spans.length; i += 1) { 4540 el = spans[i]; 4541 4542 // bail if span is in contenteditable = false 4543 if (MediumEditor.util.traverseUp(el, isCEF)) { 4544 return false; 4545 } 4546 4547 // remove empty spans, replace others with their contents 4548 MediumEditor.util.unwrap(el, this.document); 4549 } 4550 } 4551 }); 4552 4553 MediumEditor.extensions.paste = PasteHandler; 4554}()); 4555 4556(function () { 4557 'use strict'; 4558 4559 var Placeholder = MediumEditor.Extension.extend({ 4560 name: 'placeholder', 4561 4562 /* Placeholder Options */ 4563 4564 /* text: [string] 4565 * Text to display in the placeholder 4566 */ 4567 text: 'Type your text', 4568 4569 /* hideOnClick: [boolean] 4570 * Should we hide the placeholder on click (true) or when user starts typing (false) 4571 */ 4572 hideOnClick: true, 4573 4574 init: function () { 4575 MediumEditor.Extension.prototype.init.apply(this, arguments); 4576 4577 this.initPlaceholders(); 4578 this.attachEventHandlers(); 4579 }, 4580 4581 initPlaceholders: function () { 4582 this.getEditorElements().forEach(function (el) { 4583 if (!el.getAttribute('data-placeholder')) { 4584 el.setAttribute('data-placeholder', this.text); 4585 } 4586 this.updatePlaceholder(el); 4587 }, this); 4588 }, 4589 4590 destroy: function () { 4591 this.getEditorElements().forEach(function (el) { 4592 if (el.getAttribute('data-placeholder') === this.text) { 4593 el.removeAttribute('data-placeholder'); 4594 } 4595 }, this); 4596 }, 4597 4598 showPlaceholder: function (el) { 4599 if (el) { 4600 el.classList.add('medium-editor-placeholder'); 4601 } 4602 }, 4603 4604 hidePlaceholder: function (el) { 4605 if (el) { 4606 el.classList.remove('medium-editor-placeholder'); 4607 } 4608 }, 4609 4610 updatePlaceholder: function (el, dontShow) { 4611 // If the element has content, hide the placeholder 4612 if (el.querySelector('img, blockquote, ul, ol') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) { 4613 return this.hidePlaceholder(el); 4614 } 4615 4616 if (!dontShow) { 4617 this.showPlaceholder(el); 4618 } 4619 }, 4620 4621 attachEventHandlers: function () { 4622 if (this.hideOnClick) { 4623 // For the 'hideOnClick' option, the placeholder should always be hidden on focus 4624 this.subscribe('focus', this.handleFocus.bind(this)); 4625 } 4626 4627 // If the editor has content, it should always hide the placeholder 4628 this.subscribe('editableInput', this.handleInput.bind(this)); 4629 4630 // When the editor loses focus, check if the placeholder should be visible 4631 this.subscribe('blur', this.handleBlur.bind(this)); 4632 }, 4633 4634 handleInput: function (event, element) { 4635 // If the placeholder should be hidden on focus and the 4636 // element has focus, don't show the placeholder 4637 var dontShow = this.hideOnClick && (element === this.base.getFocusedElement()); 4638 4639 // Editor's content has changed, check if the placeholder should be hidden 4640 this.updatePlaceholder(element, dontShow); 4641 }, 4642 4643 handleFocus: function (event, element) { 4644 // Editor has focus, hide the placeholder 4645 this.hidePlaceholder(element); 4646 }, 4647 4648 handleBlur: function (event, element) { 4649 // Editor has lost focus, check if the placeholder should be shown 4650 this.updatePlaceholder(element); 4651 } 4652 }); 4653 4654 MediumEditor.extensions.placeholder = Placeholder; 4655}()); 4656 4657(function () { 4658 'use strict'; 4659 4660 var Toolbar = MediumEditor.Extension.extend({ 4661 name: 'toolbar', 4662 4663 /* Toolbar Options */ 4664 4665 /* align: ['left'|'center'|'right'] 4666 * When the __static__ option is true, this aligns the static toolbar 4667 * relative to the medium-editor element. 4668 */ 4669 align: 'center', 4670 4671 /* allowMultiParagraphSelection: [boolean] 4672 * enables/disables whether the toolbar should be displayed when 4673 * selecting multiple paragraphs/block elements 4674 */ 4675 allowMultiParagraphSelection: true, 4676 4677 /* buttons: [Array] 4678 * the names of the set of buttons to display on the toolbar. 4679 */ 4680 buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'], 4681 4682 /* diffLeft: [Number] 4683 * value in pixels to be added to the X axis positioning of the toolbar. 4684 */ 4685 diffLeft: 0, 4686 4687 /* diffTop: [Number] 4688 * value in pixels to be added to the Y axis positioning of the toolbar. 4689 */ 4690 diffTop: -10, 4691 4692 /* firstButtonClass: [string] 4693 * CSS class added to the first button in the toolbar. 4694 */ 4695 firstButtonClass: 'medium-editor-button-first', 4696 4697 /* lastButtonClass: [string] 4698 * CSS class added to the last button in the toolbar. 4699 */ 4700 lastButtonClass: 'medium-editor-button-last', 4701 4702 /* standardizeSelectionStart: [boolean] 4703 * enables/disables standardizing how the beginning of a range is decided 4704 * between browsers whenever the selected text is analyzed for updating toolbar buttons status. 4705 */ 4706 standardizeSelectionStart: false, 4707 4708 /* static: [boolean] 4709 * enable/disable the toolbar always displaying in the same location 4710 * relative to the medium-editor element. 4711 */ 4712 static: false, 4713 4714 /* sticky: [boolean] 4715 * When the __static__ option is true, this enables/disables the toolbar 4716 * "sticking" to the viewport and staying visible on the screen while 4717 * the page scrolls. 4718 */ 4719 sticky: false, 4720 4721 /* updateOnEmptySelection: [boolean] 4722 * When the __static__ option is true, this enables/disables updating 4723 * the state of the toolbar buttons even when the selection is collapsed 4724 * (there is no selection, just a cursor). 4725 */ 4726 updateOnEmptySelection: false, 4727 4728 /* relativeContainer: [node] 4729 * appending the toolbar to a given node instead of body 4730 */ 4731 relativeContainer: null, 4732 4733 init: function () { 4734 MediumEditor.Extension.prototype.init.apply(this, arguments); 4735 4736 this.initThrottledMethods(); 4737 4738 if (!this.relativeContainer) { 4739 this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement()); 4740 } else { 4741 this.relativeContainer.appendChild(this.getToolbarElement()); 4742 } 4743 }, 4744 4745 // Helper method to execute method for every extension, but ignoring the toolbar extension 4746 forEachExtension: function (iterator, context) { 4747 return this.base.extensions.forEach(function (command) { 4748 if (command === this) { 4749 return; 4750 } 4751 return iterator.apply(context || this, arguments); 4752 }, this); 4753 }, 4754 4755 // Toolbar creation/deletion 4756 4757 createToolbar: function () { 4758 var toolbar = this.document.createElement('div'); 4759 4760 toolbar.id = 'medium-editor-toolbar-' + this.getEditorId(); 4761 toolbar.className = 'medium-editor-toolbar'; 4762 4763 if (this.static) { 4764 toolbar.className += ' static-toolbar'; 4765 } else if (this.relativeContainer) { 4766 toolbar.className += ' medium-editor-relative-toolbar'; 4767 } else { 4768 toolbar.className += ' medium-editor-stalker-toolbar'; 4769 } 4770 4771 toolbar.appendChild(this.createToolbarButtons()); 4772 4773 // Add any forms that extensions may have 4774 this.forEachExtension(function (extension) { 4775 if (extension.hasForm) { 4776 toolbar.appendChild(extension.getForm()); 4777 } 4778 }); 4779 4780 this.attachEventHandlers(); 4781 4782 return toolbar; 4783 }, 4784 4785 createToolbarButtons: function () { 4786 var ul = this.document.createElement('ul'), 4787 li, 4788 btn, 4789 buttons, 4790 extension, 4791 buttonName, 4792 buttonOpts; 4793 4794 ul.id = 'medium-editor-toolbar-actions' + this.getEditorId(); 4795 ul.className = 'medium-editor-toolbar-actions'; 4796 ul.style.display = 'block'; 4797 4798 this.buttons.forEach(function (button) { 4799 if (typeof button === 'string') { 4800 buttonName = button; 4801 buttonOpts = null; 4802 } else { 4803 buttonName = button.name; 4804 buttonOpts = button; 4805 } 4806 4807 // If the button already exists as an extension, it'll be returned 4808 // othwerise it'll create the default built-in button 4809 extension = this.base.addBuiltInExtension(buttonName, buttonOpts); 4810 4811 if (extension && typeof extension.getButton === 'function') { 4812 btn = extension.getButton(this.base); 4813 li = this.document.createElement('li'); 4814 if (MediumEditor.util.isElement(btn)) { 4815 li.appendChild(btn); 4816 } else { 4817 li.innerHTML = btn; 4818 } 4819 ul.appendChild(li); 4820 } 4821 }, this); 4822 4823 buttons = ul.querySelectorAll('button'); 4824 if (buttons.length > 0) { 4825 buttons[0].classList.add(this.firstButtonClass); 4826 buttons[buttons.length - 1].classList.add(this.lastButtonClass); 4827 } 4828 4829 return ul; 4830 }, 4831 4832 destroy: function () { 4833 if (this.toolbar) { 4834 if (this.toolbar.parentNode) { 4835 this.toolbar.parentNode.removeChild(this.toolbar); 4836 } 4837 delete this.toolbar; 4838 } 4839 }, 4840 4841 // Toolbar accessors 4842 4843 getToolbarElement: function () { 4844 if (!this.toolbar) { 4845 this.toolbar = this.createToolbar(); 4846 } 4847 4848 return this.toolbar; 4849 }, 4850 4851 getToolbarActionsElement: function () { 4852 return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions'); 4853 }, 4854 4855 // Toolbar event handlers 4856 4857 initThrottledMethods: function () { 4858 // throttledPositionToolbar is throttled because: 4859 // - It will be called when the browser is resizing, which can fire many times very quickly 4860 // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits 4861 this.throttledPositionToolbar = MediumEditor.util.throttle(function () { 4862 if (this.base.isActive) { 4863 this.positionToolbarIfShown(); 4864 } 4865 }.bind(this)); 4866 }, 4867 4868 attachEventHandlers: function () { 4869 // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements 4870 this.subscribe('blur', this.handleBlur.bind(this)); 4871 this.subscribe('focus', this.handleFocus.bind(this)); 4872 4873 // Updating the state of the toolbar as things change 4874 this.subscribe('editableClick', this.handleEditableClick.bind(this)); 4875 this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this)); 4876 4877 // Handle mouseup on document for updating the selection in the toolbar 4878 this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this)); 4879 4880 // Add a scroll event for sticky toolbar 4881 if (this.static && this.sticky) { 4882 // On scroll (capture), re-position the toolbar 4883 this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true); 4884 } 4885 4886 // On resize, re-position the toolbar 4887 this.on(this.window, 'resize', this.handleWindowResize.bind(this)); 4888 }, 4889 4890 handleWindowScroll: function () { 4891 this.positionToolbarIfShown(); 4892 }, 4893 4894 handleWindowResize: function () { 4895 this.throttledPositionToolbar(); 4896 }, 4897 4898 handleDocumentMouseup: function (event) { 4899 // Do not trigger checkState when mouseup fires over the toolbar 4900 if (event && 4901 event.target && 4902 MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) { 4903 return false; 4904 } 4905 this.checkState(); 4906 }, 4907 4908 handleEditableClick: function () { 4909 // Delay the call to checkState to handle bug where selection is empty 4910 // immediately after clicking inside a pre-existing selection 4911 setTimeout(function () { 4912 this.checkState(); 4913 }.bind(this), 0); 4914 }, 4915 4916 handleEditableKeyup: function () { 4917 this.checkState(); 4918 }, 4919 4920 handleBlur: function () { 4921 // Kill any previously delayed calls to hide the toolbar 4922 clearTimeout(this.hideTimeout); 4923 4924 // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar 4925 // calls from happening in this specific case 4926 clearTimeout(this.delayShowTimeout); 4927 4928 // Delay the call to hideToolbar to handle bug with multiple editors on the page at once 4929 this.hideTimeout = setTimeout(function () { 4930 this.hideToolbar(); 4931 }.bind(this), 1); 4932 }, 4933 4934 handleFocus: function () { 4935 this.checkState(); 4936 }, 4937 4938 // Hiding/showing toolbar 4939 4940 isDisplayed: function () { 4941 return this.getToolbarElement().classList.contains('medium-editor-toolbar-active'); 4942 }, 4943 4944 showToolbar: function () { 4945 clearTimeout(this.hideTimeout); 4946 if (!this.isDisplayed()) { 4947 this.getToolbarElement().classList.add('medium-editor-toolbar-active'); 4948 this.trigger('showToolbar', {}, this.base.getFocusedElement()); 4949 } 4950 }, 4951 4952 hideToolbar: function () { 4953 if (this.isDisplayed()) { 4954 this.getToolbarElement().classList.remove('medium-editor-toolbar-active'); 4955 this.trigger('hideToolbar', {}, this.base.getFocusedElement()); 4956 } 4957 }, 4958 4959 isToolbarDefaultActionsDisplayed: function () { 4960 return this.getToolbarActionsElement().style.display === 'block'; 4961 }, 4962 4963 hideToolbarDefaultActions: function () { 4964 if (this.isToolbarDefaultActionsDisplayed()) { 4965 this.getToolbarActionsElement().style.display = 'none'; 4966 } 4967 }, 4968 4969 showToolbarDefaultActions: function () { 4970 this.hideExtensionForms(); 4971 4972 if (!this.isToolbarDefaultActionsDisplayed()) { 4973 this.getToolbarActionsElement().style.display = 'block'; 4974 } 4975 4976 // Using setTimeout + options.delay because: 4977 // We will actually be displaying the toolbar, which should be controlled by options.delay 4978 this.delayShowTimeout = this.base.delay(function () { 4979 this.showToolbar(); 4980 }.bind(this)); 4981 }, 4982 4983 hideExtensionForms: function () { 4984 // Hide all extension forms 4985 this.forEachExtension(function (extension) { 4986 if (extension.hasForm && extension.isDisplayed()) { 4987 extension.hideForm(); 4988 } 4989 }); 4990 }, 4991 4992 // Responding to changes in user selection 4993 4994 // Checks for existance of multiple block elements in the current selection 4995 multipleBlockElementsSelected: function () { 4996 var regexEmptyHTMLTags = /<[^\/>][^>]*><\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex 4997 regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'), 4998 selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection 4999 hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html 5000 5001 return !!hasMultiParagraphs && hasMultiParagraphs.length > 1; 5002 }, 5003 5004 modifySelection: function () { 5005 var selection = this.window.getSelection(), 5006 selectionRange = selection.getRangeAt(0); 5007 5008 /* 5009 * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start 5010 * will be at the very end of an element. In other browsers, the selectionRange start 5011 * would instead be at the very beginning of an element that actually has content. 5012 * example: 5013 * <span>foo</span><span>bar</span> 5014 * 5015 * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning 5016 * of the 'bar' span. However, there are cases where firefox will have the selectionRange start 5017 * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any 5018 * properties on the 'bar' span, they won't be reflected accurately in the toolbar 5019 * (ie 'Bold' button wouldn't be active) 5020 * 5021 * So, for cases where the selectionRange start is at the end of an element/node, find the next 5022 * adjacent text node that actually has content in it, and move the selectionRange start there. 5023 */ 5024 if (this.standardizeSelectionStart && 5025 selectionRange.startContainer.nodeValue && 5026 (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) { 5027 var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document); 5028 if (adjacentNode) { 5029 var offset = 0; 5030 while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) { 5031 offset = offset + 1; 5032 } 5033 selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset, 5034 selectionRange.endContainer, selectionRange.endOffset); 5035 } 5036 } 5037 }, 5038 5039 checkState: function () { 5040 if (this.base.preventSelectionUpdates) { 5041 return; 5042 } 5043 5044 // If no editable has focus OR selection is inside contenteditable = false 5045 // hide toolbar 5046 if (!this.base.getFocusedElement() || 5047 MediumEditor.selection.selectionInContentEditableFalse(this.window)) { 5048 return this.hideToolbar(); 5049 } 5050 5051 // If there's no selection element, selection element doesn't belong to this editor 5052 // or toolbar is disabled for this selection element 5053 // hide toolbar 5054 var selectionElement = MediumEditor.selection.getSelectionElement(this.window); 5055 if (!selectionElement || 5056 this.getEditorElements().indexOf(selectionElement) === -1 || 5057 selectionElement.getAttribute('data-disable-toolbar')) { 5058 return this.hideToolbar(); 5059 } 5060 5061 // Now we know there's a focused editable with a selection 5062 5063 // If the updateOnEmptySelection option is true, show the toolbar 5064 if (this.updateOnEmptySelection && this.static) { 5065 return this.showAndUpdateToolbar(); 5066 } 5067 5068 // If we don't have a 'valid' selection -> hide toolbar 5069 if (this.window.getSelection().toString().trim() === '' || 5070 (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) { 5071 return this.hideToolbar(); 5072 } 5073 5074 this.showAndUpdateToolbar(); 5075 }, 5076 5077 // Updating the toolbar 5078 5079 showAndUpdateToolbar: function () { 5080 this.modifySelection(); 5081 this.setToolbarButtonStates(); 5082 this.trigger('positionToolbar', {}, this.base.getFocusedElement()); 5083 this.showToolbarDefaultActions(); 5084 this.setToolbarPosition(); 5085 }, 5086 5087 setToolbarButtonStates: function () { 5088 this.forEachExtension(function (extension) { 5089 if (typeof extension.isActive === 'function' && 5090 typeof extension.setInactive === 'function') { 5091 extension.setInactive(); 5092 } 5093 }); 5094 5095 this.checkActiveButtons(); 5096 }, 5097 5098 checkActiveButtons: function () { 5099 var manualStateChecks = [], 5100 queryState = null, 5101 selectionRange = MediumEditor.selection.getSelectionRange(this.document), 5102 parentNode, 5103 updateExtensionState = function (extension) { 5104 if (typeof extension.checkState === 'function') { 5105 extension.checkState(parentNode); 5106 } else if (typeof extension.isActive === 'function' && 5107 typeof extension.isAlreadyApplied === 'function' && 5108 typeof extension.setActive === 'function') { 5109 if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) { 5110 extension.setActive(); 5111 } 5112 } 5113 }; 5114 5115 if (!selectionRange) { 5116 return; 5117 } 5118 5119 // Loop through all extensions 5120 this.forEachExtension(function (extension) { 5121 // For those extensions where we can use document.queryCommandState(), do so 5122 if (typeof extension.queryCommandState === 'function') { 5123 queryState = extension.queryCommandState(); 5124 // If queryCommandState returns a valid value, we can trust the browser 5125 // and don't need to do our manual checks 5126 if (queryState !== null) { 5127 if (queryState && typeof extension.setActive === 'function') { 5128 extension.setActive(); 5129 } 5130 return; 5131 } 5132 } 5133 // We can't use queryCommandState for this extension, so add to manualStateChecks 5134 manualStateChecks.push(extension); 5135 }); 5136 5137 parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange); 5138 5139 // Make sure the selection parent isn't outside of the contenteditable 5140 if (!this.getEditorElements().some(function (element) { 5141 return MediumEditor.util.isDescendant(element, parentNode, true); 5142 })) { 5143 return; 5144 } 5145 5146 // Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node 5147 while (parentNode) { 5148 manualStateChecks.forEach(updateExtensionState); 5149 5150 // we can abort the search upwards if we leave the contentEditable element 5151 if (MediumEditor.util.isMediumEditorElement(parentNode)) { 5152 break; 5153 } 5154 parentNode = parentNode.parentNode; 5155 } 5156 }, 5157 5158 // Positioning toolbar 5159 5160 positionToolbarIfShown: function () { 5161 if (this.isDisplayed()) { 5162 this.setToolbarPosition(); 5163 } 5164 }, 5165 5166 setToolbarPosition: function () { 5167 var container = this.base.getFocusedElement(), 5168 selection = this.window.getSelection(), 5169 anchorPreview; 5170 5171 // If there isn't a valid selection, bail 5172 if (!container) { 5173 return this; 5174 } 5175 5176 if (this.static && !this.relativeContainer) { 5177 this.showToolbar(); 5178 this.positionStaticToolbar(container); 5179 } else if (!selection.isCollapsed) { 5180 this.showToolbar(); 5181 5182 // we don't need any absolute positioning if relativeContainer is set 5183 if (!this.relativeContainer) { 5184 this.positionToolbar(selection); 5185 } 5186 } 5187 5188 anchorPreview = this.base.getExtensionByName('anchor-preview'); 5189 5190 if (anchorPreview && typeof anchorPreview.hidePreview === 'function') { 5191 anchorPreview.hidePreview(); 5192 } 5193 }, 5194 5195 positionStaticToolbar: function (container) { 5196 // position the toolbar at left 0, so we can get the real width of the toolbar 5197 this.getToolbarElement().style.left = '0'; 5198 5199 // document.documentElement for IE 9 5200 var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop, 5201 windowWidth = this.window.innerWidth, 5202 toolbarElement = this.getToolbarElement(), 5203 containerRect = container.getBoundingClientRect(), 5204 containerTop = containerRect.top + scrollTop, 5205 containerCenter = (containerRect.left + (containerRect.width / 2)), 5206 toolbarHeight = toolbarElement.offsetHeight, 5207 toolbarWidth = toolbarElement.offsetWidth, 5208 halfOffsetWidth = toolbarWidth / 2, 5209 targetLeft; 5210 5211 if (this.sticky) { 5212 // If it's beyond the height of the editor, position it at the bottom of the editor
5213 if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) { 5214 toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px'; 5215 toolbarElement.classList.remove('medium-editor-sticky-toolbar'); 5216 5217 // Stick the toolbar to the top of the window 5218 } else if (scrollTop > (containerTop - toolbarHeight)) { 5219 toolbarElement.classList.add('medium-editor-sticky-toolbar'); 5220 toolbarElement.style.top = '0px'; 5221 5222 // Normal static toolbar position 5223 } else { 5224 toolbarElement.classList.remove('medium-editor-sticky-toolbar'); 5225 toolbarElement.style.top = containerTop - toolbarHeight + 'px'; 5226 } 5227 } else { 5228 toolbarElement.style.top = containerTop - toolbarHeight + 'px'; 5229 } 5230 5231 switch (this.align) { 5232 case 'left': 5233 targetLeft = containerRect.left; 5234 break; 5235 5236 case 'right': 5237 targetLeft = containerRect.right - toolbarWidth; 5238 break; 5239 5240 case 'center': 5241 targetLeft = containerCenter - halfOffsetWidth; 5242 break; 5243 } 5244 5245 if (targetLeft < 0) { 5246 targetLeft = 0; 5247 } else if ((targetLeft + toolbarWidth) > windowWidth) { 5248 targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1); 5249 } 5250 5251 toolbarElement.style.left = targetLeft + 'px'; 5252 }, 5253 5254 positionToolbar: function (selection) { 5255 // position the toolbar at left 0, so we can get the real width of the toolbar 5256 this.getToolbarElement().style.left = '0'; 5257 5258 var windowWidth = this.window.innerWidth, 5259 range = selection.getRangeAt(0), 5260 boundary = range.getBoundingClientRect(), 5261 middleBoundary = (boundary.left + boundary.right) / 2, 5262 toolbarElement = this.getToolbarElement(), 5263 toolbarHeight = toolbarElement.offsetHeight, 5264 toolbarWidth = toolbarElement.offsetWidth, 5265 halfOffsetWidth = toolbarWidth / 2, 5266 buttonHeight = 50, 5267 defaultLeft = this.diffLeft - halfOffsetWidth; 5268 5269 if (boundary.top < buttonHeight) { 5270 toolbarElement.classList.add('medium-toolbar-arrow-over'); 5271 toolbarElement.classList.remove('medium-toolbar-arrow-under'); 5272 toolbarElement.style.top = buttonHeight + boundary.bottom - this.diffTop + this.window.pageYOffset - toolbarHeight + 'px'; 5273 } else { 5274 toolbarElement.classList.add('medium-toolbar-arrow-under'); 5275 toolbarElement.classList.remove('medium-toolbar-arrow-over'); 5276 toolbarElement.style.top = boundary.top + this.diffTop + this.window.pageYOffset - toolbarHeight + 'px'; 5277 } 5278 5279 if (middleBoundary < halfOffsetWidth) { 5280 toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px'; 5281 } else if ((windowWidth - middleBoundary) < halfOffsetWidth) { 5282 toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px'; 5283 } else { 5284 toolbarElement.style.left = defaultLeft + middleBoundary + 'px'; 5285 } 5286 } 5287 }); 5288 5289 MediumEditor.extensions.toolbar = Toolbar; 5290}()); 5291 5292(function () { 5293 'use strict'; 5294 5295 var ImageDragging = MediumEditor.Extension.extend({ 5296 init: function () { 5297 MediumEditor.Extension.prototype.init.apply(this, arguments); 5298 5299 this.subscribe('editableDrag', this.handleDrag.bind(this)); 5300 this.subscribe('editableDrop', this.handleDrop.bind(this)); 5301 }, 5302 5303 handleDrag: function (event) { 5304 var className = 'medium-editor-dragover'; 5305 event.preventDefault(); 5306 event.dataTransfer.dropEffect = 'copy'; 5307 5308 if (event.type === 'dragover') { 5309 event.target.classList.add(className); 5310 } else if (event.type === 'dragleave') { 5311 event.target.classList.remove(className); 5312 } 5313 }, 5314 5315 handleDrop: function (event) { 5316 var className = 'medium-editor-dragover', 5317 files; 5318 event.preventDefault(); 5319 event.stopPropagation(); 5320 5321 // IE9 does not support the File API, so prevent file from opening in a new window 5322 // but also don't try to actually get the file 5323 if (event.dataTransfer.files) { 5324 files = Array.prototype.slice.call(event.dataTransfer.files, 0); 5325 files.some(function (file) { 5326 if (file.type.match('image')) { 5327 var fileReader, id; 5328 fileReader = new FileReader();
vendor: 4,929 bytes, lines 5329-5432
5329 fileReader.readAsDataURL(file); 5330 5331 id = 'medium-img-' + (+new Date()); 5332 MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />'); 5333 5334 fileReader.onload = function () { 5335 var img = this.document.getElementById(id); 5336 if (img) { 5337 img.removeAttribute('id'); 5338 img.removeAttribute('class'); 5339 img.src = fileReader.result; 5340 } 5341 }.bind(this); 5342 } 5343 }.bind(this)); 5344 } 5345 event.target.classList.remove(className); 5346 } 5347 }); 5348 5349 MediumEditor.extensions.imageDragging = ImageDragging; 5350}()); 5351 5352(function () { 5353 'use strict'; 5354 5355 // Event handlers that shouldn't be exposed externally 5356 5357 function handleDisabledEnterKeydown(event, element) { 5358 if (this.options.disableReturn || element.getAttribute('data-disable-return')) { 5359 event.preventDefault(); 5360 } else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) { 5361 var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument); 5362 5363 // if current text selection is empty OR previous sibling text is empty 5364 if ((node && node.textContent.trim() === '') || 5365 (node.previousElementSibling && node.previousElementSibling.textContent.trim() === '')) { 5366 event.preventDefault(); 5367 } 5368 } 5369 } 5370 5371 function handleTabKeydown(event) { 5372 // Override tab only for pre nodes 5373 var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument), 5374 tag = node && node.nodeName.toLowerCase(); 5375 5376 if (tag === 'pre') { 5377 event.preventDefault(); 5378 MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, ' '); 5379 } 5380 5381 // Tab to indent list structures! 5382 if (MediumEditor.util.isListItem(node)) { 5383 event.preventDefault(); 5384 5385 // If Shift is down, outdent, otherwise indent 5386 if (event.shiftKey) { 5387 this.options.ownerDocument.execCommand('outdent', false, null); 5388 } else { 5389 this.options.ownerDocument.execCommand('indent', false, null); 5390 } 5391 } 5392 } 5393 5394 function handleBlockDeleteKeydowns(event) { 5395 var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument), 5396 tagName = node.nodeName.toLowerCase(), 5397 isEmpty = /^(\s+|<br\/?>)?$/i, 5398 isHeader = /h\d/i; 5399 5400 if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) && 5401 // has a preceeding sibling 5402 node.previousElementSibling && 5403 // in a header 5404 isHeader.test(tagName) && 5405 // at the very end of the block 5406 MediumEditor.selection.getCaretOffsets(node).left === 0) { 5407 if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) { 5408 // backspacing the begining of a header into an empty previous element will 5409 // change the tagName of the current node to prevent one 5410 // instead delete previous node and cancel the event. 5411 node.previousElementSibling.parentNode.removeChild(node.previousElementSibling); 5412 event.preventDefault(); 5413 } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) { 5414 // hitting return in the begining of a header will create empty header elements before the current one 5415 // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph 5416 p = this.options.ownerDocument.createElement('p'); 5417 p.innerHTML = '<br>'; 5418 node.previousElementSibling.parentNode.insertBefore(p, node); 5419 event.preventDefault(); 5420 } 5421 } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) && 5422 // between two sibling elements 5423 node.nextElementSibling && 5424 node.previousElementSibling && 5425 // not in a header 5426 !isHeader.test(tagName) && 5427 // in an empty tag 5428 isEmpty.test(node.innerHTML) && 5429 // when the next tag *is* a header 5430 isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) { 5431 // hitting delete in an empty element preceding a header, ex: 5432 // <p>[CURSOR]</p><h1>Header</h1>
5433 // Will cause the h1 to become a paragraph. 5434 // Instead, delete the paragraph node and move the cursor to the begining of the h1 5435 5436 // remove node and move cursor to start of header 5437 MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling); 5438 5439 node.previousElementSibling.parentNode.removeChild(node); 5440 5441 event.preventDefault(); 5442 } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && 5443 tagName === 'li' && 5444 // hitting backspace inside an empty li 5445 isEmpty.test(node.innerHTML) && 5446 // is first element (no preceeding siblings) 5447 !node.previousElementSibling && 5448 // parent also does not have a sibling 5449 !node.parentElement.previousElementSibling && 5450 // is not the only li in a list 5451 node.nextElementSibling && 5452 node.nextElementSibling.nodeName.toLowerCase() === 'li') { 5453 // backspacing in an empty first list element in the first list (with more elements) ex: 5454 // <ul><li>[CURSOR]</li><li>List Item 2</li></ul> 5455 // will remove the first <li> but add some extra element before (varies based on browser) 5456 // Instead, this will: 5457 // 1) remove the list element 5458 // 2) create a paragraph before the list 5459 // 3) move the cursor into the paragraph 5460 5461 // create a paragraph before the list 5462 p = this.options.ownerDocument.createElement('p'); 5463 p.innerHTML = '<br>'; 5464 node.parentElement.parentElement.insertBefore(p, node.parentElement); 5465 5466 // move the cursor into the new paragraph 5467 MediumEditor.selection.moveCursor(this.options.ownerDocument, p); 5468 5469 // remove the list element 5470 node.parentElement.removeChild(node); 5471 5472 event.preventDefault(); 5473 } 5474 } 5475 5476 function handleKeyup(event) { 5477 var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument), 5478 tagName; 5479 5480 if (!node) { 5481 return; 5482 } 5483 5484 if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0) { 5485 this.options.ownerDocument.execCommand('formatBlock', false, 'p'); 5486 } 5487 5488 if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) && !MediumEditor.util.isListItem(node)) { 5489 tagName = node.nodeName.toLowerCase(); 5490 // For anchor tags, unlink 5491 if (tagName === 'a') { 5492 this.options.ownerDocument.execCommand('unlink', false, null); 5493 } else if (!event.shiftKey && !event.ctrlKey) { 5494 // only format block if this is not a header tag 5495 if (!/h\d/.test(tagName)) { 5496 this.options.ownerDocument.execCommand('formatBlock', false, 'p'); 5497 } 5498 } 5499 } 5500 } 5501 5502 // Internal helper methods which shouldn't be exposed externally 5503 5504 function addToEditors(win) { 5505 if (!win._mediumEditors) { 5506 // To avoid breaking users who are assuming that the unique id on 5507 // medium-editor elements will start at 1, inserting a 'null' in the 5508 // array so the unique-id can always map to the index of the editor instance 5509 win._mediumEditors = [null]; 5510 } 5511 5512 // If this already has a unique id, re-use it 5513 if (!this.id) { 5514 this.id = win._mediumEditors.length; 5515 } 5516 5517 win._mediumEditors[this.id] = this; 5518 } 5519 5520 function removeFromEditors(win) { 5521 if (!win._mediumEditors || !win._mediumEditors[this.id]) { 5522 return; 5523 } 5524 5525 /* Setting the instance to null in the array instead of deleting it allows: 5526 * 1) Each instance to preserve its own unique-id, even after being destroyed 5527 * and initialized again 5528 * 2) The unique-id to always correspond to an index in the array of medium-editor 5529 * instances. Thus, we will be able to look at a contenteditable, and determine 5530 * which instance it belongs to, by indexing into the global array. 5531 */ 5532 win._mediumEditors[this.id] = null; 5533 } 5534 5535 function createElementsArray(selector) { 5536 if (!selector) { 5537 selector = []; 5538 }
5539 // If string, use as query selector 5540 if (typeof selector === 'string') { 5541 selector = this.options.ownerDocument.querySelectorAll(selector); 5542 } 5543 // If element, put into array 5544 if (MediumEditor.util.isElement(selector)) { 5545 selector = [selector]; 5546 } 5547 // Convert NodeList (or other array like object) into an array 5548 var elements = Array.prototype.slice.apply(selector); 5549 5550 // Loop through elements and convert textarea's into divs 5551 this.elements = []; 5552 elements.forEach(function (element, index) { 5553 if (element.nodeName.toLowerCase() === 'textarea') { 5554 this.elements.push(createContentEditable.call(this, element, index)); 5555 } else { 5556 this.elements.push(element); 5557 } 5558 }, this); 5559 } 5560 5561 function setExtensionDefaults(extension, defaults) { 5562 Object.keys(defaults).forEach(function (prop) { 5563 if (extension[prop] === undefined) { 5564 extension[prop] = defaults[prop]; 5565 } 5566 }); 5567 return extension; 5568 } 5569 5570 function initExtension(extension, name, instance) { 5571 var extensionDefaults = { 5572 'window': instance.options.contentWindow, 5573 'document': instance.options.ownerDocument, 5574 'base': instance 5575 }; 5576 5577 // Add default options into the extension 5578 extension = setExtensionDefaults(extension, extensionDefaults); 5579 5580 // Call init on the extension 5581 if (typeof extension.init === 'function') { 5582 extension.init(); 5583 } 5584 5585 // Set extension name (if not already set) 5586 if (!extension.name) { 5587 extension.name = name; 5588 } 5589 return extension; 5590 } 5591 5592 function isToolbarEnabled() { 5593 // If any of the elements don't have the toolbar disabled 5594 // We need a toolbar 5595 if (this.elements.every(function (element) { 5596 return !!element.getAttribute('data-disable-toolbar'); 5597 })) { 5598 return false; 5599 } 5600 5601 return this.options.toolbar !== false; 5602 } 5603 5604 function isAnchorPreviewEnabled() { 5605 // If toolbar is disabled, don't add 5606 if (!isToolbarEnabled.call(this)) { 5607 return false; 5608 } 5609 5610 return this.options.anchorPreview !== false; 5611 } 5612 5613 function isPlaceholderEnabled() { 5614 return this.options.placeholder !== false; 5615 } 5616 5617 function isAutoLinkEnabled() { 5618 return this.options.autoLink !== false; 5619 } 5620 5621 function isImageDraggingEnabled() { 5622 return this.options.imageDragging !== false; 5623 } 5624 5625 function isKeyboardCommandsEnabled() { 5626 return this.options.keyboardCommands !== false; 5627 } 5628 5629 function shouldUseFileDraggingExtension() { 5630 // Since the file-dragging extension replaces the image-dragging extension, 5631 // we need to check if the user passed an overrided image-dragging extension. 5632 // If they have, to avoid breaking users, we won't use file-dragging extension. 5633 return !this.options.extensions['imageDragging']; 5634 } 5635 5636 function createContentEditable(textarea, id) { 5637 var div = this.options.ownerDocument.createElement('div'), 5638 now = Date.now(), 5639 uniqueId = 'medium-editor-' + now + '-' + id, 5640 atts = textarea.attributes; 5641 5642 // Some browsers can move pretty fast, since we're using a timestamp 5643 // to make a unique-id, ensure that the id is actually unique on the page 5644 while (this.options.ownerDocument.getElementById(uniqueId)) { 5645 now++; 5646 uniqueId = 'medium-editor-' + now + '-' + id; 5647 } 5648 5649 div.className = textarea.className; 5650 div.id = uniqueId; 5651 div.innerHTML = textarea.value; 5652 5653 textarea.setAttribute('medium-editor-textarea-id', uniqueId); 5654 5655 // re-create all attributes from the textearea to the new created div 5656 for (var i = 0, n = atts.length; i < n; i++) { 5657 // do not re-create existing attributes 5658 if (!div.hasAttribute(atts[i].nodeName)) { 5659 div.setAttribute(atts[i].nodeName, atts[i].nodeValue); 5660 } 5661 } 5662 5663 textarea.classList.add('medium-editor-hidden'); 5664 textarea.parentNode.insertBefore( 5665 div, 5666 textarea 5667 ); 5668 5669 return div; 5670 } 5671 5672 function initElements() {
5673 this.elements.forEach(function (element, index) { 5674 if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) { 5675 element.setAttribute('contentEditable', true); 5676 element.setAttribute('spellcheck', this.options.spellcheck); 5677 } 5678 element.setAttribute('data-medium-editor-element', true); 5679 element.setAttribute('role', 'textbox'); 5680 element.setAttribute('aria-multiline', true); 5681 element.setAttribute('medium-editor-index', index); 5682 5683 if (element.hasAttribute('medium-editor-textarea-id')) { 5684 this.on(element, 'input', function (event) { 5685 var target = event.target, 5686 textarea = target.parentNode.querySelector('textarea[medium-editor-textarea-id="' + target.getAttribute('medium-editor-textarea-id') + '"]'); 5687 if (textarea) { 5688 textarea.value = this.serialize()[target.id].value; 5689 } 5690 }.bind(this)); 5691 } 5692 }, this); 5693 } 5694 5695 function attachHandlers() { 5696 var i; 5697 5698 // attach to tabs 5699 this.subscribe('editableKeydownTab', handleTabKeydown.bind(this)); 5700 5701 // Bind keys which can create or destroy a block element: backspace, delete, return 5702 this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this)); 5703 this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this)); 5704 5705 // disabling return or double return 5706 if (this.options.disableReturn || this.options.disableDoubleReturn) { 5707 this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this)); 5708 } else { 5709 for (i = 0; i < this.elements.length; i += 1) { 5710 if (this.elements[i].getAttribute('data-disable-return') || this.elements[i].getAttribute('data-disable-double-return')) { 5711 this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this)); 5712 break; 5713 } 5714 } 5715 } 5716 5717 // if we're not disabling return, add a handler to help handle cleanup 5718 // for certain cases when enter is pressed 5719 if (!this.options.disableReturn) { 5720 this.elements.forEach(function (element) { 5721 if (!element.getAttribute('data-disable-return')) { 5722 this.on(element, 'keyup', handleKeyup.bind(this)); 5723 } 5724 }, this); 5725 } 5726 } 5727 5728 function initExtensions() { 5729 5730 this.extensions = []; 5731 5732 // Passed in extensions
vendor: 4,378 bytes, lines 5733-5834
5733 Object.keys(this.options.extensions).forEach(function (name) { 5734 // Always save the toolbar extension for last 5735 if (name !== 'toolbar' && this.options.extensions[name]) { 5736 this.extensions.push(initExtension(this.options.extensions[name], name, this)); 5737 } 5738 }, this); 5739 5740 // 4 Cases for imageDragging + fileDragging extensons: 5741 // 5742 // 1. ImageDragging ON + No Custom Image Dragging Extension: 5743 // * Use fileDragging extension (default options) 5744 // 2. ImageDragging OFF + No Custom Image Dragging Extension: 5745 // * Use fileDragging extension w/ images turned off 5746 // 3. ImageDragging ON + Custom Image Dragging Extension: 5747 // * Don't use fileDragging (could interfere with custom image dragging extension) 5748 // 4. ImageDragging OFF + Custom Image Dragging: 5749 // * Don't use fileDragging (could interfere with custom image dragging extension) 5750 if (shouldUseFileDraggingExtension.call(this)) { 5751 var opts = this.options.fileDragging; 5752 if (!opts) { 5753 opts = {}; 5754 5755 // Image is in the 'allowedTypes' list by default. 5756 // If imageDragging is off override the 'allowedTypes' list with an empty one 5757 if (!isImageDraggingEnabled.call(this)) { 5758 opts.allowedTypes = []; 5759 } 5760 } 5761 this.addBuiltInExtension('fileDragging', opts); 5762 } 5763 5764 // Built-in extensions 5765 var builtIns = { 5766 paste: true, 5767 'anchor-preview': isAnchorPreviewEnabled.call(this), 5768 autoLink: isAutoLinkEnabled.call(this), 5769 keyboardCommands: isKeyboardCommandsEnabled.call(this), 5770 placeholder: isPlaceholderEnabled.call(this) 5771 }; 5772 Object.keys(builtIns).forEach(function (name) { 5773 if (builtIns[name]) { 5774 this.addBuiltInExtension(name); 5775 } 5776 }, this); 5777 5778 // Users can pass in a custom toolbar extension 5779 // so check for that first and if it's not present 5780 // just create the default toolbar 5781 var toolbarExtension = this.options.extensions['toolbar']; 5782 if (!toolbarExtension && isToolbarEnabled.call(this)) { 5783 // Backwards compatability 5784 var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, { 5785 allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated 5786 }); 5787 toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions); 5788 } 5789 5790 // If the toolbar is not disabled, so we actually have an extension 5791 // initialize it and add it to the extensions array 5792 if (toolbarExtension) { 5793 this.extensions.push(initExtension(toolbarExtension, 'toolbar', this)); 5794 } 5795 } 5796 5797 function mergeOptions(defaults, options) { 5798 var deprecatedProperties = [ 5799 ['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection'] 5800 ]; 5801 // warn about using deprecated properties 5802 if (options) { 5803 deprecatedProperties.forEach(function (pair) { 5804 if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) { 5805 MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0'); 5806 } 5807 }); 5808 } 5809 5810 return MediumEditor.util.defaults({}, options, defaults); 5811 } 5812 5813 function execActionInternal(action, opts) { 5814 /*jslint regexp: true*/ 5815 var appendAction = /^append-(.+)$/gi, 5816 justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */ 5817 match; 5818 /*jslint regexp: false*/ 5819 5820 // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific 5821 // type of block element (ie append-blockquote, append-h1, append-pre, etc.) 5822 match = appendAction.exec(action); 5823 if (match) { 5824 return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]); 5825 } 5826 5827 if (action === 'fontSize') { 5828 return this.options.ownerDocument.execCommand('fontSize', false, opts.size); 5829 } 5830 5831 if (action === 'createLink') { 5832 return this.createLink(opts); 5833 } 5834
5835 if (action === 'image') { 5836 return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection()); 5837 } 5838 5839 /* Issue: https://github.com/yabwe/medium-editor/issues/595 5840 * If the action is to justify the text */ 5841 if (justifyAction.exec(action)) { 5842 var result = this.options.ownerDocument.execCommand(action, false, null), 5843 parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument)); 5844 if (parentNode) { 5845 cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode)); 5846 } 5847 5848 return result; 5849 } 5850 5851 return this.options.ownerDocument.execCommand(action, false, null); 5852 } 5853 5854 /* If we've just justified text within a container block 5855 * Chrome may have removed <br> elements and instead wrapped lines in <div> elements 5856 * with a text-align property. If so, we want to fix this 5857 */ 5858 function cleanupJustifyDivFragments(blockContainer) { 5859 if (!blockContainer) { 5860 return; 5861 } 5862 5863 var textAlign, 5864 childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function (element) { 5865 var isDiv = element.nodeName.toLowerCase() === 'div'; 5866 if (isDiv && !textAlign) { 5867 textAlign = element.style.textAlign; 5868 } 5869 return isDiv; 5870 }); 5871 5872 /* If we found child <div> elements with text-align style attributes 5873 * we should fix this by: 5874 * 5875 * 1) Unwrapping each <div> which has a text-align style 5876 * 2) Insert a <br> element after each set of 'unwrapped' div children 5877 * 3) Set the text-align style of the parent block element 5878 */ 5879 if (childDivs.length) { 5880 // Since we're mucking with the HTML, preserve selection 5881 this.saveSelection(); 5882 childDivs.forEach(function (div) { 5883 if (div.style.textAlign === textAlign) { 5884 var lastChild = div.lastChild; 5885 if (lastChild) { 5886 // Instead of a div, extract the child elements and add a <br> 5887 MediumEditor.util.unwrap(div, this.options.ownerDocument); 5888 var br = this.options.ownerDocument.createElement('BR'); 5889 lastChild.parentNode.insertBefore(br, lastChild.nextSibling); 5890 } 5891 } 5892 }, this); 5893 blockContainer.style.textAlign = textAlign; 5894 // We're done, so restore selection 5895 this.restoreSelection(); 5896 } 5897 } 5898 5899 MediumEditor.prototype = { 5900 // NOT DOCUMENTED - exposed for backwards compatability 5901 init: function (elements, options) { 5902 this.options = mergeOptions.call(this, this.defaults, options); 5903 this.origElements = elements; 5904 5905 if (!this.options.elementsContainer) { 5906 this.options.elementsContainer = this.options.ownerDocument.body; 5907 } 5908 5909 return this.setup(); 5910 }, 5911 5912 setup: function () { 5913 if (this.isActive) { 5914 return; 5915 } 5916 5917 createElementsArray.call(this, this.origElements); 5918 5919 if (this.elements.length === 0) { 5920 return; 5921 } 5922 5923 this.isActive = true; 5924 addToEditors.call(this, this.options.contentWindow); 5925 5926 this.events = new MediumEditor.Events(this); 5927 5928 // Call initialization helpers 5929 initElements.call(this); 5930 initExtensions.call(this); 5931 attachHandlers.call(this); 5932 }, 5933 5934 destroy: function () { 5935 if (!this.isActive) { 5936 return; 5937 } 5938 5939 this.isActive = false; 5940 5941 this.extensions.forEach(function (extension) { 5942 if (typeof extension.destroy === 'function') { 5943 extension.destroy(); 5944 } 5945 }, this); 5946 5947 this.events.destroy(); 5948
vendor: 22,249 bytes, lines 5949-6445
5949 this.elements.forEach(function (element) { 5950 // Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left 5951 if (this.options.spellcheck) { 5952 element.innerHTML = element.innerHTML; 5953 } 5954 5955 // cleanup extra added attributes 5956 element.removeAttribute('contentEditable'); 5957 element.removeAttribute('spellcheck'); 5958 element.removeAttribute('data-medium-editor-element'); 5959 element.removeAttribute('role'); 5960 element.removeAttribute('aria-multiline'); 5961 element.removeAttribute('medium-editor-index'); 5962 5963 // Remove any elements created for textareas 5964 if (element.hasAttribute('medium-editor-textarea-id')) { 5965 var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]'); 5966 if (textarea) { 5967 // Un-hide the textarea 5968 textarea.classList.remove('medium-editor-hidden'); 5969 } 5970 if (element.parentNode) { 5971 element.parentNode.removeChild(element); 5972 } 5973 } 5974 }, this); 5975 this.elements = []; 5976 5977 removeFromEditors.call(this, this.options.contentWindow); 5978 }, 5979 5980 on: function (target, event, listener, useCapture) { 5981 this.events.attachDOMEvent(target, event, listener, useCapture); 5982 }, 5983 5984 off: function (target, event, listener, useCapture) { 5985 this.events.detachDOMEvent(target, event, listener, useCapture); 5986 }, 5987 5988 subscribe: function (event, listener) { 5989 this.events.attachCustomEvent(event, listener); 5990 }, 5991 5992 unsubscribe: function (event, listener) { 5993 this.events.detachCustomEvent(event, listener); 5994 }, 5995 5996 trigger: function (name, data, editable) { 5997 this.events.triggerCustomEvent(name, data, editable); 5998 }, 5999 6000 delay: function (fn) { 6001 var self = this; 6002 return setTimeout(function () { 6003 if (self.isActive) { 6004 fn(); 6005 } 6006 }, this.options.delay); 6007 }, 6008 6009 serialize: function () { 6010 var i, 6011 elementid, 6012 content = {}; 6013 for (i = 0; i < this.elements.length; i += 1) { 6014 elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i; 6015 content[elementid] = { 6016 value: this.elements[i].innerHTML.trim() 6017 }; 6018 } 6019 return content; 6020 }, 6021 6022 getExtensionByName: function (name) { 6023 var extension; 6024 if (this.extensions && this.extensions.length) { 6025 this.extensions.some(function (ext) { 6026 if (ext.name === name) { 6027 extension = ext; 6028 return true; 6029 } 6030 return false; 6031 }); 6032 } 6033 return extension; 6034 }, 6035 6036 /** 6037 * NOT DOCUMENTED - exposed as a helper for other extensions to use 6038 */ 6039 addBuiltInExtension: function (name, opts) { 6040 var extension = this.getExtensionByName(name), 6041 merged; 6042 if (extension) { 6043 return extension; 6044 } 6045 6046 switch (name) { 6047 case 'anchor': 6048 merged = MediumEditor.util.extend({}, this.options.anchor, opts); 6049 extension = new MediumEditor.extensions.anchor(merged); 6050 break; 6051 case 'anchor-preview': 6052 extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview); 6053 break; 6054 case 'autoLink': 6055 extension = new MediumEditor.extensions.autoLink(); 6056 break; 6057 case 'fileDragging': 6058 extension = new MediumEditor.extensions.fileDragging(opts); 6059 break; 6060 case 'fontsize': 6061 extension = new MediumEditor.extensions.fontSize(opts); 6062 break; 6063 case 'keyboardCommands': 6064 extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands); 6065 break; 6066 case 'paste': 6067 extension = new MediumEditor.extensions.paste(this.options.paste); 6068 break; 6069 case 'placeholder': 6070 extension = new MediumEditor.extensions.placeholder(this.options.placeholder); 6071 break; 6072 default: 6073 // All of the built-in buttons for MediumEditor are extensions 6074 // so check to see if the extension we're creating is a built-in button 6075 if (MediumEditor.extensions.button.isBuiltInButton(name)) { 6076 if (opts) { 6077 merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]); 6078 extension = new MediumEditor.extensions.button(merged); 6079 } else { 6080 extension = new MediumEditor.extensions.button(name); 6081 } 6082 } 6083 } 6084 6085 if (extension) { 6086 this.extensions.push(initExtension(extension, name, this)); 6087 } 6088 6089 return extension; 6090 }, 6091 6092 stopSelectionUpdates: function () { 6093 this.preventSelectionUpdates = true; 6094 }, 6095 6096 startSelectionUpdates: function () { 6097 this.preventSelectionUpdates = false; 6098 }, 6099 6100 checkSelection: function () { 6101 var toolbar = this.getExtensionByName('toolbar'); 6102 if (toolbar) { 6103 toolbar.checkState(); 6104 } 6105 return this; 6106 }, 6107 6108 // Wrapper around document.queryCommandState for checking whether an action has already 6109 // been applied to the current selection 6110 queryCommandState: function (action) { 6111 var fullAction = /^full-(.+)$/gi, 6112 match, 6113 queryState = null; 6114 6115 // Actions starting with 'full-' need to be modified since this is a medium-editor concept 6116 match = fullAction.exec(action); 6117 if (match) { 6118 action = match[1]; 6119 } 6120 6121 try { 6122 queryState = this.options.ownerDocument.queryCommandState(action); 6123 } catch (exc) { 6124 queryState = null; 6125 } 6126 6127 return queryState; 6128 }, 6129 6130 execAction: function (action, opts) { 6131 /*jslint regexp: true*/ 6132 var fullAction = /^full-(.+)$/gi, 6133 match, 6134 result; 6135 /*jslint regexp: false*/ 6136 6137 // Actions starting with 'full-' should be applied to to the entire contents of the editable element 6138 // (ie full-bold, full-append-pre, etc.) 6139 match = fullAction.exec(action); 6140 if (match) { 6141 // Store the current selection to be restored after applying the action 6142 this.saveSelection(); 6143 // Select all of the contents before calling the action 6144 this.selectAllContents(); 6145 result = execActionInternal.call(this, match[1], opts); 6146 // Restore the previous selection 6147 this.restoreSelection(); 6148 } else { 6149 result = execActionInternal.call(this, action, opts); 6150 } 6151 6152 // do some DOM clean-up for known browser issues after the action 6153 if (action === 'insertunorderedlist' || action === 'insertorderedlist') { 6154 MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement()); 6155 } 6156 6157 this.checkSelection(); 6158 return result; 6159 }, 6160 6161 getSelectedParentElement: function (range) { 6162 if (range === undefined) { 6163 range = this.options.contentWindow.getSelection().getRangeAt(0); 6164 } 6165 return MediumEditor.selection.getSelectedParentElement(range); 6166 }, 6167 6168 selectAllContents: function () { 6169 var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow); 6170 6171 if (currNode) { 6172 // Move to the lowest descendant node that still selects all of the contents 6173 while (currNode.children.length === 1) { 6174 currNode = currNode.children[0]; 6175 } 6176 6177 this.selectElement(currNode); 6178 } 6179 }, 6180 6181 selectElement: function (element) { 6182 MediumEditor.selection.selectNode(element, this.options.ownerDocument); 6183 6184 var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow); 6185 if (selElement) { 6186 this.events.focusElement(selElement); 6187 } 6188 }, 6189 6190 getFocusedElement: function () { 6191 var focused; 6192 this.elements.some(function (element) { 6193 // Find the element that has focus 6194 if (!focused && element.getAttribute('data-medium-focused')) { 6195 focused = element; 6196 } 6197 6198 // bail if we found the element that had focus 6199 return !!focused; 6200 }, this); 6201 6202 return focused; 6203 }, 6204 6205 // Export the state of the selection in respect to one of this 6206 // instance of MediumEditor's elements 6207 exportSelection: function () { 6208 var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow), 6209 editableElementIndex = this.elements.indexOf(selectionElement), 6210 selectionState = null; 6211 6212 if (editableElementIndex >= 0) { 6213 selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument); 6214 } 6215 6216 if (selectionState !== null && editableElementIndex !== 0) { 6217 selectionState.editableElementIndex = editableElementIndex; 6218 } 6219 6220 return selectionState; 6221 }, 6222 6223 saveSelection: function () { 6224 this.selectionState = this.exportSelection(); 6225 }, 6226 6227 // Restore a selection based on a selectionState returned by a call 6228 // to MediumEditor.exportSelection 6229 importSelection: function (selectionState, favorLaterSelectionAnchor) { 6230 if (!selectionState) { 6231 return; 6232 } 6233 6234 var editableElement = this.elements[selectionState.editableElementIndex || 0]; 6235 MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor); 6236 }, 6237 6238 restoreSelection: function () { 6239 this.importSelection(this.selectionState); 6240 }, 6241 6242 createLink: function (opts) { 6243 var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow), 6244 customEvent = {}; 6245 6246 // Make sure the selection is within an element this editor is tracking 6247 if (this.elements.indexOf(currentEditor) === -1) { 6248 return; 6249 } 6250 6251 try { 6252 this.events.disableCustomEvent('editableInput'); 6253 if (opts.url && opts.url.trim().length > 0) { 6254 var currentSelection = this.options.contentWindow.getSelection(); 6255 if (currentSelection) { 6256 var currRange = currentSelection.getRangeAt(0), 6257 commonAncestorContainer = currRange.commonAncestorContainer, 6258 exportedSelection, 6259 startContainerParentElement, 6260 endContainerParentElement, 6261 textNodes; 6262 6263 // If the selection is contained within a single text node 6264 // and the selection starts at the beginning of the text node, 6265 // MSIE still says the startContainer is the parent of the text node. 6266 // If the selection is contained within a single text node, we 6267 // want to just use the default browser 'createLink', so we need 6268 // to account for this case and adjust the commonAncestorContainer accordingly 6269 if (currRange.endContainer.nodeType === 3 && 6270 currRange.startContainer.nodeType !== 3 && 6271 currRange.startOffset === 0 && 6272 currRange.startContainer.firstChild === currRange.endContainer) { 6273 commonAncestorContainer = currRange.endContainer; 6274 } 6275 6276 startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer); 6277 endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer); 6278 6279 // If the selection is not contained within a single text node 6280 // but the selection is contained within the same block element 6281 // we want to make sure we create a single link, and not multiple links 6282 // which can happen with the built in browser functionality 6283 if (commonAncestorContainer.nodeType !== 3 && startContainerParentElement === endContainerParentElement) { 6284 var parentElement = (startContainerParentElement || currentEditor), 6285 fragment = this.options.ownerDocument.createDocumentFragment(); 6286 6287 // since we are going to create a link from an extracted text, 6288 // be sure that if we are updating a link, we won't let an empty link behind (see #754) 6289 // (Workaroung for Chrome) 6290 this.execAction('unlink'); 6291 6292 exportedSelection = this.exportSelection(); 6293 fragment.appendChild(parentElement.cloneNode(true)); 6294 6295 if (currentEditor === parentElement) { 6296 // We have to avoid the editor itself being wiped out when it's the only block element, 6297 // as our reference inside this.elements gets detached from the page when insertHTML runs. 6298 // If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length] 6299 // as the range boundaries, this happens whenever parentElement === currentEditor. 6300 // The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at 6301 // the end of the editor's content. 6302 // In Gecko: 6303 // as an empty <strong></strong> if parentElement.lastChild is a <strong> tag. 6304 // In WebKit: 6305 // an invented <br /> tag at the end in the same situation 6306 MediumEditor.selection.select( 6307 this.options.ownerDocument, 6308 parentElement.firstChild, 6309 0, 6310 parentElement.lastChild, 6311 parentElement.lastChild.nodeType === 3 ? 6312 parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length 6313 ); 6314 } else { 6315 MediumEditor.selection.select( 6316 this.options.ownerDocument, 6317 parentElement, 6318 0, 6319 parentElement, 6320 parentElement.childNodes.length 6321 ); 6322 } 6323 6324 var modifiedExportedSelection = this.exportSelection(); 6325 6326 textNodes = MediumEditor.util.findOrCreateMatchingTextNodes( 6327 this.options.ownerDocument, 6328 fragment, 6329 { 6330 start: exportedSelection.start - modifiedExportedSelection.start, 6331 end: exportedSelection.end - modifiedExportedSelection.start, 6332 editableElementIndex: exportedSelection.editableElementIndex 6333 } 6334 ); 6335 // If textNodes are not present, when changing link on images 6336 // ex: <a><img src="http://image.test.com"></a>, change fragment to currRange.startContainer 6337 // and set textNodes array to [imageElement, imageElement] 6338 if (textNodes.length === 0) { 6339 fragment = this.options.ownerDocument.createDocumentFragment(); 6340 fragment.appendChild(commonAncestorContainer.cloneNode(true)); 6341 textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild]; 6342 } 6343 6344 // Creates the link in the document fragment 6345 MediumEditor.util.createLink(this.options.ownerDocument, textNodes, opts.url.trim()); 6346 6347 // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection. 6348 var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length; 6349 6350 // Now move the created link back into the original document in a way to preserve undo/redo history 6351 MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, '')); 6352 exportedSelection.start -= leadingWhitespacesCount; 6353 exportedSelection.end -= leadingWhitespacesCount; 6354 6355 this.importSelection(exportedSelection); 6356 } else { 6357 this.options.ownerDocument.execCommand('createLink', false, opts.url); 6358 } 6359 6360 if (this.options.targetBlank || opts.target === '_blank') { 6361 MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url); 6362 } 6363 6364 if (opts.buttonClass) { 6365 MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass); 6366 } 6367 } 6368 } 6369 // Fire input event for backwards compatibility if anyone was listening directly to the DOM input event 6370 if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) { 6371 customEvent = this.options.ownerDocument.createEvent('HTMLEvents'); 6372 customEvent.initEvent('input', true, true, this.options.contentWindow); 6373 for (var i = 0; i < this.elements.length; i += 1) { 6374 this.elements[i].dispatchEvent(customEvent); 6375 } 6376 } 6377 } finally { 6378 this.events.enableCustomEvent('editableInput'); 6379 } 6380 // Fire our custom editableInput event 6381 this.events.triggerCustomEvent('editableInput', customEvent, currentEditor); 6382 }, 6383 6384 cleanPaste: function (text) { 6385 this.getExtensionByName('paste').cleanPaste(text); 6386 }, 6387 6388 pasteHTML: function (html, options) { 6389 this.getExtensionByName('paste').pasteHTML(html, options); 6390 }, 6391 6392 setContent: function (html, index) { 6393 index = index || 0; 6394 6395 if (this.elements[index]) { 6396 var target = this.elements[index]; 6397 target.innerHTML = html; 6398 this.events.updateInput(target, { target: target, currentTarget: target }); 6399 } 6400 } 6401 }; 6402}()); 6403 6404(function () { 6405 // summary: The default options hash used by the Editor 6406 6407 MediumEditor.prototype.defaults = { 6408 activeButtonClass: 'medium-editor-button-active', 6409 buttonLabels: false, 6410 delay: 0, 6411 disableReturn: false, 6412 disableDoubleReturn: false, 6413 disableEditing: false, 6414 autoLink: false, 6415 elementsContainer: false, 6416 contentWindow: window, 6417 ownerDocument: document, 6418 targetBlank: false, 6419 extensions: {}, 6420 spellcheck: true 6421 }; 6422})(); 6423 6424MediumEditor.parseVersionString = function (release) { 6425 var split = release.split('-'), 6426 version = split[0].split('.'), 6427 preRelease = (split.length > 1) ? split[1] : ''; 6428 return { 6429 major: parseInt(version[0], 10), 6430 minor: parseInt(version[1], 10), 6431 revision: parseInt(version[2], 10), 6432 preRelease: preRelease, 6433 toString: function () { 6434 return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : ''); 6435 } 6436 }; 6437}; 6438 6439MediumEditor.version = MediumEditor.parseVersionString.call(this, ({ 6440 // grunt-bump looks for this: 6441 'version': '5.8.3' 6442}).version); 6443 6444 return MediumEditor; 6445}
6445()));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.