Mercurial Hosting > sceditor
comparison src/development/jquery.sceditor.bbcode.js @ 0:4c4fc447baea
start with sceditor-3.1.1
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 04 Aug 2022 15:21:29 -0600 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c4fc447baea |
---|---|
1 (function ($) { | |
2 'use strict'; | |
3 | |
4 function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | |
5 | |
6 var $__default = /*#__PURE__*/_interopDefaultLegacy($); | |
7 | |
8 /** | |
9 * Check if the passed argument is the | |
10 * the passed type. | |
11 * | |
12 * @param {string} type | |
13 * @param {*} arg | |
14 * @returns {boolean} | |
15 */ | |
16 function isTypeof(type, arg) { | |
17 return typeof arg === type; | |
18 } | |
19 | |
20 /** | |
21 * @type {function(*): boolean} | |
22 */ | |
23 var isString = isTypeof.bind(null, 'string'); | |
24 | |
25 /** | |
26 * @type {function(*): boolean} | |
27 */ | |
28 var isUndefined = isTypeof.bind(null, 'undefined'); | |
29 | |
30 /** | |
31 * @type {function(*): boolean} | |
32 */ | |
33 var isFunction = isTypeof.bind(null, 'function'); | |
34 | |
35 /** | |
36 * @type {function(*): boolean} | |
37 */ | |
38 var isNumber = isTypeof.bind(null, 'number'); | |
39 | |
40 | |
41 /** | |
42 * Returns true if an object has no keys | |
43 * | |
44 * @param {!Object} obj | |
45 * @returns {boolean} | |
46 */ | |
47 function isEmptyObject(obj) { | |
48 return !Object.keys(obj).length; | |
49 } | |
50 | |
51 /** | |
52 * Extends the first object with any extra objects passed | |
53 * | |
54 * If the first argument is boolean and set to true | |
55 * it will extend child arrays and objects recursively. | |
56 * | |
57 * @param {!Object|boolean} targetArg | |
58 * @param {...Object} source | |
59 * @return {Object} | |
60 */ | |
61 function extend(targetArg, sourceArg) { | |
62 var isTargetBoolean = targetArg === !!targetArg; | |
63 var i = isTargetBoolean ? 2 : 1; | |
64 var target = isTargetBoolean ? sourceArg : targetArg; | |
65 var isDeep = isTargetBoolean ? targetArg : false; | |
66 | |
67 function isObject(value) { | |
68 return value !== null && typeof value === 'object' && | |
69 Object.getPrototypeOf(value) === Object.prototype; | |
70 } | |
71 | |
72 for (; i < arguments.length; i++) { | |
73 var source = arguments[i]; | |
74 | |
75 // Copy all properties for jQuery compatibility | |
76 /* eslint guard-for-in: off */ | |
77 for (var key in source) { | |
78 var targetValue = target[key]; | |
79 var value = source[key]; | |
80 | |
81 // Skip undefined values to match jQuery | |
82 if (isUndefined(value)) { | |
83 continue; | |
84 } | |
85 | |
86 // Skip special keys to prevent prototype pollution | |
87 if (key === '__proto__' || key === 'constructor') { | |
88 continue; | |
89 } | |
90 | |
91 var isValueObject = isObject(value); | |
92 var isValueArray = Array.isArray(value); | |
93 | |
94 if (isDeep && (isValueObject || isValueArray)) { | |
95 // Can only merge if target type matches otherwise create | |
96 // new target to merge into | |
97 var isSameType = isObject(targetValue) === isValueObject && | |
98 Array.isArray(targetValue) === isValueArray; | |
99 | |
100 target[key] = extend( | |
101 true, | |
102 isSameType ? targetValue : (isValueArray ? [] : {}), | |
103 value | |
104 ); | |
105 } else { | |
106 target[key] = value; | |
107 } | |
108 } | |
109 } | |
110 | |
111 return target; | |
112 } | |
113 | |
114 /** | |
115 * Removes an item from the passed array | |
116 * | |
117 * @param {!Array} arr | |
118 * @param {*} item | |
119 */ | |
120 function arrayRemove(arr, item) { | |
121 var i = arr.indexOf(item); | |
122 | |
123 if (i > -1) { | |
124 arr.splice(i, 1); | |
125 } | |
126 } | |
127 | |
128 /** | |
129 * Iterates over an array or object | |
130 * | |
131 * @param {!Object|Array} obj | |
132 * @param {function(*, *)} fn | |
133 */ | |
134 function each(obj, fn) { | |
135 if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) { | |
136 for (var i = 0; i < obj.length; i++) { | |
137 fn(i, obj[i]); | |
138 } | |
139 } else { | |
140 Object.keys(obj).forEach(function (key) { | |
141 fn(key, obj[key]); | |
142 }); | |
143 } | |
144 } | |
145 | |
146 /** | |
147 * Cache of camelCase CSS property names | |
148 * @type {Object<string, string>} | |
149 */ | |
150 var cssPropertyNameCache = {}; | |
151 | |
152 /** | |
153 * Node type constant for element nodes | |
154 * | |
155 * @type {number} | |
156 */ | |
157 var ELEMENT_NODE = 1; | |
158 | |
159 /** | |
160 * Node type constant for text nodes | |
161 * | |
162 * @type {number} | |
163 */ | |
164 var TEXT_NODE = 3; | |
165 | |
166 /** | |
167 * Node type constant for comment nodes | |
168 * | |
169 * @type {number} | |
170 */ | |
171 var COMMENT_NODE = 8; | |
172 | |
173 function toFloat(value) { | |
174 value = parseFloat(value); | |
175 | |
176 return isFinite(value) ? value : 0; | |
177 } | |
178 | |
179 /** | |
180 * Creates an element with the specified attributes | |
181 * | |
182 * Will create it in the current document unless context | |
183 * is specified. | |
184 * | |
185 * @param {!string} tag | |
186 * @param {!Object<string, string>} [attributes] | |
187 * @param {!Document} [context] | |
188 * @returns {!HTMLElement} | |
189 */ | |
190 function createElement(tag, attributes, context) { | |
191 var node = (context || document).createElement(tag); | |
192 | |
193 each(attributes || {}, function (key, value) { | |
194 if (key === 'style') { | |
195 node.style.cssText = value; | |
196 } else if (key in node) { | |
197 node[key] = value; | |
198 } else { | |
199 node.setAttribute(key, value); | |
200 } | |
201 }); | |
202 | |
203 return node; | |
204 } | |
205 | |
206 /** | |
207 * Gets the first parent node that matches the selector | |
208 * | |
209 * @param {!HTMLElement} node | |
210 * @param {!string} [selector] | |
211 * @returns {HTMLElement|undefined} | |
212 */ | |
213 function parent(node, selector) { | |
214 var parent = node || {}; | |
215 | |
216 while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) { | |
217 if (!selector || is(parent, selector)) { | |
218 return parent; | |
219 } | |
220 } | |
221 } | |
222 | |
223 /** | |
224 * Checks the passed node and all parents and | |
225 * returns the first matching node if any. | |
226 * | |
227 * @param {!HTMLElement} node | |
228 * @param {!string} selector | |
229 * @returns {HTMLElement|undefined} | |
230 */ | |
231 function closest(node, selector) { | |
232 return is(node, selector) ? node : parent(node, selector); | |
233 } | |
234 | |
235 /** | |
236 * Removes the node from the DOM | |
237 * | |
238 * @param {!HTMLElement} node | |
239 */ | |
240 function remove(node) { | |
241 if (node.parentNode) { | |
242 node.parentNode.removeChild(node); | |
243 } | |
244 } | |
245 | |
246 /** | |
247 * Appends child to parent node | |
248 * | |
249 * @param {!HTMLElement} node | |
250 * @param {!HTMLElement} child | |
251 */ | |
252 function appendChild(node, child) { | |
253 node.appendChild(child); | |
254 } | |
255 | |
256 /** | |
257 * Finds any child nodes that match the selector | |
258 * | |
259 * @param {!HTMLElement} node | |
260 * @param {!string} selector | |
261 * @returns {NodeList} | |
262 */ | |
263 function find(node, selector) { | |
264 return node.querySelectorAll(selector); | |
265 } | |
266 | |
267 /** | |
268 * For on() and off() if to add/remove the event | |
269 * to the capture phase | |
270 * | |
271 * @type {boolean} | |
272 */ | |
273 var EVENT_CAPTURE = true; | |
274 | |
275 /** | |
276 * Adds an event listener for the specified events. | |
277 * | |
278 * Events should be a space separated list of events. | |
279 * | |
280 * If selector is specified the handler will only be | |
281 * called when the event target matches the selector. | |
282 * | |
283 * @param {!Node} node | |
284 * @param {string} events | |
285 * @param {string} [selector] | |
286 * @param {function(Object)} fn | |
287 * @param {boolean} [capture=false] | |
288 * @see off() | |
289 */ | |
290 // eslint-disable-next-line max-params | |
291 function on(node, events, selector, fn, capture) { | |
292 events.split(' ').forEach(function (event) { | |
293 var handler; | |
294 | |
295 if (isString(selector)) { | |
296 handler = fn['_sce-event-' + event + selector] || function (e) { | |
297 var target = e.target; | |
298 while (target && target !== node) { | |
299 if (is(target, selector)) { | |
300 fn.call(target, e); | |
301 return; | |
302 } | |
303 | |
304 target = target.parentNode; | |
305 } | |
306 }; | |
307 | |
308 fn['_sce-event-' + event + selector] = handler; | |
309 } else { | |
310 handler = selector; | |
311 capture = fn; | |
312 } | |
313 | |
314 node.addEventListener(event, handler, capture || false); | |
315 }); | |
316 } | |
317 | |
318 /** | |
319 * Removes an event listener for the specified events. | |
320 * | |
321 * @param {!Node} node | |
322 * @param {string} events | |
323 * @param {string} [selector] | |
324 * @param {function(Object)} fn | |
325 * @param {boolean} [capture=false] | |
326 * @see on() | |
327 */ | |
328 // eslint-disable-next-line max-params | |
329 function off(node, events, selector, fn, capture) { | |
330 events.split(' ').forEach(function (event) { | |
331 var handler; | |
332 | |
333 if (isString(selector)) { | |
334 handler = fn['_sce-event-' + event + selector]; | |
335 } else { | |
336 handler = selector; | |
337 capture = fn; | |
338 } | |
339 | |
340 node.removeEventListener(event, handler, capture || false); | |
341 }); | |
342 } | |
343 | |
344 /** | |
345 * If only attr param is specified it will get | |
346 * the value of the attr param. | |
347 * | |
348 * If value is specified but null the attribute | |
349 * will be removed otherwise the attr value will | |
350 * be set to the passed value. | |
351 * | |
352 * @param {!HTMLElement} node | |
353 * @param {!string} attr | |
354 * @param {?string} [value] | |
355 */ | |
356 function attr(node, attr, value) { | |
357 if (arguments.length < 3) { | |
358 return node.getAttribute(attr); | |
359 } | |
360 | |
361 // eslint-disable-next-line eqeqeq, no-eq-null | |
362 if (value == null) { | |
363 removeAttr(node, attr); | |
364 } else { | |
365 node.setAttribute(attr, value); | |
366 } | |
367 } | |
368 | |
369 /** | |
370 * Removes the specified attribute | |
371 * | |
372 * @param {!HTMLElement} node | |
373 * @param {!string} attr | |
374 */ | |
375 function removeAttr(node, attr) { | |
376 node.removeAttribute(attr); | |
377 } | |
378 | |
379 /** | |
380 * Sets the passed elements display to none | |
381 * | |
382 * @param {!HTMLElement} node | |
383 */ | |
384 function hide(node) { | |
385 css(node, 'display', 'none'); | |
386 } | |
387 | |
388 /** | |
389 * Sets the passed elements display to default | |
390 * | |
391 * @param {!HTMLElement} node | |
392 */ | |
393 function show(node) { | |
394 css(node, 'display', ''); | |
395 } | |
396 | |
397 /** | |
398 * Toggles an elements visibility | |
399 * | |
400 * @param {!HTMLElement} node | |
401 */ | |
402 function toggle(node) { | |
403 if (isVisible(node)) { | |
404 hide(node); | |
405 } else { | |
406 show(node); | |
407 } | |
408 } | |
409 | |
410 /** | |
411 * Gets a computed CSS values or sets an inline CSS value | |
412 * | |
413 * Rules should be in camelCase format and not | |
414 * hyphenated like CSS properties. | |
415 * | |
416 * @param {!HTMLElement} node | |
417 * @param {!Object|string} rule | |
418 * @param {string|number} [value] | |
419 * @return {string|number|undefined} | |
420 */ | |
421 function css(node, rule, value) { | |
422 if (arguments.length < 3) { | |
423 if (isString(rule)) { | |
424 return node.nodeType === 1 ? getComputedStyle(node)[rule] : null; | |
425 } | |
426 | |
427 each(rule, function (key, value) { | |
428 css(node, key, value); | |
429 }); | |
430 } else { | |
431 // isNaN returns false for null, false and empty strings | |
432 // so need to check it's truthy or 0 | |
433 var isNumeric = (value || value === 0) && !isNaN(value); | |
434 node.style[rule] = isNumeric ? value + 'px' : value; | |
435 } | |
436 } | |
437 | |
438 | |
439 /** | |
440 * Gets or sets the data attributes on a node | |
441 * | |
442 * Unlike the jQuery version this only stores data | |
443 * in the DOM attributes which means only strings | |
444 * can be stored. | |
445 * | |
446 * @param {Node} node | |
447 * @param {string} [key] | |
448 * @param {string} [value] | |
449 * @return {Object|undefined} | |
450 */ | |
451 function data(node, key, value) { | |
452 var argsLength = arguments.length; | |
453 var data = {}; | |
454 | |
455 if (node.nodeType === ELEMENT_NODE) { | |
456 if (argsLength === 1) { | |
457 each(node.attributes, function (_, attr) { | |
458 if (/^data\-/i.test(attr.name)) { | |
459 data[attr.name.substr(5)] = attr.value; | |
460 } | |
461 }); | |
462 | |
463 return data; | |
464 } | |
465 | |
466 if (argsLength === 2) { | |
467 return attr(node, 'data-' + key); | |
468 } | |
469 | |
470 attr(node, 'data-' + key, String(value)); | |
471 } | |
472 } | |
473 | |
474 /** | |
475 * Checks if node matches the given selector. | |
476 * | |
477 * @param {?HTMLElement} node | |
478 * @param {string} selector | |
479 * @returns {boolean} | |
480 */ | |
481 function is(node, selector) { | |
482 var result = false; | |
483 | |
484 if (node && node.nodeType === ELEMENT_NODE) { | |
485 result = (node.matches || node.msMatchesSelector || | |
486 node.webkitMatchesSelector).call(node, selector); | |
487 } | |
488 | |
489 return result; | |
490 } | |
491 | |
492 | |
493 /** | |
494 * Returns true if node contains child otherwise false. | |
495 * | |
496 * This differs from the DOM contains() method in that | |
497 * if node and child are equal this will return false. | |
498 * | |
499 * @param {!Node} node | |
500 * @param {HTMLElement} child | |
501 * @returns {boolean} | |
502 */ | |
503 function contains(node, child) { | |
504 return node !== child && node.contains && node.contains(child); | |
505 } | |
506 | |
507 /** | |
508 * @param {Node} node | |
509 * @param {string} [selector] | |
510 * @returns {?HTMLElement} | |
511 */ | |
512 function previousElementSibling(node, selector) { | |
513 var prev = node.previousElementSibling; | |
514 | |
515 if (selector && prev) { | |
516 return is(prev, selector) ? prev : null; | |
517 } | |
518 | |
519 return prev; | |
520 } | |
521 | |
522 /** | |
523 * @param {!Node} node | |
524 * @param {!Node} refNode | |
525 * @returns {Node} | |
526 */ | |
527 function insertBefore(node, refNode) { | |
528 return refNode.parentNode.insertBefore(node, refNode); | |
529 } | |
530 | |
531 /** | |
532 * @param {?HTMLElement} node | |
533 * @returns {!Array.<string>} | |
534 */ | |
535 function classes(node) { | |
536 return node.className.trim().split(/\s+/); | |
537 } | |
538 | |
539 /** | |
540 * @param {?HTMLElement} node | |
541 * @param {string} className | |
542 * @returns {boolean} | |
543 */ | |
544 function hasClass(node, className) { | |
545 return is(node, '.' + className); | |
546 } | |
547 | |
548 /** | |
549 * @param {!HTMLElement} node | |
550 * @param {string} className | |
551 */ | |
552 function addClass(node, className) { | |
553 var classList = classes(node); | |
554 | |
555 if (classList.indexOf(className) < 0) { | |
556 classList.push(className); | |
557 } | |
558 | |
559 node.className = classList.join(' '); | |
560 } | |
561 | |
562 /** | |
563 * @param {!HTMLElement} node | |
564 * @param {string} className | |
565 */ | |
566 function removeClass(node, className) { | |
567 var classList = classes(node); | |
568 | |
569 arrayRemove(classList, className); | |
570 | |
571 node.className = classList.join(' '); | |
572 } | |
573 | |
574 /** | |
575 * Toggles a class on node. | |
576 * | |
577 * If state is specified and is truthy it will add | |
578 * the class. | |
579 * | |
580 * If state is specified and is falsey it will remove | |
581 * the class. | |
582 * | |
583 * @param {HTMLElement} node | |
584 * @param {string} className | |
585 * @param {boolean} [state] | |
586 */ | |
587 function toggleClass(node, className, state) { | |
588 state = isUndefined(state) ? !hasClass(node, className) : state; | |
589 | |
590 if (state) { | |
591 addClass(node, className); | |
592 } else { | |
593 removeClass(node, className); | |
594 } | |
595 } | |
596 | |
597 /** | |
598 * Gets or sets the width of the passed node. | |
599 * | |
600 * @param {HTMLElement} node | |
601 * @param {number|string} [value] | |
602 * @returns {number|undefined} | |
603 */ | |
604 function width(node, value) { | |
605 if (isUndefined(value)) { | |
606 var cs = getComputedStyle(node); | |
607 var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight); | |
608 var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth); | |
609 | |
610 return node.offsetWidth - padding - border; | |
611 } | |
612 | |
613 css(node, 'width', value); | |
614 } | |
615 | |
616 /** | |
617 * Gets or sets the height of the passed node. | |
618 * | |
619 * @param {HTMLElement} node | |
620 * @param {number|string} [value] | |
621 * @returns {number|undefined} | |
622 */ | |
623 function height(node, value) { | |
624 if (isUndefined(value)) { | |
625 var cs = getComputedStyle(node); | |
626 var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom); | |
627 var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth); | |
628 | |
629 return node.offsetHeight - padding - border; | |
630 } | |
631 | |
632 css(node, 'height', value); | |
633 } | |
634 | |
635 /** | |
636 * Triggers a custom event with the specified name and | |
637 * sets the detail property to the data object passed. | |
638 * | |
639 * @param {HTMLElement} node | |
640 * @param {string} eventName | |
641 * @param {Object} [data] | |
642 */ | |
643 function trigger(node, eventName, data) { | |
644 var event; | |
645 | |
646 if (isFunction(window.CustomEvent)) { | |
647 event = new CustomEvent(eventName, { | |
648 bubbles: true, | |
649 cancelable: true, | |
650 detail: data | |
651 }); | |
652 } else { | |
653 event = node.ownerDocument.createEvent('CustomEvent'); | |
654 event.initCustomEvent(eventName, true, true, data); | |
655 } | |
656 | |
657 node.dispatchEvent(event); | |
658 } | |
659 | |
660 /** | |
661 * Returns if a node is visible. | |
662 * | |
663 * @param {HTMLElement} | |
664 * @returns {boolean} | |
665 */ | |
666 function isVisible(node) { | |
667 return !!node.getClientRects().length; | |
668 } | |
669 | |
670 /** | |
671 * Convert CSS property names into camel case | |
672 * | |
673 * @param {string} string | |
674 * @returns {string} | |
675 */ | |
676 function camelCase(string) { | |
677 return string | |
678 .replace(/^-ms-/, 'ms-') | |
679 .replace(/-(\w)/g, function (match, char) { | |
680 return char.toUpperCase(); | |
681 }); | |
682 } | |
683 | |
684 | |
685 /** | |
686 * Loop all child nodes of the passed node | |
687 * | |
688 * The function should accept 1 parameter being the node. | |
689 * If the function returns false the loop will be exited. | |
690 * | |
691 * @param {HTMLElement} node | |
692 * @param {function} func Callback which is called with every | |
693 * child node as the first argument. | |
694 * @param {boolean} innermostFirst If the innermost node should be passed | |
695 * to the function before it's parents. | |
696 * @param {boolean} siblingsOnly If to only traverse the nodes siblings | |
697 * @param {boolean} [reverse=false] If to traverse the nodes in reverse | |
698 */ | |
699 // eslint-disable-next-line max-params | |
700 function traverse(node, func, innermostFirst, siblingsOnly, reverse) { | |
701 node = reverse ? node.lastChild : node.firstChild; | |
702 | |
703 while (node) { | |
704 var next = reverse ? node.previousSibling : node.nextSibling; | |
705 | |
706 if ( | |
707 (!innermostFirst && func(node) === false) || | |
708 (!siblingsOnly && traverse( | |
709 node, func, innermostFirst, siblingsOnly, reverse | |
710 ) === false) || | |
711 (innermostFirst && func(node) === false) | |
712 ) { | |
713 return false; | |
714 } | |
715 | |
716 node = next; | |
717 } | |
718 } | |
719 | |
720 /** | |
721 * Like traverse but loops in reverse | |
722 * @see traverse | |
723 */ | |
724 function rTraverse(node, func, innermostFirst, siblingsOnly) { | |
725 traverse(node, func, innermostFirst, siblingsOnly, true); | |
726 } | |
727 | |
728 /** | |
729 * Parses HTML into a document fragment | |
730 * | |
731 * @param {string} html | |
732 * @param {Document} [context] | |
733 * @since 1.4.4 | |
734 * @return {DocumentFragment} | |
735 */ | |
736 function parseHTML(html, context) { | |
737 context = context || document; | |
738 | |
739 var ret = context.createDocumentFragment(); | |
740 var tmp = createElement('div', {}, context); | |
741 | |
742 tmp.innerHTML = html; | |
743 | |
744 while (tmp.firstChild) { | |
745 appendChild(ret, tmp.firstChild); | |
746 } | |
747 | |
748 return ret; | |
749 } | |
750 | |
751 /** | |
752 * Checks if an element has any styling. | |
753 * | |
754 * It has styling if it is not a plain <div> or <p> or | |
755 * if it has a class, style attribute or data. | |
756 * | |
757 * @param {HTMLElement} elm | |
758 * @return {boolean} | |
759 * @since 1.4.4 | |
760 */ | |
761 function hasStyling(node) { | |
762 return node && (!is(node, 'p,div') || node.className || | |
763 attr(node, 'style') || !isEmptyObject(data(node))); | |
764 } | |
765 | |
766 /** | |
767 * Converts an element from one type to another. | |
768 * | |
769 * For example it can convert the element <b> to <strong> | |
770 * | |
771 * @param {HTMLElement} element | |
772 * @param {string} toTagName | |
773 * @return {HTMLElement} | |
774 * @since 1.4.4 | |
775 */ | |
776 function convertElement(element, toTagName) { | |
777 var newElement = createElement(toTagName, {}, element.ownerDocument); | |
778 | |
779 each(element.attributes, function (_, attribute) { | |
780 // Some browsers parse invalid attributes names like | |
781 // 'size"2' which throw an exception when set, just | |
782 // ignore these. | |
783 try { | |
784 attr(newElement, attribute.name, attribute.value); | |
785 } catch (ex) {} | |
786 }); | |
787 | |
788 while (element.firstChild) { | |
789 appendChild(newElement, element.firstChild); | |
790 } | |
791 | |
792 element.parentNode.replaceChild(newElement, element); | |
793 | |
794 return newElement; | |
795 } | |
796 | |
797 /** | |
798 * List of block level elements separated by bars (|) | |
799 * | |
800 * @type {string} | |
801 */ | |
802 var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' + | |
803 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|' + | |
804 'details|section|article|aside|nav|main|header|hgroup|footer|fieldset|' + | |
805 'dl|dt|dd|figure|figcaption|'; | |
806 | |
807 /** | |
808 * List of elements that do not allow children separated by bars (|) | |
809 * | |
810 * @param {Node} node | |
811 * @return {boolean} | |
812 * @since 1.4.5 | |
813 */ | |
814 function canHaveChildren(node) { | |
815 // 1 = Element | |
816 // 9 = Document | |
817 // 11 = Document Fragment | |
818 if (!/11?|9/.test(node.nodeType)) { | |
819 return false; | |
820 } | |
821 | |
822 // List of empty HTML tags separated by bar (|) character. | |
823 // Source: http://www.w3.org/TR/html4/index/elements.html | |
824 // Source: http://www.w3.org/TR/html5/syntax.html#void-elements | |
825 return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' + | |
826 '|isindex|link|meta|param|command|embed|keygen|source|track|' + | |
827 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0; | |
828 } | |
829 | |
830 /** | |
831 * Checks if an element is inline | |
832 * | |
833 * @param {HTMLElement} elm | |
834 * @param {boolean} [includeCodeAsBlock=false] | |
835 * @return {boolean} | |
836 */ | |
837 function isInline(elm, includeCodeAsBlock) { | |
838 var tagName, | |
839 nodeType = (elm || {}).nodeType || TEXT_NODE; | |
840 | |
841 if (nodeType !== ELEMENT_NODE) { | |
842 return nodeType === TEXT_NODE; | |
843 } | |
844 | |
845 tagName = elm.tagName.toLowerCase(); | |
846 | |
847 if (tagName === 'code') { | |
848 return !includeCodeAsBlock; | |
849 } | |
850 | |
851 return blockLevelList.indexOf('|' + tagName + '|') < 0; | |
852 } | |
853 | |
854 /** | |
855 * Copy the CSS from 1 node to another. | |
856 * | |
857 * Only copies CSS defined on the element e.g. style attr. | |
858 * | |
859 * @param {HTMLElement} from | |
860 * @param {HTMLElement} to | |
861 * @deprecated since v3.1.0 | |
862 */ | |
863 function copyCSS(from, to) { | |
864 if (to.style && from.style) { | |
865 to.style.cssText = from.style.cssText + to.style.cssText; | |
866 } | |
867 } | |
868 | |
869 /** | |
870 * Checks if a DOM node is empty | |
871 * | |
872 * @param {Node} node | |
873 * @returns {boolean} | |
874 */ | |
875 function isEmpty(node) { | |
876 if (node.lastChild && isEmpty(node.lastChild)) { | |
877 remove(node.lastChild); | |
878 } | |
879 | |
880 return node.nodeType === 3 ? !node.nodeValue : | |
881 (canHaveChildren(node) && !node.childNodes.length); | |
882 } | |
883 | |
884 /** | |
885 * Fixes block level elements inside in inline elements. | |
886 * | |
887 * Also fixes invalid list nesting by placing nested lists | |
888 * inside the previous li tag or wrapping them in an li tag. | |
889 * | |
890 * @param {HTMLElement} node | |
891 */ | |
892 function fixNesting(node) { | |
893 traverse(node, function (node) { | |
894 var list = 'ul,ol', | |
895 isBlock = !isInline(node, true) && node.nodeType !== COMMENT_NODE, | |
896 parent = node.parentNode; | |
897 | |
898 // Any blocklevel element inside an inline element needs fixing. | |
899 // Also <p> tags that contain blocks should be fixed | |
900 if (isBlock && (isInline(parent, true) || parent.tagName === 'P')) { | |
901 // Find the last inline parent node | |
902 var lastInlineParent = node; | |
903 while (isInline(lastInlineParent.parentNode, true) || | |
904 lastInlineParent.parentNode.tagName === 'P') { | |
905 lastInlineParent = lastInlineParent.parentNode; | |
906 } | |
907 | |
908 var before = extractContents(lastInlineParent, node); | |
909 var middle = node; | |
910 | |
911 // Clone inline styling and apply it to the blocks children | |
912 while (parent && isInline(parent, true)) { | |
913 if (parent.nodeType === ELEMENT_NODE) { | |
914 var clone = parent.cloneNode(); | |
915 while (middle.firstChild) { | |
916 appendChild(clone, middle.firstChild); | |
917 } | |
918 | |
919 appendChild(middle, clone); | |
920 } | |
921 parent = parent.parentNode; | |
922 } | |
923 | |
924 insertBefore(middle, lastInlineParent); | |
925 if (!isEmpty(before)) { | |
926 insertBefore(before, middle); | |
927 } | |
928 if (isEmpty(lastInlineParent)) { | |
929 remove(lastInlineParent); | |
930 } | |
931 } | |
932 | |
933 // Fix invalid nested lists which should be wrapped in an li tag | |
934 if (isBlock && is(node, list) && is(node.parentNode, list)) { | |
935 var li = previousElementSibling(node, 'li'); | |
936 | |
937 if (!li) { | |
938 li = createElement('li'); | |
939 insertBefore(li, node); | |
940 } | |
941 | |
942 appendChild(li, node); | |
943 } | |
944 }); | |
945 } | |
946 | |
947 /** | |
948 * Finds the common parent of two nodes | |
949 * | |
950 * @param {!HTMLElement} node1 | |
951 * @param {!HTMLElement} node2 | |
952 * @return {?HTMLElement} | |
953 */ | |
954 function findCommonAncestor(node1, node2) { | |
955 while ((node1 = node1.parentNode)) { | |
956 if (contains(node1, node2)) { | |
957 return node1; | |
958 } | |
959 } | |
960 } | |
961 | |
962 /** | |
963 * @param {?Node} | |
964 * @param {boolean} [previous=false] | |
965 * @returns {?Node} | |
966 */ | |
967 function getSibling(node, previous) { | |
968 if (!node) { | |
969 return null; | |
970 } | |
971 | |
972 return (previous ? node.previousSibling : node.nextSibling) || | |
973 getSibling(node.parentNode, previous); | |
974 } | |
975 | |
976 /** | |
977 * Removes unused whitespace from the root and all it's children. | |
978 * | |
979 * @param {!HTMLElement} root | |
980 * @since 1.4.3 | |
981 */ | |
982 function removeWhiteSpace(root) { | |
983 var nodeValue, nodeType, next, previous, previousSibling, | |
984 nextNode, trimStart, | |
985 cssWhiteSpace = css(root, 'whiteSpace'), | |
986 // Preserve newlines if is pre-line | |
987 preserveNewLines = /line$/i.test(cssWhiteSpace), | |
988 node = root.firstChild; | |
989 | |
990 // Skip pre & pre-wrap with any vendor prefix | |
991 if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) { | |
992 return; | |
993 } | |
994 | |
995 while (node) { | |
996 nextNode = node.nextSibling; | |
997 nodeValue = node.nodeValue; | |
998 nodeType = node.nodeType; | |
999 | |
1000 if (nodeType === ELEMENT_NODE && node.firstChild) { | |
1001 removeWhiteSpace(node); | |
1002 } | |
1003 | |
1004 if (nodeType === TEXT_NODE) { | |
1005 next = getSibling(node); | |
1006 previous = getSibling(node, true); | |
1007 trimStart = false; | |
1008 | |
1009 while (hasClass(previous, 'sceditor-ignore')) { | |
1010 previous = getSibling(previous, true); | |
1011 } | |
1012 | |
1013 // If previous sibling isn't inline or is a textnode that | |
1014 // ends in whitespace, time the start whitespace | |
1015 if (isInline(node) && previous) { | |
1016 previousSibling = previous; | |
1017 | |
1018 while (previousSibling.lastChild) { | |
1019 previousSibling = previousSibling.lastChild; | |
1020 | |
1021 // eslint-disable-next-line max-depth | |
1022 while (hasClass(previousSibling, 'sceditor-ignore')) { | |
1023 previousSibling = getSibling(previousSibling, true); | |
1024 } | |
1025 } | |
1026 | |
1027 trimStart = previousSibling.nodeType === TEXT_NODE ? | |
1028 /[\t\n\r ]$/.test(previousSibling.nodeValue) : | |
1029 !isInline(previousSibling); | |
1030 } | |
1031 | |
1032 // Clear zero width spaces | |
1033 nodeValue = nodeValue.replace(/\u200B/g, ''); | |
1034 | |
1035 // Strip leading whitespace | |
1036 if (!previous || !isInline(previous) || trimStart) { | |
1037 nodeValue = nodeValue.replace( | |
1038 preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/, | |
1039 '' | |
1040 ); | |
1041 } | |
1042 | |
1043 // Strip trailing whitespace | |
1044 if (!next || !isInline(next)) { | |
1045 nodeValue = nodeValue.replace( | |
1046 preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/, | |
1047 '' | |
1048 ); | |
1049 } | |
1050 | |
1051 // Remove empty text nodes | |
1052 if (!nodeValue.length) { | |
1053 remove(node); | |
1054 } else { | |
1055 node.nodeValue = nodeValue.replace( | |
1056 preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g, | |
1057 ' ' | |
1058 ); | |
1059 } | |
1060 } | |
1061 | |
1062 node = nextNode; | |
1063 } | |
1064 } | |
1065 | |
1066 /** | |
1067 * Extracts all the nodes between the start and end nodes | |
1068 * | |
1069 * @param {HTMLElement} startNode The node to start extracting at | |
1070 * @param {HTMLElement} endNode The node to stop extracting at | |
1071 * @return {DocumentFragment} | |
1072 */ | |
1073 function extractContents(startNode, endNode) { | |
1074 var range = startNode.ownerDocument.createRange(); | |
1075 | |
1076 range.setStartBefore(startNode); | |
1077 range.setEndAfter(endNode); | |
1078 | |
1079 return range.extractContents(); | |
1080 } | |
1081 | |
1082 /** | |
1083 * Gets the offset position of an element | |
1084 * | |
1085 * @param {HTMLElement} node | |
1086 * @return {Object} An object with left and top properties | |
1087 */ | |
1088 function getOffset(node) { | |
1089 var left = 0, | |
1090 top = 0; | |
1091 | |
1092 while (node) { | |
1093 left += node.offsetLeft; | |
1094 top += node.offsetTop; | |
1095 node = node.offsetParent; | |
1096 } | |
1097 | |
1098 return { | |
1099 left: left, | |
1100 top: top | |
1101 }; | |
1102 } | |
1103 | |
1104 /** | |
1105 * Gets the value of a CSS property from the elements style attribute | |
1106 * | |
1107 * @param {HTMLElement} elm | |
1108 * @param {string} property | |
1109 * @return {string} | |
1110 */ | |
1111 function getStyle(elm, property) { | |
1112 var styleValue, | |
1113 elmStyle = elm.style; | |
1114 | |
1115 if (!cssPropertyNameCache[property]) { | |
1116 cssPropertyNameCache[property] = camelCase(property); | |
1117 } | |
1118 | |
1119 property = cssPropertyNameCache[property]; | |
1120 styleValue = elmStyle[property]; | |
1121 | |
1122 // Add an exception for text-align | |
1123 if ('textAlign' === property) { | |
1124 styleValue = styleValue || css(elm, property); | |
1125 | |
1126 if (css(elm.parentNode, property) === styleValue || | |
1127 css(elm, 'display') !== 'block' || is(elm, 'hr,th')) { | |
1128 return ''; | |
1129 } | |
1130 } | |
1131 | |
1132 return styleValue; | |
1133 } | |
1134 | |
1135 /** | |
1136 * Tests if an element has a style. | |
1137 * | |
1138 * If values are specified it will check that the styles value | |
1139 * matches one of the values | |
1140 * | |
1141 * @param {HTMLElement} elm | |
1142 * @param {string} property | |
1143 * @param {string|array} [values] | |
1144 * @return {boolean} | |
1145 */ | |
1146 function hasStyle(elm, property, values) { | |
1147 var styleValue = getStyle(elm, property); | |
1148 | |
1149 if (!styleValue) { | |
1150 return false; | |
1151 } | |
1152 | |
1153 return !values || styleValue === values || | |
1154 (Array.isArray(values) && values.indexOf(styleValue) > -1); | |
1155 } | |
1156 | |
1157 /** | |
1158 * Returns true if both nodes have the same number of inline styles and all the | |
1159 * inline styles have matching values | |
1160 * | |
1161 * @param {HTMLElement} nodeA | |
1162 * @param {HTMLElement} nodeB | |
1163 * @returns {boolean} | |
1164 */ | |
1165 function stylesMatch(nodeA, nodeB) { | |
1166 var i = nodeA.style.length; | |
1167 if (i !== nodeB.style.length) { | |
1168 return false; | |
1169 } | |
1170 | |
1171 while (i--) { | |
1172 var prop = nodeA.style[i]; | |
1173 if (nodeA.style[prop] !== nodeB.style[prop]) { | |
1174 return false; | |
1175 } | |
1176 } | |
1177 | |
1178 return true; | |
1179 } | |
1180 | |
1181 /** | |
1182 * Returns true if both nodes have the same number of attributes and all the | |
1183 * attribute values match | |
1184 * | |
1185 * @param {HTMLElement} nodeA | |
1186 * @param {HTMLElement} nodeB | |
1187 * @returns {boolean} | |
1188 */ | |
1189 function attributesMatch(nodeA, nodeB) { | |
1190 var i = nodeA.attributes.length; | |
1191 if (i !== nodeB.attributes.length) { | |
1192 return false; | |
1193 } | |
1194 | |
1195 while (i--) { | |
1196 var prop = nodeA.attributes[i]; | |
1197 var notMatches = prop.name === 'style' ? | |
1198 !stylesMatch(nodeA, nodeB) : | |
1199 prop.value !== attr(nodeB, prop.name); | |
1200 | |
1201 if (notMatches) { | |
1202 return false; | |
1203 } | |
1204 } | |
1205 | |
1206 return true; | |
1207 } | |
1208 | |
1209 /** | |
1210 * Removes an element placing its children in its place | |
1211 * | |
1212 * @param {HTMLElement} node | |
1213 */ | |
1214 function removeKeepChildren(node) { | |
1215 while (node.firstChild) { | |
1216 insertBefore(node.firstChild, node); | |
1217 } | |
1218 | |
1219 remove(node); | |
1220 } | |
1221 | |
1222 /** | |
1223 * Merges inline styles and tags with parents where possible | |
1224 * | |
1225 * @param {Node} node | |
1226 * @since 3.1.0 | |
1227 */ | |
1228 function merge(node) { | |
1229 if (node.nodeType !== ELEMENT_NODE) { | |
1230 return; | |
1231 } | |
1232 | |
1233 var parent = node.parentNode; | |
1234 var tagName = node.tagName; | |
1235 var mergeTags = /B|STRONG|EM|SPAN|FONT/; | |
1236 | |
1237 // Merge children (in reverse as children can be removed) | |
1238 var i = node.childNodes.length; | |
1239 while (i--) { | |
1240 merge(node.childNodes[i]); | |
1241 } | |
1242 | |
1243 // Should only merge inline tags | |
1244 if (!isInline(node)) { | |
1245 return; | |
1246 } | |
1247 | |
1248 // Remove any inline styles that match the parent style | |
1249 i = node.style.length; | |
1250 while (i--) { | |
1251 var prop = node.style[i]; | |
1252 if (css(parent, prop) === css(node, prop)) { | |
1253 node.style.removeProperty(prop); | |
1254 } | |
1255 } | |
1256 | |
1257 // Can only remove / merge tags if no inline styling left. | |
1258 // If there is any inline style left then it means it at least partially | |
1259 // doesn't match the parent style so must stay | |
1260 if (!node.style.length) { | |
1261 removeAttr(node, 'style'); | |
1262 | |
1263 // Remove font attributes if match parent | |
1264 if (tagName === 'FONT') { | |
1265 if (css(node, 'fontFamily').toLowerCase() === | |
1266 css(parent, 'fontFamily').toLowerCase()) { | |
1267 removeAttr(node, 'face'); | |
1268 } | |
1269 | |
1270 if (css(node, 'color') === css(parent, 'color')) { | |
1271 removeAttr(node, 'color'); | |
1272 } | |
1273 | |
1274 if (css(node, 'fontSize') === css(parent, 'fontSize')) { | |
1275 removeAttr(node, 'size'); | |
1276 } | |
1277 } | |
1278 | |
1279 // Spans and font tags with no attributes can be safely removed | |
1280 if (!node.attributes.length && /SPAN|FONT/.test(tagName)) { | |
1281 removeKeepChildren(node); | |
1282 } else if (mergeTags.test(tagName)) { | |
1283 var isBold = /B|STRONG/.test(tagName); | |
1284 var isItalic = tagName === 'EM'; | |
1285 | |
1286 while (parent && isInline(parent) && | |
1287 (!isBold || /bold|700/i.test(css(parent, 'fontWeight'))) && | |
1288 (!isItalic || css(parent, 'fontStyle') === 'italic')) { | |
1289 | |
1290 // Remove if parent match | |
1291 if ((parent.tagName === tagName || | |
1292 (isBold && /B|STRONG/.test(parent.tagName))) && | |
1293 attributesMatch(parent, node)) { | |
1294 removeKeepChildren(node); | |
1295 break; | |
1296 } | |
1297 | |
1298 parent = parent.parentNode; | |
1299 } | |
1300 } | |
1301 } | |
1302 | |
1303 // Merge siblings if attributes, including inline styles, match | |
1304 var next = node.nextSibling; | |
1305 if (next && next.tagName === tagName && attributesMatch(next, node)) { | |
1306 appendChild(node, next); | |
1307 removeKeepChildren(next); | |
1308 } | |
1309 } | |
1310 | |
1311 /** | |
1312 * Default options for SCEditor | |
1313 * @type {Object} | |
1314 */ | |
1315 var defaultOptions = { | |
1316 /** @lends jQuery.sceditor.defaultOptions */ | |
1317 /** | |
1318 * Toolbar buttons order and groups. Should be comma separated and | |
1319 * have a bar | to separate groups | |
1320 * | |
1321 * @type {string} | |
1322 */ | |
1323 toolbar: 'bold,italic,underline,strike,subscript,superscript|' + | |
1324 'left,center,right,justify|font,size,color,removeformat|' + | |
1325 'cut,copy,pastetext|bulletlist,orderedlist,indent,outdent|' + | |
1326 'table|code,quote|horizontalrule,image,email,link,unlink|' + | |
1327 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', | |
1328 | |
1329 /** | |
1330 * Comma separated list of commands to excludes from the toolbar | |
1331 * | |
1332 * @type {string} | |
1333 */ | |
1334 toolbarExclude: null, | |
1335 | |
1336 /** | |
1337 * Stylesheet to include in the WYSIWYG editor. This is what will style | |
1338 * the WYSIWYG elements | |
1339 * | |
1340 * @type {string} | |
1341 */ | |
1342 style: 'jquery.sceditor.default.css', | |
1343 | |
1344 /** | |
1345 * Comma separated list of fonts for the font selector | |
1346 * | |
1347 * @type {string} | |
1348 */ | |
1349 fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + | |
1350 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', | |
1351 | |
1352 /** | |
1353 * Colors should be comma separated and have a bar | to signal a new | |
1354 * column. | |
1355 * | |
1356 * If null the colors will be auto generated. | |
1357 * | |
1358 * @type {string} | |
1359 */ | |
1360 colors: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' + | |
1361 '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' + | |
1362 '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' + | |
1363 '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' + | |
1364 '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' + | |
1365 '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' + | |
1366 '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' + | |
1367 '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef', | |
1368 | |
1369 /** | |
1370 * The locale to use. | |
1371 * @type {string} | |
1372 */ | |
1373 locale: attr(document.documentElement, 'lang') || 'en', | |
1374 | |
1375 /** | |
1376 * The Charset to use | |
1377 * @type {string} | |
1378 */ | |
1379 charset: 'utf-8', | |
1380 | |
1381 /** | |
1382 * Compatibility mode for emoticons. | |
1383 * | |
1384 * Helps if you have emoticons such as :/ which would put an emoticon | |
1385 * inside http:// | |
1386 * | |
1387 * This mode requires emoticons to be surrounded by whitespace or end of | |
1388 * line chars. This mode has limited As You Type emoticon conversion | |
1389 * support. It will not replace AYT for end of line chars, only | |
1390 * emoticons surrounded by whitespace. They will still be replaced | |
1391 * correctly when loaded just not AYT. | |
1392 * | |
1393 * @type {boolean} | |
1394 */ | |
1395 emoticonsCompat: false, | |
1396 | |
1397 /** | |
1398 * If to enable emoticons. Can be changes at runtime using the | |
1399 * emoticons() method. | |
1400 * | |
1401 * @type {boolean} | |
1402 * @since 1.4.2 | |
1403 */ | |
1404 emoticonsEnabled: true, | |
1405 | |
1406 /** | |
1407 * Emoticon root URL | |
1408 * | |
1409 * @type {string} | |
1410 */ | |
1411 emoticonsRoot: '', | |
1412 emoticons: { | |
1413 dropdown: { | |
1414 ':)': 'emoticons/smile.png', | |
1415 ':angel:': 'emoticons/angel.png', | |
1416 ':angry:': 'emoticons/angry.png', | |
1417 '8-)': 'emoticons/cool.png', | |
1418 ':\'(': 'emoticons/cwy.png', | |
1419 ':ermm:': 'emoticons/ermm.png', | |
1420 ':D': 'emoticons/grin.png', | |
1421 '<3': 'emoticons/heart.png', | |
1422 ':(': 'emoticons/sad.png', | |
1423 ':O': 'emoticons/shocked.png', | |
1424 ':P': 'emoticons/tongue.png', | |
1425 ';)': 'emoticons/wink.png' | |
1426 }, | |
1427 more: { | |
1428 ':alien:': 'emoticons/alien.png', | |
1429 ':blink:': 'emoticons/blink.png', | |
1430 ':blush:': 'emoticons/blush.png', | |
1431 ':cheerful:': 'emoticons/cheerful.png', | |
1432 ':devil:': 'emoticons/devil.png', | |
1433 ':dizzy:': 'emoticons/dizzy.png', | |
1434 ':getlost:': 'emoticons/getlost.png', | |
1435 ':happy:': 'emoticons/happy.png', | |
1436 ':kissing:': 'emoticons/kissing.png', | |
1437 ':ninja:': 'emoticons/ninja.png', | |
1438 ':pinch:': 'emoticons/pinch.png', | |
1439 ':pouty:': 'emoticons/pouty.png', | |
1440 ':sick:': 'emoticons/sick.png', | |
1441 ':sideways:': 'emoticons/sideways.png', | |
1442 ':silly:': 'emoticons/silly.png', | |
1443 ':sleeping:': 'emoticons/sleeping.png', | |
1444 ':unsure:': 'emoticons/unsure.png', | |
1445 ':woot:': 'emoticons/w00t.png', | |
1446 ':wassat:': 'emoticons/wassat.png' | |
1447 }, | |
1448 hidden: { | |
1449 ':whistling:': 'emoticons/whistling.png', | |
1450 ':love:': 'emoticons/wub.png' | |
1451 } | |
1452 }, | |
1453 | |
1454 /** | |
1455 * Width of the editor. Set to null for automatic with | |
1456 * | |
1457 * @type {?number} | |
1458 */ | |
1459 width: null, | |
1460 | |
1461 /** | |
1462 * Height of the editor including toolbar. Set to null for automatic | |
1463 * height | |
1464 * | |
1465 * @type {?number} | |
1466 */ | |
1467 height: null, | |
1468 | |
1469 /** | |
1470 * If to allow the editor to be resized | |
1471 * | |
1472 * @type {boolean} | |
1473 */ | |
1474 resizeEnabled: true, | |
1475 | |
1476 /** | |
1477 * Min resize to width, set to null for half textarea width or -1 for | |
1478 * unlimited | |
1479 * | |
1480 * @type {?number} | |
1481 */ | |
1482 resizeMinWidth: null, | |
1483 /** | |
1484 * Min resize to height, set to null for half textarea height or -1 for | |
1485 * unlimited | |
1486 * | |
1487 * @type {?number} | |
1488 */ | |
1489 resizeMinHeight: null, | |
1490 /** | |
1491 * Max resize to height, set to null for double textarea height or -1 | |
1492 * for unlimited | |
1493 * | |
1494 * @type {?number} | |
1495 */ | |
1496 resizeMaxHeight: null, | |
1497 /** | |
1498 * Max resize to width, set to null for double textarea width or -1 for | |
1499 * unlimited | |
1500 * | |
1501 * @type {?number} | |
1502 */ | |
1503 resizeMaxWidth: null, | |
1504 /** | |
1505 * If resizing by height is enabled | |
1506 * | |
1507 * @type {boolean} | |
1508 */ | |
1509 resizeHeight: true, | |
1510 /** | |
1511 * If resizing by width is enabled | |
1512 * | |
1513 * @type {boolean} | |
1514 */ | |
1515 resizeWidth: true, | |
1516 | |
1517 /** | |
1518 * Date format, will be overridden if locale specifies one. | |
1519 * | |
1520 * The words year, month and day will be replaced with the users current | |
1521 * year, month and day. | |
1522 * | |
1523 * @type {string} | |
1524 */ | |
1525 dateFormat: 'year-month-day', | |
1526 | |
1527 /** | |
1528 * Element to inset the toolbar into. | |
1529 * | |
1530 * @type {HTMLElement} | |
1531 */ | |
1532 toolbarContainer: null, | |
1533 | |
1534 /** | |
1535 * If to enable paste filtering. This is currently experimental, please | |
1536 * report any issues. | |
1537 * | |
1538 * @type {boolean} | |
1539 */ | |
1540 enablePasteFiltering: false, | |
1541 | |
1542 /** | |
1543 * If to completely disable pasting into the editor | |
1544 * | |
1545 * @type {boolean} | |
1546 */ | |
1547 disablePasting: false, | |
1548 | |
1549 /** | |
1550 * If the editor is read only. | |
1551 * | |
1552 * @type {boolean} | |
1553 */ | |
1554 readOnly: false, | |
1555 | |
1556 /** | |
1557 * If to set the editor to right-to-left mode. | |
1558 * | |
1559 * If set to null the direction will be automatically detected. | |
1560 * | |
1561 * @type {boolean} | |
1562 */ | |
1563 rtl: false, | |
1564 | |
1565 /** | |
1566 * If to auto focus the editor on page load | |
1567 * | |
1568 * @type {boolean} | |
1569 */ | |
1570 autofocus: false, | |
1571 | |
1572 /** | |
1573 * If to auto focus the editor to the end of the content | |
1574 * | |
1575 * @type {boolean} | |
1576 */ | |
1577 autofocusEnd: true, | |
1578 | |
1579 /** | |
1580 * If to auto expand the editor to fix the content | |
1581 * | |
1582 * @type {boolean} | |
1583 */ | |
1584 autoExpand: false, | |
1585 | |
1586 /** | |
1587 * If to auto update original textbox on blur | |
1588 * | |
1589 * @type {boolean} | |
1590 */ | |
1591 autoUpdate: false, | |
1592 | |
1593 /** | |
1594 * If to enable the browsers built in spell checker | |
1595 * | |
1596 * @type {boolean} | |
1597 */ | |
1598 spellcheck: true, | |
1599 | |
1600 /** | |
1601 * If to run the source editor when there is no WYSIWYG support. Only | |
1602 * really applies to mobile OS's. | |
1603 * | |
1604 * @type {boolean} | |
1605 */ | |
1606 runWithoutWysiwygSupport: false, | |
1607 | |
1608 /** | |
1609 * If to load the editor in source mode and still allow switching | |
1610 * between WYSIWYG and source mode | |
1611 * | |
1612 * @type {boolean} | |
1613 */ | |
1614 startInSourceMode: false, | |
1615 | |
1616 /** | |
1617 * Optional ID to give the editor. | |
1618 * | |
1619 * @type {string} | |
1620 */ | |
1621 id: null, | |
1622 | |
1623 /** | |
1624 * Comma separated list of plugins | |
1625 * | |
1626 * @type {string} | |
1627 */ | |
1628 plugins: '', | |
1629 | |
1630 /** | |
1631 * z-index to set the editor container to. Needed for jQuery UI dialog. | |
1632 * | |
1633 * @type {?number} | |
1634 */ | |
1635 zIndex: null, | |
1636 | |
1637 /** | |
1638 * If to trim the BBCode. Removes any spaces at the start and end of the | |
1639 * BBCode string. | |
1640 * | |
1641 * @type {boolean} | |
1642 */ | |
1643 bbcodeTrim: false, | |
1644 | |
1645 /** | |
1646 * If to disable removing block level elements by pressing backspace at | |
1647 * the start of them | |
1648 * | |
1649 * @type {boolean} | |
1650 */ | |
1651 disableBlockRemove: false, | |
1652 | |
1653 /** | |
1654 * Array of allowed URL (should be either strings or regex) for iframes. | |
1655 * | |
1656 * If it's a string then iframes where the start of the src matches the | |
1657 * specified string will be allowed. | |
1658 * | |
1659 * If it's a regex then iframes where the src matches the regex will be | |
1660 * allowed. | |
1661 * | |
1662 * @type {Array} | |
1663 */ | |
1664 allowedIframeUrls: [], | |
1665 | |
1666 /** | |
1667 * BBCode parser options, only applies if using the editor in BBCode | |
1668 * mode. | |
1669 * | |
1670 * See SCEditor.BBCodeParser.defaults for list of valid options | |
1671 * | |
1672 * @type {Object} | |
1673 */ | |
1674 parserOptions: { }, | |
1675 | |
1676 /** | |
1677 * CSS that will be added to the to dropdown menu (eg. z-index) | |
1678 * | |
1679 * @type {Object} | |
1680 */ | |
1681 dropDownCss: { } | |
1682 }; | |
1683 | |
1684 // Must start with a valid scheme | |
1685 // ^ | |
1686 // Schemes that are considered safe | |
1687 // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):| | |
1688 // Relative schemes (//:) are considered safe | |
1689 // (\\/\\/)| | |
1690 // Image data URI's are considered safe | |
1691 // data:image\\/(png|bmp|gif|p?jpe?g); | |
1692 var VALID_SCHEME_REGEX = | |
1693 /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i; | |
1694 | |
1695 /** | |
1696 * Escapes a string so it's safe to use in regex | |
1697 * | |
1698 * @param {string} str | |
1699 * @return {string} | |
1700 */ | |
1701 function regex(str) { | |
1702 return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); | |
1703 } | |
1704 /** | |
1705 * Escapes all HTML entities in a string | |
1706 * | |
1707 * If noQuotes is set to false, all single and double | |
1708 * quotes will also be escaped | |
1709 * | |
1710 * @param {string} str | |
1711 * @param {boolean} [noQuotes=true] | |
1712 * @return {string} | |
1713 * @since 1.4.1 | |
1714 */ | |
1715 function entities(str, noQuotes) { | |
1716 if (!str) { | |
1717 return str; | |
1718 } | |
1719 | |
1720 var replacements = { | |
1721 '&': '&', | |
1722 '<': '<', | |
1723 '>': '>', | |
1724 ' ': ' ', | |
1725 '\r\n': '<br />', | |
1726 '\r': '<br />', | |
1727 '\n': '<br />' | |
1728 }; | |
1729 | |
1730 if (noQuotes !== false) { | |
1731 replacements['"'] = '"'; | |
1732 replacements['\''] = '''; | |
1733 replacements['`'] = '`'; | |
1734 } | |
1735 | |
1736 str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) { | |
1737 return replacements[match] || match; | |
1738 }); | |
1739 | |
1740 return str; | |
1741 } | |
1742 /** | |
1743 * Escape URI scheme. | |
1744 * | |
1745 * Appends the current URL to a url if it has a scheme that is not: | |
1746 * | |
1747 * http | |
1748 * https | |
1749 * sftp | |
1750 * ftp | |
1751 * mailto | |
1752 * spotify | |
1753 * skype | |
1754 * ssh | |
1755 * teamspeak | |
1756 * tel | |
1757 * // | |
1758 * data:image/(png|jpeg|jpg|pjpeg|bmp|gif); | |
1759 * | |
1760 * **IMPORTANT**: This does not escape any HTML in a url, for | |
1761 * that use the escape.entities() method. | |
1762 * | |
1763 * @param {string} url | |
1764 * @return {string} | |
1765 * @since 1.4.5 | |
1766 */ | |
1767 function uriScheme(url) { | |
1768 var path, | |
1769 // If there is a : before a / then it has a scheme | |
1770 hasScheme = /^[^\/]*:/i, | |
1771 location = window.location; | |
1772 | |
1773 // Has no scheme or a valid scheme | |
1774 if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) { | |
1775 return url; | |
1776 } | |
1777 | |
1778 path = location.pathname.split('/'); | |
1779 path.pop(); | |
1780 | |
1781 return location.protocol + '//' + | |
1782 location.host + | |
1783 path.join('/') + '/' + | |
1784 url; | |
1785 } | |
1786 | |
1787 /** | |
1788 * HTML templates used by the editor and default commands | |
1789 * @type {Object} | |
1790 * @private | |
1791 */ | |
1792 var _templates = { | |
1793 html: | |
1794 '<!DOCTYPE html>' + | |
1795 '<html{attrs}>' + | |
1796 '<head>' + | |
1797 '<meta http-equiv="Content-Type" ' + | |
1798 'content="text/html;charset={charset}" />' + | |
1799 '<link rel="stylesheet" type="text/css" href="{style}" />' + | |
1800 '</head>' + | |
1801 '<body contenteditable="true" {spellcheck}><p></p></body>' + | |
1802 '</html>', | |
1803 | |
1804 toolbarButton: '<a class="sceditor-button sceditor-button-{name}" ' + | |
1805 'data-sceditor-command="{name}" unselectable="on">' + | |
1806 '<div unselectable="on">{dispName}</div></a>', | |
1807 | |
1808 emoticon: '<img src="{url}" data-sceditor-emoticon="{key}" ' + | |
1809 'alt="{key}" title="{tooltip}" />', | |
1810 | |
1811 fontOpt: '<a class="sceditor-font-option" href="#" ' + | |
1812 'data-font="{font}"><font face="{font}">{font}</font></a>', | |
1813 | |
1814 sizeOpt: '<a class="sceditor-fontsize-option" data-size="{size}" ' + | |
1815 'href="#"><font size="{size}">{size}</font></a>', | |
1816 | |
1817 pastetext: | |
1818 '<div><label for="txt">{label}</label> ' + | |
1819 '<textarea cols="20" rows="7" id="txt"></textarea></div>' + | |
1820 '<div><input type="button" class="button" value="{insert}" />' + | |
1821 '</div>', | |
1822 | |
1823 table: | |
1824 '<div><label for="rows">{rows}</label><input type="text" ' + | |
1825 'id="rows" value="2" /></div>' + | |
1826 '<div><label for="cols">{cols}</label><input type="text" ' + | |
1827 'id="cols" value="2" /></div>' + | |
1828 '<div><input type="button" class="button" value="{insert}"' + | |
1829 ' /></div>', | |
1830 | |
1831 image: | |
1832 '<div><label for="image">{url}</label> ' + | |
1833 '<input type="text" id="image" dir="ltr" placeholder="https://" /></div>' + | |
1834 '<div><label for="width">{width}</label> ' + | |
1835 '<input type="text" id="width" size="2" dir="ltr" /></div>' + | |
1836 '<div><label for="height">{height}</label> ' + | |
1837 '<input type="text" id="height" size="2" dir="ltr" /></div>' + | |
1838 '<div><input type="button" class="button" value="{insert}" />' + | |
1839 '</div>', | |
1840 | |
1841 email: | |
1842 '<div><label for="email">{label}</label> ' + | |
1843 '<input type="text" id="email" dir="ltr" /></div>' + | |
1844 '<div><label for="des">{desc}</label> ' + | |
1845 '<input type="text" id="des" /></div>' + | |
1846 '<div><input type="button" class="button" value="{insert}" />' + | |
1847 '</div>', | |
1848 | |
1849 link: | |
1850 '<div><label for="link">{url}</label> ' + | |
1851 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' + | |
1852 '<div><label for="des">{desc}</label> ' + | |
1853 '<input type="text" id="des" /></div>' + | |
1854 '<div><input type="button" class="button" value="{ins}" /></div>', | |
1855 | |
1856 youtubeMenu: | |
1857 '<div><label for="link">{label}</label> ' + | |
1858 '<input type="text" id="link" dir="ltr" placeholder="https://" /></div>' + | |
1859 '<div><input type="button" class="button" value="{insert}" />' + | |
1860 '</div>', | |
1861 | |
1862 youtube: | |
1863 '<iframe width="560" height="315" frameborder="0" allowfullscreen ' + | |
1864 'src="https://www.youtube-nocookie.com/embed/{id}?wmode=opaque&start={time}" ' + | |
1865 'data-youtube-id="{id}"></iframe>' | |
1866 }; | |
1867 | |
1868 /** | |
1869 * Replaces any params in a template with the passed params. | |
1870 * | |
1871 * If createHtml is passed it will return a DocumentFragment | |
1872 * containing the parsed template. | |
1873 * | |
1874 * @param {string} name | |
1875 * @param {Object} [params] | |
1876 * @param {boolean} [createHtml] | |
1877 * @returns {string|DocumentFragment} | |
1878 * @private | |
1879 */ | |
1880 function _tmpl (name, params, createHtml) { | |
1881 var template = _templates[name]; | |
1882 | |
1883 Object.keys(params).forEach(function (name) { | |
1884 template = template.replace( | |
1885 new RegExp(regex('{' + name + '}'), 'g'), params[name] | |
1886 ); | |
1887 }); | |
1888 | |
1889 if (createHtml) { | |
1890 template = parseHTML(template); | |
1891 } | |
1892 | |
1893 return template; | |
1894 } | |
1895 | |
1896 /** | |
1897 * Fixes a bug in FF where it sometimes wraps | |
1898 * new lines in their own list item. | |
1899 * See issue #359 | |
1900 */ | |
1901 function fixFirefoxListBug(editor) { | |
1902 // Only apply to Firefox as will break other browsers. | |
1903 if ('mozHidden' in document) { | |
1904 var node = editor.getBody(); | |
1905 var next; | |
1906 | |
1907 while (node) { | |
1908 next = node; | |
1909 | |
1910 if (next.firstChild) { | |
1911 next = next.firstChild; | |
1912 } else { | |
1913 | |
1914 while (next && !next.nextSibling) { | |
1915 next = next.parentNode; | |
1916 } | |
1917 | |
1918 if (next) { | |
1919 next = next.nextSibling; | |
1920 } | |
1921 } | |
1922 | |
1923 if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) { | |
1924 // Only remove if newlines are collapsed | |
1925 if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) { | |
1926 remove(node); | |
1927 } | |
1928 } | |
1929 | |
1930 node = next; | |
1931 } | |
1932 } | |
1933 } | |
1934 | |
1935 | |
1936 /** | |
1937 * Map of all the commands for SCEditor | |
1938 * @type {Object} | |
1939 * @name commands | |
1940 * @memberOf jQuery.sceditor | |
1941 */ | |
1942 var defaultCmds = { | |
1943 // START_COMMAND: Bold | |
1944 bold: { | |
1945 exec: 'bold', | |
1946 tooltip: 'Bold', | |
1947 shortcut: 'Ctrl+B' | |
1948 }, | |
1949 // END_COMMAND | |
1950 // START_COMMAND: Italic | |
1951 italic: { | |
1952 exec: 'italic', | |
1953 tooltip: 'Italic', | |
1954 shortcut: 'Ctrl+I' | |
1955 }, | |
1956 // END_COMMAND | |
1957 // START_COMMAND: Underline | |
1958 underline: { | |
1959 exec: 'underline', | |
1960 tooltip: 'Underline', | |
1961 shortcut: 'Ctrl+U' | |
1962 }, | |
1963 // END_COMMAND | |
1964 // START_COMMAND: Strikethrough | |
1965 strike: { | |
1966 exec: 'strikethrough', | |
1967 tooltip: 'Strikethrough' | |
1968 }, | |
1969 // END_COMMAND | |
1970 // START_COMMAND: Subscript | |
1971 subscript: { | |
1972 exec: 'subscript', | |
1973 tooltip: 'Subscript' | |
1974 }, | |
1975 // END_COMMAND | |
1976 // START_COMMAND: Superscript | |
1977 superscript: { | |
1978 exec: 'superscript', | |
1979 tooltip: 'Superscript' | |
1980 }, | |
1981 // END_COMMAND | |
1982 | |
1983 // START_COMMAND: Left | |
1984 left: { | |
1985 state: function (node) { | |
1986 if (node && node.nodeType === 3) { | |
1987 node = node.parentNode; | |
1988 } | |
1989 | |
1990 if (node) { | |
1991 var isLtr = css(node, 'direction') === 'ltr'; | |
1992 var align = css(node, 'textAlign'); | |
1993 | |
1994 // Can be -moz-left | |
1995 return /left/.test(align) || | |
1996 align === (isLtr ? 'start' : 'end'); | |
1997 } | |
1998 }, | |
1999 exec: 'justifyleft', | |
2000 tooltip: 'Align left' | |
2001 }, | |
2002 // END_COMMAND | |
2003 // START_COMMAND: Centre | |
2004 center: { | |
2005 exec: 'justifycenter', | |
2006 tooltip: 'Center' | |
2007 }, | |
2008 // END_COMMAND | |
2009 // START_COMMAND: Right | |
2010 right: { | |
2011 state: function (node) { | |
2012 if (node && node.nodeType === 3) { | |
2013 node = node.parentNode; | |
2014 } | |
2015 | |
2016 if (node) { | |
2017 var isLtr = css(node, 'direction') === 'ltr'; | |
2018 var align = css(node, 'textAlign'); | |
2019 | |
2020 // Can be -moz-right | |
2021 return /right/.test(align) || | |
2022 align === (isLtr ? 'end' : 'start'); | |
2023 } | |
2024 }, | |
2025 exec: 'justifyright', | |
2026 tooltip: 'Align right' | |
2027 }, | |
2028 // END_COMMAND | |
2029 // START_COMMAND: Justify | |
2030 justify: { | |
2031 exec: 'justifyfull', | |
2032 tooltip: 'Justify' | |
2033 }, | |
2034 // END_COMMAND | |
2035 | |
2036 // START_COMMAND: Font | |
2037 font: { | |
2038 _dropDown: function (editor, caller, callback) { | |
2039 var content = createElement('div'); | |
2040 | |
2041 on(content, 'click', 'a', function (e) { | |
2042 callback(data(this, 'font')); | |
2043 editor.closeDropDown(true); | |
2044 e.preventDefault(); | |
2045 }); | |
2046 | |
2047 editor.opts.fonts.split(',').forEach(function (font) { | |
2048 appendChild(content, _tmpl('fontOpt', { | |
2049 font: font | |
2050 }, true)); | |
2051 }); | |
2052 | |
2053 editor.createDropDown(caller, 'font-picker', content); | |
2054 }, | |
2055 exec: function (caller) { | |
2056 var editor = this; | |
2057 | |
2058 defaultCmds.font._dropDown(editor, caller, function (fontName) { | |
2059 editor.execCommand('fontname', fontName); | |
2060 }); | |
2061 }, | |
2062 tooltip: 'Font Name' | |
2063 }, | |
2064 // END_COMMAND | |
2065 // START_COMMAND: Size | |
2066 size: { | |
2067 _dropDown: function (editor, caller, callback) { | |
2068 var content = createElement('div'); | |
2069 | |
2070 on(content, 'click', 'a', function (e) { | |
2071 callback(data(this, 'size')); | |
2072 editor.closeDropDown(true); | |
2073 e.preventDefault(); | |
2074 }); | |
2075 | |
2076 for (var i = 1; i <= 7; i++) { | |
2077 appendChild(content, _tmpl('sizeOpt', { | |
2078 size: i | |
2079 }, true)); | |
2080 } | |
2081 | |
2082 editor.createDropDown(caller, 'fontsize-picker', content); | |
2083 }, | |
2084 exec: function (caller) { | |
2085 var editor = this; | |
2086 | |
2087 defaultCmds.size._dropDown(editor, caller, function (fontSize) { | |
2088 editor.execCommand('fontsize', fontSize); | |
2089 }); | |
2090 }, | |
2091 tooltip: 'Font Size' | |
2092 }, | |
2093 // END_COMMAND | |
2094 // START_COMMAND: Colour | |
2095 color: { | |
2096 _dropDown: function (editor, caller, callback) { | |
2097 var content = createElement('div'), | |
2098 html = '', | |
2099 cmd = defaultCmds.color; | |
2100 | |
2101 if (!cmd._htmlCache) { | |
2102 editor.opts.colors.split('|').forEach(function (column) { | |
2103 html += '<div class="sceditor-color-column">'; | |
2104 | |
2105 column.split(',').forEach(function (color) { | |
2106 html += | |
2107 '<a href="#" class="sceditor-color-option"' + | |
2108 ' style="background-color: ' + color + '"' + | |
2109 ' data-color="' + color + '"></a>'; | |
2110 }); | |
2111 | |
2112 html += '</div>'; | |
2113 }); | |
2114 | |
2115 cmd._htmlCache = html; | |
2116 } | |
2117 | |
2118 appendChild(content, parseHTML(cmd._htmlCache)); | |
2119 | |
2120 on(content, 'click', 'a', function (e) { | |
2121 callback(data(this, 'color')); | |
2122 editor.closeDropDown(true); | |
2123 e.preventDefault(); | |
2124 }); | |
2125 | |
2126 editor.createDropDown(caller, 'color-picker', content); | |
2127 }, | |
2128 exec: function (caller) { | |
2129 var editor = this; | |
2130 | |
2131 defaultCmds.color._dropDown(editor, caller, function (color) { | |
2132 editor.execCommand('forecolor', color); | |
2133 }); | |
2134 }, | |
2135 tooltip: 'Font Color' | |
2136 }, | |
2137 // END_COMMAND | |
2138 // START_COMMAND: Remove Format | |
2139 removeformat: { | |
2140 exec: 'removeformat', | |
2141 tooltip: 'Remove Formatting' | |
2142 }, | |
2143 // END_COMMAND | |
2144 | |
2145 // START_COMMAND: Cut | |
2146 cut: { | |
2147 exec: 'cut', | |
2148 tooltip: 'Cut', | |
2149 errorMessage: 'Your browser does not allow the cut command. ' + | |
2150 'Please use the keyboard shortcut Ctrl/Cmd-X' | |
2151 }, | |
2152 // END_COMMAND | |
2153 // START_COMMAND: Copy | |
2154 copy: { | |
2155 exec: 'copy', | |
2156 tooltip: 'Copy', | |
2157 errorMessage: 'Your browser does not allow the copy command. ' + | |
2158 'Please use the keyboard shortcut Ctrl/Cmd-C' | |
2159 }, | |
2160 // END_COMMAND | |
2161 // START_COMMAND: Paste | |
2162 paste: { | |
2163 exec: 'paste', | |
2164 tooltip: 'Paste', | |
2165 errorMessage: 'Your browser does not allow the paste command. ' + | |
2166 'Please use the keyboard shortcut Ctrl/Cmd-V' | |
2167 }, | |
2168 // END_COMMAND | |
2169 // START_COMMAND: Paste Text | |
2170 pastetext: { | |
2171 exec: function (caller) { | |
2172 var val, | |
2173 content = createElement('div'), | |
2174 editor = this; | |
2175 | |
2176 appendChild(content, _tmpl('pastetext', { | |
2177 label: editor._( | |
2178 'Paste your text inside the following box:' | |
2179 ), | |
2180 insert: editor._('Insert') | |
2181 }, true)); | |
2182 | |
2183 on(content, 'click', '.button', function (e) { | |
2184 val = find(content, '#txt')[0].value; | |
2185 | |
2186 if (val) { | |
2187 editor.wysiwygEditorInsertText(val); | |
2188 } | |
2189 | |
2190 editor.closeDropDown(true); | |
2191 e.preventDefault(); | |
2192 }); | |
2193 | |
2194 editor.createDropDown(caller, 'pastetext', content); | |
2195 }, | |
2196 tooltip: 'Paste Text' | |
2197 }, | |
2198 // END_COMMAND | |
2199 // START_COMMAND: Bullet List | |
2200 bulletlist: { | |
2201 exec: function () { | |
2202 fixFirefoxListBug(this); | |
2203 this.execCommand('insertunorderedlist'); | |
2204 }, | |
2205 tooltip: 'Bullet list' | |
2206 }, | |
2207 // END_COMMAND | |
2208 // START_COMMAND: Ordered List | |
2209 orderedlist: { | |
2210 exec: function () { | |
2211 fixFirefoxListBug(this); | |
2212 this.execCommand('insertorderedlist'); | |
2213 }, | |
2214 tooltip: 'Numbered list' | |
2215 }, | |
2216 // END_COMMAND | |
2217 // START_COMMAND: Indent | |
2218 indent: { | |
2219 state: function (parent, firstBlock) { | |
2220 // Only works with lists, for now | |
2221 var range, startParent, endParent; | |
2222 | |
2223 if (is(firstBlock, 'li')) { | |
2224 return 0; | |
2225 } | |
2226 | |
2227 if (is(firstBlock, 'ul,ol,menu')) { | |
2228 // if the whole list is selected, then this must be | |
2229 // invalidated because the browser will place a | |
2230 // <blockquote> there | |
2231 range = this.getRangeHelper().selectedRange(); | |
2232 | |
2233 startParent = range.startContainer.parentNode; | |
2234 endParent = range.endContainer.parentNode; | |
2235 | |
2236 // TODO: could use nodeType for this? | |
2237 // Maybe just check the firstBlock contains both the start | |
2238 //and end containers | |
2239 | |
2240 // Select the tag, not the textNode | |
2241 // (that's why the parentNode) | |
2242 if (startParent !== | |
2243 startParent.parentNode.firstElementChild || | |
2244 // work around a bug in FF | |
2245 (is(endParent, 'li') && endParent !== | |
2246 endParent.parentNode.lastElementChild)) { | |
2247 return 0; | |
2248 } | |
2249 } | |
2250 | |
2251 return -1; | |
2252 }, | |
2253 exec: function () { | |
2254 var editor = this, | |
2255 block = editor.getRangeHelper().getFirstBlockParent(); | |
2256 | |
2257 editor.focus(); | |
2258 | |
2259 // An indent system is quite complicated as there are loads | |
2260 // of complications and issues around how to indent text | |
2261 // As default, let's just stay with indenting the lists, | |
2262 // at least, for now. | |
2263 if (closest(block, 'ul,ol,menu')) { | |
2264 editor.execCommand('indent'); | |
2265 } | |
2266 }, | |
2267 tooltip: 'Add indent' | |
2268 }, | |
2269 // END_COMMAND | |
2270 // START_COMMAND: Outdent | |
2271 outdent: { | |
2272 state: function (parents, firstBlock) { | |
2273 return closest(firstBlock, 'ul,ol,menu') ? 0 : -1; | |
2274 }, | |
2275 exec: function () { | |
2276 var block = this.getRangeHelper().getFirstBlockParent(); | |
2277 if (closest(block, 'ul,ol,menu')) { | |
2278 this.execCommand('outdent'); | |
2279 } | |
2280 }, | |
2281 tooltip: 'Remove one indent' | |
2282 }, | |
2283 // END_COMMAND | |
2284 | |
2285 // START_COMMAND: Table | |
2286 table: { | |
2287 exec: function (caller) { | |
2288 var editor = this, | |
2289 content = createElement('div'); | |
2290 | |
2291 appendChild(content, _tmpl('table', { | |
2292 rows: editor._('Rows:'), | |
2293 cols: editor._('Cols:'), | |
2294 insert: editor._('Insert') | |
2295 }, true)); | |
2296 | |
2297 on(content, 'click', '.button', function (e) { | |
2298 var rows = Number(find(content, '#rows')[0].value), | |
2299 cols = Number(find(content, '#cols')[0].value), | |
2300 html = '<table>'; | |
2301 | |
2302 if (rows > 0 && cols > 0) { | |
2303 html += Array(rows + 1).join( | |
2304 '<tr>' + | |
2305 Array(cols + 1).join( | |
2306 '<td><br /></td>' | |
2307 ) + | |
2308 '</tr>' | |
2309 ); | |
2310 | |
2311 html += '</table>'; | |
2312 | |
2313 editor.wysiwygEditorInsertHtml(html); | |
2314 editor.closeDropDown(true); | |
2315 e.preventDefault(); | |
2316 } | |
2317 }); | |
2318 | |
2319 editor.createDropDown(caller, 'inserttable', content); | |
2320 }, | |
2321 tooltip: 'Insert a table' | |
2322 }, | |
2323 // END_COMMAND | |
2324 | |
2325 // START_COMMAND: Horizontal Rule | |
2326 horizontalrule: { | |
2327 exec: 'inserthorizontalrule', | |
2328 tooltip: 'Insert a horizontal rule' | |
2329 }, | |
2330 // END_COMMAND | |
2331 | |
2332 // START_COMMAND: Code | |
2333 code: { | |
2334 exec: function () { | |
2335 this.wysiwygEditorInsertHtml( | |
2336 '<code>', | |
2337 '<br /></code>' | |
2338 ); | |
2339 }, | |
2340 tooltip: 'Code' | |
2341 }, | |
2342 // END_COMMAND | |
2343 | |
2344 // START_COMMAND: Image | |
2345 image: { | |
2346 _dropDown: function (editor, caller, selected, cb) { | |
2347 var content = createElement('div'); | |
2348 | |
2349 appendChild(content, _tmpl('image', { | |
2350 url: editor._('URL:'), | |
2351 width: editor._('Width (optional):'), | |
2352 height: editor._('Height (optional):'), | |
2353 insert: editor._('Insert') | |
2354 }, true)); | |
2355 | |
2356 | |
2357 var urlInput = find(content, '#image')[0]; | |
2358 | |
2359 urlInput.value = selected; | |
2360 | |
2361 on(content, 'click', '.button', function (e) { | |
2362 if (urlInput.value) { | |
2363 cb( | |
2364 urlInput.value, | |
2365 find(content, '#width')[0].value, | |
2366 find(content, '#height')[0].value | |
2367 ); | |
2368 } | |
2369 | |
2370 editor.closeDropDown(true); | |
2371 e.preventDefault(); | |
2372 }); | |
2373 | |
2374 editor.createDropDown(caller, 'insertimage', content); | |
2375 }, | |
2376 exec: function (caller) { | |
2377 var editor = this; | |
2378 | |
2379 defaultCmds.image._dropDown( | |
2380 editor, | |
2381 caller, | |
2382 '', | |
2383 function (url, width, height) { | |
2384 var attrs = ''; | |
2385 | |
2386 if (width) { | |
2387 attrs += ' width="' + parseInt(width, 10) + '"'; | |
2388 } | |
2389 | |
2390 if (height) { | |
2391 attrs += ' height="' + parseInt(height, 10) + '"'; | |
2392 } | |
2393 | |
2394 attrs += ' src="' + entities(url) + '"'; | |
2395 | |
2396 editor.wysiwygEditorInsertHtml( | |
2397 '<img' + attrs + ' />' | |
2398 ); | |
2399 } | |
2400 ); | |
2401 }, | |
2402 tooltip: 'Insert an image' | |
2403 }, | |
2404 // END_COMMAND | |
2405 | |
2406 // START_COMMAND: E-mail | |
2407 email: { | |
2408 _dropDown: function (editor, caller, cb) { | |
2409 var content = createElement('div'); | |
2410 | |
2411 appendChild(content, _tmpl('email', { | |
2412 label: editor._('E-mail:'), | |
2413 desc: editor._('Description (optional):'), | |
2414 insert: editor._('Insert') | |
2415 }, true)); | |
2416 | |
2417 on(content, 'click', '.button', function (e) { | |
2418 var email = find(content, '#email')[0].value; | |
2419 | |
2420 if (email) { | |
2421 cb(email, find(content, '#des')[0].value); | |
2422 } | |
2423 | |
2424 editor.closeDropDown(true); | |
2425 e.preventDefault(); | |
2426 }); | |
2427 | |
2428 editor.createDropDown(caller, 'insertemail', content); | |
2429 }, | |
2430 exec: function (caller) { | |
2431 var editor = this; | |
2432 | |
2433 defaultCmds.email._dropDown( | |
2434 editor, | |
2435 caller, | |
2436 function (email, text) { | |
2437 if (!editor.getRangeHelper().selectedHtml() || text) { | |
2438 editor.wysiwygEditorInsertHtml( | |
2439 '<a href="' + | |
2440 'mailto:' + entities(email) + '">' + | |
2441 entities((text || email)) + | |
2442 '</a>' | |
2443 ); | |
2444 } else { | |
2445 editor.execCommand('createlink', 'mailto:' + email); | |
2446 } | |
2447 } | |
2448 ); | |
2449 }, | |
2450 tooltip: 'Insert an email' | |
2451 }, | |
2452 // END_COMMAND | |
2453 | |
2454 // START_COMMAND: Link | |
2455 link: { | |
2456 _dropDown: function (editor, caller, cb) { | |
2457 var content = createElement('div'); | |
2458 | |
2459 appendChild(content, _tmpl('link', { | |
2460 url: editor._('URL:'), | |
2461 desc: editor._('Description (optional):'), | |
2462 ins: editor._('Insert') | |
2463 }, true)); | |
2464 | |
2465 var linkInput = find(content, '#link')[0]; | |
2466 | |
2467 function insertUrl(e) { | |
2468 if (linkInput.value) { | |
2469 cb(linkInput.value, find(content, '#des')[0].value); | |
2470 } | |
2471 | |
2472 editor.closeDropDown(true); | |
2473 e.preventDefault(); | |
2474 } | |
2475 | |
2476 on(content, 'click', '.button', insertUrl); | |
2477 on(content, 'keypress', function (e) { | |
2478 // 13 = enter key | |
2479 if (e.which === 13 && linkInput.value) { | |
2480 insertUrl(e); | |
2481 } | |
2482 }, EVENT_CAPTURE); | |
2483 | |
2484 editor.createDropDown(caller, 'insertlink', content); | |
2485 }, | |
2486 exec: function (caller) { | |
2487 var editor = this; | |
2488 | |
2489 defaultCmds.link._dropDown(editor, caller, function (url, text) { | |
2490 if (text || !editor.getRangeHelper().selectedHtml()) { | |
2491 editor.wysiwygEditorInsertHtml( | |
2492 '<a href="' + entities(url) + '">' + | |
2493 entities(text || url) + | |
2494 '</a>' | |
2495 ); | |
2496 } else { | |
2497 editor.execCommand('createlink', url); | |
2498 } | |
2499 }); | |
2500 }, | |
2501 tooltip: 'Insert a link' | |
2502 }, | |
2503 // END_COMMAND | |
2504 | |
2505 // START_COMMAND: Unlink | |
2506 unlink: { | |
2507 state: function () { | |
2508 return closest(this.currentNode(), 'a') ? 0 : -1; | |
2509 }, | |
2510 exec: function () { | |
2511 var anchor = closest(this.currentNode(), 'a'); | |
2512 | |
2513 if (anchor) { | |
2514 while (anchor.firstChild) { | |
2515 insertBefore(anchor.firstChild, anchor); | |
2516 } | |
2517 | |
2518 remove(anchor); | |
2519 } | |
2520 }, | |
2521 tooltip: 'Unlink' | |
2522 }, | |
2523 // END_COMMAND | |
2524 | |
2525 | |
2526 // START_COMMAND: Quote | |
2527 quote: { | |
2528 exec: function (caller, html, author) { | |
2529 var before = '<blockquote>', | |
2530 end = '</blockquote>'; | |
2531 | |
2532 // if there is HTML passed set end to null so any selected | |
2533 // text is replaced | |
2534 if (html) { | |
2535 author = (author ? '<cite>' + | |
2536 entities(author) + | |
2537 '</cite>' : ''); | |
2538 before = before + author + html + end; | |
2539 end = null; | |
2540 // if not add a newline to the end of the inserted quote | |
2541 } else if (this.getRangeHelper().selectedHtml() === '') { | |
2542 end = '<br />' + end; | |
2543 } | |
2544 | |
2545 this.wysiwygEditorInsertHtml(before, end); | |
2546 }, | |
2547 tooltip: 'Insert a Quote' | |
2548 }, | |
2549 // END_COMMAND | |
2550 | |
2551 // START_COMMAND: Emoticons | |
2552 emoticon: { | |
2553 exec: function (caller) { | |
2554 var editor = this; | |
2555 | |
2556 var createContent = function (includeMore) { | |
2557 var moreLink, | |
2558 opts = editor.opts, | |
2559 emoticonsRoot = opts.emoticonsRoot || '', | |
2560 emoticonsCompat = opts.emoticonsCompat, | |
2561 rangeHelper = editor.getRangeHelper(), | |
2562 startSpace = emoticonsCompat && | |
2563 rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '', | |
2564 endSpace = emoticonsCompat && | |
2565 rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '', | |
2566 content = createElement('div'), | |
2567 line = createElement('div'), | |
2568 perLine = 0, | |
2569 emoticons = extend( | |
2570 {}, | |
2571 opts.emoticons.dropdown, | |
2572 includeMore ? opts.emoticons.more : {} | |
2573 ); | |
2574 | |
2575 appendChild(content, line); | |
2576 | |
2577 perLine = Math.sqrt(Object.keys(emoticons).length); | |
2578 | |
2579 on(content, 'click', 'img', function (e) { | |
2580 editor.insert(startSpace + attr(this, 'alt') + endSpace, | |
2581 null, false).closeDropDown(true); | |
2582 | |
2583 e.preventDefault(); | |
2584 }); | |
2585 | |
2586 each(emoticons, function (code, emoticon) { | |
2587 appendChild(line, createElement('img', { | |
2588 src: emoticonsRoot + (emoticon.url || emoticon), | |
2589 alt: code, | |
2590 title: emoticon.tooltip || code | |
2591 })); | |
2592 | |
2593 if (line.children.length >= perLine) { | |
2594 line = createElement('div'); | |
2595 appendChild(content, line); | |
2596 } | |
2597 }); | |
2598 | |
2599 if (!includeMore && opts.emoticons.more) { | |
2600 moreLink = createElement('a', { | |
2601 className: 'sceditor-more' | |
2602 }); | |
2603 | |
2604 appendChild(moreLink, | |
2605 document.createTextNode(editor._('More'))); | |
2606 | |
2607 on(moreLink, 'click', function (e) { | |
2608 editor.createDropDown( | |
2609 caller, 'more-emoticons', createContent(true) | |
2610 ); | |
2611 | |
2612 e.preventDefault(); | |
2613 }); | |
2614 | |
2615 appendChild(content, moreLink); | |
2616 } | |
2617 | |
2618 return content; | |
2619 }; | |
2620 | |
2621 editor.createDropDown(caller, 'emoticons', createContent(false)); | |
2622 }, | |
2623 txtExec: function (caller) { | |
2624 defaultCmds.emoticon.exec.call(this, caller); | |
2625 }, | |
2626 tooltip: 'Insert an emoticon' | |
2627 }, | |
2628 // END_COMMAND | |
2629 | |
2630 // START_COMMAND: YouTube | |
2631 youtube: { | |
2632 _dropDown: function (editor, caller, callback) { | |
2633 var content = createElement('div'); | |
2634 | |
2635 appendChild(content, _tmpl('youtubeMenu', { | |
2636 label: editor._('Video URL:'), | |
2637 insert: editor._('Insert') | |
2638 }, true)); | |
2639 | |
2640 on(content, 'click', '.button', function (e) { | |
2641 var val = find(content, '#link')[0].value; | |
2642 var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)?([a-zA-Z0-9_-]{11})/); | |
2643 var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/); | |
2644 var time = 0; | |
2645 | |
2646 if (timeMatch) { | |
2647 each(timeMatch[1].split(/[hms]/), function (i, val) { | |
2648 if (val !== '') { | |
2649 time = (time * 60) + Number(val); | |
2650 } | |
2651 }); | |
2652 } | |
2653 | |
2654 if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) { | |
2655 callback(idMatch[1], time); | |
2656 } | |
2657 | |
2658 editor.closeDropDown(true); | |
2659 e.preventDefault(); | |
2660 }); | |
2661 | |
2662 editor.createDropDown(caller, 'insertlink', content); | |
2663 }, | |
2664 exec: function (btn) { | |
2665 var editor = this; | |
2666 | |
2667 defaultCmds.youtube._dropDown(editor, btn, function (id, time) { | |
2668 editor.wysiwygEditorInsertHtml(_tmpl('youtube', { | |
2669 id: id, | |
2670 time: time | |
2671 })); | |
2672 }); | |
2673 }, | |
2674 tooltip: 'Insert a YouTube video' | |
2675 }, | |
2676 // END_COMMAND | |
2677 | |
2678 // START_COMMAND: Date | |
2679 date: { | |
2680 _date: function (editor) { | |
2681 var now = new Date(), | |
2682 year = now.getYear(), | |
2683 month = now.getMonth() + 1, | |
2684 day = now.getDate(); | |
2685 | |
2686 if (year < 2000) { | |
2687 year = 1900 + year; | |
2688 } | |
2689 | |
2690 if (month < 10) { | |
2691 month = '0' + month; | |
2692 } | |
2693 | |
2694 if (day < 10) { | |
2695 day = '0' + day; | |
2696 } | |
2697 | |
2698 return editor.opts.dateFormat | |
2699 .replace(/year/i, year) | |
2700 .replace(/month/i, month) | |
2701 .replace(/day/i, day); | |
2702 }, | |
2703 exec: function () { | |
2704 this.insertText(defaultCmds.date._date(this)); | |
2705 }, | |
2706 txtExec: function () { | |
2707 this.insertText(defaultCmds.date._date(this)); | |
2708 }, | |
2709 tooltip: 'Insert current date' | |
2710 }, | |
2711 // END_COMMAND | |
2712 | |
2713 // START_COMMAND: Time | |
2714 time: { | |
2715 _time: function () { | |
2716 var now = new Date(), | |
2717 hours = now.getHours(), | |
2718 mins = now.getMinutes(), | |
2719 secs = now.getSeconds(); | |
2720 | |
2721 if (hours < 10) { | |
2722 hours = '0' + hours; | |
2723 } | |
2724 | |
2725 if (mins < 10) { | |
2726 mins = '0' + mins; | |
2727 } | |
2728 | |
2729 if (secs < 10) { | |
2730 secs = '0' + secs; | |
2731 } | |
2732 | |
2733 return hours + ':' + mins + ':' + secs; | |
2734 }, | |
2735 exec: function () { | |
2736 this.insertText(defaultCmds.time._time()); | |
2737 }, | |
2738 txtExec: function () { | |
2739 this.insertText(defaultCmds.time._time()); | |
2740 }, | |
2741 tooltip: 'Insert current time' | |
2742 }, | |
2743 // END_COMMAND | |
2744 | |
2745 | |
2746 // START_COMMAND: Ltr | |
2747 ltr: { | |
2748 state: function (parents, firstBlock) { | |
2749 return firstBlock && firstBlock.style.direction === 'ltr'; | |
2750 }, | |
2751 exec: function () { | |
2752 var editor = this, | |
2753 rangeHelper = editor.getRangeHelper(), | |
2754 node = rangeHelper.getFirstBlockParent(); | |
2755 | |
2756 editor.focus(); | |
2757 | |
2758 if (!node || is(node, 'body')) { | |
2759 editor.execCommand('formatBlock', 'p'); | |
2760 | |
2761 node = rangeHelper.getFirstBlockParent(); | |
2762 | |
2763 if (!node || is(node, 'body')) { | |
2764 return; | |
2765 } | |
2766 } | |
2767 | |
2768 var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr'; | |
2769 css(node, 'direction', toggleValue); | |
2770 }, | |
2771 tooltip: 'Left-to-Right' | |
2772 }, | |
2773 // END_COMMAND | |
2774 | |
2775 // START_COMMAND: Rtl | |
2776 rtl: { | |
2777 state: function (parents, firstBlock) { | |
2778 return firstBlock && firstBlock.style.direction === 'rtl'; | |
2779 }, | |
2780 exec: function () { | |
2781 var editor = this, | |
2782 rangeHelper = editor.getRangeHelper(), | |
2783 node = rangeHelper.getFirstBlockParent(); | |
2784 | |
2785 editor.focus(); | |
2786 | |
2787 if (!node || is(node, 'body')) { | |
2788 editor.execCommand('formatBlock', 'p'); | |
2789 | |
2790 node = rangeHelper.getFirstBlockParent(); | |
2791 | |
2792 if (!node || is(node, 'body')) { | |
2793 return; | |
2794 } | |
2795 } | |
2796 | |
2797 var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl'; | |
2798 css(node, 'direction', toggleValue); | |
2799 }, | |
2800 tooltip: 'Right-to-Left' | |
2801 }, | |
2802 // END_COMMAND | |
2803 | |
2804 | |
2805 // START_COMMAND: Print | |
2806 print: { | |
2807 exec: 'print', | |
2808 tooltip: 'Print' | |
2809 }, | |
2810 // END_COMMAND | |
2811 | |
2812 // START_COMMAND: Maximize | |
2813 maximize: { | |
2814 state: function () { | |
2815 return this.maximize(); | |
2816 }, | |
2817 exec: function () { | |
2818 this.maximize(!this.maximize()); | |
2819 this.focus(); | |
2820 }, | |
2821 txtExec: function () { | |
2822 this.maximize(!this.maximize()); | |
2823 this.focus(); | |
2824 }, | |
2825 tooltip: 'Maximize', | |
2826 shortcut: 'Ctrl+Shift+M' | |
2827 }, | |
2828 // END_COMMAND | |
2829 | |
2830 // START_COMMAND: Source | |
2831 source: { | |
2832 state: function () { | |
2833 return this.sourceMode(); | |
2834 }, | |
2835 exec: function () { | |
2836 this.toggleSourceMode(); | |
2837 this.focus(); | |
2838 }, | |
2839 txtExec: function () { | |
2840 this.toggleSourceMode(); | |
2841 this.focus(); | |
2842 }, | |
2843 tooltip: 'View source', | |
2844 shortcut: 'Ctrl+Shift+S' | |
2845 }, | |
2846 // END_COMMAND | |
2847 | |
2848 // this is here so that commands above can be removed | |
2849 // without having to remove the , after the last one. | |
2850 // Needed for IE. | |
2851 ignore: {} | |
2852 }; | |
2853 | |
2854 var plugins = {}; | |
2855 | |
2856 /** | |
2857 * Plugin Manager class | |
2858 * @class PluginManager | |
2859 * @name PluginManager | |
2860 */ | |
2861 function PluginManager(thisObj) { | |
2862 /** | |
2863 * Alias of this | |
2864 * | |
2865 * @private | |
2866 * @type {Object} | |
2867 */ | |
2868 var base = this; | |
2869 | |
2870 /** | |
2871 * Array of all currently registered plugins | |
2872 * | |
2873 * @type {Array} | |
2874 * @private | |
2875 */ | |
2876 var registeredPlugins = []; | |
2877 | |
2878 | |
2879 /** | |
2880 * Changes a signals name from "name" into "signalName". | |
2881 * | |
2882 * @param {string} signal | |
2883 * @return {string} | |
2884 * @private | |
2885 */ | |
2886 var formatSignalName = function (signal) { | |
2887 return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1); | |
2888 }; | |
2889 | |
2890 /** | |
2891 * Calls handlers for a signal | |
2892 * | |
2893 * @see call() | |
2894 * @see callOnlyFirst() | |
2895 * @param {Array} args | |
2896 * @param {boolean} returnAtFirst | |
2897 * @return {*} | |
2898 * @private | |
2899 */ | |
2900 var callHandlers = function (args, returnAtFirst) { | |
2901 args = [].slice.call(args); | |
2902 | |
2903 var idx, ret, | |
2904 signal = formatSignalName(args.shift()); | |
2905 | |
2906 for (idx = 0; idx < registeredPlugins.length; idx++) { | |
2907 if (signal in registeredPlugins[idx]) { | |
2908 ret = registeredPlugins[idx][signal].apply(thisObj, args); | |
2909 | |
2910 if (returnAtFirst) { | |
2911 return ret; | |
2912 } | |
2913 } | |
2914 } | |
2915 }; | |
2916 | |
2917 /** | |
2918 * Calls all handlers for the passed signal | |
2919 * | |
2920 * @param {string} signal | |
2921 * @param {...string} args | |
2922 * @function | |
2923 * @name call | |
2924 * @memberOf PluginManager.prototype | |
2925 */ | |
2926 base.call = function () { | |
2927 callHandlers(arguments, false); | |
2928 }; | |
2929 | |
2930 /** | |
2931 * Calls the first handler for a signal, and returns the | |
2932 * | |
2933 * @param {string} signal | |
2934 * @param {...string} args | |
2935 * @return {*} The result of calling the handler | |
2936 * @function | |
2937 * @name callOnlyFirst | |
2938 * @memberOf PluginManager.prototype | |
2939 */ | |
2940 base.callOnlyFirst = function () { | |
2941 return callHandlers(arguments, true); | |
2942 }; | |
2943 | |
2944 /** | |
2945 * Checks if a signal has a handler | |
2946 * | |
2947 * @param {string} signal | |
2948 * @return {boolean} | |
2949 * @function | |
2950 * @name hasHandler | |
2951 * @memberOf PluginManager.prototype | |
2952 */ | |
2953 base.hasHandler = function (signal) { | |
2954 var i = registeredPlugins.length; | |
2955 signal = formatSignalName(signal); | |
2956 | |
2957 while (i--) { | |
2958 if (signal in registeredPlugins[i]) { | |
2959 return true; | |
2960 } | |
2961 } | |
2962 | |
2963 return false; | |
2964 }; | |
2965 | |
2966 /** | |
2967 * Checks if the plugin exists in plugins | |
2968 * | |
2969 * @param {string} plugin | |
2970 * @return {boolean} | |
2971 * @function | |
2972 * @name exists | |
2973 * @memberOf PluginManager.prototype | |
2974 */ | |
2975 base.exists = function (plugin) { | |
2976 if (plugin in plugins) { | |
2977 plugin = plugins[plugin]; | |
2978 | |
2979 return typeof plugin === 'function' && | |
2980 typeof plugin.prototype === 'object'; | |
2981 } | |
2982 | |
2983 return false; | |
2984 }; | |
2985 | |
2986 /** | |
2987 * Checks if the passed plugin is currently registered. | |
2988 * | |
2989 * @param {string} plugin | |
2990 * @return {boolean} | |
2991 * @function | |
2992 * @name isRegistered | |
2993 * @memberOf PluginManager.prototype | |
2994 */ | |
2995 base.isRegistered = function (plugin) { | |
2996 if (base.exists(plugin)) { | |
2997 var idx = registeredPlugins.length; | |
2998 | |
2999 while (idx--) { | |
3000 if (registeredPlugins[idx] instanceof plugins[plugin]) { | |
3001 return true; | |
3002 } | |
3003 } | |
3004 } | |
3005 | |
3006 return false; | |
3007 }; | |
3008 | |
3009 /** | |
3010 * Registers a plugin to receive signals | |
3011 * | |
3012 * @param {string} plugin | |
3013 * @return {boolean} | |
3014 * @function | |
3015 * @name register | |
3016 * @memberOf PluginManager.prototype | |
3017 */ | |
3018 base.register = function (plugin) { | |
3019 if (!base.exists(plugin) || base.isRegistered(plugin)) { | |
3020 return false; | |
3021 } | |
3022 | |
3023 plugin = new plugins[plugin](); | |
3024 registeredPlugins.push(plugin); | |
3025 | |
3026 if ('init' in plugin) { | |
3027 plugin.init.call(thisObj); | |
3028 } | |
3029 | |
3030 return true; | |
3031 }; | |
3032 | |
3033 /** | |
3034 * Deregisters a plugin. | |
3035 * | |
3036 * @param {string} plugin | |
3037 * @return {boolean} | |
3038 * @function | |
3039 * @name deregister | |
3040 * @memberOf PluginManager.prototype | |
3041 */ | |
3042 base.deregister = function (plugin) { | |
3043 var removedPlugin, | |
3044 pluginIdx = registeredPlugins.length, | |
3045 removed = false; | |
3046 | |
3047 if (!base.isRegistered(plugin)) { | |
3048 return removed; | |
3049 } | |
3050 | |
3051 while (pluginIdx--) { | |
3052 if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) { | |
3053 removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0]; | |
3054 removed = true; | |
3055 | |
3056 if ('destroy' in removedPlugin) { | |
3057 removedPlugin.destroy.call(thisObj); | |
3058 } | |
3059 } | |
3060 } | |
3061 | |
3062 return removed; | |
3063 }; | |
3064 | |
3065 /** | |
3066 * Clears all plugins and removes the owner reference. | |
3067 * | |
3068 * Calling any functions on this object after calling | |
3069 * destroy will cause a JS error. | |
3070 * | |
3071 * @name destroy | |
3072 * @memberOf PluginManager.prototype | |
3073 */ | |
3074 base.destroy = function () { | |
3075 var i = registeredPlugins.length; | |
3076 | |
3077 while (i--) { | |
3078 if ('destroy' in registeredPlugins[i]) { | |
3079 registeredPlugins[i].destroy.call(thisObj); | |
3080 } | |
3081 } | |
3082 | |
3083 registeredPlugins = []; | |
3084 thisObj = null; | |
3085 }; | |
3086 } | |
3087 PluginManager.plugins = plugins; | |
3088 | |
3089 /** | |
3090 * Gets the text, start/end node and offset for | |
3091 * length chars left or right of the passed node | |
3092 * at the specified offset. | |
3093 * | |
3094 * @param {Node} node | |
3095 * @param {number} offset | |
3096 * @param {boolean} isLeft | |
3097 * @param {number} length | |
3098 * @return {Object} | |
3099 * @private | |
3100 */ | |
3101 var outerText = function (range, isLeft, length) { | |
3102 var nodeValue, remaining, start, end, node, | |
3103 text = '', | |
3104 next = range.startContainer, | |
3105 offset = range.startOffset; | |
3106 | |
3107 // Handle cases where node is a paragraph and offset | |
3108 // refers to the index of a text node. | |
3109 // 3 = text node | |
3110 if (next && next.nodeType !== 3) { | |
3111 next = next.childNodes[offset]; | |
3112 offset = 0; | |
3113 } | |
3114 | |
3115 start = end = offset; | |
3116 | |
3117 while (length > text.length && next && next.nodeType === 3) { | |
3118 nodeValue = next.nodeValue; | |
3119 remaining = length - text.length; | |
3120 | |
3121 // If not the first node, start and end should be at their | |
3122 // max values as will be updated when getting the text | |
3123 if (node) { | |
3124 end = nodeValue.length; | |
3125 start = 0; | |
3126 } | |
3127 | |
3128 node = next; | |
3129 | |
3130 if (isLeft) { | |
3131 start = Math.max(end - remaining, 0); | |
3132 offset = start; | |
3133 | |
3134 text = nodeValue.substr(start, end - start) + text; | |
3135 next = node.previousSibling; | |
3136 } else { | |
3137 end = Math.min(remaining, nodeValue.length); | |
3138 offset = start + end; | |
3139 | |
3140 text += nodeValue.substr(start, end); | |
3141 next = node.nextSibling; | |
3142 } | |
3143 } | |
3144 | |
3145 return { | |
3146 node: node || next, | |
3147 offset: offset, | |
3148 text: text | |
3149 }; | |
3150 }; | |
3151 | |
3152 /** | |
3153 * Range helper | |
3154 * | |
3155 * @class RangeHelper | |
3156 * @name RangeHelper | |
3157 */ | |
3158 function RangeHelper(win, d, sanitize) { | |
3159 var _createMarker, _prepareInput, | |
3160 doc = d || win.contentDocument || win.document, | |
3161 startMarker = 'sceditor-start-marker', | |
3162 endMarker = 'sceditor-end-marker', | |
3163 base = this; | |
3164 | |
3165 /** | |
3166 * Inserts HTML into the current range replacing any selected | |
3167 * text. | |
3168 * | |
3169 * If endHTML is specified the selected contents will be put between | |
3170 * html and endHTML. If there is nothing selected html and endHTML are | |
3171 * just concatenate together. | |
3172 * | |
3173 * @param {string} html | |
3174 * @param {string} [endHTML] | |
3175 * @return False on fail | |
3176 * @function | |
3177 * @name insertHTML | |
3178 * @memberOf RangeHelper.prototype | |
3179 */ | |
3180 base.insertHTML = function (html, endHTML) { | |
3181 var node, div, | |
3182 range = base.selectedRange(); | |
3183 | |
3184 if (!range) { | |
3185 return false; | |
3186 } | |
3187 | |
3188 if (endHTML) { | |
3189 html += base.selectedHtml() + endHTML; | |
3190 } | |
3191 | |
3192 div = createElement('p', {}, doc); | |
3193 node = doc.createDocumentFragment(); | |
3194 div.innerHTML = sanitize(html); | |
3195 | |
3196 while (div.firstChild) { | |
3197 appendChild(node, div.firstChild); | |
3198 } | |
3199 | |
3200 base.insertNode(node); | |
3201 }; | |
3202 | |
3203 /** | |
3204 * Prepares HTML to be inserted by adding a zero width space | |
3205 * if the last child is empty and adding the range start/end | |
3206 * markers to the last child. | |
3207 * | |
3208 * @param {Node|string} node | |
3209 * @param {Node|string} [endNode] | |
3210 * @param {boolean} [returnHtml] | |
3211 * @return {Node|string} | |
3212 * @private | |
3213 */ | |
3214 _prepareInput = function (node, endNode, returnHtml) { | |
3215 var lastChild, | |
3216 frag = doc.createDocumentFragment(); | |
3217 | |
3218 if (typeof node === 'string') { | |
3219 if (endNode) { | |
3220 node += base.selectedHtml() + endNode; | |
3221 } | |
3222 | |
3223 frag = parseHTML(node); | |
3224 } else { | |
3225 appendChild(frag, node); | |
3226 | |
3227 if (endNode) { | |
3228 appendChild(frag, base.selectedRange().extractContents()); | |
3229 appendChild(frag, endNode); | |
3230 } | |
3231 } | |
3232 | |
3233 if (!(lastChild = frag.lastChild)) { | |
3234 return; | |
3235 } | |
3236 | |
3237 while (!isInline(lastChild.lastChild, true)) { | |
3238 lastChild = lastChild.lastChild; | |
3239 } | |
3240 | |
3241 if (canHaveChildren(lastChild)) { | |
3242 // Webkit won't allow the cursor to be placed inside an | |
3243 // empty tag, so add a zero width space to it. | |
3244 if (!lastChild.lastChild) { | |
3245 appendChild(lastChild, document.createTextNode('\u200B')); | |
3246 } | |
3247 } else { | |
3248 lastChild = frag; | |
3249 } | |
3250 | |
3251 base.removeMarkers(); | |
3252 | |
3253 // Append marks to last child so when restored cursor will be in | |
3254 // the right place | |
3255 appendChild(lastChild, _createMarker(startMarker)); | |
3256 appendChild(lastChild, _createMarker(endMarker)); | |
3257 | |
3258 if (returnHtml) { | |
3259 var div = createElement('div'); | |
3260 appendChild(div, frag); | |
3261 | |
3262 return div.innerHTML; | |
3263 } | |
3264 | |
3265 return frag; | |
3266 }; | |
3267 | |
3268 /** | |
3269 * The same as insertHTML except with DOM nodes instead | |
3270 * | |
3271 * <strong>Warning:</strong> the nodes must belong to the | |
3272 * document they are being inserted into. Some browsers | |
3273 * will throw exceptions if they don't. | |
3274 * | |
3275 * Returns boolean false on fail | |
3276 * | |
3277 * @param {Node} node | |
3278 * @param {Node} endNode | |
3279 * @return {false|undefined} | |
3280 * @function | |
3281 * @name insertNode | |
3282 * @memberOf RangeHelper.prototype | |
3283 */ | |
3284 base.insertNode = function (node, endNode) { | |
3285 var first, last, | |
3286 input = _prepareInput(node, endNode), | |
3287 range = base.selectedRange(), | |
3288 parent = range.commonAncestorContainer, | |
3289 emptyNodes = []; | |
3290 | |
3291 if (!input) { | |
3292 return false; | |
3293 } | |
3294 | |
3295 function removeIfEmpty(node) { | |
3296 // Only remove empty node if it wasn't already empty | |
3297 if (node && isEmpty(node) && emptyNodes.indexOf(node) < 0) { | |
3298 remove(node); | |
3299 } | |
3300 } | |
3301 | |
3302 if (range.startContainer !== range.endContainer) { | |
3303 each(parent.childNodes, function (_, node) { | |
3304 if (isEmpty(node)) { | |
3305 emptyNodes.push(node); | |
3306 } | |
3307 }); | |
3308 | |
3309 first = input.firstChild; | |
3310 last = input.lastChild; | |
3311 } | |
3312 | |
3313 range.deleteContents(); | |
3314 | |
3315 // FF allows <br /> to be selected but inserting a node | |
3316 // into <br /> will cause it not to be displayed so must | |
3317 // insert before the <br /> in FF. | |
3318 // 3 = TextNode | |
3319 if (parent && parent.nodeType !== 3 && !canHaveChildren(parent)) { | |
3320 insertBefore(input, parent); | |
3321 } else { | |
3322 range.insertNode(input); | |
3323 | |
3324 // If a node was split or its contents deleted, remove any resulting | |
3325 // empty tags. For example: | |
3326 // <p>|test</p><div>test|</div> | |
3327 // When deleteContents could become: | |
3328 // <p></p>|<div></div> | |
3329 // So remove the empty ones | |
3330 removeIfEmpty(first && first.previousSibling); | |
3331 removeIfEmpty(last && last.nextSibling); | |
3332 } | |
3333 | |
3334 base.restoreRange(); | |
3335 }; | |
3336 | |
3337 /** | |
3338 * Clones the selected Range | |
3339 * | |
3340 * @return {Range} | |
3341 * @function | |
3342 * @name cloneSelected | |
3343 * @memberOf RangeHelper.prototype | |
3344 */ | |
3345 base.cloneSelected = function () { | |
3346 var range = base.selectedRange(); | |
3347 | |
3348 if (range) { | |
3349 return range.cloneRange(); | |
3350 } | |
3351 }; | |
3352 | |
3353 /** | |
3354 * Gets the selected Range | |
3355 * | |
3356 * @return {Range} | |
3357 * @function | |
3358 * @name selectedRange | |
3359 * @memberOf RangeHelper.prototype | |
3360 */ | |
3361 base.selectedRange = function () { | |
3362 var range, firstChild, | |
3363 sel = win.getSelection(); | |
3364 | |
3365 if (!sel) { | |
3366 return; | |
3367 } | |
3368 | |
3369 // When creating a new range, set the start to the first child | |
3370 // element of the body element to avoid errors in FF. | |
3371 if (sel.rangeCount <= 0) { | |
3372 firstChild = doc.body; | |
3373 while (firstChild.firstChild) { | |
3374 firstChild = firstChild.firstChild; | |
3375 } | |
3376 | |
3377 range = doc.createRange(); | |
3378 // Must be setStartBefore otherwise it can cause infinite | |
3379 // loops with lists in WebKit. See issue 442 | |
3380 range.setStartBefore(firstChild); | |
3381 | |
3382 sel.addRange(range); | |
3383 } | |
3384 | |
3385 if (sel.rangeCount > 0) { | |
3386 range = sel.getRangeAt(0); | |
3387 } | |
3388 | |
3389 return range; | |
3390 }; | |
3391 | |
3392 /** | |
3393 * Gets if there is currently a selection | |
3394 * | |
3395 * @return {boolean} | |
3396 * @function | |
3397 * @name hasSelection | |
3398 * @since 1.4.4 | |
3399 * @memberOf RangeHelper.prototype | |
3400 */ | |
3401 base.hasSelection = function () { | |
3402 var sel = win.getSelection(); | |
3403 | |
3404 return sel && sel.rangeCount > 0; | |
3405 }; | |
3406 | |
3407 /** | |
3408 * Gets the currently selected HTML | |
3409 * | |
3410 * @return {string} | |
3411 * @function | |
3412 * @name selectedHtml | |
3413 * @memberOf RangeHelper.prototype | |
3414 */ | |
3415 base.selectedHtml = function () { | |
3416 var div, | |
3417 range = base.selectedRange(); | |
3418 | |
3419 if (range) { | |
3420 div = createElement('p', {}, doc); | |
3421 appendChild(div, range.cloneContents()); | |
3422 | |
3423 return div.innerHTML; | |
3424 } | |
3425 | |
3426 return ''; | |
3427 }; | |
3428 | |
3429 /** | |
3430 * Gets the parent node of the selected contents in the range | |
3431 * | |
3432 * @return {HTMLElement} | |
3433 * @function | |
3434 * @name parentNode | |
3435 * @memberOf RangeHelper.prototype | |
3436 */ | |
3437 base.parentNode = function () { | |
3438 var range = base.selectedRange(); | |
3439 | |
3440 if (range) { | |
3441 return range.commonAncestorContainer; | |
3442 } | |
3443 }; | |
3444 | |
3445 /** | |
3446 * Gets the first block level parent of the selected | |
3447 * contents of the range. | |
3448 * | |
3449 * @return {HTMLElement} | |
3450 * @function | |
3451 * @name getFirstBlockParent | |
3452 * @memberOf RangeHelper.prototype | |
3453 */ | |
3454 /** | |
3455 * Gets the first block level parent of the selected | |
3456 * contents of the range. | |
3457 * | |
3458 * @param {Node} [n] The element to get the first block level parent from | |
3459 * @return {HTMLElement} | |
3460 * @function | |
3461 * @name getFirstBlockParent^2 | |
3462 * @since 1.4.1 | |
3463 * @memberOf RangeHelper.prototype | |
3464 */ | |
3465 base.getFirstBlockParent = function (node) { | |
3466 var func = function (elm) { | |
3467 if (!isInline(elm, true)) { | |
3468 return elm; | |
3469 } | |
3470 | |
3471 elm = elm ? elm.parentNode : null; | |
3472 | |
3473 return elm ? func(elm) : elm; | |
3474 }; | |
3475 | |
3476 return func(node || base.parentNode()); | |
3477 }; | |
3478 | |
3479 /** | |
3480 * Inserts a node at either the start or end of the current selection | |
3481 * | |
3482 * @param {Bool} start | |
3483 * @param {Node} node | |
3484 * @function | |
3485 * @name insertNodeAt | |
3486 * @memberOf RangeHelper.prototype | |
3487 */ | |
3488 base.insertNodeAt = function (start, node) { | |
3489 var currentRange = base.selectedRange(), | |
3490 range = base.cloneSelected(); | |
3491 | |
3492 if (!range) { | |
3493 return false; | |
3494 } | |
3495 | |
3496 range.collapse(start); | |
3497 range.insertNode(node); | |
3498 | |
3499 // Reselect the current range. | |
3500 // Fixes issue with Chrome losing the selection. Issue#82 | |
3501 base.selectRange(currentRange); | |
3502 }; | |
3503 | |
3504 /** | |
3505 * Creates a marker node | |
3506 * | |
3507 * @param {string} id | |
3508 * @return {HTMLSpanElement} | |
3509 * @private | |
3510 */ | |
3511 _createMarker = function (id) { | |
3512 base.removeMarker(id); | |
3513 | |
3514 var marker = createElement('span', { | |
3515 id: id, | |
3516 className: 'sceditor-selection sceditor-ignore', | |
3517 style: 'display:none;line-height:0' | |
3518 }, doc); | |
3519 | |
3520 marker.innerHTML = ' '; | |
3521 | |
3522 return marker; | |
3523 }; | |
3524 | |
3525 /** | |
3526 * Inserts start/end markers for the current selection | |
3527 * which can be used by restoreRange to re-select the | |
3528 * range. | |
3529 * | |
3530 * @memberOf RangeHelper.prototype | |
3531 * @function | |
3532 * @name insertMarkers | |
3533 */ | |
3534 base.insertMarkers = function () { | |
3535 var currentRange = base.selectedRange(); | |
3536 var startNode = _createMarker(startMarker); | |
3537 | |
3538 base.removeMarkers(); | |
3539 base.insertNodeAt(true, startNode); | |
3540 | |
3541 // Fixes issue with end marker sometimes being placed before | |
3542 // the start marker when the range is collapsed. | |
3543 if (currentRange && currentRange.collapsed) { | |
3544 startNode.parentNode.insertBefore( | |
3545 _createMarker(endMarker), startNode.nextSibling); | |
3546 } else { | |
3547 base.insertNodeAt(false, _createMarker(endMarker)); | |
3548 } | |
3549 }; | |
3550 | |
3551 /** | |
3552 * Gets the marker with the specified ID | |
3553 * | |
3554 * @param {string} id | |
3555 * @return {Node} | |
3556 * @function | |
3557 * @name getMarker | |
3558 * @memberOf RangeHelper.prototype | |
3559 */ | |
3560 base.getMarker = function (id) { | |
3561 return doc.getElementById(id); | |
3562 }; | |
3563 | |
3564 /** | |
3565 * Removes the marker with the specified ID | |
3566 * | |
3567 * @param {string} id | |
3568 * @function | |
3569 * @name removeMarker | |
3570 * @memberOf RangeHelper.prototype | |
3571 */ | |
3572 base.removeMarker = function (id) { | |
3573 var marker = base.getMarker(id); | |
3574 | |
3575 if (marker) { | |
3576 remove(marker); | |
3577 } | |
3578 }; | |
3579 | |
3580 /** | |
3581 * Removes the start/end markers | |
3582 * | |
3583 * @function | |
3584 * @name removeMarkers | |
3585 * @memberOf RangeHelper.prototype | |
3586 */ | |
3587 base.removeMarkers = function () { | |
3588 base.removeMarker(startMarker); | |
3589 base.removeMarker(endMarker); | |
3590 }; | |
3591 | |
3592 /** | |
3593 * Saves the current range location. Alias of insertMarkers() | |
3594 * | |
3595 * @function | |
3596 * @name saveRage | |
3597 * @memberOf RangeHelper.prototype | |
3598 */ | |
3599 base.saveRange = function () { | |
3600 base.insertMarkers(); | |
3601 }; | |
3602 | |
3603 /** | |
3604 * Select the specified range | |
3605 * | |
3606 * @param {Range} range | |
3607 * @function | |
3608 * @name selectRange | |
3609 * @memberOf RangeHelper.prototype | |
3610 */ | |
3611 base.selectRange = function (range) { | |
3612 var lastChild; | |
3613 var sel = win.getSelection(); | |
3614 var container = range.endContainer; | |
3615 | |
3616 // Check if cursor is set after a BR when the BR is the only | |
3617 // child of the parent. In Firefox this causes a line break | |
3618 // to occur when something is typed. See issue #321 | |
3619 if (range.collapsed && container && | |
3620 !isInline(container, true)) { | |
3621 | |
3622 lastChild = container.lastChild; | |
3623 while (lastChild && is(lastChild, '.sceditor-ignore')) { | |
3624 lastChild = lastChild.previousSibling; | |
3625 } | |
3626 | |
3627 if (is(lastChild, 'br')) { | |
3628 var rng = doc.createRange(); | |
3629 rng.setEndAfter(lastChild); | |
3630 rng.collapse(false); | |
3631 | |
3632 if (base.compare(range, rng)) { | |
3633 range.setStartBefore(lastChild); | |
3634 range.collapse(true); | |
3635 } | |
3636 } | |
3637 } | |
3638 | |
3639 if (sel) { | |
3640 base.clear(); | |
3641 sel.addRange(range); | |
3642 } | |
3643 }; | |
3644 | |
3645 /** | |
3646 * Restores the last range saved by saveRange() or insertMarkers() | |
3647 * | |
3648 * @function | |
3649 * @name restoreRange | |
3650 * @memberOf RangeHelper.prototype | |
3651 */ | |
3652 base.restoreRange = function () { | |
3653 var isCollapsed, | |
3654 range = base.selectedRange(), | |
3655 start = base.getMarker(startMarker), | |
3656 end = base.getMarker(endMarker); | |
3657 | |
3658 if (!start || !end || !range) { | |
3659 return false; | |
3660 } | |
3661 | |
3662 isCollapsed = start.nextSibling === end; | |
3663 | |
3664 range = doc.createRange(); | |
3665 range.setStartBefore(start); | |
3666 range.setEndAfter(end); | |
3667 | |
3668 if (isCollapsed) { | |
3669 range.collapse(true); | |
3670 } | |
3671 | |
3672 base.selectRange(range); | |
3673 base.removeMarkers(); | |
3674 }; | |
3675 | |
3676 /** | |
3677 * Selects the text left and right of the current selection | |
3678 * | |
3679 * @param {number} left | |
3680 * @param {number} right | |
3681 * @since 1.4.3 | |
3682 * @function | |
3683 * @name selectOuterText | |
3684 * @memberOf RangeHelper.prototype | |
3685 */ | |
3686 base.selectOuterText = function (left, right) { | |
3687 var start, end, | |
3688 range = base.cloneSelected(); | |
3689 | |
3690 if (!range) { | |
3691 return false; | |
3692 } | |
3693 | |
3694 range.collapse(false); | |
3695 | |
3696 start = outerText(range, true, left); | |
3697 end = outerText(range, false, right); | |
3698 | |
3699 range.setStart(start.node, start.offset); | |
3700 range.setEnd(end.node, end.offset); | |
3701 | |
3702 base.selectRange(range); | |
3703 }; | |
3704 | |
3705 /** | |
3706 * Gets the text left or right of the current selection | |
3707 * | |
3708 * @param {boolean} before | |
3709 * @param {number} length | |
3710 * @return {string} | |
3711 * @since 1.4.3 | |
3712 * @function | |
3713 * @name selectOuterText | |
3714 * @memberOf RangeHelper.prototype | |
3715 */ | |
3716 base.getOuterText = function (before, length) { | |
3717 var range = base.cloneSelected(); | |
3718 | |
3719 if (!range) { | |
3720 return ''; | |
3721 } | |
3722 | |
3723 range.collapse(!before); | |
3724 | |
3725 return outerText(range, before, length).text; | |
3726 }; | |
3727 | |
3728 /** | |
3729 * Replaces keywords with values based on the current caret position | |
3730 * | |
3731 * @param {Array} keywords | |
3732 * @param {boolean} includeAfter If to include the text after the | |
3733 * current caret position or just | |
3734 * text before | |
3735 * @param {boolean} keywordsSorted If the keywords array is pre | |
3736 * sorted shortest to longest | |
3737 * @param {number} longestKeyword Length of the longest keyword | |
3738 * @param {boolean} requireWhitespace If the key must be surrounded | |
3739 * by whitespace | |
3740 * @param {string} keypressChar If this is being called from | |
3741 * a keypress event, this should be | |
3742 * set to the pressed character | |
3743 * @return {boolean} | |
3744 * @function | |
3745 * @name replaceKeyword | |
3746 * @memberOf RangeHelper.prototype | |
3747 */ | |
3748 // eslint-disable-next-line max-params | |
3749 base.replaceKeyword = function ( | |
3750 keywords, | |
3751 includeAfter, | |
3752 keywordsSorted, | |
3753 longestKeyword, | |
3754 requireWhitespace, | |
3755 keypressChar | |
3756 ) { | |
3757 if (!keywordsSorted) { | |
3758 keywords.sort(function (a, b) { | |
3759 return a[0].length - b[0].length; | |
3760 }); | |
3761 } | |
3762 | |
3763 var outerText, match, matchPos, startIndex, | |
3764 leftLen, charsLeft, keyword, keywordLen, | |
3765 whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])', | |
3766 keywordIdx = keywords.length, | |
3767 whitespaceLen = requireWhitespace ? 1 : 0, | |
3768 maxKeyLen = longestKeyword || | |
3769 keywords[keywordIdx - 1][0].length; | |
3770 | |
3771 if (requireWhitespace) { | |
3772 maxKeyLen++; | |
3773 } | |
3774 | |
3775 keypressChar = keypressChar || ''; | |
3776 outerText = base.getOuterText(true, maxKeyLen); | |
3777 leftLen = outerText.length; | |
3778 outerText += keypressChar; | |
3779 | |
3780 if (includeAfter) { | |
3781 outerText += base.getOuterText(false, maxKeyLen); | |
3782 } | |
3783 | |
3784 while (keywordIdx--) { | |
3785 keyword = keywords[keywordIdx][0]; | |
3786 keywordLen = keyword.length; | |
3787 startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen); | |
3788 matchPos = -1; | |
3789 | |
3790 if (requireWhitespace) { | |
3791 match = outerText | |
3792 .substr(startIndex) | |
3793 .match(new RegExp(whitespaceRegex + | |
3794 regex(keyword) + whitespaceRegex)); | |
3795 | |
3796 if (match) { | |
3797 // Add the length of the text that was removed by | |
3798 // substr() and also add 1 for the whitespace | |
3799 matchPos = match.index + startIndex + match[1].length; | |
3800 } | |
3801 } else { | |
3802 matchPos = outerText.indexOf(keyword, startIndex); | |
3803 } | |
3804 | |
3805 if (matchPos > -1) { | |
3806 // Make sure the match is between before and | |
3807 // after, not just entirely in one side or the other | |
3808 if (matchPos <= leftLen && | |
3809 matchPos + keywordLen + whitespaceLen >= leftLen) { | |
3810 charsLeft = leftLen - matchPos; | |
3811 | |
3812 // If the keypress char is white space then it should | |
3813 // not be replaced, only chars that are part of the | |
3814 // key should be replaced. | |
3815 base.selectOuterText( | |
3816 charsLeft, | |
3817 keywordLen - charsLeft - | |
3818 (/^\S/.test(keypressChar) ? 1 : 0) | |
3819 ); | |
3820 | |
3821 base.insertHTML(keywords[keywordIdx][1]); | |
3822 return true; | |
3823 } | |
3824 } | |
3825 } | |
3826 | |
3827 return false; | |
3828 }; | |
3829 | |
3830 /** | |
3831 * Compares two ranges. | |
3832 * | |
3833 * If rangeB is undefined it will be set to | |
3834 * the current selected range | |
3835 * | |
3836 * @param {Range} rngA | |
3837 * @param {Range} [rngB] | |
3838 * @return {boolean} | |
3839 * @function | |
3840 * @name compare | |
3841 * @memberOf RangeHelper.prototype | |
3842 */ | |
3843 base.compare = function (rngA, rngB) { | |
3844 if (!rngB) { | |
3845 rngB = base.selectedRange(); | |
3846 } | |
3847 | |
3848 if (!rngA || !rngB) { | |
3849 return !rngA && !rngB; | |
3850 } | |
3851 | |
3852 return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 && | |
3853 rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0; | |
3854 }; | |
3855 | |
3856 /** | |
3857 * Removes any current selection | |
3858 * | |
3859 * @since 1.4.6 | |
3860 * @function | |
3861 * @name clear | |
3862 * @memberOf RangeHelper.prototype | |
3863 */ | |
3864 base.clear = function () { | |
3865 var sel = win.getSelection(); | |
3866 | |
3867 if (sel) { | |
3868 if (sel.removeAllRanges) { | |
3869 sel.removeAllRanges(); | |
3870 } else if (sel.empty) { | |
3871 sel.empty(); | |
3872 } | |
3873 } | |
3874 }; | |
3875 } | |
3876 | |
3877 var USER_AGENT = navigator.userAgent; | |
3878 | |
3879 /** | |
3880 * Detects if the browser is iOS | |
3881 * | |
3882 * Needed to fix iOS specific bugs | |
3883 * | |
3884 * @function | |
3885 * @name ios | |
3886 * @memberOf jQuery.sceditor | |
3887 * @type {boolean} | |
3888 */ | |
3889 var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT); | |
3890 | |
3891 /** | |
3892 * If the browser supports WYSIWYG editing (e.g. older mobile browsers). | |
3893 * | |
3894 * @function | |
3895 * @name isWysiwygSupported | |
3896 * @return {boolean} | |
3897 */ | |
3898 var isWysiwygSupported = (function () { | |
3899 var match, isUnsupported; | |
3900 | |
3901 // IE is the only browser to support documentMode | |
3902 var ie = !!window.document.documentMode; | |
3903 var legacyEdge = '-ms-ime-align' in document.documentElement.style; | |
3904 | |
3905 var div = document.createElement('div'); | |
3906 div.contentEditable = true; | |
3907 | |
3908 // Check if the contentEditable attribute is supported | |
3909 if (!('contentEditable' in document.documentElement) || | |
3910 div.contentEditable !== 'true') { | |
3911 return false; | |
3912 } | |
3913 | |
3914 // I think blackberry supports contentEditable or will at least | |
3915 // give a valid value for the contentEditable detection above | |
3916 // so it isn't included in the below tests. | |
3917 | |
3918 // I hate having to do UA sniffing but some mobile browsers say they | |
3919 // support contentediable when it isn't usable, i.e. you can't enter | |
3920 // text. | |
3921 // This is the only way I can think of to detect them which is also how | |
3922 // every other editor I've seen deals with this issue. | |
3923 | |
3924 // Exclude Opera mobile and mini | |
3925 isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT); | |
3926 | |
3927 if (/Android/i.test(USER_AGENT)) { | |
3928 isUnsupported = true; | |
3929 | |
3930 if (/Safari/.test(USER_AGENT)) { | |
3931 // Android browser 534+ supports content editable | |
3932 // This also matches Chrome which supports content editable too | |
3933 match = /Safari\/(\d+)/.exec(USER_AGENT); | |
3934 isUnsupported = (!match || !match[1] ? true : match[1] < 534); | |
3935 } | |
3936 } | |
3937 | |
3938 // The current version of Amazon Silk supports it, older versions didn't | |
3939 // As it uses webkit like Android, assume it's the same and started | |
3940 // working at versions >= 534 | |
3941 if (/ Silk\//i.test(USER_AGENT)) { | |
3942 match = /AppleWebKit\/(\d+)/.exec(USER_AGENT); | |
3943 isUnsupported = (!match || !match[1] ? true : match[1] < 534); | |
3944 } | |
3945 | |
3946 // iOS 5+ supports content editable | |
3947 if (ios) { | |
3948 // Block any version <= 4_x(_x) | |
3949 isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT); | |
3950 } | |
3951 | |
3952 // Firefox does support WYSIWYG on mobiles so override | |
3953 // any previous value if using FF | |
3954 if (/Firefox/i.test(USER_AGENT)) { | |
3955 isUnsupported = false; | |
3956 } | |
3957 | |
3958 if (/OneBrowser/i.test(USER_AGENT)) { | |
3959 isUnsupported = false; | |
3960 } | |
3961 | |
3962 // UCBrowser works but doesn't give a unique user agent | |
3963 if (navigator.vendor === 'UCWEB') { | |
3964 isUnsupported = false; | |
3965 } | |
3966 | |
3967 // IE and legacy edge are not supported any more | |
3968 if (ie || legacyEdge) { | |
3969 isUnsupported = true; | |
3970 } | |
3971 | |
3972 return !isUnsupported; | |
3973 }()); | |
3974 | |
3975 /** | |
3976 * Checks all emoticons are surrounded by whitespace and | |
3977 * replaces any that aren't with with their emoticon code. | |
3978 * | |
3979 * @param {HTMLElement} node | |
3980 * @param {rangeHelper} rangeHelper | |
3981 * @return {void} | |
3982 */ | |
3983 function checkWhitespace(node, rangeHelper) { | |
3984 var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/; | |
3985 var emoticons = node && find(node, 'img[data-sceditor-emoticon]'); | |
3986 | |
3987 if (!node || !emoticons.length) { | |
3988 return; | |
3989 } | |
3990 | |
3991 for (var i = 0; i < emoticons.length; i++) { | |
3992 var emoticon = emoticons[i]; | |
3993 var parent = emoticon.parentNode; | |
3994 var prev = emoticon.previousSibling; | |
3995 var next = emoticon.nextSibling; | |
3996 | |
3997 if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) && | |
3998 (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) { | |
3999 continue; | |
4000 } | |
4001 | |
4002 var range = rangeHelper.cloneSelected(); | |
4003 var rangeStart = -1; | |
4004 var rangeStartContainer = range.startContainer; | |
4005 var previousText = prev.nodeValue || ''; | |
4006 | |
4007 previousText += data(emoticon, 'sceditor-emoticon'); | |
4008 | |
4009 // If the cursor is after the removed emoticon, add | |
4010 // the length of the newly added text to it | |
4011 if (rangeStartContainer === next) { | |
4012 rangeStart = previousText.length + range.startOffset; | |
4013 } | |
4014 | |
4015 // If the cursor is set before the next node, set it to | |
4016 // the end of the new text node | |
4017 if (rangeStartContainer === node && | |
4018 node.childNodes[range.startOffset] === next) { | |
4019 rangeStart = previousText.length; | |
4020 } | |
4021 | |
4022 // If the cursor is set before the removed emoticon, | |
4023 // just keep it at that position | |
4024 if (rangeStartContainer === prev) { | |
4025 rangeStart = range.startOffset; | |
4026 } | |
4027 | |
4028 if (!next || next.nodeType !== TEXT_NODE) { | |
4029 next = parent.insertBefore( | |
4030 parent.ownerDocument.createTextNode(''), next | |
4031 ); | |
4032 } | |
4033 | |
4034 next.insertData(0, previousText); | |
4035 remove(prev); | |
4036 remove(emoticon); | |
4037 | |
4038 // Need to update the range starting position if it's been modified | |
4039 if (rangeStart > -1) { | |
4040 range.setStart(next, rangeStart); | |
4041 range.collapse(true); | |
4042 rangeHelper.selectRange(range); | |
4043 } | |
4044 } | |
4045 } | |
4046 /** | |
4047 * Replaces any emoticons inside the root node with images. | |
4048 * | |
4049 * emoticons should be an object where the key is the emoticon | |
4050 * code and the value is the HTML to replace it with. | |
4051 * | |
4052 * @param {HTMLElement} root | |
4053 * @param {Object<string, string>} emoticons | |
4054 * @param {boolean} emoticonsCompat | |
4055 * @return {void} | |
4056 */ | |
4057 function replace(root, emoticons, emoticonsCompat) { | |
4058 var doc = root.ownerDocument; | |
4059 var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)'; | |
4060 var emoticonCodes = []; | |
4061 var emoticonRegex = {}; | |
4062 | |
4063 // TODO: Make this tag configurable. | |
4064 if (parent(root, 'code')) { | |
4065 return; | |
4066 } | |
4067 | |
4068 each(emoticons, function (key) { | |
4069 emoticonRegex[key] = new RegExp(space + regex(key) + space); | |
4070 emoticonCodes.push(key); | |
4071 }); | |
4072 | |
4073 // Sort keys longest to shortest so that longer keys | |
4074 // take precedence (avoids bugs with shorter keys partially | |
4075 // matching longer ones) | |
4076 emoticonCodes.sort(function (a, b) { | |
4077 return b.length - a.length; | |
4078 }); | |
4079 | |
4080 (function convert(node) { | |
4081 node = node.firstChild; | |
4082 | |
4083 while (node) { | |
4084 // TODO: Make this tag configurable. | |
4085 if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) { | |
4086 convert(node); | |
4087 } | |
4088 | |
4089 if (node.nodeType === TEXT_NODE) { | |
4090 for (var i = 0; i < emoticonCodes.length; i++) { | |
4091 var text = node.nodeValue; | |
4092 var key = emoticonCodes[i]; | |
4093 var index = emoticonsCompat ? | |
4094 text.search(emoticonRegex[key]) : | |
4095 text.indexOf(key); | |
4096 | |
4097 if (index > -1) { | |
4098 // When emoticonsCompat is enabled this will be the | |
4099 // position after any white space | |
4100 var startIndex = text.indexOf(key, index); | |
4101 var fragment = parseHTML(emoticons[key], doc); | |
4102 var after = text.substr(startIndex + key.length); | |
4103 | |
4104 fragment.appendChild(doc.createTextNode(after)); | |
4105 | |
4106 node.nodeValue = text.substr(0, startIndex); | |
4107 node.parentNode | |
4108 .insertBefore(fragment, node.nextSibling); | |
4109 } | |
4110 } | |
4111 } | |
4112 | |
4113 node = node.nextSibling; | |
4114 } | |
4115 }(root)); | |
4116 } | |
4117 | |
4118 /*! @license DOMPurify | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.2.2/LICENSE */ | |
4119 | |
4120 function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | |
4121 | |
4122 var hasOwnProperty = Object.hasOwnProperty, | |
4123 setPrototypeOf = Object.setPrototypeOf, | |
4124 isFrozen = Object.isFrozen, | |
4125 getPrototypeOf = Object.getPrototypeOf, | |
4126 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; | |
4127 var freeze = Object.freeze, | |
4128 seal = Object.seal, | |
4129 create = Object.create; // eslint-disable-line import/no-mutable-exports | |
4130 | |
4131 var _ref = typeof Reflect !== 'undefined' && Reflect, | |
4132 apply = _ref.apply, | |
4133 construct = _ref.construct; | |
4134 | |
4135 if (!apply) { | |
4136 apply = function apply(fun, thisValue, args) { | |
4137 return fun.apply(thisValue, args); | |
4138 }; | |
4139 } | |
4140 | |
4141 if (!freeze) { | |
4142 freeze = function freeze(x) { | |
4143 return x; | |
4144 }; | |
4145 } | |
4146 | |
4147 if (!seal) { | |
4148 seal = function seal(x) { | |
4149 return x; | |
4150 }; | |
4151 } | |
4152 | |
4153 if (!construct) { | |
4154 construct = function construct(Func, args) { | |
4155 return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))(); | |
4156 }; | |
4157 } | |
4158 | |
4159 var arrayForEach = unapply(Array.prototype.forEach); | |
4160 var arrayPop = unapply(Array.prototype.pop); | |
4161 var arrayPush = unapply(Array.prototype.push); | |
4162 | |
4163 var stringToLowerCase = unapply(String.prototype.toLowerCase); | |
4164 var stringMatch = unapply(String.prototype.match); | |
4165 var stringReplace = unapply(String.prototype.replace); | |
4166 var stringIndexOf = unapply(String.prototype.indexOf); | |
4167 var stringTrim = unapply(String.prototype.trim); | |
4168 | |
4169 var regExpTest = unapply(RegExp.prototype.test); | |
4170 | |
4171 var typeErrorCreate = unconstruct(TypeError); | |
4172 | |
4173 function unapply(func) { | |
4174 return function (thisArg) { | |
4175 for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | |
4176 args[_key - 1] = arguments[_key]; | |
4177 } | |
4178 | |
4179 return apply(func, thisArg, args); | |
4180 }; | |
4181 } | |
4182 | |
4183 function unconstruct(func) { | |
4184 return function () { | |
4185 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { | |
4186 args[_key2] = arguments[_key2]; | |
4187 } | |
4188 | |
4189 return construct(func, args); | |
4190 }; | |
4191 } | |
4192 | |
4193 /* Add properties to a lookup table */ | |
4194 function addToSet(set, array) { | |
4195 if (setPrototypeOf) { | |
4196 // Make 'in' and truthy checks like Boolean(set.constructor) | |
4197 // independent of any properties defined on Object.prototype. | |
4198 // Prevent prototype setters from intercepting set as a this value. | |
4199 setPrototypeOf(set, null); | |
4200 } | |
4201 | |
4202 var l = array.length; | |
4203 while (l--) { | |
4204 var element = array[l]; | |
4205 if (typeof element === 'string') { | |
4206 var lcElement = stringToLowerCase(element); | |
4207 if (lcElement !== element) { | |
4208 // Config presets (e.g. tags.js, attrs.js) are immutable. | |
4209 if (!isFrozen(array)) { | |
4210 array[l] = lcElement; | |
4211 } | |
4212 | |
4213 element = lcElement; | |
4214 } | |
4215 } | |
4216 | |
4217 set[element] = true; | |
4218 } | |
4219 | |
4220 return set; | |
4221 } | |
4222 | |
4223 /* Shallow clone an object */ | |
4224 function clone(object) { | |
4225 var newObject = create(null); | |
4226 | |
4227 var property = void 0; | |
4228 for (property in object) { | |
4229 if (apply(hasOwnProperty, object, [property])) { | |
4230 newObject[property] = object[property]; | |
4231 } | |
4232 } | |
4233 | |
4234 return newObject; | |
4235 } | |
4236 | |
4237 /* IE10 doesn't support __lookupGetter__ so lets' | |
4238 * simulate it. It also automatically checks | |
4239 * if the prop is function or getter and behaves | |
4240 * accordingly. */ | |
4241 function lookupGetter(object, prop) { | |
4242 while (object !== null) { | |
4243 var desc = getOwnPropertyDescriptor(object, prop); | |
4244 if (desc) { | |
4245 if (desc.get) { | |
4246 return unapply(desc.get); | |
4247 } | |
4248 | |
4249 if (typeof desc.value === 'function') { | |
4250 return unapply(desc.value); | |
4251 } | |
4252 } | |
4253 | |
4254 object = getPrototypeOf(object); | |
4255 } | |
4256 | |
4257 return null; | |
4258 } | |
4259 | |
4260 var html = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); | |
4261 | |
4262 // SVG | |
4263 var svg = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); | |
4264 | |
4265 var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); | |
4266 | |
4267 // List of SVG elements that are disallowed by default. | |
4268 // We still need to know them so that we can do namespace | |
4269 // checks properly in case one wants to add them to | |
4270 // allow-list. | |
4271 var svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', 'feimage', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); | |
4272 | |
4273 var mathMl = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover']); | |
4274 | |
4275 // Similarly to SVG, we want to know all MathML elements, | |
4276 // even those that we disallow by default. | |
4277 var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); | |
4278 | |
4279 var text = freeze(['#text']); | |
4280 | |
4281 var html$1 = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns']); | |
4282 | |
4283 var svg$1 = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); | |
4284 | |
4285 var mathMl$1 = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); | |
4286 | |
4287 var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); | |
4288 | |
4289 // eslint-disable-next-line unicorn/better-regex | |
4290 var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode | |
4291 var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm); | |
4292 var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape | |
4293 var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape | |
4294 var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape | |
4295 ); | |
4296 var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); | |
4297 var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex | |
4298 ); | |
4299 | |
4300 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | |
4301 | |
4302 function _toConsumableArray$1(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | |
4303 | |
4304 var getGlobal = function getGlobal() { | |
4305 return typeof window === 'undefined' ? null : window; | |
4306 }; | |
4307 | |
4308 /** | |
4309 * Creates a no-op policy for internal use only. | |
4310 * Don't export this function outside this module! | |
4311 * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory. | |
4312 * @param {Document} document The document object (to determine policy name suffix) | |
4313 * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types | |
4314 * are not supported). | |
4315 */ | |
4316 var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) { | |
4317 if ((typeof trustedTypes === 'undefined' ? 'undefined' : _typeof(trustedTypes)) !== 'object' || typeof trustedTypes.createPolicy !== 'function') { | |
4318 return null; | |
4319 } | |
4320 | |
4321 // Allow the callers to control the unique policy name | |
4322 // by adding a data-tt-policy-suffix to the script element with the DOMPurify. | |
4323 // Policy creation with duplicate names throws in Trusted Types. | |
4324 var suffix = null; | |
4325 var ATTR_NAME = 'data-tt-policy-suffix'; | |
4326 if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) { | |
4327 suffix = document.currentScript.getAttribute(ATTR_NAME); | |
4328 } | |
4329 | |
4330 var policyName = 'dompurify' + (suffix ? '#' + suffix : ''); | |
4331 | |
4332 try { | |
4333 return trustedTypes.createPolicy(policyName, { | |
4334 createHTML: function createHTML(html$$1) { | |
4335 return html$$1; | |
4336 } | |
4337 }); | |
4338 } catch (_) { | |
4339 // Policy creation failed (most likely another DOMPurify script has | |
4340 // already run). Skip creating the policy, as this will only cause errors | |
4341 // if TT are enforced. | |
4342 console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); | |
4343 return null; | |
4344 } | |
4345 }; | |
4346 | |
4347 function createDOMPurify() { | |
4348 var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); | |
4349 | |
4350 var DOMPurify = function DOMPurify(root) { | |
4351 return createDOMPurify(root); | |
4352 }; | |
4353 | |
4354 /** | |
4355 * Version label, exposed for easier checks | |
4356 * if DOMPurify is up to date or not | |
4357 */ | |
4358 DOMPurify.version = '2.2.6'; | |
4359 | |
4360 /** | |
4361 * Array of elements that DOMPurify removed during sanitation. | |
4362 * Empty if nothing was removed. | |
4363 */ | |
4364 DOMPurify.removed = []; | |
4365 | |
4366 if (!window || !window.document || window.document.nodeType !== 9) { | |
4367 // Not running in a browser, provide a factory function | |
4368 // so that you can pass your own Window | |
4369 DOMPurify.isSupported = false; | |
4370 | |
4371 return DOMPurify; | |
4372 } | |
4373 | |
4374 var originalDocument = window.document; | |
4375 | |
4376 var document = window.document; | |
4377 var DocumentFragment = window.DocumentFragment, | |
4378 HTMLTemplateElement = window.HTMLTemplateElement, | |
4379 Node = window.Node, | |
4380 Element = window.Element, | |
4381 NodeFilter = window.NodeFilter, | |
4382 _window$NamedNodeMap = window.NamedNodeMap, | |
4383 NamedNodeMap = _window$NamedNodeMap === undefined ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap, | |
4384 Text = window.Text, | |
4385 Comment = window.Comment, | |
4386 DOMParser = window.DOMParser, | |
4387 trustedTypes = window.trustedTypes; | |
4388 | |
4389 | |
4390 var ElementPrototype = Element.prototype; | |
4391 | |
4392 var cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); | |
4393 var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); | |
4394 var getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); | |
4395 var getParentNode = lookupGetter(ElementPrototype, 'parentNode'); | |
4396 | |
4397 // As per issue #47, the web-components registry is inherited by a | |
4398 // new document created via createHTMLDocument. As per the spec | |
4399 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) | |
4400 // a new empty registry is used when creating a template contents owner | |
4401 // document, so we use that as our parent document to ensure nothing | |
4402 // is inherited. | |
4403 if (typeof HTMLTemplateElement === 'function') { | |
4404 var template = document.createElement('template'); | |
4405 if (template.content && template.content.ownerDocument) { | |
4406 document = template.content.ownerDocument; | |
4407 } | |
4408 } | |
4409 | |
4410 var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument); | |
4411 var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML('') : ''; | |
4412 | |
4413 var _document = document, | |
4414 implementation = _document.implementation, | |
4415 createNodeIterator = _document.createNodeIterator, | |
4416 getElementsByTagName = _document.getElementsByTagName, | |
4417 createDocumentFragment = _document.createDocumentFragment; | |
4418 var importNode = originalDocument.importNode; | |
4419 | |
4420 | |
4421 var documentMode = {}; | |
4422 try { | |
4423 documentMode = clone(document).documentMode ? document.documentMode : {}; | |
4424 } catch (_) {} | |
4425 | |
4426 var hooks = {}; | |
4427 | |
4428 /** | |
4429 * Expose whether this browser supports running the full DOMPurify. | |
4430 */ | |
4431 DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9; | |
4432 | |
4433 var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR, | |
4434 ERB_EXPR$$1 = ERB_EXPR, | |
4435 DATA_ATTR$$1 = DATA_ATTR, | |
4436 ARIA_ATTR$$1 = ARIA_ATTR, | |
4437 IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA, | |
4438 ATTR_WHITESPACE$$1 = ATTR_WHITESPACE; | |
4439 var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI; | |
4440 | |
4441 /** | |
4442 * We consider the elements and attributes below to be safe. Ideally | |
4443 * don't add any new ones but feel free to remove unwanted ones. | |
4444 */ | |
4445 | |
4446 /* allowed element names */ | |
4447 | |
4448 var ALLOWED_TAGS = null; | |
4449 var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text))); | |
4450 | |
4451 /* Allowed attribute names */ | |
4452 var ALLOWED_ATTR = null; | |
4453 var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml))); | |
4454 | |
4455 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ | |
4456 var FORBID_TAGS = null; | |
4457 | |
4458 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ | |
4459 var FORBID_ATTR = null; | |
4460 | |
4461 /* Decide if ARIA attributes are okay */ | |
4462 var ALLOW_ARIA_ATTR = true; | |
4463 | |
4464 /* Decide if custom data attributes are okay */ | |
4465 var ALLOW_DATA_ATTR = true; | |
4466 | |
4467 /* Decide if unknown protocols are okay */ | |
4468 var ALLOW_UNKNOWN_PROTOCOLS = false; | |
4469 | |
4470 /* Output should be safe for common template engines. | |
4471 * This means, DOMPurify removes data attributes, mustaches and ERB | |
4472 */ | |
4473 var SAFE_FOR_TEMPLATES = false; | |
4474 | |
4475 /* Decide if document with <html>... should be returned */ | |
4476 var WHOLE_DOCUMENT = false; | |
4477 | |
4478 /* Track whether config is already set on this instance of DOMPurify. */ | |
4479 var SET_CONFIG = false; | |
4480 | |
4481 /* Decide if all elements (e.g. style, script) must be children of | |
4482 * document.body. By default, browsers might move them to document.head */ | |
4483 var FORCE_BODY = false; | |
4484 | |
4485 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html | |
4486 * string (or a TrustedHTML object if Trusted Types are supported). | |
4487 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead | |
4488 */ | |
4489 var RETURN_DOM = false; | |
4490 | |
4491 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html | |
4492 * string (or a TrustedHTML object if Trusted Types are supported) */ | |
4493 var RETURN_DOM_FRAGMENT = false; | |
4494 | |
4495 /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM | |
4496 * `Node` is imported into the current `Document`. If this flag is not enabled the | |
4497 * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by | |
4498 * DOMPurify. | |
4499 * | |
4500 * This defaults to `true` starting DOMPurify 2.2.0. Note that setting it to `false` | |
4501 * might cause XSS from attacks hidden in closed shadowroots in case the browser | |
4502 * supports Declarative Shadow: DOM https://web.dev/declarative-shadow-dom/ | |
4503 */ | |
4504 var RETURN_DOM_IMPORT = true; | |
4505 | |
4506 /* Try to return a Trusted Type object instead of a string, return a string in | |
4507 * case Trusted Types are not supported */ | |
4508 var RETURN_TRUSTED_TYPE = false; | |
4509 | |
4510 /* Output should be free from DOM clobbering attacks? */ | |
4511 var SANITIZE_DOM = true; | |
4512 | |
4513 /* Keep element content when removing element? */ | |
4514 var KEEP_CONTENT = true; | |
4515 | |
4516 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead | |
4517 * of importing it into a new Document and returning a sanitized copy */ | |
4518 var IN_PLACE = false; | |
4519 | |
4520 /* Allow usage of profiles like html, svg and mathMl */ | |
4521 var USE_PROFILES = {}; | |
4522 | |
4523 /* Tags to ignore content of when KEEP_CONTENT is true */ | |
4524 var FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); | |
4525 | |
4526 /* Tags that are safe for data: URIs */ | |
4527 var DATA_URI_TAGS = null; | |
4528 var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); | |
4529 | |
4530 /* Attributes safe for values like "javascript:" */ | |
4531 var URI_SAFE_ATTRIBUTES = null; | |
4532 var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'summary', 'title', 'value', 'style', 'xmlns']); | |
4533 | |
4534 /* Keep a reference to config to pass to hooks */ | |
4535 var CONFIG = null; | |
4536 | |
4537 /* Ideally, do not touch anything below this line */ | |
4538 /* ______________________________________________ */ | |
4539 | |
4540 var formElement = document.createElement('form'); | |
4541 | |
4542 /** | |
4543 * _parseConfig | |
4544 * | |
4545 * @param {Object} cfg optional config literal | |
4546 */ | |
4547 // eslint-disable-next-line complexity | |
4548 var _parseConfig = function _parseConfig(cfg) { | |
4549 if (CONFIG && CONFIG === cfg) { | |
4550 return; | |
4551 } | |
4552 | |
4553 /* Shield configuration object from tampering */ | |
4554 if (!cfg || (typeof cfg === 'undefined' ? 'undefined' : _typeof(cfg)) !== 'object') { | |
4555 cfg = {}; | |
4556 } | |
4557 | |
4558 /* Shield configuration object from prototype pollution */ | |
4559 cfg = clone(cfg); | |
4560 | |
4561 /* Set configuration parameters */ | |
4562 ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; | |
4563 ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; | |
4564 URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES; | |
4565 DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS; | |
4566 FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS) : {}; | |
4567 FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR) : {}; | |
4568 USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false; | |
4569 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true | |
4570 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true | |
4571 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false | |
4572 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false | |
4573 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false | |
4574 RETURN_DOM = cfg.RETURN_DOM || false; // Default false | |
4575 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false | |
4576 RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; // Default true | |
4577 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false | |
4578 FORCE_BODY = cfg.FORCE_BODY || false; // Default false | |
4579 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true | |
4580 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true | |
4581 IN_PLACE = cfg.IN_PLACE || false; // Default false | |
4582 IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1; | |
4583 if (SAFE_FOR_TEMPLATES) { | |
4584 ALLOW_DATA_ATTR = false; | |
4585 } | |
4586 | |
4587 if (RETURN_DOM_FRAGMENT) { | |
4588 RETURN_DOM = true; | |
4589 } | |
4590 | |
4591 /* Parse profile info */ | |
4592 if (USE_PROFILES) { | |
4593 ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text))); | |
4594 ALLOWED_ATTR = []; | |
4595 if (USE_PROFILES.html === true) { | |
4596 addToSet(ALLOWED_TAGS, html); | |
4597 addToSet(ALLOWED_ATTR, html$1); | |
4598 } | |
4599 | |
4600 if (USE_PROFILES.svg === true) { | |
4601 addToSet(ALLOWED_TAGS, svg); | |
4602 addToSet(ALLOWED_ATTR, svg$1); | |
4603 addToSet(ALLOWED_ATTR, xml); | |
4604 } | |
4605 | |
4606 if (USE_PROFILES.svgFilters === true) { | |
4607 addToSet(ALLOWED_TAGS, svgFilters); | |
4608 addToSet(ALLOWED_ATTR, svg$1); | |
4609 addToSet(ALLOWED_ATTR, xml); | |
4610 } | |
4611 | |
4612 if (USE_PROFILES.mathMl === true) { | |
4613 addToSet(ALLOWED_TAGS, mathMl); | |
4614 addToSet(ALLOWED_ATTR, mathMl$1); | |
4615 addToSet(ALLOWED_ATTR, xml); | |
4616 } | |
4617 } | |
4618 | |
4619 /* Merge configuration parameters */ | |
4620 if (cfg.ADD_TAGS) { | |
4621 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { | |
4622 ALLOWED_TAGS = clone(ALLOWED_TAGS); | |
4623 } | |
4624 | |
4625 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); | |
4626 } | |
4627 | |
4628 if (cfg.ADD_ATTR) { | |
4629 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { | |
4630 ALLOWED_ATTR = clone(ALLOWED_ATTR); | |
4631 } | |
4632 | |
4633 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); | |
4634 } | |
4635 | |
4636 if (cfg.ADD_URI_SAFE_ATTR) { | |
4637 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR); | |
4638 } | |
4639 | |
4640 /* Add #text in case KEEP_CONTENT is set to true */ | |
4641 if (KEEP_CONTENT) { | |
4642 ALLOWED_TAGS['#text'] = true; | |
4643 } | |
4644 | |
4645 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ | |
4646 if (WHOLE_DOCUMENT) { | |
4647 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); | |
4648 } | |
4649 | |
4650 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ | |
4651 if (ALLOWED_TAGS.table) { | |
4652 addToSet(ALLOWED_TAGS, ['tbody']); | |
4653 delete FORBID_TAGS.tbody; | |
4654 } | |
4655 | |
4656 // Prevent further manipulation of configuration. | |
4657 // Not available in IE8, Safari 5, etc. | |
4658 if (freeze) { | |
4659 freeze(cfg); | |
4660 } | |
4661 | |
4662 CONFIG = cfg; | |
4663 }; | |
4664 | |
4665 var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); | |
4666 | |
4667 var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']); | |
4668 | |
4669 /* Keep track of all possible SVG and MathML tags | |
4670 * so that we can perform the namespace checks | |
4671 * correctly. */ | |
4672 var ALL_SVG_TAGS = addToSet({}, svg); | |
4673 addToSet(ALL_SVG_TAGS, svgFilters); | |
4674 addToSet(ALL_SVG_TAGS, svgDisallowed); | |
4675 | |
4676 var ALL_MATHML_TAGS = addToSet({}, mathMl); | |
4677 addToSet(ALL_MATHML_TAGS, mathMlDisallowed); | |
4678 | |
4679 var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; | |
4680 var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; | |
4681 var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; | |
4682 | |
4683 /** | |
4684 * | |
4685 * | |
4686 * @param {Element} element a DOM element whose namespace is being checked | |
4687 * @returns {boolean} Return false if the element has a | |
4688 * namespace that a spec-compliant parser would never | |
4689 * return. Return true otherwise. | |
4690 */ | |
4691 var _checkValidNamespace = function _checkValidNamespace(element) { | |
4692 var parent = getParentNode(element); | |
4693 | |
4694 // In JSDOM, if we're inside shadow DOM, then parentNode | |
4695 // can be null. We just simulate parent in this case. | |
4696 if (!parent || !parent.tagName) { | |
4697 parent = { | |
4698 namespaceURI: HTML_NAMESPACE, | |
4699 tagName: 'template' | |
4700 }; | |
4701 } | |
4702 | |
4703 var tagName = stringToLowerCase(element.tagName); | |
4704 var parentTagName = stringToLowerCase(parent.tagName); | |
4705 | |
4706 if (element.namespaceURI === SVG_NAMESPACE) { | |
4707 // The only way to switch from HTML namespace to SVG | |
4708 // is via <svg>. If it happens via any other tag, then | |
4709 // it should be killed. | |
4710 if (parent.namespaceURI === HTML_NAMESPACE) { | |
4711 return tagName === 'svg'; | |
4712 } | |
4713 | |
4714 // The only way to switch from MathML to SVG is via | |
4715 // svg if parent is either <annotation-xml> or MathML | |
4716 // text integration points. | |
4717 if (parent.namespaceURI === MATHML_NAMESPACE) { | |
4718 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); | |
4719 } | |
4720 | |
4721 // We only allow elements that are defined in SVG | |
4722 // spec. All others are disallowed in SVG namespace. | |
4723 return Boolean(ALL_SVG_TAGS[tagName]); | |
4724 } | |
4725 | |
4726 if (element.namespaceURI === MATHML_NAMESPACE) { | |
4727 // The only way to switch from HTML namespace to MathML | |
4728 // is via <math>. If it happens via any other tag, then | |
4729 // it should be killed. | |
4730 if (parent.namespaceURI === HTML_NAMESPACE) { | |
4731 return tagName === 'math'; | |
4732 } | |
4733 | |
4734 // The only way to switch from SVG to MathML is via | |
4735 // <math> and HTML integration points | |
4736 if (parent.namespaceURI === SVG_NAMESPACE) { | |
4737 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; | |
4738 } | |
4739 | |
4740 // We only allow elements that are defined in MathML | |
4741 // spec. All others are disallowed in MathML namespace. | |
4742 return Boolean(ALL_MATHML_TAGS[tagName]); | |
4743 } | |
4744 | |
4745 if (element.namespaceURI === HTML_NAMESPACE) { | |
4746 // The only way to switch from SVG to HTML is via | |
4747 // HTML integration points, and from MathML to HTML | |
4748 // is via MathML text integration points | |
4749 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { | |
4750 return false; | |
4751 } | |
4752 | |
4753 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { | |
4754 return false; | |
4755 } | |
4756 | |
4757 // Certain elements are allowed in both SVG and HTML | |
4758 // namespace. We need to specify them explicitly | |
4759 // so that they don't get erronously deleted from | |
4760 // HTML namespace. | |
4761 var commonSvgAndHTMLElements = addToSet({}, ['title', 'style', 'font', 'a', 'script']); | |
4762 | |
4763 // We disallow tags that are specific for MathML | |
4764 // or SVG and should never appear in HTML namespace | |
4765 return !ALL_MATHML_TAGS[tagName] && (commonSvgAndHTMLElements[tagName] || !ALL_SVG_TAGS[tagName]); | |
4766 } | |
4767 | |
4768 // The code should never reach this place (this means | |
4769 // that the element somehow got namespace that is not | |
4770 // HTML, SVG or MathML). Return false just in case. | |
4771 return false; | |
4772 }; | |
4773 | |
4774 /** | |
4775 * _forceRemove | |
4776 * | |
4777 * @param {Node} node a DOM node | |
4778 */ | |
4779 var _forceRemove = function _forceRemove(node) { | |
4780 arrayPush(DOMPurify.removed, { element: node }); | |
4781 try { | |
4782 node.parentNode.removeChild(node); | |
4783 } catch (_) { | |
4784 try { | |
4785 node.outerHTML = emptyHTML; | |
4786 } catch (_) { | |
4787 node.remove(); | |
4788 } | |
4789 } | |
4790 }; | |
4791 | |
4792 /** | |
4793 * _removeAttribute | |
4794 * | |
4795 * @param {String} name an Attribute name | |
4796 * @param {Node} node a DOM node | |
4797 */ | |
4798 var _removeAttribute = function _removeAttribute(name, node) { | |
4799 try { | |
4800 arrayPush(DOMPurify.removed, { | |
4801 attribute: node.getAttributeNode(name), | |
4802 from: node | |
4803 }); | |
4804 } catch (_) { | |
4805 arrayPush(DOMPurify.removed, { | |
4806 attribute: null, | |
4807 from: node | |
4808 }); | |
4809 } | |
4810 | |
4811 node.removeAttribute(name); | |
4812 }; | |
4813 | |
4814 /** | |
4815 * _initDocument | |
4816 * | |
4817 * @param {String} dirty a string of dirty markup | |
4818 * @return {Document} a DOM, filled with the dirty markup | |
4819 */ | |
4820 var _initDocument = function _initDocument(dirty) { | |
4821 /* Create a HTML document */ | |
4822 var doc = void 0; | |
4823 var leadingWhitespace = void 0; | |
4824 | |
4825 if (FORCE_BODY) { | |
4826 dirty = '<remove></remove>' + dirty; | |
4827 } else { | |
4828 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ | |
4829 var matches = stringMatch(dirty, /^[\r\n\t ]+/); | |
4830 leadingWhitespace = matches && matches[0]; | |
4831 } | |
4832 | |
4833 var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; | |
4834 /* Use the DOMParser API by default, fallback later if needs be */ | |
4835 try { | |
4836 doc = new DOMParser().parseFromString(dirtyPayload, 'text/html'); | |
4837 } catch (_) {} | |
4838 | |
4839 /* Use createHTMLDocument in case DOMParser is not available */ | |
4840 if (!doc || !doc.documentElement) { | |
4841 doc = implementation.createHTMLDocument(''); | |
4842 var _doc = doc, | |
4843 body = _doc.body; | |
4844 | |
4845 body.parentNode.removeChild(body.parentNode.firstElementChild); | |
4846 body.outerHTML = dirtyPayload; | |
4847 } | |
4848 | |
4849 if (dirty && leadingWhitespace) { | |
4850 doc.body.insertBefore(document.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null); | |
4851 } | |
4852 | |
4853 /* Work on whole document or just its body */ | |
4854 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; | |
4855 }; | |
4856 | |
4857 /** | |
4858 * _createIterator | |
4859 * | |
4860 * @param {Document} root document/fragment to create iterator for | |
4861 * @return {Iterator} iterator instance | |
4862 */ | |
4863 var _createIterator = function _createIterator(root) { | |
4864 return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function () { | |
4865 return NodeFilter.FILTER_ACCEPT; | |
4866 }, false); | |
4867 }; | |
4868 | |
4869 /** | |
4870 * _isClobbered | |
4871 * | |
4872 * @param {Node} elm element to check for clobbering attacks | |
4873 * @return {Boolean} true if clobbered, false if safe | |
4874 */ | |
4875 var _isClobbered = function _isClobbered(elm) { | |
4876 if (elm instanceof Text || elm instanceof Comment) { | |
4877 return false; | |
4878 } | |
4879 | |
4880 if (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function') { | |
4881 return true; | |
4882 } | |
4883 | |
4884 return false; | |
4885 }; | |
4886 | |
4887 /** | |
4888 * _isNode | |
4889 * | |
4890 * @param {Node} obj object to check whether it's a DOM node | |
4891 * @return {Boolean} true is object is a DOM node | |
4892 */ | |
4893 var _isNode = function _isNode(object) { | |
4894 return (typeof Node === 'undefined' ? 'undefined' : _typeof(Node)) === 'object' ? object instanceof Node : object && (typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'; | |
4895 }; | |
4896 | |
4897 /** | |
4898 * _executeHook | |
4899 * Execute user configurable hooks | |
4900 * | |
4901 * @param {String} entryPoint Name of the hook's entry point | |
4902 * @param {Node} currentNode node to work on with the hook | |
4903 * @param {Object} data additional hook parameters | |
4904 */ | |
4905 var _executeHook = function _executeHook(entryPoint, currentNode, data) { | |
4906 if (!hooks[entryPoint]) { | |
4907 return; | |
4908 } | |
4909 | |
4910 arrayForEach(hooks[entryPoint], function (hook) { | |
4911 hook.call(DOMPurify, currentNode, data, CONFIG); | |
4912 }); | |
4913 }; | |
4914 | |
4915 /** | |
4916 * _sanitizeElements | |
4917 * | |
4918 * @protect nodeName | |
4919 * @protect textContent | |
4920 * @protect removeChild | |
4921 * | |
4922 * @param {Node} currentNode to check for permission to exist | |
4923 * @return {Boolean} true if node was killed, false if left alive | |
4924 */ | |
4925 var _sanitizeElements = function _sanitizeElements(currentNode) { | |
4926 var content = void 0; | |
4927 | |
4928 /* Execute a hook if present */ | |
4929 _executeHook('beforeSanitizeElements', currentNode, null); | |
4930 | |
4931 /* Check if element is clobbered or can clobber */ | |
4932 if (_isClobbered(currentNode)) { | |
4933 _forceRemove(currentNode); | |
4934 return true; | |
4935 } | |
4936 | |
4937 /* Check if tagname contains Unicode */ | |
4938 if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) { | |
4939 _forceRemove(currentNode); | |
4940 return true; | |
4941 } | |
4942 | |
4943 /* Now let's check the element's type and name */ | |
4944 var tagName = stringToLowerCase(currentNode.nodeName); | |
4945 | |
4946 /* Execute a hook if present */ | |
4947 _executeHook('uponSanitizeElement', currentNode, { | |
4948 tagName: tagName, | |
4949 allowedTags: ALLOWED_TAGS | |
4950 }); | |
4951 | |
4952 /* Detect mXSS attempts abusing namespace confusion */ | |
4953 if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { | |
4954 _forceRemove(currentNode); | |
4955 return true; | |
4956 } | |
4957 | |
4958 /* Remove element if anything forbids its presence */ | |
4959 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { | |
4960 /* Keep content except for bad-listed elements */ | |
4961 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { | |
4962 var parentNode = getParentNode(currentNode); | |
4963 var childNodes = getChildNodes(currentNode); | |
4964 var childCount = childNodes.length; | |
4965 for (var i = childCount - 1; i >= 0; --i) { | |
4966 parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)); | |
4967 } | |
4968 } | |
4969 | |
4970 _forceRemove(currentNode); | |
4971 return true; | |
4972 } | |
4973 | |
4974 /* Check whether element has a valid namespace */ | |
4975 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { | |
4976 _forceRemove(currentNode); | |
4977 return true; | |
4978 } | |
4979 | |
4980 if ((tagName === 'noscript' || tagName === 'noembed') && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) { | |
4981 _forceRemove(currentNode); | |
4982 return true; | |
4983 } | |
4984 | |
4985 /* Sanitize element content to be template-safe */ | |
4986 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { | |
4987 /* Get the element's text content */ | |
4988 content = currentNode.textContent; | |
4989 content = stringReplace(content, MUSTACHE_EXPR$$1, ' '); | |
4990 content = stringReplace(content, ERB_EXPR$$1, ' '); | |
4991 if (currentNode.textContent !== content) { | |
4992 arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() }); | |
4993 currentNode.textContent = content; | |
4994 } | |
4995 } | |
4996 | |
4997 /* Execute a hook if present */ | |
4998 _executeHook('afterSanitizeElements', currentNode, null); | |
4999 | |
5000 return false; | |
5001 }; | |
5002 | |
5003 /** | |
5004 * _isValidAttribute | |
5005 * | |
5006 * @param {string} lcTag Lowercase tag name of containing element. | |
5007 * @param {string} lcName Lowercase attribute name. | |
5008 * @param {string} value Attribute value. | |
5009 * @return {Boolean} Returns true if `value` is valid, otherwise false. | |
5010 */ | |
5011 // eslint-disable-next-line complexity | |
5012 var _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { | |
5013 /* Make sure attribute cannot clobber */ | |
5014 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { | |
5015 return false; | |
5016 } | |
5017 | |
5018 /* Allow valid data-* attributes: At least one character after "-" | |
5019 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) | |
5020 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) | |
5021 We don't need to check the value; it's always URI safe. */ | |
5022 if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { | |
5023 return false; | |
5024 | |
5025 /* Check value is safe. First, is attr inert? If so, is safe */ | |
5026 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))) ; else if (!value) ; else { | |
5027 return false; | |
5028 } | |
5029 | |
5030 return true; | |
5031 }; | |
5032 | |
5033 /** | |
5034 * _sanitizeAttributes | |
5035 * | |
5036 * @protect attributes | |
5037 * @protect nodeName | |
5038 * @protect removeAttribute | |
5039 * @protect setAttribute | |
5040 * | |
5041 * @param {Node} currentNode to sanitize | |
5042 */ | |
5043 var _sanitizeAttributes = function _sanitizeAttributes(currentNode) { | |
5044 var attr = void 0; | |
5045 var value = void 0; | |
5046 var lcName = void 0; | |
5047 var l = void 0; | |
5048 /* Execute a hook if present */ | |
5049 _executeHook('beforeSanitizeAttributes', currentNode, null); | |
5050 | |
5051 var attributes = currentNode.attributes; | |
5052 | |
5053 /* Check if we have attributes; if not we might have a text node */ | |
5054 | |
5055 if (!attributes) { | |
5056 return; | |
5057 } | |
5058 | |
5059 var hookEvent = { | |
5060 attrName: '', | |
5061 attrValue: '', | |
5062 keepAttr: true, | |
5063 allowedAttributes: ALLOWED_ATTR | |
5064 }; | |
5065 l = attributes.length; | |
5066 | |
5067 /* Go backwards over all attributes; safely remove bad ones */ | |
5068 while (l--) { | |
5069 attr = attributes[l]; | |
5070 var _attr = attr, | |
5071 name = _attr.name, | |
5072 namespaceURI = _attr.namespaceURI; | |
5073 | |
5074 value = stringTrim(attr.value); | |
5075 lcName = stringToLowerCase(name); | |
5076 | |
5077 /* Execute a hook if present */ | |
5078 hookEvent.attrName = lcName; | |
5079 hookEvent.attrValue = value; | |
5080 hookEvent.keepAttr = true; | |
5081 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set | |
5082 _executeHook('uponSanitizeAttribute', currentNode, hookEvent); | |
5083 value = hookEvent.attrValue; | |
5084 /* Did the hooks approve of the attribute? */ | |
5085 if (hookEvent.forceKeepAttr) { | |
5086 continue; | |
5087 } | |
5088 | |
5089 /* Remove attribute */ | |
5090 _removeAttribute(name, currentNode); | |
5091 | |
5092 /* Did the hooks approve of the attribute? */ | |
5093 if (!hookEvent.keepAttr) { | |
5094 continue; | |
5095 } | |
5096 | |
5097 /* Work around a security issue in jQuery 3.0 */ | |
5098 if (regExpTest(/\/>/i, value)) { | |
5099 _removeAttribute(name, currentNode); | |
5100 continue; | |
5101 } | |
5102 | |
5103 /* Sanitize attribute content to be template-safe */ | |
5104 if (SAFE_FOR_TEMPLATES) { | |
5105 value = stringReplace(value, MUSTACHE_EXPR$$1, ' '); | |
5106 value = stringReplace(value, ERB_EXPR$$1, ' '); | |
5107 } | |
5108 | |
5109 /* Is `value` valid for this attribute? */ | |
5110 var lcTag = currentNode.nodeName.toLowerCase(); | |
5111 if (!_isValidAttribute(lcTag, lcName, value)) { | |
5112 continue; | |
5113 } | |
5114 | |
5115 /* Handle invalid data-* attribute set by try-catching it */ | |
5116 try { | |
5117 if (namespaceURI) { | |
5118 currentNode.setAttributeNS(namespaceURI, name, value); | |
5119 } else { | |
5120 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ | |
5121 currentNode.setAttribute(name, value); | |
5122 } | |
5123 | |
5124 arrayPop(DOMPurify.removed); | |
5125 } catch (_) {} | |
5126 } | |
5127 | |
5128 /* Execute a hook if present */ | |
5129 _executeHook('afterSanitizeAttributes', currentNode, null); | |
5130 }; | |
5131 | |
5132 /** | |
5133 * _sanitizeShadowDOM | |
5134 * | |
5135 * @param {DocumentFragment} fragment to iterate over recursively | |
5136 */ | |
5137 var _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { | |
5138 var shadowNode = void 0; | |
5139 var shadowIterator = _createIterator(fragment); | |
5140 | |
5141 /* Execute a hook if present */ | |
5142 _executeHook('beforeSanitizeShadowDOM', fragment, null); | |
5143 | |
5144 while (shadowNode = shadowIterator.nextNode()) { | |
5145 /* Execute a hook if present */ | |
5146 _executeHook('uponSanitizeShadowNode', shadowNode, null); | |
5147 | |
5148 /* Sanitize tags and elements */ | |
5149 if (_sanitizeElements(shadowNode)) { | |
5150 continue; | |
5151 } | |
5152 | |
5153 /* Deep shadow DOM detected */ | |
5154 if (shadowNode.content instanceof DocumentFragment) { | |
5155 _sanitizeShadowDOM(shadowNode.content); | |
5156 } | |
5157 | |
5158 /* Check attributes, sanitize if necessary */ | |
5159 _sanitizeAttributes(shadowNode); | |
5160 } | |
5161 | |
5162 /* Execute a hook if present */ | |
5163 _executeHook('afterSanitizeShadowDOM', fragment, null); | |
5164 }; | |
5165 | |
5166 /** | |
5167 * Sanitize | |
5168 * Public method providing core sanitation functionality | |
5169 * | |
5170 * @param {String|Node} dirty string or DOM node | |
5171 * @param {Object} configuration object | |
5172 */ | |
5173 // eslint-disable-next-line complexity | |
5174 DOMPurify.sanitize = function (dirty, cfg) { | |
5175 var body = void 0; | |
5176 var importedNode = void 0; | |
5177 var currentNode = void 0; | |
5178 var oldNode = void 0; | |
5179 var returnNode = void 0; | |
5180 /* Make sure we have a string to sanitize. | |
5181 DO NOT return early, as this will return the wrong type if | |
5182 the user has requested a DOM object rather than a string */ | |
5183 if (!dirty) { | |
5184 dirty = '<!-->'; | |
5185 } | |
5186 | |
5187 /* Stringify, in case dirty is an object */ | |
5188 if (typeof dirty !== 'string' && !_isNode(dirty)) { | |
5189 // eslint-disable-next-line no-negated-condition | |
5190 if (typeof dirty.toString !== 'function') { | |
5191 throw typeErrorCreate('toString is not a function'); | |
5192 } else { | |
5193 dirty = dirty.toString(); | |
5194 if (typeof dirty !== 'string') { | |
5195 throw typeErrorCreate('dirty is not a string, aborting'); | |
5196 } | |
5197 } | |
5198 } | |
5199 | |
5200 /* Check we can run. Otherwise fall back or ignore */ | |
5201 if (!DOMPurify.isSupported) { | |
5202 if (_typeof(window.toStaticHTML) === 'object' || typeof window.toStaticHTML === 'function') { | |
5203 if (typeof dirty === 'string') { | |
5204 return window.toStaticHTML(dirty); | |
5205 } | |
5206 | |
5207 if (_isNode(dirty)) { | |
5208 return window.toStaticHTML(dirty.outerHTML); | |
5209 } | |
5210 } | |
5211 | |
5212 return dirty; | |
5213 } | |
5214 | |
5215 /* Assign config vars */ | |
5216 if (!SET_CONFIG) { | |
5217 _parseConfig(cfg); | |
5218 } | |
5219 | |
5220 /* Clean up removed elements */ | |
5221 DOMPurify.removed = []; | |
5222 | |
5223 /* Check if dirty is correctly typed for IN_PLACE */ | |
5224 if (typeof dirty === 'string') { | |
5225 IN_PLACE = false; | |
5226 } | |
5227 | |
5228 if (IN_PLACE) ; else if (dirty instanceof Node) { | |
5229 /* If dirty is a DOM element, append to an empty document to avoid | |
5230 elements being stripped by the parser */ | |
5231 body = _initDocument('<!---->'); | |
5232 importedNode = body.ownerDocument.importNode(dirty, true); | |
5233 if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') { | |
5234 /* Node is already a body, use as is */ | |
5235 body = importedNode; | |
5236 } else if (importedNode.nodeName === 'HTML') { | |
5237 body = importedNode; | |
5238 } else { | |
5239 // eslint-disable-next-line unicorn/prefer-node-append | |
5240 body.appendChild(importedNode); | |
5241 } | |
5242 } else { | |
5243 /* Exit directly if we have nothing to do */ | |
5244 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && | |
5245 // eslint-disable-next-line unicorn/prefer-includes | |
5246 dirty.indexOf('<') === -1) { | |
5247 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; | |
5248 } | |
5249 | |
5250 /* Initialize the document to work on */ | |
5251 body = _initDocument(dirty); | |
5252 | |
5253 /* Check we have a DOM node from the data */ | |
5254 if (!body) { | |
5255 return RETURN_DOM ? null : emptyHTML; | |
5256 } | |
5257 } | |
5258 | |
5259 /* Remove first element node (ours) if FORCE_BODY is set */ | |
5260 if (body && FORCE_BODY) { | |
5261 _forceRemove(body.firstChild); | |
5262 } | |
5263 | |
5264 /* Get node iterator */ | |
5265 var nodeIterator = _createIterator(IN_PLACE ? dirty : body); | |
5266 | |
5267 /* Now start iterating over the created document */ | |
5268 while (currentNode = nodeIterator.nextNode()) { | |
5269 /* Fix IE's strange behavior with manipulated textNodes #89 */ | |
5270 if (currentNode.nodeType === 3 && currentNode === oldNode) { | |
5271 continue; | |
5272 } | |
5273 | |
5274 /* Sanitize tags and elements */ | |
5275 if (_sanitizeElements(currentNode)) { | |
5276 continue; | |
5277 } | |
5278 | |
5279 /* Shadow DOM detected, sanitize it */ | |
5280 if (currentNode.content instanceof DocumentFragment) { | |
5281 _sanitizeShadowDOM(currentNode.content); | |
5282 } | |
5283 | |
5284 /* Check attributes, sanitize if necessary */ | |
5285 _sanitizeAttributes(currentNode); | |
5286 | |
5287 oldNode = currentNode; | |
5288 } | |
5289 | |
5290 oldNode = null; | |
5291 | |
5292 /* If we sanitized `dirty` in-place, return it. */ | |
5293 if (IN_PLACE) { | |
5294 return dirty; | |
5295 } | |
5296 | |
5297 /* Return sanitized string or DOM */ | |
5298 if (RETURN_DOM) { | |
5299 if (RETURN_DOM_FRAGMENT) { | |
5300 returnNode = createDocumentFragment.call(body.ownerDocument); | |
5301 | |
5302 while (body.firstChild) { | |
5303 // eslint-disable-next-line unicorn/prefer-node-append | |
5304 returnNode.appendChild(body.firstChild); | |
5305 } | |
5306 } else { | |
5307 returnNode = body; | |
5308 } | |
5309 | |
5310 if (RETURN_DOM_IMPORT) { | |
5311 /* | |
5312 AdoptNode() is not used because internal state is not reset | |
5313 (e.g. the past names map of a HTMLFormElement), this is safe | |
5314 in theory but we would rather not risk another attack vector. | |
5315 The state that is cloned by importNode() is explicitly defined | |
5316 by the specs. | |
5317 */ | |
5318 returnNode = importNode.call(originalDocument, returnNode, true); | |
5319 } | |
5320 | |
5321 return returnNode; | |
5322 } | |
5323 | |
5324 var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; | |
5325 | |
5326 /* Sanitize final string template-safe */ | |
5327 if (SAFE_FOR_TEMPLATES) { | |
5328 serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, ' '); | |
5329 serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, ' '); | |
5330 } | |
5331 | |
5332 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; | |
5333 }; | |
5334 | |
5335 /** | |
5336 * Public method to set the configuration once | |
5337 * setConfig | |
5338 * | |
5339 * @param {Object} cfg configuration object | |
5340 */ | |
5341 DOMPurify.setConfig = function (cfg) { | |
5342 _parseConfig(cfg); | |
5343 SET_CONFIG = true; | |
5344 }; | |
5345 | |
5346 /** | |
5347 * Public method to remove the configuration | |
5348 * clearConfig | |
5349 * | |
5350 */ | |
5351 DOMPurify.clearConfig = function () { | |
5352 CONFIG = null; | |
5353 SET_CONFIG = false; | |
5354 }; | |
5355 | |
5356 /** | |
5357 * Public method to check if an attribute value is valid. | |
5358 * Uses last set config, if any. Otherwise, uses config defaults. | |
5359 * isValidAttribute | |
5360 * | |
5361 * @param {string} tag Tag name of containing element. | |
5362 * @param {string} attr Attribute name. | |
5363 * @param {string} value Attribute value. | |
5364 * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false. | |
5365 */ | |
5366 DOMPurify.isValidAttribute = function (tag, attr, value) { | |
5367 /* Initialize shared config vars if necessary. */ | |
5368 if (!CONFIG) { | |
5369 _parseConfig({}); | |
5370 } | |
5371 | |
5372 var lcTag = stringToLowerCase(tag); | |
5373 var lcName = stringToLowerCase(attr); | |
5374 return _isValidAttribute(lcTag, lcName, value); | |
5375 }; | |
5376 | |
5377 /** | |
5378 * AddHook | |
5379 * Public method to add DOMPurify hooks | |
5380 * | |
5381 * @param {String} entryPoint entry point for the hook to add | |
5382 * @param {Function} hookFunction function to execute | |
5383 */ | |
5384 DOMPurify.addHook = function (entryPoint, hookFunction) { | |
5385 if (typeof hookFunction !== 'function') { | |
5386 return; | |
5387 } | |
5388 | |
5389 hooks[entryPoint] = hooks[entryPoint] || []; | |
5390 arrayPush(hooks[entryPoint], hookFunction); | |
5391 }; | |
5392 | |
5393 /** | |
5394 * RemoveHook | |
5395 * Public method to remove a DOMPurify hook at a given entryPoint | |
5396 * (pops it from the stack of hooks if more are present) | |
5397 * | |
5398 * @param {String} entryPoint entry point for the hook to remove | |
5399 */ | |
5400 DOMPurify.removeHook = function (entryPoint) { | |
5401 if (hooks[entryPoint]) { | |
5402 arrayPop(hooks[entryPoint]); | |
5403 } | |
5404 }; | |
5405 | |
5406 /** | |
5407 * RemoveHooks | |
5408 * Public method to remove all DOMPurify hooks at a given entryPoint | |
5409 * | |
5410 * @param {String} entryPoint entry point for the hooks to remove | |
5411 */ | |
5412 DOMPurify.removeHooks = function (entryPoint) { | |
5413 if (hooks[entryPoint]) { | |
5414 hooks[entryPoint] = []; | |
5415 } | |
5416 }; | |
5417 | |
5418 /** | |
5419 * RemoveAllHooks | |
5420 * Public method to remove all DOMPurify hooks | |
5421 * | |
5422 */ | |
5423 DOMPurify.removeAllHooks = function () { | |
5424 hooks = {}; | |
5425 }; | |
5426 | |
5427 return DOMPurify; | |
5428 } | |
5429 | |
5430 var purify = createDOMPurify(); | |
5431 | |
5432 var globalWin = window; | |
5433 var globalDoc = document; | |
5434 | |
5435 var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i; | |
5436 | |
5437 /** | |
5438 * Wrap inlines that are in the root in paragraphs. | |
5439 * | |
5440 * @param {HTMLBodyElement} body | |
5441 * @param {Document} doc | |
5442 * @private | |
5443 */ | |
5444 function wrapInlines(body, doc) { | |
5445 var wrapper; | |
5446 | |
5447 traverse(body, function (node) { | |
5448 if (isInline(node, true)) { | |
5449 // Ignore text nodes unless they contain non-whitespace chars as | |
5450 // whitespace will be collapsed. | |
5451 // Ignore sceditor-ignore elements unless wrapping siblings | |
5452 // Should still wrap both if wrapping siblings. | |
5453 if (wrapper || node.nodeType === TEXT_NODE ? | |
5454 /\S/.test(node.nodeValue) : !is(node, '.sceditor-ignore')) { | |
5455 if (!wrapper) { | |
5456 wrapper = createElement('p', {}, doc); | |
5457 insertBefore(wrapper, node); | |
5458 } | |
5459 | |
5460 appendChild(wrapper, node); | |
5461 } | |
5462 } else { | |
5463 wrapper = null; | |
5464 } | |
5465 }, false, true); | |
5466 } | |
5467 /** | |
5468 * SCEditor - A lightweight WYSIWYG editor | |
5469 * | |
5470 * @param {HTMLTextAreaElement} original The textarea to be converted | |
5471 * @param {Object} userOptions | |
5472 * @class SCEditor | |
5473 * @name SCEditor | |
5474 */ | |
5475 function SCEditor(original, userOptions) { | |
5476 /** | |
5477 * Alias of this | |
5478 * | |
5479 * @private | |
5480 */ | |
5481 var base = this; | |
5482 | |
5483 /** | |
5484 * Editor format like BBCode or HTML | |
5485 */ | |
5486 var format; | |
5487 | |
5488 /** | |
5489 * The div which contains the editor and toolbar | |
5490 * | |
5491 * @type {HTMLDivElement} | |
5492 * @private | |
5493 */ | |
5494 var editorContainer; | |
5495 | |
5496 /** | |
5497 * Map of events handlers bound to this instance. | |
5498 * | |
5499 * @type {Object} | |
5500 * @private | |
5501 */ | |
5502 var eventHandlers = {}; | |
5503 | |
5504 /** | |
5505 * The editors toolbar | |
5506 * | |
5507 * @type {HTMLDivElement} | |
5508 * @private | |
5509 */ | |
5510 var toolbar; | |
5511 | |
5512 /** | |
5513 * The editors iframe which should be in design mode | |
5514 * | |
5515 * @type {HTMLIFrameElement} | |
5516 * @private | |
5517 */ | |
5518 var wysiwygEditor; | |
5519 | |
5520 /** | |
5521 * The editors window | |
5522 * | |
5523 * @type {Window} | |
5524 * @private | |
5525 */ | |
5526 var wysiwygWindow; | |
5527 | |
5528 /** | |
5529 * The WYSIWYG editors body element | |
5530 * | |
5531 * @type {HTMLBodyElement} | |
5532 * @private | |
5533 */ | |
5534 var wysiwygBody; | |
5535 | |
5536 /** | |
5537 * The WYSIWYG editors document | |
5538 * | |
5539 * @type {Document} | |
5540 * @private | |
5541 */ | |
5542 var wysiwygDocument; | |
5543 | |
5544 /** | |
5545 * The editors textarea for viewing source | |
5546 * | |
5547 * @type {HTMLTextAreaElement} | |
5548 * @private | |
5549 */ | |
5550 var sourceEditor; | |
5551 | |
5552 /** | |
5553 * The current dropdown | |
5554 * | |
5555 * @type {HTMLDivElement} | |
5556 * @private | |
5557 */ | |
5558 var dropdown; | |
5559 | |
5560 /** | |
5561 * If the user is currently composing text via IME | |
5562 * @type {boolean} | |
5563 */ | |
5564 var isComposing; | |
5565 | |
5566 /** | |
5567 * Timer for valueChanged key handler | |
5568 * @type {number} | |
5569 */ | |
5570 var valueChangedKeyUpTimer; | |
5571 | |
5572 /** | |
5573 * The editors locale | |
5574 * | |
5575 * @private | |
5576 */ | |
5577 var locale; | |
5578 | |
5579 /** | |
5580 * Stores a cache of preloaded images | |
5581 * | |
5582 * @private | |
5583 * @type {Array.<HTMLImageElement>} | |
5584 */ | |
5585 var preLoadCache = []; | |
5586 | |
5587 /** | |
5588 * The editors rangeHelper instance | |
5589 * | |
5590 * @type {RangeHelper} | |
5591 * @private | |
5592 */ | |
5593 var rangeHelper; | |
5594 | |
5595 /** | |
5596 * An array of button state handlers | |
5597 * | |
5598 * @type {Array.<Object>} | |
5599 * @private | |
5600 */ | |
5601 var btnStateHandlers = []; | |
5602 | |
5603 /** | |
5604 * Plugin manager instance | |
5605 * | |
5606 * @type {PluginManager} | |
5607 * @private | |
5608 */ | |
5609 var pluginManager; | |
5610 | |
5611 /** | |
5612 * The current node containing the selection/caret | |
5613 * | |
5614 * @type {Node} | |
5615 * @private | |
5616 */ | |
5617 var currentNode; | |
5618 | |
5619 /** | |
5620 * The first block level parent of the current node | |
5621 * | |
5622 * @type {node} | |
5623 * @private | |
5624 */ | |
5625 var currentBlockNode; | |
5626 | |
5627 /** | |
5628 * The current node selection/caret | |
5629 * | |
5630 * @type {Object} | |
5631 * @private | |
5632 */ | |
5633 var currentSelection; | |
5634 | |
5635 /** | |
5636 * Used to make sure only 1 selection changed | |
5637 * check is called every 100ms. | |
5638 * | |
5639 * Helps improve performance as it is checked a lot. | |
5640 * | |
5641 * @type {boolean} | |
5642 * @private | |
5643 */ | |
5644 var isSelectionCheckPending; | |
5645 | |
5646 /** | |
5647 * If content is required (equivalent to the HTML5 required attribute) | |
5648 * | |
5649 * @type {boolean} | |
5650 * @private | |
5651 */ | |
5652 var isRequired; | |
5653 | |
5654 /** | |
5655 * The inline CSS style element. Will be undefined | |
5656 * until css() is called for the first time. | |
5657 * | |
5658 * @type {HTMLStyleElement} | |
5659 * @private | |
5660 */ | |
5661 var inlineCss; | |
5662 | |
5663 /** | |
5664 * Object containing a list of shortcut handlers | |
5665 * | |
5666 * @type {Object} | |
5667 * @private | |
5668 */ | |
5669 var shortcutHandlers = {}; | |
5670 | |
5671 /** | |
5672 * The min and max heights that autoExpand should stay within | |
5673 * | |
5674 * @type {Object} | |
5675 * @private | |
5676 */ | |
5677 var autoExpandBounds; | |
5678 | |
5679 /** | |
5680 * Timeout for the autoExpand function to throttle calls | |
5681 * | |
5682 * @private | |
5683 */ | |
5684 var autoExpandThrottle; | |
5685 | |
5686 /** | |
5687 * Cache of the current toolbar buttons | |
5688 * | |
5689 * @type {Object} | |
5690 * @private | |
5691 */ | |
5692 var toolbarButtons = {}; | |
5693 | |
5694 /** | |
5695 * Last scroll position before maximizing so | |
5696 * it can be restored when finished. | |
5697 * | |
5698 * @type {number} | |
5699 * @private | |
5700 */ | |
5701 var maximizeScrollPosition; | |
5702 | |
5703 /** | |
5704 * Stores the contents while a paste is taking place. | |
5705 * | |
5706 * Needed to support browsers that lack clipboard API support. | |
5707 * | |
5708 * @type {?DocumentFragment} | |
5709 * @private | |
5710 */ | |
5711 var pasteContentFragment; | |
5712 | |
5713 /** | |
5714 * All the emoticons from dropdown, more and hidden combined | |
5715 * and with the emoticons root set | |
5716 * | |
5717 * @type {!Object<string, string>} | |
5718 * @private | |
5719 */ | |
5720 var allEmoticons = {}; | |
5721 | |
5722 /** | |
5723 * Current icon set if any | |
5724 * | |
5725 * @type {?Object} | |
5726 * @private | |
5727 */ | |
5728 var icons; | |
5729 | |
5730 /** | |
5731 * Private functions | |
5732 * @private | |
5733 */ | |
5734 var init, | |
5735 replaceEmoticons, | |
5736 handleCommand, | |
5737 initEditor, | |
5738 initLocale, | |
5739 initToolBar, | |
5740 initOptions, | |
5741 initEvents, | |
5742 initResize, | |
5743 initEmoticons, | |
5744 handlePasteEvt, | |
5745 handleCutCopyEvt, | |
5746 handlePasteData, | |
5747 handleKeyDown, | |
5748 handleBackSpace, | |
5749 handleKeyPress, | |
5750 handleFormReset, | |
5751 handleMouseDown, | |
5752 handleComposition, | |
5753 handleEvent, | |
5754 handleDocumentClick, | |
5755 updateToolBar, | |
5756 updateActiveButtons, | |
5757 sourceEditorSelectedText, | |
5758 appendNewLine, | |
5759 checkSelectionChanged, | |
5760 checkNodeChanged, | |
5761 autofocus, | |
5762 emoticonsKeyPress, | |
5763 emoticonsCheckWhitespace, | |
5764 currentStyledBlockNode, | |
5765 triggerValueChanged, | |
5766 valueChangedBlur, | |
5767 valueChangedKeyUp, | |
5768 autoUpdate, | |
5769 autoExpand; | |
5770 | |
5771 /** | |
5772 * All the commands supported by the editor | |
5773 * @name commands | |
5774 * @memberOf SCEditor.prototype | |
5775 */ | |
5776 base.commands = extend(true, {}, (userOptions.commands || defaultCmds)); | |
5777 | |
5778 /** | |
5779 * Options for this editor instance | |
5780 * @name opts | |
5781 * @memberOf SCEditor.prototype | |
5782 */ | |
5783 var options = base.opts = extend( | |
5784 true, {}, defaultOptions, userOptions | |
5785 ); | |
5786 | |
5787 // Don't deep extend emoticons (fixes #565) | |
5788 base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons; | |
5789 | |
5790 if (!Array.isArray(options.allowedIframeUrls)) { | |
5791 options.allowedIframeUrls = []; | |
5792 } | |
5793 options.allowedIframeUrls.push('https://www.youtube-nocookie.com/embed/'); | |
5794 | |
5795 // Create new instance of DOMPurify for each editor instance so can | |
5796 // have different allowed iframe URLs | |
5797 // eslint-disable-next-line new-cap | |
5798 var domPurify = purify(); | |
5799 | |
5800 // Allow iframes for things like YouTube, see: | |
5801 // https://github.com/cure53/DOMPurify/issues/340#issuecomment-670758980 | |
5802 domPurify.addHook('uponSanitizeElement', function (node, data) { | |
5803 var allowedUrls = options.allowedIframeUrls; | |
5804 | |
5805 if (data.tagName === 'iframe') { | |
5806 var src = attr(node, 'src') || ''; | |
5807 | |
5808 for (var i = 0; i < allowedUrls.length; i++) { | |
5809 var url = allowedUrls[i]; | |
5810 | |
5811 if (isString(url) && src.substr(0, url.length) === url) { | |
5812 return; | |
5813 } | |
5814 | |
5815 // Handle regex | |
5816 if (url.test && url.test(src)) { | |
5817 return; | |
5818 } | |
5819 } | |
5820 | |
5821 // No match so remove | |
5822 remove(node); | |
5823 } | |
5824 }); | |
5825 | |
5826 // Convert target attribute into data-sce-target attributes so XHTML format | |
5827 // can allow them | |
5828 domPurify.addHook('afterSanitizeAttributes', function (node) { | |
5829 if ('target' in node) { | |
5830 attr(node, 'data-sce-target', attr(node, 'target')); | |
5831 } | |
5832 | |
5833 removeAttr(node, 'target'); | |
5834 }); | |
5835 | |
5836 /** | |
5837 * Sanitize HTML to avoid XSS | |
5838 * | |
5839 * @param {string} html | |
5840 * @return {string} html | |
5841 * @private | |
5842 */ | |
5843 function sanitize(html) { | |
5844 return domPurify.sanitize(html, { | |
5845 ADD_TAGS: ['iframe'], | |
5846 ADD_ATTR: ['allowfullscreen', 'frameborder', 'target'] | |
5847 }); | |
5848 } | |
5849 /** | |
5850 * Creates the editor iframe and textarea | |
5851 * @private | |
5852 */ | |
5853 init = function () { | |
5854 original._sceditor = base; | |
5855 | |
5856 // Load locale | |
5857 if (options.locale && options.locale !== 'en') { | |
5858 initLocale(); | |
5859 } | |
5860 | |
5861 editorContainer = createElement('div', { | |
5862 className: 'sceditor-container' | |
5863 }); | |
5864 | |
5865 insertBefore(editorContainer, original); | |
5866 css(editorContainer, 'z-index', options.zIndex); | |
5867 | |
5868 isRequired = original.required; | |
5869 original.required = false; | |
5870 | |
5871 var FormatCtor = SCEditor.formats[options.format]; | |
5872 format = FormatCtor ? new FormatCtor() : {}; | |
5873 /* | |
5874 * Plugins should be initialized before the formatters since | |
5875 * they may wish to add or change formatting handlers and | |
5876 * since the bbcode format caches its handlers, | |
5877 * such changes must be done first. | |
5878 */ | |
5879 pluginManager = new PluginManager(base); | |
5880 (options.plugins || '').split(',').forEach(function (plugin) { | |
5881 pluginManager.register(plugin.trim()); | |
5882 }); | |
5883 if ('init' in format) { | |
5884 format.init.call(base); | |
5885 } | |
5886 | |
5887 // create the editor | |
5888 initEmoticons(); | |
5889 initToolBar(); | |
5890 initEditor(); | |
5891 initOptions(); | |
5892 initEvents(); | |
5893 | |
5894 // force into source mode if is a browser that can't handle | |
5895 // full editing | |
5896 if (!isWysiwygSupported) { | |
5897 base.toggleSourceMode(); | |
5898 } | |
5899 | |
5900 updateActiveButtons(); | |
5901 | |
5902 var loaded = function () { | |
5903 off(globalWin, 'load', loaded); | |
5904 | |
5905 if (options.autofocus) { | |
5906 autofocus(!!options.autofocusEnd); | |
5907 } | |
5908 | |
5909 autoExpand(); | |
5910 appendNewLine(); | |
5911 // TODO: use editor doc and window? | |
5912 pluginManager.call('ready'); | |
5913 if ('onReady' in format) { | |
5914 format.onReady.call(base); | |
5915 } | |
5916 }; | |
5917 on(globalWin, 'load', loaded); | |
5918 if (globalDoc.readyState === 'complete') { | |
5919 loaded(); | |
5920 } | |
5921 }; | |
5922 | |
5923 /** | |
5924 * Init the locale variable with the specified locale if possible | |
5925 * @private | |
5926 * @return void | |
5927 */ | |
5928 initLocale = function () { | |
5929 var lang; | |
5930 | |
5931 locale = SCEditor.locale[options.locale]; | |
5932 | |
5933 if (!locale) { | |
5934 lang = options.locale.split('-'); | |
5935 locale = SCEditor.locale[lang[0]]; | |
5936 } | |
5937 | |
5938 // Locale DateTime format overrides any specified in the options | |
5939 if (locale && locale.dateFormat) { | |
5940 options.dateFormat = locale.dateFormat; | |
5941 } | |
5942 }; | |
5943 | |
5944 /** | |
5945 * Creates the editor iframe and textarea | |
5946 * @private | |
5947 */ | |
5948 initEditor = function () { | |
5949 sourceEditor = createElement('textarea'); | |
5950 wysiwygEditor = createElement('iframe', { | |
5951 frameborder: 0, | |
5952 allowfullscreen: true | |
5953 }); | |
5954 | |
5955 /* | |
5956 * This needs to be done right after they are created because, | |
5957 * for any reason, the user may not want the value to be tinkered | |
5958 * by any filters. | |
5959 */ | |
5960 if (options.startInSourceMode) { | |
5961 addClass(editorContainer, 'sourceMode'); | |
5962 hide(wysiwygEditor); | |
5963 } else { | |
5964 addClass(editorContainer, 'wysiwygMode'); | |
5965 hide(sourceEditor); | |
5966 } | |
5967 | |
5968 if (!options.spellcheck) { | |
5969 attr(editorContainer, 'spellcheck', 'false'); | |
5970 } | |
5971 | |
5972 if (globalWin.location.protocol === 'https:') { | |
5973 attr(wysiwygEditor, 'src', 'about:blank'); | |
5974 } | |
5975 | |
5976 // Add the editor to the container | |
5977 appendChild(editorContainer, wysiwygEditor); | |
5978 appendChild(editorContainer, sourceEditor); | |
5979 | |
5980 // TODO: make this optional somehow | |
5981 base.dimensions( | |
5982 options.width || width(original), | |
5983 options.height || height(original) | |
5984 ); | |
5985 | |
5986 // Add ios to HTML so can apply CSS fix to only it | |
5987 var className = ios ? ' ios' : ''; | |
5988 | |
5989 wysiwygDocument = wysiwygEditor.contentDocument; | |
5990 wysiwygDocument.open(); | |
5991 wysiwygDocument.write(_tmpl('html', { | |
5992 attrs: ' class="' + className + '"', | |
5993 spellcheck: options.spellcheck ? '' : 'spellcheck="false"', | |
5994 charset: options.charset, | |
5995 style: options.style | |
5996 })); | |
5997 wysiwygDocument.close(); | |
5998 | |
5999 wysiwygBody = wysiwygDocument.body; | |
6000 wysiwygWindow = wysiwygEditor.contentWindow; | |
6001 | |
6002 base.readOnly(!!options.readOnly); | |
6003 | |
6004 // iframe overflow fix for iOS | |
6005 if (ios) { | |
6006 height(wysiwygBody, '100%'); | |
6007 on(wysiwygBody, 'touchend', base.focus); | |
6008 } | |
6009 | |
6010 var tabIndex = attr(original, 'tabindex'); | |
6011 attr(sourceEditor, 'tabindex', tabIndex); | |
6012 attr(wysiwygEditor, 'tabindex', tabIndex); | |
6013 | |
6014 rangeHelper = new RangeHelper(wysiwygWindow, null, sanitize); | |
6015 | |
6016 // load any textarea value into the editor | |
6017 hide(original); | |
6018 base.val(original.value); | |
6019 | |
6020 var placeholder = options.placeholder || | |
6021 attr(original, 'placeholder'); | |
6022 | |
6023 if (placeholder) { | |
6024 sourceEditor.placeholder = placeholder; | |
6025 attr(wysiwygBody, 'placeholder', placeholder); | |
6026 } | |
6027 }; | |
6028 | |
6029 /** | |
6030 * Initialises options | |
6031 * @private | |
6032 */ | |
6033 initOptions = function () { | |
6034 // auto-update original textbox on blur if option set to true | |
6035 if (options.autoUpdate) { | |
6036 on(wysiwygBody, 'blur', autoUpdate); | |
6037 on(sourceEditor, 'blur', autoUpdate); | |
6038 } | |
6039 | |
6040 if (options.rtl === null) { | |
6041 options.rtl = css(sourceEditor, 'direction') === 'rtl'; | |
6042 } | |
6043 | |
6044 base.rtl(!!options.rtl); | |
6045 | |
6046 if (options.autoExpand) { | |
6047 // Need to update when images (or anything else) loads | |
6048 on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE); | |
6049 on(wysiwygBody, 'input keyup', autoExpand); | |
6050 } | |
6051 | |
6052 if (options.resizeEnabled) { | |
6053 initResize(); | |
6054 } | |
6055 | |
6056 attr(editorContainer, 'id', options.id); | |
6057 base.emoticons(options.emoticonsEnabled); | |
6058 }; | |
6059 | |
6060 /** | |
6061 * Initialises events | |
6062 * @private | |
6063 */ | |
6064 initEvents = function () { | |
6065 var form = original.form; | |
6066 var compositionEvents = 'compositionstart compositionend'; | |
6067 var eventsToForward = | |
6068 'keydown keyup keypress focus blur contextmenu input'; | |
6069 var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ? | |
6070 'selectionchange' : | |
6071 'keyup focus blur contextmenu mouseup touchend click'; | |
6072 | |
6073 on(globalDoc, 'click', handleDocumentClick); | |
6074 | |
6075 if (form) { | |
6076 on(form, 'reset', handleFormReset); | |
6077 on(form, 'submit', base.updateOriginal, EVENT_CAPTURE); | |
6078 } | |
6079 | |
6080 on(window, 'pagehide', base.updateOriginal); | |
6081 on(window, 'pageshow', handleFormReset); | |
6082 on(wysiwygBody, 'keypress', handleKeyPress); | |
6083 on(wysiwygBody, 'keydown', handleKeyDown); | |
6084 on(wysiwygBody, 'keydown', handleBackSpace); | |
6085 on(wysiwygBody, 'keyup', appendNewLine); | |
6086 on(wysiwygBody, 'blur', valueChangedBlur); | |
6087 on(wysiwygBody, 'keyup', valueChangedKeyUp); | |
6088 on(wysiwygBody, 'paste', handlePasteEvt); | |
6089 on(wysiwygBody, 'cut copy', handleCutCopyEvt); | |
6090 on(wysiwygBody, compositionEvents, handleComposition); | |
6091 on(wysiwygBody, checkSelectionEvents, checkSelectionChanged); | |
6092 on(wysiwygBody, eventsToForward, handleEvent); | |
6093 | |
6094 if (options.emoticonsCompat && globalWin.getSelection) { | |
6095 on(wysiwygBody, 'keyup', emoticonsCheckWhitespace); | |
6096 } | |
6097 | |
6098 on(wysiwygBody, 'blur', function () { | |
6099 if (!base.val()) { | |
6100 addClass(wysiwygBody, 'placeholder'); | |
6101 } | |
6102 }); | |
6103 | |
6104 on(wysiwygBody, 'focus', function () { | |
6105 removeClass(wysiwygBody, 'placeholder'); | |
6106 }); | |
6107 | |
6108 on(sourceEditor, 'blur', valueChangedBlur); | |
6109 on(sourceEditor, 'keyup', valueChangedKeyUp); | |
6110 on(sourceEditor, 'keydown', handleKeyDown); | |
6111 on(sourceEditor, compositionEvents, handleComposition); | |
6112 on(sourceEditor, eventsToForward, handleEvent); | |
6113 | |
6114 on(wysiwygDocument, 'mousedown', handleMouseDown); | |
6115 on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged); | |
6116 on(wysiwygDocument, 'keyup', appendNewLine); | |
6117 | |
6118 on(editorContainer, 'selectionchanged', checkNodeChanged); | |
6119 on(editorContainer, 'selectionchanged', updateActiveButtons); | |
6120 // Custom events to forward | |
6121 on( | |
6122 editorContainer, | |
6123 'selectionchanged valuechanged nodechanged pasteraw paste', | |
6124 handleEvent | |
6125 ); | |
6126 }; | |
6127 | |
6128 /** | |
6129 * Creates the toolbar and appends it to the container | |
6130 * @private | |
6131 */ | |
6132 initToolBar = function () { | |
6133 var group, | |
6134 commands = base.commands, | |
6135 exclude = (options.toolbarExclude || '').split(','), | |
6136 groups = options.toolbar.split('|'); | |
6137 | |
6138 toolbar = createElement('div', { | |
6139 className: 'sceditor-toolbar', | |
6140 unselectable: 'on' | |
6141 }); | |
6142 | |
6143 if (options.icons in SCEditor.icons) { | |
6144 icons = new SCEditor.icons[options.icons](); | |
6145 } | |
6146 | |
6147 each(groups, function (_, menuItems) { | |
6148 group = createElement('div', { | |
6149 className: 'sceditor-group' | |
6150 }); | |
6151 | |
6152 each(menuItems.split(','), function (_, commandName) { | |
6153 var button, shortcut, | |
6154 command = commands[commandName]; | |
6155 | |
6156 // The commandName must be a valid command and not excluded | |
6157 if (!command || exclude.indexOf(commandName) > -1) { | |
6158 return; | |
6159 } | |
6160 | |
6161 shortcut = command.shortcut; | |
6162 button = _tmpl('toolbarButton', { | |
6163 name: commandName, | |
6164 dispName: base._(command.name || | |
6165 command.tooltip || commandName) | |
6166 }, true).firstChild; | |
6167 | |
6168 if (icons && icons.create) { | |
6169 var icon = icons.create(commandName); | |
6170 if (icon) { | |
6171 insertBefore(icons.create(commandName), | |
6172 button.firstChild); | |
6173 addClass(button, 'has-icon'); | |
6174 } | |
6175 } | |
6176 | |
6177 button._sceTxtMode = !!command.txtExec; | |
6178 button._sceWysiwygMode = !!command.exec; | |
6179 toggleClass(button, 'disabled', !command.exec); | |
6180 on(button, 'click', function (e) { | |
6181 if (!hasClass(button, 'disabled')) { | |
6182 handleCommand(button, command); | |
6183 } | |
6184 | |
6185 updateActiveButtons(); | |
6186 e.preventDefault(); | |
6187 }); | |
6188 // Prevent editor losing focus when button clicked | |
6189 on(button, 'mousedown', function (e) { | |
6190 base.closeDropDown(); | |
6191 e.preventDefault(); | |
6192 }); | |
6193 | |
6194 if (command.tooltip) { | |
6195 attr(button, 'title', | |
6196 base._(command.tooltip) + | |
6197 (shortcut ? ' (' + shortcut + ')' : '') | |
6198 ); | |
6199 } | |
6200 | |
6201 if (shortcut) { | |
6202 base.addShortcut(shortcut, commandName); | |
6203 } | |
6204 | |
6205 if (command.state) { | |
6206 btnStateHandlers.push({ | |
6207 name: commandName, | |
6208 state: command.state | |
6209 }); | |
6210 // exec string commands can be passed to queryCommandState | |
6211 } else if (isString(command.exec)) { | |
6212 btnStateHandlers.push({ | |
6213 name: commandName, | |
6214 state: command.exec | |
6215 }); | |
6216 } | |
6217 | |
6218 appendChild(group, button); | |
6219 toolbarButtons[commandName] = button; | |
6220 }); | |
6221 | |
6222 // Exclude empty groups | |
6223 if (group.firstChild) { | |
6224 appendChild(toolbar, group); | |
6225 } | |
6226 }); | |
6227 | |
6228 // Append the toolbar to the toolbarContainer option if given | |
6229 appendChild(options.toolbarContainer || editorContainer, toolbar); | |
6230 }; | |
6231 | |
6232 /** | |
6233 * Creates the resizer. | |
6234 * @private | |
6235 */ | |
6236 initResize = function () { | |
6237 var minHeight, maxHeight, minWidth, maxWidth, | |
6238 mouseMoveFunc, mouseUpFunc, | |
6239 grip = createElement('div', { | |
6240 className: 'sceditor-grip' | |
6241 }), | |
6242 // Cover is used to cover the editor iframe so document | |
6243 // still gets mouse move events | |
6244 cover = createElement('div', { | |
6245 className: 'sceditor-resize-cover' | |
6246 }), | |
6247 moveEvents = 'touchmove mousemove', | |
6248 endEvents = 'touchcancel touchend mouseup', | |
6249 startX = 0, | |
6250 startY = 0, | |
6251 newX = 0, | |
6252 newY = 0, | |
6253 startWidth = 0, | |
6254 startHeight = 0, | |
6255 origWidth = width(editorContainer), | |
6256 origHeight = height(editorContainer), | |
6257 isDragging = false, | |
6258 rtl = base.rtl(); | |
6259 | |
6260 minHeight = options.resizeMinHeight || origHeight / 1.5; | |
6261 maxHeight = options.resizeMaxHeight || origHeight * 2.5; | |
6262 minWidth = options.resizeMinWidth || origWidth / 1.25; | |
6263 maxWidth = options.resizeMaxWidth || origWidth * 1.25; | |
6264 | |
6265 mouseMoveFunc = function (e) { | |
6266 // iOS uses window.event | |
6267 if (e.type === 'touchmove') { | |
6268 e = globalWin.event; | |
6269 newX = e.changedTouches[0].pageX; | |
6270 newY = e.changedTouches[0].pageY; | |
6271 } else { | |
6272 newX = e.pageX; | |
6273 newY = e.pageY; | |
6274 } | |
6275 | |
6276 var newHeight = startHeight + (newY - startY), | |
6277 newWidth = rtl ? | |
6278 startWidth - (newX - startX) : | |
6279 startWidth + (newX - startX); | |
6280 | |
6281 if (maxWidth > 0 && newWidth > maxWidth) { | |
6282 newWidth = maxWidth; | |
6283 } | |
6284 if (minWidth > 0 && newWidth < minWidth) { | |
6285 newWidth = minWidth; | |
6286 } | |
6287 if (!options.resizeWidth) { | |
6288 newWidth = false; | |
6289 } | |
6290 | |
6291 if (maxHeight > 0 && newHeight > maxHeight) { | |
6292 newHeight = maxHeight; | |
6293 } | |
6294 if (minHeight > 0 && newHeight < minHeight) { | |
6295 newHeight = minHeight; | |
6296 } | |
6297 if (!options.resizeHeight) { | |
6298 newHeight = false; | |
6299 } | |
6300 | |
6301 if (newWidth || newHeight) { | |
6302 base.dimensions(newWidth, newHeight); | |
6303 } | |
6304 | |
6305 e.preventDefault(); | |
6306 }; | |
6307 | |
6308 mouseUpFunc = function (e) { | |
6309 if (!isDragging) { | |
6310 return; | |
6311 } | |
6312 | |
6313 isDragging = false; | |
6314 | |
6315 hide(cover); | |
6316 removeClass(editorContainer, 'resizing'); | |
6317 off(globalDoc, moveEvents, mouseMoveFunc); | |
6318 off(globalDoc, endEvents, mouseUpFunc); | |
6319 | |
6320 e.preventDefault(); | |
6321 }; | |
6322 | |
6323 if (icons && icons.create) { | |
6324 var icon = icons.create('grip'); | |
6325 if (icon) { | |
6326 appendChild(grip, icon); | |
6327 addClass(grip, 'has-icon'); | |
6328 } | |
6329 } | |
6330 | |
6331 appendChild(editorContainer, grip); | |
6332 appendChild(editorContainer, cover); | |
6333 hide(cover); | |
6334 | |
6335 on(grip, 'touchstart mousedown', function (e) { | |
6336 // iOS uses window.event | |
6337 if (e.type === 'touchstart') { | |
6338 e = globalWin.event; | |
6339 startX = e.touches[0].pageX; | |
6340 startY = e.touches[0].pageY; | |
6341 } else { | |
6342 startX = e.pageX; | |
6343 startY = e.pageY; | |
6344 } | |
6345 | |
6346 startWidth = width(editorContainer); | |
6347 startHeight = height(editorContainer); | |
6348 isDragging = true; | |
6349 | |
6350 addClass(editorContainer, 'resizing'); | |
6351 show(cover); | |
6352 on(globalDoc, moveEvents, mouseMoveFunc); | |
6353 on(globalDoc, endEvents, mouseUpFunc); | |
6354 | |
6355 e.preventDefault(); | |
6356 }); | |
6357 }; | |
6358 | |
6359 /** | |
6360 * Prefixes and preloads the emoticon images | |
6361 * @private | |
6362 */ | |
6363 initEmoticons = function () { | |
6364 var emoticons = options.emoticons; | |
6365 var root = options.emoticonsRoot || ''; | |
6366 | |
6367 if (emoticons) { | |
6368 allEmoticons = extend( | |
6369 {}, emoticons.more, emoticons.dropdown, emoticons.hidden | |
6370 ); | |
6371 } | |
6372 | |
6373 each(allEmoticons, function (key, url) { | |
6374 allEmoticons[key] = _tmpl('emoticon', { | |
6375 key: key, | |
6376 // Prefix emoticon root to emoticon urls | |
6377 url: root + (url.url || url), | |
6378 tooltip: url.tooltip || key | |
6379 }); | |
6380 | |
6381 // Preload the emoticon | |
6382 if (options.emoticonsEnabled) { | |
6383 preLoadCache.push(createElement('img', { | |
6384 src: root + (url.url || url) | |
6385 })); | |
6386 } | |
6387 }); | |
6388 }; | |
6389 | |
6390 /** | |
6391 * Autofocus the editor | |
6392 * @private | |
6393 */ | |
6394 autofocus = function (focusEnd) { | |
6395 var range, txtPos, | |
6396 node = wysiwygBody.firstChild; | |
6397 | |
6398 // Can't focus invisible elements | |
6399 if (!isVisible(editorContainer)) { | |
6400 return; | |
6401 } | |
6402 | |
6403 if (base.sourceMode()) { | |
6404 txtPos = focusEnd ? sourceEditor.value.length : 0; | |
6405 | |
6406 sourceEditor.setSelectionRange(txtPos, txtPos); | |
6407 | |
6408 return; | |
6409 } | |
6410 | |
6411 removeWhiteSpace(wysiwygBody); | |
6412 | |
6413 if (focusEnd) { | |
6414 if (!(node = wysiwygBody.lastChild)) { | |
6415 node = createElement('p', {}, wysiwygDocument); | |
6416 appendChild(wysiwygBody, node); | |
6417 } | |
6418 | |
6419 while (node.lastChild) { | |
6420 node = node.lastChild; | |
6421 | |
6422 // Should place the cursor before the last <br> | |
6423 if (is(node, 'br') && node.previousSibling) { | |
6424 node = node.previousSibling; | |
6425 } | |
6426 } | |
6427 } | |
6428 | |
6429 range = wysiwygDocument.createRange(); | |
6430 | |
6431 if (!canHaveChildren(node)) { | |
6432 range.setStartBefore(node); | |
6433 | |
6434 if (focusEnd) { | |
6435 range.setStartAfter(node); | |
6436 } | |
6437 } else { | |
6438 range.selectNodeContents(node); | |
6439 } | |
6440 | |
6441 range.collapse(!focusEnd); | |
6442 rangeHelper.selectRange(range); | |
6443 currentSelection = range; | |
6444 | |
6445 if (focusEnd) { | |
6446 wysiwygBody.scrollTop = wysiwygBody.scrollHeight; | |
6447 } | |
6448 | |
6449 base.focus(); | |
6450 }; | |
6451 | |
6452 /** | |
6453 * Gets if the editor is read only | |
6454 * | |
6455 * @since 1.3.5 | |
6456 * @function | |
6457 * @memberOf SCEditor.prototype | |
6458 * @name readOnly | |
6459 * @return {boolean} | |
6460 */ | |
6461 /** | |
6462 * Sets if the editor is read only | |
6463 * | |
6464 * @param {boolean} readOnly | |
6465 * @since 1.3.5 | |
6466 * @function | |
6467 * @memberOf SCEditor.prototype | |
6468 * @name readOnly^2 | |
6469 * @return {this} | |
6470 */ | |
6471 base.readOnly = function (readOnly) { | |
6472 if (typeof readOnly !== 'boolean') { | |
6473 return !sourceEditor.readonly; | |
6474 } | |
6475 | |
6476 wysiwygBody.contentEditable = !readOnly; | |
6477 sourceEditor.readonly = !readOnly; | |
6478 | |
6479 updateToolBar(readOnly); | |
6480 | |
6481 return base; | |
6482 }; | |
6483 | |
6484 /** | |
6485 * Gets if the editor is in RTL mode | |
6486 * | |
6487 * @since 1.4.1 | |
6488 * @function | |
6489 * @memberOf SCEditor.prototype | |
6490 * @name rtl | |
6491 * @return {boolean} | |
6492 */ | |
6493 /** | |
6494 * Sets if the editor is in RTL mode | |
6495 * | |
6496 * @param {boolean} rtl | |
6497 * @since 1.4.1 | |
6498 * @function | |
6499 * @memberOf SCEditor.prototype | |
6500 * @name rtl^2 | |
6501 * @return {this} | |
6502 */ | |
6503 base.rtl = function (rtl) { | |
6504 var dir = rtl ? 'rtl' : 'ltr'; | |
6505 | |
6506 if (typeof rtl !== 'boolean') { | |
6507 return attr(sourceEditor, 'dir') === 'rtl'; | |
6508 } | |
6509 | |
6510 attr(wysiwygBody, 'dir', dir); | |
6511 attr(sourceEditor, 'dir', dir); | |
6512 | |
6513 removeClass(editorContainer, 'rtl'); | |
6514 removeClass(editorContainer, 'ltr'); | |
6515 addClass(editorContainer, dir); | |
6516 | |
6517 if (icons && icons.rtl) { | |
6518 icons.rtl(rtl); | |
6519 } | |
6520 | |
6521 return base; | |
6522 }; | |
6523 | |
6524 /** | |
6525 * Updates the toolbar to disable/enable the appropriate buttons | |
6526 * @private | |
6527 */ | |
6528 updateToolBar = function (disable) { | |
6529 var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode'; | |
6530 | |
6531 each(toolbarButtons, function (_, button) { | |
6532 toggleClass(button, 'disabled', disable || !button[mode]); | |
6533 }); | |
6534 }; | |
6535 | |
6536 /** | |
6537 * Gets the width of the editor in pixels | |
6538 * | |
6539 * @since 1.3.5 | |
6540 * @function | |
6541 * @memberOf SCEditor.prototype | |
6542 * @name width | |
6543 * @return {number} | |
6544 */ | |
6545 /** | |
6546 * Sets the width of the editor | |
6547 * | |
6548 * @param {number} width Width in pixels | |
6549 * @since 1.3.5 | |
6550 * @function | |
6551 * @memberOf SCEditor.prototype | |
6552 * @name width^2 | |
6553 * @return {this} | |
6554 */ | |
6555 /** | |
6556 * Sets the width of the editor | |
6557 * | |
6558 * The saveWidth specifies if to save the width. The stored width can be | |
6559 * used for things like restoring from maximized state. | |
6560 * | |
6561 * @param {number} width Width in pixels | |
6562 * @param {boolean} [saveWidth=true] If to store the width | |
6563 * @since 1.4.1 | |
6564 * @function | |
6565 * @memberOf SCEditor.prototype | |
6566 * @name width^3 | |
6567 * @return {this} | |
6568 */ | |
6569 base.width = function (width$1, saveWidth) { | |
6570 if (!width$1 && width$1 !== 0) { | |
6571 return width(editorContainer); | |
6572 } | |
6573 | |
6574 base.dimensions(width$1, null, saveWidth); | |
6575 | |
6576 return base; | |
6577 }; | |
6578 | |
6579 /** | |
6580 * Returns an object with the properties width and height | |
6581 * which are the width and height of the editor in px. | |
6582 * | |
6583 * @since 1.4.1 | |
6584 * @function | |
6585 * @memberOf SCEditor.prototype | |
6586 * @name dimensions | |
6587 * @return {object} | |
6588 */ | |
6589 /** | |
6590 * <p>Sets the width and/or height of the editor.</p> | |
6591 * | |
6592 * <p>If width or height is not numeric it is ignored.</p> | |
6593 * | |
6594 * @param {number} width Width in px | |
6595 * @param {number} height Height in px | |
6596 * @since 1.4.1 | |
6597 * @function | |
6598 * @memberOf SCEditor.prototype | |
6599 * @name dimensions^2 | |
6600 * @return {this} | |
6601 */ | |
6602 /** | |
6603 * <p>Sets the width and/or height of the editor.</p> | |
6604 * | |
6605 * <p>If width or height is not numeric it is ignored.</p> | |
6606 * | |
6607 * <p>The save argument specifies if to save the new sizes. | |
6608 * The saved sizes can be used for things like restoring from | |
6609 * maximized state. This should normally be left as true.</p> | |
6610 * | |
6611 * @param {number} width Width in px | |
6612 * @param {number} height Height in px | |
6613 * @param {boolean} [save=true] If to store the new sizes | |
6614 * @since 1.4.1 | |
6615 * @function | |
6616 * @memberOf SCEditor.prototype | |
6617 * @name dimensions^3 | |
6618 * @return {this} | |
6619 */ | |
6620 base.dimensions = function (width$1, height$1, save) { | |
6621 // set undefined width/height to boolean false | |
6622 width$1 = (!width$1 && width$1 !== 0) ? false : width$1; | |
6623 height$1 = (!height$1 && height$1 !== 0) ? false : height$1; | |
6624 | |
6625 if (width$1 === false && height$1 === false) { | |
6626 return { width: base.width(), height: base.height() }; | |
6627 } | |
6628 | |
6629 if (width$1 !== false) { | |
6630 if (save !== false) { | |
6631 options.width = width$1; | |
6632 } | |
6633 | |
6634 width(editorContainer, width$1); | |
6635 } | |
6636 | |
6637 if (height$1 !== false) { | |
6638 if (save !== false) { | |
6639 options.height = height$1; | |
6640 } | |
6641 | |
6642 height(editorContainer, height$1); | |
6643 } | |
6644 | |
6645 return base; | |
6646 }; | |
6647 | |
6648 /** | |
6649 * Gets the height of the editor in px | |
6650 * | |
6651 * @since 1.3.5 | |
6652 * @function | |
6653 * @memberOf SCEditor.prototype | |
6654 * @name height | |
6655 * @return {number} | |
6656 */ | |
6657 /** | |
6658 * Sets the height of the editor | |
6659 * | |
6660 * @param {number} height Height in px | |
6661 * @since 1.3.5 | |
6662 * @function | |
6663 * @memberOf SCEditor.prototype | |
6664 * @name height^2 | |
6665 * @return {this} | |
6666 */ | |
6667 /** | |
6668 * Sets the height of the editor | |
6669 * | |
6670 * The saveHeight specifies if to save the height. | |
6671 * | |
6672 * The stored height can be used for things like | |
6673 * restoring from maximized state. | |
6674 * | |
6675 * @param {number} height Height in px | |
6676 * @param {boolean} [saveHeight=true] If to store the height | |
6677 * @since 1.4.1 | |
6678 * @function | |
6679 * @memberOf SCEditor.prototype | |
6680 * @name height^3 | |
6681 * @return {this} | |
6682 */ | |
6683 base.height = function (height$1, saveHeight) { | |
6684 if (!height$1 && height$1 !== 0) { | |
6685 return height(editorContainer); | |
6686 } | |
6687 | |
6688 base.dimensions(null, height$1, saveHeight); | |
6689 | |
6690 return base; | |
6691 }; | |
6692 | |
6693 /** | |
6694 * Gets if the editor is maximised or not | |
6695 * | |
6696 * @since 1.4.1 | |
6697 * @function | |
6698 * @memberOf SCEditor.prototype | |
6699 * @name maximize | |
6700 * @return {boolean} | |
6701 */ | |
6702 /** | |
6703 * Sets if the editor is maximised or not | |
6704 * | |
6705 * @param {boolean} maximize If to maximise the editor | |
6706 * @since 1.4.1 | |
6707 * @function | |
6708 * @memberOf SCEditor.prototype | |
6709 * @name maximize^2 | |
6710 * @return {this} | |
6711 */ | |
6712 base.maximize = function (maximize) { | |
6713 var maximizeSize = 'sceditor-maximize'; | |
6714 | |
6715 if (isUndefined(maximize)) { | |
6716 return hasClass(editorContainer, maximizeSize); | |
6717 } | |
6718 | |
6719 maximize = !!maximize; | |
6720 | |
6721 if (maximize) { | |
6722 maximizeScrollPosition = globalWin.pageYOffset; | |
6723 } | |
6724 | |
6725 toggleClass(globalDoc.documentElement, maximizeSize, maximize); | |
6726 toggleClass(globalDoc.body, maximizeSize, maximize); | |
6727 toggleClass(editorContainer, maximizeSize, maximize); | |
6728 base.width(maximize ? '100%' : options.width, false); | |
6729 base.height(maximize ? '100%' : options.height, false); | |
6730 | |
6731 if (!maximize) { | |
6732 globalWin.scrollTo(0, maximizeScrollPosition); | |
6733 } | |
6734 | |
6735 autoExpand(); | |
6736 | |
6737 return base; | |
6738 }; | |
6739 | |
6740 autoExpand = function () { | |
6741 if (options.autoExpand && !autoExpandThrottle) { | |
6742 autoExpandThrottle = setTimeout(base.expandToContent, 200); | |
6743 } | |
6744 }; | |
6745 | |
6746 /** | |
6747 * Expands or shrinks the editors height to the height of it's content | |
6748 * | |
6749 * Unless ignoreMaxHeight is set to true it will not expand | |
6750 * higher than the maxHeight option. | |
6751 * | |
6752 * @since 1.3.5 | |
6753 * @param {boolean} [ignoreMaxHeight=false] | |
6754 * @function | |
6755 * @name expandToContent | |
6756 * @memberOf SCEditor.prototype | |
6757 * @see #resizeToContent | |
6758 */ | |
6759 base.expandToContent = function (ignoreMaxHeight) { | |
6760 if (base.maximize()) { | |
6761 return; | |
6762 } | |
6763 | |
6764 clearTimeout(autoExpandThrottle); | |
6765 autoExpandThrottle = false; | |
6766 | |
6767 if (!autoExpandBounds) { | |
6768 var height$1 = options.resizeMinHeight || options.height || | |
6769 height(original); | |
6770 | |
6771 autoExpandBounds = { | |
6772 min: height$1, | |
6773 max: options.resizeMaxHeight || (height$1 * 2) | |
6774 }; | |
6775 } | |
6776 | |
6777 var range = globalDoc.createRange(); | |
6778 range.selectNodeContents(wysiwygBody); | |
6779 | |
6780 var rect = range.getBoundingClientRect(); | |
6781 var current = wysiwygDocument.documentElement.clientHeight - 1; | |
6782 var spaceNeeded = rect.bottom - rect.top; | |
6783 var newHeight = base.height() + 1 + (spaceNeeded - current); | |
6784 | |
6785 if (!ignoreMaxHeight && autoExpandBounds.max !== -1) { | |
6786 newHeight = Math.min(newHeight, autoExpandBounds.max); | |
6787 } | |
6788 | |
6789 base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min))); | |
6790 }; | |
6791 | |
6792 /** | |
6793 * Destroys the editor, removing all elements and | |
6794 * event handlers. | |
6795 * | |
6796 * Leaves only the original textarea. | |
6797 * | |
6798 * @function | |
6799 * @name destroy | |
6800 * @memberOf SCEditor.prototype | |
6801 */ | |
6802 base.destroy = function () { | |
6803 // Don't destroy if the editor has already been destroyed | |
6804 if (!pluginManager) { | |
6805 return; | |
6806 } | |
6807 | |
6808 pluginManager.destroy(); | |
6809 | |
6810 rangeHelper = null; | |
6811 pluginManager = null; | |
6812 | |
6813 if (dropdown) { | |
6814 remove(dropdown); | |
6815 } | |
6816 | |
6817 off(globalDoc, 'click', handleDocumentClick); | |
6818 | |
6819 var form = original.form; | |
6820 if (form) { | |
6821 off(form, 'reset', handleFormReset); | |
6822 off(form, 'submit', base.updateOriginal, EVENT_CAPTURE); | |
6823 } | |
6824 | |
6825 off(window, 'pagehide', base.updateOriginal); | |
6826 off(window, 'pageshow', handleFormReset); | |
6827 remove(sourceEditor); | |
6828 remove(toolbar); | |
6829 remove(editorContainer); | |
6830 | |
6831 delete original._sceditor; | |
6832 show(original); | |
6833 | |
6834 original.required = isRequired; | |
6835 }; | |
6836 | |
6837 | |
6838 /** | |
6839 * Creates a menu item drop down | |
6840 * | |
6841 * @param {HTMLElement} menuItem The button to align the dropdown with | |
6842 * @param {string} name Used for styling the dropdown, will be | |
6843 * a class sceditor-name | |
6844 * @param {HTMLElement} content The HTML content of the dropdown | |
6845 * @function | |
6846 * @name createDropDown | |
6847 * @memberOf SCEditor.prototype | |
6848 */ | |
6849 base.createDropDown = function (menuItem, name, content) { | |
6850 // first click for create second click for close | |
6851 var dropDownCss, | |
6852 dropDownClass = 'sceditor-' + name; | |
6853 | |
6854 base.closeDropDown(); | |
6855 | |
6856 // Only close the dropdown if it was already open | |
6857 if (dropdown && hasClass(dropdown, dropDownClass)) { | |
6858 return; | |
6859 } | |
6860 | |
6861 dropDownCss = extend({ | |
6862 top: menuItem.offsetTop, | |
6863 left: menuItem.offsetLeft, | |
6864 marginTop: menuItem.clientHeight | |
6865 }, options.dropDownCss); | |
6866 | |
6867 dropdown = createElement('div', { | |
6868 className: 'sceditor-dropdown ' + dropDownClass | |
6869 }); | |
6870 | |
6871 css(dropdown, dropDownCss); | |
6872 appendChild(dropdown, content); | |
6873 appendChild(editorContainer, dropdown); | |
6874 on(dropdown, 'click focusin', function (e) { | |
6875 // stop clicks within the dropdown from being handled | |
6876 e.stopPropagation(); | |
6877 }); | |
6878 | |
6879 if (dropdown) { | |
6880 var first = find(dropdown, 'input,textarea')[0]; | |
6881 if (first) { | |
6882 first.focus(); | |
6883 } | |
6884 } | |
6885 }; | |
6886 | |
6887 /** | |
6888 * Handles any document click and closes the dropdown if open | |
6889 * @private | |
6890 */ | |
6891 handleDocumentClick = function (e) { | |
6892 // ignore right clicks | |
6893 if (e.which !== 3 && dropdown && !e.defaultPrevented) { | |
6894 autoUpdate(); | |
6895 | |
6896 base.closeDropDown(); | |
6897 } | |
6898 }; | |
6899 | |
6900 /** | |
6901 * Handles the WYSIWYG editors cut & copy events | |
6902 * | |
6903 * By default browsers also copy inherited styling from the stylesheet and | |
6904 * browser default styling which is unnecessary. | |
6905 * | |
6906 * This will ignore inherited styles and only copy inline styling. | |
6907 * @private | |
6908 */ | |
6909 handleCutCopyEvt = function (e) { | |
6910 var range = rangeHelper.selectedRange(); | |
6911 if (range) { | |
6912 var container = createElement('div', {}, wysiwygDocument); | |
6913 var firstParent; | |
6914 | |
6915 // Copy all inline parent nodes up to the first block parent so can | |
6916 // copy inline styles | |
6917 var parent = range.commonAncestorContainer; | |
6918 while (parent && isInline(parent, true)) { | |
6919 if (parent.nodeType === ELEMENT_NODE) { | |
6920 var clone = parent.cloneNode(); | |
6921 if (container.firstChild) { | |
6922 appendChild(clone, container.firstChild); | |
6923 } | |
6924 | |
6925 appendChild(container, clone); | |
6926 firstParent = firstParent || clone; | |
6927 } | |
6928 parent = parent.parentNode; | |
6929 } | |
6930 | |
6931 appendChild(firstParent || container, range.cloneContents()); | |
6932 removeWhiteSpace(container); | |
6933 | |
6934 e.clipboardData.setData('text/html', container.innerHTML); | |
6935 | |
6936 // TODO: Refactor into private shared module with plaintext plugin | |
6937 // innerText adds two newlines after <p> tags so convert them to | |
6938 // <div> tags | |
6939 each(find(container, 'p'), function (_, elm) { | |
6940 convertElement(elm, 'div'); | |
6941 }); | |
6942 // Remove collapsed <br> tags as innerText converts them to newlines | |
6943 each(find(container, 'br'), function (_, elm) { | |
6944 if (!elm.nextSibling || !isInline(elm.nextSibling, true)) { | |
6945 remove(elm); | |
6946 } | |
6947 }); | |
6948 | |
6949 // range.toString() doesn't include newlines so can't use that. | |
6950 // selection.toString() seems to use the same method as innerText | |
6951 // but needs to be normalised first so using container.innerText | |
6952 appendChild(wysiwygBody, container); | |
6953 e.clipboardData.setData('text/plain', container.innerText); | |
6954 remove(container); | |
6955 | |
6956 if (e.type === 'cut') { | |
6957 range.deleteContents(); | |
6958 } | |
6959 | |
6960 e.preventDefault(); | |
6961 } | |
6962 }; | |
6963 | |
6964 /** | |
6965 * Handles the WYSIWYG editors paste event | |
6966 * @private | |
6967 */ | |
6968 handlePasteEvt = function (e) { | |
6969 var editable = wysiwygBody; | |
6970 var clipboard = e.clipboardData; | |
6971 var loadImage = function (file) { | |
6972 var reader = new FileReader(); | |
6973 reader.onload = function (e) { | |
6974 handlePasteData({ | |
6975 html: '<img src="' + e.target.result + '" />' | |
6976 }); | |
6977 }; | |
6978 reader.readAsDataURL(file); | |
6979 }; | |
6980 | |
6981 // Modern browsers with clipboard API - everything other than _very_ | |
6982 // old android web views and UC browser which doesn't support the | |
6983 // paste event at all. | |
6984 if (clipboard) { | |
6985 var data = {}; | |
6986 var types = clipboard.types; | |
6987 var items = clipboard.items; | |
6988 | |
6989 e.preventDefault(); | |
6990 | |
6991 for (var i = 0; i < types.length; i++) { | |
6992 // Word sometimes adds copied text as an image so if HTML | |
6993 // exists prefer that over images | |
6994 if (types.indexOf('text/html') < 0) { | |
6995 // Normalise image pasting to paste as a data-uri | |
6996 if (globalWin.FileReader && items && | |
6997 IMAGE_MIME_REGEX.test(items[i].type)) { | |
6998 return loadImage(clipboard.items[i].getAsFile()); | |
6999 } | |
7000 } | |
7001 | |
7002 data[types[i]] = clipboard.getData(types[i]); | |
7003 } | |
7004 // Call plugins here with file? | |
7005 data.text = data['text/plain']; | |
7006 data.html = sanitize(data['text/html']); | |
7007 | |
7008 handlePasteData(data); | |
7009 // If contentsFragment exists then we are already waiting for a | |
7010 // previous paste so let the handler for that handle this one too | |
7011 } else if (!pasteContentFragment) { | |
7012 // Save the scroll position so can be restored | |
7013 // when contents is restored | |
7014 var scrollTop = editable.scrollTop; | |
7015 | |
7016 rangeHelper.saveRange(); | |
7017 | |
7018 pasteContentFragment = globalDoc.createDocumentFragment(); | |
7019 while (editable.firstChild) { | |
7020 appendChild(pasteContentFragment, editable.firstChild); | |
7021 } | |
7022 | |
7023 setTimeout(function () { | |
7024 var html = editable.innerHTML; | |
7025 | |
7026 editable.innerHTML = ''; | |
7027 appendChild(editable, pasteContentFragment); | |
7028 editable.scrollTop = scrollTop; | |
7029 pasteContentFragment = false; | |
7030 | |
7031 rangeHelper.restoreRange(); | |
7032 | |
7033 handlePasteData({ html: sanitize(html) }); | |
7034 }, 0); | |
7035 } | |
7036 }; | |
7037 | |
7038 /** | |
7039 * Gets the pasted data, filters it and then inserts it. | |
7040 * @param {Object} data | |
7041 * @private | |
7042 */ | |
7043 handlePasteData = function (data) { | |
7044 var pasteArea = createElement('div', {}, wysiwygDocument); | |
7045 | |
7046 pluginManager.call('pasteRaw', data); | |
7047 trigger(editorContainer, 'pasteraw', data); | |
7048 | |
7049 if (data.html) { | |
7050 // Sanitize again in case plugins modified the HTML | |
7051 pasteArea.innerHTML = sanitize(data.html); | |
7052 | |
7053 // fix any invalid nesting | |
7054 fixNesting(pasteArea); | |
7055 } else { | |
7056 pasteArea.innerHTML = entities(data.text || ''); | |
7057 } | |
7058 | |
7059 var paste = { | |
7060 val: pasteArea.innerHTML | |
7061 }; | |
7062 | |
7063 if ('fragmentToSource' in format) { | |
7064 paste.val = format | |
7065 .fragmentToSource(paste.val, wysiwygDocument, currentNode); | |
7066 } | |
7067 | |
7068 pluginManager.call('paste', paste); | |
7069 trigger(editorContainer, 'paste', paste); | |
7070 | |
7071 if ('fragmentToHtml' in format) { | |
7072 paste.val = format | |
7073 .fragmentToHtml(paste.val, currentNode); | |
7074 } | |
7075 | |
7076 pluginManager.call('pasteHtml', paste); | |
7077 | |
7078 var parent = rangeHelper.getFirstBlockParent(); | |
7079 base.wysiwygEditorInsertHtml(paste.val, null, true); | |
7080 merge(parent); | |
7081 }; | |
7082 | |
7083 /** | |
7084 * Closes any currently open drop down | |
7085 * | |
7086 * @param {boolean} [focus=false] If to focus the editor | |
7087 * after closing the drop down | |
7088 * @function | |
7089 * @name closeDropDown | |
7090 * @memberOf SCEditor.prototype | |
7091 */ | |
7092 base.closeDropDown = function (focus) { | |
7093 if (dropdown) { | |
7094 remove(dropdown); | |
7095 dropdown = null; | |
7096 } | |
7097 | |
7098 if (focus === true) { | |
7099 base.focus(); | |
7100 } | |
7101 }; | |
7102 | |
7103 | |
7104 /** | |
7105 * Inserts HTML into WYSIWYG editor. | |
7106 * | |
7107 * If endHtml is specified, any selected text will be placed | |
7108 * between html and endHtml. If there is no selected text html | |
7109 * and endHtml will just be concatenate together. | |
7110 * | |
7111 * @param {string} html | |
7112 * @param {string} [endHtml=null] | |
7113 * @param {boolean} [overrideCodeBlocking=false] If to insert the html | |
7114 * into code tags, by | |
7115 * default code tags only | |
7116 * support text. | |
7117 * @function | |
7118 * @name wysiwygEditorInsertHtml | |
7119 * @memberOf SCEditor.prototype | |
7120 */ | |
7121 base.wysiwygEditorInsertHtml = function ( | |
7122 html, endHtml, overrideCodeBlocking | |
7123 ) { | |
7124 var marker, scrollTop, scrollTo, | |
7125 editorHeight = height(wysiwygEditor); | |
7126 | |
7127 base.focus(); | |
7128 | |
7129 // TODO: This code tag should be configurable and | |
7130 // should maybe convert the HTML into text instead | |
7131 // Don't apply to code elements | |
7132 if (!overrideCodeBlocking && closest(currentBlockNode, 'code')) { | |
7133 return; | |
7134 } | |
7135 | |
7136 // Insert the HTML and save the range so the editor can be scrolled | |
7137 // to the end of the selection. Also allows emoticons to be replaced | |
7138 // without affecting the cursor position | |
7139 rangeHelper.insertHTML(html, endHtml); | |
7140 rangeHelper.saveRange(); | |
7141 replaceEmoticons(); | |
7142 | |
7143 // Fix any invalid nesting, e.g. if a quote or other block is inserted | |
7144 // into a paragraph | |
7145 fixNesting(wysiwygBody); | |
7146 | |
7147 // Scroll the editor after the end of the selection | |
7148 marker = find(wysiwygBody, '#sceditor-end-marker')[0]; | |
7149 show(marker); | |
7150 scrollTop = wysiwygBody.scrollTop; | |
7151 scrollTo = (getOffset(marker).top + | |
7152 (marker.offsetHeight * 1.5)) - editorHeight; | |
7153 hide(marker); | |
7154 | |
7155 // Only scroll if marker isn't already visible | |
7156 if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) { | |
7157 wysiwygBody.scrollTop = scrollTo; | |
7158 } | |
7159 | |
7160 triggerValueChanged(false); | |
7161 rangeHelper.restoreRange(); | |
7162 | |
7163 // Add a new line after the last block element | |
7164 // so can always add text after it | |
7165 appendNewLine(); | |
7166 }; | |
7167 | |
7168 /** | |
7169 * Like wysiwygEditorInsertHtml except it will convert any HTML | |
7170 * into text before inserting it. | |
7171 * | |
7172 * @param {string} text | |
7173 * @param {string} [endText=null] | |
7174 * @function | |
7175 * @name wysiwygEditorInsertText | |
7176 * @memberOf SCEditor.prototype | |
7177 */ | |
7178 base.wysiwygEditorInsertText = function (text, endText) { | |
7179 base.wysiwygEditorInsertHtml( | |
7180 entities(text), entities(endText) | |
7181 ); | |
7182 }; | |
7183 | |
7184 /** | |
7185 * Inserts text into the WYSIWYG or source editor depending on which | |
7186 * mode the editor is in. | |
7187 * | |
7188 * If endText is specified any selected text will be placed between | |
7189 * text and endText. If no text is selected text and endText will | |
7190 * just be concatenate together. | |
7191 * | |
7192 * @param {string} text | |
7193 * @param {string} [endText=null] | |
7194 * @since 1.3.5 | |
7195 * @function | |
7196 * @name insertText | |
7197 * @memberOf SCEditor.prototype | |
7198 */ | |
7199 base.insertText = function (text, endText) { | |
7200 if (base.inSourceMode()) { | |
7201 base.sourceEditorInsertText(text, endText); | |
7202 } else { | |
7203 base.wysiwygEditorInsertText(text, endText); | |
7204 } | |
7205 | |
7206 return base; | |
7207 }; | |
7208 | |
7209 /** | |
7210 * Like wysiwygEditorInsertHtml but inserts text into the | |
7211 * source mode editor instead. | |
7212 * | |
7213 * If endText is specified any selected text will be placed between | |
7214 * text and endText. If no text is selected text and endText will | |
7215 * just be concatenate together. | |
7216 * | |
7217 * The cursor will be placed after the text param. If endText is | |
7218 * specified the cursor will be placed before endText, so passing:<br /> | |
7219 * | |
7220 * '[b]', '[/b]' | |
7221 * | |
7222 * Would cause the cursor to be placed:<br /> | |
7223 * | |
7224 * [b]Selected text|[/b] | |
7225 * | |
7226 * @param {string} text | |
7227 * @param {string} [endText=null] | |
7228 * @since 1.4.0 | |
7229 * @function | |
7230 * @name sourceEditorInsertText | |
7231 * @memberOf SCEditor.prototype | |
7232 */ | |
7233 base.sourceEditorInsertText = function (text, endText) { | |
7234 var scrollTop, currentValue, | |
7235 startPos = sourceEditor.selectionStart, | |
7236 endPos = sourceEditor.selectionEnd; | |
7237 | |
7238 scrollTop = sourceEditor.scrollTop; | |
7239 sourceEditor.focus(); | |
7240 currentValue = sourceEditor.value; | |
7241 | |
7242 if (endText) { | |
7243 text += currentValue.substring(startPos, endPos) + endText; | |
7244 } | |
7245 | |
7246 sourceEditor.value = currentValue.substring(0, startPos) + | |
7247 text + | |
7248 currentValue.substring(endPos, currentValue.length); | |
7249 | |
7250 sourceEditor.selectionStart = (startPos + text.length) - | |
7251 (endText ? endText.length : 0); | |
7252 sourceEditor.selectionEnd = sourceEditor.selectionStart; | |
7253 | |
7254 sourceEditor.scrollTop = scrollTop; | |
7255 sourceEditor.focus(); | |
7256 | |
7257 triggerValueChanged(); | |
7258 }; | |
7259 | |
7260 /** | |
7261 * Gets the current instance of the rangeHelper class | |
7262 * for the editor. | |
7263 * | |
7264 * @return {RangeHelper} | |
7265 * @function | |
7266 * @name getRangeHelper | |
7267 * @memberOf SCEditor.prototype | |
7268 */ | |
7269 base.getRangeHelper = function () { | |
7270 return rangeHelper; | |
7271 }; | |
7272 | |
7273 /** | |
7274 * Gets or sets the source editor caret position. | |
7275 * | |
7276 * @param {Object} [position] | |
7277 * @return {this} | |
7278 * @function | |
7279 * @since 1.4.5 | |
7280 * @name sourceEditorCaret | |
7281 * @memberOf SCEditor.prototype | |
7282 */ | |
7283 base.sourceEditorCaret = function (position) { | |
7284 sourceEditor.focus(); | |
7285 | |
7286 if (position) { | |
7287 sourceEditor.selectionStart = position.start; | |
7288 sourceEditor.selectionEnd = position.end; | |
7289 | |
7290 return this; | |
7291 } | |
7292 | |
7293 return { | |
7294 start: sourceEditor.selectionStart, | |
7295 end: sourceEditor.selectionEnd | |
7296 }; | |
7297 }; | |
7298 | |
7299 /** | |
7300 * Gets the value of the editor. | |
7301 * | |
7302 * If the editor is in WYSIWYG mode it will return the filtered | |
7303 * HTML from it (converted to BBCode if using the BBCode plugin). | |
7304 * It it's in Source Mode it will return the unfiltered contents | |
7305 * of the source editor (if using the BBCode plugin this will be | |
7306 * BBCode again). | |
7307 * | |
7308 * @since 1.3.5 | |
7309 * @return {string} | |
7310 * @function | |
7311 * @name val | |
7312 * @memberOf SCEditor.prototype | |
7313 */ | |
7314 /** | |
7315 * Sets the value of the editor. | |
7316 * | |
7317 * If filter set true the val will be passed through the filter | |
7318 * function. If using the BBCode plugin it will pass the val to | |
7319 * the BBCode filter to convert any BBCode into HTML. | |
7320 * | |
7321 * @param {string} val | |
7322 * @param {boolean} [filter=true] | |
7323 * @return {this} | |
7324 * @since 1.3.5 | |
7325 * @function | |
7326 * @name val^2 | |
7327 * @memberOf SCEditor.prototype | |
7328 */ | |
7329 base.val = function (val, filter) { | |
7330 if (!isString(val)) { | |
7331 return base.inSourceMode() ? | |
7332 base.getSourceEditorValue(false) : | |
7333 base.getWysiwygEditorValue(filter); | |
7334 } | |
7335 | |
7336 if (!base.inSourceMode()) { | |
7337 if (filter !== false && 'toHtml' in format) { | |
7338 val = format.toHtml(val); | |
7339 } | |
7340 | |
7341 base.setWysiwygEditorValue(val); | |
7342 } else { | |
7343 base.setSourceEditorValue(val); | |
7344 } | |
7345 | |
7346 return base; | |
7347 }; | |
7348 | |
7349 /** | |
7350 * Inserts HTML/BBCode into the editor | |
7351 * | |
7352 * If end is supplied any selected text will be placed between | |
7353 * start and end. If there is no selected text start and end | |
7354 * will be concatenate together. | |
7355 * | |
7356 * If the filter param is set to true, the HTML/BBCode will be | |
7357 * passed through any plugin filters. If using the BBCode plugin | |
7358 * this will convert any BBCode into HTML. | |
7359 * | |
7360 * @param {string} start | |
7361 * @param {string} [end=null] | |
7362 * @param {boolean} [filter=true] | |
7363 * @param {boolean} [convertEmoticons=true] If to convert emoticons | |
7364 * @return {this} | |
7365 * @since 1.3.5 | |
7366 * @function | |
7367 * @name insert | |
7368 * @memberOf SCEditor.prototype | |
7369 */ | |
7370 /** | |
7371 * Inserts HTML/BBCode into the editor | |
7372 * | |
7373 * If end is supplied any selected text will be placed between | |
7374 * start and end. If there is no selected text start and end | |
7375 * will be concatenate together. | |
7376 * | |
7377 * If the filter param is set to true, the HTML/BBCode will be | |
7378 * passed through any plugin filters. If using the BBCode plugin | |
7379 * this will convert any BBCode into HTML. | |
7380 * | |
7381 * If the allowMixed param is set to true, HTML any will not be | |
7382 * escaped | |
7383 * | |
7384 * @param {string} start | |
7385 * @param {string} [end=null] | |
7386 * @param {boolean} [filter=true] | |
7387 * @param {boolean} [convertEmoticons=true] If to convert emoticons | |
7388 * @param {boolean} [allowMixed=false] | |
7389 * @return {this} | |
7390 * @since 1.4.3 | |
7391 * @function | |
7392 * @name insert^2 | |
7393 * @memberOf SCEditor.prototype | |
7394 */ | |
7395 // eslint-disable-next-line max-params | |
7396 base.insert = function ( | |
7397 start, end, filter, convertEmoticons, allowMixed | |
7398 ) { | |
7399 if (base.inSourceMode()) { | |
7400 base.sourceEditorInsertText(start, end); | |
7401 return base; | |
7402 } | |
7403 | |
7404 // Add the selection between start and end | |
7405 if (end) { | |
7406 var html = rangeHelper.selectedHtml(); | |
7407 | |
7408 if (filter !== false && 'fragmentToSource' in format) { | |
7409 html = format | |
7410 .fragmentToSource(html, wysiwygDocument, currentNode); | |
7411 } | |
7412 | |
7413 start += html + end; | |
7414 } | |
7415 // TODO: This filter should allow empty tags as it's inserting. | |
7416 if (filter !== false && 'fragmentToHtml' in format) { | |
7417 start = format.fragmentToHtml(start, currentNode); | |
7418 } | |
7419 | |
7420 // Convert any escaped HTML back into HTML if mixed is allowed | |
7421 if (filter !== false && allowMixed === true) { | |
7422 start = start.replace(/</g, '<') | |
7423 .replace(/>/g, '>') | |
7424 .replace(/&/g, '&'); | |
7425 } | |
7426 | |
7427 base.wysiwygEditorInsertHtml(start); | |
7428 | |
7429 return base; | |
7430 }; | |
7431 | |
7432 /** | |
7433 * Gets the WYSIWYG editors HTML value. | |
7434 * | |
7435 * If using a plugin that filters the Ht Ml like the BBCode plugin | |
7436 * it will return the result of the filtering (BBCode) unless the | |
7437 * filter param is set to false. | |
7438 * | |
7439 * @param {boolean} [filter=true] | |
7440 * @return {string} | |
7441 * @function | |
7442 * @name getWysiwygEditorValue | |
7443 * @memberOf SCEditor.prototype | |
7444 */ | |
7445 base.getWysiwygEditorValue = function (filter) { | |
7446 var html; | |
7447 // Create a tmp node to store contents so it can be modified | |
7448 // without affecting anything else. | |
7449 var tmp = createElement('div', {}, wysiwygDocument); | |
7450 var childNodes = wysiwygBody.childNodes; | |
7451 | |
7452 for (var i = 0; i < childNodes.length; i++) { | |
7453 appendChild(tmp, childNodes[i].cloneNode(true)); | |
7454 } | |
7455 | |
7456 appendChild(wysiwygBody, tmp); | |
7457 fixNesting(tmp); | |
7458 remove(tmp); | |
7459 | |
7460 html = tmp.innerHTML; | |
7461 | |
7462 // filter the HTML and DOM through any plugins | |
7463 if (filter !== false && format.hasOwnProperty('toSource')) { | |
7464 html = format.toSource(html, wysiwygDocument); | |
7465 } | |
7466 | |
7467 return html; | |
7468 }; | |
7469 | |
7470 /** | |
7471 * Gets the WYSIWYG editor's iFrame Body. | |
7472 * | |
7473 * @return {HTMLElement} | |
7474 * @function | |
7475 * @since 1.4.3 | |
7476 * @name getBody | |
7477 * @memberOf SCEditor.prototype | |
7478 */ | |
7479 base.getBody = function () { | |
7480 return wysiwygBody; | |
7481 }; | |
7482 | |
7483 /** | |
7484 * Gets the WYSIWYG editors container area (whole iFrame). | |
7485 * | |
7486 * @return {HTMLElement} | |
7487 * @function | |
7488 * @since 1.4.3 | |
7489 * @name getContentAreaContainer | |
7490 * @memberOf SCEditor.prototype | |
7491 */ | |
7492 base.getContentAreaContainer = function () { | |
7493 return wysiwygEditor; | |
7494 }; | |
7495 | |
7496 /** | |
7497 * Gets the text editor value | |
7498 * | |
7499 * If using a plugin that filters the text like the BBCode plugin | |
7500 * it will return the result of the filtering which is BBCode to | |
7501 * HTML so it will return HTML. If filter is set to false it will | |
7502 * just return the contents of the source editor (BBCode). | |
7503 * | |
7504 * @param {boolean} [filter=true] | |
7505 * @return {string} | |
7506 * @function | |
7507 * @since 1.4.0 | |
7508 * @name getSourceEditorValue | |
7509 * @memberOf SCEditor.prototype | |
7510 */ | |
7511 base.getSourceEditorValue = function (filter) { | |
7512 var val = sourceEditor.value; | |
7513 | |
7514 if (filter !== false && 'toHtml' in format) { | |
7515 val = format.toHtml(val); | |
7516 } | |
7517 | |
7518 return val; | |
7519 }; | |
7520 | |
7521 /** | |
7522 * Sets the WYSIWYG HTML editor value. Should only be the HTML | |
7523 * contained within the body tags | |
7524 * | |
7525 * @param {string} value | |
7526 * @function | |
7527 * @name setWysiwygEditorValue | |
7528 * @memberOf SCEditor.prototype | |
7529 */ | |
7530 base.setWysiwygEditorValue = function (value) { | |
7531 if (!value) { | |
7532 value = '<p><br /></p>'; | |
7533 } | |
7534 | |
7535 wysiwygBody.innerHTML = sanitize(value); | |
7536 replaceEmoticons(); | |
7537 | |
7538 appendNewLine(); | |
7539 triggerValueChanged(); | |
7540 autoExpand(); | |
7541 }; | |
7542 | |
7543 /** | |
7544 * Sets the text editor value | |
7545 * | |
7546 * @param {string} value | |
7547 * @function | |
7548 * @name setSourceEditorValue | |
7549 * @memberOf SCEditor.prototype | |
7550 */ | |
7551 base.setSourceEditorValue = function (value) { | |
7552 sourceEditor.value = value; | |
7553 | |
7554 triggerValueChanged(); | |
7555 }; | |
7556 | |
7557 /** | |
7558 * Updates the textarea that the editor is replacing | |
7559 * with the value currently inside the editor. | |
7560 * | |
7561 * @function | |
7562 * @name updateOriginal | |
7563 * @since 1.4.0 | |
7564 * @memberOf SCEditor.prototype | |
7565 */ | |
7566 base.updateOriginal = function () { | |
7567 original.value = base.val(); | |
7568 }; | |
7569 | |
7570 /** | |
7571 * Replaces any emoticon codes in the passed HTML | |
7572 * with their emoticon images | |
7573 * @private | |
7574 */ | |
7575 replaceEmoticons = function () { | |
7576 if (options.emoticonsEnabled) { | |
7577 replace(wysiwygBody, allEmoticons, options.emoticonsCompat); | |
7578 } | |
7579 }; | |
7580 | |
7581 /** | |
7582 * If the editor is in source code mode | |
7583 * | |
7584 * @return {boolean} | |
7585 * @function | |
7586 * @name inSourceMode | |
7587 * @memberOf SCEditor.prototype | |
7588 */ | |
7589 base.inSourceMode = function () { | |
7590 return hasClass(editorContainer, 'sourceMode'); | |
7591 }; | |
7592 | |
7593 /** | |
7594 * Gets if the editor is in sourceMode | |
7595 * | |
7596 * @return boolean | |
7597 * @function | |
7598 * @name sourceMode | |
7599 * @memberOf SCEditor.prototype | |
7600 */ | |
7601 /** | |
7602 * Sets if the editor is in sourceMode | |
7603 * | |
7604 * @param {boolean} enable | |
7605 * @return {this} | |
7606 * @function | |
7607 * @name sourceMode^2 | |
7608 * @memberOf SCEditor.prototype | |
7609 */ | |
7610 base.sourceMode = function (enable) { | |
7611 var inSourceMode = base.inSourceMode(); | |
7612 | |
7613 if (typeof enable !== 'boolean') { | |
7614 return inSourceMode; | |
7615 } | |
7616 | |
7617 if ((inSourceMode && !enable) || (!inSourceMode && enable)) { | |
7618 base.toggleSourceMode(); | |
7619 } | |
7620 | |
7621 return base; | |
7622 }; | |
7623 | |
7624 /** | |
7625 * Switches between the WYSIWYG and source modes | |
7626 * | |
7627 * @function | |
7628 * @name toggleSourceMode | |
7629 * @since 1.4.0 | |
7630 * @memberOf SCEditor.prototype | |
7631 */ | |
7632 base.toggleSourceMode = function () { | |
7633 var isInSourceMode = base.inSourceMode(); | |
7634 | |
7635 // don't allow switching to WYSIWYG if doesn't support it | |
7636 if (!isWysiwygSupported && isInSourceMode) { | |
7637 return; | |
7638 } | |
7639 | |
7640 if (!isInSourceMode) { | |
7641 rangeHelper.saveRange(); | |
7642 rangeHelper.clear(); | |
7643 } | |
7644 | |
7645 currentSelection = null; | |
7646 base.blur(); | |
7647 | |
7648 if (isInSourceMode) { | |
7649 base.setWysiwygEditorValue(base.getSourceEditorValue()); | |
7650 } else { | |
7651 base.setSourceEditorValue(base.getWysiwygEditorValue()); | |
7652 } | |
7653 | |
7654 toggle(sourceEditor); | |
7655 toggle(wysiwygEditor); | |
7656 | |
7657 toggleClass(editorContainer, 'wysiwygMode', isInSourceMode); | |
7658 toggleClass(editorContainer, 'sourceMode', !isInSourceMode); | |
7659 | |
7660 updateToolBar(); | |
7661 updateActiveButtons(); | |
7662 }; | |
7663 | |
7664 /** | |
7665 * Gets the selected text of the source editor | |
7666 * @return {string} | |
7667 * @private | |
7668 */ | |
7669 sourceEditorSelectedText = function () { | |
7670 sourceEditor.focus(); | |
7671 | |
7672 return sourceEditor.value.substring( | |
7673 sourceEditor.selectionStart, | |
7674 sourceEditor.selectionEnd | |
7675 ); | |
7676 }; | |
7677 | |
7678 /** | |
7679 * Handles the passed command | |
7680 * @private | |
7681 */ | |
7682 handleCommand = function (caller, cmd) { | |
7683 // check if in text mode and handle text commands | |
7684 if (base.inSourceMode()) { | |
7685 if (cmd.txtExec) { | |
7686 if (Array.isArray(cmd.txtExec)) { | |
7687 base.sourceEditorInsertText.apply(base, cmd.txtExec); | |
7688 } else { | |
7689 cmd.txtExec.call(base, caller, sourceEditorSelectedText()); | |
7690 } | |
7691 } | |
7692 } else if (cmd.exec) { | |
7693 if (isFunction(cmd.exec)) { | |
7694 cmd.exec.call(base, caller); | |
7695 } else { | |
7696 base.execCommand( | |
7697 cmd.exec, | |
7698 cmd.hasOwnProperty('execParam') ? cmd.execParam : null | |
7699 ); | |
7700 } | |
7701 } | |
7702 | |
7703 }; | |
7704 | |
7705 /** | |
7706 * Executes a command on the WYSIWYG editor | |
7707 * | |
7708 * @param {string} command | |
7709 * @param {String|Boolean} [param] | |
7710 * @function | |
7711 * @name execCommand | |
7712 * @memberOf SCEditor.prototype | |
7713 */ | |
7714 base.execCommand = function (command, param) { | |
7715 var executed = false, | |
7716 commandObj = base.commands[command]; | |
7717 | |
7718 base.focus(); | |
7719 | |
7720 // TODO: make configurable | |
7721 // don't apply any commands to code elements | |
7722 if (closest(rangeHelper.parentNode(), 'code')) { | |
7723 return; | |
7724 } | |
7725 | |
7726 try { | |
7727 executed = wysiwygDocument.execCommand(command, false, param); | |
7728 } catch (ex) { } | |
7729 | |
7730 // show error if execution failed and an error message exists | |
7731 if (!executed && commandObj && commandObj.errorMessage) { | |
7732 /*global alert:false*/ | |
7733 alert(base._(commandObj.errorMessage)); | |
7734 } | |
7735 | |
7736 updateActiveButtons(); | |
7737 }; | |
7738 | |
7739 /** | |
7740 * Checks if the current selection has changed and triggers | |
7741 * the selectionchanged event if it has. | |
7742 * | |
7743 * In browsers other that don't support selectionchange event it will check | |
7744 * at most once every 100ms. | |
7745 * @private | |
7746 */ | |
7747 checkSelectionChanged = function () { | |
7748 function check() { | |
7749 // Don't create new selection if there isn't one (like after | |
7750 // blur event in iOS) | |
7751 if (wysiwygWindow.getSelection() && | |
7752 wysiwygWindow.getSelection().rangeCount <= 0) { | |
7753 currentSelection = null; | |
7754 // rangeHelper could be null if editor was destroyed | |
7755 // before the timeout had finished | |
7756 } else if (rangeHelper && !rangeHelper.compare(currentSelection)) { | |
7757 currentSelection = rangeHelper.cloneSelected(); | |
7758 | |
7759 // If the selection is in an inline wrap it in a block. | |
7760 // Fixes #331 | |
7761 if (currentSelection && currentSelection.collapsed) { | |
7762 var parent = currentSelection.startContainer; | |
7763 var offset = currentSelection.startOffset; | |
7764 | |
7765 // Handle if selection is placed before/after an element | |
7766 if (offset && parent.nodeType !== TEXT_NODE) { | |
7767 parent = parent.childNodes[offset]; | |
7768 } | |
7769 | |
7770 while (parent && parent.parentNode !== wysiwygBody) { | |
7771 parent = parent.parentNode; | |
7772 } | |
7773 | |
7774 if (parent && isInline(parent, true)) { | |
7775 rangeHelper.saveRange(); | |
7776 wrapInlines(wysiwygBody, wysiwygDocument); | |
7777 rangeHelper.restoreRange(); | |
7778 } | |
7779 } | |
7780 | |
7781 trigger(editorContainer, 'selectionchanged'); | |
7782 } | |
7783 | |
7784 isSelectionCheckPending = false; | |
7785 } | |
7786 | |
7787 if (isSelectionCheckPending) { | |
7788 return; | |
7789 } | |
7790 | |
7791 isSelectionCheckPending = true; | |
7792 | |
7793 // Don't need to limit checking if browser supports the Selection API | |
7794 if ('onselectionchange' in wysiwygDocument) { | |
7795 check(); | |
7796 } else { | |
7797 setTimeout(check, 100); | |
7798 } | |
7799 }; | |
7800 | |
7801 /** | |
7802 * Checks if the current node has changed and triggers | |
7803 * the nodechanged event if it has | |
7804 * @private | |
7805 */ | |
7806 checkNodeChanged = function () { | |
7807 // check if node has changed | |
7808 var oldNode, | |
7809 node = rangeHelper.parentNode(); | |
7810 | |
7811 if (currentNode !== node) { | |
7812 oldNode = currentNode; | |
7813 currentNode = node; | |
7814 currentBlockNode = rangeHelper.getFirstBlockParent(node); | |
7815 | |
7816 trigger(editorContainer, 'nodechanged', { | |
7817 oldNode: oldNode, | |
7818 newNode: currentNode | |
7819 }); | |
7820 } | |
7821 }; | |
7822 | |
7823 /** | |
7824 * Gets the current node that contains the selection/caret in | |
7825 * WYSIWYG mode. | |
7826 * | |
7827 * Will be null in sourceMode or if there is no selection. | |
7828 * | |
7829 * @return {?Node} | |
7830 * @function | |
7831 * @name currentNode | |
7832 * @memberOf SCEditor.prototype | |
7833 */ | |
7834 base.currentNode = function () { | |
7835 return currentNode; | |
7836 }; | |
7837 | |
7838 /** | |
7839 * Gets the first block level node that contains the | |
7840 * selection/caret in WYSIWYG mode. | |
7841 * | |
7842 * Will be null in sourceMode or if there is no selection. | |
7843 * | |
7844 * @return {?Node} | |
7845 * @function | |
7846 * @name currentBlockNode | |
7847 * @memberOf SCEditor.prototype | |
7848 * @since 1.4.4 | |
7849 */ | |
7850 base.currentBlockNode = function () { | |
7851 return currentBlockNode; | |
7852 }; | |
7853 | |
7854 /** | |
7855 * Updates if buttons are active or not | |
7856 * @private | |
7857 */ | |
7858 updateActiveButtons = function () { | |
7859 var firstBlock, parent; | |
7860 var activeClass = 'active'; | |
7861 var doc = wysiwygDocument; | |
7862 var isSource = base.sourceMode(); | |
7863 | |
7864 if (base.readOnly()) { | |
7865 each(find(toolbar, activeClass), function (_, menuItem) { | |
7866 removeClass(menuItem, activeClass); | |
7867 }); | |
7868 return; | |
7869 } | |
7870 | |
7871 if (!isSource) { | |
7872 parent = rangeHelper.parentNode(); | |
7873 firstBlock = rangeHelper.getFirstBlockParent(parent); | |
7874 } | |
7875 | |
7876 for (var j = 0; j < btnStateHandlers.length; j++) { | |
7877 var state = 0; | |
7878 var btn = toolbarButtons[btnStateHandlers[j].name]; | |
7879 var stateFn = btnStateHandlers[j].state; | |
7880 var isDisabled = (isSource && !btn._sceTxtMode) || | |
7881 (!isSource && !btn._sceWysiwygMode); | |
7882 | |
7883 if (isString(stateFn)) { | |
7884 if (!isSource) { | |
7885 try { | |
7886 state = doc.queryCommandEnabled(stateFn) ? 0 : -1; | |
7887 | |
7888 // eslint-disable-next-line max-depth | |
7889 if (state > -1) { | |
7890 state = doc.queryCommandState(stateFn) ? 1 : 0; | |
7891 } | |
7892 } catch (ex) {} | |
7893 } | |
7894 } else if (!isDisabled) { | |
7895 state = stateFn.call(base, parent, firstBlock); | |
7896 } | |
7897 | |
7898 toggleClass(btn, 'disabled', isDisabled || state < 0); | |
7899 toggleClass(btn, activeClass, state > 0); | |
7900 } | |
7901 | |
7902 if (icons && icons.update) { | |
7903 icons.update(isSource, parent, firstBlock); | |
7904 } | |
7905 }; | |
7906 | |
7907 /** | |
7908 * Handles any key press in the WYSIWYG editor | |
7909 * | |
7910 * @private | |
7911 */ | |
7912 handleKeyPress = function (e) { | |
7913 // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496 | |
7914 if (e.defaultPrevented) { | |
7915 return; | |
7916 } | |
7917 | |
7918 base.closeDropDown(); | |
7919 | |
7920 // 13 = enter key | |
7921 if (e.which === 13) { | |
7922 var LIST_TAGS = 'li,ul,ol'; | |
7923 | |
7924 // "Fix" (cludge) for blocklevel elements being duplicated in some | |
7925 // browsers when enter is pressed instead of inserting a newline | |
7926 if (!is(currentBlockNode, LIST_TAGS) && | |
7927 hasStyling(currentBlockNode)) { | |
7928 | |
7929 var br = createElement('br', {}, wysiwygDocument); | |
7930 rangeHelper.insertNode(br); | |
7931 | |
7932 // Last <br> of a block will be collapsed so need to make sure | |
7933 // the <br> that was inserted isn't the last node of a block. | |
7934 var parent = br.parentNode; | |
7935 var lastChild = parent.lastChild; | |
7936 | |
7937 // Sometimes an empty next node is created after the <br> | |
7938 if (lastChild && lastChild.nodeType === TEXT_NODE && | |
7939 lastChild.nodeValue === '') { | |
7940 remove(lastChild); | |
7941 lastChild = parent.lastChild; | |
7942 } | |
7943 | |
7944 // If this is the last BR of a block and the previous | |
7945 // sibling is inline then will need an extra BR. This | |
7946 // is needed because the last BR of a block will be | |
7947 // collapsed. Fixes issue #248 | |
7948 if (!isInline(parent, true) && lastChild === br && | |
7949 isInline(br.previousSibling)) { | |
7950 rangeHelper.insertHTML('<br>'); | |
7951 } | |
7952 | |
7953 e.preventDefault(); | |
7954 } | |
7955 } | |
7956 }; | |
7957 | |
7958 /** | |
7959 * Makes sure that if there is a code or quote tag at the | |
7960 * end of the editor, that there is a new line after it. | |
7961 * | |
7962 * If there wasn't a new line at the end you wouldn't be able | |
7963 * to enter any text after a code/quote tag | |
7964 * @return {void} | |
7965 * @private | |
7966 */ | |
7967 appendNewLine = function () { | |
7968 // Check all nodes in reverse until either add a new line | |
7969 // or reach a non-empty textnode or BR at which point can | |
7970 // stop checking. | |
7971 rTraverse(wysiwygBody, function (node) { | |
7972 // Last block, add new line after if has styling | |
7973 if (node.nodeType === ELEMENT_NODE && | |
7974 !/inline/.test(css(node, 'display'))) { | |
7975 | |
7976 // Add line break after if has styling | |
7977 if (!is(node, '.sceditor-nlf') && hasStyling(node)) { | |
7978 var paragraph = createElement('p', {}, wysiwygDocument); | |
7979 paragraph.className = 'sceditor-nlf'; | |
7980 paragraph.innerHTML = '<br />'; | |
7981 appendChild(wysiwygBody, paragraph); | |
7982 return false; | |
7983 } | |
7984 } | |
7985 | |
7986 // Last non-empty text node or line break. | |
7987 // No need to add line-break after them | |
7988 if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) || | |
7989 is(node, 'br')) { | |
7990 return false; | |
7991 } | |
7992 }); | |
7993 }; | |
7994 | |
7995 /** | |
7996 * Handles form reset event | |
7997 * @private | |
7998 */ | |
7999 handleFormReset = function () { | |
8000 base.val(original.value); | |
8001 }; | |
8002 | |
8003 /** | |
8004 * Handles any mousedown press in the WYSIWYG editor | |
8005 * @private | |
8006 */ | |
8007 handleMouseDown = function () { | |
8008 base.closeDropDown(); | |
8009 }; | |
8010 | |
8011 /** | |
8012 * Translates the string into the locale language. | |
8013 * | |
8014 * Replaces any {0}, {1}, {2}, ect. with the params provided. | |
8015 * | |
8016 * @param {string} str | |
8017 * @param {...String} args | |
8018 * @return {string} | |
8019 * @function | |
8020 * @name _ | |
8021 * @memberOf SCEditor.prototype | |
8022 */ | |
8023 base._ = function () { | |
8024 var undef, | |
8025 args = arguments; | |
8026 | |
8027 if (locale && locale[args[0]]) { | |
8028 args[0] = locale[args[0]]; | |
8029 } | |
8030 | |
8031 return args[0].replace(/\{(\d+)\}/g, function (str, p1) { | |
8032 return args[p1 - 0 + 1] !== undef ? | |
8033 args[p1 - 0 + 1] : | |
8034 '{' + p1 + '}'; | |
8035 }); | |
8036 }; | |
8037 | |
8038 /** | |
8039 * Passes events on to any handlers | |
8040 * @private | |
8041 * @return void | |
8042 */ | |
8043 handleEvent = function (e) { | |
8044 if (pluginManager) { | |
8045 // Send event to all plugins | |
8046 pluginManager.call(e.type + 'Event', e, base); | |
8047 } | |
8048 | |
8049 // convert the event into a custom event to send | |
8050 var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type; | |
8051 | |
8052 if (eventHandlers[name]) { | |
8053 eventHandlers[name].forEach(function (fn) { | |
8054 fn.call(base, e); | |
8055 }); | |
8056 } | |
8057 }; | |
8058 | |
8059 /** | |
8060 * Binds a handler to the specified events | |
8061 * | |
8062 * This function only binds to a limited list of | |
8063 * supported events. | |
8064 * | |
8065 * The supported events are: | |
8066 * | |
8067 * * keyup | |
8068 * * keydown | |
8069 * * Keypress | |
8070 * * blur | |
8071 * * focus | |
8072 * * input | |
8073 * * nodechanged - When the current node containing | |
8074 * the selection changes in WYSIWYG mode | |
8075 * * contextmenu | |
8076 * * selectionchanged | |
8077 * * valuechanged | |
8078 * | |
8079 * | |
8080 * The events param should be a string containing the event(s) | |
8081 * to bind this handler to. If multiple, they should be separated | |
8082 * by spaces. | |
8083 * | |
8084 * @param {string} events | |
8085 * @param {Function} handler | |
8086 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8087 * to the WYSIWYG editor | |
8088 * @param {boolean} excludeSource if to exclude adding this handler | |
8089 * to the source editor | |
8090 * @return {this} | |
8091 * @function | |
8092 * @name bind | |
8093 * @memberOf SCEditor.prototype | |
8094 * @since 1.4.1 | |
8095 */ | |
8096 base.bind = function (events, handler, excludeWysiwyg, excludeSource) { | |
8097 events = events.split(' '); | |
8098 | |
8099 var i = events.length; | |
8100 while (i--) { | |
8101 if (isFunction(handler)) { | |
8102 var wysEvent = 'scewys' + events[i]; | |
8103 var srcEvent = 'scesrc' + events[i]; | |
8104 // Use custom events to allow passing the instance as the | |
8105 // 2nd argument. | |
8106 // Also allows unbinding without unbinding the editors own | |
8107 // event handlers. | |
8108 if (!excludeWysiwyg) { | |
8109 eventHandlers[wysEvent] = eventHandlers[wysEvent] || []; | |
8110 eventHandlers[wysEvent].push(handler); | |
8111 } | |
8112 | |
8113 if (!excludeSource) { | |
8114 eventHandlers[srcEvent] = eventHandlers[srcEvent] || []; | |
8115 eventHandlers[srcEvent].push(handler); | |
8116 } | |
8117 | |
8118 // Start sending value changed events | |
8119 if (events[i] === 'valuechanged') { | |
8120 triggerValueChanged.hasHandler = true; | |
8121 } | |
8122 } | |
8123 } | |
8124 | |
8125 return base; | |
8126 }; | |
8127 | |
8128 /** | |
8129 * Unbinds an event that was bound using bind(). | |
8130 * | |
8131 * @param {string} events | |
8132 * @param {Function} handler | |
8133 * @param {boolean} excludeWysiwyg If to exclude unbinding this | |
8134 * handler from the WYSIWYG editor | |
8135 * @param {boolean} excludeSource if to exclude unbinding this | |
8136 * handler from the source editor | |
8137 * @return {this} | |
8138 * @function | |
8139 * @name unbind | |
8140 * @memberOf SCEditor.prototype | |
8141 * @since 1.4.1 | |
8142 * @see bind | |
8143 */ | |
8144 base.unbind = function (events, handler, excludeWysiwyg, excludeSource) { | |
8145 events = events.split(' '); | |
8146 | |
8147 var i = events.length; | |
8148 while (i--) { | |
8149 if (isFunction(handler)) { | |
8150 if (!excludeWysiwyg) { | |
8151 arrayRemove( | |
8152 eventHandlers['scewys' + events[i]] || [], handler); | |
8153 } | |
8154 | |
8155 if (!excludeSource) { | |
8156 arrayRemove( | |
8157 eventHandlers['scesrc' + events[i]] || [], handler); | |
8158 } | |
8159 } | |
8160 } | |
8161 | |
8162 return base; | |
8163 }; | |
8164 | |
8165 /** | |
8166 * Blurs the editors input area | |
8167 * | |
8168 * @return {this} | |
8169 * @function | |
8170 * @name blur | |
8171 * @memberOf SCEditor.prototype | |
8172 * @since 1.3.6 | |
8173 */ | |
8174 /** | |
8175 * Adds a handler to the editors blur event | |
8176 * | |
8177 * @param {Function} handler | |
8178 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8179 * to the WYSIWYG editor | |
8180 * @param {boolean} excludeSource if to exclude adding this handler | |
8181 * to the source editor | |
8182 * @return {this} | |
8183 * @function | |
8184 * @name blur^2 | |
8185 * @memberOf SCEditor.prototype | |
8186 * @since 1.4.1 | |
8187 */ | |
8188 base.blur = function (handler, excludeWysiwyg, excludeSource) { | |
8189 if (isFunction(handler)) { | |
8190 base.bind('blur', handler, excludeWysiwyg, excludeSource); | |
8191 } else if (!base.sourceMode()) { | |
8192 wysiwygBody.blur(); | |
8193 } else { | |
8194 sourceEditor.blur(); | |
8195 } | |
8196 | |
8197 return base; | |
8198 }; | |
8199 | |
8200 /** | |
8201 * Focuses the editors input area | |
8202 * | |
8203 * @return {this} | |
8204 * @function | |
8205 * @name focus | |
8206 * @memberOf SCEditor.prototype | |
8207 */ | |
8208 /** | |
8209 * Adds an event handler to the focus event | |
8210 * | |
8211 * @param {Function} handler | |
8212 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8213 * to the WYSIWYG editor | |
8214 * @param {boolean} excludeSource if to exclude adding this handler | |
8215 * to the source editor | |
8216 * @return {this} | |
8217 * @function | |
8218 * @name focus^2 | |
8219 * @memberOf SCEditor.prototype | |
8220 * @since 1.4.1 | |
8221 */ | |
8222 base.focus = function (handler, excludeWysiwyg, excludeSource) { | |
8223 if (isFunction(handler)) { | |
8224 base.bind('focus', handler, excludeWysiwyg, excludeSource); | |
8225 } else if (!base.inSourceMode()) { | |
8226 // Already has focus so do nothing | |
8227 if (find(wysiwygDocument, ':focus').length) { | |
8228 return; | |
8229 } | |
8230 | |
8231 var container; | |
8232 var rng = rangeHelper.selectedRange(); | |
8233 | |
8234 // Fix FF bug where it shows the cursor in the wrong place | |
8235 // if the editor hasn't had focus before. See issue #393 | |
8236 if (!currentSelection) { | |
8237 autofocus(true); | |
8238 } | |
8239 | |
8240 // Check if cursor is set after a BR when the BR is the only | |
8241 // child of the parent. In Firefox this causes a line break | |
8242 // to occur when something is typed. See issue #321 | |
8243 if (rng && rng.endOffset === 1 && rng.collapsed) { | |
8244 container = rng.endContainer; | |
8245 | |
8246 if (container && container.childNodes.length === 1 && | |
8247 is(container.firstChild, 'br')) { | |
8248 rng.setStartBefore(container.firstChild); | |
8249 rng.collapse(true); | |
8250 rangeHelper.selectRange(rng); | |
8251 } | |
8252 } | |
8253 | |
8254 wysiwygWindow.focus(); | |
8255 wysiwygBody.focus(); | |
8256 } else { | |
8257 sourceEditor.focus(); | |
8258 } | |
8259 | |
8260 updateActiveButtons(); | |
8261 | |
8262 return base; | |
8263 }; | |
8264 | |
8265 /** | |
8266 * Adds a handler to the key down event | |
8267 * | |
8268 * @param {Function} handler | |
8269 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8270 * to the WYSIWYG editor | |
8271 * @param {boolean} excludeSource If to exclude adding this handler | |
8272 * to the source editor | |
8273 * @return {this} | |
8274 * @function | |
8275 * @name keyDown | |
8276 * @memberOf SCEditor.prototype | |
8277 * @since 1.4.1 | |
8278 */ | |
8279 base.keyDown = function (handler, excludeWysiwyg, excludeSource) { | |
8280 return base.bind('keydown', handler, excludeWysiwyg, excludeSource); | |
8281 }; | |
8282 | |
8283 /** | |
8284 * Adds a handler to the key press event | |
8285 * | |
8286 * @param {Function} handler | |
8287 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8288 * to the WYSIWYG editor | |
8289 * @param {boolean} excludeSource If to exclude adding this handler | |
8290 * to the source editor | |
8291 * @return {this} | |
8292 * @function | |
8293 * @name keyPress | |
8294 * @memberOf SCEditor.prototype | |
8295 * @since 1.4.1 | |
8296 */ | |
8297 base.keyPress = function (handler, excludeWysiwyg, excludeSource) { | |
8298 return base | |
8299 .bind('keypress', handler, excludeWysiwyg, excludeSource); | |
8300 }; | |
8301 | |
8302 /** | |
8303 * Adds a handler to the key up event | |
8304 * | |
8305 * @param {Function} handler | |
8306 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8307 * to the WYSIWYG editor | |
8308 * @param {boolean} excludeSource If to exclude adding this handler | |
8309 * to the source editor | |
8310 * @return {this} | |
8311 * @function | |
8312 * @name keyUp | |
8313 * @memberOf SCEditor.prototype | |
8314 * @since 1.4.1 | |
8315 */ | |
8316 base.keyUp = function (handler, excludeWysiwyg, excludeSource) { | |
8317 return base.bind('keyup', handler, excludeWysiwyg, excludeSource); | |
8318 }; | |
8319 | |
8320 /** | |
8321 * Adds a handler to the node changed event. | |
8322 * | |
8323 * Happens whenever the node containing the selection/caret | |
8324 * changes in WYSIWYG mode. | |
8325 * | |
8326 * @param {Function} handler | |
8327 * @return {this} | |
8328 * @function | |
8329 * @name nodeChanged | |
8330 * @memberOf SCEditor.prototype | |
8331 * @since 1.4.1 | |
8332 */ | |
8333 base.nodeChanged = function (handler) { | |
8334 return base.bind('nodechanged', handler, false, true); | |
8335 }; | |
8336 | |
8337 /** | |
8338 * Adds a handler to the selection changed event | |
8339 * | |
8340 * Happens whenever the selection changes in WYSIWYG mode. | |
8341 * | |
8342 * @param {Function} handler | |
8343 * @return {this} | |
8344 * @function | |
8345 * @name selectionChanged | |
8346 * @memberOf SCEditor.prototype | |
8347 * @since 1.4.1 | |
8348 */ | |
8349 base.selectionChanged = function (handler) { | |
8350 return base.bind('selectionchanged', handler, false, true); | |
8351 }; | |
8352 | |
8353 /** | |
8354 * Adds a handler to the value changed event | |
8355 * | |
8356 * Happens whenever the current editor value changes. | |
8357 * | |
8358 * Whenever anything is inserted, the value changed or | |
8359 * 1.5 secs after text is typed. If a space is typed it will | |
8360 * cause the event to be triggered immediately instead of | |
8361 * after 1.5 seconds | |
8362 * | |
8363 * @param {Function} handler | |
8364 * @param {boolean} excludeWysiwyg If to exclude adding this handler | |
8365 * to the WYSIWYG editor | |
8366 * @param {boolean} excludeSource If to exclude adding this handler | |
8367 * to the source editor | |
8368 * @return {this} | |
8369 * @function | |
8370 * @name valueChanged | |
8371 * @memberOf SCEditor.prototype | |
8372 * @since 1.4.5 | |
8373 */ | |
8374 base.valueChanged = function (handler, excludeWysiwyg, excludeSource) { | |
8375 return base | |
8376 .bind('valuechanged', handler, excludeWysiwyg, excludeSource); | |
8377 }; | |
8378 | |
8379 /** | |
8380 * Emoticons keypress handler | |
8381 * @private | |
8382 */ | |
8383 emoticonsKeyPress = function (e) { | |
8384 var replacedEmoticon, | |
8385 cachePos = 0, | |
8386 emoticonsCache = base.emoticonsCache, | |
8387 curChar = String.fromCharCode(e.which); | |
8388 | |
8389 // TODO: Make configurable | |
8390 if (closest(currentBlockNode, 'code')) { | |
8391 return; | |
8392 } | |
8393 | |
8394 if (!emoticonsCache) { | |
8395 emoticonsCache = []; | |
8396 | |
8397 each(allEmoticons, function (key, html) { | |
8398 emoticonsCache[cachePos++] = [key, html]; | |
8399 }); | |
8400 | |
8401 emoticonsCache.sort(function (a, b) { | |
8402 return a[0].length - b[0].length; | |
8403 }); | |
8404 | |
8405 base.emoticonsCache = emoticonsCache; | |
8406 base.longestEmoticonCode = | |
8407 emoticonsCache[emoticonsCache.length - 1][0].length; | |
8408 } | |
8409 | |
8410 replacedEmoticon = rangeHelper.replaceKeyword( | |
8411 base.emoticonsCache, | |
8412 true, | |
8413 true, | |
8414 base.longestEmoticonCode, | |
8415 options.emoticonsCompat, | |
8416 curChar | |
8417 ); | |
8418 | |
8419 if (replacedEmoticon) { | |
8420 if (!options.emoticonsCompat || !/^\s$/.test(curChar)) { | |
8421 e.preventDefault(); | |
8422 } | |
8423 } | |
8424 }; | |
8425 | |
8426 /** | |
8427 * Makes sure emoticons are surrounded by whitespace | |
8428 * @private | |
8429 */ | |
8430 emoticonsCheckWhitespace = function () { | |
8431 checkWhitespace(currentBlockNode, rangeHelper); | |
8432 }; | |
8433 | |
8434 /** | |
8435 * Gets if emoticons are currently enabled | |
8436 * @return {boolean} | |
8437 * @function | |
8438 * @name emoticons | |
8439 * @memberOf SCEditor.prototype | |
8440 * @since 1.4.2 | |
8441 */ | |
8442 /** | |
8443 * Enables/disables emoticons | |
8444 * | |
8445 * @param {boolean} enable | |
8446 * @return {this} | |
8447 * @function | |
8448 * @name emoticons^2 | |
8449 * @memberOf SCEditor.prototype | |
8450 * @since 1.4.2 | |
8451 */ | |
8452 base.emoticons = function (enable) { | |
8453 if (!enable && enable !== false) { | |
8454 return options.emoticonsEnabled; | |
8455 } | |
8456 | |
8457 options.emoticonsEnabled = enable; | |
8458 | |
8459 if (enable) { | |
8460 on(wysiwygBody, 'keypress', emoticonsKeyPress); | |
8461 | |
8462 if (!base.sourceMode()) { | |
8463 rangeHelper.saveRange(); | |
8464 | |
8465 replaceEmoticons(); | |
8466 triggerValueChanged(false); | |
8467 | |
8468 rangeHelper.restoreRange(); | |
8469 } | |
8470 } else { | |
8471 var emoticons = | |
8472 find(wysiwygBody, 'img[data-sceditor-emoticon]'); | |
8473 | |
8474 each(emoticons, function (_, img) { | |
8475 var text = data(img, 'sceditor-emoticon'); | |
8476 var textNode = wysiwygDocument.createTextNode(text); | |
8477 img.parentNode.replaceChild(textNode, img); | |
8478 }); | |
8479 | |
8480 off(wysiwygBody, 'keypress', emoticonsKeyPress); | |
8481 | |
8482 triggerValueChanged(); | |
8483 } | |
8484 | |
8485 return base; | |
8486 }; | |
8487 | |
8488 /** | |
8489 * Gets the current WYSIWYG editors inline CSS | |
8490 * | |
8491 * @return {string} | |
8492 * @function | |
8493 * @name css | |
8494 * @memberOf SCEditor.prototype | |
8495 * @since 1.4.3 | |
8496 */ | |
8497 /** | |
8498 * Sets inline CSS for the WYSIWYG editor | |
8499 * | |
8500 * @param {string} css | |
8501 * @return {this} | |
8502 * @function | |
8503 * @name css^2 | |
8504 * @memberOf SCEditor.prototype | |
8505 * @since 1.4.3 | |
8506 */ | |
8507 base.css = function (css) { | |
8508 if (!inlineCss) { | |
8509 inlineCss = createElement('style', { | |
8510 id: 'inline' | |
8511 }, wysiwygDocument); | |
8512 | |
8513 appendChild(wysiwygDocument.head, inlineCss); | |
8514 } | |
8515 | |
8516 if (!isString(css)) { | |
8517 return inlineCss.styleSheet ? | |
8518 inlineCss.styleSheet.cssText : inlineCss.innerHTML; | |
8519 } | |
8520 | |
8521 if (inlineCss.styleSheet) { | |
8522 inlineCss.styleSheet.cssText = css; | |
8523 } else { | |
8524 inlineCss.innerHTML = css; | |
8525 } | |
8526 | |
8527 return base; | |
8528 }; | |
8529 | |
8530 /** | |
8531 * Handles the keydown event, used for shortcuts | |
8532 * @private | |
8533 */ | |
8534 handleKeyDown = function (e) { | |
8535 var shortcut = [], | |
8536 SHIFT_KEYS = { | |
8537 '`': '~', | |
8538 '1': '!', | |
8539 '2': '@', | |
8540 '3': '#', | |
8541 '4': '$', | |
8542 '5': '%', | |
8543 '6': '^', | |
8544 '7': '&', | |
8545 '8': '*', | |
8546 '9': '(', | |
8547 '0': ')', | |
8548 '-': '_', | |
8549 '=': '+', | |
8550 ';': ': ', | |
8551 '\'': '"', | |
8552 ',': '<', | |
8553 '.': '>', | |
8554 '/': '?', | |
8555 '\\': '|', | |
8556 '[': '{', | |
8557 ']': '}' | |
8558 }, | |
8559 SPECIAL_KEYS = { | |
8560 8: 'backspace', | |
8561 9: 'tab', | |
8562 13: 'enter', | |
8563 19: 'pause', | |
8564 20: 'capslock', | |
8565 27: 'esc', | |
8566 32: 'space', | |
8567 33: 'pageup', | |
8568 34: 'pagedown', | |
8569 35: 'end', | |
8570 36: 'home', | |
8571 37: 'left', | |
8572 38: 'up', | |
8573 39: 'right', | |
8574 40: 'down', | |
8575 45: 'insert', | |
8576 46: 'del', | |
8577 91: 'win', | |
8578 92: 'win', | |
8579 93: 'select', | |
8580 96: '0', | |
8581 97: '1', | |
8582 98: '2', | |
8583 99: '3', | |
8584 100: '4', | |
8585 101: '5', | |
8586 102: '6', | |
8587 103: '7', | |
8588 104: '8', | |
8589 105: '9', | |
8590 106: '*', | |
8591 107: '+', | |
8592 109: '-', | |
8593 110: '.', | |
8594 111: '/', | |
8595 112: 'f1', | |
8596 113: 'f2', | |
8597 114: 'f3', | |
8598 115: 'f4', | |
8599 116: 'f5', | |
8600 117: 'f6', | |
8601 118: 'f7', | |
8602 119: 'f8', | |
8603 120: 'f9', | |
8604 121: 'f10', | |
8605 122: 'f11', | |
8606 123: 'f12', | |
8607 144: 'numlock', | |
8608 145: 'scrolllock', | |
8609 186: ';', | |
8610 187: '=', | |
8611 188: ',', | |
8612 189: '-', | |
8613 190: '.', | |
8614 191: '/', | |
8615 192: '`', | |
8616 219: '[', | |
8617 220: '\\', | |
8618 221: ']', | |
8619 222: '\'' | |
8620 }, | |
8621 NUMPAD_SHIFT_KEYS = { | |
8622 109: '-', | |
8623 110: 'del', | |
8624 111: '/', | |
8625 96: '0', | |
8626 97: '1', | |
8627 98: '2', | |
8628 99: '3', | |
8629 100: '4', | |
8630 101: '5', | |
8631 102: '6', | |
8632 103: '7', | |
8633 104: '8', | |
8634 105: '9' | |
8635 }, | |
8636 which = e.which, | |
8637 character = SPECIAL_KEYS[which] || | |
8638 String.fromCharCode(which).toLowerCase(); | |
8639 | |
8640 if (e.ctrlKey || e.metaKey) { | |
8641 shortcut.push('ctrl'); | |
8642 } | |
8643 | |
8644 if (e.altKey) { | |
8645 shortcut.push('alt'); | |
8646 } | |
8647 | |
8648 if (e.shiftKey) { | |
8649 shortcut.push('shift'); | |
8650 | |
8651 if (NUMPAD_SHIFT_KEYS[which]) { | |
8652 character = NUMPAD_SHIFT_KEYS[which]; | |
8653 } else if (SHIFT_KEYS[character]) { | |
8654 character = SHIFT_KEYS[character]; | |
8655 } | |
8656 } | |
8657 | |
8658 // Shift is 16, ctrl is 17 and alt is 18 | |
8659 if (character && (which < 16 || which > 18)) { | |
8660 shortcut.push(character); | |
8661 } | |
8662 | |
8663 shortcut = shortcut.join('+'); | |
8664 if (shortcutHandlers[shortcut] && | |
8665 shortcutHandlers[shortcut].call(base) === false) { | |
8666 | |
8667 e.stopPropagation(); | |
8668 e.preventDefault(); | |
8669 } | |
8670 }; | |
8671 | |
8672 /** | |
8673 * Adds a shortcut handler to the editor | |
8674 * @param {string} shortcut | |
8675 * @param {String|Function} cmd | |
8676 * @return {sceditor} | |
8677 */ | |
8678 base.addShortcut = function (shortcut, cmd) { | |
8679 shortcut = shortcut.toLowerCase(); | |
8680 | |
8681 if (isString(cmd)) { | |
8682 shortcutHandlers[shortcut] = function () { | |
8683 handleCommand(toolbarButtons[cmd], base.commands[cmd]); | |
8684 | |
8685 return false; | |
8686 }; | |
8687 } else { | |
8688 shortcutHandlers[shortcut] = cmd; | |
8689 } | |
8690 | |
8691 return base; | |
8692 }; | |
8693 | |
8694 /** | |
8695 * Removes a shortcut handler | |
8696 * @param {string} shortcut | |
8697 * @return {sceditor} | |
8698 */ | |
8699 base.removeShortcut = function (shortcut) { | |
8700 delete shortcutHandlers[shortcut.toLowerCase()]; | |
8701 | |
8702 return base; | |
8703 }; | |
8704 | |
8705 /** | |
8706 * Handles the backspace key press | |
8707 * | |
8708 * Will remove block styling like quotes/code ect if at the start. | |
8709 * @private | |
8710 */ | |
8711 handleBackSpace = function (e) { | |
8712 var node, offset, range, parent; | |
8713 | |
8714 // 8 is the backspace key | |
8715 if (options.disableBlockRemove || e.which !== 8 || | |
8716 !(range = rangeHelper.selectedRange())) { | |
8717 return; | |
8718 } | |
8719 | |
8720 node = range.startContainer; | |
8721 offset = range.startOffset; | |
8722 | |
8723 if (offset !== 0 || !(parent = currentStyledBlockNode()) || | |
8724 is(parent, 'body')) { | |
8725 return; | |
8726 } | |
8727 | |
8728 while (node !== parent) { | |
8729 while (node.previousSibling) { | |
8730 node = node.previousSibling; | |
8731 | |
8732 // Everything but empty text nodes before the cursor | |
8733 // should prevent the style from being removed | |
8734 if (node.nodeType !== TEXT_NODE || node.nodeValue) { | |
8735 return; | |
8736 } | |
8737 } | |
8738 | |
8739 if (!(node = node.parentNode)) { | |
8740 return; | |
8741 } | |
8742 } | |
8743 | |
8744 // The backspace was pressed at the start of | |
8745 // the container so clear the style | |
8746 base.clearBlockFormatting(parent); | |
8747 e.preventDefault(); | |
8748 }; | |
8749 | |
8750 /** | |
8751 * Gets the first styled block node that contains the cursor | |
8752 * @return {HTMLElement} | |
8753 */ | |
8754 currentStyledBlockNode = function () { | |
8755 var block = currentBlockNode; | |
8756 | |
8757 while (!hasStyling(block) || isInline(block, true)) { | |
8758 if (!(block = block.parentNode) || is(block, 'body')) { | |
8759 return; | |
8760 } | |
8761 } | |
8762 | |
8763 return block; | |
8764 }; | |
8765 | |
8766 /** | |
8767 * Clears the formatting of the passed block element. | |
8768 * | |
8769 * If block is false, if will clear the styling of the first | |
8770 * block level element that contains the cursor. | |
8771 * @param {HTMLElement} block | |
8772 * @since 1.4.4 | |
8773 */ | |
8774 base.clearBlockFormatting = function (block) { | |
8775 block = block || currentStyledBlockNode(); | |
8776 | |
8777 if (!block || is(block, 'body')) { | |
8778 return base; | |
8779 } | |
8780 | |
8781 rangeHelper.saveRange(); | |
8782 | |
8783 block.className = ''; | |
8784 | |
8785 attr(block, 'style', ''); | |
8786 | |
8787 if (!is(block, 'p,div,td')) { | |
8788 convertElement(block, 'p'); | |
8789 } | |
8790 | |
8791 rangeHelper.restoreRange(); | |
8792 return base; | |
8793 }; | |
8794 | |
8795 /** | |
8796 * Triggers the valueChanged signal if there is | |
8797 * a plugin that handles it. | |
8798 * | |
8799 * If rangeHelper.saveRange() has already been | |
8800 * called, then saveRange should be set to false | |
8801 * to prevent the range being saved twice. | |
8802 * | |
8803 * @since 1.4.5 | |
8804 * @param {boolean} saveRange If to call rangeHelper.saveRange(). | |
8805 * @private | |
8806 */ | |
8807 triggerValueChanged = function (saveRange) { | |
8808 if (!pluginManager || | |
8809 (!pluginManager.hasHandler('valuechangedEvent') && | |
8810 !triggerValueChanged.hasHandler)) { | |
8811 return; | |
8812 } | |
8813 | |
8814 var currentHtml, | |
8815 sourceMode = base.sourceMode(), | |
8816 hasSelection = !sourceMode && rangeHelper.hasSelection(); | |
8817 | |
8818 // Composition end isn't guaranteed to fire but must have | |
8819 // ended when triggerValueChanged() is called so reset it | |
8820 isComposing = false; | |
8821 | |
8822 // Don't need to save the range if sceditor-start-marker | |
8823 // is present as the range is already saved | |
8824 saveRange = saveRange !== false && | |
8825 !wysiwygDocument.getElementById('sceditor-start-marker'); | |
8826 | |
8827 // Clear any current timeout as it's now been triggered | |
8828 if (valueChangedKeyUpTimer) { | |
8829 clearTimeout(valueChangedKeyUpTimer); | |
8830 valueChangedKeyUpTimer = false; | |
8831 } | |
8832 | |
8833 if (hasSelection && saveRange) { | |
8834 rangeHelper.saveRange(); | |
8835 } | |
8836 | |
8837 currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML; | |
8838 | |
8839 // Only trigger if something has actually changed. | |
8840 if (currentHtml !== triggerValueChanged.lastVal) { | |
8841 triggerValueChanged.lastVal = currentHtml; | |
8842 | |
8843 trigger(editorContainer, 'valuechanged', { | |
8844 rawValue: sourceMode ? base.val() : currentHtml | |
8845 }); | |
8846 } | |
8847 | |
8848 if (hasSelection && saveRange) { | |
8849 rangeHelper.removeMarkers(); | |
8850 } | |
8851 }; | |
8852 | |
8853 /** | |
8854 * Should be called whenever there is a blur event | |
8855 * @private | |
8856 */ | |
8857 valueChangedBlur = function () { | |
8858 if (valueChangedKeyUpTimer) { | |
8859 triggerValueChanged(); | |
8860 } | |
8861 }; | |
8862 | |
8863 /** | |
8864 * Should be called whenever there is a keypress event | |
8865 * @param {Event} e The keypress event | |
8866 * @private | |
8867 */ | |
8868 valueChangedKeyUp = function (e) { | |
8869 var which = e.which, | |
8870 lastChar = valueChangedKeyUp.lastChar, | |
8871 lastWasSpace = (lastChar === 13 || lastChar === 32), | |
8872 lastWasDelete = (lastChar === 8 || lastChar === 46); | |
8873 | |
8874 valueChangedKeyUp.lastChar = which; | |
8875 | |
8876 if (isComposing) { | |
8877 return; | |
8878 } | |
8879 | |
8880 // 13 = return & 32 = space | |
8881 if (which === 13 || which === 32) { | |
8882 if (!lastWasSpace) { | |
8883 triggerValueChanged(); | |
8884 } else { | |
8885 valueChangedKeyUp.triggerNext = true; | |
8886 } | |
8887 // 8 = backspace & 46 = del | |
8888 } else if (which === 8 || which === 46) { | |
8889 if (!lastWasDelete) { | |
8890 triggerValueChanged(); | |
8891 } else { | |
8892 valueChangedKeyUp.triggerNext = true; | |
8893 } | |
8894 } else if (valueChangedKeyUp.triggerNext) { | |
8895 triggerValueChanged(); | |
8896 valueChangedKeyUp.triggerNext = false; | |
8897 } | |
8898 | |
8899 // Clear the previous timeout and set a new one. | |
8900 clearTimeout(valueChangedKeyUpTimer); | |
8901 | |
8902 // Trigger the event 1.5s after the last keypress if space | |
8903 // isn't pressed. This might need to be lowered, will need | |
8904 // to look into what the slowest average Chars Per Min is. | |
8905 valueChangedKeyUpTimer = setTimeout(function () { | |
8906 if (!isComposing) { | |
8907 triggerValueChanged(); | |
8908 } | |
8909 }, 1500); | |
8910 }; | |
8911 | |
8912 handleComposition = function (e) { | |
8913 isComposing = /start/i.test(e.type); | |
8914 | |
8915 if (!isComposing) { | |
8916 triggerValueChanged(); | |
8917 } | |
8918 }; | |
8919 | |
8920 autoUpdate = function () { | |
8921 base.updateOriginal(); | |
8922 }; | |
8923 | |
8924 // run the initializer | |
8925 init(); | |
8926 } | |
8927 | |
8928 /** | |
8929 * Map containing the loaded SCEditor locales | |
8930 * @type {Object} | |
8931 * @name locale | |
8932 * @memberOf sceditor | |
8933 */ | |
8934 SCEditor.locale = {}; | |
8935 | |
8936 SCEditor.formats = {}; | |
8937 SCEditor.icons = {}; | |
8938 | |
8939 | |
8940 /** | |
8941 * Static command helper class | |
8942 * @class command | |
8943 * @name sceditor.command | |
8944 */ | |
8945 SCEditor.command = | |
8946 /** @lends sceditor.command */ | |
8947 { | |
8948 /** | |
8949 * Gets a command | |
8950 * | |
8951 * @param {string} name | |
8952 * @return {Object|null} | |
8953 * @since v1.3.5 | |
8954 */ | |
8955 get: function (name) { | |
8956 return defaultCmds[name] || null; | |
8957 }, | |
8958 | |
8959 /** | |
8960 * <p>Adds a command to the editor or updates an existing | |
8961 * command if a command with the specified name already exists.</p> | |
8962 * | |
8963 * <p>Once a command is add it can be included in the toolbar by | |
8964 * adding it's name to the toolbar option in the constructor. It | |
8965 * can also be executed manually by calling | |
8966 * {@link sceditor.execCommand}</p> | |
8967 * | |
8968 * @example | |
8969 * SCEditor.command.set("hello", | |
8970 * { | |
8971 * exec: function () { | |
8972 * alert("Hello World!"); | |
8973 * } | |
8974 * }); | |
8975 * | |
8976 * @param {string} name | |
8977 * @param {Object} cmd | |
8978 * @return {this|false} Returns false if name or cmd is false | |
8979 * @since v1.3.5 | |
8980 */ | |
8981 set: function (name, cmd) { | |
8982 if (!name || !cmd) { | |
8983 return false; | |
8984 } | |
8985 | |
8986 // merge any existing command properties | |
8987 cmd = extend(defaultCmds[name] || {}, cmd); | |
8988 | |
8989 cmd.remove = function () { | |
8990 SCEditor.command.remove(name); | |
8991 }; | |
8992 | |
8993 defaultCmds[name] = cmd; | |
8994 return this; | |
8995 }, | |
8996 | |
8997 /** | |
8998 * Removes a command | |
8999 * | |
9000 * @param {string} name | |
9001 * @return {this} | |
9002 * @since v1.3.5 | |
9003 */ | |
9004 remove: function (name) { | |
9005 if (defaultCmds[name]) { | |
9006 delete defaultCmds[name]; | |
9007 } | |
9008 | |
9009 return this; | |
9010 } | |
9011 }; | |
9012 | |
9013 /** | |
9014 * SCEditor | |
9015 * http://www.sceditor.com/ | |
9016 * | |
9017 * Copyright (C) 2017, Sam Clarke (samclarke.com) | |
9018 * | |
9019 * SCEditor is licensed under the MIT license: | |
9020 * http://www.opensource.org/licenses/mit-license.php | |
9021 * | |
9022 * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor | |
9023 * @author Sam Clarke | |
9024 */ | |
9025 | |
9026 | |
9027 window.sceditor = { | |
9028 command: SCEditor.command, | |
9029 commands: defaultCmds, | |
9030 defaultOptions: defaultOptions, | |
9031 | |
9032 ios: ios, | |
9033 isWysiwygSupported: isWysiwygSupported, | |
9034 | |
9035 regexEscape: regex, | |
9036 escapeEntities: entities, | |
9037 escapeUriScheme: uriScheme, | |
9038 | |
9039 dom: { | |
9040 css: css, | |
9041 attr: attr, | |
9042 removeAttr: removeAttr, | |
9043 is: is, | |
9044 closest: closest, | |
9045 width: width, | |
9046 height: height, | |
9047 traverse: traverse, | |
9048 rTraverse: rTraverse, | |
9049 parseHTML: parseHTML, | |
9050 hasStyling: hasStyling, | |
9051 convertElement: convertElement, | |
9052 blockLevelList: blockLevelList, | |
9053 canHaveChildren: canHaveChildren, | |
9054 isInline: isInline, | |
9055 copyCSS: copyCSS, | |
9056 fixNesting: fixNesting, | |
9057 findCommonAncestor: findCommonAncestor, | |
9058 getSibling: getSibling, | |
9059 removeWhiteSpace: removeWhiteSpace, | |
9060 extractContents: extractContents, | |
9061 getOffset: getOffset, | |
9062 getStyle: getStyle, | |
9063 hasStyle: hasStyle | |
9064 }, | |
9065 locale: SCEditor.locale, | |
9066 icons: SCEditor.icons, | |
9067 utils: { | |
9068 each: each, | |
9069 isEmptyObject: isEmptyObject, | |
9070 extend: extend | |
9071 }, | |
9072 plugins: PluginManager.plugins, | |
9073 formats: SCEditor.formats, | |
9074 create: function (textarea, options) { | |
9075 options = options || {}; | |
9076 | |
9077 // Don't allow the editor to be initialised | |
9078 // on it's own source editor | |
9079 if (parent(textarea, '.sceditor-container')) { | |
9080 return; | |
9081 } | |
9082 | |
9083 if (options.runWithoutWysiwygSupport || isWysiwygSupported) { | |
9084 /*eslint no-new: off*/ | |
9085 (new SCEditor(textarea, options)); | |
9086 } | |
9087 }, | |
9088 instance: function (textarea) { | |
9089 return textarea._sceditor; | |
9090 } | |
9091 }; | |
9092 | |
9093 /** | |
9094 * SCEditor | |
9095 * http://www.sceditor.com/ | |
9096 * | |
9097 * Copyright (C) 2017, Sam Clarke (samclarke.com) | |
9098 * | |
9099 * SCEditor is licensed under the MIT license: | |
9100 * http://www.opensource.org/licenses/mit-license.php | |
9101 * | |
9102 * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor | |
9103 * @author Sam Clarke | |
9104 * @requires jQuery | |
9105 */ | |
9106 | |
9107 | |
9108 // For backwards compatibility | |
9109 $__default['default'].sceditor = window.sceditor; | |
9110 | |
9111 /** | |
9112 * Creates an instance of sceditor on all textareas | |
9113 * matched by the jQuery selector. | |
9114 * | |
9115 * If options is set to "state" it will return bool value | |
9116 * indicating if the editor has been initialised on the | |
9117 * matched textarea(s). If there is only one textarea | |
9118 * it will return the bool value for that textarea. | |
9119 * If more than one textarea is matched it will | |
9120 * return an array of bool values for each textarea. | |
9121 * | |
9122 * If options is set to "instance" it will return the | |
9123 * current editor instance for the textarea(s). Like the | |
9124 * state option, if only one textarea is matched this will | |
9125 * return just the instance for that textarea. If more than | |
9126 * one textarea is matched it will return an array of | |
9127 * instances each textarea. | |
9128 * | |
9129 * @param {Object|string} [options] Should either be an Object of options or | |
9130 * the strings "state" or "instance" | |
9131 * @return {this|Array<SCEditor>|Array<boolean>|SCEditor|boolean} | |
9132 */ | |
9133 $__default['default'].fn.sceditor = function (options) { | |
9134 var instance; | |
9135 var ret = []; | |
9136 | |
9137 this.each(function () { | |
9138 instance = this._sceditor; | |
9139 | |
9140 // Add state of instance to ret if that is what options is set to | |
9141 if (options === 'state') { | |
9142 ret.push(!!instance); | |
9143 } else if (options === 'instance') { | |
9144 ret.push(instance); | |
9145 } else if (!instance) { | |
9146 $__default['default'].sceditor.create(this, options); | |
9147 } | |
9148 }); | |
9149 | |
9150 // If nothing in the ret array then must be init so return this | |
9151 if (!ret.length) { | |
9152 return this; | |
9153 } | |
9154 | |
9155 return ret.length === 1 ? ret[0] : ret; | |
9156 }; | |
9157 | |
9158 }(jQuery)); | |
9159 ;/** | |
9160 * SCEditor BBCode Plugin | |
9161 * http://www.sceditor.com/ | |
9162 * | |
9163 * Copyright (C) 2011-2017, Sam Clarke (samclarke.com) | |
9164 * | |
9165 * SCEditor is licensed under the MIT license: | |
9166 * http://www.opensource.org/licenses/mit-license.php | |
9167 * | |
9168 * @fileoverview SCEditor BBCode Format | |
9169 * @author Sam Clarke | |
9170 */ | |
9171 (function (sceditor) { | |
9172 /*eslint max-depth: off*/ | |
9173 'use strict'; | |
9174 | |
9175 var escapeEntities = sceditor.escapeEntities; | |
9176 var escapeUriScheme = sceditor.escapeUriScheme; | |
9177 var dom = sceditor.dom; | |
9178 var utils = sceditor.utils; | |
9179 | |
9180 var css = dom.css; | |
9181 var attr = dom.attr; | |
9182 var is = dom.is; | |
9183 var extend = utils.extend; | |
9184 var each = utils.each; | |
9185 | |
9186 var EMOTICON_DATA_ATTR = 'data-sceditor-emoticon'; | |
9187 | |
9188 var getEditorCommand = sceditor.command.get; | |
9189 | |
9190 var QuoteType = { | |
9191 /** @lends BBCodeParser.QuoteType */ | |
9192 /** | |
9193 * Always quote the attribute value | |
9194 * @type {Number} | |
9195 */ | |
9196 always: 1, | |
9197 | |
9198 /** | |
9199 * Never quote the attributes value | |
9200 * @type {Number} | |
9201 */ | |
9202 never: 2, | |
9203 | |
9204 /** | |
9205 * Only quote the attributes value when it contains spaces to equals | |
9206 * @type {Number} | |
9207 */ | |
9208 auto: 3 | |
9209 }; | |
9210 | |
9211 var defaultCommandsOverrides = { | |
9212 bold: { | |
9213 txtExec: ['[b]', '[/b]'] | |
9214 }, | |
9215 italic: { | |
9216 txtExec: ['[i]', '[/i]'] | |
9217 }, | |
9218 underline: { | |
9219 txtExec: ['[u]', '[/u]'] | |
9220 }, | |
9221 strike: { | |
9222 txtExec: ['[s]', '[/s]'] | |
9223 }, | |
9224 subscript: { | |
9225 txtExec: ['[sub]', '[/sub]'] | |
9226 }, | |
9227 superscript: { | |
9228 txtExec: ['[sup]', '[/sup]'] | |
9229 }, | |
9230 left: { | |
9231 txtExec: ['[left]', '[/left]'] | |
9232 }, | |
9233 center: { | |
9234 txtExec: ['[center]', '[/center]'] | |
9235 }, | |
9236 right: { | |
9237 txtExec: ['[right]', '[/right]'] | |
9238 }, | |
9239 justify: { | |
9240 txtExec: ['[justify]', '[/justify]'] | |
9241 }, | |
9242 font: { | |
9243 txtExec: function (caller) { | |
9244 var editor = this; | |
9245 | |
9246 getEditorCommand('font')._dropDown( | |
9247 editor, | |
9248 caller, | |
9249 function (fontName) { | |
9250 editor.insertText( | |
9251 '[font=' + fontName + ']', | |
9252 '[/font]' | |
9253 ); | |
9254 } | |
9255 ); | |
9256 } | |
9257 }, | |
9258 size: { | |
9259 txtExec: function (caller) { | |
9260 var editor = this; | |
9261 | |
9262 getEditorCommand('size')._dropDown( | |
9263 editor, | |
9264 caller, | |
9265 function (fontSize) { | |
9266 editor.insertText( | |
9267 '[size=' + fontSize + ']', | |
9268 '[/size]' | |
9269 ); | |
9270 } | |
9271 ); | |
9272 } | |
9273 }, | |
9274 color: { | |
9275 txtExec: function (caller) { | |
9276 var editor = this; | |
9277 | |
9278 getEditorCommand('color')._dropDown( | |
9279 editor, | |
9280 caller, | |
9281 function (color) { | |
9282 editor.insertText( | |
9283 '[color=' + color + ']', | |
9284 '[/color]' | |
9285 ); | |
9286 } | |
9287 ); | |
9288 } | |
9289 }, | |
9290 bulletlist: { | |
9291 txtExec: function (caller, selected) { | |
9292 this.insertText( | |
9293 '[ul]\n[li]' + | |
9294 selected.split(/\r?\n/).join('[/li]\n[li]') + | |
9295 '[/li]\n[/ul]' | |
9296 ); | |
9297 } | |
9298 }, | |
9299 orderedlist: { | |
9300 txtExec: function (caller, selected) { | |
9301 this.insertText( | |
9302 '[ol]\n[li]' + | |
9303 selected.split(/\r?\n/).join('[/li]\n[li]') + | |
9304 '[/li]\n[/ol]' | |
9305 ); | |
9306 } | |
9307 }, | |
9308 table: { | |
9309 txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] | |
9310 }, | |
9311 horizontalrule: { | |
9312 txtExec: ['[hr]'] | |
9313 }, | |
9314 code: { | |
9315 txtExec: ['[code]', '[/code]'] | |
9316 }, | |
9317 image: { | |
9318 txtExec: function (caller, selected) { | |
9319 var editor = this; | |
9320 | |
9321 getEditorCommand('image')._dropDown( | |
9322 editor, | |
9323 caller, | |
9324 selected, | |
9325 function (url, width, height) { | |
9326 var attrs = ''; | |
9327 | |
9328 if (width) { | |
9329 attrs += ' width=' + width; | |
9330 } | |
9331 | |
9332 if (height) { | |
9333 attrs += ' height=' + height; | |
9334 } | |
9335 | |
9336 editor.insertText( | |
9337 '[img' + attrs + ']' + url + '[/img]' | |
9338 ); | |
9339 } | |
9340 ); | |
9341 } | |
9342 }, | |
9343 email: { | |
9344 txtExec: function (caller, selected) { | |
9345 var editor = this; | |
9346 | |
9347 getEditorCommand('email')._dropDown( | |
9348 editor, | |
9349 caller, | |
9350 function (url, text) { | |
9351 editor.insertText( | |
9352 '[email=' + url + ']' + | |
9353 (text || selected || url) + | |
9354 '[/email]' | |
9355 ); | |
9356 } | |
9357 ); | |
9358 } | |
9359 }, | |
9360 link: { | |
9361 txtExec: function (caller, selected) { | |
9362 var editor = this; | |
9363 | |
9364 getEditorCommand('link')._dropDown( | |
9365 editor, | |
9366 caller, | |
9367 function (url, text) { | |
9368 editor.insertText( | |
9369 '[url=' + url + ']' + | |
9370 (text || selected || url) + | |
9371 '[/url]' | |
9372 ); | |
9373 } | |
9374 ); | |
9375 } | |
9376 }, | |
9377 quote: { | |
9378 txtExec: ['[quote]', '[/quote]'] | |
9379 }, | |
9380 youtube: { | |
9381 txtExec: function (caller) { | |
9382 var editor = this; | |
9383 | |
9384 getEditorCommand('youtube')._dropDown( | |
9385 editor, | |
9386 caller, | |
9387 function (id) { | |
9388 editor.insertText('[youtube]' + id + '[/youtube]'); | |
9389 } | |
9390 ); | |
9391 } | |
9392 }, | |
9393 rtl: { | |
9394 txtExec: ['[rtl]', '[/rtl]'] | |
9395 }, | |
9396 ltr: { | |
9397 txtExec: ['[ltr]', '[/ltr]'] | |
9398 } | |
9399 }; | |
9400 | |
9401 var bbcodeHandlers = { | |
9402 // START_COMMAND: Bold | |
9403 b: { | |
9404 tags: { | |
9405 b: null, | |
9406 strong: null | |
9407 }, | |
9408 styles: { | |
9409 // 401 is for FF 3.5 | |
9410 'font-weight': ['bold', 'bolder', '401', '700', '800', '900'] | |
9411 }, | |
9412 format: '[b]{0}[/b]', | |
9413 html: '<strong>{0}</strong>' | |
9414 }, | |
9415 // END_COMMAND | |
9416 | |
9417 // START_COMMAND: Italic | |
9418 i: { | |
9419 tags: { | |
9420 i: null, | |
9421 em: null | |
9422 }, | |
9423 styles: { | |
9424 'font-style': ['italic', 'oblique'] | |
9425 }, | |
9426 format: '[i]{0}[/i]', | |
9427 html: '<em>{0}</em>' | |
9428 }, | |
9429 // END_COMMAND | |
9430 | |
9431 // START_COMMAND: Underline | |
9432 u: { | |
9433 tags: { | |
9434 u: null | |
9435 }, | |
9436 styles: { | |
9437 'text-decoration': ['underline'] | |
9438 }, | |
9439 format: '[u]{0}[/u]', | |
9440 html: '<u>{0}</u>' | |
9441 }, | |
9442 // END_COMMAND | |
9443 | |
9444 // START_COMMAND: Strikethrough | |
9445 s: { | |
9446 tags: { | |
9447 s: null, | |
9448 strike: null | |
9449 }, | |
9450 styles: { | |
9451 'text-decoration': ['line-through'] | |
9452 }, | |
9453 format: '[s]{0}[/s]', | |
9454 html: '<s>{0}</s>' | |
9455 }, | |
9456 // END_COMMAND | |
9457 | |
9458 // START_COMMAND: Subscript | |
9459 sub: { | |
9460 tags: { | |
9461 sub: null | |
9462 }, | |
9463 format: '[sub]{0}[/sub]', | |
9464 html: '<sub>{0}</sub>' | |
9465 }, | |
9466 // END_COMMAND | |
9467 | |
9468 // START_COMMAND: Superscript | |
9469 sup: { | |
9470 tags: { | |
9471 sup: null | |
9472 }, | |
9473 format: '[sup]{0}[/sup]', | |
9474 html: '<sup>{0}</sup>' | |
9475 }, | |
9476 // END_COMMAND | |
9477 | |
9478 // START_COMMAND: Font | |
9479 font: { | |
9480 tags: { | |
9481 font: { | |
9482 face: null | |
9483 } | |
9484 }, | |
9485 styles: { | |
9486 'font-family': null | |
9487 }, | |
9488 quoteType: QuoteType.never, | |
9489 format: function (element, content) { | |
9490 var font; | |
9491 | |
9492 if (!is(element, 'font') || !(font = attr(element, 'face'))) { | |
9493 font = css(element, 'font-family'); | |
9494 } | |
9495 | |
9496 return '[font=' + _stripQuotes(font) + ']' + | |
9497 content + '[/font]'; | |
9498 }, | |
9499 html: '<font face="{defaultattr}">{0}</font>' | |
9500 }, | |
9501 // END_COMMAND | |
9502 | |
9503 // START_COMMAND: Size | |
9504 size: { | |
9505 tags: { | |
9506 font: { | |
9507 size: null | |
9508 } | |
9509 }, | |
9510 styles: { | |
9511 'font-size': null | |
9512 }, | |
9513 format: function (element, content) { | |
9514 var fontSize = attr(element, 'size'), | |
9515 size = 2; | |
9516 | |
9517 if (!fontSize) { | |
9518 fontSize = css(element, 'fontSize'); | |
9519 } | |
9520 | |
9521 // Most browsers return px value but IE returns 1-7 | |
9522 if (fontSize.indexOf('px') > -1) { | |
9523 // convert size to an int | |
9524 fontSize = fontSize.replace('px', '') - 0; | |
9525 | |
9526 if (fontSize < 12) { | |
9527 size = 1; | |
9528 } | |
9529 if (fontSize > 15) { | |
9530 size = 3; | |
9531 } | |
9532 if (fontSize > 17) { | |
9533 size = 4; | |
9534 } | |
9535 if (fontSize > 23) { | |
9536 size = 5; | |
9537 } | |
9538 if (fontSize > 31) { | |
9539 size = 6; | |
9540 } | |
9541 if (fontSize > 47) { | |
9542 size = 7; | |
9543 } | |
9544 } else { | |
9545 size = fontSize; | |
9546 } | |
9547 | |
9548 return '[size=' + size + ']' + content + '[/size]'; | |
9549 }, | |
9550 html: '<font size="{defaultattr}">{!0}</font>' | |
9551 }, | |
9552 // END_COMMAND | |
9553 | |
9554 // START_COMMAND: Color | |
9555 color: { | |
9556 tags: { | |
9557 font: { | |
9558 color: null | |
9559 } | |
9560 }, | |
9561 styles: { | |
9562 color: null | |
9563 }, | |
9564 quoteType: QuoteType.never, | |
9565 format: function (elm, content) { | |
9566 var color; | |
9567 | |
9568 if (!is(elm, 'font') || !(color = attr(elm, 'color'))) { | |
9569 color = elm.style.color || css(elm, 'color'); | |
9570 } | |
9571 | |
9572 return '[color=' + _normaliseColour(color) + ']' + | |
9573 content + '[/color]'; | |
9574 }, | |
9575 html: function (token, attrs, content) { | |
9576 return '<font color="' + | |
9577 escapeEntities(_normaliseColour(attrs.defaultattr), true) + | |
9578 '">' + content + '</font>'; | |
9579 } | |
9580 }, | |
9581 // END_COMMAND | |
9582 | |
9583 // START_COMMAND: Lists | |
9584 ul: { | |
9585 tags: { | |
9586 ul: null | |
9587 }, | |
9588 breakStart: true, | |
9589 isInline: false, | |
9590 skipLastLineBreak: true, | |
9591 format: '[ul]{0}[/ul]', | |
9592 html: '<ul>{0}</ul>' | |
9593 }, | |
9594 list: { | |
9595 breakStart: true, | |
9596 isInline: false, | |
9597 skipLastLineBreak: true, | |
9598 html: '<ul>{0}</ul>' | |
9599 }, | |
9600 ol: { | |
9601 tags: { | |
9602 ol: null | |
9603 }, | |
9604 breakStart: true, | |
9605 isInline: false, | |
9606 skipLastLineBreak: true, | |
9607 format: '[ol]{0}[/ol]', | |
9608 html: '<ol>{0}</ol>' | |
9609 }, | |
9610 li: { | |
9611 tags: { | |
9612 li: null | |
9613 }, | |
9614 isInline: false, | |
9615 closedBy: ['/ul', '/ol', '/list', '*', 'li'], | |
9616 format: '[li]{0}[/li]', | |
9617 html: '<li>{0}</li>' | |
9618 }, | |
9619 '*': { | |
9620 isInline: false, | |
9621 closedBy: ['/ul', '/ol', '/list', '*', 'li'], | |
9622 html: '<li>{0}</li>' | |
9623 }, | |
9624 // END_COMMAND | |
9625 | |
9626 // START_COMMAND: Table | |
9627 table: { | |
9628 tags: { | |
9629 table: null | |
9630 }, | |
9631 isInline: false, | |
9632 isHtmlInline: true, | |
9633 skipLastLineBreak: true, | |
9634 format: '[table]{0}[/table]', | |
9635 html: '<table>{0}</table>' | |
9636 }, | |
9637 tr: { | |
9638 tags: { | |
9639 tr: null | |
9640 }, | |
9641 isInline: false, | |
9642 skipLastLineBreak: true, | |
9643 format: '[tr]{0}[/tr]', | |
9644 html: '<tr>{0}</tr>' | |
9645 }, | |
9646 th: { | |
9647 tags: { | |
9648 th: null | |
9649 }, | |
9650 allowsEmpty: true, | |
9651 isInline: false, | |
9652 format: '[th]{0}[/th]', | |
9653 html: '<th>{0}</th>' | |
9654 }, | |
9655 td: { | |
9656 tags: { | |
9657 td: null | |
9658 }, | |
9659 allowsEmpty: true, | |
9660 isInline: false, | |
9661 format: '[td]{0}[/td]', | |
9662 html: '<td>{0}</td>' | |
9663 }, | |
9664 // END_COMMAND | |
9665 | |
9666 // START_COMMAND: Emoticons | |
9667 emoticon: { | |
9668 allowsEmpty: true, | |
9669 tags: { | |
9670 img: { | |
9671 src: null, | |
9672 'data-sceditor-emoticon': null | |
9673 } | |
9674 }, | |
9675 format: function (element, content) { | |
9676 return attr(element, EMOTICON_DATA_ATTR) + content; | |
9677 }, | |
9678 html: '{0}' | |
9679 }, | |
9680 // END_COMMAND | |
9681 | |
9682 // START_COMMAND: Horizontal Rule | |
9683 hr: { | |
9684 tags: { | |
9685 hr: null | |
9686 }, | |
9687 allowsEmpty: true, | |
9688 isSelfClosing: true, | |
9689 isInline: false, | |
9690 format: '[hr]{0}', | |
9691 html: '<hr />' | |
9692 }, | |
9693 // END_COMMAND | |
9694 | |
9695 // START_COMMAND: Image | |
9696 img: { | |
9697 allowsEmpty: true, | |
9698 tags: { | |
9699 img: { | |
9700 src: null | |
9701 } | |
9702 }, | |
9703 allowedChildren: ['#'], | |
9704 quoteType: QuoteType.never, | |
9705 format: function (element, content) { | |
9706 var width, height, | |
9707 attribs = '', | |
9708 style = function (name) { | |
9709 return element.style ? element.style[name] : null; | |
9710 }; | |
9711 | |
9712 // check if this is an emoticon image | |
9713 if (attr(element, EMOTICON_DATA_ATTR)) { | |
9714 return content; | |
9715 } | |
9716 | |
9717 width = attr(element, 'width') || style('width'); | |
9718 height = attr(element, 'height') || style('height'); | |
9719 | |
9720 // only add width and height if one is specified | |
9721 if ((element.complete && (width || height)) || | |
9722 (width && height)) { | |
9723 | |
9724 attribs = '=' + dom.width(element) + 'x' + | |
9725 dom.height(element); | |
9726 } | |
9727 | |
9728 return '[img' + attribs + ']' + attr(element, 'src') + '[/img]'; | |
9729 }, | |
9730 html: function (token, attrs, content) { | |
9731 var undef, width, height, match, | |
9732 attribs = ''; | |
9733 | |
9734 // handle [img width=340 height=240]url[/img] | |
9735 width = attrs.width; | |
9736 height = attrs.height; | |
9737 | |
9738 // handle [img=340x240]url[/img] | |
9739 if (attrs.defaultattr) { | |
9740 match = attrs.defaultattr.split(/x/i); | |
9741 | |
9742 width = match[0]; | |
9743 height = (match.length === 2 ? match[1] : match[0]); | |
9744 } | |
9745 | |
9746 if (width !== undef) { | |
9747 attribs += ' width="' + escapeEntities(width, true) + '"'; | |
9748 } | |
9749 | |
9750 if (height !== undef) { | |
9751 attribs += ' height="' + escapeEntities(height, true) + '"'; | |
9752 } | |
9753 | |
9754 return '<img' + attribs + | |
9755 ' src="' + escapeUriScheme(content) + '" />'; | |
9756 } | |
9757 }, | |
9758 // END_COMMAND | |
9759 | |
9760 // START_COMMAND: URL | |
9761 url: { | |
9762 allowsEmpty: true, | |
9763 tags: { | |
9764 a: { | |
9765 href: null | |
9766 } | |
9767 }, | |
9768 quoteType: QuoteType.never, | |
9769 format: function (element, content) { | |
9770 var url = attr(element, 'href'); | |
9771 | |
9772 // make sure this link is not an e-mail, | |
9773 // if it is return e-mail BBCode | |
9774 if (url.substr(0, 7) === 'mailto:') { | |
9775 return '[email="' + url.substr(7) + '"]' + | |
9776 content + '[/email]'; | |
9777 } | |
9778 | |
9779 return '[url=' + url + ']' + content + '[/url]'; | |
9780 }, | |
9781 html: function (token, attrs, content) { | |
9782 attrs.defaultattr = | |
9783 escapeEntities(attrs.defaultattr, true) || content; | |
9784 | |
9785 return '<a href="' + escapeUriScheme(attrs.defaultattr) + '">' + | |
9786 content + '</a>'; | |
9787 } | |
9788 }, | |
9789 // END_COMMAND | |
9790 | |
9791 // START_COMMAND: E-mail | |
9792 email: { | |
9793 quoteType: QuoteType.never, | |
9794 html: function (token, attrs, content) { | |
9795 return '<a href="mailto:' + | |
9796 (escapeEntities(attrs.defaultattr, true) || content) + | |
9797 '">' + content + '</a>'; | |
9798 } | |
9799 }, | |
9800 // END_COMMAND | |
9801 | |
9802 // START_COMMAND: Quote | |
9803 quote: { | |
9804 tags: { | |
9805 blockquote: null | |
9806 }, | |
9807 isInline: false, | |
9808 quoteType: QuoteType.never, | |
9809 format: function (element, content) { | |
9810 var authorAttr = 'data-author'; | |
9811 var author = ''; | |
9812 var cite; | |
9813 var children = element.children; | |
9814 | |
9815 for (var i = 0; !cite && i < children.length; i++) { | |
9816 if (is(children[i], 'cite')) { | |
9817 cite = children[i]; | |
9818 } | |
9819 } | |
9820 | |
9821 if (cite || attr(element, authorAttr)) { | |
9822 author = cite && cite.textContent || | |
9823 attr(element, authorAttr); | |
9824 | |
9825 attr(element, authorAttr, author); | |
9826 | |
9827 if (cite) { | |
9828 element.removeChild(cite); | |
9829 } | |
9830 | |
9831 content = this.elementToBbcode(element); | |
9832 author = '=' + author.replace(/(^\s+|\s+$)/g, ''); | |
9833 | |
9834 if (cite) { | |
9835 element.insertBefore(cite, element.firstChild); | |
9836 } | |
9837 } | |
9838 | |
9839 return '[quote' + author + ']' + content + '[/quote]'; | |
9840 }, | |
9841 html: function (token, attrs, content) { | |
9842 if (attrs.defaultattr) { | |
9843 content = '<cite>' + escapeEntities(attrs.defaultattr) + | |
9844 '</cite>' + content; | |
9845 } | |
9846 | |
9847 return '<blockquote>' + content + '</blockquote>'; | |
9848 } | |
9849 }, | |
9850 // END_COMMAND | |
9851 | |
9852 // START_COMMAND: Code | |
9853 code: { | |
9854 tags: { | |
9855 code: null | |
9856 }, | |
9857 isInline: false, | |
9858 allowedChildren: ['#', '#newline'], | |
9859 format: '[code]{0}[/code]', | |
9860 html: '<code>{0}</code>' | |
9861 }, | |
9862 // END_COMMAND | |
9863 | |
9864 | |
9865 // START_COMMAND: Left | |
9866 left: { | |
9867 styles: { | |
9868 'text-align': [ | |
9869 'left', | |
9870 '-webkit-left', | |
9871 '-moz-left', | |
9872 '-khtml-left' | |
9873 ] | |
9874 }, | |
9875 isInline: false, | |
9876 allowsEmpty: true, | |
9877 format: '[left]{0}[/left]', | |
9878 html: '<div align="left">{0}</div>' | |
9879 }, | |
9880 // END_COMMAND | |
9881 | |
9882 // START_COMMAND: Centre | |
9883 center: { | |
9884 styles: { | |
9885 'text-align': [ | |
9886 'center', | |
9887 '-webkit-center', | |
9888 '-moz-center', | |
9889 '-khtml-center' | |
9890 ] | |
9891 }, | |
9892 isInline: false, | |
9893 allowsEmpty: true, | |
9894 format: '[center]{0}[/center]', | |
9895 html: '<div align="center">{0}</div>' | |
9896 }, | |
9897 // END_COMMAND | |
9898 | |
9899 // START_COMMAND: Right | |
9900 right: { | |
9901 styles: { | |
9902 'text-align': [ | |
9903 'right', | |
9904 '-webkit-right', | |
9905 '-moz-right', | |
9906 '-khtml-right' | |
9907 ] | |
9908 }, | |
9909 isInline: false, | |
9910 allowsEmpty: true, | |
9911 format: '[right]{0}[/right]', | |
9912 html: '<div align="right">{0}</div>' | |
9913 }, | |
9914 // END_COMMAND | |
9915 | |
9916 // START_COMMAND: Justify | |
9917 justify: { | |
9918 styles: { | |
9919 'text-align': [ | |
9920 'justify', | |
9921 '-webkit-justify', | |
9922 '-moz-justify', | |
9923 '-khtml-justify' | |
9924 ] | |
9925 }, | |
9926 isInline: false, | |
9927 allowsEmpty: true, | |
9928 format: '[justify]{0}[/justify]', | |
9929 html: '<div align="justify">{0}</div>' | |
9930 }, | |
9931 // END_COMMAND | |
9932 | |
9933 // START_COMMAND: YouTube | |
9934 youtube: { | |
9935 allowsEmpty: true, | |
9936 tags: { | |
9937 iframe: { | |
9938 'data-youtube-id': null | |
9939 } | |
9940 }, | |
9941 format: function (element, content) { | |
9942 element = attr(element, 'data-youtube-id'); | |
9943 | |
9944 return element ? '[youtube]' + element + '[/youtube]' : content; | |
9945 }, | |
9946 html: '<iframe width="560" height="315" frameborder="0" ' + | |
9947 'src="https://www.youtube-nocookie.com/embed/{0}?wmode=opaque" ' + | |
9948 'data-youtube-id="{0}" allowfullscreen></iframe>' | |
9949 }, | |
9950 // END_COMMAND | |
9951 | |
9952 | |
9953 // START_COMMAND: Rtl | |
9954 rtl: { | |
9955 styles: { | |
9956 direction: ['rtl'] | |
9957 }, | |
9958 isInline: false, | |
9959 format: '[rtl]{0}[/rtl]', | |
9960 html: '<div style="direction: rtl">{0}</div>' | |
9961 }, | |
9962 // END_COMMAND | |
9963 | |
9964 // START_COMMAND: Ltr | |
9965 ltr: { | |
9966 styles: { | |
9967 direction: ['ltr'] | |
9968 }, | |
9969 isInline: false, | |
9970 format: '[ltr]{0}[/ltr]', | |
9971 html: '<div style="direction: ltr">{0}</div>' | |
9972 }, | |
9973 // END_COMMAND | |
9974 | |
9975 // this is here so that commands above can be removed | |
9976 // without having to remove the , after the last one. | |
9977 // Needed for IE. | |
9978 ignore: {} | |
9979 }; | |
9980 | |
9981 /** | |
9982 * Formats a string replacing {name} with the values of | |
9983 * obj.name properties. | |
9984 * | |
9985 * If there is no property for the specified {name} then | |
9986 * it will be left intact. | |
9987 * | |
9988 * @param {string} str | |
9989 * @param {Object} obj | |
9990 * @return {string} | |
9991 * @since 2.0.0 | |
9992 */ | |
9993 function formatBBCodeString(str, obj) { | |
9994 return str.replace(/\{([^}]+)\}/g, function (match, group) { | |
9995 var undef, | |
9996 escape = true; | |
9997 | |
9998 if (group.charAt(0) === '!') { | |
9999 escape = false; | |
10000 group = group.substring(1); | |
10001 } | |
10002 | |
10003 if (group === '0') { | |
10004 escape = false; | |
10005 } | |
10006 | |
10007 if (obj[group] === undef) { | |
10008 return match; | |
10009 } | |
10010 | |
10011 return escape ? escapeEntities(obj[group], true) : obj[group]; | |
10012 }); | |
10013 } | |
10014 | |
10015 /** | |
10016 * Removes the first and last divs from the HTML. | |
10017 * | |
10018 * This is needed for pasting | |
10019 * @param {string} html | |
10020 * @return {string} | |
10021 * @private | |
10022 */ | |
10023 function removeFirstLastDiv(html) { | |
10024 var node, next, removeDiv, | |
10025 output = document.createElement('div'); | |
10026 | |
10027 removeDiv = function (node, isFirst) { | |
10028 // Don't remove divs that have styling | |
10029 if (dom.hasStyling(node)) { | |
10030 return; | |
10031 } | |
10032 | |
10033 if ((node.childNodes.length !== 1 || | |
10034 !is(node.firstChild, 'br'))) { | |
10035 while ((next = node.firstChild)) { | |
10036 output.insertBefore(next, node); | |
10037 } | |
10038 } | |
10039 | |
10040 if (isFirst) { | |
10041 var lastChild = output.lastChild; | |
10042 | |
10043 if (node !== lastChild && is(lastChild, 'div') && | |
10044 node.nextSibling === lastChild) { | |
10045 output.insertBefore(document.createElement('br'), node); | |
10046 } | |
10047 } | |
10048 | |
10049 output.removeChild(node); | |
10050 }; | |
10051 | |
10052 css(output, 'display', 'none'); | |
10053 output.innerHTML = html.replace(/<\/div>\n/g, '</div>'); | |
10054 | |
10055 if ((node = output.firstChild) && is(node, 'div')) { | |
10056 removeDiv(node, true); | |
10057 } | |
10058 | |
10059 if ((node = output.lastChild) && is(node, 'div')) { | |
10060 removeDiv(node); | |
10061 } | |
10062 | |
10063 return output.innerHTML; | |
10064 } | |
10065 | |
10066 function isFunction(fn) { | |
10067 return typeof fn === 'function'; | |
10068 } | |
10069 | |
10070 /** | |
10071 * Removes any leading or trailing quotes ('") | |
10072 * | |
10073 * @return string | |
10074 * @since v1.4.0 | |
10075 */ | |
10076 function _stripQuotes(str) { | |
10077 return str ? | |
10078 str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str; | |
10079 } | |
10080 | |
10081 /** | |
10082 * Formats a string replacing {0}, {1}, {2}, ect. with | |
10083 * the params provided | |
10084 * | |
10085 * @param {string} str The string to format | |
10086 * @param {...string} arg The strings to replace | |
10087 * @return {string} | |
10088 * @since v1.4.0 | |
10089 */ | |
10090 function _formatString(str) { | |
10091 var undef; | |
10092 var args = arguments; | |
10093 | |
10094 return str.replace(/\{(\d+)\}/g, function (_, matchNum) { | |
10095 return args[matchNum - 0 + 1] !== undef ? | |
10096 args[matchNum - 0 + 1] : | |
10097 '{' + matchNum + '}'; | |
10098 }); | |
10099 } | |
10100 | |
10101 var TOKEN_OPEN = 'open'; | |
10102 var TOKEN_CONTENT = 'content'; | |
10103 var TOKEN_NEWLINE = 'newline'; | |
10104 var TOKEN_CLOSE = 'close'; | |
10105 | |
10106 | |
10107 /* | |
10108 * @typedef {Object} TokenizeToken | |
10109 * @property {string} type | |
10110 * @property {string} name | |
10111 * @property {string} val | |
10112 * @property {Object.<string, string>} attrs | |
10113 * @property {array} children | |
10114 * @property {TokenizeToken} closing | |
10115 */ | |
10116 | |
10117 /** | |
10118 * Tokenize token object | |
10119 * | |
10120 * @param {string} type The type of token this is, | |
10121 * should be one of tokenType | |
10122 * @param {string} name The name of this token | |
10123 * @param {string} val The originally matched string | |
10124 * @param {array} attrs Any attributes. Only set on | |
10125 * TOKEN_TYPE_OPEN tokens | |
10126 * @param {array} children Any children of this token | |
10127 * @param {TokenizeToken} closing This tokens closing tag. | |
10128 * Only set on TOKEN_TYPE_OPEN tokens | |
10129 * @class {TokenizeToken} | |
10130 * @name {TokenizeToken} | |
10131 * @memberOf BBCodeParser.prototype | |
10132 */ | |
10133 // eslint-disable-next-line max-params | |
10134 function TokenizeToken(type, name, val, attrs, children, closing) { | |
10135 var base = this; | |
10136 | |
10137 base.type = type; | |
10138 base.name = name; | |
10139 base.val = val; | |
10140 base.attrs = attrs || {}; | |
10141 base.children = children || []; | |
10142 base.closing = closing || null; | |
10143 }; | |
10144 | |
10145 TokenizeToken.prototype = { | |
10146 /** @lends BBCodeParser.prototype.TokenizeToken */ | |
10147 /** | |
10148 * Clones this token | |
10149 * | |
10150 * @return {TokenizeToken} | |
10151 */ | |
10152 clone: function () { | |
10153 var base = this; | |
10154 | |
10155 return new TokenizeToken( | |
10156 base.type, | |
10157 base.name, | |
10158 base.val, | |
10159 extend({}, base.attrs), | |
10160 [], | |
10161 base.closing ? base.closing.clone() : null | |
10162 ); | |
10163 }, | |
10164 /** | |
10165 * Splits this token at the specified child | |
10166 * | |
10167 * @param {TokenizeToken} splitAt The child to split at | |
10168 * @return {TokenizeToken} The right half of the split token or | |
10169 * empty clone if invalid splitAt lcoation | |
10170 */ | |
10171 splitAt: function (splitAt) { | |
10172 var offsetLength; | |
10173 var base = this; | |
10174 var clone = base.clone(); | |
10175 var offset = base.children.indexOf(splitAt); | |
10176 | |
10177 if (offset > -1) { | |
10178 // Work out how many items are on the right side of the split | |
10179 // to pass to splice() | |
10180 offsetLength = base.children.length - offset; | |
10181 clone.children = base.children.splice(offset, offsetLength); | |
10182 } | |
10183 | |
10184 return clone; | |
10185 } | |
10186 }; | |
10187 | |
10188 | |
10189 /** | |
10190 * SCEditor BBCode parser class | |
10191 * | |
10192 * @param {Object} options | |
10193 * @class BBCodeParser | |
10194 * @name BBCodeParser | |
10195 * @since v1.4.0 | |
10196 */ | |
10197 function BBCodeParser(options) { | |
10198 var base = this; | |
10199 | |
10200 base.opts = extend({}, BBCodeParser.defaults, options); | |
10201 | |
10202 /** | |
10203 * Takes a BBCode string and splits it into open, | |
10204 * content and close tags. | |
10205 * | |
10206 * It does no checking to verify a tag has a matching open | |
10207 * or closing tag or if the tag is valid child of any tag | |
10208 * before it. For that the tokens should be passed to the | |
10209 * parse function. | |
10210 * | |
10211 * @param {string} str | |
10212 * @return {array} | |
10213 * @memberOf BBCodeParser.prototype | |
10214 */ | |
10215 base.tokenize = function (str) { | |
10216 var matches, type, i; | |
10217 var tokens = []; | |
10218 // The token types in reverse order of precedence | |
10219 // (they're looped in reverse) | |
10220 var tokenTypes = [ | |
10221 { | |
10222 type: TOKEN_CONTENT, | |
10223 regex: /^([^\[\r\n]+|\[)/ | |
10224 }, | |
10225 { | |
10226 type: TOKEN_NEWLINE, | |
10227 regex: /^(\r\n|\r|\n)/ | |
10228 }, | |
10229 { | |
10230 type: TOKEN_OPEN, | |
10231 regex: /^\[[^\[\]]+\]/ | |
10232 }, | |
10233 // Close must come before open as they are | |
10234 // the same except close has a / at the start. | |
10235 { | |
10236 type: TOKEN_CLOSE, | |
10237 regex: /^\[\/[^\[\]]+\]/ | |
10238 } | |
10239 ]; | |
10240 | |
10241 strloop: | |
10242 while (str.length) { | |
10243 i = tokenTypes.length; | |
10244 while (i--) { | |
10245 type = tokenTypes[i].type; | |
10246 | |
10247 // Check if the string matches any of the tokens | |
10248 if (!(matches = str.match(tokenTypes[i].regex)) || | |
10249 !matches[0]) { | |
10250 continue; | |
10251 } | |
10252 | |
10253 // Add the match to the tokens list | |
10254 tokens.push(tokenizeTag(type, matches[0])); | |
10255 | |
10256 // Remove the match from the string | |
10257 str = str.substr(matches[0].length); | |
10258 | |
10259 // The token has been added so start again | |
10260 continue strloop; | |
10261 } | |
10262 | |
10263 // If there is anything left in the string which doesn't match | |
10264 // any of the tokens then just assume it's content and add it. | |
10265 if (str.length) { | |
10266 tokens.push(tokenizeTag(TOKEN_CONTENT, str)); | |
10267 } | |
10268 | |
10269 str = ''; | |
10270 } | |
10271 | |
10272 return tokens; | |
10273 }; | |
10274 | |
10275 /** | |
10276 * Extracts the name an params from a tag | |
10277 * | |
10278 * @param {string} type | |
10279 * @param {string} val | |
10280 * @return {Object} | |
10281 * @private | |
10282 */ | |
10283 function tokenizeTag(type, val) { | |
10284 var matches, attrs, name, | |
10285 openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/, | |
10286 closeRegex = /\[\/([^\[\]]+)\]/; | |
10287 | |
10288 // Extract the name and attributes from opening tags and | |
10289 // just the name from closing tags. | |
10290 if (type === TOKEN_OPEN && (matches = val.match(openRegex))) { | |
10291 name = lower(matches[1]); | |
10292 | |
10293 if (matches[2] && (matches[2] = matches[2].trim())) { | |
10294 attrs = tokenizeAttrs(matches[2]); | |
10295 } | |
10296 } | |
10297 | |
10298 if (type === TOKEN_CLOSE && | |
10299 (matches = val.match(closeRegex))) { | |
10300 name = lower(matches[1]); | |
10301 } | |
10302 | |
10303 if (type === TOKEN_NEWLINE) { | |
10304 name = '#newline'; | |
10305 } | |
10306 | |
10307 // Treat all tokens without a name and | |
10308 // all unknown BBCodes as content | |
10309 if (!name || ((type === TOKEN_OPEN || type === TOKEN_CLOSE) && | |
10310 !bbcodeHandlers[name])) { | |
10311 | |
10312 type = TOKEN_CONTENT; | |
10313 name = '#'; | |
10314 } | |
10315 | |
10316 return new TokenizeToken(type, name, val, attrs); | |
10317 } | |
10318 | |
10319 /** | |
10320 * Extracts the individual attributes from a string containing | |
10321 * all the attributes. | |
10322 * | |
10323 * @param {string} attrs | |
10324 * @return {Object} Assoc array of attributes | |
10325 * @private | |
10326 */ | |
10327 function tokenizeAttrs(attrs) { | |
10328 var matches, | |
10329 /* | |
10330 ([^\s=]+) Anything that's not a space or equals | |
10331 = Equals sign = | |
10332 (?: | |
10333 (?: | |
10334 (["']) The opening quote | |
10335 ( | |
10336 (?:\\\2|[^\2])*? Anything that isn't the | |
10337 unescaped opening quote | |
10338 ) | |
10339 \2 The opening quote again which | |
10340 will close the string | |
10341 ) | |
10342 | If not a quoted string then match | |
10343 ( | |
10344 (?:.(?!\s\S+=))*.? Anything that isn't part of | |
10345 [space][non-space][=] which | |
10346 would be a new attribute | |
10347 ) | |
10348 ) | |
10349 */ | |
10350 attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g, | |
10351 ret = {}; | |
10352 | |
10353 // if only one attribute then remove the = from the start and | |
10354 // strip any quotes | |
10355 if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) { | |
10356 ret.defaultattr = _stripQuotes(attrs.substr(1)); | |
10357 } else { | |
10358 if (attrs.charAt(0) === '=') { | |
10359 attrs = 'defaultattr' + attrs; | |
10360 } | |
10361 | |
10362 // No need to strip quotes here, the regex will do that. | |
10363 while ((matches = attrRegex.exec(attrs))) { | |
10364 ret[lower(matches[1])] = | |
10365 _stripQuotes(matches[3]) || matches[4]; | |
10366 } | |
10367 } | |
10368 | |
10369 return ret; | |
10370 } | |
10371 | |
10372 /** | |
10373 * Parses a string into an array of BBCodes | |
10374 * | |
10375 * @param {string} str | |
10376 * @param {boolean} preserveNewLines If to preserve all new lines, not | |
10377 * strip any based on the passed | |
10378 * formatting options | |
10379 * @return {array} Array of BBCode objects | |
10380 * @memberOf BBCodeParser.prototype | |
10381 */ | |
10382 base.parse = function (str, preserveNewLines) { | |
10383 var ret = parseTokens(base.tokenize(str)); | |
10384 var opts = base.opts; | |
10385 | |
10386 if (opts.fixInvalidNesting) { | |
10387 fixNesting(ret); | |
10388 } | |
10389 | |
10390 normaliseNewLines(ret, null, preserveNewLines); | |
10391 | |
10392 if (opts.removeEmptyTags) { | |
10393 removeEmpty(ret); | |
10394 } | |
10395 | |
10396 return ret; | |
10397 }; | |
10398 | |
10399 /** | |
10400 * Checks if an array of TokenizeToken's contains the | |
10401 * specified token. | |
10402 * | |
10403 * Checks the tokens name and type match another tokens | |
10404 * name and type in the array. | |
10405 * | |
10406 * @param {string} name | |
10407 * @param {string} type | |
10408 * @param {array} arr | |
10409 * @return {Boolean} | |
10410 * @private | |
10411 */ | |
10412 function hasTag(name, type, arr) { | |
10413 var i = arr.length; | |
10414 | |
10415 while (i--) { | |
10416 if (arr[i].type === type && arr[i].name === name) { | |
10417 return true; | |
10418 } | |
10419 } | |
10420 | |
10421 return false; | |
10422 } | |
10423 | |
10424 /** | |
10425 * Checks if the child tag is allowed as one | |
10426 * of the parent tags children. | |
10427 * | |
10428 * @param {TokenizeToken} parent | |
10429 * @param {TokenizeToken} child | |
10430 * @return {Boolean} | |
10431 * @private | |
10432 */ | |
10433 function isChildAllowed(parent, child) { | |
10434 var parentBBCode = parent ? bbcodeHandlers[parent.name] : {}, | |
10435 allowedChildren = parentBBCode.allowedChildren; | |
10436 | |
10437 if (base.opts.fixInvalidChildren && allowedChildren) { | |
10438 return allowedChildren.indexOf(child.name || '#') > -1; | |
10439 } | |
10440 | |
10441 return true; | |
10442 } | |
10443 | |
10444 // TODO: Tidy this parseTokens() function up a bit. | |
10445 /** | |
10446 * Parses an array of tokens created by tokenize() | |
10447 * | |
10448 * @param {array} toks | |
10449 * @return {array} Parsed tokens | |
10450 * @see tokenize() | |
10451 * @private | |
10452 */ | |
10453 function parseTokens(toks) { | |
10454 var token, bbcode, curTok, clone, i, next, | |
10455 cloned = [], | |
10456 output = [], | |
10457 openTags = [], | |
10458 /** | |
10459 * Returns the currently open tag or undefined | |
10460 * @return {TokenizeToken} | |
10461 */ | |
10462 currentTag = function () { | |
10463 return last(openTags); | |
10464 }, | |
10465 /** | |
10466 * Adds a tag to either the current tags children | |
10467 * or to the output array. | |
10468 * @param {TokenizeToken} token | |
10469 * @private | |
10470 */ | |
10471 addTag = function (token) { | |
10472 if (currentTag()) { | |
10473 currentTag().children.push(token); | |
10474 } else { | |
10475 output.push(token); | |
10476 } | |
10477 }, | |
10478 /** | |
10479 * Checks if this tag closes the current tag | |
10480 * @param {string} name | |
10481 * @return {Void} | |
10482 */ | |
10483 closesCurrentTag = function (name) { | |
10484 return currentTag() && | |
10485 (bbcode = bbcodeHandlers[currentTag().name]) && | |
10486 bbcode.closedBy && | |
10487 bbcode.closedBy.indexOf(name) > -1; | |
10488 }; | |
10489 | |
10490 while ((token = toks.shift())) { | |
10491 next = toks[0]; | |
10492 | |
10493 /* | |
10494 * Fixes any invalid children. | |
10495 * | |
10496 * If it is an element which isn't allowed as a child of it's | |
10497 * parent then it will be converted to content of the parent | |
10498 * element. i.e. | |
10499 * [code]Code [b]only[/b] allows text.[/code] | |
10500 * Will become: | |
10501 * <code>Code [b]only[/b] allows text.</code> | |
10502 * Instead of: | |
10503 * <code>Code <b>only</b> allows text.</code> | |
10504 */ | |
10505 // Ignore tags that can't be children | |
10506 if (!isChildAllowed(currentTag(), token)) { | |
10507 | |
10508 // exclude closing tags of current tag | |
10509 if (token.type !== TOKEN_CLOSE || !currentTag() || | |
10510 token.name !== currentTag().name) { | |
10511 token.name = '#'; | |
10512 token.type = TOKEN_CONTENT; | |
10513 } | |
10514 } | |
10515 | |
10516 switch (token.type) { | |
10517 case TOKEN_OPEN: | |
10518 // Check it this closes a parent, | |
10519 // e.g. for lists [*]one [*]two | |
10520 if (closesCurrentTag(token.name)) { | |
10521 openTags.pop(); | |
10522 } | |
10523 | |
10524 addTag(token); | |
10525 bbcode = bbcodeHandlers[token.name]; | |
10526 | |
10527 // If this tag is not self closing and it has a closing | |
10528 // tag then it is open and has children so add it to the | |
10529 // list of open tags. If has the closedBy property then | |
10530 // it is closed by other tags so include everything as | |
10531 // it's children until one of those tags is reached. | |
10532 if (bbcode && !bbcode.isSelfClosing && | |
10533 (bbcode.closedBy || | |
10534 hasTag(token.name, TOKEN_CLOSE, toks))) { | |
10535 openTags.push(token); | |
10536 } else if (!bbcode || !bbcode.isSelfClosing) { | |
10537 token.type = TOKEN_CONTENT; | |
10538 } | |
10539 break; | |
10540 | |
10541 case TOKEN_CLOSE: | |
10542 // check if this closes the current tag, | |
10543 // e.g. [/list] would close an open [*] | |
10544 if (currentTag() && token.name !== currentTag().name && | |
10545 closesCurrentTag('/' + token.name)) { | |
10546 | |
10547 openTags.pop(); | |
10548 } | |
10549 | |
10550 // If this is closing the currently open tag just pop | |
10551 // the close tag off the open tags array | |
10552 if (currentTag() && token.name === currentTag().name) { | |
10553 currentTag().closing = token; | |
10554 openTags.pop(); | |
10555 | |
10556 // If this is closing an open tag that is the parent of | |
10557 // the current tag then clone all the tags including the | |
10558 // current one until reaching the parent that is being | |
10559 // closed. Close the parent and then add the clones back | |
10560 // in. | |
10561 } else if (hasTag(token.name, TOKEN_OPEN, openTags)) { | |
10562 | |
10563 // Remove the tag from the open tags | |
10564 while ((curTok = openTags.pop())) { | |
10565 | |
10566 // If it's the tag that is being closed then | |
10567 // discard it and break the loop. | |
10568 if (curTok.name === token.name) { | |
10569 curTok.closing = token; | |
10570 break; | |
10571 } | |
10572 | |
10573 // Otherwise clone this tag and then add any | |
10574 // previously cloned tags as it's children | |
10575 clone = curTok.clone(); | |
10576 | |
10577 if (cloned.length) { | |
10578 clone.children.push(last(cloned)); | |
10579 } | |
10580 | |
10581 cloned.push(clone); | |
10582 } | |
10583 | |
10584 // Place block linebreak before cloned tags | |
10585 if (next && next.type === TOKEN_NEWLINE) { | |
10586 bbcode = bbcodeHandlers[token.name]; | |
10587 if (bbcode && bbcode.isInline === false) { | |
10588 addTag(next); | |
10589 toks.shift(); | |
10590 } | |
10591 } | |
10592 | |
10593 // Add the last cloned child to the now current tag | |
10594 // (the parent of the tag which was being closed) | |
10595 addTag(last(cloned)); | |
10596 | |
10597 // Add all the cloned tags to the open tags list | |
10598 i = cloned.length; | |
10599 while (i--) { | |
10600 openTags.push(cloned[i]); | |
10601 } | |
10602 | |
10603 cloned.length = 0; | |
10604 | |
10605 // This tag is closing nothing so treat it as content | |
10606 } else { | |
10607 token.type = TOKEN_CONTENT; | |
10608 addTag(token); | |
10609 } | |
10610 break; | |
10611 | |
10612 case TOKEN_NEWLINE: | |
10613 // handle things like | |
10614 // [*]list\nitem\n[*]list1 | |
10615 // where it should come out as | |
10616 // [*]list\nitem[/*]\n[*]list1[/*] | |
10617 // instead of | |
10618 // [*]list\nitem\n[/*][*]list1[/*] | |
10619 if (currentTag() && next && closesCurrentTag( | |
10620 (next.type === TOKEN_CLOSE ? '/' : '') + | |
10621 next.name | |
10622 )) { | |
10623 // skip if the next tag is the closing tag for | |
10624 // the option tag, i.e. [/*] | |
10625 if (!(next.type === TOKEN_CLOSE && | |
10626 next.name === currentTag().name)) { | |
10627 bbcode = bbcodeHandlers[currentTag().name]; | |
10628 | |
10629 if (bbcode && bbcode.breakAfter) { | |
10630 openTags.pop(); | |
10631 } else if (bbcode && | |
10632 bbcode.isInline === false && | |
10633 base.opts.breakAfterBlock && | |
10634 bbcode.breakAfter !== false) { | |
10635 openTags.pop(); | |
10636 } | |
10637 } | |
10638 } | |
10639 | |
10640 addTag(token); | |
10641 break; | |
10642 | |
10643 default: // content | |
10644 addTag(token); | |
10645 break; | |
10646 } | |
10647 } | |
10648 | |
10649 return output; | |
10650 } | |
10651 | |
10652 /** | |
10653 * Normalise all new lines | |
10654 * | |
10655 * Removes any formatting new lines from the BBCode | |
10656 * leaving only content ones. I.e. for a list: | |
10657 * | |
10658 * [list] | |
10659 * [*] list item one | |
10660 * with a line break | |
10661 * [*] list item two | |
10662 * [/list] | |
10663 * | |
10664 * would become | |
10665 * | |
10666 * [list] [*] list item one | |
10667 * with a line break [*] list item two [/list] | |
10668 * | |
10669 * Which makes it easier to convert to HTML or add | |
10670 * the formatting new lines back in when converting | |
10671 * back to BBCode | |
10672 * | |
10673 * @param {array} children | |
10674 * @param {TokenizeToken} parent | |
10675 * @param {boolean} onlyRemoveBreakAfter | |
10676 * @return {void} | |
10677 */ | |
10678 function normaliseNewLines(children, parent, onlyRemoveBreakAfter) { | |
10679 var token, left, right, parentBBCode, bbcode, | |
10680 removedBreakEnd, removedBreakBefore, remove; | |
10681 var childrenLength = children.length; | |
10682 // TODO: this function really needs tidying up | |
10683 if (parent) { | |
10684 parentBBCode = bbcodeHandlers[parent.name]; | |
10685 } | |
10686 | |
10687 var i = childrenLength; | |
10688 while (i--) { | |
10689 if (!(token = children[i])) { | |
10690 continue; | |
10691 } | |
10692 | |
10693 if (token.type === TOKEN_NEWLINE) { | |
10694 left = i > 0 ? children[i - 1] : null; | |
10695 right = i < childrenLength - 1 ? children[i + 1] : null; | |
10696 remove = false; | |
10697 | |
10698 // Handle the start and end new lines | |
10699 // e.g. [tag]\n and \n[/tag] | |
10700 if (!onlyRemoveBreakAfter && parentBBCode && | |
10701 parentBBCode.isSelfClosing !== true) { | |
10702 // First child of parent so must be opening line break | |
10703 // (breakStartBlock, breakStart) e.g. [tag]\n | |
10704 if (!left) { | |
10705 if (parentBBCode.isInline === false && | |
10706 base.opts.breakStartBlock && | |
10707 parentBBCode.breakStart !== false) { | |
10708 remove = true; | |
10709 } | |
10710 | |
10711 if (parentBBCode.breakStart) { | |
10712 remove = true; | |
10713 } | |
10714 // Last child of parent so must be end line break | |
10715 // (breakEndBlock, breakEnd) | |
10716 // e.g. \n[/tag] | |
10717 // remove last line break (breakEndBlock, breakEnd) | |
10718 } else if (!removedBreakEnd && !right) { | |
10719 if (parentBBCode.isInline === false && | |
10720 base.opts.breakEndBlock && | |
10721 parentBBCode.breakEnd !== false) { | |
10722 remove = true; | |
10723 } | |
10724 | |
10725 if (parentBBCode.breakEnd) { | |
10726 remove = true; | |
10727 } | |
10728 | |
10729 removedBreakEnd = remove; | |
10730 } | |
10731 } | |
10732 | |
10733 if (left && left.type === TOKEN_OPEN) { | |
10734 if ((bbcode = bbcodeHandlers[left.name])) { | |
10735 if (!onlyRemoveBreakAfter) { | |
10736 if (bbcode.isInline === false && | |
10737 base.opts.breakAfterBlock && | |
10738 bbcode.breakAfter !== false) { | |
10739 remove = true; | |
10740 } | |
10741 | |
10742 if (bbcode.breakAfter) { | |
10743 remove = true; | |
10744 } | |
10745 } else if (bbcode.isInline === false) { | |
10746 remove = true; | |
10747 } | |
10748 } | |
10749 } | |
10750 | |
10751 if (!onlyRemoveBreakAfter && !removedBreakBefore && | |
10752 right && right.type === TOKEN_OPEN) { | |
10753 | |
10754 if ((bbcode = bbcodeHandlers[right.name])) { | |
10755 if (bbcode.isInline === false && | |
10756 base.opts.breakBeforeBlock && | |
10757 bbcode.breakBefore !== false) { | |
10758 remove = true; | |
10759 } | |
10760 | |
10761 if (bbcode.breakBefore) { | |
10762 remove = true; | |
10763 } | |
10764 | |
10765 removedBreakBefore = remove; | |
10766 | |
10767 if (remove) { | |
10768 children.splice(i, 1); | |
10769 continue; | |
10770 } | |
10771 } | |
10772 } | |
10773 | |
10774 if (remove) { | |
10775 children.splice(i, 1); | |
10776 } | |
10777 | |
10778 // reset double removedBreakBefore removal protection. | |
10779 // This is needed for cases like \n\n[\tag] where | |
10780 // only 1 \n should be removed but without this they both | |
10781 // would be. | |
10782 removedBreakBefore = false; | |
10783 } else if (token.type === TOKEN_OPEN) { | |
10784 normaliseNewLines(token.children, token, | |
10785 onlyRemoveBreakAfter); | |
10786 } | |
10787 } | |
10788 } | |
10789 | |
10790 /** | |
10791 * Fixes any invalid nesting. | |
10792 * | |
10793 * If it is a block level element inside 1 or more inline elements | |
10794 * then those inline elements will be split at the point where the | |
10795 * block level is and the block level element placed between the split | |
10796 * parts. i.e. | |
10797 * [inline]A[blocklevel]B[/blocklevel]C[/inline] | |
10798 * Will become: | |
10799 * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline] | |
10800 * | |
10801 * @param {array} children | |
10802 * @param {array} [parents] Null if there is no parents | |
10803 * @param {boolea} [insideInline] If inside an inline element | |
10804 * @param {array} [rootArr] Root array if there is one | |
10805 * @return {array} | |
10806 * @private | |
10807 */ | |
10808 function fixNesting(children, parents, insideInline, rootArr) { | |
10809 var token, i, parent, parentIndex, parentParentChildren, right; | |
10810 | |
10811 var isInline = function (token) { | |
10812 var bbcode = bbcodeHandlers[token.name]; | |
10813 | |
10814 return !bbcode || bbcode.isInline !== false; | |
10815 }; | |
10816 | |
10817 parents = parents || []; | |
10818 rootArr = rootArr || children; | |
10819 | |
10820 // This must check the length each time as it can change when | |
10821 // tokens are moved to fix the nesting. | |
10822 for (i = 0; i < children.length; i++) { | |
10823 if (!(token = children[i]) || token.type !== TOKEN_OPEN) { | |
10824 continue; | |
10825 } | |
10826 | |
10827 if (insideInline && !isInline(token)) { | |
10828 // if this is a blocklevel element inside an inline one then | |
10829 // split the parent at the block level element | |
10830 parent = last(parents); | |
10831 right = parent.splitAt(token); | |
10832 | |
10833 parentParentChildren = parents.length > 1 ? | |
10834 parents[parents.length - 2].children : rootArr; | |
10835 | |
10836 // If parent inline is allowed inside this tag, clone it and | |
10837 // wrap this tags children in it. | |
10838 if (isChildAllowed(token, parent)) { | |
10839 var clone = parent.clone(); | |
10840 clone.children = token.children; | |
10841 token.children = [clone]; | |
10842 } | |
10843 | |
10844 parentIndex = parentParentChildren.indexOf(parent); | |
10845 if (parentIndex > -1) { | |
10846 // remove the block level token from the right side of | |
10847 // the split inline element | |
10848 right.children.splice(0, 1); | |
10849 | |
10850 // insert the block level token and the right side after | |
10851 // the left side of the inline token | |
10852 parentParentChildren.splice( | |
10853 parentIndex + 1, 0, token, right | |
10854 ); | |
10855 | |
10856 // If token is a block and is followed by a newline, | |
10857 // then move the newline along with it to the new parent | |
10858 var next = right.children[0]; | |
10859 if (next && next.type === TOKEN_NEWLINE) { | |
10860 if (!isInline(token)) { | |
10861 right.children.splice(0, 1); | |
10862 parentParentChildren.splice( | |
10863 parentIndex + 2, 0, next | |
10864 ); | |
10865 } | |
10866 } | |
10867 | |
10868 // return to parents loop as the | |
10869 // children have now increased | |
10870 return; | |
10871 } | |
10872 | |
10873 } | |
10874 | |
10875 parents.push(token); | |
10876 | |
10877 fixNesting( | |
10878 token.children, | |
10879 parents, | |
10880 insideInline || isInline(token), | |
10881 rootArr | |
10882 ); | |
10883 | |
10884 parents.pop(); | |
10885 } | |
10886 } | |
10887 | |
10888 /** | |
10889 * Removes any empty BBCodes which are not allowed to be empty. | |
10890 * | |
10891 * @param {array} tokens | |
10892 * @private | |
10893 */ | |
10894 function removeEmpty(tokens) { | |
10895 var token, bbcode; | |
10896 | |
10897 /** | |
10898 * Checks if all children are whitespace or not | |
10899 * @private | |
10900 */ | |
10901 var isTokenWhiteSpace = function (children) { | |
10902 var j = children.length; | |
10903 | |
10904 while (j--) { | |
10905 var type = children[j].type; | |
10906 | |
10907 if (type === TOKEN_OPEN || type === TOKEN_CLOSE) { | |
10908 return false; | |
10909 } | |
10910 | |
10911 if (type === TOKEN_CONTENT && | |
10912 /\S|\u00A0/.test(children[j].val)) { | |
10913 return false; | |
10914 } | |
10915 } | |
10916 | |
10917 return true; | |
10918 }; | |
10919 | |
10920 var i = tokens.length; | |
10921 while (i--) { | |
10922 // So skip anything that isn't a tag since only tags can be | |
10923 // empty, content can't | |
10924 if (!(token = tokens[i]) || token.type !== TOKEN_OPEN) { | |
10925 continue; | |
10926 } | |
10927 | |
10928 bbcode = bbcodeHandlers[token.name]; | |
10929 | |
10930 // Remove any empty children of this tag first so that if they | |
10931 // are all removed this one doesn't think it's not empty. | |
10932 removeEmpty(token.children); | |
10933 | |
10934 if (isTokenWhiteSpace(token.children) && bbcode && | |
10935 !bbcode.isSelfClosing && !bbcode.allowsEmpty) { | |
10936 tokens.splice.apply(tokens, [i, 1].concat(token.children)); | |
10937 } | |
10938 } | |
10939 } | |
10940 | |
10941 /** | |
10942 * Converts a BBCode string to HTML | |
10943 * | |
10944 * @param {string} str | |
10945 * @param {boolean} preserveNewLines If to preserve all new lines, not | |
10946 * strip any based on the passed | |
10947 * formatting options | |
10948 * @return {string} | |
10949 * @memberOf BBCodeParser.prototype | |
10950 */ | |
10951 base.toHTML = function (str, preserveNewLines) { | |
10952 return convertToHTML(base.parse(str, preserveNewLines), true); | |
10953 }; | |
10954 | |
10955 /** | |
10956 * @private | |
10957 */ | |
10958 function convertToHTML(tokens, isRoot) { | |
10959 var undef, token, bbcode, content, html, needsBlockWrap, | |
10960 blockWrapOpen, isInline, lastChild, | |
10961 ret = ''; | |
10962 | |
10963 isInline = function (bbcode) { | |
10964 return (!bbcode || (bbcode.isHtmlInline !== undef ? | |
10965 bbcode.isHtmlInline : bbcode.isInline)) !== false; | |
10966 }; | |
10967 | |
10968 while (tokens.length > 0) { | |
10969 if (!(token = tokens.shift())) { | |
10970 continue; | |
10971 } | |
10972 | |
10973 if (token.type === TOKEN_OPEN) { | |
10974 lastChild = token.children[token.children.length - 1] || {}; | |
10975 bbcode = bbcodeHandlers[token.name]; | |
10976 needsBlockWrap = isRoot && isInline(bbcode); | |
10977 content = convertToHTML(token.children, false); | |
10978 | |
10979 if (bbcode && bbcode.html) { | |
10980 // Only add a line break to the end if this is | |
10981 // blocklevel and the last child wasn't block-level | |
10982 if (!isInline(bbcode) && | |
10983 isInline(bbcodeHandlers[lastChild.name]) && | |
10984 !bbcode.isPreFormatted && | |
10985 !bbcode.skipLastLineBreak) { | |
10986 // Add placeholder br to end of block level | |
10987 // elements | |
10988 content += '<br />'; | |
10989 } | |
10990 | |
10991 if (!isFunction(bbcode.html)) { | |
10992 token.attrs['0'] = content; | |
10993 html = formatBBCodeString( | |
10994 bbcode.html, | |
10995 token.attrs | |
10996 ); | |
10997 } else { | |
10998 html = bbcode.html.call( | |
10999 base, | |
11000 token, | |
11001 token.attrs, | |
11002 content | |
11003 ); | |
11004 } | |
11005 } else { | |
11006 html = token.val + content + | |
11007 (token.closing ? token.closing.val : ''); | |
11008 } | |
11009 } else if (token.type === TOKEN_NEWLINE) { | |
11010 if (!isRoot) { | |
11011 ret += '<br />'; | |
11012 continue; | |
11013 } | |
11014 | |
11015 // If not already in a block wrap then start a new block | |
11016 if (!blockWrapOpen) { | |
11017 ret += '<div>'; | |
11018 } | |
11019 | |
11020 ret += '<br />'; | |
11021 | |
11022 // Normally the div acts as a line-break with by moving | |
11023 // whatever comes after onto a new line. | |
11024 // If this is the last token, add an extra line-break so it | |
11025 // shows as there will be nothing after it. | |
11026 if (!tokens.length) { | |
11027 ret += '<br />'; | |
11028 } | |
11029 | |
11030 ret += '</div>\n'; | |
11031 blockWrapOpen = false; | |
11032 continue; | |
11033 // content | |
11034 } else { | |
11035 needsBlockWrap = isRoot; | |
11036 html = escapeEntities(token.val, true); | |
11037 } | |
11038 | |
11039 if (needsBlockWrap && !blockWrapOpen) { | |
11040 ret += '<div>'; | |
11041 blockWrapOpen = true; | |
11042 } else if (!needsBlockWrap && blockWrapOpen) { | |
11043 ret += '</div>\n'; | |
11044 blockWrapOpen = false; | |
11045 } | |
11046 | |
11047 ret += html; | |
11048 } | |
11049 | |
11050 if (blockWrapOpen) { | |
11051 ret += '</div>\n'; | |
11052 } | |
11053 | |
11054 return ret; | |
11055 } | |
11056 | |
11057 /** | |
11058 * Takes a BBCode string, parses it then converts it back to BBCode. | |
11059 * | |
11060 * This will auto fix the BBCode and format it with the specified | |
11061 * options. | |
11062 * | |
11063 * @param {string} str | |
11064 * @param {boolean} preserveNewLines If to preserve all new lines, not | |
11065 * strip any based on the passed | |
11066 * formatting options | |
11067 * @return {string} | |
11068 * @memberOf BBCodeParser.prototype | |
11069 */ | |
11070 base.toBBCode = function (str, preserveNewLines) { | |
11071 return convertToBBCode(base.parse(str, preserveNewLines)); | |
11072 }; | |
11073 | |
11074 /** | |
11075 * Converts parsed tokens back into BBCode with the | |
11076 * formatting specified in the options and with any | |
11077 * fixes specified. | |
11078 * | |
11079 * @param {array} toks Array of parsed tokens from base.parse() | |
11080 * @return {string} | |
11081 * @private | |
11082 */ | |
11083 function convertToBBCode(toks) { | |
11084 var token, attr, bbcode, isBlock, isSelfClosing, quoteType, | |
11085 breakBefore, breakStart, breakEnd, breakAfter, | |
11086 ret = ''; | |
11087 | |
11088 while (toks.length > 0) { | |
11089 if (!(token = toks.shift())) { | |
11090 continue; | |
11091 } | |
11092 // TODO: tidy this | |
11093 bbcode = bbcodeHandlers[token.name]; | |
11094 isBlock = !(!bbcode || bbcode.isInline !== false); | |
11095 isSelfClosing = bbcode && bbcode.isSelfClosing; | |
11096 | |
11097 breakBefore = (isBlock && base.opts.breakBeforeBlock && | |
11098 bbcode.breakBefore !== false) || | |
11099 (bbcode && bbcode.breakBefore); | |
11100 | |
11101 breakStart = (isBlock && !isSelfClosing && | |
11102 base.opts.breakStartBlock && | |
11103 bbcode.breakStart !== false) || | |
11104 (bbcode && bbcode.breakStart); | |
11105 | |
11106 breakEnd = (isBlock && base.opts.breakEndBlock && | |
11107 bbcode.breakEnd !== false) || | |
11108 (bbcode && bbcode.breakEnd); | |
11109 | |
11110 breakAfter = (isBlock && base.opts.breakAfterBlock && | |
11111 bbcode.breakAfter !== false) || | |
11112 (bbcode && bbcode.breakAfter); | |
11113 | |
11114 quoteType = (bbcode ? bbcode.quoteType : null) || | |
11115 base.opts.quoteType || QuoteType.auto; | |
11116 | |
11117 if (!bbcode && token.type === TOKEN_OPEN) { | |
11118 ret += token.val; | |
11119 | |
11120 if (token.children) { | |
11121 ret += convertToBBCode(token.children); | |
11122 } | |
11123 | |
11124 if (token.closing) { | |
11125 ret += token.closing.val; | |
11126 } | |
11127 } else if (token.type === TOKEN_OPEN) { | |
11128 if (breakBefore) { | |
11129 ret += '\n'; | |
11130 } | |
11131 | |
11132 // Convert the tag and it's attributes to BBCode | |
11133 ret += '[' + token.name; | |
11134 if (token.attrs) { | |
11135 if (token.attrs.defaultattr) { | |
11136 ret += '=' + quote( | |
11137 token.attrs.defaultattr, | |
11138 quoteType, | |
11139 'defaultattr' | |
11140 ); | |
11141 | |
11142 delete token.attrs.defaultattr; | |
11143 } | |
11144 | |
11145 for (attr in token.attrs) { | |
11146 if (token.attrs.hasOwnProperty(attr)) { | |
11147 ret += ' ' + attr + '=' + | |
11148 quote(token.attrs[attr], quoteType, attr); | |
11149 } | |
11150 } | |
11151 } | |
11152 ret += ']'; | |
11153 | |
11154 if (breakStart) { | |
11155 ret += '\n'; | |
11156 } | |
11157 | |
11158 // Convert the tags children to BBCode | |
11159 if (token.children) { | |
11160 ret += convertToBBCode(token.children); | |
11161 } | |
11162 | |
11163 // add closing tag if not self closing | |
11164 if (!isSelfClosing && !bbcode.excludeClosing) { | |
11165 if (breakEnd) { | |
11166 ret += '\n'; | |
11167 } | |
11168 | |
11169 ret += '[/' + token.name + ']'; | |
11170 } | |
11171 | |
11172 if (breakAfter) { | |
11173 ret += '\n'; | |
11174 } | |
11175 | |
11176 // preserve whatever was recognized as the | |
11177 // closing tag if it is a self closing tag | |
11178 if (token.closing && isSelfClosing) { | |
11179 ret += token.closing.val; | |
11180 } | |
11181 } else { | |
11182 ret += token.val; | |
11183 } | |
11184 } | |
11185 | |
11186 return ret; | |
11187 } | |
11188 | |
11189 /** | |
11190 * Quotes an attribute | |
11191 * | |
11192 * @param {string} str | |
11193 * @param {BBCodeParser.QuoteType} quoteType | |
11194 * @param {string} name | |
11195 * @return {string} | |
11196 * @private | |
11197 */ | |
11198 function quote(str, quoteType, name) { | |
11199 var needsQuotes = /\s|=/.test(str); | |
11200 | |
11201 if (isFunction(quoteType)) { | |
11202 return quoteType(str, name); | |
11203 } | |
11204 | |
11205 if (quoteType === QuoteType.never || | |
11206 (quoteType === QuoteType.auto && !needsQuotes)) { | |
11207 return str; | |
11208 } | |
11209 | |
11210 return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; | |
11211 } | |
11212 | |
11213 /** | |
11214 * Returns the last element of an array or null | |
11215 * | |
11216 * @param {array} arr | |
11217 * @return {Object} Last element | |
11218 * @private | |
11219 */ | |
11220 function last(arr) { | |
11221 if (arr.length) { | |
11222 return arr[arr.length - 1]; | |
11223 } | |
11224 | |
11225 return null; | |
11226 } | |
11227 | |
11228 /** | |
11229 * Converts a string to lowercase. | |
11230 * | |
11231 * @param {string} str | |
11232 * @return {string} Lowercase version of str | |
11233 * @private | |
11234 */ | |
11235 function lower(str) { | |
11236 return str.toLowerCase(); | |
11237 } | |
11238 }; | |
11239 | |
11240 /** | |
11241 * Quote type | |
11242 * @type {Object} | |
11243 * @class QuoteType | |
11244 * @name BBCodeParser.QuoteType | |
11245 * @since 1.4.0 | |
11246 */ | |
11247 BBCodeParser.QuoteType = QuoteType; | |
11248 | |
11249 /** | |
11250 * Default BBCode parser options | |
11251 * @type {Object} | |
11252 */ | |
11253 BBCodeParser.defaults = { | |
11254 /** | |
11255 * If to add a new line before block level elements | |
11256 * | |
11257 * @type {Boolean} | |
11258 */ | |
11259 breakBeforeBlock: false, | |
11260 | |
11261 /** | |
11262 * If to add a new line after the start of block level elements | |
11263 * | |
11264 * @type {Boolean} | |
11265 */ | |
11266 breakStartBlock: false, | |
11267 | |
11268 /** | |
11269 * If to add a new line before the end of block level elements | |
11270 * | |
11271 * @type {Boolean} | |
11272 */ | |
11273 breakEndBlock: false, | |
11274 | |
11275 /** | |
11276 * If to add a new line after block level elements | |
11277 * | |
11278 * @type {Boolean} | |
11279 */ | |
11280 breakAfterBlock: true, | |
11281 | |
11282 /** | |
11283 * If to remove empty tags | |
11284 * | |
11285 * @type {Boolean} | |
11286 */ | |
11287 removeEmptyTags: true, | |
11288 | |
11289 /** | |
11290 * If to fix invalid nesting, | |
11291 * i.e. block level elements inside inline elements. | |
11292 * | |
11293 * @type {Boolean} | |
11294 */ | |
11295 fixInvalidNesting: true, | |
11296 | |
11297 /** | |
11298 * If to fix invalid children. | |
11299 * i.e. A tag which is inside a parent that doesn't | |
11300 * allow that type of tag. | |
11301 * | |
11302 * @type {Boolean} | |
11303 */ | |
11304 fixInvalidChildren: true, | |
11305 | |
11306 /** | |
11307 * Attribute quote type | |
11308 * | |
11309 * @type {BBCodeParser.QuoteType} | |
11310 * @since 1.4.1 | |
11311 */ | |
11312 quoteType: QuoteType.auto, | |
11313 | |
11314 /** | |
11315 * Whether to use strict matching on attributes and styles. | |
11316 * | |
11317 * When true this will perform AND matching requiring all tag | |
11318 * attributes and styles to match. | |
11319 * | |
11320 * When false will perform OR matching and will match if any of | |
11321 * a tags attributes or styles match. | |
11322 * | |
11323 * @type {Boolean} | |
11324 * @since 3.1.0 | |
11325 */ | |
11326 strictMatch: false | |
11327 }; | |
11328 | |
11329 /** | |
11330 * Converts a number 0-255 to hex. | |
11331 * | |
11332 * Will return 00 if number is not a valid number. | |
11333 * | |
11334 * @param {any} number | |
11335 * @return {string} | |
11336 * @private | |
11337 */ | |
11338 function toHex(number) { | |
11339 number = parseInt(number, 10); | |
11340 | |
11341 if (isNaN(number)) { | |
11342 return '00'; | |
11343 } | |
11344 | |
11345 number = Math.max(0, Math.min(number, 255)).toString(16); | |
11346 | |
11347 return number.length < 2 ? '0' + number : number; | |
11348 } | |
11349 /** | |
11350 * Normalises a CSS colour to hex #xxxxxx format | |
11351 * | |
11352 * @param {string} colorStr | |
11353 * @return {string} | |
11354 * @private | |
11355 */ | |
11356 function _normaliseColour(colorStr) { | |
11357 var match; | |
11358 | |
11359 colorStr = colorStr || '#000'; | |
11360 | |
11361 // rgb(n,n,n); | |
11362 if ((match = | |
11363 colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) { | |
11364 return '#' + | |
11365 toHex(match[1]) + | |
11366 toHex(match[2]) + | |
11367 toHex(match[3]); | |
11368 } | |
11369 | |
11370 // expand shorthand | |
11371 if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) { | |
11372 return '#' + | |
11373 match[1] + match[1] + | |
11374 match[2] + match[2] + | |
11375 match[3] + match[3]; | |
11376 } | |
11377 | |
11378 return colorStr; | |
11379 } | |
11380 | |
11381 /** | |
11382 * SCEditor BBCode format | |
11383 * @since 2.0.0 | |
11384 */ | |
11385 function bbcodeFormat() { | |
11386 var base = this; | |
11387 | |
11388 base.stripQuotes = _stripQuotes; | |
11389 | |
11390 /** | |
11391 * cache of all the tags pointing to their bbcodes to enable | |
11392 * faster lookup of which bbcode a tag should have | |
11393 * @private | |
11394 */ | |
11395 var tagsToBBCodes = {}; | |
11396 | |
11397 /** | |
11398 * Allowed children of specific HTML tags. Empty array if no | |
11399 * children other than text nodes are allowed | |
11400 * @private | |
11401 */ | |
11402 var validChildren = { | |
11403 ul: ['li', 'ol', 'ul'], | |
11404 ol: ['li', 'ol', 'ul'], | |
11405 table: ['tr'], | |
11406 tr: ['td', 'th'], | |
11407 code: ['br', 'p', 'div'] | |
11408 }; | |
11409 | |
11410 /** | |
11411 * Populates tagsToBBCodes and stylesToBBCodes for easier lookups | |
11412 * | |
11413 * @private | |
11414 */ | |
11415 function buildBbcodeCache() { | |
11416 each(bbcodeHandlers, function (bbcode, handler) { | |
11417 var | |
11418 isBlock = handler.isInline === false, | |
11419 tags = bbcodeHandlers[bbcode].tags, | |
11420 styles = bbcodeHandlers[bbcode].styles; | |
11421 | |
11422 if (styles) { | |
11423 tagsToBBCodes['*'] = tagsToBBCodes['*'] || {}; | |
11424 tagsToBBCodes['*'][isBlock] = | |
11425 tagsToBBCodes['*'][isBlock] || {}; | |
11426 tagsToBBCodes['*'][isBlock][bbcode] = [ | |
11427 ['style', Object.entries(styles)] | |
11428 ]; | |
11429 } | |
11430 | |
11431 if (tags) { | |
11432 each(tags, function (tag, values) { | |
11433 if (values && values.style) { | |
11434 values.style = Object.entries(values.style); | |
11435 } | |
11436 | |
11437 tagsToBBCodes[tag] = tagsToBBCodes[tag] || {}; | |
11438 tagsToBBCodes[tag][isBlock] = | |
11439 tagsToBBCodes[tag][isBlock] || {}; | |
11440 tagsToBBCodes[tag][isBlock][bbcode] = | |
11441 values && Object.entries(values); | |
11442 }); | |
11443 } | |
11444 }); | |
11445 }; | |
11446 | |
11447 /** | |
11448 * Handles adding newlines after block level elements | |
11449 * | |
11450 * @param {HTMLElement} element The element to convert | |
11451 * @param {string} content The tags text content | |
11452 * @return {string} | |
11453 * @private | |
11454 */ | |
11455 function handleBlockNewlines(element, content) { | |
11456 var tag = element.nodeName.toLowerCase(); | |
11457 var isInline = dom.isInline; | |
11458 if (!isInline(element, true) || tag === 'br') { | |
11459 var isLastBlockChild, parent, parentLastChild, | |
11460 previousSibling = element.previousSibling; | |
11461 | |
11462 // Skips selection makers and ignored elements | |
11463 // Skip empty inline elements | |
11464 while (previousSibling && | |
11465 previousSibling.nodeType === 1 && | |
11466 !is(previousSibling, 'br') && | |
11467 isInline(previousSibling, true) && | |
11468 !previousSibling.firstChild) { | |
11469 previousSibling = previousSibling.previousSibling; | |
11470 } | |
11471 | |
11472 // If it's the last block of an inline that is the last | |
11473 // child of a block then it shouldn't cause a line break | |
11474 // <block><inline><br></inline></block> | |
11475 do { | |
11476 parent = element.parentNode; | |
11477 parentLastChild = parent && parent.lastChild; | |
11478 | |
11479 isLastBlockChild = parentLastChild === element; | |
11480 element = parent; | |
11481 } while (parent && isLastBlockChild && isInline(parent, true)); | |
11482 | |
11483 // If this block is: | |
11484 // * Not the last child of a block level element | |
11485 // * Is a <li> tag (lists are blocks) | |
11486 if (!isLastBlockChild || tag === 'li') { | |
11487 content += '\n'; | |
11488 } | |
11489 | |
11490 // Check for: | |
11491 // <block>text<block>text</block></block> | |
11492 // | |
11493 // The second opening <block> opening tag should cause a | |
11494 // line break because the previous sibing is inline. | |
11495 if (tag !== 'br' && previousSibling && | |
11496 !is(previousSibling, 'br') && | |
11497 isInline(previousSibling, true)) { | |
11498 content = '\n' + content; | |
11499 } | |
11500 } | |
11501 | |
11502 return content; | |
11503 } | |
11504 | |
11505 /** | |
11506 * Handles a HTML tag and finds any matching BBCodes | |
11507 * | |
11508 * @param {HTMLElement} element The element to convert | |
11509 * @param {string} content The Tags text content | |
11510 * @param {boolean} blockLevel | |
11511 * @return {string} Content with any matching BBCode tags | |
11512 * wrapped around it. | |
11513 * @private | |
11514 */ | |
11515 function handleTags(element, content, blockLevel) { | |
11516 function isStyleMatch(style) { | |
11517 var property = style[0]; | |
11518 var values = style[1]; | |
11519 var val = dom.getStyle(element, property); | |
11520 var parent = element.parentNode; | |
11521 | |
11522 // if the parent has the same style use that instead of this one | |
11523 // so you don't end up with [i]parent[i]child[/i][/i] | |
11524 if (!val || parent && dom.hasStyle(parent, property, val)) { | |
11525 return false; | |
11526 } | |
11527 | |
11528 return !values || values.includes(val); | |
11529 } | |
11530 | |
11531 function createAttributeMatch(isStrict) { | |
11532 return function (attribute) { | |
11533 var name = attribute[0]; | |
11534 var value = attribute[1]; | |
11535 | |
11536 // code tags should skip most styles | |
11537 if (name === 'style' && element.nodeName === 'CODE') { | |
11538 return false; | |
11539 } | |
11540 | |
11541 if (name === 'style' && value) { | |
11542 return value[isStrict ? 'every' : 'some'](isStyleMatch); | |
11543 } else { | |
11544 var val = attr(element, name); | |
11545 | |
11546 return val && (!value || value.includes(val)); | |
11547 } | |
11548 }; | |
11549 } | |
11550 | |
11551 function handleTag(tag) { | |
11552 if (!tagsToBBCodes[tag] || !tagsToBBCodes[tag][blockLevel]) { | |
11553 return; | |
11554 } | |
11555 | |
11556 // loop all bbcodes for this tag | |
11557 each(tagsToBBCodes[tag][blockLevel], function (bbcode, attrs) { | |
11558 var fn, format, | |
11559 isStrict = bbcodeHandlers[bbcode].strictMatch; | |
11560 | |
11561 if (typeof isStrict === 'undefined') { | |
11562 isStrict = base.opts.strictMatch; | |
11563 } | |
11564 | |
11565 // Skip if the element doesn't have the attribute or the | |
11566 // attribute doesn't match one of the required values | |
11567 fn = isStrict ? 'every' : 'some'; | |
11568 if (attrs && !attrs[fn](createAttributeMatch(isStrict))) { | |
11569 return; | |
11570 } | |
11571 | |
11572 format = bbcodeHandlers[bbcode].format; | |
11573 if (isFunction(format)) { | |
11574 content = format.call(base, element, content); | |
11575 } else { | |
11576 content = _formatString(format, content); | |
11577 } | |
11578 return false; | |
11579 }); | |
11580 } | |
11581 | |
11582 handleTag('*'); | |
11583 handleTag(element.nodeName.toLowerCase()); | |
11584 return content; | |
11585 } | |
11586 | |
11587 /** | |
11588 * Converts a HTML dom element to BBCode starting from | |
11589 * the innermost element and working backwards | |
11590 * | |
11591 * @private | |
11592 * @param {HTMLElement} element | |
11593 * @return {string} BBCode | |
11594 * @memberOf SCEditor.plugins.bbcode.prototype | |
11595 */ | |
11596 function elementToBbcode(element) { | |
11597 var toBBCode = function (node, vChildren) { | |
11598 var ret = ''; | |
11599 | |
11600 dom.traverse(node, function (node) { | |
11601 var content = '', | |
11602 nodeType = node.nodeType, | |
11603 tag = node.nodeName.toLowerCase(), | |
11604 vChild = validChildren[tag], | |
11605 firstChild = node.firstChild, | |
11606 isValidChild = true; | |
11607 | |
11608 if (typeof vChildren === 'object') { | |
11609 isValidChild = vChildren.indexOf(tag) > -1; | |
11610 | |
11611 // Emoticons should always be converted | |
11612 if (is(node, 'img') && attr(node, EMOTICON_DATA_ATTR)) { | |
11613 isValidChild = true; | |
11614 } | |
11615 | |
11616 // if this tag is one of the parents allowed children | |
11617 // then set this tags allowed children to whatever it | |
11618 // allows, otherwise set to what the parent allows | |
11619 if (!isValidChild) { | |
11620 vChild = vChildren; | |
11621 } | |
11622 } | |
11623 | |
11624 // 3 = text and 1 = element | |
11625 if (nodeType !== 3 && nodeType !== 1) { | |
11626 return; | |
11627 } | |
11628 | |
11629 if (nodeType === 1) { | |
11630 // skip empty nlf elements (new lines automatically | |
11631 // added after block level elements like quotes) | |
11632 if (is(node, '.sceditor-nlf') && !firstChild) { | |
11633 return; | |
11634 } | |
11635 | |
11636 // don't convert iframe contents | |
11637 if (tag !== 'iframe') { | |
11638 content = toBBCode(node, vChild); | |
11639 } | |
11640 | |
11641 // TODO: isValidChild is no longer needed. Should use | |
11642 // valid children bbcodes instead by creating BBCode | |
11643 // tokens like the parser. | |
11644 if (isValidChild) { | |
11645 // code tags should skip most styles | |
11646 if (tag !== 'code') { | |
11647 // First parse inline codes | |
11648 content = handleTags(node, content, false); | |
11649 } | |
11650 | |
11651 content = handleTags(node, content, true); | |
11652 ret += handleBlockNewlines(node, content); | |
11653 } else { | |
11654 ret += content; | |
11655 } | |
11656 } else { | |
11657 ret += node.nodeValue; | |
11658 } | |
11659 }, false, true); | |
11660 | |
11661 return ret; | |
11662 }; | |
11663 | |
11664 return toBBCode(element); | |
11665 }; | |
11666 | |
11667 /** | |
11668 * Initializer | |
11669 * @private | |
11670 */ | |
11671 base.init = function () { | |
11672 base.opts = this.opts; | |
11673 base.elementToBbcode = elementToBbcode; | |
11674 | |
11675 // build the BBCode cache | |
11676 buildBbcodeCache(); | |
11677 | |
11678 this.commands = extend( | |
11679 true, {}, defaultCommandsOverrides, this.commands | |
11680 ); | |
11681 | |
11682 // Add BBCode helper methods | |
11683 this.toBBCode = base.toSource; | |
11684 this.fromBBCode = base.toHtml; | |
11685 }; | |
11686 | |
11687 /** | |
11688 * Converts BBCode into HTML | |
11689 * | |
11690 * @param {boolean} asFragment | |
11691 * @param {string} source | |
11692 * @param {boolean} [legacyAsFragment] Used by fromBBCode() method | |
11693 */ | |
11694 function toHtml(asFragment, source, legacyAsFragment) { | |
11695 var parser = new BBCodeParser(base.opts.parserOptions); | |
11696 var html = parser.toHTML( | |
11697 base.opts.bbcodeTrim ? source.trim() : source | |
11698 ); | |
11699 | |
11700 return (asFragment || legacyAsFragment) ? | |
11701 removeFirstLastDiv(html) : html; | |
11702 } | |
11703 | |
11704 /** | |
11705 * Converts HTML into BBCode | |
11706 * | |
11707 * @param {boolean} asFragment | |
11708 * @param {string} html | |
11709 * @param {!Document} [context] | |
11710 * @param {!HTMLElement} [parent] | |
11711 * @return {string} | |
11712 * @private | |
11713 */ | |
11714 function toSource(asFragment, html, context, parent) { | |
11715 context = context || document; | |
11716 | |
11717 var bbcode, elements; | |
11718 var containerParent = context.createElement('div'); | |
11719 var container = context.createElement('div'); | |
11720 var parser = new BBCodeParser(base.opts.parserOptions); | |
11721 | |
11722 container.innerHTML = html; | |
11723 css(containerParent, 'visibility', 'hidden'); | |
11724 containerParent.appendChild(container); | |
11725 context.body.appendChild(containerParent); | |
11726 | |
11727 if (asFragment) { | |
11728 // Add text before and after so removeWhiteSpace doesn't remove | |
11729 // leading and trailing whitespace | |
11730 containerParent.insertBefore( | |
11731 context.createTextNode('#'), | |
11732 containerParent.firstChild | |
11733 ); | |
11734 containerParent.appendChild(context.createTextNode('#')); | |
11735 } | |
11736 | |
11737 // Match parents white-space handling | |
11738 if (parent) { | |
11739 css(container, 'whiteSpace', css(parent, 'whiteSpace')); | |
11740 } | |
11741 | |
11742 // Remove all nodes with sceditor-ignore class | |
11743 elements = container.getElementsByClassName('sceditor-ignore'); | |
11744 while (elements.length) { | |
11745 elements[0].parentNode.removeChild(elements[0]); | |
11746 } | |
11747 | |
11748 dom.removeWhiteSpace(containerParent); | |
11749 | |
11750 bbcode = elementToBbcode(container); | |
11751 | |
11752 context.body.removeChild(containerParent); | |
11753 | |
11754 bbcode = parser.toBBCode(bbcode, true); | |
11755 | |
11756 if (base.opts.bbcodeTrim) { | |
11757 bbcode = bbcode.trim(); | |
11758 } | |
11759 | |
11760 return bbcode; | |
11761 }; | |
11762 | |
11763 base.toHtml = toHtml.bind(null, false); | |
11764 base.fragmentToHtml = toHtml.bind(null, true); | |
11765 base.toSource = toSource.bind(null, false); | |
11766 base.fragmentToSource = toSource.bind(null, true); | |
11767 }; | |
11768 | |
11769 /** | |
11770 * Gets a BBCode | |
11771 * | |
11772 * @param {string} name | |
11773 * @return {Object|null} | |
11774 * @since 2.0.0 | |
11775 */ | |
11776 bbcodeFormat.get = function (name) { | |
11777 return bbcodeHandlers[name] || null; | |
11778 }; | |
11779 | |
11780 /** | |
11781 * Adds a BBCode to the parser or updates an existing | |
11782 * BBCode if a BBCode with the specified name already exists. | |
11783 * | |
11784 * @param {string} name | |
11785 * @param {Object} bbcode | |
11786 * @return {this} | |
11787 * @since 2.0.0 | |
11788 */ | |
11789 bbcodeFormat.set = function (name, bbcode) { | |
11790 if (name && bbcode) { | |
11791 // merge any existing command properties | |
11792 bbcode = extend(bbcodeHandlers[name] || {}, bbcode); | |
11793 | |
11794 bbcode.remove = function () { | |
11795 delete bbcodeHandlers[name]; | |
11796 }; | |
11797 | |
11798 bbcodeHandlers[name] = bbcode; | |
11799 } | |
11800 | |
11801 return this; | |
11802 }; | |
11803 | |
11804 /** | |
11805 * Renames a BBCode | |
11806 * | |
11807 * This does not change the format or HTML handling, those must be | |
11808 * changed manually. | |
11809 * | |
11810 * @param {string} name [description] | |
11811 * @param {string} newName [description] | |
11812 * @return {this|false} | |
11813 * @since 2.0.0 | |
11814 */ | |
11815 bbcodeFormat.rename = function (name, newName) { | |
11816 if (name in bbcodeHandlers) { | |
11817 bbcodeHandlers[newName] = bbcodeHandlers[name]; | |
11818 | |
11819 delete bbcodeHandlers[name]; | |
11820 } | |
11821 | |
11822 return this; | |
11823 }; | |
11824 | |
11825 /** | |
11826 * Removes a BBCode | |
11827 * | |
11828 * @param {string} name | |
11829 * @return {this} | |
11830 * @since 2.0.0 | |
11831 */ | |
11832 bbcodeFormat.remove = function (name) { | |
11833 if (name in bbcodeHandlers) { | |
11834 delete bbcodeHandlers[name]; | |
11835 } | |
11836 | |
11837 return this; | |
11838 }; | |
11839 | |
11840 bbcodeFormat.formatBBCodeString = formatBBCodeString; | |
11841 | |
11842 sceditor.formats.bbcode = bbcodeFormat; | |
11843 sceditor.BBCodeParser = BBCodeParser; | |
11844 }(sceditor)); |