comparison src/development/jquery.sceditor.bbcode.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 function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
5
6 var $__default = /*#__PURE__*/_interopDefaultLegacy($);
7
8 /**
9 * Check if the passed argument is the
10 * the passed type.
11 *
12 * @param {string} type
13 * @param {*} arg
14 * @returns {boolean}
15 */
16 function isTypeof(type, arg) {
17 return typeof arg === type;
18 }
19
20 /**
21 * @type {function(*): boolean}
22 */
23 var isString = isTypeof.bind(null, 'string');
24
25 /**
26 * @type {function(*): boolean}
27 */
28 var isUndefined = isTypeof.bind(null, 'undefined');
29
30 /**
31 * @type {function(*): boolean}
32 */
33 var isFunction = isTypeof.bind(null, 'function');
34
35 /**
36 * @type {function(*): boolean}
37 */
38 var isNumber = isTypeof.bind(null, 'number');
39
40
41 /**
42 * Returns true if an object has no keys
43 *
44 * @param {!Object} obj
45 * @returns {boolean}
46 */
47 function isEmptyObject(obj) {
48 return !Object.keys(obj).length;
49 }
50
51 /**
52 * Extends the first object with any extra objects passed
53 *
54 * If the first argument is boolean and set to true
55 * it will extend child arrays and objects recursively.
56 *
57 * @param {!Object|boolean} targetArg
58 * @param {...Object} source
59 * @return {Object}
60 */
61 function extend(targetArg, sourceArg) {
62 var isTargetBoolean = targetArg === !!targetArg;
63 var i = isTargetBoolean ? 2 : 1;
64 var target = isTargetBoolean ? sourceArg : targetArg;
65 var isDeep = isTargetBoolean ? targetArg : false;
66
67 function isObject(value) {
68 return value !== null && typeof value === 'object' &&
69 Object.getPrototypeOf(value) === Object.prototype;
70 }
71
72 for (; i < arguments.length; i++) {
73 var source = arguments[i];
74
75 // Copy all properties for jQuery compatibility
76 /* eslint guard-for-in: off */
77 for (var key in source) {
78 var targetValue = target[key];
79 var value = source[key];
80
81 // Skip undefined values to match jQuery
82 if (isUndefined(value)) {
83 continue;
84 }
85
86 // Skip special keys to prevent prototype pollution
87 if (key === '__proto__' || key === 'constructor') {
88 continue;
89 }
90
91 var isValueObject = isObject(value);
92 var isValueArray = Array.isArray(value);
93
94 if (isDeep && (isValueObject || isValueArray)) {
95 // Can only merge if target type matches otherwise create
96 // new target to merge into
97 var isSameType = isObject(targetValue) === isValueObject &&
98 Array.isArray(targetValue) === isValueArray;
99
100 target[key] = extend(
101 true,
102 isSameType ? targetValue : (isValueArray ? [] : {}),
103 value
104 );
105 } else {
106 target[key] = value;
107 }
108 }
109 }
110
111 return target;
112 }
113
114 /**
115 * Removes an item from the passed array
116 *
117 * @param {!Array} arr
118 * @param {*} item
119 */
120 function arrayRemove(arr, item) {
121 var i = arr.indexOf(item);
122
123 if (i > -1) {
124 arr.splice(i, 1);
125 }
126 }
127
128 /**
129 * Iterates over an array or object
130 *
131 * @param {!Object|Array} obj
132 * @param {function(*, *)} fn
133 */
134 function each(obj, fn) {
135 if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) {
136 for (var i = 0; i < obj.length; i++) {
137 fn(i, obj[i]);
138 }
139 } else {
140 Object.keys(obj).forEach(function (key) {
141 fn(key, obj[key]);
142 });
143 }
144 }
145
146 /**
147 * Cache of camelCase CSS property names
148 * @type {Object<string, string>}
149 */
150 var cssPropertyNameCache = {};
151
152 /**
153 * Node type constant for element nodes
154 *
155 * @type {number}
156 */
157 var ELEMENT_NODE = 1;
158
159 /**
160 * Node type constant for text nodes
161 *
162 * @type {number}
163 */
164 var TEXT_NODE = 3;
165
166 /**
167 * Node type constant for comment nodes
168 *
169 * @type {number}
170 */
171 var COMMENT_NODE = 8;
172
173 function toFloat(value) {
174 value = parseFloat(value);
175
176 return isFinite(value) ? value : 0;
177 }
178
179 /**
180 * Creates an element with the specified attributes
181 *
182 * Will create it in the current document unless context
183 * is specified.
184 *
185 * @param {!string} tag
186 * @param {!Object<string, string>} [attributes]
187 * @param {!Document} [context]
188 * @returns {!HTMLElement}
189 */
190 function createElement(tag, attributes, context) {
191 var node = (context || document).createElement(tag);
192
193 each(attributes || {}, function (key, value) {
194 if (key === 'style') {
195 node.style.cssText = value;
196 } else if (key in node) {
197 node[key] = value;
198 } else {
199 node.setAttribute(key, value);
200 }
201 });
202
203 return node;
204 }
205
206 /**
207 * Gets the first parent node that matches the selector
208 *
209 * @param {!HTMLElement} node
210 * @param {!string} [selector]
211 * @returns {HTMLElement|undefined}
212 */
213 function parent(node, selector) {
214 var parent = node || {};
215
216 while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) {
217 if (!selector || is(parent, selector)) {
218 return parent;
219 }
220 }
221 }
222
223 /**
224 * Checks the passed node and all parents and
225 * returns the first matching node if any.
226 *
227 * @param {!HTMLElement} node
228 * @param {!string} selector
229 * @returns {HTMLElement|undefined}
230 */
231 function closest(node, selector) {
232 return is(node, selector) ? node : parent(node, selector);
233 }
234
235 /**
236 * Removes the node from the DOM
237 *
238 * @param {!HTMLElement} node
239 */
240 function remove(node) {
241 if (node.parentNode) {
242 node.parentNode.removeChild(node);
243 }
244 }
245
246 /**
247 * Appends child to parent node
248 *
249 * @param {!HTMLElement} node
250 * @param {!HTMLElement} child
251 */
252 function appendChild(node, child) {
253 node.appendChild(child);
254 }
255
256 /**
257 * Finds any child nodes that match the selector
258 *
259 * @param {!HTMLElement} node
260 * @param {!string} selector
261 * @returns {NodeList}
262 */
263 function find(node, selector) {
264 return node.querySelectorAll(selector);
265 }
266
267 /**
268 * For on() and off() if to add/remove the event
269 * to the capture phase
270 *
271 * @type {boolean}
272 */
273 var EVENT_CAPTURE = true;
274
275 /**
276 * Adds an event listener for the specified events.
277 *
278 * Events should be a space separated list of events.
279 *
280 * If selector is specified the handler will only be
281 * called when the event target matches the selector.
282 *
283 * @param {!Node} node
284 * @param {string} events
285 * @param {string} [selector]
286 * @param {function(Object)} fn
287 * @param {boolean} [capture=false]
288 * @see off()
289 */
290 // eslint-disable-next-line max-params
291 function on(node, events, selector, fn, capture) {
292 events.split(' ').forEach(function (event) {
293 var handler;
294
295 if (isString(selector)) {
296 handler = fn['_sce-event-' + event + selector] || function (e) {
297 var target = e.target;
298 while (target && target !== node) {
299 if (is(target, selector)) {
300 fn.call(target, e);
301 return;
302 }
303
304 target = target.parentNode;
305 }
306 };
307
308 fn['_sce-event-' + event + selector] = handler;
309 } else {
310 handler = selector;
311 capture = fn;
312 }
313
314 node.addEventListener(event, handler, capture || false);
315 });
316 }
317
318 /**
319 * Removes an event listener for the specified events.
320 *
321 * @param {!Node} node
322 * @param {string} events
323 * @param {string} [selector]
324 * @param {function(Object)} fn
325 * @param {boolean} [capture=false]
326 * @see on()
327 */
328 // eslint-disable-next-line max-params
329 function off(node, events, selector, fn, capture) {
330 events.split(' ').forEach(function (event) {
331 var handler;
332
333 if (isString(selector)) {
334 handler = fn['_sce-event-' + event + selector];
335 } else {
336 handler = selector;
337 capture = fn;
338 }
339
340 node.removeEventListener(event, handler, capture || false);
341 });
342 }
343
344 /**
345 * If only attr param is specified it will get
346 * the value of the attr param.
347 *
348 * If value is specified but null the attribute
349 * will be removed otherwise the attr value will
350 * be set to the passed value.
351 *
352 * @param {!HTMLElement} node
353 * @param {!string} attr
354 * @param {?string} [value]
355 */
356 function attr(node, attr, value) {
357 if (arguments.length < 3) {
358 return node.getAttribute(attr);
359 }
360
361 // eslint-disable-next-line eqeqeq, no-eq-null
362 if (value == null) {
363 removeAttr(node, attr);
364 } else {
365 node.setAttribute(attr, value);
366 }
367 }
368
369 /**
370 * Removes the specified attribute
371 *
372 * @param {!HTMLElement} node
373 * @param {!string} attr
374 */
375 function removeAttr(node, attr) {
376 node.removeAttribute(attr);
377 }
378
379 /**
380 * Sets the passed elements display to none
381 *
382 * @param {!HTMLElement} node
383 */
384 function hide(node) {
385 css(node, 'display', 'none');
386 }
387
388 /**
389 * Sets the passed elements display to default
390 *
391 * @param {!HTMLElement} node
392 */
393 function show(node) {
394 css(node, 'display', '');
395 }
396
397 /**
398 * Toggles an elements visibility
399 *
400 * @param {!HTMLElement} node
401 */
402 function toggle(node) {
403 if (isVisible(node)) {
404 hide(node);
405 } else {
406 show(node);
407 }
408 }
409
410 /**
411 * Gets a computed CSS values or sets an inline CSS value
412 *
413 * Rules should be in camelCase format and not
414 * hyphenated like CSS properties.
415 *
416 * @param {!HTMLElement} node
417 * @param {!Object|string} rule
418 * @param {string|number} [value]
419 * @return {string|number|undefined}
420 */
421 function css(node, rule, value) {
422 if (arguments.length < 3) {
423 if (isString(rule)) {
424 return node.nodeType === 1 ? getComputedStyle(node)[rule] : null;
425 }
426
427 each(rule, function (key, value) {
428 css(node, key, value);
429 });
430 } else {
431 // isNaN returns false for null, false and empty strings
432 // so need to check it's truthy or 0
433 var isNumeric = (value || value === 0) && !isNaN(value);
434 node.style[rule] = isNumeric ? value + 'px' : value;
435 }
436 }
437
438
439 /**
440 * Gets or sets the data attributes on a node
441 *
442 * Unlike the jQuery version this only stores data
443 * in the DOM attributes which means only strings
444 * can be stored.
445 *
446 * @param {Node} node
447 * @param {string} [key]
448 * @param {string} [value]
449 * @return {Object|undefined}
450 */
451 function data(node, key, value) {
452 var argsLength = arguments.length;
453 var data = {};
454
455 if (node.nodeType === ELEMENT_NODE) {
456 if (argsLength === 1) {
457 each(node.attributes, function (_, attr) {
458 if (/^data\-/i.test(attr.name)) {
459 data[attr.name.substr(5)] = attr.value;
460 }
461 });
462
463 return data;
464 }
465
466 if (argsLength === 2) {
467 return attr(node, 'data-' + key);
468 }
469
470 attr(node, 'data-' + key, String(value));
471 }
472 }
473
474 /**
475 * Checks if node matches the given selector.
476 *
477 * @param {?HTMLElement} node
478 * @param {string} selector
479 * @returns {boolean}
480 */
481 function is(node, selector) {
482 var result = false;
483
484 if (node && node.nodeType === ELEMENT_NODE) {
485 result = (node.matches || node.msMatchesSelector ||
486 node.webkitMatchesSelector).call(node, selector);
487 }
488
489 return result;
490 }
491
492
493 /**
494 * Returns true if node contains child otherwise false.
495 *
496 * This differs from the DOM contains() method in that
497 * if node and child are equal this will return false.
498 *
499 * @param {!Node} node
500 * @param {HTMLElement} child
501 * @returns {boolean}
502 */
503 function contains(node, child) {
504 return node !== child && node.contains && node.contains(child);
505 }
506
507 /**
508 * @param {Node} node
509 * @param {string} [selector]
510 * @returns {?HTMLElement}
511 */
512 function previousElementSibling(node, selector) {
513 var prev = node.previousElementSibling;
514
515 if (selector && prev) {
516 return is(prev, selector) ? prev : null;
517 }
518
519 return prev;
520 }
521
522 /**
523 * @param {!Node} node
524 * @param {!Node} refNode
525 * @returns {Node}
526 */
527 function insertBefore(node, refNode) {
528 return refNode.parentNode.insertBefore(node, refNode);
529 }
530
531 /**
532 * @param {?HTMLElement} node
533 * @returns {!Array.<string>}
534 */
535 function classes(node) {
536 return node.className.trim().split(/\s+/);
537 }
538
539 /**
540 * @param {?HTMLElement} node
541 * @param {string} className
542 * @returns {boolean}
543 */
544 function hasClass(node, className) {
545 return is(node, '.' + className);
546 }
547
548 /**
549 * @param {!HTMLElement} node
550 * @param {string} className
551 */
552 function addClass(node, className) {
553 var classList = classes(node);
554
555 if (classList.indexOf(className) < 0) {
556 classList.push(className);
557 }
558
559 node.className = classList.join(' ');
560 }
561
562 /**
563 * @param {!HTMLElement} node
564 * @param {string} className
565 */
566 function removeClass(node, className) {
567 var classList = classes(node);
568
569 arrayRemove(classList, className);
570
571 node.className = classList.join(' ');
572 }
573
574 /**
575 * Toggles a class on node.
576 *
577 * If state is specified and is truthy it will add
578 * the class.
579 *
580 * If state is specified and is falsey it will remove
581 * the class.
582 *
583 * @param {HTMLElement} node
584 * @param {string} className
585 * @param {boolean} [state]
586 */
587 function toggleClass(node, className, state) {
588 state = isUndefined(state) ? !hasClass(node, className) : state;
589
590 if (state) {
591 addClass(node, className);
592 } else {
593 removeClass(node, className);
594 }
595 }
596
597 /**
598 * Gets or sets the width of the passed node.
599 *
600 * @param {HTMLElement} node
601 * @param {number|string} [value]
602 * @returns {number|undefined}
603 */
604 function width(node, value) {
605 if (isUndefined(value)) {
606 var cs = getComputedStyle(node);
607 var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight);
608 var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth);
609
610 return node.offsetWidth - padding - border;
611 }
612
613 css(node, 'width', value);
614 }
615
616 /**
617 * Gets or sets the height of the passed node.
618 *
619 * @param {HTMLElement} node
620 * @param {number|string} [value]
621 * @returns {number|undefined}
622 */
623 function height(node, value) {
624 if (isUndefined(value)) {
625 var cs = getComputedStyle(node);
626 var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom);
627 var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth);
628
629 return node.offsetHeight - padding - border;
630 }
631
632 css(node, 'height', value);
633 }
634
635 /**
636 * Triggers a custom event with the specified name and
637 * sets the detail property to the data object passed.
638 *
639 * @param {HTMLElement} node
640 * @param {string} eventName
641 * @param {Object} [data]
642 */
643 function trigger(node, eventName, data) {
644 var event;
645
646 if (isFunction(window.CustomEvent)) {
647 event = new CustomEvent(eventName, {
648 bubbles: true,
649 cancelable: true,
650 detail: data
651 });
652 } else {
653 event = node.ownerDocument.createEvent('CustomEvent');
654 event.initCustomEvent(eventName, true, true, data);
655 }
656
657 node.dispatchEvent(event);
658 }
659
660 /**
661 * Returns if a node is visible.
662 *
663 * @param {HTMLElement}
664 * @returns {boolean}
665 */
666 function isVisible(node) {
667 return !!node.getClientRects().length;
668 }
669
670 /**
671 * Convert CSS property names into camel case
672 *
673 * @param {string} string
674 * @returns {string}
675 */
676 function camelCase(string) {
677 return string
678 .replace(/^-ms-/, 'ms-')
679 .replace(/-(\w)/g, function (match, char) {
680 return char.toUpperCase();
681 });
682 }
683
684
685 /**
686 * Loop all child nodes of the passed node
687 *
688 * The function should accept 1 parameter being the node.
689 * If the function returns false the loop will be exited.
690 *
691 * @param {HTMLElement} node
692 * @param {function} func Callback which is called with every
693 * child node as the first argument.
694 * @param {boolean} innermostFirst If the innermost node should be passed
695 * to the function before it's parents.
696 * @param {boolean} siblingsOnly If to only traverse the nodes siblings
697 * @param {boolean} [reverse=false] If to traverse the nodes in reverse
698 */
699 // eslint-disable-next-line max-params
700 function traverse(node, func, innermostFirst, siblingsOnly, reverse) {
701 node = reverse ? node.lastChild : node.firstChild;
702
703 while (node) {
704 var next = reverse ? node.previousSibling : node.nextSibling;
705
706 if (
707 (!innermostFirst && func(node) === false) ||
708 (!siblingsOnly && traverse(
709 node, func, innermostFirst, siblingsOnly, reverse
710 ) === false) ||
711 (innermostFirst && func(node) === false)
712 ) {
713 return false;
714 }
715
716 node = next;
717 }
718 }
719
720 /**
721 * Like traverse but loops in reverse
722 * @see traverse
723 */
724 function rTraverse(node, func, innermostFirst, siblingsOnly) {
725 traverse(node, func, innermostFirst, siblingsOnly, true);
726 }
727
728 /**
729 * Parses HTML into a document fragment
730 *
731 * @param {string} html
732 * @param {Document} [context]
733 * @since 1.4.4
734 * @return {DocumentFragment}
735 */
736 function parseHTML(html, context) {
737 context = context || document;
738
739 var ret = context.createDocumentFragment();
740 var tmp = createElement('div', {}, context);
741
742 tmp.innerHTML = html;
743
744 while (tmp.firstChild) {
745 appendChild(ret, tmp.firstChild);
746 }
747
748 return ret;
749 }
750
751 /**
752 * Checks if an element has any styling.
753 *
754 * It has styling if it is not a plain <div> or <p> or
755 * if it has a class, style attribute or data.
756 *
757 * @param {HTMLElement} elm
758 * @return {boolean}
759 * @since 1.4.4
760 */
761 function hasStyling(node) {
762 return node && (!is(node, 'p,div') || node.className ||
763 attr(node, 'style') || !isEmptyObject(data(node)));
764 }
765
766 /**
767 * Converts an element from one type to another.
768 *
769 * For example it can convert the element <b> to <strong>
770 *
771 * @param {HTMLElement} element
772 * @param {string} toTagName
773 * @return {HTMLElement}
774 * @since 1.4.4
775 */
776 function convertElement(element, toTagName) {
777 var newElement = createElement(toTagName, {}, element.ownerDocument);
778
779 each(element.attributes, function (_, attribute) {
780 // Some browsers parse invalid attributes names like
781 // 'size"2' which throw an exception when set, just
782 // ignore these.
783 try {
784 attr(newElement, attribute.name, attribute.value);
785 } catch (ex) {}
786 });
787
788 while (element.firstChild) {
789 appendChild(newElement, element.firstChild);
790 }
791
792 element.parentNode.replaceChild(newElement, element);
793
794 return newElement;
795 }
796
797 /**
798 * List of block level elements separated by bars (|)
799 *
800 * @type {string}
801 */
802 var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' +
803 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|' +
804 'details|section|article|aside|nav|main|header|hgroup|footer|fieldset|' +
805 'dl|dt|dd|figure|figcaption|';
806
807 /**
808 * List of elements that do not allow children separated by bars (|)
809 *
810 * @param {Node} node
811 * @return {boolean}
812 * @since 1.4.5
813 */
814 function canHaveChildren(node) {
815 // 1 = Element
816 // 9 = Document
817 // 11 = Document Fragment
818 if (!/11?|9/.test(node.nodeType)) {
819 return false;
820 }
821
822 // List of empty HTML tags separated by bar (|) character.
823 // Source: http://www.w3.org/TR/html4/index/elements.html
824 // Source: http://www.w3.org/TR/html5/syntax.html#void-elements
825 return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' +
826 '|isindex|link|meta|param|command|embed|keygen|source|track|' +
827 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0;
828 }
829
830 /**
831 * Checks if an element is inline
832 *
833 * @param {HTMLElement} elm
834 * @param {boolean} [includeCodeAsBlock=false]
835 * @return {boolean}
836 */
837 function isInline(elm, includeCodeAsBlock) {
838 var tagName,
839 nodeType = (elm || {}).nodeType || TEXT_NODE;
840
841 if (nodeType !== ELEMENT_NODE) {
842 return nodeType === TEXT_NODE;
843 }
844
845 tagName = elm.tagName.toLowerCase();
846
847 if (tagName === 'code') {
848 return !includeCodeAsBlock;
849 }
850
851 return blockLevelList.indexOf('|' + tagName + '|') < 0;
852 }
853
854 /**
855 * Copy the CSS from 1 node to another.
856 *
857 * Only copies CSS defined on the element e.g. style attr.
858 *
859 * @param {HTMLElement} from
860 * @param {HTMLElement} to
861 * @deprecated since v3.1.0
862 */
863 function copyCSS(from, to) {
864 if (to.style && from.style) {
865 to.style.cssText = from.style.cssText + to.style.cssText;
866 }
867 }
868
869 /**
870 * Checks if a DOM node is empty
871 *
872 * @param {Node} node
873 * @returns {boolean}
874 */
875 function isEmpty(node) {
876 if (node.lastChild && isEmpty(node.lastChild)) {
877 remove(node.lastChild);
878 }
879
880 return node.nodeType === 3 ? !node.nodeValue :
881 (canHaveChildren(node) && !node.childNodes.length);
882 }
883
884 /**
885 * Fixes block level elements inside in inline elements.
886 *
887 * Also fixes invalid list nesting by placing nested lists
888 * inside the previous li tag or wrapping them in an li tag.
889 *
890 * @param {HTMLElement} node
891 */
892 function fixNesting(node) {
893 traverse(node, function (node) {
894 var list = 'ul,ol',
895 isBlock = !isInline(node, true) && node.nodeType !== COMMENT_NODE,
896 parent = node.parentNode;
897
898 // Any blocklevel element inside an inline element needs fixing.
899 // Also <p> tags that contain blocks should be fixed
900 if (isBlock && (isInline(parent, true) || parent.tagName === 'P')) {
901 // Find the last inline parent node
902 var lastInlineParent = node;
903 while (isInline(lastInlineParent.parentNode, true) ||
904 lastInlineParent.parentNode.tagName === 'P') {
905 lastInlineParent = lastInlineParent.parentNode;
906 }
907
908 var before = extractContents(lastInlineParent, node);
909 var middle = node;
910
911 // Clone inline styling and apply it to the blocks children
912 while (parent && isInline(parent, true)) {
913 if (parent.nodeType === ELEMENT_NODE) {
914 var clone = parent.cloneNode();
915 while (middle.firstChild) {
916 appendChild(clone, middle.firstChild);
917 }
918
919 appendChild(middle, clone);
920 }
921 parent = parent.parentNode;
922 }
923
924 insertBefore(middle, lastInlineParent);
925 if (!isEmpty(before)) {
926 insertBefore(before, middle);
927 }
928 if (isEmpty(lastInlineParent)) {
929 remove(lastInlineParent);
930 }
931 }
932
933 // Fix invalid nested lists which should be wrapped in an li tag
934 if (isBlock && is(node, list) && is(node.parentNode, list)) {
935 var li = previousElementSibling(node, 'li');
936
937 if (!li) {
938 li = createElement('li');
939 insertBefore(li, node);
940 }
941
942 appendChild(li, node);
943 }
944 });
945 }
946
947 /**
948 * Finds the common parent of two nodes
949 *
950 * @param {!HTMLElement} node1
951 * @param {!HTMLElement} node2
952 * @return {?HTMLElement}
953 */
954 function findCommonAncestor(node1, node2) {
955 while ((node1 = node1.parentNode)) {
956 if (contains(node1, node2)) {
957 return node1;
958 }
959 }
960 }
961
962 /**
963 * @param {?Node}
964 * @param {boolean} [previous=false]
965 * @returns {?Node}
966 */
967 function getSibling(node, previous) {
968 if (!node) {
969 return null;
970 }
971
972 return (previous ? node.previousSibling : node.nextSibling) ||
973 getSibling(node.parentNode, previous);
974 }
975
976 /**
977 * Removes unused whitespace from the root and all it's children.
978 *
979 * @param {!HTMLElement} root
980 * @since 1.4.3
981 */
982 function removeWhiteSpace(root) {
983 var nodeValue, nodeType, next, previous, previousSibling,
984 nextNode, trimStart,
985 cssWhiteSpace = css(root, 'whiteSpace'),
986 // Preserve newlines if is pre-line
987 preserveNewLines = /line$/i.test(cssWhiteSpace),
988 node = root.firstChild;
989
990 // Skip pre & pre-wrap with any vendor prefix
991 if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) {
992 return;
993 }
994
995 while (node) {
996 nextNode = node.nextSibling;
997 nodeValue = node.nodeValue;
998 nodeType = node.nodeType;
999
1000 if (nodeType === ELEMENT_NODE && node.firstChild) {
1001 removeWhiteSpace(node);
1002 }
1003
1004 if (nodeType === TEXT_NODE) {
1005 next = getSibling(node);
1006 previous = getSibling(node, true);
1007 trimStart = false;
1008
1009 while (hasClass(previous, 'sceditor-ignore')) {
1010 previous = getSibling(previous, true);
1011 }
1012
1013 // If previous sibling isn't inline or is a textnode that
1014 // ends in whitespace, time the start whitespace
1015 if (isInline(node) && previous) {
1016 previousSibling = previous;
1017
1018 while (previousSibling.lastChild) {
1019 previousSibling = previousSibling.lastChild;
1020
1021 // eslint-disable-next-line max-depth
1022 while (hasClass(previousSibling, 'sceditor-ignore')) {
1023 previousSibling = getSibling(previousSibling, true);
1024 }
1025 }
1026
1027 trimStart = previousSibling.nodeType === TEXT_NODE ?
1028 /[\t\n\r ]$/.test(previousSibling.nodeValue) :
1029 !isInline(previousSibling);
1030 }
1031
1032 // Clear zero width spaces
1033 nodeValue = nodeValue.replace(/\u200B/g, '');
1034
1035 // Strip leading whitespace
1036 if (!previous || !isInline(previous) || trimStart) {
1037 nodeValue = nodeValue.replace(
1038 preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/,
1039 ''
1040 );
1041 }
1042
1043 // Strip trailing whitespace
1044 if (!next || !isInline(next)) {
1045 nodeValue = nodeValue.replace(
1046 preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/,
1047 ''
1048 );
1049 }
1050
1051 // Remove empty text nodes
1052 if (!nodeValue.length) {
1053 remove(node);
1054 } else {
1055 node.nodeValue = nodeValue.replace(
1056 preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g,
1057 ' '
1058 );
1059 }
1060 }
1061
1062 node = nextNode;
1063 }
1064 }
1065
1066 /**
1067 * Extracts all the nodes between the start and end nodes
1068 *
1069 * @param {HTMLElement} startNode The node to start extracting at
1070 * @param {HTMLElement} endNode The node to stop extracting at
1071 * @return {DocumentFragment}
1072 */
1073 function extractContents(startNode, endNode) {
1074 var range = startNode.ownerDocument.createRange();
1075
1076 range.setStartBefore(startNode);
1077 range.setEndAfter(endNode);
1078
1079 return range.extractContents();
1080 }
1081
1082 /**
1083 * Gets the offset position of an element
1084 *
1085 * @param {HTMLElement} node
1086 * @return {Object} An object with left and top properties
1087 */
1088 function getOffset(node) {
1089 var left = 0,
1090 top = 0;
1091
1092 while (node) {
1093 left += node.offsetLeft;
1094 top += node.offsetTop;
1095 node = node.offsetParent;
1096 }
1097
1098 return {
1099 left: left,
1100 top: top
1101 };
1102 }
1103
1104 /**
1105 * Gets the value of a CSS property from the elements style attribute
1106 *
1107 * @param {HTMLElement} elm
1108 * @param {string} property
1109 * @return {string}
1110 */
1111 function getStyle(elm, property) {
1112 var styleValue,
1113 elmStyle = elm.style;
1114
1115 if (!cssPropertyNameCache[property]) {
1116 cssPropertyNameCache[property] = camelCase(property);
1117 }
1118
1119 property = cssPropertyNameCache[property];
1120 styleValue = elmStyle[property];
1121
1122 // Add an exception for text-align
1123 if ('textAlign' === property) {
1124 styleValue = styleValue || css(elm, property);
1125
1126 if (css(elm.parentNode, property) === styleValue ||
1127 css(elm, 'display') !== 'block' || is(elm, 'hr,th')) {
1128 return '';
1129 }
1130 }
1131
1132 return styleValue;
1133 }
1134
1135 /**
1136 * Tests if an element has a style.
1137 *
1138 * If values are specified it will check that the styles value
1139 * matches one of the values
1140 *
1141 * @param {HTMLElement} elm
1142 * @param {string} property
1143 * @param {string|array} [values]
1144 * @return {boolean}
1145 */
1146 function hasStyle(elm, property, values) {
1147 var styleValue = getStyle(elm, property);
1148
1149 if (!styleValue) {
1150 return false;
1151 }
1152
1153 return !values || styleValue === values ||
1154 (Array.isArray(values) && values.indexOf(styleValue) > -1);
1155 }
1156
1157 /**
1158 * Returns true if both nodes have the same number of inline styles and all the
1159 * inline styles have matching values
1160 *
1161 * @param {HTMLElement} nodeA
1162 * @param {HTMLElement} nodeB
1163 * @returns {boolean}
1164 */
1165 function stylesMatch(nodeA, nodeB) {
1166 var i = nodeA.style.length;
1167 if (i !== nodeB.style.length) {
1168 return false;
1169 }
1170
1171 while (i--) {
1172 var prop = nodeA.style[i];
1173 if (nodeA.style[prop] !== nodeB.style[prop]) {
1174 return false;
1175 }
1176 }
1177
1178 return true;
1179 }
1180
1181 /**
1182 * Returns true if both nodes have the same number of attributes and all the
1183 * attribute values match
1184 *
1185 * @param {HTMLElement} nodeA
1186 * @param {HTMLElement} nodeB
1187 * @returns {boolean}
1188 */
1189 function attributesMatch(nodeA, nodeB) {
1190 var i = nodeA.attributes.length;
1191 if (i !== nodeB.attributes.length) {
1192 return false;
1193 }
1194
1195 while (i--) {
1196 var prop = nodeA.attributes[i];
1197 var notMatches = prop.name === 'style' ?
1198 !stylesMatch(nodeA, nodeB) :
1199 prop.value !== attr(nodeB, prop.name);
1200
1201 if (notMatches) {
1202 return false;
1203 }
1204 }
1205
1206 return true;
1207 }
1208
1209 /**
1210 * Removes an element placing its children in its place
1211 *
1212 * @param {HTMLElement} node
1213 */
1214 function removeKeepChildren(node) {
1215 while (node.firstChild) {
1216 insertBefore(node.firstChild, node);
1217 }
1218
1219 remove(node);
1220 }
1221
1222 /**
1223 * Merges inline styles and tags with parents where possible
1224 *
1225 * @param {Node} node
1226 * @since 3.1.0
1227 */
1228 function merge(node) {
1229 if (node.nodeType !== ELEMENT_NODE) {
1230 return;
1231 }
1232
1233 var parent = node.parentNode;
1234 var tagName = node.tagName;
1235 var mergeTags = /B|STRONG|EM|SPAN|FONT/;
1236
1237 // Merge children (in reverse as children can be removed)
1238 var i = node.childNodes.length;
1239 while (i--) {
1240 merge(node.childNodes[i]);
1241 }
1242
1243 // Should only merge inline tags
1244 if (!isInline(node)) {
1245 return;
1246 }
1247
1248 // Remove any inline styles that match the parent style
1249 i = node.style.length;
1250 while (i--) {
1251 var prop = node.style[i];
1252 if (css(parent, prop) === css(node, prop)) {
1253 node.style.removeProperty(prop);
1254 }
1255 }
1256
1257 // Can only remove / merge tags if no inline styling left.
1258 // If there is any inline style left then it means it at least partially
1259 // doesn't match the parent style so must stay
1260 if (!node.style.length) {
1261 removeAttr(node, 'style');
1262
1263 // Remove font attributes if match parent
1264 if (tagName === 'FONT') {
1265 if (css(node, 'fontFamily').toLowerCase() ===
1266 css(parent, 'fontFamily').toLowerCase()) {
1267 removeAttr(node, 'face');
1268 }
1269
1270 if (css(node, 'color') === css(parent, 'color')) {
1271 removeAttr(node, 'color');
1272 }
1273
1274 if (css(node, 'fontSize') === css(parent, 'fontSize')) {
1275 removeAttr(node, 'size');
1276 }
1277 }
1278
1279 // Spans and font tags with no attributes can be safely removed
1280 if (!node.attributes.length && /SPAN|FONT/.test(tagName)) {
1281 removeKeepChildren(node);
1282 } else if (mergeTags.test(tagName)) {
1283 var isBold = /B|STRONG/.test(tagName);
1284 var isItalic = tagName === 'EM';
1285
1286 while (parent && isInline(parent) &&
1287 (!isBold || /bold|700/i.test(css(parent, 'fontWeight'))) &&
1288 (!isItalic || css(parent, 'fontStyle') === 'italic')) {
1289
1290 // Remove if parent match
1291 if ((parent.tagName === tagName ||
1292 (isBold && /B|STRONG/.test(parent.tagName))) &&
1293 attributesMatch(parent, node)) {
1294 removeKeepChildren(node);
1295 break;
1296 }
1297
1298 parent = parent.parentNode;
1299 }
1300 }
1301 }
1302
1303 // Merge siblings if attributes, including inline styles, match
1304 var next = node.nextSibling;
1305 if (next && next.tagName === tagName && attributesMatch(next, node)) {
1306 appendChild(node, next);
1307 removeKeepChildren(next);
1308 }
1309 }
1310
1311 /**
1312 * Default options for SCEditor
1313 * @type {Object}
1314 */
1315 var defaultOptions = {
1316 /** @lends jQuery.sceditor.defaultOptions */
1317 /**
1318 * Toolbar buttons order and groups. Should be comma separated and
1319 * have a bar | to separate groups
1320 *
1321 * @type {string}
1322 */
1323 toolbar: 'bold,italic,underline,strike,subscript,superscript|' +
1324 'left,center,right,justify|font,size,color,removeformat|' +
1325 'cut,copy,pastetext|bulletlist,orderedlist,indent,outdent|' +
1326 'table|code,quote|horizontalrule,image,email,link,unlink|' +
1327 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source',
1328
1329 /**
1330 * Comma separated list of commands to excludes from the toolbar
1331 *
1332 * @type {string}
1333 */
1334 toolbarExclude: null,
1335
1336 /**
1337 * Stylesheet to include in the WYSIWYG editor. This is what will style
1338 * the WYSIWYG elements
1339 *
1340 * @type {string}
1341 */
1342 style: 'jquery.sceditor.default.css',
1343
1344 /**
1345 * Comma separated list of fonts for the font selector
1346 *
1347 * @type {string}
1348 */
1349 fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' +
1350 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana',
1351
1352 /**
1353 * Colors should be comma separated and have a bar | to signal a new
1354 * column.
1355 *
1356 * If null the colors will be auto generated.
1357 *
1358 * @type {string}
1359 */
1360 colors: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' +
1361 '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' +
1362 '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' +
1363 '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' +
1364 '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' +
1365 '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' +
1366 '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' +
1367 '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef',
1368
1369 /**
1370 * The locale to use.
1371 * @type {string}
1372 */
1373 locale: attr(document.documentElement, 'lang') || 'en',
1374
1375 /**
1376 * The Charset to use
1377 * @type {string}
1378 */
1379 charset: 'utf-8',
1380
1381 /**
1382 * Compatibility mode for emoticons.
1383 *
1384 * Helps if you have emoticons such as :/ which would put an emoticon
1385 * inside http://
1386 *
1387 * This mode requires emoticons to be surrounded by whitespace or end of
1388 * line chars. This mode has limited As You Type emoticon conversion
1389 * support. It will not replace AYT for end of line chars, only
1390 * emoticons surrounded by whitespace. They will still be replaced
1391 * correctly when loaded just not AYT.
1392 *
1393 * @type {boolean}
1394 */
1395 emoticonsCompat: false,
1396
1397 /**
1398 * If to enable emoticons. Can be changes at runtime using the
1399 * emoticons() method.
1400 *
1401 * @type {boolean}
1402 * @since 1.4.2
1403 */
1404 emoticonsEnabled: true,
1405
1406 /**
1407 * Emoticon root URL
1408 *
1409 * @type {string}
1410 */
1411 emoticonsRoot: '',
1412 emoticons: {
1413 dropdown: {
1414 ':)': 'emoticons/smile.png',
1415 ':angel:': 'emoticons/angel.png',
1416 ':angry:': 'emoticons/angry.png',
1417 '8-)': 'emoticons/cool.png',
1418 ':\'(': 'emoticons/cwy.png',
1419 ':ermm:': 'emoticons/ermm.png',
1420 ':D': 'emoticons/grin.png',
1421 '<3': 'emoticons/heart.png',
1422 ':(': 'emoticons/sad.png',
1423 ':O': 'emoticons/shocked.png',
1424 ':P': 'emoticons/tongue.png',
1425 ';)': 'emoticons/wink.png'
1426 },
1427 more: {
1428 ':alien:': 'emoticons/alien.png',
1429 ':blink:': 'emoticons/blink.png',
1430 ':blush:': 'emoticons/blush.png',
1431 ':cheerful:': 'emoticons/cheerful.png',
1432 ':devil:': 'emoticons/devil.png',
1433 ':dizzy:': 'emoticons/dizzy.png',
1434 ':getlost:': 'emoticons/getlost.png',
1435 ':happy:': 'emoticons/happy.png',
1436 ':kissing:': 'emoticons/kissing.png',
1437 ':ninja:': 'emoticons/ninja.png',
1438 ':pinch:': 'emoticons/pinch.png',
1439 ':pouty:': 'emoticons/pouty.png',
1440 ':sick:': 'emoticons/sick.png',
1441 ':sideways:': 'emoticons/sideways.png',
1442 ':silly:': 'emoticons/silly.png',
1443 ':sleeping:': 'emoticons/sleeping.png',
1444 ':unsure:': 'emoticons/unsure.png',
1445 ':woot:': 'emoticons/w00t.png',
1446 ':wassat:': 'emoticons/wassat.png'
1447 },
1448 hidden: {
1449 ':whistling:': 'emoticons/whistling.png',
1450 ':love:': 'emoticons/wub.png'
1451 }
1452 },
1453
1454 /**
1455 * Width of the editor. Set to null for automatic with
1456 *
1457 * @type {?number}
1458 */
1459 width: null,
1460
1461 /**
1462 * Height of the editor including toolbar. Set to null for automatic
1463 * height
1464 *
1465 * @type {?number}
1466 */
1467 height: null,
1468
1469 /**
1470 * If to allow the editor to be resized
1471 *
1472 * @type {boolean}
1473 */
1474 resizeEnabled: true,
1475
1476 /**
1477 * Min resize to width, set to null for half textarea width or -1 for
1478 * unlimited
1479 *
1480 * @type {?number}
1481 */
1482 resizeMinWidth: null,
1483 /**
1484 * Min resize to height, set to null for half textarea height or -1 for
1485 * unlimited
1486 *
1487 * @type {?number}
1488 */
1489 resizeMinHeight: null,
1490 /**
1491 * Max resize to height, set to null for double textarea height or -1
1492 * for unlimited
1493 *
1494 * @type {?number}
1495 */
1496 resizeMaxHeight: null,
1497 /**
1498 * Max resize to width, set to null for double textarea width or -1 for
1499 * unlimited
1500 *
1501 * @type {?number}
1502 */
1503 resizeMaxWidth: null,
1504 /**
1505 * If resizing by height is enabled
1506 *
1507 * @type {boolean}
1508 */
1509 resizeHeight: true,
1510 /**
1511 * If resizing by width is enabled
1512 *
1513 * @type {boolean}
1514 */
1515 resizeWidth: true,
1516
1517 /**
1518 * Date format, will be overridden if locale specifies one.
1519 *
1520 * The words year, month and day will be replaced with the users current
1521 * year, month and day.
1522 *
1523 * @type {string}
1524 */
1525 dateFormat: 'year-month-day',
1526
1527 /**
1528 * Element to inset the toolbar into.
1529 *
1530 * @type {HTMLElement}
1531 */
1532 toolbarContainer: null,
1533
1534 /**
1535 * If to enable paste filtering. This is currently experimental, please
1536 * report any issues.
1537 *
1538 * @type {boolean}
1539 */
1540 enablePasteFiltering: false,
1541
1542 /**
1543 * If to completely disable pasting into the editor
1544 *
1545 * @type {boolean}
1546 */
1547 disablePasting: false,
1548
1549 /**
1550 * If the editor is read only.
1551 *
1552 * @type {boolean}
1553 */
1554 readOnly: false,
1555
1556 /**
1557 * If to set the editor to right-to-left mode.
1558 *
1559 * If set to null the direction will be automatically detected.
1560 *
1561 * @type {boolean}
1562 */
1563 rtl: false,
1564
1565 /**
1566 * If to auto focus the editor on page load
1567 *
1568 * @type {boolean}
1569 */
1570 autofocus: false,
1571
1572 /**
1573 * If to auto focus the editor to the end of the content
1574 *
1575 * @type {boolean}
1576 */
1577 autofocusEnd: true,
1578
1579 /**
1580 * If to auto expand the editor to fix the content
1581 *
1582 * @type {boolean}
1583 */
1584 autoExpand: false,
1585
1586 /**
1587 * If to auto update original textbox on blur
1588 *
1589 * @type {boolean}
1590 */
1591 autoUpdate: false,
1592
1593 /**
1594 * If to enable the browsers built in spell checker
1595 *
1596 * @type {boolean}
1597 */
1598 spellcheck: true,
1599
1600 /**
1601 * If to run the source editor when there is no WYSIWYG support. Only
1602 * really applies to mobile OS's.
1603 *
1604 * @type {boolean}
1605 */
1606 runWithoutWysiwygSupport: false,
1607
1608 /**
1609 * If to load the editor in source mode and still allow switching
1610 * between WYSIWYG and source mode
1611 *
1612 * @type {boolean}
1613 */
1614 startInSourceMode: false,
1615
1616 /**
1617 * Optional ID to give the editor.
1618 *
1619 * @type {string}
1620 */
1621 id: null,
1622
1623 /**
1624 * Comma separated list of plugins
1625 *
1626 * @type {string}
1627 */
1628 plugins: '',
1629
1630 /**
1631 * z-index to set the editor container to. Needed for jQuery UI dialog.
1632 *
1633 * @type {?number}
1634 */
1635 zIndex: null,
1636
1637 /**
1638 * If to trim the BBCode. Removes any spaces at the start and end of the
1639 * BBCode string.
1640 *
1641 * @type {boolean}
1642 */
1643 bbcodeTrim: false,
1644
1645 /**
1646 * If to disable removing block level elements by pressing backspace at
1647 * the start of them
1648 *
1649 * @type {boolean}
1650 */
1651 disableBlockRemove: false,
1652
1653 /**
1654 * Array of allowed URL (should be either strings or regex) for iframes.
1655 *
1656 * If it's a string then iframes where the start of the src matches the
1657 * specified string will be allowed.
1658 *
1659 * If it's a regex then iframes where the src matches the regex will be
1660 * allowed.
1661 *
1662 * @type {Array}
1663 */
1664 allowedIframeUrls: [],
1665
1666 /**
1667 * BBCode parser options, only applies if using the editor in BBCode
1668 * mode.
1669 *
1670 * See SCEditor.BBCodeParser.defaults for list of valid options
1671 *
1672 * @type {Object}
1673 */
1674 parserOptions: { },
1675
1676 /**
1677 * CSS that will be added to the to dropdown menu (eg. z-index)
1678 *
1679 * @type {Object}
1680 */
1681 dropDownCss: { }
1682 };
1683
1684 // Must start with a valid scheme
1685 // ^
1686 // Schemes that are considered safe
1687 // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
1688 // Relative schemes (//:) are considered safe
1689 // (\\/\\/)|
1690 // Image data URI's are considered safe
1691 // data:image\\/(png|bmp|gif|p?jpe?g);
1692 var VALID_SCHEME_REGEX =
1693 /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
1694
1695 /**
1696 * Escapes a string so it's safe to use in regex
1697 *
1698 * @param {string} str
1699 * @return {string}
1700 */
1701 function regex(str) {
1702 return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
1703 }
1704 /**
1705 * Escapes all HTML entities in a string
1706 *
1707 * If noQuotes is set to false, all single and double
1708 * quotes will also be escaped
1709 *
1710 * @param {string} str
1711 * @param {boolean} [noQuotes=true]
1712 * @return {string}
1713 * @since 1.4.1
1714 */
1715 function entities(str, noQuotes) {
1716 if (!str) {
1717 return str;
1718 }
1719
1720 var replacements = {
1721 '&': '&amp;',
1722 '<': '&lt;',
1723 '>': '&gt;',
1724 ' ': '&nbsp; ',
1725 '\r\n': '<br />',
1726 '\r': '<br />',
1727 '\n': '<br />'
1728 };
1729
1730 if (noQuotes !== false) {
1731 replacements['"'] = '&#34;';
1732 replacements['\''] = '&#39;';
1733 replacements['`'] = '&#96;';
1734 }
1735
1736 str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) {
1737 return replacements[match] || match;
1738 });
1739
1740 return str;
1741 }
1742 /**
1743 * Escape URI scheme.
1744 *
1745 * Appends the current URL to a url if it has a scheme that is not:
1746 *
1747 * http
1748 * https
1749 * sftp
1750 * ftp
1751 * mailto
1752 * spotify
1753 * skype
1754 * ssh
1755 * teamspeak
1756 * tel
1757 * //
1758 * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
1759 *
1760 * **IMPORTANT**: This does not escape any HTML in a url, for
1761 * that use the escape.entities() method.
1762 *
1763 * @param {string} url
1764 * @return {string}
1765 * @since 1.4.5
1766 */
1767 function uriScheme(url) {
1768 var path,
1769 // If there is a : before a / then it has a scheme
1770 hasScheme = /^[^\/]*:/i,
1771 location = window.location;
1772
1773 // Has no scheme or a valid scheme
1774 if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
1775 return url;
1776 }
1777
1778 path = location.pathname.split('/');
1779 path.pop();
1780
1781 return location.protocol + '//' +
1782 location.host +
1783 path.join('/') + '/' +
1784 url;
1785 }
1786
1787 /**
1788 * HTML templates used by the editor and default commands
1789 * @type {Object}
1790 * @private
1791 */
1792 var _templates = {
1793 html:
1794 '<!DOCTYPE html>' +
1795 '<html{attrs}>' +
1796 '<head>' +
1797 '<meta http-equiv="Content-Type" ' +
1798 'content="text/html;charset={charset}" />' +
1799 '<link rel="stylesheet" type="text/css" href="{style}" />' +
1800 '</head>' +
1801 '<body contenteditable="true" {spellcheck}><p></p></body>' +
1802 '</html>',
1803
1804 toolbarButton: '<a class="sceditor-button sceditor-button-{name}" ' +
1805 'data-sceditor-command="{name}" unselectable="on">' +
1806 '<div unselectable="on">{dispName}</div></a>',
1807
1808 emoticon: '<img src="{url}" data-sceditor-emoticon="{key}" ' +
1809 'alt="{key}" title="{tooltip}" />',
1810
1811 fontOpt: '<a class="sceditor-font-option" href="#" ' +
1812 'data-font="{font}"><font face="{font}">{font}</font></a>',
1813
1814 sizeOpt: '<a class="sceditor-fontsize-option" data-size="{size}" ' +
1815 'href="#"><font size="{size}">{size}</font></a>',
1816
1817 pastetext:
1818 '<div><label for="txt">{label}</label> ' +
1819 '<textarea cols="20" rows="7" id="txt"></textarea></div>' +
1820 '<div><input type="button" class="button" value="{insert}" />' +
1821 '</div>',
1822
1823 table:
1824 '<div><label for="rows">{rows}</label><input type="text" ' +
1825 'id="rows" value="2" /></div>' +
1826 '<div><label for="cols">{cols}</label><input type="text" ' +
1827 'id="cols" value="2" /></div>' +
1828 '<div><input type="button" class="button" value="{insert}"' +
1829 ' /></div>',
1830
1831 image:
1832 '<div><label for="image">{url}</label> ' +
1833 '<input type="text" id="image" dir="ltr" placeholder="https://" /></div>' +
1834 '<div><label for="width">{width}</label> ' +
1835 '<input type="text" id="width" size="2" dir="ltr" /></div>' +
1836 '<div><label for="height">{height}</label> ' +
1837 '<input type="text" id="height" size="2" dir="ltr" /></div>' +
1838 '<div><input type="button" class="button" value="{insert}" />' +
1839 '</div>',
1840
1841 email:
1842 '<div><label for="email">{label}</label> ' +
1843 '<input type="text" id="email" dir="ltr" /></div>' +
1844 '<div><label for="des">{desc}</label> ' +
1845 '<input type="text" id="des" /></div>' +
1846 '<div><input type="button" class="button" value="{insert}" />' +
1847 '</div>',
1848
1849 link:
1850 '<div><label for="link">{url}</label> ' +
1851 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' +
1852 '<div><label for="des">{desc}</label> ' +
1853 '<input type="text" id="des" /></div>' +
1854 '<div><input type="button" class="button" value="{ins}" /></div>',
1855
1856 youtubeMenu:
1857 '<div><label for="link">{label}</label> ' +
1858 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' +
1859 '<div><input type="button" class="button" value="{insert}" />' +
1860 '</div>',
1861
1862 youtube:
1863 '<iframe width="560" height="315" frameborder="0" allowfullscreen ' +
1864 'src="https://www.youtube-nocookie.com/embed/{id}?wmode=opaque&start={time}" ' +
1865 'data-youtube-id="{id}"></iframe>'
1866 };
1867
1868 /**
1869 * Replaces any params in a template with the passed params.
1870 *
1871 * If createHtml is passed it will return a DocumentFragment
1872 * containing the parsed template.
1873 *
1874 * @param {string} name
1875 * @param {Object} [params]
1876 * @param {boolean} [createHtml]
1877 * @returns {string|DocumentFragment}
1878 * @private
1879 */
1880 function _tmpl (name, params, createHtml) {
1881 var template = _templates[name];
1882
1883 Object.keys(params).forEach(function (name) {
1884 template = template.replace(
1885 new RegExp(regex('{' + name + '}'), 'g'), params[name]
1886 );
1887 });
1888
1889 if (createHtml) {
1890 template = parseHTML(template);
1891 }
1892
1893 return template;
1894 }
1895
1896 /**
1897 * Fixes a bug in FF where it sometimes wraps
1898 * new lines in their own list item.
1899 * See issue #359
1900 */
1901 function fixFirefoxListBug(editor) {
1902 // Only apply to Firefox as will break other browsers.
1903 if ('mozHidden' in document) {
1904 var node = editor.getBody();
1905 var next;
1906
1907 while (node) {
1908 next = node;
1909
1910 if (next.firstChild) {
1911 next = next.firstChild;
1912 } else {
1913
1914 while (next && !next.nextSibling) {
1915 next = next.parentNode;
1916 }
1917
1918 if (next) {
1919 next = next.nextSibling;
1920 }
1921 }
1922
1923 if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) {
1924 // Only remove if newlines are collapsed
1925 if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) {
1926 remove(node);
1927 }
1928 }
1929
1930 node = next;
1931 }
1932 }
1933 }
1934
1935
1936 /**
1937 * Map of all the commands for SCEditor
1938 * @type {Object}
1939 * @name commands
1940 * @memberOf jQuery.sceditor
1941 */
1942 var defaultCmds = {
1943 // START_COMMAND: Bold
1944 bold: {
1945 exec: 'bold',
1946 tooltip: 'Bold',
1947 shortcut: 'Ctrl+B'
1948 },
1949 // END_COMMAND
1950 // START_COMMAND: Italic
1951 italic: {
1952 exec: 'italic',
1953 tooltip: 'Italic',
1954 shortcut: 'Ctrl+I'
1955 },
1956 // END_COMMAND
1957 // START_COMMAND: Underline
1958 underline: {
1959 exec: 'underline',
1960 tooltip: 'Underline',
1961 shortcut: 'Ctrl+U'
1962 },
1963 // END_COMMAND
1964 // START_COMMAND: Strikethrough
1965 strike: {
1966 exec: 'strikethrough',
1967 tooltip: 'Strikethrough'
1968 },
1969 // END_COMMAND
1970 // START_COMMAND: Subscript
1971 subscript: {
1972 exec: 'subscript',
1973 tooltip: 'Subscript'
1974 },
1975 // END_COMMAND
1976 // START_COMMAND: Superscript
1977 superscript: {
1978 exec: 'superscript',
1979 tooltip: 'Superscript'
1980 },
1981 // END_COMMAND
1982
1983 // START_COMMAND: Left
1984 left: {
1985 state: function (node) {
1986 if (node && node.nodeType === 3) {
1987 node = node.parentNode;
1988 }
1989
1990 if (node) {
1991 var isLtr = css(node, 'direction') === 'ltr';
1992 var align = css(node, 'textAlign');
1993
1994 // Can be -moz-left
1995 return /left/.test(align) ||
1996 align === (isLtr ? 'start' : 'end');
1997 }
1998 },
1999 exec: 'justifyleft',
2000 tooltip: 'Align left'
2001 },
2002 // END_COMMAND
2003 // START_COMMAND: Centre
2004 center: {
2005 exec: 'justifycenter',
2006 tooltip: 'Center'
2007 },
2008 // END_COMMAND
2009 // START_COMMAND: Right
2010 right: {
2011 state: function (node) {
2012 if (node && node.nodeType === 3) {
2013 node = node.parentNode;
2014 }
2015
2016 if (node) {
2017 var isLtr = css(node, 'direction') === 'ltr';
2018 var align = css(node, 'textAlign');
2019
2020 // Can be -moz-right
2021 return /right/.test(align) ||
2022 align === (isLtr ? 'end' : 'start');
2023 }
2024 },
2025 exec: 'justifyright',
2026 tooltip: 'Align right'
2027 },
2028 // END_COMMAND
2029 // START_COMMAND: Justify
2030 justify: {
2031 exec: 'justifyfull',
2032 tooltip: 'Justify'
2033 },
2034 // END_COMMAND
2035
2036 // START_COMMAND: Font
2037 font: {
2038 _dropDown: function (editor, caller, callback) {
2039 var content = createElement('div');
2040
2041 on(content, 'click', 'a', function (e) {
2042 callback(data(this, 'font'));
2043 editor.closeDropDown(true);
2044 e.preventDefault();
2045 });
2046
2047 editor.opts.fonts.split(',').forEach(function (font) {
2048 appendChild(content, _tmpl('fontOpt', {
2049 font: font
2050 }, true));
2051 });
2052
2053 editor.createDropDown(caller, 'font-picker', content);
2054 },
2055 exec: function (caller) {
2056 var editor = this;
2057
2058 defaultCmds.font._dropDown(editor, caller, function (fontName) {
2059 editor.execCommand('fontname', fontName);
2060 });
2061 },
2062 tooltip: 'Font Name'
2063 },
2064 // END_COMMAND
2065 // START_COMMAND: Size
2066 size: {
2067 _dropDown: function (editor, caller, callback) {
2068 var content = createElement('div');
2069
2070 on(content, 'click', 'a', function (e) {
2071 callback(data(this, 'size'));
2072 editor.closeDropDown(true);
2073 e.preventDefault();
2074 });
2075
2076 for (var i = 1; i <= 7; i++) {
2077 appendChild(content, _tmpl('sizeOpt', {
2078 size: i
2079 }, true));
2080 }
2081
2082 editor.createDropDown(caller, 'fontsize-picker', content);
2083 },
2084 exec: function (caller) {
2085 var editor = this;
2086
2087 defaultCmds.size._dropDown(editor, caller, function (fontSize) {
2088 editor.execCommand('fontsize', fontSize);
2089 });
2090 },
2091 tooltip: 'Font Size'
2092 },
2093 // END_COMMAND
2094 // START_COMMAND: Colour
2095 color: {
2096 _dropDown: function (editor, caller, callback) {
2097 var content = createElement('div'),
2098 html = '',
2099 cmd = defaultCmds.color;
2100
2101 if (!cmd._htmlCache) {
2102 editor.opts.colors.split('|').forEach(function (column) {
2103 html += '<div class="sceditor-color-column">';
2104
2105 column.split(',').forEach(function (color) {
2106 html +=
2107 '<a href="#" class="sceditor-color-option"' +
2108 ' style="background-color: ' + color + '"' +
2109 ' data-color="' + color + '"></a>';
2110 });
2111
2112 html += '</div>';
2113 });
2114
2115 cmd._htmlCache = html;
2116 }
2117
2118 appendChild(content, parseHTML(cmd._htmlCache));
2119
2120 on(content, 'click', 'a', function (e) {
2121 callback(data(this, 'color'));
2122 editor.closeDropDown(true);
2123 e.preventDefault();
2124 });
2125
2126 editor.createDropDown(caller, 'color-picker', content);
2127 },
2128 exec: function (caller) {
2129 var editor = this;
2130
2131 defaultCmds.color._dropDown(editor, caller, function (color) {
2132 editor.execCommand('forecolor', color);
2133 });
2134 },
2135 tooltip: 'Font Color'
2136 },
2137 // END_COMMAND
2138 // START_COMMAND: Remove Format
2139 removeformat: {
2140 exec: 'removeformat',
2141 tooltip: 'Remove Formatting'
2142 },
2143 // END_COMMAND
2144
2145 // START_COMMAND: Cut
2146 cut: {
2147 exec: 'cut',
2148 tooltip: 'Cut',
2149 errorMessage: 'Your browser does not allow the cut command. ' +
2150 'Please use the keyboard shortcut Ctrl/Cmd-X'
2151 },
2152 // END_COMMAND
2153 // START_COMMAND: Copy
2154 copy: {
2155 exec: 'copy',
2156 tooltip: 'Copy',
2157 errorMessage: 'Your browser does not allow the copy command. ' +
2158 'Please use the keyboard shortcut Ctrl/Cmd-C'
2159 },
2160 // END_COMMAND
2161 // START_COMMAND: Paste
2162 paste: {
2163 exec: 'paste',
2164 tooltip: 'Paste',
2165 errorMessage: 'Your browser does not allow the paste command. ' +
2166 'Please use the keyboard shortcut Ctrl/Cmd-V'
2167 },
2168 // END_COMMAND
2169 // START_COMMAND: Paste Text
2170 pastetext: {
2171 exec: function (caller) {
2172 var val,
2173 content = createElement('div'),
2174 editor = this;
2175
2176 appendChild(content, _tmpl('pastetext', {
2177 label: editor._(
2178 'Paste your text inside the following box:'
2179 ),
2180 insert: editor._('Insert')
2181 }, true));
2182
2183 on(content, 'click', '.button', function (e) {
2184 val = find(content, '#txt')[0].value;
2185
2186 if (val) {
2187 editor.wysiwygEditorInsertText(val);
2188 }
2189
2190 editor.closeDropDown(true);
2191 e.preventDefault();
2192 });
2193
2194 editor.createDropDown(caller, 'pastetext', content);
2195 },
2196 tooltip: 'Paste Text'
2197 },
2198 // END_COMMAND
2199 // START_COMMAND: Bullet List
2200 bulletlist: {
2201 exec: function () {
2202 fixFirefoxListBug(this);
2203 this.execCommand('insertunorderedlist');
2204 },
2205 tooltip: 'Bullet list'
2206 },
2207 // END_COMMAND
2208 // START_COMMAND: Ordered List
2209 orderedlist: {
2210 exec: function () {
2211 fixFirefoxListBug(this);
2212 this.execCommand('insertorderedlist');
2213 },
2214 tooltip: 'Numbered list'
2215 },
2216 // END_COMMAND
2217 // START_COMMAND: Indent
2218 indent: {
2219 state: function (parent, firstBlock) {
2220 // Only works with lists, for now
2221 var range, startParent, endParent;
2222
2223 if (is(firstBlock, 'li')) {
2224 return 0;
2225 }
2226
2227 if (is(firstBlock, 'ul,ol,menu')) {
2228 // if the whole list is selected, then this must be
2229 // invalidated because the browser will place a
2230 // <blockquote> there
2231 range = this.getRangeHelper().selectedRange();
2232
2233 startParent = range.startContainer.parentNode;
2234 endParent = range.endContainer.parentNode;
2235
2236 // TODO: could use nodeType for this?
2237 // Maybe just check the firstBlock contains both the start
2238 //and end containers
2239
2240 // Select the tag, not the textNode
2241 // (that's why the parentNode)
2242 if (startParent !==
2243 startParent.parentNode.firstElementChild ||
2244 // work around a bug in FF
2245 (is(endParent, 'li') && endParent !==
2246 endParent.parentNode.lastElementChild)) {
2247 return 0;
2248 }
2249 }
2250
2251 return -1;
2252 },
2253 exec: function () {
2254 var editor = this,
2255 block = editor.getRangeHelper().getFirstBlockParent();
2256
2257 editor.focus();
2258
2259 // An indent system is quite complicated as there are loads
2260 // of complications and issues around how to indent text
2261 // As default, let's just stay with indenting the lists,
2262 // at least, for now.
2263 if (closest(block, 'ul,ol,menu')) {
2264 editor.execCommand('indent');
2265 }
2266 },
2267 tooltip: 'Add indent'
2268 },
2269 // END_COMMAND
2270 // START_COMMAND: Outdent
2271 outdent: {
2272 state: function (parents, firstBlock) {
2273 return closest(firstBlock, 'ul,ol,menu') ? 0 : -1;
2274 },
2275 exec: function () {
2276 var block = this.getRangeHelper().getFirstBlockParent();
2277 if (closest(block, 'ul,ol,menu')) {
2278 this.execCommand('outdent');
2279 }
2280 },
2281 tooltip: 'Remove one indent'
2282 },
2283 // END_COMMAND
2284
2285 // START_COMMAND: Table
2286 table: {
2287 exec: function (caller) {
2288 var editor = this,
2289 content = createElement('div');
2290
2291 appendChild(content, _tmpl('table', {
2292 rows: editor._('Rows:'),
2293 cols: editor._('Cols:'),
2294 insert: editor._('Insert')
2295 }, true));
2296
2297 on(content, 'click', '.button', function (e) {
2298 var rows = Number(find(content, '#rows')[0].value),
2299 cols = Number(find(content, '#cols')[0].value),
2300 html = '<table>';
2301
2302 if (rows > 0 && cols > 0) {
2303 html += Array(rows + 1).join(
2304 '<tr>' +
2305 Array(cols + 1).join(
2306 '<td><br /></td>'
2307 ) +
2308 '</tr>'
2309 );
2310
2311 html += '</table>';
2312
2313 editor.wysiwygEditorInsertHtml(html);
2314 editor.closeDropDown(true);
2315 e.preventDefault();
2316 }
2317 });
2318
2319 editor.createDropDown(caller, 'inserttable', content);
2320 },
2321 tooltip: 'Insert a table'
2322 },
2323 // END_COMMAND
2324
2325 // START_COMMAND: Horizontal Rule
2326 horizontalrule: {
2327 exec: 'inserthorizontalrule',
2328 tooltip: 'Insert a horizontal rule'
2329 },
2330 // END_COMMAND
2331
2332 // START_COMMAND: Code
2333 code: {
2334 exec: function () {
2335 this.wysiwygEditorInsertHtml(
2336 '<code>',
2337 '<br /></code>'
2338 );
2339 },
2340 tooltip: 'Code'
2341 },
2342 // END_COMMAND
2343
2344 // START_COMMAND: Image
2345 image: {
2346 _dropDown: function (editor, caller, selected, cb) {
2347 var content = createElement('div');
2348
2349 appendChild(content, _tmpl('image', {
2350 url: editor._('URL:'),
2351 width: editor._('Width (optional):'),
2352 height: editor._('Height (optional):'),
2353 insert: editor._('Insert')
2354 }, true));
2355
2356
2357 var urlInput = find(content, '#image')[0];
2358
2359 urlInput.value = selected;
2360
2361 on(content, 'click', '.button', function (e) {
2362 if (urlInput.value) {
2363 cb(
2364 urlInput.value,
2365 find(content, '#width')[0].value,
2366 find(content, '#height')[0].value
2367 );
2368 }
2369
2370 editor.closeDropDown(true);
2371 e.preventDefault();
2372 });
2373
2374 editor.createDropDown(caller, 'insertimage', content);
2375 },
2376 exec: function (caller) {
2377 var editor = this;
2378
2379 defaultCmds.image._dropDown(
2380 editor,
2381 caller,
2382 '',
2383 function (url, width, height) {
2384 var attrs = '';
2385
2386 if (width) {
2387 attrs += ' width="' + parseInt(width, 10) + '"';
2388 }
2389
2390 if (height) {
2391 attrs += ' height="' + parseInt(height, 10) + '"';
2392 }
2393
2394 attrs += ' src="' + entities(url) + '"';
2395
2396 editor.wysiwygEditorInsertHtml(
2397 '<img' + attrs + ' />'
2398 );
2399 }
2400 );
2401 },
2402 tooltip: 'Insert an image'
2403 },
2404 // END_COMMAND
2405
2406 // START_COMMAND: E-mail
2407 email: {
2408 _dropDown: function (editor, caller, cb) {
2409 var content = createElement('div');
2410
2411 appendChild(content, _tmpl('email', {
2412 label: editor._('E-mail:'),
2413 desc: editor._('Description (optional):'),
2414 insert: editor._('Insert')
2415 }, true));
2416
2417 on(content, 'click', '.button', function (e) {
2418 var email = find(content, '#email')[0].value;
2419
2420 if (email) {
2421 cb(email, find(content, '#des')[0].value);
2422 }
2423
2424 editor.closeDropDown(true);
2425 e.preventDefault();
2426 });
2427
2428 editor.createDropDown(caller, 'insertemail', content);
2429 },
2430 exec: function (caller) {
2431 var editor = this;
2432
2433 defaultCmds.email._dropDown(
2434 editor,
2435 caller,
2436 function (email, text) {
2437 if (!editor.getRangeHelper().selectedHtml() || text) {
2438 editor.wysiwygEditorInsertHtml(
2439 '<a href="' +
2440 'mailto:' + entities(email) + '">' +
2441 entities((text || email)) +
2442 '</a>'
2443 );
2444 } else {
2445 editor.execCommand('createlink', 'mailto:' + email);
2446 }
2447 }
2448 );
2449 },
2450 tooltip: 'Insert an email'
2451 },
2452 // END_COMMAND
2453
2454 // START_COMMAND: Link
2455 link: {
2456 _dropDown: function (editor, caller, cb) {
2457 var content = createElement('div');
2458
2459 appendChild(content, _tmpl('link', {
2460 url: editor._('URL:'),
2461 desc: editor._('Description (optional):'),
2462 ins: editor._('Insert')
2463 }, true));
2464
2465 var linkInput = find(content, '#link')[0];
2466
2467 function insertUrl(e) {
2468 if (linkInput.value) {
2469 cb(linkInput.value, find(content, '#des')[0].value);
2470 }
2471
2472 editor.closeDropDown(true);
2473 e.preventDefault();
2474 }
2475
2476 on(content, 'click', '.button', insertUrl);
2477 on(content, 'keypress', function (e) {
2478 // 13 = enter key
2479 if (e.which === 13 && linkInput.value) {
2480 insertUrl(e);
2481 }
2482 }, EVENT_CAPTURE);
2483
2484 editor.createDropDown(caller, 'insertlink', content);
2485 },
2486 exec: function (caller) {
2487 var editor = this;
2488
2489 defaultCmds.link._dropDown(editor, caller, function (url, text) {
2490 if (text || !editor.getRangeHelper().selectedHtml()) {
2491 editor.wysiwygEditorInsertHtml(
2492 '<a href="' + entities(url) + '">' +
2493 entities(text || url) +
2494 '</a>'
2495 );
2496 } else {
2497 editor.execCommand('createlink', url);
2498 }
2499 });
2500 },
2501 tooltip: 'Insert a link'
2502 },
2503 // END_COMMAND
2504
2505 // START_COMMAND: Unlink
2506 unlink: {
2507 state: function () {
2508 return closest(this.currentNode(), 'a') ? 0 : -1;
2509 },
2510 exec: function () {
2511 var anchor = closest(this.currentNode(), 'a');
2512
2513 if (anchor) {
2514 while (anchor.firstChild) {
2515 insertBefore(anchor.firstChild, anchor);
2516 }
2517
2518 remove(anchor);
2519 }
2520 },
2521 tooltip: 'Unlink'
2522 },
2523 // END_COMMAND
2524
2525
2526 // START_COMMAND: Quote
2527 quote: {
2528 exec: function (caller, html, author) {
2529 var before = '<blockquote>',
2530 end = '</blockquote>';
2531
2532 // if there is HTML passed set end to null so any selected
2533 // text is replaced
2534 if (html) {
2535 author = (author ? '<cite>' +
2536 entities(author) +
2537 '</cite>' : '');
2538 before = before + author + html + end;
2539 end = null;
2540 // if not add a newline to the end of the inserted quote
2541 } else if (this.getRangeHelper().selectedHtml() === '') {
2542 end = '<br />' + end;
2543 }
2544
2545 this.wysiwygEditorInsertHtml(before, end);
2546 },
2547 tooltip: 'Insert a Quote'
2548 },
2549 // END_COMMAND
2550
2551 // START_COMMAND: Emoticons
2552 emoticon: {
2553 exec: function (caller) {
2554 var editor = this;
2555
2556 var createContent = function (includeMore) {
2557 var moreLink,
2558 opts = editor.opts,
2559 emoticonsRoot = opts.emoticonsRoot || '',
2560 emoticonsCompat = opts.emoticonsCompat,
2561 rangeHelper = editor.getRangeHelper(),
2562 startSpace = emoticonsCompat &&
2563 rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
2564 endSpace = emoticonsCompat &&
2565 rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
2566 content = createElement('div'),
2567 line = createElement('div'),
2568 perLine = 0,
2569 emoticons = extend(
2570 {},
2571 opts.emoticons.dropdown,
2572 includeMore ? opts.emoticons.more : {}
2573 );
2574
2575 appendChild(content, line);
2576
2577 perLine = Math.sqrt(Object.keys(emoticons).length);
2578
2579 on(content, 'click', 'img', function (e) {
2580 editor.insert(startSpace + attr(this, 'alt') + endSpace,
2581 null, false).closeDropDown(true);
2582
2583 e.preventDefault();
2584 });
2585
2586 each(emoticons, function (code, emoticon) {
2587 appendChild(line, createElement('img', {
2588 src: emoticonsRoot + (emoticon.url || emoticon),
2589 alt: code,
2590 title: emoticon.tooltip || code
2591 }));
2592
2593 if (line.children.length >= perLine) {
2594 line = createElement('div');
2595 appendChild(content, line);
2596 }
2597 });
2598
2599 if (!includeMore && opts.emoticons.more) {
2600 moreLink = createElement('a', {
2601 className: 'sceditor-more'
2602 });
2603
2604 appendChild(moreLink,
2605 document.createTextNode(editor._('More')));
2606
2607 on(moreLink, 'click', function (e) {
2608 editor.createDropDown(
2609 caller, 'more-emoticons', createContent(true)
2610 );
2611
2612 e.preventDefault();
2613 });
2614
2615 appendChild(content, moreLink);
2616 }
2617
2618 return content;
2619 };
2620
2621 editor.createDropDown(caller, 'emoticons', createContent(false));
2622 },
2623 txtExec: function (caller) {
2624 defaultCmds.emoticon.exec.call(this, caller);
2625 },
2626 tooltip: 'Insert an emoticon'
2627 },
2628 // END_COMMAND
2629
2630 // START_COMMAND: YouTube
2631 youtube: {
2632 _dropDown: function (editor, caller, callback) {
2633 var content = createElement('div');
2634
2635 appendChild(content, _tmpl('youtubeMenu', {
2636 label: editor._('Video URL:'),
2637 insert: editor._('Insert')
2638 }, true));
2639
2640 on(content, 'click', '.button', function (e) {
2641 var val = find(content, '#link')[0].value;
2642 var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)?([a-zA-Z0-9_-]{11})/);
2643 var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/);
2644 var time = 0;
2645
2646 if (timeMatch) {
2647 each(timeMatch[1].split(/[hms]/), function (i, val) {
2648 if (val !== '') {
2649 time = (time * 60) + Number(val);
2650 }
2651 });
2652 }
2653
2654 if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) {
2655 callback(idMatch[1], time);
2656 }
2657
2658 editor.closeDropDown(true);
2659 e.preventDefault();
2660 });
2661
2662 editor.createDropDown(caller, 'insertlink', content);
2663 },
2664 exec: function (btn) {
2665 var editor = this;
2666
2667 defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
2668 editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
2669 id: id,
2670 time: time
2671 }));
2672 });
2673 },
2674 tooltip: 'Insert a YouTube video'
2675 },
2676 // END_COMMAND
2677
2678 // START_COMMAND: Date
2679 date: {
2680 _date: function (editor) {
2681 var now = new Date(),
2682 year = now.getYear(),
2683 month = now.getMonth() + 1,
2684 day = now.getDate();
2685
2686 if (year < 2000) {
2687 year = 1900 + year;
2688 }
2689
2690 if (month < 10) {
2691 month = '0' + month;
2692 }
2693
2694 if (day < 10) {
2695 day = '0' + day;
2696 }
2697
2698 return editor.opts.dateFormat
2699 .replace(/year/i, year)
2700 .replace(/month/i, month)
2701 .replace(/day/i, day);
2702 },
2703 exec: function () {
2704 this.insertText(defaultCmds.date._date(this));
2705 },
2706 txtExec: function () {
2707 this.insertText(defaultCmds.date._date(this));
2708 },
2709 tooltip: 'Insert current date'
2710 },
2711 // END_COMMAND
2712
2713 // START_COMMAND: Time
2714 time: {
2715 _time: function () {
2716 var now = new Date(),
2717 hours = now.getHours(),
2718 mins = now.getMinutes(),
2719 secs = now.getSeconds();
2720
2721 if (hours < 10) {
2722 hours = '0' + hours;
2723 }
2724
2725 if (mins < 10) {
2726 mins = '0' + mins;
2727 }
2728
2729 if (secs < 10) {
2730 secs = '0' + secs;
2731 }
2732
2733 return hours + ':' + mins + ':' + secs;
2734 },
2735 exec: function () {
2736 this.insertText(defaultCmds.time._time());
2737 },
2738 txtExec: function () {
2739 this.insertText(defaultCmds.time._time());
2740 },
2741 tooltip: 'Insert current time'
2742 },
2743 // END_COMMAND
2744
2745
2746 // START_COMMAND: Ltr
2747 ltr: {
2748 state: function (parents, firstBlock) {
2749 return firstBlock && firstBlock.style.direction === 'ltr';
2750 },
2751 exec: function () {
2752 var editor = this,
2753 rangeHelper = editor.getRangeHelper(),
2754 node = rangeHelper.getFirstBlockParent();
2755
2756 editor.focus();
2757
2758 if (!node || is(node, 'body')) {
2759 editor.execCommand('formatBlock', 'p');
2760
2761 node = rangeHelper.getFirstBlockParent();
2762
2763 if (!node || is(node, 'body')) {
2764 return;
2765 }
2766 }
2767
2768 var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr';
2769 css(node, 'direction', toggleValue);
2770 },
2771 tooltip: 'Left-to-Right'
2772 },
2773 // END_COMMAND
2774
2775 // START_COMMAND: Rtl
2776 rtl: {
2777 state: function (parents, firstBlock) {
2778 return firstBlock && firstBlock.style.direction === 'rtl';
2779 },
2780 exec: function () {
2781 var editor = this,
2782 rangeHelper = editor.getRangeHelper(),
2783 node = rangeHelper.getFirstBlockParent();
2784
2785 editor.focus();
2786
2787 if (!node || is(node, 'body')) {
2788 editor.execCommand('formatBlock', 'p');
2789
2790 node = rangeHelper.getFirstBlockParent();
2791
2792 if (!node || is(node, 'body')) {
2793 return;
2794 }
2795 }
2796
2797 var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl';
2798 css(node, 'direction', toggleValue);
2799 },
2800 tooltip: 'Right-to-Left'
2801 },
2802 // END_COMMAND
2803
2804
2805 // START_COMMAND: Print
2806 print: {
2807 exec: 'print',
2808 tooltip: 'Print'
2809 },
2810 // END_COMMAND
2811
2812 // START_COMMAND: Maximize
2813 maximize: {
2814 state: function () {
2815 return this.maximize();
2816 },
2817 exec: function () {
2818 this.maximize(!this.maximize());
2819 this.focus();
2820 },
2821 txtExec: function () {
2822 this.maximize(!this.maximize());
2823 this.focus();
2824 },
2825 tooltip: 'Maximize',
2826 shortcut: 'Ctrl+Shift+M'
2827 },
2828 // END_COMMAND
2829
2830 // START_COMMAND: Source
2831 source: {
2832 state: function () {
2833 return this.sourceMode();
2834 },
2835 exec: function () {
2836 this.toggleSourceMode();
2837 this.focus();
2838 },
2839 txtExec: function () {
2840 this.toggleSourceMode();
2841 this.focus();
2842 },
2843 tooltip: 'View source',
2844 shortcut: 'Ctrl+Shift+S'
2845 },
2846 // END_COMMAND
2847
2848 // this is here so that commands above can be removed
2849 // without having to remove the , after the last one.
2850 // Needed for IE.
2851 ignore: {}
2852 };
2853
2854 var plugins = {};
2855
2856 /**
2857 * Plugin Manager class
2858 * @class PluginManager
2859 * @name PluginManager
2860 */
2861 function PluginManager(thisObj) {
2862 /**
2863 * Alias of this
2864 *
2865 * @private
2866 * @type {Object}
2867 */
2868 var base = this;
2869
2870 /**
2871 * Array of all currently registered plugins
2872 *
2873 * @type {Array}
2874 * @private
2875 */
2876 var registeredPlugins = [];
2877
2878
2879 /**
2880 * Changes a signals name from "name" into "signalName".
2881 *
2882 * @param {string} signal
2883 * @return {string}
2884 * @private
2885 */
2886 var formatSignalName = function (signal) {
2887 return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
2888 };
2889
2890 /**
2891 * Calls handlers for a signal
2892 *
2893 * @see call()
2894 * @see callOnlyFirst()
2895 * @param {Array} args
2896 * @param {boolean} returnAtFirst
2897 * @return {*}
2898 * @private
2899 */
2900 var callHandlers = function (args, returnAtFirst) {
2901 args = [].slice.call(args);
2902
2903 var idx, ret,
2904 signal = formatSignalName(args.shift());
2905
2906 for (idx = 0; idx < registeredPlugins.length; idx++) {
2907 if (signal in registeredPlugins[idx]) {
2908 ret = registeredPlugins[idx][signal].apply(thisObj, args);
2909
2910 if (returnAtFirst) {
2911 return ret;
2912 }
2913 }
2914 }
2915 };
2916
2917 /**
2918 * Calls all handlers for the passed signal
2919 *
2920 * @param {string} signal
2921 * @param {...string} args
2922 * @function
2923 * @name call
2924 * @memberOf PluginManager.prototype
2925 */
2926 base.call = function () {
2927 callHandlers(arguments, false);
2928 };
2929
2930 /**
2931 * Calls the first handler for a signal, and returns the
2932 *
2933 * @param {string} signal
2934 * @param {...string} args
2935 * @return {*} The result of calling the handler
2936 * @function
2937 * @name callOnlyFirst
2938 * @memberOf PluginManager.prototype
2939 */
2940 base.callOnlyFirst = function () {
2941 return callHandlers(arguments, true);
2942 };
2943
2944 /**
2945 * Checks if a signal has a handler
2946 *
2947 * @param {string} signal
2948 * @return {boolean}
2949 * @function
2950 * @name hasHandler
2951 * @memberOf PluginManager.prototype
2952 */
2953 base.hasHandler = function (signal) {
2954 var i = registeredPlugins.length;
2955 signal = formatSignalName(signal);
2956
2957 while (i--) {
2958 if (signal in registeredPlugins[i]) {
2959 return true;
2960 }
2961 }
2962
2963 return false;
2964 };
2965
2966 /**
2967 * Checks if the plugin exists in plugins
2968 *
2969 * @param {string} plugin
2970 * @return {boolean}
2971 * @function
2972 * @name exists
2973 * @memberOf PluginManager.prototype
2974 */
2975 base.exists = function (plugin) {
2976 if (plugin in plugins) {
2977 plugin = plugins[plugin];
2978
2979 return typeof plugin === 'function' &&
2980 typeof plugin.prototype === 'object';
2981 }
2982
2983 return false;
2984 };
2985
2986 /**
2987 * Checks if the passed plugin is currently registered.
2988 *
2989 * @param {string} plugin
2990 * @return {boolean}
2991 * @function
2992 * @name isRegistered
2993 * @memberOf PluginManager.prototype
2994 */
2995 base.isRegistered = function (plugin) {
2996 if (base.exists(plugin)) {
2997 var idx = registeredPlugins.length;
2998
2999 while (idx--) {
3000 if (registeredPlugins[idx] instanceof plugins[plugin]) {
3001 return true;
3002 }
3003 }
3004 }
3005
3006 return false;
3007 };
3008
3009 /**
3010 * Registers a plugin to receive signals
3011 *
3012 * @param {string} plugin
3013 * @return {boolean}
3014 * @function
3015 * @name register
3016 * @memberOf PluginManager.prototype
3017 */
3018 base.register = function (plugin) {
3019 if (!base.exists(plugin) || base.isRegistered(plugin)) {
3020 return false;
3021 }
3022
3023 plugin = new plugins[plugin]();
3024 registeredPlugins.push(plugin);
3025
3026 if ('init' in plugin) {
3027 plugin.init.call(thisObj);
3028 }
3029
3030 return true;
3031 };
3032
3033 /**
3034 * Deregisters a plugin.
3035 *
3036 * @param {string} plugin
3037 * @return {boolean}
3038 * @function
3039 * @name deregister
3040 * @memberOf PluginManager.prototype
3041 */
3042 base.deregister = function (plugin) {
3043 var removedPlugin,
3044 pluginIdx = registeredPlugins.length,
3045 removed = false;
3046
3047 if (!base.isRegistered(plugin)) {
3048 return removed;
3049 }
3050
3051 while (pluginIdx--) {
3052 if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) {
3053 removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0];
3054 removed = true;
3055
3056 if ('destroy' in removedPlugin) {
3057 removedPlugin.destroy.call(thisObj);
3058 }
3059 }
3060 }
3061
3062 return removed;
3063 };
3064
3065 /**
3066 * Clears all plugins and removes the owner reference.
3067 *
3068 * Calling any functions on this object after calling
3069 * destroy will cause a JS error.
3070 *
3071 * @name destroy
3072 * @memberOf PluginManager.prototype
3073 */
3074 base.destroy = function () {
3075 var i = registeredPlugins.length;
3076
3077 while (i--) {
3078 if ('destroy' in registeredPlugins[i]) {
3079 registeredPlugins[i].destroy.call(thisObj);
3080 }
3081 }
3082
3083 registeredPlugins = [];
3084 thisObj = null;
3085 };
3086 }
3087 PluginManager.plugins = plugins;
3088
3089 /**
3090 * Gets the text, start/end node and offset for
3091 * length chars left or right of the passed node
3092 * at the specified offset.
3093 *
3094 * @param {Node} node
3095 * @param {number} offset
3096 * @param {boolean} isLeft
3097 * @param {number} length
3098 * @return {Object}
3099 * @private
3100 */
3101 var outerText = function (range, isLeft, length) {
3102 var nodeValue, remaining, start, end, node,
3103 text = '',
3104 next = range.startContainer,
3105 offset = range.startOffset;
3106
3107 // Handle cases where node is a paragraph and offset
3108 // refers to the index of a text node.
3109 // 3 = text node
3110 if (next && next.nodeType !== 3) {
3111 next = next.childNodes[offset];
3112 offset = 0;
3113 }
3114
3115 start = end = offset;
3116
3117 while (length > text.length && next && next.nodeType === 3) {
3118 nodeValue = next.nodeValue;
3119 remaining = length - text.length;
3120
3121 // If not the first node, start and end should be at their
3122 // max values as will be updated when getting the text
3123 if (node) {
3124 end = nodeValue.length;
3125 start = 0;
3126 }
3127
3128 node = next;
3129
3130 if (isLeft) {
3131 start = Math.max(end - remaining, 0);
3132 offset = start;
3133
3134 text = nodeValue.substr(start, end - start) + text;
3135 next = node.previousSibling;
3136 } else {
3137 end = Math.min(remaining, nodeValue.length);
3138 offset = start + end;
3139
3140 text += nodeValue.substr(start, end);
3141 next = node.nextSibling;
3142 }
3143 }
3144
3145 return {
3146 node: node || next,
3147 offset: offset,
3148 text: text
3149 };
3150 };
3151
3152 /**
3153 * Range helper
3154 *
3155 * @class RangeHelper
3156 * @name RangeHelper
3157 */
3158 function RangeHelper(win, d, sanitize) {
3159 var _createMarker, _prepareInput,
3160 doc = d || win.contentDocument || win.document,
3161 startMarker = 'sceditor-start-marker',
3162 endMarker = 'sceditor-end-marker',
3163 base = this;
3164
3165 /**
3166 * Inserts HTML into the current range replacing any selected
3167 * text.
3168 *
3169 * If endHTML is specified the selected contents will be put between
3170 * html and endHTML. If there is nothing selected html and endHTML are
3171 * just concatenate together.
3172 *
3173 * @param {string} html
3174 * @param {string} [endHTML]
3175 * @return False on fail
3176 * @function
3177 * @name insertHTML
3178 * @memberOf RangeHelper.prototype
3179 */
3180 base.insertHTML = function (html, endHTML) {
3181 var node, div,
3182 range = base.selectedRange();
3183
3184 if (!range) {
3185 return false;
3186 }
3187
3188 if (endHTML) {
3189 html += base.selectedHtml() + endHTML;
3190 }
3191
3192 div = createElement('p', {}, doc);
3193 node = doc.createDocumentFragment();
3194 div.innerHTML = sanitize(html);
3195
3196 while (div.firstChild) {
3197 appendChild(node, div.firstChild);
3198 }
3199
3200 base.insertNode(node);
3201 };
3202
3203 /**
3204 * Prepares HTML to be inserted by adding a zero width space
3205 * if the last child is empty and adding the range start/end
3206 * markers to the last child.
3207 *
3208 * @param {Node|string} node
3209 * @param {Node|string} [endNode]
3210 * @param {boolean} [returnHtml]
3211 * @return {Node|string}
3212 * @private
3213 */
3214 _prepareInput = function (node, endNode, returnHtml) {
3215 var lastChild,
3216 frag = doc.createDocumentFragment();
3217
3218 if (typeof node === 'string') {
3219 if (endNode) {
3220 node += base.selectedHtml() + endNode;
3221 }
3222
3223 frag = parseHTML(node);
3224 } else {
3225 appendChild(frag, node);
3226
3227 if (endNode) {
3228 appendChild(frag, base.selectedRange().extractContents());
3229 appendChild(frag, endNode);
3230 }
3231 }
3232
3233 if (!(lastChild = frag.lastChild)) {
3234 return;
3235 }
3236
3237 while (!isInline(lastChild.lastChild, true)) {
3238 lastChild = lastChild.lastChild;
3239 }
3240
3241 if (canHaveChildren(lastChild)) {
3242 // Webkit won't allow the cursor to be placed inside an
3243 // empty tag, so add a zero width space to it.
3244 if (!lastChild.lastChild) {
3245 appendChild(lastChild, document.createTextNode('\u200B'));
3246 }
3247 } else {
3248 lastChild = frag;
3249 }
3250
3251 base.removeMarkers();
3252
3253 // Append marks to last child so when restored cursor will be in
3254 // the right place
3255 appendChild(lastChild, _createMarker(startMarker));
3256 appendChild(lastChild, _createMarker(endMarker));
3257
3258 if (returnHtml) {
3259 var div = createElement('div');
3260 appendChild(div, frag);
3261
3262 return div.innerHTML;
3263 }
3264
3265 return frag;
3266 };
3267
3268 /**
3269 * The same as insertHTML except with DOM nodes instead
3270 *
3271 * <strong>Warning:</strong> the nodes must belong to the
3272 * document they are being inserted into. Some browsers
3273 * will throw exceptions if they don't.
3274 *
3275 * Returns boolean false on fail
3276 *
3277 * @param {Node} node
3278 * @param {Node} endNode
3279 * @return {false|undefined}
3280 * @function
3281 * @name insertNode
3282 * @memberOf RangeHelper.prototype
3283 */
3284 base.insertNode = function (node, endNode) {
3285 var first, last,
3286 input = _prepareInput(node, endNode),
3287 range = base.selectedRange(),
3288 parent = range.commonAncestorContainer,
3289 emptyNodes = [];
3290
3291 if (!input) {
3292 return false;
3293 }
3294
3295 function removeIfEmpty(node) {
3296 // Only remove empty node if it wasn't already empty
3297 if (node && isEmpty(node) && emptyNodes.indexOf(node) < 0) {
3298 remove(node);
3299 }
3300 }
3301
3302 if (range.startContainer !== range.endContainer) {
3303 each(parent.childNodes, function (_, node) {
3304 if (isEmpty(node)) {
3305 emptyNodes.push(node);
3306 }
3307 });
3308
3309 first = input.firstChild;
3310 last = input.lastChild;
3311 }
3312
3313 range.deleteContents();
3314
3315 // FF allows <br /> to be selected but inserting a node
3316 // into <br /> will cause it not to be displayed so must
3317 // insert before the <br /> in FF.
3318 // 3 = TextNode
3319 if (parent && parent.nodeType !== 3 && !canHaveChildren(parent)) {
3320 insertBefore(input, parent);
3321 } else {
3322 range.insertNode(input);
3323
3324 // If a node was split or its contents deleted, remove any resulting
3325 // empty tags. For example:
3326 // <p>|test</p><div>test|</div>
3327 // When deleteContents could become:
3328 // <p></p>|<div></div>
3329 // So remove the empty ones
3330 removeIfEmpty(first && first.previousSibling);
3331 removeIfEmpty(last && last.nextSibling);
3332 }
3333
3334 base.restoreRange();
3335 };
3336
3337 /**
3338 * Clones the selected Range
3339 *
3340 * @return {Range}
3341 * @function
3342 * @name cloneSelected
3343 * @memberOf RangeHelper.prototype
3344 */
3345 base.cloneSelected = function () {
3346 var range = base.selectedRange();
3347
3348 if (range) {
3349 return range.cloneRange();
3350 }
3351 };
3352
3353 /**
3354 * Gets the selected Range
3355 *
3356 * @return {Range}
3357 * @function
3358 * @name selectedRange
3359 * @memberOf RangeHelper.prototype
3360 */
3361 base.selectedRange = function () {
3362 var range, firstChild,
3363 sel = win.getSelection();
3364
3365 if (!sel) {
3366 return;
3367 }
3368
3369 // When creating a new range, set the start to the first child
3370 // element of the body element to avoid errors in FF.
3371 if (sel.rangeCount <= 0) {
3372 firstChild = doc.body;
3373 while (firstChild.firstChild) {
3374 firstChild = firstChild.firstChild;
3375 }
3376
3377 range = doc.createRange();
3378 // Must be setStartBefore otherwise it can cause infinite
3379 // loops with lists in WebKit. See issue 442
3380 range.setStartBefore(firstChild);
3381
3382 sel.addRange(range);
3383 }
3384
3385 if (sel.rangeCount > 0) {
3386 range = sel.getRangeAt(0);
3387 }
3388
3389 return range;
3390 };
3391
3392 /**
3393 * Gets if there is currently a selection
3394 *
3395 * @return {boolean}
3396 * @function
3397 * @name hasSelection
3398 * @since 1.4.4
3399 * @memberOf RangeHelper.prototype
3400 */
3401 base.hasSelection = function () {
3402 var sel = win.getSelection();
3403
3404 return sel && sel.rangeCount > 0;
3405 };
3406
3407 /**
3408 * Gets the currently selected HTML
3409 *
3410 * @return {string}
3411 * @function
3412 * @name selectedHtml
3413 * @memberOf RangeHelper.prototype
3414 */
3415 base.selectedHtml = function () {
3416 var div,
3417 range = base.selectedRange();
3418
3419 if (range) {
3420 div = createElement('p', {}, doc);
3421 appendChild(div, range.cloneContents());
3422
3423 return div.innerHTML;
3424 }
3425
3426 return '';
3427 };
3428
3429 /**
3430 * Gets the parent node of the selected contents in the range
3431 *
3432 * @return {HTMLElement}
3433 * @function
3434 * @name parentNode
3435 * @memberOf RangeHelper.prototype
3436 */
3437 base.parentNode = function () {
3438 var range = base.selectedRange();
3439
3440 if (range) {
3441 return range.commonAncestorContainer;
3442 }
3443 };
3444
3445 /**
3446 * Gets the first block level parent of the selected
3447 * contents of the range.
3448 *
3449 * @return {HTMLElement}
3450 * @function
3451 * @name getFirstBlockParent
3452 * @memberOf RangeHelper.prototype
3453 */
3454 /**
3455 * Gets the first block level parent of the selected
3456 * contents of the range.
3457 *
3458 * @param {Node} [n] The element to get the first block level parent from
3459 * @return {HTMLElement}
3460 * @function
3461 * @name getFirstBlockParent^2
3462 * @since 1.4.1
3463 * @memberOf RangeHelper.prototype
3464 */
3465 base.getFirstBlockParent = function (node) {
3466 var func = function (elm) {
3467 if (!isInline(elm, true)) {
3468 return elm;
3469 }
3470
3471 elm = elm ? elm.parentNode : null;
3472
3473 return elm ? func(elm) : elm;
3474 };
3475
3476 return func(node || base.parentNode());
3477 };
3478
3479 /**
3480 * Inserts a node at either the start or end of the current selection
3481 *
3482 * @param {Bool} start
3483 * @param {Node} node
3484 * @function
3485 * @name insertNodeAt
3486 * @memberOf RangeHelper.prototype
3487 */
3488 base.insertNodeAt = function (start, node) {
3489 var currentRange = base.selectedRange(),
3490 range = base.cloneSelected();
3491
3492 if (!range) {
3493 return false;
3494 }
3495
3496 range.collapse(start);
3497 range.insertNode(node);
3498
3499 // Reselect the current range.
3500 // Fixes issue with Chrome losing the selection. Issue#82
3501 base.selectRange(currentRange);
3502 };
3503
3504 /**
3505 * Creates a marker node
3506 *
3507 * @param {string} id
3508 * @return {HTMLSpanElement}
3509 * @private
3510 */
3511 _createMarker = function (id) {
3512 base.removeMarker(id);
3513
3514 var marker = createElement('span', {
3515 id: id,
3516 className: 'sceditor-selection sceditor-ignore',
3517 style: 'display:none;line-height:0'
3518 }, doc);
3519
3520 marker.innerHTML = ' ';
3521
3522 return marker;
3523 };
3524
3525 /**
3526 * Inserts start/end markers for the current selection
3527 * which can be used by restoreRange to re-select the
3528 * range.
3529 *
3530 * @memberOf RangeHelper.prototype
3531 * @function
3532 * @name insertMarkers
3533 */
3534 base.insertMarkers = function () {
3535 var currentRange = base.selectedRange();
3536 var startNode = _createMarker(startMarker);
3537
3538 base.removeMarkers();
3539 base.insertNodeAt(true, startNode);
3540
3541 // Fixes issue with end marker sometimes being placed before
3542 // the start marker when the range is collapsed.
3543 if (currentRange && currentRange.collapsed) {
3544 startNode.parentNode.insertBefore(
3545 _createMarker(endMarker), startNode.nextSibling);
3546 } else {
3547 base.insertNodeAt(false, _createMarker(endMarker));
3548 }
3549 };
3550
3551 /**
3552 * Gets the marker with the specified ID
3553 *
3554 * @param {string} id
3555 * @return {Node}
3556 * @function
3557 * @name getMarker
3558 * @memberOf RangeHelper.prototype
3559 */
3560 base.getMarker = function (id) {
3561 return doc.getElementById(id);
3562 };
3563
3564 /**
3565 * Removes the marker with the specified ID
3566 *
3567 * @param {string} id
3568 * @function
3569 * @name removeMarker
3570 * @memberOf RangeHelper.prototype
3571 */
3572 base.removeMarker = function (id) {
3573 var marker = base.getMarker(id);
3574
3575 if (marker) {
3576 remove(marker);
3577 }
3578 };
3579
3580 /**
3581 * Removes the start/end markers
3582 *
3583 * @function
3584 * @name removeMarkers
3585 * @memberOf RangeHelper.prototype
3586 */
3587 base.removeMarkers = function () {
3588 base.removeMarker(startMarker);
3589 base.removeMarker(endMarker);
3590 };
3591
3592 /**
3593 * Saves the current range location. Alias of insertMarkers()
3594 *
3595 * @function
3596 * @name saveRage
3597 * @memberOf RangeHelper.prototype
3598 */
3599 base.saveRange = function () {
3600 base.insertMarkers();
3601 };
3602
3603 /**
3604 * Select the specified range
3605 *
3606 * @param {Range} range
3607 * @function
3608 * @name selectRange
3609 * @memberOf RangeHelper.prototype
3610 */
3611 base.selectRange = function (range) {
3612 var lastChild;
3613 var sel = win.getSelection();
3614 var container = range.endContainer;
3615
3616 // Check if cursor is set after a BR when the BR is the only
3617 // child of the parent. In Firefox this causes a line break
3618 // to occur when something is typed. See issue #321
3619 if (range.collapsed && container &&
3620 !isInline(container, true)) {
3621
3622 lastChild = container.lastChild;
3623 while (lastChild && is(lastChild, '.sceditor-ignore')) {
3624 lastChild = lastChild.previousSibling;
3625 }
3626
3627 if (is(lastChild, 'br')) {
3628 var rng = doc.createRange();
3629 rng.setEndAfter(lastChild);
3630 rng.collapse(false);
3631
3632 if (base.compare(range, rng)) {
3633 range.setStartBefore(lastChild);
3634 range.collapse(true);
3635 }
3636 }
3637 }
3638
3639 if (sel) {
3640 base.clear();
3641 sel.addRange(range);
3642 }
3643 };
3644
3645 /**
3646 * Restores the last range saved by saveRange() or insertMarkers()
3647 *
3648 * @function
3649 * @name restoreRange
3650 * @memberOf RangeHelper.prototype
3651 */
3652 base.restoreRange = function () {
3653 var isCollapsed,
3654 range = base.selectedRange(),
3655 start = base.getMarker(startMarker),
3656 end = base.getMarker(endMarker);
3657
3658 if (!start || !end || !range) {
3659 return false;
3660 }
3661
3662 isCollapsed = start.nextSibling === end;
3663
3664 range = doc.createRange();
3665 range.setStartBefore(start);
3666 range.setEndAfter(end);
3667
3668 if (isCollapsed) {
3669 range.collapse(true);
3670 }
3671
3672 base.selectRange(range);
3673 base.removeMarkers();
3674 };
3675
3676 /**
3677 * Selects the text left and right of the current selection
3678 *
3679 * @param {number} left
3680 * @param {number} right
3681 * @since 1.4.3
3682 * @function
3683 * @name selectOuterText
3684 * @memberOf RangeHelper.prototype
3685 */
3686 base.selectOuterText = function (left, right) {
3687 var start, end,
3688 range = base.cloneSelected();
3689
3690 if (!range) {
3691 return false;
3692 }
3693
3694 range.collapse(false);
3695
3696 start = outerText(range, true, left);
3697 end = outerText(range, false, right);
3698
3699 range.setStart(start.node, start.offset);
3700 range.setEnd(end.node, end.offset);
3701
3702 base.selectRange(range);
3703 };
3704
3705 /**
3706 * Gets the text left or right of the current selection
3707 *
3708 * @param {boolean} before
3709 * @param {number} length
3710 * @return {string}
3711 * @since 1.4.3
3712 * @function
3713 * @name selectOuterText
3714 * @memberOf RangeHelper.prototype
3715 */
3716 base.getOuterText = function (before, length) {
3717 var range = base.cloneSelected();
3718
3719 if (!range) {
3720 return '';
3721 }
3722
3723 range.collapse(!before);
3724
3725 return outerText(range, before, length).text;
3726 };
3727
3728 /**
3729 * Replaces keywords with values based on the current caret position
3730 *
3731 * @param {Array} keywords
3732 * @param {boolean} includeAfter If to include the text after the
3733 * current caret position or just
3734 * text before
3735 * @param {boolean} keywordsSorted If the keywords array is pre
3736 * sorted shortest to longest
3737 * @param {number} longestKeyword Length of the longest keyword
3738 * @param {boolean} requireWhitespace If the key must be surrounded
3739 * by whitespace
3740 * @param {string} keypressChar If this is being called from
3741 * a keypress event, this should be
3742 * set to the pressed character
3743 * @return {boolean}
3744 * @function
3745 * @name replaceKeyword
3746 * @memberOf RangeHelper.prototype
3747 */
3748 // eslint-disable-next-line max-params
3749 base.replaceKeyword = function (
3750 keywords,
3751 includeAfter,
3752 keywordsSorted,
3753 longestKeyword,
3754 requireWhitespace,
3755 keypressChar
3756 ) {
3757 if (!keywordsSorted) {
3758 keywords.sort(function (a, b) {
3759 return a[0].length - b[0].length;
3760 });
3761 }
3762
3763 var outerText, match, matchPos, startIndex,
3764 leftLen, charsLeft, keyword, keywordLen,
3765 whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])',
3766 keywordIdx = keywords.length,
3767 whitespaceLen = requireWhitespace ? 1 : 0,
3768 maxKeyLen = longestKeyword ||
3769 keywords[keywordIdx - 1][0].length;
3770
3771 if (requireWhitespace) {
3772 maxKeyLen++;
3773 }
3774
3775 keypressChar = keypressChar || '';
3776 outerText = base.getOuterText(true, maxKeyLen);
3777 leftLen = outerText.length;
3778 outerText += keypressChar;
3779
3780 if (includeAfter) {
3781 outerText += base.getOuterText(false, maxKeyLen);
3782 }
3783
3784 while (keywordIdx--) {
3785 keyword = keywords[keywordIdx][0];
3786 keywordLen = keyword.length;
3787 startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen);
3788 matchPos = -1;
3789
3790 if (requireWhitespace) {
3791 match = outerText
3792 .substr(startIndex)
3793 .match(new RegExp(whitespaceRegex +
3794 regex(keyword) + whitespaceRegex));
3795
3796 if (match) {
3797 // Add the length of the text that was removed by
3798 // substr() and also add 1 for the whitespace
3799 matchPos = match.index + startIndex + match[1].length;
3800 }
3801 } else {
3802 matchPos = outerText.indexOf(keyword, startIndex);
3803 }
3804
3805 if (matchPos > -1) {
3806 // Make sure the match is between before and
3807 // after, not just entirely in one side or the other
3808 if (matchPos <= leftLen &&
3809 matchPos + keywordLen + whitespaceLen >= leftLen) {
3810 charsLeft = leftLen - matchPos;
3811
3812 // If the keypress char is white space then it should
3813 // not be replaced, only chars that are part of the
3814 // key should be replaced.
3815 base.selectOuterText(
3816 charsLeft,
3817 keywordLen - charsLeft -
3818 (/^\S/.test(keypressChar) ? 1 : 0)
3819 );
3820
3821 base.insertHTML(keywords[keywordIdx][1]);
3822 return true;
3823 }
3824 }
3825 }
3826
3827 return false;
3828 };
3829
3830 /**
3831 * Compares two ranges.
3832 *
3833 * If rangeB is undefined it will be set to
3834 * the current selected range
3835 *
3836 * @param {Range} rngA
3837 * @param {Range} [rngB]
3838 * @return {boolean}
3839 * @function
3840 * @name compare
3841 * @memberOf RangeHelper.prototype
3842 */
3843 base.compare = function (rngA, rngB) {
3844 if (!rngB) {
3845 rngB = base.selectedRange();
3846 }
3847
3848 if (!rngA || !rngB) {
3849 return !rngA && !rngB;
3850 }
3851
3852 return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 &&
3853 rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0;
3854 };
3855
3856 /**
3857 * Removes any current selection
3858 *
3859 * @since 1.4.6
3860 * @function
3861 * @name clear
3862 * @memberOf RangeHelper.prototype
3863 */
3864 base.clear = function () {
3865 var sel = win.getSelection();
3866
3867 if (sel) {
3868 if (sel.removeAllRanges) {
3869 sel.removeAllRanges();
3870 } else if (sel.empty) {
3871 sel.empty();
3872 }
3873 }
3874 };
3875 }
3876
3877 var USER_AGENT = navigator.userAgent;
3878
3879 /**
3880 * Detects if the browser is iOS
3881 *
3882 * Needed to fix iOS specific bugs
3883 *
3884 * @function
3885 * @name ios
3886 * @memberOf jQuery.sceditor
3887 * @type {boolean}
3888 */
3889 var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT);
3890
3891 /**
3892 * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
3893 *
3894 * @function
3895 * @name isWysiwygSupported
3896 * @return {boolean}
3897 */
3898 var isWysiwygSupported = (function () {
3899 var match, isUnsupported;
3900
3901 // IE is the only browser to support documentMode
3902 var ie = !!window.document.documentMode;
3903 var legacyEdge = '-ms-ime-align' in document.documentElement.style;
3904
3905 var div = document.createElement('div');
3906 div.contentEditable = true;
3907
3908 // Check if the contentEditable attribute is supported
3909 if (!('contentEditable' in document.documentElement) ||
3910 div.contentEditable !== 'true') {
3911 return false;
3912 }
3913
3914 // I think blackberry supports contentEditable or will at least
3915 // give a valid value for the contentEditable detection above
3916 // so it isn't included in the below tests.
3917
3918 // I hate having to do UA sniffing but some mobile browsers say they
3919 // support contentediable when it isn't usable, i.e. you can't enter
3920 // text.
3921 // This is the only way I can think of to detect them which is also how
3922 // every other editor I've seen deals with this issue.
3923
3924 // Exclude Opera mobile and mini
3925 isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT);
3926
3927 if (/Android/i.test(USER_AGENT)) {
3928 isUnsupported = true;
3929
3930 if (/Safari/.test(USER_AGENT)) {
3931 // Android browser 534+ supports content editable
3932 // This also matches Chrome which supports content editable too
3933 match = /Safari\/(\d+)/.exec(USER_AGENT);
3934 isUnsupported = (!match || !match[1] ? true : match[1] < 534);
3935 }
3936 }
3937
3938 // The current version of Amazon Silk supports it, older versions didn't
3939 // As it uses webkit like Android, assume it's the same and started
3940 // working at versions >= 534
3941 if (/ Silk\//i.test(USER_AGENT)) {
3942 match = /AppleWebKit\/(\d+)/.exec(USER_AGENT);
3943 isUnsupported = (!match || !match[1] ? true : match[1] < 534);
3944 }
3945
3946 // iOS 5+ supports content editable
3947 if (ios) {
3948 // Block any version <= 4_x(_x)
3949 isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT);
3950 }
3951
3952 // Firefox does support WYSIWYG on mobiles so override
3953 // any previous value if using FF
3954 if (/Firefox/i.test(USER_AGENT)) {
3955 isUnsupported = false;
3956 }
3957
3958 if (/OneBrowser/i.test(USER_AGENT)) {
3959 isUnsupported = false;
3960 }
3961
3962 // UCBrowser works but doesn't give a unique user agent
3963 if (navigator.vendor === 'UCWEB') {
3964 isUnsupported = false;
3965 }
3966
3967 // IE and legacy edge are not supported any more
3968 if (ie || legacyEdge) {
3969 isUnsupported = true;
3970 }
3971
3972 return !isUnsupported;
3973 }());
3974
3975 /**
3976 * Checks all emoticons are surrounded by whitespace and
3977 * replaces any that aren't with with their emoticon code.
3978 *
3979 * @param {HTMLElement} node
3980 * @param {rangeHelper} rangeHelper
3981 * @return {void}
3982 */
3983 function checkWhitespace(node, rangeHelper) {
3984 var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/;
3985 var emoticons = node && find(node, 'img[data-sceditor-emoticon]');
3986
3987 if (!node || !emoticons.length) {
3988 return;
3989 }
3990
3991 for (var i = 0; i < emoticons.length; i++) {
3992 var emoticon = emoticons[i];
3993 var parent = emoticon.parentNode;
3994 var prev = emoticon.previousSibling;
3995 var next = emoticon.nextSibling;
3996
3997 if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) &&
3998 (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) {
3999 continue;
4000 }
4001
4002 var range = rangeHelper.cloneSelected();
4003 var rangeStart = -1;
4004 var rangeStartContainer = range.startContainer;
4005 var previousText = prev.nodeValue || '';
4006
4007 previousText += data(emoticon, 'sceditor-emoticon');
4008
4009 // If the cursor is after the removed emoticon, add
4010 // the length of the newly added text to it
4011 if (rangeStartContainer === next) {
4012 rangeStart = previousText.length + range.startOffset;
4013 }
4014
4015 // If the cursor is set before the next node, set it to
4016 // the end of the new text node
4017 if (rangeStartContainer === node &&
4018 node.childNodes[range.startOffset] === next) {
4019 rangeStart = previousText.length;
4020 }
4021
4022 // If the cursor is set before the removed emoticon,
4023 // just keep it at that position
4024 if (rangeStartContainer === prev) {
4025 rangeStart = range.startOffset;
4026 }
4027
4028 if (!next || next.nodeType !== TEXT_NODE) {
4029 next = parent.insertBefore(
4030 parent.ownerDocument.createTextNode(''), next
4031 );
4032 }
4033
4034 next.insertData(0, previousText);
4035 remove(prev);
4036 remove(emoticon);
4037
4038 // Need to update the range starting position if it's been modified
4039 if (rangeStart > -1) {
4040 range.setStart(next, rangeStart);
4041 range.collapse(true);
4042 rangeHelper.selectRange(range);
4043 }
4044 }
4045 }
4046 /**
4047 * Replaces any emoticons inside the root node with images.
4048 *
4049 * emoticons should be an object where the key is the emoticon
4050 * code and the value is the HTML to replace it with.
4051 *
4052 * @param {HTMLElement} root
4053 * @param {Object<string, string>} emoticons
4054 * @param {boolean} emoticonsCompat
4055 * @return {void}
4056 */
4057 function replace(root, emoticons, emoticonsCompat) {
4058 var doc = root.ownerDocument;
4059 var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)';
4060 var emoticonCodes = [];
4061 var emoticonRegex = {};
4062
4063 // TODO: Make this tag configurable.
4064 if (parent(root, 'code')) {
4065 return;
4066 }
4067
4068 each(emoticons, function (key) {
4069 emoticonRegex[key] = new RegExp(space + regex(key) + space);
4070 emoticonCodes.push(key);
4071 });
4072
4073 // Sort keys longest to shortest so that longer keys
4074 // take precedence (avoids bugs with shorter keys partially
4075 // matching longer ones)
4076 emoticonCodes.sort(function (a, b) {
4077 return b.length - a.length;
4078 });
4079
4080 (function convert(node) {
4081 node = node.firstChild;
4082
4083 while (node) {
4084 // TODO: Make this tag configurable.
4085 if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) {
4086 convert(node);
4087 }
4088
4089 if (node.nodeType === TEXT_NODE) {
4090 for (var i = 0; i < emoticonCodes.length; i++) {
4091 var text = node.nodeValue;
4092 var key = emoticonCodes[i];
4093 var index = emoticonsCompat ?
4094 text.search(emoticonRegex[key]) :
4095 text.indexOf(key);
4096
4097 if (index > -1) {
4098 // When emoticonsCompat is enabled this will be the
4099 // position after any white space
4100 var startIndex = text.indexOf(key, index);
4101 var fragment = parseHTML(emoticons[key], doc);
4102 var after = text.substr(startIndex + key.length);
4103
4104 fragment.appendChild(doc.createTextNode(after));
4105
4106 node.nodeValue = text.substr(0, startIndex);
4107 node.parentNode
4108 .insertBefore(fragment, node.nextSibling);
4109 }
4110 }
4111 }
4112
4113 node = node.nextSibling;
4114 }
4115 }(root));
4116 }
4117
4118 /*! @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 */
4119
4120 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); } }
4121
4122 var hasOwnProperty = Object.hasOwnProperty,
4123 setPrototypeOf = Object.setPrototypeOf,
4124 isFrozen = Object.isFrozen,
4125 getPrototypeOf = Object.getPrototypeOf,
4126 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
4127 var freeze = Object.freeze,
4128 seal = Object.seal,
4129 create = Object.create; // eslint-disable-line import/no-mutable-exports
4130
4131 var _ref = typeof Reflect !== 'undefined' && Reflect,
4132 apply = _ref.apply,
4133 construct = _ref.construct;
4134
4135 if (!apply) {
4136 apply = function apply(fun, thisValue, args) {
4137 return fun.apply(thisValue, args);
4138 };
4139 }
4140
4141 if (!freeze) {
4142 freeze = function freeze(x) {
4143 return x;
4144 };
4145 }
4146
4147 if (!seal) {
4148 seal = function seal(x) {
4149 return x;
4150 };
4151 }
4152
4153 if (!construct) {
4154 construct = function construct(Func, args) {
4155 return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))();
4156 };
4157 }
4158
4159 var arrayForEach = unapply(Array.prototype.forEach);
4160 var arrayPop = unapply(Array.prototype.pop);
4161 var arrayPush = unapply(Array.prototype.push);
4162
4163 var stringToLowerCase = unapply(String.prototype.toLowerCase);
4164 var stringMatch = unapply(String.prototype.match);
4165 var stringReplace = unapply(String.prototype.replace);
4166 var stringIndexOf = unapply(String.prototype.indexOf);
4167 var stringTrim = unapply(String.prototype.trim);
4168
4169 var regExpTest = unapply(RegExp.prototype.test);
4170
4171 var typeErrorCreate = unconstruct(TypeError);
4172
4173 function unapply(func) {
4174 return function (thisArg) {
4175 for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
4176 args[_key - 1] = arguments[_key];
4177 }
4178
4179 return apply(func, thisArg, args);
4180 };
4181 }
4182
4183 function unconstruct(func) {
4184 return function () {
4185 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
4186 args[_key2] = arguments[_key2];
4187 }
4188
4189 return construct(func, args);
4190 };
4191 }
4192
4193 /* Add properties to a lookup table */
4194 function addToSet(set, array) {
4195 if (setPrototypeOf) {
4196 // Make 'in' and truthy checks like Boolean(set.constructor)
4197 // independent of any properties defined on Object.prototype.
4198 // Prevent prototype setters from intercepting set as a this value.
4199 setPrototypeOf(set, null);
4200 }
4201
4202 var l = array.length;
4203 while (l--) {
4204 var element = array[l];
4205 if (typeof element === 'string') {
4206 var lcElement = stringToLowerCase(element);
4207 if (lcElement !== element) {
4208 // Config presets (e.g. tags.js, attrs.js) are immutable.
4209 if (!isFrozen(array)) {
4210 array[l] = lcElement;
4211 }
4212
4213 element = lcElement;
4214 }
4215 }
4216
4217 set[element] = true;
4218 }
4219
4220 return set;
4221 }
4222
4223 /* Shallow clone an object */
4224 function clone(object) {
4225 var newObject = create(null);
4226
4227 var property = void 0;
4228 for (property in object) {
4229 if (apply(hasOwnProperty, object, [property])) {
4230 newObject[property] = object[property];
4231 }
4232 }
4233
4234 return newObject;
4235 }
4236
4237 /* IE10 doesn't support __lookupGetter__ so lets'
4238 * simulate it. It also automatically checks
4239 * if the prop is function or getter and behaves
4240 * accordingly. */
4241 function lookupGetter(object, prop) {
4242 while (object !== null) {
4243 var desc = getOwnPropertyDescriptor(object, prop);
4244 if (desc) {
4245 if (desc.get) {
4246 return unapply(desc.get);
4247 }
4248
4249 if (typeof desc.value === 'function') {
4250 return unapply(desc.value);
4251 }
4252 }
4253
4254 object = getPrototypeOf(object);
4255 }
4256
4257 return null;
4258 }
4259
4260 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']);
4261
4262 // SVG
4263 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']);
4264
4265 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']);
4266
4267 // List of SVG elements that are disallowed by default.
4268 // We still need to know them so that we can do namespace
4269 // checks properly in case one wants to add them to
4270 // allow-list.
4271 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']);
4272
4273 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']);
4274
4275 // Similarly to SVG, we want to know all MathML elements,
4276 // even those that we disallow by default.
4277 var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
4278
4279 var text = freeze(['#text']);
4280
4281 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']);
4282
4283 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']);
4284
4285 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']);
4286
4287 var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
4288
4289 // eslint-disable-next-line unicorn/better-regex
4290 var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
4291 var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm);
4292 var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
4293 var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
4294 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
4295 );
4296 var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
4297 var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
4298 );
4299
4300 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; };
4301
4302 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); } }
4303
4304 var getGlobal = function getGlobal() {
4305 return typeof window === 'undefined' ? null : window;
4306 };
4307
4308 /**
4309 * Creates a no-op policy for internal use only.
4310 * Don't export this function outside this module!
4311 * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory.
4312 * @param {Document} document The document object (to determine policy name suffix)
4313 * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types
4314 * are not supported).
4315 */
4316 var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) {
4317 if ((typeof trustedTypes === 'undefined' ? 'undefined' : _typeof(trustedTypes)) !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
4318 return null;
4319 }
4320
4321 // Allow the callers to control the unique policy name
4322 // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
4323 // Policy creation with duplicate names throws in Trusted Types.
4324 var suffix = null;
4325 var ATTR_NAME = 'data-tt-policy-suffix';
4326 if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) {
4327 suffix = document.currentScript.getAttribute(ATTR_NAME);
4328 }
4329
4330 var policyName = 'dompurify' + (suffix ? '#' + suffix : '');
4331
4332 try {
4333 return trustedTypes.createPolicy(policyName, {
4334 createHTML: function createHTML(html$$1) {
4335 return html$$1;
4336 }
4337 });
4338 } catch (_) {
4339 // Policy creation failed (most likely another DOMPurify script has
4340 // already run). Skip creating the policy, as this will only cause errors
4341 // if TT are enforced.
4342 console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
4343 return null;
4344 }
4345 };
4346
4347 function createDOMPurify() {
4348 var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
4349
4350 var DOMPurify = function DOMPurify(root) {
4351 return createDOMPurify(root);
4352 };
4353
4354 /**
4355 * Version label, exposed for easier checks
4356 * if DOMPurify is up to date or not
4357 */
4358 DOMPurify.version = '2.2.6';
4359
4360 /**
4361 * Array of elements that DOMPurify removed during sanitation.
4362 * Empty if nothing was removed.
4363 */
4364 DOMPurify.removed = [];
4365
4366 if (!window || !window.document || window.document.nodeType !== 9) {
4367 // Not running in a browser, provide a factory function
4368 // so that you can pass your own Window
4369 DOMPurify.isSupported = false;
4370
4371 return DOMPurify;
4372 }
4373
4374 var originalDocument = window.document;
4375
4376 var document = window.document;
4377 var DocumentFragment = window.DocumentFragment,
4378 HTMLTemplateElement = window.HTMLTemplateElement,
4379 Node = window.Node,
4380 Element = window.Element,
4381 NodeFilter = window.NodeFilter,
4382 _window$NamedNodeMap = window.NamedNodeMap,
4383 NamedNodeMap = _window$NamedNodeMap === undefined ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
4384 Text = window.Text,
4385 Comment = window.Comment,
4386 DOMParser = window.DOMParser,
4387 trustedTypes = window.trustedTypes;
4388
4389
4390 var ElementPrototype = Element.prototype;
4391
4392 var cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
4393 var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
4394 var getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
4395 var getParentNode = lookupGetter(ElementPrototype, 'parentNode');
4396
4397 // As per issue #47, the web-components registry is inherited by a
4398 // new document created via createHTMLDocument. As per the spec
4399 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
4400 // a new empty registry is used when creating a template contents owner
4401 // document, so we use that as our parent document to ensure nothing
4402 // is inherited.
4403 if (typeof HTMLTemplateElement === 'function') {
4404 var template = document.createElement('template');
4405 if (template.content && template.content.ownerDocument) {
4406 document = template.content.ownerDocument;
4407 }
4408 }
4409
4410 var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument);
4411 var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML('') : '';
4412
4413 var _document = document,
4414 implementation = _document.implementation,
4415 createNodeIterator = _document.createNodeIterator,
4416 getElementsByTagName = _document.getElementsByTagName,
4417 createDocumentFragment = _document.createDocumentFragment;
4418 var importNode = originalDocument.importNode;
4419
4420
4421 var documentMode = {};
4422 try {
4423 documentMode = clone(document).documentMode ? document.documentMode : {};
4424 } catch (_) {}
4425
4426 var hooks = {};
4427
4428 /**
4429 * Expose whether this browser supports running the full DOMPurify.
4430 */
4431 DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9;
4432
4433 var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR,
4434 ERB_EXPR$$1 = ERB_EXPR,
4435 DATA_ATTR$$1 = DATA_ATTR,
4436 ARIA_ATTR$$1 = ARIA_ATTR,
4437 IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA,
4438 ATTR_WHITESPACE$$1 = ATTR_WHITESPACE;
4439 var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI;
4440
4441 /**
4442 * We consider the elements and attributes below to be safe. Ideally
4443 * don't add any new ones but feel free to remove unwanted ones.
4444 */
4445
4446 /* allowed element names */
4447
4448 var ALLOWED_TAGS = null;
4449 var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text)));
4450
4451 /* Allowed attribute names */
4452 var ALLOWED_ATTR = null;
4453 var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml)));
4454
4455 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
4456 var FORBID_TAGS = null;
4457
4458 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
4459 var FORBID_ATTR = null;
4460
4461 /* Decide if ARIA attributes are okay */
4462 var ALLOW_ARIA_ATTR = true;
4463
4464 /* Decide if custom data attributes are okay */
4465 var ALLOW_DATA_ATTR = true;
4466
4467 /* Decide if unknown protocols are okay */
4468 var ALLOW_UNKNOWN_PROTOCOLS = false;
4469
4470 /* Output should be safe for common template engines.
4471 * This means, DOMPurify removes data attributes, mustaches and ERB
4472 */
4473 var SAFE_FOR_TEMPLATES = false;
4474
4475 /* Decide if document with <html>... should be returned */
4476 var WHOLE_DOCUMENT = false;
4477
4478 /* Track whether config is already set on this instance of DOMPurify. */
4479 var SET_CONFIG = false;
4480
4481 /* Decide if all elements (e.g. style, script) must be children of
4482 * document.body. By default, browsers might move them to document.head */
4483 var FORCE_BODY = false;
4484
4485 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
4486 * string (or a TrustedHTML object if Trusted Types are supported).
4487 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
4488 */
4489 var RETURN_DOM = false;
4490
4491 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
4492 * string (or a TrustedHTML object if Trusted Types are supported) */
4493 var RETURN_DOM_FRAGMENT = false;
4494
4495 /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM
4496 * `Node` is imported into the current `Document`. If this flag is not enabled the
4497 * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by
4498 * DOMPurify.
4499 *
4500 * This defaults to `true` starting DOMPurify 2.2.0. Note that setting it to `false`
4501 * might cause XSS from attacks hidden in closed shadowroots in case the browser
4502 * supports Declarative Shadow: DOM https://web.dev/declarative-shadow-dom/
4503 */
4504 var RETURN_DOM_IMPORT = true;
4505
4506 /* Try to return a Trusted Type object instead of a string, return a string in
4507 * case Trusted Types are not supported */
4508 var RETURN_TRUSTED_TYPE = false;
4509
4510 /* Output should be free from DOM clobbering attacks? */
4511 var SANITIZE_DOM = true;
4512
4513 /* Keep element content when removing element? */
4514 var KEEP_CONTENT = true;
4515
4516 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
4517 * of importing it into a new Document and returning a sanitized copy */
4518 var IN_PLACE = false;
4519
4520 /* Allow usage of profiles like html, svg and mathMl */
4521 var USE_PROFILES = {};
4522
4523 /* Tags to ignore content of when KEEP_CONTENT is true */
4524 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']);
4525
4526 /* Tags that are safe for data: URIs */
4527 var DATA_URI_TAGS = null;
4528 var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
4529
4530 /* Attributes safe for values like "javascript:" */
4531 var URI_SAFE_ATTRIBUTES = null;
4532 var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'summary', 'title', 'value', 'style', 'xmlns']);
4533
4534 /* Keep a reference to config to pass to hooks */
4535 var CONFIG = null;
4536
4537 /* Ideally, do not touch anything below this line */
4538 /* ______________________________________________ */
4539
4540 var formElement = document.createElement('form');
4541
4542 /**
4543 * _parseConfig
4544 *
4545 * @param {Object} cfg optional config literal
4546 */
4547 // eslint-disable-next-line complexity
4548 var _parseConfig = function _parseConfig(cfg) {
4549 if (CONFIG && CONFIG === cfg) {
4550 return;
4551 }
4552
4553 /* Shield configuration object from tampering */
4554 if (!cfg || (typeof cfg === 'undefined' ? 'undefined' : _typeof(cfg)) !== 'object') {
4555 cfg = {};
4556 }
4557
4558 /* Shield configuration object from prototype pollution */
4559 cfg = clone(cfg);
4560
4561 /* Set configuration parameters */
4562 ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
4563 ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;
4564 URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES;
4565 DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS;
4566 FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS) : {};
4567 FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR) : {};
4568 USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;
4569 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
4570 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
4571 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
4572 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
4573 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
4574 RETURN_DOM = cfg.RETURN_DOM || false; // Default false
4575 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
4576 RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; // Default true
4577 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
4578 FORCE_BODY = cfg.FORCE_BODY || false; // Default false
4579 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
4580 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
4581 IN_PLACE = cfg.IN_PLACE || false; // Default false
4582 IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1;
4583 if (SAFE_FOR_TEMPLATES) {
4584 ALLOW_DATA_ATTR = false;
4585 }
4586
4587 if (RETURN_DOM_FRAGMENT) {
4588 RETURN_DOM = true;
4589 }
4590
4591 /* Parse profile info */
4592 if (USE_PROFILES) {
4593 ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text)));
4594 ALLOWED_ATTR = [];
4595 if (USE_PROFILES.html === true) {
4596 addToSet(ALLOWED_TAGS, html);
4597 addToSet(ALLOWED_ATTR, html$1);
4598 }
4599
4600 if (USE_PROFILES.svg === true) {
4601 addToSet(ALLOWED_TAGS, svg);
4602 addToSet(ALLOWED_ATTR, svg$1);
4603 addToSet(ALLOWED_ATTR, xml);
4604 }
4605
4606 if (USE_PROFILES.svgFilters === true) {
4607 addToSet(ALLOWED_TAGS, svgFilters);
4608 addToSet(ALLOWED_ATTR, svg$1);
4609 addToSet(ALLOWED_ATTR, xml);
4610 }
4611
4612 if (USE_PROFILES.mathMl === true) {
4613 addToSet(ALLOWED_TAGS, mathMl);
4614 addToSet(ALLOWED_ATTR, mathMl$1);
4615 addToSet(ALLOWED_ATTR, xml);
4616 }
4617 }
4618
4619 /* Merge configuration parameters */
4620 if (cfg.ADD_TAGS) {
4621 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
4622 ALLOWED_TAGS = clone(ALLOWED_TAGS);
4623 }
4624
4625 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS);
4626 }
4627
4628 if (cfg.ADD_ATTR) {
4629 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
4630 ALLOWED_ATTR = clone(ALLOWED_ATTR);
4631 }
4632
4633 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR);
4634 }
4635
4636 if (cfg.ADD_URI_SAFE_ATTR) {
4637 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR);
4638 }
4639
4640 /* Add #text in case KEEP_CONTENT is set to true */
4641 if (KEEP_CONTENT) {
4642 ALLOWED_TAGS['#text'] = true;
4643 }
4644
4645 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
4646 if (WHOLE_DOCUMENT) {
4647 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
4648 }
4649
4650 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
4651 if (ALLOWED_TAGS.table) {
4652 addToSet(ALLOWED_TAGS, ['tbody']);
4653 delete FORBID_TAGS.tbody;
4654 }
4655
4656 // Prevent further manipulation of configuration.
4657 // Not available in IE8, Safari 5, etc.
4658 if (freeze) {
4659 freeze(cfg);
4660 }
4661
4662 CONFIG = cfg;
4663 };
4664
4665 var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
4666
4667 var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']);
4668
4669 /* Keep track of all possible SVG and MathML tags
4670 * so that we can perform the namespace checks
4671 * correctly. */
4672 var ALL_SVG_TAGS = addToSet({}, svg);
4673 addToSet(ALL_SVG_TAGS, svgFilters);
4674 addToSet(ALL_SVG_TAGS, svgDisallowed);
4675
4676 var ALL_MATHML_TAGS = addToSet({}, mathMl);
4677 addToSet(ALL_MATHML_TAGS, mathMlDisallowed);
4678
4679 var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
4680 var SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
4681 var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
4682
4683 /**
4684 *
4685 *
4686 * @param {Element} element a DOM element whose namespace is being checked
4687 * @returns {boolean} Return false if the element has a
4688 * namespace that a spec-compliant parser would never
4689 * return. Return true otherwise.
4690 */
4691 var _checkValidNamespace = function _checkValidNamespace(element) {
4692 var parent = getParentNode(element);
4693
4694 // In JSDOM, if we're inside shadow DOM, then parentNode
4695 // can be null. We just simulate parent in this case.
4696 if (!parent || !parent.tagName) {
4697 parent = {
4698 namespaceURI: HTML_NAMESPACE,
4699 tagName: 'template'
4700 };
4701 }
4702
4703 var tagName = stringToLowerCase(element.tagName);
4704 var parentTagName = stringToLowerCase(parent.tagName);
4705
4706 if (element.namespaceURI === SVG_NAMESPACE) {
4707 // The only way to switch from HTML namespace to SVG
4708 // is via <svg>. If it happens via any other tag, then
4709 // it should be killed.
4710 if (parent.namespaceURI === HTML_NAMESPACE) {
4711 return tagName === 'svg';
4712 }
4713
4714 // The only way to switch from MathML to SVG is via
4715 // svg if parent is either <annotation-xml> or MathML
4716 // text integration points.
4717 if (parent.namespaceURI === MATHML_NAMESPACE) {
4718 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
4719 }
4720
4721 // We only allow elements that are defined in SVG
4722 // spec. All others are disallowed in SVG namespace.
4723 return Boolean(ALL_SVG_TAGS[tagName]);
4724 }
4725
4726 if (element.namespaceURI === MATHML_NAMESPACE) {
4727 // The only way to switch from HTML namespace to MathML
4728 // is via <math>. If it happens via any other tag, then
4729 // it should be killed.
4730 if (parent.namespaceURI === HTML_NAMESPACE) {
4731 return tagName === 'math';
4732 }
4733
4734 // The only way to switch from SVG to MathML is via
4735 // <math> and HTML integration points
4736 if (parent.namespaceURI === SVG_NAMESPACE) {
4737 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
4738 }
4739
4740 // We only allow elements that are defined in MathML
4741 // spec. All others are disallowed in MathML namespace.
4742 return Boolean(ALL_MATHML_TAGS[tagName]);
4743 }
4744
4745 if (element.namespaceURI === HTML_NAMESPACE) {
4746 // The only way to switch from SVG to HTML is via
4747 // HTML integration points, and from MathML to HTML
4748 // is via MathML text integration points
4749 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
4750 return false;
4751 }
4752
4753 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
4754 return false;
4755 }
4756
4757 // Certain elements are allowed in both SVG and HTML
4758 // namespace. We need to specify them explicitly
4759 // so that they don't get erronously deleted from
4760 // HTML namespace.
4761 var commonSvgAndHTMLElements = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
4762
4763 // We disallow tags that are specific for MathML
4764 // or SVG and should never appear in HTML namespace
4765 return !ALL_MATHML_TAGS[tagName] && (commonSvgAndHTMLElements[tagName] || !ALL_SVG_TAGS[tagName]);
4766 }
4767
4768 // The code should never reach this place (this means
4769 // that the element somehow got namespace that is not
4770 // HTML, SVG or MathML). Return false just in case.
4771 return false;
4772 };
4773
4774 /**
4775 * _forceRemove
4776 *
4777 * @param {Node} node a DOM node
4778 */
4779 var _forceRemove = function _forceRemove(node) {
4780 arrayPush(DOMPurify.removed, { element: node });
4781 try {
4782 node.parentNode.removeChild(node);
4783 } catch (_) {
4784 try {
4785 node.outerHTML = emptyHTML;
4786 } catch (_) {
4787 node.remove();
4788 }
4789 }
4790 };
4791
4792 /**
4793 * _removeAttribute
4794 *
4795 * @param {String} name an Attribute name
4796 * @param {Node} node a DOM node
4797 */
4798 var _removeAttribute = function _removeAttribute(name, node) {
4799 try {
4800 arrayPush(DOMPurify.removed, {
4801 attribute: node.getAttributeNode(name),
4802 from: node
4803 });
4804 } catch (_) {
4805 arrayPush(DOMPurify.removed, {
4806 attribute: null,
4807 from: node
4808 });
4809 }
4810
4811 node.removeAttribute(name);
4812 };
4813
4814 /**
4815 * _initDocument
4816 *
4817 * @param {String} dirty a string of dirty markup
4818 * @return {Document} a DOM, filled with the dirty markup
4819 */
4820 var _initDocument = function _initDocument(dirty) {
4821 /* Create a HTML document */
4822 var doc = void 0;
4823 var leadingWhitespace = void 0;
4824
4825 if (FORCE_BODY) {
4826 dirty = '<remove></remove>' + dirty;
4827 } else {
4828 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
4829 var matches = stringMatch(dirty, /^[\r\n\t ]+/);
4830 leadingWhitespace = matches && matches[0];
4831 }
4832
4833 var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
4834 /* Use the DOMParser API by default, fallback later if needs be */
4835 try {
4836 doc = new DOMParser().parseFromString(dirtyPayload, 'text/html');
4837 } catch (_) {}
4838
4839 /* Use createHTMLDocument in case DOMParser is not available */
4840 if (!doc || !doc.documentElement) {
4841 doc = implementation.createHTMLDocument('');
4842 var _doc = doc,
4843 body = _doc.body;
4844
4845 body.parentNode.removeChild(body.parentNode.firstElementChild);
4846 body.outerHTML = dirtyPayload;
4847 }
4848
4849 if (dirty && leadingWhitespace) {
4850 doc.body.insertBefore(document.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null);
4851 }
4852
4853 /* Work on whole document or just its body */
4854 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
4855 };
4856
4857 /**
4858 * _createIterator
4859 *
4860 * @param {Document} root document/fragment to create iterator for
4861 * @return {Iterator} iterator instance
4862 */
4863 var _createIterator = function _createIterator(root) {
4864 return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function () {
4865 return NodeFilter.FILTER_ACCEPT;
4866 }, false);
4867 };
4868
4869 /**
4870 * _isClobbered
4871 *
4872 * @param {Node} elm element to check for clobbering attacks
4873 * @return {Boolean} true if clobbered, false if safe
4874 */
4875 var _isClobbered = function _isClobbered(elm) {
4876 if (elm instanceof Text || elm instanceof Comment) {
4877 return false;
4878 }
4879
4880 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') {
4881 return true;
4882 }
4883
4884 return false;
4885 };
4886
4887 /**
4888 * _isNode
4889 *
4890 * @param {Node} obj object to check whether it's a DOM node
4891 * @return {Boolean} true is object is a DOM node
4892 */
4893 var _isNode = function _isNode(object) {
4894 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';
4895 };
4896
4897 /**
4898 * _executeHook
4899 * Execute user configurable hooks
4900 *
4901 * @param {String} entryPoint Name of the hook's entry point
4902 * @param {Node} currentNode node to work on with the hook
4903 * @param {Object} data additional hook parameters
4904 */
4905 var _executeHook = function _executeHook(entryPoint, currentNode, data) {
4906 if (!hooks[entryPoint]) {
4907 return;
4908 }
4909
4910 arrayForEach(hooks[entryPoint], function (hook) {
4911 hook.call(DOMPurify, currentNode, data, CONFIG);
4912 });
4913 };
4914
4915 /**
4916 * _sanitizeElements
4917 *
4918 * @protect nodeName
4919 * @protect textContent
4920 * @protect removeChild
4921 *
4922 * @param {Node} currentNode to check for permission to exist
4923 * @return {Boolean} true if node was killed, false if left alive
4924 */
4925 var _sanitizeElements = function _sanitizeElements(currentNode) {
4926 var content = void 0;
4927
4928 /* Execute a hook if present */
4929 _executeHook('beforeSanitizeElements', currentNode, null);
4930
4931 /* Check if element is clobbered or can clobber */
4932 if (_isClobbered(currentNode)) {
4933 _forceRemove(currentNode);
4934 return true;
4935 }
4936
4937 /* Check if tagname contains Unicode */
4938 if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) {
4939 _forceRemove(currentNode);
4940 return true;
4941 }
4942
4943 /* Now let's check the element's type and name */
4944 var tagName = stringToLowerCase(currentNode.nodeName);
4945
4946 /* Execute a hook if present */
4947 _executeHook('uponSanitizeElement', currentNode, {
4948 tagName: tagName,
4949 allowedTags: ALLOWED_TAGS
4950 });
4951
4952 /* Detect mXSS attempts abusing namespace confusion */
4953 if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
4954 _forceRemove(currentNode);
4955 return true;
4956 }
4957
4958 /* Remove element if anything forbids its presence */
4959 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
4960 /* Keep content except for bad-listed elements */
4961 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
4962 var parentNode = getParentNode(currentNode);
4963 var childNodes = getChildNodes(currentNode);
4964 var childCount = childNodes.length;
4965 for (var i = childCount - 1; i >= 0; --i) {
4966 parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
4967 }
4968 }
4969
4970 _forceRemove(currentNode);
4971 return true;
4972 }
4973
4974 /* Check whether element has a valid namespace */
4975 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
4976 _forceRemove(currentNode);
4977 return true;
4978 }
4979
4980 if ((tagName === 'noscript' || tagName === 'noembed') && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) {
4981 _forceRemove(currentNode);
4982 return true;
4983 }
4984
4985 /* Sanitize element content to be template-safe */
4986 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
4987 /* Get the element's text content */
4988 content = currentNode.textContent;
4989 content = stringReplace(content, MUSTACHE_EXPR$$1, ' ');
4990 content = stringReplace(content, ERB_EXPR$$1, ' ');
4991 if (currentNode.textContent !== content) {
4992 arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
4993 currentNode.textContent = content;
4994 }
4995 }
4996
4997 /* Execute a hook if present */
4998 _executeHook('afterSanitizeElements', currentNode, null);
4999
5000 return false;
5001 };
5002
5003 /**
5004 * _isValidAttribute
5005 *
5006 * @param {string} lcTag Lowercase tag name of containing element.
5007 * @param {string} lcName Lowercase attribute name.
5008 * @param {string} value Attribute value.
5009 * @return {Boolean} Returns true if `value` is valid, otherwise false.
5010 */
5011 // eslint-disable-next-line complexity
5012 var _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
5013 /* Make sure attribute cannot clobber */
5014 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
5015 return false;
5016 }
5017
5018 /* Allow valid data-* attributes: At least one character after "-"
5019 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
5020 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
5021 We don't need to check the value; it's always URI safe. */
5022 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]) {
5023 return false;
5024
5025 /* Check value is safe. First, is attr inert? If so, is safe */
5026 } 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 {
5027 return false;
5028 }
5029
5030 return true;
5031 };
5032
5033 /**
5034 * _sanitizeAttributes
5035 *
5036 * @protect attributes
5037 * @protect nodeName
5038 * @protect removeAttribute
5039 * @protect setAttribute
5040 *
5041 * @param {Node} currentNode to sanitize
5042 */
5043 var _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
5044 var attr = void 0;
5045 var value = void 0;
5046 var lcName = void 0;
5047 var l = void 0;
5048 /* Execute a hook if present */
5049 _executeHook('beforeSanitizeAttributes', currentNode, null);
5050
5051 var attributes = currentNode.attributes;
5052
5053 /* Check if we have attributes; if not we might have a text node */
5054
5055 if (!attributes) {
5056 return;
5057 }
5058
5059 var hookEvent = {
5060 attrName: '',
5061 attrValue: '',
5062 keepAttr: true,
5063 allowedAttributes: ALLOWED_ATTR
5064 };
5065 l = attributes.length;
5066
5067 /* Go backwards over all attributes; safely remove bad ones */
5068 while (l--) {
5069 attr = attributes[l];
5070 var _attr = attr,
5071 name = _attr.name,
5072 namespaceURI = _attr.namespaceURI;
5073
5074 value = stringTrim(attr.value);
5075 lcName = stringToLowerCase(name);
5076
5077 /* Execute a hook if present */
5078 hookEvent.attrName = lcName;
5079 hookEvent.attrValue = value;
5080 hookEvent.keepAttr = true;
5081 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
5082 _executeHook('uponSanitizeAttribute', currentNode, hookEvent);
5083 value = hookEvent.attrValue;
5084 /* Did the hooks approve of the attribute? */
5085 if (hookEvent.forceKeepAttr) {
5086 continue;
5087 }
5088
5089 /* Remove attribute */
5090 _removeAttribute(name, currentNode);
5091
5092 /* Did the hooks approve of the attribute? */
5093 if (!hookEvent.keepAttr) {
5094 continue;
5095 }
5096
5097 /* Work around a security issue in jQuery 3.0 */
5098 if (regExpTest(/\/>/i, value)) {
5099 _removeAttribute(name, currentNode);
5100 continue;
5101 }
5102
5103 /* Sanitize attribute content to be template-safe */
5104 if (SAFE_FOR_TEMPLATES) {
5105 value = stringReplace(value, MUSTACHE_EXPR$$1, ' ');
5106 value = stringReplace(value, ERB_EXPR$$1, ' ');
5107 }
5108
5109 /* Is `value` valid for this attribute? */
5110 var lcTag = currentNode.nodeName.toLowerCase();
5111 if (!_isValidAttribute(lcTag, lcName, value)) {
5112 continue;
5113 }
5114
5115 /* Handle invalid data-* attribute set by try-catching it */
5116 try {
5117 if (namespaceURI) {
5118 currentNode.setAttributeNS(namespaceURI, name, value);
5119 } else {
5120 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
5121 currentNode.setAttribute(name, value);
5122 }
5123
5124 arrayPop(DOMPurify.removed);
5125 } catch (_) {}
5126 }
5127
5128 /* Execute a hook if present */
5129 _executeHook('afterSanitizeAttributes', currentNode, null);
5130 };
5131
5132 /**
5133 * _sanitizeShadowDOM
5134 *
5135 * @param {DocumentFragment} fragment to iterate over recursively
5136 */
5137 var _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
5138 var shadowNode = void 0;
5139 var shadowIterator = _createIterator(fragment);
5140
5141 /* Execute a hook if present */
5142 _executeHook('beforeSanitizeShadowDOM', fragment, null);
5143
5144 while (shadowNode = shadowIterator.nextNode()) {
5145 /* Execute a hook if present */
5146 _executeHook('uponSanitizeShadowNode', shadowNode, null);
5147
5148 /* Sanitize tags and elements */
5149 if (_sanitizeElements(shadowNode)) {
5150 continue;
5151 }
5152
5153 /* Deep shadow DOM detected */
5154 if (shadowNode.content instanceof DocumentFragment) {
5155 _sanitizeShadowDOM(shadowNode.content);
5156 }
5157
5158 /* Check attributes, sanitize if necessary */
5159 _sanitizeAttributes(shadowNode);
5160 }
5161
5162 /* Execute a hook if present */
5163 _executeHook('afterSanitizeShadowDOM', fragment, null);
5164 };
5165
5166 /**
5167 * Sanitize
5168 * Public method providing core sanitation functionality
5169 *
5170 * @param {String|Node} dirty string or DOM node
5171 * @param {Object} configuration object
5172 */
5173 // eslint-disable-next-line complexity
5174 DOMPurify.sanitize = function (dirty, cfg) {
5175 var body = void 0;
5176 var importedNode = void 0;
5177 var currentNode = void 0;
5178 var oldNode = void 0;
5179 var returnNode = void 0;
5180 /* Make sure we have a string to sanitize.
5181 DO NOT return early, as this will return the wrong type if
5182 the user has requested a DOM object rather than a string */
5183 if (!dirty) {
5184 dirty = '<!-->';
5185 }
5186
5187 /* Stringify, in case dirty is an object */
5188 if (typeof dirty !== 'string' && !_isNode(dirty)) {
5189 // eslint-disable-next-line no-negated-condition
5190 if (typeof dirty.toString !== 'function') {
5191 throw typeErrorCreate('toString is not a function');
5192 } else {
5193 dirty = dirty.toString();
5194 if (typeof dirty !== 'string') {
5195 throw typeErrorCreate('dirty is not a string, aborting');
5196 }
5197 }
5198 }
5199
5200 /* Check we can run. Otherwise fall back or ignore */
5201 if (!DOMPurify.isSupported) {
5202 if (_typeof(window.toStaticHTML) === 'object' || typeof window.toStaticHTML === 'function') {
5203 if (typeof dirty === 'string') {
5204 return window.toStaticHTML(dirty);
5205 }
5206
5207 if (_isNode(dirty)) {
5208 return window.toStaticHTML(dirty.outerHTML);
5209 }
5210 }
5211
5212 return dirty;
5213 }
5214
5215 /* Assign config vars */
5216 if (!SET_CONFIG) {
5217 _parseConfig(cfg);
5218 }
5219
5220 /* Clean up removed elements */
5221 DOMPurify.removed = [];
5222
5223 /* Check if dirty is correctly typed for IN_PLACE */
5224 if (typeof dirty === 'string') {
5225 IN_PLACE = false;
5226 }
5227
5228 if (IN_PLACE) ; else if (dirty instanceof Node) {
5229 /* If dirty is a DOM element, append to an empty document to avoid
5230 elements being stripped by the parser */
5231 body = _initDocument('<!---->');
5232 importedNode = body.ownerDocument.importNode(dirty, true);
5233 if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {
5234 /* Node is already a body, use as is */
5235 body = importedNode;
5236 } else if (importedNode.nodeName === 'HTML') {
5237 body = importedNode;
5238 } else {
5239 // eslint-disable-next-line unicorn/prefer-node-append
5240 body.appendChild(importedNode);
5241 }
5242 } else {
5243 /* Exit directly if we have nothing to do */
5244 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
5245 // eslint-disable-next-line unicorn/prefer-includes
5246 dirty.indexOf('<') === -1) {
5247 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
5248 }
5249
5250 /* Initialize the document to work on */
5251 body = _initDocument(dirty);
5252
5253 /* Check we have a DOM node from the data */
5254 if (!body) {
5255 return RETURN_DOM ? null : emptyHTML;
5256 }
5257 }
5258
5259 /* Remove first element node (ours) if FORCE_BODY is set */
5260 if (body && FORCE_BODY) {
5261 _forceRemove(body.firstChild);
5262 }
5263
5264 /* Get node iterator */
5265 var nodeIterator = _createIterator(IN_PLACE ? dirty : body);
5266
5267 /* Now start iterating over the created document */
5268 while (currentNode = nodeIterator.nextNode()) {
5269 /* Fix IE's strange behavior with manipulated textNodes #89 */
5270 if (currentNode.nodeType === 3 && currentNode === oldNode) {
5271 continue;
5272 }
5273
5274 /* Sanitize tags and elements */
5275 if (_sanitizeElements(currentNode)) {
5276 continue;
5277 }
5278
5279 /* Shadow DOM detected, sanitize it */
5280 if (currentNode.content instanceof DocumentFragment) {
5281 _sanitizeShadowDOM(currentNode.content);
5282 }
5283
5284 /* Check attributes, sanitize if necessary */
5285 _sanitizeAttributes(currentNode);
5286
5287 oldNode = currentNode;
5288 }
5289
5290 oldNode = null;
5291
5292 /* If we sanitized `dirty` in-place, return it. */
5293 if (IN_PLACE) {
5294 return dirty;
5295 }
5296
5297 /* Return sanitized string or DOM */
5298 if (RETURN_DOM) {
5299 if (RETURN_DOM_FRAGMENT) {
5300 returnNode = createDocumentFragment.call(body.ownerDocument);
5301
5302 while (body.firstChild) {
5303 // eslint-disable-next-line unicorn/prefer-node-append
5304 returnNode.appendChild(body.firstChild);
5305 }
5306 } else {
5307 returnNode = body;
5308 }
5309
5310 if (RETURN_DOM_IMPORT) {
5311 /*
5312 AdoptNode() is not used because internal state is not reset
5313 (e.g. the past names map of a HTMLFormElement), this is safe
5314 in theory but we would rather not risk another attack vector.
5315 The state that is cloned by importNode() is explicitly defined
5316 by the specs.
5317 */
5318 returnNode = importNode.call(originalDocument, returnNode, true);
5319 }
5320
5321 return returnNode;
5322 }
5323
5324 var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
5325
5326 /* Sanitize final string template-safe */
5327 if (SAFE_FOR_TEMPLATES) {
5328 serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, ' ');
5329 serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, ' ');
5330 }
5331
5332 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
5333 };
5334
5335 /**
5336 * Public method to set the configuration once
5337 * setConfig
5338 *
5339 * @param {Object} cfg configuration object
5340 */
5341 DOMPurify.setConfig = function (cfg) {
5342 _parseConfig(cfg);
5343 SET_CONFIG = true;
5344 };
5345
5346 /**
5347 * Public method to remove the configuration
5348 * clearConfig
5349 *
5350 */
5351 DOMPurify.clearConfig = function () {
5352 CONFIG = null;
5353 SET_CONFIG = false;
5354 };
5355
5356 /**
5357 * Public method to check if an attribute value is valid.
5358 * Uses last set config, if any. Otherwise, uses config defaults.
5359 * isValidAttribute
5360 *
5361 * @param {string} tag Tag name of containing element.
5362 * @param {string} attr Attribute name.
5363 * @param {string} value Attribute value.
5364 * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.
5365 */
5366 DOMPurify.isValidAttribute = function (tag, attr, value) {
5367 /* Initialize shared config vars if necessary. */
5368 if (!CONFIG) {
5369 _parseConfig({});
5370 }
5371
5372 var lcTag = stringToLowerCase(tag);
5373 var lcName = stringToLowerCase(attr);
5374 return _isValidAttribute(lcTag, lcName, value);
5375 };
5376
5377 /**
5378 * AddHook
5379 * Public method to add DOMPurify hooks
5380 *
5381 * @param {String} entryPoint entry point for the hook to add
5382 * @param {Function} hookFunction function to execute
5383 */
5384 DOMPurify.addHook = function (entryPoint, hookFunction) {
5385 if (typeof hookFunction !== 'function') {
5386 return;
5387 }
5388
5389 hooks[entryPoint] = hooks[entryPoint] || [];
5390 arrayPush(hooks[entryPoint], hookFunction);
5391 };
5392
5393 /**
5394 * RemoveHook
5395 * Public method to remove a DOMPurify hook at a given entryPoint
5396 * (pops it from the stack of hooks if more are present)
5397 *
5398 * @param {String} entryPoint entry point for the hook to remove
5399 */
5400 DOMPurify.removeHook = function (entryPoint) {
5401 if (hooks[entryPoint]) {
5402 arrayPop(hooks[entryPoint]);
5403 }
5404 };
5405
5406 /**
5407 * RemoveHooks
5408 * Public method to remove all DOMPurify hooks at a given entryPoint
5409 *
5410 * @param {String} entryPoint entry point for the hooks to remove
5411 */
5412 DOMPurify.removeHooks = function (entryPoint) {
5413 if (hooks[entryPoint]) {
5414 hooks[entryPoint] = [];
5415 }
5416 };
5417
5418 /**
5419 * RemoveAllHooks
5420 * Public method to remove all DOMPurify hooks
5421 *
5422 */
5423 DOMPurify.removeAllHooks = function () {
5424 hooks = {};
5425 };
5426
5427 return DOMPurify;
5428 }
5429
5430 var purify = createDOMPurify();
5431
5432 var globalWin = window;
5433 var globalDoc = document;
5434
5435 var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
5436
5437 /**
5438 * Wrap inlines that are in the root in paragraphs.
5439 *
5440 * @param {HTMLBodyElement} body
5441 * @param {Document} doc
5442 * @private
5443 */
5444 function wrapInlines(body, doc) {
5445 var wrapper;
5446
5447 traverse(body, function (node) {
5448 if (isInline(node, true)) {
5449 // Ignore text nodes unless they contain non-whitespace chars as
5450 // whitespace will be collapsed.
5451 // Ignore sceditor-ignore elements unless wrapping siblings
5452 // Should still wrap both if wrapping siblings.
5453 if (wrapper || node.nodeType === TEXT_NODE ?
5454 /\S/.test(node.nodeValue) : !is(node, '.sceditor-ignore')) {
5455 if (!wrapper) {
5456 wrapper = createElement('p', {}, doc);
5457 insertBefore(wrapper, node);
5458 }
5459
5460 appendChild(wrapper, node);
5461 }
5462 } else {
5463 wrapper = null;
5464 }
5465 }, false, true);
5466 }
5467 /**
5468 * SCEditor - A lightweight WYSIWYG editor
5469 *
5470 * @param {HTMLTextAreaElement} original The textarea to be converted
5471 * @param {Object} userOptions
5472 * @class SCEditor
5473 * @name SCEditor
5474 */
5475 function SCEditor(original, userOptions) {
5476 /**
5477 * Alias of this
5478 *
5479 * @private
5480 */
5481 var base = this;
5482
5483 /**
5484 * Editor format like BBCode or HTML
5485 */
5486 var format;
5487
5488 /**
5489 * The div which contains the editor and toolbar
5490 *
5491 * @type {HTMLDivElement}
5492 * @private
5493 */
5494 var editorContainer;
5495
5496 /**
5497 * Map of events handlers bound to this instance.
5498 *
5499 * @type {Object}
5500 * @private
5501 */
5502 var eventHandlers = {};
5503
5504 /**
5505 * The editors toolbar
5506 *
5507 * @type {HTMLDivElement}
5508 * @private
5509 */
5510 var toolbar;
5511
5512 /**
5513 * The editors iframe which should be in design mode
5514 *
5515 * @type {HTMLIFrameElement}
5516 * @private
5517 */
5518 var wysiwygEditor;
5519
5520 /**
5521 * The editors window
5522 *
5523 * @type {Window}
5524 * @private
5525 */
5526 var wysiwygWindow;
5527
5528 /**
5529 * The WYSIWYG editors body element
5530 *
5531 * @type {HTMLBodyElement}
5532 * @private
5533 */
5534 var wysiwygBody;
5535
5536 /**
5537 * The WYSIWYG editors document
5538 *
5539 * @type {Document}
5540 * @private
5541 */
5542 var wysiwygDocument;
5543
5544 /**
5545 * The editors textarea for viewing source
5546 *
5547 * @type {HTMLTextAreaElement}
5548 * @private
5549 */
5550 var sourceEditor;
5551
5552 /**
5553 * The current dropdown
5554 *
5555 * @type {HTMLDivElement}
5556 * @private
5557 */
5558 var dropdown;
5559
5560 /**
5561 * If the user is currently composing text via IME
5562 * @type {boolean}
5563 */
5564 var isComposing;
5565
5566 /**
5567 * Timer for valueChanged key handler
5568 * @type {number}
5569 */
5570 var valueChangedKeyUpTimer;
5571
5572 /**
5573 * The editors locale
5574 *
5575 * @private
5576 */
5577 var locale;
5578
5579 /**
5580 * Stores a cache of preloaded images
5581 *
5582 * @private
5583 * @type {Array.<HTMLImageElement>}
5584 */
5585 var preLoadCache = [];
5586
5587 /**
5588 * The editors rangeHelper instance
5589 *
5590 * @type {RangeHelper}
5591 * @private
5592 */
5593 var rangeHelper;
5594
5595 /**
5596 * An array of button state handlers
5597 *
5598 * @type {Array.<Object>}
5599 * @private
5600 */
5601 var btnStateHandlers = [];
5602
5603 /**
5604 * Plugin manager instance
5605 *
5606 * @type {PluginManager}
5607 * @private
5608 */
5609 var pluginManager;
5610
5611 /**
5612 * The current node containing the selection/caret
5613 *
5614 * @type {Node}
5615 * @private
5616 */
5617 var currentNode;
5618
5619 /**
5620 * The first block level parent of the current node
5621 *
5622 * @type {node}
5623 * @private
5624 */
5625 var currentBlockNode;
5626
5627 /**
5628 * The current node selection/caret
5629 *
5630 * @type {Object}
5631 * @private
5632 */
5633 var currentSelection;
5634
5635 /**
5636 * Used to make sure only 1 selection changed
5637 * check is called every 100ms.
5638 *
5639 * Helps improve performance as it is checked a lot.
5640 *
5641 * @type {boolean}
5642 * @private
5643 */
5644 var isSelectionCheckPending;
5645
5646 /**
5647 * If content is required (equivalent to the HTML5 required attribute)
5648 *
5649 * @type {boolean}
5650 * @private
5651 */
5652 var isRequired;
5653
5654 /**
5655 * The inline CSS style element. Will be undefined
5656 * until css() is called for the first time.
5657 *
5658 * @type {HTMLStyleElement}
5659 * @private
5660 */
5661 var inlineCss;
5662
5663 /**
5664 * Object containing a list of shortcut handlers
5665 *
5666 * @type {Object}
5667 * @private
5668 */
5669 var shortcutHandlers = {};
5670
5671 /**
5672 * The min and max heights that autoExpand should stay within
5673 *
5674 * @type {Object}
5675 * @private
5676 */
5677 var autoExpandBounds;
5678
5679 /**
5680 * Timeout for the autoExpand function to throttle calls
5681 *
5682 * @private
5683 */
5684 var autoExpandThrottle;
5685
5686 /**
5687 * Cache of the current toolbar buttons
5688 *
5689 * @type {Object}
5690 * @private
5691 */
5692 var toolbarButtons = {};
5693
5694 /**
5695 * Last scroll position before maximizing so
5696 * it can be restored when finished.
5697 *
5698 * @type {number}
5699 * @private
5700 */
5701 var maximizeScrollPosition;
5702
5703 /**
5704 * Stores the contents while a paste is taking place.
5705 *
5706 * Needed to support browsers that lack clipboard API support.
5707 *
5708 * @type {?DocumentFragment}
5709 * @private
5710 */
5711 var pasteContentFragment;
5712
5713 /**
5714 * All the emoticons from dropdown, more and hidden combined
5715 * and with the emoticons root set
5716 *
5717 * @type {!Object<string, string>}
5718 * @private
5719 */
5720 var allEmoticons = {};
5721
5722 /**
5723 * Current icon set if any
5724 *
5725 * @type {?Object}
5726 * @private
5727 */
5728 var icons;
5729
5730 /**
5731 * Private functions
5732 * @private
5733 */
5734 var init,
5735 replaceEmoticons,
5736 handleCommand,
5737 initEditor,
5738 initLocale,
5739 initToolBar,
5740 initOptions,
5741 initEvents,
5742 initResize,
5743 initEmoticons,
5744 handlePasteEvt,
5745 handleCutCopyEvt,
5746 handlePasteData,
5747 handleKeyDown,
5748 handleBackSpace,
5749 handleKeyPress,
5750 handleFormReset,
5751 handleMouseDown,
5752 handleComposition,
5753 handleEvent,
5754 handleDocumentClick,
5755 updateToolBar,
5756 updateActiveButtons,
5757 sourceEditorSelectedText,
5758 appendNewLine,
5759 checkSelectionChanged,
5760 checkNodeChanged,
5761 autofocus,
5762 emoticonsKeyPress,
5763 emoticonsCheckWhitespace,
5764 currentStyledBlockNode,
5765 triggerValueChanged,
5766 valueChangedBlur,
5767 valueChangedKeyUp,
5768 autoUpdate,
5769 autoExpand;
5770
5771 /**
5772 * All the commands supported by the editor
5773 * @name commands
5774 * @memberOf SCEditor.prototype
5775 */
5776 base.commands = extend(true, {}, (userOptions.commands || defaultCmds));
5777
5778 /**
5779 * Options for this editor instance
5780 * @name opts
5781 * @memberOf SCEditor.prototype
5782 */
5783 var options = base.opts = extend(
5784 true, {}, defaultOptions, userOptions
5785 );
5786
5787 // Don't deep extend emoticons (fixes #565)
5788 base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons;
5789
5790 if (!Array.isArray(options.allowedIframeUrls)) {
5791 options.allowedIframeUrls = [];
5792 }
5793 options.allowedIframeUrls.push('https://www.youtube-nocookie.com/embed/');
5794
5795 // Create new instance of DOMPurify for each editor instance so can
5796 // have different allowed iframe URLs
5797 // eslint-disable-next-line new-cap
5798 var domPurify = purify();
5799
5800 // Allow iframes for things like YouTube, see:
5801 // https://github.com/cure53/DOMPurify/issues/340#issuecomment-670758980
5802 domPurify.addHook('uponSanitizeElement', function (node, data) {
5803 var allowedUrls = options.allowedIframeUrls;
5804
5805 if (data.tagName === 'iframe') {
5806 var src = attr(node, 'src') || '';
5807
5808 for (var i = 0; i < allowedUrls.length; i++) {
5809 var url = allowedUrls[i];
5810
5811 if (isString(url) && src.substr(0, url.length) === url) {
5812 return;
5813 }
5814
5815 // Handle regex
5816 if (url.test && url.test(src)) {
5817 return;
5818 }
5819 }
5820
5821 // No match so remove
5822 remove(node);
5823 }
5824 });
5825
5826 // Convert target attribute into data-sce-target attributes so XHTML format
5827 // can allow them
5828 domPurify.addHook('afterSanitizeAttributes', function (node) {
5829 if ('target' in node) {
5830 attr(node, 'data-sce-target', attr(node, 'target'));
5831 }
5832
5833 removeAttr(node, 'target');
5834 });
5835
5836 /**
5837 * Sanitize HTML to avoid XSS
5838 *
5839 * @param {string} html
5840 * @return {string} html
5841 * @private
5842 */
5843 function sanitize(html) {
5844 return domPurify.sanitize(html, {
5845 ADD_TAGS: ['iframe'],
5846 ADD_ATTR: ['allowfullscreen', 'frameborder', 'target']
5847 });
5848 }
5849 /**
5850 * Creates the editor iframe and textarea
5851 * @private
5852 */
5853 init = function () {
5854 original._sceditor = base;
5855
5856 // Load locale
5857 if (options.locale && options.locale !== 'en') {
5858 initLocale();
5859 }
5860
5861 editorContainer = createElement('div', {
5862 className: 'sceditor-container'
5863 });
5864
5865 insertBefore(editorContainer, original);
5866 css(editorContainer, 'z-index', options.zIndex);
5867
5868 isRequired = original.required;
5869 original.required = false;
5870
5871 var FormatCtor = SCEditor.formats[options.format];
5872 format = FormatCtor ? new FormatCtor() : {};
5873 /*
5874 * Plugins should be initialized before the formatters since
5875 * they may wish to add or change formatting handlers and
5876 * since the bbcode format caches its handlers,
5877 * such changes must be done first.
5878 */
5879 pluginManager = new PluginManager(base);
5880 (options.plugins || '').split(',').forEach(function (plugin) {
5881 pluginManager.register(plugin.trim());
5882 });
5883 if ('init' in format) {
5884 format.init.call(base);
5885 }
5886
5887 // create the editor
5888 initEmoticons();
5889 initToolBar();
5890 initEditor();
5891 initOptions();
5892 initEvents();
5893
5894 // force into source mode if is a browser that can't handle
5895 // full editing
5896 if (!isWysiwygSupported) {
5897 base.toggleSourceMode();
5898 }
5899
5900 updateActiveButtons();
5901
5902 var loaded = function () {
5903 off(globalWin, 'load', loaded);
5904
5905 if (options.autofocus) {
5906 autofocus(!!options.autofocusEnd);
5907 }
5908
5909 autoExpand();
5910 appendNewLine();
5911 // TODO: use editor doc and window?
5912 pluginManager.call('ready');
5913 if ('onReady' in format) {
5914 format.onReady.call(base);
5915 }
5916 };
5917 on(globalWin, 'load', loaded);
5918 if (globalDoc.readyState === 'complete') {
5919 loaded();
5920 }
5921 };
5922
5923 /**
5924 * Init the locale variable with the specified locale if possible
5925 * @private
5926 * @return void
5927 */
5928 initLocale = function () {
5929 var lang;
5930
5931 locale = SCEditor.locale[options.locale];
5932
5933 if (!locale) {
5934 lang = options.locale.split('-');
5935 locale = SCEditor.locale[lang[0]];
5936 }
5937
5938 // Locale DateTime format overrides any specified in the options
5939 if (locale && locale.dateFormat) {
5940 options.dateFormat = locale.dateFormat;
5941 }
5942 };
5943
5944 /**
5945 * Creates the editor iframe and textarea
5946 * @private
5947 */
5948 initEditor = function () {
5949 sourceEditor = createElement('textarea');
5950 wysiwygEditor = createElement('iframe', {
5951 frameborder: 0,
5952 allowfullscreen: true
5953 });
5954
5955 /*
5956 * This needs to be done right after they are created because,
5957 * for any reason, the user may not want the value to be tinkered
5958 * by any filters.
5959 */
5960 if (options.startInSourceMode) {
5961 addClass(editorContainer, 'sourceMode');
5962 hide(wysiwygEditor);
5963 } else {
5964 addClass(editorContainer, 'wysiwygMode');
5965 hide(sourceEditor);
5966 }
5967
5968 if (!options.spellcheck) {
5969 attr(editorContainer, 'spellcheck', 'false');
5970 }
5971
5972 if (globalWin.location.protocol === 'https:') {
5973 attr(wysiwygEditor, 'src', 'about:blank');
5974 }
5975
5976 // Add the editor to the container
5977 appendChild(editorContainer, wysiwygEditor);
5978 appendChild(editorContainer, sourceEditor);
5979
5980 // TODO: make this optional somehow
5981 base.dimensions(
5982 options.width || width(original),
5983 options.height || height(original)
5984 );
5985
5986 // Add ios to HTML so can apply CSS fix to only it
5987 var className = ios ? ' ios' : '';
5988
5989 wysiwygDocument = wysiwygEditor.contentDocument;
5990 wysiwygDocument.open();
5991 wysiwygDocument.write(_tmpl('html', {
5992 attrs: ' class="' + className + '"',
5993 spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
5994 charset: options.charset,
5995 style: options.style
5996 }));
5997 wysiwygDocument.close();
5998
5999 wysiwygBody = wysiwygDocument.body;
6000 wysiwygWindow = wysiwygEditor.contentWindow;
6001
6002 base.readOnly(!!options.readOnly);
6003
6004 // iframe overflow fix for iOS
6005 if (ios) {
6006 height(wysiwygBody, '100%');
6007 on(wysiwygBody, 'touchend', base.focus);
6008 }
6009
6010 var tabIndex = attr(original, 'tabindex');
6011 attr(sourceEditor, 'tabindex', tabIndex);
6012 attr(wysiwygEditor, 'tabindex', tabIndex);
6013
6014 rangeHelper = new RangeHelper(wysiwygWindow, null, sanitize);
6015
6016 // load any textarea value into the editor
6017 hide(original);
6018 base.val(original.value);
6019
6020 var placeholder = options.placeholder ||
6021 attr(original, 'placeholder');
6022
6023 if (placeholder) {
6024 sourceEditor.placeholder = placeholder;
6025 attr(wysiwygBody, 'placeholder', placeholder);
6026 }
6027 };
6028
6029 /**
6030 * Initialises options
6031 * @private
6032 */
6033 initOptions = function () {
6034 // auto-update original textbox on blur if option set to true
6035 if (options.autoUpdate) {
6036 on(wysiwygBody, 'blur', autoUpdate);
6037 on(sourceEditor, 'blur', autoUpdate);
6038 }
6039
6040 if (options.rtl === null) {
6041 options.rtl = css(sourceEditor, 'direction') === 'rtl';
6042 }
6043
6044 base.rtl(!!options.rtl);
6045
6046 if (options.autoExpand) {
6047 // Need to update when images (or anything else) loads
6048 on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE);
6049 on(wysiwygBody, 'input keyup', autoExpand);
6050 }
6051
6052 if (options.resizeEnabled) {
6053 initResize();
6054 }
6055
6056 attr(editorContainer, 'id', options.id);
6057 base.emoticons(options.emoticonsEnabled);
6058 };
6059
6060 /**
6061 * Initialises events
6062 * @private
6063 */
6064 initEvents = function () {
6065 var form = original.form;
6066 var compositionEvents = 'compositionstart compositionend';
6067 var eventsToForward =
6068 'keydown keyup keypress focus blur contextmenu input';
6069 var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ?
6070 'selectionchange' :
6071 'keyup focus blur contextmenu mouseup touchend click';
6072
6073 on(globalDoc, 'click', handleDocumentClick);
6074
6075 if (form) {
6076 on(form, 'reset', handleFormReset);
6077 on(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
6078 }
6079
6080 on(window, 'pagehide', base.updateOriginal);
6081 on(window, 'pageshow', handleFormReset);
6082 on(wysiwygBody, 'keypress', handleKeyPress);
6083 on(wysiwygBody, 'keydown', handleKeyDown);
6084 on(wysiwygBody, 'keydown', handleBackSpace);
6085 on(wysiwygBody, 'keyup', appendNewLine);
6086 on(wysiwygBody, 'blur', valueChangedBlur);
6087 on(wysiwygBody, 'keyup', valueChangedKeyUp);
6088 on(wysiwygBody, 'paste', handlePasteEvt);
6089 on(wysiwygBody, 'cut copy', handleCutCopyEvt);
6090 on(wysiwygBody, compositionEvents, handleComposition);
6091 on(wysiwygBody, checkSelectionEvents, checkSelectionChanged);
6092 on(wysiwygBody, eventsToForward, handleEvent);
6093
6094 if (options.emoticonsCompat && globalWin.getSelection) {
6095 on(wysiwygBody, 'keyup', emoticonsCheckWhitespace);
6096 }
6097
6098 on(wysiwygBody, 'blur', function () {
6099 if (!base.val()) {
6100 addClass(wysiwygBody, 'placeholder');
6101 }
6102 });
6103
6104 on(wysiwygBody, 'focus', function () {
6105 removeClass(wysiwygBody, 'placeholder');
6106 });
6107
6108 on(sourceEditor, 'blur', valueChangedBlur);
6109 on(sourceEditor, 'keyup', valueChangedKeyUp);
6110 on(sourceEditor, 'keydown', handleKeyDown);
6111 on(sourceEditor, compositionEvents, handleComposition);
6112 on(sourceEditor, eventsToForward, handleEvent);
6113
6114 on(wysiwygDocument, 'mousedown', handleMouseDown);
6115 on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged);
6116 on(wysiwygDocument, 'keyup', appendNewLine);
6117
6118 on(editorContainer, 'selectionchanged', checkNodeChanged);
6119 on(editorContainer, 'selectionchanged', updateActiveButtons);
6120 // Custom events to forward
6121 on(
6122 editorContainer,
6123 'selectionchanged valuechanged nodechanged pasteraw paste',
6124 handleEvent
6125 );
6126 };
6127
6128 /**
6129 * Creates the toolbar and appends it to the container
6130 * @private
6131 */
6132 initToolBar = function () {
6133 var group,
6134 commands = base.commands,
6135 exclude = (options.toolbarExclude || '').split(','),
6136 groups = options.toolbar.split('|');
6137
6138 toolbar = createElement('div', {
6139 className: 'sceditor-toolbar',
6140 unselectable: 'on'
6141 });
6142
6143 if (options.icons in SCEditor.icons) {
6144 icons = new SCEditor.icons[options.icons]();
6145 }
6146
6147 each(groups, function (_, menuItems) {
6148 group = createElement('div', {
6149 className: 'sceditor-group'
6150 });
6151
6152 each(menuItems.split(','), function (_, commandName) {
6153 var button, shortcut,
6154 command = commands[commandName];
6155
6156 // The commandName must be a valid command and not excluded
6157 if (!command || exclude.indexOf(commandName) > -1) {
6158 return;
6159 }
6160
6161 shortcut = command.shortcut;
6162 button = _tmpl('toolbarButton', {
6163 name: commandName,
6164 dispName: base._(command.name ||
6165 command.tooltip || commandName)
6166 }, true).firstChild;
6167
6168 if (icons && icons.create) {
6169 var icon = icons.create(commandName);
6170 if (icon) {
6171 insertBefore(icons.create(commandName),
6172 button.firstChild);
6173 addClass(button, 'has-icon');
6174 }
6175 }
6176
6177 button._sceTxtMode = !!command.txtExec;
6178 button._sceWysiwygMode = !!command.exec;
6179 toggleClass(button, 'disabled', !command.exec);
6180 on(button, 'click', function (e) {
6181 if (!hasClass(button, 'disabled')) {
6182 handleCommand(button, command);
6183 }
6184
6185 updateActiveButtons();
6186 e.preventDefault();
6187 });
6188 // Prevent editor losing focus when button clicked
6189 on(button, 'mousedown', function (e) {
6190 base.closeDropDown();
6191 e.preventDefault();
6192 });
6193
6194 if (command.tooltip) {
6195 attr(button, 'title',
6196 base._(command.tooltip) +
6197 (shortcut ? ' (' + shortcut + ')' : '')
6198 );
6199 }
6200
6201 if (shortcut) {
6202 base.addShortcut(shortcut, commandName);
6203 }
6204
6205 if (command.state) {
6206 btnStateHandlers.push({
6207 name: commandName,
6208 state: command.state
6209 });
6210 // exec string commands can be passed to queryCommandState
6211 } else if (isString(command.exec)) {
6212 btnStateHandlers.push({
6213 name: commandName,
6214 state: command.exec
6215 });
6216 }
6217
6218 appendChild(group, button);
6219 toolbarButtons[commandName] = button;
6220 });
6221
6222 // Exclude empty groups
6223 if (group.firstChild) {
6224 appendChild(toolbar, group);
6225 }
6226 });
6227
6228 // Append the toolbar to the toolbarContainer option if given
6229 appendChild(options.toolbarContainer || editorContainer, toolbar);
6230 };
6231
6232 /**
6233 * Creates the resizer.
6234 * @private
6235 */
6236 initResize = function () {
6237 var minHeight, maxHeight, minWidth, maxWidth,
6238 mouseMoveFunc, mouseUpFunc,
6239 grip = createElement('div', {
6240 className: 'sceditor-grip'
6241 }),
6242 // Cover is used to cover the editor iframe so document
6243 // still gets mouse move events
6244 cover = createElement('div', {
6245 className: 'sceditor-resize-cover'
6246 }),
6247 moveEvents = 'touchmove mousemove',
6248 endEvents = 'touchcancel touchend mouseup',
6249 startX = 0,
6250 startY = 0,
6251 newX = 0,
6252 newY = 0,
6253 startWidth = 0,
6254 startHeight = 0,
6255 origWidth = width(editorContainer),
6256 origHeight = height(editorContainer),
6257 isDragging = false,
6258 rtl = base.rtl();
6259
6260 minHeight = options.resizeMinHeight || origHeight / 1.5;
6261 maxHeight = options.resizeMaxHeight || origHeight * 2.5;
6262 minWidth = options.resizeMinWidth || origWidth / 1.25;
6263 maxWidth = options.resizeMaxWidth || origWidth * 1.25;
6264
6265 mouseMoveFunc = function (e) {
6266 // iOS uses window.event
6267 if (e.type === 'touchmove') {
6268 e = globalWin.event;
6269 newX = e.changedTouches[0].pageX;
6270 newY = e.changedTouches[0].pageY;
6271 } else {
6272 newX = e.pageX;
6273 newY = e.pageY;
6274 }
6275
6276 var newHeight = startHeight + (newY - startY),
6277 newWidth = rtl ?
6278 startWidth - (newX - startX) :
6279 startWidth + (newX - startX);
6280
6281 if (maxWidth > 0 && newWidth > maxWidth) {
6282 newWidth = maxWidth;
6283 }
6284 if (minWidth > 0 && newWidth < minWidth) {
6285 newWidth = minWidth;
6286 }
6287 if (!options.resizeWidth) {
6288 newWidth = false;
6289 }
6290
6291 if (maxHeight > 0 && newHeight > maxHeight) {
6292 newHeight = maxHeight;
6293 }
6294 if (minHeight > 0 && newHeight < minHeight) {
6295 newHeight = minHeight;
6296 }
6297 if (!options.resizeHeight) {
6298 newHeight = false;
6299 }
6300
6301 if (newWidth || newHeight) {
6302 base.dimensions(newWidth, newHeight);
6303 }
6304
6305 e.preventDefault();
6306 };
6307
6308 mouseUpFunc = function (e) {
6309 if (!isDragging) {
6310 return;
6311 }
6312
6313 isDragging = false;
6314
6315 hide(cover);
6316 removeClass(editorContainer, 'resizing');
6317 off(globalDoc, moveEvents, mouseMoveFunc);
6318 off(globalDoc, endEvents, mouseUpFunc);
6319
6320 e.preventDefault();
6321 };
6322
6323 if (icons && icons.create) {
6324 var icon = icons.create('grip');
6325 if (icon) {
6326 appendChild(grip, icon);
6327 addClass(grip, 'has-icon');
6328 }
6329 }
6330
6331 appendChild(editorContainer, grip);
6332 appendChild(editorContainer, cover);
6333 hide(cover);
6334
6335 on(grip, 'touchstart mousedown', function (e) {
6336 // iOS uses window.event
6337 if (e.type === 'touchstart') {
6338 e = globalWin.event;
6339 startX = e.touches[0].pageX;
6340 startY = e.touches[0].pageY;
6341 } else {
6342 startX = e.pageX;
6343 startY = e.pageY;
6344 }
6345
6346 startWidth = width(editorContainer);
6347 startHeight = height(editorContainer);
6348 isDragging = true;
6349
6350 addClass(editorContainer, 'resizing');
6351 show(cover);
6352 on(globalDoc, moveEvents, mouseMoveFunc);
6353 on(globalDoc, endEvents, mouseUpFunc);
6354
6355 e.preventDefault();
6356 });
6357 };
6358
6359 /**
6360 * Prefixes and preloads the emoticon images
6361 * @private
6362 */
6363 initEmoticons = function () {
6364 var emoticons = options.emoticons;
6365 var root = options.emoticonsRoot || '';
6366
6367 if (emoticons) {
6368 allEmoticons = extend(
6369 {}, emoticons.more, emoticons.dropdown, emoticons.hidden
6370 );
6371 }
6372
6373 each(allEmoticons, function (key, url) {
6374 allEmoticons[key] = _tmpl('emoticon', {
6375 key: key,
6376 // Prefix emoticon root to emoticon urls
6377 url: root + (url.url || url),
6378 tooltip: url.tooltip || key
6379 });
6380
6381 // Preload the emoticon
6382 if (options.emoticonsEnabled) {
6383 preLoadCache.push(createElement('img', {
6384 src: root + (url.url || url)
6385 }));
6386 }
6387 });
6388 };
6389
6390 /**
6391 * Autofocus the editor
6392 * @private
6393 */
6394 autofocus = function (focusEnd) {
6395 var range, txtPos,
6396 node = wysiwygBody.firstChild;
6397
6398 // Can't focus invisible elements
6399 if (!isVisible(editorContainer)) {
6400 return;
6401 }
6402
6403 if (base.sourceMode()) {
6404 txtPos = focusEnd ? sourceEditor.value.length : 0;
6405
6406 sourceEditor.setSelectionRange(txtPos, txtPos);
6407
6408 return;
6409 }
6410
6411 removeWhiteSpace(wysiwygBody);
6412
6413 if (focusEnd) {
6414 if (!(node = wysiwygBody.lastChild)) {
6415 node = createElement('p', {}, wysiwygDocument);
6416 appendChild(wysiwygBody, node);
6417 }
6418
6419 while (node.lastChild) {
6420 node = node.lastChild;
6421
6422 // Should place the cursor before the last <br>
6423 if (is(node, 'br') && node.previousSibling) {
6424 node = node.previousSibling;
6425 }
6426 }
6427 }
6428
6429 range = wysiwygDocument.createRange();
6430
6431 if (!canHaveChildren(node)) {
6432 range.setStartBefore(node);
6433
6434 if (focusEnd) {
6435 range.setStartAfter(node);
6436 }
6437 } else {
6438 range.selectNodeContents(node);
6439 }
6440
6441 range.collapse(!focusEnd);
6442 rangeHelper.selectRange(range);
6443 currentSelection = range;
6444
6445 if (focusEnd) {
6446 wysiwygBody.scrollTop = wysiwygBody.scrollHeight;
6447 }
6448
6449 base.focus();
6450 };
6451
6452 /**
6453 * Gets if the editor is read only
6454 *
6455 * @since 1.3.5
6456 * @function
6457 * @memberOf SCEditor.prototype
6458 * @name readOnly
6459 * @return {boolean}
6460 */
6461 /**
6462 * Sets if the editor is read only
6463 *
6464 * @param {boolean} readOnly
6465 * @since 1.3.5
6466 * @function
6467 * @memberOf SCEditor.prototype
6468 * @name readOnly^2
6469 * @return {this}
6470 */
6471 base.readOnly = function (readOnly) {
6472 if (typeof readOnly !== 'boolean') {
6473 return !sourceEditor.readonly;
6474 }
6475
6476 wysiwygBody.contentEditable = !readOnly;
6477 sourceEditor.readonly = !readOnly;
6478
6479 updateToolBar(readOnly);
6480
6481 return base;
6482 };
6483
6484 /**
6485 * Gets if the editor is in RTL mode
6486 *
6487 * @since 1.4.1
6488 * @function
6489 * @memberOf SCEditor.prototype
6490 * @name rtl
6491 * @return {boolean}
6492 */
6493 /**
6494 * Sets if the editor is in RTL mode
6495 *
6496 * @param {boolean} rtl
6497 * @since 1.4.1
6498 * @function
6499 * @memberOf SCEditor.prototype
6500 * @name rtl^2
6501 * @return {this}
6502 */
6503 base.rtl = function (rtl) {
6504 var dir = rtl ? 'rtl' : 'ltr';
6505
6506 if (typeof rtl !== 'boolean') {
6507 return attr(sourceEditor, 'dir') === 'rtl';
6508 }
6509
6510 attr(wysiwygBody, 'dir', dir);
6511 attr(sourceEditor, 'dir', dir);
6512
6513 removeClass(editorContainer, 'rtl');
6514 removeClass(editorContainer, 'ltr');
6515 addClass(editorContainer, dir);
6516
6517 if (icons && icons.rtl) {
6518 icons.rtl(rtl);
6519 }
6520
6521 return base;
6522 };
6523
6524 /**
6525 * Updates the toolbar to disable/enable the appropriate buttons
6526 * @private
6527 */
6528 updateToolBar = function (disable) {
6529 var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode';
6530
6531 each(toolbarButtons, function (_, button) {
6532 toggleClass(button, 'disabled', disable || !button[mode]);
6533 });
6534 };
6535
6536 /**
6537 * Gets the width of the editor in pixels
6538 *
6539 * @since 1.3.5
6540 * @function
6541 * @memberOf SCEditor.prototype
6542 * @name width
6543 * @return {number}
6544 */
6545 /**
6546 * Sets the width of the editor
6547 *
6548 * @param {number} width Width in pixels
6549 * @since 1.3.5
6550 * @function
6551 * @memberOf SCEditor.prototype
6552 * @name width^2
6553 * @return {this}
6554 */
6555 /**
6556 * Sets the width of the editor
6557 *
6558 * The saveWidth specifies if to save the width. The stored width can be
6559 * used for things like restoring from maximized state.
6560 *
6561 * @param {number} width Width in pixels
6562 * @param {boolean} [saveWidth=true] If to store the width
6563 * @since 1.4.1
6564 * @function
6565 * @memberOf SCEditor.prototype
6566 * @name width^3
6567 * @return {this}
6568 */
6569 base.width = function (width$1, saveWidth) {
6570 if (!width$1 && width$1 !== 0) {
6571 return width(editorContainer);
6572 }
6573
6574 base.dimensions(width$1, null, saveWidth);
6575
6576 return base;
6577 };
6578
6579 /**
6580 * Returns an object with the properties width and height
6581 * which are the width and height of the editor in px.
6582 *
6583 * @since 1.4.1
6584 * @function
6585 * @memberOf SCEditor.prototype
6586 * @name dimensions
6587 * @return {object}
6588 */
6589 /**
6590 * <p>Sets the width and/or height of the editor.</p>
6591 *
6592 * <p>If width or height is not numeric it is ignored.</p>
6593 *
6594 * @param {number} width Width in px
6595 * @param {number} height Height in px
6596 * @since 1.4.1
6597 * @function
6598 * @memberOf SCEditor.prototype
6599 * @name dimensions^2
6600 * @return {this}
6601 */
6602 /**
6603 * <p>Sets the width and/or height of the editor.</p>
6604 *
6605 * <p>If width or height is not numeric it is ignored.</p>
6606 *
6607 * <p>The save argument specifies if to save the new sizes.
6608 * The saved sizes can be used for things like restoring from
6609 * maximized state. This should normally be left as true.</p>
6610 *
6611 * @param {number} width Width in px
6612 * @param {number} height Height in px
6613 * @param {boolean} [save=true] If to store the new sizes
6614 * @since 1.4.1
6615 * @function
6616 * @memberOf SCEditor.prototype
6617 * @name dimensions^3
6618 * @return {this}
6619 */
6620 base.dimensions = function (width$1, height$1, save) {
6621 // set undefined width/height to boolean false
6622 width$1 = (!width$1 && width$1 !== 0) ? false : width$1;
6623 height$1 = (!height$1 && height$1 !== 0) ? false : height$1;
6624
6625 if (width$1 === false && height$1 === false) {
6626 return { width: base.width(), height: base.height() };
6627 }
6628
6629 if (width$1 !== false) {
6630 if (save !== false) {
6631 options.width = width$1;
6632 }
6633
6634 width(editorContainer, width$1);
6635 }
6636
6637 if (height$1 !== false) {
6638 if (save !== false) {
6639 options.height = height$1;
6640 }
6641
6642 height(editorContainer, height$1);
6643 }
6644
6645 return base;
6646 };
6647
6648 /**
6649 * Gets the height of the editor in px
6650 *
6651 * @since 1.3.5
6652 * @function
6653 * @memberOf SCEditor.prototype
6654 * @name height
6655 * @return {number}
6656 */
6657 /**
6658 * Sets the height of the editor
6659 *
6660 * @param {number} height Height in px
6661 * @since 1.3.5
6662 * @function
6663 * @memberOf SCEditor.prototype
6664 * @name height^2
6665 * @return {this}
6666 */
6667 /**
6668 * Sets the height of the editor
6669 *
6670 * The saveHeight specifies if to save the height.
6671 *
6672 * The stored height can be used for things like
6673 * restoring from maximized state.
6674 *
6675 * @param {number} height Height in px
6676 * @param {boolean} [saveHeight=true] If to store the height
6677 * @since 1.4.1
6678 * @function
6679 * @memberOf SCEditor.prototype
6680 * @name height^3
6681 * @return {this}
6682 */
6683 base.height = function (height$1, saveHeight) {
6684 if (!height$1 && height$1 !== 0) {
6685 return height(editorContainer);
6686 }
6687
6688 base.dimensions(null, height$1, saveHeight);
6689
6690 return base;
6691 };
6692
6693 /**
6694 * Gets if the editor is maximised or not
6695 *
6696 * @since 1.4.1
6697 * @function
6698 * @memberOf SCEditor.prototype
6699 * @name maximize
6700 * @return {boolean}
6701 */
6702 /**
6703 * Sets if the editor is maximised or not
6704 *
6705 * @param {boolean} maximize If to maximise the editor
6706 * @since 1.4.1
6707 * @function
6708 * @memberOf SCEditor.prototype
6709 * @name maximize^2
6710 * @return {this}
6711 */
6712 base.maximize = function (maximize) {
6713 var maximizeSize = 'sceditor-maximize';
6714
6715 if (isUndefined(maximize)) {
6716 return hasClass(editorContainer, maximizeSize);
6717 }
6718
6719 maximize = !!maximize;
6720
6721 if (maximize) {
6722 maximizeScrollPosition = globalWin.pageYOffset;
6723 }
6724
6725 toggleClass(globalDoc.documentElement, maximizeSize, maximize);
6726 toggleClass(globalDoc.body, maximizeSize, maximize);
6727 toggleClass(editorContainer, maximizeSize, maximize);
6728 base.width(maximize ? '100%' : options.width, false);
6729 base.height(maximize ? '100%' : options.height, false);
6730
6731 if (!maximize) {
6732 globalWin.scrollTo(0, maximizeScrollPosition);
6733 }
6734
6735 autoExpand();
6736
6737 return base;
6738 };
6739
6740 autoExpand = function () {
6741 if (options.autoExpand && !autoExpandThrottle) {
6742 autoExpandThrottle = setTimeout(base.expandToContent, 200);
6743 }
6744 };
6745
6746 /**
6747 * Expands or shrinks the editors height to the height of it's content
6748 *
6749 * Unless ignoreMaxHeight is set to true it will not expand
6750 * higher than the maxHeight option.
6751 *
6752 * @since 1.3.5
6753 * @param {boolean} [ignoreMaxHeight=false]
6754 * @function
6755 * @name expandToContent
6756 * @memberOf SCEditor.prototype
6757 * @see #resizeToContent
6758 */
6759 base.expandToContent = function (ignoreMaxHeight) {
6760 if (base.maximize()) {
6761 return;
6762 }
6763
6764 clearTimeout(autoExpandThrottle);
6765 autoExpandThrottle = false;
6766
6767 if (!autoExpandBounds) {
6768 var height$1 = options.resizeMinHeight || options.height ||
6769 height(original);
6770
6771 autoExpandBounds = {
6772 min: height$1,
6773 max: options.resizeMaxHeight || (height$1 * 2)
6774 };
6775 }
6776
6777 var range = globalDoc.createRange();
6778 range.selectNodeContents(wysiwygBody);
6779
6780 var rect = range.getBoundingClientRect();
6781 var current = wysiwygDocument.documentElement.clientHeight - 1;
6782 var spaceNeeded = rect.bottom - rect.top;
6783 var newHeight = base.height() + 1 + (spaceNeeded - current);
6784
6785 if (!ignoreMaxHeight && autoExpandBounds.max !== -1) {
6786 newHeight = Math.min(newHeight, autoExpandBounds.max);
6787 }
6788
6789 base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min)));
6790 };
6791
6792 /**
6793 * Destroys the editor, removing all elements and
6794 * event handlers.
6795 *
6796 * Leaves only the original textarea.
6797 *
6798 * @function
6799 * @name destroy
6800 * @memberOf SCEditor.prototype
6801 */
6802 base.destroy = function () {
6803 // Don't destroy if the editor has already been destroyed
6804 if (!pluginManager) {
6805 return;
6806 }
6807
6808 pluginManager.destroy();
6809
6810 rangeHelper = null;
6811 pluginManager = null;
6812
6813 if (dropdown) {
6814 remove(dropdown);
6815 }
6816
6817 off(globalDoc, 'click', handleDocumentClick);
6818
6819 var form = original.form;
6820 if (form) {
6821 off(form, 'reset', handleFormReset);
6822 off(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
6823 }
6824
6825 off(window, 'pagehide', base.updateOriginal);
6826 off(window, 'pageshow', handleFormReset);
6827 remove(sourceEditor);
6828 remove(toolbar);
6829 remove(editorContainer);
6830
6831 delete original._sceditor;
6832 show(original);
6833
6834 original.required = isRequired;
6835 };
6836
6837
6838 /**
6839 * Creates a menu item drop down
6840 *
6841 * @param {HTMLElement} menuItem The button to align the dropdown with
6842 * @param {string} name Used for styling the dropdown, will be
6843 * a class sceditor-name
6844 * @param {HTMLElement} content The HTML content of the dropdown
6845 * @function
6846 * @name createDropDown
6847 * @memberOf SCEditor.prototype
6848 */
6849 base.createDropDown = function (menuItem, name, content) {
6850 // first click for create second click for close
6851 var dropDownCss,
6852 dropDownClass = 'sceditor-' + name;
6853
6854 base.closeDropDown();
6855
6856 // Only close the dropdown if it was already open
6857 if (dropdown && hasClass(dropdown, dropDownClass)) {
6858 return;
6859 }
6860
6861 dropDownCss = extend({
6862 top: menuItem.offsetTop,
6863 left: menuItem.offsetLeft,
6864 marginTop: menuItem.clientHeight
6865 }, options.dropDownCss);
6866
6867 dropdown = createElement('div', {
6868 className: 'sceditor-dropdown ' + dropDownClass
6869 });
6870
6871 css(dropdown, dropDownCss);
6872 appendChild(dropdown, content);
6873 appendChild(editorContainer, dropdown);
6874 on(dropdown, 'click focusin', function (e) {
6875 // stop clicks within the dropdown from being handled
6876 e.stopPropagation();
6877 });
6878
6879 if (dropdown) {
6880 var first = find(dropdown, 'input,textarea')[0];
6881 if (first) {
6882 first.focus();
6883 }
6884 }
6885 };
6886
6887 /**
6888 * Handles any document click and closes the dropdown if open
6889 * @private
6890 */
6891 handleDocumentClick = function (e) {
6892 // ignore right clicks
6893 if (e.which !== 3 && dropdown && !e.defaultPrevented) {
6894 autoUpdate();
6895
6896 base.closeDropDown();
6897 }
6898 };
6899
6900 /**
6901 * Handles the WYSIWYG editors cut & copy events
6902 *
6903 * By default browsers also copy inherited styling from the stylesheet and
6904 * browser default styling which is unnecessary.
6905 *
6906 * This will ignore inherited styles and only copy inline styling.
6907 * @private
6908 */
6909 handleCutCopyEvt = function (e) {
6910 var range = rangeHelper.selectedRange();
6911 if (range) {
6912 var container = createElement('div', {}, wysiwygDocument);
6913 var firstParent;
6914
6915 // Copy all inline parent nodes up to the first block parent so can
6916 // copy inline styles
6917 var parent = range.commonAncestorContainer;
6918 while (parent && isInline(parent, true)) {
6919 if (parent.nodeType === ELEMENT_NODE) {
6920 var clone = parent.cloneNode();
6921 if (container.firstChild) {
6922 appendChild(clone, container.firstChild);
6923 }
6924
6925 appendChild(container, clone);
6926 firstParent = firstParent || clone;
6927 }
6928 parent = parent.parentNode;
6929 }
6930
6931 appendChild(firstParent || container, range.cloneContents());
6932 removeWhiteSpace(container);
6933
6934 e.clipboardData.setData('text/html', container.innerHTML);
6935
6936 // TODO: Refactor into private shared module with plaintext plugin
6937 // innerText adds two newlines after <p> tags so convert them to
6938 // <div> tags
6939 each(find(container, 'p'), function (_, elm) {
6940 convertElement(elm, 'div');
6941 });
6942 // Remove collapsed <br> tags as innerText converts them to newlines
6943 each(find(container, 'br'), function (_, elm) {
6944 if (!elm.nextSibling || !isInline(elm.nextSibling, true)) {
6945 remove(elm);
6946 }
6947 });
6948
6949 // range.toString() doesn't include newlines so can't use that.
6950 // selection.toString() seems to use the same method as innerText
6951 // but needs to be normalised first so using container.innerText
6952 appendChild(wysiwygBody, container);
6953 e.clipboardData.setData('text/plain', container.innerText);
6954 remove(container);
6955
6956 if (e.type === 'cut') {
6957 range.deleteContents();
6958 }
6959
6960 e.preventDefault();
6961 }
6962 };
6963
6964 /**
6965 * Handles the WYSIWYG editors paste event
6966 * @private
6967 */
6968 handlePasteEvt = function (e) {
6969 var editable = wysiwygBody;
6970 var clipboard = e.clipboardData;
6971 var loadImage = function (file) {
6972 var reader = new FileReader();
6973 reader.onload = function (e) {
6974 handlePasteData({
6975 html: '<img src="' + e.target.result + '" />'
6976 });
6977 };
6978 reader.readAsDataURL(file);
6979 };
6980
6981 // Modern browsers with clipboard API - everything other than _very_
6982 // old android web views and UC browser which doesn't support the
6983 // paste event at all.
6984 if (clipboard) {
6985 var data = {};
6986 var types = clipboard.types;
6987 var items = clipboard.items;
6988
6989 e.preventDefault();
6990
6991 for (var i = 0; i < types.length; i++) {
6992 // Word sometimes adds copied text as an image so if HTML
6993 // exists prefer that over images
6994 if (types.indexOf('text/html') < 0) {
6995 // Normalise image pasting to paste as a data-uri
6996 if (globalWin.FileReader && items &&
6997 IMAGE_MIME_REGEX.test(items[i].type)) {
6998 return loadImage(clipboard.items[i].getAsFile());
6999 }
7000 }
7001
7002 data[types[i]] = clipboard.getData(types[i]);
7003 }
7004 // Call plugins here with file?
7005 data.text = data['text/plain'];
7006 data.html = sanitize(data['text/html']);
7007
7008 handlePasteData(data);
7009 // If contentsFragment exists then we are already waiting for a
7010 // previous paste so let the handler for that handle this one too
7011 } else if (!pasteContentFragment) {
7012 // Save the scroll position so can be restored
7013 // when contents is restored
7014 var scrollTop = editable.scrollTop;
7015
7016 rangeHelper.saveRange();
7017
7018 pasteContentFragment = globalDoc.createDocumentFragment();
7019 while (editable.firstChild) {
7020 appendChild(pasteContentFragment, editable.firstChild);
7021 }
7022
7023 setTimeout(function () {
7024 var html = editable.innerHTML;
7025
7026 editable.innerHTML = '';
7027 appendChild(editable, pasteContentFragment);
7028 editable.scrollTop = scrollTop;
7029 pasteContentFragment = false;
7030
7031 rangeHelper.restoreRange();
7032
7033 handlePasteData({ html: sanitize(html) });
7034 }, 0);
7035 }
7036 };
7037
7038 /**
7039 * Gets the pasted data, filters it and then inserts it.
7040 * @param {Object} data
7041 * @private
7042 */
7043 handlePasteData = function (data) {
7044 var pasteArea = createElement('div', {}, wysiwygDocument);
7045
7046 pluginManager.call('pasteRaw', data);
7047 trigger(editorContainer, 'pasteraw', data);
7048
7049 if (data.html) {
7050 // Sanitize again in case plugins modified the HTML
7051 pasteArea.innerHTML = sanitize(data.html);
7052
7053 // fix any invalid nesting
7054 fixNesting(pasteArea);
7055 } else {
7056 pasteArea.innerHTML = entities(data.text || '');
7057 }
7058
7059 var paste = {
7060 val: pasteArea.innerHTML
7061 };
7062
7063 if ('fragmentToSource' in format) {
7064 paste.val = format
7065 .fragmentToSource(paste.val, wysiwygDocument, currentNode);
7066 }
7067
7068 pluginManager.call('paste', paste);
7069 trigger(editorContainer, 'paste', paste);
7070
7071 if ('fragmentToHtml' in format) {
7072 paste.val = format
7073 .fragmentToHtml(paste.val, currentNode);
7074 }
7075
7076 pluginManager.call('pasteHtml', paste);
7077
7078 var parent = rangeHelper.getFirstBlockParent();
7079 base.wysiwygEditorInsertHtml(paste.val, null, true);
7080 merge(parent);
7081 };
7082
7083 /**
7084 * Closes any currently open drop down
7085 *
7086 * @param {boolean} [focus=false] If to focus the editor
7087 * after closing the drop down
7088 * @function
7089 * @name closeDropDown
7090 * @memberOf SCEditor.prototype
7091 */
7092 base.closeDropDown = function (focus) {
7093 if (dropdown) {
7094 remove(dropdown);
7095 dropdown = null;
7096 }
7097
7098 if (focus === true) {
7099 base.focus();
7100 }
7101 };
7102
7103
7104 /**
7105 * Inserts HTML into WYSIWYG editor.
7106 *
7107 * If endHtml is specified, any selected text will be placed
7108 * between html and endHtml. If there is no selected text html
7109 * and endHtml will just be concatenate together.
7110 *
7111 * @param {string} html
7112 * @param {string} [endHtml=null]
7113 * @param {boolean} [overrideCodeBlocking=false] If to insert the html
7114 * into code tags, by
7115 * default code tags only
7116 * support text.
7117 * @function
7118 * @name wysiwygEditorInsertHtml
7119 * @memberOf SCEditor.prototype
7120 */
7121 base.wysiwygEditorInsertHtml = function (
7122 html, endHtml, overrideCodeBlocking
7123 ) {
7124 var marker, scrollTop, scrollTo,
7125 editorHeight = height(wysiwygEditor);
7126
7127 base.focus();
7128
7129 // TODO: This code tag should be configurable and
7130 // should maybe convert the HTML into text instead
7131 // Don't apply to code elements
7132 if (!overrideCodeBlocking && closest(currentBlockNode, 'code')) {
7133 return;
7134 }
7135
7136 // Insert the HTML and save the range so the editor can be scrolled
7137 // to the end of the selection. Also allows emoticons to be replaced
7138 // without affecting the cursor position
7139 rangeHelper.insertHTML(html, endHtml);
7140 rangeHelper.saveRange();
7141 replaceEmoticons();
7142
7143 // Fix any invalid nesting, e.g. if a quote or other block is inserted
7144 // into a paragraph
7145 fixNesting(wysiwygBody);
7146
7147 // Scroll the editor after the end of the selection
7148 marker = find(wysiwygBody, '#sceditor-end-marker')[0];
7149 show(marker);
7150 scrollTop = wysiwygBody.scrollTop;
7151 scrollTo = (getOffset(marker).top +
7152 (marker.offsetHeight * 1.5)) - editorHeight;
7153 hide(marker);
7154
7155 // Only scroll if marker isn't already visible
7156 if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) {
7157 wysiwygBody.scrollTop = scrollTo;
7158 }
7159
7160 triggerValueChanged(false);
7161 rangeHelper.restoreRange();
7162
7163 // Add a new line after the last block element
7164 // so can always add text after it
7165 appendNewLine();
7166 };
7167
7168 /**
7169 * Like wysiwygEditorInsertHtml except it will convert any HTML
7170 * into text before inserting it.
7171 *
7172 * @param {string} text
7173 * @param {string} [endText=null]
7174 * @function
7175 * @name wysiwygEditorInsertText
7176 * @memberOf SCEditor.prototype
7177 */
7178 base.wysiwygEditorInsertText = function (text, endText) {
7179 base.wysiwygEditorInsertHtml(
7180 entities(text), entities(endText)
7181 );
7182 };
7183
7184 /**
7185 * Inserts text into the WYSIWYG or source editor depending on which
7186 * mode the editor is in.
7187 *
7188 * If endText is specified any selected text will be placed between
7189 * text and endText. If no text is selected text and endText will
7190 * just be concatenate together.
7191 *
7192 * @param {string} text
7193 * @param {string} [endText=null]
7194 * @since 1.3.5
7195 * @function
7196 * @name insertText
7197 * @memberOf SCEditor.prototype
7198 */
7199 base.insertText = function (text, endText) {
7200 if (base.inSourceMode()) {
7201 base.sourceEditorInsertText(text, endText);
7202 } else {
7203 base.wysiwygEditorInsertText(text, endText);
7204 }
7205
7206 return base;
7207 };
7208
7209 /**
7210 * Like wysiwygEditorInsertHtml but inserts text into the
7211 * source mode editor instead.
7212 *
7213 * If endText is specified any selected text will be placed between
7214 * text and endText. If no text is selected text and endText will
7215 * just be concatenate together.
7216 *
7217 * The cursor will be placed after the text param. If endText is
7218 * specified the cursor will be placed before endText, so passing:<br />
7219 *
7220 * '[b]', '[/b]'
7221 *
7222 * Would cause the cursor to be placed:<br />
7223 *
7224 * [b]Selected text|[/b]
7225 *
7226 * @param {string} text
7227 * @param {string} [endText=null]
7228 * @since 1.4.0
7229 * @function
7230 * @name sourceEditorInsertText
7231 * @memberOf SCEditor.prototype
7232 */
7233 base.sourceEditorInsertText = function (text, endText) {
7234 var scrollTop, currentValue,
7235 startPos = sourceEditor.selectionStart,
7236 endPos = sourceEditor.selectionEnd;
7237
7238 scrollTop = sourceEditor.scrollTop;
7239 sourceEditor.focus();
7240 currentValue = sourceEditor.value;
7241
7242 if (endText) {
7243 text += currentValue.substring(startPos, endPos) + endText;
7244 }
7245
7246 sourceEditor.value = currentValue.substring(0, startPos) +
7247 text +
7248 currentValue.substring(endPos, currentValue.length);
7249
7250 sourceEditor.selectionStart = (startPos + text.length) -
7251 (endText ? endText.length : 0);
7252 sourceEditor.selectionEnd = sourceEditor.selectionStart;
7253
7254 sourceEditor.scrollTop = scrollTop;
7255 sourceEditor.focus();
7256
7257 triggerValueChanged();
7258 };
7259
7260 /**
7261 * Gets the current instance of the rangeHelper class
7262 * for the editor.
7263 *
7264 * @return {RangeHelper}
7265 * @function
7266 * @name getRangeHelper
7267 * @memberOf SCEditor.prototype
7268 */
7269 base.getRangeHelper = function () {
7270 return rangeHelper;
7271 };
7272
7273 /**
7274 * Gets or sets the source editor caret position.
7275 *
7276 * @param {Object} [position]
7277 * @return {this}
7278 * @function
7279 * @since 1.4.5
7280 * @name sourceEditorCaret
7281 * @memberOf SCEditor.prototype
7282 */
7283 base.sourceEditorCaret = function (position) {
7284 sourceEditor.focus();
7285
7286 if (position) {
7287 sourceEditor.selectionStart = position.start;
7288 sourceEditor.selectionEnd = position.end;
7289
7290 return this;
7291 }
7292
7293 return {
7294 start: sourceEditor.selectionStart,
7295 end: sourceEditor.selectionEnd
7296 };
7297 };
7298
7299 /**
7300 * Gets the value of the editor.
7301 *
7302 * If the editor is in WYSIWYG mode it will return the filtered
7303 * HTML from it (converted to BBCode if using the BBCode plugin).
7304 * It it's in Source Mode it will return the unfiltered contents
7305 * of the source editor (if using the BBCode plugin this will be
7306 * BBCode again).
7307 *
7308 * @since 1.3.5
7309 * @return {string}
7310 * @function
7311 * @name val
7312 * @memberOf SCEditor.prototype
7313 */
7314 /**
7315 * Sets the value of the editor.
7316 *
7317 * If filter set true the val will be passed through the filter
7318 * function. If using the BBCode plugin it will pass the val to
7319 * the BBCode filter to convert any BBCode into HTML.
7320 *
7321 * @param {string} val
7322 * @param {boolean} [filter=true]
7323 * @return {this}
7324 * @since 1.3.5
7325 * @function
7326 * @name val^2
7327 * @memberOf SCEditor.prototype
7328 */
7329 base.val = function (val, filter) {
7330 if (!isString(val)) {
7331 return base.inSourceMode() ?
7332 base.getSourceEditorValue(false) :
7333 base.getWysiwygEditorValue(filter);
7334 }
7335
7336 if (!base.inSourceMode()) {
7337 if (filter !== false && 'toHtml' in format) {
7338 val = format.toHtml(val);
7339 }
7340
7341 base.setWysiwygEditorValue(val);
7342 } else {
7343 base.setSourceEditorValue(val);
7344 }
7345
7346 return base;
7347 };
7348
7349 /**
7350 * Inserts HTML/BBCode into the editor
7351 *
7352 * If end is supplied any selected text will be placed between
7353 * start and end. If there is no selected text start and end
7354 * will be concatenate together.
7355 *
7356 * If the filter param is set to true, the HTML/BBCode will be
7357 * passed through any plugin filters. If using the BBCode plugin
7358 * this will convert any BBCode into HTML.
7359 *
7360 * @param {string} start
7361 * @param {string} [end=null]
7362 * @param {boolean} [filter=true]
7363 * @param {boolean} [convertEmoticons=true] If to convert emoticons
7364 * @return {this}
7365 * @since 1.3.5
7366 * @function
7367 * @name insert
7368 * @memberOf SCEditor.prototype
7369 */
7370 /**
7371 * Inserts HTML/BBCode into the editor
7372 *
7373 * If end is supplied any selected text will be placed between
7374 * start and end. If there is no selected text start and end
7375 * will be concatenate together.
7376 *
7377 * If the filter param is set to true, the HTML/BBCode will be
7378 * passed through any plugin filters. If using the BBCode plugin
7379 * this will convert any BBCode into HTML.
7380 *
7381 * If the allowMixed param is set to true, HTML any will not be
7382 * escaped
7383 *
7384 * @param {string} start
7385 * @param {string} [end=null]
7386 * @param {boolean} [filter=true]
7387 * @param {boolean} [convertEmoticons=true] If to convert emoticons
7388 * @param {boolean} [allowMixed=false]
7389 * @return {this}
7390 * @since 1.4.3
7391 * @function
7392 * @name insert^2
7393 * @memberOf SCEditor.prototype
7394 */
7395 // eslint-disable-next-line max-params
7396 base.insert = function (
7397 start, end, filter, convertEmoticons, allowMixed
7398 ) {
7399 if (base.inSourceMode()) {
7400 base.sourceEditorInsertText(start, end);
7401 return base;
7402 }
7403
7404 // Add the selection between start and end
7405 if (end) {
7406 var html = rangeHelper.selectedHtml();
7407
7408 if (filter !== false && 'fragmentToSource' in format) {
7409 html = format
7410 .fragmentToSource(html, wysiwygDocument, currentNode);
7411 }
7412
7413 start += html + end;
7414 }
7415 // TODO: This filter should allow empty tags as it's inserting.
7416 if (filter !== false && 'fragmentToHtml' in format) {
7417 start = format.fragmentToHtml(start, currentNode);
7418 }
7419
7420 // Convert any escaped HTML back into HTML if mixed is allowed
7421 if (filter !== false && allowMixed === true) {
7422 start = start.replace(/&lt;/g, '<')
7423 .replace(/&gt;/g, '>')
7424 .replace(/&amp;/g, '&');
7425 }
7426
7427 base.wysiwygEditorInsertHtml(start);
7428
7429 return base;
7430 };
7431
7432 /**
7433 * Gets the WYSIWYG editors HTML value.
7434 *
7435 * If using a plugin that filters the Ht Ml like the BBCode plugin
7436 * it will return the result of the filtering (BBCode) unless the
7437 * filter param is set to false.
7438 *
7439 * @param {boolean} [filter=true]
7440 * @return {string}
7441 * @function
7442 * @name getWysiwygEditorValue
7443 * @memberOf SCEditor.prototype
7444 */
7445 base.getWysiwygEditorValue = function (filter) {
7446 var html;
7447 // Create a tmp node to store contents so it can be modified
7448 // without affecting anything else.
7449 var tmp = createElement('div', {}, wysiwygDocument);
7450 var childNodes = wysiwygBody.childNodes;
7451
7452 for (var i = 0; i < childNodes.length; i++) {
7453 appendChild(tmp, childNodes[i].cloneNode(true));
7454 }
7455
7456 appendChild(wysiwygBody, tmp);
7457 fixNesting(tmp);
7458 remove(tmp);
7459
7460 html = tmp.innerHTML;
7461
7462 // filter the HTML and DOM through any plugins
7463 if (filter !== false && format.hasOwnProperty('toSource')) {
7464 html = format.toSource(html, wysiwygDocument);
7465 }
7466
7467 return html;
7468 };
7469
7470 /**
7471 * Gets the WYSIWYG editor's iFrame Body.
7472 *
7473 * @return {HTMLElement}
7474 * @function
7475 * @since 1.4.3
7476 * @name getBody
7477 * @memberOf SCEditor.prototype
7478 */
7479 base.getBody = function () {
7480 return wysiwygBody;
7481 };
7482
7483 /**
7484 * Gets the WYSIWYG editors container area (whole iFrame).
7485 *
7486 * @return {HTMLElement}
7487 * @function
7488 * @since 1.4.3
7489 * @name getContentAreaContainer
7490 * @memberOf SCEditor.prototype
7491 */
7492 base.getContentAreaContainer = function () {
7493 return wysiwygEditor;
7494 };
7495
7496 /**
7497 * Gets the text editor value
7498 *
7499 * If using a plugin that filters the text like the BBCode plugin
7500 * it will return the result of the filtering which is BBCode to
7501 * HTML so it will return HTML. If filter is set to false it will
7502 * just return the contents of the source editor (BBCode).
7503 *
7504 * @param {boolean} [filter=true]
7505 * @return {string}
7506 * @function
7507 * @since 1.4.0
7508 * @name getSourceEditorValue
7509 * @memberOf SCEditor.prototype
7510 */
7511 base.getSourceEditorValue = function (filter) {
7512 var val = sourceEditor.value;
7513
7514 if (filter !== false && 'toHtml' in format) {
7515 val = format.toHtml(val);
7516 }
7517
7518 return val;
7519 };
7520
7521 /**
7522 * Sets the WYSIWYG HTML editor value. Should only be the HTML
7523 * contained within the body tags
7524 *
7525 * @param {string} value
7526 * @function
7527 * @name setWysiwygEditorValue
7528 * @memberOf SCEditor.prototype
7529 */
7530 base.setWysiwygEditorValue = function (value) {
7531 if (!value) {
7532 value = '<p><br /></p>';
7533 }
7534
7535 wysiwygBody.innerHTML = sanitize(value);
7536 replaceEmoticons();
7537
7538 appendNewLine();
7539 triggerValueChanged();
7540 autoExpand();
7541 };
7542
7543 /**
7544 * Sets the text editor value
7545 *
7546 * @param {string} value
7547 * @function
7548 * @name setSourceEditorValue
7549 * @memberOf SCEditor.prototype
7550 */
7551 base.setSourceEditorValue = function (value) {
7552 sourceEditor.value = value;
7553
7554 triggerValueChanged();
7555 };
7556
7557 /**
7558 * Updates the textarea that the editor is replacing
7559 * with the value currently inside the editor.
7560 *
7561 * @function
7562 * @name updateOriginal
7563 * @since 1.4.0
7564 * @memberOf SCEditor.prototype
7565 */
7566 base.updateOriginal = function () {
7567 original.value = base.val();
7568 };
7569
7570 /**
7571 * Replaces any emoticon codes in the passed HTML
7572 * with their emoticon images
7573 * @private
7574 */
7575 replaceEmoticons = function () {
7576 if (options.emoticonsEnabled) {
7577 replace(wysiwygBody, allEmoticons, options.emoticonsCompat);
7578 }
7579 };
7580
7581 /**
7582 * If the editor is in source code mode
7583 *
7584 * @return {boolean}
7585 * @function
7586 * @name inSourceMode
7587 * @memberOf SCEditor.prototype
7588 */
7589 base.inSourceMode = function () {
7590 return hasClass(editorContainer, 'sourceMode');
7591 };
7592
7593 /**
7594 * Gets if the editor is in sourceMode
7595 *
7596 * @return boolean
7597 * @function
7598 * @name sourceMode
7599 * @memberOf SCEditor.prototype
7600 */
7601 /**
7602 * Sets if the editor is in sourceMode
7603 *
7604 * @param {boolean} enable
7605 * @return {this}
7606 * @function
7607 * @name sourceMode^2
7608 * @memberOf SCEditor.prototype
7609 */
7610 base.sourceMode = function (enable) {
7611 var inSourceMode = base.inSourceMode();
7612
7613 if (typeof enable !== 'boolean') {
7614 return inSourceMode;
7615 }
7616
7617 if ((inSourceMode && !enable) || (!inSourceMode && enable)) {
7618 base.toggleSourceMode();
7619 }
7620
7621 return base;
7622 };
7623
7624 /**
7625 * Switches between the WYSIWYG and source modes
7626 *
7627 * @function
7628 * @name toggleSourceMode
7629 * @since 1.4.0
7630 * @memberOf SCEditor.prototype
7631 */
7632 base.toggleSourceMode = function () {
7633 var isInSourceMode = base.inSourceMode();
7634
7635 // don't allow switching to WYSIWYG if doesn't support it
7636 if (!isWysiwygSupported && isInSourceMode) {
7637 return;
7638 }
7639
7640 if (!isInSourceMode) {
7641 rangeHelper.saveRange();
7642 rangeHelper.clear();
7643 }
7644
7645 currentSelection = null;
7646 base.blur();
7647
7648 if (isInSourceMode) {
7649 base.setWysiwygEditorValue(base.getSourceEditorValue());
7650 } else {
7651 base.setSourceEditorValue(base.getWysiwygEditorValue());
7652 }
7653
7654 toggle(sourceEditor);
7655 toggle(wysiwygEditor);
7656
7657 toggleClass(editorContainer, 'wysiwygMode', isInSourceMode);
7658 toggleClass(editorContainer, 'sourceMode', !isInSourceMode);
7659
7660 updateToolBar();
7661 updateActiveButtons();
7662 };
7663
7664 /**
7665 * Gets the selected text of the source editor
7666 * @return {string}
7667 * @private
7668 */
7669 sourceEditorSelectedText = function () {
7670 sourceEditor.focus();
7671
7672 return sourceEditor.value.substring(
7673 sourceEditor.selectionStart,
7674 sourceEditor.selectionEnd
7675 );
7676 };
7677
7678 /**
7679 * Handles the passed command
7680 * @private
7681 */
7682 handleCommand = function (caller, cmd) {
7683 // check if in text mode and handle text commands
7684 if (base.inSourceMode()) {
7685 if (cmd.txtExec) {
7686 if (Array.isArray(cmd.txtExec)) {
7687 base.sourceEditorInsertText.apply(base, cmd.txtExec);
7688 } else {
7689 cmd.txtExec.call(base, caller, sourceEditorSelectedText());
7690 }
7691 }
7692 } else if (cmd.exec) {
7693 if (isFunction(cmd.exec)) {
7694 cmd.exec.call(base, caller);
7695 } else {
7696 base.execCommand(
7697 cmd.exec,
7698 cmd.hasOwnProperty('execParam') ? cmd.execParam : null
7699 );
7700 }
7701 }
7702
7703 };
7704
7705 /**
7706 * Executes a command on the WYSIWYG editor
7707 *
7708 * @param {string} command
7709 * @param {String|Boolean} [param]
7710 * @function
7711 * @name execCommand
7712 * @memberOf SCEditor.prototype
7713 */
7714 base.execCommand = function (command, param) {
7715 var executed = false,
7716 commandObj = base.commands[command];
7717
7718 base.focus();
7719
7720 // TODO: make configurable
7721 // don't apply any commands to code elements
7722 if (closest(rangeHelper.parentNode(), 'code')) {
7723 return;
7724 }
7725
7726 try {
7727 executed = wysiwygDocument.execCommand(command, false, param);
7728 } catch (ex) { }
7729
7730 // show error if execution failed and an error message exists
7731 if (!executed && commandObj && commandObj.errorMessage) {
7732 /*global alert:false*/
7733 alert(base._(commandObj.errorMessage));
7734 }
7735
7736 updateActiveButtons();
7737 };
7738
7739 /**
7740 * Checks if the current selection has changed and triggers
7741 * the selectionchanged event if it has.
7742 *
7743 * In browsers other that don't support selectionchange event it will check
7744 * at most once every 100ms.
7745 * @private
7746 */
7747 checkSelectionChanged = function () {
7748 function check() {
7749 // Don't create new selection if there isn't one (like after
7750 // blur event in iOS)
7751 if (wysiwygWindow.getSelection() &&
7752 wysiwygWindow.getSelection().rangeCount <= 0) {
7753 currentSelection = null;
7754 // rangeHelper could be null if editor was destroyed
7755 // before the timeout had finished
7756 } else if (rangeHelper && !rangeHelper.compare(currentSelection)) {
7757 currentSelection = rangeHelper.cloneSelected();
7758
7759 // If the selection is in an inline wrap it in a block.
7760 // Fixes #331
7761 if (currentSelection && currentSelection.collapsed) {
7762 var parent = currentSelection.startContainer;
7763 var offset = currentSelection.startOffset;
7764
7765 // Handle if selection is placed before/after an element
7766 if (offset && parent.nodeType !== TEXT_NODE) {
7767 parent = parent.childNodes[offset];
7768 }
7769
7770 while (parent && parent.parentNode !== wysiwygBody) {
7771 parent = parent.parentNode;
7772 }
7773
7774 if (parent && isInline(parent, true)) {
7775 rangeHelper.saveRange();
7776 wrapInlines(wysiwygBody, wysiwygDocument);
7777 rangeHelper.restoreRange();
7778 }
7779 }
7780
7781 trigger(editorContainer, 'selectionchanged');
7782 }
7783
7784 isSelectionCheckPending = false;
7785 }
7786
7787 if (isSelectionCheckPending) {
7788 return;
7789 }
7790
7791 isSelectionCheckPending = true;
7792
7793 // Don't need to limit checking if browser supports the Selection API
7794 if ('onselectionchange' in wysiwygDocument) {
7795 check();
7796 } else {
7797 setTimeout(check, 100);
7798 }
7799 };
7800
7801 /**
7802 * Checks if the current node has changed and triggers
7803 * the nodechanged event if it has
7804 * @private
7805 */
7806 checkNodeChanged = function () {
7807 // check if node has changed
7808 var oldNode,
7809 node = rangeHelper.parentNode();
7810
7811 if (currentNode !== node) {
7812 oldNode = currentNode;
7813 currentNode = node;
7814 currentBlockNode = rangeHelper.getFirstBlockParent(node);
7815
7816 trigger(editorContainer, 'nodechanged', {
7817 oldNode: oldNode,
7818 newNode: currentNode
7819 });
7820 }
7821 };
7822
7823 /**
7824 * Gets the current node that contains the selection/caret in
7825 * WYSIWYG mode.
7826 *
7827 * Will be null in sourceMode or if there is no selection.
7828 *
7829 * @return {?Node}
7830 * @function
7831 * @name currentNode
7832 * @memberOf SCEditor.prototype
7833 */
7834 base.currentNode = function () {
7835 return currentNode;
7836 };
7837
7838 /**
7839 * Gets the first block level node that contains the
7840 * selection/caret in WYSIWYG mode.
7841 *
7842 * Will be null in sourceMode or if there is no selection.
7843 *
7844 * @return {?Node}
7845 * @function
7846 * @name currentBlockNode
7847 * @memberOf SCEditor.prototype
7848 * @since 1.4.4
7849 */
7850 base.currentBlockNode = function () {
7851 return currentBlockNode;
7852 };
7853
7854 /**
7855 * Updates if buttons are active or not
7856 * @private
7857 */
7858 updateActiveButtons = function () {
7859 var firstBlock, parent;
7860 var activeClass = 'active';
7861 var doc = wysiwygDocument;
7862 var isSource = base.sourceMode();
7863
7864 if (base.readOnly()) {
7865 each(find(toolbar, activeClass), function (_, menuItem) {
7866 removeClass(menuItem, activeClass);
7867 });
7868 return;
7869 }
7870
7871 if (!isSource) {
7872 parent = rangeHelper.parentNode();
7873 firstBlock = rangeHelper.getFirstBlockParent(parent);
7874 }
7875
7876 for (var j = 0; j < btnStateHandlers.length; j++) {
7877 var state = 0;
7878 var btn = toolbarButtons[btnStateHandlers[j].name];
7879 var stateFn = btnStateHandlers[j].state;
7880 var isDisabled = (isSource && !btn._sceTxtMode) ||
7881 (!isSource && !btn._sceWysiwygMode);
7882
7883 if (isString(stateFn)) {
7884 if (!isSource) {
7885 try {
7886 state = doc.queryCommandEnabled(stateFn) ? 0 : -1;
7887
7888 // eslint-disable-next-line max-depth
7889 if (state > -1) {
7890 state = doc.queryCommandState(stateFn) ? 1 : 0;
7891 }
7892 } catch (ex) {}
7893 }
7894 } else if (!isDisabled) {
7895 state = stateFn.call(base, parent, firstBlock);
7896 }
7897
7898 toggleClass(btn, 'disabled', isDisabled || state < 0);
7899 toggleClass(btn, activeClass, state > 0);
7900 }
7901
7902 if (icons && icons.update) {
7903 icons.update(isSource, parent, firstBlock);
7904 }
7905 };
7906
7907 /**
7908 * Handles any key press in the WYSIWYG editor
7909 *
7910 * @private
7911 */
7912 handleKeyPress = function (e) {
7913 // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496
7914 if (e.defaultPrevented) {
7915 return;
7916 }
7917
7918 base.closeDropDown();
7919
7920 // 13 = enter key
7921 if (e.which === 13) {
7922 var LIST_TAGS = 'li,ul,ol';
7923
7924 // "Fix" (cludge) for blocklevel elements being duplicated in some
7925 // browsers when enter is pressed instead of inserting a newline
7926 if (!is(currentBlockNode, LIST_TAGS) &&
7927 hasStyling(currentBlockNode)) {
7928
7929 var br = createElement('br', {}, wysiwygDocument);
7930 rangeHelper.insertNode(br);
7931
7932 // Last <br> of a block will be collapsed so need to make sure
7933 // the <br> that was inserted isn't the last node of a block.
7934 var parent = br.parentNode;
7935 var lastChild = parent.lastChild;
7936
7937 // Sometimes an empty next node is created after the <br>
7938 if (lastChild && lastChild.nodeType === TEXT_NODE &&
7939 lastChild.nodeValue === '') {
7940 remove(lastChild);
7941 lastChild = parent.lastChild;
7942 }
7943
7944 // If this is the last BR of a block and the previous
7945 // sibling is inline then will need an extra BR. This
7946 // is needed because the last BR of a block will be
7947 // collapsed. Fixes issue #248
7948 if (!isInline(parent, true) && lastChild === br &&
7949 isInline(br.previousSibling)) {
7950 rangeHelper.insertHTML('<br>');
7951 }
7952
7953 e.preventDefault();
7954 }
7955 }
7956 };
7957
7958 /**
7959 * Makes sure that if there is a code or quote tag at the
7960 * end of the editor, that there is a new line after it.
7961 *
7962 * If there wasn't a new line at the end you wouldn't be able
7963 * to enter any text after a code/quote tag
7964 * @return {void}
7965 * @private
7966 */
7967 appendNewLine = function () {
7968 // Check all nodes in reverse until either add a new line
7969 // or reach a non-empty textnode or BR at which point can
7970 // stop checking.
7971 rTraverse(wysiwygBody, function (node) {
7972 // Last block, add new line after if has styling
7973 if (node.nodeType === ELEMENT_NODE &&
7974 !/inline/.test(css(node, 'display'))) {
7975
7976 // Add line break after if has styling
7977 if (!is(node, '.sceditor-nlf') && hasStyling(node)) {
7978 var paragraph = createElement('p', {}, wysiwygDocument);
7979 paragraph.className = 'sceditor-nlf';
7980 paragraph.innerHTML = '<br />';
7981 appendChild(wysiwygBody, paragraph);
7982 return false;
7983 }
7984 }
7985
7986 // Last non-empty text node or line break.
7987 // No need to add line-break after them
7988 if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) ||
7989 is(node, 'br')) {
7990 return false;
7991 }
7992 });
7993 };
7994
7995 /**
7996 * Handles form reset event
7997 * @private
7998 */
7999 handleFormReset = function () {
8000 base.val(original.value);
8001 };
8002
8003 /**
8004 * Handles any mousedown press in the WYSIWYG editor
8005 * @private
8006 */
8007 handleMouseDown = function () {
8008 base.closeDropDown();
8009 };
8010
8011 /**
8012 * Translates the string into the locale language.
8013 *
8014 * Replaces any {0}, {1}, {2}, ect. with the params provided.
8015 *
8016 * @param {string} str
8017 * @param {...String} args
8018 * @return {string}
8019 * @function
8020 * @name _
8021 * @memberOf SCEditor.prototype
8022 */
8023 base._ = function () {
8024 var undef,
8025 args = arguments;
8026
8027 if (locale && locale[args[0]]) {
8028 args[0] = locale[args[0]];
8029 }
8030
8031 return args[0].replace(/\{(\d+)\}/g, function (str, p1) {
8032 return args[p1 - 0 + 1] !== undef ?
8033 args[p1 - 0 + 1] :
8034 '{' + p1 + '}';
8035 });
8036 };
8037
8038 /**
8039 * Passes events on to any handlers
8040 * @private
8041 * @return void
8042 */
8043 handleEvent = function (e) {
8044 if (pluginManager) {
8045 // Send event to all plugins
8046 pluginManager.call(e.type + 'Event', e, base);
8047 }
8048
8049 // convert the event into a custom event to send
8050 var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type;
8051
8052 if (eventHandlers[name]) {
8053 eventHandlers[name].forEach(function (fn) {
8054 fn.call(base, e);
8055 });
8056 }
8057 };
8058
8059 /**
8060 * Binds a handler to the specified events
8061 *
8062 * This function only binds to a limited list of
8063 * supported events.
8064 *
8065 * The supported events are:
8066 *
8067 * * keyup
8068 * * keydown
8069 * * Keypress
8070 * * blur
8071 * * focus
8072 * * input
8073 * * nodechanged - When the current node containing
8074 * the selection changes in WYSIWYG mode
8075 * * contextmenu
8076 * * selectionchanged
8077 * * valuechanged
8078 *
8079 *
8080 * The events param should be a string containing the event(s)
8081 * to bind this handler to. If multiple, they should be separated
8082 * by spaces.
8083 *
8084 * @param {string} events
8085 * @param {Function} handler
8086 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8087 * to the WYSIWYG editor
8088 * @param {boolean} excludeSource if to exclude adding this handler
8089 * to the source editor
8090 * @return {this}
8091 * @function
8092 * @name bind
8093 * @memberOf SCEditor.prototype
8094 * @since 1.4.1
8095 */
8096 base.bind = function (events, handler, excludeWysiwyg, excludeSource) {
8097 events = events.split(' ');
8098
8099 var i = events.length;
8100 while (i--) {
8101 if (isFunction(handler)) {
8102 var wysEvent = 'scewys' + events[i];
8103 var srcEvent = 'scesrc' + events[i];
8104 // Use custom events to allow passing the instance as the
8105 // 2nd argument.
8106 // Also allows unbinding without unbinding the editors own
8107 // event handlers.
8108 if (!excludeWysiwyg) {
8109 eventHandlers[wysEvent] = eventHandlers[wysEvent] || [];
8110 eventHandlers[wysEvent].push(handler);
8111 }
8112
8113 if (!excludeSource) {
8114 eventHandlers[srcEvent] = eventHandlers[srcEvent] || [];
8115 eventHandlers[srcEvent].push(handler);
8116 }
8117
8118 // Start sending value changed events
8119 if (events[i] === 'valuechanged') {
8120 triggerValueChanged.hasHandler = true;
8121 }
8122 }
8123 }
8124
8125 return base;
8126 };
8127
8128 /**
8129 * Unbinds an event that was bound using bind().
8130 *
8131 * @param {string} events
8132 * @param {Function} handler
8133 * @param {boolean} excludeWysiwyg If to exclude unbinding this
8134 * handler from the WYSIWYG editor
8135 * @param {boolean} excludeSource if to exclude unbinding this
8136 * handler from the source editor
8137 * @return {this}
8138 * @function
8139 * @name unbind
8140 * @memberOf SCEditor.prototype
8141 * @since 1.4.1
8142 * @see bind
8143 */
8144 base.unbind = function (events, handler, excludeWysiwyg, excludeSource) {
8145 events = events.split(' ');
8146
8147 var i = events.length;
8148 while (i--) {
8149 if (isFunction(handler)) {
8150 if (!excludeWysiwyg) {
8151 arrayRemove(
8152 eventHandlers['scewys' + events[i]] || [], handler);
8153 }
8154
8155 if (!excludeSource) {
8156 arrayRemove(
8157 eventHandlers['scesrc' + events[i]] || [], handler);
8158 }
8159 }
8160 }
8161
8162 return base;
8163 };
8164
8165 /**
8166 * Blurs the editors input area
8167 *
8168 * @return {this}
8169 * @function
8170 * @name blur
8171 * @memberOf SCEditor.prototype
8172 * @since 1.3.6
8173 */
8174 /**
8175 * Adds a handler to the editors blur event
8176 *
8177 * @param {Function} handler
8178 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8179 * to the WYSIWYG editor
8180 * @param {boolean} excludeSource if to exclude adding this handler
8181 * to the source editor
8182 * @return {this}
8183 * @function
8184 * @name blur^2
8185 * @memberOf SCEditor.prototype
8186 * @since 1.4.1
8187 */
8188 base.blur = function (handler, excludeWysiwyg, excludeSource) {
8189 if (isFunction(handler)) {
8190 base.bind('blur', handler, excludeWysiwyg, excludeSource);
8191 } else if (!base.sourceMode()) {
8192 wysiwygBody.blur();
8193 } else {
8194 sourceEditor.blur();
8195 }
8196
8197 return base;
8198 };
8199
8200 /**
8201 * Focuses the editors input area
8202 *
8203 * @return {this}
8204 * @function
8205 * @name focus
8206 * @memberOf SCEditor.prototype
8207 */
8208 /**
8209 * Adds an event handler to the focus event
8210 *
8211 * @param {Function} handler
8212 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8213 * to the WYSIWYG editor
8214 * @param {boolean} excludeSource if to exclude adding this handler
8215 * to the source editor
8216 * @return {this}
8217 * @function
8218 * @name focus^2
8219 * @memberOf SCEditor.prototype
8220 * @since 1.4.1
8221 */
8222 base.focus = function (handler, excludeWysiwyg, excludeSource) {
8223 if (isFunction(handler)) {
8224 base.bind('focus', handler, excludeWysiwyg, excludeSource);
8225 } else if (!base.inSourceMode()) {
8226 // Already has focus so do nothing
8227 if (find(wysiwygDocument, ':focus').length) {
8228 return;
8229 }
8230
8231 var container;
8232 var rng = rangeHelper.selectedRange();
8233
8234 // Fix FF bug where it shows the cursor in the wrong place
8235 // if the editor hasn't had focus before. See issue #393
8236 if (!currentSelection) {
8237 autofocus(true);
8238 }
8239
8240 // Check if cursor is set after a BR when the BR is the only
8241 // child of the parent. In Firefox this causes a line break
8242 // to occur when something is typed. See issue #321
8243 if (rng && rng.endOffset === 1 && rng.collapsed) {
8244 container = rng.endContainer;
8245
8246 if (container && container.childNodes.length === 1 &&
8247 is(container.firstChild, 'br')) {
8248 rng.setStartBefore(container.firstChild);
8249 rng.collapse(true);
8250 rangeHelper.selectRange(rng);
8251 }
8252 }
8253
8254 wysiwygWindow.focus();
8255 wysiwygBody.focus();
8256 } else {
8257 sourceEditor.focus();
8258 }
8259
8260 updateActiveButtons();
8261
8262 return base;
8263 };
8264
8265 /**
8266 * Adds a handler to the key down event
8267 *
8268 * @param {Function} handler
8269 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8270 * to the WYSIWYG editor
8271 * @param {boolean} excludeSource If to exclude adding this handler
8272 * to the source editor
8273 * @return {this}
8274 * @function
8275 * @name keyDown
8276 * @memberOf SCEditor.prototype
8277 * @since 1.4.1
8278 */
8279 base.keyDown = function (handler, excludeWysiwyg, excludeSource) {
8280 return base.bind('keydown', handler, excludeWysiwyg, excludeSource);
8281 };
8282
8283 /**
8284 * Adds a handler to the key press event
8285 *
8286 * @param {Function} handler
8287 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8288 * to the WYSIWYG editor
8289 * @param {boolean} excludeSource If to exclude adding this handler
8290 * to the source editor
8291 * @return {this}
8292 * @function
8293 * @name keyPress
8294 * @memberOf SCEditor.prototype
8295 * @since 1.4.1
8296 */
8297 base.keyPress = function (handler, excludeWysiwyg, excludeSource) {
8298 return base
8299 .bind('keypress', handler, excludeWysiwyg, excludeSource);
8300 };
8301
8302 /**
8303 * Adds a handler to the key up event
8304 *
8305 * @param {Function} handler
8306 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8307 * to the WYSIWYG editor
8308 * @param {boolean} excludeSource If to exclude adding this handler
8309 * to the source editor
8310 * @return {this}
8311 * @function
8312 * @name keyUp
8313 * @memberOf SCEditor.prototype
8314 * @since 1.4.1
8315 */
8316 base.keyUp = function (handler, excludeWysiwyg, excludeSource) {
8317 return base.bind('keyup', handler, excludeWysiwyg, excludeSource);
8318 };
8319
8320 /**
8321 * Adds a handler to the node changed event.
8322 *
8323 * Happens whenever the node containing the selection/caret
8324 * changes in WYSIWYG mode.
8325 *
8326 * @param {Function} handler
8327 * @return {this}
8328 * @function
8329 * @name nodeChanged
8330 * @memberOf SCEditor.prototype
8331 * @since 1.4.1
8332 */
8333 base.nodeChanged = function (handler) {
8334 return base.bind('nodechanged', handler, false, true);
8335 };
8336
8337 /**
8338 * Adds a handler to the selection changed event
8339 *
8340 * Happens whenever the selection changes in WYSIWYG mode.
8341 *
8342 * @param {Function} handler
8343 * @return {this}
8344 * @function
8345 * @name selectionChanged
8346 * @memberOf SCEditor.prototype
8347 * @since 1.4.1
8348 */
8349 base.selectionChanged = function (handler) {
8350 return base.bind('selectionchanged', handler, false, true);
8351 };
8352
8353 /**
8354 * Adds a handler to the value changed event
8355 *
8356 * Happens whenever the current editor value changes.
8357 *
8358 * Whenever anything is inserted, the value changed or
8359 * 1.5 secs after text is typed. If a space is typed it will
8360 * cause the event to be triggered immediately instead of
8361 * after 1.5 seconds
8362 *
8363 * @param {Function} handler
8364 * @param {boolean} excludeWysiwyg If to exclude adding this handler
8365 * to the WYSIWYG editor
8366 * @param {boolean} excludeSource If to exclude adding this handler
8367 * to the source editor
8368 * @return {this}
8369 * @function
8370 * @name valueChanged
8371 * @memberOf SCEditor.prototype
8372 * @since 1.4.5
8373 */
8374 base.valueChanged = function (handler, excludeWysiwyg, excludeSource) {
8375 return base
8376 .bind('valuechanged', handler, excludeWysiwyg, excludeSource);
8377 };
8378
8379 /**
8380 * Emoticons keypress handler
8381 * @private
8382 */
8383 emoticonsKeyPress = function (e) {
8384 var replacedEmoticon,
8385 cachePos = 0,
8386 emoticonsCache = base.emoticonsCache,
8387 curChar = String.fromCharCode(e.which);
8388
8389 // TODO: Make configurable
8390 if (closest(currentBlockNode, 'code')) {
8391 return;
8392 }
8393
8394 if (!emoticonsCache) {
8395 emoticonsCache = [];
8396
8397 each(allEmoticons, function (key, html) {
8398 emoticonsCache[cachePos++] = [key, html];
8399 });
8400
8401 emoticonsCache.sort(function (a, b) {
8402 return a[0].length - b[0].length;
8403 });
8404
8405 base.emoticonsCache = emoticonsCache;
8406 base.longestEmoticonCode =
8407 emoticonsCache[emoticonsCache.length - 1][0].length;
8408 }
8409
8410 replacedEmoticon = rangeHelper.replaceKeyword(
8411 base.emoticonsCache,
8412 true,
8413 true,
8414 base.longestEmoticonCode,
8415 options.emoticonsCompat,
8416 curChar
8417 );
8418
8419 if (replacedEmoticon) {
8420 if (!options.emoticonsCompat || !/^\s$/.test(curChar)) {
8421 e.preventDefault();
8422 }
8423 }
8424 };
8425
8426 /**
8427 * Makes sure emoticons are surrounded by whitespace
8428 * @private
8429 */
8430 emoticonsCheckWhitespace = function () {
8431 checkWhitespace(currentBlockNode, rangeHelper);
8432 };
8433
8434 /**
8435 * Gets if emoticons are currently enabled
8436 * @return {boolean}
8437 * @function
8438 * @name emoticons
8439 * @memberOf SCEditor.prototype
8440 * @since 1.4.2
8441 */
8442 /**
8443 * Enables/disables emoticons
8444 *
8445 * @param {boolean} enable
8446 * @return {this}
8447 * @function
8448 * @name emoticons^2
8449 * @memberOf SCEditor.prototype
8450 * @since 1.4.2
8451 */
8452 base.emoticons = function (enable) {
8453 if (!enable && enable !== false) {
8454 return options.emoticonsEnabled;
8455 }
8456
8457 options.emoticonsEnabled = enable;
8458
8459 if (enable) {
8460 on(wysiwygBody, 'keypress', emoticonsKeyPress);
8461
8462 if (!base.sourceMode()) {
8463 rangeHelper.saveRange();
8464
8465 replaceEmoticons();
8466 triggerValueChanged(false);
8467
8468 rangeHelper.restoreRange();
8469 }
8470 } else {
8471 var emoticons =
8472 find(wysiwygBody, 'img[data-sceditor-emoticon]');
8473
8474 each(emoticons, function (_, img) {
8475 var text = data(img, 'sceditor-emoticon');
8476 var textNode = wysiwygDocument.createTextNode(text);
8477 img.parentNode.replaceChild(textNode, img);
8478 });
8479
8480 off(wysiwygBody, 'keypress', emoticonsKeyPress);
8481
8482 triggerValueChanged();
8483 }
8484
8485 return base;
8486 };
8487
8488 /**
8489 * Gets the current WYSIWYG editors inline CSS
8490 *
8491 * @return {string}
8492 * @function
8493 * @name css
8494 * @memberOf SCEditor.prototype
8495 * @since 1.4.3
8496 */
8497 /**
8498 * Sets inline CSS for the WYSIWYG editor
8499 *
8500 * @param {string} css
8501 * @return {this}
8502 * @function
8503 * @name css^2
8504 * @memberOf SCEditor.prototype
8505 * @since 1.4.3
8506 */
8507 base.css = function (css) {
8508 if (!inlineCss) {
8509 inlineCss = createElement('style', {
8510 id: 'inline'
8511 }, wysiwygDocument);
8512
8513 appendChild(wysiwygDocument.head, inlineCss);
8514 }
8515
8516 if (!isString(css)) {
8517 return inlineCss.styleSheet ?
8518 inlineCss.styleSheet.cssText : inlineCss.innerHTML;
8519 }
8520
8521 if (inlineCss.styleSheet) {
8522 inlineCss.styleSheet.cssText = css;
8523 } else {
8524 inlineCss.innerHTML = css;
8525 }
8526
8527 return base;
8528 };
8529
8530 /**
8531 * Handles the keydown event, used for shortcuts
8532 * @private
8533 */
8534 handleKeyDown = function (e) {
8535 var shortcut = [],
8536 SHIFT_KEYS = {
8537 '`': '~',
8538 '1': '!',
8539 '2': '@',
8540 '3': '#',
8541 '4': '$',
8542 '5': '%',
8543 '6': '^',
8544 '7': '&',
8545 '8': '*',
8546 '9': '(',
8547 '0': ')',
8548 '-': '_',
8549 '=': '+',
8550 ';': ': ',
8551 '\'': '"',
8552 ',': '<',
8553 '.': '>',
8554 '/': '?',
8555 '\\': '|',
8556 '[': '{',
8557 ']': '}'
8558 },
8559 SPECIAL_KEYS = {
8560 8: 'backspace',
8561 9: 'tab',
8562 13: 'enter',
8563 19: 'pause',
8564 20: 'capslock',
8565 27: 'esc',
8566 32: 'space',
8567 33: 'pageup',
8568 34: 'pagedown',
8569 35: 'end',
8570 36: 'home',
8571 37: 'left',
8572 38: 'up',
8573 39: 'right',
8574 40: 'down',
8575 45: 'insert',
8576 46: 'del',
8577 91: 'win',
8578 92: 'win',
8579 93: 'select',
8580 96: '0',
8581 97: '1',
8582 98: '2',
8583 99: '3',
8584 100: '4',
8585 101: '5',
8586 102: '6',
8587 103: '7',
8588 104: '8',
8589 105: '9',
8590 106: '*',
8591 107: '+',
8592 109: '-',
8593 110: '.',
8594 111: '/',
8595 112: 'f1',
8596 113: 'f2',
8597 114: 'f3',
8598 115: 'f4',
8599 116: 'f5',
8600 117: 'f6',
8601 118: 'f7',
8602 119: 'f8',
8603 120: 'f9',
8604 121: 'f10',
8605 122: 'f11',
8606 123: 'f12',
8607 144: 'numlock',
8608 145: 'scrolllock',
8609 186: ';',
8610 187: '=',
8611 188: ',',
8612 189: '-',
8613 190: '.',
8614 191: '/',
8615 192: '`',
8616 219: '[',
8617 220: '\\',
8618 221: ']',
8619 222: '\''
8620 },
8621 NUMPAD_SHIFT_KEYS = {
8622 109: '-',
8623 110: 'del',
8624 111: '/',
8625 96: '0',
8626 97: '1',
8627 98: '2',
8628 99: '3',
8629 100: '4',
8630 101: '5',
8631 102: '6',
8632 103: '7',
8633 104: '8',
8634 105: '9'
8635 },
8636 which = e.which,
8637 character = SPECIAL_KEYS[which] ||
8638 String.fromCharCode(which).toLowerCase();
8639
8640 if (e.ctrlKey || e.metaKey) {
8641 shortcut.push('ctrl');
8642 }
8643
8644 if (e.altKey) {
8645 shortcut.push('alt');
8646 }
8647
8648 if (e.shiftKey) {
8649 shortcut.push('shift');
8650
8651 if (NUMPAD_SHIFT_KEYS[which]) {
8652 character = NUMPAD_SHIFT_KEYS[which];
8653 } else if (SHIFT_KEYS[character]) {
8654 character = SHIFT_KEYS[character];
8655 }
8656 }
8657
8658 // Shift is 16, ctrl is 17 and alt is 18
8659 if (character && (which < 16 || which > 18)) {
8660 shortcut.push(character);
8661 }
8662
8663 shortcut = shortcut.join('+');
8664 if (shortcutHandlers[shortcut] &&
8665 shortcutHandlers[shortcut].call(base) === false) {
8666
8667 e.stopPropagation();
8668 e.preventDefault();
8669 }
8670 };
8671
8672 /**
8673 * Adds a shortcut handler to the editor
8674 * @param {string} shortcut
8675 * @param {String|Function} cmd
8676 * @return {sceditor}
8677 */
8678 base.addShortcut = function (shortcut, cmd) {
8679 shortcut = shortcut.toLowerCase();
8680
8681 if (isString(cmd)) {
8682 shortcutHandlers[shortcut] = function () {
8683 handleCommand(toolbarButtons[cmd], base.commands[cmd]);
8684
8685 return false;
8686 };
8687 } else {
8688 shortcutHandlers[shortcut] = cmd;
8689 }
8690
8691 return base;
8692 };
8693
8694 /**
8695 * Removes a shortcut handler
8696 * @param {string} shortcut
8697 * @return {sceditor}
8698 */
8699 base.removeShortcut = function (shortcut) {
8700 delete shortcutHandlers[shortcut.toLowerCase()];
8701
8702 return base;
8703 };
8704
8705 /**
8706 * Handles the backspace key press
8707 *
8708 * Will remove block styling like quotes/code ect if at the start.
8709 * @private
8710 */
8711 handleBackSpace = function (e) {
8712 var node, offset, range, parent;
8713
8714 // 8 is the backspace key
8715 if (options.disableBlockRemove || e.which !== 8 ||
8716 !(range = rangeHelper.selectedRange())) {
8717 return;
8718 }
8719
8720 node = range.startContainer;
8721 offset = range.startOffset;
8722
8723 if (offset !== 0 || !(parent = currentStyledBlockNode()) ||
8724 is(parent, 'body')) {
8725 return;
8726 }
8727
8728 while (node !== parent) {
8729 while (node.previousSibling) {
8730 node = node.previousSibling;
8731
8732 // Everything but empty text nodes before the cursor
8733 // should prevent the style from being removed
8734 if (node.nodeType !== TEXT_NODE || node.nodeValue) {
8735 return;
8736 }
8737 }
8738
8739 if (!(node = node.parentNode)) {
8740 return;
8741 }
8742 }
8743
8744 // The backspace was pressed at the start of
8745 // the container so clear the style
8746 base.clearBlockFormatting(parent);
8747 e.preventDefault();
8748 };
8749
8750 /**
8751 * Gets the first styled block node that contains the cursor
8752 * @return {HTMLElement}
8753 */
8754 currentStyledBlockNode = function () {
8755 var block = currentBlockNode;
8756
8757 while (!hasStyling(block) || isInline(block, true)) {
8758 if (!(block = block.parentNode) || is(block, 'body')) {
8759 return;
8760 }
8761 }
8762
8763 return block;
8764 };
8765
8766 /**
8767 * Clears the formatting of the passed block element.
8768 *
8769 * If block is false, if will clear the styling of the first
8770 * block level element that contains the cursor.
8771 * @param {HTMLElement} block
8772 * @since 1.4.4
8773 */
8774 base.clearBlockFormatting = function (block) {
8775 block = block || currentStyledBlockNode();
8776
8777 if (!block || is(block, 'body')) {
8778 return base;
8779 }
8780
8781 rangeHelper.saveRange();
8782
8783 block.className = '';
8784
8785 attr(block, 'style', '');
8786
8787 if (!is(block, 'p,div,td')) {
8788 convertElement(block, 'p');
8789 }
8790
8791 rangeHelper.restoreRange();
8792 return base;
8793 };
8794
8795 /**
8796 * Triggers the valueChanged signal if there is
8797 * a plugin that handles it.
8798 *
8799 * If rangeHelper.saveRange() has already been
8800 * called, then saveRange should be set to false
8801 * to prevent the range being saved twice.
8802 *
8803 * @since 1.4.5
8804 * @param {boolean} saveRange If to call rangeHelper.saveRange().
8805 * @private
8806 */
8807 triggerValueChanged = function (saveRange) {
8808 if (!pluginManager ||
8809 (!pluginManager.hasHandler('valuechangedEvent') &&
8810 !triggerValueChanged.hasHandler)) {
8811 return;
8812 }
8813
8814 var currentHtml,
8815 sourceMode = base.sourceMode(),
8816 hasSelection = !sourceMode && rangeHelper.hasSelection();
8817
8818 // Composition end isn't guaranteed to fire but must have
8819 // ended when triggerValueChanged() is called so reset it
8820 isComposing = false;
8821
8822 // Don't need to save the range if sceditor-start-marker
8823 // is present as the range is already saved
8824 saveRange = saveRange !== false &&
8825 !wysiwygDocument.getElementById('sceditor-start-marker');
8826
8827 // Clear any current timeout as it's now been triggered
8828 if (valueChangedKeyUpTimer) {
8829 clearTimeout(valueChangedKeyUpTimer);
8830 valueChangedKeyUpTimer = false;
8831 }
8832
8833 if (hasSelection && saveRange) {
8834 rangeHelper.saveRange();
8835 }
8836
8837 currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML;
8838
8839 // Only trigger if something has actually changed.
8840 if (currentHtml !== triggerValueChanged.lastVal) {
8841 triggerValueChanged.lastVal = currentHtml;
8842
8843 trigger(editorContainer, 'valuechanged', {
8844 rawValue: sourceMode ? base.val() : currentHtml
8845 });
8846 }
8847
8848 if (hasSelection && saveRange) {
8849 rangeHelper.removeMarkers();
8850 }
8851 };
8852
8853 /**
8854 * Should be called whenever there is a blur event
8855 * @private
8856 */
8857 valueChangedBlur = function () {
8858 if (valueChangedKeyUpTimer) {
8859 triggerValueChanged();
8860 }
8861 };
8862
8863 /**
8864 * Should be called whenever there is a keypress event
8865 * @param {Event} e The keypress event
8866 * @private
8867 */
8868 valueChangedKeyUp = function (e) {
8869 var which = e.which,
8870 lastChar = valueChangedKeyUp.lastChar,
8871 lastWasSpace = (lastChar === 13 || lastChar === 32),
8872 lastWasDelete = (lastChar === 8 || lastChar === 46);
8873
8874 valueChangedKeyUp.lastChar = which;
8875
8876 if (isComposing) {
8877 return;
8878 }
8879
8880 // 13 = return & 32 = space
8881 if (which === 13 || which === 32) {
8882 if (!lastWasSpace) {
8883 triggerValueChanged();
8884 } else {
8885 valueChangedKeyUp.triggerNext = true;
8886 }
8887 // 8 = backspace & 46 = del
8888 } else if (which === 8 || which === 46) {
8889 if (!lastWasDelete) {
8890 triggerValueChanged();
8891 } else {
8892 valueChangedKeyUp.triggerNext = true;
8893 }
8894 } else if (valueChangedKeyUp.triggerNext) {
8895 triggerValueChanged();
8896 valueChangedKeyUp.triggerNext = false;
8897 }
8898
8899 // Clear the previous timeout and set a new one.
8900 clearTimeout(valueChangedKeyUpTimer);
8901
8902 // Trigger the event 1.5s after the last keypress if space
8903 // isn't pressed. This might need to be lowered, will need
8904 // to look into what the slowest average Chars Per Min is.
8905 valueChangedKeyUpTimer = setTimeout(function () {
8906 if (!isComposing) {
8907 triggerValueChanged();
8908 }
8909 }, 1500);
8910 };
8911
8912 handleComposition = function (e) {
8913 isComposing = /start/i.test(e.type);
8914
8915 if (!isComposing) {
8916 triggerValueChanged();
8917 }
8918 };
8919
8920 autoUpdate = function () {
8921 base.updateOriginal();
8922 };
8923
8924 // run the initializer
8925 init();
8926 }
8927
8928 /**
8929 * Map containing the loaded SCEditor locales
8930 * @type {Object}
8931 * @name locale
8932 * @memberOf sceditor
8933 */
8934 SCEditor.locale = {};
8935
8936 SCEditor.formats = {};
8937 SCEditor.icons = {};
8938
8939
8940 /**
8941 * Static command helper class
8942 * @class command
8943 * @name sceditor.command
8944 */
8945 SCEditor.command =
8946 /** @lends sceditor.command */
8947 {
8948 /**
8949 * Gets a command
8950 *
8951 * @param {string} name
8952 * @return {Object|null}
8953 * @since v1.3.5
8954 */
8955 get: function (name) {
8956 return defaultCmds[name] || null;
8957 },
8958
8959 /**
8960 * <p>Adds a command to the editor or updates an existing
8961 * command if a command with the specified name already exists.</p>
8962 *
8963 * <p>Once a command is add it can be included in the toolbar by
8964 * adding it's name to the toolbar option in the constructor. It
8965 * can also be executed manually by calling
8966 * {@link sceditor.execCommand}</p>
8967 *
8968 * @example
8969 * SCEditor.command.set("hello",
8970 * {
8971 * exec: function () {
8972 * alert("Hello World!");
8973 * }
8974 * });
8975 *
8976 * @param {string} name
8977 * @param {Object} cmd
8978 * @return {this|false} Returns false if name or cmd is false
8979 * @since v1.3.5
8980 */
8981 set: function (name, cmd) {
8982 if (!name || !cmd) {
8983 return false;
8984 }
8985
8986 // merge any existing command properties
8987 cmd = extend(defaultCmds[name] || {}, cmd);
8988
8989 cmd.remove = function () {
8990 SCEditor.command.remove(name);
8991 };
8992
8993 defaultCmds[name] = cmd;
8994 return this;
8995 },
8996
8997 /**
8998 * Removes a command
8999 *
9000 * @param {string} name
9001 * @return {this}
9002 * @since v1.3.5
9003 */
9004 remove: function (name) {
9005 if (defaultCmds[name]) {
9006 delete defaultCmds[name];
9007 }
9008
9009 return this;
9010 }
9011 };
9012
9013 /**
9014 * SCEditor
9015 * http://www.sceditor.com/
9016 *
9017 * Copyright (C) 2017, Sam Clarke (samclarke.com)
9018 *
9019 * SCEditor is licensed under the MIT license:
9020 * http://www.opensource.org/licenses/mit-license.php
9021 *
9022 * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor
9023 * @author Sam Clarke
9024 */
9025
9026
9027 window.sceditor = {
9028 command: SCEditor.command,
9029 commands: defaultCmds,
9030 defaultOptions: defaultOptions,
9031
9032 ios: ios,
9033 isWysiwygSupported: isWysiwygSupported,
9034
9035 regexEscape: regex,
9036 escapeEntities: entities,
9037 escapeUriScheme: uriScheme,
9038
9039 dom: {
9040 css: css,
9041 attr: attr,
9042 removeAttr: removeAttr,
9043 is: is,
9044 closest: closest,
9045 width: width,
9046 height: height,
9047 traverse: traverse,
9048 rTraverse: rTraverse,
9049 parseHTML: parseHTML,
9050 hasStyling: hasStyling,
9051 convertElement: convertElement,
9052 blockLevelList: blockLevelList,
9053 canHaveChildren: canHaveChildren,
9054 isInline: isInline,
9055 copyCSS: copyCSS,
9056 fixNesting: fixNesting,
9057 findCommonAncestor: findCommonAncestor,
9058 getSibling: getSibling,
9059 removeWhiteSpace: removeWhiteSpace,
9060 extractContents: extractContents,
9061 getOffset: getOffset,
9062 getStyle: getStyle,
9063 hasStyle: hasStyle
9064 },
9065 locale: SCEditor.locale,
9066 icons: SCEditor.icons,
9067 utils: {
9068 each: each,
9069 isEmptyObject: isEmptyObject,
9070 extend: extend
9071 },
9072 plugins: PluginManager.plugins,
9073 formats: SCEditor.formats,
9074 create: function (textarea, options) {
9075 options = options || {};
9076
9077 // Don't allow the editor to be initialised
9078 // on it's own source editor
9079 if (parent(textarea, '.sceditor-container')) {
9080 return;
9081 }
9082
9083 if (options.runWithoutWysiwygSupport || isWysiwygSupported) {
9084 /*eslint no-new: off*/
9085 (new SCEditor(textarea, options));
9086 }
9087 },
9088 instance: function (textarea) {
9089 return textarea._sceditor;
9090 }
9091 };
9092
9093 /**
9094 * SCEditor
9095 * http://www.sceditor.com/
9096 *
9097 * Copyright (C) 2017, Sam Clarke (samclarke.com)
9098 *
9099 * SCEditor is licensed under the MIT license:
9100 * http://www.opensource.org/licenses/mit-license.php
9101 *
9102 * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor
9103 * @author Sam Clarke
9104 * @requires jQuery
9105 */
9106
9107
9108 // For backwards compatibility
9109 $__default['default'].sceditor = window.sceditor;
9110
9111 /**
9112 * Creates an instance of sceditor on all textareas
9113 * matched by the jQuery selector.
9114 *
9115 * If options is set to "state" it will return bool value
9116 * indicating if the editor has been initialised on the
9117 * matched textarea(s). If there is only one textarea
9118 * it will return the bool value for that textarea.
9119 * If more than one textarea is matched it will
9120 * return an array of bool values for each textarea.
9121 *
9122 * If options is set to "instance" it will return the
9123 * current editor instance for the textarea(s). Like the
9124 * state option, if only one textarea is matched this will
9125 * return just the instance for that textarea. If more than
9126 * one textarea is matched it will return an array of
9127 * instances each textarea.
9128 *
9129 * @param {Object|string} [options] Should either be an Object of options or
9130 * the strings "state" or "instance"
9131 * @return {this|Array<SCEditor>|Array<boolean>|SCEditor|boolean}
9132 */
9133 $__default['default'].fn.sceditor = function (options) {
9134 var instance;
9135 var ret = [];
9136
9137 this.each(function () {
9138 instance = this._sceditor;
9139
9140 // Add state of instance to ret if that is what options is set to
9141 if (options === 'state') {
9142 ret.push(!!instance);
9143 } else if (options === 'instance') {
9144 ret.push(instance);
9145 } else if (!instance) {
9146 $__default['default'].sceditor.create(this, options);
9147 }
9148 });
9149
9150 // If nothing in the ret array then must be init so return this
9151 if (!ret.length) {
9152 return this;
9153 }
9154
9155 return ret.length === 1 ? ret[0] : ret;
9156 };
9157
9158 }(jQuery));
9159 ;/**
9160 * SCEditor BBCode Plugin
9161 * http://www.sceditor.com/
9162 *
9163 * Copyright (C) 2011-2017, Sam Clarke (samclarke.com)
9164 *
9165 * SCEditor is licensed under the MIT license:
9166 * http://www.opensource.org/licenses/mit-license.php
9167 *
9168 * @fileoverview SCEditor BBCode Format
9169 * @author Sam Clarke
9170 */
9171 (function (sceditor) {
9172 /*eslint max-depth: off*/
9173 'use strict';
9174
9175 var escapeEntities = sceditor.escapeEntities;
9176 var escapeUriScheme = sceditor.escapeUriScheme;
9177 var dom = sceditor.dom;
9178 var utils = sceditor.utils;
9179
9180 var css = dom.css;
9181 var attr = dom.attr;
9182 var is = dom.is;
9183 var extend = utils.extend;
9184 var each = utils.each;
9185
9186 var EMOTICON_DATA_ATTR = 'data-sceditor-emoticon';
9187
9188 var getEditorCommand = sceditor.command.get;
9189
9190 var QuoteType = {
9191 /** @lends BBCodeParser.QuoteType */
9192 /**
9193 * Always quote the attribute value
9194 * @type {Number}
9195 */
9196 always: 1,
9197
9198 /**
9199 * Never quote the attributes value
9200 * @type {Number}
9201 */
9202 never: 2,
9203
9204 /**
9205 * Only quote the attributes value when it contains spaces to equals
9206 * @type {Number}
9207 */
9208 auto: 3
9209 };
9210
9211 var defaultCommandsOverrides = {
9212 bold: {
9213 txtExec: ['[b]', '[/b]']
9214 },
9215 italic: {
9216 txtExec: ['[i]', '[/i]']
9217 },
9218 underline: {
9219 txtExec: ['[u]', '[/u]']
9220 },
9221 strike: {
9222 txtExec: ['[s]', '[/s]']
9223 },
9224 subscript: {
9225 txtExec: ['[sub]', '[/sub]']
9226 },
9227 superscript: {
9228 txtExec: ['[sup]', '[/sup]']
9229 },
9230 left: {
9231 txtExec: ['[left]', '[/left]']
9232 },
9233 center: {
9234 txtExec: ['[center]', '[/center]']
9235 },
9236 right: {
9237 txtExec: ['[right]', '[/right]']
9238 },
9239 justify: {
9240 txtExec: ['[justify]', '[/justify]']
9241 },
9242 font: {
9243 txtExec: function (caller) {
9244 var editor = this;
9245
9246 getEditorCommand('font')._dropDown(
9247 editor,
9248 caller,
9249 function (fontName) {
9250 editor.insertText(
9251 '[font=' + fontName + ']',
9252 '[/font]'
9253 );
9254 }
9255 );
9256 }
9257 },
9258 size: {
9259 txtExec: function (caller) {
9260 var editor = this;
9261
9262 getEditorCommand('size')._dropDown(
9263 editor,
9264 caller,
9265 function (fontSize) {
9266 editor.insertText(
9267 '[size=' + fontSize + ']',
9268 '[/size]'
9269 );
9270 }
9271 );
9272 }
9273 },
9274 color: {
9275 txtExec: function (caller) {
9276 var editor = this;
9277
9278 getEditorCommand('color')._dropDown(
9279 editor,
9280 caller,
9281 function (color) {
9282 editor.insertText(
9283 '[color=' + color + ']',
9284 '[/color]'
9285 );
9286 }
9287 );
9288 }
9289 },
9290 bulletlist: {
9291 txtExec: function (caller, selected) {
9292 this.insertText(
9293 '[ul]\n[li]' +
9294 selected.split(/\r?\n/).join('[/li]\n[li]') +
9295 '[/li]\n[/ul]'
9296 );
9297 }
9298 },
9299 orderedlist: {
9300 txtExec: function (caller, selected) {
9301 this.insertText(
9302 '[ol]\n[li]' +
9303 selected.split(/\r?\n/).join('[/li]\n[li]') +
9304 '[/li]\n[/ol]'
9305 );
9306 }
9307 },
9308 table: {
9309 txtExec: ['[table][tr][td]', '[/td][/tr][/table]']
9310 },
9311 horizontalrule: {
9312 txtExec: ['[hr]']
9313 },
9314 code: {
9315 txtExec: ['[code]', '[/code]']
9316 },
9317 image: {
9318 txtExec: function (caller, selected) {
9319 var editor = this;
9320
9321 getEditorCommand('image')._dropDown(
9322 editor,
9323 caller,
9324 selected,
9325 function (url, width, height) {
9326 var attrs = '';
9327
9328 if (width) {
9329 attrs += ' width=' + width;
9330 }
9331
9332 if (height) {
9333 attrs += ' height=' + height;
9334 }
9335
9336 editor.insertText(
9337 '[img' + attrs + ']' + url + '[/img]'
9338 );
9339 }
9340 );
9341 }
9342 },
9343 email: {
9344 txtExec: function (caller, selected) {
9345 var editor = this;
9346
9347 getEditorCommand('email')._dropDown(
9348 editor,
9349 caller,
9350 function (url, text) {
9351 editor.insertText(
9352 '[email=' + url + ']' +
9353 (text || selected || url) +
9354 '[/email]'
9355 );
9356 }
9357 );
9358 }
9359 },
9360 link: {
9361 txtExec: function (caller, selected) {
9362 var editor = this;
9363
9364 getEditorCommand('link')._dropDown(
9365 editor,
9366 caller,
9367 function (url, text) {
9368 editor.insertText(
9369 '[url=' + url + ']' +
9370 (text || selected || url) +
9371 '[/url]'
9372 );
9373 }
9374 );
9375 }
9376 },
9377 quote: {
9378 txtExec: ['[quote]', '[/quote]']
9379 },
9380 youtube: {
9381 txtExec: function (caller) {
9382 var editor = this;
9383
9384 getEditorCommand('youtube')._dropDown(
9385 editor,
9386 caller,
9387 function (id) {
9388 editor.insertText('[youtube]' + id + '[/youtube]');
9389 }
9390 );
9391 }
9392 },
9393 rtl: {
9394 txtExec: ['[rtl]', '[/rtl]']
9395 },
9396 ltr: {
9397 txtExec: ['[ltr]', '[/ltr]']
9398 }
9399 };
9400
9401 var bbcodeHandlers = {
9402 // START_COMMAND: Bold
9403 b: {
9404 tags: {
9405 b: null,
9406 strong: null
9407 },
9408 styles: {
9409 // 401 is for FF 3.5
9410 'font-weight': ['bold', 'bolder', '401', '700', '800', '900']
9411 },
9412 format: '[b]{0}[/b]',
9413 html: '<strong>{0}</strong>'
9414 },
9415 // END_COMMAND
9416
9417 // START_COMMAND: Italic
9418 i: {
9419 tags: {
9420 i: null,
9421 em: null
9422 },
9423 styles: {
9424 'font-style': ['italic', 'oblique']
9425 },
9426 format: '[i]{0}[/i]',
9427 html: '<em>{0}</em>'
9428 },
9429 // END_COMMAND
9430
9431 // START_COMMAND: Underline
9432 u: {
9433 tags: {
9434 u: null
9435 },
9436 styles: {
9437 'text-decoration': ['underline']
9438 },
9439 format: '[u]{0}[/u]',
9440 html: '<u>{0}</u>'
9441 },
9442 // END_COMMAND
9443
9444 // START_COMMAND: Strikethrough
9445 s: {
9446 tags: {
9447 s: null,
9448 strike: null
9449 },
9450 styles: {
9451 'text-decoration': ['line-through']
9452 },
9453 format: '[s]{0}[/s]',
9454 html: '<s>{0}</s>'
9455 },
9456 // END_COMMAND
9457
9458 // START_COMMAND: Subscript
9459 sub: {
9460 tags: {
9461 sub: null
9462 },
9463 format: '[sub]{0}[/sub]',
9464 html: '<sub>{0}</sub>'
9465 },
9466 // END_COMMAND
9467
9468 // START_COMMAND: Superscript
9469 sup: {
9470 tags: {
9471 sup: null
9472 },
9473 format: '[sup]{0}[/sup]',
9474 html: '<sup>{0}</sup>'
9475 },
9476 // END_COMMAND
9477
9478 // START_COMMAND: Font
9479 font: {
9480 tags: {
9481 font: {
9482 face: null
9483 }
9484 },
9485 styles: {
9486 'font-family': null
9487 },
9488 quoteType: QuoteType.never,
9489 format: function (element, content) {
9490 var font;
9491
9492 if (!is(element, 'font') || !(font = attr(element, 'face'))) {
9493 font = css(element, 'font-family');
9494 }
9495
9496 return '[font=' + _stripQuotes(font) + ']' +
9497 content + '[/font]';
9498 },
9499 html: '<font face="{defaultattr}">{0}</font>'
9500 },
9501 // END_COMMAND
9502
9503 // START_COMMAND: Size
9504 size: {
9505 tags: {
9506 font: {
9507 size: null
9508 }
9509 },
9510 styles: {
9511 'font-size': null
9512 },
9513 format: function (element, content) {
9514 var fontSize = attr(element, 'size'),
9515 size = 2;
9516
9517 if (!fontSize) {
9518 fontSize = css(element, 'fontSize');
9519 }
9520
9521 // Most browsers return px value but IE returns 1-7
9522 if (fontSize.indexOf('px') > -1) {
9523 // convert size to an int
9524 fontSize = fontSize.replace('px', '') - 0;
9525
9526 if (fontSize < 12) {
9527 size = 1;
9528 }
9529 if (fontSize > 15) {
9530 size = 3;
9531 }
9532 if (fontSize > 17) {
9533 size = 4;
9534 }
9535 if (fontSize > 23) {
9536 size = 5;
9537 }
9538 if (fontSize > 31) {
9539 size = 6;
9540 }
9541 if (fontSize > 47) {
9542 size = 7;
9543 }
9544 } else {
9545 size = fontSize;
9546 }
9547
9548 return '[size=' + size + ']' + content + '[/size]';
9549 },
9550 html: '<font size="{defaultattr}">{!0}</font>'
9551 },
9552 // END_COMMAND
9553
9554 // START_COMMAND: Color
9555 color: {
9556 tags: {
9557 font: {
9558 color: null
9559 }
9560 },
9561 styles: {
9562 color: null
9563 },
9564 quoteType: QuoteType.never,
9565 format: function (elm, content) {
9566 var color;
9567
9568 if (!is(elm, 'font') || !(color = attr(elm, 'color'))) {
9569 color = elm.style.color || css(elm, 'color');
9570 }
9571
9572 return '[color=' + _normaliseColour(color) + ']' +
9573 content + '[/color]';
9574 },
9575 html: function (token, attrs, content) {
9576 return '<font color="' +
9577 escapeEntities(_normaliseColour(attrs.defaultattr), true) +
9578 '">' + content + '</font>';
9579 }
9580 },
9581 // END_COMMAND
9582
9583 // START_COMMAND: Lists
9584 ul: {
9585 tags: {
9586 ul: null
9587 },
9588 breakStart: true,
9589 isInline: false,
9590 skipLastLineBreak: true,
9591 format: '[ul]{0}[/ul]',
9592 html: '<ul>{0}</ul>'
9593 },
9594 list: {
9595 breakStart: true,
9596 isInline: false,
9597 skipLastLineBreak: true,
9598 html: '<ul>{0}</ul>'
9599 },
9600 ol: {
9601 tags: {
9602 ol: null
9603 },
9604 breakStart: true,
9605 isInline: false,
9606 skipLastLineBreak: true,
9607 format: '[ol]{0}[/ol]',
9608 html: '<ol>{0}</ol>'
9609 },
9610 li: {
9611 tags: {
9612 li: null
9613 },
9614 isInline: false,
9615 closedBy: ['/ul', '/ol', '/list', '*', 'li'],
9616 format: '[li]{0}[/li]',
9617 html: '<li>{0}</li>'
9618 },
9619 '*': {
9620 isInline: false,
9621 closedBy: ['/ul', '/ol', '/list', '*', 'li'],
9622 html: '<li>{0}</li>'
9623 },
9624 // END_COMMAND
9625
9626 // START_COMMAND: Table
9627 table: {
9628 tags: {
9629 table: null
9630 },
9631 isInline: false,
9632 isHtmlInline: true,
9633 skipLastLineBreak: true,
9634 format: '[table]{0}[/table]',
9635 html: '<table>{0}</table>'
9636 },
9637 tr: {
9638 tags: {
9639 tr: null
9640 },
9641 isInline: false,
9642 skipLastLineBreak: true,
9643 format: '[tr]{0}[/tr]',
9644 html: '<tr>{0}</tr>'
9645 },
9646 th: {
9647 tags: {
9648 th: null
9649 },
9650 allowsEmpty: true,
9651 isInline: false,
9652 format: '[th]{0}[/th]',
9653 html: '<th>{0}</th>'
9654 },
9655 td: {
9656 tags: {
9657 td: null
9658 },
9659 allowsEmpty: true,
9660 isInline: false,
9661 format: '[td]{0}[/td]',
9662 html: '<td>{0}</td>'
9663 },
9664 // END_COMMAND
9665
9666 // START_COMMAND: Emoticons
9667 emoticon: {
9668 allowsEmpty: true,
9669 tags: {
9670 img: {
9671 src: null,
9672 'data-sceditor-emoticon': null
9673 }
9674 },
9675 format: function (element, content) {
9676 return attr(element, EMOTICON_DATA_ATTR) + content;
9677 },
9678 html: '{0}'
9679 },
9680 // END_COMMAND
9681
9682 // START_COMMAND: Horizontal Rule
9683 hr: {
9684 tags: {
9685 hr: null
9686 },
9687 allowsEmpty: true,
9688 isSelfClosing: true,
9689 isInline: false,
9690 format: '[hr]{0}',
9691 html: '<hr />'
9692 },
9693 // END_COMMAND
9694
9695 // START_COMMAND: Image
9696 img: {
9697 allowsEmpty: true,
9698 tags: {
9699 img: {
9700 src: null
9701 }
9702 },
9703 allowedChildren: ['#'],
9704 quoteType: QuoteType.never,
9705 format: function (element, content) {
9706 var width, height,
9707 attribs = '',
9708 style = function (name) {
9709 return element.style ? element.style[name] : null;
9710 };
9711
9712 // check if this is an emoticon image
9713 if (attr(element, EMOTICON_DATA_ATTR)) {
9714 return content;
9715 }
9716
9717 width = attr(element, 'width') || style('width');
9718 height = attr(element, 'height') || style('height');
9719
9720 // only add width and height if one is specified
9721 if ((element.complete && (width || height)) ||
9722 (width && height)) {
9723
9724 attribs = '=' + dom.width(element) + 'x' +
9725 dom.height(element);
9726 }
9727
9728 return '[img' + attribs + ']' + attr(element, 'src') + '[/img]';
9729 },
9730 html: function (token, attrs, content) {
9731 var undef, width, height, match,
9732 attribs = '';
9733
9734 // handle [img width=340 height=240]url[/img]
9735 width = attrs.width;
9736 height = attrs.height;
9737
9738 // handle [img=340x240]url[/img]
9739 if (attrs.defaultattr) {
9740 match = attrs.defaultattr.split(/x/i);
9741
9742 width = match[0];
9743 height = (match.length === 2 ? match[1] : match[0]);
9744 }
9745
9746 if (width !== undef) {
9747 attribs += ' width="' + escapeEntities(width, true) + '"';
9748 }
9749
9750 if (height !== undef) {
9751 attribs += ' height="' + escapeEntities(height, true) + '"';
9752 }
9753
9754 return '<img' + attribs +
9755 ' src="' + escapeUriScheme(content) + '" />';
9756 }
9757 },
9758 // END_COMMAND
9759
9760 // START_COMMAND: URL
9761 url: {
9762 allowsEmpty: true,
9763 tags: {
9764 a: {
9765 href: null
9766 }
9767 },
9768 quoteType: QuoteType.never,
9769 format: function (element, content) {
9770 var url = attr(element, 'href');
9771
9772 // make sure this link is not an e-mail,
9773 // if it is return e-mail BBCode
9774 if (url.substr(0, 7) === 'mailto:') {
9775 return '[email="' + url.substr(7) + '"]' +
9776 content + '[/email]';
9777 }
9778
9779 return '[url=' + url + ']' + content + '[/url]';
9780 },
9781 html: function (token, attrs, content) {
9782 attrs.defaultattr =
9783 escapeEntities(attrs.defaultattr, true) || content;
9784
9785 return '<a href="' + escapeUriScheme(attrs.defaultattr) + '">' +
9786 content + '</a>';
9787 }
9788 },
9789 // END_COMMAND
9790
9791 // START_COMMAND: E-mail
9792 email: {
9793 quoteType: QuoteType.never,
9794 html: function (token, attrs, content) {
9795 return '<a href="mailto:' +
9796 (escapeEntities(attrs.defaultattr, true) || content) +
9797 '">' + content + '</a>';
9798 }
9799 },
9800 // END_COMMAND
9801
9802 // START_COMMAND: Quote
9803 quote: {
9804 tags: {
9805 blockquote: null
9806 },
9807 isInline: false,
9808 quoteType: QuoteType.never,
9809 format: function (element, content) {
9810 var authorAttr = 'data-author';
9811 var author = '';
9812 var cite;
9813 var children = element.children;
9814
9815 for (var i = 0; !cite && i < children.length; i++) {
9816 if (is(children[i], 'cite')) {
9817 cite = children[i];
9818 }
9819 }
9820
9821 if (cite || attr(element, authorAttr)) {
9822 author = cite && cite.textContent ||
9823 attr(element, authorAttr);
9824
9825 attr(element, authorAttr, author);
9826
9827 if (cite) {
9828 element.removeChild(cite);
9829 }
9830
9831 content = this.elementToBbcode(element);
9832 author = '=' + author.replace(/(^\s+|\s+$)/g, '');
9833
9834 if (cite) {
9835 element.insertBefore(cite, element.firstChild);
9836 }
9837 }
9838
9839 return '[quote' + author + ']' + content + '[/quote]';
9840 },
9841 html: function (token, attrs, content) {
9842 if (attrs.defaultattr) {
9843 content = '<cite>' + escapeEntities(attrs.defaultattr) +
9844 '</cite>' + content;
9845 }
9846
9847 return '<blockquote>' + content + '</blockquote>';
9848 }
9849 },
9850 // END_COMMAND
9851
9852 // START_COMMAND: Code
9853 code: {
9854 tags: {
9855 code: null
9856 },
9857 isInline: false,
9858 allowedChildren: ['#', '#newline'],
9859 format: '[code]{0}[/code]',
9860 html: '<code>{0}</code>'
9861 },
9862 // END_COMMAND
9863
9864
9865 // START_COMMAND: Left
9866 left: {
9867 styles: {
9868 'text-align': [
9869 'left',
9870 '-webkit-left',
9871 '-moz-left',
9872 '-khtml-left'
9873 ]
9874 },
9875 isInline: false,
9876 allowsEmpty: true,
9877 format: '[left]{0}[/left]',
9878 html: '<div align="left">{0}</div>'
9879 },
9880 // END_COMMAND
9881
9882 // START_COMMAND: Centre
9883 center: {
9884 styles: {
9885 'text-align': [
9886 'center',
9887 '-webkit-center',
9888 '-moz-center',
9889 '-khtml-center'
9890 ]
9891 },
9892 isInline: false,
9893 allowsEmpty: true,
9894 format: '[center]{0}[/center]',
9895 html: '<div align="center">{0}</div>'
9896 },
9897 // END_COMMAND
9898
9899 // START_COMMAND: Right
9900 right: {
9901 styles: {
9902 'text-align': [
9903 'right',
9904 '-webkit-right',
9905 '-moz-right',
9906 '-khtml-right'
9907 ]
9908 },
9909 isInline: false,
9910 allowsEmpty: true,
9911 format: '[right]{0}[/right]',
9912 html: '<div align="right">{0}</div>'
9913 },
9914 // END_COMMAND
9915
9916 // START_COMMAND: Justify
9917 justify: {
9918 styles: {
9919 'text-align': [
9920 'justify',
9921 '-webkit-justify',
9922 '-moz-justify',
9923 '-khtml-justify'
9924 ]
9925 },
9926 isInline: false,
9927 allowsEmpty: true,
9928 format: '[justify]{0}[/justify]',
9929 html: '<div align="justify">{0}</div>'
9930 },
9931 // END_COMMAND
9932
9933 // START_COMMAND: YouTube
9934 youtube: {
9935 allowsEmpty: true,
9936 tags: {
9937 iframe: {
9938 'data-youtube-id': null
9939 }
9940 },
9941 format: function (element, content) {
9942 element = attr(element, 'data-youtube-id');
9943
9944 return element ? '[youtube]' + element + '[/youtube]' : content;
9945 },
9946 html: '<iframe width="560" height="315" frameborder="0" ' +
9947 'src="https://www.youtube-nocookie.com/embed/{0}?wmode=opaque" ' +
9948 'data-youtube-id="{0}" allowfullscreen></iframe>'
9949 },
9950 // END_COMMAND
9951
9952
9953 // START_COMMAND: Rtl
9954 rtl: {
9955 styles: {
9956 direction: ['rtl']
9957 },
9958 isInline: false,
9959 format: '[rtl]{0}[/rtl]',
9960 html: '<div style="direction: rtl">{0}</div>'
9961 },
9962 // END_COMMAND
9963
9964 // START_COMMAND: Ltr
9965 ltr: {
9966 styles: {
9967 direction: ['ltr']
9968 },
9969 isInline: false,
9970 format: '[ltr]{0}[/ltr]',
9971 html: '<div style="direction: ltr">{0}</div>'
9972 },
9973 // END_COMMAND
9974
9975 // this is here so that commands above can be removed
9976 // without having to remove the , after the last one.
9977 // Needed for IE.
9978 ignore: {}
9979 };
9980
9981 /**
9982 * Formats a string replacing {name} with the values of
9983 * obj.name properties.
9984 *
9985 * If there is no property for the specified {name} then
9986 * it will be left intact.
9987 *
9988 * @param {string} str
9989 * @param {Object} obj
9990 * @return {string}
9991 * @since 2.0.0
9992 */
9993 function formatBBCodeString(str, obj) {
9994 return str.replace(/\{([^}]+)\}/g, function (match, group) {
9995 var undef,
9996 escape = true;
9997
9998 if (group.charAt(0) === '!') {
9999 escape = false;
10000 group = group.substring(1);
10001 }
10002
10003 if (group === '0') {
10004 escape = false;
10005 }
10006
10007 if (obj[group] === undef) {
10008 return match;
10009 }
10010
10011 return escape ? escapeEntities(obj[group], true) : obj[group];
10012 });
10013 }
10014
10015 /**
10016 * Removes the first and last divs from the HTML.
10017 *
10018 * This is needed for pasting
10019 * @param {string} html
10020 * @return {string}
10021 * @private
10022 */
10023 function removeFirstLastDiv(html) {
10024 var node, next, removeDiv,
10025 output = document.createElement('div');
10026
10027 removeDiv = function (node, isFirst) {
10028 // Don't remove divs that have styling
10029 if (dom.hasStyling(node)) {
10030 return;
10031 }
10032
10033 if ((node.childNodes.length !== 1 ||
10034 !is(node.firstChild, 'br'))) {
10035 while ((next = node.firstChild)) {
10036 output.insertBefore(next, node);
10037 }
10038 }
10039
10040 if (isFirst) {
10041 var lastChild = output.lastChild;
10042
10043 if (node !== lastChild && is(lastChild, 'div') &&
10044 node.nextSibling === lastChild) {
10045 output.insertBefore(document.createElement('br'), node);
10046 }
10047 }
10048
10049 output.removeChild(node);
10050 };
10051
10052 css(output, 'display', 'none');
10053 output.innerHTML = html.replace(/<\/div>\n/g, '</div>');
10054
10055 if ((node = output.firstChild) && is(node, 'div')) {
10056 removeDiv(node, true);
10057 }
10058
10059 if ((node = output.lastChild) && is(node, 'div')) {
10060 removeDiv(node);
10061 }
10062
10063 return output.innerHTML;
10064 }
10065
10066 function isFunction(fn) {
10067 return typeof fn === 'function';
10068 }
10069
10070 /**
10071 * Removes any leading or trailing quotes ('")
10072 *
10073 * @return string
10074 * @since v1.4.0
10075 */
10076 function _stripQuotes(str) {
10077 return str ?
10078 str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str;
10079 }
10080
10081 /**
10082 * Formats a string replacing {0}, {1}, {2}, ect. with
10083 * the params provided
10084 *
10085 * @param {string} str The string to format
10086 * @param {...string} arg The strings to replace
10087 * @return {string}
10088 * @since v1.4.0
10089 */
10090 function _formatString(str) {
10091 var undef;
10092 var args = arguments;
10093
10094 return str.replace(/\{(\d+)\}/g, function (_, matchNum) {
10095 return args[matchNum - 0 + 1] !== undef ?
10096 args[matchNum - 0 + 1] :
10097 '{' + matchNum + '}';
10098 });
10099 }
10100
10101 var TOKEN_OPEN = 'open';
10102 var TOKEN_CONTENT = 'content';
10103 var TOKEN_NEWLINE = 'newline';
10104 var TOKEN_CLOSE = 'close';
10105
10106
10107 /*
10108 * @typedef {Object} TokenizeToken
10109 * @property {string} type
10110 * @property {string} name
10111 * @property {string} val
10112 * @property {Object.<string, string>} attrs
10113 * @property {array} children
10114 * @property {TokenizeToken} closing
10115 */
10116
10117 /**
10118 * Tokenize token object
10119 *
10120 * @param {string} type The type of token this is,
10121 * should be one of tokenType
10122 * @param {string} name The name of this token
10123 * @param {string} val The originally matched string
10124 * @param {array} attrs Any attributes. Only set on
10125 * TOKEN_TYPE_OPEN tokens
10126 * @param {array} children Any children of this token
10127 * @param {TokenizeToken} closing This tokens closing tag.
10128 * Only set on TOKEN_TYPE_OPEN tokens
10129 * @class {TokenizeToken}
10130 * @name {TokenizeToken}
10131 * @memberOf BBCodeParser.prototype
10132 */
10133 // eslint-disable-next-line max-params
10134 function TokenizeToken(type, name, val, attrs, children, closing) {
10135 var base = this;
10136
10137 base.type = type;
10138 base.name = name;
10139 base.val = val;
10140 base.attrs = attrs || {};
10141 base.children = children || [];
10142 base.closing = closing || null;
10143 };
10144
10145 TokenizeToken.prototype = {
10146 /** @lends BBCodeParser.prototype.TokenizeToken */
10147 /**
10148 * Clones this token
10149 *
10150 * @return {TokenizeToken}
10151 */
10152 clone: function () {
10153 var base = this;
10154
10155 return new TokenizeToken(
10156 base.type,
10157 base.name,
10158 base.val,
10159 extend({}, base.attrs),
10160 [],
10161 base.closing ? base.closing.clone() : null
10162 );
10163 },
10164 /**
10165 * Splits this token at the specified child
10166 *
10167 * @param {TokenizeToken} splitAt The child to split at
10168 * @return {TokenizeToken} The right half of the split token or
10169 * empty clone if invalid splitAt lcoation
10170 */
10171 splitAt: function (splitAt) {
10172 var offsetLength;
10173 var base = this;
10174 var clone = base.clone();
10175 var offset = base.children.indexOf(splitAt);
10176
10177 if (offset > -1) {
10178 // Work out how many items are on the right side of the split
10179 // to pass to splice()
10180 offsetLength = base.children.length - offset;
10181 clone.children = base.children.splice(offset, offsetLength);
10182 }
10183
10184 return clone;
10185 }
10186 };
10187
10188
10189 /**
10190 * SCEditor BBCode parser class
10191 *
10192 * @param {Object} options
10193 * @class BBCodeParser
10194 * @name BBCodeParser
10195 * @since v1.4.0
10196 */
10197 function BBCodeParser(options) {
10198 var base = this;
10199
10200 base.opts = extend({}, BBCodeParser.defaults, options);
10201
10202 /**
10203 * Takes a BBCode string and splits it into open,
10204 * content and close tags.
10205 *
10206 * It does no checking to verify a tag has a matching open
10207 * or closing tag or if the tag is valid child of any tag
10208 * before it. For that the tokens should be passed to the
10209 * parse function.
10210 *
10211 * @param {string} str
10212 * @return {array}
10213 * @memberOf BBCodeParser.prototype
10214 */
10215 base.tokenize = function (str) {
10216 var matches, type, i;
10217 var tokens = [];
10218 // The token types in reverse order of precedence
10219 // (they're looped in reverse)
10220 var tokenTypes = [
10221 {
10222 type: TOKEN_CONTENT,
10223 regex: /^([^\[\r\n]+|\[)/
10224 },
10225 {
10226 type: TOKEN_NEWLINE,
10227 regex: /^(\r\n|\r|\n)/
10228 },
10229 {
10230 type: TOKEN_OPEN,
10231 regex: /^\[[^\[\]]+\]/
10232 },
10233 // Close must come before open as they are
10234 // the same except close has a / at the start.
10235 {
10236 type: TOKEN_CLOSE,
10237 regex: /^\[\/[^\[\]]+\]/
10238 }
10239 ];
10240
10241 strloop:
10242 while (str.length) {
10243 i = tokenTypes.length;
10244 while (i--) {
10245 type = tokenTypes[i].type;
10246
10247 // Check if the string matches any of the tokens
10248 if (!(matches = str.match(tokenTypes[i].regex)) ||
10249 !matches[0]) {
10250 continue;
10251 }
10252
10253 // Add the match to the tokens list
10254 tokens.push(tokenizeTag(type, matches[0]));
10255
10256 // Remove the match from the string
10257 str = str.substr(matches[0].length);
10258
10259 // The token has been added so start again
10260 continue strloop;
10261 }
10262
10263 // If there is anything left in the string which doesn't match
10264 // any of the tokens then just assume it's content and add it.
10265 if (str.length) {
10266 tokens.push(tokenizeTag(TOKEN_CONTENT, str));
10267 }
10268
10269 str = '';
10270 }
10271
10272 return tokens;
10273 };
10274
10275 /**
10276 * Extracts the name an params from a tag
10277 *
10278 * @param {string} type
10279 * @param {string} val
10280 * @return {Object}
10281 * @private
10282 */
10283 function tokenizeTag(type, val) {
10284 var matches, attrs, name,
10285 openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/,
10286 closeRegex = /\[\/([^\[\]]+)\]/;
10287
10288 // Extract the name and attributes from opening tags and
10289 // just the name from closing tags.
10290 if (type === TOKEN_OPEN && (matches = val.match(openRegex))) {
10291 name = lower(matches[1]);
10292
10293 if (matches[2] && (matches[2] = matches[2].trim())) {
10294 attrs = tokenizeAttrs(matches[2]);
10295 }
10296 }
10297
10298 if (type === TOKEN_CLOSE &&
10299 (matches = val.match(closeRegex))) {
10300 name = lower(matches[1]);
10301 }
10302
10303 if (type === TOKEN_NEWLINE) {
10304 name = '#newline';
10305 }
10306
10307 // Treat all tokens without a name and
10308 // all unknown BBCodes as content
10309 if (!name || ((type === TOKEN_OPEN || type === TOKEN_CLOSE) &&
10310 !bbcodeHandlers[name])) {
10311
10312 type = TOKEN_CONTENT;
10313 name = '#';
10314 }
10315
10316 return new TokenizeToken(type, name, val, attrs);
10317 }
10318
10319 /**
10320 * Extracts the individual attributes from a string containing
10321 * all the attributes.
10322 *
10323 * @param {string} attrs
10324 * @return {Object} Assoc array of attributes
10325 * @private
10326 */
10327 function tokenizeAttrs(attrs) {
10328 var matches,
10329 /*
10330 ([^\s=]+) Anything that's not a space or equals
10331 = Equals sign =
10332 (?:
10333 (?:
10334 (["']) The opening quote
10335 (
10336 (?:\\\2|[^\2])*? Anything that isn't the
10337 unescaped opening quote
10338 )
10339 \2 The opening quote again which
10340 will close the string
10341 )
10342 | If not a quoted string then match
10343 (
10344 (?:.(?!\s\S+=))*.? Anything that isn't part of
10345 [space][non-space][=] which
10346 would be a new attribute
10347 )
10348 )
10349 */
10350 attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g,
10351 ret = {};
10352
10353 // if only one attribute then remove the = from the start and
10354 // strip any quotes
10355 if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) {
10356 ret.defaultattr = _stripQuotes(attrs.substr(1));
10357 } else {
10358 if (attrs.charAt(0) === '=') {
10359 attrs = 'defaultattr' + attrs;
10360 }
10361
10362 // No need to strip quotes here, the regex will do that.
10363 while ((matches = attrRegex.exec(attrs))) {
10364 ret[lower(matches[1])] =
10365 _stripQuotes(matches[3]) || matches[4];
10366 }
10367 }
10368
10369 return ret;
10370 }
10371
10372 /**
10373 * Parses a string into an array of BBCodes
10374 *
10375 * @param {string} str
10376 * @param {boolean} preserveNewLines If to preserve all new lines, not
10377 * strip any based on the passed
10378 * formatting options
10379 * @return {array} Array of BBCode objects
10380 * @memberOf BBCodeParser.prototype
10381 */
10382 base.parse = function (str, preserveNewLines) {
10383 var ret = parseTokens(base.tokenize(str));
10384 var opts = base.opts;
10385
10386 if (opts.fixInvalidNesting) {
10387 fixNesting(ret);
10388 }
10389
10390 normaliseNewLines(ret, null, preserveNewLines);
10391
10392 if (opts.removeEmptyTags) {
10393 removeEmpty(ret);
10394 }
10395
10396 return ret;
10397 };
10398
10399 /**
10400 * Checks if an array of TokenizeToken's contains the
10401 * specified token.
10402 *
10403 * Checks the tokens name and type match another tokens
10404 * name and type in the array.
10405 *
10406 * @param {string} name
10407 * @param {string} type
10408 * @param {array} arr
10409 * @return {Boolean}
10410 * @private
10411 */
10412 function hasTag(name, type, arr) {
10413 var i = arr.length;
10414
10415 while (i--) {
10416 if (arr[i].type === type && arr[i].name === name) {
10417 return true;
10418 }
10419 }
10420
10421 return false;
10422 }
10423
10424 /**
10425 * Checks if the child tag is allowed as one
10426 * of the parent tags children.
10427 *
10428 * @param {TokenizeToken} parent
10429 * @param {TokenizeToken} child
10430 * @return {Boolean}
10431 * @private
10432 */
10433 function isChildAllowed(parent, child) {
10434 var parentBBCode = parent ? bbcodeHandlers[parent.name] : {},
10435 allowedChildren = parentBBCode.allowedChildren;
10436
10437 if (base.opts.fixInvalidChildren && allowedChildren) {
10438 return allowedChildren.indexOf(child.name || '#') > -1;
10439 }
10440
10441 return true;
10442 }
10443
10444 // TODO: Tidy this parseTokens() function up a bit.
10445 /**
10446 * Parses an array of tokens created by tokenize()
10447 *
10448 * @param {array} toks
10449 * @return {array} Parsed tokens
10450 * @see tokenize()
10451 * @private
10452 */
10453 function parseTokens(toks) {
10454 var token, bbcode, curTok, clone, i, next,
10455 cloned = [],
10456 output = [],
10457 openTags = [],
10458 /**
10459 * Returns the currently open tag or undefined
10460 * @return {TokenizeToken}
10461 */
10462 currentTag = function () {
10463 return last(openTags);
10464 },
10465 /**
10466 * Adds a tag to either the current tags children
10467 * or to the output array.
10468 * @param {TokenizeToken} token
10469 * @private
10470 */
10471 addTag = function (token) {
10472 if (currentTag()) {
10473 currentTag().children.push(token);
10474 } else {
10475 output.push(token);
10476 }
10477 },
10478 /**
10479 * Checks if this tag closes the current tag
10480 * @param {string} name
10481 * @return {Void}
10482 */
10483 closesCurrentTag = function (name) {
10484 return currentTag() &&
10485 (bbcode = bbcodeHandlers[currentTag().name]) &&
10486 bbcode.closedBy &&
10487 bbcode.closedBy.indexOf(name) > -1;
10488 };
10489
10490 while ((token = toks.shift())) {
10491 next = toks[0];
10492
10493 /*
10494 * Fixes any invalid children.
10495 *
10496 * If it is an element which isn't allowed as a child of it's
10497 * parent then it will be converted to content of the parent
10498 * element. i.e.
10499 * [code]Code [b]only[/b] allows text.[/code]
10500 * Will become:
10501 * <code>Code [b]only[/b] allows text.</code>
10502 * Instead of:
10503 * <code>Code <b>only</b> allows text.</code>
10504 */
10505 // Ignore tags that can't be children
10506 if (!isChildAllowed(currentTag(), token)) {
10507
10508 // exclude closing tags of current tag
10509 if (token.type !== TOKEN_CLOSE || !currentTag() ||
10510 token.name !== currentTag().name) {
10511 token.name = '#';
10512 token.type = TOKEN_CONTENT;
10513 }
10514 }
10515
10516 switch (token.type) {
10517 case TOKEN_OPEN:
10518 // Check it this closes a parent,
10519 // e.g. for lists [*]one [*]two
10520 if (closesCurrentTag(token.name)) {
10521 openTags.pop();
10522 }
10523
10524 addTag(token);
10525 bbcode = bbcodeHandlers[token.name];
10526
10527 // If this tag is not self closing and it has a closing
10528 // tag then it is open and has children so add it to the
10529 // list of open tags. If has the closedBy property then
10530 // it is closed by other tags so include everything as
10531 // it's children until one of those tags is reached.
10532 if (bbcode && !bbcode.isSelfClosing &&
10533 (bbcode.closedBy ||
10534 hasTag(token.name, TOKEN_CLOSE, toks))) {
10535 openTags.push(token);
10536 } else if (!bbcode || !bbcode.isSelfClosing) {
10537 token.type = TOKEN_CONTENT;
10538 }
10539 break;
10540
10541 case TOKEN_CLOSE:
10542 // check if this closes the current tag,
10543 // e.g. [/list] would close an open [*]
10544 if (currentTag() && token.name !== currentTag().name &&
10545 closesCurrentTag('/' + token.name)) {
10546
10547 openTags.pop();
10548 }
10549
10550 // If this is closing the currently open tag just pop
10551 // the close tag off the open tags array
10552 if (currentTag() && token.name === currentTag().name) {
10553 currentTag().closing = token;
10554 openTags.pop();
10555
10556 // If this is closing an open tag that is the parent of
10557 // the current tag then clone all the tags including the
10558 // current one until reaching the parent that is being
10559 // closed. Close the parent and then add the clones back
10560 // in.
10561 } else if (hasTag(token.name, TOKEN_OPEN, openTags)) {
10562
10563 // Remove the tag from the open tags
10564 while ((curTok = openTags.pop())) {
10565
10566 // If it's the tag that is being closed then
10567 // discard it and break the loop.
10568 if (curTok.name === token.name) {
10569 curTok.closing = token;
10570 break;
10571 }
10572
10573 // Otherwise clone this tag and then add any
10574 // previously cloned tags as it's children
10575 clone = curTok.clone();
10576
10577 if (cloned.length) {
10578 clone.children.push(last(cloned));
10579 }
10580
10581 cloned.push(clone);
10582 }
10583
10584 // Place block linebreak before cloned tags
10585 if (next && next.type === TOKEN_NEWLINE) {
10586 bbcode = bbcodeHandlers[token.name];
10587 if (bbcode && bbcode.isInline === false) {
10588 addTag(next);
10589 toks.shift();
10590 }
10591 }
10592
10593 // Add the last cloned child to the now current tag
10594 // (the parent of the tag which was being closed)
10595 addTag(last(cloned));
10596
10597 // Add all the cloned tags to the open tags list
10598 i = cloned.length;
10599 while (i--) {
10600 openTags.push(cloned[i]);
10601 }
10602
10603 cloned.length = 0;
10604
10605 // This tag is closing nothing so treat it as content
10606 } else {
10607 token.type = TOKEN_CONTENT;
10608 addTag(token);
10609 }
10610 break;
10611
10612 case TOKEN_NEWLINE:
10613 // handle things like
10614 // [*]list\nitem\n[*]list1
10615 // where it should come out as
10616 // [*]list\nitem[/*]\n[*]list1[/*]
10617 // instead of
10618 // [*]list\nitem\n[/*][*]list1[/*]
10619 if (currentTag() && next && closesCurrentTag(
10620 (next.type === TOKEN_CLOSE ? '/' : '') +
10621 next.name
10622 )) {
10623 // skip if the next tag is the closing tag for
10624 // the option tag, i.e. [/*]
10625 if (!(next.type === TOKEN_CLOSE &&
10626 next.name === currentTag().name)) {
10627 bbcode = bbcodeHandlers[currentTag().name];
10628
10629 if (bbcode && bbcode.breakAfter) {
10630 openTags.pop();
10631 } else if (bbcode &&
10632 bbcode.isInline === false &&
10633 base.opts.breakAfterBlock &&
10634 bbcode.breakAfter !== false) {
10635 openTags.pop();
10636 }
10637 }
10638 }
10639
10640 addTag(token);
10641 break;
10642
10643 default: // content
10644 addTag(token);
10645 break;
10646 }
10647 }
10648
10649 return output;
10650 }
10651
10652 /**
10653 * Normalise all new lines
10654 *
10655 * Removes any formatting new lines from the BBCode
10656 * leaving only content ones. I.e. for a list:
10657 *
10658 * [list]
10659 * [*] list item one
10660 * with a line break
10661 * [*] list item two
10662 * [/list]
10663 *
10664 * would become
10665 *
10666 * [list] [*] list item one
10667 * with a line break [*] list item two [/list]
10668 *
10669 * Which makes it easier to convert to HTML or add
10670 * the formatting new lines back in when converting
10671 * back to BBCode
10672 *
10673 * @param {array} children
10674 * @param {TokenizeToken} parent
10675 * @param {boolean} onlyRemoveBreakAfter
10676 * @return {void}
10677 */
10678 function normaliseNewLines(children, parent, onlyRemoveBreakAfter) {
10679 var token, left, right, parentBBCode, bbcode,
10680 removedBreakEnd, removedBreakBefore, remove;
10681 var childrenLength = children.length;
10682 // TODO: this function really needs tidying up
10683 if (parent) {
10684 parentBBCode = bbcodeHandlers[parent.name];
10685 }
10686
10687 var i = childrenLength;
10688 while (i--) {
10689 if (!(token = children[i])) {
10690 continue;
10691 }
10692
10693 if (token.type === TOKEN_NEWLINE) {
10694 left = i > 0 ? children[i - 1] : null;
10695 right = i < childrenLength - 1 ? children[i + 1] : null;
10696 remove = false;
10697
10698 // Handle the start and end new lines
10699 // e.g. [tag]\n and \n[/tag]
10700 if (!onlyRemoveBreakAfter && parentBBCode &&
10701 parentBBCode.isSelfClosing !== true) {
10702 // First child of parent so must be opening line break
10703 // (breakStartBlock, breakStart) e.g. [tag]\n
10704 if (!left) {
10705 if (parentBBCode.isInline === false &&
10706 base.opts.breakStartBlock &&
10707 parentBBCode.breakStart !== false) {
10708 remove = true;
10709 }
10710
10711 if (parentBBCode.breakStart) {
10712 remove = true;
10713 }
10714 // Last child of parent so must be end line break
10715 // (breakEndBlock, breakEnd)
10716 // e.g. \n[/tag]
10717 // remove last line break (breakEndBlock, breakEnd)
10718 } else if (!removedBreakEnd && !right) {
10719 if (parentBBCode.isInline === false &&
10720 base.opts.breakEndBlock &&
10721 parentBBCode.breakEnd !== false) {
10722 remove = true;
10723 }
10724
10725 if (parentBBCode.breakEnd) {
10726 remove = true;
10727 }
10728
10729 removedBreakEnd = remove;
10730 }
10731 }
10732
10733 if (left && left.type === TOKEN_OPEN) {
10734 if ((bbcode = bbcodeHandlers[left.name])) {
10735 if (!onlyRemoveBreakAfter) {
10736 if (bbcode.isInline === false &&
10737 base.opts.breakAfterBlock &&
10738 bbcode.breakAfter !== false) {
10739 remove = true;
10740 }
10741
10742 if (bbcode.breakAfter) {
10743 remove = true;
10744 }
10745 } else if (bbcode.isInline === false) {
10746 remove = true;
10747 }
10748 }
10749 }
10750
10751 if (!onlyRemoveBreakAfter && !removedBreakBefore &&
10752 right && right.type === TOKEN_OPEN) {
10753
10754 if ((bbcode = bbcodeHandlers[right.name])) {
10755 if (bbcode.isInline === false &&
10756 base.opts.breakBeforeBlock &&
10757 bbcode.breakBefore !== false) {
10758 remove = true;
10759 }
10760
10761 if (bbcode.breakBefore) {
10762 remove = true;
10763 }
10764
10765 removedBreakBefore = remove;
10766
10767 if (remove) {
10768 children.splice(i, 1);
10769 continue;
10770 }
10771 }
10772 }
10773
10774 if (remove) {
10775 children.splice(i, 1);
10776 }
10777
10778 // reset double removedBreakBefore removal protection.
10779 // This is needed for cases like \n\n[\tag] where
10780 // only 1 \n should be removed but without this they both
10781 // would be.
10782 removedBreakBefore = false;
10783 } else if (token.type === TOKEN_OPEN) {
10784 normaliseNewLines(token.children, token,
10785 onlyRemoveBreakAfter);
10786 }
10787 }
10788 }
10789
10790 /**
10791 * Fixes any invalid nesting.
10792 *
10793 * If it is a block level element inside 1 or more inline elements
10794 * then those inline elements will be split at the point where the
10795 * block level is and the block level element placed between the split
10796 * parts. i.e.
10797 * [inline]A[blocklevel]B[/blocklevel]C[/inline]
10798 * Will become:
10799 * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline]
10800 *
10801 * @param {array} children
10802 * @param {array} [parents] Null if there is no parents
10803 * @param {boolea} [insideInline] If inside an inline element
10804 * @param {array} [rootArr] Root array if there is one
10805 * @return {array}
10806 * @private
10807 */
10808 function fixNesting(children, parents, insideInline, rootArr) {
10809 var token, i, parent, parentIndex, parentParentChildren, right;
10810
10811 var isInline = function (token) {
10812 var bbcode = bbcodeHandlers[token.name];
10813
10814 return !bbcode || bbcode.isInline !== false;
10815 };
10816
10817 parents = parents || [];
10818 rootArr = rootArr || children;
10819
10820 // This must check the length each time as it can change when
10821 // tokens are moved to fix the nesting.
10822 for (i = 0; i < children.length; i++) {
10823 if (!(token = children[i]) || token.type !== TOKEN_OPEN) {
10824 continue;
10825 }
10826
10827 if (insideInline && !isInline(token)) {
10828 // if this is a blocklevel element inside an inline one then
10829 // split the parent at the block level element
10830 parent = last(parents);
10831 right = parent.splitAt(token);
10832
10833 parentParentChildren = parents.length > 1 ?
10834 parents[parents.length - 2].children : rootArr;
10835
10836 // If parent inline is allowed inside this tag, clone it and
10837 // wrap this tags children in it.
10838 if (isChildAllowed(token, parent)) {
10839 var clone = parent.clone();
10840 clone.children = token.children;
10841 token.children = [clone];
10842 }
10843
10844 parentIndex = parentParentChildren.indexOf(parent);
10845 if (parentIndex > -1) {
10846 // remove the block level token from the right side of
10847 // the split inline element
10848 right.children.splice(0, 1);
10849
10850 // insert the block level token and the right side after
10851 // the left side of the inline token
10852 parentParentChildren.splice(
10853 parentIndex + 1, 0, token, right
10854 );
10855
10856 // If token is a block and is followed by a newline,
10857 // then move the newline along with it to the new parent
10858 var next = right.children[0];
10859 if (next && next.type === TOKEN_NEWLINE) {
10860 if (!isInline(token)) {
10861 right.children.splice(0, 1);
10862 parentParentChildren.splice(
10863 parentIndex + 2, 0, next
10864 );
10865 }
10866 }
10867
10868 // return to parents loop as the
10869 // children have now increased
10870 return;
10871 }
10872
10873 }
10874
10875 parents.push(token);
10876
10877 fixNesting(
10878 token.children,
10879 parents,
10880 insideInline || isInline(token),
10881 rootArr
10882 );
10883
10884 parents.pop();
10885 }
10886 }
10887
10888 /**
10889 * Removes any empty BBCodes which are not allowed to be empty.
10890 *
10891 * @param {array} tokens
10892 * @private
10893 */
10894 function removeEmpty(tokens) {
10895 var token, bbcode;
10896
10897 /**
10898 * Checks if all children are whitespace or not
10899 * @private
10900 */
10901 var isTokenWhiteSpace = function (children) {
10902 var j = children.length;
10903
10904 while (j--) {
10905 var type = children[j].type;
10906
10907 if (type === TOKEN_OPEN || type === TOKEN_CLOSE) {
10908 return false;
10909 }
10910
10911 if (type === TOKEN_CONTENT &&
10912 /\S|\u00A0/.test(children[j].val)) {
10913 return false;
10914 }
10915 }
10916
10917 return true;
10918 };
10919
10920 var i = tokens.length;
10921 while (i--) {
10922 // So skip anything that isn't a tag since only tags can be
10923 // empty, content can't
10924 if (!(token = tokens[i]) || token.type !== TOKEN_OPEN) {
10925 continue;
10926 }
10927
10928 bbcode = bbcodeHandlers[token.name];
10929
10930 // Remove any empty children of this tag first so that if they
10931 // are all removed this one doesn't think it's not empty.
10932 removeEmpty(token.children);
10933
10934 if (isTokenWhiteSpace(token.children) && bbcode &&
10935 !bbcode.isSelfClosing && !bbcode.allowsEmpty) {
10936 tokens.splice.apply(tokens, [i, 1].concat(token.children));
10937 }
10938 }
10939 }
10940
10941 /**
10942 * Converts a BBCode string to HTML
10943 *
10944 * @param {string} str
10945 * @param {boolean} preserveNewLines If to preserve all new lines, not
10946 * strip any based on the passed
10947 * formatting options
10948 * @return {string}
10949 * @memberOf BBCodeParser.prototype
10950 */
10951 base.toHTML = function (str, preserveNewLines) {
10952 return convertToHTML(base.parse(str, preserveNewLines), true);
10953 };
10954
10955 /**
10956 * @private
10957 */
10958 function convertToHTML(tokens, isRoot) {
10959 var undef, token, bbcode, content, html, needsBlockWrap,
10960 blockWrapOpen, isInline, lastChild,
10961 ret = '';
10962
10963 isInline = function (bbcode) {
10964 return (!bbcode || (bbcode.isHtmlInline !== undef ?
10965 bbcode.isHtmlInline : bbcode.isInline)) !== false;
10966 };
10967
10968 while (tokens.length > 0) {
10969 if (!(token = tokens.shift())) {
10970 continue;
10971 }
10972
10973 if (token.type === TOKEN_OPEN) {
10974 lastChild = token.children[token.children.length - 1] || {};
10975 bbcode = bbcodeHandlers[token.name];
10976 needsBlockWrap = isRoot && isInline(bbcode);
10977 content = convertToHTML(token.children, false);
10978
10979 if (bbcode && bbcode.html) {
10980 // Only add a line break to the end if this is
10981 // blocklevel and the last child wasn't block-level
10982 if (!isInline(bbcode) &&
10983 isInline(bbcodeHandlers[lastChild.name]) &&
10984 !bbcode.isPreFormatted &&
10985 !bbcode.skipLastLineBreak) {
10986 // Add placeholder br to end of block level
10987 // elements
10988 content += '<br />';
10989 }
10990
10991 if (!isFunction(bbcode.html)) {
10992 token.attrs['0'] = content;
10993 html = formatBBCodeString(
10994 bbcode.html,
10995 token.attrs
10996 );
10997 } else {
10998 html = bbcode.html.call(
10999 base,
11000 token,
11001 token.attrs,
11002 content
11003 );
11004 }
11005 } else {
11006 html = token.val + content +
11007 (token.closing ? token.closing.val : '');
11008 }
11009 } else if (token.type === TOKEN_NEWLINE) {
11010 if (!isRoot) {
11011 ret += '<br />';
11012 continue;
11013 }
11014
11015 // If not already in a block wrap then start a new block
11016 if (!blockWrapOpen) {
11017 ret += '<div>';
11018 }
11019
11020 ret += '<br />';
11021
11022 // Normally the div acts as a line-break with by moving
11023 // whatever comes after onto a new line.
11024 // If this is the last token, add an extra line-break so it
11025 // shows as there will be nothing after it.
11026 if (!tokens.length) {
11027 ret += '<br />';
11028 }
11029
11030 ret += '</div>\n';
11031 blockWrapOpen = false;
11032 continue;
11033 // content
11034 } else {
11035 needsBlockWrap = isRoot;
11036 html = escapeEntities(token.val, true);
11037 }
11038
11039 if (needsBlockWrap && !blockWrapOpen) {
11040 ret += '<div>';
11041 blockWrapOpen = true;
11042 } else if (!needsBlockWrap && blockWrapOpen) {
11043 ret += '</div>\n';
11044 blockWrapOpen = false;
11045 }
11046
11047 ret += html;
11048 }
11049
11050 if (blockWrapOpen) {
11051 ret += '</div>\n';
11052 }
11053
11054 return ret;
11055 }
11056
11057 /**
11058 * Takes a BBCode string, parses it then converts it back to BBCode.
11059 *
11060 * This will auto fix the BBCode and format it with the specified
11061 * options.
11062 *
11063 * @param {string} str
11064 * @param {boolean} preserveNewLines If to preserve all new lines, not
11065 * strip any based on the passed
11066 * formatting options
11067 * @return {string}
11068 * @memberOf BBCodeParser.prototype
11069 */
11070 base.toBBCode = function (str, preserveNewLines) {
11071 return convertToBBCode(base.parse(str, preserveNewLines));
11072 };
11073
11074 /**
11075 * Converts parsed tokens back into BBCode with the
11076 * formatting specified in the options and with any
11077 * fixes specified.
11078 *
11079 * @param {array} toks Array of parsed tokens from base.parse()
11080 * @return {string}
11081 * @private
11082 */
11083 function convertToBBCode(toks) {
11084 var token, attr, bbcode, isBlock, isSelfClosing, quoteType,
11085 breakBefore, breakStart, breakEnd, breakAfter,
11086 ret = '';
11087
11088 while (toks.length > 0) {
11089 if (!(token = toks.shift())) {
11090 continue;
11091 }
11092 // TODO: tidy this
11093 bbcode = bbcodeHandlers[token.name];
11094 isBlock = !(!bbcode || bbcode.isInline !== false);
11095 isSelfClosing = bbcode && bbcode.isSelfClosing;
11096
11097 breakBefore = (isBlock && base.opts.breakBeforeBlock &&
11098 bbcode.breakBefore !== false) ||
11099 (bbcode && bbcode.breakBefore);
11100
11101 breakStart = (isBlock && !isSelfClosing &&
11102 base.opts.breakStartBlock &&
11103 bbcode.breakStart !== false) ||
11104 (bbcode && bbcode.breakStart);
11105
11106 breakEnd = (isBlock && base.opts.breakEndBlock &&
11107 bbcode.breakEnd !== false) ||
11108 (bbcode && bbcode.breakEnd);
11109
11110 breakAfter = (isBlock && base.opts.breakAfterBlock &&
11111 bbcode.breakAfter !== false) ||
11112 (bbcode && bbcode.breakAfter);
11113
11114 quoteType = (bbcode ? bbcode.quoteType : null) ||
11115 base.opts.quoteType || QuoteType.auto;
11116
11117 if (!bbcode && token.type === TOKEN_OPEN) {
11118 ret += token.val;
11119
11120 if (token.children) {
11121 ret += convertToBBCode(token.children);
11122 }
11123
11124 if (token.closing) {
11125 ret += token.closing.val;
11126 }
11127 } else if (token.type === TOKEN_OPEN) {
11128 if (breakBefore) {
11129 ret += '\n';
11130 }
11131
11132 // Convert the tag and it's attributes to BBCode
11133 ret += '[' + token.name;
11134 if (token.attrs) {
11135 if (token.attrs.defaultattr) {
11136 ret += '=' + quote(
11137 token.attrs.defaultattr,
11138 quoteType,
11139 'defaultattr'
11140 );
11141
11142 delete token.attrs.defaultattr;
11143 }
11144
11145 for (attr in token.attrs) {
11146 if (token.attrs.hasOwnProperty(attr)) {
11147 ret += ' ' + attr + '=' +
11148 quote(token.attrs[attr], quoteType, attr);
11149 }
11150 }
11151 }
11152 ret += ']';
11153
11154 if (breakStart) {
11155 ret += '\n';
11156 }
11157
11158 // Convert the tags children to BBCode
11159 if (token.children) {
11160 ret += convertToBBCode(token.children);
11161 }
11162
11163 // add closing tag if not self closing
11164 if (!isSelfClosing && !bbcode.excludeClosing) {
11165 if (breakEnd) {
11166 ret += '\n';
11167 }
11168
11169 ret += '[/' + token.name + ']';
11170 }
11171
11172 if (breakAfter) {
11173 ret += '\n';
11174 }
11175
11176 // preserve whatever was recognized as the
11177 // closing tag if it is a self closing tag
11178 if (token.closing && isSelfClosing) {
11179 ret += token.closing.val;
11180 }
11181 } else {
11182 ret += token.val;
11183 }
11184 }
11185
11186 return ret;
11187 }
11188
11189 /**
11190 * Quotes an attribute
11191 *
11192 * @param {string} str
11193 * @param {BBCodeParser.QuoteType} quoteType
11194 * @param {string} name
11195 * @return {string}
11196 * @private
11197 */
11198 function quote(str, quoteType, name) {
11199 var needsQuotes = /\s|=/.test(str);
11200
11201 if (isFunction(quoteType)) {
11202 return quoteType(str, name);
11203 }
11204
11205 if (quoteType === QuoteType.never ||
11206 (quoteType === QuoteType.auto && !needsQuotes)) {
11207 return str;
11208 }
11209
11210 return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
11211 }
11212
11213 /**
11214 * Returns the last element of an array or null
11215 *
11216 * @param {array} arr
11217 * @return {Object} Last element
11218 * @private
11219 */
11220 function last(arr) {
11221 if (arr.length) {
11222 return arr[arr.length - 1];
11223 }
11224
11225 return null;
11226 }
11227
11228 /**
11229 * Converts a string to lowercase.
11230 *
11231 * @param {string} str
11232 * @return {string} Lowercase version of str
11233 * @private
11234 */
11235 function lower(str) {
11236 return str.toLowerCase();
11237 }
11238 };
11239
11240 /**
11241 * Quote type
11242 * @type {Object}
11243 * @class QuoteType
11244 * @name BBCodeParser.QuoteType
11245 * @since 1.4.0
11246 */
11247 BBCodeParser.QuoteType = QuoteType;
11248
11249 /**
11250 * Default BBCode parser options
11251 * @type {Object}
11252 */
11253 BBCodeParser.defaults = {
11254 /**
11255 * If to add a new line before block level elements
11256 *
11257 * @type {Boolean}
11258 */
11259 breakBeforeBlock: false,
11260
11261 /**
11262 * If to add a new line after the start of block level elements
11263 *
11264 * @type {Boolean}
11265 */
11266 breakStartBlock: false,
11267
11268 /**
11269 * If to add a new line before the end of block level elements
11270 *
11271 * @type {Boolean}
11272 */
11273 breakEndBlock: false,
11274
11275 /**
11276 * If to add a new line after block level elements
11277 *
11278 * @type {Boolean}
11279 */
11280 breakAfterBlock: true,
11281
11282 /**
11283 * If to remove empty tags
11284 *
11285 * @type {Boolean}
11286 */
11287 removeEmptyTags: true,
11288
11289 /**
11290 * If to fix invalid nesting,
11291 * i.e. block level elements inside inline elements.
11292 *
11293 * @type {Boolean}
11294 */
11295 fixInvalidNesting: true,
11296
11297 /**
11298 * If to fix invalid children.
11299 * i.e. A tag which is inside a parent that doesn't
11300 * allow that type of tag.
11301 *
11302 * @type {Boolean}
11303 */
11304 fixInvalidChildren: true,
11305
11306 /**
11307 * Attribute quote type
11308 *
11309 * @type {BBCodeParser.QuoteType}
11310 * @since 1.4.1
11311 */
11312 quoteType: QuoteType.auto,
11313
11314 /**
11315 * Whether to use strict matching on attributes and styles.
11316 *
11317 * When true this will perform AND matching requiring all tag
11318 * attributes and styles to match.
11319 *
11320 * When false will perform OR matching and will match if any of
11321 * a tags attributes or styles match.
11322 *
11323 * @type {Boolean}
11324 * @since 3.1.0
11325 */
11326 strictMatch: false
11327 };
11328
11329 /**
11330 * Converts a number 0-255 to hex.
11331 *
11332 * Will return 00 if number is not a valid number.
11333 *
11334 * @param {any} number
11335 * @return {string}
11336 * @private
11337 */
11338 function toHex(number) {
11339 number = parseInt(number, 10);
11340
11341 if (isNaN(number)) {
11342 return '00';
11343 }
11344
11345 number = Math.max(0, Math.min(number, 255)).toString(16);
11346
11347 return number.length < 2 ? '0' + number : number;
11348 }
11349 /**
11350 * Normalises a CSS colour to hex #xxxxxx format
11351 *
11352 * @param {string} colorStr
11353 * @return {string}
11354 * @private
11355 */
11356 function _normaliseColour(colorStr) {
11357 var match;
11358
11359 colorStr = colorStr || '#000';
11360
11361 // rgb(n,n,n);
11362 if ((match =
11363 colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) {
11364 return '#' +
11365 toHex(match[1]) +
11366 toHex(match[2]) +
11367 toHex(match[3]);
11368 }
11369
11370 // expand shorthand
11371 if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) {
11372 return '#' +
11373 match[1] + match[1] +
11374 match[2] + match[2] +
11375 match[3] + match[3];
11376 }
11377
11378 return colorStr;
11379 }
11380
11381 /**
11382 * SCEditor BBCode format
11383 * @since 2.0.0
11384 */
11385 function bbcodeFormat() {
11386 var base = this;
11387
11388 base.stripQuotes = _stripQuotes;
11389
11390 /**
11391 * cache of all the tags pointing to their bbcodes to enable
11392 * faster lookup of which bbcode a tag should have
11393 * @private
11394 */
11395 var tagsToBBCodes = {};
11396
11397 /**
11398 * Allowed children of specific HTML tags. Empty array if no
11399 * children other than text nodes are allowed
11400 * @private
11401 */
11402 var validChildren = {
11403 ul: ['li', 'ol', 'ul'],
11404 ol: ['li', 'ol', 'ul'],
11405 table: ['tr'],
11406 tr: ['td', 'th'],
11407 code: ['br', 'p', 'div']
11408 };
11409
11410 /**
11411 * Populates tagsToBBCodes and stylesToBBCodes for easier lookups
11412 *
11413 * @private
11414 */
11415 function buildBbcodeCache() {
11416 each(bbcodeHandlers, function (bbcode, handler) {
11417 var
11418 isBlock = handler.isInline === false,
11419 tags = bbcodeHandlers[bbcode].tags,
11420 styles = bbcodeHandlers[bbcode].styles;
11421
11422 if (styles) {
11423 tagsToBBCodes['*'] = tagsToBBCodes['*'] || {};
11424 tagsToBBCodes['*'][isBlock] =
11425 tagsToBBCodes['*'][isBlock] || {};
11426 tagsToBBCodes['*'][isBlock][bbcode] = [
11427 ['style', Object.entries(styles)]
11428 ];
11429 }
11430
11431 if (tags) {
11432 each(tags, function (tag, values) {
11433 if (values && values.style) {
11434 values.style = Object.entries(values.style);
11435 }
11436
11437 tagsToBBCodes[tag] = tagsToBBCodes[tag] || {};
11438 tagsToBBCodes[tag][isBlock] =
11439 tagsToBBCodes[tag][isBlock] || {};
11440 tagsToBBCodes[tag][isBlock][bbcode] =
11441 values && Object.entries(values);
11442 });
11443 }
11444 });
11445 };
11446
11447 /**
11448 * Handles adding newlines after block level elements
11449 *
11450 * @param {HTMLElement} element The element to convert
11451 * @param {string} content The tags text content
11452 * @return {string}
11453 * @private
11454 */
11455 function handleBlockNewlines(element, content) {
11456 var tag = element.nodeName.toLowerCase();
11457 var isInline = dom.isInline;
11458 if (!isInline(element, true) || tag === 'br') {
11459 var isLastBlockChild, parent, parentLastChild,
11460 previousSibling = element.previousSibling;
11461
11462 // Skips selection makers and ignored elements
11463 // Skip empty inline elements
11464 while (previousSibling &&
11465 previousSibling.nodeType === 1 &&
11466 !is(previousSibling, 'br') &&
11467 isInline(previousSibling, true) &&
11468 !previousSibling.firstChild) {
11469 previousSibling = previousSibling.previousSibling;
11470 }
11471
11472 // If it's the last block of an inline that is the last
11473 // child of a block then it shouldn't cause a line break
11474 // <block><inline><br></inline></block>
11475 do {
11476 parent = element.parentNode;
11477 parentLastChild = parent && parent.lastChild;
11478
11479 isLastBlockChild = parentLastChild === element;
11480 element = parent;
11481 } while (parent && isLastBlockChild && isInline(parent, true));
11482
11483 // If this block is:
11484 // * Not the last child of a block level element
11485 // * Is a <li> tag (lists are blocks)
11486 if (!isLastBlockChild || tag === 'li') {
11487 content += '\n';
11488 }
11489
11490 // Check for:
11491 // <block>text<block>text</block></block>
11492 //
11493 // The second opening <block> opening tag should cause a
11494 // line break because the previous sibing is inline.
11495 if (tag !== 'br' && previousSibling &&
11496 !is(previousSibling, 'br') &&
11497 isInline(previousSibling, true)) {
11498 content = '\n' + content;
11499 }
11500 }
11501
11502 return content;
11503 }
11504
11505 /**
11506 * Handles a HTML tag and finds any matching BBCodes
11507 *
11508 * @param {HTMLElement} element The element to convert
11509 * @param {string} content The Tags text content
11510 * @param {boolean} blockLevel
11511 * @return {string} Content with any matching BBCode tags
11512 * wrapped around it.
11513 * @private
11514 */
11515 function handleTags(element, content, blockLevel) {
11516 function isStyleMatch(style) {
11517 var property = style[0];
11518 var values = style[1];
11519 var val = dom.getStyle(element, property);
11520 var parent = element.parentNode;
11521
11522 // if the parent has the same style use that instead of this one
11523 // so you don't end up with [i]parent[i]child[/i][/i]
11524 if (!val || parent && dom.hasStyle(parent, property, val)) {
11525 return false;
11526 }
11527
11528 return !values || values.includes(val);
11529 }
11530
11531 function createAttributeMatch(isStrict) {
11532 return function (attribute) {
11533 var name = attribute[0];
11534 var value = attribute[1];
11535
11536 // code tags should skip most styles
11537 if (name === 'style' && element.nodeName === 'CODE') {
11538 return false;
11539 }
11540
11541 if (name === 'style' && value) {
11542 return value[isStrict ? 'every' : 'some'](isStyleMatch);
11543 } else {
11544 var val = attr(element, name);
11545
11546 return val && (!value || value.includes(val));
11547 }
11548 };
11549 }
11550
11551 function handleTag(tag) {
11552 if (!tagsToBBCodes[tag] || !tagsToBBCodes[tag][blockLevel]) {
11553 return;
11554 }
11555
11556 // loop all bbcodes for this tag
11557 each(tagsToBBCodes[tag][blockLevel], function (bbcode, attrs) {
11558 var fn, format,
11559 isStrict = bbcodeHandlers[bbcode].strictMatch;
11560
11561 if (typeof isStrict === 'undefined') {
11562 isStrict = base.opts.strictMatch;
11563 }
11564
11565 // Skip if the element doesn't have the attribute or the
11566 // attribute doesn't match one of the required values
11567 fn = isStrict ? 'every' : 'some';
11568 if (attrs && !attrs[fn](createAttributeMatch(isStrict))) {
11569 return;
11570 }
11571
11572 format = bbcodeHandlers[bbcode].format;
11573 if (isFunction(format)) {
11574 content = format.call(base, element, content);
11575 } else {
11576 content = _formatString(format, content);
11577 }
11578 return false;
11579 });
11580 }
11581
11582 handleTag('*');
11583 handleTag(element.nodeName.toLowerCase());
11584 return content;
11585 }
11586
11587 /**
11588 * Converts a HTML dom element to BBCode starting from
11589 * the innermost element and working backwards
11590 *
11591 * @private
11592 * @param {HTMLElement} element
11593 * @return {string} BBCode
11594 * @memberOf SCEditor.plugins.bbcode.prototype
11595 */
11596 function elementToBbcode(element) {
11597 var toBBCode = function (node, vChildren) {
11598 var ret = '';
11599
11600 dom.traverse(node, function (node) {
11601 var content = '',
11602 nodeType = node.nodeType,
11603 tag = node.nodeName.toLowerCase(),
11604 vChild = validChildren[tag],
11605 firstChild = node.firstChild,
11606 isValidChild = true;
11607
11608 if (typeof vChildren === 'object') {
11609 isValidChild = vChildren.indexOf(tag) > -1;
11610
11611 // Emoticons should always be converted
11612 if (is(node, 'img') && attr(node, EMOTICON_DATA_ATTR)) {
11613 isValidChild = true;
11614 }
11615
11616 // if this tag is one of the parents allowed children
11617 // then set this tags allowed children to whatever it
11618 // allows, otherwise set to what the parent allows
11619 if (!isValidChild) {
11620 vChild = vChildren;
11621 }
11622 }
11623
11624 // 3 = text and 1 = element
11625 if (nodeType !== 3 && nodeType !== 1) {
11626 return;
11627 }
11628
11629 if (nodeType === 1) {
11630 // skip empty nlf elements (new lines automatically
11631 // added after block level elements like quotes)
11632 if (is(node, '.sceditor-nlf') && !firstChild) {
11633 return;
11634 }
11635
11636 // don't convert iframe contents
11637 if (tag !== 'iframe') {
11638 content = toBBCode(node, vChild);
11639 }
11640
11641 // TODO: isValidChild is no longer needed. Should use
11642 // valid children bbcodes instead by creating BBCode
11643 // tokens like the parser.
11644 if (isValidChild) {
11645 // code tags should skip most styles
11646 if (tag !== 'code') {
11647 // First parse inline codes
11648 content = handleTags(node, content, false);
11649 }
11650
11651 content = handleTags(node, content, true);
11652 ret += handleBlockNewlines(node, content);
11653 } else {
11654 ret += content;
11655 }
11656 } else {
11657 ret += node.nodeValue;
11658 }
11659 }, false, true);
11660
11661 return ret;
11662 };
11663
11664 return toBBCode(element);
11665 };
11666
11667 /**
11668 * Initializer
11669 * @private
11670 */
11671 base.init = function () {
11672 base.opts = this.opts;
11673 base.elementToBbcode = elementToBbcode;
11674
11675 // build the BBCode cache
11676 buildBbcodeCache();
11677
11678 this.commands = extend(
11679 true, {}, defaultCommandsOverrides, this.commands
11680 );
11681
11682 // Add BBCode helper methods
11683 this.toBBCode = base.toSource;
11684 this.fromBBCode = base.toHtml;
11685 };
11686
11687 /**
11688 * Converts BBCode into HTML
11689 *
11690 * @param {boolean} asFragment
11691 * @param {string} source
11692 * @param {boolean} [legacyAsFragment] Used by fromBBCode() method
11693 */
11694 function toHtml(asFragment, source, legacyAsFragment) {
11695 var parser = new BBCodeParser(base.opts.parserOptions);
11696 var html = parser.toHTML(
11697 base.opts.bbcodeTrim ? source.trim() : source
11698 );
11699
11700 return (asFragment || legacyAsFragment) ?
11701 removeFirstLastDiv(html) : html;
11702 }
11703
11704 /**
11705 * Converts HTML into BBCode
11706 *
11707 * @param {boolean} asFragment
11708 * @param {string} html
11709 * @param {!Document} [context]
11710 * @param {!HTMLElement} [parent]
11711 * @return {string}
11712 * @private
11713 */
11714 function toSource(asFragment, html, context, parent) {
11715 context = context || document;
11716
11717 var bbcode, elements;
11718 var containerParent = context.createElement('div');
11719 var container = context.createElement('div');
11720 var parser = new BBCodeParser(base.opts.parserOptions);
11721
11722 container.innerHTML = html;
11723 css(containerParent, 'visibility', 'hidden');
11724 containerParent.appendChild(container);
11725 context.body.appendChild(containerParent);
11726
11727 if (asFragment) {
11728 // Add text before and after so removeWhiteSpace doesn't remove
11729 // leading and trailing whitespace
11730 containerParent.insertBefore(
11731 context.createTextNode('#'),
11732 containerParent.firstChild
11733 );
11734 containerParent.appendChild(context.createTextNode('#'));
11735 }
11736
11737 // Match parents white-space handling
11738 if (parent) {
11739 css(container, 'whiteSpace', css(parent, 'whiteSpace'));
11740 }
11741
11742 // Remove all nodes with sceditor-ignore class
11743 elements = container.getElementsByClassName('sceditor-ignore');
11744 while (elements.length) {
11745 elements[0].parentNode.removeChild(elements[0]);
11746 }
11747
11748 dom.removeWhiteSpace(containerParent);
11749
11750 bbcode = elementToBbcode(container);
11751
11752 context.body.removeChild(containerParent);
11753
11754 bbcode = parser.toBBCode(bbcode, true);
11755
11756 if (base.opts.bbcodeTrim) {
11757 bbcode = bbcode.trim();
11758 }
11759
11760 return bbcode;
11761 };
11762
11763 base.toHtml = toHtml.bind(null, false);
11764 base.fragmentToHtml = toHtml.bind(null, true);
11765 base.toSource = toSource.bind(null, false);
11766 base.fragmentToSource = toSource.bind(null, true);
11767 };
11768
11769 /**
11770 * Gets a BBCode
11771 *
11772 * @param {string} name
11773 * @return {Object|null}
11774 * @since 2.0.0
11775 */
11776 bbcodeFormat.get = function (name) {
11777 return bbcodeHandlers[name] || null;
11778 };
11779
11780 /**
11781 * Adds a BBCode to the parser or updates an existing
11782 * BBCode if a BBCode with the specified name already exists.
11783 *
11784 * @param {string} name
11785 * @param {Object} bbcode
11786 * @return {this}
11787 * @since 2.0.0
11788 */
11789 bbcodeFormat.set = function (name, bbcode) {
11790 if (name && bbcode) {
11791 // merge any existing command properties
11792 bbcode = extend(bbcodeHandlers[name] || {}, bbcode);
11793
11794 bbcode.remove = function () {
11795 delete bbcodeHandlers[name];
11796 };
11797
11798 bbcodeHandlers[name] = bbcode;
11799 }
11800
11801 return this;
11802 };
11803
11804 /**
11805 * Renames a BBCode
11806 *
11807 * This does not change the format or HTML handling, those must be
11808 * changed manually.
11809 *
11810 * @param {string} name [description]
11811 * @param {string} newName [description]
11812 * @return {this|false}
11813 * @since 2.0.0
11814 */
11815 bbcodeFormat.rename = function (name, newName) {
11816 if (name in bbcodeHandlers) {
11817 bbcodeHandlers[newName] = bbcodeHandlers[name];
11818
11819 delete bbcodeHandlers[name];
11820 }
11821
11822 return this;
11823 };
11824
11825 /**
11826 * Removes a BBCode
11827 *
11828 * @param {string} name
11829 * @return {this}
11830 * @since 2.0.0
11831 */
11832 bbcodeFormat.remove = function (name) {
11833 if (name in bbcodeHandlers) {
11834 delete bbcodeHandlers[name];
11835 }
11836
11837 return this;
11838 };
11839
11840 bbcodeFormat.formatBBCodeString = formatBBCodeString;
11841
11842 sceditor.formats.bbcode = bbcodeFormat;
11843 sceditor.BBCodeParser = BBCodeParser;
11844 }(sceditor));