From 562506fb050a00d7777f96b19b6685972cdd2cdc Mon Sep 17 00:00:00 2001 From: Petro Salema Date: Thu, 27 Jun 2013 10:01:52 +0200 Subject: [PATCH] Refactor the code around where splitting is done to make it more robust --- build/hotfix-changelog.md | 4 +- src/lib/util/dom.js | 210 ++++++++++++++++++++++++++++++-------- 2 files changed, 168 insertions(+), 46 deletions(-) diff --git a/build/hotfix-changelog.md b/build/hotfix-changelog.md index 61fd334b09..53a874f5e8 100644 --- a/build/hotfix-changelog.md +++ b/build/hotfix-changelog.md @@ -11,5 +11,5 @@ All changes are categorized into one of the following keywords: - **BUGFIX**: paste-plugin: Browsers no longer scroll to the top of an editable after content was pasted. -- **BUGFIX**: Add a block/table not longer result in get an empty paragraph - before the inserted element. +- **BUGFIX**: Adding blocks and tables will no longer results in empty paragraphs + being littered before the inserted element. diff --git a/src/lib/util/dom.js b/src/lib/util/dom.js index 67ca622ac2..a0f284c628 100755 --- a/src/lib/util/dom.js +++ b/src/lib/util/dom.js @@ -72,6 +72,159 @@ define(['jquery', 'util/class', 'aloha/ecma5shims'], function (jQuery, Class, $_ 'LI': true }; + /** + * Can't use elem.childNodes.length because + * http://www.quirksmode.org/dom/w3c_core.html + * "IE up to 8 does not count empty text nodes." + * + * Taken from Dom2.js + */ + function numChildren(elem) { + var count = 0; + var child = elem.firstChild; + while (child) { + count += 1; + child = child.nextSibling; + } + return count; + } + + /** + * Taken from Dom2.js + */ + function nodeLength(node) { + if (1 === node.nodeType) { + return numChildren(node); + } + if (3 === node.nodeType) { + return node.length; + } + return 0; + } + + /** + * Checks if the element given is an aloha-editing-p helper, added by split. + * + * @param {HTMLElement} node + * @return {Boolean} True if the given element is an + * aloha-editing-paragraph. + */ + function isAlohaEditingP(node) { + return ( + node.className === 'aloha-editing-p' + && nodeLength(node) === 1 + && node.children[0].nodeName === 'BR' + && node.children[0].className === 'aloha-end-br' + ); + } + + /** + * Starting from the given node, will walk forward (right-ward) through the + * node until an element is found that matches the predicate `match` or we + * reach the last element in the tree inside the editing host. + * + * @param {HTMLElement} node An element that must be inside an editable. + * @param {function(HTMLElement):Boolean} match A prediate function to + * determine wether or not the + * node matches one we are + * looking for. + * @return {HTMLElement} The matched node that is forward in the DOM tree + * from `node`; null if nothing can be found that + * matches `match`. + */ + function findNodeForward(node, match) { + if (!node) { + return null; + } + if (match(node)) { + return node; + } + var next = node.firstChild + || node.nextSibling + || ( + node.parentNode + && !GENTICS.Utils.Dom.isEditingHost(node.parentNode) + && node.parentNode.nextSibling + ); + return next ? findNodeForward(next, match) : null; + } + + function isVisiblyEmpty(node) { + if (!node) { + return true; + } + // TODO: use isChildlessElement() + if ('BR' === node.nodeName) { + return false; + } + if (node.nodeType === Node.TEXT_NODE) { + // TODO: would prefer to use + // (Html.isWhitespaces(node) || Html.isZeroWidthCharacters(node)) + // but cannot because of circular dependency + if (node.data.search(/\S/) === -1) { + return true; + } + // Fix for IE with zero-width characters + if (1 === node.data.length && node.data.charCodeAt(0) >= 0x2000) { + return true; + } + return false; + } + var numChildren = nodeLength(node); + if (0 === numChildren) { + return true; + } + var children = node.childNodes; + var i; + for (i = 0; i < numChildren; i++) { + if (!isVisiblyEmpty(children[i])) { + return false; + } + } + return true; + } + + /** + * Checks for the opposite condition of isVisiblyEmpty(). + * + * @param {HTMLElement} node + * @return {Boolean} True if the given node is visible not empty. + */ + function isNotVisiblyEmpty(node) { + return !isVisiblyEmpty(node); + } + + /** + * Checks whether the given element is a "phantom" element-- ie: an element + * that is either invisible or an aloha-editing-paragraph element. + * + * @param {HTMLElement} node + * @return {Boolean} True if the element is a "phantom" element. + */ + function isPhantomNode(node) { + return isVisiblyEmpty(node) || isAlohaEditingP(node); + } + + /** + * Inserts the DOM node `element` appropriately during a split operation. + * + * The element `head` is used to reference where the element should be + * inserted. Depending on the structure of this `head` node, the `element` + * will either replace/overwrite `head` or be appending immediately after + * `head`. + * + * @param {HTMLElement} head The "head" element of the "head and tail" + * nodes resulting from splitting a DOM element. + * @param {HTMLElement} element The element to be inserted between the head + * and tail split parts. + */ + function insertAfterSplit(head, element) { + if (head.nodeType !== Node.TEXT_NODE && isPhantomNode(head)) { + jQuery(head).replaceWith(element); + } else { + jQuery(head).after(element); + } + } /** * @namespace GENTICS.Utils @@ -1175,39 +1328,21 @@ define(['jquery', 'util/class', 'aloha/ecma5shims'], function (jQuery, Class, $_ return true; } if (splitParts) { - // if the DOM could be split, we insert the new object in between the split parts - - if (splitParts[0].nodeType !== Node.TEXT_NODE && ( - this.isEmpty(splitParts[0]) || this.isAlohaEditingP(splitParts[0]) - )) { - splitParts.eq(0).replaceWith(object); - } else { - splitParts.eq(0).after(object); - } - - var secondElement = splitParts.eq(1); - if (secondElement.length > 0 - && (this.isAlohaEditingP(secondElement[0]) - || this.isEmpty(secondElement[0]))) { - // Search for an element after the aditional split part, - // and if is founded, remove the second empty part - var parentsToEditable = secondElement.parents('.aloha-editor').get(), - hasNext = false; - - parentsToEditable.reverse().push(secondElement); - jQuery.each(parentsToEditable, function (index, elm) { - var element = jQuery(this); - if (element.next().length > 0) { - hasNext = true; - return false; // break - } - }); - if (hasNext) { - // append the new dom - secondElement.remove(); + // ASSERT(splitParts.length === 2) + var head = splitParts[0]; + var tail = splitParts[1]; + insertAfterSplit(head, object); + if (isPhantomNode(tail)) { + var afterTail = tail.nextSibling + || (tail.parentNode && tail.parentNode.firstChild); + if (findNodeForward(afterTail, isNotVisiblyEmpty)) { + // Because the tail element that is generated from + // the splitting is superfluous since there is + // already a visible element in which to place the + // selection. + jQuery(tail).remove(); } } - return true; } // could not split, so could not insert @@ -1217,19 +1352,6 @@ define(['jquery', 'util/class', 'aloha/ecma5shims'], function (jQuery, Class, $_ return false; }, - /** - * Checks if the element given is an aloha-editing-p helper, added by - * split - * - * @param {DOMObject} element Element to be checked - */ - isAlohaEditingP: function (element) { - return (element.className === 'aloha-editing-p' - && element.children.length === 1 - && element.children[0].nodeName.toLowerCase() === 'br' - && element.children[0].className === 'aloha-end-br'); - }, - /** * Remove the given DOM object from the DOM and modify the given range to reflect the user expected range after the object was removed * TODO: finish this