(function () { 'use strict'; /** * Check if the passed argument is the * the passed type. * * @param {string} type * @param {*} arg * @returns {boolean} */ function isTypeof(type, arg) { return typeof arg === type; } /** * @type {function(*): boolean} */ var isString = isTypeof.bind(null, 'string'); /** * @type {function(*): boolean} */ var isUndefined = isTypeof.bind(null, 'undefined'); /** * @type {function(*): boolean} */ var isFunction = isTypeof.bind(null, 'function'); /** * @type {function(*): boolean} */ var isNumber = isTypeof.bind(null, 'number'); /** * Returns true if an object has no keys * * @param {!Object} obj * @returns {boolean} */ function isEmptyObject(obj) { return !Object.keys(obj).length; } /** * Extends the first object with any extra objects passed * * If the first argument is boolean and set to true * it will extend child arrays and objects recursively. * * @param {!Object|boolean} targetArg * @param {...Object} source * @return {Object} */ function extend(targetArg, sourceArg) { var isTargetBoolean = targetArg === !!targetArg; var i = isTargetBoolean ? 2 : 1; var target = isTargetBoolean ? sourceArg : targetArg; var isDeep = isTargetBoolean ? targetArg : false; function isObject(value) { return value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype; } for (; i < arguments.length; i++) { var source = arguments[i]; // Copy all properties for jQuery compatibility /* eslint guard-for-in: off */ for (var key in source) { var targetValue = target[key]; var value = source[key]; // Skip undefined values to match jQuery if (isUndefined(value)) { continue; } // Skip special keys to prevent prototype pollution if (key === '__proto__' || key === 'constructor') { continue; } var isValueObject = isObject(value); var isValueArray = Array.isArray(value); if (isDeep && (isValueObject || isValueArray)) { // Can only merge if target type matches otherwise create // new target to merge into var isSameType = isObject(targetValue) === isValueObject && Array.isArray(targetValue) === isValueArray; target[key] = extend( true, isSameType ? targetValue : (isValueArray ? [] : {}), value ); } else { target[key] = value; } } } return target; } /** * Removes an item from the passed array * * @param {!Array} arr * @param {*} item */ function arrayRemove(arr, item) { var i = arr.indexOf(item); if (i > -1) { arr.splice(i, 1); } } /** * Iterates over an array or object * * @param {!Object|Array} obj * @param {function(*, *)} fn */ function each(obj, fn) { if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) { for (var i = 0; i < obj.length; i++) { fn(i, obj[i]); } } else { Object.keys(obj).forEach(function (key) { fn(key, obj[key]); }); } } /** * Cache of camelCase CSS property names * @type {Object} */ var cssPropertyNameCache = {}; /** * Node type constant for element nodes * * @type {number} */ var ELEMENT_NODE = 1; /** * Node type constant for text nodes * * @type {number} */ var TEXT_NODE = 3; /** * Node type constant for comment nodes * * @type {number} */ var COMMENT_NODE = 8; function toFloat(value) { value = parseFloat(value); return isFinite(value) ? value : 0; } /** * Creates an element with the specified attributes * * Will create it in the current document unless context * is specified. * * @param {!string} tag * @param {!Object} [attributes] * @param {!Document} [context] * @returns {!HTMLElement} */ function createElement(tag, attributes, context) { var node = (context || document).createElement(tag); each(attributes || {}, function (key, value) { if (key === 'style') { node.style.cssText = value; } else if (key in node) { node[key] = value; } else { node.setAttribute(key, value); } }); return node; } /** * Gets the first parent node that matches the selector * * @param {!HTMLElement} node * @param {!string} [selector] * @returns {HTMLElement|undefined} */ function parent(node, selector) { var parent = node || {}; while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) { if (!selector || is(parent, selector)) { return parent; } } } /** * Checks the passed node and all parents and * returns the first matching node if any. * * @param {!HTMLElement} node * @param {!string} selector * @returns {HTMLElement|undefined} */ function closest(node, selector) { return is(node, selector) ? node : parent(node, selector); } /** * Removes the node from the DOM * * @param {!HTMLElement} node */ function remove(node) { if (node.parentNode) { node.parentNode.removeChild(node); } } /** * Appends child to parent node * * @param {!HTMLElement} node * @param {!HTMLElement} child */ function appendChild(node, child) { node.appendChild(child); } /** * Finds any child nodes that match the selector * * @param {!HTMLElement} node * @param {!string} selector * @returns {NodeList} */ function find(node, selector) { return node.querySelectorAll(selector); } /** * For on() and off() if to add/remove the event * to the capture phase * * @type {boolean} */ var EVENT_CAPTURE = true; /** * Adds an event listener for the specified events. * * Events should be a space separated list of events. * * If selector is specified the handler will only be * called when the event target matches the selector. * * @param {!Node} node * @param {string} events * @param {string} [selector] * @param {function(Object)} fn * @param {boolean} [capture=false] * @see off() */ // eslint-disable-next-line max-params function on(node, events, selector, fn, capture) { events.split(' ').forEach(function (event) { var handler; if (isString(selector)) { handler = fn['_sce-event-' + event + selector] || function (e) { var target = e.target; while (target && target !== node) { if (is(target, selector)) { fn.call(target, e); return; } target = target.parentNode; } }; fn['_sce-event-' + event + selector] = handler; } else { handler = selector; capture = fn; } node.addEventListener(event, handler, capture || false); }); } /** * Removes an event listener for the specified events. * * @param {!Node} node * @param {string} events * @param {string} [selector] * @param {function(Object)} fn * @param {boolean} [capture=false] * @see on() */ // eslint-disable-next-line max-params function off(node, events, selector, fn, capture) { events.split(' ').forEach(function (event) { var handler; if (isString(selector)) { handler = fn['_sce-event-' + event + selector]; } else { handler = selector; capture = fn; } node.removeEventListener(event, handler, capture || false); }); } /** * If only attr param is specified it will get * the value of the attr param. * * If value is specified but null the attribute * will be removed otherwise the attr value will * be set to the passed value. * * @param {!HTMLElement} node * @param {!string} attr * @param {?string} [value] */ function attr(node, attr, value) { if (arguments.length < 3) { return node.getAttribute(attr); } // eslint-disable-next-line eqeqeq, no-eq-null if (value == null) { removeAttr(node, attr); } else { node.setAttribute(attr, value); } } /** * Removes the specified attribute * * @param {!HTMLElement} node * @param {!string} attr */ function removeAttr(node, attr) { node.removeAttribute(attr); } /** * Sets the passed elements display to none * * @param {!HTMLElement} node */ function hide(node) { css(node, 'display', 'none'); } /** * Sets the passed elements display to default * * @param {!HTMLElement} node */ function show(node) { css(node, 'display', ''); } /** * Toggles an elements visibility * * @param {!HTMLElement} node */ function toggle(node) { if (isVisible(node)) { hide(node); } else { show(node); } } /** * Gets a computed CSS values or sets an inline CSS value * * Rules should be in camelCase format and not * hyphenated like CSS properties. * * @param {!HTMLElement} node * @param {!Object|string} rule * @param {string|number} [value] * @return {string|number|undefined} */ function css(node, rule, value) { if (arguments.length < 3) { if (isString(rule)) { return node.nodeType === 1 ? getComputedStyle(node)[rule] : null; } each(rule, function (key, value) { css(node, key, value); }); } else { // isNaN returns false for null, false and empty strings // so need to check it's truthy or 0 var isNumeric = (value || value === 0) && !isNaN(value); node.style[rule] = isNumeric ? value + 'px' : value; } } /** * Gets or sets the data attributes on a node * * Unlike the jQuery version this only stores data * in the DOM attributes which means only strings * can be stored. * * @param {Node} node * @param {string} [key] * @param {string} [value] * @return {Object|undefined} */ function data(node, key, value) { var argsLength = arguments.length; var data = {}; if (node.nodeType === ELEMENT_NODE) { if (argsLength === 1) { each(node.attributes, function (_, attr) { if (/^data\-/i.test(attr.name)) { data[attr.name.substr(5)] = attr.value; } }); return data; } if (argsLength === 2) { return attr(node, 'data-' + key); } attr(node, 'data-' + key, String(value)); } } /** * Checks if node matches the given selector. * * @param {?HTMLElement} node * @param {string} selector * @returns {boolean} */ function is(node, selector) { var result = false; if (node && node.nodeType === ELEMENT_NODE) { result = (node.matches || node.msMatchesSelector || node.webkitMatchesSelector).call(node, selector); } return result; } /** * Returns true if node contains child otherwise false. * * This differs from the DOM contains() method in that * if node and child are equal this will return false. * * @param {!Node} node * @param {HTMLElement} child * @returns {boolean} */ function contains(node, child) { return node !== child && node.contains && node.contains(child); } /** * @param {Node} node * @param {string} [selector] * @returns {?HTMLElement} */ function previousElementSibling(node, selector) { var prev = node.previousElementSibling; if (selector && prev) { return is(prev, selector) ? prev : null; } return prev; } /** * @param {!Node} node * @param {!Node} refNode * @returns {Node} */ function insertBefore(node, refNode) { return refNode.parentNode.insertBefore(node, refNode); } /** * @param {?HTMLElement} node * @returns {!Array.} */ function classes(node) { return node.className.trim().split(/\s+/); } /** * @param {?HTMLElement} node * @param {string} className * @returns {boolean} */ function hasClass(node, className) { return is(node, '.' + className); } /** * @param {!HTMLElement} node * @param {string} className */ function addClass(node, className) { var classList = classes(node); if (classList.indexOf(className) < 0) { classList.push(className); } node.className = classList.join(' '); } /** * @param {!HTMLElement} node * @param {string} className */ function removeClass(node, className) { var classList = classes(node); arrayRemove(classList, className); node.className = classList.join(' '); } /** * Toggles a class on node. * * If state is specified and is truthy it will add * the class. * * If state is specified and is falsey it will remove * the class. * * @param {HTMLElement} node * @param {string} className * @param {boolean} [state] */ function toggleClass(node, className, state) { state = isUndefined(state) ? !hasClass(node, className) : state; if (state) { addClass(node, className); } else { removeClass(node, className); } } /** * Gets or sets the width of the passed node. * * @param {HTMLElement} node * @param {number|string} [value] * @returns {number|undefined} */ function width(node, value) { if (isUndefined(value)) { var cs = getComputedStyle(node); var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight); var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth); return node.offsetWidth - padding - border; } css(node, 'width', value); } /** * Gets or sets the height of the passed node. * * @param {HTMLElement} node * @param {number|string} [value] * @returns {number|undefined} */ function height(node, value) { if (isUndefined(value)) { var cs = getComputedStyle(node); var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom); var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth); return node.offsetHeight - padding - border; } css(node, 'height', value); } /** * Triggers a custom event with the specified name and * sets the detail property to the data object passed. * * @param {HTMLElement} node * @param {string} eventName * @param {Object} [data] */ function trigger(node, eventName, data) { var event; if (isFunction(window.CustomEvent)) { event = new CustomEvent(eventName, { bubbles: true, cancelable: true, detail: data }); } else { event = node.ownerDocument.createEvent('CustomEvent'); event.initCustomEvent(eventName, true, true, data); } node.dispatchEvent(event); } /** * Returns if a node is visible. * * @param {HTMLElement} * @returns {boolean} */ function isVisible(node) { return !!node.getClientRects().length; } /** * Convert CSS property names into camel case * * @param {string} string * @returns {string} */ function camelCase(string) { return string .replace(/^-ms-/, 'ms-') .replace(/-(\w)/g, function (match, char) { return char.toUpperCase(); }); } /** * Loop all child nodes of the passed node * * The function should accept 1 parameter being the node. * If the function returns false the loop will be exited. * * @param {HTMLElement} node * @param {function} func Callback which is called with every * child node as the first argument. * @param {boolean} innermostFirst If the innermost node should be passed * to the function before it's parents. * @param {boolean} siblingsOnly If to only traverse the nodes siblings * @param {boolean} [reverse=false] If to traverse the nodes in reverse */ // eslint-disable-next-line max-params function traverse(node, func, innermostFirst, siblingsOnly, reverse) { node = reverse ? node.lastChild : node.firstChild; while (node) { var next = reverse ? node.previousSibling : node.nextSibling; if ( (!innermostFirst && func(node) === false) || (!siblingsOnly && traverse( node, func, innermostFirst, siblingsOnly, reverse ) === false) || (innermostFirst && func(node) === false) ) { return false; } node = next; } } /** * Like traverse but loops in reverse * @see traverse */ function rTraverse(node, func, innermostFirst, siblingsOnly) { traverse(node, func, innermostFirst, siblingsOnly, true); } /** * Parses HTML into a document fragment * * @param {string} html * @param {Document} [context] * @since 1.4.4 * @return {DocumentFragment} */ function parseHTML(html, context) { context = context || document; var ret = context.createDocumentFragment(); var tmp = createElement('div', {}, context); tmp.innerHTML = html; while (tmp.firstChild) { appendChild(ret, tmp.firstChild); } return ret; } /** * Checks if an element has any styling. * * It has styling if it is not a plain
or

or * if it has a class, style attribute or data. * * @param {HTMLElement} elm * @return {boolean} * @since 1.4.4 */ function hasStyling(node) { return node && (!is(node, 'p,div') || node.className || attr(node, 'style') || !isEmptyObject(data(node))); } /** * Converts an element from one type to another. * * For example it can convert the element to * * @param {HTMLElement} element * @param {string} toTagName * @return {HTMLElement} * @since 1.4.4 */ function convertElement(element, toTagName) { var newElement = createElement(toTagName, {}, element.ownerDocument); each(element.attributes, function (_, attribute) { // Some browsers parse invalid attributes names like // 'size"2' which throw an exception when set, just // ignore these. try { attr(newElement, attribute.name, attribute.value); } catch (ex) {} }); while (element.firstChild) { appendChild(newElement, element.firstChild); } element.parentNode.replaceChild(newElement, element); return newElement; } /** * List of block level elements separated by bars (|) * * @type {string} */ var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' + 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|' + 'details|section|article|aside|nav|main|header|hgroup|footer|fieldset|' + 'dl|dt|dd|figure|figcaption|'; /** * List of elements that do not allow children separated by bars (|) * * @param {Node} node * @return {boolean} * @since 1.4.5 */ function canHaveChildren(node) { // 1 = Element // 9 = Document // 11 = Document Fragment if (!/11?|9/.test(node.nodeType)) { return false; } // List of empty HTML tags separated by bar (|) character. // Source: http://www.w3.org/TR/html4/index/elements.html // Source: http://www.w3.org/TR/html5/syntax.html#void-elements return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' + '|isindex|link|meta|param|command|embed|keygen|source|track|' + 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0; } /** * Checks if an element is inline * * @param {HTMLElement} elm * @param {boolean} [includeCodeAsBlock=false] * @return {boolean} */ function isInline(elm, includeCodeAsBlock) { var tagName, nodeType = (elm || {}).nodeType || TEXT_NODE; if (nodeType !== ELEMENT_NODE) { return nodeType === TEXT_NODE; } tagName = elm.tagName.toLowerCase(); if (tagName === 'code') { return !includeCodeAsBlock; } return blockLevelList.indexOf('|' + tagName + '|') < 0; } /** * Copy the CSS from 1 node to another. * * Only copies CSS defined on the element e.g. style attr. * * @param {HTMLElement} from * @param {HTMLElement} to * @deprecated since v3.1.0 */ function copyCSS(from, to) { if (to.style && from.style) { to.style.cssText = from.style.cssText + to.style.cssText; } } /** * Checks if a DOM node is empty * * @param {Node} node * @returns {boolean} */ function isEmpty(node) { if (node.lastChild && isEmpty(node.lastChild)) { remove(node.lastChild); } return node.nodeType === 3 ? !node.nodeValue : (canHaveChildren(node) && !node.childNodes.length); } /** * Fixes block level elements inside in inline elements. * * Also fixes invalid list nesting by placing nested lists * inside the previous li tag or wrapping them in an li tag. * * @param {HTMLElement} node */ function fixNesting(node) { traverse(node, function (node) { var list = 'ul,ol', isBlock = !isInline(node, true) && node.nodeType !== COMMENT_NODE, parent = node.parentNode; // Any blocklevel element inside an inline element needs fixing. // Also

tags that contain blocks should be fixed if (isBlock && (isInline(parent, true) || parent.tagName === 'P')) { // Find the last inline parent node var lastInlineParent = node; while (isInline(lastInlineParent.parentNode, true) || lastInlineParent.parentNode.tagName === 'P') { lastInlineParent = lastInlineParent.parentNode; } var before = extractContents(lastInlineParent, node); var middle = node; // Clone inline styling and apply it to the blocks children while (parent && isInline(parent, true)) { if (parent.nodeType === ELEMENT_NODE) { var clone = parent.cloneNode(); while (middle.firstChild) { appendChild(clone, middle.firstChild); } appendChild(middle, clone); } parent = parent.parentNode; } insertBefore(middle, lastInlineParent); if (!isEmpty(before)) { insertBefore(before, middle); } if (isEmpty(lastInlineParent)) { remove(lastInlineParent); } } // Fix invalid nested lists which should be wrapped in an li tag if (isBlock && is(node, list) && is(node.parentNode, list)) { var li = previousElementSibling(node, 'li'); if (!li) { li = createElement('li'); insertBefore(li, node); } appendChild(li, node); } }); } /** * Finds the common parent of two nodes * * @param {!HTMLElement} node1 * @param {!HTMLElement} node2 * @return {?HTMLElement} */ function findCommonAncestor(node1, node2) { while ((node1 = node1.parentNode)) { if (contains(node1, node2)) { return node1; } } } /** * @param {?Node} * @param {boolean} [previous=false] * @returns {?Node} */ function getSibling(node, previous) { if (!node) { return null; } return (previous ? node.previousSibling : node.nextSibling) || getSibling(node.parentNode, previous); } /** * Removes unused whitespace from the root and all it's children. * * @param {!HTMLElement} root * @since 1.4.3 */ function removeWhiteSpace(root) { var nodeValue, nodeType, next, previous, previousSibling, nextNode, trimStart, cssWhiteSpace = css(root, 'whiteSpace'), // Preserve newlines if is pre-line preserveNewLines = /line$/i.test(cssWhiteSpace), node = root.firstChild; // Skip pre & pre-wrap with any vendor prefix if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) { return; } while (node) { nextNode = node.nextSibling; nodeValue = node.nodeValue; nodeType = node.nodeType; if (nodeType === ELEMENT_NODE && node.firstChild) { removeWhiteSpace(node); } if (nodeType === TEXT_NODE) { next = getSibling(node); previous = getSibling(node, true); trimStart = false; while (hasClass(previous, 'sceditor-ignore')) { previous = getSibling(previous, true); } // If previous sibling isn't inline or is a textnode that // ends in whitespace, time the start whitespace if (isInline(node) && previous) { previousSibling = previous; while (previousSibling.lastChild) { previousSibling = previousSibling.lastChild; // eslint-disable-next-line max-depth while (hasClass(previousSibling, 'sceditor-ignore')) { previousSibling = getSibling(previousSibling, true); } } trimStart = previousSibling.nodeType === TEXT_NODE ? /[\t\n\r ]$/.test(previousSibling.nodeValue) : !isInline(previousSibling); } // Clear zero width spaces nodeValue = nodeValue.replace(/\u200B/g, ''); // Strip leading whitespace if (!previous || !isInline(previous) || trimStart) { nodeValue = nodeValue.replace( preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/, '' ); } // Strip trailing whitespace if (!next || !isInline(next)) { nodeValue = nodeValue.replace( preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/, '' ); } // Remove empty text nodes if (!nodeValue.length) { remove(node); } else { node.nodeValue = nodeValue.replace( preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g, ' ' ); } } node = nextNode; } } /** * Extracts all the nodes between the start and end nodes * * @param {HTMLElement} startNode The node to start extracting at * @param {HTMLElement} endNode The node to stop extracting at * @return {DocumentFragment} */ function extractContents(startNode, endNode) { var range = startNode.ownerDocument.createRange(); range.setStartBefore(startNode); range.setEndAfter(endNode); return range.extractContents(); } /** * Gets the offset position of an element * * @param {HTMLElement} node * @return {Object} An object with left and top properties */ function getOffset(node) { var left = 0, top = 0; while (node) { left += node.offsetLeft; top += node.offsetTop; node = node.offsetParent; } return { left: left, top: top }; } /** * Gets the value of a CSS property from the elements style attribute * * @param {HTMLElement} elm * @param {string} property * @return {string} */ function getStyle(elm, property) { var styleValue, elmStyle = elm.style; if (!cssPropertyNameCache[property]) { cssPropertyNameCache[property] = camelCase(property); } property = cssPropertyNameCache[property]; styleValue = elmStyle[property]; // Add an exception for text-align if ('textAlign' === property) { styleValue = styleValue || css(elm, property); if (css(elm.parentNode, property) === styleValue || css(elm, 'display') !== 'block' || is(elm, 'hr,th')) { return ''; } } return styleValue; } /** * Tests if an element has a style. * * If values are specified it will check that the styles value * matches one of the values * * @param {HTMLElement} elm * @param {string} property * @param {string|array} [values] * @return {boolean} */ function hasStyle(elm, property, values) { var styleValue = getStyle(elm, property); if (!styleValue) { return false; } return !values || styleValue === values || (Array.isArray(values) && values.indexOf(styleValue) > -1); } /** * Returns true if both nodes have the same number of inline styles and all the * inline styles have matching values * * @param {HTMLElement} nodeA * @param {HTMLElement} nodeB * @returns {boolean} */ function stylesMatch(nodeA, nodeB) { var i = nodeA.style.length; if (i !== nodeB.style.length) { return false; } while (i--) { var prop = nodeA.style[i]; if (nodeA.style[prop] !== nodeB.style[prop]) { return false; } } return true; } /** * Returns true if both nodes have the same number of attributes and all the * attribute values match * * @param {HTMLElement} nodeA * @param {HTMLElement} nodeB * @returns {boolean} */ function attributesMatch(nodeA, nodeB) { var i = nodeA.attributes.length; if (i !== nodeB.attributes.length) { return false; } while (i--) { var prop = nodeA.attributes[i]; var notMatches = prop.name === 'style' ? !stylesMatch(nodeA, nodeB) : prop.value !== attr(nodeB, prop.name); if (notMatches) { return false; } } return true; } /** * Removes an element placing its children in its place * * @param {HTMLElement} node */ function removeKeepChildren(node) { while (node.firstChild) { insertBefore(node.firstChild, node); } remove(node); } /** * Merges inline styles and tags with parents where possible * * @param {Node} node * @since 3.1.0 */ function merge(node) { if (node.nodeType !== ELEMENT_NODE) { return; } var parent = node.parentNode; var tagName = node.tagName; var mergeTags = /B|STRONG|EM|SPAN|FONT/; // Merge children (in reverse as children can be removed) var i = node.childNodes.length; while (i--) { merge(node.childNodes[i]); } // Should only merge inline tags and should not merge
tags if (!isInline(node) || tagName === 'BR') { return; } // Remove any inline styles that match the parent style i = node.style.length; while (i--) { var prop = node.style[i]; if (css(parent, prop) === css(node, prop)) { node.style.removeProperty(prop); } } // Can only remove / merge tags if no inline styling left. // If there is any inline style left then it means it at least partially // doesn't match the parent style so must stay if (!node.style.length) { removeAttr(node, 'style'); // Remove font attributes if match parent if (tagName === 'FONT') { if (css(node, 'fontFamily').toLowerCase() === css(parent, 'fontFamily').toLowerCase()) { removeAttr(node, 'face'); } if (css(node, 'color') === css(parent, 'color')) { removeAttr(node, 'color'); } if (css(node, 'fontSize') === css(parent, 'fontSize')) { removeAttr(node, 'size'); } } // Spans and font tags with no attributes can be safely removed if (!node.attributes.length && /SPAN|FONT/.test(tagName)) { removeKeepChildren(node); } else if (mergeTags.test(tagName)) { var isBold = /B|STRONG/.test(tagName); var isItalic = tagName === 'EM'; while (parent && isInline(parent) && (!isBold || /bold|700/i.test(css(parent, 'fontWeight'))) && (!isItalic || css(parent, 'fontStyle') === 'italic')) { // Remove if parent match if ((parent.tagName === tagName || (isBold && /B|STRONG/.test(parent.tagName))) && attributesMatch(parent, node)) { removeKeepChildren(node); break; } parent = parent.parentNode; } } } // Merge siblings if attributes, including inline styles, match var next = node.nextSibling; if (next && next.tagName === tagName && attributesMatch(next, node)) { appendChild(node, next); removeKeepChildren(next); } } /** * Default options for SCEditor * @type {Object} */ var defaultOptions = { /** @lends jQuery.sceditor.defaultOptions */ /** * Toolbar buttons order and groups. Should be comma separated and * have a bar | to separate groups * * @type {string} */ toolbar: 'bold,italic,underline,strike,subscript,superscript|' + 'left,center,right,justify|font,size,color,removeformat|' + 'cut,copy,pastetext|bulletlist,orderedlist,indent,outdent|' + 'table|code,mono,quote|horizontalrule,image,email,link,unlink|' + 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', /** * Comma separated list of commands to excludes from the toolbar * * @type {string} */ toolbarExclude: null, /** * Stylesheet to include in the WYSIWYG editor. This is what will style * the WYSIWYG elements * * @type {string} */ style: 'jquery.sceditor.default.css', /** * Comma separated list of fonts for the font selector * * @type {string} */ fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', /** * Colors should be comma separated and have a bar | to signal a new * column. * * If null the colors will be auto generated. * * @type {string} */ colors: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' + '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' + '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' + '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' + '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' + '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' + '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' + '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef', /** * The locale to use. * @type {string} */ locale: attr(document.documentElement, 'lang') || 'en', /** * The Charset to use * @type {string} */ charset: 'utf-8', /** * Compatibility mode for emoticons. * * Helps if you have emoticons such as :/ which would put an emoticon * inside http:// * * This mode requires emoticons to be surrounded by whitespace or end of * line chars. This mode has limited As You Type emoticon conversion * support. It will not replace AYT for end of line chars, only * emoticons surrounded by whitespace. They will still be replaced * correctly when loaded just not AYT. * * @type {boolean} */ emoticonsCompat: false, /** * If to enable emoticons. Can be changes at runtime using the * emoticons() method. * * @type {boolean} * @since 1.4.2 */ emoticonsEnabled: true, /** * Emoticon root URL * * @type {string} */ emoticonsRoot: '', emoticons: { dropdown: { ':)': 'emoticons/smile.png', ':angel:': 'emoticons/angel.png', ':angry:': 'emoticons/angry.png', '8-)': 'emoticons/cool.png', ':\'(': 'emoticons/cwy.png', ':ermm:': 'emoticons/ermm.png', ':D': 'emoticons/grin.png', '<3': 'emoticons/heart.png', ':(': 'emoticons/sad.png', ':O': 'emoticons/shocked.png', ':P': 'emoticons/tongue.png', ';)': 'emoticons/wink.png' }, more: { ':alien:': 'emoticons/alien.png', ':blink:': 'emoticons/blink.png', ':blush:': 'emoticons/blush.png', ':cheerful:': 'emoticons/cheerful.png', ':devil:': 'emoticons/devil.png', ':dizzy:': 'emoticons/dizzy.png', ':getlost:': 'emoticons/getlost.png', ':happy:': 'emoticons/happy.png', ':kissing:': 'emoticons/kissing.png', ':ninja:': 'emoticons/ninja.png', ':pinch:': 'emoticons/pinch.png', ':pouty:': 'emoticons/pouty.png', ':sick:': 'emoticons/sick.png', ':sideways:': 'emoticons/sideways.png', ':silly:': 'emoticons/silly.png', ':sleeping:': 'emoticons/sleeping.png', ':unsure:': 'emoticons/unsure.png', ':woot:': 'emoticons/w00t.png', ':wassat:': 'emoticons/wassat.png' }, hidden: { ':whistling:': 'emoticons/whistling.png', ':love:': 'emoticons/wub.png' } }, /** * Width of the editor. Set to null for automatic with * * @type {?number} */ width: null, /** * Height of the editor including toolbar. Set to null for automatic * height * * @type {?number} */ height: null, /** * If to allow the editor to be resized * * @type {boolean} */ resizeEnabled: true, /** * Min resize to width, set to null for half textarea width or -1 for * unlimited * * @type {?number} */ resizeMinWidth: null, /** * Min resize to height, set to null for half textarea height or -1 for * unlimited * * @type {?number} */ resizeMinHeight: null, /** * Max resize to height, set to null for double textarea height or -1 * for unlimited * * @type {?number} */ resizeMaxHeight: null, /** * Max resize to width, set to null for double textarea width or -1 for * unlimited * * @type {?number} */ resizeMaxWidth: null, /** * If resizing by height is enabled * * @type {boolean} */ resizeHeight: true, /** * If resizing by width is enabled * * @type {boolean} */ resizeWidth: true, /** * Date format, will be overridden if locale specifies one. * * The words year, month and day will be replaced with the users current * year, month and day. * * @type {string} */ dateFormat: 'year-month-day', /** * Element to inset the toolbar into. * * @type {HTMLElement} */ toolbarContainer: null, /** * If to enable paste filtering. This is currently experimental, please * report any issues. * * @type {boolean} */ enablePasteFiltering: false, /** * If to completely disable pasting into the editor * * @type {boolean} */ disablePasting: false, /** * If the editor is read only. * * @type {boolean} */ readOnly: false, /** * If to set the editor to right-to-left mode. * * If set to null the direction will be automatically detected. * * @type {boolean} */ rtl: false, /** * If to auto focus the editor on page load * * @type {boolean} */ autofocus: false, /** * If to auto focus the editor to the end of the content * * @type {boolean} */ autofocusEnd: true, /** * If to auto expand the editor to fix the content * * @type {boolean} */ autoExpand: false, /** * If to auto update original textbox on blur * * @type {boolean} */ autoUpdate: false, /** * If to enable the browsers built in spell checker * * @type {boolean} */ spellcheck: true, /** * If to run the source editor when there is no WYSIWYG support. Only * really applies to mobile OS's. * * @type {boolean} */ runWithoutWysiwygSupport: false, /** * If to load the editor in source mode and still allow switching * between WYSIWYG and source mode * * @type {boolean} */ startInSourceMode: false, /** * Optional ID to give the editor. * * @type {string} */ id: null, /** * Comma separated list of plugins * * @type {string} */ plugins: '', /** * z-index to set the editor container to. Needed for jQuery UI dialog. * * @type {?number} */ zIndex: null, /** * If to trim the BBCode. Removes any spaces at the start and end of the * BBCode string. * * @type {boolean} */ bbcodeTrim: false, /** * If to disable removing block level elements by pressing backspace at * the start of them * * @type {boolean} */ disableBlockRemove: false, /** * Array of allowed URL (should be either strings or regex) for iframes. * * If it's a string then iframes where the start of the src matches the * specified string will be allowed. * * If it's a regex then iframes where the src matches the regex will be * allowed. * * @type {Array} */ allowedIframeUrls: [], /** * BBCode parser options, only applies if using the editor in BBCode * mode. * * See SCEditor.BBCodeParser.defaults for list of valid options * * @type {Object} */ parserOptions: { }, /** * CSS that will be added to the to dropdown menu (eg. z-index) * * @type {Object} */ dropDownCss: { }, /** * An array of tags that are allowed in the editor content. * If a tag is not listed here, it will be removed when the content is * sanitized. * * 1 Tag is already added by default: ['iframe']. No need to add this * further. * * @type {Array} */ allowedTags: [], /** * An array of attributes that are allowed on tags in the editor content. * If an attribute is not listed here, it will be removed when the content * is sanitized. * * 3 Attributes are already added by default: * ['allowfullscreen', 'frameborder', 'target']. * No need to add these further. * * @type {Array} */ allowedAttributes: [] }; // Must start with a valid scheme // ^ // Schemes that are considered safe // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):| // Relative schemes (//:) are considered safe // (\\/\\/)| // Image data URI's are considered safe // data:image\\/(png|bmp|gif|p?jpe?g); var VALID_SCHEME_REGEX = /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i; /** * Escapes a string so it's safe to use in regex * * @param {string} str * @return {string} */ function regex(str) { return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); } /** * Escapes all HTML entities in a string * * If noQuotes is set to false, all single and double * quotes will also be escaped * * @param {string} str * @param {boolean} [noQuotes=true] * @return {string} * @since 1.4.1 */ function entities(str, noQuotes) { if (!str) { return str; } var replacements = { '&': '&', '<': '<', '>': '>', ' ': '  ', '\r\n': '
', '\r': '
', '\n': '
' }; if (noQuotes !== false) { replacements['"'] = '"'; replacements['\''] = '''; replacements['`'] = '`'; } str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) { return replacements[match] || match; }); return str; } /** * Escape URI scheme. * * Appends the current URL to a url if it has a scheme that is not: * * http * https * sftp * ftp * mailto * spotify * skype * ssh * teamspeak * tel * // * data:image/(png|jpeg|jpg|pjpeg|bmp|gif); * * **IMPORTANT**: This does not escape any HTML in a url, for * that use the escape.entities() method. * * @param {string} url * @return {string} * @since 1.4.5 */ function uriScheme(url) { var path, // If there is a : before a / then it has a scheme hasScheme = /^[^\/]*:/i, location = window.location; // Has no scheme or a valid scheme if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) { return url; } path = location.pathname.split('/'); path.pop(); return location.protocol + '//' + location.host + path.join('/') + '/' + url; } /** * HTML templates used by the editor and default commands * @type {Object} * @private */ var _templates = { html: '' + '' + '' + '' + '' + '' + '

' + '', toolbarButton: '' + '
{dispName}
', emoticon: '', fontOpt: '{font}', sizeOpt: '{size}', pastetext: '
' + '
' + '
' + '
', table: '
' + '
' + '
', image: '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
', email: '
' + '
' + '
' + '
' + '
' + '
', link: '
' + '
' + '
' + '
' + '
', youtubeMenu: '
' + '
' + '
' + '
', youtube: '' }; /** * Replaces any params in a template with the passed params. * * If createHtml is passed it will return a DocumentFragment * containing the parsed template. * * @param {string} name * @param {Object} [params] * @param {boolean} [createHtml] * @returns {string|DocumentFragment} * @private */ function _tmpl (name, params, createHtml) { var template = _templates[name]; Object.keys(params).forEach(function (name) { template = template.replace( new RegExp(regex('{' + name + '}'), 'g'), params[name] ); }); if (createHtml) { template = parseHTML(template); } return template; } /** * Fixes a bug in FF where it sometimes wraps * new lines in their own list item. * See issue #359 */ function fixFirefoxListBug(editor) { // Only apply to Firefox as will break other browsers. if ('mozHidden' in document) { var node = editor.getBody(); var next; while (node) { next = node; if (next.firstChild) { next = next.firstChild; } else { while (next && !next.nextSibling) { next = next.parentNode; } if (next) { next = next.nextSibling; } } if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) { // Only remove if newlines are collapsed if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) { remove(node); } } node = next; } } } /** * Map of all the commands for SCEditor * @type {Object} * @name commands * @memberOf jQuery.sceditor */ var defaultCmds = { // START_COMMAND: Bold bold: { exec: 'bold', tooltip: 'Bold', shortcut: 'Ctrl+B' }, // END_COMMAND // START_COMMAND: Italic italic: { exec: 'italic', tooltip: 'Italic', shortcut: 'Ctrl+I' }, // END_COMMAND // START_COMMAND: Underline underline: { exec: 'underline', tooltip: 'Underline', shortcut: 'Ctrl+U' }, // END_COMMAND // START_COMMAND: Strikethrough strike: { exec: 'strikethrough', tooltip: 'Strikethrough' }, // END_COMMAND // START_COMMAND: Subscript subscript: { exec: 'subscript', tooltip: 'Subscript' }, // END_COMMAND // START_COMMAND: Superscript superscript: { exec: 'superscript', tooltip: 'Superscript' }, // END_COMMAND // START_COMMAND: Left left: { state: function (node) { if (node && node.nodeType === 3) { node = node.parentNode; } if (node) { var isLtr = css(node, 'direction') === 'ltr'; var align = css(node, 'textAlign'); // Can be -moz-left return /left/.test(align) || align === (isLtr ? 'start' : 'end'); } }, exec: 'justifyleft', tooltip: 'Align left' }, // END_COMMAND // START_COMMAND: Centre center: { exec: 'justifycenter', tooltip: 'Center' }, // END_COMMAND // START_COMMAND: Right right: { state: function (node) { if (node && node.nodeType === 3) { node = node.parentNode; } if (node) { var isLtr = css(node, 'direction') === 'ltr'; var align = css(node, 'textAlign'); // Can be -moz-right return /right/.test(align) || align === (isLtr ? 'end' : 'start'); } }, exec: 'justifyright', tooltip: 'Align right' }, // END_COMMAND // START_COMMAND: Justify justify: { exec: 'justifyfull', tooltip: 'Justify' }, // END_COMMAND // START_COMMAND: Font font: { _dropDown: function (editor, caller, callback) { var content = createElement('div'); on(content, 'click', 'a', function (e) { callback(data(this, 'font')); editor.closeDropDown(true); e.preventDefault(); }); editor.opts.fonts.split(',').forEach(function (font) { appendChild(content, _tmpl('fontOpt', { font: font }, true)); }); editor.createDropDown(caller, 'font-picker', content); }, exec: function (caller) { var editor = this; defaultCmds.font._dropDown(editor, caller, function (fontName) { editor.execCommand('fontname', fontName); }); }, tooltip: 'Font Name' }, // END_COMMAND // START_COMMAND: Size size: { _dropDown: function (editor, caller, callback) { var content = createElement('div'); on(content, 'click', 'a', function (e) { callback(data(this, 'size')); editor.closeDropDown(true); e.preventDefault(); }); for (var i = 1; i <= 7; i++) { appendChild(content, _tmpl('sizeOpt', { size: i }, true)); } editor.createDropDown(caller, 'fontsize-picker', content); }, exec: function (caller) { var editor = this; defaultCmds.size._dropDown(editor, caller, function (fontSize) { editor.execCommand('fontsize', fontSize); }); }, tooltip: 'Font Size' }, // END_COMMAND // START_COMMAND: Colour color: { _dropDown: function (editor, caller, callback) { var content = createElement('div'), html = '', cmd = defaultCmds.color; if (!cmd._htmlCache) { editor.opts.colors.split('|').forEach(function (column) { html += '
'; column.split(',').forEach(function (color) { html += ''; }); html += '
'; }); cmd._htmlCache = html; } appendChild(content, parseHTML(cmd._htmlCache)); on(content, 'click', 'a', function (e) { callback(data(this, 'color')); editor.closeDropDown(true); e.preventDefault(); }); editor.createDropDown(caller, 'color-picker', content); }, exec: function (caller) { var editor = this; defaultCmds.color._dropDown(editor, caller, function (color) { editor.execCommand('forecolor', color); }); }, tooltip: 'Font Color' }, // END_COMMAND // START_COMMAND: Remove Format removeformat: { exec: 'removeformat', tooltip: 'Remove Formatting' }, // END_COMMAND // START_COMMAND: Cut cut: { exec: 'cut', tooltip: 'Cut', errorMessage: 'Your browser does not allow the cut command. ' + 'Please use the keyboard shortcut Ctrl/Cmd-X' }, // END_COMMAND // START_COMMAND: Copy copy: { exec: 'copy', tooltip: 'Copy', errorMessage: 'Your browser does not allow the copy command. ' + 'Please use the keyboard shortcut Ctrl/Cmd-C' }, // END_COMMAND // START_COMMAND: Paste paste: { exec: 'paste', tooltip: 'Paste', errorMessage: 'Your browser does not allow the paste command. ' + 'Please use the keyboard shortcut Ctrl/Cmd-V' }, // END_COMMAND // START_COMMAND: Paste Text pastetext: { exec: function (caller) { var val, content = createElement('div'), editor = this; appendChild(content, _tmpl('pastetext', { label: editor._( 'Paste your text inside the following box:' ), insert: editor._('Insert') }, true)); on(content, 'click', '.button', function (e) { val = find(content, '#txt')[0].value; if (val) { editor.wysiwygEditorInsertText(val); } editor.closeDropDown(true); e.preventDefault(); }); editor.createDropDown(caller, 'pastetext', content); }, tooltip: 'Paste Text' }, // END_COMMAND // START_COMMAND: Bullet List bulletlist: { exec: function () { fixFirefoxListBug(this); this.execCommand('insertunorderedlist'); }, tooltip: 'Bullet list' }, // END_COMMAND // START_COMMAND: Ordered List orderedlist: { exec: function () { fixFirefoxListBug(this); this.execCommand('insertorderedlist'); }, tooltip: 'Numbered list' }, // END_COMMAND // START_COMMAND: Indent indent: { state: function (parent, firstBlock) { // Only works with lists, for now var range, startParent, endParent; if (is(firstBlock, 'li')) { return 0; } if (is(firstBlock, 'ul,ol,menu')) { // if the whole list is selected, then this must be // invalidated because the browser will place a //
there range = this.getRangeHelper().selectedRange(); startParent = range.startContainer.parentNode; endParent = range.endContainer.parentNode; // TODO: could use nodeType for this? // Maybe just check the firstBlock contains both the start //and end containers // Select the tag, not the textNode // (that's why the parentNode) if (startParent !== startParent.parentNode.firstElementChild || // work around a bug in FF (is(endParent, 'li') && endParent !== endParent.parentNode.lastElementChild)) { return 0; } } return -1; }, exec: function () { var editor = this, block = editor.getRangeHelper().getFirstBlockParent(); editor.focus(); // An indent system is quite complicated as there are loads // of complications and issues around how to indent text // As default, let's just stay with indenting the lists, // at least, for now. if (closest(block, 'ul,ol,menu')) { editor.execCommand('indent'); } }, tooltip: 'Add indent' }, // END_COMMAND // START_COMMAND: Outdent outdent: { state: function (parents, firstBlock) { return closest(firstBlock, 'ul,ol,menu') ? 0 : -1; }, exec: function () { var block = this.getRangeHelper().getFirstBlockParent(); if (closest(block, 'ul,ol,menu')) { this.execCommand('outdent'); } }, tooltip: 'Remove one indent' }, // END_COMMAND // START_COMMAND: Table table: { exec: function (caller) { var editor = this, content = createElement('div'); appendChild(content, _tmpl('table', { rows: editor._('Rows:'), cols: editor._('Cols:'), insert: editor._('Insert') }, true)); on(content, 'click', '.button', function (e) { var rows = Number(find(content, '#rows')[0].value), cols = Number(find(content, '#cols')[0].value), html = ''; if (rows > 0 && cols > 0) { html += Array(rows + 1).join( '' + Array(cols + 1).join( '' ) + '' ); html += '

'; editor.wysiwygEditorInsertHtml(html); editor.closeDropDown(true); e.preventDefault(); } }); editor.createDropDown(caller, 'inserttable', content); }, tooltip: 'Insert a table' }, // END_COMMAND // START_COMMAND: Horizontal Rule horizontalrule: { exec: 'inserthorizontalrule', tooltip: 'Insert a horizontal rule' }, // END_COMMAND // START_COMMAND: Code code: { exec: function () { this.wysiwygEditorInsertHtml( '', '
' ); }, tooltip: 'Code' }, // END_COMMAND // START_COMMAND: Image image: { _dropDown: function (editor, caller, selected, cb) { var content = createElement('div'); appendChild(content, _tmpl('image', { url: editor._('URL:'), width: editor._('Width (optional):'), height: editor._('Height (optional):'), insert: editor._('Insert') }, true)); var urlInput = find(content, '#image')[0]; urlInput.value = selected; on(content, 'click', '.button', function (e) { if (urlInput.value) { cb( urlInput.value, find(content, '#width')[0].value, find(content, '#height')[0].value ); } editor.closeDropDown(true); e.preventDefault(); }); editor.createDropDown(caller, 'insertimage', content); }, exec: function (caller) { var editor = this; defaultCmds.image._dropDown( editor, caller, '', function (url, width, height) { var attrs = ''; if (width) { attrs += ' width="' + parseInt(width, 10) + '"'; } if (height) { attrs += ' height="' + parseInt(height, 10) + '"'; } attrs += ' src="' + entities(url) + '"'; editor.wysiwygEditorInsertHtml( '' ); } ); }, tooltip: 'Insert an image' }, // END_COMMAND // START_COMMAND: E-mail email: { _dropDown: function (editor, caller, cb) { var content = createElement('div'); appendChild(content, _tmpl('email', { label: editor._('E-mail:'), desc: editor._('Description (optional):'), insert: editor._('Insert') }, true)); on(content, 'click', '.button', function (e) { var email = find(content, '#email')[0].value; if (email) { cb(email, find(content, '#des')[0].value); } editor.closeDropDown(true); e.preventDefault(); }); editor.createDropDown(caller, 'insertemail', content); }, exec: function (caller) { var editor = this; defaultCmds.email._dropDown( editor, caller, function (email, text) { if (!editor.getRangeHelper().selectedHtml() || text) { editor.wysiwygEditorInsertHtml( '' + entities((text || email)) + '' ); } else { editor.execCommand('createlink', 'mailto:' + email); } } ); }, tooltip: 'Insert an email' }, // END_COMMAND // START_COMMAND: Link link: { _dropDown: function (editor, caller, cb) { var content = createElement('div'); appendChild(content, _tmpl('link', { url: editor._('URL:'), desc: editor._('Description (optional):'), ins: editor._('Insert') }, true)); var linkInput = find(content, '#link')[0]; function insertUrl(e) { if (linkInput.value) { cb(linkInput.value, find(content, '#des')[0].value); } editor.closeDropDown(true); e.preventDefault(); } on(content, 'click', '.button', insertUrl); on(content, 'keypress', function (e) { // 13 = enter key if (e.which === 13 && linkInput.value) { insertUrl(e); } }, EVENT_CAPTURE); editor.createDropDown(caller, 'insertlink', content); }, exec: function (caller) { var editor = this; defaultCmds.link._dropDown(editor, caller, function (url, text) { if (text || !editor.getRangeHelper().selectedHtml()) { editor.wysiwygEditorInsertHtml( '' + entities(text || url) + '' ); } else { editor.execCommand('createlink', url); } }); }, tooltip: 'Insert a link' }, // END_COMMAND // START_COMMAND: Unlink unlink: { state: function () { return closest(this.currentNode(), 'a') ? 0 : -1; }, exec: function () { var anchor = closest(this.currentNode(), 'a'); if (anchor) { while (anchor.firstChild) { insertBefore(anchor.firstChild, anchor); } remove(anchor); } }, tooltip: 'Unlink' }, // END_COMMAND // START_COMMAND: Quote quote: { exec: function (caller, html, author) { var before = '
', end = '
'; // if there is HTML passed set end to null so any selected // text is replaced if (html) { author = (author ? '' + entities(author) + '' : ''); before = before + author + html + end; end = null; // if not add a newline to the end of the inserted quote } else if (this.getRangeHelper().selectedHtml() === '') { end = '
' + end; } this.wysiwygEditorInsertHtml(before, end); }, tooltip: 'Insert a Quote' }, // END_COMMAND // START_COMMAND: Emoticons emoticon: { exec: function (caller) { var editor = this; var createContent = function (includeMore) { var moreLink, opts = editor.opts, emoticonsRoot = opts.emoticonsRoot || '', emoticonsCompat = opts.emoticonsCompat, rangeHelper = editor.getRangeHelper(), startSpace = emoticonsCompat && rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '', endSpace = emoticonsCompat && rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '', content = createElement('div'), line = createElement('div'), perLine = 0, emoticons = extend( {}, opts.emoticons.dropdown, includeMore ? opts.emoticons.more : {} ); appendChild(content, line); perLine = Math.sqrt(Object.keys(emoticons).length); on(content, 'click', 'img', function (e) { editor.insert(startSpace + attr(this, 'alt') + endSpace, null, false).closeDropDown(true); e.preventDefault(); }); each(emoticons, function (code, emoticon) { appendChild(line, createElement('img', { src: emoticonsRoot + (emoticon.url || emoticon), alt: code, title: emoticon.tooltip || code })); if (line.children.length >= perLine) { line = createElement('div'); appendChild(content, line); } }); if (!includeMore && opts.emoticons.more) { moreLink = createElement('a', { className: 'sceditor-more' }); appendChild(moreLink, document.createTextNode(editor._('More'))); on(moreLink, 'click', function (e) { editor.createDropDown( caller, 'more-emoticons', createContent(true) ); e.preventDefault(); }); appendChild(content, moreLink); } return content; }; editor.createDropDown(caller, 'emoticons', createContent(false)); }, txtExec: function (caller) { defaultCmds.emoticon.exec.call(this, caller); }, tooltip: 'Insert an emoticon' }, // END_COMMAND // START_COMMAND: YouTube youtube: { _dropDown: function (editor, caller, callback) { var content = createElement('div'); appendChild(content, _tmpl('youtubeMenu', { label: editor._('Video URL:'), insert: editor._('Insert') }, true)); on(content, 'click', '.button', function (e) { var val = find(content, '#link')[0].value; var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)?([a-zA-Z0-9_-]{11})/); var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/); var time = 0; if (timeMatch) { each(timeMatch[1].split(/[hms]/), function (i, val) { if (val !== '') { time = (time * 60) + Number(val); } }); } if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) { callback(idMatch[1], time); } editor.closeDropDown(true); e.preventDefault(); }); editor.createDropDown(caller, 'insertlink', content); }, exec: function (btn) { var editor = this; defaultCmds.youtube._dropDown(editor, btn, function (id, time) { editor.wysiwygEditorInsertHtml(_tmpl('youtube', { id: id, time: time })); }); }, tooltip: 'Insert a YouTube video' }, // END_COMMAND // START_COMMAND: Date date: { _date: function (editor) { var now = new Date(), year = now.getYear(), month = now.getMonth() + 1, day = now.getDate(); if (year < 2000) { year = 1900 + year; } if (month < 10) { month = '0' + month; } if (day < 10) { day = '0' + day; } return editor.opts.dateFormat .replace(/year/i, year) .replace(/month/i, month) .replace(/day/i, day); }, exec: function () { this.insertText(defaultCmds.date._date(this)); }, txtExec: function () { this.insertText(defaultCmds.date._date(this)); }, tooltip: 'Insert current date' }, // END_COMMAND // START_COMMAND: Time time: { _time: function () { var now = new Date(), hours = now.getHours(), mins = now.getMinutes(), secs = now.getSeconds(); if (hours < 10) { hours = '0' + hours; } if (mins < 10) { mins = '0' + mins; } if (secs < 10) { secs = '0' + secs; } return hours + ':' + mins + ':' + secs; }, exec: function () { this.insertText(defaultCmds.time._time()); }, txtExec: function () { this.insertText(defaultCmds.time._time()); }, tooltip: 'Insert current time' }, // END_COMMAND // START_COMMAND: Ltr ltr: { state: function (parents, firstBlock) { return firstBlock && firstBlock.style.direction === 'ltr'; }, exec: function () { var editor = this, rangeHelper = editor.getRangeHelper(), node = rangeHelper.getFirstBlockParent(); editor.focus(); if (!node || is(node, 'body')) { editor.execCommand('formatBlock', 'p'); node = rangeHelper.getFirstBlockParent(); if (!node || is(node, 'body')) { return; } } var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr'; css(node, 'direction', toggleValue); }, tooltip: 'Left-to-Right' }, // END_COMMAND // START_COMMAND: Rtl rtl: { state: function (parents, firstBlock) { return firstBlock && firstBlock.style.direction === 'rtl'; }, exec: function () { var editor = this, rangeHelper = editor.getRangeHelper(), node = rangeHelper.getFirstBlockParent(); editor.focus(); if (!node || is(node, 'body')) { editor.execCommand('formatBlock', 'p'); node = rangeHelper.getFirstBlockParent(); if (!node || is(node, 'body')) { return; } } var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl'; css(node, 'direction', toggleValue); }, tooltip: 'Right-to-Left' }, // END_COMMAND // START_COMMAND: Print print: { exec: 'print', tooltip: 'Print' }, // END_COMMAND // START_COMMAND: Maximize maximize: { state: function () { return this.maximize(); }, exec: function () { this.maximize(!this.maximize()); this.focus(); }, txtExec: function () { this.maximize(!this.maximize()); this.focus(); }, tooltip: 'Maximize', shortcut: 'Ctrl+Shift+M' }, // END_COMMAND // START_COMMAND: Source source: { state: function () { return this.sourceMode(); }, exec: function () { this.toggleSourceMode(); this.focus(); }, txtExec: function () { this.toggleSourceMode(); this.focus(); }, tooltip: 'View source', shortcut: 'Ctrl+Shift+S' }, // END_COMMAND // START_COMMAND: Centre mono: { state: function (parents) { return !!closest(parents, 'span.f-bb-mono'); }, exec: function () { var editor = this, rangeHelper = editor.getRangeHelper(), mono = closest(rangeHelper.parentNode(), 'span.f-bb-mono'), range = rangeHelper.selectedRange(); editor.focus(); if (mono) { if (mono.nextSibling) { range.setStartBefore(mono.nextSibling); range.setEndBefore(mono.nextSibling); } } else { this.wysiwygEditorInsertHtml( '', '' ); } }, tooltip: 'Mono' }, // END_COMMAND // this is here so that commands above can be removed // without having to remove the , after the last one. // Needed for IE. ignore: {} }; var plugins = {}; /** * Plugin Manager class * @class PluginManager * @name PluginManager */ function PluginManager(thisObj) { /** * Alias of this * * @private * @type {Object} */ var base = this; /** * Array of all currently registered plugins * * @type {Array} * @private */ var registeredPlugins = []; /** * Changes a signals name from "name" into "signalName". * * @param {string} signal * @return {string} * @private */ var formatSignalName = function (signal) { return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1); }; /** * Calls handlers for a signal * * @see call() * @see callOnlyFirst() * @param {Array} args * @param {boolean} returnAtFirst * @return {*} * @private */ var callHandlers = function (args, returnAtFirst) { args = [].slice.call(args); var idx, ret, signal = formatSignalName(args.shift()); for (idx = 0; idx < registeredPlugins.length; idx++) { if (signal in registeredPlugins[idx]) { ret = registeredPlugins[idx][signal].apply(thisObj, args); if (returnAtFirst) { return ret; } } } }; /** * Calls all handlers for the passed signal * * @param {string} signal * @param {...string} args * @function * @name call * @memberOf PluginManager.prototype */ base.call = function () { callHandlers(arguments, false); }; /** * Calls the first handler for a signal, and returns the * * @param {string} signal * @param {...string} args * @return {*} The result of calling the handler * @function * @name callOnlyFirst * @memberOf PluginManager.prototype */ base.callOnlyFirst = function () { return callHandlers(arguments, true); }; /** * Checks if a signal has a handler * * @param {string} signal * @return {boolean} * @function * @name hasHandler * @memberOf PluginManager.prototype */ base.hasHandler = function (signal) { var i = registeredPlugins.length; signal = formatSignalName(signal); while (i--) { if (signal in registeredPlugins[i]) { return true; } } return false; }; /** * Checks if the plugin exists in plugins * * @param {string} plugin * @return {boolean} * @function * @name exists * @memberOf PluginManager.prototype */ base.exists = function (plugin) { if (plugin in plugins) { plugin = plugins[plugin]; return typeof plugin === 'function' && typeof plugin.prototype === 'object'; } return false; }; /** * Checks if the passed plugin is currently registered. * * @param {string} plugin * @return {boolean} * @function * @name isRegistered * @memberOf PluginManager.prototype */ base.isRegistered = function (plugin) { if (base.exists(plugin)) { var idx = registeredPlugins.length; while (idx--) { if (registeredPlugins[idx] instanceof plugins[plugin]) { return true; } } } return false; }; /** * Registers a plugin to receive signals * * @param {string} plugin * @return {boolean} * @function * @name register * @memberOf PluginManager.prototype */ base.register = function (plugin) { if (!base.exists(plugin) || base.isRegistered(plugin)) { return false; } plugin = new plugins[plugin](); registeredPlugins.push(plugin); if ('init' in plugin) { plugin.init.call(thisObj); } return true; }; /** * Deregisters a plugin. * * @param {string} plugin * @return {boolean} * @function * @name deregister * @memberOf PluginManager.prototype */ base.deregister = function (plugin) { var removedPlugin, pluginIdx = registeredPlugins.length, removed = false; if (!base.isRegistered(plugin)) { return removed; } while (pluginIdx--) { if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) { removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0]; removed = true; if ('destroy' in removedPlugin) { removedPlugin.destroy.call(thisObj); } } } return removed; }; /** * Clears all plugins and removes the owner reference. * * Calling any functions on this object after calling * destroy will cause a JS error. * * @name destroy * @memberOf PluginManager.prototype */ base.destroy = function () { var i = registeredPlugins.length; while (i--) { if ('destroy' in registeredPlugins[i]) { registeredPlugins[i].destroy.call(thisObj); } } registeredPlugins = []; thisObj = null; }; } PluginManager.plugins = plugins; /** * Gets the text, start/end node and offset for * length chars left or right of the passed node * at the specified offset. * * @param {Node} node * @param {number} offset * @param {boolean} isLeft * @param {number} length * @return {Object} * @private */ var outerText = function (range, isLeft, length) { var nodeValue, remaining, start, end, node, text = '', next = range.startContainer, offset = range.startOffset; // Handle cases where node is a paragraph and offset // refers to the index of a text node. // 3 = text node if (next && next.nodeType !== 3) { next = next.childNodes[offset]; offset = 0; } start = end = offset; while (length > text.length && next && next.nodeType === 3) { nodeValue = next.nodeValue; remaining = length - text.length; // If not the first node, start and end should be at their // max values as will be updated when getting the text if (node) { end = nodeValue.length; start = 0; } node = next; if (isLeft) { start = Math.max(end - remaining, 0); offset = start; text = nodeValue.substr(start, end - start) + text; next = node.previousSibling; } else { end = Math.min(remaining, nodeValue.length); offset = start + end; text += nodeValue.substr(start, end); next = node.nextSibling; } } return { node: node || next, offset: offset, text: text }; }; /** * Range helper * * @class RangeHelper * @name RangeHelper */ function RangeHelper(win, d, sanitize) { var _createMarker, _prepareInput, doc = d || win.contentDocument || win.document, startMarker = 'sceditor-start-marker', endMarker = 'sceditor-end-marker', base = this; /** * Inserts HTML into the current range replacing any selected * text. * * If endHTML is specified the selected contents will be put between * html and endHTML. If there is nothing selected html and endHTML are * just concatenate together. * * @param {string} html * @param {string} [endHTML] * @return False on fail * @function * @name insertHTML * @memberOf RangeHelper.prototype */ base.insertHTML = function (html, endHTML) { var node, div, range = base.selectedRange(); if (!range) { return false; } if (endHTML) { html += base.selectedHtml() + endHTML; } div = createElement('p', {}, doc); node = doc.createDocumentFragment(); div.innerHTML = sanitize(html); while (div.firstChild) { appendChild(node, div.firstChild); } base.insertNode(node); }; /** * Prepares HTML to be inserted by adding a zero width space * if the last child is empty and adding the range start/end * markers to the last child. * * @param {Node|string} node * @param {Node|string} [endNode] * @param {boolean} [returnHtml] * @return {Node|string} * @private */ _prepareInput = function (node, endNode, returnHtml) { var lastChild, frag = doc.createDocumentFragment(); if (typeof node === 'string') { if (endNode) { node += base.selectedHtml() + endNode; } frag = parseHTML(node); } else { appendChild(frag, node); if (endNode) { appendChild(frag, base.selectedRange().extractContents()); appendChild(frag, endNode); } } if (!(lastChild = frag.lastChild)) { return; } while (!isInline(lastChild.lastChild, true)) { lastChild = lastChild.lastChild; } if (canHaveChildren(lastChild)) { // Webkit won't allow the cursor to be placed inside an // empty tag, so add a zero width space to it. if (!lastChild.lastChild) { appendChild(lastChild, document.createTextNode('\u200B')); } } else { lastChild = frag; } base.removeMarkers(); // Append marks to last child so when restored cursor will be in // the right place appendChild(lastChild, _createMarker(startMarker)); appendChild(lastChild, _createMarker(endMarker)); if (returnHtml) { var div = createElement('div'); appendChild(div, frag); return div.innerHTML; } return frag; }; /** * The same as insertHTML except with DOM nodes instead * * Warning: the nodes must belong to the * document they are being inserted into. Some browsers * will throw exceptions if they don't. * * Returns boolean false on fail * * @param {Node} node * @param {Node} endNode * @return {false|undefined} * @function * @name insertNode * @memberOf RangeHelper.prototype */ base.insertNode = function (node, endNode) { var first, last, input = _prepareInput(node, endNode), range = base.selectedRange(), parent = range.commonAncestorContainer, emptyNodes = []; if (!input) { return false; } function removeIfEmpty(node) { // Only remove empty node if it wasn't already empty if (node && isEmpty(node) && emptyNodes.indexOf(node) < 0) { remove(node); } } if (range.startContainer !== range.endContainer) { each(parent.childNodes, function (_, node) { if (isEmpty(node)) { emptyNodes.push(node); } }); first = input.firstChild; last = input.lastChild; } range.deleteContents(); // FF allows
to be selected but inserting a node // into
will cause it not to be displayed so must // insert before the
in FF. // 3 = TextNode if (parent && parent.nodeType !== 3 && !canHaveChildren(parent)) { insertBefore(input, parent); } else { range.insertNode(input); // If a node was split or its contents deleted, remove any resulting // empty tags. For example: //

|test

test|
// When deleteContents could become: //

|
// So remove the empty ones removeIfEmpty(first && first.previousSibling); removeIfEmpty(last && last.nextSibling); } base.restoreRange(); }; /** * Clones the selected Range * * @return {Range} * @function * @name cloneSelected * @memberOf RangeHelper.prototype */ base.cloneSelected = function () { var range = base.selectedRange(); if (range) { return range.cloneRange(); } }; /** * Gets the selected Range * * @return {Range} * @function * @name selectedRange * @memberOf RangeHelper.prototype */ base.selectedRange = function () { var range, firstChild, sel = win.getSelection(); if (!sel) { return; } // When creating a new range, set the start to the first child // element of the body element to avoid errors in FF. if (sel.rangeCount <= 0) { firstChild = doc.body; while (firstChild.firstChild) { firstChild = firstChild.firstChild; } range = doc.createRange(); // Must be setStartBefore otherwise it can cause infinite // loops with lists in WebKit. See issue 442 range.setStartBefore(firstChild); sel.addRange(range); } if (sel.rangeCount > 0) { range = sel.getRangeAt(0); } return range; }; /** * Gets if there is currently a selection * * @return {boolean} * @function * @name hasSelection * @since 1.4.4 * @memberOf RangeHelper.prototype */ base.hasSelection = function () { var sel = win.getSelection(); return sel && sel.rangeCount > 0; }; /** * Gets the currently selected HTML * * @return {string} * @function * @name selectedHtml * @memberOf RangeHelper.prototype */ base.selectedHtml = function () { var div, range = base.selectedRange(); if (range) { div = createElement('p', {}, doc); appendChild(div, range.cloneContents()); return div.innerHTML; } return ''; }; /** * Gets the parent node of the selected contents in the range * * @return {HTMLElement} * @function * @name parentNode * @memberOf RangeHelper.prototype */ base.parentNode = function () { var range = base.selectedRange(); if (range) { return range.commonAncestorContainer; } }; /** * Gets the first block level parent of the selected * contents of the range. * * @return {HTMLElement} * @function * @name getFirstBlockParent * @memberOf RangeHelper.prototype */ /** * Gets the first block level parent of the selected * contents of the range. * * @param {Node} [n] The element to get the first block level parent from * @return {HTMLElement} * @function * @name getFirstBlockParent^2 * @since 1.4.1 * @memberOf RangeHelper.prototype */ base.getFirstBlockParent = function (node) { var func = function (elm) { if (!isInline(elm, true)) { return elm; } elm = elm ? elm.parentNode : null; return elm ? func(elm) : elm; }; return func(node || base.parentNode()); }; /** * Inserts a node at either the start or end of the current selection * * @param {Bool} start * @param {Node} node * @function * @name insertNodeAt * @memberOf RangeHelper.prototype */ base.insertNodeAt = function (start, node) { var currentRange = base.selectedRange(), range = base.cloneSelected(); if (!range) { return false; } range.collapse(start); range.insertNode(node); // Reselect the current range. // Fixes issue with Chrome losing the selection. Issue#82 base.selectRange(currentRange); }; /** * Creates a marker node * * @param {string} id * @return {HTMLSpanElement} * @private */ _createMarker = function (id) { base.removeMarker(id); var marker = createElement('span', { id: id, className: 'sceditor-selection sceditor-ignore', style: 'display:none;line-height:0' }, doc); marker.innerHTML = ' '; return marker; }; /** * Inserts start/end markers for the current selection * which can be used by restoreRange to re-select the * range. * * @memberOf RangeHelper.prototype * @function * @name insertMarkers */ base.insertMarkers = function () { var currentRange = base.selectedRange(); var startNode = _createMarker(startMarker); base.removeMarkers(); base.insertNodeAt(true, startNode); // Fixes issue with end marker sometimes being placed before // the start marker when the range is collapsed. if (currentRange && currentRange.collapsed) { startNode.parentNode.insertBefore( _createMarker(endMarker), startNode.nextSibling); } else { base.insertNodeAt(false, _createMarker(endMarker)); } }; /** * Gets the marker with the specified ID * * @param {string} id * @return {Node} * @function * @name getMarker * @memberOf RangeHelper.prototype */ base.getMarker = function (id) { return doc.getElementById(id); }; /** * Removes the marker with the specified ID * * @param {string} id * @function * @name removeMarker * @memberOf RangeHelper.prototype */ base.removeMarker = function (id) { var marker = base.getMarker(id); if (marker) { remove(marker); } }; /** * Removes the start/end markers * * @function * @name removeMarkers * @memberOf RangeHelper.prototype */ base.removeMarkers = function () { base.removeMarker(startMarker); base.removeMarker(endMarker); }; /** * Saves the current range location. Alias of insertMarkers() * * @function * @name saveRage * @memberOf RangeHelper.prototype */ base.saveRange = function () { base.insertMarkers(); }; /** * Select the specified range * * @param {Range} range * @function * @name selectRange * @memberOf RangeHelper.prototype */ base.selectRange = function (range) { var lastChild; var sel = win.getSelection(); var container = range.endContainer; // Check if cursor is set after a BR when the BR is the only // child of the parent. In Firefox this causes a line break // to occur when something is typed. See issue #321 if (range.collapsed && container && !isInline(container, true)) { lastChild = container.lastChild; while (lastChild && is(lastChild, '.sceditor-ignore')) { lastChild = lastChild.previousSibling; } if (is(lastChild, 'br')) { var rng = doc.createRange(); rng.setEndAfter(lastChild); rng.collapse(false); if (base.compare(range, rng)) { range.setStartBefore(lastChild); range.collapse(true); } } } if (sel) { base.clear(); sel.addRange(range); } }; /** * Restores the last range saved by saveRange() or insertMarkers() * * @function * @name restoreRange * @memberOf RangeHelper.prototype */ base.restoreRange = function () { var isCollapsed, range = base.selectedRange(), start = base.getMarker(startMarker), end = base.getMarker(endMarker); if (!start || !end || !range) { return false; } isCollapsed = start.nextSibling === end; range = doc.createRange(); range.setStartBefore(start); range.setEndAfter(end); if (isCollapsed) { range.collapse(true); } base.selectRange(range); base.removeMarkers(); }; /** * Selects the text left and right of the current selection * * @param {number} left * @param {number} right * @since 1.4.3 * @function * @name selectOuterText * @memberOf RangeHelper.prototype */ base.selectOuterText = function (left, right) { var start, end, range = base.cloneSelected(); if (!range) { return false; } range.collapse(false); start = outerText(range, true, left); end = outerText(range, false, right); range.setStart(start.node, start.offset); range.setEnd(end.node, end.offset); base.selectRange(range); }; /** * Gets the text left or right of the current selection * * @param {boolean} before * @param {number} length * @return {string} * @since 1.4.3 * @function * @name selectOuterText * @memberOf RangeHelper.prototype */ base.getOuterText = function (before, length) { var range = base.cloneSelected(); if (!range) { return ''; } range.collapse(!before); return outerText(range, before, length).text; }; /** * Replaces keywords with values based on the current caret position * * @param {Array} keywords * @param {boolean} includeAfter If to include the text after the * current caret position or just * text before * @param {boolean} keywordsSorted If the keywords array is pre * sorted shortest to longest * @param {number} longestKeyword Length of the longest keyword * @param {boolean} requireWhitespace If the key must be surrounded * by whitespace * @param {string} keypressChar If this is being called from * a keypress event, this should be * set to the pressed character * @return {boolean} * @function * @name replaceKeyword * @memberOf RangeHelper.prototype */ // eslint-disable-next-line max-params base.replaceKeyword = function ( keywords, includeAfter, keywordsSorted, longestKeyword, requireWhitespace, keypressChar ) { if (!keywordsSorted) { keywords.sort(function (a, b) { return a[0].length - b[0].length; }); } var outerText, match, matchPos, startIndex, leftLen, charsLeft, keyword, keywordLen, whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])', keywordIdx = keywords.length, whitespaceLen = requireWhitespace ? 1 : 0, maxKeyLen = longestKeyword || keywords[keywordIdx - 1][0].length; if (requireWhitespace) { maxKeyLen++; } keypressChar = keypressChar || ''; outerText = base.getOuterText(true, maxKeyLen); leftLen = outerText.length; outerText += keypressChar; if (includeAfter) { outerText += base.getOuterText(false, maxKeyLen); } while (keywordIdx--) { keyword = keywords[keywordIdx][0]; keywordLen = keyword.length; startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen); matchPos = -1; if (requireWhitespace) { match = outerText .substr(startIndex) .match(new RegExp(whitespaceRegex + regex(keyword) + whitespaceRegex)); if (match) { // Add the length of the text that was removed by // substr() and also add 1 for the whitespace matchPos = match.index + startIndex + match[1].length; } } else { matchPos = outerText.indexOf(keyword, startIndex); } if (matchPos > -1) { // Make sure the match is between before and // after, not just entirely in one side or the other if (matchPos <= leftLen && matchPos + keywordLen + whitespaceLen >= leftLen) { charsLeft = leftLen - matchPos; // If the keypress char is white space then it should // not be replaced, only chars that are part of the // key should be replaced. base.selectOuterText( charsLeft, keywordLen - charsLeft - (/^\S/.test(keypressChar) ? 1 : 0) ); base.insertHTML(keywords[keywordIdx][1]); return true; } } } return false; }; /** * Compares two ranges. * * If rangeB is undefined it will be set to * the current selected range * * @param {Range} rngA * @param {Range} [rngB] * @return {boolean} * @function * @name compare * @memberOf RangeHelper.prototype */ base.compare = function (rngA, rngB) { if (!rngB) { rngB = base.selectedRange(); } if (!rngA || !rngB) { return !rngA && !rngB; } return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 && rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0; }; /** * Removes any current selection * * @since 1.4.6 * @function * @name clear * @memberOf RangeHelper.prototype */ base.clear = function () { var sel = win.getSelection(); if (sel) { if (sel.removeAllRanges) { sel.removeAllRanges(); } else if (sel.empty) { sel.empty(); } } }; } var USER_AGENT = navigator.userAgent; /** * Detects if the browser is iOS * * Needed to fix iOS specific bugs * * @function * @name ios * @memberOf jQuery.sceditor * @type {boolean} */ var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT); /** * If the browser supports WYSIWYG editing (e.g. older mobile browsers). * * @function * @name isWysiwygSupported * @return {boolean} */ var isWysiwygSupported = (function () { var match, isUnsupported; // IE is the only browser to support documentMode var ie = !!window.document.documentMode; var legacyEdge = '-ms-ime-align' in document.documentElement.style; var div = document.createElement('div'); div.contentEditable = true; // Check if the contentEditable attribute is supported if (!('contentEditable' in document.documentElement) || div.contentEditable !== 'true') { return false; } // I think blackberry supports contentEditable or will at least // give a valid value for the contentEditable detection above // so it isn't included in the below tests. // I hate having to do UA sniffing but some mobile browsers say they // support contentediable when it isn't usable, i.e. you can't enter // text. // This is the only way I can think of to detect them which is also how // every other editor I've seen deals with this issue. // Exclude Opera mobile and mini isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT); if (/Android/i.test(USER_AGENT)) { isUnsupported = true; if (/Safari/.test(USER_AGENT)) { // Android browser 534+ supports content editable // This also matches Chrome which supports content editable too match = /Safari\/(\d+)/.exec(USER_AGENT); isUnsupported = (!match || !match[1] ? true : match[1] < 534); } } // The current version of Amazon Silk supports it, older versions didn't // As it uses webkit like Android, assume it's the same and started // working at versions >= 534 if (/ Silk\//i.test(USER_AGENT)) { match = /AppleWebKit\/(\d+)/.exec(USER_AGENT); isUnsupported = (!match || !match[1] ? true : match[1] < 534); } // iOS 5+ supports content editable if (ios) { // Block any version <= 4_x(_x) isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT); } // Firefox does support WYSIWYG on mobiles so override // any previous value if using FF if (/Firefox/i.test(USER_AGENT)) { isUnsupported = false; } if (/OneBrowser/i.test(USER_AGENT)) { isUnsupported = false; } // UCBrowser works but doesn't give a unique user agent if (navigator.vendor === 'UCWEB') { isUnsupported = false; } // IE and legacy edge are not supported any more if (ie || legacyEdge) { isUnsupported = true; } return !isUnsupported; }()); /** * Checks all emoticons are surrounded by whitespace and * replaces any that aren't with with their emoticon code. * * @param {HTMLElement} node * @param {rangeHelper} rangeHelper * @return {void} */ function checkWhitespace(node, rangeHelper) { var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/; var emoticons = node && find(node, 'img[data-sceditor-emoticon]'); if (!node || !emoticons.length) { return; } for (var i = 0; i < emoticons.length; i++) { var emoticon = emoticons[i]; var parent = emoticon.parentNode; var prev = emoticon.previousSibling; var next = emoticon.nextSibling; if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) && (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) { continue; } var range = rangeHelper.cloneSelected(); var rangeStart = -1; var rangeStartContainer = range.startContainer; var previousText = (prev && prev.nodeValue) || ''; previousText += data(emoticon, 'sceditor-emoticon'); // If the cursor is after the removed emoticon, add // the length of the newly added text to it if (rangeStartContainer === next) { rangeStart = previousText.length + range.startOffset; } // If the cursor is set before the next node, set it to // the end of the new text node if (rangeStartContainer === node && node.childNodes[range.startOffset] === next) { rangeStart = previousText.length; } // If the cursor is set before the removed emoticon, // just keep it at that position if (rangeStartContainer === prev) { rangeStart = range.startOffset; } if (!next || next.nodeType !== TEXT_NODE) { next = parent.insertBefore( parent.ownerDocument.createTextNode(''), next ); } next.insertData(0, previousText); remove(emoticon); if (prev) { remove(prev); } // Need to update the range starting position if it's been modified if (rangeStart > -1) { range.setStart(next, rangeStart); range.collapse(true); rangeHelper.selectRange(range); } } } /** * Replaces any emoticons inside the root node with images. * * emoticons should be an object where the key is the emoticon * code and the value is the HTML to replace it with. * * @param {HTMLElement} root * @param {Object} emoticons * @param {boolean} emoticonsCompat * @return {void} */ function replace(root, emoticons, emoticonsCompat) { var doc = root.ownerDocument; var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)'; var emoticonCodes = []; var emoticonRegex = {}; // TODO: Make this tag configurable. if (parent(root, 'code')) { return; } each(emoticons, function (key) { emoticonRegex[key] = new RegExp(space + regex(key) + space); emoticonCodes.push(key); }); // Sort keys longest to shortest so that longer keys // take precedence (avoids bugs with shorter keys partially // matching longer ones) emoticonCodes.sort(function (a, b) { return b.length - a.length; }); (function convert(node) { node = node.firstChild; while (node) { // TODO: Make this tag configurable. if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) { convert(node); } if (node.nodeType === TEXT_NODE) { for (var i = 0; i < emoticonCodes.length; i++) { var text = node.nodeValue; var key = emoticonCodes[i]; var index = emoticonsCompat ? text.search(emoticonRegex[key]) : text.indexOf(key); if (index > -1) { // When emoticonsCompat is enabled this will be the // position after any white space var startIndex = text.indexOf(key, index); var fragment = parseHTML(emoticons[key], doc); var after = text.substr(startIndex + key.length); fragment.appendChild(doc.createTextNode(after)); node.nodeValue = text.substr(0, startIndex); node.parentNode .insertBefore(fragment, node.nextSibling); } } } node = node.nextSibling; } }(root)); } /*! @license DOMPurify 2.4.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.4.3/LICENSE */ function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var hasOwnProperty = Object.hasOwnProperty, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; var freeze = Object.freeze, seal = Object.seal, create = Object.create; // eslint-disable-line import/no-mutable-exports var _ref = typeof Reflect !== 'undefined' && Reflect, apply = _ref.apply, construct = _ref.construct; if (!apply) { apply = function apply(fun, thisValue, args) { return fun.apply(thisValue, args); }; } if (!freeze) { freeze = function freeze(x) { return x; }; } if (!seal) { seal = function seal(x) { return x; }; } if (!construct) { construct = function construct(Func, args) { return _construct(Func, _toConsumableArray(args)); }; } var arrayForEach = unapply(Array.prototype.forEach); var arrayPop = unapply(Array.prototype.pop); var arrayPush = unapply(Array.prototype.push); var stringToLowerCase = unapply(String.prototype.toLowerCase); var stringToString = unapply(String.prototype.toString); var stringMatch = unapply(String.prototype.match); var stringReplace = unapply(String.prototype.replace); var stringIndexOf = unapply(String.prototype.indexOf); var stringTrim = unapply(String.prototype.trim); var regExpTest = unapply(RegExp.prototype.test); var typeErrorCreate = unconstruct(TypeError); function unapply(func) { return function (thisArg) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return apply(func, thisArg, args); }; } function unconstruct(func) { return function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return construct(func, args); }; } /* Add properties to a lookup table */ function addToSet(set, array, transformCaseFunc) { transformCaseFunc = transformCaseFunc ? transformCaseFunc : stringToLowerCase; if (setPrototypeOf) { // Make 'in' and truthy checks like Boolean(set.constructor) // independent of any properties defined on Object.prototype. // Prevent prototype setters from intercepting set as a this value. setPrototypeOf(set, null); } var l = array.length; while (l--) { var element = array[l]; if (typeof element === 'string') { var lcElement = transformCaseFunc(element); if (lcElement !== element) { // Config presets (e.g. tags.js, attrs.js) are immutable. if (!isFrozen(array)) { array[l] = lcElement; } element = lcElement; } } set[element] = true; } return set; } /* Shallow clone an object */ function clone(object) { var newObject = create(null); var property; for (property in object) { if (apply(hasOwnProperty, object, [property]) === true) { newObject[property] = object[property]; } } return newObject; } /* IE10 doesn't support __lookupGetter__ so lets' * simulate it. It also automatically checks * if the prop is function or getter and behaves * accordingly. */ function lookupGetter(object, prop) { while (object !== null) { var desc = getOwnPropertyDescriptor(object, prop); if (desc) { if (desc.get) { return unapply(desc.get); } if (typeof desc.value === 'function') { return unapply(desc.value); } } object = getPrototypeOf(object); } function fallbackValue(element) { console.warn('fallback value for', element); return null; } return fallbackValue; } var html$1 = 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']); // SVG var svg$1 = 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']); var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); // List of SVG elements that are disallowed by default. // We still need to know them so that we can do namespace // checks properly in case one wants to add them to // allow-list. var svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', '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']); var mathMl$1 = 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']); // Similarly to SVG, we want to know all MathML elements, // even those that we disallow by default. var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); var text = freeze(['#text']); var html = 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', 'nonce', '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', 'slot']); var svg = 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', 'transform-origin', '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']); var mathMl = 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']); var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); var MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode var ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); var TMPLIT_EXPR = seal(/\${[\w\W]*}/gm); var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape 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 ); var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex ); var DOCTYPE_NAME = seal(/^html$/i); var getGlobal = function getGlobal() { return typeof window === 'undefined' ? null : window; }; /** * Creates a no-op policy for internal use only. * Don't export this function outside this module! * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory. * @param {Document} document The document object (to determine policy name suffix) * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types * are not supported). */ var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) { if (_typeof(trustedTypes) !== 'object' || typeof trustedTypes.createPolicy !== 'function') { return null; } // Allow the callers to control the unique policy name // by adding a data-tt-policy-suffix to the script element with the DOMPurify. // Policy creation with duplicate names throws in Trusted Types. var suffix = null; var ATTR_NAME = 'data-tt-policy-suffix'; if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) { suffix = document.currentScript.getAttribute(ATTR_NAME); } var policyName = 'dompurify' + (suffix ? '#' + suffix : ''); try { return trustedTypes.createPolicy(policyName, { createHTML: function createHTML(html) { return html; }, createScriptURL: function createScriptURL(scriptUrl) { return scriptUrl; } }); } catch (_) { // Policy creation failed (most likely another DOMPurify script has // already run). Skip creating the policy, as this will only cause errors // if TT are enforced. console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); return null; } }; function createDOMPurify() { var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); var DOMPurify = function DOMPurify(root) { return createDOMPurify(root); }; /** * Version label, exposed for easier checks * if DOMPurify is up to date or not */ DOMPurify.version = '2.4.3'; /** * Array of elements that DOMPurify removed during sanitation. * Empty if nothing was removed. */ DOMPurify.removed = []; if (!window || !window.document || window.document.nodeType !== 9) { // Not running in a browser, provide a factory function // so that you can pass your own Window DOMPurify.isSupported = false; return DOMPurify; } var originalDocument = window.document; var document = window.document; var DocumentFragment = window.DocumentFragment, HTMLTemplateElement = window.HTMLTemplateElement, Node = window.Node, Element = window.Element, NodeFilter = window.NodeFilter, _window$NamedNodeMap = window.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap, HTMLFormElement = window.HTMLFormElement, DOMParser = window.DOMParser, trustedTypes = window.trustedTypes; var ElementPrototype = Element.prototype; var cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); var getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); var getParentNode = lookupGetter(ElementPrototype, 'parentNode'); // As per issue #47, the web-components registry is inherited by a // new document created via createHTMLDocument. As per the spec // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) // a new empty registry is used when creating a template contents owner // document, so we use that as our parent document to ensure nothing // is inherited. if (typeof HTMLTemplateElement === 'function') { var template = document.createElement('template'); if (template.content && template.content.ownerDocument) { document = template.content.ownerDocument; } } var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument); var emptyHTML = trustedTypesPolicy ? trustedTypesPolicy.createHTML('') : ''; var _document = document, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName; var importNode = originalDocument.importNode; var documentMode = {}; try { documentMode = clone(document).documentMode ? document.documentMode : {}; } catch (_) {} var hooks = {}; /** * Expose whether this browser supports running the full DOMPurify. */ DOMPurify.isSupported = typeof getParentNode === 'function' && implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9; var MUSTACHE_EXPR$1 = MUSTACHE_EXPR, ERB_EXPR$1 = ERB_EXPR, TMPLIT_EXPR$1 = TMPLIT_EXPR, DATA_ATTR$1 = DATA_ATTR, ARIA_ATTR$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$1 = ATTR_WHITESPACE; var IS_ALLOWED_URI$1 = IS_ALLOWED_URI; /** * We consider the elements and attributes below to be safe. Ideally * don't add any new ones but feel free to remove unwanted ones. */ /* allowed element names */ var ALLOWED_TAGS = null; var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray(html$1), _toConsumableArray(svg$1), _toConsumableArray(svgFilters), _toConsumableArray(mathMl$1), _toConsumableArray(text))); /* Allowed attribute names */ var ALLOWED_ATTR = null; var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray(html), _toConsumableArray(svg), _toConsumableArray(mathMl), _toConsumableArray(xml))); /* * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements. * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. */ var CUSTOM_ELEMENT_HANDLING = Object.seal(Object.create(null, { tagNameCheck: { writable: true, configurable: false, enumerable: true, value: null }, attributeNameCheck: { writable: true, configurable: false, enumerable: true, value: null }, allowCustomizedBuiltInElements: { writable: true, configurable: false, enumerable: true, value: false } })); /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ var FORBID_TAGS = null; /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ var FORBID_ATTR = null; /* Decide if ARIA attributes are okay */ var ALLOW_ARIA_ATTR = true; /* Decide if custom data attributes are okay */ var ALLOW_DATA_ATTR = true; /* Decide if unknown protocols are okay */ var ALLOW_UNKNOWN_PROTOCOLS = false; /* Output should be safe for common template engines. * This means, DOMPurify removes data attributes, mustaches and ERB */ var SAFE_FOR_TEMPLATES = false; /* Decide if document with ... should be returned */ var WHOLE_DOCUMENT = false; /* Track whether config is already set on this instance of DOMPurify. */ var SET_CONFIG = false; /* Decide if all elements (e.g. style, script) must be children of * document.body. By default, browsers might move them to document.head */ var FORCE_BODY = false; /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html * string (or a TrustedHTML object if Trusted Types are supported). * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead */ var RETURN_DOM = false; /* Decide if a DOM `DocumentFragment` should be returned, instead of a html * string (or a TrustedHTML object if Trusted Types are supported) */ var RETURN_DOM_FRAGMENT = false; /* Try to return a Trusted Type object instead of a string, return a string in * case Trusted Types are not supported */ var RETURN_TRUSTED_TYPE = false; /* Output should be free from DOM clobbering attacks? * This sanitizes markups named with colliding, clobberable built-in DOM APIs. */ var SANITIZE_DOM = true; /* Achieve full DOM Clobbering protection by isolating the namespace of named * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. * * HTML/DOM spec rules that enable DOM Clobbering: * - Named Access on Window (§7.3.3) * - DOM Tree Accessors (§3.1.5) * - Form Element Parent-Child Relations (§4.10.3) * - Iframe srcdoc / Nested WindowProxies (§4.8.5) * - HTMLCollection (§4.2.10.2) * * Namespace isolation is implemented by prefixing `id` and `name` attributes * with a constant string, i.e., `user-content-` */ var SANITIZE_NAMED_PROPS = false; var SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; /* Keep element content when removing element? */ var KEEP_CONTENT = true; /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead * of importing it into a new Document and returning a sanitized copy */ var IN_PLACE = false; /* Allow usage of profiles like html, svg and mathMl */ var USE_PROFILES = {}; /* Tags to ignore content of when KEEP_CONTENT is true */ var FORBID_CONTENTS = null; var DEFAULT_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']); /* Tags that are safe for data: URIs */ var DATA_URI_TAGS = null; var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); /* Attributes safe for values like "javascript:" */ var URI_SAFE_ATTRIBUTES = null; var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; /* Document namespace */ var NAMESPACE = HTML_NAMESPACE; var IS_EMPTY_INPUT = false; /* Allowed XHTML+XML namespaces */ var ALLOWED_NAMESPACES = null; var DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); /* Parsing of strict XHTML documents */ var PARSER_MEDIA_TYPE; var SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; var DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; var transformCaseFunc; /* Keep a reference to config to pass to hooks */ var CONFIG = null; /* Ideally, do not touch anything below this line */ /* ______________________________________________ */ var formElement = document.createElement('form'); var isRegexOrFunction = function isRegexOrFunction(testValue) { return testValue instanceof RegExp || testValue instanceof Function; }; /** * _parseConfig * * @param {Object} cfg optional config literal */ // eslint-disable-next-line complexity var _parseConfig = function _parseConfig(cfg) { if (CONFIG && CONFIG === cfg) { return; } /* Shield configuration object from tampering */ if (!cfg || _typeof(cfg) !== 'object') { cfg = {}; } /* Shield configuration object from prototype pollution */ cfg = clone(cfg); PARSER_MEDIA_TYPE = // eslint-disable-next-line unicorn/prefer-includes SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? PARSER_MEDIA_TYPE = DEFAULT_PARSER_MEDIA_TYPE : PARSER_MEDIA_TYPE = cfg.PARSER_MEDIA_TYPE; // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; /* Set configuration parameters */ ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; ALLOWED_NAMESPACES = 'ALLOWED_NAMESPACES' in cfg ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent transformCaseFunc // eslint-disable-line indent ) // eslint-disable-line indent : DEFAULT_URI_SAFE_ATTRIBUTES; DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent transformCaseFunc // eslint-disable-line indent ) // eslint-disable-line indent : DEFAULT_DATA_URI_TAGS; FORBID_CONTENTS = 'FORBID_CONTENTS' in cfg ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false; ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false RETURN_DOM = cfg.RETURN_DOM || false; // Default false RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false FORCE_BODY = cfg.FORCE_BODY || false; // Default false SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true IN_PLACE = cfg.IN_PLACE || false; // Default false IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$1; NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; } if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; } if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; } if (SAFE_FOR_TEMPLATES) { ALLOW_DATA_ATTR = false; } if (RETURN_DOM_FRAGMENT) { RETURN_DOM = true; } /* Parse profile info */ if (USE_PROFILES) { ALLOWED_TAGS = addToSet({}, _toConsumableArray(text)); ALLOWED_ATTR = []; if (USE_PROFILES.html === true) { addToSet(ALLOWED_TAGS, html$1); addToSet(ALLOWED_ATTR, html); } if (USE_PROFILES.svg === true) { addToSet(ALLOWED_TAGS, svg$1); addToSet(ALLOWED_ATTR, svg); addToSet(ALLOWED_ATTR, xml); } if (USE_PROFILES.svgFilters === true) { addToSet(ALLOWED_TAGS, svgFilters); addToSet(ALLOWED_ATTR, svg); addToSet(ALLOWED_ATTR, xml); } if (USE_PROFILES.mathMl === true) { addToSet(ALLOWED_TAGS, mathMl$1); addToSet(ALLOWED_ATTR, mathMl); addToSet(ALLOWED_ATTR, xml); } } /* Merge configuration parameters */ if (cfg.ADD_TAGS) { if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS = clone(ALLOWED_TAGS); } addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); } if (cfg.ADD_ATTR) { if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { ALLOWED_ATTR = clone(ALLOWED_ATTR); } addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); } if (cfg.ADD_URI_SAFE_ATTR) { addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); } if (cfg.FORBID_CONTENTS) { if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { FORBID_CONTENTS = clone(FORBID_CONTENTS); } addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); } /* Add #text in case KEEP_CONTENT is set to true */ if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; } /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ if (WHOLE_DOCUMENT) { addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); } /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ if (ALLOWED_TAGS.table) { addToSet(ALLOWED_TAGS, ['tbody']); delete FORBID_TAGS.tbody; } // Prevent further manipulation of configuration. // Not available in IE8, Safari 5, etc. if (freeze) { freeze(cfg); } CONFIG = cfg; }; var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']); // Certain elements are allowed in both SVG and HTML // namespace. We need to specify them explicitly // so that they don't get erroneously deleted from // HTML namespace. var COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); /* Keep track of all possible SVG and MathML tags * so that we can perform the namespace checks * correctly. */ var ALL_SVG_TAGS = addToSet({}, svg$1); addToSet(ALL_SVG_TAGS, svgFilters); addToSet(ALL_SVG_TAGS, svgDisallowed); var ALL_MATHML_TAGS = addToSet({}, mathMl$1); addToSet(ALL_MATHML_TAGS, mathMlDisallowed); /** * * * @param {Element} element a DOM element whose namespace is being checked * @returns {boolean} Return false if the element has a * namespace that a spec-compliant parser would never * return. Return true otherwise. */ var _checkValidNamespace = function _checkValidNamespace(element) { var parent = getParentNode(element); // In JSDOM, if we're inside shadow DOM, then parentNode // can be null. We just simulate parent in this case. if (!parent || !parent.tagName) { parent = { namespaceURI: NAMESPACE, tagName: 'template' }; } var tagName = stringToLowerCase(element.tagName); var parentTagName = stringToLowerCase(parent.tagName); if (!ALLOWED_NAMESPACES[element.namespaceURI]) { return false; } if (element.namespaceURI === SVG_NAMESPACE) { // The only way to switch from HTML namespace to SVG // is via . If it happens via any other tag, then // it should be killed. if (parent.namespaceURI === HTML_NAMESPACE) { return tagName === 'svg'; } // The only way to switch from MathML to SVG is via` // svg if parent is either or MathML // text integration points. if (parent.namespaceURI === MATHML_NAMESPACE) { return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); } // We only allow elements that are defined in SVG // spec. All others are disallowed in SVG namespace. return Boolean(ALL_SVG_TAGS[tagName]); } if (element.namespaceURI === MATHML_NAMESPACE) { // The only way to switch from HTML namespace to MathML // is via . If it happens via any other tag, then // it should be killed. if (parent.namespaceURI === HTML_NAMESPACE) { return tagName === 'math'; } // The only way to switch from SVG to MathML is via // and HTML integration points if (parent.namespaceURI === SVG_NAMESPACE) { return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; } // We only allow elements that are defined in MathML // spec. All others are disallowed in MathML namespace. return Boolean(ALL_MATHML_TAGS[tagName]); } if (element.namespaceURI === HTML_NAMESPACE) { // The only way to switch from SVG to HTML is via // HTML integration points, and from MathML to HTML // is via MathML text integration points if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { return false; } if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { return false; } // We disallow tags that are specific for MathML // or SVG and should never appear in HTML namespace return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); } // For XHTML and XML documents that support custom namespaces if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { return true; } // The code should never reach this place (this means // that the element somehow got namespace that is not // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). // Return false just in case. return false; }; /** * _forceRemove * * @param {Node} node a DOM node */ var _forceRemove = function _forceRemove(node) { arrayPush(DOMPurify.removed, { element: node }); try { // eslint-disable-next-line unicorn/prefer-dom-node-remove node.parentNode.removeChild(node); } catch (_) { try { node.outerHTML = emptyHTML; } catch (_) { node.remove(); } } }; /** * _removeAttribute * * @param {String} name an Attribute name * @param {Node} node a DOM node */ var _removeAttribute = function _removeAttribute(name, node) { try { arrayPush(DOMPurify.removed, { attribute: node.getAttributeNode(name), from: node }); } catch (_) { arrayPush(DOMPurify.removed, { attribute: null, from: node }); } node.removeAttribute(name); // We void attribute values for unremovable "is"" attributes if (name === 'is' && !ALLOWED_ATTR[name]) { if (RETURN_DOM || RETURN_DOM_FRAGMENT) { try { _forceRemove(node); } catch (_) {} } else { try { node.setAttribute(name, ''); } catch (_) {} } } }; /** * _initDocument * * @param {String} dirty a string of dirty markup * @return {Document} a DOM, filled with the dirty markup */ var _initDocument = function _initDocument(dirty) { /* Create a HTML document */ var doc; var leadingWhitespace; if (FORCE_BODY) { dirty = '' + dirty; } else { /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ var matches = stringMatch(dirty, /^[\r\n\t ]+/); leadingWhitespace = matches && matches[0]; } if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) dirty = '' + dirty + ''; } var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; /* * Use the DOMParser API by default, fallback later if needs be * DOMParser not work for svg when has multiple root element. */ if (NAMESPACE === HTML_NAMESPACE) { try { doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); } catch (_) {} } /* Use createHTMLDocument in case DOMParser is not available */ if (!doc || !doc.documentElement) { doc = implementation.createDocument(NAMESPACE, 'template', null); try { doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; } catch (_) {// Syntax error if dirtyPayload is invalid xml } } var body = doc.body || doc.documentElement; if (dirty && leadingWhitespace) { body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); } /* Work on whole document or just its body */ if (NAMESPACE === HTML_NAMESPACE) { return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; } return WHOLE_DOCUMENT ? doc.documentElement : body; }; /** * _createIterator * * @param {Document} root document/fragment to create iterator for * @return {Iterator} iterator instance */ var _createIterator = function _createIterator(root) { return createNodeIterator.call(root.ownerDocument || root, root, // eslint-disable-next-line no-bitwise NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null, false); }; /** * _isClobbered * * @param {Node} elm element to check for clobbering attacks * @return {Boolean} true if clobbered, false if safe */ var _isClobbered = function _isClobbered(elm) { return elm instanceof HTMLFormElement && (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' || typeof elm.hasChildNodes !== 'function'); }; /** * _isNode * * @param {Node} obj object to check whether it's a DOM node * @return {Boolean} true is object is a DOM node */ var _isNode = function _isNode(object) { return _typeof(Node) === 'object' ? object instanceof Node : object && _typeof(object) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'; }; /** * _executeHook * Execute user configurable hooks * * @param {String} entryPoint Name of the hook's entry point * @param {Node} currentNode node to work on with the hook * @param {Object} data additional hook parameters */ var _executeHook = function _executeHook(entryPoint, currentNode, data) { if (!hooks[entryPoint]) { return; } arrayForEach(hooks[entryPoint], function (hook) { hook.call(DOMPurify, currentNode, data, CONFIG); }); }; /** * _sanitizeElements * * @protect nodeName * @protect textContent * @protect removeChild * * @param {Node} currentNode to check for permission to exist * @return {Boolean} true if node was killed, false if left alive */ var _sanitizeElements = function _sanitizeElements(currentNode) { var content; /* Execute a hook if present */ _executeHook('beforeSanitizeElements', currentNode, null); /* Check if element is clobbered or can clobber */ if (_isClobbered(currentNode)) { _forceRemove(currentNode); return true; } /* Check if tagname contains Unicode */ if (regExpTest(/[\u0080-\uFFFF]/, currentNode.nodeName)) { _forceRemove(currentNode); return true; } /* Now let's check the element's type and name */ var tagName = transformCaseFunc(currentNode.nodeName); /* Execute a hook if present */ _executeHook('uponSanitizeElement', currentNode, { tagName: tagName, allowedTags: ALLOWED_TAGS }); /* Detect mXSS attempts abusing namespace confusion */ if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { _forceRemove(currentNode); return true; } /* Mitigate a problem with templates inside select */ if (tagName === 'select' && regExpTest(/