Skip to content

Commit

Permalink
fix(taSelection): Fix a bug in insert HTML.
Browse files Browse the repository at this point in the history
Also added in the taDOM directive for better reuse.
  • Loading branch information
SimeonC authored and SimeonC committed Feb 5, 2015
1 parent 106bfb6 commit 7003b27
Showing 1 changed file with 143 additions and 49 deletions.
192 changes: 143 additions & 49 deletions lib/DOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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("<div>" + html + "</div>");
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.
Expand All @@ -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 `<img ...>`
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(/^(|<br(|\/)>)$/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(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
range.setStartBefore(parent);
range.setEndBefore(parent);
angular.element(parent).remove();
}
if(/^(|<br(|\/)>)$/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('<li>');
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(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
}else{
range.deleteContents();
}
Expand All @@ -382,4 +427,53 @@ function($window, $document){
}
};
return api;
}]);
}]).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;
});

0 comments on commit 7003b27

Please sign in to comment.