inline-editor.js (32250B)
1 /** 2 * Licensed under MIT, https://github.com/sofish/pen 3 * 4 * Customized and fixed by Elementor team 5 */ 6 7 (function(root, doc) { 8 9 var InlineEditor, debugMode, selection, utils = {}; 10 var slice = Array.prototype.slice; 11 12 // allow command list 13 var commandsReg = { 14 block: /^(?:p|h[1-6]|blockquote|pre)$/, 15 inline: /^(?:justify(center|full|left|right)|strikethrough|insert(un)?orderedlist|(in|out)dent)$/, 16 biu: /^(bold|italic|underline)$/, 17 source: /^(?:createlink|unlink)$/, 18 insert: /^(?:inserthorizontalrule|insertimage|insert)$/, 19 wrap: /^(?:code)$/ 20 }; 21 22 var lineBreakReg = /^(?:blockquote|pre|div)$/i, 23 effectNodeReg = /(?:[pubia]|strong|em|h[1-6]|blockquote|code|[uo]l|li)/i; 24 25 var strReg = { 26 whiteSpace: /(^\s+)|(\s+$)/g, 27 mailTo: /^(?!mailto:|.+\/|.+#|.+\?)(.*@.*\..+)$/, 28 http: /^(?!\w+?:\/\/|mailto:|\/|\.\/|\?|#)(.*)$/ 29 }; 30 31 var autoLinkReg = { 32 url: /((https?|ftp):\/\/|www\.)[^\s<]{3,}/gi, 33 prefix: /^(?:https?|ftp):\/\//i, 34 notLink: /^(?:img|a|input|audio|video|source|code|pre|script|head|title|style)$/i, 35 maxLength: 100 36 }; 37 38 var styleBackupDict = { 39 bold: { 40 styleKey: 'font-weight', 41 correctValue: 'normal' 42 }, 43 italic: { 44 styleKey: 'font-style', 45 correctValue: 'normal' 46 }, 47 underline: { 48 styleKey: 'text-decoration', 49 correctValue: 'none' 50 } 51 }; 52 53 // type detect 54 utils.is = function(obj, type) { 55 return Object.prototype.toString.call(obj).slice(8, -1) === type; 56 }; 57 58 utils.forEach = function(obj, iterator, arrayLike) { 59 if (!obj) return; 60 if (arrayLike == null) arrayLike = utils.is(obj, 'Array'); 61 if (arrayLike) { 62 for (var i = 0, l = obj.length; i < l; i++) iterator(obj[i], i, obj); 63 } else { 64 for (var key in obj) { 65 if (obj.hasOwnProperty(key)) iterator(obj[key], key, obj); 66 } 67 } 68 }; 69 70 // copy props from a obj 71 utils.copy = function(defaults, source) { 72 utils.forEach(source, function (value, key) { 73 defaults[key] = utils.is(value, 'Object') ? utils.copy({}, value) : 74 utils.is(value, 'Array') ? utils.copy([], value) : value; 75 }); 76 return defaults; 77 }; 78 79 // log 80 utils.log = function(message, force) { 81 if (debugMode || force) 82 console.log('%cPEN DEBUGGER: %c' + message, 'font-family:arial,sans-serif;color:#1abf89;line-height:2em;', 'font-family:cursor,monospace;color:#333;'); 83 }; 84 85 utils.delayExec = function (fn) { 86 var timer = null; 87 return function (delay) { 88 clearTimeout(timer); 89 timer = setTimeout(function() { 90 fn(); 91 }, delay || 1); 92 }; 93 }; 94 95 // merge: make it easy to have a fallback 96 utils.merge = function(config) { 97 98 // default settings 99 var defaults = { 100 class: 'pen', 101 placeholderClass: 'pen-placeholder', 102 placeholderAttr: 'data-pen-placeholder', 103 debug: false, 104 toolbar: null, // custom toolbar 105 mode: 'basic', 106 ignoreLineBreak: false, 107 toolbarIconsPrefix: 'fa fa-', 108 toolbarIconsDictionary: {externalLink: 'eicon-editor-external-link'}, 109 stay: config.stay || !config.debug, 110 stayMsg: 'Are you going to leave here?', 111 textarea: '<textarea name="content"></textarea>', 112 list: [ 113 'blockquote', 'h2', 'h3', 'p', 'code', 'insertOrderedList', 'insertUnorderedList', 'inserthorizontalrule', 114 'indent', 'outdent', 'bold', 'italic', 'underline', 'createlink', 'insertimage' 115 ], 116 titles: {}, 117 cleanAttrs: ['id', 'class', 'style', 'name'], 118 cleanTags: ['script'], 119 linksInNewWindow: false 120 }; 121 122 // user-friendly config 123 if (config.nodeType === 1) { 124 defaults.editor = config; 125 } else if (config.match && config.match(/^#[\S]+$/)) { 126 defaults.editor = doc.getElementById(config.slice(1)); 127 } else { 128 defaults = utils.copy(defaults, config); 129 } 130 131 return defaults; 132 }; 133 134 function commandOverall(cmd, val) { 135 var message = ' to exec 「' + cmd + '」 command' + (val ? (' with value: ' + val) : ''); 136 137 try { 138 doc.execCommand(cmd, false, val); 139 } catch(err) { 140 // TODO: there's an error when insert a image to document, but not a bug 141 return utils.log('fail' + message, true); 142 } 143 144 utils.log('success' + message); 145 } 146 147 function commandInsert(ctx, name, val) { 148 var node = getNode(ctx); 149 if (!node) return; 150 ctx._range.selectNode(node); 151 ctx._range.collapse(false); 152 153 // hide menu when a image was inserted 154 if(name === 'insertimage' && ctx._menu) toggleNode(ctx._menu, true); 155 156 return commandOverall(name, val); 157 } 158 159 function commandBlock(ctx, name) { 160 var effectNodes = getEffectNodes(ctx), 161 tagsList = effectNodes.map(function(node) { 162 return node.nodeName.toLowerCase(); 163 }); 164 165 if (tagsList.indexOf(name) !== -1) name = 'p'; 166 167 return commandOverall('formatblock', name); 168 } 169 170 function commandWrap(ctx, tag, value) { 171 value = '<' + tag + '>' + (value||selection.toString()) + '</' + tag + '>'; 172 return commandOverall('insertHTML', value); 173 } 174 175 function commandLink(ctx, tag, value) { 176 if (ctx.config.linksInNewWindow && 'unlink' !== tag) { 177 value = '<a href="' + value + '" target="_blank">' + (selection.toString()) + '</a>'; 178 return commandOverall('insertHTML', value); 179 } else { 180 return commandOverall(tag, value); 181 } 182 } 183 184 function createTool(ctx, name, type, group) { 185 var title = ctx.config.titles[name] || '', 186 iconElement = document.createElement( 'div' ); 187 188 iconElement.classList.add('pen-icon'); 189 190 iconElement.setAttribute('title', title); 191 192 if ('parent' === type) { 193 iconElement.classList.add('pen-group-icon'); 194 195 iconElement.setAttribute('data-group-toggle', name); 196 } else { 197 iconElement.setAttribute('data-action', name); 198 } 199 200 if('child' === type) { 201 iconElement.setAttribute('data-group', group); 202 } 203 204 var iconDictionary = ctx.config.toolbarIconsDictionary[ name ]; 205 206 if ( iconDictionary && iconDictionary.text ) { 207 iconElement.textContent = iconDictionary.text; 208 } else { 209 var iconClass; 210 211 if ( iconDictionary && iconDictionary.className ) { 212 iconClass = iconDictionary.className; 213 } else { 214 iconClass = ctx.config.toolbarIconsPrefix + name; 215 } 216 217 iconElement.innerHTML += '<i class="' + iconClass + '" ></i>'; 218 } 219 220 return iconElement.outerHTML; 221 } 222 223 function getMenuTools(ctx) { 224 return slice.call(ctx._menu.children); 225 } 226 227 function activateGroup(ctx, group) { 228 var tools = getMenuTools(ctx); 229 230 tools.forEach(function(tool) { 231 toggleNode(tool, tool.getAttribute('data-group') !== group); 232 }); 233 234 toggleMenuClose(ctx, ! group); 235 236 ctx.refreshMenuPosition(); 237 } 238 239 function showMainMenu(ctx) { 240 activateGroup(ctx, null); 241 242 toggleLinkInput(ctx, true); 243 244 toggleUnlinkTool(ctx, !ctx._urlInput || ctx._urlInput.value === ''); 245 } 246 247 function showLinkInput(ctx) { 248 var tools = getMenuTools(ctx); 249 250 tools.forEach(function(tool) { 251 toggleNode(tool, true); 252 }); 253 254 toggleLinkInput(ctx); 255 256 toggleMenuClose(ctx); 257 } 258 259 function toggleLinkInput(ctx, hide) { 260 var linkInput = ctx._menu.querySelector('.pen-input-wrapper'); 261 262 if (! linkInput) { 263 return; 264 } 265 266 toggleNode(linkInput, hide); 267 } 268 269 function toggleUnlinkTool(ctx, hide) { 270 var unlinkTool = ctx._menu.querySelector('[data-action="unlink"]'); 271 272 if (! unlinkTool) { 273 return; 274 } 275 276 toggleNode(unlinkTool, hide); 277 278 ctx.refreshMenuPosition(); 279 } 280 281 function toggleMenuClose(ctx, hide) { 282 var closeButton = ctx._menu.querySelector('[data-action="close"]'); 283 284 toggleNode(closeButton, hide); 285 286 ctx.refreshMenuPosition(); 287 } 288 289 function createLinkInput(ctx) { 290 var inputWrapper = doc.createElement('div'), 291 urlInput = doc.createElement('input'), 292 newWindowLabel = doc.createElement('label'), 293 newWindowCheckbox = doc.createElement('input'), 294 newWindowIcon = doc.createElement('i'); 295 296 inputWrapper.className = 'pen-input-wrapper'; 297 298 urlInput.className = 'pen-url-input'; 299 urlInput.type = 'url'; 300 urlInput.placeholder = 'http://'; 301 302 newWindowLabel.className = 'pen-icon pen-input-label'; 303 304 newWindowCheckbox.className = 'pen-external-url-checkbox'; 305 newWindowCheckbox.type = 'checkbox'; 306 307 newWindowIcon.className = ctx.config.toolbarIconsDictionary.externalLink.className; 308 309 newWindowLabel.appendChild(newWindowCheckbox); 310 newWindowLabel.appendChild(newWindowIcon); 311 312 inputWrapper.appendChild(urlInput); 313 inputWrapper.appendChild(newWindowLabel); 314 315 return inputWrapper; 316 } 317 318 function menuApply(ctx, action, value) { 319 ctx.execCommand(action, value); 320 321 ctx._range = ctx.getRange(); 322 323 ctx.highlight().menu(); 324 } 325 326 function onToolbarClick(ctx, target) { 327 var toolbar = ctx._toolbar || ctx._menu, 328 action; 329 330 while (!(action = target.getAttribute('data-action'))) { 331 if (target.parentNode === toolbar) { 332 break; 333 } 334 335 target = target.parentNode; 336 } 337 338 var groupToggle = target.getAttribute('data-group-toggle'); 339 340 if (groupToggle) { 341 activateGroup(ctx, groupToggle); 342 } 343 344 if (!action) return; 345 346 if ('close' === action) { 347 showMainMenu(ctx); 348 349 return; 350 } 351 352 if (!/(?:createlink)|(?:insertimage)/.test(action)) return menuApply(ctx, action); 353 354 if (!ctx._urlInput) return; 355 356 // create link 357 var input = ctx._urlInput; 358 if (toolbar === ctx._menu) showLinkInput(ctx); 359 else { 360 ctx._inputActive = true; 361 ctx.menu(); 362 } 363 if (ctx._menu.style.display === 'none') return; 364 365 setTimeout(function() { input.focus(); }, 10); 366 367 var createLink = function() { 368 var inputValue = input.value; 369 370 if (inputValue) { 371 ctx.config.linksInNewWindow = ctx._externalUrlCheckbox.checked; 372 373 inputValue = input.value 374 .replace(strReg.whiteSpace, '') 375 .replace(strReg.mailTo, 'mailto:$1') 376 .replace(strReg.http, 'http://$1'); 377 } else { 378 action = 'unlink'; 379 } 380 381 menuApply(ctx, action, inputValue); 382 }; 383 384 input.onkeypress = function(e) { 385 if (e.which === 13) { 386 e.preventDefault(); 387 388 createLink() 389 } 390 }; 391 392 ctx._externalUrlCheckbox.onchange = createLink; 393 } 394 395 function initToolbar(ctx) { 396 var icons = '', inputStr = createLinkInput(ctx).outerHTML; 397 398 ctx._toolbar = ctx.config.toolbar; 399 400 if (!ctx._toolbar) { 401 var toolList = ctx.config.list; 402 403 if (! Object.values(toolList).length) { 404 return; 405 } 406 407 utils.forEach(toolList, function (name, key) { 408 if (Array.isArray(name)) { 409 var children = name; 410 411 name = key; 412 413 icons += createTool(ctx, name, 'parent'); 414 415 utils.forEach(children, function(childName) { 416 icons += createTool(ctx, childName, 'child', name); 417 }, true); 418 } else { 419 icons += createTool(ctx, name); 420 } 421 }); 422 423 var toolListValues = Object.values(toolList); 424 425 if (toolListValues.indexOf('createlink') >= 0 || toolListValues.indexOf('insertimage') >= 0) 426 icons += inputStr; 427 428 icons += createTool(ctx, 'close'); 429 } else if (ctx._toolbar.querySelectorAll('[data-action=createlink]').length || 430 ctx._toolbar.querySelectorAll('[data-action=insertimage]').length) { 431 icons += inputStr; 432 } 433 434 if (icons) { 435 ctx._menu = doc.createElement('div'); 436 ctx._menu.setAttribute('class', ctx.config.class + '-menu pen-menu'); 437 ctx._menu.innerHTML = icons; 438 ctx._urlInput = ctx._menu.querySelector('.pen-url-input'); 439 ctx._externalUrlCheckbox = ctx._menu.querySelector('.pen-external-url-checkbox'); 440 toggleNode(ctx._menu, true); 441 doc.body.appendChild(ctx._menu); 442 } 443 } 444 445 function initEvents(ctx) { 446 var toolbar = ctx._toolbar || ctx._menu, editor = ctx.config.editor; 447 448 var toggleMenu = utils.delayExec(function() { 449 if (toolbar) { 450 ctx.highlight().menu(); 451 } 452 }); 453 454 var outsideClick = function() {}; 455 456 function updateStatus(delay) { 457 ctx._range = ctx.getRange(); 458 toggleMenu(delay); 459 } 460 461 if (ctx._menu) { 462 var setpos = function() { 463 if (ctx._menu.style.display === 'flex') ctx.menu(); 464 }; 465 466 // change menu offset when window resize / scroll 467 addListener(ctx, root, 'resize', setpos); 468 addListener(ctx, root, 'scroll', setpos); 469 470 // toggle toolbar on mouse select 471 var selecting = false; 472 addListener(ctx, editor, 'mousedown', function() { 473 selecting = true; 474 }); 475 476 addListener(ctx, editor, 'mouseleave', function() { 477 if (selecting) updateStatus(800); 478 selecting = false; 479 }); 480 481 addListener(ctx, editor, 'mouseup', function() { 482 if (selecting) updateStatus(200); 483 selecting = false; 484 }); 485 486 // Hide menu when focusing outside of editor 487 outsideClick = function(e) { 488 if (ctx._menu && !containsNode(editor, e.target) && !containsNode(ctx._menu, e.target)) { 489 removeListener(ctx, doc, 'click', outsideClick); 490 toggleMenu(100); 491 } 492 }; 493 } else { 494 addListener(ctx, editor, 'click', function() { 495 updateStatus(0); 496 }); 497 } 498 499 addListener(ctx, editor, 'keyup', function(e) { 500 checkPlaceholder(ctx); 501 502 if (ctx.isEmpty()) { 503 if (ctx.config.mode === 'advanced') { 504 handleEmptyContent(ctx); 505 } 506 507 return; 508 } 509 510 if (isCaretAtEnd(ctx) && !isCaretAtStart(ctx) && ctx.config.mode !== 'advanced') { 511 editor.innerHTML = editor.innerHTML.replace( /\u200b/, '' ); 512 513 addEmptyCharAtEnd(ctx); 514 } 515 516 // toggle toolbar on key select 517 if (e.which !== 13 || e.shiftKey) return updateStatus(400); 518 var node = getNode(ctx, true); 519 if (!node || !node.nextSibling || !lineBreakReg.test(node.nodeName)) return; 520 if (node.nodeName !== node.nextSibling.nodeName) return; 521 // hack for webkit, make 'enter' behavior like as firefox. 522 if (node.lastChild.nodeName !== 'BR') node.appendChild(doc.createElement('br')); 523 utils.forEach(node.nextSibling.childNodes, function(child) { 524 if (child) node.appendChild(child); 525 }, true); 526 node.parentNode.removeChild(node.nextSibling); 527 focusNode(ctx, node.lastChild, ctx.getRange()); 528 }); 529 530 // check line break 531 addListener(ctx, editor, 'keydown', function(e) { 532 editor.classList.remove(ctx.config.placeholderClass); 533 534 if (e.which !== 13 || e.shiftKey) return; 535 536 if ( ctx.config.ignoreLineBreak ) { 537 e.preventDefault(); 538 539 return; 540 } 541 542 var node = getNode(ctx, true); 543 544 if(!node || !lineBreakReg.test(node.nodeName)) { 545 if (ctx.config.mode === 'basic') { 546 e.preventDefault(); 547 548 commandOverall('insertHTML', '<br>'); 549 } 550 551 return; 552 } 553 554 if (!node) { 555 return; 556 } 557 558 var lastChild = node.lastChild; 559 if (!lastChild || !lastChild.previousSibling) return; 560 if (lastChild.previousSibling.textContent || lastChild.textContent) return; 561 // quit block mode for 2 'enter' 562 e.preventDefault(); 563 var p = doc.createElement('p'); 564 p.innerHTML = '<br>'; 565 node.removeChild(lastChild); 566 if (!node.nextSibling) node.parentNode.appendChild(p); 567 else node.parentNode.insertBefore(p, node.nextSibling); 568 focusNode(ctx, p, ctx.getRange()); 569 }); 570 571 if (toolbar) { 572 addListener(ctx, toolbar, 'click', function(e) { 573 onToolbarClick(ctx, e.target); 574 }); 575 } 576 577 addListener(ctx, editor, 'focus', function() { 578 if (ctx.isEmpty() && ctx.config.mode === 'advanced') handleEmptyContent(ctx); 579 addListener(ctx, doc, 'click', outsideClick); 580 }); 581 582 addListener(ctx, editor, 'blur', function() { 583 checkPlaceholder(ctx); 584 ctx.checkContentChange(); 585 }); 586 587 // listen for paste and clear style 588 addListener(ctx, editor, 'paste', function() { 589 setTimeout(function() { 590 ctx.cleanContent(); 591 }); 592 }); 593 } 594 595 function addListener(ctx, target, type, listener) { 596 if (ctx._events.hasOwnProperty(type)) { 597 ctx._events[type].push(listener); 598 } else { 599 ctx._eventTargets = ctx._eventTargets || []; 600 ctx._eventsCache = ctx._eventsCache || []; 601 var index = ctx._eventTargets.indexOf(target); 602 if (index < 0) index = ctx._eventTargets.push(target) - 1; 603 ctx._eventsCache[index] = ctx._eventsCache[index] || {}; 604 ctx._eventsCache[index][type] = ctx._eventsCache[index][type] || []; 605 ctx._eventsCache[index][type].push(listener); 606 607 target.addEventListener(type, listener, false); 608 } 609 return ctx; 610 } 611 612 // trigger local events 613 function triggerListener(ctx, type) { 614 if (!ctx._events.hasOwnProperty(type)) return; 615 var args = slice.call(arguments, 2); 616 utils.forEach(ctx._events[type], function (listener) { 617 listener.apply(ctx, args); 618 }); 619 } 620 621 function removeListener(ctx, target, type, listener) { 622 var events = ctx._events[type]; 623 if (!events) { 624 var _index = ctx._eventTargets.indexOf(target); 625 if (_index >= 0) events = ctx._eventsCache[_index][type]; 626 } 627 if (!events) return ctx; 628 var index = events.indexOf(listener); 629 if (index >= 0) events.splice(index, 1); 630 target.removeEventListener(type, listener, false); 631 return ctx; 632 } 633 634 function removeAllListeners(ctx) { 635 utils.forEach(this._events, function (events) { 636 events.length = 0; 637 }, false); 638 if (!ctx._eventsCache) return ctx; 639 utils.forEach(ctx._eventsCache, function (events, index) { 640 var target = ctx._eventTargets[index]; 641 utils.forEach(events, function (listeners, type) { 642 utils.forEach(listeners, function (listener) { 643 target.removeEventListener(type, listener, false); 644 }, true); 645 }, false); 646 }, true); 647 ctx._eventTargets = []; 648 ctx._eventsCache = []; 649 return ctx; 650 } 651 652 function checkPlaceholder(ctx) { 653 ctx.config.editor.classList[ctx.isEmpty() ? 'add' : 'remove'](ctx.config.placeholderClass); 654 } 655 656 function trim(str) { 657 return (str || '').trim().replace(/\u200b/g, ''); 658 } 659 660 // node.contains is not implemented in IE10/IE11 661 function containsNode(parent, child) { 662 if (parent === child) return true; 663 child = child.parentNode; 664 while (child) { 665 if (child === parent) return true; 666 child = child.parentNode; 667 } 668 return false; 669 } 670 671 function getNode(ctx, byRoot) { 672 var node, 673 root = ctx.config.editor; 674 675 ctx._range = ctx._range || ctx.getRange(); 676 677 node = ctx._range.commonAncestorContainer; 678 679 // Fix selection detection for Firefox 680 if (node.hasChildNodes() && ctx._range.startOffset + 1 === ctx._range.endOffset) { 681 node = node.childNodes[ctx._range.startOffset]; 682 } 683 684 if (!node || node === root) return null; 685 686 while (node && (node.nodeType !== 1) && (node.parentNode !== root)) node = node.parentNode; 687 688 while (node && byRoot && (node.parentNode !== root)) node = node.parentNode; 689 690 return containsNode(root, node) ? node : null; 691 } 692 693 function getEffectNodes(ctx) { 694 return getNodeParents(ctx).filter(function(node) { 695 return node.nodeName.match(effectNodeReg); 696 }); 697 } 698 699 function getNodeParents(ctx) { 700 var nodes = [], 701 el = getNode(ctx); 702 703 while (el && el !== ctx.config.editor) { 704 if (el.nodeType === Node.ELEMENT_NODE) { 705 nodes.push(el); 706 } 707 708 el = el.parentNode; 709 } 710 711 return nodes; 712 } 713 714 function handleEmptyContent(ctx) { 715 var range = ctx._range = ctx.getRange(); 716 717 ctx.config.editor.innerHTML = ''; 718 719 var p = doc.createElement('p'); 720 721 p.innerHTML = '<br>'; 722 723 range.insertNode(p); 724 725 focusNode(ctx, p.childNodes[0], range); 726 } 727 728 function addEmptyCharAtEnd(ctx) { 729 var range = ctx.getRange(), 730 emptyCharNode = doc.createTextNode('\u200b'); 731 732 range.selectNodeContents(ctx.config.editor); 733 range.collapse(false); 734 range.insertNode(emptyCharNode); 735 736 focusNode(ctx, emptyCharNode, range); 737 } 738 739 function isCaretAtEnd(ctx) { 740 var range = ctx.getRange(), 741 clonedRange = range.cloneRange(); 742 743 clonedRange.selectNodeContents(ctx.config.editor); 744 clonedRange.setStart(range.endContainer, range.endOffset); 745 746 return clonedRange.toString() === ''; 747 } 748 749 function isCaretAtStart(ctx) { 750 var range = ctx.getRange(), 751 clonedRange = range.cloneRange(); 752 753 clonedRange.selectNodeContents(ctx.config.editor); 754 clonedRange.setEnd(range.startContainer, range.startOffset); 755 756 return clonedRange.toString() === ''; 757 } 758 759 function focusNode(ctx, node, range) { 760 range.setStartAfter(node); 761 range.setEndBefore(node); 762 range.collapse(false); 763 ctx.setRange(range); 764 } 765 766 function autoLink(node) { 767 if (node.nodeType === 1) { 768 if (autoLinkReg.notLink.test(node.tagName)) return; 769 utils.forEach(node.childNodes, function (child) { 770 autoLink(child); 771 }, true); 772 } else if (node.nodeType === 3) { 773 var result = urlToLink(node.nodeValue || ''); 774 if (!result.links) return; 775 var frag = doc.createDocumentFragment(), 776 div = doc.createElement('div'); 777 div.innerHTML = result.text; 778 while (div.childNodes.length) frag.appendChild(div.childNodes[0]); 779 node.parentNode.replaceChild(frag, node); 780 } 781 } 782 783 function urlToLink(str) { 784 var count = 0; 785 str = str.replace(autoLinkReg.url, function(url) { 786 var realUrl = url, displayUrl = url; 787 count++; 788 if (url.length > autoLinkReg.maxLength) displayUrl = url.slice(0, autoLinkReg.maxLength) + '...'; 789 // Add http prefix if necessary 790 if (!autoLinkReg.prefix.test(realUrl)) realUrl = 'http://' + realUrl; 791 return '<a href="' + realUrl + '">' + displayUrl + '</a>'; 792 }); 793 return {links: count, text: str}; 794 } 795 796 function toggleNode(node, hide) { 797 node.style.display = hide ? 'none' : 'flex'; 798 } 799 800 InlineEditor = function(config) { 801 802 if (!config) throw new Error('Can\'t find config'); 803 804 debugMode = config.debug; 805 806 // merge user config 807 var defaults = utils.merge(config); 808 809 var editor = defaults.editor; 810 811 if (!editor || editor.nodeType !== 1) throw new Error('Can\'t find editor'); 812 813 // set default class 814 editor.classList.add.apply(editor.classList, defaults.class.split(' ')); 815 816 // set contenteditable 817 editor.setAttribute('contenteditable', 'true'); 818 819 // assign config 820 this.config = defaults; 821 822 // set placeholder 823 if (defaults.placeholder) editor.setAttribute(this.config.placeholderAttr, defaults.placeholder); 824 checkPlaceholder(this); 825 826 // save the selection obj 827 this.selection = selection; 828 829 // define local events 830 this._events = {change: []}; 831 832 // enable toolbar 833 initToolbar(this); 834 835 // init events 836 initEvents(this); 837 838 // to check content change 839 this._prevContent = this.getContent(); 840 841 // enable markdown covert 842 if (this.markdown) this.markdown.init(this); 843 844 // stay on the page 845 if (this.config.stay) this.stay(this.config); 846 847 if(this.config.input) { 848 this.addOnSubmitListener(this.config.input); 849 } 850 851 if (this.config.mode === 'advanced') { 852 this.getRange().selectNodeContents(editor); 853 854 this.setRange(); 855 } else { 856 addEmptyCharAtEnd(this); 857 } 858 }; 859 860 InlineEditor.prototype.on = function(type, listener) { 861 addListener(this, this.config.editor, type, listener); 862 return this; 863 }; 864 865 InlineEditor.prototype.addOnSubmitListener = function(inputElement) { 866 var form = inputElement.form; 867 var me = this; 868 form.addEventListener("submit", function() { 869 inputElement.value = me.config.saveAsMarkdown ? me.toMd(me.config.editor.innerHTML) : me.config.editor.innerHTML; 870 }); 871 }; 872 873 InlineEditor.prototype.isEmpty = function(node) { 874 node = node || this.config.editor; 875 return !(node.querySelector('img')) && !(node.querySelector('blockquote')) && 876 !(node.querySelector('li')) && !trim(node.textContent); 877 }; 878 879 InlineEditor.prototype.getContent = function() { 880 return this.isEmpty() ? '' : trim(this.config.editor.innerHTML); 881 }; 882 883 InlineEditor.prototype.setContent = function(html) { 884 this.config.editor.innerHTML = html; 885 this.cleanContent(); 886 return this; 887 }; 888 889 InlineEditor.prototype.checkContentChange = function () { 890 var prevContent = this._prevContent, currentContent = this.getContent(); 891 if (prevContent === currentContent) return; 892 this._prevContent = currentContent; 893 triggerListener(this, 'change', currentContent, prevContent); 894 }; 895 896 InlineEditor.prototype.getRange = function() { 897 var editor = this.config.editor, range = selection.rangeCount && selection.getRangeAt(0); 898 if (!range) range = doc.createRange(); 899 if (!containsNode(editor, range.commonAncestorContainer)) { 900 range.selectNodeContents(editor); 901 range.collapse(false); 902 } 903 return range; 904 }; 905 906 InlineEditor.prototype.setRange = function(range) { 907 range = range || this._range; 908 909 if (!range) { 910 range = this.getRange(); 911 range.collapse(false); // set to end 912 } 913 try { 914 selection.removeAllRanges(); 915 selection.addRange(range); 916 } catch (e) {/* IE throws error sometimes*/} 917 return this; 918 }; 919 920 InlineEditor.prototype.focus = function(focusStart) { 921 if (!focusStart) this.setRange(); 922 this.config.editor.focus(); 923 return this; 924 }; 925 926 InlineEditor.prototype.execCommand = function(name, value) { 927 name = name.toLowerCase(); 928 this.setRange(); 929 930 if (commandsReg.block.test(name)) { 931 commandBlock(this, name); 932 } else if (commandsReg.inline.test(name)) { 933 commandOverall(name, value); 934 } else if (commandsReg.biu.test(name)) { 935 // Temporarily removing all override style rules 936 // to make sure the command will be executed correctly 937 var styleBackup = styleBackupDict[ name ]; 938 939 styleBackup.backupValue = this.config.editor.style[ styleBackup.styleKey ]; 940 941 this.config.editor.style[ styleBackup.styleKey ] = styleBackup.correctValue; 942 943 commandOverall(name, value); 944 945 this.config.editor.style[ styleBackup.styleKey ] = styleBackup.backupValue; 946 } else if (commandsReg.source.test(name)) { 947 commandLink(this, name, value); 948 } else if (commandsReg.insert.test(name)) { 949 commandInsert(this, name, value); 950 } else if (commandsReg.wrap.test(name)) { 951 commandWrap(this, name, value); 952 } else { 953 utils.log('can not find command function for name: ' + name + (value ? (', value: ' + value) : ''), true); 954 } 955 956 if (name === 'indent') this.checkContentChange(); 957 }; 958 959 // remove attrs and tags 960 // pen.cleanContent({cleanAttrs: ['style'], cleanTags: ['id']}) 961 InlineEditor.prototype.cleanContent = function(options) { 962 var editor = this.config.editor; 963 964 if (!options) options = this.config; 965 utils.forEach(options.cleanAttrs, function (attr) { 966 utils.forEach(editor.querySelectorAll('[' + attr + ']'), function(item) { 967 item.removeAttribute(attr); 968 }, true); 969 }, true); 970 utils.forEach(options.cleanTags, function (tag) { 971 utils.forEach(editor.querySelectorAll(tag), function(item) { 972 item.parentNode.removeChild(item); 973 }, true); 974 }, true); 975 976 checkPlaceholder(this); 977 this.checkContentChange(); 978 return this; 979 }; 980 981 // auto link content, return content 982 InlineEditor.prototype.autoLink = function() { 983 autoLink(this.config.editor); 984 return this.getContent(); 985 }; 986 987 // highlight menu 988 InlineEditor.prototype.highlight = function() { 989 var toolbar = this._toolbar || this._menu, 990 node = getNode(this); 991 992 // remove all highlights 993 utils.forEach(toolbar.querySelectorAll('.active'), function(el) { 994 el.classList.remove('active'); 995 }, true); 996 997 if (!node) return this; 998 999 var nodeParents = getNodeParents(this), 1000 urlInput = this._urlInput, 1001 externalUrlCheckbox = this._externalUrlCheckbox, 1002 highlight; 1003 1004 if (urlInput && toolbar === this._menu) { 1005 // reset url inputs 1006 urlInput.value = ''; 1007 1008 this._externalUrlCheckbox.checked = false; 1009 } 1010 1011 highlight = function(str) { 1012 if (!str) return; 1013 var el = toolbar.querySelector('[data-action=' + str + ']'); 1014 return el && el.classList.add('active'); 1015 }; 1016 1017 utils.forEach(nodeParents, function(item) { 1018 var tag = item.nodeName.toLowerCase(), 1019 align = item.style.textAlign, 1020 textDecoration = item.style.textDecoration; 1021 1022 if (align) { 1023 if ('justify' === align) { 1024 align = 'full'; 1025 } 1026 1027 highlight('justify' + align[0].toUpperCase() + align.slice(1)); 1028 } 1029 1030 if ('underline' === textDecoration) { 1031 highlight('underline'); 1032 } 1033 1034 if (! tag.match(effectNodeReg)) { 1035 return; 1036 } 1037 1038 switch(tag) { 1039 case 'a': 1040 urlInput.value = item.getAttribute('href'); 1041 1042 externalUrlCheckbox.checked = item.getAttribute('target') === '_blank'; 1043 1044 tag = 'createlink'; 1045 1046 break; 1047 case 'img': 1048 urlInput.value = item.getAttribute('src'); 1049 1050 tag = 'insertimage'; 1051 1052 break; 1053 case 'i': 1054 case 'em': 1055 tag = 'italic'; 1056 1057 break; 1058 case 'u': 1059 tag = 'underline'; 1060 1061 break; 1062 case 'b': 1063 case 'strong': 1064 tag = 'bold'; 1065 1066 break; 1067 case 'strike': 1068 tag = 'strikethrough'; 1069 1070 break; 1071 case 'ul': 1072 tag = 'insertUnorderedList'; 1073 break; 1074 1075 case 'ol': 1076 tag = 'insertOrderedList'; 1077 1078 break; 1079 case 'li': 1080 tag = 'indent'; 1081 1082 break; 1083 } 1084 1085 highlight(tag); 1086 }, true); 1087 1088 return this; 1089 }; 1090 1091 // show menu 1092 InlineEditor.prototype.menu = function() { 1093 if (!this._menu) return this; 1094 1095 if (selection.isCollapsed) { 1096 this._menu.style.display = 'none'; //hide menu 1097 this._inputActive = false; 1098 return this; 1099 } 1100 1101 if (this._toolbar) { 1102 if (!this._urlInput || !this._inputActive) return this; 1103 } 1104 1105 showMainMenu(this); 1106 }; 1107 1108 InlineEditor.prototype.refreshMenuPosition = function() { 1109 var offset = this._range.getBoundingClientRect() 1110 , menuPadding = 10 1111 , top = offset.top - menuPadding 1112 , left = offset.left + (offset.width / 2) 1113 , menu = this._menu 1114 , menuOffset = {x: 0, y: 0} 1115 , stylesheet = this._stylesheet; 1116 1117 // fixes some browser double click visual discontinuity 1118 // if the offset has no width or height it should not be used 1119 if (offset.width === 0 && offset.height === 0) return this; 1120 1121 // store the stylesheet used for positioning the menu horizontally 1122 if (this._stylesheet === undefined) { 1123 var style = document.createElement("style"); 1124 document.head.appendChild(style); 1125 this._stylesheet = stylesheet = style.sheet; 1126 } 1127 // display it to caculate its width & height 1128 menu.style.display = 'flex'; 1129 1130 menuOffset.x = left - (menu.clientWidth / 2); 1131 menuOffset.y = top - menu.clientHeight; 1132 1133 // check to see if menu has over-extended its bounding box. if it has, 1134 // 1) apply a new class if overflowed on top; 1135 // 2) apply a new rule if overflowed on the left 1136 if (stylesheet.cssRules.length > 0) { 1137 stylesheet.deleteRule(0); 1138 } 1139 if (menuOffset.x < 0) { 1140 menuOffset.x = 0; 1141 stylesheet.insertRule('.pen-menu:after {left: ' + left + 'px;}', 0); 1142 } else { 1143 stylesheet.insertRule('.pen-menu:after {left: 50%; }', 0); 1144 } 1145 if (menuOffset.y < 0) { 1146 menu.classList.add('pen-menu-below'); 1147 menuOffset.y = offset.top + offset.height + menuPadding; 1148 } else { 1149 menu.classList.remove('pen-menu-below'); 1150 } 1151 1152 menu.style.top = menuOffset.y + 'px'; 1153 menu.style.left = menuOffset.x + 'px'; 1154 1155 return this; 1156 }; 1157 1158 InlineEditor.prototype.stay = function(config) { 1159 var ctx = this; 1160 if (!window.onbeforeunload) { 1161 window.onbeforeunload = function() { 1162 if (!ctx._isDestroyed) return config.stayMsg; 1163 }; 1164 } 1165 }; 1166 1167 InlineEditor.prototype.destroy = function() { 1168 var config = this.config; 1169 1170 removeAllListeners(this); 1171 1172 config.editor.classList.remove.apply(config.editor.classList, config.class.split(' ').concat(config.placeholderClass)); 1173 1174 config.editor.removeAttribute('contenteditable'); 1175 1176 config.editor.removeAttribute(config.placeholderAttr); 1177 1178 try { 1179 selection.removeAllRanges(); 1180 if (this._menu) this._menu.parentNode.removeChild(this._menu); 1181 } catch (e) {/* IE throws error sometimes*/} 1182 1183 this._isDestroyed = true; 1184 1185 return this; 1186 }; 1187 1188 InlineEditor.prototype.rebuild = function() { 1189 initToolbar(this); 1190 1191 initEvents(this); 1192 1193 return this; 1194 }; 1195 1196 // a fallback for old browers 1197 root.ElementorInlineEditor = function(config) { 1198 if (!config) return utils.log('can\'t find config', true); 1199 1200 var defaults = utils.merge(config) 1201 , klass = defaults.editor.getAttribute('class'); 1202 1203 klass = klass ? klass.replace(/\bpen\b/g, '') + ' pen-textarea ' + defaults.class : 'pen pen-textarea'; 1204 defaults.editor.setAttribute('class', klass); 1205 defaults.editor.innerHTML = defaults.textarea; 1206 return defaults.editor; 1207 }; 1208 1209 // export content as markdown 1210 var regs = { 1211 a: [/<a\b[^>]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'], 1212 img: [/<img\b[^>]*src=["']([^\"+|[^']+)[^>]*>/ig, ''], 1213 b: [/<b\b[^>]*>(.*?)<\/b>/ig, '**$1**'], 1214 i: [/<i\b[^>]*>(.*?)<\/i>/ig, '***$1***'], 1215 h: [/<h([1-6])\b[^>]*>(.*?)<\/h\1>/ig, function(a, b, c) { 1216 return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n'; 1217 }], 1218 li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'], 1219 blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'], 1220 pre: [/<pre\b[^>]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'], 1221 code: [/<code\b[^>]*>(.*?)<\/code>/ig, '\n`\n$1\n`\n'], 1222 p: [/<p\b[^>]*>(.*?)<\/p>/ig, '\n$1\n'], 1223 hr: [/<hr\b[^>]*>/ig, '\n---\n'] 1224 }; 1225 1226 InlineEditor.prototype.toMd = function() { 1227 var html = this.getContent() 1228 .replace(/\n+/g, '') // remove line break 1229 .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol 1230 1231 for(var p in regs) { 1232 if (regs.hasOwnProperty(p)) 1233 html = html.replace.apply(html, regs[p]); 1234 } 1235 return html.replace(/\*{5}/g, '**'); 1236 }; 1237 1238 // make it accessible 1239 if (doc.getSelection) { 1240 selection = doc.getSelection(); 1241 root.ElementorInlineEditor = InlineEditor; 1242 } 1243 1244 }(window, document));