diff --git a/components/lib/editor/Editor.js b/components/lib/editor/Editor.js index 1d3612d5bf..072ed69956 100644 --- a/components/lib/editor/Editor.js +++ b/components/lib/editor/Editor.js @@ -62,12 +62,23 @@ export const Editor = React.memo(React.forwardRef((props, ref) => { quill.current.setContents(quill.current.clipboard.convert(props.value)); } - quill.current.on('text-change', (delta, source) => { + quill.current.on('text-change', (delta, oldContents, source) => { let firstChild = contentRef.current.children[0]; let html = firstChild ? firstChild.innerHTML : null; let text = quill.current.getText(); if (html === '


') { - html = null; + html = null; + } + + // GitHub #2271 prevent infinite loop on clipboard paste of HTML + if (source === "api") { + const htmlValue = contentRef.current.children[0]; + const editorValue = document.createElement("div"); + editorValue.innerHTML = props.value || ""; + // this is necessary because Quill rearranged style elements + if (DomHandler.isEqualElement(htmlValue, editorValue)) { + return; + } } if (props.onTextChange) { diff --git a/components/lib/utils/DomHandler.js b/components/lib/utils/DomHandler.js index 42d568a7bb..e0bff47c7f 100644 --- a/components/lib/utils/DomHandler.js +++ b/components/lib/utils/DomHandler.js @@ -953,4 +953,86 @@ export default class DomHandler { return ((element && element.nodeType === 9) || this.isExist(element)) ? element : null; } } + + /** + * Get the attribute names for an element and sorts them alpha for comparison + */ + static getAttributeNames(node) { + let index, rv, attrs; + + rv = []; + attrs = node.attributes; + for (index = 0; index < attrs.length; ++index) { + rv.push(attrs[index].nodeName); + } + rv.sort(); + return rv; + } + + /** + * Compare two elements for equality. Even will compare if the style element + * is out of order for example: + * + * elem1 = style="color: red; font-size: 28px" + * elem2 = style="font-size: 28px; color: red" + */ + static isEqualElement(elm1, elm2) { + let attrs1, attrs2, name, node1, node2; + + // Compare attributes without order sensitivity + attrs1 = DomHandler.getAttributeNames(elm1); + attrs2 = DomHandler.getAttributeNames(elm2); + if (attrs1.join(",") !== attrs2.join(",")) { + // console.log("Found nodes with different sets of attributes; not equiv"); + return false; + } + + // ...and values + // unless you want to compare DOM0 event handlers + // (onclick="...") + for (let index = 0; index < attrs1.length; ++index) { + name = attrs1[index]; + if (name === 'style') { + const astyle = elm1.style; + const bstyle = elm2.style; + const rexDigitsOnly = /^\d+$/; + for (const key of Object.keys(astyle)) { + if (!rexDigitsOnly.test(key) && astyle[key] !== bstyle[key]) { + // Not equivalent, stop + //console.log("Found nodes with mis-matched values for attribute '" + name + "'; not equiv"); + return false; + } + } + } else { + if (elm1.getAttribute(name) !== elm2.getAttribute(name)) { + // console.log("Found nodes with mis-matched values for attribute '" + name + "'; not equiv"); + return false; + } + } + } + + // Walk the children + for (node1 = elm1.firstChild, node2 = elm2.firstChild; node1 && node2; node1 = node1.nextSibling, node2 = node2.nextSibling) { + if (node1.nodeType !== node2.nodeType) { + // display("Found nodes of different types; not equiv"); + return false; + } + if (node1.nodeType === 1) { // Element + if (!DomHandler.isEqualElement(node1, node2)) { + return false; + } + } else if (node1.nodeValue !== node2.nodeValue) { + // console.log("Found nodes with mis-matched nodeValues; not equiv"); + return false; + } + } + if (node1 || node2) { + // One of the elements had more nodes than the other + // console.log("Found more children of one element than the other; not equivalent"); + return false; + } + + // Seem the same + return true; + } } \ No newline at end of file