PluginProbe
Elementor Website Builder – more than just a page builder / 3.7.2
Elementor Website Builder – more than just a page builder v3.7.2
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / assets / lib / inline-editor / js / inline-editor.js

inline-editor.js in Elementor Website Builder – more than just a page builder 3.7.2, at assets/lib/inline-editor/js/inline-editor.js

1,245 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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, '![]($1)'],
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));
1245