Skip to content

Commit 7003b27

Browse files
SimeonCSimeonC
SimeonC
authored and
SimeonC
committedFeb 5, 2015
fix(taSelection): Fix a bug in insert HTML.
Also added in the taDOM directive for better reuse.
1 parent 106bfb6 commit 7003b27

File tree

1 file changed

+143
-49
lines changed

1 file changed

+143
-49
lines changed
 

‎lib/DOM.js

+143-49
Original file line numberDiff line numberDiff line change
@@ -222,31 +222,48 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
222222
}catch(e){}
223223
};
224224
};
225-
}]).service('taSelection', ['$window', '$document',
225+
}]).service('taSelection', ['$window', '$document', 'taDOM',
226226
/* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
227-
function($window, $document){
227+
function($window, $document, taDOM){
228228
// need to dereference the document else the calls don't work correctly
229229
var _document = $document[0];
230230
var rangy = $window.rangy;
231+
var brException = function (element, offset) {
232+
/* check if selection is a BR element at the beginning of a container. If so, get
233+
* the parentNode instead.
234+
* offset should be zero in this case. Otherwise, return the original
235+
* element.
236+
*/
237+
if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
238+
return {
239+
element: element.parentNode,
240+
offset: 0
241+
};
242+
} else {
243+
return {
244+
element: element,
245+
offset: offset
246+
};
247+
}
248+
};
231249
var api = {
232250
getSelection: function(){
233251
var range = rangy.getSelection().getRangeAt(0);
234252
var container = range.commonAncestorContainer;
235-
// Check if the container is a text node and return its parent if so
236-
container = container.nodeType === 3 ? container.parentNode : container;
237-
return {
238-
start: {
239-
element: range.startContainer,
240-
offset: range.startOffset
241-
},
242-
end: {
243-
element: range.endContainer,
244-
offset: range.endOffset
245-
},
246-
container: container,
253+
var selection = {
254+
start: brException(range.startContainer, range.startOffset),
255+
end: brException(range.endContainer, range.endOffset),
247256
collapsed: range.collapsed
248-
249257
};
258+
// Check if the container is a text node and return its parent if so
259+
container = container.nodeType === 3 ? container.parentNode : container;
260+
if (container.parentNode === selection.start.element ||
261+
container.parentNode === selection.end.element) {
262+
selection.container = container.parentNode;
263+
} else {
264+
selection.container = container;
265+
}
266+
return selection;
250267
},
251268
getOnlySelectedElements: function(){
252269
var range = rangy.getSelection().getRangeAt(0);
@@ -304,12 +321,13 @@ function($window, $document){
304321
// from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
305322
// topNode is the contenteditable normally, all manipulation MUST be inside this.
306323
insertHtml: function(html, topNode){
307-
var parent, secondParent, _childI, nodes, startIndex, startNodes, endNodes, i, lastNode;
324+
var parent, secondParent, _childI, nodes, startIndex, startNodes, endNodes, i, lastNode, _tempFrag;
308325
var element = angular.element("<div>" + html + "</div>");
309326
var range = rangy.getSelection().getRangeAt(0);
310327
var frag = _document.createDocumentFragment();
311328
var children = element[0].childNodes;
312329
var isInline = true;
330+
313331
if(children.length > 0){
314332
// NOTE!! We need to do the following:
315333
// 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){
336354
if(isInline){
337355
range.deleteContents();
338356
}else{ // not inline insert
339-
if(range.collapsed && range.startContainer !== topNode && range.startContainer.parentNode !== topNode){
340-
// split element into 2 and insert block element in middle
341-
if(range.startContainer.nodeType === 3){ // if text node
342-
parent = range.startContainer.parentNode;
343-
nodes = parent.childNodes;
344-
// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
345-
startNodes = [];
346-
endNodes = [];
347-
for(startIndex = 0; startIndex < nodes.length; startIndex++){
348-
startNodes.push(nodes[startIndex]);
349-
if(nodes[startIndex] === range.startContainer) break;
357+
if(range.collapsed && range.startContainer !== topNode){
358+
if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
359+
// this log is to catch when innerHTML is something like `<img ...>`
360+
parent = range.startContainer;
361+
if(range.startOffset === 1){
362+
// before single tag
363+
range.setStartAfter(parent);
364+
range.setEndAfter(parent);
365+
}else{
366+
// after single tag
367+
range.setStartBefore(parent);
368+
range.setEndBefore(parent);
350369
}
351-
endNodes.push(_document.createTextNode(range.startContainer.nodeValue.substring(range.startOffset)));
352-
range.startContainer.nodeValue = range.startContainer.nodeValue.substring(0, range.startOffset);
353-
for(i = startIndex + 1; i < nodes.length; i++) endNodes.push(nodes[i]);
354-
355-
secondParent = parent.cloneNode();
356-
parent.childNodes = startNodes;
357-
secondParent.childNodes = endNodes;
358370
}else{
359-
parent = range.startContainer;
360-
secondParent = parent.cloneNode();
361-
secondParent.innerHTML = parent.innerHTML.substring(range.startOffset);
362-
parent.innerHTML = parent.innerHTML.substring(0, range.startOffset);
363-
}
364-
angular.element(parent).after(secondParent);
365-
// put cursor to end of inserted content
366-
range.setStartAfter(parent);
367-
range.setEndAfter(parent);
368-
if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
369-
range.setStartBefore(parent);
370-
range.setEndBefore(parent);
371-
angular.element(parent).remove();
371+
// split element into 2 and insert block element in middle
372+
if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
373+
parent = range.startContainer.parentNode;
374+
secondParent = parent.cloneNode();
375+
// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
376+
taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);
377+
378+
// Escape out of the inline tags like b
379+
while(!VALIDELEMENTS.test(parent.nodeName)){
380+
angular.element(parent).after(secondParent);
381+
parent = parent.parentNode;
382+
var _lastSecondParent = secondParent;
383+
secondParent = parent.cloneNode();
384+
// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
385+
taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
386+
}
387+
}else{
388+
parent = range.startContainer;
389+
secondParent = parent.cloneNode();
390+
taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
391+
}
392+
393+
angular.element(parent).after(secondParent);
394+
// put cursor to end of inserted content
395+
range.setStartAfter(parent);
396+
range.setEndAfter(parent);
397+
398+
if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
399+
range.setStartBefore(parent);
400+
range.setEndBefore(parent);
401+
angular.element(parent).remove();
402+
}
403+
if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
404+
if(parent.nodeName.toLowerCase() === 'li'){
405+
_tempFrag = _document.createDocumentFragment();
406+
for(i = 0; i < frag.childNodes.length; i++){
407+
element = angular.element('<li>');
408+
taDOM.transferChildNodes(frag.childNodes[i], element[0]);
409+
taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
410+
_tempFrag.appendChild(element[0]);
411+
}
412+
frag = _tempFrag;
413+
if(lastNode){
414+
lastNode = frag.childNodes[frag.childNodes.length - 1];
415+
lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
416+
}
417+
}
372418
}
373-
if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
374419
}else{
375420
range.deleteContents();
376421
}
@@ -382,4 +427,53 @@ function($window, $document){
382427
}
383428
};
384429
return api;
385-
}]);
430+
}]).service('taDOM', function(){
431+
var taDOM = {
432+
// recursive function that returns an array of angular.elements that have the passed attribute set on them
433+
getByAttribute: function(element, attribute){
434+
var resultingElements = [];
435+
var childNodes = element.children();
436+
if(childNodes.length){
437+
angular.forEach(childNodes, function(child){
438+
resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
439+
});
440+
}
441+
if(element.attr(attribute) !== undefined) resultingElements.push(element);
442+
return resultingElements;
443+
},
444+
445+
transferChildNodes: function(source, target){
446+
// clear out target
447+
target.innerHTML = '';
448+
while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
449+
return target;
450+
},
451+
452+
splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
453+
if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
454+
var startNodes = document.createDocumentFragment();
455+
var endNodes = document.createDocumentFragment();
456+
var index = 0;
457+
458+
while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
459+
startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
460+
index++;
461+
}
462+
463+
if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
464+
startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
465+
nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
466+
}
467+
while(nodes.length > 0) endNodes.appendChild(nodes[0]);
468+
469+
taDOM.transferChildNodes(startNodes, target1);
470+
taDOM.transferChildNodes(endNodes, target2);
471+
},
472+
473+
transferNodeAttributes: function(source, target){
474+
for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
475+
return target;
476+
}
477+
};
478+
return taDOM;
479+
});

0 commit comments

Comments
 (0)