textangular
Version:
A radically powerful Text-Editor/Wysiwyg editor for Angular.js
844 lines (802 loc) • 70.7 kB
JavaScript
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> </P>' : '<p> </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() + '> </' + attrs.taDefaultWrap.toUpperCase() + '>' :
'<' + attrs.taDefaultWrap + '> </' + 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">( | )<\/span>/ig, ' ');
}
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 += ' ';
}
return result;
}).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, ' ');
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 (_