UNPKG

suneditor

Version:

Vanilla JavaScript WYSIWYG web editor (2.x legacy version, actively maintained)

1,333 lines (1,162 loc) 84.3 kB
/* * wysiwyg web editor * * suneditor.js * Copyright 2017 JiHong Lee. * MIT license. */ 'use strict'; /** * @description utility function */ const util = { _d: null, _w: null, isIE: null, isIE_Edge: null, isOSX_IOS: null, isChromium: null, isMobile: null, isResizeObserverSupported: null, _propertiesInit: function () { if (this._d) return; this._d = document; this._w = window; this.isIE = navigator.userAgent.indexOf('Trident') > -1; this.isIE_Edge = (navigator.userAgent.indexOf('Trident') > -1) || (navigator.appVersion.indexOf('Edge') > -1); this.isOSX_IOS = /(Mac|iPhone|iPod|iPad)/.test(navigator.platform); this.isChromium = !!window.chrome; this.isResizeObserverSupported = (typeof ResizeObserver === 'function'); this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || ((navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) && 'ontouchstart' in window); }, _allowedEmptyNodeList: '.se-component, pre, blockquote, hr, li, table, img, iframe, video, audio, canvas', /** * @description HTML Reserved Word Converter. * @param {String} contents * @returns {String} HTML string * @private */ _HTMLConvertor: function (contents) { const ec = {'&': '&amp;', '\u00A0': '&nbsp;', '\'': '&apos;', '"': '&quot;', '<': '&lt;', '>': '&gt;'}; return contents.replace(/&|\u00A0|'|"|<|>/g, function (m) { return (typeof ec[m] === 'string') ? ec[m] : m; }); }, /** * @description Unicode Character 'ZERO WIDTH SPACE' (\u200B) */ zeroWidthSpace: String.fromCharCode(8203), /** * @description Regular expression to find 'zero width space' (/\u200B/g) */ zeroWidthRegExp: new RegExp(String.fromCharCode(8203), 'g'), /** * @description Regular expression to find only 'zero width space' (/^\u200B+$/) */ onlyZeroWidthRegExp: new RegExp('^' + String.fromCharCode(8203) + '+$'), fontValueMap: { 'xx-small': 1, 'x-small': 2, 'small': 3, 'medium': 4, 'large': 5, 'x-large': 6, 'xx-large': 7 }, /** * @description A method that checks If the text is blank or to see if it contains 'ZERO WIDTH SPACE' or empty (util.zeroWidthSpace) * @param {String|Node} text String value or Node * @returns {Boolean} */ onlyZeroWidthSpace: function (text) { if (text === null || text === undefined) return false; if (typeof text !== 'string') text = text.textContent; return text === '' || this.onlyZeroWidthRegExp.test(text); }, /** * @description Gets XMLHttpRequest object * @returns {XMLHttpRequest|ActiveXObject} */ getXMLHttpRequest: function () { /** IE */ if (this._w.ActiveXObject) { try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e1) { return null; } } } /** netscape */ else if (this._w.XMLHttpRequest) { return new XMLHttpRequest(); } /** fail */ else { return null; } }, /** * @description Object.values * @param {Object|null} obj Object parameter. * @returns {Array} */ getValues: function (obj) { return !obj ? [] : this._w.Object.keys(obj).map(function (i) { return obj[i]; }); }, /** * @description Convert the CamelCase To the KebabCase. * @param {String|Array} param [Camel string] * @returns {String|Array} */ camelToKebabCase: function (param) { if (typeof param === 'string') { return param.replace(/[A-Z]/g, function (letter) { return "-" + letter.toLowerCase(); }); } else { return param.map(function(str) { return util.camelToKebabCase(str); }); } }, /** * @description Convert the KebabCase To the CamelCase. * @param {String|Array} param [KebabCase string] * @returns {String|Array} */ kebabToCamelCase: function (param) { if (typeof param === 'string') { return param.replace(/-[a-zA-Z]/g, function (letter) { return letter.replace('-', '').toUpperCase(); }); } else { return param.map(function(str) { return util.camelToKebabCase(str); }); } }, /** * @description Create Element node * @param {String} elementName Element name * @returns {Element} */ createElement: function (elementName) { return this._d.createElement(elementName); }, /** * @description Create text node * @param {String} text text contents * @returns {Node} */ createTextNode: function (text) { return this._d.createTextNode(text || ''); }, /** * @description The editor checks tags by string. * If there is "<" or ">" in the attribute of tag, HTML is broken when checking the tag. * When using an attribute with "<" or ">", use "HTMLEncoder" to save. (ex: math(katex)) * @param {String} contents HTML or Text string * @returns {String} */ HTMLEncoder: function (contents) { const ec = {'<': '$lt;', '>': '$gt;'}; return contents.replace(/<|>/g, function (m) { return (typeof ec[m] === 'string') ? ec[m] : m; }); }, /** * @description The editor checks tags by string. * If there is "<" or ">" in the attribute of tag, HTML is broken when checking the tag. * Decoder of data stored as "HTMLEncoder" (ex: math(katex)) * @param {String} contents HTML or Text string * @returns {String} */ HTMLDecoder: function (contents) { const ec = {'$lt;': '<', '$gt;': '>'}; return contents.replace(/\$lt;|\$gt;/g, function (m) { return (typeof ec[m] === 'string') ? ec[m] : m; }); }, /** * @description This method run Object.prototype.hasOwnProperty.call(obj, key) * @param {Object} obj Object * @param {String} key obj.key * @returns {Boolean} */ hasOwn: function (obj, key) { return this._hasOwn.call(obj, key); }, _hasOwn: Object.prototype.hasOwnProperty, /** * @deprecated * @description Get the the tag path of the arguments value * If not found, return the first found value * @param {Array} nameArray File name array * @param {String} extension js, css * @returns {String} */ getIncludePath: function (nameArray, extension) { let path = ''; const pathList = []; const tagName = extension === 'js' ? 'script' : 'link'; const src = extension === 'js' ? 'src' : 'href'; let fileName = '(?:'; for (let i = 0, len = nameArray.length; i < len; i++) { fileName += nameArray[i] + (i < len - 1 ? '|' : ')'); } const regExp = new this._w.RegExp('(^|.*[\\/])' + fileName + '(\\.[^\\/]+)?\.' + extension + '(?:\\?.*|;.*)?$', 'i'); const extRegExp = new this._w.RegExp('.+\\.' + extension + '(?:\\?.*|;.*)?$', 'i'); for (let c = this._d.getElementsByTagName(tagName), i = 0; i < c.length; i++) { if (extRegExp.test(c[i][src])) { pathList.push(c[i]); } } for (let i = 0; i < pathList.length; i++) { let editorTag = pathList[i][src].match(regExp); if (editorTag) { path = editorTag[0]; break; } } if (path === '') path = pathList.length > 0 ? pathList[0][src] : ''; -1 === path.indexOf(':/') && '//' !== path.slice(0, 2) && (path = 0 === path.indexOf('/') ? location.href.match(/^.*?:\/\/[^\/]*/)[0] + path : location.href.match(/^[^\?]*\/(?:)/)[0] + path); if (!path) throw '[SUNEDITOR.util.getIncludePath.fail] The SUNEDITOR installation path could not be automatically detected. (name: +' + name + ', extension: ' + extension + ')'; return path; }, /** * @deprecated * @description Returns the CSS text that has been applied to the current page. * @param {Document|null} doc To get the CSS text of an document(core._wd). If null get the current document. * @returns {String} Styles string */ getPageStyle: function (doc) { let cssText = ''; const sheets = (doc || this._d).styleSheets; for (let i = 0, len = sheets.length, rules; i < len; i++) { try { rules = sheets[i].cssRules; } catch (e) { continue; } if (rules) { for (let c = 0, cLen = rules.length; c < cLen; c++) { cssText += rules[c].cssText; } } } return cssText; }, /** * @description Get the argument iframe's document object * @param {Element} iframe Iframe element (context.element.wysiwygFrame) * @returns {Document} */ getIframeDocument: function (iframe) { let wDocument = iframe.contentWindow || iframe.contentDocument; if (wDocument.document) wDocument = wDocument.document; return wDocument; }, /** * @description Get attributes of argument element to string ('class="---" name="---" ') * @param {Element} element Element object * @param {Array|null} exceptAttrs Array of attribute names to exclude from the result * @returns {String} */ getAttributesToString: function (element, exceptAttrs) { if (!element.attributes) return ''; const attrs = element.attributes; let attrString = ''; for (let i = 0, len = attrs.length; i < len; i++) { if (exceptAttrs && exceptAttrs.indexOf(attrs[i].name) > -1) continue; attrString += attrs[i].name + '="' + attrs[i].value + '" '; } return attrString; }, /** * @descriptionGets Get the length in bytes of a string. * referencing code: "https://github.com/shaan1974/myrdin/blob/master/expressions/string.js#L11" * @param {String} text String text * @returns {Number} */ getByteLength: function(text) { if (!text || !text.toString) return 0; text = text.toString(); const encoder = this._w.encodeURIComponent; let cr, cl; if (this.isIE_Edge) { cl = this._w.unescape(encoder(text)).length; cr = 0; if (encoder(text).match(/(%0A|%0D)/gi) !== null) { cr = encoder(text).match(/(%0A|%0D)/gi).length; } return cl + cr; } else { cl = (new this._w.TextEncoder('utf-8').encode(text)).length; cr = 0; if (encoder(text).match(/(%0A|%0D)/gi) !== null) { cr = encoder(text).match(/(%0A|%0D)/gi).length; } return cl + cr; } }, /** * @description It is judged whether it is the edit region top div element or iframe's body tag. * @param {Node} element The node to check * @returns {Boolean} */ isWysiwygDiv: function (element) { return element && element.nodeType === 1 && (this.hasClass(element, 'se-wrapper-wysiwyg') || /^BODY$/i.test(element.nodeName)); }, /** * @description It is judged whether it is the contenteditable property is false. * @param {Node} element The node to check * @returns {Boolean} */ isNonEditable: function (element) { return element && element.nodeType === 1 && element.getAttribute('contenteditable') === 'false'; }, /** * @description It is judged whether it is a node related to the text style. * (strong|span|font|b|var|i|em|u|ins|s|strike|del|sub|sup|mark|a|label|code) * @param {Node} element The node to check * @returns {Boolean} */ isTextStyleElement: function (element) { return element && element.nodeType !== 3 && /^(strong|span|font|b|var|i|em|u|ins|s|strike|del|sub|sup|mark|a|label|code|summary)$/i.test(element.nodeName); }, /** * @description It is judged whether it is the input element (INPUT, TEXTAREA) * @param {Node} element The node to check * @returns */ isInputElement: function (element) { return element && element.nodeType === 1 && /^(INPUT|TEXTAREA)$/i.test(element.nodeName); }, /** * @description It is judged whether it is the format element (P, DIV, H[1-6], PRE, LI | class="__se__format__replace_xxx") * Format element also contain "free format Element" * @param {Node} element The node to check * @returns {Boolean} */ isFormatElement: function (element) { return element && element.nodeType === 1 && (/^(P|DIV|H[1-6]|PRE|LI|TH|TD|DETAILS)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__replace_.+(\\s|$)|(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element); }, /** * @description It is judged whether it is the range format element. (BLOCKQUOTE, OL, UL, FIGCAPTION, TABLE, THEAD, TBODY, TR, TH, TD | class="__se__format__range_xxx") * Range format element is wrap the "format element" and "component" * @param {Node} element The node to check * @returns {Boolean} */ isRangeFormatElement: function (element) { return element && element.nodeType === 1 && (/^(BLOCKQUOTE|OL|UL|FIGCAPTION|TABLE|THEAD|TBODY|TR|TH|TD|DETAILS)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__range_.+(\\s|$)')); }, /** * @description It is judged whether it is the closure range format element. (TH, TD | class="__se__format__range__closure_xxx") * Closure range format elements is included in the range format element. * - Closure range format element is wrap the "format element" and "component" * ※ You cannot exit this format with the Enter key or Backspace key. * ※ Use it only in special cases. ([ex] format of table cells) * @param {Node} element The node to check * @returns {Boolean} */ isClosureRangeFormatElement: function (element) { return element && element.nodeType === 1 && (/^(TH|TD)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__range__closure_.+(\\s|$)')); }, /** * @description It is judged whether it is the free format element. (PRE | class="__se__format__free_xxx") * Free format elements is included in the format element. * Free format elements's line break is "BR" tag. * ※ Entering the Enter key in the space on the last line ends "Free Format" and appends "Format". * @param {Node} element The node to check * @returns {Boolean} */ isFreeFormatElement: function (element) { return element && element.nodeType === 1 && (/^PRE$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element); }, /** * @description It is judged whether it is the closure free format element. (class="__se__format__free__closure_xxx") * Closure free format elements is included in the free format element. * - Closure free format elements's line break is "BR" tag. * ※ You cannot exit this format with the Enter key or Backspace key. * ※ Use it only in special cases. ([ex] format of table cells) * @param {Node} element The node to check * @returns {Boolean} */ isClosureFreeFormatElement: function (element) { return element && element.nodeType === 1 && this.hasClass(element, '(\\s|^)__se__format__free__closure_.+(\\s|$)'); }, /** * @description It is judged whether it is the component[img, iframe, video, audio, table] cover(class="se-component") and table, hr * @param {Node} element The node to check * @returns {Boolean} */ isComponent: function (element) { return element && (/se-component/.test(element.className) || /^(TABLE|HR)$/.test(element.nodeName)); }, /** * @description Checks for "__se__uneditable" in the class list. * Components with class "__se__uneditable" cannot be modified. * @param {Element} element The element to check * @returns {Boolean} */ isUneditableComponent: function (element) { return element && this.hasClass(element, '__se__uneditable'); }, /** * @description It is judged whether it is the component [img, iframe] cover(class="se-component") * @param {Node} element The node to check * @returns {Boolean} */ isMediaComponent: function (element) { return element && /se-component/.test(element.className); }, /** * @description It is judged whether it is the not checking node. (class="katex", "__se__tag") * @param {Node} element The node to check * @returns {Boolean} */ isNotCheckingNode: function (element) { return element && /katex|__se__tag/.test(element.className); }, /** * @description If a parent node that contains an argument node finds a format node (util.isFormatElement), it returns that node. * @param {Node} element Reference node. * @param {Function|null} validation Additional validation function. * @returns {Element|null} */ getFormatElement: function (element, validation) { if (!element) return null; if (!validation) { validation = function () { return true; }; } while (element) { if (this.isWysiwygDiv(element)) return null; if (this.isRangeFormatElement(element)) element.firstElementChild; if (this.isFormatElement(element) && validation(element)) return element; element = element.parentNode; } return null; }, /** * @description If a parent node that contains an argument node finds a format node (util.isRangeFormatElement), it returns that node. * @param {Node} element Reference node. * @param {Function|null} validation Additional validation function. * @returns {Element|null} */ getRangeFormatElement: function (element, validation) { if (!element) return null; if (!validation) { validation = function () { return true; }; } while (element) { if (this.isWysiwygDiv(element)) return null; if (this.isRangeFormatElement(element) && !/^(THEAD|TBODY|TR)$/i.test(element.nodeName) && validation(element)) return element; element = element.parentNode; } return null; }, /** * @description If a parent node that contains an argument node finds a free format node (util.isFreeFormatElement), it returns that node. * @param {Node} element Reference node. * @param {Function|null} validation Additional validation function. * @returns {Element|null} */ getFreeFormatElement: function (element, validation) { if (!element) return null; if (!validation) { validation = function () { return true; }; } while (element) { if (this.isWysiwygDiv(element)) return null; if (this.isFreeFormatElement(element) && validation(element)) return element; element = element.parentNode; } return null; }, /** * @description If a parent node that contains an argument node finds a closure free format node (util.isClosureFreeFormatElement), it returns that node. * @param {Node} element Reference node. * @param {Function|null} validation Additional validation function. * @returns {Element|null} */ getClosureFreeFormatElement: function (element, validation) { if (!element) return null; if (!validation) { validation = function () { return true; }; } while (element) { if (this.isWysiwygDiv(element)) return null; if (this.isClosureFreeFormatElement(element) && validation(element)) return element; element = element.parentNode; } return null; }, /** * @description Add style and className of copyEl to originEl * @param {Element} originEl Origin element * @param {Element} copyEl Element to copy * @param {Array|null} blacklist Blacklist array(LowerCase) */ copyTagAttributes: function (originEl, copyEl, blacklist) { if (copyEl.style.cssText) { const copyStyles = copyEl.style; for (let i = 0, len = copyStyles.length; i < len; i++) { originEl.style[copyStyles[i]] = copyStyles[copyStyles[i]]; } } const attrs = copyEl.attributes; for (let i = 0, len = attrs.length, name; i < len; i++) { name = attrs[i].name.toLowerCase(); if ((blacklist && blacklist.indexOf(name) > -1) || !attrs[i].value) originEl.removeAttribute(name); else if (name !== 'style') originEl.setAttribute(attrs[i].name, attrs[i].value); } }, /** * @description Copy and apply attributes of format tag that should be maintained. (style, class) Ignore "__se__format__" class * @param {Element} originEl Origin element * @param {Element} copyEl Element to copy */ copyFormatAttributes: function (originEl, copyEl) { copyEl = copyEl.cloneNode(false); copyEl.className = copyEl.className.replace(/(\s|^)__se__format__[^\s]+/g, ''); this.copyTagAttributes(originEl, copyEl); }, /** * @description Get the item from the array that matches the condition. * @param {Array|HTMLCollection|NodeList} array Array to get item * @param {Function|null} validation Conditional function * @param {Boolean} multi If true, returns all items that meet the criteria otherwise, returns an empty array. * If false, returns only one item that meet the criteria otherwise return null. * @returns {Array|Node|null} */ getArrayItem: function (array, validation, multi) { if (!array || array.length === 0) return null; validation = validation || function () { return true; }; const arr = []; for (let i = 0, len = array.length, a; i < len; i++) { a = array[i]; if (validation(a)) { if (!multi) return a; else arr.push(a); } } return !multi ? null : arr; }, /** * @description Check if an array contains an element * @param {Array|HTMLCollection|NodeList} array element array * @param {Node} element The element to check for * @returns {Boolean} */ arrayIncludes: function(array, element) { for (let i = 0; i < array.length; i++) { if (array[i] === element) { return true; } } return false; }, /** * @description Get the index of the argument value in the element array * @param {Array|HTMLCollection|NodeList} array element array * @param {Node} element The element to find index * @returns {Number} */ getArrayIndex: function (array, element) { let idx = -1; for (let i = 0, len = array.length; i < len; i++) { if (array[i] === element) { idx = i; break; } } return idx; }, /** * @description Get the next index of the argument value in the element array * @param {Array|HTMLCollection|NodeList} array element array * @param {Node} item The element to find index * @returns {Number} */ nextIdx: function (array, item) { let idx = this.getArrayIndex(array, item); if (idx === -1) return -1; return idx + 1; }, /** * @description Get the previous index of the argument value in the element array * @param {Array|HTMLCollection|NodeList} array Element array * @param {Node} item The element to find index * @returns {Number} */ prevIdx: function (array, item) { let idx = this.getArrayIndex(array, item); if (idx === -1) return -1; return idx - 1; }, /** * @description Returns the index compared to other sibling nodes. * @param {Node} node The Node to find index * @returns {Number} */ getPositionIndex: function (node) { let idx = 0; while ((node = node.previousSibling)) { idx += 1; } return idx; }, /** * @description Returns the position of the "node" in the "parentNode" in a numerical array. * ex) <p><span>aa</span><span>bb</span></p> : getNodePath(node: "bb", parentNode: "<P>") -> [1, 0] * @param {Node} node The Node to find position path * @param {Node|null} parentNode Parent node. If null, wysiwyg div area * @param {Object|null} _newOffsets If you send an object of the form "{s: 0, e: 0}", the text nodes that are attached together are merged into one, centered on the "node" argument. * "_newOffsets.s" stores the length of the combined characters after "node" and "_newOffsets.e" stores the length of the combined characters before "node". * Do not use unless absolutely necessary. * @returns {Array} */ getNodePath: function (node, parentNode, _newOffsets) { const path = []; let finds = true; this.getParentElement(node, function (el) { if (el === parentNode) finds = false; if (finds && !this.isWysiwygDiv(el)) { // merge text nodes if (_newOffsets && el.nodeType === 3) { let temp = null, tempText = null; _newOffsets.s = _newOffsets.e = 0; let previous = el.previousSibling; while (previous && previous.nodeType === 3) { tempText = previous.textContent.replace(this.zeroWidthRegExp, ''); _newOffsets.s += tempText.length; el.textContent = tempText + el.textContent; temp = previous; previous = previous.previousSibling; this.removeItem(temp); } let next = el.nextSibling; while (next && next.nodeType === 3) { tempText = next.textContent.replace(this.zeroWidthRegExp, ''); _newOffsets.e += tempText.length; el.textContent += tempText; temp = next; next = next.nextSibling; this.removeItem(temp); } } // index push path.push(el); } return false; }.bind(this)); return path.map(this.getPositionIndex).reverse(); }, /** * @description Returns the node in the location of the path array obtained from "util.getNodePath". * @param {Array} offsets Position array, array obtained from "util.getNodePath" * @param {Node} parentNode Base parent element * @returns {Node} */ getNodeFromPath: function (offsets, parentNode) { let current = parentNode; let nodes; for (let i = 0, len = offsets.length; i < len; i++) { nodes = current.childNodes; if (nodes.length === 0) break; if (nodes.length <= offsets[i]) { current = nodes[nodes.length - 1]; } else { current = nodes[offsets[i]]; } } return current; }, /** * @description Compares the style and class for equal values. * Returns true if both are text nodes. * @param {Node} a Node to compare * @param {Node} b Node to compare * @returns {Boolean} */ isSameAttributes: function (a, b) { if (a.nodeType === 3 && b.nodeType === 3) return true; if (a.nodeType === 3 || b.nodeType === 3) return false; const style_a = a.style; const style_b = b.style; let compStyle = 0; for (let i = 0, len = style_a.length; i < len; i++) { if (style_a[style_a[i]] === style_b[style_a[i]]) compStyle++; } const class_a = a.classList; const class_b = b.classList; const reg = this._w.RegExp; let compClass = 0; for (let i = 0, len = class_a.length; i < len; i++) { if (reg('(\s|^)' + class_a[i] + '(\s|$)').test(class_b.value)) compClass++; } return (compStyle === style_b.length && compStyle === style_a.length) && (compClass === class_b.length && compClass === class_a.length); }, /** * @description Check the line element(util.isFormatElement) is empty. * @param {Element} element Format element node * @returns {Boolean} */ isEmptyLine: function (element) { return !element || !element.parentNode || (!element.querySelector('IMG, IFRAME, AUDIO, VIDEO, CANVAS, TABLE') && element.children.length === 0 && this.onlyZeroWidthSpace(element.textContent)); }, /** * @description Check the span's attributes are empty. * @param {Element|null} element Element node * @returns {Boolean} */ isSpanWithoutAttr: function (element) { return !!element && element.nodeType === 1 && /^SPAN$/i.test(element.nodeName) && !element.className && !element.style.cssText; }, /** * @description Check the node is a list (ol, ul) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isList: function (node) { return node && /^(OL|UL)$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a list cell (li) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isListCell: function (node) { return node && /^LI$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a table (table, thead, tbody, tr, th, td) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isTable: function (node) { return node && /^(TABLE|THEAD|TBODY|TR|TH|TD)$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a table cell (td, th) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isCell: function (node) { return node && /^(TD|TH)$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a break node (BR) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isBreak: function (node) { return node && /^BR$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a anchor node (A) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isAnchor: function (node) { return node && /^A$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a media node (img, iframe, audio, video, canvas) * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isMedia: function (node) { return node && /^(IMG|IFRAME|AUDIO|VIDEO|CANVAS)$/i.test(typeof node === 'string' ? node : node.nodeName); }, /** * @description Check the node is a figure tag or util.isMedia() * @param {Node|String} node The element or element name to check * @returns {Boolean} */ isFigures: function (node) { return node && (this.isMedia(node) || /^(FIGURE)$/i.test(typeof node === 'string' ? node : node.nodeName)); }, /** * @description Checks for numeric (with decimal point). * @param {String|Number} text Text string or number * @returns {Boolean} */ isNumber: function (text) { return !!text && /^-?\d+(\.\d+)?$/.test(text + ''); }, /** * @description Get a number. * @param {String|Number} text Text string or number * @param {Number} maxDec Maximum number of decimal places (-1 : Infinity) * @returns {Number} */ getNumber: function (text, maxDec) { if (!text) return 0; let number = (text + '').match(/-?\d+(\.\d+)?/); if (!number || !number[0]) return 0; number = number[0]; return maxDec < 0 ? number * 1 : maxDec === 0 ? this._w.Math.round(number * 1) : (number * 1).toFixed(maxDec) * 1; }, /** * @description Get all "children" of the argument value element (Without text nodes) * @param {Element} element element to get child node * @param {Function|null} validation Conditional function * @returns {Array} */ getListChildren: function (element, validation) { const children = []; if (!element || !element.children || element.children.length === 0) return children; validation = validation || function () { return true; }; (function recursionFunc(current) { if (element !== current && validation(current)) { children.push(current); } if (!!current.children) { for (let i = 0, len = current.children.length; i < len; i++) { recursionFunc(current.children[i]); } } })(element); return children; }, /** * @description Get all "childNodes" of the argument value element (Include text nodes) * @param {Node} element element to get child node * @param {Function|null} validation Conditional function * @returns {Array} */ getListChildNodes: function (element, validation) { const children = []; if (!element || element.childNodes.length === 0) return children; validation = validation || function () { return true; }; (function recursionFunc(current) { if (element !== current && validation(current)) { children.push(current); } for (let i = 0, len = current.childNodes.length; i < len; i++) { recursionFunc(current.childNodes[i]); } })(element); return children; }, /** * @description Returns the number of parents nodes. * "0" when the parent node is the WYSIWYG area. * "-1" when the element argument is the WYSIWYG area. * @param {Node} element The element to check * @returns {Number} */ getElementDepth: function (element) { if (!element || this.isWysiwygDiv(element)) return -1; let depth = 0; element = element.parentNode; while (element && !this.isWysiwygDiv(element)) { depth += 1; element = element.parentNode; } return depth; }, /** * @description Compares two elements to find a common ancestor, and returns the order of the two elements. * @param {Node} a Node to compare. * @param {Node} b Node to compare. * @returns {Object} { ancesstor, a, b, result: (a > b ? 1 : a < b ? -1 : 0) }; */ compareElements: function (a, b) { let aNode = a, bNode = b; while (aNode && bNode && aNode.parentNode !== bNode.parentNode) { aNode = aNode.parentNode; bNode = bNode.parentNode; } if (!aNode || !bNode) return { ancestor: null, a: a, b: b, result: 0 }; const children = aNode.parentNode.childNodes; const aIndex = this.getArrayIndex(children, aNode); const bIndex = this.getArrayIndex(children, bNode); return { ancestor: aNode.parentNode, a: aNode, b: bNode, result: aIndex > bIndex ? 1 : aIndex < bIndex ? -1 : 0 }; }, /** * @description Get the parent element of the argument value. * A tag that satisfies the query condition is imported. * Returns null if not found. * @param {Node} element Reference element * @param {String|Function} query Query String (nodeName, .className, #ID, :name) or validation function. * Not use it like jquery. * Only one condition can be entered at a time. * @returns {Element|null} */ getParentElement: function (element, query) { let check; if (typeof query === 'function') { check = query; } else { let attr; if (/^\./.test(query)) { attr = 'className'; query = query.split('.')[1]; } else if (/^#/.test(query)) { attr = 'id'; query = '^' + query.split('#')[1] + '$'; } else if (/^:/.test(query)) { attr = 'name'; query = '^' + query.split(':')[1] + '$'; } else { attr = 'nodeName'; query = '^' + query + '$'; } const regExp = new this._w.RegExp(query, 'i'); check = function (el) { return regExp.test(el[attr]); }; } while (element && !check(element)) { if (this.isWysiwygDiv(element)) { return null; } element = element.parentNode; } return element; }, /** * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child * Returns null if not found. * @param {Node} node Reference element * @param {Node|null} ceiling Highest boundary allowed * @returns {Node|null} */ getPreviousDeepestNode: function (node, ceiling) { let previousNode = node.previousSibling; if (!previousNode) { for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) { if (parentNode === ceiling) return null; if (parentNode.previousSibling) { previousNode = parentNode.previousSibling; break; } } if (!previousNode) return null; } while (previousNode.lastChild) previousNode = previousNode.lastChild; return previousNode; }, /** * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child * Returns null if not found. * @param {Node} node Reference element * @param {Node|null} ceiling Highest boundary allowed * @returns {Node|null} */ getNextDeepestNode: function (node, ceiling) { let nextNode = node.nextSibling; if (!nextNode) { for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) { if (parentNode === ceiling) return null; if (parentNode.nextSibling) { nextNode = parentNode.nextSibling; break; } } if (!nextNode) return null; } while (nextNode.firstChild) nextNode = nextNode.firstChild; return nextNode; }, /** * @description Get the child element of the argument value. * A tag that satisfies the query condition is imported. * Returns null if not found. * @param {Node} element Reference element * @param {String|Function} query Query String (nodeName, .className, #ID, :name) or validation function. * @param {Boolean} last If true returns the last node among the found child nodes. (default: first node) * Not use it like jquery. * Only one condition can be entered at a time. * @returns {Element|null} */ getChildElement: function (element, query, last) { let check; if (typeof query === 'function') { check = query; } else { let attr; if (/^\./.test(query)) { attr = 'className'; query = query.split('.')[1]; } else if (/^#/.test(query)) { attr = 'id'; query = '^' + query.split('#')[1] + '$'; } else if (/^:/.test(query)) { attr = 'name'; query = '^' + query.split(':')[1] + '$'; } else { attr = 'nodeName'; query = '^' + (query === 'text' ? '#' + query : query) + '$'; } const regExp = new this._w.RegExp(query, 'i'); check = function (el) { return regExp.test(el[attr]); }; } const childList = this.getListChildNodes(element, function (current) { return check(current); }); return childList[last ? childList.length - 1 : 0]; }, /** * @description 1. The first node of all the child nodes of the "first" element is returned. * 2. The last node of all the child nodes of the "last" element is returned. * 3. When there is no "last" element, the first and last nodes of all the children of the "first" element are returned. * { sc: "first", ec: "last" } * @param {Node} first First element * @param {Node|null} last Last element * @returns {Object} */ getEdgeChildNodes: function (first, last) { if (!first) return; if (!last) last = first; while (first && first.nodeType === 1 && first.childNodes.length > 0 && !this.isBreak(first)) first = first.firstChild; while (last && last.nodeType === 1 && last.childNodes.length > 0 && !this.isBreak(last)) last = last.lastChild; return { sc: first, ec: last || first }; }, /** * @description Returns the position of the left and top of argument. {left:0, top:0} * @param {Node} element Target node * @param {Element|null} wysiwygFrame When use iframe option, iframe object should be sent (context.element.wysiwygFrame) * @returns {Object} */ getOffset: function (element, wysiwygFrame) { let offsetLeft = 0; let offsetTop = 0; let offsetElement = element.nodeType === 3 ? element.parentElement : element; const wysiwyg = this.getParentElement(element, this.isWysiwygDiv.bind(this)); while (offsetElement && !this.hasClass(offsetElement, 'se-container') && offsetElement !== wysiwyg) { offsetLeft += offsetElement.offsetLeft; offsetTop += offsetElement.offsetTop; offsetElement = offsetElement.offsetParent; } const iframe = wysiwygFrame && /iframe/i.test(wysiwygFrame.nodeName); return { left: offsetLeft + (iframe ? wysiwygFrame.parentElement.offsetLeft : 0), top: (offsetTop - (wysiwyg ? wysiwyg.scrollTop : 0)) + (iframe ? wysiwygFrame.parentElement.offsetTop : 0) }; }, /** * @description It compares the start and end indexes of "a" and "b" and returns the number of overlapping indexes in the range. * ex) 1, 5, 4, 6 => "2" (4 ~ 5) * @param {Number} aStart Start index of "a" * @param {Number} aEnd End index of "a" * @param {Number} bStart Start index of "b" * @param {Number} bEnd Start index of "b" * @returns {Number} */ getOverlapRangeAtIndex: function (aStart, aEnd, bStart, bEnd) { if (aStart <= bEnd ? aEnd < bStart : aEnd > bStart) return 0; const overlap = (aStart > bStart ? aStart : bStart) - (aEnd < bEnd ? aEnd : bEnd); return (overlap < 0 ? overlap * -1 : overlap) + 1; }, /** * @description Set the text content value of the argument value element * @param {Node} element Element to replace text content * @param {String} txt Text to be applied */ changeTxt: function (element, txt) { if (!element || !txt) return; element.textContent = txt; }, /** * @description Replace element * @param {Element} element Target element * @param {String|Element} newElement String or element of the new element to apply */ changeElement: function (element, newElement) { if (typeof newElement === 'string') { if (element.outerHTML) { element.outerHTML = newElement; } else { const doc = this.createElement('DIV'); doc.innerHTML = newElement; newElement = doc.firstChild; element.parentNode.replaceChild(newElement, element); } } else if (newElement.nodeType === 1) { element.parentNode.replaceChild(newElement, element); } }, /** * @description Set style, if all styles are deleted, the style properties are deleted. * @param {Element} element Element to set style * @param {String} styleName Style attribute name (marginLeft, textAlign...) * @param {String|Number} value Style value */ setStyle: function (element, styleName, value) { element.style[styleName] = value; if (!value && !element.style.cssText) { element.removeAttribute('style'); } }, /** * @description Determine whether any of the matched elements are assigned the given class * @param {Element} element Elements to search class name * @param {String} className Class name to search for * @returns {Boolean} */ hasClass: function (element, className) { if (!element) return; return (new this._w.RegExp(className)).test(element.className); }, /** * @description Append the className value of the argument value element * @param {Element} element Elements to add class name * @param {String} className Class name to be add */ addClass: function (element, className) { if (!element) return; const check = new this._w.RegExp('(\\s|^)' + className + '(\\s|$)'); if (check.test(element.className)) return; element.className += (element.className.length > 0 ? ' ' : '') + className; }, /** * @description Delete the className value of the argument value element * @param {Element} element Elements to remove class name * @param {String} className Class name to be remove */ removeClass: function (element, className) { if (!element) return; const check = new this._w.RegExp('(\\s|^)' + className + '(\\s|$)'); element.className = element.className.replace(check, ' ').trim(); if (!element.className.trim()) element.removeAttribute('class'); }, /** * @description Argument value If there is no class name, insert it and delete the class name if it exists * @param {Element} element Elements to replace class name * @param {String} className Class name to be change * @returns {Boolean|undefined} */ toggleClass: function (element, className) { if (!element) return; let result = false; const check = new this._w.RegExp('(\\s|^)' + className + '(\\s|$)'); if (check.test(element.className)) { element.className = element.className.replace(check, ' ').trim(); } else { element.className += ' ' + className; result = true; } if (!element.className.trim()) element.removeAttribute('class'); return result; }, /** * @description Checks if element can't be easily enabled * @param {Element} element Element to check for */ isImportantDisabled: function (element) { return element.hasAttribute('data-important-disabled'); }, /** * @description In the predefined code view mode, the buttons except the executable button are changed to the 'disabled' state. * core.codeViewDisabledButtons (An array of buttons whose class name is not "se-code-view