First paragraph
After first paragraphSecond paragraph
diff --git a/src/scripts/modules/BaseFormatter.js b/src/scripts/modules/BaseFormatter.js index 2f7e1f8..d1071ab 100644 --- a/src/scripts/modules/BaseFormatter.js +++ b/src/scripts/modules/BaseFormatter.js @@ -46,9 +46,10 @@ const BaseFormatter = Module({ methods: { init () { const { mediator } = this; + validTags = mediator.get('config:toolbar:validTags'); blockTags = mediator.get('config:toolbar:blockTags'); - listTags = mediator.get('config:toolbar:listTags'); + listTags = mediator.get('config:toolbar:listTags'); }, /** @@ -106,6 +107,7 @@ const BaseFormatter = Module({ formatDefault () { const { mediator } = this; const rootElem = mediator.get('selection:rootelement'); + mediator.exec('commands:format:default'); this.removeStyledSpans(rootElem); }, @@ -122,22 +124,19 @@ const BaseFormatter = Module({ this.ensureRootElems(rootElem); this.removeStyleAttributes(rootElem); this.removeEmptyNodes(rootElem, { recursive: true }); - - // ----- - - // this.removeBrNodes(rootElem); - // // this.removeEmptyNodes(rootElem); - // this.removeFontTags(rootElem); - // this.removeStyledSpans(rootElem); - // this.clearEntities(rootElem); - // this.removeZeroWidthSpaces(rootElem); - // this.defaultOrphanedTextNodes(rootElem); - // this.removeEmptyNodes(rootElem, { recursive: true }); }, /** * PRIVATE METHODS: */ + cloneNodes (rootElement) { + let clonedNodes = []; + rootElement.childNodes.forEach((node) => { + clonedNodes.push(node.cloneNode(true)); + }); + return clonedNodes; + }, + injectHooks (rootElement) { while (!/\w+/.test(rootElement.firstChild.textContent)) { DOM.removeNode(rootElement.firstChild); @@ -306,7 +305,6 @@ const BaseFormatter = Module({ defaultOrphanedTextNodes (rootElem) { const { childNodes } = rootElem; - for (let i = 0; i < childNodes.length; i++) { let childNode = childNodes[i]; if (childNode.nodeType === Node.TEXT_NODE && /\w+/.test(childNode.textContent)) { diff --git a/src/scripts/modules/BlockFormatter.js b/src/scripts/modules/BlockFormatter.js index 623d718..04b563f 100644 --- a/src/scripts/modules/BlockFormatter.js +++ b/src/scripts/modules/BlockFormatter.js @@ -84,7 +84,7 @@ const BlockFormatter = Module({ }, cleanupBlockquote (rootElem) { - const blockquoteParagraphs = rootElem.querySelectorAll('BLOCKQUOTE P'); + const blockquoteParagraphs = rootElem.querySelectorAll('blockquote p'); blockquoteParagraphs.forEach((paragraph) => { DOM.unwrap(paragraph); }); diff --git a/src/scripts/modules/ContentEditable.js b/src/scripts/modules/ContentEditable.js index 485fc9c..cfa8b09 100644 --- a/src/scripts/modules/ContentEditable.js +++ b/src/scripts/modules/ContentEditable.js @@ -152,7 +152,6 @@ const ContentEditable = Module({ } else { let currentSelection = mediator.get('selection:current'); let currentRange = mediator.get('selection:range'); - currentRange.deleteContents(); let tmpContainer = document.createElement('container'); diff --git a/src/scripts/modules/ListFormatter.js b/src/scripts/modules/ListFormatter.js index 920cebc..aa295e9 100644 --- a/src/scripts/modules/ListFormatter.js +++ b/src/scripts/modules/ListFormatter.js @@ -46,12 +46,11 @@ const ListFormatter = Module({ process (opts) { const { mediator } = this; const canvasDoc = mediator.get('canvas:document'); - let toggle = false; mediator.exec('canvas:cache:selection'); + switch (opts.style) { case 'ordered': - toggle = mediator.get('selection:in:or:contains', ['OL']); if (mediator.get('selection:in:or:contains', ['UL'])) { mediator.exec('commands:exec', { command: 'insertUnorderedList', @@ -63,8 +62,8 @@ const ListFormatter = Module({ contextDocument: canvasDoc }); break; + case 'unordered': - toggle = mediator.get('selection:in:or:contains', ['UL']); if (mediator.get('selection:in:or:contains', ['OL'])) { mediator.exec('commands:exec', { command: 'insertOrderedList', @@ -76,12 +75,14 @@ const ListFormatter = Module({ contextDocument: canvasDoc }); break; + case 'outdent': mediator.exec('commands:exec', { command: 'outdent', contextDocument: canvasDoc }); break; + case 'indent': mediator.exec('commands:exec', { command: 'indent', @@ -90,14 +91,7 @@ const ListFormatter = Module({ break; } - if (toggle) { - // mediator.exec('canvas:select:cachedSelection'); - this.postProcessToggle(opts); - } else { - mediator.exec('canvas:select:ensure:offsets'); - } - - // mediator.exec('canvas:select:cachedSelection'); + mediator.exec('canvas:select:ensure:offsets'); }, commit () { @@ -132,130 +126,6 @@ const ListFormatter = Module({ } }, - prepListItemsForToggle () { - const { mediator } = this; - - const canvasDoc = mediator.get('canvas:document'); - const canvasBody = mediator.get('canvas:body'); - - const { - anchorNode, - focusNode, - } = mediator.get('canvas:selection'); - - const anchorLiNode = DOM.getClosest(anchorNode, 'LI', canvasBody); - const focusLiNode = DOM.getClosest(focusNode, 'LI', canvasBody); - - mediator.exec('canvas:cache:selection'); - - let selectedLiNodes = [anchorLiNode]; - let nextLiNode = anchorLiNode.nextSibling; - while (nextLiNode && nextLiNode !== focusLiNode) { - selectedLiNodes.push(nextLiNode); - nextLiNode = nextLiNode.nextSibling; - } - selectedLiNodes.push(focusLiNode); - - selectedLiNodes.forEach((selectedLiNode) => { - let contentWrapper = canvasDoc.createElement('span'); - selectedLiNode.appendChild(contentWrapper); - while (selectedLiNode.firstChild !== contentWrapper) { - contentWrapper.appendChild(selectedLiNode.firstChild); - } - }); - - mediator.exec('canvas:select:cachedSelection'); - - return; - // const canvasBody = mediator.get('canvas:body'); - // const canvasDoc = mediator.get('canvas:document'); - // - // let rootBlock = anchorNode; - // while(rootBlock.parentNode !== canvasBody) { - // rootBlock = rootBlock.parentNode; - // } - // - // const liNodes = rootBlock.querySelectorAll('li'); - // liNodes.forEach((liNode) => { - // let pNode = canvasDoc.createElement('span'); - // liNode.appendChild(pNode); - // while (liNode.firstChild !== pNode) { - // pNode.appendChild(liNode.firstChild); - // } - // }); - }, - - postProcessToggle () { - const { mediator } = this; - // return; - - const canvasDoc = mediator.get('canvas:document'); - const canvasBody = mediator.get('canvas:body'); - - mediator.exec('canvas:cache:selection'); - - const { - anchorNode, - focusNode - } = mediator.get('canvas:selection'); - - const walkToRoot = function (node) { - let rootNode = node; - while ( rootNode.parentNode !== canvasBody ) { - rootNode = rootNode.parentNode; - } - return rootNode; - }; - - const anchorRootNode = walkToRoot(anchorNode); - const focusRootNode = walkToRoot(focusNode); - - let currentNode = anchorRootNode; - let currentParagraph; - - const createParagraph = function () { - currentParagraph = canvasDoc.createElement('p'); - DOM.insertBefore(currentParagraph, currentNode); - }; - - const handleBrNode = function (brNode) { - createParagraph(); - currentNode = brNode.nextSibling; - DOM.removeNode(brNode); - }; - - const handleDivNode = function (divNode) { - createParagraph(); - currentNode = divNode.nextSibling; - while (divNode.firstChild) { - currentParagraph.appendChild(divNode.firstChild); - } - DOM.removeNode(divNode); - }; - - createParagraph(); - - while (currentNode !== focusRootNode) { - if (currentNode.nodeName === 'BR') { - handleBrNode(currentNode); - } else if (currentNode.nodeName === 'DIV') { - handleDivNode(currentNode); - } else { - let orphanedNode = currentNode; - currentNode = currentNode.nextSibling; - currentParagraph.appendChild(orphanedNode); - } - } - - if (focusRootNode.nodeName === 'DIV') { - handleDivNode(focusRootNode); - } else { - currentParagraph.appendChild(focusRootNode); - } - - mediator.exec('canvas:select:cachedSelection'); - }, - cleanupListDOM (rootElem) { const listContainers = rootElem.querySelectorAll('OL, UL'); diff --git a/src/scripts/modules/Selection.js b/src/scripts/modules/Selection.js index a1c1317..a452d81 100644 --- a/src/scripts/modules/Selection.js +++ b/src/scripts/modules/Selection.js @@ -201,7 +201,7 @@ const Selection = Module({ getAnchorNode () { const currentSelection = this.getCurrentSelection(); - return currentSelection.anchorNode; + return currentSelection && currentSelection.anchorNode; }, getCommonAncestor () { @@ -293,14 +293,49 @@ const Selection = Module({ endContainer, endOffset } = this.getCurrentRange(); + let startCoordinates = []; let endCoordinates = []; + let startPrefixTrimLength = 0; + let endPrefixTrimLength = 0; + let endSuffixTrimLength = 0; const startTrimmablePrefix = startContainer.textContent.match(/^(\r?\n|\r)?(\s+)?/); const endTrimmablePrefix = endContainer.textContent.match(/^(\r?\n|\r)?(\s+)?/); + const endTrimmableSuffix = endContainer.textContent.match(/(\r?\n|\r)?(\s+)?$/); + const startTrimmableSides = DOM.trimmableSides(startContainer.firstChild ? startContainer.firstChild : startContainer); + const endTrimmableSides = DOM.trimmableSides(endContainer.lastChild ? endContainer.lastChild : endContainer); + + if (startTrimmablePrefix && startTrimmablePrefix[0].length) { + startPrefixTrimLength += startTrimmablePrefix[0].length; + if (!startTrimmableSides.left) { + startPrefixTrimLength -= startTrimmablePrefix[0].match(/\s/) ? 1 : 0; + } + startOffset -= startPrefixTrimLength; + if (endContainer === startContainer) { + endOffset -= startPrefixTrimLength; + } + } + + if (endTrimmablePrefix && endTrimmablePrefix[0].length && endContainer !== startContainer) { + endPrefixTrimLength += endTrimmablePrefix[0].length; + if (!endTrimmableSides.left) { + endPrefixTrimLength -= endTrimmablePrefix[0].match(/\s/) ? 1 : 0; + } + endOffset -= endPrefixTrimLength; + } + + if (endTrimmableSuffix && endTrimmableSuffix[0].length) { + endSuffixTrimLength += endTrimmableSuffix[0].length; + if (!endTrimmableSides.right) { + endSuffixTrimLength -= endTrimmableSuffix[0].match(/\s/) ? 1 : 0; + } + let trimmedTextLength = endContainer.textContent.length - endPrefixTrimLength - endSuffixTrimLength; + endOffset = Math.min(trimmedTextLength, endOffset); + } - startOffset -= startTrimmablePrefix ? startTrimmablePrefix[0].length : 0; - endOffset -= endTrimmablePrefix ? endTrimmablePrefix[0].length : 0; + startOffset = Math.max(0, startOffset); + endOffset = Math.min(endContainer.textContent.length, endOffset); startCoordinates.unshift(startOffset); endCoordinates.unshift(endOffset); @@ -331,7 +366,6 @@ const Selection = Module({ } const isIn = DOM.isIn(anchorNode, selectors, rootEl); - if (isIn) { return isIn; } @@ -341,8 +375,8 @@ const Selection = Module({ let contains = false; if (rangeFrag.childNodes.length) { - selectors.forEach(selector => { - contains = contains || rangeFrag.childNodes[0].nodeName === selector; + rangeFrag.childNodes.forEach(childNode => { + contains = contains || selectors.indexOf(childNode.nodeName) > -1; }); } @@ -365,6 +399,7 @@ const Selection = Module({ if (anchorNode.nodeType !== Node.ELEMENT_NODE) { anchorNode = anchorNode.parentNode; } + if (focusNode.nodeType !== Node.ELEMENT_NODE) { focusNode = focusNode.parentNode; } diff --git a/src/scripts/utils/DOM.js b/src/scripts/utils/DOM.js index 32a217a..6c683e1 100644 --- a/src/scripts/utils/DOM.js +++ b/src/scripts/utils/DOM.js @@ -254,7 +254,7 @@ const DOM = { closestElement(node) { let returnNode = node; - while (returnNode.nodeType !== 1) { + while (returnNode && returnNode.nodeType !== 1) { returnNode = returnNode.parentNode; } @@ -553,10 +553,18 @@ const DOM = { trimNodeText (node) { if (node.nodeType === Node.TEXT_NODE) { - const trimmedText = node.textContent - .replace(/\s{2,}/g, ' ') - .replace(/\r?\n|\r/g, '') - .trim(); + const trimmableSides = DOM.trimmableSides(node); + let trimmedText = node.textContent + .replace(/\s{2,}/g, ' ') + .replace(/\r?\n|\r/g, ''); + + if (trimmableSides.left) { + trimmedText = trimmedText.replace(/^\s+?/, ''); + } + if (trimmableSides.right) { + trimmedText = trimmedText.replace(/\s+?$/, ''); + } + node.textContent = trimmedText; } else { node.childNodes.forEach((childNode) => { @@ -565,6 +573,18 @@ const DOM = { } }, + trimmableSides (node) { + const { parentNode } = node; + const isInline = DOM.nodeIsInline(node); + const isFirstChild = parentNode && node === parentNode.firstChild; + const isLastChild = parentNode && node === parentNode.lastChild; + + return { + left: !isInline && isFirstChild, + right: !isInline && isLastChild + }; + }, + nodesToHTMLString (nodes) { let HTMLString = ''; @@ -579,6 +599,18 @@ const DOM = { return HTMLString; }, + nodeIsInline (node) { + const inlineTagNames = ['B', 'STRONG', 'I', 'U', 'S', 'SUP', 'SUB']; + + if (!node) { return false; } + if (node.nodeType !== Node.ELEMENT_NODE) { + node = DOM.closestElement(node); + } + if (!node) { return false; } + + return inlineTagNames.indexOf(node.nodeName) > -1; + }, + //Pseudo-private methods _getGetMethodName(selector) { let methodName = null; diff --git a/test/server/html/index.html b/test/server/html/index.html index fe89512..903b301 100644 --- a/test/server/html/index.html +++ b/test/server/html/index.html @@ -177,7 +177,7 @@
First paragraph
After first paragraphSecond paragraph
${contentBlockText}`); }); }); diff --git a/test/unit/modules/ListFormatter.spec.js b/test/unit/modules/ListFormatter.spec.js index 84287f4..7bcd05c 100644 --- a/test/unit/modules/ListFormatter.spec.js +++ b/test/unit/modules/ListFormatter.spec.js @@ -56,7 +56,6 @@ describe('modules/ListFormatter', function () { mediator.exec('format:list', orderedListOpts); expect(editableEl.getElementsByTagName('ol').length).toBe(0); expect(editableEl.getElementsByTagName('li').length).toBe(0); - }); it('should toggle unordered lists', () => {