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