UNPKG

textangular

Version:

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

844 lines (802 loc) 70.7 kB
angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM']) .service('_taBlankTest', [function(){ return function(_blankVal){ // we radically restructure this code. // what was here before was incredibly fragile. // What we do now is to check that the html is non-blank visually // which we check by looking at html->text if(!_blankVal) return true; // find first non-tag match - ie start of string or after tag that is not whitespace // var t0 = performance.now(); // Takes a small fraction of a mSec to do this... var _text_ = stripHtmlToText(_blankVal); // var t1 = performance.now(); // console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:'); if (_text_=== '') { // img generates a visible item so it is not blank! if (/<img[^>]+>/.test(_blankVal)) { return false; } return true; } else { return false; } }; }]) .directive('taButton', [function(){ return { link: function(scope, element, attrs){ element.attr('unselectable', 'on'); element.on('mousedown', function(e, eventData){ /* istanbul ignore else: this is for catching the jqLite testing*/ if(eventData) angular.extend(e, eventData); // this prevents focusout from firing on the editor when clicking toolbar buttons e.preventDefault(); return false; }); } }; }]) .directive('taBind', [ 'taSanitize', '$timeout', '$document', 'taFixChrome', 'taBrowserTag', 'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions', '_taBlankTest', '$parse', 'taDOM', 'textAngularManager', function( taSanitize, $timeout, $document, taFixChrome, taBrowserTag, taSelection, taSelectableElements, taApplyCustomRenderers, taOptions, _taBlankTest, $parse, taDOM, textAngularManager){ // Uses for this are textarea or input with ng-model and ta-bind='text' // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model return { priority: 2, // So we override validators correctly require: ['ngModel','?ngModelOptions'], link: function(scope, element, attrs, controller){ var ngModel = controller[0]; var ngModelOptions = controller[1] || {}; // the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly. var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable'); var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'; var _isReadonly = false; var _focussed = false; var _skipRender = false; var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer; var _keepStyles = attrs.taKeepStyles || taOptions.keepStyles; var _lastKey; // see http://www.javascripter.net/faq/keycodes.htm for good information // NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox) // BLOCKED_KEYS are special keys... // Tab, pause/break, CapsLock, Esc, Page Up, End, Home, // Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete, // f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12 // NumLock, ScrollLock var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i; // UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation // Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox), // Numpad +, Numpad -, (; :), (= +), // (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ") // NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i; var _pasteHandler; // defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element // non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct... var _defaultVal, _defaultTest; var _CTRL_KEY = 0x0001; var _META_KEY = 0x0002; var _ALT_KEY = 0x0004; var _SHIFT_KEY = 0x0008; // KEYCODEs we use var _ENTER_KEYCODE = 13; var _SHIFT_KEYCODE = 16; var _TAB_KEYCODE = 9; var _LEFT_ARROW_KEYCODE = 37; var _RIGHT_ARROW_KEYCODE = 39; // map events to special keys... // mappings is an array of maps from events to specialKeys as declared in textAngularSetup var _keyMappings = [ // ctrl/command + z { specialKey: 'UndoKey', forbiddenModifiers: _ALT_KEY + _SHIFT_KEY, mustHaveModifiers: [_META_KEY + _CTRL_KEY], keyCode: 90 }, // ctrl/command + shift + z { specialKey: 'RedoKey', forbiddenModifiers: _ALT_KEY, mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY], keyCode: 90 }, // ctrl/command + y { specialKey: 'RedoKey', forbiddenModifiers: _ALT_KEY + _SHIFT_KEY, mustHaveModifiers: [_META_KEY + _CTRL_KEY], keyCode: 89 }, // TabKey { specialKey: 'TabKey', forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY, mustHaveModifiers: [], keyCode: _TAB_KEYCODE }, // shift + TabKey { specialKey: 'ShiftTabKey', forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY, mustHaveModifiers: [_SHIFT_KEY], keyCode: _TAB_KEYCODE } ]; function _mapKeys(event) { var specialKey; _keyMappings.forEach(function (map){ if (map.keyCode === event.keyCode) { var netModifiers = (event.metaKey ? _META_KEY: 0) + (event.ctrlKey ? _CTRL_KEY: 0) + (event.shiftKey ? _SHIFT_KEY: 0) + (event.altKey ? _ALT_KEY: 0); if (map.forbiddenModifiers & netModifiers) return; if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){ specialKey = map.specialKey; } } }); return specialKey; } // set the default to be a paragraph value if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p'; /* istanbul ignore next: ie specific test */ if(attrs.taDefaultWrap === ''){ _defaultVal = ''; _defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>'; }else{ _defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)? (attrs.taDefaultWrap.toLowerCase() === 'br' ? '<BR><BR>' : '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>') : (_browserDetect.ie <= 8)? '<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' : '<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>'; _defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)? (attrs.taDefaultWrap.toLowerCase() === 'br' ? '<br><br>' : '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>') : (_browserDetect.ie <= 8)? '<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' : '<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>'; } /* istanbul ignore else */ if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support var _ensureContentWrapped = function(value) { if (_taBlankTest(value)) return value; var domTest = angular.element("<div>" + value + "</div>"); //console.log('domTest.children().length():', domTest.children().length); //console.log('_ensureContentWrapped', domTest.children()); //console.log(value, attrs.taDefaultWrap); if (domTest.children().length === 0) { // if we have a <br> and the attrs.taDefaultWrap is a <p> we need to remove the <br> //value = value.replace(/<br>/i, ''); value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">"; } else { var _children = domTest[0].childNodes; var i; var _foundBlockElement = false; for (i = 0; i < _children.length; i++) { if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break; } if (!_foundBlockElement) { value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">"; } else{ value = ""; for(i = 0; i < _children.length; i++){ var node = _children[i]; var nodeName = node.nodeName.toLowerCase(); //console.log('node#:', i, 'name:', nodeName); if(nodeName === '#comment') { value += '<!--' + node.nodeValue + '-->'; } else if(nodeName === '#text') { // determine if this is all whitespace, if so, we will leave it as it is. // otherwise, we will wrap it as it is var text = node.textContent; if (!text.trim()) { // just whitespace value += text; } else { // not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to. value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">"; } } else if(!nodeName.match(BLOCKELEMENTS)){ /* istanbul ignore next: Doesn't seem to trigger on tests */ var _subVal = (node.outerHTML || node.nodeValue); /* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */ if(_subVal.trim() !== '') value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">"; else value += _subVal; } else { value += node.outerHTML; } //console.log(value); } } } //console.log(value); return value; }; if(attrs.taPaste) { _pasteHandler = $parse(attrs.taPaste); } element.addClass('ta-bind'); var _undoKeyupTimeout; scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = { _stack: [], _index: 0, _max: 1000, push: function(value){ if((typeof value === "undefined" || value === null) || ((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value; if(this._index < this._stack.length - 1){ this._stack = this._stack.slice(0,this._index+1); } this._stack.push(value); if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout); if(this._stack.length > this._max) this._stack.shift(); this._index = this._stack.length - 1; return value; }, undo: function(){ return this.setToIndex(this._index-1); }, redo: function(){ return this.setToIndex(this._index+1); }, setToIndex: function(index){ if(index < 0 || index > this._stack.length - 1){ return undefined; } this._index = index; return this.current(); }, current: function(){ return this._stack[this._index]; } }; // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code. var _compileHtml = function(){ if(_isContentEditable) { return element[0].innerHTML; } if(_isInputFriendly) { return element.val(); } throw ('textAngular Error: attempting to update non-editable taBind'); }; var selectorClickHandler = function(event){ // emit the element-select event, pass the element scope.$emit('ta-element-select', this); event.preventDefault(); return false; }; //used for updating when inserting wrapped elements var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){ /* istanbul ignore else */ if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){ // check we don't apply the handler twice element.find(selector) .off('click', selectorClickHandler) .on('click', selectorClickHandler); }); }; var _setViewValue = function(_val, triggerUndo, skipRender){ _skipRender = skipRender || false; if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine if(typeof _val === "undefined" || _val === null) _val = _compileHtml(); if(_taBlankTest(_val)){ // this avoids us from tripping the ng-pristine flag if we click in and out with out typing if(ngModel.$viewValue !== '') ngModel.$setViewValue(''); if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push(''); }else{ _reApplyOnSelectorHandlers(); if(ngModel.$viewValue !== _val){ ngModel.$setViewValue(_val); if(triggerUndo) ngModel.$undoManager.push(_val); } } ngModel.$render(); }; var _setInnerHTML = function(newval){ element[0].innerHTML = newval; }; var _redoUndoTimeout; var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){ /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */ if(!_isReadonly && _isContentEditable){ var content = ngModel.$undoManager.undo(); if(typeof content !== "undefined" && content !== null){ _setInnerHTML(content); _setViewValue(content, false); if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout); _redoUndoTimeout = $timeout(function(){ element[0].focus(); taSelection.setSelectionToElementEnd(element[0]); }, 1); } } }; var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){ /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */ if(!_isReadonly && _isContentEditable){ var content = ngModel.$undoManager.redo(); if(typeof content !== "undefined" && content !== null){ _setInnerHTML(content); _setViewValue(content, false); /* istanbul ignore next */ if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout); _redoUndoTimeout = $timeout(function(){ element[0].focus(); taSelection.setSelectionToElementEnd(element[0]); }, 1); } } }; //used for updating when inserting wrapped elements scope['updateTaBind' + (attrs.id || '')] = function(){ if(!_isReadonly) _setViewValue(undefined, undefined, true); }; // catch DOM XSS via taSanitize // Sanitizing both ways is identical var _sanitize = function(unsafe){ return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe, _keepStyles), ngModel.$oldViewValue, _disableSanitizer)); }; // trigger the validation calls if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) { return !_taBlankTest(modelValue || viewValue); }; // parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel ngModel.$parsers.push(_sanitize); ngModel.$parsers.unshift(_ensureContentWrapped); // because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server ngModel.$formatters.push(_sanitize); ngModel.$formatters.unshift(_ensureContentWrapped); ngModel.$formatters.unshift(function(value){ return ngModel.$undoManager.push(value || ''); }); //this code is used to update the models when data is entered/deleted if(_isInputFriendly){ scope.events = {}; if(!_isContentEditable){ // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive element.on('change blur', scope.events.change = scope.events.blur = function(){ if(!_isReadonly) ngModel.$setViewValue(_compileHtml()); }); element.on('keydown', scope.events.keydown = function(event, eventData){ /* istanbul ignore else: this is for catching the jqLite testing*/ if(eventData) angular.extend(event, eventData); // Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea /* istanbul ignore else: otherwise normal functionality */ if(event.keyCode === _TAB_KEYCODE){ // tab was pressed // get caret position/selection var start = this.selectionStart; var end = this.selectionEnd; var value = element.val(); if(event.shiftKey){ // find \t var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start); if(_tab !== -1 && _tab >= _linebreak){ // set textarea value to: text before caret + tab + text after caret element.val(value.substring(0, _tab) + value.substring(_tab + 1)); // put caret at right position again (add one for the tab) this.selectionStart = this.selectionEnd = start - 1; } }else{ // set textarea value to: text before caret + tab + text after caret element.val(value.substring(0, start) + "\t" + value.substring(end)); // put caret at right position again (add one for the tab) this.selectionStart = this.selectionEnd = start + 1; } // prevent the focus lose event.preventDefault(); } }); var _repeat = function(string, n){ var result = ''; for(var _n = 0; _n < n; _n++) result += string; return result; }; // add a forEach function that will work on a NodeList, etc.. var forEach = function (array, callback, scope) { for (var i= 0; i<array.length; i++) { callback.call(scope, i, array[i]); } }; // handle <ul> or <ol> nodes var recursiveListFormat = function(listNode, tablevel){ var _html = ''; var _subnodes = listNode.childNodes; tablevel++; // tab out and add the <ul> or <ol> html piece _html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4); forEach(_subnodes, function (index, node) { /* istanbul ignore next: browser catch */ var nodeName = node.nodeName.toLowerCase(); if (nodeName === '#comment') { _html += '<!--' + node.nodeValue + '-->'; return; } if (nodeName === '#text') { _html += node.textContent; return; } /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */ if(!node.outerHTML) { // no html to add return; } if(nodeName === 'ul' || nodeName === 'ol') { _html += '\n' + recursiveListFormat(node, tablevel); } else { // no reformatting within this subnode, so just do the tabing... _html += '\n' + _repeat('\t', tablevel) + node.outerHTML; } }); // now add on the </ol> or </ul> piece _html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<')); return _html; }; // handle formating of something like: // <ol><!--First comment--> // <li>Test Line 1<!--comment test list 1--></li> // <ul><!--comment ul--> // <li>Nested Line 1</li> // <!--comment between nested lines--><li>Nested Line 2</li> // </ul> // <li>Test Line 3</li> // </ol> ngModel.$formatters.unshift(function(htmlValue){ // tabulate the HTML so it looks nicer // // first get a list of the nodes... // we do this by using the element parser... // // doing this -- which is simpiler -- breaks our tests... //var _nodes=angular.element(htmlValue); var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes; if(_nodes.length > 0){ // do the reformatting of the layout... htmlValue = ''; forEach(_nodes, function (index, node) { var nodeName = node.nodeName.toLowerCase(); if (nodeName === '#comment') { htmlValue += '<!--' + node.nodeValue + '-->'; return; } if (nodeName === '#text') { htmlValue += node.textContent; return; } /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */ if(!node.outerHTML) { // nothing to format! return; } if(htmlValue.length > 0) { // we aready have some content, so drop to a new line htmlValue += '\n'; } if(nodeName === 'ul' || nodeName === 'ol') { // okay a set of list stuff we want to reformat in a nested way htmlValue += '' + recursiveListFormat(node, 0); } else { // just use the original without any additional formating htmlValue += '' + node.outerHTML; } }); } return htmlValue; }); }else{ // all the code specific to contenteditable divs var _processingPaste = false; /* istanbul ignore next: phantom js cannot test this for some reason */ var processpaste = function(text) { var _isOneNote = text!==undefined? text.match(/content=["']*OneNote.File/i): false; /* istanbul ignore else: don't care if nothing pasted */ //console.log(text); if(text && text.trim().length){ // test paste from word/microsoft product if(text.match(/class=["']*Mso(Normal|List)/i) || text.match(/content=["']*Word.Document/i) || text.match(/content=["']*OneNote.File/i)){ var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i); if(!textFragment) textFragment = text; else textFragment = textFragment[1]; textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, ''); var dom = angular.element("<div>" + textFragment + "</div>"); var targetDom = angular.element("<div></div>"); var _list = { element: null, lastIndent: [], lastLi: null, isUl: false }; _list.lastIndent.peek = function(){ var n = this.length; if (n>0) return this[n-1]; }; var _resetList = function(isUl){ _list.isUl = isUl; _list.element = angular.element(isUl ? "<ul>" : "<ol>"); _list.lastIndent = []; _list.lastIndent.peek = function(){ var n = this.length; if (n>0) return this[n-1]; }; _list.lastLevelMatch = null; }; for(var i = 0; i <= dom[0].childNodes.length; i++){ if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text"){ continue; } else { var tagName = dom[0].childNodes[i].tagName.toLowerCase(); if(tagName !== 'p' && tagName !== 'ul' && tagName !== 'h1' && tagName !== 'h2' && tagName !== 'h3' && tagName !== 'h4' && tagName !== 'h5' && tagName !== 'h6' && tagName !== 'table'){ continue; } } var el = angular.element(dom[0].childNodes[i]); var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i); if(_listMatch){ if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){ continue; } var isUl = _listMatch[1].toLowerCase() === 'bullet' || (_listMatch[1].toLowerCase() !== 'number' && !(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].innerHTML) || /^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].childNodes[0].innerHTML))); var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i); var indent = parseFloat((_indentMatch)?_indentMatch[1]:0); var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i); // prefers the mso-list syntax if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]); if ((_levelMatch && (!_list.lastLevelMatch || _levelMatch[1] !== _list.lastLevelMatch[1])) || !_listMatch[3] || _listMatch[3].toLowerCase() === 'first' || (_list.lastIndent.peek() === null) || (_list.isUl !== isUl && _list.lastIndent.peek() === indent)) { _resetList(isUl); targetDom.append(_list.element); } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){ _list.element = angular.element(isUl ? '<ul>' : '<ol>'); _list.lastLi.append(_list.element); } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){ while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){ if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){ _list.element = _list.element.parent(); continue; }else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){ _list.element = _list.element.parent(); }else{ // else it's it should be a sibling break; } _list.lastIndent.pop(); } _list.isUl = _list.element[0].tagName.toLowerCase() === 'ul'; if (isUl !== _list.isUl) { _resetList(isUl); targetDom.append(_list.element); } } _list.lastLevelMatch = _levelMatch; if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent); _list.lastLi = angular.element('<li>'); _list.element.append(_list.lastLi); _list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, '')); el.remove(); }else{ _resetList(false); targetDom.append(el); } } var _unwrapElement = function(node){ node = angular.element(node); for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]); node.remove(); }; angular.forEach(targetDom.find('span'), function(node){ node.removeAttribute('lang'); if(node.attributes.length <= 0) _unwrapElement(node); }); angular.forEach(targetDom.find('font'), _unwrapElement); text = targetDom.html(); if(_isOneNote){ text = targetDom.html() || dom.html(); } // LF characters instead of spaces in some spots and they are replaced by '/n', so we need to just swap them to spaces text = text.replace(/\n/g, ' '); }else{ // remove unnecessary chrome insert text = text.replace(/<(|\/)meta[^>]*?>/ig, ''); if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){ // entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!! if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){ var _el = angular.element('<div>' + text + '</div>'); _el.find('textarea').remove(); for(var _b = 0; _b < binds.length; _b++){ var _target = binds[_b][0].parentNode.parentNode; for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){ _target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target); } _target.parentNode.removeChild(_target); } text = _el.html().replace('<br class="Apple-interchange-newline">', ''); } }else if(text.match(/^<span/)){ // in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting // if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing // on paste from even ourselves! if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) { text = text.replace(/<(|\/)span[^>]*?>/ig, ''); } } // Webkit on Apple tags text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;'); } if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) { // insert missing parent of li element text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>'); } // parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste text = text.replace(/^[ |\u00A0]+/gm, function (match) { var result = ''; for (var i = 0; i < match.length; i++) { result += '&nbsp;'; } return result; }).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;'); if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text; // turn span vertical-align:super into <sup></sup> text = text.replace(/<span style=("|')([^<]*?)vertical-align\s*:\s*super;?([^>]*?)("|')>([^<]+?)<\/span>/g, "<sup style='$2$3'>$5</sup>"); text = taSanitize(text, '', _disableSanitizer); //console.log('DONE\n', text); taSelection.insertHtml(text, element[0]); $timeout(function(){ ngModel.$setViewValue(_compileHtml()); _processingPaste = false; element.removeClass('processing-paste'); }, 0); }else{ _processingPaste = false; element.removeClass('processing-paste'); } }; element.on('paste', scope.events.paste = function(e, eventData){ /* istanbul ignore else: this is for catching the jqLite testing*/ if(eventData) angular.extend(e, eventData); if(_isReadonly || _processingPaste){ e.stopPropagation(); e.preventDefault(); return false; } // Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718 _processingPaste = true; element.addClass('processing-paste'); var pastedContent; var clipboardData = (e.originalEvent || e).clipboardData; /* istanbul ignore next: Handle legacy IE paste */ if ( !clipboardData && window.clipboardData && window.clipboardData.getData ){ pastedContent = window.clipboardData.getData("Text"); processpaste(pastedContent); e.stopPropagation(); e.preventDefault(); return false; } if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event var _types = ""; for(var _t = 0; _t < clipboardData.types.length; _t++){ _types += " " + clipboardData.types[_t]; } /* istanbul ignore next: browser tests */ if (/text\/html/i.test(_types)) { pastedContent = clipboardData.getData('text/html'); } else if (/text\/plain/i.test(_types)) { pastedContent = clipboardData.getData('text/plain'); } processpaste(pastedContent); e.stopPropagation(); e.preventDefault(); return false; } else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup var _savedSelection = rangy.saveSelection(), _tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>'); $document.find('body').append(_tempDiv); _tempDiv[0].focus(); $timeout(function(){ // restore selection rangy.restoreSelection(_savedSelection); processpaste(_tempDiv[0].innerHTML); element[0].focus(); _tempDiv.remove(); }, 0); } }); element.on('cut', scope.events.cut = function(e){ // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display if(!_isReadonly) $timeout(function(){ ngModel.$setViewValue(_compileHtml()); }, 0); else e.preventDefault(); }); element.on('keydown', scope.events.keydown = function(event, eventData){ /* istanbul ignore else: this is for catching the jqLite testing*/ if(eventData) angular.extend(event, eventData); if (event.keyCode === _SHIFT_KEYCODE) { taSelection.setStateShiftKey(true); } else { taSelection.setStateShiftKey(false); } event.specialKey = _mapKeys(event); var userSpecialKey; /* istanbul ignore next: difficult to test */ taOptions.keyMappings.forEach(function (mapping) { if (event.specialKey === mapping.commandKeyCode) { // taOptions has remapped this binding... so // we disable our own event.specialKey = undefined; } if (mapping.testForKey(event)) { userSpecialKey = mapping.commandKeyCode; } if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) { // this is necessary to fully stop the propagation. if (!mapping.enablePropagation) { event.preventDefault(); } } }); /* istanbul ignore next: difficult to test */ if (typeof userSpecialKey !== 'undefined') { event.specialKey = userSpecialKey; } /* istanbul ignore next: difficult to test as can't seem to select */ if ((typeof event.specialKey !== 'undefined') && ( event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey' )) { event.preventDefault(); textAngularManager.sendKeyCommand(scope, event); } /* istanbul ignore else: readonly check */ if(!_isReadonly){ if (event.specialKey==='UndoKey') { _undo(); event.preventDefault(); } if (event.specialKey==='RedoKey') { _redo(); event.preventDefault(); } /* istanbul ignore next: difficult to test as can't seem to select */ if(event.keyCode === _ENTER_KEYCODE && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { var contains = function(a, obj) { for (var i = 0; i < a.length; i++) { if (a[i] === obj) { return true; } } return false; }; var $selection; var selection = taSelection.getSelectionElement(); // shifted to nodeName here from tagName since it is more widely supported see: http://stackoverflow.com/questions/4878484/difference-between-tagname-and-nodename if(!selection.nodeName.match(VALIDELEMENTS)) return; var _new = angular.element(_defaultVal); // if we are in the last element of a blockquote, or ul or ol and the element is blank // we need to pull the element outside of the said type var moveOutsideElements = ['blockquote', 'ul', 'ol']; if (contains(moveOutsideElements, selection.parentNode.tagName.toLowerCase())) { if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && !selection.nextSibling) { // if last element is blank, pull element outside. $selection = angular.element(selection); var _parent = $selection.parent(); _parent.after(_new); $selection.remove(); if (_