From a93caa117d42ed46ae58122dec4c7952087f411d Mon Sep 17 00:00:00 2001 From: Fred Every Date: Tue, 3 Jul 2018 12:31:02 +0200 Subject: [PATCH 1/6] #9 - Avoid breaking normalization --- src/scripts/modules/BaseFormatter.js | 54 +++++----- src/scripts/modules/ListFormatter.js | 142 ++------------------------- 2 files changed, 38 insertions(+), 158 deletions(-) diff --git a/src/scripts/modules/BaseFormatter.js b/src/scripts/modules/BaseFormatter.js index 2f7e1f8..3cf2d23 100644 --- a/src/scripts/modules/BaseFormatter.js +++ b/src/scripts/modules/BaseFormatter.js @@ -23,10 +23,15 @@ * mediator.exec('format:clean', elem); // Clean the HTML inside elem */ import Module from '../core/Module'; +import commands from '../utils/commands'; import DOM from '../utils/DOM'; import zeroWidthSpace from '../utils/zeroWidthSpace'; -let validTags, blockTags, listTags; +import toolbarConfig from '../config/toolbar'; + +let validTags = toolbarConfig.getValidTags(); +let blockTags = toolbarConfig.getBlockTags(); +let listTags = toolbarConfig.getListTags(); const BaseFormatter = Module({ name: 'BaseFormatter', @@ -44,12 +49,7 @@ 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'); - }, + init () {}, /** * @func exportToCanvas @@ -64,7 +64,10 @@ const BaseFormatter = Module({ this.injectHooks(rootElement); const rangeCoordinates = mediator.get('selection:range:coordinates'); - const clonedNodes = DOM.cloneNodes(rootElement, { trim: true }); + const clonedNodes = this.cloneNodes(rootElement); + clonedNodes.forEach((node) => { + DOM.trimNodeText(node); + }); mediator.exec('canvas:content', clonedNodes); mediator.exec('canvas:select:by:coordinates', rangeCoordinates); @@ -82,6 +85,11 @@ const BaseFormatter = Module({ importFromCanvas (opts={}) { const { mediator } = this; const canvasBody = mediator.get('canvas:body'); + // --- NB REMOVE + // mediator.exec('selection:select:all'); + // mediator.exec('canvas:export:all'); + // return; + // --- NB REMOVE (END) mediator.exec('canvas:cache:selection'); mediator.exec('format:clean', canvasBody); @@ -106,7 +114,7 @@ const BaseFormatter = Module({ formatDefault () { const { mediator } = this; const rootElem = mediator.get('selection:rootelement'); - mediator.exec('commands:format:default'); + commands.defaultBlockFormat(); this.removeStyledSpans(rootElem); }, @@ -138,6 +146,14 @@ const BaseFormatter = Module({ /** * 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); @@ -154,8 +170,7 @@ const BaseFormatter = Module({ formatEmptyNewLine () { const { mediator } = this; const anchorNode = mediator.get('selection:anchornode'); - const preventNewlineDefault = mediator.get('config:toolbar:preventNewlineDefault'); - const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, preventNewlineDefault); + const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, toolbarConfig.preventNewlineDefault); const anchorIsContentEditable = anchorNode.hasAttribute && anchorNode.hasAttribute('contenteditable'); if (canDefaultNewline || anchorIsContentEditable) { @@ -163,12 +178,10 @@ const BaseFormatter = Module({ } }, - formatBlockquoteNewLine () { + formateBlockquoteNewLine () { const { mediator } = this; - mediator.exec('commands:exec', { - command: 'outdent' - }); + commands.exec('outdent'); this.formatDefault(); const currentRangeClone = mediator.get('selection:range').cloneRange(); @@ -199,7 +212,7 @@ const BaseFormatter = Module({ const isContentEditable = startContainer.nodeType === Node.ELEMENT_NODE && startContainer.hasAttribute('contenteditable'); if (containerIsBlockquote) { - this.formatBlockquoteNewLine(); + this.formateBlockquoteNewLine(); } else if (containerIsEmpty || isContentEditable) { this.formatEmptyNewLine(); } @@ -227,10 +240,9 @@ const BaseFormatter = Module({ const isLastChild = brNode === brNode.parentNode.lastChild; const isDoubleBreak = brNode.nextSibling && brNode.nextSibling.nodeName === 'BR'; const isInBlock = DOM.isIn(brNode, blockTags, rootElem); - const isOrphan = brNode.parentNode === rootElem; - if (isLastChild || isOrphan) { - brNodesToRemove.push(brNode); + if (isLastChild) { + brNodesToRemove.push(isLastChild); return; } @@ -284,9 +296,8 @@ const BaseFormatter = Module({ let isInvalid = validTags.indexOf(currentNode.nodeName) < 0; let isBrNode = currentNode.nodeName === 'BR'; // BR nodes are handled elsewhere let isTypesterElem = currentNode.className && /typester/.test(currentNode.className); - let isElement = currentNode.nodeType !== Node.TEXT_NODE; - if (isInvalid && !isBrNode && !isTypesterElem && isElement) { + if (isInvalid && !isBrNode && !isTypesterElem) { invalidElements.unshift(currentNode); } } @@ -306,7 +317,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/ListFormatter.js b/src/scripts/modules/ListFormatter.js index 920cebc..e7d10f4 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,20 +91,13 @@ 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 () { const { mediator, cleanupListDOM } = this; mediator.exec('format:import:from:canvas', { - importFilter: cleanupListDOM + // importFilter: cleanupListDOM }); }, @@ -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'); From 91deb60c9468e8d7412f1c9befb5d6ba0b625f53 Mon Sep 17 00:00:00 2001 From: Fred Every Date: Tue, 3 Jul 2018 21:28:48 +0200 Subject: [PATCH 2/6] #9 - Cross browser fixes + WIP --- src/scripts/modules/BlockFormatter.js | 2 +- src/scripts/modules/ContentEditable.js | 4 +- src/scripts/modules/Selection.js | 73 ++++++++++++++++++++++--- src/scripts/utils/DOM.js | 29 ++++++++-- test/server/html/index.html | 2 +- test/unit/modules/BaseFormatter.spec.js | 6 +- test/unit/modules/Commands.spec.js | 7 +++ test/unit/modules/ListFormatter.spec.js | 8 +++ 8 files changed, 111 insertions(+), 20 deletions(-) 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..0c91829 100644 --- a/src/scripts/modules/ContentEditable.js +++ b/src/scripts/modules/ContentEditable.js @@ -152,7 +152,9 @@ const ContentEditable = Module({ } else { let currentSelection = mediator.get('selection:current'); let currentRange = mediator.get('selection:range'); - + if (!currentSelection) { + console.log('insertHTML:currentSelection'); + } currentRange.deleteContents(); let tmpContainer = document.createElement('container'); diff --git a/src/scripts/modules/Selection.js b/src/scripts/modules/Selection.js index a1c1317..82bc5fb 100644 --- a/src/scripts/modules/Selection.js +++ b/src/scripts/modules/Selection.js @@ -187,6 +187,9 @@ const Selection = Module({ const { props } = this; const currentSelection = this.getCurrentSelection(); let currentRange; + if (!currentSelection) { + console.log('getCurrentRange:currentSelection', currentSelection); + } if (this.validateSelection(currentSelection)) { currentRange = currentSelection.getRangeAt(0); @@ -201,11 +204,17 @@ const Selection = Module({ getAnchorNode () { const currentSelection = this.getCurrentSelection(); - return currentSelection.anchorNode; + if (!currentSelection) { + console.log('getAnchorNode:currentSelection', currentSelection); + } + return currentSelection && currentSelection.anchorNode; }, getCommonAncestor () { const currentSelection = this.getCurrentSelection(); + if (!currentSelection) { + console.log('getCommonAncestor:currentSelection', currentSelection); + } if (currentSelection.rangeCount > 0) { const selectionRange = currentSelection.getRangeAt(0); return selectionRange.commonAncestorContainer; @@ -298,9 +307,50 @@ const Selection = Module({ 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 startElement = DOM.closestElement(startContainer); + const endElement = DOM.closestElement(endContainer); + + console.log({ + startTrimmablePrefix, + endTrimmablePrefix, + startPrefixLength: startTrimmablePrefix && startTrimmablePrefix[0].length, + endPrefixLength: endTrimmablePrefix && endTrimmablePrefix[0].length, + startContent: startContainer.textContent, + endContent: endContainer.textContent, + endSuffix: endContainer.textContent.match(/(\r?\n|\r)?(\s+)?$/), + startOffset, + endOffset + }); + + if (startTrimmablePrefix && startTrimmablePrefix[0].length) { + startOffset -= startTrimmablePrefix[0].length; + if (DOM.nodeIsInline(startElement)) { + startOffset -= startTrimmablePrefix[0].match(/\s/) ? 1 : 0; + } + } + + if (endTrimmablePrefix && endTrimmablePrefix[0].length) { + endOffset -= endTrimmablePrefix[0].length; + if (DOM.nodeIsInline(endElement)) { + endOffset -= endTrimmablePrefix[0].match(/\s/) ? 1 : 0; + } + } + + if (endTrimmableSuffix && endTrimmableSuffix[0].length) { + // endOffset -= endTrimmableSuffix[0].length; + if (DOM.nodeIsInline(endElement)) { + endOffset -= endTrimmableSuffix[0].match(/\s/) ? 1 : 0; + } + } + + console.log({ + startOffset, + 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 +381,6 @@ const Selection = Module({ } const isIn = DOM.isIn(anchorNode, selectors, rootEl); - if (isIn) { return isIn; } @@ -341,8 +390,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; }); } @@ -353,7 +402,9 @@ const Selection = Module({ const currentSelection = this.getCurrentSelection(); let { anchorNode, focusNode } = currentSelection; const selectionContainsNode = currentSelection.containsNode(node, true); - + if (!currentSelection) { + console.log('containsNode:currentSelection', currentSelection); + } if (!currentSelection.rangeCount) { return false; } @@ -487,7 +538,9 @@ const Selection = Module({ updateRange (range, opts={}) { const { mediator, props } = this; const currentSelection = this.getCurrentSelection(); - + if (!currentSelection) { + console.log('updateRange:currentSelection', currentSelection); + } if (opts.silent) { props.silenceChanges.push(true); // silence removeAllRanges props.silenceChanges.push(true); // silence addRange @@ -634,6 +687,8 @@ const Selection = Module({ const startOffset = startCoordinates.pop(); const endOffset = endCoordinates.pop(); + console.log('selectByCoordinates', { rangeCoordinates }); + let startContainer = dom.el[0]; let endContainer = dom.el[0]; @@ -647,6 +702,8 @@ const Selection = Module({ endContainer = endContainer.childNodes[endIndex]; } + console.log({ startContainer, endContainer, startLength: startContainer.length, endLength: endContainer.length, startOffset, endOffset }); + newRange.setStart(startContainer, startOffset); newRange.setEnd(endContainer, endOffset); diff --git a/src/scripts/utils/DOM.js b/src/scripts/utils/DOM.js index 32a217a..93132c9 100644 --- a/src/scripts/utils/DOM.js +++ b/src/scripts/utils/DOM.js @@ -253,9 +253,11 @@ const DOM = { closestElement(node) { let returnNode = node; + console.log('closestElement', node); - while (returnNode.nodeType !== 1) { + while (returnNode && returnNode.nodeType !== 1) { returnNode = returnNode.parentNode; + console.log('returnNode', returnNode); } return returnNode; @@ -553,10 +555,13 @@ 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 parentElement = DOM.closestElement(node); + let trimmedText = node.textContent + .replace(/\s{2,}/g, ' ') + .replace(/\r?\n|\r/g, ''); + if (!DOM.nodeIsInline(parentElement)) { + trimmedText = trimmedText.trim(); + } node.textContent = trimmedText; } else { node.childNodes.forEach((childNode) => { @@ -579,6 +584,20 @@ 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) { + return true; + } else { + return inlineTagNames.indexOf(node.nodeName) < 0; + } + }, + //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 @@

