comparison src/development/sceditor.js @ 0:4c4fc447baea

start with sceditor-3.1.1
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 04 Aug 2022 15:21:29 -0600
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4c4fc447baea
1 (function () {
2 'use strict';
3
4 /**
5 * Check if the passed argument is the
6 * the passed type.
7 *
8 * @param {string} type
9 * @param {*} arg
10 * @returns {boolean}
11 */
12 function isTypeof(type, arg) {
13 return typeof arg === type;
14 }
15
16 /**
17 * @type {function(*): boolean}
18 */
19 var isString = isTypeof.bind(null, 'string');
20
21 /**
22 * @type {function(*): boolean}
23 */
24 var isUndefined = isTypeof.bind(null, 'undefined');
25
26 /**
27 * @type {function(*): boolean}
28 */
29 var isFunction = isTypeof.bind(null, 'function');
30
31 /**
32 * @type {function(*): boolean}
33 */
34 var isNumber = isTypeof.bind(null, 'number');
35
36
37 /**
38 * Returns true if an object has no keys
39 *
40 * @param {!Object} obj
41 * @returns {boolean}
42 */
43 function isEmptyObject(obj) {
44 return !Object.keys(obj).length;
45 }
46
47 /**
48 * Extends the first object with any extra objects passed
49 *
50 * If the first argument is boolean and set to true
51 * it will extend child arrays and objects recursively.
52 *
53 * @param {!Object|boolean} targetArg
54 * @param {...Object} source
55 * @return {Object}
56 */
57 function extend(targetArg, sourceArg) {
58 var isTargetBoolean = targetArg === !!targetArg;
59 var i = isTargetBoolean ? 2 : 1;
60 var target = isTargetBoolean ? sourceArg : targetArg;
61 var isDeep = isTargetBoolean ? targetArg : false;
62
63 function isObject(value) {
64 return value !== null && typeof value === 'object' &&
65 Object.getPrototypeOf(value) === Object.prototype;
66 }
67
68 for (; i < arguments.length; i++) {
69 var source = arguments[i];
70
71 // Copy all properties for jQuery compatibility
72 /* eslint guard-for-in: off */
73 for (var key in source) {
74 var targetValue = target[key];
75 var value = source[key];
76
77 // Skip undefined values to match jQuery
78 if (isUndefined(value)) {
79 continue;
80 }
81
82 // Skip special keys to prevent prototype pollution
83 if (key === '__proto__' || key === 'constructor') {
84 continue;
85 }
86
87 var isValueObject = isObject(value);
88 var isValueArray = Array.isArray(value);
89
90 if (isDeep && (isValueObject || isValueArray)) {
91 // Can only merge if target type matches otherwise create
92 // new target to merge into
93 var isSameType = isObject(targetValue) === isValueObject &&
94 Array.isArray(targetValue) === isValueArray;
95
96 target[key] = extend(
97 true,
98 isSameType ? targetValue : (isValueArray ? [] : {}),
99 value
100 );
101 } else {
102 target[key] = value;
103 }
104 }
105 }
106
107 return target;
108 }
109
110 /**
111 * Removes an item from the passed array
112 *
113 * @param {!Array} arr
114 * @param {*} item
115 */
116 function arrayRemove(arr, item) {
117 var i = arr.indexOf(item);
118
119 if (i > -1) {
120 arr.splice(i, 1);
121 }
122 }
123
124 /**
125 * Iterates over an array or object
126 *
127 * @param {!Object|Array} obj
128 * @param {function(*, *)} fn
129 */
130 function each(obj, fn) {
131 if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) {
132 for (var i = 0; i < obj.length; i++) {
133 fn(i, obj[i]);
134 }
135 } else {
136 Object.keys(obj).forEach(function (key) {
137 fn(key, obj[key]);
138 });
139 }
140 }
141
142 /**
143 * Cache of camelCase CSS property names
144 * @type {Object<string, string>}
145 */
146 var cssPropertyNameCache = {};
147
148 /**
149 * Node type constant for element nodes
150 *
151 * @type {number}
152 */
153 var ELEMENT_NODE = 1;
154
155 /**
156 * Node type constant for text nodes
157 *
158 * @type {number}
159 */
160 var TEXT_NODE = 3;
161
162 /**
163 * Node type constant for comment nodes
164 *
165 * @type {number}
166 */
167 var COMMENT_NODE = 8;
168
169 function toFloat(value) {
170 value = parseFloat(value);
171
172 return isFinite(value) ? value : 0;
173 }
174
175 /**
176 * Creates an element with the specified attributes
177 *
178 * Will create it in the current document unless context
179 * is specified.
180 *
181 * @param {!string} tag
182 * @param {!Object<string, string>} [attributes]
183 * @param {!Document} [context]
184 * @returns {!HTMLElement}
185 */
186 function createElement(tag, attributes, context) {
187 var node = (context || document).createElement(tag);
188
189 each(attributes || {}, function (key, value) {
190 if (key === 'style') {
191 node.style.cssText = value;
192 } else if (key in node) {
193 node[key] = value;
194 } else {
195 node.setAttribute(key, value);
196 }
197 });
198
199 return node;
200 }
201
202 /**
203 * Gets the first parent node that matches the selector
204 *
205 * @param {!HTMLElement} node
206 * @param {!string} [selector]
207 * @returns {HTMLElement|undefined}
208 */
209 function parent(node, selector) {
210 var parent = node || {};
211
212 while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) {
213 if (!selector || is(parent, selector)) {
214 return parent;
215 }
216 }
217 }
218
219 /**
220 * Checks the passed node and all parents and
221 * returns the first matching node if any.
222 *
223 * @param {!HTMLElement} node
224 * @param {!string} selector
225 * @returns {HTMLElement|undefined}
226 */
227 function closest(node, selector) {
228 return is(node, selector) ? node : parent(node, selector);
229 }
230
231 /**
232 * Removes the node from the DOM
233 *
234 * @param {!HTMLElement} node
235 */
236 function remove(node) {
237 if (node.parentNode) {
238 node.parentNode.removeChild(node);
239 }
240 }
241
242 /**
243 * Appends child to parent node
244 *
245 * @param {!HTMLElement} node
246 * @param {!HTMLElement} child
247 */
248 function appendChild(node, child) {
249 node.appendChild(child);
250 }
251
252 /**
253 * Finds any child nodes that match the selector
254 *
255 * @param {!HTMLElement} node
256 * @param {!string} selector
257 * @returns {NodeList}
258 */
259 function find(node, selector) {
260 return node.querySelectorAll(selector);
261 }
262
263 /**
264 * For on() and off() if to add/remove the event
265 * to the capture phase
266 *
267 * @type {boolean}
268 */
269 var EVENT_CAPTURE = true;
270
271 /**
272 * Adds an event listener for the specified events.
273 *
274 * Events should be a space separated list of events.
275 *
276 * If selector is specified the handler will only be
277 * called when the event target matches the selector.
278 *
279 * @param {!Node} node
280 * @param {string} events
281 * @param {string} [selector]
282 * @param {function(Object)} fn
283 * @param {boolean} [capture=false]
284 * @see off()
285 */
286 // eslint-disable-next-line max-params
287 function on(node, events, selector, fn, capture) {
288 events.split(' ').forEach(function (event) {
289 var handler;
290
291 if (isString(selector)) {
292 handler = fn['_sce-event-' + event + selector] || function (e) {
293 var target = e.target;
294 while (target && target !== node) {
295 if (is(target, selector)) {
296 fn.call(target, e);
297 return;
298 }
299
300 target = target.parentNode;
301 }
302 };
303
304 fn['_sce-event-' + event + selector] = handler;
305 } else {
306 handler = selector;
307 capture = fn;
308 }
309
310 node.addEventListener(event, handler, capture || false);
311 });
312 }
313
314 /**
315 * Removes an event listener for the specified events.
316 *
317 * @param {!Node} node
318 * @param {string} events
319 * @param {string} [selector]
320 * @param {function(Object)} fn
321 * @param {boolean} [capture=false]
322 * @see on()
323 */
324 // eslint-disable-next-line max-params
325 function off(node, events, selector, fn, capture) {
326 events.split(' ').forEach(function (event) {
327 var handler;
328
329 if (isString(selector)) {
330 handler = fn['_sce-event-' + event + selector];
331 } else {
332 handler = selector;
333 capture = fn;
334 }
335
336 node.removeEventListener(event, handler, capture || false);
337 });
338 }
339
340 /**
341 * If only attr param is specified it will get
342 * the value of the attr param.
343 *
344 * If value is specified but null the attribute
345 * will be removed otherwise the attr value will
346 * be set to the passed value.
347 *
348 * @param {!HTMLElement} node
349 * @param {!string} attr
350 * @param {?string} [value]
351 */
352 function attr(node, attr, value) {
353 if (arguments.length < 3) {
354 return node.getAttribute(attr);
355 }
356
357 // eslint-disable-next-line eqeqeq, no-eq-null
358 if (value == null) {
359 removeAttr(node, attr);
360 } else {
361 node.setAttribute(attr, value);
362 }
363 }
364
365 /**
366 * Removes the specified attribute
367 *
368 * @param {!HTMLElement} node
369 * @param {!string} attr
370 */
371 function removeAttr(node, attr) {
372 node.removeAttribute(attr);
373 }
374
375 /**
376 * Sets the passed elements display to none
377 *
378 * @param {!HTMLElement} node
379 */
380 function hide(node) {
381 css(node, 'display', 'none');
382 }
383
384 /**
385 * Sets the passed elements display to default
386 *
387 * @param {!HTMLElement} node
388 */
389 function show(node) {
390 css(node, 'display', '');
391 }
392
393 /**
394 * Toggles an elements visibility
395 *
396 * @param {!HTMLElement} node
397 */
398 function toggle(node) {
399 if (isVisible(node)) {
400 hide(node);
401 } else {
402 show(node);
403 }
404 }
405
406 /**
407 * Gets a computed CSS values or sets an inline CSS value
408 *
409 * Rules should be in camelCase format and not
410 * hyphenated like CSS properties.
411 *
412 * @param {!HTMLElement} node
413 * @param {!Object|string} rule
414 * @param {string|number} [value]
415 * @return {string|number|undefined}
416 */
417 function css(node, rule, value) {
418 if (arguments.length < 3) {
419 if (isString(rule)) {
420 return node.nodeType === 1 ? getComputedStyle(node)[rule] : null;
421 }
422
423 each(rule, function (key, value) {
424 css(node, key, value);
425 });
426 } else {
427 // isNaN returns false for null, false and empty strings
428 // so need to check it's truthy or 0
429 var isNumeric = (value || value === 0) && !isNaN(value);
430 node.style[rule] = isNumeric ? value + 'px' : value;
431 }
432 }
433
434
435 /**
436 * Gets or sets the data attributes on a node
437 *
438 * Unlike the jQuery version this only stores data
439 * in the DOM attributes which means only strings
440 * can be stored.
441 *
442 * @param {Node} node
443 * @param {string} [key]
444 * @param {string} [value]
445 * @return {Object|undefined}
446 */
447 function data(node, key, value) {
448 var argsLength = arguments.length;
449 var data = {};
450
451 if (node.nodeType === ELEMENT_NODE) {
452 if (argsLength === 1) {
453 each(node.attributes, function (_, attr) {
454 if (/^data\-/i.test(attr.name)) {
455 data[attr.name.substr(5)] = attr.value;
456 }
457 });
458
459 return data;
460 }
461
462 if (argsLength === 2) {
463 return attr(node, 'data-' + key);
464 }
465
466 attr(node, 'data-' + key, String(value));
467 }
468 }
469
470 /**
471 * Checks if node matches the given selector.
472 *
473 * @param {?HTMLElement} node
474 * @param {string} selector
475 * @returns {boolean}
476 */
477 function is(node, selector) {
478 var result = false;
479
480 if (node && node.nodeType === ELEMENT_NODE) {
481 result = (node.matches || node.msMatchesSelector ||
482 node.webkitMatchesSelector).call(node, selector);
483 }
484
485 return result;
486 }
487
488
489 /**
490 * Returns true if node contains child otherwise false.
491 *
492 * This differs from the DOM contains() method in that
493 * if node and child are equal this will return false.
494 *
495 * @param {!Node} node
496 * @param {HTMLElement} child
497 * @returns {boolean}
498 */
499 function contains(node, child) {
500 return node !== child && node.contains && node.contains(child);
501 }
502
503 /**
504 * @param {Node} node
505 * @param {string} [selector]
506 * @returns {?HTMLElement}
507 */
508 function previousElementSibling(node, selector) {
509 var prev = node.previousElementSibling;
510
511 if (selector && prev) {
512 return is(prev, selector) ? prev : null;
513 }
514
515 return prev;
516 }
517
518 /**
519 * @param {!Node} node
520 * @param {!Node} refNode
521 * @returns {Node}
522 */
523 function insertBefore(node, refNode) {
524 return refNode.parentNode.insertBefore(node, refNode);
525 }
526
527 /**
528 * @param {?HTMLElement} node
529 * @returns {!Array.<string>}
530 */
531 function classes(node) {
532 return node.className.trim().split(/\s+/);
533 }
534
535 /**
536 * @param {?HTMLElement} node
537 * @param {string} className
538 * @returns {boolean}
539 */
540 function hasClass(node, className) {
541 return is(node, '.' + className);
542 }
543
544 /**
545 * @param {!HTMLElement} node
546 * @param {string} className
547 */
548 function addClass(node, className) {
549 var classList = classes(node);
550
551 if (classList.indexOf(className) < 0) {
552 classList.push(className);
553 }
554
555 node.className = classList.join(' ');
556 }
557
558 /**
559 * @param {!HTMLElement} node
560 * @param {string} className
561 */
562 function removeClass(node, className) {
563 var classList = classes(node);
564
565 arrayRemove(classList, className);
566
567 node.className = classList.join(' ');
568 }
569
570 /**
571 * Toggles a class on node.
572 *
573 * If state is specified and is truthy it will add
574 * the class.
575 *
576 * If state is specified and is falsey it will remove
577 * the class.
578 *
579 * @param {HTMLElement} node
580 * @param {string} className
581 * @param {boolean} [state]
582 */
583 function toggleClass(node, className, state) {
584 state = isUndefined(state) ? !hasClass(node, className) : state;
585
586 if (state) {
587 addClass(node, className);
588 } else {
589 removeClass(node, className);
590 }
591 }
592
593 /**
594 * Gets or sets the width of the passed node.
595 *
596 * @param {HTMLElement} node
597 * @param {number|string} [value]
598 * @returns {number|undefined}
599 */
600 function width(node, value) {
601 if (isUndefined(value)) {
602 var cs = getComputedStyle(node);
603 var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight);
604 var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth);
605
606 return node.offsetWidth - padding - border;
607 }
608
609 css(node, 'width', value);
610 }
611
612 /**
613 * Gets or sets the height of the passed node.
614 *
615 * @param {HTMLElement} node
616 * @param {number|string} [value]
617 * @returns {number|undefined}
618 */
619 function height(node, value) {
620 if (isUndefined(value)) {
621 var cs = getComputedStyle(node);
622 var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom);
623 var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth);
624
625 return node.offsetHeight - padding - border;
626 }
627
628 css(node, 'height', value);
629 }
630
631 /**
632 * Triggers a custom event with the specified name and
633 * sets the detail property to the data object passed.
634 *
635 * @param {HTMLElement} node
636 * @param {string} eventName
637 * @param {Object} [data]
638 */
639 function trigger(node, eventName, data) {
640 var event;
641
642 if (isFunction(window.CustomEvent)) {
643 event = new CustomEvent(eventName, {
644 bubbles: true,
645 cancelable: true,
646 detail: data
647 });
648 } else {
649 event = node.ownerDocument.createEvent('CustomEvent');
650 event.initCustomEvent(eventName, true, true, data);
651 }
652
653 node.dispatchEvent(event);
654 }
655
656 /**
657 * Returns if a node is visible.
658 *
659 * @param {HTMLElement}
660 * @returns {boolean}
661 */
662 function isVisible(node) {
663 return !!node.getClientRects().length;
664 }
665
666 /**
667 * Convert CSS property names into camel case
668 *
669 * @param {string} string
670 * @returns {string}
671 */
672 function camelCase(string) {
673 return string
674 .replace(/^-ms-/, 'ms-')
675 .replace(/-(\w)/g, function (match, char) {
676 return char.toUpperCase();
677 });
678 }
679
680
681 /**
682 * Loop all child nodes of the passed node
683 *
684 * The function should accept 1 parameter being the node.
685 * If the function returns false the loop will be exited.
686 *
687 * @param {HTMLElement} node
688 * @param {function} func Callback which is called with every
689 * child node as the first argument.
690 * @param {boolean} innermostFirst If the innermost node should be passed
691 * to the function before it's parents.
692 * @param {boolean} siblingsOnly If to only traverse the nodes siblings
693 * @param {boolean} [reverse=false] If to traverse the nodes in reverse
694 */
695 // eslint-disable-next-line max-params
696 function traverse(node, func, innermostFirst, siblingsOnly, reverse) {
697 node = reverse ? node.lastChild : node.firstChild;
698
699 while (node) {
700 var next = reverse ? node.previousSibling : node.nextSibling;
701
702 if (
703 (!innermostFirst && func(node) === false) ||
704 (!siblingsOnly && traverse(
705 node, func, innermostFirst, siblingsOnly, reverse
706 ) === false) ||
707 (innermostFirst && func(node) === false)
708 ) {
709 return false;
710 }
711
712 node = next;
713 }
714 }
715
716 /**
717 * Like traverse but loops in reverse
718 * @see traverse
719 */
720 function rTraverse(node, func, innermostFirst, siblingsOnly) {
721 traverse(node, func, innermostFirst, siblingsOnly, true);
722 }
723
724 /**
725 * Parses HTML into a document fragment
726 *
727 * @param {string} html
728 * @param {Document} [context]
729 * @since 1.4.4
730 * @return {DocumentFragment}
731 */
732 function parseHTML(html, context) {
733 context = context || document;
734
735 var ret = context.createDocumentFragment();
736 var tmp = createElement('div', {}, context);
737
738 tmp.innerHTML = html;
739
740 while (tmp.firstChild) {
741 appendChild(ret, tmp.firstChild);
742 }
743
744 return ret;
745 }
746
747 /**
748 * Checks if an element has any styling.
749 *
750 * It has styling if it is not a plain <div> or <p> or
751 * if it has a class, style attribute or data.
752 *
753 * @param {HTMLElement} elm
754 * @return {boolean}
755 * @since 1.4.4
756 */
757 function hasStyling(node) {
758 return node && (!is(node, 'p,div') || node.className ||
759 attr(node, 'style') || !isEmptyObject(data(node)));
760 }
761
762 /**
763 * Converts an element from one type to another.
764 *
765 * For example it can convert the element <b> to <strong>
766 *
767 * @param {HTMLElement} element
768 * @param {string} toTagName
769 * @return {HTMLElement}
770 * @since 1.4.4
771 */
772 function convertElement(element, toTagName) {
773 var newElement = createElement(toTagName, {}, element.ownerDocument);
774
775 each(element.attributes, function (_, attribute) {
776 // Some browsers parse invalid attributes names like
777 // 'size"2' which throw an exception when set, just
778 // ignore these.
779 try {
780 attr(newElement, attribute.name, attribute.value);
781 } catch (ex) {}
782 });
783
784 while (element.firstChild) {
785 appendChild(newElement, element.firstChild);
786 }
787
788 element.parentNode.replaceChild(newElement, element);
789
790 return newElement;
791 }
792
793 /**
794 * List of block level elements separated by bars (|)
795 *
796 * @type {string}
797 */
798 var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' +
799 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|' +
800 'details|section|article|aside|nav|main|header|hgroup|footer|fieldset|' +
801 'dl|dt|dd|figure|figcaption|';
802
803 /**
804 * List of elements that do not allow children separated by bars (|)
805 *
806 * @param {Node} node
807 * @return {boolean}
808 * @since 1.4.5
809 */
810 function canHaveChildren(node) {
811 // 1 = Element
812 // 9 = Document
813 // 11 = Document Fragment
814 if (!/11?|9/.test(node.nodeType)) {
815 return false;
816 }
817
818 // List of empty HTML tags separated by bar (|) character.
819 // Source: http://www.w3.org/TR/html4/index/elements.html
820 // Source: http://www.w3.org/TR/html5/syntax.html#void-elements
821 return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' +
822 '|isindex|link|meta|param|command|embed|keygen|source|track|' +
823 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0;
824 }
825
826 /**
827 * Checks if an element is inline
828 *
829 * @param {HTMLElement} elm
830 * @param {boolean} [includeCodeAsBlock=false]
831 * @return {boolean}
832 */
833 function isInline(elm, includeCodeAsBlock) {
834 var tagName,
835 nodeType = (elm || {}).nodeType || TEXT_NODE;
836
837 if (nodeType !== ELEMENT_NODE) {
838 return nodeType === TEXT_NODE;
839 }
840
841 tagName = elm.tagName.toLowerCase();
842
843 if (tagName === 'code') {
844 return !includeCodeAsBlock;
845 }
846
847 return blockLevelList.indexOf('|' + tagName + '|') < 0;
848 }
849
850 /**
851 * Copy the CSS from 1 node to another.
852 *
853 * Only copies CSS defined on the element e.g. style attr.
854 *
855 * @param {HTMLElement} from
856 * @param {HTMLElement} to
857 * @deprecated since v3.1.0
858 */
859 function copyCSS(from, to) {
860 if (to.style && from.style) {
861 to.style.cssText = from.style.cssText + to.style.cssText;
862 }
863 }
864
865 /**
866 * Checks if a DOM node is empty
867 *
868 * @param {Node} node
869 * @returns {boolean}
870 */
871 function isEmpty(node) {
872 if (node.lastChild && isEmpty(node.lastChild)) {
873 remove(node.lastChild);
874 }
875
876 return node.nodeType === 3 ? !node.nodeValue :
877 (canHaveChildren(node) && !node.childNodes.length);
878 }
879
880 /**
881 * Fixes block level elements inside in inline elements.
882 *
883 * Also fixes invalid list nesting by placing nested lists
884 * inside the previous li tag or wrapping them in an li tag.
885 *
886 * @param {HTMLElement} node
887 */
888 function fixNesting(node) {
889 traverse(node, function (node) {
890 var list = 'ul,ol',
891 isBlock = !isInline(node, true) && node.nodeType !== COMMENT_NODE,
892 parent = node.parentNode;
893
894 // Any blocklevel element inside an inline element needs fixing.
895 // Also <p> tags that contain blocks should be fixed
896 if (isBlock && (isInline(parent, true) || parent.tagName === 'P')) {
897 // Find the last inline parent node
898 var lastInlineParent = node;
899 while (isInline(lastInlineParent.parentNode, true) ||
900 lastInlineParent.parentNode.tagName === 'P') {
901 lastInlineParent = lastInlineParent.parentNode;
902 }
903
904 var before = extractContents(lastInlineParent, node);
905 var middle = node;
906
907 // Clone inline styling and apply it to the blocks children
908 while (parent && isInline(parent, true)) {
909 if (parent.nodeType === ELEMENT_NODE) {
910 var clone = parent.cloneNode();
911 while (middle.firstChild) {
912 appendChild(clone, middle.firstChild);
913 }
914
915 appendChild(middle, clone);
916 }
917 parent = parent.parentNode;
918 }
919
920 insertBefore(middle, lastInlineParent);
921 if (!isEmpty(before)) {
922 insertBefore(before, middle);
923 }
924 if (isEmpty(lastInlineParent)) {
925 remove(lastInlineParent);
926 }
927 }
928
929 // Fix invalid nested lists which should be wrapped in an li tag
930 if (isBlock && is(node, list) && is(node.parentNode, list)) {
931 var li = previousElementSibling(node, 'li');
932
933 if (!li) {
934 li = createElement('li');
935 insertBefore(li, node);
936 }
937
938 appendChild(li, node);
939 }
940 });
941 }
942
943 /**
944 * Finds the common parent of two nodes
945 *
946 * @param {!HTMLElement} node1
947 * @param {!HTMLElement} node2
948 * @return {?HTMLElement}
949 */
950 function findCommonAncestor(node1, node2) {
951 while ((node1 = node1.parentNode)) {
952 if (contains(node1, node2)) {
953 return node1;
954 }
955 }
956 }
957
958 /**
959 * @param {?Node}
960 * @param {boolean} [previous=false]
961 * @returns {?Node}
962 */
963 function getSibling(node, previous) {
964 if (!node) {
965 return null;
966 }
967
968 return (previous ? node.previousSibling : node.nextSibling) ||
969 getSibling(node.parentNode, previous);
970 }
971
972 /**
973 * Removes unused whitespace from the root and all it's children.
974 *
975 * @param {!HTMLElement} root
976 * @since 1.4.3
977 */
978 function removeWhiteSpace(root) {
979 var nodeValue, nodeType, next, previous, previousSibling,
980 nextNode, trimStart,
981 cssWhiteSpace = css(root, 'whiteSpace'),
982 // Preserve newlines if is pre-line
983 preserveNewLines = /line$/i.test(cssWhiteSpace),
984 node = root.firstChild;
985
986 // Skip pre & pre-wrap with any vendor prefix
987 if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) {
988 return;
989 }
990
991 while (node) {
992 nextNode = node.nextSibling;
993 nodeValue = node.nodeValue;
994 nodeType = node.nodeType;
995
996 if (nodeType === ELEMENT_NODE && node.firstChild) {
997 removeWhiteSpace(node);
998 }
999
1000 if (nodeType === TEXT_NODE) {
1001 next = getSibling(node);
1002 previous = getSibling(node, true);
1003 trimStart = false;
1004
1005 while (hasClass(previous, 'sceditor-ignore')) {
1006 previous = getSibling(previous, true);
1007 }
1008
1009 // If previous sibling isn't inline or is a textnode that
1010 // ends in whitespace, time the start whitespace
1011 if (isInline(node) && previous) {
1012 previousSibling = previous;
1013
1014 while (previousSibling.lastChild) {
1015 previousSibling = previousSibling.lastChild;
1016
1017 // eslint-disable-next-line max-depth
1018 while (hasClass(previousSibling, 'sceditor-ignore')) {
1019 previousSibling = getSibling(previousSibling, true);
1020 }
1021 }
1022
1023 trimStart = previousSibling.nodeType === TEXT_NODE ?
1024 /[\t\n\r ]$/.test(previousSibling.nodeValue) :
1025 !isInline(previousSibling);
1026 }
1027
1028 // Clear zero width spaces
1029 nodeValue = nodeValue.replace(/\u200B/g, '');
1030
1031 // Strip leading whitespace
1032 if (!previous || !isInline(previous) || trimStart) {
1033 nodeValue = nodeValue.replace(
1034 preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/,
1035 ''
1036 );
1037 }
1038
1039 // Strip trailing whitespace
1040 if (!next || !isInline(next)) {
1041 nodeValue = nodeValue.replace(
1042 preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/,
1043 ''
1044 );
1045 }
1046
1047 // Remove empty text nodes
1048 if (!nodeValue.length) {
1049 remove(node);
1050 } else {
1051 node.nodeValue = nodeValue.replace(
1052 preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g,
1053 ' '
1054 );
1055 }
1056 }
1057
1058 node = nextNode;
1059 }
1060 }
1061
1062 /**
1063 * Extracts all the nodes between the start and end nodes
1064 *
1065 * @param {HTMLElement} startNode The node to start extracting at
1066 * @param {HTMLElement} endNode The node to stop extracting at
1067 * @return {DocumentFragment}
1068 */
1069 function extractContents(startNode, endNode) {
1070 var range = startNode.ownerDocument.createRange();
1071
1072 range.setStartBefore(startNode);
1073 range.setEndAfter(endNode);
1074
1075 return range.extractContents();
1076 }
1077
1078 /**
1079 * Gets the offset position of an element
1080 *
1081 * @param {HTMLElement} node
1082 * @return {Object} An object with left and top properties
1083 */
1084 function getOffset(node) {
1085 var left = 0,
1086 top = 0;
1087
1088 while (node) {
1089 left += node.offsetLeft;
1090 top += node.offsetTop;
1091 node = node.offsetParent;
1092 }
1093
1094 return {
1095 left: left,
1096 top: top
1097 };
1098 }
1099
1100 /**
1101 * Gets the value of a CSS property from the elements style attribute
1102 *
1103 * @param {HTMLElement} elm
1104 * @param {string} property
1105 * @return {string}
1106 */
1107 function getStyle(elm, property) {
1108 var styleValue,
1109 elmStyle = elm.style;
1110
1111 if (!cssPropertyNameCache[property]) {
1112 cssPropertyNameCache[property] = camelCase(property);
1113 }
1114
1115 property = cssPropertyNameCache[property];
1116 styleValue = elmStyle[property];
1117
1118 // Add an exception for text-align
1119 if ('textAlign' === property) {
1120 styleValue = styleValue || css(elm, property);
1121
1122 if (css(elm.parentNode, property) === styleValue ||
1123 css(elm, 'display') !== 'block' || is(elm, 'hr,th')) {
1124 return '';
1125 }
1126 }
1127
1128 return styleValue;
1129 }
1130
1131 /**
1132 * Tests if an element has a style.
1133 *
1134 * If values are specified it will check that the styles value
1135 * matches one of the values
1136 *
1137 * @param {HTMLElement} elm
1138 * @param {string} property
1139 * @param {string|array} [values]
1140 * @return {boolean}
1141 */
1142 function hasStyle(elm, property, values) {
1143 var styleValue = getStyle(elm, property);
1144
1145 if (!styleValue) {
1146 return false;
1147 }
1148
1149 return !values || styleValue === values ||
1150 (Array.isArray(values) && values.indexOf(styleValue) > -1);
1151 }
1152
1153 /**
1154 * Returns true if both nodes have the same number of inline styles and all the
1155 * inline styles have matching values
1156 *
1157 * @param {HTMLElement} nodeA
1158 * @param {HTMLElement} nodeB
1159 * @returns {boolean}
1160 */
1161 function stylesMatch(nodeA, nodeB) {
1162 var i = nodeA.style.length;
1163 if (i !== nodeB.style.length) {
1164 return false;
1165 }
1166
1167 while (i--) {
1168 var prop = nodeA.style[i];
1169 if (nodeA.style[prop] !== nodeB.style[prop]) {
1170 return false;
1171 }
1172 }
1173
1174 return true;
1175 }
1176
1177 /**
1178 * Returns true if both nodes have the same number of attributes and all the
1179 * attribute values match
1180 *
1181 * @param {HTMLElement} nodeA
1182 * @param {HTMLElement} nodeB
1183 * @returns {boolean}
1184 */
1185 function attributesMatch(nodeA, nodeB) {
1186 var i = nodeA.attributes.length;
1187 if (i !== nodeB.attributes.length) {
1188 return false;
1189 }
1190
1191 while (i--) {
1192 var prop = nodeA.attributes[i];
1193 var notMatches = prop.name === 'style' ?
1194 !stylesMatch(nodeA, nodeB) :
1195 prop.value !== attr(nodeB, prop.name);
1196
1197 if (notMatches) {
1198 return false;
1199 }
1200 }
1201
1202 return true;
1203 }
1204
1205 /**
1206 * Removes an element placing its children in its place
1207 *
1208 * @param {HTMLElement} node
1209 */
1210 function removeKeepChildren(node) {
1211 while (node.firstChild) {
1212 insertBefore(node.firstChild, node);
1213 }
1214
1215 remove(node);
1216 }
1217
1218 /**
1219 * Merges inline styles and tags with parents where possible
1220 *
1221 * @param {Node} node
1222 * @since 3.1.0
1223 */
1224 function merge(node) {
1225 if (node.nodeType !== ELEMENT_NODE) {
1226 return;
1227 }
1228
1229 var parent = node.parentNode;
1230 var tagName = node.tagName;
1231 var mergeTags = /B|STRONG|EM|SPAN|FONT/;
1232
1233 // Merge children (in reverse as children can be removed)
1234 var i = node.childNodes.length;
1235 while (i--) {
1236 merge(node.childNodes[i]);
1237 }
1238
1239 // Should only merge inline tags
1240 if (!isInline(node)) {
1241 return;
1242 }
1243
1244 // Remove any inline styles that match the parent style
1245 i = node.style.length;
1246 while (i--) {
1247 var prop = node.style[i];
1248 if (css(parent, prop) === css(node, prop)) {
1249 node.style.removeProperty(prop);
1250 }
1251 }
1252
1253 // Can only remove / merge tags if no inline styling left.
1254 // If there is any inline style left then it means it at least partially
1255 // doesn't match the parent style so must stay
1256 if (!node.style.length) {
1257 removeAttr(node, 'style');
1258
1259 // Remove font attributes if match parent
1260 if (tagName === 'FONT') {
1261 if (css(node, 'fontFamily').toLowerCase() ===
1262 css(parent, 'fontFamily').toLowerCase()) {
1263 removeAttr(node, 'face');
1264 }
1265
1266 if (css(node, 'color') === css(parent, 'color')) {
1267 removeAttr(node, 'color');
1268 }
1269
1270 if (css(node, 'fontSize') === css(parent, 'fontSize')) {
1271 removeAttr(node, 'size');
1272 }
1273 }
1274
1275 // Spans and font tags with no attributes can be safely removed
1276 if (!node.attributes.length && /SPAN|FONT/.test(tagName)) {
1277 removeKeepChildren(node);
1278 } else if (mergeTags.test(tagName)) {
1279 var isBold = /B|STRONG/.test(tagName);
1280 var isItalic = tagName === 'EM';
1281
1282 while (parent && isInline(parent) &&
1283 (!isBold || /bold|700/i.test(css(parent, 'fontWeight'))) &&
1284 (!isItalic || css(parent, 'fontStyle') === 'italic')) {
1285
1286 // Remove if parent match
1287 if ((parent.tagName === tagName ||
1288 (isBold && /B|STRONG/.test(parent.tagName))) &&
1289 attributesMatch(parent, node)) {
1290 removeKeepChildren(node);
1291 break;
1292 }
1293
1294 parent = parent.parentNode;
1295 }
1296 }
1297 }
1298
1299 // Merge siblings if attributes, including inline styles, match
1300 var next = node.nextSibling;
1301 if (next && next.tagName === tagName && attributesMatch(next, node)) {
1302 appendChild(node, next);
1303 removeKeepChildren(next);
1304 }
1305 }
1306
1307 /**
1308 * Default options for SCEditor
1309 * @type {Object}
1310 */
1311 var defaultOptions = {
1312 /** @lends jQuery.sceditor.defaultOptions */
1313 /**
1314 * Toolbar buttons order and groups. Should be comma separated and
1315 * have a bar | to separate groups
1316 *
1317 * @type {string}
1318 */
1319 toolbar: 'bold,italic,underline,strike,subscript,superscript|' +
1320 'left,center,right,justify|font,size,color,removeformat|' +
1321 'cut,copy,pastetext|bulletlist,orderedlist,indent,outdent|' +
1322 'table|code,quote|horizontalrule,image,email,link,unlink|' +
1323 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source',
1324
1325 /**
1326 * Comma separated list of commands to excludes from the toolbar
1327 *
1328 * @type {string}
1329 */
1330 toolbarExclude: null,
1331
1332 /**
1333 * Stylesheet to include in the WYSIWYG editor. This is what will style
1334 * the WYSIWYG elements
1335 *
1336 * @type {string}
1337 */
1338 style: 'jquery.sceditor.default.css',
1339
1340 /**
1341 * Comma separated list of fonts for the font selector
1342 *
1343 * @type {string}
1344 */
1345 fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' +
1346 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana',
1347
1348 /**
1349 * Colors should be comma separated and have a bar | to signal a new
1350 * column.
1351 *
1352 * If null the colors will be auto generated.
1353 *
1354 * @type {string}
1355 */
1356 colors: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' +
1357 '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' +
1358 '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' +
1359 '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' +
1360 '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' +
1361 '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' +
1362 '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' +
1363 '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef',
1364
1365 /**
1366 * The locale to use.
1367 * @type {string}
1368 */
1369 locale: attr(document.documentElement, 'lang') || 'en',
1370
1371 /**
1372 * The Charset to use
1373 * @type {string}
1374 */
1375 charset: 'utf-8',
1376
1377 /**
1378 * Compatibility mode for emoticons.
1379 *
1380 * Helps if you have emoticons such as :/ which would put an emoticon
1381 * inside http://
1382 *
1383 * This mode requires emoticons to be surrounded by whitespace or end of
1384 * line chars. This mode has limited As You Type emoticon conversion
1385 * support. It will not replace AYT for end of line chars, only
1386 * emoticons surrounded by whitespace. They will still be replaced
1387 * correctly when loaded just not AYT.
1388 *
1389 * @type {boolean}
1390 */
1391 emoticonsCompat: false,
1392
1393 /**
1394 * If to enable emoticons. Can be changes at runtime using the
1395 * emoticons() method.
1396 *
1397 * @type {boolean}
1398 * @since 1.4.2
1399 */
1400 emoticonsEnabled: true,
1401
1402 /**
1403 * Emoticon root URL
1404 *
1405 * @type {string}
1406 */
1407 emoticonsRoot: '',
1408 emoticons: {
1409 dropdown: {
1410 ':)': 'emoticons/smile.png',
1411 ':angel:': 'emoticons/angel.png',
1412 ':angry:': 'emoticons/angry.png',
1413 '8-)': 'emoticons/cool.png',
1414 ':\'(': 'emoticons/cwy.png',
1415 ':ermm:': 'emoticons/ermm.png',
1416 ':D': 'emoticons/grin.png',
1417 '<3': 'emoticons/heart.png',
1418 ':(': 'emoticons/sad.png',
1419 ':O': 'emoticons/shocked.png',
1420 ':P': 'emoticons/tongue.png',
1421 ';)': 'emoticons/wink.png'
1422 },
1423 more: {
1424 ':alien:': 'emoticons/alien.png',
1425 ':blink:': 'emoticons/blink.png',
1426 ':blush:': 'emoticons/blush.png',
1427 ':cheerful:': 'emoticons/cheerful.png',
1428 ':devil:': 'emoticons/devil.png',
1429 ':dizzy:': 'emoticons/dizzy.png',
1430 ':getlost:': 'emoticons/getlost.png',
1431 ':happy:': 'emoticons/happy.png',
1432 ':kissing:': 'emoticons/kissing.png',
1433 ':ninja:': 'emoticons/ninja.png',
1434 ':pinch:': 'emoticons/pinch.png',
1435 ':pouty:': 'emoticons/pouty.png',
1436 ':sick:': 'emoticons/sick.png',
1437 ':sideways:': 'emoticons/sideways.png',
1438 ':silly:': 'emoticons/silly.png',
1439 ':sleeping:': 'emoticons/sleeping.png',
1440 ':unsure:': 'emoticons/unsure.png',
1441 ':woot:': 'emoticons/w00t.png',
1442 ':wassat:': 'emoticons/wassat.png'
1443 },
1444 hidden: {
1445 ':whistling:': 'emoticons/whistling.png',
1446 ':love:': 'emoticons/wub.png'
1447 }
1448 },
1449
1450 /**
1451 * Width of the editor. Set to null for automatic with
1452 *
1453 * @type {?number}
1454 */
1455 width: null,
1456
1457 /**
1458 * Height of the editor including toolbar. Set to null for automatic
1459 * height
1460 *
1461 * @type {?number}
1462 */
1463 height: null,
1464
1465 /**
1466 * If to allow the editor to be resized
1467 *
1468 * @type {boolean}
1469 */
1470 resizeEnabled: true,
1471
1472 /**
1473 * Min resize to width, set to null for half textarea width or -1 for
1474 * unlimited
1475 *
1476 * @type {?number}
1477 */
1478 resizeMinWidth: null,
1479 /**
1480 * Min resize to height, set to null for half textarea height or -1 for
1481 * unlimited
1482 *
1483 * @type {?number}
1484 */
1485 resizeMinHeight: null,
1486 /**
1487 * Max resize to height, set to null for double textarea height or -1
1488 * for unlimited
1489 *
1490 * @type {?number}
1491 */
1492 resizeMaxHeight: null,
1493 /**
1494 * Max resize to width, set to null for double textarea width or -1 for
1495 * unlimited
1496 *
1497 * @type {?number}
1498 */
1499 resizeMaxWidth: null,
1500 /**
1501 * If resizing by height is enabled
1502 *
1503 * @type {boolean}
1504 */
1505 resizeHeight: true,
1506 /**
1507 * If resizing by width is enabled
1508 *
1509 * @type {boolean}
1510 */
1511 resizeWidth: true,
1512
1513 /**
1514 * Date format, will be overridden if locale specifies one.
1515 *
1516 * The words year, month and day will be replaced with the users current
1517 * year, month and day.
1518 *
1519 * @type {string}
1520 */
1521 dateFormat: 'year-month-day',
1522
1523 /**
1524 * Element to inset the toolbar into.
1525 *
1526 * @type {HTMLElement}
1527 */
1528 toolbarContainer: null,
1529
1530 /**
1531 * If to enable paste filtering. This is currently experimental, please
1532 * report any issues.
1533 *
1534 * @type {boolean}
1535 */
1536 enablePasteFiltering: false,
1537
1538 /**
1539 * If to completely disable pasting into the editor
1540 *
1541 * @type {boolean}
1542 */
1543 disablePasting: false,
1544
1545 /**
1546 * If the editor is read only.
1547 *
1548 * @type {boolean}
1549 */
1550 readOnly: false,
1551
1552 /**
1553 * If to set the editor to right-to-left mode.
1554 *
1555 * If set to null the direction will be automatically detected.
1556 *
1557 * @type {boolean}
1558 */
1559 rtl: false,
1560
1561 /**
1562 * If to auto focus the editor on page load
1563 *
1564 * @type {boolean}
1565 */
1566 autofocus: false,
1567
1568 /**
1569 * If to auto focus the editor to the end of the content
1570 *
1571 * @type {boolean}
1572 */
1573 autofocusEnd: true,
1574
1575 /**
1576 * If to auto expand the editor to fix the content
1577 *
1578 * @type {boolean}
1579 */
1580 autoExpand: false,
1581
1582 /**
1583 * If to auto update original textbox on blur
1584 *
1585 * @type {boolean}
1586 */
1587 autoUpdate: false,
1588
1589 /**
1590 * If to enable the browsers built in spell checker
1591 *
1592 * @type {boolean}
1593 */
1594 spellcheck: true,
1595
1596 /**
1597 * If to run the source editor when there is no WYSIWYG support. Only
1598 * really applies to mobile OS's.
1599 *
1600 * @type {boolean}
1601 */
1602 runWithoutWysiwygSupport: false,
1603
1604 /**
1605 * If to load the editor in source mode and still allow switching
1606 * between WYSIWYG and source mode
1607 *
1608 * @type {boolean}
1609 */
1610 startInSourceMode: false,
1611
1612 /**
1613 * Optional ID to give the editor.
1614 *
1615 * @type {string}
1616 */
1617 id: null,
1618
1619 /**
1620 * Comma separated list of plugins
1621 *
1622 * @type {string}
1623 */
1624 plugins: '',
1625
1626 /**
1627 * z-index to set the editor container to. Needed for jQuery UI dialog.
1628 *
1629 * @type {?number}
1630 */
1631 zIndex: null,
1632
1633 /**
1634 * If to trim the BBCode. Removes any spaces at the start and end of the
1635 * BBCode string.
1636 *
1637 * @type {boolean}
1638 */
1639 bbcodeTrim: false,
1640
1641 /**
1642 * If to disable removing block level elements by pressing backspace at
1643 * the start of them
1644 *
1645 * @type {boolean}
1646 */
1647 disableBlockRemove: false,
1648
1649 /**
1650 * Array of allowed URL (should be either strings or regex) for iframes.
1651 *
1652 * If it's a string then iframes where the start of the src matches the
1653 * specified string will be allowed.
1654 *
1655 * If it's a regex then iframes where the src matches the regex will be
1656 * allowed.
1657 *
1658 * @type {Array}
1659 */
1660 allowedIframeUrls: [],
1661
1662 /**
1663 * BBCode parser options, only applies if using the editor in BBCode
1664 * mode.
1665 *
1666 * See SCEditor.BBCodeParser.defaults for list of valid options
1667 *
1668 * @type {Object}
1669 */
1670 parserOptions: { },
1671
1672 /**
1673 * CSS that will be added to the to dropdown menu (eg. z-index)
1674 *
1675 * @type {Object}
1676 */
1677 dropDownCss: { }
1678 };
1679
1680 // Must start with a valid scheme
1681 // ^
1682 // Schemes that are considered safe
1683 // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
1684 // Relative schemes (//:) are considered safe
1685 // (\\/\\/)|
1686 // Image data URI's are considered safe
1687 // data:image\\/(png|bmp|gif|p?jpe?g);
1688 var VALID_SCHEME_REGEX =
1689 /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
1690
1691 /**
1692 * Escapes a string so it's safe to use in regex
1693 *
1694 * @param {string} str
1695 * @return {string}
1696 */
1697 function regex(str) {
1698 return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
1699 }
1700 /**
1701 * Escapes all HTML entities in a string
1702 *
1703 * If noQuotes is set to false, all single and double
1704 * quotes will also be escaped
1705 *
1706 * @param {string} str
1707 * @param {boolean} [noQuotes=true]
1708 * @return {string}
1709 * @since 1.4.1
1710 */
1711 function entities(str, noQuotes) {
1712 if (!str) {
1713 return str;
1714 }
1715
1716 var replacements = {
1717 '&': '&amp;',
1718 '<': '&lt;',
1719 '>': '&gt;',
1720 ' ': '&nbsp; ',
1721 '\r\n': '<br />',
1722 '\r': '<br />',
1723 '\n': '<br />'
1724 };
1725
1726 if (noQuotes !== false) {
1727 replacements['"'] = '&#34;';
1728 replacements['\''] = '&#39;';
1729 replacements['`'] = '&#96;';
1730 }
1731
1732 str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) {
1733 return replacements[match] || match;
1734 });
1735
1736 return str;
1737 }
1738 /**
1739 * Escape URI scheme.
1740 *
1741 * Appends the current URL to a url if it has a scheme that is not:
1742 *
1743 * http
1744 * https
1745 * sftp
1746 * ftp
1747 * mailto
1748 * spotify
1749 * skype
1750 * ssh
1751 * teamspeak
1752 * tel
1753 * //
1754 * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
1755 *
1756 * **IMPORTANT**: This does not escape any HTML in a url, for
1757 * that use the escape.entities() method.
1758 *
1759 * @param {string} url
1760 * @return {string}
1761 * @since 1.4.5
1762 */
1763 function uriScheme(url) {
1764 var path,
1765 // If there is a : before a / then it has a scheme
1766 hasScheme = /^[^\/]*:/i,
1767 location = window.location;
1768
1769 // Has no scheme or a valid scheme
1770 if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
1771 return url;
1772 }
1773
1774 path = location.pathname.split('/');
1775 path.pop();
1776
1777 return location.protocol + '//' +
1778 location.host +
1779 path.join('/') + '/' +
1780 url;
1781 }
1782
1783 /**
1784 * HTML templates used by the editor and default commands
1785 * @type {Object}
1786 * @private
1787 */
1788 var _templates = {
1789 html:
1790 '<!DOCTYPE html>' +
1791 '<html{attrs}>' +
1792 '<head>' +
1793 '<meta http-equiv="Content-Type" ' +
1794 'content="text/html;charset={charset}" />' +
1795 '<link rel="stylesheet" type="text/css" href="{style}" />' +
1796 '</head>' +
1797 '<body contenteditable="true" {spellcheck}><p></p></body>' +
1798 '</html>',
1799
1800 toolbarButton: '<a class="sceditor-button sceditor-button-{name}" ' +
1801 'data-sceditor-command="{name}" unselectable="on">' +
1802 '<div unselectable="on">{dispName}</div></a>',
1803
1804 emoticon: '<img src="{url}" data-sceditor-emoticon="{key}" ' +
1805 'alt="{key}" title="{tooltip}" />',
1806
1807 fontOpt: '<a class="sceditor-font-option" href="#" ' +
1808 'data-font="{font}"><font face="{font}">{font}</font></a>',
1809
1810 sizeOpt: '<a class="sceditor-fontsize-option" data-size="{size}" ' +
1811 'href="#"><font size="{size}">{size}</font></a>',
1812
1813 pastetext:
1814 '<div><label for="txt">{label}</label> ' +
1815 '<textarea cols="20" rows="7" id="txt"></textarea></div>' +
1816 '<div><input type="button" class="button" value="{insert}" />' +
1817 '</div>',
1818
1819 table:
1820 '<div><label for="rows">{rows}</label><input type="text" ' +
1821 'id="rows" value="2" /></div>' +
1822 '<div><label for="cols">{cols}</label><input type="text" ' +
1823 'id="cols" value="2" /></div>' +
1824 '<div><input type="button" class="button" value="{insert}"' +
1825 ' /></div>',
1826
1827 image:
1828 '<div><label for="image">{url}</label> ' +
1829 '<input type="text" id="image" dir="ltr" placeholder="https://" /></div>' +
1830 '<div><label for="width">{width}</label> ' +
1831 '<input type="text" id="width" size="2" dir="ltr" /></div>' +
1832 '<div><label for="height">{height}</label> ' +
1833 '<input type="text" id="height" size="2" dir="ltr" /></div>' +
1834 '<div><input type="button" class="button" value="{insert}" />' +
1835 '</div>',
1836
1837 email:
1838 '<div><label for="email">{label}</label> ' +
1839 '<input type="text" id="email" dir="ltr" /></div>' +
1840 '<div><label for="des">{desc}</label> ' +
1841 '<input type="text" id="des" /></div>' +
1842 '<div><input type="button" class="button" value="{insert}" />' +
1843 '</div>',
1844
1845 link:
1846 '<div><label for="link">{url}</label> ' +
1847 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' +
1848 '<div><label for="des">{desc}</label> ' +
1849 '<input type="text" id="des" /></div>' +
1850 '<div><input type="button" class="button" value="{ins}" /></div>',
1851
1852 youtubeMenu:
1853 '<div><label for="link">{label}</label> ' +
1854 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' +
1855 '<div><input type="button" class="button" value="{insert}" />' +
1856 '</div>',
1857
1858 youtube:
1859 '<iframe width="560" height="315" frameborder="0" allowfullscreen ' +
1860 'src="https://www.youtube-nocookie.com/embed/{id}?wmode=opaque&start={time}" ' +
1861 'data-youtube-id="{id}"></iframe>'
1862 };
1863
1864 /**
1865 * Replaces any params in a template with the passed params.
1866 *
1867 * If createHtml is passed it will return a DocumentFragment
1868 * containing the parsed template.
1869 *
1870 * @param {string} name
1871 * @param {Object} [params]
1872 * @param {boolean} [createHtml]
1873 * @returns {string|DocumentFragment}
1874 * @private
1875 */
1876 function _tmpl (name, params, createHtml) {
1877 var template = _templates[name];
1878
1879 Object.keys(params).forEach(function (name) {
1880 template = template.replace(
1881 new RegExp(regex('{' + name + '}'), 'g'), params[name]
1882 );
1883 });
1884
1885 if (createHtml) {
1886 template = parseHTML(template);
1887 }
1888
1889 return template;
1890 }
1891
1892 /**
1893 * Fixes a bug in FF where it sometimes wraps
1894 * new lines in their own list item.
1895 * See issue #359
1896 */
1897 function fixFirefoxListBug(editor) {
1898 // Only apply to Firefox as will break other browsers.
1899 if ('mozHidden' in document) {
1900 var node = editor.getBody();
1901 var next;
1902
1903 while (node) {
1904 next = node;
1905
1906 if (next.firstChild) {
1907 next = next.firstChild;
1908 } else {
1909
1910 while (next && !next.nextSibling) {
1911 next = next.parentNode;
1912 }
1913
1914 if (next) {
1915 next = next.nextSibling;
1916 }
1917 }
1918
1919 if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) {
1920 // Only remove if newlines are collapsed
1921 if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) {
1922 remove(node);
1923 }
1924 }
1925
1926 node = next;
1927 }
1928 }
1929 }
1930
1931
1932 /**
1933 * Map of all the commands for SCEditor
1934 * @type {Object}
1935 * @name commands
1936 * @memberOf jQuery.sceditor
1937 */
1938 var defaultCmds = {
1939 // START_COMMAND: Bold
1940 bold: {
1941 exec: 'bold',
1942 tooltip: 'Bold',
1943 shortcut: 'Ctrl+B'
1944 },
1945 // END_COMMAND
1946 // START_COMMAND: Italic
1947 italic: {
1948 exec: 'italic',
1949 tooltip: 'Italic',
1950 shortcut: 'Ctrl+I'
1951 },
1952 // END_COMMAND
1953 // START_COMMAND: Underline
1954 underline: {
1955 exec: 'underline',
1956 tooltip: 'Underline',
1957 shortcut: 'Ctrl+U'
1958 },
1959 // END_COMMAND
1960 // START_COMMAND: Strikethrough
1961 strike: {
1962 exec: 'strikethrough',
1963 tooltip: 'Strikethrough'
1964 },
1965 // END_COMMAND
1966 // START_COMMAND: Subscript
1967 subscript: {
1968 exec: 'subscript',
1969 tooltip: 'Subscript'
1970 },
1971 // END_COMMAND
1972 // START_COMMAND: Superscript
1973 superscript: {
1974 exec: 'superscript',
1975 tooltip: 'Superscript'
1976 },
1977 // END_COMMAND
1978
1979 // START_COMMAND: Left
1980 left: {
1981 state: function (node) {
1982 if (node && node.nodeType === 3) {
1983 node = node.parentNode;
1984 }
1985
1986 if (node) {
1987 var isLtr = css(node, 'direction') === 'ltr';
1988 var align = css(node, 'textAlign');
1989
1990 // Can be -moz-left
1991 return /left/.test(align) ||
1992 align === (isLtr ? 'start' : 'end');
1993 }
1994 },
1995 exec: 'justifyleft',
1996 tooltip: 'Align left'
1997 },
1998 // END_COMMAND
1999 // START_COMMAND: Centre
2000 center: {
2001 exec: 'justifycenter',
2002 tooltip: 'Center'
2003 },
2004 // END_COMMAND
2005 // START_COMMAND: Right
2006 right: {
2007 state: function (node) {
2008 if (node && node.nodeType === 3) {
2009 node = node.parentNode;
2010 }
2011
2012 if (node) {
2013 var isLtr = css(node, 'direction') === 'ltr';
2014 var align = css(node, 'textAlign');
2015
2016 // Can be -moz-right
2017 return /right/.test(align) ||
2018 align === (isLtr ? 'end' : 'start');
2019 }
2020 },
2021 exec: 'justifyright',
2022 tooltip: 'Align right'
2023 },
2024 // END_COMMAND
2025 // START_COMMAND: Justify
2026 justify: {
2027 exec: 'justifyfull',
2028 tooltip: 'Justify'
2029 },
2030 // END_COMMAND
2031
2032 // START_COMMAND: Font
2033 font: {
2034 _dropDown: function (editor, caller, callback) {
2035 var content = createElement('div');
2036
2037 on(content, 'click', 'a', function (e) {
2038 callback(data(this, 'font'));
2039 editor.closeDropDown(true);
2040 e.preventDefault();
2041 });
2042
2043 editor.opts.fonts.split(',').forEach(function (font) {
2044 appendChild(content, _tmpl('fontOpt', {
2045 font: font
2046 }, true));
2047 });
2048
2049 editor.createDropDown(caller, 'font-picker', content);
2050 },
2051 exec: function (caller) {
2052 var editor = this;
2053
2054 defaultCmds.font._dropDown(editor, caller, function (fontName) {
2055 editor.execCommand('fontname', fontName);
2056 });
2057 },
2058 tooltip: 'Font Name'
2059 },
2060 // END_COMMAND
2061 // START_COMMAND: Size
2062 size: {
2063 _dropDown: function (editor, caller, callback) {
2064 var content = createElement('div');
2065
2066 on(content, 'click', 'a', function (e) {
2067 callback(data(this, 'size'));
2068 editor.closeDropDown(true);
2069 e.preventDefault();
2070 });
2071
2072 for (var i = 1; i <= 7; i++) {
2073 appendChild(content, _tmpl('sizeOpt', {
2074 size: i
2075 }, true));
2076 }
2077
2078 editor.createDropDown(caller, 'fontsize-picker', content);
2079 },
2080 exec: function (caller) {
2081 var editor = this;
2082
2083 defaultCmds.size._dropDown(editor, caller, function (fontSize) {
2084 editor.execCommand('fontsize', fontSize);
2085 });
2086 },
2087 tooltip: 'Font Size'
2088 },
2089 // END_COMMAND
2090 // START_COMMAND: Colour
2091 color: {
2092 _dropDown: function (editor, caller, callback) {
2093 var content = createElement('div'),
2094 html = '',
2095 cmd = defaultCmds.color;
2096
2097 if (!cmd._htmlCache) {
2098 editor.opts.colors.split('|').forEach(function (column) {
2099 html += '<div class="sceditor-color-column">';
2100
2101 column.split(',').forEach(function (color) {
2102 html +=
2103 '<a href="#" class="sceditor-color-option"' +
2104 ' style="background-color: ' + color + '"' +
2105 ' data-color="' + color + '"></a>';
2106 });
2107
2108 html += '</div>';
2109 });
2110
2111 cmd._htmlCache = html;
2112 }
2113
2114 appendChild(content, parseHTML(cmd._htmlCache));
2115
2116 on(content, 'click', 'a', function (e) {
2117 callback(data(this, 'color'));
2118 editor.closeDropDown(true);
2119 e.preventDefault();
2120 });
2121
2122 editor.createDropDown(caller, 'color-picker', content);
2123 },
2124 exec: function (caller) {
2125 var editor = this;
2126
2127 defaultCmds.color._dropDown(editor, caller, function (color) {
2128 editor.execCommand('forecolor', color);
2129 });
2130 },
2131 tooltip: 'Font Color'
2132 },
2133 // END_COMMAND
2134 // START_COMMAND: Remove Format
2135 removeformat: {
2136 exec: 'removeformat',
2137 tooltip: 'Remove Formatting'
2138 },
2139 // END_COMMAND
2140
2141 // START_COMMAND: Cut
2142 cut: {
2143 exec: 'cut',
2144 tooltip: 'Cut',
2145 errorMessage: 'Your browser does not allow the cut command. ' +
2146 'Please use the keyboard shortcut Ctrl/Cmd-X'
2147 },
2148 // END_COMMAND
2149 // START_COMMAND: Copy
2150 copy: {
2151 exec: 'copy',
2152 tooltip: 'Copy',
2153 errorMessage: 'Your browser does not allow the copy command. ' +
2154 'Please use the keyboard shortcut Ctrl/Cmd-C'
2155 },
2156 // END_COMMAND
2157 // START_COMMAND: Paste
2158 paste: {
2159 exec: 'paste',
2160 tooltip: 'Paste',
2161 errorMessage: 'Your browser does not allow the paste command. ' +
2162 'Please use the keyboard shortcut Ctrl/Cmd-V'
2163 },
2164 // END_COMMAND
2165 // START_COMMAND: Paste Text
2166 pastetext: {
2167 exec: function (caller) {
2168 var val,
2169 content = createElement('div'),
2170 editor = this;
2171
2172 appendChild(content, _tmpl('pastetext', {
2173 label: editor._(
2174 'Paste your text inside the following box:'
2175 ),
2176 insert: editor._('Insert')
2177 }, true));
2178
2179 on(content, 'click', '.button', function (e) {
2180 val = find(content, '#txt')[0].value;
2181
2182 if (val) {
2183 editor.wysiwygEditorInsertText(val);
2184 }
2185
2186 editor.closeDropDown(true);
2187 e.preventDefault();
2188 });
2189
2190 editor.createDropDown(caller, 'pastetext', content);
2191 },
2192 tooltip: 'Paste Text'
2193 },
2194 // END_COMMAND
2195 // START_COMMAND: Bullet List
2196 bulletlist: {
2197 exec: function () {
2198 fixFirefoxListBug(this);
2199 this.execCommand('insertunorderedlist');
2200 },
2201 tooltip: 'Bullet list'
2202 },
2203 // END_COMMAND
2204 // START_COMMAND: Ordered List
2205 orderedlist: {
2206 exec: function () {
2207 fixFirefoxListBug(this);
2208 this.execCommand('insertorderedlist');
2209 },
2210 tooltip: 'Numbered list'
2211 },
2212 // END_COMMAND
2213 // START_COMMAND: Indent
2214 indent: {
2215 state: function (parent, firstBlock) {
2216 // Only works with lists, for now
2217 var range, startParent, endParent;
2218
2219 if (is(firstBlock, 'li')) {
2220 return 0;
2221 }
2222
2223 if (is(firstBlock, 'ul,ol,menu')) {
2224 // if the whole list is selected, then this must be
2225 // invalidated because the browser will place a
2226 // <blockquote> there
2227 range = this.getRangeHelper().selectedRange();
2228
2229 startParent = range.startContainer.parentNode;
2230 endParent = range.endContainer.parentNode;
2231
2232 // TODO: could use nodeType for this?
2233 // Maybe just check the firstBlock contains both the start
2234 //and end containers
2235
2236 // Select the tag, not the textNode
2237 // (that's why the parentNode)
2238 if (startParent !==
2239 startParent.parentNode.firstElementChild ||
2240 // work around a bug in FF
2241 (is(endParent, 'li') && endParent !==
2242 endParent.parentNode.lastElementChild)) {
2243 return 0;
2244 }
2245 }
2246
2247 return -1;
2248 },
2249 exec: function () {
2250 var editor = this,
2251 block = editor.getRangeHelper().getFirstBlockParent();
2252
2253 editor.focus();
2254
2255 // An indent system is quite complicated as there are loads
2256 // of complications and issues around how to indent text
2257 // As default, let's just stay with indenting the lists,
2258 // at least, for now.
2259 if (closest(block, 'ul,ol,menu')) {
2260 editor.execCommand('indent');
2261 }
2262 },
2263 tooltip: 'Add indent'
2264 },
2265 // END_COMMAND
2266 // START_COMMAND: Outdent
2267 outdent: {
2268 state: function (parents, firstBlock) {
2269 return closest(firstBlock, 'ul,ol,menu') ? 0 : -1;
2270 },
2271 exec: function () {
2272 var block = this.getRangeHelper().getFirstBlockParent();
2273 if (closest(block, 'ul,ol,menu')) {
2274 this.execCommand('outdent');
2275 }
2276 },
2277 tooltip: 'Remove one indent'
2278 },
2279 // END_COMMAND
2280
2281 // START_COMMAND: Table
2282 table: {
2283 exec: function (caller) {
2284 var editor = this,
2285 content = createElement('div');
2286
2287 appendChild(content, _tmpl('table', {
2288 rows: editor._('Rows:'),
2289 cols: editor._('Cols:'),
2290 insert: editor._('Insert')
2291 }, true));
2292
2293 on(content, 'click', '.button', function (e) {
2294 var rows = Number(find(content, '#rows')[0].value),
2295 cols = Number(find(content, '#cols')[0].value),
2296 html = '<table>';
2297
2298 if (rows > 0 && cols > 0) {
2299 html += Array(rows + 1).join(
2300 '<tr>' +
2301 Array(cols + 1).join(
2302 '<td><br /></td>'
2303 ) +
2304 '</tr>'
2305 );
2306
2307 html += '</table>';
2308
2309 editor.wysiwygEditorInsertHtml(html);
2310 editor.closeDropDown(true);
2311 e.preventDefault();
2312 }
2313 });
2314
2315 editor.createDropDown(caller, 'inserttable', content);
2316 },
2317 tooltip: 'Insert a table'
2318 },
2319 // END_COMMAND
2320
2321 // START_COMMAND: Horizontal Rule
2322 horizontalrule: {
2323 exec: 'inserthorizontalrule',
2324 tooltip: 'Insert a horizontal rule'
2325 },
2326 // END_COMMAND
2327
2328 // START_COMMAND: Code
2329 code: {
2330 exec: function () {
2331 this.wysiwygEditorInsertHtml(
2332 '<code>',
2333 '<br /></code>'
2334 );
2335 },
2336 tooltip: 'Code'
2337 },
2338 // END_COMMAND
2339
2340 // START_COMMAND: Image
2341 image: {
2342 _dropDown: function (editor, caller, selected, cb) {
2343 var content = createElement('div');
2344
2345 appendChild(content, _tmpl('image', {
2346 url: editor._('URL:'),
2347 width: editor._('Width (optional):'),
2348 height: editor._('Height (optional):'),
2349 insert: editor._('Insert')
2350 }, true));
2351
2352
2353 var urlInput = find(content, '#image')[0];
2354
2355 urlInput.value = selected;
2356
2357 on(content, 'click', '.button', function (e) {
2358 if (urlInput.value) {
2359 cb(
2360 urlInput.value,
2361 find(content, '#width')[0].value,
2362 find(content, '#height')[0].value
2363 );
2364 }
2365
2366 editor.closeDropDown(true);
2367 e.preventDefault();
2368 });
2369
2370 editor.createDropDown(caller, 'insertimage', content);
2371 },
2372 exec: function (caller) {
2373 var editor = this;
2374
2375 defaultCmds.image._dropDown(
2376 editor,
2377 caller,
2378 '',
2379 function (url, width, height) {
2380 var attrs = '';
2381
2382 if (width) {
2383 attrs += ' width="' + parseInt(width, 10) + '"';
2384 }
2385
2386 if (height) {
2387 attrs += ' height="' + parseInt(height, 10) + '"';
2388 }
2389
2390 attrs += ' src="' + entities(url) + '"';
2391
2392 editor.wysiwygEditorInsertHtml(
2393 '<img' + attrs + ' />'
2394 );
2395 }
2396 );
2397 },
2398 tooltip: 'Insert an image'
2399 },
2400 // END_COMMAND
2401
2402 // START_COMMAND: E-mail
2403 email: {
2404 _dropDown: function (editor, caller, cb) {
2405 var content = createElement('div');
2406
2407 appendChild(content, _tmpl('email', {
2408 label: editor._('E-mail:'),
2409 desc: editor._('Description (optional):'),
2410 insert: editor._('Insert')
2411 }, true));
2412
2413 on(content, 'click', '.button', function (e) {
2414 var email = find(content, '#email')[0].value;
2415
2416 if (email) {
2417 cb(email, find(content, '#des')[0].value);
2418 }
2419
2420 editor.closeDropDown(true);
2421 e.preventDefault();
2422 });
2423
2424 editor.createDropDown(caller, 'insertemail', content);
2425 },
2426 exec: function (caller) {
2427 var editor = this;
2428
2429 defaultCmds.email._dropDown(
2430 editor,
2431 caller,
2432 function (email, text) {
2433 if (!editor.getRangeHelper().selectedHtml() || text) {
2434 editor.wysiwygEditorInsertHtml(
2435 '<a href="' +
2436 'mailto:' + entities(email) + '">' +
2437 entities((text || email)) +
2438 '</a>'
2439 );
2440 } else {
2441 editor.execCommand('createlink', 'mailto:' + email);
2442 }
2443 }
2444 );
2445 },
2446 tooltip: 'Insert an email'
2447 },
2448 // END_COMMAND
2449
2450 // START_COMMAND: Link
2451 link: {
2452 _dropDown: function (editor, caller, cb) {
2453 var content = createElement('div');
2454
2455 appendChild(content, _tmpl('link', {
2456 url: editor._('URL:'),
2457 desc: editor._('Description (optional):'),
2458 ins: editor._('Insert')
2459 }, true));
2460
2461 var linkInput = find(content, '#link')[0];
2462
2463 function insertUrl(e) {
2464 if (linkInput.value) {
2465 cb(linkInput.value, find(content, '#des')[0].value);
2466 }
2467
2468 editor.closeDropDown(true);
2469 e.preventDefault();
2470 }
2471
2472 on(content, 'click', '.button', insertUrl);
2473 on(content, 'keypress', function (e) {
2474 // 13 = enter key
2475 if (e.which === 13 && linkInput.value) {
2476 insertUrl(e);
2477 }
2478 }, EVENT_CAPTURE);
2479
2480 editor.createDropDown(caller, 'insertlink', content);
2481 },
2482 exec: function (caller) {
2483 var editor = this;
2484
2485 defaultCmds.link._dropDown(editor, caller, function (url, text) {
2486 if (text || !editor.getRangeHelper().selectedHtml()) {
2487 editor.wysiwygEditorInsertHtml(
2488 '<a href="' + entities(url) + '">' +
2489 entities(text || url) +
2490 '</a>'
2491 );
2492 } else {
2493 editor.execCommand('createlink', url);
2494 }
2495 });
2496 },
2497 tooltip: 'Insert a link'
2498 },
2499 // END_COMMAND
2500
2501 // START_COMMAND: Unlink
2502 unlink: {
2503 state: function () {
2504 return closest(this.currentNode(), 'a') ? 0 : -1;
2505 },
2506 exec: function () {
2507 var anchor = closest(this.currentNode(), 'a');
2508
2509 if (anchor) {
2510 while (anchor.firstChild) {
2511 insertBefore(anchor.firstChild, anchor);
2512 }
2513
2514 remove(anchor);
2515 }
2516 },
2517 tooltip: 'Unlink'
2518 },
2519 // END_COMMAND
2520
2521
2522 // START_COMMAND: Quote
2523 quote: {
2524 exec: function (caller, html, author) {
2525 var before = '<blockquote>',
2526 end = '</blockquote>';
2527
2528 // if there is HTML passed set end to null so any selected
2529 // text is replaced
2530 if (html) {
2531 author = (author ? '<cite>' +
2532 entities(author) +
2533 '</cite>' : '');
2534 before = before + author + html + end;
2535 end = null;
2536 // if not add a newline to the end of the inserted quote
2537 } else if (this.getRangeHelper().selectedHtml() === '') {
2538 end = '<br />' + end;
2539 }
2540
2541 this.wysiwygEditorInsertHtml(before, end);
2542 },
2543 tooltip: 'Insert a Quote'
2544 },
2545 // END_COMMAND
2546
2547 // START_COMMAND: Emoticons
2548 emoticon: {
2549 exec: function (caller) {
2550 var editor = this;
2551
2552 var createContent = function (includeMore) {
2553 var moreLink,
2554 opts = editor.opts,
2555 emoticonsRoot = opts.emoticonsRoot || '',
2556 emoticonsCompat = opts.emoticonsCompat,
2557 rangeHelper = editor.getRangeHelper(),
2558 startSpace = emoticonsCompat &&
2559 rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
2560 endSpace = emoticonsCompat &&
2561 rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
2562 content = createElement('div'),
2563 line = createElement('div'),
2564 perLine = 0,
2565 emoticons = extend(
2566 {},
2567 opts.emoticons.dropdown,
2568 includeMore ? opts.emoticons.more : {}
2569 );
2570
2571 appendChild(content, line);
2572
2573 perLine = Math.sqrt(Object.keys(emoticons).length);
2574
2575 on(content, 'click', 'img', function (e) {
2576 editor.insert(startSpace + attr(this, 'alt') + endSpace,
2577 null, false).closeDropDown(true);
2578
2579 e.preventDefault();
2580 });
2581
2582 each(emoticons, function (code, emoticon) {
2583 appendChild(line, createElement('img', {
2584 src: emoticonsRoot + (emoticon.url || emoticon),
2585 alt: code,
2586 title: emoticon.tooltip || code
2587 }));
2588
2589 if (line.children.length >= perLine) {
2590 line = createElement('div');
2591 appendChild(content, line);
2592 }
2593 });
2594
2595 if (!includeMore && opts.emoticons.more) {
2596 moreLink = createElement('a', {
2597 className: 'sceditor-more'
2598 });
2599
2600 appendChild(moreLink,
2601 document.createTextNode(editor._('More')));
2602
2603 on(moreLink, 'click', function (e) {
2604 editor.createDropDown(
2605 caller, 'more-emoticons', createContent(true)
2606 );
2607
2608 e.preventDefault();
2609 });
2610
2611 appendChild(content, moreLink);
2612 }
2613
2614 return content;
2615 };
2616
2617 editor.createDropDown(caller, 'emoticons', createContent(false));
2618 },
2619 txtExec: function (caller) {
2620 defaultCmds.emoticon.exec.call(this, caller);
2621 },
2622 tooltip: 'Insert an emoticon'
2623 },
2624 // END_COMMAND
2625
2626 // START_COMMAND: YouTube
2627 youtube: {
2628 _dropDown: function (editor, caller, callback) {
2629 var content = createElement('div');
2630
2631 appendChild(content, _tmpl('youtubeMenu', {
2632 label: editor._('Video URL:'),
2633 insert: editor._('Insert')
2634 }, true));
2635
2636 on(content, 'click', '.button', function (e) {
2637 var val = find(content, '#link')[0].value;
2638 var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)?([a-zA-Z0-9_-]{11})/);
2639 var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/);
2640 var time = 0;
2641
2642 if (timeMatch) {
2643 each(timeMatch[1].split(/[hms]/), function (i, val) {
2644 if (val !== '') {
2645 time = (time * 60) + Number(val);
2646 }
2647 });
2648 }
2649
2650 if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) {
2651 callback(idMatch[1], time);
2652 }
2653
2654 editor.closeDropDown(true);
2655 e.preventDefault();
2656 });
2657
2658 editor.createDropDown(caller, 'insertlink', content);
2659 },
2660 exec: function (btn) {
2661 var editor = this;
2662
2663 defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
2664 editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
2665 id: id,
2666 time: time
2667 }));
2668 });
2669 },
2670 tooltip: 'Insert a YouTube video'
2671 },
2672 // END_COMMAND
2673
2674 // START_COMMAND: Date
2675 date: {
2676 _date: function (editor) {
2677 var now = new Date(),
2678 year = now.getYear(),
2679 month = now.getMonth() + 1,
2680 day = now.getDate();
2681
2682 if (year < 2000) {
2683 year = 1900 + year;
2684 }
2685
2686 if (month < 10) {
2687 month = '0' + month;
2688 }
2689
2690 if (day < 10) {
2691 day = '0' + day;
2692 }
2693
2694 return editor.opts.dateFormat
2695 .replace(/year/i, year)
2696 .replace(/month/i, month)
2697 .replace(/day/i, day);
2698 },
2699 exec: function () {
2700 this.insertText(defaultCmds.date._date(this));
2701 },
2702 txtExec: function () {
2703 this.insertText(defaultCmds.date._date(this));
2704 },
2705 tooltip: 'Insert current date'
2706 },
2707 // END_COMMAND
2708
2709 // START_COMMAND: Time
2710 time: {
2711 _time: function () {
2712 var now = new Date(),
2713 hours = now.getHours(),
2714 mins = now.getMinutes(),
2715 secs = now.getSeconds();
2716
2717 if (hours < 10) {
2718 hours = '0' + hours;
2719 }
2720
2721 if (mins < 10) {
2722 mins = '0' + mins;
2723 }
2724
2725 if (secs < 10) {
2726 secs = '0' + secs;
2727 }
2728
2729 return hours + ':' + mins + ':' + secs;
2730 },
2731 exec: function () {
2732 this.insertText(defaultCmds.time._time());
2733 },
2734 txtExec: function () {
2735 this.insertText(defaultCmds.time._time());
2736 },
2737 tooltip: 'Insert current time'
2738 },
2739 // END_COMMAND
2740
2741
2742 // START_COMMAND: Ltr
2743 ltr: {
2744 state: function (parents, firstBlock) {
2745 return firstBlock && firstBlock.style.direction === 'ltr';
2746 },
2747 exec: function () {
2748 var editor = this,
2749 rangeHelper = editor.getRangeHelper(),
2750 node = rangeHelper.getFirstBlockParent();
2751
2752 editor.focus();
2753
2754 if (!node || is(node, 'body')) {
2755 editor.execCommand('formatBlock', 'p');
2756
2757 node = rangeHelper.getFirstBlockParent();
2758
2759 if (!node || is(node, 'body')) {
2760 return;
2761 }
2762 }
2763
2764 var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr';
2765 css(node, 'direction', toggleValue);
2766 },
2767 tooltip: 'Left-to-Right'
2768 },
2769 // END_COMMAND
2770
2771 // START_COMMAND: Rtl
2772 rtl: {
2773 state: function (parents, firstBlock) {
2774 return firstBlock && firstBlock.style.direction === 'rtl';
2775 },
2776 exec: function () {
2777 var editor = this,
2778 rangeHelper = editor.getRangeHelper(),
2779 node = rangeHelper.getFirstBlockParent();
2780
2781 editor.focus();
2782
2783 if (!node || is(node, 'body')) {
2784 editor.execCommand('formatBlock', 'p');
2785
2786 node = rangeHelper.getFirstBlockParent();
2787
2788 if (!node || is(node, 'body')) {
2789 return;
2790 }
2791 }
2792
2793 var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl';
2794 css(node, 'direction', toggleValue);
2795 },
2796 tooltip: 'Right-to-Left'
2797 },
2798 // END_COMMAND
2799
2800
2801 // START_COMMAND: Print
2802 print: {
2803 exec: 'print',
2804 tooltip: 'Print'
2805 },
2806 // END_COMMAND
2807
2808 // START_COMMAND: Maximize
2809 maximize: {
2810 state: function () {
2811 return this.maximize();
2812 },
2813 exec: function () {
2814 this.maximize(!this.maximize());
2815 this.focus();
2816 },
2817 txtExec: function () {
2818 this.maximize(!this.maximize());
2819 this.focus();
2820 },
2821 tooltip: 'Maximize',
2822 shortcut: 'Ctrl+Shift+M'
2823 },
2824 // END_COMMAND
2825
2826 // START_COMMAND: Source
2827 source: {
2828 state: function () {
2829 return this.sourceMode();
2830 },
2831 exec: function () {
2832 this.toggleSourceMode();
2833 this.focus();
2834 },
2835 txtExec: function () {
2836 this.toggleSourceMode();
2837 this.focus();
2838 },
2839 tooltip: 'View source',
2840 shortcut: 'Ctrl+Shift+S'
2841 },
2842 // END_COMMAND
2843
2844 // this is here so that commands above can be removed
2845 // without having to remove the , after the last one.
2846 // Needed for IE.
2847 ignore: {}
2848 };
2849
2850 var plugins = {};
2851
2852 /**
2853 * Plugin Manager class
2854 * @class PluginManager
2855 * @name PluginManager
2856 */
2857 function PluginManager(thisObj) {
2858 /**
2859 * Alias of this
2860 *
2861 * @private
2862 * @type {Object}
2863 */
2864 var base = this;
2865
2866 /**
2867 * Array of all currently registered plugins
2868 *
2869 * @type {Array}
2870 * @private
2871 */
2872 var registeredPlugins = [];
2873
2874
2875 /**
2876 * Changes a signals name from "name" into "signalName".
2877 *
2878 * @param {string} signal
2879 * @return {string}
2880 * @private
2881 */
2882 var formatSignalName = function (signal) {
2883 return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
2884 };
2885
2886 /**
2887 * Calls handlers for a signal
2888 *
2889 * @see call()
2890 * @see callOnlyFirst()
2891 * @param {Array} args
2892 * @param {boolean} returnAtFirst
2893 * @return {*}
2894 * @private
2895 */
2896 var callHandlers = function (args, returnAtFirst) {
2897 args = [].slice.call(args);
2898
2899 var idx, ret,
2900 signal = formatSignalName(args.shift());
2901
2902 for (idx = 0; idx < registeredPlugins.length; idx++) {
2903 if (signal in registeredPlugins[idx]) {
2904 ret = registeredPlugins[idx][signal].apply(thisObj, args);
2905
2906 if (returnAtFirst) {
2907 return ret;
2908 }
2909 }
2910 }
2911 };
2912
2913 /**
2914 * Calls all handlers for the passed signal
2915 *
2916 * @param {string} signal
2917 * @param {...string} args
2918 * @function
2919 * @name call
2920 * @memberOf PluginManager.prototype
2921 */
2922 base.call = function () {
2923 callHandlers(arguments, false);
2924 };
2925
2926 /**
2927 * Calls the first handler for a signal, and returns the
2928 *
2929 * @param {string} signal
2930 * @param {...string} args
2931 * @return {*} The result of calling the handler
2932 * @function
2933 * @name callOnlyFirst
2934 * @memberOf PluginManager.prototype
2935 */
2936 base.callOnlyFirst = function () {
2937 return callHandlers(arguments, true);
2938 };
2939
2940 /**
2941 * Checks if a signal has a handler
2942 *
2943 * @param {string} signal
2944 * @return {boolean}
2945 * @function
2946 * @name hasHandler
2947 * @memberOf PluginManager.prototype
2948 */
2949 base.hasHandler = function (signal) {
2950 var i = registeredPlugins.length;
2951 signal = formatSignalName(signal);
2952
2953 while (i--) {
2954 if (signal in registeredPlugins[i]) {
2955 return true;
2956 }
2957 }
2958
2959 return false;
2960 };
2961
2962 /**
2963 * Checks if the plugin exists in plugins
2964 *
2965 * @param {string} plugin
2966 * @return {boolean}
2967 * @function
2968 * @name exists
2969 * @memberOf PluginManager.prototype
2970 */
2971 base.exists = function (plugin) {
2972 if (plugin in plugins) {
2973 plugin = plugins[plugin];
2974
2975 return typeof plugin === 'function' &&
2976 typeof plugin.prototype === 'object';
2977 }
2978
2979 return false;
2980 };
2981
2982 /**
2983 * Checks if the passed plugin is currently registered.
2984 *
2985 * @param {string} plugin
2986 * @return {boolean}
2987 * @function
2988 * @name isRegistered
2989 * @memberOf PluginManager.prototype
2990 */
2991 base.isRegistered = function (plugin) {
2992 if (base.exists(plugin)) {
2993 var idx = registeredPlugins.length;
2994
2995 while (idx--) {
2996 if (registeredPlugins[idx] instanceof plugins[plugin]) {
2997 return true;
2998 }
2999 }
3000 }
3001
3002 return false;
3003 };
3004
3005 /**
3006 * Registers a plugin to receive signals
3007 *
3008 * @param {string} plugin
3009 * @return {boolean}
3010 * @function
3011 * @name register
3012 * @memberOf PluginManager.prototype
3013 */
3014 base.register = function (plugin) {
3015 if (!base.exists(plugin) || base.isRegistered(plugin)) {
3016 return false;
3017 }
3018
3019 plugin = new plugins[plugin]();
3020 registeredPlugins.push(plugin);
3021
3022 if ('init' in plugin) {
3023 plugin.init.call(thisObj);
3024 }
3025
3026 return true;
3027 };
3028
3029 /**
3030 * Deregisters a plugin.
3031 *
3032 * @param {string} plugin
3033 * @return {boolean}
3034 * @function
3035 * @name deregister
3036 * @memberOf PluginManager.prototype
3037 */
3038 base.deregister = function (plugin) {
3039 var removedPlugin,
3040 pluginIdx = registeredPlugins.length,
3041 removed = false;
3042
3043 if (!base.isRegistered(plugin)) {
3044 return removed;
3045 }
3046
3047 while (pluginIdx--) {
3048 if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) {
3049 removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0];
3050 removed = true;
3051
3052 if ('destroy' in removedPlugin) {
3053 removedPlugin.destroy.call(thisObj);
3054 }
3055 }
3056 }
3057
3058 return removed;
3059 };
3060
3061 /**
3062 * Clears all plugins and removes the owner reference.
3063 *
3064 * Calling any functions on this object after calling
3065 * destroy will cause a JS error.
3066 *
3067 * @name destroy
3068 * @memberOf PluginManager.prototype
3069 */
3070 base.destroy = function () {
3071 var i = registeredPlugins.length;
3072
3073 while (i--) {
3074 if ('destroy' in registeredPlugins[i]) {
3075 registeredPlugins[i].destroy.call(thisObj);
3076 }
3077 }
3078
3079 registeredPlugins = [];
3080 thisObj = null;
3081 };
3082 }
3083 PluginManager.plugins = plugins;
3084
3085 /**
3086 * Gets the text, start/end node and offset for
3087 * length chars left or right of the passed node
3088 * at the specified offset.
3089 *
3090 * @param {Node} node
3091 * @param {number} offset
3092 * @param {boolean} isLeft
3093 * @param {number} length
3094 * @return {Object}
3095 * @private
3096 */
3097 var outerText = function (range, isLeft, length) {
3098 var nodeValue, remaining, start, end, node,
3099 text = '',
3100 next = range.startContainer,
3101 offset = range.startOffset;
3102
3103 // Handle cases where node is a paragraph and offset
3104 // refers to the index of a text node.
3105 // 3 = text node
3106 if (next && next.nodeType !== 3) {
3107 next = next.childNodes[offset];
3108 offset = 0;
3109 }
3110
3111 start = end = offset;
3112
3113 while (length > text.length && next && next.nodeType === 3) {
3114 nodeValue = next.nodeValue;
3115 remaining = length - text.length;
3116
3117 // If not the first node, start and end should be at their
3118 // max values as will be updated when getting the text
3119 if (node) {
3120 end = nodeValue.length;
3121 start = 0;
3122 }
3123
3124 node = next;
3125
3126 if (isLeft) {
3127 start = Math.max(end - remaining, 0);
3128 offset = start;
3129
3130 text = nodeValue.substr(start, end - start) + text;
3131 next = node.previousSibling;
3132 } else {
3133 end = Math.min(remaining, nodeValue.length);
3134 offset = start + end;
3135
3136 text += nodeValue.substr(start, end);
3137 next = node.nextSibling;
3138 }
3139 }
3140
3141 return {
3142 node: node || next,
3143 offset: offset,
3144 text: text
3145 };
3146 };
3147
3148 /**
3149 * Range helper
3150 *
3151 * @class RangeHelper
3152 * @name RangeHelper
3153 */
3154 function RangeHelper(win, d, sanitize) {
3155 var _createMarker, _prepareInput,
3156 doc = d || win.contentDocument || win.document,
3157 startMarker = 'sceditor-start-marker',
3158 endMarker = 'sceditor-end-marker',
3159 base = this;
3160
3161 /**
3162 * Inserts HTML into the current range replacing any selected
3163 * text.
3164 *
3165 * If endHTML is specified the selected contents will be put between
3166 * html and endHTML. If there is nothing selected html and endHTML are
3167 * just concatenate together.
3168 *
3169 * @param {string} html
3170 * @param {string} [endHTML]
3171 * @return False on fail
3172 * @function
3173 * @name insertHTML
3174 * @memberOf RangeHelper.prototype
3175 */
3176 base.insertHTML = function (html, endHTML) {
3177 var node, div,
3178 range = base.selectedRange();
3179
3180 if (!range) {
3181 return false;
3182 }
3183
3184 if (endHTML) {
3185 html += base.selectedHtml() + endHTML;
3186 }
3187
3188 div = createElement('p', {}, doc);
3189 node = doc.createDocumentFragment();
3190 div.innerHTML = sanitize(html);
3191
3192 while (div.firstChild) {
3193 appendChild(node, div.firstChild);
3194 }
3195
3196 base.insertNode(node);
3197 };
3198
3199 /**
3200 * Prepares HTML to be inserted by adding a zero width space
3201 * if the last child is empty and adding the range start/end
3202 * markers to the last child.
3203 *
3204 * @param {Node|string} node
3205 * @param {Node|string} [endNode]
3206 * @param {boolean} [returnHtml]
3207 * @return {Node|string}
3208 * @private
3209 */
3210 _prepareInput = function (node, endNode, returnHtml) {
3211 var lastChild,
3212 frag = doc.createDocumentFragment();
3213
3214 if (typeof node === 'string') {
3215 if (endNode) {
3216 node += base.selectedHtml() + endNode;
3217 }
3218
3219 frag = parseHTML(node);
3220 } else {
3221 appendChild(frag, node);
3222
3223 if (endNode) {
3224 appendChild(frag, base.selectedRange().extractContents());
3225 appendChild(frag, endNode);
3226 }
3227 }
3228
3229 if (!(lastChild = frag.lastChild)) {
3230 return;
3231 }
3232
3233 while (!isInline(lastChild.lastChild, true)) {
3234 lastChild = lastChild.lastChild;
3235 }
3236
3237 if (canHaveChildren(lastChild)) {
3238 // Webkit won't allow the cursor to be placed inside an
3239 // empty tag, so add a zero width space to it.
3240 if (!lastChild.lastChild) {
3241 appendChild(lastChild, document.createTextNode('\u200B'));
3242 }
3243 } else {
3244 lastChild = frag;
3245 }
3246
3247 base.removeMarkers();
3248
3249 // Append marks to last child so when restored cursor will be in
3250 // the right place
3251 appendChild(lastChild, _createMarker(startMarker));
3252 appendChild(lastChild, _createMarker(endMarker));
3253
3254 if (returnHtml) {
3255 var div = createElement('div');
3256 appendChild(div, frag);
3257
3258 return div.innerHTML;
3259 }
3260
3261 return frag;
3262 };
3263
3264 /**
3265 * The same as insertHTML except with DOM nodes instead
3266 *
3267 * <strong>Warning:</strong> the nodes must belong to the
3268 * document they are being inserted into. Some browsers
3269 * will throw exceptions if they don't.
3270 *
3271 * Returns boolean false on fail
3272 *
3273 * @param {Node} node
3274 * @param {Node} endNode
3275 * @return {false|undefined}
3276 * @function
3277 * @name insertNode
3278 * @memberOf RangeHelper.prototype
3279 */
3280 base.insertNode = function (node, endNode) {
3281 var first, last,
3282 input = _prepareInput(node, endNode),
3283 range = base.selectedRange(),
3284 parent = range.commonAncestorContainer,
3285 emptyNodes = [];
3286
3287 if (!input) {
3288 return false;
3289 }
3290
3291 function removeIfEmpty(node) {
3292 // Only remove empty node if it wasn't already empty
3293 if (node && isEmpty(node) && emptyNodes.indexOf(node) < 0) {
3294 remove(node);
3295 }
3296 }
3297
3298 if (range.startContainer !== range.endContainer) {
3299 each(parent.childNodes, function (_, node) {
3300 if (isEmpty(node)) {
3301 emptyNodes.push(node);
3302 }
3303 });
3304
3305 first = input.firstChild;
3306 last = input.lastChild;
3307 }
3308
3309 range.deleteContents();
3310
3311 // FF allows <br /> to be selected but inserting a node
3312 // into <br /> will cause it not to be displayed so must
3313 // insert before the <br /> in FF.
3314 // 3 = TextNode
3315 if (parent && parent.nodeType !== 3 && !canHaveChildren(parent)) {
3316 insertBefore(input, parent);
3317 } else {
3318 range.insertNode(input);
3319
3320 // If a node was split or its contents deleted, remove any resulting
3321 // empty tags. For example:
3322 // <p>|test</p><div>test|</div>
3323 // When deleteContents could become:
3324 // <p></p>|<div></div>
3325 // So remove the empty ones
3326 removeIfEmpty(first && first.previousSibling);
3327 removeIfEmpty(last && last.nextSibling);
3328 }
3329
3330 base.restoreRange();
3331 };
3332
3333 /**
3334 * Clones the selected Range
3335 *
3336 * @return {Range}
3337 * @function
3338 * @name cloneSelected
3339 * @memberOf RangeHelper.prototype
3340 */
3341 base.cloneSelected = function () {
3342 var range = base.selectedRange();
3343
3344 if (range) {
3345 return range.cloneRange();
3346 }
3347 };
3348
3349 /**
3350 * Gets the selected Range
3351 *
3352 * @return {Range}
3353 * @function
3354 * @name selectedRange
3355 * @memberOf RangeHelper.prototype
3356 */
3357 base.selectedRange = function () {
3358 var range, firstChild,
3359 sel = win.getSelection();
3360
3361 if (!sel) {
3362 return;
3363 }
3364
3365 // When creating a new range, set the start to the first child
3366 // element of the body element to avoid errors in FF.
3367 if (sel.rangeCount <= 0) {
3368 firstChild = doc.body;
3369 while (firstChild.firstChild) {
3370 firstChild = firstChild.firstChild;
3371 }
3372
3373 range = doc.createRange();
3374 // Must be setStartBefore otherwise it can cause infinite
3375 // loops with lists in WebKit. See issue 442
3376 range.setStartBefore(firstChild);
3377
3378 sel.addRange(range);
3379 }
3380
3381 if (sel.rangeCount > 0) {
3382 range = sel.getRangeAt(0);
3383 }
3384
3385 return range;
3386 };
3387
3388 /**
3389 * Gets if there is currently a selection
3390 *
3391 * @return {boolean}
3392 * @function
3393 * @name hasSelection
3394 * @since 1.4.4
3395 * @memberOf RangeHelper.prototype
3396 */
3397 base.hasSelection = function () {
3398 var sel = win.getSelection();
3399
3400 return sel && sel.rangeCount > 0;
3401 };
3402
3403 /**
3404 * Gets the currently selected HTML
3405 *
3406 * @return {string}
3407 * @function
3408 * @name selectedHtml
3409 * @memberOf RangeHelper.prototype
3410 */
3411 base.selectedHtml = function () {
3412 var div,
3413 range = base.selectedRange();
3414
3415 if (range) {
3416 div = createElement('p', {}, doc);
3417 appendChild(div, range.cloneContents());
3418
3419 return div.innerHTML;
3420 }
3421
3422 return '';
3423 };
3424
3425 /**
3426 * Gets the parent node of the selected contents in the range
3427 *
3428 * @return {HTMLElement}
3429 * @function
3430 * @name parentNode
3431 * @memberOf RangeHelper.prototype
3432 */
3433 base.parentNode = function () {
3434 var range = base.selectedRange();
3435
3436 if (range) {
3437 return range.commonAncestorContainer;
3438 }
3439 };
3440
3441 /**
3442 * Gets the first block level parent of the selected
3443 * contents of the range.
3444 *
3445 * @return {HTMLElement}
3446 * @function
3447 * @name getFirstBlockParent
3448 * @memberOf RangeHelper.prototype
3449 */
3450 /**
3451 * Gets the first block level parent of the selected
3452 * contents of the range.
3453 *
3454 * @param {Node} [n] The element to get the first block level parent from
3455 * @return {HTMLElement}
3456 * @function
3457 * @name getFirstBlockParent^2
3458 * @since 1.4.1
3459 * @memberOf RangeHelper.prototype
3460 */
3461 base.getFirstBlockParent = function (node) {
3462 var func = function (elm) {
3463 if (!isInline(elm, true)) {
3464 return elm;
3465 }
3466
3467 elm = elm ? elm.parentNode : null;
3468
3469 return elm ? func(elm) : elm;
3470 };
3471
3472 return func(node || base.parentNode());
3473 };
3474
3475 /**
3476 * Inserts a node at either the start or end of the current selection
3477 *
3478 * @param {Bool} start
3479 * @param {Node} node
3480 * @function
3481 * @name insertNodeAt
3482 * @memberOf RangeHelper.prototype
3483 */
3484 base.insertNodeAt = function (start, node) {
3485 var currentRange = base.selectedRange(),
3486 range = base.cloneSelected();
3487
3488 if (!range) {
3489 return false;
3490 }
3491
3492 range.collapse(start);
3493 range.insertNode(node);
3494
3495 // Reselect the current range.
3496 // Fixes issue with Chrome losing the selection. Issue#82
3497 base.selectRange(currentRange);
3498 };
3499
3500 /**
3501 * Creates a marker node
3502 *
3503 * @param {string} id
3504 * @return {HTMLSpanElement}
3505 * @private
3506 */
3507 _createMarker = function (id) {
3508 base.removeMarker(id);
3509
3510 var marker = createElement('span', {
3511 id: id,
3512 className: 'sceditor-selection sceditor-ignore',
3513 style: 'display:none;line-height:0'
3514 }, doc);
3515
3516 marker.innerHTML = ' ';
3517
3518 return marker;
3519 };
3520
3521 /**
3522 * Inserts start/end markers for the current selection
3523 * which can be used by restoreRange to re-select the
3524 * range.
3525 *
3526 * @memberOf RangeHelper.prototype
3527 * @function
3528 * @name insertMarkers
3529 */
3530 base.insertMarkers = function () {
3531 var currentRange = base.selectedRange();
3532 var startNode = _createMarker(startMarker);
3533
3534 base.removeMarkers();
3535 base.insertNodeAt(true, startNode);
3536
3537 // Fixes issue with end marker sometimes being placed before
3538 // the start marker when the range is collapsed.
3539 if (currentRange && currentRange.collapsed) {
3540 startNode.parentNode.insertBefore(
3541 _createMarker(endMarker), startNode.nextSibling);
3542 } else {
3543 base.insertNodeAt(false, _createMarker(endMarker));
3544 }
3545 };
3546
3547 /**
3548 * Gets the marker with the specified ID
3549 *
3550 * @param {string} id
3551 * @return {Node}
3552 * @function
3553 * @name getMarker
3554 * @memberOf RangeHelper.prototype
3555 */
3556 base.getMarker = function (id) {
3557 return doc.getElementById(id);
3558 };
3559
3560 /**
3561 * Removes the marker with the specified ID
3562 *
3563 * @param {string} id
3564 * @function
3565 * @name removeMarker
3566 * @memberOf RangeHelper.prototype
3567 */
3568 base.removeMarker = function (id) {
3569 var marker = base.getMarker(id);
3570
3571 if (marker) {
3572 remove(marker);
3573 }
3574 };
3575
3576 /**
3577 * Removes the start/end markers
3578 *
3579 * @function
3580 * @name removeMarkers
3581 * @memberOf RangeHelper.prototype
3582 */
3583 base.removeMarkers = function () {
3584 base.removeMarker(startMarker);
3585 base.removeMarker(endMarker);
3586 };
3587
3588 /**
3589 * Saves the current range location. Alias of insertMarkers()
3590 *
3591 * @function
3592 * @name saveRage
3593 * @memberOf RangeHelper.prototype
3594 */
3595 base.saveRange = function () {
3596 base.insertMarkers();
3597 };
3598
3599 /**
3600 * Select the specified range
3601 *
3602 * @param {Range} range
3603 * @function
3604 * @name selectRange
3605 * @memberOf RangeHelper.prototype
3606 */
3607 base.selectRange = function (range) {
3608 var lastChild;
3609 var sel = win.getSelection();
3610 var container = range.endContainer;
3611
3612 // Check if cursor is set after a BR when the BR is the only
3613 // child of the parent. In Firefox this causes a line break
3614 // to occur when something is typed. See issue #321
3615 if (range.collapsed && container &&
3616 !isInline(container, true)) {
3617
3618 lastChild = container.lastChild;
3619 while (lastChild && is(lastChild, '.sceditor-ignore')) {
3620 lastChild = lastChild.previousSibling;
3621 }
3622
3623 if (is(lastChild, 'br')) {
3624 var rng = doc.createRange();
3625 rng.setEndAfter(lastChild);
3626 rng.collapse(false);
3627
3628 if (base.compare(range, rng)) {
3629 range.setStartBefore(lastChild);
3630 range.collapse(true);
3631 }
3632 }
3633 }
3634
3635 if (sel) {
3636 base.clear();
3637 sel.addRange(range);
3638 }
3639 };
3640
3641 /**
3642 * Restores the last range saved by saveRange() or insertMarkers()
3643 *
3644 * @function
3645 * @name restoreRange
3646 * @memberOf RangeHelper.prototype
3647 */
3648 base.restoreRange = function () {
3649 var isCollapsed,
3650 range = base.selectedRange(),
3651 start = base.getMarker(startMarker),
3652 end = base.getMarker(endMarker);
3653
3654 if (!start || !end || !range) {
3655 return false;
3656 }
3657
3658 isCollapsed = start.nextSibling === end;
3659
3660 range = doc.createRange();
3661 range.setStartBefore(start);
3662 range.setEndAfter(end);
3663
3664 if (isCollapsed) {
3665 range.collapse(true);
3666 }
3667
3668 base.selectRange(range);
3669 base.removeMarkers();
3670 };
3671
3672 /**
3673 * Selects the text left and right of the current selection
3674 *
3675 * @param {number} left
3676 * @param {number} right
3677 * @since 1.4.3
3678 * @function
3679 * @name selectOuterText
3680 * @memberOf RangeHelper.prototype
3681 */
3682 base.selectOuterText = function (left, right) {
3683 var start, end,
3684 range = base.cloneSelected();
3685
3686 if (!range) {
3687 return false;
3688 }
3689
3690 range.collapse(false);
3691
3692 start = outerText(range, true, left);
3693 end = outerText(range, false, right);
3694
3695 range.setStart(start.node, start.offset);
3696 range.setEnd(end.node, end.offset);
3697
3698 base.selectRange(range);
3699 };
3700
3701 /**
3702 * Gets the text left or right of the current selection
3703 *
3704 * @param {boolean} before
3705 * @param {number} length
3706 * @return {string}
3707 * @since 1.4.3
3708 * @function
3709 * @name selectOuterText
3710 * @memberOf RangeHelper.prototype
3711 */
3712 base.getOuterText = function (before, length) {
3713 var range = base.cloneSelected();
3714
3715 if (!range) {
3716 return '';
3717 }
3718
3719 range.collapse(!before);
3720
3721 return outerText(range, before, length).text;
3722 };
3723
3724 /**
3725 * Replaces keywords with values based on the current caret position
3726 *
3727 * @param {Array} keywords
3728 * @param {boolean} includeAfter If to include the text after the
3729 * current caret position or just
3730 * text before
3731 * @param {boolean} keywordsSorted If the keywords array is pre
3732 * sorted shortest to longest
3733 * @param {number} longestKeyword Length of the longest keyword
3734 * @param {boolean} requireWhitespace If the key must be surrounded
3735 * by whitespace
3736 * @param {string} keypressChar If this is being called from
3737 * a keypress event, this should be
3738 * set to the pressed character
3739 * @return {boolean}
3740 * @function
3741 * @name replaceKeyword
3742 * @memberOf RangeHelper.prototype
3743 */
3744 // eslint-disable-next-line max-params
3745 base.replaceKeyword = function (
3746 keywords,
3747 includeAfter,
3748 keywordsSorted,
3749 longestKeyword,
3750 requireWhitespace,
3751 keypressChar
3752 ) {
3753 if (!keywordsSorted) {
3754 keywords.sort(function (a, b) {
3755 return a[0].length - b[0].length;
3756 });
3757 }
3758
3759 var outerText, match, matchPos, startIndex,
3760 leftLen, charsLeft, keyword, keywordLen,
3761 whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])',
3762 keywordIdx = keywords.length,
3763 whitespaceLen = requireWhitespace ? 1 : 0,
3764 maxKeyLen = longestKeyword ||
3765 keywords[keywordIdx - 1][0].length;
3766
3767 if (requireWhitespace) {
3768 maxKeyLen++;
3769 }
3770
3771 keypressChar = keypressChar || '';
3772 outerText = base.getOuterText(true, maxKeyLen);
3773 leftLen = outerText.length;
3774 outerText += keypressChar;
3775
3776 if (includeAfter) {
3777 outerText += base.getOuterText(false, maxKeyLen);
3778 }
3779
3780 while (keywordIdx--) {
3781 keyword = keywords[keywordIdx][0];
3782 keywordLen = keyword.length;
3783 startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen);
3784 matchPos = -1;
3785
3786 if (requireWhitespace) {
3787 match = outerText
3788 .substr(startIndex)
3789 .match(new RegExp(whitespaceRegex +
3790 regex(keyword) + whitespaceRegex));
3791
3792 if (match) {
3793 // Add the length of the text that was removed by
3794 // substr() and also add 1 for the whitespace
3795 matchPos = match.index + startIndex + match[1].length;
3796 }
3797 } else {
3798 matchPos = outerText.indexOf(keyword, startIndex);
3799 }
3800
3801 if (matchPos > -1) {
3802 // Make sure the match is between before and
3803 // after, not just entirely in one side or the other
3804 if (matchPos <= leftLen &&
3805 matchPos + keywordLen + whitespaceLen >= leftLen) {
3806 charsLeft = leftLen - matchPos;
3807
3808 // If the keypress char is white space then it should
3809 // not be replaced, only chars that are part of the
3810 // key should be replaced.
3811 base.selectOuterText(
3812 charsLeft,
3813 keywordLen - charsLeft -
3814 (/^\S/.test(keypressChar) ? 1 : 0)
3815 );
3816
3817 base.insertHTML(keywords[keywordIdx][1]);
3818 return true;
3819 }
3820 }
3821 }
3822
3823 return false;
3824 };
3825
3826 /**
3827 * Compares two ranges.
3828 *
3829 * If rangeB is undefined it will be set to
3830 * the current selected range
3831 *
3832 * @param {Range} rngA
3833 * @param {Range} [rngB]
3834 * @return {boolean}
3835 * @function
3836 * @name compare
3837 * @memberOf RangeHelper.prototype
3838 */
3839 base.compare = function (rngA, rngB) {
3840 if (!rngB) {
3841 rngB = base.selectedRange();
3842 }
3843
3844 if (!rngA || !rngB) {
3845 return !rngA && !rngB;
3846 }
3847
3848 return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 &&
3849 rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0;
3850 };
3851
3852 /**
3853 * Removes any current selection
3854 *
3855 * @since 1.4.6
3856 * @function
3857 * @name clear
3858 * @memberOf RangeHelper.prototype
3859 */
3860 base.clear = function () {
3861 var sel = win.getSelection();
3862
3863 if (sel) {
3864 if (sel.removeAllRanges) {
3865 sel.removeAllRanges();
3866 } else if (sel.empty) {
3867 sel.empty();
3868 }
3869 }
3870 };
3871 }
3872
3873 var USER_AGENT = navigator.userAgent;
3874
3875 /**
3876 * Detects if the browser is iOS
3877 *
3878 * Needed to fix iOS specific bugs
3879 *
3880 * @function
3881 * @name ios
3882 * @memberOf jQuery.sceditor
3883 * @type {boolean}
3884 */
3885 var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT);
3886
3887 /**
3888 * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
3889 *
3890 * @function
3891 * @name isWysiwygSupported
3892 * @return {boolean}
3893 */
3894 var isWysiwygSupported = (function () {
3895 var match, isUnsupported;
3896
3897 // IE is the only browser to support documentMode
3898 var ie = !!window.document.documentMode;
3899 var legacyEdge = '-ms-ime-align' in document.documentElement.style;
3900
3901 var div = document.createElement('div');
3902 div.contentEditable = true;
3903
3904 // Check if the contentEditable attribute is supported
3905 if (!('contentEditable' in document.documentElement) ||
3906 div.contentEditable !== 'true') {
3907 return false;
3908 }
3909
3910 // I think blackberry supports contentEditable or will at least
3911 // give a valid value for the contentEditable detection above
3912 // so it isn't included in the below tests.
3913
3914 // I hate having to do UA sniffing but some mobile browsers say they
3915 // support contentediable when it isn't usable, i.e. you can't enter
3916 // text.
3917 // This is the only way I can think of to detect them which is also how
3918 // every other editor I've seen deals with this issue.
3919
3920 // Exclude Opera mobile and mini
3921 isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT);
3922
3923 if (/Android/i.test(USER_AGENT)) {
3924 isUnsupported = true;
3925
3926 if (/Safari/.test(USER_AGENT)) {
3927 // Android browser 534+ supports content editable
3928 // This also matches Chrome which supports content editable too
3929 match = /Safari\/(\d+)/.exec(USER_AGENT);
3930 isUnsupported = (!match || !match[1] ? true : match[1] < 534);
3931 }
3932 }
3933
3934 // The current version of Amazon Silk supports it, older versions didn't
3935 // As it uses webkit like Android, assume it's the same and started
3936 // working at versions >= 534
3937 if (/ Silk\//i.test(USER_AGENT)) {
3938 match = /AppleWebKit\/(\d+)/.exec(USER_AGENT);
3939 isUnsupported = (!match || !match[1] ? true : match[1] < 534);
3940 }
3941
3942 // iOS 5+ supports content editable
3943 if (ios) {
3944 // Block any version <= 4_x(_x)
3945 isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT);
3946 }
3947
3948 // Firefox does support WYSIWYG on mobiles so override
3949 // any previous value if using FF
3950 if (/Firefox/i.test(USER_AGENT)) {
3951 isUnsupported = false;
3952 }
3953
3954 if (/OneBrowser/i.test(USER_AGENT)) {
3955 isUnsupported = false;
3956 }
3957
3958 // UCBrowser works but doesn't give a unique user agent
3959 if (navigator.vendor === 'UCWEB') {
3960 isUnsupported = false;
3961 }
3962
3963 // IE and legacy edge are not supported any more
3964 if (ie || legacyEdge) {
3965 isUnsupported = true;
3966 }
3967
3968 return !isUnsupported;
3969 }());
3970
3971 /**
3972 * Checks all emoticons are surrounded by whitespace and
3973 * replaces any that aren't with with their emoticon code.
3974 *
3975 * @param {HTMLElement} node
3976 * @param {rangeHelper} rangeHelper
3977 * @return {void}
3978 */
3979 function checkWhitespace(node, rangeHelper) {
3980 var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/;
3981 var emoticons = node && find(node, 'img[data-sceditor-emoticon]');
3982
3983 if (!node || !emoticons.length) {
3984 return;
3985 }
3986
3987 for (var i = 0; i < emoticons.length; i++) {
3988 var emoticon = emoticons[i];
3989 var parent = emoticon.parentNode;
3990 var prev = emoticon.previousSibling;
3991 var next = emoticon.nextSibling;
3992
3993 if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) &&
3994 (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) {
3995 continue;
3996 }
3997
3998 var range = rangeHelper.cloneSelected();
3999 var rangeStart = -1;
4000 var rangeStartContainer = range.startContainer;
4001 var previousText = prev.nodeValue || '';
4002
4003 previousText += data(emoticon, 'sceditor-emoticon');
4004
4005 // If the cursor is after the removed emoticon, add
4006 // the length of the newly added text to it
4007 if (rangeStartContainer === next) {
4008 rangeStart = previousText.length + range.startOffset;
4009 }
4010
4011 // If the cursor is set before the next node, set it to
4012 // the end of the new text node
4013 if (rangeStartContainer === node &&
4014 node.childNodes[range.startOffset] === next) {
4015 rangeStart = previousText.length;
4016 }
4017
4018 // If the cursor is set before the removed emoticon,
4019 // just keep it at that position
4020 if (rangeStartContainer === prev) {
4021 rangeStart = range.startOffset;
4022 }
4023
4024 if (!next || next.nodeType !== TEXT_NODE) {
4025 next = parent.insertBefore(
4026 parent.ownerDocument.createTextNode(''), next
4027 );
4028 }
4029
4030 next.insertData(0, previousText);
4031 remove(prev);
4032 remove(emoticon);
4033
4034 // Need to update the range starting position if it's been modified
4035 if (rangeStart > -1) {
4036 range.setStart(next, rangeStart);
4037 range.collapse(true);
4038 rangeHelper.selectRange(range);
4039 }
4040 }
4041 }
4042 /**
4043 * Replaces any emoticons inside the root node with images.
4044 *
4045 * emoticons should be an object where the key is the emoticon
4046 * code and the value is the HTML to replace it with.
4047 *
4048 * @param {HTMLElement} root
4049 * @param {Object<string, string>} emoticons
4050 * @param {boolean} emoticonsCompat
4051 * @return {void}
4052 */
4053 function replace(root, emoticons, emoticonsCompat) {
4054 var doc = root.ownerDocument;
4055 var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)';
4056 var emoticonCodes = [];
4057 var emoticonRegex = {};
4058
4059 // TODO: Make this tag configurable.
4060 if (parent(root, 'code')) {
4061 return;
4062 }
4063
4064 each(emoticons, function (key) {
4065 emoticonRegex[key] = new RegExp(space + regex(key) + space);
4066 emoticonCodes.push(key);
4067 });
4068
4069 // Sort keys longest to shortest so that longer keys
4070 // take precedence (avoids bugs with shorter keys partially
4071 // matching longer ones)
4072 emoticonCodes.sort(function (a, b) {
4073 return b.length - a.length;
4074 });
4075
4076 (function convert(node) {
4077 node = node.firstChild;
4078
4079 while (node) {
4080 // TODO: Make this tag configurable.
4081 if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) {
4082 convert(node);
4083 }
4084
4085 if (node.nodeType === TEXT_NODE) {
4086 for (var i = 0; i < emoticonCodes.length; i++) {
4087 var text = node.nodeValue;
4088 var key = emoticonCodes[i];
4089 var index = emoticonsCompat ?
4090 text.search(emoticonRegex[key]) :
4091 text.indexOf(key);
4092
4093 if (index > -1) {
4094 // When emoticonsCompat is enabled this will be the
4095 // position after any white space
4096 var startIndex = text.indexOf(key, index);
4097 var fragment = parseHTML(emoticons[key], doc);
4098 var after = text.substr(startIndex + key.length);
4099
4100 fragment.appendChild(doc.createTextNode(after));
4101
4102 node.nodeValue = text.substr(0, startIndex);
4103 node.parentNode
4104 .insertBefore(fragment, node.nextSibling);
4105 }
4106 }
4107 }
4108
4109 node = node.nextSibling;
4110 }
4111 }(root));
4112 }
4113
4114 /*! @license DOMPurify | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.2.2/LICENSE */
4115
4116 function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
4117
4118 var hasOwnProperty = Object.hasOwnProperty,
4119 setPrototypeOf = Object.setPrototypeOf,
4120 isFrozen = Object.isFrozen,
4121 getPrototypeOf = Object.getPrototypeOf,
4122 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
4123 var freeze = Object.freeze,
4124 seal = Object.seal,
4125 create = Object.create; // eslint-disable-line import/no-mutable-exports
4126
4127 var _ref = typeof Reflect !== 'undefined' && Reflect,
4128 apply = _ref.apply,
4129 construct = _ref.construct;
4130
4131 if (!apply) {
4132 apply = function apply(fun, thisValue, args) {
4133 return fun.apply(thisValue, args);
4134 };
4135 }
4136
4137 if (!freeze) {
4138 freeze = function freeze(x) {
4139 return x;
4140 };
4141 }
4142
4143 if (!seal) {
4144 seal = function seal(x) {
4145 return x;
4146 };
4147 }
4148
4149 if (!construct) {
4150 construct = function construct(Func, args) {
4151 return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))();
4152 };
4153 }
4154
4155 var arrayForEach = unapply(Array.prototype.forEach);
4156 var arrayPop = unapply(Array.prototype.pop);
4157 var arrayPush = unapply(Array.prototype.push);
4158
4159 var stringToLowerCase = unapply(String.prototype.toLowerCase);
4160 var stringMatch = unapply(String.prototype.match);
4161 var stringReplace = unapply(String.prototype.replace);
4162 var stringIndexOf = unapply(String.prototype.indexOf);
4163 var stringTrim = unapply(String.prototype.trim);
4164
4165 var regExpTest = unapply(RegExp.prototype.test);
4166
4167 var typeErrorCreate = unconstruct(TypeError);
4168
4169 function unapply(func) {
4170 return function (thisArg) {
4171 for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
4172 args[_key - 1] = arguments[_key];
4173 }
4174
4175 return apply(func, thisArg, args);
4176 };
4177 }
4178
4179 function unconstruct(func) {
4180 return function () {
4181 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
4182 args[_key2] = arguments[_key2];
4183 }
4184
4185 return construct(func, args);
4186 };
4187 }
4188
4189 /* Add properties to a lookup table */
4190 function addToSet(set, array) {
4191 if (setPrototypeOf) {
4192 // Make 'in' and truthy checks like Boolean(set.constructor)
4193 // independent of any properties defined on Object.prototype.
4194 // Prevent prototype setters from intercepting set as a this value.
4195 setPrototypeOf(set, null);
4196 }
4197
4198 var l = array.length;
4199 while (l--) {
4200 var element = array[l];
4201 if (typeof element === 'string') {
4202 var lcElement = stringToLowerCase(element);
4203 if (lcElement !== element) {
4204 // Config presets (e.g. tags.js, attrs.js) are immutable.
4205 if (!isFrozen(array)) {
4206 array[l] = lcElement;
4207 }
4208
4209 element = lcElement;
4210 }
4211 }
4212
4213 set[element] = true;
4214 }
4215
4216 return set;
4217 }
4218
4219 /* Shallow clone an object */
4220 function clone(object) {
4221 var newObject = create(null);
4222
4223 var property = void 0;
4224 for (property in object) {
4225 if (apply(hasOwnProperty, object, [property])) {
4226 newObject[property] = object[property];
4227 }
4228 }
4229
4230 return newObject;
4231 }
4232
4233 /* IE10 doesn't support __lookupGetter__ so lets'
4234 * simulate it. It also automatically checks
4235 * if the prop is function or getter and behaves
4236 * accordingly. */
4237 function lookupGetter(object, prop) {
4238 while (object !== null) {
4239 var desc = getOwnPropertyDescriptor(object, prop);
4240 if (desc) {
4241 if (desc.get) {
4242 return unapply(desc.get);
4243 }
4244
4245 if (typeof desc.value === 'function') {
4246 return unapply(desc.value);
4247 }
4248 }
4249
4250 object = getPrototypeOf(object);
4251 }
4252
4253 return null;
4254 }
4255
4256 var html = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
4257
4258 // SVG
4259 var svg = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
4260
4261 var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
4262
4263 // List of SVG elements that are disallowed by default.
4264 // We still need to know them so that we can do namespace
4265 // checks properly in case one wants to add them to
4266 // allow-list.
4267 var svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', 'feimage', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);
4268
4269 var mathMl = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover']);
4270
4271 // Similarly to SVG, we want to know all MathML elements,
4272 // even those that we disallow by default.
4273 var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
4274
4275 var text = freeze(['#text']);
4276
4277 var html$1 = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns']);
4278
4279 var svg$1 = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
4280
4281 var mathMl$1 = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
4282
4283 var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
4284
4285 // eslint-disable-next-line unicorn/better-regex
4286 var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
4287 var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm);
4288 var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
4289 var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
4290 var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
4291 );
4292 var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
4293 var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
4294 );
4295
4296 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
4297
4298 function _toConsumableArray$1(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
4299
4300 var getGlobal = function getGlobal() {
4301 return typeof window === 'undefined' ? null : window;
4302 };
4303
4304 /**
4305 * Creates a no-op policy for internal use only.
4306 * Don't export this function outside this module!
4307 * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory.
4308 * @param {Document} document The document object (to determine policy name suffix)
4309 * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types
4310 * are not supported).
4311 */
4312 var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) {
4313 if ((typeof trustedTypes === 'undefined' ? 'undefined' : _typeof(trustedTypes)) !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
4314 return null;
4315 }
4316
4317 // Allow the callers to control the unique policy name
4318 // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
4319 // Policy creation with duplicate names throws in Trusted Types.
4320 var suffix = null;
4321 var ATTR_NAME = 'data-tt-policy-suffix';
4322 if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) {
4323 suffix = document.currentScript.getAttribute(ATTR_NAME);
4324 }
4325
4326 var policyName = 'dompurify' + (suffix ? '#' + suffix : '');
4327
4328 try {
4329 return trustedTypes.createPolicy(policyName, {
4330 createHTML: function createHTML(html$$1) {
4331 return html$$1;
4332 }
4333 });
4334 } catch (_) {
4335 // Policy creation failed (most likely another DOMPurify script has
4336 // already run). Skip creating the policy, as this will only cause errors
4337 // if TT are enforced.
4338 console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
4339 return null;
4340 }
4341 };
4342
4343 function createDOMPurify() {
4344 var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
4345
4346 var DOMPurify = function DOMPurify(root) {
4347 return createDOMPurify(root);
4348 };
4349
4350 /**
4351 * Version label, exposed for easier checks
4352 * if DOMPurify is up to date or not
4353 */
4354 DOMPurify.version = '2.2.6';
4355
4356 /**
4357 * Array of elements that DOMPurify removed during sanitation.
4358 * Empty if nothing was removed.
4359 */
4360 DOMPurify.removed = [];
4361
4362 if (!window || !window.document || window.document.nodeType !== 9) {
4363 // Not running in a browser, provide a factory function
4364 // so that you can pass your own Window
4365 DOMPurify.isSupported = false;
4366
4367 return DOMPurify;
4368 }
4369
4370 var originalDocument = window.document;
4371
4372 var document = window.document;
4373 var DocumentFragment = window.DocumentFragment,
4374 HTMLTemplateElement = window.HTMLTemplateElement,
4375 Node = window.Node,
4376 Element = window.Element,
4377 NodeFilter = window.NodeFilter,
4378 _window$NamedNodeMap = window.NamedNodeMap,
4379 NamedNodeMap = _window$NamedNodeMap === undefined ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
4380 Text = window.Text,
4381 Comment = window.Comment,
4382 DOMParser = window.DOMParser,
4383 trustedTypes = window.trustedTypes;
4384
4385
4386 var ElementPrototype = Element.prototype;
4387
4388 var cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
4389 var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
4390 var getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
4391 var getParentNode = lookupGetter(ElementPrototype, 'parentNode');
4392
4393 // As per issue #47, the web-components registry is inherited by a
4394 // new document created via createHTMLDocument. As per the spec
4395 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
4396 // a new empty registry is used when creating a template contents owner
4397 // document, so we use that as our parent document to ensure nothing
4398 // is inherited.
4399 if (typeof HTMLTemplateElement === 'function') {
4400 var template = document.createElement('template');
4401 if (template.content && template.content.ownerDocument) {
4402 document = template.content.ownerDocument;
4403 }
4404 }
4405
4406 var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument);
4407 var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML('') : '';
4408
4409 var _document = document,
4410 implementation = _document.implementation,
4411 createNodeIterator = _document.createNodeIterator,
4412 getElementsByTagName = _document.getElementsByTagName,
4413 createDocumentFragment = _document.createDocumentFragment;
4414 var importNode = originalDocument.importNode;
4415
4416
4417 var documentMode = {};
4418 try {
4419 documentMode = clone(document).documentMode ? document.documentMode : {};
4420 } catch (_) {}
4421
4422 var hooks = {};
4423
4424 /**
4425 * Expose whether this browser supports running the full DOMPurify.
4426 */
4427 DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9;
4428
4429 var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR,
4430 ERB_EXPR$$1 = ERB_EXPR,
4431 DATA_ATTR$$1 = DATA_ATTR,
4432 ARIA_ATTR$$1 = ARIA_ATTR,
4433 IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA,
4434 ATTR_WHITESPACE$$1 = ATTR_WHITESPACE;
4435 var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI;
4436
4437 /**
4438 * We consider the elements and attributes below to be safe. Ideally
4439 * don't add any new ones but feel free to remove unwanted ones.
4440 */
4441
4442 /* allowed element names */
4443
4444 var ALLOWED_TAGS = null;
4445 var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text)));
4446
4447 /* Allowed attribute names */
4448 var ALLOWED_ATTR = null;
4449 var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml)));
4450
4451 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
4452 var FORBID_TAGS = null;
4453
4454 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
4455 var FORBID_ATTR = null;
4456
4457 /* Decide if ARIA attributes are okay */
4458 var ALLOW_ARIA_ATTR = true;
4459
4460 /* Decide if custom data attributes are okay */
4461 var ALLOW_DATA_ATTR = true;
4462
4463 /* Decide if unknown protocols are okay */
4464 var ALLOW_UNKNOWN_PROTOCOLS = false;
4465
4466 /* Output should be safe for common template engines.
4467 * This means, DOMPurify removes data attributes, mustaches and ERB
4468 */
4469 var SAFE_FOR_TEMPLATES = false;
4470
4471 /* Decide if document with <html>... should be returned */
4472 var WHOLE_DOCUMENT = false;
4473
4474 /* Track whether config is already set on this instance of DOMPurify. */
4475 var SET_CONFIG = false;
4476
4477 /* Decide if all elements (e.g. style, script) must be children of
4478 * document.body. By default, browsers might move them to document.head */
4479 var FORCE_BODY = false;
4480
4481 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
4482 * string (or a TrustedHTML object if Trusted Types are supported).
4483 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
4484 */
4485 var RETURN_DOM = false;
4486
4487 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
4488 * string (or a TrustedHTML object if Trusted Types are supported) */
4489 var RETURN_DOM_FRAGMENT = false;
4490
4491 /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM
4492 * `Node` is imported into the current `Document`. If this flag is not enabled the
4493 * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by
4494 * DOMPurify.
4495 *
4496 * This defaults to `true` starting DOMPurify 2.2.0. Note that setting it to `false`
4497 * might cause XSS from attacks hidden in closed shadowroots in case the browser
4498 * supports Declarative Shadow: DOM https://web.dev/declarative-shadow-dom/
4499 */
4500 var RETURN_DOM_IMPORT = true;
4501
4502 /* Try to return a Trusted Type object instead of a string, return a string in
4503 * case Trusted Types are not supported */
4504 var RETURN_TRUSTED_TYPE = false;
4505
4506 /* Output should be free from DOM clobbering attacks? */
4507 var SANITIZE_DOM = true;
4508
4509 /* Keep element content when removing element? */
4510 var KEEP_CONTENT = true;
4511
4512 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
4513 * of importing it into a new Document and returning a sanitized copy */
4514 var IN_PLACE = false;
4515
4516 /* Allow usage of profiles like html, svg and mathMl */
4517 var USE_PROFILES = {};
4518
4519 /* Tags to ignore content of when KEEP_CONTENT is true */
4520 var FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
4521
4522 /* Tags that are safe for data: URIs */
4523 var DATA_URI_TAGS = null;
4524 var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
4525
4526 /* Attributes safe for values like "javascript:" */
4527 var URI_SAFE_ATTRIBUTES = null;
4528 var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'summary', 'title', 'value', 'style', 'xmlns']);
4529
4530 /* Keep a reference to config to pass to hooks */
4531 var CONFIG = null;
4532
4533 /* Ideally, do not touch anything below this line */
4534 /* ______________________________________________ */
4535
4536 var formElement = document.createElement('form');
4537
4538 /**
4539 * _parseConfig
4540 *
4541 * @param {Object} cfg optional config literal
4542 */
4543 // eslint-disable-next-line complexity
4544 var _parseConfig = function _parseConfig(cfg) {
4545 if (CONFIG && CONFIG === cfg) {
4546 return;
4547 }
4548
4549 /* Shield configuration object from tampering */
4550 if (!cfg || (typeof cfg === 'undefined' ? 'undefined' : _typeof(cfg)) !== 'object') {
4551 cfg = {};
4552 }
4553
4554 /* Shield configuration object from prototype pollution */
4555 cfg = clone(cfg);
4556
4557 /* Set configuration parameters */
4558 ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
4559 ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;
4560 URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES;
4561 DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS;
4562 FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS) : {};
4563 FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR) : {};
4564 USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;
4565 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
4566 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
4567 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
4568 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
4569 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
4570 RETURN_DOM = cfg.RETURN_DOM || false; // Default false
4571 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
4572 RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; // Default true
4573 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
4574 FORCE_BODY = cfg.FORCE_BODY || false; // Default false
4575 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
4576 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
4577 IN_PLACE = cfg.IN_PLACE || false; // Default false
4578 IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1;
4579 if (SAFE_FOR_TEMPLATES) {
4580 ALLOW_DATA_ATTR = false;
4581 }
4582
4583 if (RETURN_DOM_FRAGMENT) {
4584 RETURN_DOM = true;
4585 }
4586
4587 /* Parse profile info */
4588 if (USE_PROFILES) {
4589 ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text)));
4590 ALLOWED_ATTR = [];
4591 if (USE_PROFILES.html === true) {
4592 addToSet(ALLOWED_TAGS, html);
4593 addToSet(ALLOWED_ATTR, html$1);
4594 }
4595
4596 if (USE_PROFILES.svg === true) {
4597 addToSet(ALLOWED_TAGS, svg);
4598 addToSet(ALLOWED_ATTR, svg$1);
4599 addToSet(ALLOWED_ATTR, xml);
4600 }
4601
4602 if (USE_PROFILES.svgFilters === true) {
4603 addToSet(ALLOWED_TAGS, svgFilters);
4604 addToSet(ALLOWED_ATTR, svg$1);
4605 addToSet(ALLOWED_ATTR, xml);
4606 }
4607
4608 if (USE_PROFILES.mathMl === true) {
4609 addToSet(ALLOWED_TAGS, mathMl);
4610 addToSet(ALLOWED_ATTR, mathMl$1);
4611 addToSet(ALLOWED_ATTR, xml);
4612 }
4613 }
4614
4615 /* Merge configuration parameters */
4616 if (cfg.ADD_TAGS) {
4617 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
4618 ALLOWED_TAGS = clone(ALLOWED_TAGS);
4619 }
4620
4621 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS);
4622 }
4623
4624 if (cfg.ADD_ATTR) {
4625 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
4626 ALLOWED_ATTR = clone(ALLOWED_ATTR);
4627 }
4628
4629 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR);
4630 }
4631
4632 if (cfg.ADD_URI_SAFE_ATTR) {
4633 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR);
4634 }
4635
4636 /* Add #text in case KEEP_CONTENT is set to true */
4637 if (KEEP_CONTENT) {
4638 ALLOWED_TAGS['#text'] = true;
4639 }
4640
4641 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
4642 if (WHOLE_DOCUMENT) {
4643 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
4644 }
4645
4646 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
4647 if (ALLOWED_TAGS.table) {
4648 addToSet(ALLOWED_TAGS, ['tbody']);
4649 delete FORBID_TAGS.tbody;
4650 }
4651
4652 // Prevent further manipulation of configuration.
4653 // Not available in IE8, Safari 5, etc.
4654 if (freeze) {
4655 freeze(cfg);
4656 }
4657
4658 CONFIG = cfg;
4659 };
4660
4661 var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
4662
4663 var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']);
4664
4665 /* Keep track of all possible SVG and MathML tags
4666 * so that we can perform the namespace checks
4667 * correctly. */
4668 var ALL_SVG_TAGS = addToSet({}, svg);
4669 addToSet(ALL_SVG_TAGS, svgFilters);
4670 addToSet(ALL_SVG_TAGS, svgDisallowed);
4671
4672 var ALL_MATHML_TAGS = addToSet({}, mathMl);
4673 addToSet(ALL_MATHML_TAGS, mathMlDisallowed);
4674
4675 var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
4676 var SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
4677 var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
4678
4679 /**
4680 *
4681 *
4682 * @param {Element} element a DOM element whose namespace is being checked
4683 * @returns {boolean} Return false if the element has a
4684 * namespace that a spec-compliant parser would never
4685 * return. Return true otherwise.
4686 */
4687 var _checkValidNamespace = function _checkValidNamespace(element) {
4688 var parent = getParentNode(element);
4689
4690 // In JSDOM, if we're inside shadow DOM, then parentNode
4691 // can be null. We just simulate parent in this case.
4692 if (!parent || !parent.tagName) {
4693 parent = {
4694 namespaceURI: HTML_NAMESPACE,
4695 tagName: 'template'
4696 };
4697 }
4698
4699 var tagName = stringToLowerCase(element.tagName);
4700 var parentTagName = stringToLowerCase(parent.tagName);
4701
4702 if (element.namespaceURI === SVG_NAMESPACE) {
4703 // The only way to switch from HTML namespace to SVG
4704 // is via <svg>. If it happens via any other tag, then
4705 // it should be killed.
4706 if (parent.namespaceURI === HTML_NAMESPACE) {
4707 return tagName === 'svg';
4708 }
4709
4710 // The only way to switch from MathML to SVG is via
4711 // svg if parent is either <annotation-xml> or MathML
4712 // text integration points.
4713 if (parent.namespaceURI === MATHML_NAMESPACE) {
4714 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
4715 }
4716
4717 // We only allow elements that are defined in SVG
4718 // spec. All others are disallowed in SVG namespace.
4719 return Boolean(ALL_SVG_TAGS[tagName]);
4720 }
4721
4722 if (element.namespaceURI === MATHML_NAMESPACE) {
4723 // The only way to switch from HTML namespace to MathML
4724 // is via <math>. If it happens via any other tag, then
4725 // it should be killed.
4726 if (parent.namespaceURI === HTML_NAMESPACE) {
4727 return tagName === 'math';
4728 }
4729
4730 // The only way to switch from SVG to MathML is via
4731 // <math> and HTML integration points
4732 if (parent.namespaceURI === SVG_NAMESPACE) {
4733 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
4734 }
4735
4736 // We only allow elements that are defined in MathML
4737 // spec. All others are disallowed in MathML namespace.
4738 return Boolean(ALL_MATHML_TAGS[tagName]);
4739 }
4740
4741 if (element.namespaceURI === HTML_NAMESPACE) {
4742 // The only way to switch from SVG to HTML is via
4743 // HTML integration points, and from MathML to HTML
4744 // is via MathML text integration points
4745 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
4746 return false;
4747 }
4748
4749 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
4750 return false;
4751 }
4752
4753 // Certain elements are allowed in both SVG and HTML
4754 // namespace. We need to specify them explicitly
4755 // so that they don't get erronously deleted from
4756 // HTML namespace.
4757 var commonSvgAndHTMLElements = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
4758
4759 // We disallow tags that are specific for MathML
4760 // or SVG and should never appear in HTML namespace
4761 return !ALL_MATHML_TAGS[tagName] && (commonSvgAndHTMLElements[tagName] || !ALL_SVG_TAGS[tagName]);
4762 }
4763
4764 // The code should never reach this place (this means
4765 // that the element somehow got namespace that is not
4766 // HTML, SVG or MathML). Return false just in case.
4767 return false;
4768 };
4769
4770 /**
4771 * _forceRemove
4772 *
4773 * @param {Node} node a DOM node
4774 */
4775 var _forceRemove = function _forceRemove(node) {
4776 arrayPush(DOMPurify.removed, { element: node });
4777 try {
4778 node.parentNode.removeChild(node);
4779 } catch (_) {
4780 try {
4781 node.outerHTML = emptyHTML;
4782 } catch (_) {
4783 node.remove();
4784 }
4785 }
4786 };
4787
4788 /**
4789 * _removeAttribute
4790 *
4791 * @param {String} name an Attribute name
4792 * @param {Node} node a DOM node
4793 */
4794 var _removeAttribute = function _removeAttribute(name, node) {
4795 try {
4796 arrayPush(DOMPurify.removed, {
4797 attribute: node.getAttributeNode(name),
4798 from: node
4799 });
4800 } catch (_) {
4801 arrayPush(DOMPurify.removed, {
4802 attribute: null,
4803 from: node
4804 });
4805 }
4806
4807 node.removeAttribute(name);
4808 };
4809
4810 /**
4811 * _initDocument
4812 *
4813 * @param {String} dirty a string of dirty markup
4814 * @return {Document} a DOM, filled with the dirty markup
4815 */
4816 var _initDocument = function _initDocument(dirty) {
4817 /* Create a HTML document */
4818 var doc = void 0;
4819 var leadingWhitespace = void 0;
4820
4821 if (FORCE_BODY) {
4822 dirty = '<remove></remove>' + dirty;
4823 } else {
4824 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
4825 var matches = stringMatch(dirty, /^[\r\n\t ]+/);
4826 leadingWhitespace = matches && matches[0];
4827 }
4828
4829 var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
4830 /* Use the DOMParser API by default, fallback later if needs be */
4831 try {
4832 doc = new DOMParser().parseFromString(dirtyPayload, 'text/html');
4833 } catch (_) {}
4834
4835 /* Use createHTMLDocument in case DOMParser is not available */
4836 if (!doc || !doc.documentElement) {
4837 doc = implementation.createHTMLDocument('');
4838 var _doc = doc,
4839 body = _doc.body;
4840
4841 body.parentNode.removeChild(body.parentNode.firstElementChild);
4842 body.outerHTML = dirtyPayload;
4843 }
4844
4845 if (dirty && leadingWhitespace) {
4846 doc.body.insertBefore(document.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null);
4847 }
4848
4849 /* Work on whole document or just its body */
4850 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
4851 };
4852
4853 /**
4854 * _createIterator
4855 *
4856 * @param {Document} root document/fragment to create iterator for
4857 * @return {Iterator} iterator instance
4858 */
4859 var _createIterator = function _createIterator(root) {
4860 return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function () {
4861 return NodeFilter.FILTER_ACCEPT;
4862 }, false);
4863 };
4864
4865 /**
4866 * _isClobbered
4867 *
4868 * @param {Node} elm element to check for clobbering attacks
4869 * @return {Boolean} true if clobbered, false if safe
4870 */
4871 var _isClobbered = function _isClobbered(elm) {
4872 if (elm instanceof Text || elm instanceof Comment) {
4873 return false;
4874 }
4875
4876 if (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function') {
4877 return true;
4878 }
4879
4880 return false;
4881 };
4882
4883 /**
4884 * _isNode
4885 *
4886 * @param {Node} obj object to check whether it's a DOM node
4887 * @return {Boolean} true is object is a DOM node
4888 */
4889 var _isNode = function _isNode(object) {
4890 return (typeof Node === 'undefined' ? 'undefined' : _typeof(Node)) === 'object' ? object instanceof Node : object && (typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string';
4891 };
4892
4893 /**
4894 * _executeHook
4895 * Execute user configurable hooks
4896 *
4897 * @param {String} entryPoint Name of the hook's entry point
4898 * @param {Node} currentNode node to work on with the hook
4899 * @param {Object} data additional hook parameters
4900 */
4901 var _executeHook = function _executeHook(entryPoint, currentNode, data) {
4902 if (!hooks[entryPoint]) {
4903 return;
4904 }
4905
4906 arrayForEach(hooks[entryPoint], function (hook) {
4907 hook.call(DOMPurify, currentNode, data, CONFIG);
4908 });
4909 };
4910
4911 /**
4912 * _sanitizeElements
4913 *
4914 * @protect nodeName
4915 * @protect textContent
4916 * @protect removeChild
4917 *
4918 * @param {Node} currentNode to check for permission to exist
4919 * @return {Boolean} true if node was killed, false if left alive
4920 */
4921 var _sanitizeElements = function _sanitizeElements(currentNode) {
4922 var content = void 0;
4923
4924 /* Execute a hook if present */
4925 _executeHook('beforeSanitizeElements', currentNode, null);
4926
4927 /* Check if element is clobbered or can clobber */
4928 if (_isClobbered(currentNode)) {
4929 _forceRemove(currentNode);
4930 return true;
4931 }
4932
4933 /* Check if tagname contains Unicode */
4934 if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) {
4935 _forceRemove(currentNode);
4936 return true;
4937 }
4938
4939 /* Now let's check the element's type and name */
4940 var tagName = stringToLowerCase(currentNode.nodeName);
4941
4942 /* Execute a hook if present */
4943 _executeHook('uponSanitizeElement', currentNode, {
4944 tagName: tagName,
4945 allowedTags: ALLOWED_TAGS
4946 });
4947
4948 /* Detect mXSS attempts abusing namespace confusion */
4949 if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
4950 _forceRemove(currentNode);
4951 return true;
4952 }
4953
4954 /* Remove element if anything forbids its presence */
4955 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
4956 /* Keep content except for bad-listed elements */
4957 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
4958 var parentNode = getParentNode(currentNode);
4959 var childNodes = getChildNodes(currentNode);
4960 var childCount = childNodes.length;
4961 for (var i = childCount - 1; i >= 0; --i) {
4962 parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
4963 }
4964 }
4965
4966 _forceRemove(currentNode);
4967 return true;
4968 }
4969
4970 /* Check whether element has a valid namespace */
4971 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
4972 _forceRemove(currentNode);
4973 return true;
4974 }
4975
4976 if ((tagName === 'noscript' || tagName === 'noembed') && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) {
4977 _forceRemove(currentNode);
4978 return true;
4979 }
4980
4981 /* Sanitize element content to be template-safe */
4982 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
4983 /* Get the element's text content */
4984 content = currentNode.textContent;
4985 content = stringReplace(content, MUSTACHE_EXPR$$1, ' ');
4986 content = stringReplace(content, ERB_EXPR$$1, ' ');
4987 if (currentNode.textContent !== content) {
4988 arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
4989 currentNode.textContent = content;
4990 }
4991 }
4992
4993 /* Execute a hook if present */
4994 _executeHook('afterSanitizeElements', currentNode, null);
4995
4996 return false;
4997 };
4998
4999 /**
5000 * _isValidAttribute
5001 *
5002 * @param {string} lcTag Lowercase tag name of containing element.
5003 * @param {string} lcName Lowercase attribute name.
5004 * @param {string} value Attribute value.
5005 * @return {Boolean} Returns true if `value` is valid, otherwise false.
5006 */
5007 // eslint-disable-next-line complexity
5008 var _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
5009 /* Make sure attribute cannot clobber */
5010 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
5011 return false;
5012 }
5013
5014 /* Allow valid data-* attributes: At least one character after "-"
5015 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
5016 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
5017 We don't need to check the value; it's always URI safe. */
5018 if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
5019 return false;
5020
5021 /* Check value is safe. First, is attr inert? If so, is safe */
5022 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))) ; else if (!value) ; else {
5023 return false;
5024 }
5025
5026 return true;
5027 };
5028
5029 /**
5030 * _sanitizeAttributes
5031 *
5032 * @protect attributes
5033 * @protect nodeName
5034 * @protect removeAttribute
5035 * @protect setAttribute
5036 *
5037 * @param {Node} currentNode to sanitize
5038 */
5039 var _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
5040 var attr = void 0;
5041 var value = void 0;
5042 var lcName = void 0;
5043 var l = void 0;
5044 /* Execute a hook if present */
5045 _executeHook('beforeSanitizeAttributes', currentNode, null);
5046
5047 var attributes = currentNode.attributes;
5048
5049 /* Check if we have attributes; if not we might have a text node */
5050
5051 if (!attributes) {
5052 return;
5053 }
5054
5055 var hookEvent = {
5056 attrName: '',
5057 attrValue: '',
5058 keepAttr: true,
5059 allowedAttributes: ALLOWED_ATTR
5060 };
5061 l = attributes.length;
5062
5063 /* Go backwards over all attributes; safely remove bad ones */
5064 while (l--) {
5065 attr = attributes[l];
5066 var _attr = attr,
5067 name = _attr.name,
5068 namespaceURI = _attr.namespaceURI;
5069
5070 value = stringTrim(attr.value);
5071 lcName = stringToLowerCase(name);
5072
5073 /* Execute a hook if present */
5074 hookEvent.attrName = lcName;
5075 hookEvent.attrValue = value;
5076 hookEvent.keepAttr = true;
5077 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
5078 _executeHook('uponSanitizeAttribute', currentNode, hookEvent);
5079 value = hookEvent.attrValue;
5080 /* Did the hooks approve of the attribute? */
5081 if (hookEvent.forceKeepAttr) {
5082 continue;
5083 }
5084
5085 /* Remove attribute */
5086 _removeAttribute(name, currentNode);
5087
5088 /* Did the hooks approve of the attribute? */
5089 if (!hookEvent.keepAttr) {
5090 continue;
5091 }
5092
5093 /* Work around a security issue in jQuery 3.0 */
5094 if (regExpTest(/\/>/i, value)) {
5095 _removeAttribute(name, currentNode);
5096 continue;
5097 }
5098
5099 /* Sanitize attribute content to be template-safe */
5100 if (SAFE_FOR_TEMPLATES) {
5101 value = stringReplace(value, MUSTACHE_EXPR$$1, ' ');
5102 value = stringReplace(value, ERB_EXPR$$1, ' ');
5103 }
5104
5105 /* Is `value` valid for this attribute? */
5106 var lcTag = currentNode.nodeName.toLowerCase();
5107 if (!_isValidAttribute(lcTag, lcName, value)) {
5108 continue;
5109 }
5110
5111 /* Handle invalid data-* attribute set by try-catching it */
5112 try {
5113 if (namespaceURI) {
5114 currentNode.setAttributeNS(namespaceURI, name, value);
5115 } else {
5116 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
5117 currentNode.setAttribute(name, value);
5118 }
5119
5120 arrayPop(DOMPurify.removed);
5121 } catch (_) {}
5122 }
5123
5124 /* Execute a hook if present */
5125 _executeHook('afterSanitizeAttributes', currentNode, null);
5126 };
5127
5128 /**
5129 * _sanitizeShadowDOM
5130 *
5131 * @param {DocumentFragment} fragment to iterate over recursively
5132 */
5133 var _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
5134 var shadowNode = void 0;
5135 var shadowIterator = _createIterator(fragment);
5136
5137 /* Execute a hook if present */
5138 _executeHook('beforeSanitizeShadowDOM', fragment, null);
5139
5140 while (shadowNode = shadowIterator.nextNode()) {
5141 /* Execute a hook if present */
5142 _executeHook('uponSanitizeShadowNode', shadowNode, null);
5143
5144 /* Sanitize tags and elements */
5145 if (_sanitizeElements(shadowNode)) {
5146 continue;
5147 }
5148
5149 /* Deep shadow DOM detected */
5150 if (shadowNode.content instanceof DocumentFragment) {
5151 _sanitizeShadowDOM(shadowNode.content);
5152 }
5153
5154 /* Check attributes, sanitize if necessary */
5155 _sanitizeAttributes(shadowNode);
5156 }
5157
5158 /* Execute a hook if present */
5159 _executeHook('afterSanitizeShadowDOM', fragment, null);
5160 };
5161
5162 /**
5163 * Sanitize
5164 * Public method providing core sanitation functionality
5165 *
5166 * @param {String|Node} dirty string or DOM node
5167 * @param {Object} configuration object
5168 */
5169 // eslint-disable-next-line complexity
5170 DOMPurify.sanitize = function (dirty, cfg) {
5171 var body = void 0;
5172 var importedNode = void 0;
5173 var currentNode = void 0;
5174 var oldNode = void 0;
5175 var returnNode = void 0;
5176 /* Make sure we have a string to sanitize.
5177 DO NOT return early, as this will return the wrong type if
5178 the user has requested a DOM object rather than a string */
5179 if (!dirty) {
5180 dirty = '<!-->';
5181 }
5182
5183 /* Stringify, in case dirty is an object */
5184 if (typeof dirty !== 'string' && !_isNode(dirty)) {
5185 // eslint-disable-next-line no-negated-condition
5186 if (typeof dirty.toString !== 'function') {
5187 throw typeErrorCreate('toString is not a function');
5188 } else {
5189 dirty = dirty.toString();
5190 if (typeof dirty !== 'string') {
5191 throw typeErrorCreate('dirty is not a string, aborting');
5192 }
5193 }
5194 }
5195
5196 /* Check we can run. Otherwise fall back or ignore */
5197 if (!DOMPurify.isSupported) {
5198 if (_typeof(window.toStaticHTML) === 'object' || typeof window.toStaticHTML === 'function') {
5199 if (typeof dirty === 'string') {
5200 return window.toStaticHTML(dirty);
5201 }
5202
5203 if (_isNode(dirty)) {
5204 return window.toStaticHTML(dirty.outerHTML);
5205 }
5206 }
5207
5208 return dirty;
5209 }
5210
5211 /* Assign config vars */
5212 if (!SET_CONFIG) {
5213 _parseConfig(cfg);
5214 }
5215
5216 /* Clean up removed elements */
5217 DOMPurify.removed = [];
5218
5219 /* Check if dirty is correctly typed for IN_PLACE */
5220 if (typeof dirty === 'string') {
5221 IN_PLACE = false;
5222 }
5223
5224 if (IN_PLACE) ; else if (dirty instanceof Node) {
5225 /* If dirty is a DOM element, append to an empty document to avoid
5226 elements being stripped by the parser */
5227 body = _initDocument('<!---->');
5228 importedNode = body.ownerDocument.importNode(dirty, true);
5229 if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {
5230 /* Node is already a body, use as is */
5231 body = importedNode;
5232 } else if (importedNode.nodeName === 'HTML') {
5233 body = importedNode;
5234 } else {
5235 // eslint-disable-next-line unicorn/prefer-node-append
5236 body.appendChild(importedNode);
5237 }
5238 } else {
5239 /* Exit directly if we have nothing to do */
5240 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
5241 // eslint-disable-next-line unicorn/prefer-includes
5242 dirty.indexOf('<') === -1) {
5243 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
5244 }
5245
5246 /* Initialize the document to work on */
5247 body = _initDocument(dirty);
5248
5249 /* Check we have a DOM node from the data */
5250 if (!body) {
5251 return RETURN_DOM ? null : emptyHTML;
5252 }
5253 }
5254
5255 /* Remove first element node (ours) if FORCE_BODY is set */
5256 if (body && FORCE_BODY) {
5257 _forceRemove(body.firstChild);
5258 }
5259
5260 /* Get node iterator */
5261 var nodeIterator = _createIterator(IN_PLACE ? dirty : body);
5262
5263 /* Now start iterating over the created document */
5264 while (currentNode = nodeIterator.nextNode()) {
5265 /* Fix IE's strange behavior with manipulated textNodes #89 */
5266 if (currentNode.nodeType === 3 && currentNode === oldNode) {
5267 continue;
5268 }
5269
5270 /* Sanitize tags and elements */
5271 if (_sanitizeElements(currentNode)) {
5272 continue;
5273 }
5274
5275 /* Shadow DOM detected, sanitize it */
5276 if (currentNode.content instanceof DocumentFragment) {
5277 _sanitizeShadowDOM(currentNode.content);
5278 }
5279
5280 /* Check attributes, sanitize if necessary */
5281 _sanitizeAttributes(currentNode);
5282
5283 oldNode = currentNode;
5284 }
5285
5286 oldNode = null;
5287
5288 /* If we sanitized `dirty` in-place, return it. */
5289 if (IN_PLACE) {
5290 return dirty;
5291 }
5292
5293 /* Return sanitized string or DOM */
5294 if (RETURN_DOM) {
5295 if (RETURN_DOM_FRAGMENT) {
5296 returnNode = createDocumentFragment.call(body.ownerDocument);
5297
5298 while (body.firstChild) {
5299 // eslint-disable-next-line unicorn/prefer-node-append
5300 returnNode.appendChild(body.firstChild);
5301 }
5302 } else {
5303 returnNode = body;
5304 }
5305
5306 if (RETURN_DOM_IMPORT) {
5307 /*
5308 AdoptNode() is not used because internal state is not reset
5309 (e.g. the past names map of a HTMLFormElement), this is safe
5310 in theory but we would rather not risk another attack vector.
5311 The state that is cloned by importNode() is explicitly defined
5312 by the specs.
5313 */
5314 returnNode = importNode.call(originalDocument, returnNode, true);
5315 }
5316
5317 return returnNode;
5318 }
5319
5320 var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
5321
5322 /* Sanitize final string template-safe */
5323 if (SAFE_FOR_TEMPLATES) {
5324 serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, ' ');
5325 serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, ' ');
5326 }
5327
5328 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
5329 };
5330
5331 /**
5332 * Public method to set the configuration once
5333 * setConfig
5334 *
5335 * @param {Object} cfg configuration object
5336 */
5337 DOMPurify.setConfig = function (cfg) {
5338 _parseConfig(cfg);
5339 SET_CONFIG = true;
5340 };
5341
5342 /**
5343 * Public method to remove the configuration
5344 * clearConfig
5345 *
5346 */
5347 DOMPurify.clearConfig = function () {
5348 CONFIG = null;
5349 SET_CONFIG = false;
5350 };
5351
5352 /**
5353 * Public method to check if an attribute value is valid.
5354 * Uses last set config, if any. Otherwise, uses config defaults.
5355 * isValidAttribute
5356 *
5357 * @param {string} tag Tag name of containing element.
5358 * @param {string} attr Attribute name.
5359 * @param {string} value Attribute value.
5360 * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.
5361 */
5362 DOMPurify.isValidAttribute = function (tag, attr, value) {
5363 /* Initialize shared config vars if necessary. */
5364 if (!CONFIG) {
5365 _parseConfig({});
5366 }
5367
5368 var lcTag = stringToLowerCase(tag);
5369 var lcName = stringToLowerCase(attr);
5370 return _isValidAttribute(lcTag, lcName, value);
5371 };
5372
5373 /**
5374 * AddHook
5375 * Public method to add DOMPurify hooks
5376 *
5377 * @param {String} entryPoint entry point for the hook to add
5378 * @param {Function} hookFunction function to execute
5379 */
5380 DOMPurify.addHook = function (entryPoint, hookFunction) {
5381 if (typeof hookFunction !== 'function') {
5382 return;
5383 }
5384
5385 hooks[entryPoint] = hooks[entryPoint] || [];
5386 arrayPush(hooks[entryPoint], hookFunction);
5387 };
5388
5389 /**
5390 * RemoveHook
5391 * Public method to remove a DOMPurify hook at a given entryPoint
5392 * (pops it from the stack of hooks if more are present)
5393 *
5394 * @param {String} entryPoint entry point for the hook to remove
5395 */
5396 DOMPurify.removeHook = function (entryPoint) {
5397 if (hooks[entryPoint]) {
5398 arrayPop(hooks[entryPoint]);
5399 }
5400 };
5401
5402 /**
5403 * RemoveHooks
5404 * Public method to remove all DOMPurify hooks at a given entryPoint
5405 *
5406 * @param {String} entryPoint entry point for the hooks to remove
5407 */
5408 DOMPurify.removeHooks = function (entryPoint) {
5409 if (hooks[entryPoint]) {
5410 hooks[entryPoint] = [];
5411 }
5412 };
5413
5414 /**
5415 * RemoveAllHooks
5416 * Public method to remove all DOMPurify hooks
5417 *
5418 */
5419 DOMPurify.removeAllHooks = function () {
5420 hooks = {};
5421 };
5422
5423 return DOMPurify;
5424 }
5425
5426 var purify = createDOMPurify();
5427
5428 var globalWin = window;
5429 var globalDoc = document;
5430
5431 var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
5432
5433 /**
5434 * Wrap inlines that are in the root in paragraphs.
5435 *
5436 * @param {HTMLBodyElement} body
5437 * @param {Document} doc
5438 * @private
5439 */
5440 function wrapInlines(body, doc) {
5441 var wrapper;
5442
5443 traverse(body, function (node) {
5444 if (isInline(node, true)) {
5445 // Ignore text nodes unless they contain non-whitespace chars as
5446 // whitespace will be collapsed.
5447 // Ignore sceditor-ignore elements unless wrapping siblings
5448 // Should still wrap both if wrapping siblings.
5449 if (wrapper || node.nodeType === TEXT_NODE ?
5450 /\S/.test(node.nodeValue) : !is(node, '.sceditor-ignore')) {
5451 if (!wrapper) {
5452 wrapper = createElement('p', {}, doc);
5453 insertBefore(wrapper, node);
5454 }
5455
5456 appendChild(wrapper, node);
5457 }
5458 } else {
5459 wrapper = null;
5460 }
5461 }, false, true);
5462 }
5463 /**
5464 * SCEditor - A lightweight WYSIWYG editor
5465 *
5466 * @param {HTMLTextAreaElement} original The textarea to be converted
5467 * @param {Object} userOptions
5468 * @class SCEditor
5469 * @name SCEditor
5470 */
5471 function SCEditor(original, userOptions) {
5472 /**
5473 * Alias of this
5474 *
5475 * @private
5476 */
5477 var base = this;
5478
5479 /**
5480 * Editor format like BBCode or HTML
5481 */
5482 var format;
5483
5484 /**
5485 * The div which contains the editor and toolbar
5486 *
5487 * @type {HTMLDivElement}
5488 * @private
5489 */
5490 var editorContainer;
5491
5492 /**
5493 * Map of events handlers bound to this instance.
5494 *
5495 * @type {Object}
5496 * @private
5497 */
5498 var eventHandlers = {};
5499
5500 /**
5501 * The editors toolbar
5502 *
5503 * @type {HTMLDivElement}
5504 * @private
5505 */
5506 var toolbar;
5507
5508 /**
5509 * The editors iframe which should be in design mode
5510 *
5511 * @type {HTMLIFrameElement}
5512 * @private
5513 */
5514 var wysiwygEditor;
5515
5516 /**
5517 * The editors window
5518 *
5519 * @type {Window}
5520 * @private
5521 */
5522 var wysiwygWindow;
5523
5524 /**
5525 * The WYSIWYG editors body element
5526 *
5527 * @type {HTMLBodyElement}
5528 * @private
5529 */
5530 var wysiwygBody;
5531
5532 /**
5533 * The WYSIWYG editors document
5534 *
5535 * @type {Document}
5536 * @private
5537 */
5538 var wysiwygDocument;
5539
5540 /**
5541 * The editors textarea for viewing source
5542 *
5543 * @type {HTMLTextAreaElement}
5544 * @private
5545 */
5546 var sourceEditor;
5547
5548 /**
5549 * The current dropdown
5550 *
5551 * @type {HTMLDivElement}
5552 * @private
5553 */
5554 var dropdown;
5555
5556 /**
5557 * If the user is currently composing text via IME
5558 * @type {boolean}
5559 */
5560 var isComposing;
5561
5562 /**
5563 * Timer for valueChanged key handler
5564 * @type {number}
5565 */
5566 var valueChangedKeyUpTimer;
5567
5568 /**
5569 * The editors locale
5570 *
5571 * @private
5572 */
5573 var locale;
5574
5575 /**
5576 * Stores a cache of preloaded images
5577 *
5578 * @private
5579 * @type {Array.<HTMLImageElement>}
5580 */
5581 var preLoadCache = [];
5582
5583 /**
5584 * The editors rangeHelper instance
5585 *
5586 * @type {RangeHelper}
5587 * @private
5588 */
5589 var rangeHelper;
5590
5591 /**
5592 * An array of button state handlers
5593 *
5594 * @type {Array.<Object>}
5595 * @private
5596 */
5597 var btnStateHandlers = [];
5598
5599 /**
5600 * Plugin manager instance
5601 *
5602 * @type {PluginManager}
5603 * @private
5604 */
5605 var pluginManager;
5606
5607 /**
5608 * The current node containing the selection/caret
5609 *
5610 * @type {Node}
5611 * @private
5612 */
5613 var currentNode;
5614
5615 /**
5616 * The first block level parent of the current node
5617 *
5618 * @type {node}
5619 * @private
5620 */
5621 var currentBlockNode;
5622
5623 /**
5624 * The current node selection/caret
5625 *
5626 * @type {Object}
5627 * @private
5628 */
5629 var currentSelection;
5630
5631 /**
5632 * Used to make sure only 1 selection changed
5633 * check is called every 100ms.
5634 *
5635 * Helps improve performance as it is checked a lot.
5636 *
5637 * @type {boolean}
5638 * @private
5639 */
5640 var isSelectionCheckPending;
5641
5642 /**
5643 * If content is required (equivalent to the HTML5 required attribute)
5644 *
5645 * @type {boolean}
5646 * @private
5647 */
5648 var isRequired;
5649
5650 /**
5651 * The inline CSS style element. Will be undefined
5652 * until css() is called for the first time.
5653 *
5654 * @type {HTMLStyleElement}
5655 * @private
5656 */
5657 var inlineCss;
5658
5659 /**
5660 * Object containing a list of shortcut handlers
5661 *
5662 * @type {Object}
5663 * @private
5664 */
5665 var shortcutHandlers = {};
5666
5667 /**
5668 * The min and max heights that autoExpand should stay within
5669 *
5670 * @type {Object}
5671 * @private
5672 */
5673 var autoExpandBounds;
5674
5675 /**
5676 * Timeout for the autoExpand function to throttle calls
5677 *
5678 * @private
5679 */
5680 var autoExpandThrottle;
5681
5682 /**
5683 * Cache of the current toolbar buttons
5684 *
5685 * @type {Object}
5686 * @private
5687 */
5688 var toolbarButtons = {};
5689
5690 /**
5691 * Last scroll position before maximizing so
5692 * it can be restored when finished.
5693 *
5694 * @type {number}
5695 * @private
5696 */
5697 var maximizeScrollPosition;
5698
5699 /**
5700 * Stores the contents while a paste is taking place.
5701 *
5702 * Needed to support browsers that lack clipboard API support.
5703 *
5704 * @type {?DocumentFragment}
5705 * @private
5706 */
5707 var pasteContentFragment;
5708
5709 /**
5710 * All the emoticons from dropdown, more and hidden combined
5711 * and with the emoticons root set
5712 *
5713 * @type {!Object<string, string>}
5714 * @private
5715 */
5716 var allEmoticons = {};
5717
5718 /**
5719 * Current icon set if any
5720 *
5721 * @type {?Object}
5722 * @private
5723 */
5724 var icons;
5725
5726 /**
5727 * Private functions
5728 * @private
5729 */
5730 var init,
5731 replaceEmoticons,
5732 handleCommand,
5733 initEditor,
5734 initLocale,
5735 initToolBar,
5736 initOptions,
5737 initEvents,
5738 initResize,
5739 initEmoticons,
5740 handlePasteEvt,
5741 handleCutCopyEvt,
5742 handlePasteData,
5743 handleKeyDown,
5744 handleBackSpace,
5745 handleKeyPress,
5746 handleFormReset,
5747 handleMouseDown,
5748 handleComposition,
5749 handleEvent,
5750 handleDocumentClick,
5751 updateToolBar,
5752 updateActiveButtons,
5753 sourceEditorSelectedText,
5754 appendNewLine,
5755 checkSelectionChanged,
5756 checkNodeChanged,
5757 autofocus,
5758 emoticonsKeyPress,
5759 emoticonsCheckWhitespace,
5760 currentStyledBlockNode,
5761 triggerValueChanged,
5762 valueChangedBlur,
5763 valueChangedKeyUp,
5764 autoUpdate,
5765 autoExpand;
5766
5767 /**
5768 * All the commands supported by the editor
5769 * @name commands
5770 * @memberOf SCEditor.prototype
5771 */
5772 base.commands = extend(true, {}, (userOptions.commands || defaultCmds));
5773
5774 /**
5775 * Options for this editor instance
5776 * @name opts
5777 * @memberOf SCEditor.prototype
5778 */
5779 var options = base.opts = extend(
5780 true, {}, defaultOptions, userOptions
5781 );
5782
5783 // Don't deep extend emoticons (fixes #565)
5784 base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons;
5785
5786 if (!Array.isArray(options.allowedIframeUrls)) {
5787 options.allowedIframeUrls = [];
5788 }
5789 options.allowedIframeUrls.push('https://www.youtube-nocookie.com/embed/');
5790
5791 // Create new instance of DOMPurify for each editor instance so can
5792 // have different allowed iframe URLs
5793 // eslint-disable-next-line new-cap
5794 var domPurify = purify();
5795
5796 // Allow iframes for things like YouTube, see:
5797 // https://github.com/cure53/DOMPurify/issues/340#issuecomment-670758980
5798 domPurify.addHook('uponSanitizeElement', function (node, data) {
5799 var allowedUrls = options.allowedIframeUrls;
5800
5801 if (data.tagName === 'iframe') {
5802 var src = attr(node, 'src') || '';
5803
5804 for (var i = 0; i < allowedUrls.length; i++) {
5805 var url = allowedUrls[i];
5806
5807 if (isString(url) && src.substr(0, url.length) === url) {
5808 return;
5809 }
5810
5811 // Handle regex
5812 if (url.test && url.test(src)) {
5813 return;
5814 }
5815 }
5816
5817 // No match so remove
5818 remove(node);
5819 }
5820 });
5821
5822 // Convert target attribute into data-sce-target attributes so XHTML format
5823 // can allow them
5824 domPurify.addHook('afterSanitizeAttributes', function (node) {
5825 if ('target' in node) {
5826 attr(node, 'data-sce-target', attr(node, 'target'));
5827 }
5828
5829 removeAttr(node, 'target');
5830 });
5831
5832 /**
5833 * Sanitize HTML to avoid XSS
5834 *
5835 * @param {string} html
5836 * @return {string} html
5837 * @private
5838 */
5839 function sanitize(html) {
5840 return domPurify.sanitize(html, {
5841 ADD_TAGS: ['iframe'],
5842 ADD_ATTR: ['allowfullscreen', 'frameborder', 'target']
5843 });
5844 }
5845 /**
5846 * Creates the editor iframe and textarea
5847 * @private
5848 */
5849 init = function () {
5850 original._sceditor = base;
5851
5852 // Load locale
5853 if (options.locale && options.locale !== 'en') {
5854 initLocale();
5855 }
5856
5857 editorContainer = createElement('div', {
5858 className: 'sceditor-container'
5859 });
5860
5861 insertBefore(editorContainer, original);
5862 css(editorContainer, 'z-index', options.zIndex);
5863
5864 isRequired = original.required;
5865 original.required = false;
5866
5867 var FormatCtor = SCEditor.formats[options.format];
5868 format = FormatCtor ? new FormatCtor() : {};
5869 /*
5870 * Plugins should be initialized before the formatters since
5871 * they may wish to add or change formatting handlers and
5872 * since the bbcode format caches its handlers,
5873 * such changes must be done first.
5874 */
5875 pluginManager = new PluginManager(base);
5876 (options.plugins || '').split(',').forEach(function (plugin) {
5877 pluginManager.register(plugin.trim());
5878 });
5879 if ('init' in format) {
5880 format.init.call(base);
5881 }
5882
5883 // create the editor
5884 initEmoticons();
5885 initToolBar();
5886 initEditor();
5887 initOptions();
5888 initEvents();
5889
5890 // force into source mode if is a browser that can't handle
5891 // full editing
5892 if (!isWysiwygSupported) {
5893 base.toggleSourceMode();
5894 }
5895
5896 updateActiveButtons();
5897
5898 var loaded = function () {
5899 off(globalWin, 'load', loaded);
5900
5901 if (options.autofocus) {
5902 autofocus(!!options.autofocusEnd);
5903 }
5904
5905 autoExpand();
5906 appendNewLine();
5907 // TODO: use editor doc and window?
5908 pluginManager.call('ready');
5909 if ('onReady' in format) {
5910 format.onReady.call(base);
5911 }
5912 };
5913 on(globalWin, 'load', loaded);
5914 if (globalDoc.readyState === 'complete') {
5915 loaded();
5916 }
5917 };
5918
5919 /**
5920 * Init the locale variable with the specified locale if possible
5921 * @private
5922 * @return void
5923 */
5924 initLocale = function () {
5925 var lang;
5926
5927 locale = SCEditor.locale[options.locale];
5928
5929 if (!locale) {
5930 lang = options.locale.split('-');
5931 locale = SCEditor.locale[lang[0]];
5932 }
5933
5934 // Locale DateTime format overrides any specified in the options
5935 if (locale && locale.dateFormat) {
5936 options.dateFormat = locale.dateFormat;
5937 }
5938 };
5939
5940 /**
5941 * Creates the editor iframe and textarea
5942 * @private
5943 */
5944 initEditor = function () {
5945 sourceEditor = createElement('textarea');
5946 wysiwygEditor = createElement('iframe', {
5947 frameborder: 0,
5948 allowfullscreen: true
5949 });
5950
5951 /*
5952 * This needs to be done right after they are created because,
5953 * for any reason, the user may not want the value to be tinkered
5954 * by any filters.
5955 */
5956 if (options.startInSourceMode) {
5957 addClass(editorContainer, 'sourceMode');
5958 hide(wysiwygEditor);
5959 } else {
5960 addClass(editorContainer, 'wysiwygMode');
5961 hide(sourceEditor);
5962 }
5963
5964 if (!options.spellcheck) {
5965 attr(editorContainer, 'spellcheck', 'false');
5966 }
5967
5968 if (globalWin.location.protocol === 'https:') {
5969 attr(wysiwygEditor, 'src', 'about:blank');
5970 }
5971
5972 // Add the editor to the container
5973 appendChild(editorContainer, wysiwygEditor);
5974 appendChild(editorContainer, sourceEditor);
5975
5976 // TODO: make this optional somehow
5977 base.dimensions(
5978 options.width || width(original),
5979 options.height || height(original)
5980 );
5981
5982 // Add ios to HTML so can apply CSS fix to only it
5983 var className = ios ? ' ios' : '';
5984
5985 wysiwygDocument = wysiwygEditor.contentDocument;
5986 wysiwygDocument.open();
5987 wysiwygDocument.write(_tmpl('html', {
5988 attrs: ' class="' + className + '"',
5989 spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
5990 charset: options.charset,
5991 style: options.style
5992 }));
5993 wysiwygDocument.close();
5994
5995 wysiwygBody = wysiwygDocument.body;
5996 wysiwygWindow = wysiwygEditor.contentWindow;
5997
5998 base.readOnly(!!options.readOnly);
5999
6000 // iframe overflow fix for iOS
6001 if (ios) {
6002 height(wysiwygBody, '100%');
6003 on(wysiwygBody, 'touchend', base.focus);
6004 }
6005
6006 var tabIndex = attr(original, 'tabindex');
6007 attr(sourceEditor, 'tabindex', tabIndex);
6008 attr(wysiwygEditor, 'tabindex', tabIndex);
6009
6010 rangeHelper = new RangeHelper(wysiwygWindow, null, sanitize);
6011
6012 // load any textarea value into the editor
6013 hide(original);
6014 base.val(original.value);
6015
6016 var placeholder = options.placeholder ||
6017 attr(original, 'placeholder');
6018
6019 if (placeholder) {
6020 sourceEditor.placeholder = placeholder;
6021 attr(wysiwygBody, 'placeholder', placeholder);
6022 }
6023 };
6024
6025 /**
6026 * Initialises options
6027 * @private
6028 */
6029 initOptions = function () {
6030 // auto-update original textbox on blur if option set to true
6031 if (options.autoUpdate) {
6032 on(wysiwygBody, 'blur', autoUpdate);
6033 on(sourceEditor, 'blur', autoUpdate);
6034 }
6035
6036 if (options.rtl === null) {
6037 options.rtl = css(sourceEditor, 'direction') === 'rtl';
6038 }
6039
6040 base.rtl(!!options.rtl);
6041
6042 if (options.autoExpand) {
6043 // Need to update when images (or anything else) loads
6044 on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE);
6045 on(wysiwygBody, 'input keyup', autoExpand);
6046 }
6047
6048 if (options.resizeEnabled) {
6049 initResize();
6050 }
6051
6052 attr(editorContainer, 'id', options.id);
6053 base.emoticons(options.emoticonsEnabled);
6054 };
6055
6056 /**
6057 * Initialises events
6058 * @private
6059 */
6060 initEvents = function () {
6061 var form = original.form;
6062 var compositionEvents = 'compositionstart compositionend';
6063 var eventsToForward =
6064 'keydown keyup keypress focus blur contextmenu input';
6065 var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ?
6066 'selectionchange' :
6067 'keyup focus blur contextmenu mouseup touchend click';
6068
6069 on(globalDoc, 'click', handleDocumentClick);
6070
6071 if (form) {
6072 on(form, 'reset', handleFormReset);
6073 on(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
6074 }
6075
6076 on(window, 'pagehide', base.updateOriginal);
6077 on(window, 'pageshow', handleFormReset);
6078 on(wysiwygBody, 'keypress', handleKeyPress);
6079 on(wysiwygBody, 'keydown', handleKeyDown);
6080 on(wysiwygBody, 'keydown', handleBackSpace);
6081 on(wysiwygBody, 'keyup', appendNewLine);
6082 on(wysiwygBody, 'blur', valueChangedBlur);
6083 on(wysiwygBody, 'keyup', valueChangedKeyUp);
6084 on(wysiwygBody, 'paste', handlePasteEvt);
6085 on(wysiwygBody, 'cut copy', handleCutCopyEvt);
6086 on(wysiwygBody, compositionEvents, handleComposition);
6087 on(wysiwygBody, checkSelectionEvents, checkSelectionChanged);
6088 on(wysiwygBody, eventsToForward, handleEvent);
6089
6090 if (options.emoticonsCompat && globalWin.getSelection) {
6091 on(wysiwygBody, 'keyup', emoticonsCheckWhitespace);
6092 }
6093
6094 on(wysiwygBody, 'blur', function () {
6095 if (!base.val()) {
6096 addClass(wysiwygBody, 'placeholder');
6097 }
6098 });
6099
6100 on(wysiwygBody, 'focus', function () {
6101 removeClass(wysiwygBody, 'placeholder');
6102 });
6103
6104 on(sourceEditor, 'blur', valueChangedBlur);
6105 on(sourceEditor, 'keyup', valueChangedKeyUp);
6106 on(sourceEditor, 'keydown', handleKeyDown);
6107 on(sourceEditor, compositionEvents, handleComposition);
6108 on(sourceEditor, eventsToForward, handleEvent);
6109
6110 on(wysiwygDocument, 'mousedown', handleMouseDown);
6111 on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged);
6112 on(wysiwygDocument, 'keyup', appendNewLine);
6113
6114 on(editorContainer, 'selectionchanged', checkNodeChanged);
6115 on(editorContainer, 'selectionchanged', updateActiveButtons);
6116 // Custom events to forward
6117 on(
6118 editorContainer,
6119 'selectionchanged valuechanged nodechanged pasteraw paste',
6120 handleEvent
6121 );
6122 };
6123
6124 /**
6125 * Creates the toolbar and appends it to the container
6126 * @private
6127 */
6128 initToolBar = function () {
6129 var group,
6130 commands = base.commands,
6131 exclude = (options.toolbarExclude || '').split(','),
6132 groups = options.toolbar.split('|');
6133
6134 toolbar = createElement('div', {
6135 className: 'sceditor-toolbar',
6136 unselectable: 'on'
6137 });
6138
6139 if (options.icons in SCEditor.icons) {
6140 icons = new SCEditor.icons[options.icons]();
6141 }
6142
6143 each(groups, function (_, menuItems) {
6144 group = createElement('div', {
6145 className: 'sceditor-group'
6146 });
6147
6148 each(menuItems.split(','), function (_, commandName) {
6149 var button, shortcut,
6150 command = commands[commandName];
6151
6152 // The commandName must be a valid command and not excluded
6153 if (!command || exclude.indexOf(commandName) > -1) {
6154 return;
6155 }
6156
6157 shortcut = command.shortcut;
6158 button = _tmpl('toolbarButton', {
6159 name: commandName,
6160 dispName: base._(command.name ||
6161 command.tooltip || commandName)
6162 }, true).firstChild;
6163
6164 if (icons && icons.create) {
6165 var icon = icons.create(commandName);
6166 if (icon) {
6167 insertBefore(icons.create(commandName),
6168 button.firstChild);
6169 addClass(button, 'has-icon');
6170 }
6171 }
6172
6173 button._sceTxtMode = !!command.txtExec;
6174 button._sceWysiwygMode = !!command.exec;
6175 toggleClass(button, 'disabled', !command.exec);
6176 on(button, 'click', function (e) {
6177 if (!hasClass(button, 'disabled')) {
6178 handleCommand(button, command);
6179 }
6180
6181 updateActiveButtons();
6182 e.preventDefault();
6183 });
6184 // Prevent editor losing focus when button clicked
6185 on(button, 'mousedown', function (e) {
6186 base.closeDropDown();
6187 e.preventDefault();
6188 });
6189
6190 if (command.tooltip) {
6191 attr(button, 'title',
6192 base._(command.tooltip) +
6193 (shortcut ? ' (' + shortcut + ')' : '')
6194 );
6195 }
6196
6197 if (shortcut) {
6198 base.addShortcut(shortcut, commandName);
6199 }
6200
6201 if (command.state) {
6202 btnStateHandlers.push({
6203 name: commandName,
6204 state: command.state
6205 });
6206 // exec string commands can be passed to queryCommandState
6207 } else if (isString(command.exec)) {
6208 btnStateHandlers.push({
6209 name: commandName,
6210 state: command.exec
6211 });
6212 }
6213
6214 appendChild(group, button);
6215 toolbarButtons[commandName] = button;
6216 });
6217
6218 // Exclude empty groups
6219 if (group.firstChild) {
6220 appendChild(toolbar, group);
6221 }
6222 });
6223
6224 // Append the toolbar to the toolbarContainer option if given
6225 appendChild(options.toolbarContainer || editorContainer, toolbar);
6226 };
6227
6228 /**
6229 * Creates the resizer.
6230 * @private
6231 */
6232 initResize = function () {
6233 var minHeight, maxHeight, minWidth, maxWidth,
6234 mouseMoveFunc, mouseUpFunc,
6235 grip = createElement('div', {
6236 className: 'sceditor-grip'
6237 }),
6238 // Cover is used to cover the editor iframe so document
6239 // still gets mouse move events
6240 cover = createElement('div', {
6241 className: 'sceditor-resize-cover'
6242 }),
6243 moveEvents = 'touchmove mousemove',
6244 endEvents = 'touchcancel touchend mouseup',
6245 startX = 0,
6246 startY = 0,
6247 newX = 0,
6248 newY = 0,
6249 startWidth = 0,
6250 startHeight = 0,
6251 origWidth = width(editorContainer),
6252 origHeight = height(editorContainer),
6253 isDragging = false,
6254 rtl = base.rtl();
6255
6256 minHeight = options.resizeMinHeight || origHeight / 1.5;
6257 maxHeight = options.resizeMaxHeight || origHeight * 2.5;
6258 minWidth = options.resizeMinWidth || origWidth / 1.25;
6259 maxWidth = options.resizeMaxWidth || origWidth * 1.25;
6260
6261 mouseMoveFunc = function (e) {
6262 // iOS uses window.event
6263 if (e.type === 'touchmove') {
6264 e = globalWin.event;
6265 newX = e.changedTouches[0].pageX;
6266 newY = e.changedTouches[0].pageY;
6267 } else {
6268 newX = e.pageX;
6269 newY = e.pageY;
6270 }
6271
6272 var newHeight = startHeight + (newY - startY),
6273 newWidth = rtl ?
6274 startWidth - (newX - startX) :
6275 startWidth + (newX - startX);
6276
6277 if (maxWidth > 0 && newWidth > maxWidth) {
6278 newWidth = maxWidth;
6279 }
6280 if (minWidth > 0 && newWidth < minWidth) {
6281 newWidth = minWidth;
6282 }
6283 if (!options.resizeWidth) {
6284 newWidth = false;
6285 }
6286
6287 if (maxHeight > 0 && newHeight > maxHeight) {
6288 newHeight = maxHeight;
6289 }
6290 if (minHeight > 0 && newHeight < minHeight) {
6291 newHeight = minHeight;
6292 }
6293 if (!options.resizeHeight) {
6294 newHeight = false;
6295 }
6296
6297 if (newWidth || newHeight) {
6298 base.dimensions(newWidth, newHeight);
6299 }
6300
6301 e.preventDefault();
6302 };
6303
6304 mouseUpFunc = function (e) {
6305 if (!isDragging) {
6306 return;
6307 }
6308
6309 isDragging = false;
6310
6311 hide(cover);
6312 removeClass(editorContainer, 'resizing');
6313 off(globalDoc, moveEvents, mouseMoveFunc);
6314 off(globalDoc, endEvents, mouseUpFunc);
6315
6316 e.preventDefault();
6317 };
6318
6319 if (icons && icons.create) {
6320 var icon = icons.create('grip');
6321 if (icon) {
6322 appendChild(grip, icon);
6323 addClass(grip, 'has-icon');
6324 }
6325 }
6326
6327 appendChild(editorContainer, grip);
6328 appendChild(editorContainer, cover);
6329 hide(cover);
6330
6331 on(grip, 'touchstart mousedown', function (e) {
6332 // iOS uses window.event
6333 if (e.type === 'touchstart') {
6334 e = globalWin.event;
6335 startX = e.touches[0].pageX;
6336 startY = e.touches[0].pageY;
6337 } else {
6338 startX = e.pageX;
6339 startY = e.pageY;
6340 }
6341
6342 startWidth = width(editorContainer);
6343 startHeight = height(editorContainer);
6344 isDragging = true;
6345
6346 addClass(editorContainer, 'resizing');
6347 show(cover);
6348 on(globalDoc, moveEvents, mouseMoveFunc);
6349 on(globalDoc, endEvents, mouseUpFunc);
6350
6351 e.preventDefault();
6352 });
6353 };
6354
6355 /**
6356 * Prefixes and preloads the emoticon images
6357 * @private
6358 */
6359 initEmoticons = function () {
6360 var emoticons = options.emoticons;
6361 var root = options.emoticonsRoot || '';
6362
6363 if (emoticons) {
6364 allEmoticons = extend(
6365 {}, emoticons.more, emoticons.dropdown, emoticons.hidden
6366 );
6367 }
6368
6369 each(allEmoticons, function (key, url) {
6370 allEmoticons[key] = _tmpl('emoticon', {
6371 key: key,
6372 // Prefix emoticon root to emoticon urls
6373 url: root + (url.url || url),
6374 tooltip: url.tooltip || key
6375 });
6376
6377 // Preload the emoticon
6378 if (options.emoticonsEnabled) {
6379 preLoadCache.push(createElement('img', {
6380 src: root + (url.url || url)
6381 }));
6382 }
6383 });
6384 };
6385
6386 /**
6387 * Autofocus the editor
6388 * @private
6389 */
6390 autofocus = function (focusEnd) {
6391 var range, txtPos,
6392 node = wysiwygBody.firstChild;
6393
6394 // Can't focus invisible elements
6395 if (!isVisible(editorContainer)) {
6396 return;
6397 }
6398
6399 if (base.sourceMode()) {
6400 txtPos = focusEnd ? sourceEditor.value.length : 0;
6401
6402 sourceEditor.setSelectionRange(txtPos, txtPos);
6403
6404 return;
6405 }
6406
6407 removeWhiteSpace(wysiwygBody);
6408
6409 if (focusEnd) {
6410 if (!(node = wysiwygBody.lastChild)) {
6411 node = createElement('p', {}, wysiwygDocument);
6412 appendChild(wysiwygBody, node);
6413 }
6414
6415 while (node.lastChild) {
6416 node = node.lastChild;
6417
6418 // Should place the cursor before the last <br>
6419 if (is(node, 'br') && node.previousSibling) {
6420 node = node.previousSibling;
6421 }
6422 }
6423 }
6424
6425 range = wysiwygDocument.createRange();
6426
6427 if (!canHaveChildren(node)) {
6428 range.setStartBefore(node);
6429
6430 if (focusEnd) {
6431 range.setStartAfter(node);
6432 }
6433 } else {
6434 range.selectNodeContents(node);
6435 }
6436
6437 range.collapse(!focusEnd);
6438 rangeHelper.selectRange(range);
6439 currentSelection = range;
6440
6441 if (focusEnd) {
6442 wysiwygBody.scrollTop = wysiwygBody.scrollHeight;
6443 }
6444
6445 base.focus();
6446 };
6447
6448 /**
6449 * Gets if the editor is read only
6450 *
6451 * @since 1.3.5
6452 * @function
6453 * @memberOf SCEditor.prototype
6454 * @name readOnly
6455 * @return {boolean}
6456 */
6457 /**
6458 * Sets if the editor is read only
6459 *
6460 * @param {boolean} readOnly
6461 * @since 1.3.5
6462 * @function
6463 * @memberOf SCEditor.prototype
6464 * @name readOnly^2
6465 * @return {this}
6466 */
6467 base.readOnly = function (readOnly) {
6468 if (typeof readOnly !== 'boolean') {
6469 return !sourceEditor.readonly;
6470 }
6471
6472 wysiwygBody.contentEditable = !readOnly;
6473 sourceEditor.readonly = !readOnly;
6474
6475 updateToolBar(readOnly);
6476
6477 return base;
6478 };
6479
6480 /**
6481 * Gets if the editor is in RTL mode
6482 *
6483 * @since 1.4.1
6484 * @function
6485 * @memberOf SCEditor.prototype
6486 * @name rtl
6487 * @return {boolean}
6488 */
6489 /**
6490 * Sets if the editor is in RTL mode
6491 *
6492 * @param {boolean} rtl
6493 * @since 1.4.1
6494 * @function
6495 * @memberOf SCEditor.prototype
6496 * @name rtl^2
6497 * @return {this}
6498 */
6499 base.rtl = function (rtl) {
6500 var dir = rtl ? 'rtl' : 'ltr';
6501
6502 if (typeof rtl !== 'boolean') {
6503 return attr(sourceEditor, 'dir') === 'rtl';
6504 }
6505
6506 attr(wysiwygBody, 'dir', dir);
6507 attr(sourceEditor, 'dir', dir);
6508
6509 removeClass(editorContainer, 'rtl');
6510 removeClass(editorContainer, 'ltr');
6511 addClass(editorContainer, dir);
6512
6513 if (icons && icons.rtl) {
6514 icons.rtl(rtl);
6515 }
6516
6517 return base;
6518 };
6519
6520 /**
6521 * Updates the toolbar to disable/enable the appropriate buttons
6522 * @private
6523 */
6524 updateToolBar = function (disable) {
6525 var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode';
6526
6527 each(toolbarButtons, function (_, button) {
6528 toggleClass(button, 'disabled', disable || !button[mode]);
6529 });
6530 };
6531
6532 /**
6533 * Gets the width of the editor in pixels
6534 *
6535 * @since 1.3.5
6536 * @function
6537 * @memberOf SCEditor.prototype
6538 * @name width
6539 * @return {number}
6540 */
6541 /**
6542 * Sets the width of the editor
6543 *
6544 * @param {number} width Width in pixels
6545 * @since 1.3.5
6546 * @function
6547 * @memberOf SCEditor.prototype
6548 * @name width^2
6549 * @return {this}
6550 */
6551 /**
6552 * Sets the width of the editor
6553 *
6554 * The saveWidth specifies if to save the width. The stored width can be
6555 * used for things like restoring from maximized state.
6556 *
6557 * @param {number} width Width in pixels
6558 * @param {boolean} [saveWidth=true] If to store the width
6559 * @since 1.4.1
6560 * @function
6561 * @memberOf SCEditor.prototype
6562 * @name width^3
6563 * @return {this}
6564 */
6565 base.width = function (width$1, saveWidth) {
6566 if (!width$1 && width$1 !== 0) {
6567 return width(editorContainer);
6568 }
6569
6570 base.dimensions(width$1, null, saveWidth);
6571
6572 return base;
6573 };
6574
6575 /**
6576 * Returns an object with the properties width and height
6577 * which are the width and height of the editor in px.
6578 *
6579 * @since 1.4.1
6580 * @function
6581 * @memberOf SCEditor.prototype
6582 * @name dimensions
6583 * @return {object}
6584 */
6585 /**
6586 * <p>Sets the width and/or height of the editor.</p>
6587 *
6588 * <p>If width or height is not numeric it is ignored.</p>
6589 *
6590 * @param {number} width Width in px
6591 * @param {number} height Height in px
6592 * @since 1.4.1
6593 * @function
6594 * @memberOf SCEditor.prototype
6595 * @name dimensions^2
6596 * @return {this}
6597 */
6598 /**
6599 * <p>Sets the width and/or height of the editor.</p>
6600 *
6601 * <p>If width or height is not numeric it is ignored.</p>
6602 *
6603 * <p>The save argument specifies if to save the new sizes.
6604 * The saved sizes can be used for things like restoring from
6605 * maximized state. This should normally be left as true.</p>
6606 *
6607 * @param {number} width Width in px
6608 * @param {number} height Height in px
6609 * @param {boolean} [save=true] If to store the new sizes
6610 * @since 1.4.1
6611 * @function
6612 * @memberOf SCEditor.prototype
6613 * @name dimensions^3
6614 * @return {this}
6615 */
6616 base.dimensions = function (width$1, height$1, save) {
6617 // set undefined width/height to boolean false
6618 width$1 = (!width$1 && width$1 !== 0) ? false : width$1;
6619 height$1 = (!height$1 && height$1 !== 0) ? false : height$1;
6620
6621 if (width$1 === false && height$1 === false) {
6622 return { width: base.width(), height: base.height() };
6623 }
6624
6625 if (width$1 !== false) {
6626 if (save !== false) {
6627 options.width = width$1;
6628 }
6629
6630 width(editorContainer, width$1);
6631 }
6632
6633 if (height$1 !== false) {
6634 if (save !== false) {
6635 options.height = height$1;
6636 }
6637
6638 height(editorContainer, height$1);
6639 }
6640
6641 return base;
6642 };
6643
6644 /**
6645 * Gets the height of the editor in px
6646 *
6647 * @since 1.3.5
6648 * @function
6649 * @memberOf SCEditor.prototype
6650 * @name height
6651 * @return {number}
6652 */
6653 /**
6654 * Sets the height of the editor
6655 *
6656 * @param {number} height Height in px
6657 * @since 1.3.5
6658 * @function
6659 * @memberOf SCEditor.prototype
6660 * @name height^2
6661 * @return {this}
6662 */
6663 /**
6664 * Sets the height of the editor
6665 *
6666 * The saveHeight specifies if to save the height.
6667 *
6668 * The stored height can be used for things like
6669 * restoring from maximized state.
6670 *
6671 * @param {number} height Height in px
6672 * @param {boolean} [saveHeight=true] If to store the height
6673 * @since 1.4.1
6674 * @function
6675 * @memberOf SCEditor.prototype
6676 * @name height^3
6677 * @return {this}
6678 */
6679 base.height = function (height$1, saveHeight) {
6680 if (!height$1 && height$1 !== 0) {
6681 return height(editorContainer);
6682 }
6683
6684 base.dimensions(null, height$1, saveHeight);
6685
6686 return base;
6687 };
6688
6689 /**
6690 * Gets if the editor is maximised or not
6691 *
6692 * @since 1.4.1
6693 * @function
6694 * @memberOf SCEditor.prototype
6695 * @name maximize
6696 * @return {boolean}
6697 */
6698 /**
6699 * Sets if the editor is maximised or not
6700 *
6701 * @param {boolean} maximize If to maximise the editor
6702 * @since 1.4.1
6703 * @function
6704 * @memberOf SCEditor.prototype
6705 * @name maximize^2
6706 * @return {this}
6707 */
6708 base.maximize = function (maximize) {
6709 var maximizeSize = 'sceditor-maximize';
6710
6711 if (isUndefined(maximize)) {
6712 return hasClass(editorContainer, maximizeSize);
6713 }
6714
6715 maximize = !!maximize;
6716
6717 if (maximize) {
6718 maximizeScrollPosition = globalWin.pageYOffset;
6719 }
6720
6721 toggleClass(globalDoc.documentElement, maximizeSize, maximize);
6722 toggleClass(globalDoc.body, maximizeSize, maximize);
6723 toggleClass(editorContainer, maximizeSize, maximize);
6724 base.width(maximize ? '100%' : options.width, false);
6725 base.height(maximize ? '100%' : options.height, false);
6726
6727 if (!maximize) {
6728 globalWin.scrollTo(0, maximizeScrollPosition);
6729 }
6730
6731 autoExpand();
6732
6733 return base;
6734 };
6735
6736 autoExpand = function () {
6737 if (options.autoExpand && !autoExpandThrottle) {
6738 autoExpandThrottle = setTimeout(base.expandToContent, 200);
6739 }
6740 };
6741
6742 /**
6743 * Expands or shrinks the editors height to the height of it's content
6744 *
6745 * Unless ignoreMaxHeight is set to true it will not expand
6746 * higher than the maxHeight option.
6747 *
6748 * @since 1.3.5
6749 * @param {boolean} [ignoreMaxHeight=false]
6750 * @function
6751 * @name expandToContent
6752 * @memberOf SCEditor.prototype
6753 * @see #resizeToContent
6754 */
6755 base.expandToContent = function (ignoreMaxHeight) {
6756 if (base.maximize()) {
6757 return;
6758 }
6759
6760 clearTimeout(autoExpandThrottle);
6761 autoExpandThrottle = false;
6762
6763 if (!autoExpandBounds) {
6764 var height$1 = options.resizeMinHeight || options.height ||
6765 height(original);
6766
6767 autoExpandBounds = {
6768 min: height$1,
6769 max: options.resizeMaxHeight || (height$1 * 2)
6770 };
6771 }
6772
6773 var range = globalDoc.createRange();
6774 range.selectNodeContents(wysiwygBody);
6775
6776 var rect = range.getBoundingClientRect();
6777 var current = wysiwygDocument.documentElement.clientHeight - 1;
6778 var spaceNeeded = rect.bottom - rect.top;
6779 var newHeight = base.height() + 1 + (spaceNeeded - current);
6780
6781 if (!ignoreMaxHeight && autoExpandBounds.max !== -1) {
6782 newHeight = Math.min(newHeight, autoExpandBounds.max);
6783 }
6784
6785 base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min)));
6786 };
6787
6788 /**
6789 * Destroys the editor, removing all elements and
6790 * event handlers.
6791 *
6792 * Leaves only the original textarea.
6793 *
6794 * @function
6795 * @name destroy
6796 * @memberOf SCEditor.prototype
6797 */
6798 base.destroy = function () {
6799 // Don't destroy if the editor has already been destroyed
6800 if (!pluginManager) {
6801 return;
6802 }
6803
6804 pluginManager.destroy();
6805
6806 rangeHelper = null;
6807 pluginManager = null;
6808
6809 if (dropdown) {
6810 remove(dropdown);
6811 }
6812
6813 off(globalDoc, 'click', handleDocumentClick);
6814
6815 var form = original.form;
6816 if (form) {
6817 off(form, 'reset', handleFormReset);
6818 off(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
6819 }
6820
6821 off(window, 'pagehide', base.updateOriginal);
6822 off(window, 'pageshow', handleFormReset);
6823 remove(sourceEditor);
6824 remove(toolbar);
6825 remove(editorContainer);
6826
6827 delete original._sceditor;
6828 show(original);
6829
6830 original.required = isRequired;
6831 };
6832
6833
6834 /**
6835 * Creates a menu item drop down
6836 *
6837 * @param {HTMLElement} menuItem The button to align the dropdown with
6838 * @param {string} name Used for styling the dropdown, will be
6839 * a class sceditor-name
6840 * @param {HTMLElement} content The HTML content of the dropdown
6841 * @function
6842 * @name createDropDown
6843 * @memberOf SCEditor.prototype
6844 */
6845 base.createDropDown = function (menuItem, name, content) {
6846 // first click for create second click for close
6847 var dropDownCss,
6848 dropDownClass = 'sceditor-' + name;
6849
6850 base.closeDropDown();
6851
6852 // Only close the dropdown if it was already open
6853 if (dropdown && hasClass(dropdown, dropDownClass)) {
6854 return;
6855 }
6856
6857 dropDownCss = extend({
6858 top: menuItem.offsetTop,
6859 left: menuItem.offsetLeft,
6860 marginTop: menuItem.clientHeight
6861 }, options.dropDownCss);
6862
6863 dropdown = createElement('div', {
6864 className: 'sceditor-dropdown ' + dropDownClass
6865 });
6866
6867 css(dropdown, dropDownCss);
6868 appendChild(dropdown, content);
6869 appendChild(editorContainer, dropdown);
6870 on(dropdown, 'click focusin', function (e) {
6871 // stop clicks within the dropdown from being handled
6872 e.stopPropagation();
6873 });
6874
6875 if (dropdown) {
6876 var first = find(dropdown, 'input,textarea')[0];
6877 if (first) {
6878 first.focus();
6879 }
6880 }
6881 };
6882
6883 /**
6884 * Handles any document click and closes the dropdown if open
6885 * @private
6886 */
6887 handleDocumentClick = function (e) {
6888 // ignore right clicks
6889 if (e.which !== 3 && dropdown && !e.defaultPrevented) {
6890 autoUpdate();
6891
6892 base.closeDropDown();
6893 }
6894 };
6895
6896 /**
6897 * Handles the WYSIWYG editors cut & copy events
6898 *
6899 * By default browsers also copy inherited styling from the stylesheet and
6900 * browser default styling which is unnecessary.
6901 *
6902 * This will ignore inherited styles and only copy inline styling.
6903 * @private
6904 */
6905 handleCutCopyEvt = function (e) {
6906 var range = rangeHelper.selectedRange();
6907 if (range) {
6908 var container = createElement('div', {}, wysiwygDocument);
6909 var firstParent;
6910
6911 // Copy all inline parent nodes up to the first block parent so can
6912 // copy inline styles
6913 var parent = range.commonAncestorContainer;
6914 while (parent && isInline(parent, true)) {
6915 if (parent.nodeType === ELEMENT_NODE) {
6916 var clone = parent.cloneNode();
6917 if (container.firstChild) {
6918 appendChild(clone, container.firstChild);
6919 }
6920
6921 appendChild(container, clone);
6922 firstParent = firstParent || clone;
6923 }
6924 parent = parent.parentNode;
6925 }
6926
6927 appendChild(firstParent || container, range.cloneContents());
6928 removeWhiteSpace(container);
6929
6930 e.clipboardData.setData('text/html', container.innerHTML);
6931
6932 // TODO: Refactor into private shared module with plaintext plugin
6933 // innerText adds two newlines after <p> tags so convert them to
6934 // <div> tags
6935 each(find(container, 'p'), function (_, elm) {
6936 convertElement(elm, 'div');
6937 });
6938 // Remove collapsed <br> tags as innerText converts them to newlines
6939 each(find(container, 'br'), function (_, elm) {
6940 if (!elm.nextSibling || !isInline(elm.nextSibling, true)) {
6941 remove(elm);
6942 }
6943 });
6944
6945 // range.toString() doesn't include newlines so can't use that.
6946 // selection.toString() seems to use the same method as innerText
6947 // but needs to be normalised first so using container.innerText
6948 appendChild(wysiwygBody, container);
6949 e.clipboardData.setData('text/plain', container.innerText);
6950 remove(container);
6951
6952 if (e.type === 'cut') {
6953 range.deleteContents();
6954 }
6955
6956 e.preventDefault();
6957 }
6958 };
6959
6960 /**
6961 * Handles the WYSIWYG editors paste event
6962 * @private
6963 */
6964 handlePasteEvt = function (e) {
6965 var editable = wysiwygBody;
6966 var clipboard = e.clipboardData;
6967 var loadImage = function (file) {
6968 var reader = new FileReader();
6969 reader.onload = function (e) {
6970 handlePasteData({
6971 html: '<img src="' + e.target.result + '" />'
6972 });
6973 };
6974 reader.readAsDataURL(file);
6975 };
6976
6977 // Modern browsers with clipboard API - everything other than _very_
6978 // old android web views and UC browser which doesn't support the
6979 // paste event at all.
6980 if (clipboard) {
6981 var data = {};
6982 var types = clipboard.types;
6983 var items = clipboard.items;
6984
6985 e.preventDefault();
6986
6987 for (var i = 0; i < types.length; i++) {
6988 // Word sometimes adds copied text as an image so if HTML
6989 // exists prefer that over images
6990 if (types.indexOf('text/html') < 0) {
6991 // Normalise image pasting to paste as a data-uri
6992 if (globalWin.FileReader && items &&
6993 IMAGE_MIME_REGEX.test(items[i].type)) {
6994 return loadImage(clipboard.items[i].getAsFile());
6995 }
6996 }
6997
6998 data[types[i]] = clipboard.getData(types[i]);
6999 }
7000 // Call plugins here with file?
7001 data.text = data['text/plain'];
7002 data.html = sanitize(data['text/html']);
7003
7004 handlePasteData(data);
7005 // If contentsFragment exists then we are already waiting for a
7006 // previous paste so let the handler for that handle this one too
7007 } else if (!pasteContentFragment) {
7008 // Save the scroll position so can be restored
7009 // when contents is restored
7010 var scrollTop = editable.scrollTop;
7011
7012 rangeHelper.saveRange();
7013
7014 pasteContentFragment = globalDoc.createDocumentFragment();
7015 while (editable.firstChild) {
7016 appendChild(pasteContentFragment, editable.firstChild);
7017 }
7018
7019 setTimeout(function () {
7020 var html = editable.innerHTML;
7021
7022 editable.innerHTML = '';
7023 appendChild(editable, pasteContentFragment);
7024 editable.scrollTop = scrollTop;
7025 pasteContentFragment = false;
7026
7027 rangeHelper.restoreRange();
7028
7029 handlePasteData({ html: sanitize(html) });
7030 }, 0);
7031 }
7032 };
7033
7034 /**
7035 * Gets the pasted data, filters it and then inserts it.
7036 * @param {Object} data
7037 * @private
7038 */
7039 handlePasteData = function (data) {
7040 var pasteArea = createElement('div', {}, wysiwygDocument);
7041
7042 pluginManager.call('pasteRaw', data);
7043 trigger(editorContainer, 'pasteraw', data);
7044
7045 if (data.html) {
7046 // Sanitize again in case plugins modified the HTML
7047 pasteArea.innerHTML = sanitize(data.html);
7048
7049 // fix any invalid nesting
7050 fixNesting(pasteArea);
7051 } else {
7052 pasteArea.innerHTML = entities(data.text || '');
7053 }
7054
7055 var paste = {
7056 val: pasteArea.innerHTML
7057 };
7058
7059 if ('fragmentToSource' in format) {
7060 paste.val = format
7061 .fragmentToSource(paste.val, wysiwygDocument, currentNode);
7062 }
7063
7064 pluginManager.call('paste', paste);
7065 trigger(editorContainer, 'paste', paste);
7066
7067 if ('fragmentToHtml' in format) {
7068 paste.val = format
7069 .fragmentToHtml(paste.val, currentNode);
7070 }
7071
7072 pluginManager.call('pasteHtml', paste);
7073
7074 var parent = rangeHelper.getFirstBlockParent();
7075 base.wysiwygEditorInsertHtml(paste.val, null, true);
7076 merge(parent);
7077 };
7078
7079 /**
7080 * Closes any currently open drop down
7081 *
7082 * @param {boolean} [focus=false] If to focus the editor
7083 * after closing the drop down
7084 * @function
7085 * @name closeDropDown
7086 * @memberOf SCEditor.prototype
7087 */
7088 base.closeDropDown = function (focus) {
7089 if (dropdown) {
7090 remove(dropdown);
7091 dropdown = null;
7092 }
7093
7094 if (focus === true) {
7095 base.focus();
7096 }
7097 };
7098
7099
7100 /**
7101 * Inserts HTML into WYSIWYG editor.
7102 *
7103 * If endHtml is specified, any selected text will be placed
7104 * between html and endHtml. If there is no selected text html
7105 * and endHtml will just be concatenate together.
7106 *
7107 * @param {string} html
7108 * @param {string} [endHtml=null]
7109 * @param {boolean} [overrideCodeBlocking=false] If to insert the html
7110 * into code tags, by
7111 * default code tags only
7112 * support text.
7113 * @function
7114 * @name wysiwygEditorInsertHtml
7115 * @memberOf SCEditor.prototype
7116 */
7117 base.wysiwygEditorInsertHtml = function (
7118 html, endHtml, overrideCodeBlocking
7119 ) {
7120 var marker, scrollTop, scrollTo,
7121 editorHeight = height(wysiwygEditor);
7122
7123 base.focus();
7124
7125 // TODO: This code tag should be configurable and
7126 // should maybe convert the HTML into text instead
7127 // Don't apply to code elements
7128 if (!overrideCodeBlocking && closest(currentBlockNode, 'code')) {
7129 return;
7130 }
7131
7132 // Insert the HTML and save the range so the editor can be scrolled
7133 // to the end of the selection. Also allows emoticons to be replaced
7134 // without affecting the cursor position
7135 rangeHelper.insertHTML(html, endHtml);
7136 rangeHelper.saveRange();
7137 replaceEmoticons();
7138
7139 // Fix any invalid nesting, e.g. if a quote or other block is inserted
7140 // into a paragraph
7141 fixNesting(wysiwygBody);
7142
7143 // Scroll the editor after the end of the selection
7144 marker = find(wysiwygBody, '#sceditor-end-marker')[0];
7145 show(marker);
7146 scrollTop = wysiwygBody.scrollTop;
7147 scrollTo = (getOffset(marker).top +
7148 (marker.offsetHeight * 1.5)) - editorHeight;
7149 hide(marker);
7150
7151 // Only scroll if marker isn't already visible
7152 if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) {
7153 wysiwygBody.scrollTop = scrollTo;
7154 }
7155
7156 triggerValueChanged(false);
7157 rangeHelper.restoreRange();
7158
7159 // Add a new line after the last block element
7160 // so can always add text after it
7161 appendNewLine();
7162 };
7163
7164 /**
7165 * Like wysiwygEditorInsertHtml except it will convert any HTML
7166 * into text before inserting it.
7167 *
7168 * @param {string} text
7169 * @param {string} [endText=null]
7170 * @function
7171 * @name wysiwygEditorInsertText
7172 * @memberOf SCEditor.prototype
7173 */
7174 base.wysiwygEditorInsertText = function (text, endText) {
7175 base.wysiwygEditorInsertHtml(
7176 entities(text), entities(endText)
7177 );
7178 };
7179
7180 /**
7181 * Inserts text into the WYSIWYG or source editor depending on which
7182 * mode the editor is in.
7183 *
7184 * If endText is specified any selected text will be placed between
7185 * text and endText. If no text is selected text and endText will
7186 * just be concatenate together.
7187 *
7188 * @param {string} text
7189 * @param {string} [endText=null]
7190 * @since 1.3.5
7191 * @function
7192 * @name insertText
7193 * @memberOf SCEditor.prototype
7194 */
7195 base.insertText = function (text, endText) {
7196 if (base.inSourceMode()) {
7197 base.sourceEditorInsertText(text, endText);
7198 } else {
7199 base.wysiwygEditorInsertText(text, endText);
7200 }
7201
7202 return base;
7203 };
7204
7205 /**
7206 * Like wysiwygEditorInsertHtml but inserts text into the
7207 * source mode editor instead.
7208 *
7209 * If endText is specified any selected text will be placed between
7210 * text and endText. If no text is selected text and endText will
7211 * just be concatenate together.
7212 *
7213 * The cursor will be placed after the text param. If endText is
7214 * specified the cursor will be placed before endText, so passing:<br />
7215 *
7216 * '[b]', '[/b]'
7217 *
7218 * Would cause the cursor to be placed:<br />
7219 *
7220 * [b]Selected text|[/b]
7221 *
7222 * @param {string} text
7223 * @param {string} [endText=null]
7224 * @since 1.4.0
7225 * @function
7226 * @name sourceEditorInsertText
7227 * @memberOf SCEditor.prototype
7228 */
7229 base.sourceEditorInsertText = function (text, endText) {
7230 var scrollTop, currentValue,
7231 startPos = sourceEditor.selectionStart,
7232 endPos = sourceEditor.selectionEnd;
7233
7234 scrollTop = sourceEditor.scrollTop;
7235 sourceEditor.focus();
7236 currentValue = sourceEditor.value;
7237
7238 if (endText) {
7239 text += currentValue.substring(startPos, endPos) + endText;
7240 }
7241
7242 sourceEditor.value = currentValue.substring(0, startPos) +
7243 text +
7244 currentValue.substring(endPos, currentValue.length);
7245
7246 sourceEditor.selectionStart = (startPos + text.length) -
7247 (endText ? endText.length : 0);
7248 sourceEditor.selectionEnd = sourceEditor.selectionStart;
7249
7250 sourceEditor.scrollTop = scrollTop;
7251 sourceEditor.focus();
7252
7253 triggerValueChanged();
7254 };
7255
7256 /**
7257 * Gets the current instance of the rangeHelper class
7258 * for the editor.
7259 *
7260 * @return {RangeHelper}
7261 * @function
7262 * @name getRangeHelper
7263 * @memberOf SCEditor.prototype
7264 */
7265 base.getRangeHelper = function () {
7266 return rangeHelper;
7267 };
7268
7269 /**
7270 * Gets or sets the source editor caret position.
7271 *
7272 * @param {Object} [position]
7273 * @return {this}
7274 * @function
7275 * @since 1.4.5
7276 * @name sourceEditorCaret
7277 * @memberOf SCEditor.prototype
7278 */
7279 base.sourceEditorCaret = function (position) {
7280 sourceEditor.focus();
7281
7282 if (position) {
7283 sourceEditor.selectionStart = position.start;
7284 sourceEditor.selectionEnd = position.end;
7285
7286 return this;
7287 }
7288
7289 return {
7290 start: sourceEditor.selectionStart,
7291 end: sourceEditor.selectionEnd
7292 };
7293 };
7294
7295 /**
7296 * Gets the value of the editor.
7297 *
7298 * If the editor is in WYSIWYG mode it will return the filtered
7299 * HTML from it (converted to BBCode if using the BBCode plugin).
7300 * It it's in Source Mode it will return the unfiltered contents
7301 * of the source editor (if using the BBCode plugin this will be
7302 * BBCode again).
7303 *
7304 * @since 1.3.5
7305 * @return {string}
7306 * @function
7307 * @name val
7308 * @memberOf SCEditor.prototype
7309 */
7310 /**
7311 * Sets the value of the editor.
7312 *
7313 * If filter set true the val will be passed through the filter
7314 * function. If using the BBCode plugin it will pass the val to
7315 * the BBCode filter to convert any BBCode into HTML.
7316 *
7317 * @param {string} val
7318 * @param {boolean} [filter=true]
7319 * @return {this}
7320 * @since 1.3.5
7321 * @function
7322 * @name val^2
7323 * @memberOf SCEditor.prototype
7324 */
7325 base.val = function (val, filter) {
7326 if (!isString(val)) {
7327 return base.inSourceMode() ?
7328 base.getSourceEditorValue(false) :
7329 base.getWysiwygEditorValue(filter);
7330 }
7331
7332 if (!base.inSourceMode()) {
7333 if (filter !== false && 'toHtml' in format) {
7334 val = format.toHtml(val);
7335 }
7336
7337 base.setWysiwygEditorValue(val);
7338 } else {
7339 base.setSourceEditorValue(val);
7340 }
7341
7342 return base;
7343 };
7344
7345 /**
7346 * Inserts HTML/BBCode into the editor
7347 *
7348 * If end is supplied any selected text will be placed between
7349 * start and end. If there is no selected text start and end
7350 * will be concatenate together.
7351 *
7352 * If the filter param is set to true, the HTML/BBCode will be
7353 * passed through any plugin filters. If using the BBCode plugin
7354 * this will convert any BBCode into HTML.
7355 *
7356 * @param {string} start
7357 * @param {string} [end=null]
7358 * @param {boolean} [filter=true]
7359 * @param {boolean} [convertEmoticons=true] If to convert emoticons
7360 * @return {this}
7361 * @since 1.3.5
7362 * @function
7363 * @name insert
7364 * @memberOf SCEditor.prototype
7365 */
7366 /**
7367 * Inserts HTML/BBCode into the editor
7368 *
7369 * If end is supplied any selected text will be placed between
7370 * start and end. If there is no selected text start and end
7371 * will be concatenate together.
7372 *
7373 * If the filter param is set to true, the HTML/BBCode will be
7374 * passed through any plugin filters. If using the BBCode plugin
7375 * this will convert any BBCode into HTML.
7376 *
7377 * If the allowMixed param is set to true, HTML any will not be
7378 * escaped
7379 *
7380 * @param {string} start
7381 * @param {string} [end=null]
7382 * @param {boolean} [filter=true]
7383 * @param {boolean} [convertEmoticons=true] If to convert emoticons
7384 * @param {boolean} [allowMixed=false]
7385 * @return {this}
7386 * @since 1.4.3
7387 * @function
7388 * @name insert^2
7389 * @memberOf SCEditor.prototype
7390 */
7391 // eslint-disable-next-line max-params
7392 base.insert = function (
7393 start, end, filter, convertEmoticons, allowMixed
7394 ) {
7395 if (base.inSourceMode()) {
7396 base.sourceEditorInsertText(start, end);
7397 return base;
7398 }
7399
7400 // Add the selection between start and end
7401 if (end) {
7402 var html = rangeHelper.selectedHtml();
7403
7404 if (filter !== false && 'fragmentToSource' in format) {
7405 html = format
7406 .fragmentToSource(html, wysiwygDocument, currentNode);
7407 }
7408
7409 start += html + end;
7410 }
7411 // TODO: This filter should allow empty tags as it's inserting.
7412 if (filter !== false && 'fragmentToHtml' in format) {
7413 start = format.fragmentToHtml(start, currentNode);
7414 }
7415
7416 // Convert any escaped HTML back into HTML if mixed is allowed
7417 if (filter !== false && allowMixed === true) {
7418 start = start.replace(/&lt;/g, '<')
7419 .replace(/&gt;/g, '>')
7420 .replace(/&amp;/g, '&');
7421 }
7422
7423 base.wysiwygEditorInsertHtml(start);
7424
7425 return base;
7426 };
7427
7428 /**
7429 * Gets the WYSIWYG editors HTML value.
7430 *
7431 * If using a plugin that filters the Ht Ml like the BBCode plugin
7432 * it will return the result of the filtering (BBCode) unless the
7433 * filter param is set to false.
7434 *
7435 * @param {boolean} [filter=true]
7436 * @return {string}
7437 * @function
7438 * @name getWysiwygEditorValue
7439 * @memberOf SCEditor.prototype
7440 */
7441 base.getWysiwygEditorValue = function (filter) {
7442 var html;
7443 // Create a tmp node to store contents so it can be modified
7444 // without affecting anything else.
7445 var tmp = createElement('div', {}, wysiwygDocument);
7446 var childNodes = wysiwygBody.childNodes;
7447
7448 for (var i = 0; i < childNodes.length; i++) {
7449 appendChild(tmp, childNodes[i].cloneNode(true));
7450 }
7451
7452 appendChild(wysiwygBody, tmp);
7453 fixNesting(tmp);
7454 remove(tmp);
7455
7456 html = tmp.innerHTML;
7457
7458 // filter the HTML and DOM through any plugins
7459 if (filter !== false && format.hasOwnProperty('toSource')) {
7460 html = format.toSource(html, wysiwygDocument);
7461 }
7462
7463 return html;
7464 };
7465
7466 /**
7467 * Gets the WYSIWYG editor's iFrame Body.
7468 *
7469 * @return {HTMLElement}
7470 * @function
7471 * @since 1.4.3
7472 * @name getBody
7473 * @memberOf SCEditor.prototype
7474 */
7475 base.getBody = function () {
7476 return wysiwygBody;
7477 };
7478
7479 /**
7480 * Gets the WYSIWYG editors container area (whole iFrame).
7481 *
7482 * @return {HTMLElement}
7483 * @function
7484 * @since 1.4.3
7485 * @name getContentAreaContainer
7486 * @memberOf SCEditor.prototype
7487 */
7488 base.getContentAreaContainer = function () {
7489 return wysiwygEditor;
7490 };
7491
7492 /**
7493 * Gets the text editor value
7494 *
7495 * If using a plugin that filters the text like the BBCode plugin
7496 * it will return the result of the filtering which is BBCode to
7497 * HTML so it will return HTML. If filter is set to false it will
7498 * just return the contents of the source editor (BBCode).
7499 *
7500 * @param {boolean} [filter=true]
7501 * @return {string}
7502 * @function
7503 * @since 1.4.0
7504 * @name getSourceEditorValue
7505 * @memberOf SCEditor.prototype
7506 */
7507 base.getSourceEditorValue = function (filter) {
7508 var val = sourceEditor.value;
7509
7510 if (filter !== false && 'toHtml' in format) {
7511 val = format.toHtml(val);
7512 }
7513
7514 return val;
7515 };
7516
7517 /**
7518 * Sets the WYSIWYG HTML editor value. Should only be the HTML
7519 * contained within the body tags
7520 *
7521 * @param {string} value
7522 * @function
7523 * @name setWysiwygEditorValue
7524 * @memberOf SCEditor.prototype
7525 */
7526 base.setWysiwygEditorValue = function (value) {
7527 if (!value) {
7528 value = '<p><br /></p>';
7529 }
7530
7531 wysiwygBody.innerHTML = sanitize(value);
7532 replaceEmoticons();
7533
7534 appendNewLine();
7535 triggerValueChanged();
7536 autoExpand();
7537 };
7538
7539 /**
7540 * Sets the text editor value
7541 *
7542 * @param {string} value
7543 * @function
7544 * @name setSourceEditorValue
7545 * @memberOf SCEditor.prototype
7546 */
7547 base.setSourceEditorValue = function (value) {
7548 sourceEditor.value = value;
7549
7550 triggerValueChanged();
7551 };
7552
7553 /**
7554 * Updates the textarea that the editor is replacing
7555 * with the value currently inside the editor.
7556 *
7557 * @function
7558 * @name updateOriginal
7559 * @since 1.4.0
7560 * @memberOf SCEditor.prototype
7561 */
7562 base.updateOriginal = function () {
7563 original.value = base.val();
7564 };
7565
7566 /**
7567 * Replaces any emoticon codes in the passed HTML
7568 * with their emoticon images
7569 * @private
7570 */
7571 replaceEmoticons = function () {
7572 if (options.emoticonsEnabled) {
7573 replace(wysiwygBody, allEmoticons, options.emoticonsCompat);
7574 }
7575 };
7576
7577 /**
7578 * If the editor is in source code mode
7579 *
7580 * @return {boolean}
7581 * @function
7582 * @name inSourceMode
7583 * @memberOf SCEditor.prototype
7584 */
7585 base.inSourceMode = function () {
7586 return hasClass(editorContainer, 'sourceMode');
7587 };
7588
7589 /**
7590 * Gets if the editor is in sourceMode
7591 *
7592 * @return boolean
7593 * @function
7594 * @name sourceMode
7595 * @memberOf SCEditor.prototype
7596 */
7597 /**
7598 * Sets if the editor is in sourceMode
7599 *
7600 * @param {boolean} enable
7601 * @return {this}
7602 * @function
7603 * @name sourceMode^2
7604 * @memberOf SCEditor.prototype
7605 */
7606 base.sourceMode = function (enable) {
7607 var inSourceMode = base.inSourceMode();
7608
7609 if (typeof enable !== 'boolean') {
7610 return inSourceMode;
7611 }
7612
7613 if ((inSourceMode && !enable) || (!inSourceMode && enable)) {
7614 base.toggleSourceMode();
7615 }
7616
7617 return base;
7618 };
7619
7620 /**
7621 * Switches between the WYSIWYG and source modes
7622 *
7623 * @function
7624 * @name toggleSourceMode
7625 * @since 1.4.0
7626 * @memberOf SCEditor.prototype
7627 */
7628 base.toggleSourceMode = function () {
7629 var isInSourceMode = base.inSourceMode();
7630
7631 // don't allow switching to WYSIWYG if doesn't support it
7632 if (!isWysiwygSupported && isInSourceMode) {
7633 return;
7634 }
7635
7636 if (!isInSourceMode) {
7637 rangeHelper.saveRange();
7638 rangeHelper.clear();
7639 }
7640
7641 currentSelection = null;
7642 base.blur();
7643
7644 if (isInSourceMode) {
7645 base.setWysiwygEditorValue(base.getSourceEditorValue());
7646 } else {
7647 base.setSourceEditorValue(base.getWysiwygEditorValue());
7648 }
7649
7650 toggle(sourceEditor);
7651 toggle(wysiwygEditor);
7652
7653 toggleClass(editorContainer, 'wysiwygMode', isInSourceMode);
7654 toggleClass(editorContainer, 'sourceMode', !isInSourceMode);
7655
7656 updateToolBar();
7657 updateActiveButtons();
7658 };
7659
7660 /**
7661 * Gets the selected text of the source editor
7662 * @return {string}
7663 * @private
7664 */
7665 sourceEditorSelectedText = function () {
7666 sourceEditor.focus();
7667
7668 return sourceEditor.value.substring(
7669 sourceEditor.selectionStart,
7670 sourceEditor.selectionEnd
7671 );
7672 };
7673
7674 /**
7675 * Handles the passed command
7676 * @private
7677 */
7678 handleCommand = function (caller, cmd) {
7679 // check if in text mode and handle text commands
7680 if (base.inSourceMode()) {
7681 if (cmd.txtExec) {
7682 if (Array.isArray(cmd.txtExec)) {
7683 base.sourceEditorInsertText.apply(base, cmd.txtExec);
7684 } else {
7685 cmd.txtExec.call(base, caller, sourceEditorSelectedText());
7686 }
7687 }
7688 } else if (cmd.exec) {
7689 if (isFunction(cmd.exec)) {
7690 cmd.exec.call(base, caller);
7691 } else {
7692 base.execCommand(
7693 cmd.exec,
7694 cmd.hasOwnProperty('execParam') ? cmd.execParam : null
7695 );
7696 }
7697 }
7698
7699 };
7700
7701 /**
7702 * Executes a command on the WYSIWYG editor
7703 *
7704 * @param {string} command
7705 * @param {String|Boolean} [param]
7706 * @function
7707 * @name execCommand
7708 * @memberOf SCEditor.prototype
7709 */
7710 base.execCommand = function (command, param) {
7711 var executed = false,
7712 commandObj = base.commands[command];
7713
7714 base.focus();
7715
7716 // TODO: make configurable
7717 // don't apply any commands to code elements
7718 if (closest(rangeHelper.parentNode(), 'code')) {
7719 return;
7720 }
7721
7722 try {
7723 executed = wysiwygDocument.execCommand(command, false, param);
7724 } catch (ex) { }
7725
7726 // show error if execution failed and an error message exists
7727 if (!executed && commandObj && commandObj.errorMessage) {
7728 /*global alert:false*/
7729 alert(base._(commandObj.errorMessage));
7730 }
7731
7732 updateActiveButtons();
7733 };
7734
7735 /**
7736 * Checks if the current selection has changed and triggers
7737 * the selectionchanged event if it has.
7738 *
7739 * In browsers other that don't support selectionchange event it will check
7740 * at most once every 100ms.
7741 * @private
7742 */
7743 checkSelectionChanged = function () {
7744 function check() {
7745 // Don't create new selection if there isn't one (like after
7746 // blur event in iOS)
7747 if (wysiwygWindow.getSelection() &&
7748 wysiwygWindow.getSelection().rangeCount <= 0) {
7749 currentSelection = null;
7750 // rangeHelper could be null if editor was destroyed
7751 // before the timeout had finished
7752 } else if (rangeHelper && !rangeHelper.compare(currentSelection)) {
7753 currentSelection = rangeHelper.cloneSelected();
7754
7755 // If the selection is in an inline wrap it in a block.
7756 // Fixes #331
7757 if (currentSelection && currentSelection.collapsed) {
7758 var parent = currentSelection.startContainer;
7759 var offset = currentSelection.startOffset;
7760
7761 // Handle if selection is placed before/after an element
7762 if (offset && parent.nodeType !== TEXT_NODE) {
7763 parent = parent.childNodes[offset];
7764 }
7765
7766 while (parent && parent.parentNode !== wysiwygBody) {
7767 parent = parent.parentNode;
7768 }
7769
7770 if (parent && isInline(parent, true)) {
7771 rangeHelper.saveRange();
7772 wrapInlines(wysiwygBody, wysiwygDocument);
7773 rangeHelper.restoreRange();
7774 }
7775 }
7776
7777 trigger(editorContainer, 'selectionchanged');
7778 }
7779
7780 isSelectionCheckPending = false;
7781 }
7782
7783 if (isSelectionCheckPending) {
7784 return;
7785 }
7786
7787 isSelectionCheckPending = true;
7788
7789 // Don't need to limit checking if browser supports the Selection API
7790 if ('onselectionchange' in wysiwygDocument) {
7791 check();
7792 } else {
7793 setTimeout(check, 100);
7794 }
7795 };
7796
7797 /**
7798 * Checks if the current node has changed and triggers
7799 * the nodechanged event if it has
7800 * @private
7801 */
7802 checkNodeChanged = function () {
7803 // check if node has changed
7804 var oldNode,
7805 node = rangeHelper.parentNode();
7806
7807 if (currentNode !== node) {
7808 oldNode = currentNode;
7809 currentNode = node;
7810 currentBlockNode = rangeHelper.getFirstBlockParent(node);
7811
7812 trigger(editorContainer, 'nodechanged', {
7813 oldNode: oldNode,
7814 newNode: currentNode
7815 });
7816 }
7817 };
7818
7819 /**
7820 * Gets the current node that contains the selection/caret in
7821 * WYSIWYG mode.
7822 *
7823 * Will be null in sourceMode or if there is no selection.
7824 *
7825 * @return {?Node}
7826 * @function
7827 * @name currentNode
7828 * @memberOf SCEditor.prototype
7829 */
7830 base.currentNode = function () {
7831 return currentNode;
7832 };
7833
7834 /**
7835 * Gets the first block level node that contains the
7836 * selection/caret in WYSIWYG mode.
7837 *
7838 * Will be null in sourceMode or if there is no selection.
7839 *
7840 * @return {?Node}
7841 * @function
7842 * @name currentBlockNode
7843 * @memberOf SCEditor.prototype
7844 * @since 1.4.4
7845 */
7846 base.currentBlockNode = function () {
7847 return currentBlockNode;
7848 };
7849
7850 /**
7851 * Updates if buttons are active or not
7852 * @private
7853 */
7854 updateActiveButtons = function () {
7855 var firstBlock, parent;
7856 var activeClass = 'active';
7857 var doc = wysiwygDocument;
7858 var isSource = base.sourceMode();
7859
7860 if (base.readOnly()) {
7861 each(find(toolbar, activeClass), function (_, menuItem) {
7862 removeClass(menuItem, activeClass);
7863 });
7864 return;
7865 }
7866
7867 if (!isSource) {
7868 parent = rangeHelper.parentNode();
7869 firstBlock = rangeHelper.getFirstBlockParent(parent);
7870 }
7871
7872 for (var j = 0; j < btnStateHandlers.length; j++) {
7873 var state = 0;
7874 var btn = toolbarButtons[btnStateHandlers[j].name];
7875 var stateFn = btnStateHandlers[j].state;
7876 var isDisabled = (isSource && !btn._sceTxtMode) ||
7877 (!isSource && !btn._sceWysiwygMode);
7878
7879 if (isString(stateFn)) {
7880 if (!isSource) {
7881 try {
7882 state = doc.queryCommandEnabled(stateFn) ? 0 : -1;
7883
7884 // eslint-disable-next-line max-depth
7885 if (state > -1) {
7886 state = doc.queryCommandState(stateFn) ? 1 : 0;
7887 }
7888 } catch (ex) {}
7889 }
7890 } else if (!isDisabled) {
7891 state = stateFn.call(base, parent, firstBlock);
7892 }
7893
7894 toggleClass(btn, 'disabled', isDisabled || state < 0);
7895 toggleClass(btn, activeClass, state > 0);
7896 }
7897
7898 if (icons && icons.update) {
7899 icons.update(isSource, parent, firstBlock);
7900 }
7901 };
7902
7903 /**
7904 * Handles any key press in the WYSIWYG editor
7905 *
7906 * @private
7907 */
7908 handleKeyPress = function (e) {
7909 // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496
7910 if (e.defaultPrevented) {
7911 return;
7912 }
7913
7914 base.closeDropDown();
7915
7916 // 13 = enter key
7917 if (e.which === 13) {
7918 var LIST_TAGS = 'li,ul,ol';
7919
7920 // "Fix" (cludge) for blocklevel elements being duplicated in some
7921 // browsers when enter is pressed instead of inserting a newline
7922 if (!is(currentBlockNode, LIST_TAGS) &&
7923 hasStyling(currentBlockNode)) {
7924
7925 var br = createElement('br', {}, wysiwygDocument);
7926 rangeHelper.insertNode(br);
7927
7928 // Last <br> of a block will be collapsed so need to make sure
7929 // the <br> that was inserted isn't the last node of a block.
7930 var parent = br.parentNode;
7931 var lastChild = parent.lastChild;
7932
7933 // Sometimes an empty next node is created after the <br>
7934 if (lastChild && lastChild.nodeType === TEXT_NODE &&
7935 lastChild.nodeValue === '') {
7936 remove(lastChild);
7937 lastChild = parent.lastChild;
7938 }
7939
7940 // If this is the last BR of a block and the previous
7941 // sibling is inline then will need an extra BR. This
7942 // is needed because the last BR of a block will be
7943 // collapsed. Fixes issue #248
7944 if (!isInline(parent, true) && lastChild === br &&
7945 isInline(br.previousSibling)) {
7946 rangeHelper.insertHTML('<br>');
7947 }
7948
7949 e.preventDefault();
7950 }
7951 }
7952 };
7953
7954 /**
7955 * Makes sure that if there is a code or quote tag at the
7956 * end of the editor, that there is a new line after it.
7957 *
7958 * If there wasn't a new line at the end you wouldn't be able
7959 * to enter any text after a code/quote tag
7960 * @return {void}
7961 * @private
7962 */
7963 appendNewLine = function () {
7964 // Check all nodes in reverse until either add a new line
7965 // or reach a non-empty textnode or BR at which point can
7966 // stop checking.
7967 rTraverse(wysiwygBody, function (node) {
7968 // Last block, add new line after if has styling
7969 if (node.nodeType === ELEMENT_NODE &&
7970 !/inline/.test(css(node, 'display'))) {
7971
7972 // Add line break after if has styling
7973 if (!is(node, '.sceditor-nlf') && hasStyling(node)) {
7974 var paragraph = createElement('p', {}, wysiwygDocument);
7975 paragraph.className = 'sceditor-nlf';
7976 paragraph.innerHTML = '<br />';
7977 appendChild(wysiwygBody, paragraph);
7978 return false;
7979 }
7980 }
7981
7982 // Last non-empty text node or line break.
7983 // No need to add line-break after them
7984 if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) ||
7985 is(node, 'br')) {
7986 return false;
7987 }
7988 });
7989 };
7990
7991 /**
7992 * Handles form reset event
7993 * @private
7994 */
7995 handleFormReset = function () {
7996 base.val(original.value);
7997 };
7998
7999 /**
8000 * Handles any mousedown press in the WYSIWYG editor
8001 * @private
8002 */
8003 handleMouseDown = function () {
8004 base.closeDropDown();
8005 };
8006
8007 /**
8008 * Translates the string into the locale language.
8009 *
8010 * Replaces any {0}, {1}, {2}, ect. with the params provided.
8011 *
8012 * @param {string} str
8013 * @param {...String} args
8014 * @return {string}
8015 * @function
8016 * @name _
8017 * @memberOf SCEditor.prototype
8018 */
8019 base._ = function () {
8020 var undef,
8021 args = arguments;
8022
8023 if (locale && locale[args[0]]) {
8024 args[0] = locale[args[0]];
8025 }
8026
8027 return args[0].replace(/\{(\d+)\}/g, function (str, p1) {
8028 return args[p1 - 0 + 1] !== undef ?
8029 args[p1 - 0 + 1] :
8030 '{' + p1 + '}';
8031 });
8032 };
8033
8034 /**
8035 * Passes events on to any handlers
8036 * @private
8037 * @return void
8038 */
8039 handleEvent = function (e) {
8040 if (pluginManager) {
8041 // Send event to all plugins
8042 pluginManager.call(e.type + 'Event', e, base);
8043 }
8044
8045 // convert the event into a custom event to send
8046 var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type;
8047
8048 if (eventHandlers[name]) {
8049 eventHandlers[name].forEach(function (fn) {
8050 fn.call(base, e);
8051 });
8052 }
8053 };
8054
8055 /**
8056 * Binds a handler to the specified events
8057 *
8058 * This function only binds to a limited list of
8059 * supported events.
8060 *
8061 * The supported events are:
8062 *
8063 * * keyup
8064 * * keydown
8065 * * Keypress
8066 * * blur
8067 * * focus
8068 * * input
8069 * * nodechanged - When the current node containing
8070 * the selection changes in WYSIWYG mode
8071 * * contextmenu
8072 * * selectionchanged
8073 * * valuechanged
8074 *
8075 *
8076 * The events param should be a string containing the event(s)
8077 * to bind this handler to. If multiple, they should be separated
8078 * by spaces.
8079 *
8080 * @param {string} events
8081 * @param {Function} handler
8082 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8083 * to the WYSIWYG editor
8084 * @param {boolean} excludeSource if to exclude adding this handler
8085 * to the source editor
8086 * @return {this}
8087 * @function
8088 * @name bind
8089 * @memberOf SCEditor.prototype
8090 * @since 1.4.1
8091 */
8092 base.bind = function (events, handler, excludeWysiwyg, excludeSource) {
8093 events = events.split(' ');
8094
8095 var i = events.length;
8096 while (i--) {
8097 if (isFunction(handler)) {
8098 var wysEvent = 'scewys' + events[i];
8099 var srcEvent = 'scesrc' + events[i];
8100 // Use custom events to allow passing the instance as the
8101 // 2nd argument.
8102 // Also allows unbinding without unbinding the editors own
8103 // event handlers.
8104 if (!excludeWysiwyg) {
8105 eventHandlers[wysEvent] = eventHandlers[wysEvent] || [];
8106 eventHandlers[wysEvent].push(handler);
8107 }
8108
8109 if (!excludeSource) {
8110 eventHandlers[srcEvent] = eventHandlers[srcEvent] || [];
8111 eventHandlers[srcEvent].push(handler);
8112 }
8113
8114 // Start sending value changed events
8115 if (events[i] === 'valuechanged') {
8116 triggerValueChanged.hasHandler = true;
8117 }
8118 }
8119 }
8120
8121 return base;
8122 };
8123
8124 /**
8125 * Unbinds an event that was bound using bind().
8126 *
8127 * @param {string} events
8128 * @param {Function} handler
8129 * @param {boolean} excludeWysiwyg If to exclude unbinding this
8130 * handler from the WYSIWYG editor
8131 * @param {boolean} excludeSource if to exclude unbinding this
8132 * handler from the source editor
8133 * @return {this}
8134 * @function
8135 * @name unbind
8136 * @memberOf SCEditor.prototype
8137 * @since 1.4.1
8138 * @see bind
8139 */
8140 base.unbind = function (events, handler, excludeWysiwyg, excludeSource) {
8141 events = events.split(' ');
8142
8143 var i = events.length;
8144 while (i--) {
8145 if (isFunction(handler)) {
8146 if (!excludeWysiwyg) {
8147 arrayRemove(
8148 eventHandlers['scewys' + events[i]] || [], handler);
8149 }
8150
8151 if (!excludeSource) {
8152 arrayRemove(
8153 eventHandlers['scesrc' + events[i]] || [], handler);
8154 }
8155 }
8156 }
8157
8158 return base;
8159 };
8160
8161 /**
8162 * Blurs the editors input area
8163 *
8164 * @return {this}
8165 * @function
8166 * @name blur
8167 * @memberOf SCEditor.prototype
8168 * @since 1.3.6
8169 */
8170 /**
8171 * Adds a handler to the editors blur event
8172 *
8173 * @param {Function} handler
8174 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8175 * to the WYSIWYG editor
8176 * @param {boolean} excludeSource if to exclude adding this handler
8177 * to the source editor
8178 * @return {this}
8179 * @function
8180 * @name blur^2
8181 * @memberOf SCEditor.prototype
8182 * @since 1.4.1
8183 */
8184 base.blur = function (handler, excludeWysiwyg, excludeSource) {
8185 if (isFunction(handler)) {
8186 base.bind('blur', handler, excludeWysiwyg, excludeSource);
8187 } else if (!base.sourceMode()) {
8188 wysiwygBody.blur();
8189 } else {
8190 sourceEditor.blur();
8191 }
8192
8193 return base;
8194 };
8195
8196 /**
8197 * Focuses the editors input area
8198 *
8199 * @return {this}
8200 * @function
8201 * @name focus
8202 * @memberOf SCEditor.prototype
8203 */
8204 /**
8205 * Adds an event handler to the focus event
8206 *
8207 * @param {Function} handler
8208 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8209 * to the WYSIWYG editor
8210 * @param {boolean} excludeSource if to exclude adding this handler
8211 * to the source editor
8212 * @return {this}
8213 * @function
8214 * @name focus^2
8215 * @memberOf SCEditor.prototype
8216 * @since 1.4.1
8217 */
8218 base.focus = function (handler, excludeWysiwyg, excludeSource) {
8219 if (isFunction(handler)) {
8220 base.bind('focus', handler, excludeWysiwyg, excludeSource);
8221 } else if (!base.inSourceMode()) {
8222 // Already has focus so do nothing
8223 if (find(wysiwygDocument, ':focus').length) {
8224 return;
8225 }
8226
8227 var container;
8228 var rng = rangeHelper.selectedRange();
8229
8230 // Fix FF bug where it shows the cursor in the wrong place
8231 // if the editor hasn't had focus before. See issue #393
8232 if (!currentSelection) {
8233 autofocus(true);
8234 }
8235
8236 // Check if cursor is set after a BR when the BR is the only
8237 // child of the parent. In Firefox this causes a line break
8238 // to occur when something is typed. See issue #321
8239 if (rng && rng.endOffset === 1 && rng.collapsed) {
8240 container = rng.endContainer;
8241
8242 if (container && container.childNodes.length === 1 &&
8243 is(container.firstChild, 'br')) {
8244 rng.setStartBefore(container.firstChild);
8245 rng.collapse(true);
8246 rangeHelper.selectRange(rng);
8247 }
8248 }
8249
8250 wysiwygWindow.focus();
8251 wysiwygBody.focus();
8252 } else {
8253 sourceEditor.focus();
8254 }
8255
8256 updateActiveButtons();
8257
8258 return base;
8259 };
8260
8261 /**
8262 * Adds a handler to the key down event
8263 *
8264 * @param {Function} handler
8265 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8266 * to the WYSIWYG editor
8267 * @param {boolean} excludeSource If to exclude adding this handler
8268 * to the source editor
8269 * @return {this}
8270 * @function
8271 * @name keyDown
8272 * @memberOf SCEditor.prototype
8273 * @since 1.4.1
8274 */
8275 base.keyDown = function (handler, excludeWysiwyg, excludeSource) {
8276 return base.bind('keydown', handler, excludeWysiwyg, excludeSource);
8277 };
8278
8279 /**
8280 * Adds a handler to the key press event
8281 *
8282 * @param {Function} handler
8283 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8284 * to the WYSIWYG editor
8285 * @param {boolean} excludeSource If to exclude adding this handler
8286 * to the source editor
8287 * @return {this}
8288 * @function
8289 * @name keyPress
8290 * @memberOf SCEditor.prototype
8291 * @since 1.4.1
8292 */
8293 base.keyPress = function (handler, excludeWysiwyg, excludeSource) {
8294 return base
8295 .bind('keypress', handler, excludeWysiwyg, excludeSource);
8296 };
8297
8298 /**
8299 * Adds a handler to the key up event
8300 *
8301 * @param {Function} handler
8302 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8303 * to the WYSIWYG editor
8304 * @param {boolean} excludeSource If to exclude adding this handler
8305 * to the source editor
8306 * @return {this}
8307 * @function
8308 * @name keyUp
8309 * @memberOf SCEditor.prototype
8310 * @since 1.4.1
8311 */
8312 base.keyUp = function (handler, excludeWysiwyg, excludeSource) {
8313 return base.bind('keyup', handler, excludeWysiwyg, excludeSource);
8314 };
8315
8316 /**
8317 * Adds a handler to the node changed event.
8318 *
8319 * Happens whenever the node containing the selection/caret
8320 * changes in WYSIWYG mode.
8321 *
8322 * @param {Function} handler
8323 * @return {this}
8324 * @function
8325 * @name nodeChanged
8326 * @memberOf SCEditor.prototype
8327 * @since 1.4.1
8328 */
8329 base.nodeChanged = function (handler) {
8330 return base.bind('nodechanged', handler, false, true);
8331 };
8332
8333 /**
8334 * Adds a handler to the selection changed event
8335 *
8336 * Happens whenever the selection changes in WYSIWYG mode.
8337 *
8338 * @param {Function} handler
8339 * @return {this}
8340 * @function
8341 * @name selectionChanged
8342 * @memberOf SCEditor.prototype
8343 * @since 1.4.1
8344 */
8345 base.selectionChanged = function (handler) {
8346 return base.bind('selectionchanged', handler, false, true);
8347 };
8348
8349 /**
8350 * Adds a handler to the value changed event
8351 *
8352 * Happens whenever the current editor value changes.
8353 *
8354 * Whenever anything is inserted, the value changed or
8355 * 1.5 secs after text is typed. If a space is typed it will
8356 * cause the event to be triggered immediately instead of
8357 * after 1.5 seconds
8358 *
8359 * @param {Function} handler
8360 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8361 * to the WYSIWYG editor
8362 * @param {boolean} excludeSource If to exclude adding this handler
8363 * to the source editor
8364 * @return {this}
8365 * @function
8366 * @name valueChanged
8367 * @memberOf SCEditor.prototype
8368 * @since 1.4.5
8369 */
8370 base.valueChanged = function (handler, excludeWysiwyg, excludeSource) {
8371 return base
8372 .bind('valuechanged', handler, excludeWysiwyg, excludeSource);
8373 };
8374
8375 /**
8376 * Emoticons keypress handler
8377 * @private
8378 */
8379 emoticonsKeyPress = function (e) {
8380 var replacedEmoticon,
8381 cachePos = 0,
8382 emoticonsCache = base.emoticonsCache,
8383 curChar = String.fromCharCode(e.which);
8384
8385 // TODO: Make configurable
8386 if (closest(currentBlockNode, 'code')) {
8387 return;
8388 }
8389
8390 if (!emoticonsCache) {
8391 emoticonsCache = [];
8392
8393 each(allEmoticons, function (key, html) {
8394 emoticonsCache[cachePos++] = [key, html];
8395 });
8396
8397 emoticonsCache.sort(function (a, b) {
8398 return a[0].length - b[0].length;
8399 });
8400
8401 base.emoticonsCache = emoticonsCache;
8402 base.longestEmoticonCode =
8403 emoticonsCache[emoticonsCache.length - 1][0].length;
8404 }
8405
8406 replacedEmoticon = rangeHelper.replaceKeyword(
8407 base.emoticonsCache,
8408 true,
8409 true,
8410 base.longestEmoticonCode,
8411 options.emoticonsCompat,
8412 curChar
8413 );
8414
8415 if (replacedEmoticon) {
8416 if (!options.emoticonsCompat || !/^\s$/.test(curChar)) {
8417 e.preventDefault();
8418 }
8419 }
8420 };
8421
8422 /**
8423 * Makes sure emoticons are surrounded by whitespace
8424 * @private
8425 */
8426 emoticonsCheckWhitespace = function () {
8427 checkWhitespace(currentBlockNode, rangeHelper);
8428 };
8429
8430 /**
8431 * Gets if emoticons are currently enabled
8432 * @return {boolean}
8433 * @function
8434 * @name emoticons
8435 * @memberOf SCEditor.prototype
8436 * @since 1.4.2
8437 */
8438 /**
8439 * Enables/disables emoticons
8440 *
8441 * @param {boolean} enable
8442 * @return {this}
8443 * @function
8444 * @name emoticons^2
8445 * @memberOf SCEditor.prototype
8446 * @since 1.4.2
8447 */
8448 base.emoticons = function (enable) {
8449 if (!enable && enable !== false) {
8450 return options.emoticonsEnabled;
8451 }
8452
8453 options.emoticonsEnabled = enable;
8454
8455 if (enable) {
8456 on(wysiwygBody, 'keypress', emoticonsKeyPress);
8457
8458 if (!base.sourceMode()) {
8459 rangeHelper.saveRange();
8460
8461 replaceEmoticons();
8462 triggerValueChanged(false);
8463
8464 rangeHelper.restoreRange();
8465 }
8466 } else {
8467 var emoticons =
8468 find(wysiwygBody, 'img[data-sceditor-emoticon]');
8469
8470 each(emoticons, function (_, img) {
8471 var text = data(img, 'sceditor-emoticon');
8472 var textNode = wysiwygDocument.createTextNode(text);
8473 img.parentNode.replaceChild(textNode, img);
8474 });
8475
8476 off(wysiwygBody, 'keypress', emoticonsKeyPress);
8477
8478 triggerValueChanged();
8479 }
8480
8481 return base;
8482 };
8483
8484 /**
8485 * Gets the current WYSIWYG editors inline CSS
8486 *
8487 * @return {string}
8488 * @function
8489 * @name css
8490 * @memberOf SCEditor.prototype
8491 * @since 1.4.3
8492 */
8493 /**
8494 * Sets inline CSS for the WYSIWYG editor
8495 *
8496 * @param {string} css
8497 * @return {this}
8498 * @function
8499 * @name css^2
8500 * @memberOf SCEditor.prototype
8501 * @since 1.4.3
8502 */
8503 base.css = function (css) {
8504 if (!inlineCss) {
8505 inlineCss = createElement('style', {
8506 id: 'inline'
8507 }, wysiwygDocument);
8508
8509 appendChild(wysiwygDocument.head, inlineCss);
8510 }
8511
8512 if (!isString(css)) {
8513 return inlineCss.styleSheet ?
8514 inlineCss.styleSheet.cssText : inlineCss.innerHTML;
8515 }
8516
8517 if (inlineCss.styleSheet) {
8518 inlineCss.styleSheet.cssText = css;
8519 } else {
8520 inlineCss.innerHTML = css;
8521 }
8522
8523 return base;
8524 };
8525
8526 /**
8527 * Handles the keydown event, used for shortcuts
8528 * @private
8529 */
8530 handleKeyDown = function (e) {
8531 var shortcut = [],
8532 SHIFT_KEYS = {
8533 '`': '~',
8534 '1': '!',
8535 '2': '@',
8536 '3': '#',
8537 '4': '$',
8538 '5': '%',
8539 '6': '^',
8540 '7': '&',
8541 '8': '*',
8542 '9': '(',
8543 '0': ')',
8544 '-': '_',
8545 '=': '+',
8546 ';': ': ',
8547 '\'': '"',
8548 ',': '<',
8549 '.': '>',
8550 '/': '?',
8551 '\\': '|',
8552 '[': '{',
8553 ']': '}'
8554 },
8555 SPECIAL_KEYS = {
8556 8: 'backspace',
8557 9: 'tab',
8558 13: 'enter',
8559 19: 'pause',
8560 20: 'capslock',
8561 27: 'esc',
8562 32: 'space',
8563 33: 'pageup',
8564 34: 'pagedown',
8565 35: 'end',
8566 36: 'home',
8567 37: 'left',
8568 38: 'up',
8569 39: 'right',
8570 40: 'down',
8571 45: 'insert',
8572 46: 'del',
8573 91: 'win',
8574 92: 'win',
8575 93: 'select',
8576 96: '0',
8577 97: '1',
8578 98: '2',
8579 99: '3',
8580 100: '4',
8581 101: '5',
8582 102: '6',
8583 103: '7',
8584 104: '8',
8585 105: '9',
8586 106: '*',
8587 107: '+',
8588 109: '-',
8589 110: '.',
8590 111: '/',
8591 112: 'f1',
8592 113: 'f2',
8593 114: 'f3',
8594 115: 'f4',
8595 116: 'f5',
8596 117: 'f6',
8597 118: 'f7',
8598 119: 'f8',
8599 120: 'f9',
8600 121: 'f10',
8601 122: 'f11',
8602 123: 'f12',
8603 144: 'numlock',
8604 145: 'scrolllock',
8605 186: ';',
8606 187: '=',
8607 188: ',',
8608 189: '-',
8609 190: '.',
8610 191: '/',
8611 192: '`',
8612 219: '[',
8613 220: '\\',
8614 221: ']',
8615 222: '\''
8616 },
8617 NUMPAD_SHIFT_KEYS = {
8618 109: '-',
8619 110: 'del',
8620 111: '/',
8621 96: '0',
8622 97: '1',
8623 98: '2',
8624 99: '3',
8625 100: '4',
8626 101: '5',
8627 102: '6',
8628 103: '7',
8629 104: '8',
8630 105: '9'
8631 },
8632 which = e.which,
8633 character = SPECIAL_KEYS[which] ||
8634 String.fromCharCode(which).toLowerCase();
8635
8636 if (e.ctrlKey || e.metaKey) {
8637 shortcut.push('ctrl');
8638 }
8639
8640 if (e.altKey) {
8641 shortcut.push('alt');
8642 }
8643
8644 if (e.shiftKey) {
8645 shortcut.push('shift');
8646
8647 if (NUMPAD_SHIFT_KEYS[which]) {
8648 character = NUMPAD_SHIFT_KEYS[which];
8649 } else if (SHIFT_KEYS[character]) {
8650 character = SHIFT_KEYS[character];
8651 }
8652 }
8653
8654 // Shift is 16, ctrl is 17 and alt is 18
8655 if (character && (which < 16 || which > 18)) {
8656 shortcut.push(character);
8657 }
8658
8659 shortcut = shortcut.join('+');
8660 if (shortcutHandlers[shortcut] &&
8661 shortcutHandlers[shortcut].call(base) === false) {
8662
8663 e.stopPropagation();
8664 e.preventDefault();
8665 }
8666 };
8667
8668 /**
8669 * Adds a shortcut handler to the editor
8670 * @param {string} shortcut
8671 * @param {String|Function} cmd
8672 * @return {sceditor}
8673 */
8674 base.addShortcut = function (shortcut, cmd) {
8675 shortcut = shortcut.toLowerCase();
8676
8677 if (isString(cmd)) {
8678 shortcutHandlers[shortcut] = function () {
8679 handleCommand(toolbarButtons[cmd], base.commands[cmd]);
8680
8681 return false;
8682 };
8683 } else {
8684 shortcutHandlers[shortcut] = cmd;
8685 }
8686
8687 return base;
8688 };
8689
8690 /**
8691 * Removes a shortcut handler
8692 * @param {string} shortcut
8693 * @return {sceditor}
8694 */
8695 base.removeShortcut = function (shortcut) {
8696 delete shortcutHandlers[shortcut.toLowerCase()];
8697
8698 return base;
8699 };
8700
8701 /**
8702 * Handles the backspace key press
8703 *
8704 * Will remove block styling like quotes/code ect if at the start.
8705 * @private
8706 */
8707 handleBackSpace = function (e) {
8708 var node, offset, range, parent;
8709
8710 // 8 is the backspace key
8711 if (options.disableBlockRemove || e.which !== 8 ||
8712 !(range = rangeHelper.selectedRange())) {
8713 return;
8714 }
8715
8716 node = range.startContainer;
8717 offset = range.startOffset;
8718
8719 if (offset !== 0 || !(parent = currentStyledBlockNode()) ||
8720 is(parent, 'body')) {
8721 return;
8722 }
8723
8724 while (node !== parent) {
8725 while (node.previousSibling) {
8726 node = node.previousSibling;
8727
8728 // Everything but empty text nodes before the cursor
8729 // should prevent the style from being removed
8730 if (node.nodeType !== TEXT_NODE || node.nodeValue) {
8731 return;
8732 }
8733 }
8734
8735 if (!(node = node.parentNode)) {
8736 return;
8737 }
8738 }
8739
8740 // The backspace was pressed at the start of
8741 // the container so clear the style
8742 base.clearBlockFormatting(parent);
8743 e.preventDefault();
8744 };
8745
8746 /**
8747 * Gets the first styled block node that contains the cursor
8748 * @return {HTMLElement}
8749 */
8750 currentStyledBlockNode = function () {
8751 var block = currentBlockNode;
8752
8753 while (!hasStyling(block) || isInline(block, true)) {
8754 if (!(block = block.parentNode) || is(block, 'body')) {
8755 return;
8756 }
8757 }
8758
8759 return block;
8760 };
8761
8762 /**
8763 * Clears the formatting of the passed block element.
8764 *
8765 * If block is false, if will clear the styling of the first
8766 * block level element that contains the cursor.
8767 * @param {HTMLElement} block
8768 * @since 1.4.4
8769 */
8770 base.clearBlockFormatting = function (block) {
8771 block = block || currentStyledBlockNode();
8772
8773 if (!block || is(block, 'body')) {
8774 return base;
8775 }
8776
8777 rangeHelper.saveRange();
8778
8779 block.className = '';
8780
8781 attr(block, 'style', '');
8782
8783 if (!is(block, 'p,div,td')) {
8784 convertElement(block, 'p');
8785 }
8786
8787 rangeHelper.restoreRange();
8788 return base;
8789 };
8790
8791 /**
8792 * Triggers the valueChanged signal if there is
8793 * a plugin that handles it.
8794 *
8795 * If rangeHelper.saveRange() has already been
8796 * called, then saveRange should be set to false
8797 * to prevent the range being saved twice.
8798 *
8799 * @since 1.4.5
8800 * @param {boolean} saveRange If to call rangeHelper.saveRange().
8801 * @private
8802 */
8803 triggerValueChanged = function (saveRange) {
8804 if (!pluginManager ||
8805 (!pluginManager.hasHandler('valuechangedEvent') &&
8806 !triggerValueChanged.hasHandler)) {
8807 return;
8808 }
8809
8810 var currentHtml,
8811 sourceMode = base.sourceMode(),
8812 hasSelection = !sourceMode && rangeHelper.hasSelection();
8813
8814 // Composition end isn't guaranteed to fire but must have
8815 // ended when triggerValueChanged() is called so reset it
8816 isComposing = false;
8817
8818 // Don't need to save the range if sceditor-start-marker
8819 // is present as the range is already saved
8820 saveRange = saveRange !== false &&
8821 !wysiwygDocument.getElementById('sceditor-start-marker');
8822
8823 // Clear any current timeout as it's now been triggered
8824 if (valueChangedKeyUpTimer) {
8825 clearTimeout(valueChangedKeyUpTimer);
8826 valueChangedKeyUpTimer = false;
8827 }
8828
8829 if (hasSelection && saveRange) {
8830 rangeHelper.saveRange();
8831 }
8832
8833 currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML;
8834
8835 // Only trigger if something has actually changed.
8836 if (currentHtml !== triggerValueChanged.lastVal) {
8837 triggerValueChanged.lastVal = currentHtml;
8838
8839 trigger(editorContainer, 'valuechanged', {
8840 rawValue: sourceMode ? base.val() : currentHtml
8841 });
8842 }
8843
8844 if (hasSelection && saveRange) {
8845 rangeHelper.removeMarkers();
8846 }
8847 };
8848
8849 /**
8850 * Should be called whenever there is a blur event
8851 * @private
8852 */
8853 valueChangedBlur = function () {
8854 if (valueChangedKeyUpTimer) {
8855 triggerValueChanged();
8856 }
8857 };
8858
8859 /**
8860 * Should be called whenever there is a keypress event
8861 * @param {Event} e The keypress event
8862 * @private
8863 */
8864 valueChangedKeyUp = function (e) {
8865 var which = e.which,
8866 lastChar = valueChangedKeyUp.lastChar,
8867 lastWasSpace = (lastChar === 13 || lastChar === 32),
8868 lastWasDelete = (lastChar === 8 || lastChar === 46);
8869
8870 valueChangedKeyUp.lastChar = which;
8871
8872 if (isComposing) {
8873 return;
8874 }
8875
8876 // 13 = return & 32 = space
8877 if (which === 13 || which === 32) {
8878 if (!lastWasSpace) {
8879 triggerValueChanged();
8880 } else {
8881 valueChangedKeyUp.triggerNext = true;
8882 }
8883 // 8 = backspace & 46 = del
8884 } else if (which === 8 || which === 46) {
8885 if (!lastWasDelete) {
8886 triggerValueChanged();
8887 } else {
8888 valueChangedKeyUp.triggerNext = true;
8889 }
8890 } else if (valueChangedKeyUp.triggerNext) {
8891 triggerValueChanged();
8892 valueChangedKeyUp.triggerNext = false;
8893 }
8894
8895 // Clear the previous timeout and set a new one.
8896 clearTimeout(valueChangedKeyUpTimer);
8897
8898 // Trigger the event 1.5s after the last keypress if space
8899 // isn't pressed. This might need to be lowered, will need
8900 // to look into what the slowest average Chars Per Min is.
8901 valueChangedKeyUpTimer = setTimeout(function () {
8902 if (!isComposing) {
8903 triggerValueChanged();
8904 }
8905 }, 1500);
8906 };
8907
8908 handleComposition = function (e) {
8909 isComposing = /start/i.test(e.type);
8910
8911 if (!isComposing) {
8912 triggerValueChanged();
8913 }
8914 };
8915
8916 autoUpdate = function () {
8917 base.updateOriginal();
8918 };
8919
8920 // run the initializer
8921 init();
8922 }
8923
8924 /**
8925 * Map containing the loaded SCEditor locales
8926 * @type {Object}
8927 * @name locale
8928 * @memberOf sceditor
8929 */
8930 SCEditor.locale = {};
8931
8932 SCEditor.formats = {};
8933 SCEditor.icons = {};
8934
8935
8936 /**
8937 * Static command helper class
8938 * @class command
8939 * @name sceditor.command
8940 */
8941 SCEditor.command =
8942 /** @lends sceditor.command */
8943 {
8944 /**
8945 * Gets a command
8946 *
8947 * @param {string} name
8948 * @return {Object|null}
8949 * @since v1.3.5
8950 */
8951 get: function (name) {
8952 return defaultCmds[name] || null;
8953 },
8954
8955 /**
8956 * <p>Adds a command to the editor or updates an existing
8957 * command if a command with the specified name already exists.</p>
8958 *
8959 * <p>Once a command is add it can be included in the toolbar by
8960 * adding it's name to the toolbar option in the constructor. It
8961 * can also be executed manually by calling
8962 * {@link sceditor.execCommand}</p>
8963 *
8964 * @example
8965 * SCEditor.command.set("hello",
8966 * {
8967 * exec: function () {
8968 * alert("Hello World!");
8969 * }
8970 * });
8971 *
8972 * @param {string} name
8973 * @param {Object} cmd
8974 * @return {this|false} Returns false if name or cmd is false
8975 * @since v1.3.5
8976 */
8977 set: function (name, cmd) {
8978 if (!name || !cmd) {
8979 return false;
8980 }
8981
8982 // merge any existing command properties
8983 cmd = extend(defaultCmds[name] || {}, cmd);
8984
8985 cmd.remove = function () {
8986 SCEditor.command.remove(name);
8987 };
8988
8989 defaultCmds[name] = cmd;
8990 return this;
8991 },
8992
8993 /**
8994 * Removes a command
8995 *
8996 * @param {string} name
8997 * @return {this}
8998 * @since v1.3.5
8999 */
9000 remove: function (name) {
9001 if (defaultCmds[name]) {
9002 delete defaultCmds[name];
9003 }
9004
9005 return this;
9006 }
9007 };
9008
9009 /**
9010 * SCEditor
9011 * http://www.sceditor.com/
9012 *
9013 * Copyright (C) 2017, Sam Clarke (samclarke.com)
9014 *
9015 * SCEditor is licensed under the MIT license:
9016 * http://www.opensource.org/licenses/mit-license.php
9017 *
9018 * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor
9019 * @author Sam Clarke
9020 */
9021
9022
9023 window.sceditor = {
9024 command: SCEditor.command,
9025 commands: defaultCmds,
9026 defaultOptions: defaultOptions,
9027
9028 ios: ios,
9029 isWysiwygSupported: isWysiwygSupported,
9030
9031 regexEscape: regex,
9032 escapeEntities: entities,
9033 escapeUriScheme: uriScheme,
9034
9035 dom: {
9036 css: css,
9037 attr: attr,
9038 removeAttr: removeAttr,
9039 is: is,
9040 closest: closest,
9041 width: width,
9042 height: height,
9043 traverse: traverse,
9044 rTraverse: rTraverse,
9045 parseHTML: parseHTML,
9046 hasStyling: hasStyling,
9047 convertElement: convertElement,
9048 blockLevelList: blockLevelList,
9049 canHaveChildren: canHaveChildren,
9050 isInline: isInline,
9051 copyCSS: copyCSS,
9052 fixNesting: fixNesting,
9053 findCommonAncestor: findCommonAncestor,
9054 getSibling: getSibling,
9055 removeWhiteSpace: removeWhiteSpace,
9056 extractContents: extractContents,
9057 getOffset: getOffset,
9058 getStyle: getStyle,
9059 hasStyle: hasStyle
9060 },
9061 locale: SCEditor.locale,
9062 icons: SCEditor.icons,
9063 utils: {
9064 each: each,
9065 isEmptyObject: isEmptyObject,
9066 extend: extend
9067 },
9068 plugins: PluginManager.plugins,
9069 formats: SCEditor.formats,
9070 create: function (textarea, options) {
9071 options = options || {};
9072
9073 // Don't allow the editor to be initialised
9074 // on it's own source editor
9075 if (parent(textarea, '.sceditor-container')) {
9076 return;
9077 }
9078
9079 if (options.runWithoutWysiwygSupport || isWysiwygSupported) {
9080 /*eslint no-new: off*/
9081 (new SCEditor(textarea, options));
9082 }
9083 },
9084 instance: function (textarea) {
9085 return textarea._sceditor;
9086 }
9087 };
9088
9089 }());