taBind.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
  2. .service('_taBlankTest', [function(){
  3. var INLINETAGS_NONBLANK = /<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;
  4. return function(_defaultTest){
  5. return function(_blankVal){
  6. if(!_blankVal) return true;
  7. // find first non-tag match - ie start of string or after tag that is not whitespace
  8. var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
  9. var _firstTagIndex;
  10. if(!_firstMatch){
  11. // find the end of the first tag removing all the
  12. // Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
  13. _blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
  14. _firstTagIndex = _blankVal.indexOf('>');
  15. }else{
  16. _firstTagIndex = _firstMatch.index;
  17. }
  18. _blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
  19. // check for no tags entry
  20. if(/^[^<>]+$/i.test(_blankVal)) return false;
  21. // this regex is to match any number of whitespace only between two tags
  22. if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s|&nbsp;)*<\/[^>]+>$/ig.test(_blankVal)) return true;
  23. // this regex tests if there is a tag followed by some optional whitespace and some text after that
  24. else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
  25. else return true;
  26. };
  27. };
  28. }])
  29. .directive('taButton', [function(){
  30. return {
  31. link: function(scope, element, attrs){
  32. element.attr('unselectable', 'on');
  33. element.on('mousedown', function(e, eventData){
  34. /* istanbul ignore else: this is for catching the jqLite testing*/
  35. if(eventData) angular.extend(e, eventData);
  36. // this prevents focusout from firing on the editor when clicking toolbar buttons
  37. e.preventDefault();
  38. return false;
  39. });
  40. }
  41. };
  42. }])
  43. .directive('taBind', [
  44. 'taSanitize', '$timeout', '$window', '$document', 'taFixChrome', 'taBrowserTag',
  45. 'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
  46. '_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
  47. function(
  48. taSanitize, $timeout, $window, $document, taFixChrome, taBrowserTag,
  49. taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
  50. _taBlankTest, $parse, taDOM, textAngularManager){
  51. // Uses for this are textarea or input with ng-model and ta-bind='text'
  52. // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
  53. return {
  54. priority: 2, // So we override validators correctly
  55. require: ['ngModel','?ngModelOptions'],
  56. link: function(scope, element, attrs, controller){
  57. var ngModel = controller[0];
  58. var ngModelOptions = controller[1] || {};
  59. // the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
  60. var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
  61. var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
  62. var _isReadonly = false;
  63. var _focussed = false;
  64. var _skipRender = false;
  65. var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
  66. var _lastKey;
  67. // see http://www.javascripter.net/faq/keycodes.htm for good information
  68. // NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
  69. // BLOCKED_KEYS are special keys...
  70. // Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
  71. // Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
  72. // f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
  73. // NumLock, ScrollLock
  74. 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;
  75. // UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
  76. // Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
  77. // Numpad +, Numpad -, (; :), (= +),
  78. // (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
  79. // NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
  80. 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;
  81. var _pasteHandler;
  82. // defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
  83. // non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
  84. var _defaultVal, _defaultTest;
  85. var _CTRL_KEY = 0x0001;
  86. var _META_KEY = 0x0002;
  87. var _ALT_KEY = 0x0004;
  88. var _SHIFT_KEY = 0x0008;
  89. // map events to special keys...
  90. // mappings is an array of maps from events to specialKeys as declared in textAngularSetup
  91. var _keyMappings = [
  92. // ctrl/command + z
  93. {
  94. specialKey: 'UndoKey',
  95. forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  96. mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  97. keyCode: 90
  98. },
  99. // ctrl/command + shift + z
  100. {
  101. specialKey: 'RedoKey',
  102. forbiddenModifiers: _ALT_KEY,
  103. mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
  104. keyCode: 90
  105. },
  106. // ctrl/command + y
  107. {
  108. specialKey: 'RedoKey',
  109. forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  110. mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  111. keyCode: 89
  112. },
  113. // TabKey
  114. {
  115. specialKey: 'TabKey',
  116. forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
  117. mustHaveModifiers: [],
  118. keyCode: 9
  119. },
  120. // shift + TabKey
  121. {
  122. specialKey: 'ShiftTabKey',
  123. forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
  124. mustHaveModifiers: [_SHIFT_KEY],
  125. keyCode: 9
  126. }
  127. ];
  128. function _mapKeys(event) {
  129. var specialKey;
  130. _keyMappings.forEach(function (map){
  131. if (map.keyCode === event.keyCode) {
  132. var netModifiers = (event.metaKey ? _META_KEY: 0) +
  133. (event.ctrlKey ? _CTRL_KEY: 0) +
  134. (event.shiftKey ? _SHIFT_KEY: 0) +
  135. (event.altKey ? _ALT_KEY: 0);
  136. if (map.forbiddenModifiers & netModifiers) return;
  137. if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){
  138. specialKey = map.specialKey;
  139. }
  140. }
  141. });
  142. return specialKey;
  143. }
  144. // set the default to be a paragraph value
  145. if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
  146. /* istanbul ignore next: ie specific test */
  147. if(attrs.taDefaultWrap === ''){
  148. _defaultVal = '';
  149. _defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
  150. }else{
  151. _defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  152. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  153. (_browserDetect.ie <= 8)?
  154. '<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
  155. '<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
  156. _defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  157. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  158. (_browserDetect.ie <= 8)?
  159. '<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
  160. '<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
  161. }
  162. /* istanbul ignore else */
  163. if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support
  164. var _blankTest = _taBlankTest(_defaultTest);
  165. var _ensureContentWrapped = function(value) {
  166. if (_blankTest(value)) return value;
  167. var domTest = angular.element("<div>" + value + "</div>");
  168. //console.log('domTest.children().length():', domTest.children().length);
  169. if (domTest.children().length === 0) {
  170. value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  171. } else {
  172. var _children = domTest[0].childNodes;
  173. var i;
  174. var _foundBlockElement = false;
  175. for (i = 0; i < _children.length; i++) {
  176. if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
  177. }
  178. if (!_foundBlockElement) {
  179. value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  180. }
  181. else{
  182. value = "";
  183. for(i = 0; i < _children.length; i++){
  184. var node = _children[i];
  185. var nodeName = node.nodeName.toLowerCase();
  186. //console.log(nodeName);
  187. if(nodeName === '#comment') {
  188. value += '<!--' + node.nodeValue + '-->';
  189. } else if(nodeName === '#text') {
  190. // determine if this is all whitespace, if so, we will leave it as it is.
  191. // otherwise, we will wrap it as it is
  192. var text = node.textContent;
  193. if (!text.trim()) {
  194. // just whitespace
  195. value += text;
  196. } else {
  197. // not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
  198. value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
  199. }
  200. } else if(!nodeName.match(BLOCKELEMENTS)){
  201. /* istanbul ignore next: Doesn't seem to trigger on tests */
  202. var _subVal = (node.outerHTML || node.nodeValue);
  203. /* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
  204. if(_subVal.trim() !== '')
  205. value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
  206. else value += _subVal;
  207. } else {
  208. value += node.outerHTML;
  209. }
  210. }
  211. }
  212. }
  213. //console.log(value);
  214. return value;
  215. };
  216. if(attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);
  217. element.addClass('ta-bind');
  218. var _undoKeyupTimeout;
  219. scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
  220. _stack: [],
  221. _index: 0,
  222. _max: 1000,
  223. push: function(value){
  224. if((typeof value === "undefined" || value === null) ||
  225. ((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
  226. if(this._index < this._stack.length - 1){
  227. this._stack = this._stack.slice(0,this._index+1);
  228. }
  229. this._stack.push(value);
  230. if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  231. if(this._stack.length > this._max) this._stack.shift();
  232. this._index = this._stack.length - 1;
  233. return value;
  234. },
  235. undo: function(){
  236. return this.setToIndex(this._index-1);
  237. },
  238. redo: function(){
  239. return this.setToIndex(this._index+1);
  240. },
  241. setToIndex: function(index){
  242. if(index < 0 || index > this._stack.length - 1){
  243. return undefined;
  244. }
  245. this._index = index;
  246. return this.current();
  247. },
  248. current: function(){
  249. return this._stack[this._index];
  250. }
  251. };
  252. var _redoUndoTimeout;
  253. var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
  254. /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  255. if(!_isReadonly && _isContentEditable){
  256. var content = ngModel.$undoManager.undo();
  257. if(typeof content !== "undefined" && content !== null){
  258. _setInnerHTML(content);
  259. _setViewValue(content, false);
  260. if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  261. _redoUndoTimeout = $timeout(function(){
  262. element[0].focus();
  263. taSelection.setSelectionToElementEnd(element[0]);
  264. }, 1);
  265. }
  266. }
  267. };
  268. var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
  269. /* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  270. if(!_isReadonly && _isContentEditable){
  271. var content = ngModel.$undoManager.redo();
  272. if(typeof content !== "undefined" && content !== null){
  273. _setInnerHTML(content);
  274. _setViewValue(content, false);
  275. /* istanbul ignore next */
  276. if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  277. _redoUndoTimeout = $timeout(function(){
  278. element[0].focus();
  279. taSelection.setSelectionToElementEnd(element[0]);
  280. }, 1);
  281. }
  282. }
  283. };
  284. // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
  285. var _compileHtml = function(){
  286. if(_isContentEditable) return element[0].innerHTML;
  287. if(_isInputFriendly) return element.val();
  288. throw ('textAngular Error: attempting to update non-editable taBind');
  289. };
  290. var _setViewValue = function(_val, triggerUndo, skipRender){
  291. _skipRender = skipRender || false;
  292. if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
  293. if(typeof _val === "undefined" || _val === null) _val = _compileHtml();
  294. if(_blankTest(_val)){
  295. // this avoids us from tripping the ng-pristine flag if we click in and out with out typing
  296. if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
  297. if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
  298. }else{
  299. _reApplyOnSelectorHandlers();
  300. if(ngModel.$viewValue !== _val){
  301. ngModel.$setViewValue(_val);
  302. if(triggerUndo) ngModel.$undoManager.push(_val);
  303. }
  304. }
  305. ngModel.$render();
  306. };
  307. //used for updating when inserting wrapped elements
  308. scope['updateTaBind' + (attrs.id || '')] = function(){
  309. if(!_isReadonly) _setViewValue(undefined, undefined, true);
  310. };
  311. // catch DOM XSS via taSanitize
  312. // Sanitizing both ways is identical
  313. var _sanitize = function(unsafe){
  314. return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
  315. };
  316. // trigger the validation calls
  317. if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
  318. return !_blankTest(modelValue || viewValue);
  319. };
  320. // parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
  321. ngModel.$parsers.push(_sanitize);
  322. ngModel.$parsers.unshift(_ensureContentWrapped);
  323. // because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
  324. ngModel.$formatters.push(_sanitize);
  325. ngModel.$formatters.unshift(_ensureContentWrapped);
  326. ngModel.$formatters.unshift(function(value){
  327. return ngModel.$undoManager.push(value || '');
  328. });
  329. //this code is used to update the models when data is entered/deleted
  330. if(_isInputFriendly){
  331. scope.events = {};
  332. if(!_isContentEditable){
  333. // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
  334. element.on('change blur', scope.events.change = scope.events.blur = function(){
  335. if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
  336. });
  337. element.on('keydown', scope.events.keydown = function(event, eventData){
  338. /* istanbul ignore else: this is for catching the jqLite testing*/
  339. if(eventData) angular.extend(event, eventData);
  340. // Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
  341. /* istanbul ignore else: otherwise normal functionality */
  342. if(event.keyCode === 9){ // tab was pressed
  343. // get caret position/selection
  344. var start = this.selectionStart;
  345. var end = this.selectionEnd;
  346. var value = element.val();
  347. if(event.shiftKey){
  348. // find \t
  349. var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
  350. if(_tab !== -1 && _tab >= _linebreak){
  351. // set textarea value to: text before caret + tab + text after caret
  352. element.val(value.substring(0, _tab) + value.substring(_tab + 1));
  353. // put caret at right position again (add one for the tab)
  354. this.selectionStart = this.selectionEnd = start - 1;
  355. }
  356. }else{
  357. // set textarea value to: text before caret + tab + text after caret
  358. element.val(value.substring(0, start) + "\t" + value.substring(end));
  359. // put caret at right position again (add one for the tab)
  360. this.selectionStart = this.selectionEnd = start + 1;
  361. }
  362. // prevent the focus lose
  363. event.preventDefault();
  364. }
  365. });
  366. var _repeat = function(string, n){
  367. var result = '';
  368. for(var _n = 0; _n < n; _n++) result += string;
  369. return result;
  370. };
  371. // add a forEach function that will work on a NodeList, etc..
  372. var forEach = function (array, callback, scope) {
  373. for (var i= 0; i<array.length; i++) {
  374. callback.call(scope, i, array[i]);
  375. }
  376. };
  377. // handle <ul> or <ol> nodes
  378. var recursiveListFormat = function(listNode, tablevel){
  379. var _html = '';
  380. var _subnodes = listNode.childNodes;
  381. tablevel++;
  382. // tab out and add the <ul> or <ol> html piece
  383. _html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
  384. forEach(_subnodes, function (index, node) {
  385. /* istanbul ignore next: browser catch */
  386. var nodeName = node.nodeName.toLowerCase();
  387. if (nodeName === '#comment') {
  388. _html += '<!--' + node.nodeValue + '-->';
  389. return;
  390. }
  391. if (nodeName === '#text') {
  392. _html += node.textContent;
  393. return;
  394. }
  395. /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
  396. if(!node.outerHTML) {
  397. // no html to add
  398. return;
  399. }
  400. if(nodeName === 'ul' || nodeName === 'ol') {
  401. _html += '\n' + recursiveListFormat(node, tablevel);
  402. }
  403. else {
  404. // no reformatting within this subnode, so just do the tabing...
  405. _html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
  406. }
  407. });
  408. // now add on the </ol> or </ul> piece
  409. _html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
  410. return _html;
  411. };
  412. // handle formating of something like:
  413. // <ol><!--First comment-->
  414. // <li>Test Line 1<!--comment test list 1--></li>
  415. // <ul><!--comment ul-->
  416. // <li>Nested Line 1</li>
  417. // <!--comment between nested lines--><li>Nested Line 2</li>
  418. // </ul>
  419. // <li>Test Line 3</li>
  420. // </ol>
  421. ngModel.$formatters.unshift(function(htmlValue){
  422. // tabulate the HTML so it looks nicer
  423. //
  424. // first get a list of the nodes...
  425. // we do this by using the element parser...
  426. //
  427. // doing this -- which is simpiler -- breaks our tests...
  428. //var _nodes=angular.element(htmlValue);
  429. var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
  430. if(_nodes.length > 0){
  431. // do the reformatting of the layout...
  432. htmlValue = '';
  433. forEach(_nodes, function (index, node) {
  434. var nodeName = node.nodeName.toLowerCase();
  435. if (nodeName === '#comment') {
  436. htmlValue += '<!--' + node.nodeValue + '-->';
  437. return;
  438. }
  439. if (nodeName === '#text') {
  440. htmlValue += node.textContent;
  441. return;
  442. }
  443. /* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
  444. if(!node.outerHTML)
  445. {
  446. // nothing to format!
  447. return;
  448. }
  449. if(htmlValue.length > 0) {
  450. // we aready have some content, so drop to a new line
  451. htmlValue += '\n';
  452. }
  453. if(nodeName === 'ul' || nodeName === 'ol') {
  454. // okay a set of list stuff we want to reformat in a nested way
  455. htmlValue += '' + recursiveListFormat(node, 0);
  456. }
  457. else {
  458. // just use the original without any additional formating
  459. htmlValue += '' + node.outerHTML;
  460. }
  461. });
  462. }
  463. return htmlValue;
  464. });
  465. }else{
  466. // all the code specific to contenteditable divs
  467. var _processingPaste = false;
  468. /* istanbul ignore next: phantom js cannot test this for some reason */
  469. var processpaste = function(text) {
  470. /* istanbul ignore else: don't care if nothing pasted */
  471. if(text && text.trim().length){
  472. // test paste from word/microsoft product
  473. if(text.match(/class=["']*Mso(Normal|List)/i)){
  474. var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
  475. if(!textFragment) textFragment = text;
  476. else textFragment = textFragment[1];
  477. textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
  478. var dom = angular.element("<div>" + textFragment + "</div>");
  479. var targetDom = angular.element("<div></div>");
  480. var _list = {
  481. element: null,
  482. lastIndent: [],
  483. lastLi: null,
  484. isUl: false
  485. };
  486. _list.lastIndent.peek = function(){
  487. var n = this.length;
  488. if (n>0) return this[n-1];
  489. };
  490. var _resetList = function(isUl){
  491. _list.isUl = isUl;
  492. _list.element = angular.element(isUl ? "<ul>" : "<ol>");
  493. _list.lastIndent = [];
  494. _list.lastIndent.peek = function(){
  495. var n = this.length;
  496. if (n>0) return this[n-1];
  497. };
  498. _list.lastLevelMatch = null;
  499. };
  500. for(var i = 0; i <= dom[0].childNodes.length; i++){
  501. if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text" || dom[0].childNodes[i].tagName.toLowerCase() !== "p") continue;
  502. var el = angular.element(dom[0].childNodes[i]);
  503. var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);
  504. if(_listMatch){
  505. if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){
  506. continue;
  507. }
  508. 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)));
  509. var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
  510. var indent = parseFloat((_indentMatch)?_indentMatch[1]:0);
  511. var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
  512. // prefers the mso-list syntax
  513. if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);
  514. 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)) {
  515. _resetList(isUl);
  516. targetDom.append(_list.element);
  517. } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){
  518. _list.element = angular.element(isUl ? "<ul>" : "<ol>");
  519. _list.lastLi.append(_list.element);
  520. } else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  521. while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  522. if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){
  523. _list.element = _list.element.parent();
  524. continue;
  525. }else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){
  526. _list.element = _list.element.parent();
  527. }else{ // else it's it should be a sibling
  528. break;
  529. }
  530. _list.lastIndent.pop();
  531. }
  532. _list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
  533. if (isUl !== _list.isUl) {
  534. _resetList(isUl);
  535. targetDom.append(_list.element);
  536. }
  537. }
  538. _list.lastLevelMatch = _levelMatch;
  539. if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
  540. _list.lastLi = angular.element("<li>");
  541. _list.element.append(_list.lastLi);
  542. _list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
  543. el.remove();
  544. }else{
  545. _resetList(false);
  546. targetDom.append(el);
  547. }
  548. }
  549. var _unwrapElement = function(node){
  550. node = angular.element(node);
  551. for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
  552. node.remove();
  553. };
  554. angular.forEach(targetDom.find('span'), function(node){
  555. node.removeAttribute('lang');
  556. if(node.attributes.length <= 0) _unwrapElement(node);
  557. });
  558. angular.forEach(targetDom.find('font'), _unwrapElement);
  559. text = targetDom.html();
  560. }else{
  561. // remove unnecessary chrome insert
  562. text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
  563. if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){
  564. // entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
  565. if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){
  566. var _el = angular.element("<div>" + text + "</div>");
  567. _el.find('textarea').remove();
  568. var binds = taDOM.getByAttribute(_el, 'ta-bind');
  569. for(var _b = 0; _b < binds.length; _b++){
  570. var _target = binds[_b][0].parentNode.parentNode;
  571. for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){
  572. _target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
  573. }
  574. _target.parentNode.removeChild(_target);
  575. }
  576. text = _el.html().replace('<br class="Apple-interchange-newline">', '');
  577. }
  578. }else if(text.match(/^<span/)){
  579. // in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
  580. // if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
  581. // on paste from even ourselves!
  582. if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) {
  583. text = text.replace(/<(|\/)span[^>]*?>/ig, '');
  584. }
  585. }
  586. // Webkit on Apple tags
  587. text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;');
  588. }
  589. if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
  590. // insert missing parent of li element
  591. text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
  592. }
  593. // parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
  594. text = text.replace(/^[ |\u00A0]+/gm, function (match) {
  595. var result = '';
  596. for (var i = 0; i < match.length; i++) {
  597. result += '&nbsp;';
  598. }
  599. return result;
  600. }).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
  601. if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
  602. text = taSanitize(text, '', _disableSanitizer);
  603. taSelection.insertHtml(text, element[0]);
  604. $timeout(function(){
  605. ngModel.$setViewValue(_compileHtml());
  606. _processingPaste = false;
  607. element.removeClass('processing-paste');
  608. }, 0);
  609. }else{
  610. _processingPaste = false;
  611. element.removeClass('processing-paste');
  612. }
  613. };
  614. element.on('paste', scope.events.paste = function(e, eventData){
  615. /* istanbul ignore else: this is for catching the jqLite testing*/
  616. if(eventData) angular.extend(e, eventData);
  617. if(_isReadonly || _processingPaste){
  618. e.stopPropagation();
  619. e.preventDefault();
  620. return false;
  621. }
  622. // Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
  623. _processingPaste = true;
  624. element.addClass('processing-paste');
  625. var pastedContent;
  626. var clipboardData = (e.originalEvent || e).clipboardData;
  627. if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
  628. var _types = "";
  629. for(var _t = 0; _t < clipboardData.types.length; _t++){
  630. _types += " " + clipboardData.types[_t];
  631. }
  632. /* istanbul ignore next: browser tests */
  633. if (/text\/html/i.test(_types)) {
  634. pastedContent = clipboardData.getData('text/html');
  635. } else if (/text\/plain/i.test(_types)) {
  636. pastedContent = clipboardData.getData('text/plain');
  637. }
  638. processpaste(pastedContent);
  639. e.stopPropagation();
  640. e.preventDefault();
  641. return false;
  642. } else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
  643. var _savedSelection = $window.rangy.saveSelection(),
  644. _tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
  645. $document.find('body').append(_tempDiv);
  646. _tempDiv[0].focus();
  647. $timeout(function(){
  648. // restore selection
  649. $window.rangy.restoreSelection(_savedSelection);
  650. processpaste(_tempDiv[0].innerHTML);
  651. element[0].focus();
  652. _tempDiv.remove();
  653. }, 0);
  654. }
  655. });
  656. element.on('cut', scope.events.cut = function(e){
  657. // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
  658. if(!_isReadonly) $timeout(function(){
  659. ngModel.$setViewValue(_compileHtml());
  660. }, 0);
  661. else e.preventDefault();
  662. });
  663. element.on('keydown', scope.events.keydown = function(event, eventData){
  664. /* istanbul ignore else: this is for catching the jqLite testing*/
  665. if(eventData) angular.extend(event, eventData);
  666. event.specialKey = _mapKeys(event);
  667. var userSpecialKey;
  668. /* istanbul ignore next: difficult to test */
  669. taOptions.keyMappings.forEach(function (mapping) {
  670. if (event.specialKey === mapping.commandKeyCode) {
  671. // taOptions has remapped this binding... so
  672. // we disable our own
  673. event.specialKey = undefined;
  674. }
  675. if (mapping.testForKey(event)) {
  676. userSpecialKey = mapping.commandKeyCode;
  677. }
  678. if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
  679. // this is necessary to fully stop the propagation.
  680. if (!mapping.enablePropagation) {
  681. event.preventDefault();
  682. }
  683. }
  684. });
  685. /* istanbul ignore next: difficult to test */
  686. if (typeof userSpecialKey !== 'undefined') {
  687. event.specialKey = userSpecialKey;
  688. }
  689. /* istanbul ignore next: difficult to test as can't seem to select */
  690. if ((typeof event.specialKey !== 'undefined') && (
  691. event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
  692. )) {
  693. event.preventDefault();
  694. textAngularManager.sendKeyCommand(scope, event);
  695. }
  696. /* istanbul ignore else: readonly check */
  697. if(!_isReadonly){
  698. if (event.specialKey==='UndoKey') {
  699. _undo();
  700. event.preventDefault();
  701. }
  702. if (event.specialKey==='RedoKey') {
  703. _redo();
  704. event.preventDefault();
  705. }
  706. /* istanbul ignore next: difficult to test as can't seem to select */
  707. if(event.keyCode === 13 && !event.shiftKey){
  708. var $selection;
  709. var selection = taSelection.getSelectionElement();
  710. if(!selection.tagName.match(VALIDELEMENTS)) return;
  711. var _new = angular.element(_defaultVal);
  712. if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && selection.parentNode.tagName.toLowerCase() === 'blockquote' && !selection.nextSibling) {
  713. // if last element in blockquote and element is blank, pull element outside of blockquote.
  714. $selection = angular.element(selection);
  715. var _parent = $selection.parent();
  716. _parent.after(_new);
  717. $selection.remove();
  718. if(_parent.children().length === 0) _parent.remove();
  719. taSelection.setSelectionToElementStart(_new[0]);
  720. event.preventDefault();
  721. }else if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim()) && selection.tagName.toLowerCase() === 'blockquote'){
  722. $selection = angular.element(selection);
  723. $selection.after(_new);
  724. $selection.remove();
  725. taSelection.setSelectionToElementStart(_new[0]);
  726. event.preventDefault();
  727. }
  728. }
  729. }
  730. });
  731. var _keyupTimeout;
  732. element.on('keyup', scope.events.keyup = function(event, eventData){
  733. /* istanbul ignore else: this is for catching the jqLite testing*/
  734. if(eventData) angular.extend(event, eventData);
  735. /* istanbul ignore next: FF specific bug fix */
  736. if (event.keyCode === 9) {
  737. var _selection = taSelection.getSelection();
  738. if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  739. return;
  740. }
  741. if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  742. if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
  743. // if enter - insert new taDefaultWrap, if shift+enter insert <br/>
  744. if(_defaultVal !== '' && event.keyCode === 13){
  745. if(!event.shiftKey){
  746. // new paragraph, br should be caught correctly
  747. var selection = taSelection.getSelectionElement();
  748. while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
  749. selection = selection.parentNode;
  750. }
  751. if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
  752. var _new = angular.element(_defaultVal);
  753. angular.element(selection).replaceWith(_new);
  754. taSelection.setSelectionToElementStart(_new[0]);
  755. }
  756. }
  757. }
  758. var val = _compileHtml();
  759. if(_defaultVal !== '' && val.trim() === ''){
  760. _setInnerHTML(_defaultVal);
  761. taSelection.setSelectionToElementStart(element.children()[0]);
  762. }else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
  763. /* we no longer do this, since there can be comments here and white space
  764. var _savedSelection = $window.rangy.saveSelection();
  765. val = _compileHtml();
  766. val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
  767. _setInnerHTML(val);
  768. $window.rangy.restoreSelection(_savedSelection);
  769. */
  770. }
  771. var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
  772. if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
  773. _keyupTimeout = $timeout(function() {
  774. _setViewValue(val, triggerUndo, true);
  775. }, ngModelOptions.$options.debounce || 400);
  776. if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
  777. _lastKey = event.keyCode;
  778. }
  779. });
  780. element.on('blur', scope.events.blur = function(){
  781. _focussed = false;
  782. /* istanbul ignore else: if readonly don't update model */
  783. if(!_isReadonly){
  784. _setViewValue(undefined, undefined, true);
  785. }else{
  786. _skipRender = true; // don't redo the whole thing, just check the placeholder logic
  787. ngModel.$render();
  788. }
  789. });
  790. // Placeholders not supported on ie 8 and below
  791. if(attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)){
  792. var rule;
  793. if(attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
  794. else throw('textAngular Error: An unique ID is required for placeholders to work');
  795. scope.$on('$destroy', function(){
  796. removeCSSRule(rule);
  797. });
  798. }
  799. element.on('focus', scope.events.focus = function(){
  800. _focussed = true;
  801. element.removeClass('placeholder-text');
  802. _reApplyOnSelectorHandlers();
  803. });
  804. element.on('mouseup', scope.events.mouseup = function(){
  805. var _selection = taSelection.getSelection();
  806. if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  807. });
  808. // prevent propagation on mousedown in editor, see #206
  809. element.on('mousedown', scope.events.mousedown = function(event, eventData){
  810. /* istanbul ignore else: this is for catching the jqLite testing*/
  811. if(eventData) angular.extend(event, eventData);
  812. event.stopPropagation();
  813. });
  814. }
  815. }
  816. var selectorClickHandler = function(event){
  817. // emit the element-select event, pass the element
  818. scope.$emit('ta-element-select', this);
  819. event.preventDefault();
  820. return false;
  821. };
  822. var fileDropHandler = function(event, eventData){
  823. /* istanbul ignore else: this is for catching the jqLite testing*/
  824. if(eventData) angular.extend(event, eventData);
  825. // emit the drop event, pass the element, preventing should be done elsewhere
  826. if(!dropFired && !_isReadonly){
  827. dropFired = true;
  828. var dataTransfer;
  829. if(event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
  830. else dataTransfer = event.dataTransfer;
  831. scope.$emit('ta-drop-event', this, event, dataTransfer);
  832. $timeout(function(){
  833. dropFired = false;
  834. _setViewValue(undefined, undefined, true);
  835. }, 100);
  836. }
  837. };
  838. //used for updating when inserting wrapped elements
  839. var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){
  840. /* istanbul ignore else */
  841. if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){
  842. // check we don't apply the handler twice
  843. element.find(selector)
  844. .off('click', selectorClickHandler)
  845. .on('click', selectorClickHandler);
  846. });
  847. };
  848. var _setInnerHTML = function(newval){
  849. element[0].innerHTML = newval;
  850. };
  851. var _renderTimeout;
  852. var _renderInProgress = false;
  853. // changes to the model variable from outside the html/text inputs
  854. ngModel.$render = function(){
  855. /* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
  856. if(_renderInProgress) return;
  857. else _renderInProgress = true;
  858. // catch model being null or undefined
  859. var val = ngModel.$viewValue || '';
  860. // if the editor isn't focused it needs to be updated, otherwise it's receiving user input
  861. if(!_skipRender){
  862. /* istanbul ignore else: in other cases we don't care */
  863. if(_isContentEditable && _focussed){
  864. // update while focussed
  865. element.removeClass('placeholder-text');
  866. if(_renderTimeout) $timeout.cancel(_renderTimeout);
  867. _renderTimeout = $timeout(function(){
  868. /* istanbul ignore if: Can't be bothered testing this... */
  869. if(!_focussed){
  870. element[0].focus();
  871. taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
  872. }
  873. _renderTimeout = undefined;
  874. }, 1);
  875. }
  876. if(_isContentEditable){
  877. // WYSIWYG Mode
  878. if(attrs.placeholder){
  879. if(val === ''){
  880. // blank
  881. _setInnerHTML(_defaultVal);
  882. }else{
  883. // not-blank
  884. _setInnerHTML(val);
  885. }
  886. }else{
  887. _setInnerHTML((val === '') ? _defaultVal : val);
  888. }
  889. // if in WYSIWYG and readOnly we kill the use of links by clicking
  890. if(!_isReadonly){
  891. _reApplyOnSelectorHandlers();
  892. element.on('drop', fileDropHandler);
  893. }else{
  894. element.off('drop', fileDropHandler);
  895. }
  896. }else if(element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input'){
  897. // make sure the end user can SEE the html code as a display. This is a read-only display element
  898. _setInnerHTML(taApplyCustomRenderers(val));
  899. }else{
  900. // only for input and textarea inputs
  901. element.val(val);
  902. }
  903. }
  904. if(_isContentEditable && attrs.placeholder){
  905. if(val === ''){
  906. if(_focussed) element.removeClass('placeholder-text');
  907. else element.addClass('placeholder-text');
  908. }else{
  909. element.removeClass('placeholder-text');
  910. }
  911. }
  912. _renderInProgress = _skipRender = false;
  913. };
  914. if(attrs.taReadonly){
  915. //set initial value
  916. _isReadonly = scope.$eval(attrs.taReadonly);
  917. if(_isReadonly){
  918. element.addClass('ta-readonly');
  919. // we changed to readOnly mode (taReadonly='true')
  920. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  921. element.attr('disabled', 'disabled');
  922. }
  923. if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  924. element.removeAttr('contenteditable');
  925. }
  926. }else{
  927. element.removeClass('ta-readonly');
  928. // we changed to NOT readOnly mode (taReadonly='false')
  929. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  930. element.removeAttr('disabled');
  931. }else if(_isContentEditable){
  932. element.attr('contenteditable', 'true');
  933. }
  934. }
  935. // taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
  936. // Otherwise it is readonly by default
  937. scope.$watch(attrs.taReadonly, function(newVal, oldVal){
  938. if(oldVal === newVal) return;
  939. if(newVal){
  940. element.addClass('ta-readonly');
  941. // we changed to readOnly mode (taReadonly='true')
  942. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  943. element.attr('disabled', 'disabled');
  944. }
  945. if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  946. element.removeAttr('contenteditable');
  947. }
  948. // turn ON selector click handlers
  949. angular.forEach(taSelectableElements, function(selector){
  950. element.find(selector).on('click', selectorClickHandler);
  951. });
  952. element.off('drop', fileDropHandler);
  953. }else{
  954. element.removeClass('ta-readonly');
  955. // we changed to NOT readOnly mode (taReadonly='false')
  956. if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  957. element.removeAttr('disabled');
  958. }else if(_isContentEditable){
  959. element.attr('contenteditable', 'true');
  960. }
  961. // remove the selector click handlers
  962. angular.forEach(taSelectableElements, function(selector){
  963. element.find(selector).off('click', selectorClickHandler);
  964. });
  965. element.on('drop', fileDropHandler);
  966. }
  967. _isReadonly = newVal;
  968. });
  969. }
  970. // Initialise the selectableElements
  971. // if in WYSIWYG and readOnly we kill the use of links by clicking
  972. if(_isContentEditable && !_isReadonly){
  973. angular.forEach(taSelectableElements, function(selector){
  974. element.find(selector).on('click', selectorClickHandler);
  975. });
  976. element.on('drop', fileDropHandler);
  977. element.on('blur', function(){
  978. /* istanbul ignore next: webkit fix */
  979. if(_browserDetect.webkit) { // detect webkit
  980. globalContentEditableBlur = true;
  981. }
  982. });
  983. }
  984. }
  985. };
  986. }]);