PageRenderTime 51ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/files/angular.textangular/1.2.2/textAngular.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1060 lines | 787 code | 61 blank | 212 comment | 180 complexity | 63f91081dc7b3a7864a541c70db04ca6 MD5 | raw file
  1. /*
  2. textAngular
  3. Author : Austin Anderson
  4. License : 2013 MIT
  5. Version 1.2.2
  6. See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
  7. */
  8. (function(){ // encapsulate all variables so they don't become global vars
  9. "Use Strict";
  10. // fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
  11. // this is set true when a blur occurs as the blur of the ta-bind triggers before the click
  12. var globalContentEditableBlur = false;
  13. /* istanbul ignore next: Browser Un-Focus fix for webkit */
  14. if(/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)) { // detect webkit
  15. document.addEventListener("click", function(){
  16. var curelement = window.event.target;
  17. if(globalContentEditableBlur && curelement !== null){
  18. var isEditable = false;
  19. var tempEl = curelement;
  20. while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
  21. isEditable = tempEl.contentEditable === 'true';
  22. tempEl = tempEl.parentNode;
  23. }
  24. if(!isEditable){
  25. document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
  26. curelement.focus(); // focus the wanted element.
  27. }
  28. }
  29. globalContentEditableBlur = false;
  30. }, false); // add global click handler
  31. angular.element(document).ready(function () {
  32. angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" style="width:1px;height:1px;border:none;margin:0;padding:0;position:absolute; top: -10000; left: -10000;" unselectable="on" tabIndex="-1">'));
  33. });
  34. }
  35. // IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
  36. // We need this as IE sometimes plays funny tricks with the contenteditable.
  37. // ----------------------------------------------------------
  38. // If you're not in IE (or IE version is less than 5) then:
  39. // ie === undefined
  40. // If you're in IE (>=5) then you can determine which version:
  41. // ie === 7; // IE7
  42. // Thus, to detect IE:
  43. // if (ie) {}
  44. // And to detect the version:
  45. // ie === 6 // IE6
  46. // ie > 7 // IE8, IE9, IE10 ...
  47. // ie < 9 // Anything less than IE9
  48. // ----------------------------------------------------------
  49. /* istanbul ignore next: untestable browser check */
  50. var ie = (function(){
  51. var undef,rv = -1; // Return value assumes failure.
  52. var ua = window.navigator.userAgent;
  53. var msie = ua.indexOf('MSIE ');
  54. var trident = ua.indexOf('Trident/');
  55. if (msie > 0) {
  56. // IE 10 or older => return version number
  57. rv = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
  58. } else if (trident > 0) {
  59. // IE 11 (or newer) => return version number
  60. var rvNum = ua.indexOf('rv:');
  61. rv = parseInt(ua.substring(rvNum + 3, ua.indexOf('.', rvNum)), 10);
  62. }
  63. return ((rv > -1) ? rv : undef);
  64. }());
  65. // Thanks to answer in http://stackoverflow.com/questions/2308134/trim-in-javascript-not-working-in-ie
  66. /* istanbul ignore next: trim shim for older browsers */
  67. if(typeof String.prototype.trim !== 'function') {
  68. String.prototype.trim = function() {
  69. return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  70. };
  71. }
  72. // tests against the current jqLite/jquery implementation if this can be an element
  73. function validElementString(string){
  74. try{
  75. return angular.element(string).length !== 0;
  76. }catch(any){
  77. return false;
  78. }
  79. }
  80. /*
  81. Custom stylesheet for the placeholders rules.
  82. Credit to: http://davidwalsh.name/add-rules-stylesheets
  83. */
  84. var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule;
  85. /* istanbul ignore else: IE <8 test*/
  86. if(ie > 8 || ie === undefined){
  87. var topsheet = (function() {
  88. // Create the <style> tag
  89. var style = document.createElement("style");
  90. /* istanbul ignore else : WebKit hack :( */
  91. if(/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)) style.appendChild(document.createTextNode(""));
  92. // Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
  93. document.head.insertBefore(style,document.head.firstChild);
  94. return style.sheet;
  95. })();
  96. // this sheet is used for the placeholders later on.
  97. sheet = (function() {
  98. // Create the <style> tag
  99. var style = document.createElement("style");
  100. /* istanbul ignore else : WebKit hack :( */
  101. if(/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)) style.appendChild(document.createTextNode(""));
  102. // Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
  103. document.head.appendChild(style);
  104. return style.sheet;
  105. })();
  106. // use as: addCSSRule("header", "float: left");
  107. addCSSRule = function(selector, rules) {
  108. _addCSSRule(sheet, selector, rules);
  109. };
  110. _addCSSRule = function(sheet, selector, rules){
  111. var insertIndex;
  112. /* istanbul ignore else: firefox catch */
  113. if(sheet.rules) insertIndex = Math.max(sheet.rules.length - 1, 0);
  114. else if(sheet.cssRules) insertIndex = Math.max(sheet.cssRules.length - 1, 0);
  115. /* istanbul ignore else: untestable IE option */
  116. if(sheet.insertRule) {
  117. sheet.insertRule(selector + "{" + rules + "}", insertIndex);
  118. }
  119. else {
  120. sheet.addRule(selector, rules, insertIndex);
  121. }
  122. // return the index of the stylesheet rule
  123. return insertIndex;
  124. };
  125. removeCSSRule = function(index){
  126. _removeCSSRule(sheet, index);
  127. };
  128. _removeCSSRule = function(sheet, index){
  129. /* istanbul ignore else: untestable IE option */
  130. if(sheet.removeRule){
  131. sheet.removeRule(index);
  132. }else{
  133. sheet.deleteRule(index);
  134. }
  135. };
  136. // add generic styling for the editor
  137. _addCSSRule(topsheet, '.ta-scroll-window.form-control', "height: auto; min-height: 300px; overflow: auto; font-family: inherit; font-size: 100%; position: relative; padding: 0;");
  138. _addCSSRule(topsheet, '.ta-root.focussed .ta-scroll-window.form-control', 'border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);');
  139. _addCSSRule(topsheet, '.ta-editor.ta-html', "min-height: 300px; height: auto; overflow: auto; font-family: inherit; font-size: 100%;");
  140. _addCSSRule(topsheet, '.ta-scroll-window > .ta-bind', "height: auto; min-height: 300px; padding: 6px 12px;");
  141. // add the styling for the awesomness of the resizer
  142. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay', 'z-index: 100; position: absolute; display: none;');
  143. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-info', 'position: absolute; bottom: 16px; right: 16px; border: 1px solid black; background-color: #FFF; padding: 0 4px; opacity: 0.7;');
  144. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-background', 'position: absolute; bottom: 5px; right: 5px; left: 5px; top: 5px; border: 1px solid black; background-color: rgba(0, 0, 0, 0.2);');
  145. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner', 'width: 10px; height: 10px; position: absolute;');
  146. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl', 'top: 0; left: 0; border-left: 1px solid black; border-top: 1px solid black;');
  147. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr', 'top: 0; right: 0; border-right: 1px solid black; border-top: 1px solid black;');
  148. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl', 'bottom: 0; left: 0; border-left: 1px solid black; border-bottom: 1px solid black;');
  149. _addCSSRule(topsheet, '.ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-br', 'bottom: 0; right: 0; border: 1px solid black; cursor: se-resize; background-color: white;');
  150. }
  151. // recursive function that returns an array of angular.elements that have the passed attribute set on them
  152. function getByAttribute(element, attribute){
  153. var resultingElements = [];
  154. var childNodes = element.children();
  155. if(childNodes.length){
  156. angular.forEach(childNodes, function(child){
  157. resultingElements = resultingElements.concat(getByAttribute(angular.element(child), attribute));
  158. });
  159. }
  160. if(element.attr(attribute) !== undefined) resultingElements.push(element);
  161. return resultingElements;
  162. }
  163. // this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
  164. var dropFired = false;
  165. var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup']); //This makes ngSanitize required
  166. // setup the global contstant functions for setting up the toolbar
  167. // all tool definitions
  168. var taTools = {};
  169. /*
  170. A tool definition is an object with the following key/value parameters:
  171. action: [function(deferred, restoreSelection)]
  172. a function that is executed on clicking on the button - this will allways be executed using ng-click and will
  173. overwrite any ng-click value in the display attribute.
  174. The function is passed a deferred object ($q.defer()), if this is wanted to be used `return false;` from the action and
  175. manually call `deferred.resolve();` elsewhere to notify the editor that the action has finished.
  176. restoreSelection is only defined if the rangy library is included and it can be called as `restoreSelection()` to restore the users
  177. selection in the WYSIWYG editor.
  178. display: [string]?
  179. Optional, an HTML element to be displayed as the button. The `scope` of the button is the tool definition object with some additional functions
  180. If set this will cause buttontext and iconclass to be ignored
  181. buttontext: [string]?
  182. if this is defined it will replace the contents of the element contained in the `display` element
  183. iconclass: [string]?
  184. if this is defined an icon (<i>) will be appended to the `display` element with this string as it's class
  185. tooltiptext: [string]?
  186. Optional, a plain text description of the action, used for the title attribute of the action button in the toolbar by default.
  187. activestate: [function(commonElement)]?
  188. this function is called on every caret movement, if it returns true then the class taOptions.classes.toolbarButtonActive
  189. will be applied to the `display` element, else the class will be removed
  190. disabled: [function()]?
  191. if this function returns true then the tool will have the class taOptions.classes.disabled applied to it, else it will be removed
  192. Other functions available on the scope are:
  193. name: [string]
  194. the name of the tool, this is the first parameter passed into taRegisterTool
  195. isDisabled: [function()]
  196. returns true if the tool is disabled, false if it isn't
  197. displayActiveToolClass: [function(boolean)]
  198. returns true if the tool is 'active' in the currently focussed toolbar
  199. onElementSelect: [Object]
  200. This object contains the following key/value pairs and is used to trigger the ta-element-select event
  201. element: [String]
  202. an element name, will only trigger the onElementSelect action if the tagName of the element matches this string
  203. filter: [function(element)]?
  204. an optional filter that returns a boolean, if true it will trigger the onElementSelect.
  205. action: [function(event, element, editorScope)]
  206. the action that should be executed if the onElementSelect function runs
  207. */
  208. // name and toolDefinition to add into the tools available to be added on the toolbar
  209. function registerTextAngularTool(name, toolDefinition){
  210. if(!name || name === '' || taTools.hasOwnProperty(name)) throw('textAngular Error: A unique name is required for a Tool Definition');
  211. if(
  212. (toolDefinition.display && (toolDefinition.display === '' || !validElementString(toolDefinition.display))) ||
  213. (!toolDefinition.display && !toolDefinition.buttontext && !toolDefinition.iconclass)
  214. )
  215. throw('textAngular Error: Tool Definition for "' + name + '" does not have a valid display/iconclass/buttontext value');
  216. taTools[name] = toolDefinition;
  217. }
  218. textAngular.constant('taRegisterTool', registerTextAngularTool);
  219. textAngular.value('taTools', taTools);
  220. textAngular.config([function(){
  221. // clear taTools variable. Just catches testing and any other time that this config may run multiple times...
  222. angular.forEach(taTools, function(value, key){ delete taTools[key]; });
  223. }]);
  224. textAngular.directive("textAngular", [
  225. '$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand', 'textAngularManager', '$window', '$document', '$animate', '$log',
  226. function($compile, $timeout, taOptions, taSelection, taExecCommand, textAngularManager, $window, $document, $animate, $log){
  227. return {
  228. require: '?ngModel',
  229. scope: {},
  230. restrict: "EA",
  231. link: function(scope, element, attrs, ngModel){
  232. // all these vars should not be accessable outside this directive
  233. var _keydown, _keyup, _keypress, _mouseup, _mousedown, _focusin, _focusout,
  234. _originalContents, _toolbars,
  235. _serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
  236. _name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial,
  237. _taExecCommand;
  238. var oneEvent = function(_element, event, action){
  239. $timeout(function(){
  240. // shim the .one till fixed
  241. var _func = function(){
  242. _element.off(event, _func);
  243. action();
  244. };
  245. _element.on(event, _func);
  246. }, 100);
  247. };
  248. _taExecCommand = taExecCommand(attrs.taDefaultWrap);
  249. // get the settings from the defaults and add our specific functions that need to be on the scope
  250. angular.extend(scope, angular.copy(taOptions), {
  251. // wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
  252. wrapSelection: function(command, opt, isSelectableElementTool){
  253. // catch errors like FF erroring when you try to force an undo with nothing done
  254. _taExecCommand(command, false, opt);
  255. if(isSelectableElementTool){
  256. // re-apply the selectable tool events
  257. scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
  258. }
  259. // refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
  260. // You still have focus on the text/html input it just doesn't show up
  261. scope.displayElements.text[0].focus();
  262. },
  263. showHtml: false
  264. });
  265. // setup the options from the optional attributes
  266. if(attrs.taFocussedClass) scope.classes.focussed = attrs.taFocussedClass;
  267. if(attrs.taTextEditorClass) scope.classes.textEditor = attrs.taTextEditorClass;
  268. if(attrs.taHtmlEditorClass) scope.classes.htmlEditor = attrs.taHtmlEditorClass;
  269. // optional setup functions
  270. if(attrs.taTextEditorSetup) scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
  271. if(attrs.taHtmlEditorSetup) scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
  272. // optional fileDropHandler function
  273. if(attrs.taFileDrop) scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
  274. else scope.fileDropHandler = scope.defaultFileDropHandler;
  275. _originalContents = element[0].innerHTML;
  276. // clear the original content
  277. element[0].innerHTML = '';
  278. // Setup the HTML elements as variable references for use later
  279. scope.displayElements = {
  280. // we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
  281. // wheras the input will ALLWAYS have the correct value.
  282. forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
  283. html: angular.element("<textarea></textarea>"),
  284. text: angular.element("<div></div>"),
  285. // other toolbased elements
  286. scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
  287. popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
  288. popoverArrow: angular.element('<div class="arrow"></div>'),
  289. popoverContainer: angular.element('<div class="popover-content"></div>'),
  290. resize: {
  291. overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
  292. background: angular.element('<div class="ta-resizer-handle-background"></div>'),
  293. anchors: [
  294. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
  295. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
  296. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
  297. angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
  298. ],
  299. info: angular.element('<div class="ta-resizer-handle-info"></div>')
  300. }
  301. };
  302. // Setup the popover
  303. scope.displayElements.popover.append(scope.displayElements.popoverArrow);
  304. scope.displayElements.popover.append(scope.displayElements.popoverContainer);
  305. scope.displayElements.scrollWindow.append(scope.displayElements.popover);
  306. scope.displayElements.popover.on('mousedown', function(e, eventData){
  307. /* istanbul ignore else: this is for catching the jqLite testing*/
  308. if(eventData) angular.extend(e, eventData);
  309. // this prevents focusout from firing on the editor when clicking anything in the popover
  310. e.preventDefault();
  311. return false;
  312. });
  313. // define the popover show and hide functions
  314. scope.showPopover = function(_el){
  315. scope.displayElements.popover.css('display', 'block');
  316. scope.reflowPopover(_el);
  317. $animate.addClass(scope.displayElements.popover, 'in');
  318. oneEvent(element, 'click keyup', function(){scope.hidePopover();});
  319. };
  320. scope.reflowPopover = function(_el){
  321. if(scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop){
  322. scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + 'px');
  323. scope.displayElements.popover.removeClass('top').addClass('bottom');
  324. }else{
  325. scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + 'px');
  326. scope.displayElements.popover.removeClass('bottom').addClass('top');
  327. }
  328. var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
  329. var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
  330. scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
  331. scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
  332. };
  333. scope.hidePopover = function(){
  334. $animate.removeClass(scope.displayElements.popover, 'in', /* istanbul ignore next: dosen't test with mocked animate */ function(){
  335. scope.displayElements.popover.css('display', '');
  336. scope.displayElements.popoverContainer.attr('style', '');
  337. scope.displayElements.popoverContainer.attr('class', 'popover-content');
  338. });
  339. };
  340. // setup the resize overlay
  341. scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
  342. angular.forEach(scope.displayElements.resize.anchors, function(anchor){ scope.displayElements.resize.overlay.append(anchor);});
  343. scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
  344. scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);
  345. // define the show and hide events
  346. scope.reflowResizeOverlay = function(_el){
  347. _el = angular.element(_el)[0];
  348. scope.displayElements.resize.overlay.css({
  349. 'display': 'block',
  350. 'left': _el.offsetLeft - 5 + 'px',
  351. 'top': _el.offsetTop - 5 + 'px',
  352. 'width': _el.offsetWidth + 10 + 'px',
  353. 'height': _el.offsetHeight + 10 + 'px'
  354. });
  355. scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
  356. };
  357. /* istanbul ignore next: pretty sure phantomjs won't test this */
  358. scope.showResizeOverlay = function(_el){
  359. var resizeMouseDown = function(event){
  360. var startPosition = {
  361. width: parseInt(_el.attr('width')),
  362. height: parseInt(_el.attr('height')),
  363. x: event.clientX,
  364. y: event.clientY
  365. };
  366. if(startPosition.width === undefined) startPosition.width = _el[0].offsetWidth;
  367. if(startPosition.height === undefined) startPosition.height = _el[0].offsetHeight;
  368. scope.hidePopover();
  369. var ratio = startPosition.height / startPosition.width;
  370. var mousemove = function(event){
  371. // calculate new size
  372. var pos = {
  373. x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
  374. y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
  375. };
  376. var applyImageSafeCSS = function(_el, css){
  377. _el = angular.element(_el);
  378. if(_el[0].tagName.toLowerCase() === 'img'){
  379. if(css.height){
  380. _el.attr('height', css.height);
  381. delete css.height;
  382. }
  383. if(css.width){
  384. _el.attr('width', css.width);
  385. delete css.width;
  386. }
  387. }
  388. _el.css(css);
  389. };
  390. if(event.shiftKey){
  391. // keep ratio
  392. var newRatio = pos.y / pos.x;
  393. applyImageSafeCSS(_el, {
  394. width: ratio > newRatio ? pos.x : pos.y / ratio,
  395. height: ratio > newRatio ? pos.x * ratio : pos.y
  396. });
  397. }else{
  398. applyImageSafeCSS(_el, {
  399. width: pos.x,
  400. height: pos.y
  401. });
  402. }
  403. // reflow the popover tooltip
  404. scope.reflowResizeOverlay(_el);
  405. };
  406. $document.find('body').on('mousemove', mousemove);
  407. oneEvent(scope.displayElements.resize.overlay, 'mouseup', function(){
  408. $document.find('body').off('mousemove', mousemove);
  409. scope.showPopover(_el);
  410. });
  411. event.stopPropagation();
  412. event.preventDefault();
  413. };
  414. scope.displayElements.resize.anchors[3].on('mousedown', resizeMouseDown);
  415. scope.reflowResizeOverlay(_el);
  416. oneEvent(element, 'click', function(){scope.hideResizeOverlay();});
  417. };
  418. /* istanbul ignore next: pretty sure phantomjs won't test this */
  419. scope.hideResizeOverlay = function(){
  420. scope.displayElements.resize.overlay.css('display', '');
  421. };
  422. // allow for insertion of custom directives on the textarea and div
  423. scope.setup.htmlEditorSetup(scope.displayElements.html);
  424. scope.setup.textEditorSetup(scope.displayElements.text);
  425. scope.displayElements.html.attr({
  426. 'id': 'taHtmlElement' + _serial,
  427. 'ng-show': 'showHtml',
  428. 'ta-bind': 'ta-bind',
  429. 'ng-model': 'html'
  430. });
  431. scope.displayElements.text.attr({
  432. 'id': 'taTextElement' + _serial,
  433. 'contentEditable': 'true',
  434. 'ta-bind': 'ta-bind',
  435. 'ng-model': 'html'
  436. });
  437. scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
  438. if(attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
  439. if(attrs.taUnsafeSanitizer){
  440. scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  441. scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  442. }
  443. // add the main elements to the origional element
  444. scope.displayElements.scrollWindow.append(scope.displayElements.text);
  445. element.append(scope.displayElements.scrollWindow);
  446. element.append(scope.displayElements.html);
  447. scope.displayElements.forminput.attr('name', _name);
  448. element.append(scope.displayElements.forminput);
  449. if(attrs.tabindex){
  450. element.removeAttr('tabindex');
  451. scope.displayElements.text.attr('tabindex', attrs.tabindex);
  452. scope.displayElements.html.attr('tabindex', attrs.tabindex);
  453. }
  454. if (attrs.placeholder) {
  455. scope.displayElements.text.attr('placeholder', attrs.placeholder);
  456. scope.displayElements.html.attr('placeholder', attrs.placeholder);
  457. }
  458. if(attrs.taDisabled){
  459. scope.displayElements.text.attr('ta-readonly', 'disabled');
  460. scope.displayElements.html.attr('ta-readonly', 'disabled');
  461. scope.disabled = scope.$parent.$eval(attrs.taDisabled);
  462. scope.$parent.$watch(attrs.taDisabled, function(newVal){
  463. scope.disabled = newVal;
  464. if(scope.disabled){
  465. element.addClass(scope.classes.disabled);
  466. }else{
  467. element.removeClass(scope.classes.disabled);
  468. }
  469. });
  470. }
  471. // compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
  472. $compile(scope.displayElements.scrollWindow)(scope);
  473. $compile(scope.displayElements.html)(scope);
  474. scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
  475. scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];
  476. // add the classes manually last
  477. element.addClass("ta-root");
  478. scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
  479. scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);
  480. // used in the toolbar actions
  481. scope._actionRunning = false;
  482. var _savedSelection = false;
  483. scope.startAction = function(){
  484. scope._actionRunning = true;
  485. // if rangy library is loaded return a function to reload the current selection
  486. if($window.rangy && $window.rangy.saveSelection){
  487. _savedSelection = $window.rangy.saveSelection();
  488. return function(){
  489. if(_savedSelection) $window.rangy.restoreSelection(_savedSelection);
  490. };
  491. }
  492. };
  493. scope.endAction = function(){
  494. scope._actionRunning = false;
  495. if(_savedSelection) $window.rangy.removeMarkers(_savedSelection);
  496. _savedSelection = false;
  497. scope.updateSelectedStyles();
  498. // only update if in text or WYSIWYG mode
  499. if(!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
  500. };
  501. // note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
  502. // cascades to displayElements.text and displayElements.html automatically.
  503. _focusin = function(){
  504. element.addClass(scope.classes.focussed);
  505. _toolbars.focus();
  506. };
  507. scope.displayElements.html.on('focus', _focusin);
  508. scope.displayElements.text.on('focus', _focusin);
  509. _focusout = function(e){
  510. // if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
  511. if(!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
  512. element.removeClass(scope.classes.focussed);
  513. _toolbars.unfocus();
  514. // to prevent multiple apply error defer to next seems to work.
  515. $timeout(function(){ element.triggerHandler('blur'); }, 0);
  516. }
  517. e.preventDefault();
  518. return false;
  519. };
  520. scope.displayElements.html.on('blur', _focusout);
  521. scope.displayElements.text.on('blur', _focusout);
  522. // Setup the default toolbar tools, this way allows the user to add new tools like plugins.
  523. // This is on the editor for future proofing if we find a better way to do this.
  524. scope.queryFormatBlockState = function(command){
  525. // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  526. return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
  527. };
  528. scope.queryCommandState = function(command){
  529. // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  530. return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
  531. };
  532. scope.switchView = function(){
  533. scope.showHtml = !scope.showHtml;
  534. //Show the HTML view
  535. if(scope.showHtml){
  536. //defer until the element is visible
  537. $timeout(function(){
  538. // [0] dereferences the DOM object from the angular.element
  539. return scope.displayElements.html[0].focus();
  540. }, 100);
  541. }else{
  542. //Show the WYSIWYG view
  543. //defer until the element is visible
  544. $timeout(function(){
  545. // [0] dereferences the DOM object from the angular.element
  546. return scope.displayElements.text[0].focus();
  547. }, 100);
  548. }
  549. };
  550. // changes to the model variable from outside the html/text inputs
  551. // if no ngModel, then the only input is from inside text-angular
  552. if(attrs.ngModel){
  553. var _firstRun = true;
  554. ngModel.$render = function(){
  555. if(_firstRun){
  556. // we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
  557. _firstRun = false;
  558. // if view value is null or undefined initially and there was original content, set to the original content
  559. var _initialValue = scope.$parent.$eval(attrs.ngModel);
  560. if((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')){
  561. // on passing through to taBind it will be sanitised
  562. ngModel.$setViewValue(_originalContents);
  563. }
  564. }
  565. scope.displayElements.forminput.val(ngModel.$viewValue);
  566. // if the editors aren't focused they need to be updated, otherwise they are doing the updating
  567. /* istanbul ignore else: don't care */
  568. if(!scope._elementSelectTriggered && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
  569. // catch model being null or undefined
  570. scope.html = ngModel.$viewValue || '';
  571. }
  572. };
  573. // trigger the validation calls
  574. var _validity = function(value){
  575. if(attrs.required) ngModel.$setValidity('required', !(!value || value.trim() === ''));
  576. return value;
  577. };
  578. ngModel.$parsers.push(_validity);
  579. ngModel.$formatters.push(_validity);
  580. }else{
  581. // if no ngModel then update from the contents of the origional html.
  582. scope.displayElements.forminput.val(_originalContents);
  583. scope.html = _originalContents;
  584. }
  585. // changes from taBind back up to here
  586. scope.$watch('html', function(newValue, oldValue){
  587. if(newValue !== oldValue){
  588. if(attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
  589. scope.displayElements.forminput.val(newValue);
  590. }
  591. });
  592. if(attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(_name, scope, attrs.taTargetToolbars.split(','));
  593. else{
  594. var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
  595. // passthrough init of toolbar options
  596. if(attrs.taToolbar) _toolbar.attr('ta-toolbar', attrs.taToolbar);
  597. if(attrs.taToolbarClass) _toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
  598. if(attrs.taToolbarGroupClass) _toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
  599. if(attrs.taToolbarButtonClass) _toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
  600. if(attrs.taToolbarActiveButtonClass) _toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
  601. if(attrs.taFocussedClass) _toolbar.attr('ta-focussed-class', attrs.taFocussedClass);
  602. element.prepend(_toolbar);
  603. $compile(_toolbar)(scope.$parent);
  604. _toolbars = textAngularManager.registerEditor(_name, scope, ['textAngularToolbar' + _serial]);
  605. }
  606. scope.$on('$destroy', function(){
  607. textAngularManager.unregisterEditor(_name);
  608. });
  609. // catch element select event and pass to toolbar tools
  610. scope.$on('ta-element-select', function(event, element){
  611. _toolbars.triggerElementSelect(event, element);
  612. });
  613. scope.$on('ta-drop-event', function(event, element, dropEvent, dataTransfer){
  614. scope.displayElements.text[0].focus();
  615. if(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0){
  616. angular.forEach(dataTransfer.files, function(file){
  617. // taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
  618. // If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
  619. try{
  620. return scope.fileDropHandler(file, scope.wrapSelection) ||
  621. (scope.fileDropHandler !== scope.defaultFileDropHandler &&
  622. scope.defaultFileDropHandler(file, scope.wrapSelection));
  623. }catch(error){
  624. $log.error(error);
  625. }
  626. });
  627. dropEvent.preventDefault();
  628. dropEvent.stopPropagation();
  629. }
  630. });
  631. // the following is for applying the active states to the tools that support it
  632. scope._bUpdateSelectedStyles = false;
  633. // loop through all the tools polling their activeState function if it exists
  634. scope.updateSelectedStyles = function(){
  635. var _selection;
  636. // test if the common element ISN'T the root ta-text node
  637. if((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]){
  638. _toolbars.updateSelectedStyles(angular.element(_selection));
  639. }else _toolbars.updateSelectedStyles();
  640. // used to update the active state when a key is held down, ie the left arrow
  641. if(scope._bUpdateSelectedStyles) $timeout(scope.updateSelectedStyles, 200);
  642. };
  643. // start updating on keydown
  644. _keydown = function(){
  645. /* istanbul ignore else: don't run if already running */
  646. if(!scope._bUpdateSelectedStyles){
  647. scope._bUpdateSelectedStyles = true;
  648. scope.$apply(function(){
  649. scope.updateSelectedStyles();
  650. });
  651. }
  652. };
  653. scope.displayElements.html.on('keydown', _keydown);
  654. scope.displayElements.text.on('keydown', _keydown);
  655. // stop updating on key up and update the display/model
  656. _keyup = function(){
  657. scope._bUpdateSelectedStyles = false;
  658. };
  659. scope.displayElements.html.on('keyup', _keyup);
  660. scope.displayElements.text.on('keyup', _keyup);
  661. // stop updating on key up and update the display/model
  662. _keypress = function(event, eventData){
  663. /* istanbul ignore else: this is for catching the jqLite testing*/
  664. if(eventData) angular.extend(event, eventData);
  665. scope.$apply(function(){
  666. if(_toolbars.sendKeyCommand(event)){
  667. /* istanbul ignore else: don't run if already running */
  668. if(!scope._bUpdateSelectedStyles){
  669. scope.updateSelectedStyles();
  670. }
  671. event.preventDefault();
  672. return false;
  673. }
  674. });
  675. };
  676. scope.displayElements.html.on('keypress', _keypress);
  677. scope.displayElements.text.on('keypress', _keypress);
  678. // update the toolbar active states when we click somewhere in the text/html boxed
  679. _mouseup = function(){
  680. // ensure only one execution of updateSelectedStyles()
  681. scope._bUpdateSelectedStyles = false;
  682. scope.$apply(function(){
  683. scope.updateSelectedStyles();
  684. });
  685. };
  686. scope.displayElements.html.on('mouseup', _mouseup);
  687. scope.displayElements.text.on('mouseup', _mouseup);
  688. }
  689. };
  690. }
  691. ]).factory('taBrowserTag', [function(){
  692. return function(tag){
  693. /* istanbul ignore next: ie specific test */
  694. if(!tag) return (ie <= 8)? 'P' : 'p';
  695. else if(tag === '') return (ie === undefined)? 'div' : (ie <= 8)? 'P' : 'p';
  696. else return (ie <= 8)? tag.toUpperCase() : tag;
  697. };
  698. }]).factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
  699. var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/ig;
  700. var LISTELEMENTS = /^(ul|li|ol)$/ig;
  701. var listToDefault = function(listElement, defaultWrap){
  702. var $target, i;
  703. // if all selected then we should remove the list
  704. // grab all li elements and convert to taDefaultWrap tags
  705. var children = listElement.find('li');
  706. for(i = children.length - 1; i >= 0; i--){
  707. $target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
  708. listElement.after($target);
  709. }
  710. listElement.remove();
  711. taSelection.setSelectionToElementEnd($target[0]);
  712. };
  713. var listToList = function(listElement, newListTag){
  714. var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
  715. listElement.after($target);
  716. listElement.remove();
  717. taSelection.setSelectionToElementEnd($target.find('li')[0]);
  718. };
  719. var childElementsToList = function(elements, listElement, newListTag){
  720. var html = '';
  721. for(var i = 0; i < elements.length; i++){
  722. html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
  723. }
  724. var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
  725. listElement.after($target);
  726. listElement.remove();
  727. taSelection.setSelectionToElementEnd($target.find('li')[0]);
  728. };
  729. return function(taDefaultWrap){
  730. taDefaultWrap = taBrowserTag(taDefaultWrap);
  731. return function(command, showUI, options){
  732. var i, $target, html, _nodes, next;
  733. var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  734. var selectedElement = taSelection.getSelectionElement();
  735. var $selected = angular.element(selectedElement);
  736. if(selectedElement !== undefined){
  737. var tagName = selectedElement.tagName.toLowerCase();
  738. if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
  739. var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
  740. if(tagName === selfTag){
  741. // if all selected then we should remove the list
  742. // grab all li elements and convert to taDefaultWrap tags
  743. return listToDefault($selected, taDefaultWrap);
  744. }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){
  745. // catch for the previous statement if only one li exists
  746. return listToDefault($selected.parent(), taDefaultWrap);
  747. }else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){
  748. // catch for the previous statement if only one li exists
  749. return listToList($selected.parent(), selfTag);
  750. }else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
  751. // if it's one of those block elements we have to change the contents
  752. // if it's a ol/ul we are changing from one to the other
  753. if(tagName === 'ol' || tagName === 'ul'){
  754. return listToList($selected, selfTag);
  755. }else{
  756. var childBlockElements = false;
  757. angular.forEach($selected.children(), function(elem){
  758. if(elem.tagName.match(BLOCKELEMENTS)) {
  759. childBlockElements = true;
  760. }
  761. });
  762. if(childBlockElements){
  763. return childElementsToList($selected.children(), $selected, selfTag);
  764. }else{
  765. return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
  766. }
  767. }
  768. }else if(tagName.match(BLOCKELEMENTS)){
  769. // if we get here then all the contents of the ta-bind are selected
  770. _nodes = taSelection.getOnlySelectedElements();
  771. if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
  772. if(_nodes[0].tagName.toLowerCase() === selfTag){
  773. // remove
  774. return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
  775. }else{
  776. return listToList(angular.element(_nodes[0]), selfTag);
  777. }
  778. }else{
  779. html = '';
  780. var $nodes = [];
  781. for(i = 0; i < _nodes.length; i++){
  782. /* istanbul ignore else: catch for real-world can't make it occur in testing */
  783. if(_nodes[i].nodeType !== 3){
  784. var $n = angular.element(_nodes[i]);
  785. html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
  786. $nodes.unshift($n);
  787. }
  788. }
  789. $target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
  790. $nodes.pop().replaceWith($target);
  791. angular.forEach($nodes, function($node){ $node.remove(); });
  792. }
  793. taSelection.setSelectionToElementEnd($target[0]);
  794. return;
  795. }
  796. }else if(command.toLowerCase() === 'formatblock'){
  797. var optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
  798. if(tagName === 'li') $target = $selected.parent();
  799. else $target = $selected;
  800. // find the first blockElement
  801. while(!$target[0].tagName.match(BLOCKELEMENTS)){
  802. $target = $target.parent();
  803. tagName = $target[0].tagName.toLowerCase();
  804. }
  805. if(tagName === optionsTagName){
  806. // $target is wrap element
  807. _nodes = $target.children();
  808. var hasBlock = false;
  809. for(i = 0; i < _nodes.length; i++){
  810. hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
  811. }
  812. if(hasBlock){
  813. $target.after(_nodes);
  814. next = $target.next();
  815. $target.remove();
  816. $target = next;
  817. }else{
  818. defaultWrapper.append($target[0].childNodes);
  819. $target.after(defaultWrapper);
  820. $target.remove();
  821. $target = defaultWrapper;
  822. }
  823. }else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
  824. //unwrap logic for parent
  825. var blockElement = $target.parent();
  826. var contents = blockElement.contents();
  827. for(i = 0; i < contents.length; i ++){
  828. /* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
  829. if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
  830. defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  831. defaultWrapper[0].innerHTML = contents[i].outerHTML;
  832. contents[i] = defaultWrapper[0];
  833. }
  834. blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
  835. }
  836. blockElement.remove();
  837. }else if(tagName.match(LISTELEMENTS)){
  838. // wrapping a list element
  839. $target.wrap(options);
  840. }else{
  841. // default wrap behaviour
  842. _nodes = taSelection.getOnlySelectedElements();
  843. if(_nodes.length === 0) _nodes = [$target[0]];
  844. // find the parent block element if any of the nodes are inline or text
  845. var inlineNodePresent = false;
  846. angular.forEach(_nodes, function(node){
  847. if(node.nodeType === 3 || !node.tagName.match(BLOCKELEMENTS)){
  848. inlineNodePresent = true;
  849. }
  850. });
  851. if(inlineNodePresent){
  852. while(_nodes[0].nodeType === 3 || !_nodes[0].tagName.match(BLOCKELEMENTS)){
  853. _nodes = [_nodes[0].parentNode];
  854. }
  855. }
  856. if(angular.element(_nodes[0]).hasClass('ta-bind')){
  857. $target = angular.element(options);
  858. $target[0].innerHTML = _nodes[0].innerHTML;
  859. _nodes[0].innerHTML = $target[0].outerHTML;
  860. }else if(optionsTagName === 'blockquote'){
  861. // blockquotes wrap other block elements
  862. html = '';
  863. for(i = 0; i < _nodes.length; i++){
  864. html += _nodes[i].outerHTML;
  865. }
  866. $target = angular.element(options);
  867. $target[0].innerHTML = html;
  868. _nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
  869. angular.forEach(_nodes, function(node){
  870. node.parentNode.removeChild(node);
  871. });
  872. }
  873. else {
  874. // regular block elements replace other block elements
  875. for(i = 0; i < _nodes.length; i++){
  876. $target = angular.element(options);
  877. $target[0].innerHTML = _nodes[i].innerHTML;
  878. _nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
  879. _nodes[i].parentNode.removeChild(_nodes[i]);
  880. }
  881. }
  882. }
  883. taSelection.setSelectionToElementEnd($target[0]);
  884. return;
  885. }
  886. }
  887. try{
  888. $document[0].execCommand(command, showUI, options);
  889. }catch(e){}
  890. };
  891. };
  892. }]).directive('taBind', ['taSanitize', '$timeout', '$window', '$document', 'taFixChrome', 'taBrowserTag', 'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
  893. function(taSanitize, $timeout, $window, $document, taFixChrome, taBrowserTag, taSelection, taSelectableElements, taApplyCustomRenderers, taOptions){
  894. // Uses for this are textarea or input with ng-model and ta-bind='text'
  895. // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
  896. return {
  897. require: 'ngModel',
  898. scope: {},
  899. link: function(scope, element, attrs, ngModel){
  900. // the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
  901. var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
  902. var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
  903. var _isReadonly = false;
  904. var _focussed = false;
  905. var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
  906. // defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
  907. // non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
  908. var _defaultVal, _defaultTest;
  909. // set the default to be a paragraph value
  910. if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
  911. /* istanbul ignore next: ie specific test */
  912. if(attrs.taDefaultWrap === ''){
  913. _defaultVal = '';
  914. _defaultTest = (ie === undefined)? '<div><br></div>' : (ie >= 11)? '<p><br></p>' : (ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
  915. }else{
  916. _defaultVal = (ie === undefined || ie >= 11)?
  917. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  918. (ie <= 8)?
  919. '<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
  920. '<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
  921. _defaultTest = (ie === undefined || ie >= 11)?
  922. '<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  923. (ie <= 8)?
  924. '<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
  925. '<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
  926. }
  927. element.addClass('ta-bind');
  928. // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
  929. var _compileHtml = function(){
  930. if(_isContentEditable) return element[0].innerHTML;
  931. if(_isInputFriendly) return element.val();
  932. throw ('textAngular Error: attempting to update non-editable taBind');
  933. };
  934. var _setViewValue = function(val){
  935. if(!val) val = _compileHtml();
  936. if(val === _defaultTest){
  937. // this avoids us from tripping the ng-pristine flag if we click in and out with out typing
  938. if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
  939. }else{
  940. if(ngModel.$viewValue !== val) ngModel.$setViewValue(val);
  941. }
  942. };
  943. //used for updating when inserting wrapped elements
  944. scope.$parent['updateTaBind' + (attrs.id || '')] = function(){
  945. if(!_isReadonly) _setViewValue();
  946. };
  947. //this code is used to update the models when data is entered/deleted
  948. if(_isInputFriendly){
  949. if(!_isContentEditable){
  950. element.on('paste cut', function(){
  951. // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
  952. if(!_isReadonly) $timeout(function(){
  953. ngModel.$setViewValue(_compileHtml());
  954. }, 0);
  955. });
  956. // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
  957. element.on('change blur', function(){
  958. if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
  959. });
  960. }else{
  961. element.on('cut', function(e){
  962. // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
  963. if(!_isReadonly)
  964. $timeout(function(){
  965. _setViewValue();
  966. }, 0);
  967. else e.preventDefault();
  968. });
  969. element.on('paste', function(e, eventData){
  970. /* istanbul ignore else: this is for catching the jqLite testing*/
  971. if(eventData) angular.extend(e, eventData);
  972. var text;
  973. // for non-ie
  974. if(e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData))
  975. text = (e.originalEvent || e).clipboardData.getData('text/plain');
  976. // for ie
  977. else if($window.clipboardData)
  978. text = $window.clipboardData.getData('Text');
  979. // if theres non text data and we aren't in read-only do default
  980. if(!text && !_isReadonly) return true;
  981. // prevent the default paste command
  982. e.preventDefault();
  983. if(!_isReadonly){
  984. var _working = angular.element('<div></div>');
  985. _working[0].innerHTML = text;
  986. // this strips out all HTML tags
  987. text = _working.text();
  988. if ($document[0].selection){
  989. var range = $document[0].selection.createRange();
  990. range.pasteHTML(text);
  991. }
  992. else{
  993. $document[0].execCommand('insertText', false, text);
  994. }
  995. _setViewValue();
  996. }
  997. });
  998. // all the code specific to cont