From 7003b27bb110a5c7f0909e77f5c033fe35766fb0 Mon Sep 17 00:00:00 2001 From: SimeonC Date: Thu, 5 Feb 2015 13:16:50 +1300 Subject: [PATCH] fix(taSelection): Fix a bug in insert HTML. Also added in the taDOM directive for better reuse. --- lib/DOM.js | 192 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 143 insertions(+), 49 deletions(-) diff --git a/lib/DOM.js b/lib/DOM.js index 0f3e2447..2749fa44 100644 --- a/lib/DOM.js +++ b/lib/DOM.js @@ -222,31 +222,48 @@ angular.module('textAngular.DOM', ['textAngular.factories']) }catch(e){} }; }; -}]).service('taSelection', ['$window', '$document', +}]).service('taSelection', ['$window', '$document', 'taDOM', /* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */ -function($window, $document){ +function($window, $document, taDOM){ // need to dereference the document else the calls don't work correctly var _document = $document[0]; var rangy = $window.rangy; + var brException = function (element, offset) { + /* check if selection is a BR element at the beginning of a container. If so, get + * the parentNode instead. + * offset should be zero in this case. Otherwise, return the original + * element. + */ + if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) { + return { + element: element.parentNode, + offset: 0 + }; + } else { + return { + element: element, + offset: offset + }; + } + }; var api = { getSelection: function(){ var range = rangy.getSelection().getRangeAt(0); var container = range.commonAncestorContainer; - // Check if the container is a text node and return its parent if so - container = container.nodeType === 3 ? container.parentNode : container; - return { - start: { - element: range.startContainer, - offset: range.startOffset - }, - end: { - element: range.endContainer, - offset: range.endOffset - }, - container: container, + var selection = { + start: brException(range.startContainer, range.startOffset), + end: brException(range.endContainer, range.endOffset), collapsed: range.collapsed - }; + // Check if the container is a text node and return its parent if so + container = container.nodeType === 3 ? container.parentNode : container; + if (container.parentNode === selection.start.element || + container.parentNode === selection.end.element) { + selection.container = container.parentNode; + } else { + selection.container = container; + } + return selection; }, getOnlySelectedElements: function(){ var range = rangy.getSelection().getRangeAt(0); @@ -304,12 +321,13 @@ function($window, $document){ // from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div // topNode is the contenteditable normally, all manipulation MUST be inside this. insertHtml: function(html, topNode){ - var parent, secondParent, _childI, nodes, startIndex, startNodes, endNodes, i, lastNode; + var parent, secondParent, _childI, nodes, startIndex, startNodes, endNodes, i, lastNode, _tempFrag; var element = angular.element("
" + html + "
"); var range = rangy.getSelection().getRangeAt(0); var frag = _document.createDocumentFragment(); var children = element[0].childNodes; var isInline = true; + if(children.length > 0){ // NOTE!! We need to do the following: // check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between. @@ -336,41 +354,68 @@ function($window, $document){ if(isInline){ range.deleteContents(); }else{ // not inline insert - if(range.collapsed && range.startContainer !== topNode && range.startContainer.parentNode !== topNode){ - // split element into 2 and insert block element in middle - if(range.startContainer.nodeType === 3){ // if text node - parent = range.startContainer.parentNode; - nodes = parent.childNodes; - // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes. - startNodes = []; - endNodes = []; - for(startIndex = 0; startIndex < nodes.length; startIndex++){ - startNodes.push(nodes[startIndex]); - if(nodes[startIndex] === range.startContainer) break; + if(range.collapsed && range.startContainer !== topNode){ + if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){ + // this log is to catch when innerHTML is something like `` + parent = range.startContainer; + if(range.startOffset === 1){ + // before single tag + range.setStartAfter(parent); + range.setEndAfter(parent); + }else{ + // after single tag + range.setStartBefore(parent); + range.setEndBefore(parent); } - endNodes.push(_document.createTextNode(range.startContainer.nodeValue.substring(range.startOffset))); - range.startContainer.nodeValue = range.startContainer.nodeValue.substring(0, range.startOffset); - for(i = startIndex + 1; i < nodes.length; i++) endNodes.push(nodes[i]); - - secondParent = parent.cloneNode(); - parent.childNodes = startNodes; - secondParent.childNodes = endNodes; }else{ - parent = range.startContainer; - secondParent = parent.cloneNode(); - secondParent.innerHTML = parent.innerHTML.substring(range.startOffset); - parent.innerHTML = parent.innerHTML.substring(0, range.startOffset); - } - angular.element(parent).after(secondParent); - // put cursor to end of inserted content - range.setStartAfter(parent); - range.setEndAfter(parent); - if(/^(|)$/i.test(parent.innerHTML.trim())){ - range.setStartBefore(parent); - range.setEndBefore(parent); - angular.element(parent).remove(); + // split element into 2 and insert block element in middle + if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node + parent = range.startContainer.parentNode; + secondParent = parent.cloneNode(); + // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes. + taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset); + + // Escape out of the inline tags like b + while(!VALIDELEMENTS.test(parent.nodeName)){ + angular.element(parent).after(secondParent); + parent = parent.parentNode; + var _lastSecondParent = secondParent; + secondParent = parent.cloneNode(); + // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes. + taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent); + } + }else{ + parent = range.startContainer; + secondParent = parent.cloneNode(); + taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset); + } + + angular.element(parent).after(secondParent); + // put cursor to end of inserted content + range.setStartAfter(parent); + range.setEndAfter(parent); + + if(/^(|)$/i.test(parent.innerHTML.trim())){ + range.setStartBefore(parent); + range.setEndBefore(parent); + angular.element(parent).remove(); + } + if(/^(|)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove(); + if(parent.nodeName.toLowerCase() === 'li'){ + _tempFrag = _document.createDocumentFragment(); + for(i = 0; i < frag.childNodes.length; i++){ + element = angular.element('
  • '); + taDOM.transferChildNodes(frag.childNodes[i], element[0]); + taDOM.transferNodeAttributes(frag.childNodes[i], element[0]); + _tempFrag.appendChild(element[0]); + } + frag = _tempFrag; + if(lastNode){ + lastNode = frag.childNodes[frag.childNodes.length - 1]; + lastNode = lastNode.childNodes[lastNode.childNodes.length - 1]; + } + } } - if(/^(|)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove(); }else{ range.deleteContents(); } @@ -382,4 +427,53 @@ function($window, $document){ } }; return api; -}]); \ No newline at end of file +}]).service('taDOM', function(){ + var taDOM = { + // recursive function that returns an array of angular.elements that have the passed attribute set on them + getByAttribute: function(element, attribute){ + var resultingElements = []; + var childNodes = element.children(); + if(childNodes.length){ + angular.forEach(childNodes, function(child){ + resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute)); + }); + } + if(element.attr(attribute) !== undefined) resultingElements.push(element); + return resultingElements; + }, + + transferChildNodes: function(source, target){ + // clear out target + target.innerHTML = ''; + while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]); + return target; + }, + + splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){ + if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex'); + var startNodes = document.createDocumentFragment(); + var endNodes = document.createDocumentFragment(); + var index = 0; + + while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){ + startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object. + index++; + } + + if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){ + startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex))); + nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex); + } + while(nodes.length > 0) endNodes.appendChild(nodes[0]); + + taDOM.transferChildNodes(startNodes, target1); + taDOM.transferChildNodes(endNodes, target2); + }, + + transferNodeAttributes: function(source, target){ + for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value); + return target; + } + }; + return taDOM; +}); \ No newline at end of file