Typester test server

node.childNodes.forEach(function (childNode) { if (childNode.nodeType === Node.TEXT_NODE) { if (childNode.textContent.trim().length) { - appendHtmlText(childNode.textContent.trim(), opts.indentation + 1, true); + appendHtmlText(childNode.textContent.replace(/\s/g, '\u00B7'), opts.indentation + 1, true); } } else { appendHtmlText(generateHtmlText(childNode, { diff --git a/test/unit/modules/BaseFormatter.spec.js b/test/unit/modules/BaseFormatter.spec.js index 24be414..35c770e 100644 --- a/test/unit/modules/BaseFormatter.spec.js +++ b/test/unit/modules/BaseFormatter.spec.js @@ -53,16 +53,14 @@ describe('modules/BaseFormatter', function () { it('should clean html on export', () => { const canvasBody = mediator.get('canvas:body'); - canvasBody.innerHTML = ` -

Heading copy

+ canvasBody.innerHTML = `

Heading copy

Sub heading copy

First paragraph

After first paragraph

Second paragraph

- - `; + `; selectionHelper.selectFromTo(canvasBody.firstChild, 0, canvasBody.firstChild, 1); mediator.exec('format:import:from:canvas'); if (!/\w+/.test(canvasBody.firstChild.textContent) && !/\w+/.test(editableEl.firstChild.textContent)) { diff --git a/test/unit/modules/Commands.spec.js b/test/unit/modules/Commands.spec.js index c10d088..28fbea1 100644 --- a/test/unit/modules/Commands.spec.js +++ b/test/unit/modules/Commands.spec.js @@ -4,6 +4,7 @@ import Mediator from '../../../src/scripts/core/Mediator'; import Commands from '../../../src/scripts/modules/Commands'; import Config from '../../../src/scripts/modules/Config'; import selectionHelper from '../helpers/selection'; +import DOM from '../../../src/scripts/utils/DOM'; describe('modules/Commands', function () { let mediator, $editableEl, editableEl; @@ -66,6 +67,12 @@ describe('modules/Commands', function () { style: 'BLOCKQUOTE' }); + + const blockquoteParagraphs = editableEl.querySelectorAll('blockquote p'); + blockquoteParagraphs.forEach((paragraph) => { + DOM.unwrap(paragraph); + }); + expect(editableEl.innerHTML).toBe(`
${contentBlockText}
`); }); }); diff --git a/test/unit/modules/ListFormatter.spec.js b/test/unit/modules/ListFormatter.spec.js index 84287f4..e1a35e0 100644 --- a/test/unit/modules/ListFormatter.spec.js +++ b/test/unit/modules/ListFormatter.spec.js @@ -40,6 +40,7 @@ describe('modules/ListFormatter', function () { afterEach(() => { editableEl.innerHTML = ''; mediator.emit('app:destroy'); + console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); }); it('should toggle ordered lists', () => { @@ -47,15 +48,22 @@ describe('modules/ListFormatter', function () { expect(editableEl.getElementsByTagName('ol').length).toBe(0); expect(editableEl.getElementsByTagName('li').length).toBe(0); + console.log('---------------------------------------------------------------------------------------[2:start]') selectionHelper.selectFirstAndLastTextNodes(editableEl); + console.log(document.getSelection().toString()); mediator.exec('format:list', orderedListOpts); + console.log('---------------------------------------------------------------------------------------[2:end]') expect(editableEl.getElementsByTagName('ol').length).toBe(1); expect(editableEl.getElementsByTagName('li').length).toBe(5); + console.log('---------------------------------------------------------------------------------------[2:start]') selectionHelper.selectFirstAndLastTextNodes(editableEl); + console.log(document.getSelection().toString()); mediator.exec('format:list', orderedListOpts); + console.log('---------------------------------------------------------------------------------------[2:end]') expect(editableEl.getElementsByTagName('ol').length).toBe(0); expect(editableEl.getElementsByTagName('li').length).toBe(0); + console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); }); From 1775c044969468c9d93939abb2ccb5cb42a38467 Mon Sep 17 00:00:00 2001 From: Fred Every Date: Thu, 5 Jul 2018 14:20:53 +0200 Subject: [PATCH 3/6] #9 - Range with trim refinements + log cleanup --- src/scripts/modules/ContentEditable.js | 3 -- src/scripts/modules/Selection.js | 44 ++++++++++++------------- src/scripts/utils/DOM.js | 2 -- test/unit/modules/ListFormatter.spec.js | 9 ----- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/scripts/modules/ContentEditable.js b/src/scripts/modules/ContentEditable.js index 0c91829..cfa8b09 100644 --- a/src/scripts/modules/ContentEditable.js +++ b/src/scripts/modules/ContentEditable.js @@ -152,9 +152,6 @@ const ContentEditable = Module({ } else { let currentSelection = mediator.get('selection:current'); let currentRange = mediator.get('selection:range'); - if (!currentSelection) { - console.log('insertHTML:currentSelection'); - } currentRange.deleteContents(); let tmpContainer = document.createElement('container'); diff --git a/src/scripts/modules/Selection.js b/src/scripts/modules/Selection.js index 82bc5fb..cc80130 100644 --- a/src/scripts/modules/Selection.js +++ b/src/scripts/modules/Selection.js @@ -187,9 +187,6 @@ const Selection = Module({ const { props } = this; const currentSelection = this.getCurrentSelection(); let currentRange; - if (!currentSelection) { - console.log('getCurrentRange:currentSelection', currentSelection); - } if (this.validateSelection(currentSelection)) { currentRange = currentSelection.getRangeAt(0); @@ -204,17 +201,11 @@ const Selection = Module({ getAnchorNode () { const currentSelection = this.getCurrentSelection(); - if (!currentSelection) { - console.log('getAnchorNode:currentSelection', currentSelection); - } return currentSelection && currentSelection.anchorNode; }, getCommonAncestor () { const currentSelection = this.getCurrentSelection(); - if (!currentSelection) { - console.log('getCommonAncestor:currentSelection', currentSelection); - } if (currentSelection.rangeCount > 0) { const selectionRange = currentSelection.getRangeAt(0); return selectionRange.commonAncestorContainer; @@ -310,10 +301,14 @@ const Selection = Module({ const endTrimmableSuffix = endContainer.textContent.match(/(\r?\n|\r)?(\s+)?$/); const startElement = DOM.closestElement(startContainer); const endElement = DOM.closestElement(endContainer); + let startPrefixTrimLength = 0; + let endPrefixTrimLength = 0; + let endSuffixTrimLength = 0; console.log({ startTrimmablePrefix, endTrimmablePrefix, + endTrimmableSuffix, startPrefixLength: startTrimmablePrefix && startTrimmablePrefix[0].length, endPrefixLength: endTrimmablePrefix && endTrimmablePrefix[0].length, startContent: startContainer.textContent, @@ -324,24 +319,32 @@ const Selection = Module({ }); if (startTrimmablePrefix && startTrimmablePrefix[0].length) { - startOffset -= startTrimmablePrefix[0].length; + startPrefixTrimLength += startTrimmablePrefix[0].length; if (DOM.nodeIsInline(startElement)) { - startOffset -= startTrimmablePrefix[0].match(/\s/) ? 1 : 0; + startPrefixTrimLength -= startTrimmablePrefix[0].match(/\s/) ? 1 : 0; + } + startOffset -= startPrefixTrimLength; + if (endContainer === startContainer) { + endOffset -= startPrefixTrimLength; } } - if (endTrimmablePrefix && endTrimmablePrefix[0].length) { - endOffset -= endTrimmablePrefix[0].length; + if (endTrimmablePrefix && endTrimmablePrefix[0].length && endContainer !== startContainer) { + endPrefixTrimLength += endTrimmablePrefix[0].length; if (DOM.nodeIsInline(endElement)) { - endOffset -= endTrimmablePrefix[0].match(/\s/) ? 1 : 0; + endPrefixTrimLength -= endTrimmablePrefix[0].match(/\s/) ? 1 : 0; } + endOffset -= endPrefixTrimLength; } if (endTrimmableSuffix && endTrimmableSuffix[0].length) { - // endOffset -= endTrimmableSuffix[0].length; + endSuffixTrimLength += endTrimmableSuffix[0].length; if (DOM.nodeIsInline(endElement)) { - endOffset -= endTrimmableSuffix[0].match(/\s/) ? 1 : 0; + endSuffixTrimLength -= endTrimmableSuffix[0].match(/\s/) ? 1 : 0; } + let trimmedTextLength = endContainer.textContent.length - endPrefixTrimLength - endSuffixTrimLength; + console.log('>>', { endOffset, trimmedTextLength, textLength: endContainer.textContent.length, endPrefixTrimLength, endSuffixTrimLength }); + endOffset = Math.min(trimmedTextLength, endOffset); } console.log({ @@ -402,9 +405,7 @@ const Selection = Module({ const currentSelection = this.getCurrentSelection(); let { anchorNode, focusNode } = currentSelection; const selectionContainsNode = currentSelection.containsNode(node, true); - if (!currentSelection) { - console.log('containsNode:currentSelection', currentSelection); - } + if (!currentSelection.rangeCount) { return false; } @@ -416,6 +417,7 @@ const Selection = Module({ if (anchorNode.nodeType !== Node.ELEMENT_NODE) { anchorNode = anchorNode.parentNode; } + if (focusNode.nodeType !== Node.ELEMENT_NODE) { focusNode = focusNode.parentNode; } @@ -538,9 +540,7 @@ const Selection = Module({ updateRange (range, opts={}) { const { mediator, props } = this; const currentSelection = this.getCurrentSelection(); - if (!currentSelection) { - console.log('updateRange:currentSelection', currentSelection); - } + if (opts.silent) { props.silenceChanges.push(true); // silence removeAllRanges props.silenceChanges.push(true); // silence addRange diff --git a/src/scripts/utils/DOM.js b/src/scripts/utils/DOM.js index 93132c9..7196a69 100644 --- a/src/scripts/utils/DOM.js +++ b/src/scripts/utils/DOM.js @@ -253,11 +253,9 @@ const DOM = { closestElement(node) { let returnNode = node; - console.log('closestElement', node); while (returnNode && returnNode.nodeType !== 1) { returnNode = returnNode.parentNode; - console.log('returnNode', returnNode); } return returnNode; diff --git a/test/unit/modules/ListFormatter.spec.js b/test/unit/modules/ListFormatter.spec.js index e1a35e0..7bcd05c 100644 --- a/test/unit/modules/ListFormatter.spec.js +++ b/test/unit/modules/ListFormatter.spec.js @@ -40,7 +40,6 @@ describe('modules/ListFormatter', function () { afterEach(() => { editableEl.innerHTML = ''; mediator.emit('app:destroy'); - console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); }); it('should toggle ordered lists', () => { @@ -48,23 +47,15 @@ describe('modules/ListFormatter', function () { expect(editableEl.getElementsByTagName('ol').length).toBe(0); expect(editableEl.getElementsByTagName('li').length).toBe(0); - console.log('---------------------------------------------------------------------------------------[2:start]') selectionHelper.selectFirstAndLastTextNodes(editableEl); - console.log(document.getSelection().toString()); mediator.exec('format:list', orderedListOpts); - console.log('---------------------------------------------------------------------------------------[2:end]') expect(editableEl.getElementsByTagName('ol').length).toBe(1); expect(editableEl.getElementsByTagName('li').length).toBe(5); - console.log('---------------------------------------------------------------------------------------[2:start]') selectionHelper.selectFirstAndLastTextNodes(editableEl); - console.log(document.getSelection().toString()); mediator.exec('format:list', orderedListOpts); - console.log('---------------------------------------------------------------------------------------[2:end]') expect(editableEl.getElementsByTagName('ol').length).toBe(0); expect(editableEl.getElementsByTagName('li').length).toBe(0); - console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); - }); it('should toggle unordered lists', () => { From 3f05140d315e4a43e42f6f9b728f051fa1093245 Mon Sep 17 00:00:00 2001 From: Fred Every Date: Thu, 5 Jul 2018 16:26:37 +0200 Subject: [PATCH 4/6] #9 - Another refinement on selection ranging & trimming --- src/scripts/modules/ListFormatter.js | 2 +- src/scripts/modules/Selection.js | 40 +++++++--------------------- src/scripts/utils/DOM.js | 35 +++++++++++++++++------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/scripts/modules/ListFormatter.js b/src/scripts/modules/ListFormatter.js index e7d10f4..aa295e9 100644 --- a/src/scripts/modules/ListFormatter.js +++ b/src/scripts/modules/ListFormatter.js @@ -97,7 +97,7 @@ const ListFormatter = Module({ commit () { const { mediator, cleanupListDOM } = this; mediator.exec('format:import:from:canvas', { - // importFilter: cleanupListDOM + importFilter: cleanupListDOM }); }, diff --git a/src/scripts/modules/Selection.js b/src/scripts/modules/Selection.js index cc80130..a452d81 100644 --- a/src/scripts/modules/Selection.js +++ b/src/scripts/modules/Selection.js @@ -293,34 +293,22 @@ const Selection = Module({ endContainer, endOffset } = this.getCurrentRange(); + let startCoordinates = []; let endCoordinates = []; - - 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 startElement = DOM.closestElement(startContainer); - const endElement = DOM.closestElement(endContainer); let startPrefixTrimLength = 0; let endPrefixTrimLength = 0; let endSuffixTrimLength = 0; - console.log({ - startTrimmablePrefix, - endTrimmablePrefix, - endTrimmableSuffix, - startPrefixLength: startTrimmablePrefix && startTrimmablePrefix[0].length, - endPrefixLength: endTrimmablePrefix && endTrimmablePrefix[0].length, - startContent: startContainer.textContent, - endContent: endContainer.textContent, - endSuffix: endContainer.textContent.match(/(\r?\n|\r)?(\s+)?$/), - startOffset, - endOffset - }); + 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 (DOM.nodeIsInline(startElement)) { + if (!startTrimmableSides.left) { startPrefixTrimLength -= startTrimmablePrefix[0].match(/\s/) ? 1 : 0; } startOffset -= startPrefixTrimLength; @@ -331,7 +319,7 @@ const Selection = Module({ if (endTrimmablePrefix && endTrimmablePrefix[0].length && endContainer !== startContainer) { endPrefixTrimLength += endTrimmablePrefix[0].length; - if (DOM.nodeIsInline(endElement)) { + if (!endTrimmableSides.left) { endPrefixTrimLength -= endTrimmablePrefix[0].match(/\s/) ? 1 : 0; } endOffset -= endPrefixTrimLength; @@ -339,19 +327,13 @@ const Selection = Module({ if (endTrimmableSuffix && endTrimmableSuffix[0].length) { endSuffixTrimLength += endTrimmableSuffix[0].length; - if (DOM.nodeIsInline(endElement)) { + if (!endTrimmableSides.right) { endSuffixTrimLength -= endTrimmableSuffix[0].match(/\s/) ? 1 : 0; } let trimmedTextLength = endContainer.textContent.length - endPrefixTrimLength - endSuffixTrimLength; - console.log('>>', { endOffset, trimmedTextLength, textLength: endContainer.textContent.length, endPrefixTrimLength, endSuffixTrimLength }); endOffset = Math.min(trimmedTextLength, endOffset); } - console.log({ - startOffset, - endOffset - }); - startOffset = Math.max(0, startOffset); endOffset = Math.min(endContainer.textContent.length, endOffset); @@ -687,8 +669,6 @@ const Selection = Module({ const startOffset = startCoordinates.pop(); const endOffset = endCoordinates.pop(); - console.log('selectByCoordinates', { rangeCoordinates }); - let startContainer = dom.el[0]; let endContainer = dom.el[0]; @@ -702,8 +682,6 @@ const Selection = Module({ endContainer = endContainer.childNodes[endIndex]; } - console.log({ startContainer, endContainer, startLength: startContainer.length, endLength: endContainer.length, startOffset, endOffset }); - newRange.setStart(startContainer, startOffset); newRange.setEnd(endContainer, endOffset); diff --git a/src/scripts/utils/DOM.js b/src/scripts/utils/DOM.js index 7196a69..6c683e1 100644 --- a/src/scripts/utils/DOM.js +++ b/src/scripts/utils/DOM.js @@ -553,13 +553,18 @@ const DOM = { trimNodeText (node) { if (node.nodeType === Node.TEXT_NODE) { - const parentElement = DOM.closestElement(node); + const trimmableSides = DOM.trimmableSides(node); let trimmedText = node.textContent .replace(/\s{2,}/g, ' ') .replace(/\r?\n|\r/g, ''); - if (!DOM.nodeIsInline(parentElement)) { - trimmedText = trimmedText.trim(); + + if (trimmableSides.left) { + trimmedText = trimmedText.replace(/^\s+?/, ''); + } + if (trimmableSides.right) { + trimmedText = trimmedText.replace(/\s+?$/, ''); } + node.textContent = trimmedText; } else { node.childNodes.forEach((childNode) => { @@ -568,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 = ''; @@ -585,15 +602,13 @@ const DOM = { nodeIsInline (node) { const inlineTagNames = ['B', 'STRONG', 'I', 'U', 'S', 'SUP', 'SUB']; - if (!node) { - return false; - } - + if (!node) { return false; } if (node.nodeType !== Node.ELEMENT_NODE) { - return true; - } else { - return inlineTagNames.indexOf(node.nodeName) < 0; + node = DOM.closestElement(node); } + if (!node) { return false; } + + return inlineTagNames.indexOf(node.nodeName) > -1; }, //Pseudo-private methods From cb166817278bbaf552075636c3c4e9f6d75f88bc Mon Sep 17 00:00:00 2001 From: Fred Every Date: Fri, 6 Jul 2018 15:26:58 +0200 Subject: [PATCH 5/6] #9 - Code cleanup --- src/scripts/modules/BaseFormatter.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/scripts/modules/BaseFormatter.js b/src/scripts/modules/BaseFormatter.js index 3cf2d23..b33e681 100644 --- a/src/scripts/modules/BaseFormatter.js +++ b/src/scripts/modules/BaseFormatter.js @@ -85,11 +85,6 @@ const BaseFormatter = Module({ importFromCanvas (opts={}) { const { mediator } = this; const canvasBody = mediator.get('canvas:body'); - // --- NB REMOVE - // mediator.exec('selection:select:all'); - // mediator.exec('canvas:export:all'); - // return; - // --- NB REMOVE (END) mediator.exec('canvas:cache:selection'); mediator.exec('format:clean', canvasBody); From 484bf4a0e9af6c92837906d0c9b03bd3d5cd98c1 Mon Sep 17 00:00:00 2001 From: Fred Every Date: Mon, 9 Jul 2018 15:39:44 +0200 Subject: [PATCH 6/6] #9 - Fix rebase reversion --- src/scripts/modules/BaseFormatter.js | 53 ++++++++++++---------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/scripts/modules/BaseFormatter.js b/src/scripts/modules/BaseFormatter.js index b33e681..d1071ab 100644 --- a/src/scripts/modules/BaseFormatter.js +++ b/src/scripts/modules/BaseFormatter.js @@ -23,15 +23,10 @@ * mediator.exec('format:clean', elem); // Clean the HTML inside elem */ import Module from '../core/Module'; -import commands from '../utils/commands'; import DOM from '../utils/DOM'; import zeroWidthSpace from '../utils/zeroWidthSpace'; -import toolbarConfig from '../config/toolbar'; - -let validTags = toolbarConfig.getValidTags(); -let blockTags = toolbarConfig.getBlockTags(); -let listTags = toolbarConfig.getListTags(); +let validTags, blockTags, listTags; const BaseFormatter = Module({ name: 'BaseFormatter', @@ -49,7 +44,13 @@ const BaseFormatter = Module({ } }, methods: { - init () {}, + init () { + const { mediator } = this; + + validTags = mediator.get('config:toolbar:validTags'); + blockTags = mediator.get('config:toolbar:blockTags'); + listTags = mediator.get('config:toolbar:listTags'); + }, /** * @func exportToCanvas @@ -64,10 +65,7 @@ const BaseFormatter = Module({ this.injectHooks(rootElement); const rangeCoordinates = mediator.get('selection:range:coordinates'); - const clonedNodes = this.cloneNodes(rootElement); - clonedNodes.forEach((node) => { - DOM.trimNodeText(node); - }); + const clonedNodes = DOM.cloneNodes(rootElement, { trim: true }); mediator.exec('canvas:content', clonedNodes); mediator.exec('canvas:select:by:coordinates', rangeCoordinates); @@ -109,7 +107,8 @@ const BaseFormatter = Module({ formatDefault () { const { mediator } = this; const rootElem = mediator.get('selection:rootelement'); - commands.defaultBlockFormat(); + + mediator.exec('commands:format:default'); this.removeStyledSpans(rootElem); }, @@ -125,17 +124,6 @@ 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 }); }, /** @@ -165,7 +153,8 @@ const BaseFormatter = Module({ formatEmptyNewLine () { const { mediator } = this; const anchorNode = mediator.get('selection:anchornode'); - const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, toolbarConfig.preventNewlineDefault); + const preventNewlineDefault = mediator.get('config:toolbar:preventNewlineDefault'); + const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, preventNewlineDefault); const anchorIsContentEditable = anchorNode.hasAttribute && anchorNode.hasAttribute('contenteditable'); if (canDefaultNewline || anchorIsContentEditable) { @@ -173,10 +162,12 @@ const BaseFormatter = Module({ } }, - formateBlockquoteNewLine () { + formatBlockquoteNewLine () { const { mediator } = this; - commands.exec('outdent'); + mediator.exec('commands:exec', { + command: 'outdent' + }); this.formatDefault(); const currentRangeClone = mediator.get('selection:range').cloneRange(); @@ -207,7 +198,7 @@ const BaseFormatter = Module({ const isContentEditable = startContainer.nodeType === Node.ELEMENT_NODE && startContainer.hasAttribute('contenteditable'); if (containerIsBlockquote) { - this.formateBlockquoteNewLine(); + this.formatBlockquoteNewLine(); } else if (containerIsEmpty || isContentEditable) { this.formatEmptyNewLine(); } @@ -235,9 +226,10 @@ const BaseFormatter = Module({ const isLastChild = brNode === brNode.parentNode.lastChild; const isDoubleBreak = brNode.nextSibling && brNode.nextSibling.nodeName === 'BR'; const isInBlock = DOM.isIn(brNode, blockTags, rootElem); + const isOrphan = brNode.parentNode === rootElem; - if (isLastChild) { - brNodesToRemove.push(isLastChild); + if (isLastChild || isOrphan) { + brNodesToRemove.push(brNode); return; } @@ -291,8 +283,9 @@ const BaseFormatter = Module({ let isInvalid = validTags.indexOf(currentNode.nodeName) < 0; let isBrNode = currentNode.nodeName === 'BR'; // BR nodes are handled elsewhere let isTypesterElem = currentNode.className && /typester/.test(currentNode.className); + let isElement = currentNode.nodeType !== Node.TEXT_NODE; - if (isInvalid && !isBrNode && !isTypesterElem) { + if (isInvalid && !isBrNode && !isTypesterElem && isElement) { invalidElements.unshift(currentNode); } }