From ec186895141d66d1d55a4cca0afc8ce837810a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geelen=20=28sge=29?= Date: Wed, 9 Jun 2021 15:01:02 +0200 Subject: [PATCH 01/18] [FIX] fix getRangePosition util in Iframe context (cherry picked from commit 184744e5d6bbeb14123fc1a384b4c411740bc504) --- src/commandbar.js | 2 +- src/editor.js | 1 + src/tablepicker.js | 2 +- src/utils/utils.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commandbar.js b/src/commandbar.js index a704e2f..755e804 100644 --- a/src/commandbar.js +++ b/src/commandbar.js @@ -266,7 +266,7 @@ export class CommandBar { } _resetPosition() { - const position = getRangePosition(this.el); + const position = getRangePosition(this.el, this.options.document); if (!position) { this.hide(); return; diff --git a/src/editor.js b/src/editor.js index 10e765e..fee3d2e 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1241,6 +1241,7 @@ export class OdooEditor extends EventTarget { } this.commandBar = new CommandBar({ editable: this.editable, + document: this.document, _t: this.options._t, onShow: () => { this.commandbarTablePicker.hide(); diff --git a/src/tablepicker.js b/src/tablepicker.js index ae7fb41..2e635be 100644 --- a/src/tablepicker.js +++ b/src/tablepicker.js @@ -133,7 +133,7 @@ export class TablePicker extends EventTarget { } }; - const offset = getRangePosition(this.el); + const offset = getRangePosition(this.el, this.options.document); this.el.style.left = `${offset.left}px`; this.el.style.top = `${offset.top}px`; diff --git a/src/utils/utils.js b/src/utils/utils.js index 0db8579..56964f8 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1781,7 +1781,7 @@ export function rgbToHex(rgb = '') { ); } -export function getRangePosition(el, options = {}) { +export function getRangePosition(el, document, options = {}) { const selection = document.getSelection(); if (!selection.isCollapsed || !selection.rangeCount) return; const range = selection.getRangeAt(0); From 304fa597be6bffcb012133e88924a898983019e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geelen=20=28sge=29?= Date: Tue, 15 Jun 2021 09:44:55 +0200 Subject: [PATCH 02/18] [FIX] commandbar small fixes Related to Odoo task : https://www.odoo.com/web#action=333&active_id=1695&cids=1&id=2522564&menu_id=4720&model=project.task&view_type=form --- src/editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor.js b/src/editor.js index fee3d2e..611fa21 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1209,15 +1209,15 @@ export class OdooEditor extends EventTarget { groupName: 'Basic blocks', title: 'Checklist', description: 'Track tasks with a checklist.', - fontawesome: 'fa-tasks', + fontawesome: 'fa-check-square-o', callback: () => { this.execCommand('toggleList', 'CL'); }, }, { groupName: 'Basic blocks', - title: 'Horizontal rule', - description: 'Insert an horizantal rule.', + title: 'Separator', + description: 'Insert an horizontal rule separator.', fontawesome: 'fa-minus', callback: () => { this.execCommand('insertHorizontalRule'); From 5fea955f9c7ad643075250037b6b64e3c69b3419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geelen=20=28sge=29?= Date: Wed, 16 Jun 2021 11:24:31 +0200 Subject: [PATCH 03/18] [FIX] prevent commandbar to close when manually scrolling with the mouse --- src/commandbar.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commandbar.js b/src/commandbar.js index 755e804..ba0418d 100644 --- a/src/commandbar.js +++ b/src/commandbar.js @@ -26,6 +26,9 @@ export class CommandBar { this._mainWrapperElement = document.createElement('div'); this._mainWrapperElement.className = 'oe-commandbar-mainWrapper'; this.el.append(this._mainWrapperElement); + this.el.addEventListener('mousedown', event => { + event.stopPropagation(); + }); } destroy() { @@ -117,6 +120,7 @@ export class CommandBar { event => { this._currentValidate(); event.preventDefault(); + event.stopPropagation(); }, true, ); From 6aac22a13edeb4673551dbd0e1e4c76d68e2886c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geelen=20=28sge=29?= Date: Fri, 18 Jun 2021 16:35:22 +0200 Subject: [PATCH 04/18] [FIX] commandbar: also remove scrollbar in Iframes when commandbar is open --- src/editor.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editor.js b/src/editor.js index 611fa21..4321ad7 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1252,6 +1252,11 @@ export class OdooEditor extends EventTarget { for (const element of document.querySelectorAll(this.options.noScrollSelector)) { element.classList.add('oe-noscroll'); } + for (const element of this.document.querySelectorAll( + this.options.noScrollSelector, + )) { + element.classList.add('oe-noscroll'); + } this.observerActive(); }, preValidate: () => { @@ -1265,6 +1270,9 @@ export class OdooEditor extends EventTarget { for (const element of document.querySelectorAll('.oe-noscroll')) { element.classList.remove('oe-noscroll'); } + for (const element of this.document.querySelectorAll('.oe-noscroll')) { + element.classList.remove('oe-noscroll'); + } this.observerActive(); }, commands: [...mainCommands, ...(this.options.commands || [])], From dc1e6e1f22300da430d9fb40fb611926b5400cdd Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 7 May 2021 09:28:56 +0200 Subject: [PATCH 05/18] [ADD] insert four spaces on press tab (always prevent default behavior) --- src/editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor.js b/src/editor.js index 4321ad7..6ee9754 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1556,6 +1556,7 @@ export class OdooEditor extends EventTarget { this.execCommand('insertText', '\u00A0 \u00A0\u00A0'); } ev.preventDefault(); + ev.stopPropagation(); } else if (IS_KEYBOARD_EVENT_UNDO(ev)) { // Ctrl-Z ev.preventDefault(); From 396f59091576d989bdd6db0821d6df3f8e965fbf Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 7 May 2021 13:28:42 +0200 Subject: [PATCH 06/18] [IMP] keep cursor in an unremovable inline element after it was emptied We achieve this by inserting and selecting a zero-width space when deleting the element. This is so far limited to unremovable elements but we could potentially expand this logic to all inlines if we choose to have that behavior. In that case though we'd have to handle the systematic removal of these potentially parasitic zero-width spaces, and adapt a lot of tests. --- src/editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor.js b/src/editor.js index 6ee9754..d73d812 100644 --- a/src/editor.js +++ b/src/editor.js @@ -12,6 +12,7 @@ import {} from './commands/align.js'; import { sanitize } from './utils/sanitize.js'; import { nodeToObject, objectToNode } from './utils/serialize.js'; import { + childNodeIndex, closestBlock, commonParentGet, containsUnremovable, From 52f7fd5a3226c48874ed39b029b56d46cff7835b Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Tue, 11 May 2021 12:52:55 +0200 Subject: [PATCH 07/18] [FIX] always identify and unbold bold text The current method to identify bold text failed when the "bolder" font-weight was smaller than 500, which happens if the normal font-weight is set to less than 400. This is the case with Bootstrap's "lead" class for instance. Checking if it's bigger than 500 is useful for marking headings as being bold. However we need to also check if the element's font-weight is bigger than its parent block - in which case it should be considered bold. Using `font-weight: normal` to unbold again fails if the inherited font-weight is smaller than 400 (eg: text in a ".lead" element inherits a font-weight of 300 but `font-weight: normal` is the same as `font-weight: 400` so setting `font-weight: normal` would make the text still end up bolder than the ".lead" element). In these cases we need to use the font-weight of said block. It's an imperfect heuristic but it gets the job done in the vast majority of cases. --- src/editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor.js b/src/editor.js index d73d812..6ee9754 100644 --- a/src/editor.js +++ b/src/editor.js @@ -12,7 +12,6 @@ import {} from './commands/align.js'; import { sanitize } from './utils/sanitize.js'; import { nodeToObject, objectToNode } from './utils/serialize.js'; import { - childNodeIndex, closestBlock, commonParentGet, containsUnremovable, From 4901eb8b93a78fb693f8cab1e0b4a3442a778ebd Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Thu, 27 May 2021 15:23:20 +0200 Subject: [PATCH 08/18] [FIX] properly paste escaped text and filtered html Thus far, the editor only handled text-only pasting. This commit makes it so that it now handles html pasting, and ensures that text pasting is properly escaped (to prevent evaluating html strings on paste). When pasting html, the editor filters the nodes, their attributes and their classes through a combination of a whitelist and a blacklist. Nodes that match blacklisted or non-whitelisted selectors are removed and their contents are unwrapped recursively. Attributes and classes that are blacklisted or non-whitelisted are removed. What is kept are basic styling elements (eg: h1, strong, ...), images (with their src attribute), links (with the href attribute), and a selection of classes the edition of which is supported by the editor. --- src/editor.js | 201 ++++++++++++++++++++++++++++++++++++++++----- src/utils/utils.js | 16 ++++ 2 files changed, 196 insertions(+), 21 deletions(-) diff --git a/src/editor.js b/src/editor.js index 6ee9754..e85c16b 100644 --- a/src/editor.js +++ b/src/editor.js @@ -41,6 +41,7 @@ import { getUrlsInfosInString, URL_REGEX, isBold, + unwrapContents, } from './utils/utils.js'; import { editorCommands } from './commands.js'; import { CommandBar } from './commandbar.js'; @@ -58,6 +59,47 @@ const IS_KEYBOARD_EVENT_UNDO = ev => ev.key === 'z' && (ev.ctrlKey || ev.metaKey const IS_KEYBOARD_EVENT_REDO = ev => ev.key === 'y' && (ev.ctrlKey || ev.metaKey); const IS_KEYBOARD_EVENT_BOLD = ev => ev.key === 'b' && (ev.ctrlKey || ev.metaKey); +const CLIPBOARD_BLACKLISTS = { + unwrap: ['.Apple-interchange-newline', 'DIV'], // These elements' children will be unwrapped. + remove: ['META', 'STYLE', 'SCRIPT'], // These elements will be removed along with their children. +}; +const CLIPBOARD_WHITELISTS = { + nodes: [ + // Style + 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', + // List + 'UL', 'OL', 'LI', + // Inline style + 'I', 'B', 'U', 'EM', 'STRONG', + // Table + 'TABLE', 'TH', 'TBODY', 'TR', 'TD', + // Miscellaneous + 'IMG', 'BR', 'A', '.fa', + ], + classes: [ + // Media + /^float-/, + 'd-block', + 'mx-auto', + 'img-fluid', + 'img-thumbnail', + 'rounded', + 'rounded-circle', + /^padding-/, + /^shadow/, + // Odoo colors + /^text-o-/, + /^bg-o-/, + // Odoo checklists + 'o_checked', + 'o_checklist', + // Miscellaneous + /^btn/, + /^fa/, + ], + attributes: ['class', 'href', 'src'], +} + function defaultOptions(defaultObject, object) { const newObject = Object.assign({}, defaultObject, object); for (const [key, value] of Object.entries(object)) { @@ -1449,6 +1491,119 @@ export class OdooEditor extends EventTarget { } } + // PASTING / DROPPING + + /** + * Prepare clipboard data (text/html) for safe pasting into the editor. + * + * @private + * @param {string} clipboardData + * @returns {string} + */ + _prepareClipboardData(clipboardData) { + const container = document.createElement('fake-container'); + container.innerHTML = clipboardData; + for (const child of [...container.childNodes]) { + this._cleanForPaste(child) + } + return container.innerHTML; + } + /** + * Prepare clipboard data (text/plain) for safe pasting into the editor. + * + * @private + * @param {string} clipboardData + * @returns {string} + */ + _prepareTextClipboardData(clipboardData) { + const isXML = !!clipboardData.match(/<[a-z]+[a-z0-9-]*( [^>]*)*>[\s\S\n\r]*<\/[a-z]+[a-z0-9-]*>/i); + const isJS = !isXML && !!clipboardData.match(/\(\);|this\.|self\.|function\s?\(|super\.|[a-z0-9]\.[a-z].*;/i); + + const container = document.createElement('fake-container'); + const pre = document.createElement('pre'); + pre.innerHTML = clipboardData.trim() + .replace(//g, '>') + // Get that text as an array of text nodes separated by
where + // needed. + .replace(/(\n+)/g, '
'); + + if (isJS || isXML) { + container.appendChild(pre); + } else { + for (const node of pre.childNodes) { + container.appendChild(node); + }; + } + return container.innerHTML; + } + /** + * Clean a node for safely pasting. Cleaning an element involves unwrapping + * its contents if it's an illegal (blacklisted or not whitelisted) element, + * or removing its illegal attributes and classes. + * + * @param {Node} node + */ + _cleanForPaste(node) { + if (!this._isWhitelisted(node) || this._isBlacklisted(node)) { + if (node.matches(CLIPBOARD_BLACKLISTS.remove.join(','))) { + node.remove(); + } else { + // Unwrap the illegal node's contents. + for (const unwrappedNode of unwrapContents(node)) { + this._cleanForPaste(unwrappedNode); + } + } + } else if (node.nodeType !== Node.TEXT_NODE) { + // Remove all illegal attributes and classes from the node, then + // clean its children. + for (const attribute of [...node.attributes]) { + if (!this._isWhitelisted(attribute)) { + node.removeAttribute(attribute.name); + } + } + for (const klass of [...node.classList]) { + if (!this._isWhitelisted(klass)) { + node.classList.remove(klass); + } + } + for (const child of [...node.childNodes]) { + this._cleanForPaste(child); + } + } + } + /** + * Return true if the given attribute, class or node is whitelisted for + * pasting, false otherwise. + * + * @private + * @param {Attr | string | Node} item + * @returns {boolean} + */ + _isWhitelisted(item) { + if (item instanceof Attr) { + return CLIPBOARD_WHITELISTS.attributes.includes(item.name); + } else if (typeof item === 'string') { + return CLIPBOARD_WHITELISTS.classes.some(okClass => ( + okClass instanceof RegExp ? okClass.test(item) : okClass === item + )); + } else { + return item.nodeType === Node.TEXT_NODE || + item.matches(CLIPBOARD_WHITELISTS.nodes.join(',')); + } + } + /** + * Return true if the given node is blacklisted for pasting, false + * otherwise. + * + * @private + * @param {Node} node + * @returns {boolean} + */ + _isBlacklisted(node) { + return node.nodeType !== Node.TEXT_NODE && + node.matches([].concat(...Object.values(CLIPBOARD_BLACKLISTS)).join(',')); + } + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -1812,33 +1967,37 @@ export class OdooEditor extends EventTarget { } /** - * Prevent the pasting of HTML and paste text only instead. + * Handle safe pasting of html or plain text into the editor. */ _onPaste(ev) { ev.preventDefault(); - const pastedText = (ev.originalEvent || ev).clipboardData.getData('text/plain'); - const splitAroundUrl = pastedText.split(URL_REGEX); - const linkAttrs = - Object.entries(this.options.defaultLinkAttributes) - .map(entry => entry.join('="')) - .join('" ') + '" '; - - for (let i = 0; i < splitAroundUrl.length; i++) { - // Even indexes will always be plain text, and odd indexes will always be URL. - if (i % 2) { - const url = /^https?:\/\//gi.test(splitAroundUrl[i]) - ? splitAroundUrl[i] - : 'https://' + splitAroundUrl[i]; - this.execCommand( - 'insertHTML', - `${splitAroundUrl[i]}`, - ); - } else if (splitAroundUrl[i] !== '') { - this.execCommand('insertText', splitAroundUrl[i]); + const clipboardData = ev.clipboardData.getData('text/html'); + if (clipboardData) { + this.execCommand('insertHTML', this._prepareClipboardData(clipboardData)); + } else { + const text = ev.clipboardData.getData('text/plain'); + const splitAroundUrl = text.split(URL_REGEX); + const linkAttrs = + Object.entries(this.options.defaultLinkAttributes) + .map(entry => entry.join('="')) + .join('" ') + '" '; + + for (let i = 0; i < splitAroundUrl.length; i++) { + // Even indexes will always be plain text, and odd indexes will always be URL. + if (i % 2) { + const url = /^https?:\/\//gi.test(splitAroundUrl[i]) + ? splitAroundUrl[i] + : 'https://' + splitAroundUrl[i]; + this.execCommand( + 'insertHTML', + `${splitAroundUrl[i]}`, + ); + } else if (splitAroundUrl[i] !== '') { + this.execCommand('insertHTML', this._prepareTextClipboardData(splitAroundUrl[i])); + } } } } - /** * Prevent the dropping of HTML and paste text only instead. */ diff --git a/src/utils/utils.js b/src/utils/utils.js index 56964f8..600188e 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1254,6 +1254,22 @@ export function insertText(sel, content) { setCursor(...boundariesOut(txt), false); } + +/** + * Remove node from the DOM while preserving their contents if any. + * + * @param {Node} node + * @returns {Node[]} + */ +export function unwrapContents (node) { + const contents = [...node.childNodes]; + for (const child of contents) { + node.parentNode.insertBefore(child, node); + }; + node.parentNode.removeChild(node); + return contents; +} + /** * Add a BR in the given node if its closest ancestor block has nothing to make * it visible, and/or add a zero-width space in the given node if it's an empty From c66c4a55e1cec6aef6b8b5e09f97387dba077307 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 28 May 2021 10:13:45 +0200 Subject: [PATCH 09/18] [FIX] ensure dropping of html behaves the same way as pasting --- src/editor.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/editor.js b/src/editor.js index e85c16b..8caa1a6 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1999,7 +1999,7 @@ export class OdooEditor extends EventTarget { } } /** - * Prevent the dropping of HTML and paste text only instead. + * Handle safe dropping of html into the editor. */ _onDrop(ev) { ev.preventDefault(); @@ -2013,21 +2013,21 @@ export class OdooEditor extends EventTarget { ancestor = ancestor.parentNode; } const transferItem = [...(ev.originalEvent || ev).dataTransfer.items].find( - item => item.type === 'text/plain', + item => item.type === 'text/html', ); if (transferItem) { transferItem.getAsString(pastedText => { if (isInEditor && !sel.isCollapsed) { this.deleteRange(sel); } - if (document.caretPositionFromPoint) { + if (this.document.caretPositionFromPoint) { const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY); setCursor(range.offsetNode, range.offset); - } else if (document.caretRangeFromPoint) { + } else if (this.document.caretRangeFromPoint) { const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY); setCursor(range.startContainer, range.startOffset); } - insertText(this.document.getSelection(), pastedText); + this.execCommand('insertHTML', this._prepareClipboardData(pastedText)); }); } this.historyStep(); From 3eb65222067b7b2861fd17685d796b062eef05b0 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 28 May 2021 12:33:04 +0200 Subject: [PATCH 10/18] [FIX] prevent


on insert html When pasting a paragraph within a paragraph, the original paragraph should split rather than allowing for nested paragraphs. --- src/commands.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/commands.js b/src/commands.js index ab7f365..f8bf557 100644 --- a/src/commands.js +++ b/src/commands.js @@ -67,6 +67,22 @@ function insert(editor, data, isText = true) { let nodeToInsert; const insertedNodes = [...fakeEl.childNodes]; while ((nodeToInsert = fakeEl.childNodes[0])) { + if (isBlock(nodeToInsert) && !isBlock(startNode)) { + // Split blocks at the edges if inserting new blocks (preventing + //

text

scenarios). + while (startNode.parentElement !== editor.editable && !isBlock(startNode)) { + let offset = childNodeIndex(startNode); + if (!insertBefore) { + offset += 1; + } + if (offset) { + const [left, right] = splitElement(startNode.parentElement, offset); + startNode = insertBefore ? right : left; + } else { + startNode = startNode.parentElement; + } + } + } if (insertBefore) { startNode.before(nodeToInsert); insertBefore = false; From 0a9d42c86d60b5a3662b4eebc3f334dd6cd965f2 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Mon, 14 Jun 2021 10:24:39 +0200 Subject: [PATCH 11/18] [FIX] prevent editing the toolbar with the toolbar Clicking in the (non-floating) toolbar, then on one of its buttons would edit the toolbar itself (eg., clicking on a list button would insert a list in the toolbar). This adds a check on toolbar events so they can only affect what is in the editable area. --- src/editor.js | 9 ++++++--- src/utils/utils.js | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/editor.js b/src/editor.js index 8caa1a6..4086124 100644 --- a/src/editor.js +++ b/src/editor.js @@ -2036,10 +2036,13 @@ export class OdooEditor extends EventTarget { _bindToolbar() { for (const buttonEl of this.toolbar.querySelectorAll('[data-call]')) { buttonEl.addEventListener('mousedown', ev => { - this.execCommand(buttonEl.dataset.call, buttonEl.dataset.arg1); + const sel = this.document.getSelection() + if (sel.anchorNode && ancestors(sel.anchorNode).includes(this.editable)) { + this.execCommand(buttonEl.dataset.call, buttonEl.dataset.arg1); - ev.preventDefault(); - this._updateToolbar(); + ev.preventDefault(); + this._updateToolbar(); + } }); } } diff --git a/src/utils/utils.js b/src/utils/utils.js index 600188e..31f8a70 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -256,6 +256,7 @@ export function closestElement(node, selector) { * Returns a list of all the ancestors nodes of the provided node. * * @param {Node} node + * @param {Node} [editable] include to prevent bubbling up further than the editable. * @returns {HTMLElement[]} */ export function ancestors(node, editable) { From 225993ec7ccd03a095b7a7b488a05dd1ed4fcbe9 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Thu, 17 Jun 2021 16:31:29 +0200 Subject: [PATCH 12/18] [FIX] properly create multi-paragraph list with whitespace This addresses a bug that occurs when selecting over two paragraphs that are separated by some whitespace. It would create multiple indented lists. Task: 2567795 --- src/commands.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commands.js b/src/commands.js index f8bf557..e91c39e 100644 --- a/src/commands.js +++ b/src/commands.js @@ -411,10 +411,14 @@ export const editorCommands = { const blocks = new Set(); for (const node of getTraversedNodes(editor.editable)) { - const block = closestBlock(node); - if (!['OL', 'UL'].includes(block.tagName)) { - const ublock = block.closest('ol, ul'); - ublock && getListMode(ublock) == mode ? li.add(block) : blocks.add(block); + if (node.nodeType === Node.TEXT_NODE && !isVisibleStr(node)) { + node.remove(); + } else { + const block = closestBlock(node); + if (!['OL', 'UL'].includes(block.tagName)) { + const ublock = block.closest('ol, ul'); + ublock && getListMode(ublock) == mode ? li.add(block) : blocks.add(block); + } } } From e3a55f4cd2eb3c4fbb254bbd0381d0aa412380c2 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Tue, 22 Jun 2021 16:22:12 +0200 Subject: [PATCH 13/18] [FIX] prevent shrunk blocks on split during insert html When inserting HTML, we sometimes need to split blocks to prevent weird nesting of blocks. However, that process has the potential of generating shrunk blocks (blocks without a height), such as

. This prevents it from happening. --- src/commands.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands.js b/src/commands.js index e91c39e..89a57cc 100644 --- a/src/commands.js +++ b/src/commands.js @@ -17,6 +17,7 @@ import { isBlock, isBold, isContentTextNode, + isShrunkBlock, isVisible, isVisibleStr, leftDeepFirstPath, @@ -89,6 +90,9 @@ function insert(editor, data, isText = true) { } else { startNode.after(nodeToInsert); } + if (isShrunkBlock(startNode)) { + startNode.remove(); + } startNode = nodeToInsert; } From e61b0b50f7589dc05fdacd01d840ca6aba2a97cb Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 25 Jun 2021 16:11:36 +0200 Subject: [PATCH 14/18] [FIX] do not merge similar option elements when sanitizing The sanitizer merges similar elements together but