UNPKG

textangular

Version:

A radically powerful Text-Editor/Wysiwyg editor for Angular.js

952 lines (941 loc) 58.8 kB
angular.module('textAngular.DOM', ['textAngular.factories']) .factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){ var listToDefault = function(listElement, defaultWrap){ var $target, i; // if all selected then we should remove the list // grab all li elements and convert to taDefaultWrap tags var children = listElement.find('li'); for(i = children.length - 1; i >= 0; i--){ $target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>'); listElement.after($target); } listElement.remove(); taSelection.setSelectionToElementEnd($target[0]); }; var listElementToSelfTag = function(list, listElement, selfTag, bDefault, defaultWrap){ var $target, i; // if all selected then we should remove the list // grab all li elements var priorElement; var nextElement; var children = list.find('li'); var foundIndex; for (i = 0; i<children.length; i++) { if (children[i].outerHTML === listElement[0].outerHTML) { // found it... foundIndex = i; if (i>0) { priorElement = children[i-1]; } if (i+1<children.length) { nextElement = children[i+1]; } break; } } //console.log('listElementToSelfTag', list, listElement, selfTag, bDefault, priorElement, nextElement); // un-list the listElement var html = ''; if (bDefault) { html += '<' + defaultWrap + '>' + listElement[0].innerHTML + '</' + defaultWrap + '>'; } else { html += '<' + taBrowserTag(selfTag) + '>'; html += '<li>' + listElement[0].innerHTML + '</li>'; html += '</' + taBrowserTag(selfTag) + '>'; } $target = angular.element(html); //console.log('$target', $target[0]); if (!priorElement) { // this is the first the list, so we just remove it... listElement.remove(); list.after(angular.element(list[0].outerHTML)); list.after($target); list.remove(); taSelection.setSelectionToElementEnd($target[0]); return; } else if (!nextElement) { // this is the last in the list, so we just remove it.. listElement.remove(); list.after($target); taSelection.setSelectionToElementEnd($target[0]); } else { var p = list.parent(); // okay it was some where in the middle... so we need to break apart the list... var html1 = ''; var listTag = list[0].nodeName.toLowerCase(); html1 += '<' + listTag + '>'; for(i = 0; i < foundIndex; i++){ html1 += '<li>' + children[i].innerHTML + '</li>'; } html1 += '</' + listTag + '>'; var html2 = ''; html2 += '<' + listTag + '>'; for(i = foundIndex+1; i < children.length; i++){ html2 += '<li>' + children[i].innerHTML + '</li>'; } html2 += '</' + listTag + '>'; //console.log(html1, $target[0], html2); list.after(angular.element(html2)); list.after($target); list.after(angular.element(html1)); list.remove(); //console.log('parent ******XXX*****', p[0]); taSelection.setSelectionToElementEnd($target[0]); } }; var listElementsToSelfTag = function(list, listElements, selfTag, bDefault, defaultWrap){ var $target, i, j, p; // grab all li elements var priorElement; var afterElement; //console.log('list:', list, 'listElements:', listElements, 'selfTag:', selfTag, 'bDefault:', bDefault); var children = list.find('li'); var foundIndexes = []; for (i = 0; i<children.length; i++) { for (j = 0; j<listElements.length; j++) { if (children[i].isEqualNode(listElements[j])) { // found it... foundIndexes[j] = i; } } } if (foundIndexes[0] > 0) { priorElement = children[foundIndexes[0] - 1]; } if (foundIndexes[listElements.length-1] + 1 < children.length) { afterElement = children[foundIndexes[listElements.length-1] + 1]; } //console.log('listElementsToSelfTag', list, listElements, selfTag, bDefault, !priorElement, !afterElement, foundIndexes[listElements.length-1], children.length); // un-list the listElements var html = ''; if (bDefault) { for (j = 0; j < listElements.length; j++) { html += '<' + defaultWrap + '>' + listElements[j].innerHTML + '</' + defaultWrap + '>'; listElements[j].remove(); } } else { html += '<' + taBrowserTag(selfTag) + '>'; for (j = 0; j < listElements.length; j++) { html += listElements[j].outerHTML; listElements[j].remove(); } html += '</' + taBrowserTag(selfTag) + '>'; } $target = angular.element(html); if (!priorElement) { // this is the first the list, so we just remove it... list.after(angular.element(list[0].outerHTML)); list.after($target); list.remove(); taSelection.setSelectionToElementEnd($target[0]); return; } else if (!afterElement) { // this is the last in the list, so we just remove it.. list.after($target); taSelection.setSelectionToElementEnd($target[0]); return; } else { // okay it was some where in the middle... so we need to break apart the list... var html1 = ''; var listTag = list[0].nodeName.toLowerCase(); html1 += '<' + listTag + '>'; for(i = 0; i < foundIndexes[0]; i++){ html1 += '<li>' + children[i].innerHTML + '</li>'; } html1 += '</' + listTag + '>'; var html2 = ''; html2 += '<' + listTag + '>'; for(i = foundIndexes[listElements.length-1]+1; i < children.length; i++){ html2 += '<li>' + children[i].innerHTML + '</li>'; } html2 += '</' + listTag + '>'; list.after(angular.element(html2)); list.after($target); list.after(angular.element(html1)); list.remove(); //console.log('parent ******YYY*****', list.parent()[0]); taSelection.setSelectionToElementEnd($target[0]); } }; var selectLi = function(liElement){ if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]); else taSelection.setSelectionToElementEnd(liElement); }; var listToList = function(listElement, newListTag){ var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>'); listElement.after($target); listElement.remove(); selectLi($target.find('li')[0]); }; var childElementsToList = function(elements, listElement, newListTag){ var html = ''; for(var i = 0; i < elements.length; i++){ html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>'; } var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>'); listElement.after($target); listElement.remove(); selectLi($target.find('li')[0]); }; var turnBlockIntoBlocks = function(element, options) { for(var i = 0; i<element.childNodes.length; i++) { var _n = element.childNodes[i]; /* istanbul ignore next - more complex testing*/ if (_n.tagName && _n.tagName.match(BLOCKELEMENTS)) { turnBlockIntoBlocks(_n, options); } } /* istanbul ignore next - very rare condition that we do not test*/ if (element.parentNode === null) { // nothing left to do.. return element; } /* istanbul ignore next - not sure have to test this */ if (options === '<br>'){ return element; } else { var $target = angular.element(options); $target[0].innerHTML = element.innerHTML; element.parentNode.insertBefore($target[0], element); element.parentNode.removeChild(element); return $target; } }; return function(taDefaultWrap, topNode){ // NOTE: here we are dealing with the html directly from the browser and not the html the user sees. // IF you want to modify the html the user sees, do it when the user does a switchView taDefaultWrap = taBrowserTag(taDefaultWrap); return function(command, showUI, options, defaultTagAttributes){ var i, $target, html, _nodes, next, optionsTagName, selectedElement, ourSelection; var defaultWrapper = angular.element('<' + taDefaultWrap + '>'); try{ if (taSelection.getSelection) { ourSelection = taSelection.getSelection(); } selectedElement = taSelection.getSelectionElement(); // special checks and fixes when we are selecting the whole container var __h, _innerNode; /* istanbul ignore next */ if (selectedElement.tagName !== undefined) { if (selectedElement.tagName.toLowerCase() === 'div' && /taTextElement.+/.test(selectedElement.id) && ourSelection && ourSelection.start && ourSelection.start.offset === 1 && ourSelection.end.offset === 1) { // opps we are actually selecting the whole container! //console.log('selecting whole container!'); __h = selectedElement.innerHTML; if (/<br>/i.test(__h)) { // Firefox adds <br>'s and so we remove the <br> __h = __h.replace(/<br>/i, '&#8203;'); // no space-space } if (/<br\/>/i.test(__h)) { // Firefox adds <br/>'s and so we remove the <br/> __h = __h.replace(/<br\/>/i, '&#8203;'); // no space-space } // remove stacked up <span>'s if (/<span>(<span>)+/i.test(__h)) { __h = __.replace(/<span>(<span>)+/i, '<span>'); } // remove stacked up </span>'s if (/<\/span>(<\/span>)+/i.test(__h)) { __h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>'); } if (/<span><\/span>/i.test(__h)) { // if we end up with a <span></span> here we remove it... __h = __h.replace(/<span><\/span>/i, ''); } //console.log('inner whole container', selectedElement.childNodes); _innerNode = '<div>' + __h + '</div>'; selectedElement.innerHTML = _innerNode; taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]); selectedElement = taSelection.getSelectionElement(); } else if (selectedElement.tagName.toLowerCase() === 'span' && ourSelection && ourSelection.start && ourSelection.start.offset === 1 && ourSelection.end.offset === 1) { // just a span -- this is a problem... //console.log('selecting span!'); __h = selectedElement.innerHTML; if (/<br>/i.test(__h)) { // Firefox adds <br>'s and so we remove the <br> __h = __h.replace(/<br>/i, '&#8203;'); // no space-space } if (/<br\/>/i.test(__h)) { // Firefox adds <br/>'s and so we remove the <br/> __h = __h.replace(/<br\/>/i, '&#8203;'); // no space-space } // remove stacked up <span>'s if (/<span>(<span>)+/i.test(__h)) { __h = __.replace(/<span>(<span>)+/i, '<span>'); } // remove stacked up </span>'s if (/<\/span>(<\/span>)+/i.test(__h)) { __h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>'); } if (/<span><\/span>/i.test(__h)) { // if we end up with a <span></span> here we remove it... __h = __h.replace(/<span><\/span>/i, ''); } //console.log('inner span', selectedElement.childNodes); // we wrap this in a <div> because otherwise the browser get confused when we attempt to select the whole node // and the focus is not set correctly no matter what we do _innerNode = '<div>' + __h + '</div>'; selectedElement.innerHTML = _innerNode; taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]); selectedElement = taSelection.getSelectionElement(); //console.log(selectedElement.innerHTML); } else if (selectedElement.tagName.toLowerCase() === 'p' && ourSelection && ourSelection.start && ourSelection.start.offset === 1 && ourSelection.end.offset === 1) { //console.log('p special'); // we need to remove the </br> that firefox adds! __h = selectedElement.innerHTML; if (/<br>/i.test(__h)) { // Firefox adds <br>'s and so we remove the <br> __h = __h.replace(/<br>/i, '&#8203;'); // no space-space selectedElement.innerHTML = __h; } } else if (selectedElement.tagName.toLowerCase() === 'li' && ourSelection && ourSelection.start && ourSelection.start.offset === ourSelection.end.offset) { // we need to remove the </br> that firefox adds! __h = selectedElement.innerHTML; if (/<br>/i.test(__h)) { // Firefox adds <br>'s and so we remove the <br> __h = __h.replace(/<br>/i, ''); // nothing selectedElement.innerHTML = __h; } } } }catch(e){} //console.log('************** selectedElement:', selectedElement); /* istanbul ignore if: */ if (!selectedElement){return;} var $selected = angular.element(selectedElement); var tagName = (selectedElement && selectedElement.tagName && selectedElement.tagName.toLowerCase()) || /* istanbul ignore next: */ ""; if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){ var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul'); var selectedElements = taSelection.getOnlySelectedElements(); //console.log('PPPPPPPPPPPPP', tagName, selfTag, selectedElements, tagName.match(BLOCKELEMENTS), $selected.hasClass('ta-bind'), $selected.parent()[0].tagName); if (selectedElements.length>1 && (tagName === 'ol' || tagName === 'ul' )) { return listElementsToSelfTag($selected, selectedElements, selfTag, selfTag===tagName, taDefaultWrap); } if(tagName === selfTag){ // if all selected then we should remove the list // grab all li elements and convert to taDefaultWrap tags //console.log('tagName===selfTag'); if ($selected[0].childNodes.length !== selectedElements.length && selectedElements.length===1) { $selected = angular.element(selectedElements[0]); return listElementToSelfTag($selected.parent(), $selected, selfTag, true, taDefaultWrap); } else { return listToDefault($selected, taDefaultWrap); } }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){ // catch for the previous statement if only one li exists return listToDefault($selected.parent(), taDefaultWrap); }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){ // catch for the previous statement if only one li exists return listToList($selected.parent(), selfTag); }else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){ // if it's one of those block elements we have to change the contents // if it's a ol/ul we are changing from one to the other if (selectedElements.length) { if ($selected[0].childNodes.length !== selectedElements.length && selectedElements.length===1) { //console.log('&&&&&&&&&&&&&&& --------- &&&&&&&&&&&&&&&&', selectedElements[0], $selected[0].childNodes); $selected = angular.element(selectedElements[0]); return listElementToSelfTag($selected.parent(), $selected, selfTag, selfTag===tagName, taDefaultWrap); } } if(tagName === 'ol' || tagName === 'ul'){ // now if this is a set of selected elements... behave diferently return listToList($selected, selfTag); }else{ var childBlockElements = false; angular.forEach($selected.children(), function(elem){ if(elem.tagName.match(BLOCKELEMENTS)) { childBlockElements = true; } }); if(childBlockElements){ return childElementsToList($selected.children(), $selected, selfTag); }else{ return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag); } } }else if(tagName.match(BLOCKELEMENTS)){ // if we get here then the contents of the ta-bind are selected _nodes = taSelection.getOnlySelectedElements(); //console.log('_nodes', _nodes, tagName); if(_nodes.length === 0){ // here is if there is only text in ta-bind ie <div ta-bind>test content</div> $target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>'); $selected.html(''); $selected.append($target); }else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){ if(_nodes[0].tagName.toLowerCase() === selfTag){ // remove return listToDefault(angular.element(_nodes[0]), taDefaultWrap); }else{ return listToList(angular.element(_nodes[0]), selfTag); } }else{ html = ''; var $nodes = []; for(i = 0; i < _nodes.length; i++){ /* istanbul ignore else: catch for real-world can't make it occur in testing */ if(_nodes[i].nodeType !== 3){ var $n = angular.element(_nodes[i]); /* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */ if(_nodes[i].tagName.toLowerCase() === 'li') continue; else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){ html += $n[0].innerHTML; // if it's a list, add all it's children }else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){ html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children }else{ html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>'; } $nodes.unshift($n); } } //console.log('$nodes', $nodes); $target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>'); $nodes.pop().replaceWith($target); angular.forEach($nodes, function($node){ $node.remove(); }); } taSelection.setSelectionToElementEnd($target[0]); return; } }else if(command.toLowerCase() === 'formatblock'){ optionsTagName = options.toLowerCase().replace(/[<>]/ig, ''); if(optionsTagName.trim() === 'default') { optionsTagName = taDefaultWrap; options = '<' + taDefaultWrap + '>'; } if(tagName === 'li') { $target = $selected.parent(); } else { $target = $selected; } // find the first blockElement while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){ $target = $target.parent(); /* istanbul ignore next */ tagName = ($target[0].tagName || '').toLowerCase(); } if(tagName === optionsTagName){ // $target is wrap element _nodes = $target.children(); var hasBlock = false; for(i = 0; i < _nodes.length; i++){ hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS); } if(hasBlock){ $target.after(_nodes); next = $target.next(); $target.remove(); $target = next; }else{ defaultWrapper.append($target[0].childNodes); $target.after(defaultWrapper); $target.remove(); $target = defaultWrapper; } }else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){ //unwrap logic for parent var blockElement = $target.parent(); var contents = blockElement.contents(); for(i = 0; i < contents.length; i ++){ /* istanbul ignore next: can't test - some wierd thing with how phantomjs works */ if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){ defaultWrapper = angular.element('<' + taDefaultWrap + '>'); defaultWrapper[0].innerHTML = contents[i].outerHTML; contents[i] = defaultWrapper[0]; } blockElement.parent()[0].insertBefore(contents[i], blockElement[0]); } blockElement.remove(); }else if(tagName.match(LISTELEMENTS)){ // wrapping a list element $target.wrap(options); }else{ // default wrap behaviour _nodes = taSelection.getOnlySelectedElements(); if(_nodes.length === 0) { // no nodes at all.... _nodes = [$target[0]]; } // find the parent block element if any of the nodes are inline or text for(i = 0; i < _nodes.length; i++){ if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){ while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){ _nodes[i] = _nodes[i].parentNode; } } } // remove any duplicates from the array of _nodes! _nodes = _nodes.filter(function(value, index, self) { return self.indexOf(value) === index; }); // remove all whole taTextElement if it is here... unless it is the only element! if (_nodes.length>1) { _nodes = _nodes.filter(function (value, index, self) { return !(value.nodeName.toLowerCase() === 'div' && /^taTextElement/.test(value.id)); }); } if(angular.element(_nodes[0]).hasClass('ta-bind')){ $target = angular.element(options); $target[0].innerHTML = _nodes[0].innerHTML; _nodes[0].innerHTML = $target[0].outerHTML; }else if(optionsTagName === 'blockquote'){ // blockquotes wrap other block elements html = ''; for(i = 0; i < _nodes.length; i++){ html += _nodes[i].outerHTML; } $target = angular.element(options); $target[0].innerHTML = html; _nodes[0].parentNode.insertBefore($target[0],_nodes[0]); for(i = _nodes.length - 1; i >= 0; i--){ /* istanbul ignore else: */ if (_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]); } } else /* istanbul ignore next: not tested since identical to blockquote */ if (optionsTagName === 'pre' && taSelection.getStateShiftKey()) { //console.log('shift pre', _nodes); // pre wrap other block elements html = ''; for (i = 0; i < _nodes.length; i++) { html += _nodes[i].outerHTML; } $target = angular.element(options); $target[0].innerHTML = html; _nodes[0].parentNode.insertBefore($target[0], _nodes[0]); for (i = _nodes.length - 1; i >= 0; i--) { /* istanbul ignore else: */ if (_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]); } } else { //console.log(optionsTagName, _nodes); // regular block elements replace other block elements for (i = 0; i < _nodes.length; i++) { var newBlock = turnBlockIntoBlocks(_nodes[i], options); if (_nodes[i] === $target[0]) { $target = angular.element(newBlock); } } } } taSelection.setSelectionToElementEnd($target[0]); // looses focus when we have the whole container selected and no text! // refocus on the shown display element, this fixes a bug when using firefox $target[0].focus(); return; }else if(command.toLowerCase() === 'createlink'){ /* istanbul ignore next: firefox specific fix */ if (tagName === 'a') { // already a link!!! we are just replacing it... taSelection.getSelectionElement().href = options; return; } var tagBegin = '<a href="' + options + '" target="' + (defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') + '">', tagEnd = '</a>', _selection = taSelection.getSelection(); if(_selection.collapsed){ //console.log('collapsed'); // insert text at selection, then select then just let normal exec-command run taSelection.insertHtml(tagBegin + options + tagEnd, topNode); }else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){ var node = angular.element(tagBegin + tagEnd)[0]; rangy.getSelection().getRangeAt(0).surroundContents(node); } return; }else if(command.toLowerCase() === 'inserthtml'){ //console.log('inserthtml'); taSelection.insertHtml(options, topNode); return; } try{ $document[0].execCommand(command, showUI, options); }catch(e){} }; }; }]).service('taSelection', ['$document', 'taDOM', '$log', /* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */ function($document, taDOM, $log){ // need to dereference the document else the calls don't work correctly var _document = $document[0]; var bShiftState; 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; try { // catch any errors from rangy and ignore the issue range = rangy.getSelection().getRangeAt(0); } catch(e) { //console.info(e); return undefined; } var container = range.commonAncestorContainer; var selection = { start: brException(range.startContainer, range.startOffset), end: brException(range.endContainer, range.endOffset), collapsed: range.collapsed }; // This has problems under Firefox. // On Firefox with // <p>Try me !</p> // <ul> // <li>line 1</li> // <li>line 2</li> // </ul> // <p>line 3</p> // <ul> // <li>line 4</li> // <li>line 5</li> // </ul> // <p>Hello textAngular</p> // WITH the cursor after the 3 on line 3, it gets the commonAncestorContainer as: // <TextNode textContent='line 3'> // AND Chrome gets the commonAncestorContainer as: // <p>line 3</p> // // Check if the container is a text node and return its parent if so // unless this is the whole taTextElement. If so we return the textNode if (container.nodeType === 3) { if (container.parentNode.nodeName.toLowerCase() === 'div' && /^taTextElement/.test(container.parentNode.id)) { // textNode where the parent is the whole <div>!!! //console.log('textNode ***************** container:', container); } else { container = container.parentNode; } } if (container.nodeName.toLowerCase() === 'div' && /^taTextElement/.test(container.id)) { //console.log('*********taTextElement************'); //console.log('commonAncestorContainer:', container); selection.start.element = container.childNodes[selection.start.offset]; selection.end.element = container.childNodes[selection.end.offset]; selection.container = container; } else { if (container.parentNode === selection.start.element || container.parentNode === selection.end.element) { selection.container = container.parentNode; } else { selection.container = container; } } //console.log('***selection container:', selection.container.nodeName, selection.start.offset, selection.container); return selection; }, // if we use the LEFT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one... // Chrome and Firefox behave differently so so fix this for Firefox here. No adjustment needed for Chrome. updateLeftArrowKey: function(element) { var range = rangy.getSelection().getRangeAt(0); if (range && range.collapsed) { var _nodes = api.getFlattenedDom(range); if (!_nodes.findIndex) return; var _node = range.startContainer; var indexStartContainer = _nodes.findIndex(function(element, index){ if (element.node===_node) return true; var _indexp = element.parents.indexOf(_node); return (_indexp !== -1); }); var m; var nextNodeToRight; //console.log('indexStartContainer', indexStartContainer, _nodes.length, 'startContainer:', _node, _node === _nodes[indexStartContainer].node); _nodes.forEach(function (n, i) { //console.log(i, n.node); n.parents.forEach(function (nn, j){ //console.log(i, j, nn); }); }); if (indexStartContainer+1 < _nodes.length) { // we need the node just after this startContainer // so we can check and see it this is a special place nextNodeToRight = _nodes[indexStartContainer+1].node; //console.log(nextNodeToRight, range.startContainer); } //console.log('updateLeftArrowKey', range.startOffset, range.startContainer.textContent); // this first section handles the case for Chrome browser // if the first character of the nextNode is a \ufeff we know that we are just before the special span... // and so we most left by one character if (nextNodeToRight && nextNodeToRight.textContent) { m = /^\ufeff([^\ufeff]*)$/.exec(nextNodeToRight.textContent); if (m) { // we are before the special node with begins with a \ufeff character //console.log('LEFT ...found it...', 'startOffset:', range.startOffset, m[0].length, m[1].length); // no need to change anything in this case return; } } var nextNodeToLeft; if (indexStartContainer > 0) { // we need the node just after this startContainer // so we can check and see it this is a special place nextNodeToLeft = _nodes[indexStartContainer-1].node; //console.log(nextNodeToLeft, nextNodeToLeft); } if (range.startOffset === 0 && nextNodeToLeft) { //console.log(nextNodeToLeft, range.startOffset, nextNodeToLeft.textContent); m = /^\ufeff([^\ufeff]*)$/.exec(nextNodeToLeft.textContent); if (m) { //console.log('LEFT &&&&&&&&&&&&&&&&&&&...found it...&&&&&&&&&&&', nextNodeToLeft, m[0].length, m[1].length); // move over to the left my one -- Firefox triggers this case api.setSelectionToElementEnd(nextNodeToLeft); return; } } } return; }, // if we use the RIGHT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one... updateRightArrowKey: function(element) { // we do not need to make any adjustments here, so we ignore all this code if (false) { var range = rangy.getSelection().getRangeAt(0); if (range && range.collapsed) { var _nodes = api.getFlattenedDom(range); if (!_nodes.findIndex) return; var _node = range.startContainer; var indexStartContainer = _nodes.findIndex(function (element, index) { if (element.node === _node) return true; var _indexp = element.parents.indexOf(_node); return (_indexp !== -1); }); var _sel; var i; var m; // if the last character is a \ufeff we know that we are just before the special span... // and so we most right by one character var indexFound = _nodes.findIndex(function (n, index) { if (n.textContent) { var m = /^\ufeff([^\ufeff]*)$/.exec(n.textContent); if (m) { return true; } else { return false; } } else { return false; } }); if (indexFound === -1) { return; } //console.log(indexFound, range.startContainer, range.startOffset); _node = _nodes[indexStartContainer]; //console.log('indexStartContainer', indexStartContainer); if (_node && _node.textContent) { m = /^\ufeff([^\ufeff]*)$/.exec(_node.textContent); if (m && range.startOffset - 1 === m[1].length) { //console.log('RIGHT found it...&&&&&&&&&&&', range.startOffset); // no need to make any adjustment return; } } //console.log(range.startOffset); if (_nodes && range.startOffset === 0) { indexStartContainer = _nodes.indexOf(range.startContainer); if (indexStartContainer !== -1 && indexStartContainer > 0) { _node = _nodes[indexStartContainer - 1]; if (_node.textContent) { m = /\ufeff([^\ufeff]*)$/.exec(_node.textContent); if (m && true || range.startOffset === m[1].length + 1) { //console.log('RIGHT &&&&&&&&&&&&&&&&&&&...found it...&&&&&&&&&&&', range.startOffset, m[1].length); // no need to make any adjustment return; } } } } } } }, getFlattenedDom: function(range) { var parent = range.commonAncestorContainer.parentNode; if (!parent) { return range.commonAncestorContainer.childNodes; } var nodes = Array.prototype.slice.call(parent.childNodes); // converts NodeList to Array var indexStartContainer = nodes.indexOf(range.startContainer); // make sure that we have a big enough set of nodes if (indexStartContainer+1 < nodes.length && indexStartContainer > 0) { // we are good // we can go down one node or up one node } else { if (parent.parentNode) { parent = parent.parentNode; } } // now walk the parent nodes = []; function addNodes(_set) { if (_set.node.childNodes.length) { var childNodes = Array.prototype.slice.call(_set.node.childNodes); // converts NodeList to Array childNodes.forEach(function(n) { var _t = _set.parents.slice(); if (_t.slice(-1)[0]!==_set.node) { _t.push(_set.node); } addNodes({parents: _t, node: n}); }); } else { nodes.push({parents: _set.parents, node: _set.node}); } } addNodes({parents: [parent], node: parent}); return nodes; }, getOnlySelectedElements: function(){ var range = rangy.getSelection().getRangeAt(0); var container = range.commonAncestorContainer; // Node.TEXT_NODE === 3 // Node.ELEMENT_NODE === 1 // Node.COMMENT_NODE === 8 // Check if the container is a text node and return its parent if so container = container.nodeType === 3 ? container.parentNode : container; // get the nodes in the range that are ELEMENT_NODE and are children of the container // in this range... return range.getNodes([1], function(node){ return node.parentNode === container; }); }, // this includes the container element if all children are selected getAllSelectedElements: function(){ var range = rangy.getSelection().getRangeAt(0); var container = range.commonAncestorContainer; // Node.TEXT_NODE === 3 // Node.ELEMENT_NODE === 1 // Node.COMMENT_NODE === 8 // Check if the container is a text node and return its parent if so container = container.nodeType === 3 ? container.parentNode : container; // get the nodes in the range that are ELEMENT_NODE and are children of the container // in this range... var selectedNodes = range.getNodes([1], function(node){ return node.parentNode === container; }); var innerHtml = container.innerHTML; // remove the junk that rangy has put down innerHtml = innerHtml.replace(/<span id=.selectionBoundary[^>]+>\ufeff?<\/span>/ig, ''); //console.log(innerHtml); //console.log(range.toHtml()); //console.log(innerHtml === range.toHtml()); if (innerHtml === range.toHtml() && // not the whole taTextElement (!(container.nodeName.toLowerCase() === 'div' && /^taTextElement/.test(container.id))) ) { var arr = []; for(var i = selectedNodes.length; i--; arr.unshift(selectedNodes[i])); selectedNodes = arr; selectedNodes.push(container); //$log.debug(selectedNodes); } return selectedNodes; }, // Some basic selection functions getSelectionElement: function () { var s = api.getSelection(); if (s) { return api.getSelection().container; } else { return undefined; } }, setSelection: function(elStart, elEnd, start, end){ var range = rangy.createRange(); range.setStart(elStart, start); range.setEnd(elEnd, end); rangy.getSelection().setSingleRange(range); }, setSelectionBeforeElement: function (el){ var range = rangy.createRange(); range.selectNode(el); range.collapse(true); rangy.getSelection().setSingleRange(range); }, setSelectionAfterElement: function (el){ var range = rangy.createRange(); range.selectNode(el); range.collapse(false); rangy.getSelection().setSingleRange(range); }, setSelectionToElementStart: function (el){ var range = rangy.createRange(); range.selectNodeContents(el); range.collapse(true); rangy.getSelection().setSingleRange(range); }, setSelectionToElementEnd: function (el){ var range = rangy.createRange(); range.selectNodeContents(el); range.collapse(false); if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){ range.startOffset = range.endOffset = range.startOffset - 1; } rangy.getSelection().setSingleRange(range); }, setStateShiftKey: function(bS) { bShiftState = bS; }, getStateShiftKey: function() { return bShiftState; }, // 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, 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. // If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture). nodes = []; for(_childI = 0; _childI < children.length; _childI++){ var _cnode = children[_childI]; if (_cnode.nodeName.toLowerCase() === 'p' && _cnode.innerHTML.trim() === '') { // empty p element continue; } /**************** * allow any text to be inserted... if(( _cnode.nodeType === 3 && _cnode.nodeValue === '\ufeff'[0] && _cnode.nodeValue.trim() === '') // empty no-space space element ) { // no change to is