
META-INF.resources.bower_components.textAngular.src.taBind.js Maven / Gradle / Ivy
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 (/
]+>/.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 '
', ie is '' 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) ? '
' : (_browserDetect.ie >= 11) ? '
' : (_browserDetect.ie <= 8) ? '
' : '
';
} else {
_defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11) ?
(attrs.taDefaultWrap.toLowerCase() === 'br' ? '
' : '<' + attrs.taDefaultWrap + '>
' + 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' ? '
' : '<' + attrs.taDefaultWrap + '>
' + 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("" + value + "");
//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
and the attrs.taDefaultWrap is a we need to remove the
//value = value.replace(/
/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 += '';
} 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
...
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 or nodes
var recursiveListFormat = function (listNode, tablevel) {
var _html = '';
var _subnodes = listNode.childNodes;
tablevel++;
// tab out and add the or 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 += '';
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
or
piece
_html += '\n' + _repeat('\t', tablevel - 1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
return _html;
};
// handle formating of something like:
//
// - Test Line 1
//
// - Nested Line 1
// - Nested Line 2
//
// - Test Line 3
//
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('' + htmlValue + '')[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 += '';
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(/([\s\S]*?)/i);
if (!textFragment) textFragment = text;
else textFragment = textFragment[1];
textFragment = textFragment.replace(/[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
var dom = angular.element("" + textFragment + "");
var targetDom = angular.element("");
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 ? "" : "");
_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<>]]' : '');
_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('- ');
_list.element.append(_list.lastLi);
_list.lastLi.html(el.html().replace(/[\s\S]*?/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('' + text + '');
_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('
', '');
}
} else if (text.match(/^ ' here we destroy the spacing
// on paste from even ourselves!
if (!text.match(/.<\/span>/ig)) {
text = text.replace(/<(|\/)span[^>]*?>/ig, '');
}
}
// Webkit on Apple tags
text = text.replace(/
]*?>/ig, '').replace(/( | )<\/span>/ig, ' ');
}
if (/- /i.test(text) && /(
|).*- /i.test(text) === false) {
// insert missing parent of li element
text = text.replace(/
- .*<\/li(\s.*)?>/i, '
$&
');
}
// 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, '
').replace(/\t/g, ' ');
if (_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
// turn span vertical-align:super into
text = text.replace(/]*?)("|')>([^<]+?)<\/span>/g, "$5");
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('');
$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 (/^
$/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 (_parent.children().length === 0) _parent.remove();
taSelection.setSelectionToElementStart(_new[0]);
event.preventDefault();
}
if (/^<[^>]+>
<\/[^>]+>$/i.test(selection.innerHTML.trim())) {
$selection = angular.element(selection);
$selection.after(_new);
$selection.remove();
taSelection.setSelectionToElementStart(_new[0]);
event.preventDefault();
}
}
}
}
});
var _keyupTimeout;
element.on('keyup', scope.events.keyup = function (event, eventData) {
/* istanbul ignore else: this is for catching the jqLite testing*/
if (eventData) angular.extend(event, eventData);
taSelection.setStateShiftKey(false); // clear the ShiftKey state
/* istanbul ignore next: FF specific bug fix */
if (event.keyCode === _TAB_KEYCODE) {
var _selection = taSelection.getSelection();
if (_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
return;
}
// we do this here during the 'keyup' so that the browser has already moved the slection by one character...
if (event.keyCode === _LEFT_ARROW_KEYCODE && !event.shiftKey) {
taSelection.updateLeftArrowKey(element);
}
// we do this here during the 'keyup' so that the browser has already moved the slection by one character...
if (event.keyCode === _RIGHT_ARROW_KEYCODE && !event.shiftKey) {
taSelection.updateRightArrowKey(element);
}
if (_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
if (!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)) {
/* istanbul ignore next: Ignore any _ENTER_KEYCODE that has ctrlKey, metaKey or alKey */
if (event.keyCode === _ENTER_KEYCODE && (event.ctrlKey || event.metaKey || event.altKey)) {
// we ignore any ENTER_ KEYCODE that is anything but plain or a shift one...
} else {
// if enter - insert new taDefaultWrap, if shift+enter insert
if (_defaultVal !== '' && _defaultVal !== '
' && event.keyCode === _ENTER_KEYCODE && !event.ctrlKey && !event.metaKey && !event.altKey) {
var selection = taSelection.getSelectionElement();
while (!selection.nodeName.match(VALIDELEMENTS) && selection !== element[0]) {
selection = selection.parentNode;
}
if (!event.shiftKey) {
// new paragraph, br should be caught correctly
// shifted to nodeName here from tagName since it is more widely supported see: http://stackoverflow.com/questions/4878484/difference-between-tagname-and-nodename
//console.log('Enter', selection.nodeName, attrs.taDefaultWrap, selection.innerHTML.trim());
if (selection.tagName.toLowerCase() !==
attrs.taDefaultWrap &&
selection.nodeName.toLowerCase() !== 'li' &&
(selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '
')
) {
// Chrome starts with a
after an EnterKey
// so we replace this with the _defaultVal
var _new = angular.element(_defaultVal);
angular.element(selection).replaceWith(_new);
taSelection.setSelectionToElementStart(_new[0]);
}
} else {
// shift + Enter
var tagName = selection.tagName.toLowerCase();
//console.log('Shift+Enter', selection.tagName, attrs.taDefaultWrap, selection.innerHTML.trim());
// For an LI: We see: LI p ....
// For a P: We see: P p ....
// on Safari, the browser ignores the Shift+Enter and acts just as an Enter Key
// For an LI: We see: LI p
// For a P: We see: P p
if ((tagName === attrs.taDefaultWrap ||
tagName === 'li' ||
tagName === 'pre' ||
tagName === 'div') &&
!/.+
/.test(selection.innerHTML.trim())) {
var ps = selection.previousSibling;
//console.log('wrong....', ps);
// we need to remove this selection and fix the previousSibling up...
if (ps) {
ps.innerHTML = ps.innerHTML + '
';
angular.element(selection).remove();
taSelection.setSelectionToElementEnd(ps);
}
}
}
}
var val = _compileHtml();
if (_defaultVal !== '' && (val.trim() === '' || val.trim() === '
')) {
_setInnerHTML(_defaultVal);
taSelection.setSelectionToElementStart(element.children()[0]);
} else if (val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== '') {
/* we no longer do this, since there can be comments here and white space
var _savedSelection = rangy.saveSelection();
val = _compileHtml();
val = "<" + attrs.taDefaultWrap + ">" + val + "" + attrs.taDefaultWrap + ">";
_setInnerHTML(val);
rangy.restoreSelection(_savedSelection);
*/
}
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
if (_keyupTimeout) $timeout.cancel(_keyupTimeout);
_keyupTimeout = $timeout(function () {
_setViewValue(val, triggerUndo, true);
}, ngModelOptions.$options.debounce || 400);
if (!triggerUndo) _undoKeyupTimeout = $timeout(function () {
ngModel.$undoManager.push(val);
}, 250);
_lastKey = event.keyCode;
}
}
});
// when there is a change from a spelling correction in the browser, the only
// change that is seen is a 'input' and the $watch('html') sees nothing... So
// we added this element.on('input') to catch this change and call the _setViewValue()
// so the ngModel is updated and all works as it should.
var _inputTimeout;
element.on('input', function () {
if (_compileHtml() !== ngModel.$viewValue) {
// we wait a time now to allow the natural $watch('html') to handle this change
// and then after a 1 second delay, if there is still a difference we will do the
// _setViewValue() call.
/* istanbul ignore if: can't test */
if (_inputTimeout) $timeout.cancel(_inputTimeout);
/* istanbul ignore next: cant' test? */
_inputTimeout = $timeout(function () {
var _savedSelection = rangy.saveSelection();
var _val = _compileHtml();
if (_val !== ngModel.$viewValue) {
//console.log('_setViewValue');
//console.log('old:', ngModel.$viewValue);
//console.log('new:', _val);
_setViewValue(_val, true);
}
// if the savedSelection marker is gone at this point, we cannot restore the selection!!!
//console.log('rangy.restoreSelection', ngModel.$viewValue.length, _savedSelection);
if (ngModel.$viewValue.length !== 0) {
rangy.restoreSelection(_savedSelection);
}
}, 1000);
}
});
element.on('blur', scope.events.blur = function () {
_focussed = false;
/* istanbul ignore else: if readonly don't update model */
if (!_isReadonly) {
_setViewValue(undefined, undefined, true);
} else {
_skipRender = true; // don't redo the whole thing, just check the placeholder logic
ngModel.$render();
}
});
// Placeholders not supported on ie 8 and below
if (attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)) {
var rule;
if (attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
else throw('textAngular Error: An unique ID is required for placeholders to work');
scope.$on('$destroy', function () {
removeCSSRule(rule);
});
}
element.on('focus', scope.events.focus = function () {
_focussed = true;
element.removeClass('placeholder-text');
_reApplyOnSelectorHandlers();
});
element.on('mouseup', scope.events.mouseup = function () {
var _selection = taSelection.getSelection();
if (_selection && _selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
});
// prevent propagation on mousedown in editor, see #206
element.on('mousedown', scope.events.mousedown = function (event, eventData) {
/* istanbul ignore else: this is for catching the jqLite testing*/
if (eventData) angular.extend(event, eventData);
event.stopPropagation();
});
}
}
var fileDropHandler = function (event, eventData) {
/* istanbul ignore else: this is for catching the jqLite testing*/
if (eventData) angular.extend(event, eventData);
// emit the drop event, pass the element, preventing should be done elsewhere
if (!dropFired && !_isReadonly) {
dropFired = true;
var dataTransfer;
if (event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
else dataTransfer = event.dataTransfer;
scope.$emit('ta-drop-event', this, event, dataTransfer);
$timeout(function () {
dropFired = false;
_setViewValue(undefined, undefined, true);
}, 100);
}
};
var _renderTimeout;
var _renderInProgress = false;
// changes to the model variable from outside the html/text inputs
ngModel.$render = function () {
/* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
if (_renderInProgress) return;
else _renderInProgress = true;
// catch model being null or undefined
var val = ngModel.$viewValue || '';
// if the editor isn't focused it needs to be updated, otherwise it's receiving user input
if (!_skipRender) {
/* istanbul ignore else: in other cases we don't care */
if (_isContentEditable && _focussed) {
// update while focussed
element.removeClass('placeholder-text');
/* istanbul ignore next: don't know how to test this */
if (_renderTimeout) $timeout.cancel(_renderTimeout);
_renderTimeout = $timeout(function () {
/* istanbul ignore if: Can't be bothered testing this... */
if (!_focussed) {
element[0].focus();
taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
}
_renderTimeout = undefined;
}, 1);
}
if (_isContentEditable) {
// WYSIWYG Mode
if (attrs.placeholder) {
if (val === '') {
// blank
_setInnerHTML(_defaultVal);
} else {
// not-blank
_setInnerHTML(val);
}
} else {
_setInnerHTML((val === '') ? _defaultVal : val);
}
// if in WYSIWYG and readOnly we kill the use of links by clicking
if (!_isReadonly) {
_reApplyOnSelectorHandlers();
element.on('drop', fileDropHandler);
} else {
element.off('drop', fileDropHandler);
}
} else if (element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input') {
// make sure the end user can SEE the html code as a display. This is a read-only display element
_setInnerHTML(taApplyCustomRenderers(val));
} else {
// only for input and textarea inputs
element.val(val);
}
}
if (_isContentEditable && attrs.placeholder) {
if (val === '') {
if (_focussed) element.removeClass('placeholder-text');
else element.addClass('placeholder-text');
} else {
element.removeClass('placeholder-text');
}
}
_renderInProgress = _skipRender = false;
};
if (attrs.taReadonly) {
//set initial value
_isReadonly = scope.$eval(attrs.taReadonly);
if (_isReadonly) {
element.addClass('ta-readonly');
// we changed to readOnly mode (taReadonly='true')
if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
element.attr('disabled', 'disabled');
}
if (element.attr('contenteditable') !== undefined && element.attr('contenteditable')) {
element.removeAttr('contenteditable');
}
} else {
element.removeClass('ta-readonly');
// we changed to NOT readOnly mode (taReadonly='false')
if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
element.removeAttr('disabled');
} else if (_isContentEditable) {
element.attr('contenteditable', 'true');
}
}
// taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
// Otherwise it is readonly by default
scope.$watch(attrs.taReadonly, function (newVal, oldVal) {
if (oldVal === newVal) return;
if (newVal) {
element.addClass('ta-readonly');
// we changed to readOnly mode (taReadonly='true')
if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
element.attr('disabled', 'disabled');
}
if (element.attr('contenteditable') !== undefined && element.attr('contenteditable')) {
element.removeAttr('contenteditable');
}
// turn ON selector click handlers
angular.forEach(taSelectableElements, function (selector) {
element.find(selector).on('click', selectorClickHandler);
});
element.off('drop', fileDropHandler);
} else {
element.removeClass('ta-readonly');
// we changed to NOT readOnly mode (taReadonly='false')
if (element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input') {
element.removeAttr('disabled');
} else if (_isContentEditable) {
element.attr('contenteditable', 'true');
}
// remove the selector click handlers
angular.forEach(taSelectableElements, function (selector) {
element.find(selector).off('click', selectorClickHandler);
});
element.on('drop', fileDropHandler);
}
_isReadonly = newVal;
});
}
// Initialise the selectableElements
// if in WYSIWYG and readOnly we kill the use of links by clicking
if (_isContentEditable && !_isReadonly) {
angular.forEach(taSelectableElements, function (selector) {
element.find(selector).on('click', selectorClickHandler);
});
element.on('drop', fileDropHandler);
}
}
};
}]);