diff --git a/sample/html/out/document-editor.html b/sample/html/out/document-editor.html index e539c9c94..2a0aed52d 100644 --- a/sample/html/out/document-editor.html +++ b/sample/html/out/document-editor.html @@ -942,6 +942,14 @@
Parameters:
+

nativeFocus()

+
+ Focus to wysiwyg area using "native focus function" +
+
+ +

focus()

@@ -1968,6 +1976,7 @@

core

  • controllersOn
  • controllersOff
  • execCommand
  • +
  • nativeFocus
  • focus
  • focusEdge
  • setRange
  • diff --git a/sample/html/out/document-util.html b/sample/html/out/document-util.html index 1f26d39d5..dfc017530 100644 --- a/sample/html/out/document-util.html +++ b/sample/html/out/document-util.html @@ -335,6 +335,34 @@
    Parameters:
    +

    isTextStyleElement(element) → {Boolean}

    +
    + 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) +
    +
    Parameters:
    + + + + + + + + + + + + + + + +
    NameTypeDescription
    element + Element + The element to check
    +
    + +

    isFormatElement(element) → {Boolean}

    @@ -2111,6 +2139,7 @@

    util

  • getAttributesToString
  • getByteLength
  • isWysiwygDiv
  • +
  • isTextStyleElement
  • isFormatElement
  • isRangeFormatElement
  • isFreeFormatElement
  • diff --git a/src/assets/css/suneditor.css b/src/assets/css/suneditor.css index 8d6d44009..140cf3b2f 100755 --- a/src/assets/css/suneditor.css +++ b/src/assets/css/suneditor.css @@ -100,6 +100,9 @@ .sun-editor .se-btn-primary:hover, .sun-editor .se-btn-primary:focus {color:#000; background-color:#80bdff; border-color:#3f9dff; outline:0 none;} .sun-editor .se-btn-primary:active {color:#fff; background-color:#3f9dff; border-color:#4592ff; -webkit-box-shadow:inset 0 3px 5px #4592ff; box-shadow:inset 0 3px 5px #4592ff;} +/** --- Input focus effect */ +.sun-editor input:focus, .sun-editor select:focus, .sun-editor textarea:focus {border-color:#80bdff !important; border-width:1px; border-style:solid; outline:0; -webkit-box-shadow:0 0 0 0.2rem #c7deff; box-shadow:0 0 0 0.2rem #c7deff;} + /* se-btn button active*/ .sun-editor .se-btn:enabled.active {color:#4592ff; outline:0 none;} .sun-editor .se-btn:enabled.active:hover, .sun-editor .se-btn:enabled.active:focus {background-color:#e1e1e1; border-color:#d1d1d1; outline:0 none;} @@ -203,7 +206,6 @@ /* submenu layer - form group (color selector) */ .sun-editor .se-submenu-form-group {display:flex; width:100%; height:auto; padding:4px;} .sun-editor .se-submenu-form-group input {flex:auto; display:inline-block; width:auto; height:33px; color:#555; font-size:12px; margin:1px 0 1px 0; padding:0; border-radius:0.25rem; border:1px solid #ccc;} -.sun-editor .se-submenu-form-group input:focus {border-color:#80bdff; outline:0; -webkit-box-shadow:0 0 0 0.2rem #c7deff; box-shadow:0 0 0 0.2rem #c7deff;} .sun-editor .se-submenu-form-group button {float:right; width:34px; height:34px; margin:0 0 0 4px !important;} .sun-editor .se-submenu-form-group button.se-btn {border:1px solid #ccc;} .sun-editor .se-submenu-form-group > div {position:relative;} @@ -263,7 +265,6 @@ .sun-editor .se-dialog .se-dialog-inner .se-dialog-form .se-input-select {display:inline-block; width:auto; height:34px; font-size:14px; text-align:center; line-height:1.42857143; color:#000; border:1px solid #ccc; border-radius:4px; transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;} .sun-editor .se-dialog .se-dialog-inner .se-dialog-form .se-input-control {display:inline-block; width:70px; height:34px; font-size:14px; text-align:center; line-height:1.42857143; color:#000; border:1px solid #ccc; border-radius:4px; transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;} .sun-editor .se-dialog .se-dialog-inner .se-dialog-form .se-input-form {display:block; width:100%; height:34px; font-size:14px; line-height:1.42857143; padding:0 4px; color:#000; border:1px solid #ccc; border-radius:4px; transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;} -.sun-editor .se-dialog .se-dialog-inner .se-dialog-form input:focus, .sun-editor .se-dialog .se-dialog-inner .se-dialog-form select:focus, .sun-editor .se-dialog .se-dialog-inner .se-dialog-form textarea:focus {border-color:#80bdff; outline:0; -webkit-box-shadow:0 0 0 0.2rem #c7deff; box-shadow:0 0 0 0.2rem #c7deff;} .sun-editor .se-dialog .se-dialog-inner .se-dialog-form .se-video-ratio {width:70px; margin-left:4px;} .sun-editor .se-dialog .se-dialog-inner .se-dialog-form a {color:#004cff;} /* dialog - revert button */ @@ -284,7 +285,7 @@ /** --- controller ---------------------------------------------------------- */ .sun-editor .se-controller .se-arrow.se-arrow-up {border-bottom-color:rgba(0, 0, 0, .25);} -.sun-editor .se-controller {position:absolute; display:none; overflow:visible; z-index:4; border:1px solid rgba(0, 0, 0, .25); border-radius:4px; text-align:start; text-decoration:none; text-shadow:none; text-transform:none; letter-spacing:normal; word-break:normal; word-spacing:normal; word-wrap:normal; white-space:normal; background-color:#fff; -webkit-background-clip:padding-box; background-clip:padding-box; -webkit-box-shadow:0 5px 10px rgba(0, 0, 0, .2); box-shadow:0 5px 10px rgba(0, 0, 0, .2); line-break:auto;} +.sun-editor .se-controller {position:absolute; display:none; overflow:visible; z-index:5; border:1px solid rgba(0, 0, 0, .25); border-radius:4px; text-align:start; text-decoration:none; text-shadow:none; text-transform:none; letter-spacing:normal; word-break:normal; word-spacing:normal; word-wrap:normal; white-space:normal; background-color:#fff; -webkit-background-clip:padding-box; background-clip:padding-box; -webkit-box-shadow:0 5px 10px rgba(0, 0, 0, .2); box-shadow:0 5px 10px rgba(0, 0, 0, .2); line-break:auto;} /* controller - button group */ .sun-editor .se-controller .se-btn-group {position:relative; display:flex; vertical-align:middle; padding:2px; top:0; left:0;} diff --git a/src/lib/core.js b/src/lib/core.js index 9a3d55f90..2642e7e1b 100755 --- a/src/lib/core.js +++ b/src/lib/core.js @@ -593,10 +593,9 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic }, /** - * @description Use focus method of document - * @private + * @description Focus to wysiwyg area using "native focus function" */ - _nativeFocus: function () { + nativeFocus: function () { const caption = util.getParentElement(this.getSelectionNode(), 'figcaption'); if (caption) { caption.focus(); @@ -614,7 +613,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic if (context.element.wysiwygFrame.style.display === 'none') return; if (options.iframe) { - this._nativeFocus(); + this.nativeFocus(); } else { try { const range = this.getRange(); @@ -628,7 +627,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic this.setRange(range.startContainer, range.startOffset, range.endContainer, range.endOffset); } } catch (e) { - this._nativeFocus(); + this.nativeFocus(); } } @@ -650,10 +649,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } else if (videoComponent) { this.selectComponent(videoComponent, 'video'); } - } else { + } else if (focusEl) { focusEl = util.getChildElement(focusEl, function (current) { return current.childNodes.length === 0 || current.nodeType === 3; }, true); - if (!focusEl) this._nativeFocus(); + if (!focusEl) this.nativeFocus(); else this.setRange(focusEl, focusEl.textContent.length, focusEl, focusEl.textContent.length); + } else { + this.nativeFocus(); } }, @@ -675,7 +676,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic range.setStart(startCon, startOff); range.setEnd(endCon, endOff); } catch (error) { - this._nativeFocus(); + this.nativeFocus(); } const selection = this.getSelection(); @@ -1103,14 +1104,24 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic * @returns {undefined|Object} */ insertNode: function (oNode, afterNode) { + const isComp = util.isFormatElement(oNode) || util.isRangeFormatElement(oNode) || util.isComponent(oNode); + + if (!afterNode && isComp) { + const r = this.removeNode(); + if (r.container.nodeType === 3 || util.isBreak(r.container)) { + const depthFormat = util.getParentElement(r.container, function (current) { return this.isRangeFormatElement(current); }.bind(util)); + afterNode = util.splitElement(r.container, r.offset, !depthFormat ? 0 : util.getElementDepth(depthFormat) + 1); + if (afterNode) afterNode = afterNode.previousSibling; + } + } + const range = this.getRange(); const startCon = range.startContainer; const startOff = range.startOffset; const endCon = range.endContainer; const endOff = range.endOffset; const commonCon = range.commonAncestorContainer; - let parentNode = null; - let originAfter = null; + let parentNode, originAfter = null; if (!afterNode) { parentNode = startCon; @@ -1153,11 +1164,36 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic if (!this.isEdgePoint(startCon, startOff)) removeNode = startCon.splitText(startOff); parentNode.removeChild(removeNode); + if (parentNode.childNodes.length === 0 && isComp) { + parentNode.innerHTML = '
    '; + } } else { - this.removeNode(); - parentNode = commonCon; - afterNode = endCon; + const removedTag = this.removeNode(); + const container = removedTag.container; + const prevContainer = removedTag.prevContainer; + if (container && container.childNodes.length === 0 && isComp) { + if (util.isFormatElement(container)) { + container.innerHTML = '
    '; + } else if (util.isRangeFormatElement(container)) { + container.innerHTML = '


    '; + } + } + + if (!isComp && prevContainer) { + parentNode = prevContainer.nodeType === 3 ? prevContainer.parentNode : prevContainer; + if (parentNode.contains(container)) { + afterNode = container; + while (afterNode.parentNode === parentNode) { + afterNode = afterNode.parentNode; + } + } else { + afterNode = null; + } + } else { + parentNode = isComp ? commonCon : container; + afterNode = isComp ? endCon : null; + } while (afterNode && afterNode.parentNode !== commonCon) { afterNode = afterNode.parentNode; @@ -1175,6 +1211,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic // --- insert node --- try { if (util.isFormatElement(oNode) || util.isRangeFormatElement(oNode) || (!util.isListCell(parentNode) && util.isComponent(oNode))) { + const oldParent = parentNode; if (util.isList(afterNode)) { parentNode = afterNode; afterNode = null; @@ -1185,12 +1222,18 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic parentNode = rangeCon ? container : container.parentNode; afterNode = rangeCon ? null : container.nextSibling; } + + if (oldParent.childNodes.length === 0 && parentNode !== oldParent) util.removeItem(oldParent); } if (afterNode === commonCon && util.isBreak(afterNode) && !util.isBreak(oNode)) { afterNode = afterNode.nextSibling; } + if (isComp && !util.isRangeFormatElement(parentNode) && !util.isListCell(parentNode) && !util.isWysiwygDiv(parentNode)) { + afterNode = parentNode.nextElementSibling; + parentNode = parentNode.parentNode; + } parentNode.insertBefore(oNode, afterNode); } catch (e) { parentNode.appendChild(oNode); @@ -1278,7 +1321,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } } else { if (childNodes.length === 0) { - if (util.isFormatElement(commonCon) || util.isRangeFormatElement(commonCon) || util.isFormatElement(commonCon)) { + if (util.isFormatElement(commonCon) || util.isRangeFormatElement(commonCon) || util.isWysiwygDiv(commonCon)) { return { container: commonCon, offset: 0 @@ -1366,6 +1409,11 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } container = endCon && endCon.parentNode ? endCon : startCon && startCon.parentNode ? startCon : (range.endContainer || range.startContainer); + + if (!util.isWysiwygDiv(container)) { + const rc = util.removeItemAllParents(container); + if (rc) container = rc.sc || rc.ec || context.element.wysiwyg; + } // set range this.setRange(container, offset, container, offset); @@ -1374,7 +1422,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic return { container: container, - offset: offset + offset: offset, + prevContainer: startCon && startCon.parentNode ? startCon : null }; }, @@ -3258,8 +3307,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic container = pNode.firstChild; offset = container.textContent.length; } else if (util.onlyZeroWidthSpace(container)) { - container = pNode; - offset = 0; + container = newInnerNode; + offset = 1; } // node change @@ -3333,12 +3382,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic this.focus(); break; case 'codeView': - this.toggleCodeView(); util.toggleClass(target, 'active'); + this.toggleCodeView(); break; case 'fullScreen': - this.toggleFullScreen(target); util.toggleClass(target, 'active'); + this.toggleFullScreen(target); break; case 'indent': case 'outdent': @@ -3361,8 +3410,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic this.preview(); break; case 'showBlocks': - this.toggleDisplayBlocks(); util.toggleClass(target, 'active'); + this.toggleDisplayBlocks(); break; case 'save': if (typeof options.callBackSave === 'function') { @@ -3497,7 +3546,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } } - this._nativeFocus(); + this.nativeFocus(); // history stack this.history.push(false); @@ -3807,6 +3856,42 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } }, + /** + * @description Returns HTML string according to tag type and configuration. + * Use only "cleanHTML", "convertContentsForEditor" + * @param {Node} node Node + * @param {Boolean} requireFormat If true, text nodes that do not have a format node is wrapped with the format tag. + * @private + */ + _makeLine: function (node, requireFormat) { + // element + if (node.nodeType === 1) { + if (util._notAllowedTags(node)) return ''; + if (!requireFormat || (util.isFormatElement(node) || util.isRangeFormatElement(node) || util.isComponent(node) || util.isMedia(node))) { + return node.outerHTML; + } else { + return '

    ' + node.outerHTML + '

    '; + } + } + // text + if (node.nodeType === 3) { + if (!requireFormat) return node.textContent; + const textArray = node.textContent.split(/\n/g); + let html = ''; + for (let i = 0, tLen = textArray.length, text; i < tLen; i++) { + text = textArray[i].trim(); + if (text.length > 0) html += '

    ' + text + '

    '; + } + return html; + } + // comments + if (node.nodeType === 8 && this._allowHTMLComments) { + return '<__comment__>' + node.textContent.trim() + ''; + } + + return ''; + }, + /** * @description Gets the clean HTML code for editor * @param {String} html HTML string @@ -3815,19 +3900,25 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic * @returns {String} */ cleanHTML: function (html, whitelist) { - const tagsAllowed = new this._w.RegExp('^(meta|script|link|style|[a-z]+\:[a-z]+)$', 'i'); - const domTree = this._d.createRange().createContextualFragment(html).childNodes; + const dom = this._d.createRange().createContextualFragment(html); + util._consistencyCheckOfHTML(dom); + + const domTree = dom.childNodes; let cleanHTML = ''; + let requireFormat = false; for (let i = 0, len = domTree.length, t; i < len; i++) { t = domTree[i]; - if (t.nodeType === 8 && this._allowHTMLComments) { - cleanHTML += '<__comment__>' + t.textContent.trim() + ''; - } else if (!tagsAllowed.test(t.nodeName)) { - cleanHTML += t.nodeType === 1 ? t.outerHTML : t.nodeType === 3 ? t.textContent : ''; + if (t.nodeType === 1 && !util.isTextStyleElement(t) && !util.isBreak(t) && !util._notAllowedTags(t)) { + requireFormat = true; + break; } } + for (let i = 0, len = domTree.length; i < len; i++) { + cleanHTML += this._makeLine(domTree[i], requireFormat); + } + cleanHTML = cleanHTML .replace(/\n/g, '') .replace(/<(script|style).*>(\n|.)*<\/(script|style)>/g, '') @@ -3862,39 +3953,18 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic * @returns {String} */ convertContentsForEditor: function (contents) { + const dom = this._d.createRange().createContextualFragment(contents); + util._consistencyCheckOfHTML(dom); + let returnHTML = ''; - const domTree = this._d.createRange().createContextualFragment(contents).childNodes; - - for (let i = 0, len = domTree.length, baseHtml, t; i < len; i++) { - t = domTree[i]; - - if (t.nodeType === 8) { - if (this._allowHTMLComments) { - baseHtml = '<__comment__>' + t.textContent.trim() + ''; - } else { - continue; - } - } else { - baseHtml = t.outerHTML || t.textContent; - } - - if (t.nodeType === 3) { - const textArray = baseHtml.split(/\n/g); - let text = ''; - for (let t = 0, tLen = textArray.length; t < tLen; t++) { - text = textArray[t].trim(); - if (text.length > 0) returnHTML += '

    ' + text + '

    '; - } - } else { - returnHTML += util.htmlRemoveWhiteSpace(baseHtml); - } + const domTree = dom.childNodes; + for (let i = 0, len = domTree.length; i < len; i++) { + returnHTML += this._makeLine(domTree[i], true); } - if (returnHTML.length === 0) { - contents = util._HTMLConvertor(contents); - return '

    ' + (contents.length > 0 ? contents : '
    ') + '

    '; - } + if (returnHTML.length === 0) return '


    '; + returnHTML = util.htmlRemoveWhiteSpace(returnHTML); returnHTML = returnHTML .replace(this.editorTagsWhitelistRegExp, '') .replace(/\n/g, '') @@ -4247,10 +4317,10 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic if (rangeEl) { format = util.createElement(formatName || 'P'); format.innerHTML = rangeEl.innerHTML; - if (format.childNodes.length === 0) format.innerHTML = '
    '; + if (util.onlyZeroWidthSpace(format)) format.innerHTML = util.zeroWidthSpace; rangeEl.innerHTML = format.outerHTML; - format = rangeEl.firstElementChild; + format = rangeEl.firstChild; focusNode = util.getEdgeChildNodes(format, null).sc; if (!focusNode) { @@ -4268,10 +4338,10 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic if (commonCon.childNodes.length === 1 && util.isBreak(commonCon.firstChild)) { br = commonCon.firstChild; } else { - br = util.createElement('BR'); + br = util.createTextNode(util.zeroWidthSpace); commonCon.appendChild(br); } - this.setRange(br, 0, br, 0); + this.setRange(br, 1, br, 1); } this.execCommand('formatBlock', false, (formatName || 'P')); @@ -4294,6 +4364,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } offset = focusNode.nodeType === 3 ? focusNode.textContent.length : 1; + this.effectNode = null; this.setRange(focusNode, offset, focusNode, offset); } }; @@ -4456,7 +4527,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic let target = e.target; if (core._bindControllersOff) e.stopPropagation(); - if (/^input|textarea$/i.test(target.nodeName)) { + if (/^(input|textarea|select|option)$/i.test(target.nodeName)) { core._antiBlur = false; } else { e.preventDefault(); @@ -4482,7 +4553,11 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic }, onClick_toolbar: function (e) { - e.preventDefault(); + if (/^(input|textarea|select|option)$/i.test(e.target.nodeName)) { + core._antiBlur = false; + } else { + e.preventDefault(); + } e.stopPropagation(); let target = e.target; @@ -4567,13 +4642,13 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } } - core._editorRange(); _w.setTimeout(core._editorRange.bind(core)); + core._editorRange(); const selectionNode = core.getSelectionNode(); const formatEl = util.getFormatElement(selectionNode); const rangeEl = util.getRangeFormatElement(selectionNode); - if (core.getRange().collapsed && (!formatEl || formatEl === rangeEl) && targetElement.getAttribute('contenteditable') !== 'false') { + if ((!formatEl || formatEl === rangeEl) && targetElement.getAttribute('contenteditable') !== 'false') { if (util.isList(rangeEl)) { const oLi = util.createElement('LI'); const prevLi = selectionNode.nextElementSibling; @@ -4584,7 +4659,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } e.preventDefault(); core.focus(); - } else { + } else { event._applyTagEffects(); } @@ -4755,18 +4830,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic onInput_wysiwyg: function (e) { core._editorRange(); - const range = core.getRange(); - const selectRange = !range.collapsed || range.startContainer !== range.endContainer; - const data = (e.data === null ? '' : e.data === undefined ? ' ' : e.data) || ''; - + const data = (e.data === null ? '' : e.data === undefined ? ' ' : e.data) || ''; if (!core._charCount(data)) { e.preventDefault(); e.stopPropagation(); } - // component check - if (selectRange || !data) core._checkComponents(); - // history stack core.history.push(true); @@ -4781,7 +4850,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic const alt = e.altKey; core.submenuOff(); - if (!event._directionKeyCode.test(keyCode)) _w.setTimeout(core._resourcesStateChange); + if (core._isBalloon) { event._hideToolbar(); } @@ -4831,7 +4900,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic while (attrs[0]) { formatEl.removeAttribute(attrs[0].name); } - core._nativeFocus(); + core.nativeFocus(); return false; } @@ -5356,10 +5425,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic core.setRange(selectionNode, so < 0 ? 0 : so, selectionNode, eo < 0 ? 0 : eo); } - const selectRange = !range.collapsed || range.startContainer !== range.endContainer; core._charCount(''); - // component check - if (selectRange) core._checkComponents(); + // history stack core.history.push(true); @@ -5920,12 +5987,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic try { const parseDocument = core._parser.parseFromString(html, 'text/html'); const children = parseDocument.body.childNodes; - let c, a; + let c, a, t; while ((c = children[0])) { - core.insertNode(c, a); + t = core.insertNode(c, a); a = c; } - const offset = a.nodeType === 3 ? a.textContent.length : 1; + const offset = a.nodeType === 3 ? (t.endOffset || a.textContent.length): 1; core.setRange(a, offset, a, offset); } catch (error) { core.execCommand('insertHTML', false, html); @@ -5942,6 +6009,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _ic } } + core.effectNode = null; core.focus(); // history stack diff --git a/src/lib/util.js b/src/lib/util.js index 9b1d20495..4dc624b57 100755 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -251,6 +251,16 @@ const util = { return 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) + * @param {Element|String} element The element 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)$/i.test(element.nodeName); + }, + /** * @description It is judged whether it is the format element (P, DIV, H[1-6], PRE, LI) * Format element also contain "free format Element" @@ -258,8 +268,7 @@ const util = { * @returns {Boolean} */ isFormatElement: function (element) { - if (element && element.nodeType === 1 && (/^(P|DIV|H[1-6]|PRE|LI|TD|TH)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__replace_.+(\\s|$)|(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element)) return true; - return false; + return (element && element.nodeType === 1 && (/^(P|DIV|H[1-6]|PRE|LI|TD|TH)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__replace_.+(\\s|$)|(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element)); }, /** @@ -269,8 +278,7 @@ const util = { * @returns {Boolean} */ isRangeFormatElement: function (element) { - if (element && element.nodeType === 1 && (/^(BLOCKQUOTE|OL|UL|FIGCAPTION|TABLE|THEAD|TBODY|TR|TH|TD)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__range_.+(\\s|$)'))) return true; - return false; + return (element && element.nodeType === 1 && (/^(BLOCKQUOTE|OL|UL|FIGCAPTION|TABLE|THEAD|TBODY|TR|TH|TD)$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__range_.+(\\s|$)'))); }, /** @@ -281,8 +289,7 @@ const util = { * @returns {Boolean} */ isFreeFormatElement: function (element) { - if (element && element.nodeType === 1 && (/^PRE$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element)) return true; - return false; + return (element && element.nodeType === 1 && (/^PRE$/i.test(element.nodeName) || this.hasClass(element, '(\\s|^)__se__format__free_.+(\\s|$)')) && !this.isComponent(element) && !this.isWysiwygDiv(element)); }, /** @@ -1042,7 +1049,7 @@ const util = { * @returns {Element} */ detachNestedList: function (baseNode, all) { - const rNode = this.__deleteNestedList(baseNode); + const rNode = this._deleteNestedList(baseNode); let rangeElement, cNodes; if (rNode) { @@ -1065,7 +1072,7 @@ const util = { } for (let i = 0, len = rChildren.length; i < len; i++) { - this.__deleteNestedList(rChildren[i]); + this._deleteNestedList(rChildren[i]); } if (rNode) { @@ -1080,7 +1087,7 @@ const util = { * @description Sub function of util.detachNestedList method. * @private */ - __deleteNestedList: function (baseNode) { + _deleteNestedList: function (baseNode) { const baseParent = baseNode.parentNode; let sibling = baseParent; let parent = sibling.parentNode; @@ -1418,7 +1425,7 @@ const util = { * @private */ _isIgnoreNodeChange: function (element) { - return element.nodeType !== 3 && (element.getAttribute('contenteditable') === 'false' || !/^(strong|span|font|b|var|i|em|u|ins|s|strike|del|sub|sup|mark|a|label)$/i.test(typeof element === 'string' ? element : element.nodeName)); + return element.nodeType !== 3 && (element.getAttribute('contenteditable') === 'false' || !this.isTextStyleElement(element)); }, /** @@ -1441,6 +1448,15 @@ const util = { return element.nodeType !== 3 && (this.isComponent(element) || /^(br|input|select|canvas|img|iframe|audio|video)$/i.test(typeof element === 'string' ? element : element.nodeName)); }, + /** + * @description Check not Allowed tags + * @param {Element} element Element to check + * @private + */ + _notAllowedTags: function (element) { + return /^(meta|script|link|style|[a-z]+\:[a-z]+)$/i.test(element.nodeName); + }, + /** * @description Create whitelist RegExp object. * Return RegExp format: new RegExp("<\\/?(" + (?!\\b list[i] \\b) + ")[^>^<])+>", "g") @@ -1459,6 +1475,60 @@ const util = { regStr += '[^>^<])+>'; return new RegExp(regStr, 'g'); + }, + + /** + * @description Fix tags that do not fit the editor format. + * @param {DOCUMENT_FRAGMENT_NODE} documentFragment Document fragment (nodeType === 11) + * @private + */ + _consistencyCheckOfHTML: function (documentFragment) { + // wrong position + const wrongTags = this.getListChildren(documentFragment, function (current) { + return (this.isFormatElement(current) || this.isComponent(current)) && current.parentNode.nodeType === 1 && !this.isRangeFormatElement(current.parentNode); + }.bind(this)); + + const checkTags = []; + for (let i = 0, len = wrongTags.length, t, tp; i < len; i++) { + t = wrongTags[i]; + tp = t.parentNode; + tp.parentNode.insertBefore(t, tp); + checkTags.push(tp); + } + + for (let i = 0, len = checkTags.length, t; i < len; i++) { + t = checkTags[i]; + if (this.onlyZeroWidthSpace(t.textContent.trim())) { + this.removeItem(t); + } + } + + // remove empty tags + const emptyTags = this.getListChildren(documentFragment, function (current) { + return (!this.isTable(current) && !this.isListCell(current)) && (this.isFormatElement(current) || this.isRangeFormatElement(current) || this.isTextStyleElement(current)) && current.childNodes.length === 0 && !util.getParentElement(current, '.katex'); + }.bind(this)); + + for (let i in emptyTags) { + this.removeItem(emptyTags[i]); + } + + // wrong list + const wrongList = this.getListChildren(documentFragment, function (current) { + return this.isList(current.parentNode) && !this.isList(current) && !this.isListCell(current); + }.bind(this)); + + for (let i = 0, len = wrongList.length, t, tp, children; i < len; i++) { + t = wrongList[i]; + + tp = this.createElement('LI'); + children = t.childNodes; + while (children[0]) { + tp.appendChild(children[0]); + } + + t.parentNode.insertBefore(tp, t); + this.removeItem(t); + } } }; diff --git a/src/plugins/dialog/image.js b/src/plugins/dialog/image.js index 9fe0972f6..dabd49eec 100644 --- a/src/plugins/dialog/image.js +++ b/src/plugins/dialog/image.js @@ -537,7 +537,7 @@ export default { }, checkImagesInfo: function () { - const images = this.context.element.wysiwyg.getElementsByTagName('IMG'); + const images = [].slice.call(this.context.element.wysiwyg.getElementsByTagName('IMG')); const imagePlugin = this.plugins.image; const imagesInfo = this._variable._imagesInfo; @@ -551,9 +551,18 @@ export default { 'size': img.getAttribute('data-file-size') || 0 }); } + return; + } else { + let infoUpdate = false; + for (let i = 0, len = imagesInfo.length; i < len; i++) { + if (images.indexOf(imagesInfo[i].element) === -1) { + infoUpdate = true; + break; + } + } + // pass + if (!infoUpdate) return; } - // pass - return; } // check images @@ -996,11 +1005,13 @@ export default { const dataIndex = imageEl.getAttribute('data-index') * 1; let focusEl = (imageContainer.previousElementSibling || imageContainer.nextElementSibling); + const emptyDiv = imageContainer.parentNode; this.util.removeItem(imageContainer); this.plugins.image.init.call(this); - this.controllersOff(); + if (emptyDiv !== this.context.element.wysiwyg) this.util.removeItemAllParents(emptyDiv); + // focus this.focusEdge(focusEl); diff --git a/src/plugins/dialog/video.js b/src/plugins/dialog/video.js index 5910b332f..fb12cc5d1 100644 --- a/src/plugins/dialog/video.js +++ b/src/plugins/dialog/video.js @@ -588,10 +588,14 @@ export default { this._variable._videosCnt--; const container = this.context.video._container; let focusEl = (container.previousElementSibling || container.nextElementSibling); + + const emptyDiv = container.parentNode; this.util.removeItem(container); this.plugins.video.init.call(this); this.controllersOff(); + if (emptyDiv !== this.context.element.wysiwyg) this.util.removeItemAllParents(emptyDiv); + // focus this.focusEdge(focusEl); diff --git a/src/plugins/submenu/table.js b/src/plugins/submenu/table.js index 88aafab8f..00157657b 100644 --- a/src/plugins/submenu/table.js +++ b/src/plugins/submenu/table.js @@ -1384,8 +1384,11 @@ export default { this.plugins.table.resizeTable.call(this); break; case 'remove': + const emptyDiv = contextTable._element.parentNode; this.util.removeItem(contextTable._element); this.controllersOff(); + + if (emptyDiv !== this.context.element.wysiwyg) this.util.removeItemAllParents(emptyDiv); } this.focus(); diff --git a/test/dev/suneditor_build_test.js b/test/dev/suneditor_build_test.js index 90098973e..60c1266bd 100644 --- a/test/dev/suneditor_build_test.js +++ b/test/dev/suneditor_build_test.js @@ -26,7 +26,7 @@ const align = require('../../src/plugins/submenu/align') suneditor.create("sample1", { - plugins: [custom_plugin_dialog, custom_container, plugins.blockquote, plugins.link, plugins.table, plugins.textStyle], + plugins: [custom_plugin_dialog, custom_container, Resolutions, plugins.template, plugins.blockquote, plugins.link, plugins.table, plugins.textStyle], // mode: "balloon", // iframe: true, width: '100%', @@ -40,7 +40,7 @@ suneditor.create("sample1", { buttonClass:'', title:'Custom - Link', dataDisplay:'dialog', - innerHTML:'' + innerHTML:'D' }, { name: 'custom_container', @@ -48,9 +48,17 @@ suneditor.create("sample1", { buttonClass:'', title:'Custom - Container', dataDisplay:'container', - innerHTML:'' + innerHTML:'C' + }, + { + name: 'Resolutions', + dataCommand: 'Resolutions', + buttonClass:'', + title:'Resolutions - Container', + dataDisplay:'submenu', + innerHTML:'S' }, - 'bold', 'italic' + 'bold', 'italic', 'template' ] ], maxCharCount: 2000 @@ -77,7 +85,7 @@ let s1 = suneditor.create('editor', { 'preview', 'print', 'save', 'template'] ], icons: { - undo: '', + undo: 'U', bold: 'B' }, width: '100%', @@ -85,13 +93,13 @@ let s1 = suneditor.create('editor', { defaultStyle: 'font-size: 10px;', // fullPage: true, // pasteTagsWhitelist: 'p|h[1-6]', - attributesWhitelist: { - table: "style", - tbody: "style", - thead: "style", - tr: "style", - td: "style" - }, + // attributesWhitelist: { + // table: "style", + // tbody: "style", + // thead: "style", + // tr: "style", + // td: "style" + // }, addTagsWhitelist: '//', // addTagsWhitelist: '//', formats: [ @@ -186,23 +194,24 @@ window.cm = CodeMirror // }); window.sun_destroy1 = function () { + s1.destroy() // s1.setDefaultStyle('font-family: cursive; font-size: 10px;'); - s1.setContents(''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ' '+ - ''+ - ''+ - ''+ - ''+ - - ''+ - '') + // s1.setContents(''+ + // ''+ + // ''+ + // ''+ + // ''+ + // ''+ + // ' '+ + // ''+ + // ''+ + // ''+ + // ''+ + + // ''+ + // '') } window.sun_create1 = function () { @@ -254,13 +263,13 @@ let ss = window.ss = suneditor.create(document.getElementById('editor1'), { fullPage: true, addTagsWhitelist: 'mark|canvas|label|select|option|input|nav|button', imageUploadUrl: 'http://localhost:3000/files/upload', - attributesWhitelist: { - table: "style", - tbody: "style", - thead: "style", - tr: "style", - td: "style" - }, + // attributesWhitelist: { + // table: "style", + // tbody: "style", + // thead: "style", + // tr: "style", + // td: "style" + // }, templates: [ { name: 'Template-1', @@ -437,7 +446,7 @@ const editor = suneditor.init({ let s2 = window.s2 = editor.create(document.getElementById('editor2'), { lang: lang.ru, - mode: 'balloon', + mode: 'balloon-always', // toolbarWidth: 500, plugins: plugins, // maxHeight: '400px',