Built motion from commit b598105.|2.0.10
[motion2.git] / public / bower_components / textAngular / dist / textAngularSetup.js
1
2 // tests against the current jqLite/jquery implementation if this can be an element
3 function validElementString(string){
4         try{
5                 return angular.element(string).length !== 0;
6         }catch(any){
7                 return false;
8         }
9 }
10 // setup the global contstant functions for setting up the toolbar
11
12 // all tool definitions
13 var taTools = {};
14 /*
15         A tool definition is an object with the following key/value parameters:
16                 action: [function(deferred, restoreSelection)]
17                                 a function that is executed on clicking on the button - this will allways be executed using ng-click and will
18                                 overwrite any ng-click value in the display attribute.
19                                 The function is passed a deferred object ($q.defer()), if this is wanted to be used `return false;` from the action and
20                                 manually call `deferred.resolve();` elsewhere to notify the editor that the action has finished.
21                                 restoreSelection is only defined if the rangy library is included and it can be called as `restoreSelection()` to restore the users
22                                 selection in the WYSIWYG editor.
23                 display: [string]?
24                                 Optional, an HTML element to be displayed as the button. The `scope` of the button is the tool definition object with some additional functions
25                                 If set this will cause buttontext and iconclass to be ignored
26                 class: [string]?
27                                 Optional, if set will override the taOptions.classes.toolbarButton class.
28                 buttontext: [string]?
29                                 if this is defined it will replace the contents of the element contained in the `display` element
30                 iconclass: [string]?
31                                 if this is defined an icon (<i>) will be appended to the `display` element with this string as it's class
32                 tooltiptext: [string]?
33                                 Optional, a plain text description of the action, used for the title attribute of the action button in the toolbar by default.
34                 activestate: [function(commonElement)]?
35                                 this function is called on every caret movement, if it returns true then the class taOptions.classes.toolbarButtonActive
36                                 will be applied to the `display` element, else the class will be removed
37                 disabled: [function()]?
38                                 if this function returns true then the tool will have the class taOptions.classes.disabled applied to it, else it will be removed
39         Other functions available on the scope are:
40                 name: [string]
41                                 the name of the tool, this is the first parameter passed into taRegisterTool
42                 isDisabled: [function()]
43                                 returns true if the tool is disabled, false if it isn't
44                 displayActiveToolClass: [function(boolean)]
45                                 returns true if the tool is 'active' in the currently focussed toolbar
46                 onElementSelect: [Object]
47                                 This object contains the following key/value pairs and is used to trigger the ta-element-select event
48                                 element: [String]
49                                         an element name, will only trigger the onElementSelect action if the tagName of the element matches this string
50                                 filter: [function(element)]?
51                                         an optional filter that returns a boolean, if true it will trigger the onElementSelect.
52                                 action: [function(event, element, editorScope)]
53                                         the action that should be executed if the onElementSelect function runs
54 */
55 // name and toolDefinition to add into the tools available to be added on the toolbar
56 function registerTextAngularTool(name, toolDefinition){
57         if(!name || name === '' || taTools.hasOwnProperty(name)) throw('textAngular Error: A unique name is required for a Tool Definition');
58         if(
59                 (toolDefinition.display && (toolDefinition.display === '' || !validElementString(toolDefinition.display))) ||
60                 (!toolDefinition.display && !toolDefinition.buttontext && !toolDefinition.iconclass)
61         )
62                 throw('textAngular Error: Tool Definition for "' + name + '" does not have a valid display/iconclass/buttontext value');
63         taTools[name] = toolDefinition;
64 }
65
66 angular.module('textAngularSetup', [])
67 .constant('taRegisterTool', registerTextAngularTool)
68 .value('taTools', taTools)
69 // Here we set up the global display defaults, to set your own use a angular $provider#decorator.
70 .value('taOptions',  {
71         //////////////////////////////////////////////////////////////////////////////////////
72     // forceTextAngularSanitize
73     // set false to allow the textAngular-sanitize provider to be replaced
74     // with angular-sanitize or a custom provider.
75         forceTextAngularSanitize: true,
76         ///////////////////////////////////////////////////////////////////////////////////////
77         // keyMappings
78         // allow customizable keyMappings for specialized key boards or languages
79         //
80         // keyMappings provides key mappings that are attached to a given commandKeyCode.
81         // To modify a specific keyboard binding, simply provide function which returns true
82         // for the event you wish to map to.
83         // Or to disable a specific keyboard binding, provide a function which returns false.
84         // Note: 'RedoKey' and 'UndoKey' are internally bound to the redo and undo functionality.
85         // At present, the following commandKeyCodes are in use:
86         // 98, 'TabKey', 'ShiftTabKey', 105, 117, 'UndoKey', 'RedoKey'
87         //
88         // To map to an new commandKeyCode, add a new key mapping such as:
89         // {commandKeyCode: 'CustomKey', testForKey: function (event) {
90         //  if (event.keyCode=57 && event.ctrlKey && !event.shiftKey && !event.altKey) return true;
91         // } }
92         // to the keyMappings. This example maps ctrl+9 to 'CustomKey'
93         // Then where taRegisterTool(...) is called, add a commandKeyCode: 'CustomKey' and your
94         // tool will be bound to ctrl+9.
95         //
96         // To disble one of the already bound commandKeyCodes such as 'RedoKey' or 'UndoKey' add:
97         // {commandKeyCode: 'RedoKey', testForKey: function (event) { return false; } },
98         // {commandKeyCode: 'UndoKey', testForKey: function (event) { return false; } },
99         // to disable them.
100         //
101         keyMappings : [],
102         toolbar: [
103                 ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'],
104                 ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'],
105                 ['justifyLeft','justifyCenter','justifyRight','justifyFull','indent','outdent'],
106                 ['html', 'insertImage', 'insertLink', 'insertVideo', 'wordcount', 'charcount']
107         ],
108         classes: {
109                 focussed: "focussed",
110                 toolbar: "btn-toolbar",
111                 toolbarGroup: "btn-group",
112                 toolbarButton: "btn btn-default",
113                 toolbarButtonActive: "active",
114                 disabled: "disabled",
115                 textEditor: 'form-control',
116                 htmlEditor: 'form-control'
117         },
118         defaultTagAttributes : {
119                 a: {target:""}
120         },
121         setup: {
122                 // wysiwyg mode
123                 textEditorSetup: function($element){ /* Do some processing here */ },
124                 // raw html
125                 htmlEditorSetup: function($element){ /* Do some processing here */ }
126         },
127         defaultFileDropHandler:
128                 /* istanbul ignore next: untestable image processing */
129                 function(file, insertAction){
130                         var reader = new FileReader();
131                         if(file.type.substring(0, 5) === 'image'){
132                                 reader.onload = function() {
133                                         if(reader.result !== '') insertAction('insertImage', reader.result, true);
134                                 };
135
136                                 reader.readAsDataURL(file);
137                                 // NOTE: For async procedures return a promise and resolve it when the editor should update the model.
138                                 return true;
139                         }
140                         return false;
141                 }
142 })
143
144 // This is the element selector string that is used to catch click events within a taBind, prevents the default and $emits a 'ta-element-select' event
145 // these are individually used in an angular.element().find() call. What can go here depends on whether you have full jQuery loaded or just jQLite with angularjs.
146 // div is only used as div.ta-insert-video caught in filter.
147 .value('taSelectableElements', ['a','img'])
148
149 // This is an array of objects with the following options:
150 //                              selector: <string> a jqLite or jQuery selector string
151 //                              customAttribute: <string> an attribute to search for
152 //                              renderLogic: <function(element)>
153 // Both or one of selector and customAttribute must be defined.
154 .value('taCustomRenderers', [
155         {
156                 // Parse back out: '<div class="ta-insert-video" ta-insert-video src="' + urlLink + '" allowfullscreen="true" width="300" frameborder="0" height="250"></div>'
157                 // To correct video element. For now only support youtube
158                 selector: 'img',
159                 customAttribute: 'ta-insert-video',
160                 renderLogic: function(element){
161                         var iframe = angular.element('<iframe></iframe>');
162                         var attributes = element.prop("attributes");
163                         // loop through element attributes and apply them on iframe
164                         angular.forEach(attributes, function(attr) {
165                                 iframe.attr(attr.name, attr.value);
166                         });
167                         iframe.attr('src', iframe.attr('ta-insert-video'));
168                         element.replaceWith(iframe);
169                 }
170         }
171 ])
172
173 .value('taTranslations', {
174         // moved to sub-elements
175         //toggleHTML: "Toggle HTML",
176         //insertImage: "Please enter a image URL to insert",
177         //insertLink: "Please enter a URL to insert",
178         //insertVideo: "Please enter a youtube URL to embed",
179         html: {
180                 tooltip: 'Toggle html / Rich Text'
181         },
182         // tooltip for heading - might be worth splitting
183         heading: {
184                 tooltip: 'Heading '
185         },
186         p: {
187                 tooltip: 'Paragraph'
188         },
189         pre: {
190                 tooltip: 'Preformatted text'
191         },
192         ul: {
193                 tooltip: 'Unordered List'
194         },
195         ol: {
196                 tooltip: 'Ordered List'
197         },
198         quote: {
199                 tooltip: 'Quote/unquote selection or paragraph'
200         },
201         undo: {
202                 tooltip: 'Undo'
203         },
204         redo: {
205                 tooltip: 'Redo'
206         },
207         bold: {
208                 tooltip: 'Bold'
209         },
210         italic: {
211                 tooltip: 'Italic'
212         },
213         underline: {
214                 tooltip: 'Underline'
215         },
216         strikeThrough:{
217                 tooltip: 'Strikethrough'
218         },
219         justifyLeft: {
220                 tooltip: 'Align text left'
221         },
222         justifyRight: {
223                 tooltip: 'Align text right'
224         },
225         justifyFull: {
226                 tooltip: 'Justify text'
227         },
228         justifyCenter: {
229                 tooltip: 'Center'
230         },
231         indent: {
232                 tooltip: 'Increase indent'
233         },
234         outdent: {
235                 tooltip: 'Decrease indent'
236         },
237         clear: {
238                 tooltip: 'Clear formatting'
239         },
240         insertImage: {
241                 dialogPrompt: 'Please enter an image URL to insert',
242                 tooltip: 'Insert image',
243                 hotkey: 'the - possibly language dependent hotkey ... for some future implementation'
244         },
245         insertVideo: {
246                 tooltip: 'Insert video',
247                 dialogPrompt: 'Please enter a youtube URL to embed'
248         },
249         insertLink: {
250                 tooltip: 'Insert / edit link',
251                 dialogPrompt: "Please enter a URL to insert"
252         },
253         editLink: {
254                 reLinkButton: {
255                         tooltip: "Relink"
256                 },
257                 unLinkButton: {
258                         tooltip: "Unlink"
259                 },
260                 targetToggle: {
261                         buttontext: "Open in New Window"
262                 }
263         },
264         wordcount: {
265                 tooltip: 'Display words Count'
266         },
267                 charcount: {
268                 tooltip: 'Display characters Count'
269         }
270 })
271 .factory('taToolFunctions', ['$window','taTranslations', function($window, taTranslations) {
272         return {
273                 imgOnSelectAction: function(event, $element, editorScope){
274                         // setup the editor toolbar
275                         // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic/display
276                         var finishEdit = function(){
277                                 editorScope.updateTaBindtaTextElement();
278                                 editorScope.hidePopover();
279                         };
280                         event.preventDefault();
281                         editorScope.displayElements.popover.css('width', '375px');
282                         var container = editorScope.displayElements.popoverContainer;
283                         container.empty();
284                         var buttonGroup = angular.element('<div class="btn-group" style="padding-right: 6px;">');
285                         var fullButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">100% </button>');
286                         fullButton.on('click', function(event){
287                                 event.preventDefault();
288                                 $element.css({
289                                         'width': '100%',
290                                         'height': ''
291                                 });
292                                 finishEdit();
293                         });
294                         var halfButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">50% </button>');
295                         halfButton.on('click', function(event){
296                                 event.preventDefault();
297                                 $element.css({
298                                         'width': '50%',
299                                         'height': ''
300                                 });
301                                 finishEdit();
302                         });
303                         var quartButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">25% </button>');
304                         quartButton.on('click', function(event){
305                                 event.preventDefault();
306                                 $element.css({
307                                         'width': '25%',
308                                         'height': ''
309                                 });
310                                 finishEdit();
311                         });
312                         var resetButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">Reset</button>');
313                         resetButton.on('click', function(event){
314                                 event.preventDefault();
315                                 $element.css({
316                                         width: '',
317                                         height: ''
318                                 });
319                                 finishEdit();
320                         });
321                         buttonGroup.append(fullButton);
322                         buttonGroup.append(halfButton);
323                         buttonGroup.append(quartButton);
324                         buttonGroup.append(resetButton);
325                         container.append(buttonGroup);
326
327                         buttonGroup = angular.element('<div class="btn-group" style="padding-right: 6px;">');
328                         var floatLeft = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-left"></i></button>');
329                         floatLeft.on('click', function(event){
330                                 event.preventDefault();
331                                 // webkit
332                                 $element.css('float', 'left');
333                                 // firefox
334                                 $element.css('cssFloat', 'left');
335                                 // IE < 8
336                                 $element.css('styleFloat', 'left');
337                                 finishEdit();
338                         });
339                         var floatRight = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-right"></i></button>');
340                         floatRight.on('click', function(event){
341                                 event.preventDefault();
342                                 // webkit
343                                 $element.css('float', 'right');
344                                 // firefox
345                                 $element.css('cssFloat', 'right');
346                                 // IE < 8
347                                 $element.css('styleFloat', 'right');
348                                 finishEdit();
349                         });
350                         var floatNone = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-justify"></i></button>');
351                         floatNone.on('click', function(event){
352                                 event.preventDefault();
353                                 // webkit
354                                 $element.css('float', '');
355                                 // firefox
356                                 $element.css('cssFloat', '');
357                                 // IE < 8
358                                 $element.css('styleFloat', '');
359                                 finishEdit();
360                         });
361                         buttonGroup.append(floatLeft);
362                         buttonGroup.append(floatNone);
363                         buttonGroup.append(floatRight);
364                         container.append(buttonGroup);
365
366                         buttonGroup = angular.element('<div class="btn-group">');
367                         var remove = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-trash-o"></i></button>');
368                         remove.on('click', function(event){
369                                 event.preventDefault();
370                                 $element.remove();
371                                 finishEdit();
372                         });
373                         buttonGroup.append(remove);
374                         container.append(buttonGroup);
375
376                         editorScope.showPopover($element);
377                         editorScope.showResizeOverlay($element);
378                 },
379                 aOnSelectAction: function(event, $element, editorScope){
380                         // setup the editor toolbar
381                         // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic
382                         event.preventDefault();
383                         editorScope.displayElements.popover.css('width', '436px');
384                         var container = editorScope.displayElements.popoverContainer;
385                         container.empty();
386                         container.css('line-height', '28px');
387                         var link = angular.element('<a href="' + $element.attr('href') + '" target="_blank">' + $element.attr('href') + '</a>');
388                         link.css({
389                                 'display': 'inline-block',
390                                 'max-width': '200px',
391                                 'overflow': 'hidden',
392                                 'text-overflow': 'ellipsis',
393                                 'white-space': 'nowrap',
394                                 'vertical-align': 'middle'
395                         });
396                         container.append(link);
397                         var buttonGroup = angular.element('<div class="btn-group pull-right">');
398                         var reLinkButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on" title="' + taTranslations.editLink.reLinkButton.tooltip + '"><i class="fa fa-edit icon-edit"></i></button>');
399                         reLinkButton.on('click', function(event){
400                                 event.preventDefault();
401                                 var urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, $element.attr('href'));
402                                 if(urlLink && urlLink !== '' && urlLink !== 'http://'){
403                                         $element.attr('href', urlLink);
404                                         editorScope.updateTaBindtaTextElement();
405                                 }
406                                 editorScope.hidePopover();
407                         });
408                         buttonGroup.append(reLinkButton);
409                         var unLinkButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on" title="' + taTranslations.editLink.unLinkButton.tooltip + '"><i class="fa fa-unlink icon-unlink"></i></button>');
410                         // directly before this click event is fired a digest is fired off whereby the reference to $element is orphaned off
411                         unLinkButton.on('click', function(event){
412                                 event.preventDefault();
413                                 $element.replaceWith($element.contents());
414                                 editorScope.updateTaBindtaTextElement();
415                                 editorScope.hidePopover();
416                         });
417                         buttonGroup.append(unLinkButton);
418                         var targetToggle = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on">' + taTranslations.editLink.targetToggle.buttontext + '</button>');
419                         if($element.attr('target') === '_blank'){
420                                 targetToggle.addClass('active');
421                         }
422                         targetToggle.on('click', function(event){
423                                 event.preventDefault();
424                                 $element.attr('target', ($element.attr('target') === '_blank') ? '' : '_blank');
425                                 targetToggle.toggleClass('active');
426                                 editorScope.updateTaBindtaTextElement();
427                         });
428                         buttonGroup.append(targetToggle);
429                         container.append(buttonGroup);
430                         editorScope.showPopover($element);
431                 },
432                 extractYoutubeVideoId: function(url) {
433                         var re = /(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i;
434                         var match = url.match(re);
435                         return (match && match[1]) || null;
436                 }
437         };
438 }])
439 .run(['taRegisterTool', '$window', 'taTranslations', 'taSelection', 'taToolFunctions', '$sanitize', 'taOptions', function(taRegisterTool, $window, taTranslations, taSelection, taToolFunctions, $sanitize, taOptions){
440         // test for the version of $sanitize that is in use
441         // You can disable this check by setting taOptions.textAngularSanitize == false
442         var gv = {}; $sanitize('', gv);
443         /* istanbul ignore next, throws error */
444         if ((taOptions.forceTextAngularSanitize===true) && (gv.version !== 'taSanitize')) {
445                 throw angular.$$minErr('textAngular')("textAngularSetup", "The textAngular-sanitize provider has been replaced by another -- have you included angular-sanitize by mistake?");
446         }
447         taRegisterTool("html", {
448                 iconclass: 'fa fa-code',
449                 tooltiptext: taTranslations.html.tooltip,
450                 action: function(){
451                         this.$editor().switchView();
452                 },
453                 activeState: function(){
454                         return this.$editor().showHtml;
455                 }
456         });
457         // add the Header tools
458         // convenience functions so that the loop works correctly
459         var _retActiveStateFunction = function(q){
460                 return function(){ return this.$editor().queryFormatBlockState(q); };
461         };
462         var headerAction = function(){
463                 return this.$editor().wrapSelection("formatBlock", "<" + this.name.toUpperCase() +">");
464         };
465         angular.forEach(['h1','h2','h3','h4','h5','h6'], function(h){
466                 taRegisterTool(h.toLowerCase(), {
467                         buttontext: h.toUpperCase(),
468                         tooltiptext: taTranslations.heading.tooltip + h.charAt(1),
469                         action: headerAction,
470                         activeState: _retActiveStateFunction(h.toLowerCase())
471                 });
472         });
473         taRegisterTool('p', {
474                 buttontext: 'P',
475                 tooltiptext: taTranslations.p.tooltip,
476                 action: function(){
477                         return this.$editor().wrapSelection("formatBlock", "<P>");
478                 },
479                 activeState: function(){ return this.$editor().queryFormatBlockState('p'); }
480         });
481         // key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext
482         taRegisterTool('pre', {
483                 buttontext: 'pre',
484                 tooltiptext: taTranslations.pre.tooltip,
485                 action: function(){
486                         return this.$editor().wrapSelection("formatBlock", "<PRE>");
487                 },
488                 activeState: function(){ return this.$editor().queryFormatBlockState('pre'); }
489         });
490         taRegisterTool('ul', {
491                 iconclass: 'fa fa-list-ul',
492                 tooltiptext: taTranslations.ul.tooltip,
493                 action: function(){
494                         return this.$editor().wrapSelection("insertUnorderedList", null);
495                 },
496                 activeState: function(){ return this.$editor().queryCommandState('insertUnorderedList'); }
497         });
498         taRegisterTool('ol', {
499                 iconclass: 'fa fa-list-ol',
500                 tooltiptext: taTranslations.ol.tooltip,
501                 action: function(){
502                         return this.$editor().wrapSelection("insertOrderedList", null);
503                 },
504                 activeState: function(){ return this.$editor().queryCommandState('insertOrderedList'); }
505         });
506         taRegisterTool('quote', {
507                 iconclass: 'fa fa-quote-right',
508                 tooltiptext: taTranslations.quote.tooltip,
509                 action: function(){
510                         return this.$editor().wrapSelection("formatBlock", "<BLOCKQUOTE>");
511                 },
512                 activeState: function(){ return this.$editor().queryFormatBlockState('blockquote'); }
513         });
514         taRegisterTool('undo', {
515                 iconclass: 'fa fa-undo',
516                 tooltiptext: taTranslations.undo.tooltip,
517                 action: function(){
518                         return this.$editor().wrapSelection("undo", null);
519                 }
520         });
521         taRegisterTool('redo', {
522                 iconclass: 'fa fa-repeat',
523                 tooltiptext: taTranslations.redo.tooltip,
524                 action: function(){
525                         return this.$editor().wrapSelection("redo", null);
526                 }
527         });
528         taRegisterTool('bold', {
529                 iconclass: 'fa fa-bold',
530                 tooltiptext: taTranslations.bold.tooltip,
531                 action: function(){
532                         return this.$editor().wrapSelection("bold", null);
533                 },
534                 activeState: function(){
535                         return this.$editor().queryCommandState('bold');
536                 },
537                 commandKeyCode: 98
538         });
539         taRegisterTool('justifyLeft', {
540                 iconclass: 'fa fa-align-left',
541                 tooltiptext: taTranslations.justifyLeft.tooltip,
542                 action: function(){
543                         return this.$editor().wrapSelection("justifyLeft", null);
544                 },
545                 activeState: function(commonElement){
546                         /* istanbul ignore next: */
547                         if (commonElement && commonElement.nodeName === '#document') return false;
548                         var result = false;
549                         if (commonElement)
550                                 result =
551                                         commonElement.css('text-align') === 'left' ||
552                                         commonElement.attr('align') === 'left' ||
553                                         (
554                                                 commonElement.css('text-align') !== 'right' &&
555                                                 commonElement.css('text-align') !== 'center' &&
556                                                 commonElement.css('text-align') !== 'justify' && !this.$editor().queryCommandState('justifyRight') && !this.$editor().queryCommandState('justifyCenter')
557                                         ) && !this.$editor().queryCommandState('justifyFull');
558                         result = result || this.$editor().queryCommandState('justifyLeft');
559                         return result;
560                 }
561         });
562         taRegisterTool('justifyRight', {
563                 iconclass: 'fa fa-align-right',
564                 tooltiptext: taTranslations.justifyRight.tooltip,
565                 action: function(){
566                         return this.$editor().wrapSelection("justifyRight", null);
567                 },
568                 activeState: function(commonElement){
569                         /* istanbul ignore next: */
570                         if (commonElement && commonElement.nodeName === '#document') return false;
571                         var result = false;
572                         if(commonElement) result = commonElement.css('text-align') === 'right';
573                         result = result || this.$editor().queryCommandState('justifyRight');
574                         return result;
575                 }
576         });
577         taRegisterTool('justifyFull', {
578                 iconclass: 'fa fa-align-justify',
579                 tooltiptext: taTranslations.justifyFull.tooltip,
580                 action: function(){
581                         return this.$editor().wrapSelection("justifyFull", null);
582                 },
583                 activeState: function(commonElement){
584                         var result = false;
585                         if(commonElement) result = commonElement.css('text-align') === 'justify';
586                         result = result || this.$editor().queryCommandState('justifyFull');
587                         return result;
588                 }
589         });
590         taRegisterTool('justifyCenter', {
591                 iconclass: 'fa fa-align-center',
592                 tooltiptext: taTranslations.justifyCenter.tooltip,
593                 action: function(){
594                         return this.$editor().wrapSelection("justifyCenter", null);
595                 },
596                 activeState: function(commonElement){
597                         /* istanbul ignore next: */
598                         if (commonElement && commonElement.nodeName === '#document') return false;
599                         var result = false;
600                         if(commonElement) result = commonElement.css('text-align') === 'center';
601                         result = result || this.$editor().queryCommandState('justifyCenter');
602                         return result;
603                 }
604         });
605         taRegisterTool('indent', {
606                 iconclass: 'fa fa-indent',
607                 tooltiptext: taTranslations.indent.tooltip,
608                 action: function(){
609                         return this.$editor().wrapSelection("indent", null);
610                 },
611                 activeState: function(){
612                         return this.$editor().queryFormatBlockState('blockquote');
613                 },
614                 commandKeyCode: 'TabKey'
615         });
616         taRegisterTool('outdent', {
617                 iconclass: 'fa fa-outdent',
618                 tooltiptext: taTranslations.outdent.tooltip,
619                 action: function(){
620                         return this.$editor().wrapSelection("outdent", null);
621                 },
622                 activeState: function(){
623                         return false;
624                 },
625                 commandKeyCode: 'ShiftTabKey'
626         });
627         taRegisterTool('italics', {
628                 iconclass: 'fa fa-italic',
629                 tooltiptext: taTranslations.italic.tooltip,
630                 action: function(){
631                         return this.$editor().wrapSelection("italic", null);
632                 },
633                 activeState: function(){
634                         return this.$editor().queryCommandState('italic');
635                 },
636                 commandKeyCode: 105
637         });
638         taRegisterTool('underline', {
639                 iconclass: 'fa fa-underline',
640                 tooltiptext: taTranslations.underline.tooltip,
641                 action: function(){
642                         return this.$editor().wrapSelection("underline", null);
643                 },
644                 activeState: function(){
645                         return this.$editor().queryCommandState('underline');
646                 },
647                 commandKeyCode: 117
648         });
649         taRegisterTool('strikeThrough', {
650                 iconclass: 'fa fa-strikethrough',
651                 tooltiptext: taTranslations.strikeThrough.tooltip,
652                 action: function(){
653                         return this.$editor().wrapSelection("strikeThrough", null);
654                 },
655                 activeState: function(){
656                         return document.queryCommandState('strikeThrough');
657                 }
658         });
659         taRegisterTool('clear', {
660                 iconclass: 'fa fa-ban',
661                 tooltiptext: taTranslations.clear.tooltip,
662                 action: function(deferred, restoreSelection){
663                         var i;
664                         this.$editor().wrapSelection("removeFormat", null);
665                         var possibleNodes = angular.element(taSelection.getSelectionElement());
666                         // remove lists
667                         var removeListElements = function(list){
668                                 list = angular.element(list);
669                                 var prevElement = list;
670                                 angular.forEach(list.children(), function(liElem){
671                                         var newElem = angular.element('<p></p>');
672                                         newElem.html(angular.element(liElem).html());
673                                         prevElement.after(newElem);
674                                         prevElement = newElem;
675                                 });
676                                 list.remove();
677                         };
678                         angular.forEach(possibleNodes.find("ul"), removeListElements);
679                         angular.forEach(possibleNodes.find("ol"), removeListElements);
680                         if(possibleNodes[0].tagName.toLowerCase() === 'li'){
681                                 var _list = possibleNodes[0].parentNode.childNodes;
682                                 var _preLis = [], _postLis = [], _found = false;
683                                 for(i = 0; i < _list.length; i++){
684                                         if(_list[i] === possibleNodes[0]){
685                                                 _found = true;
686                                         }else if(!_found) _preLis.push(_list[i]);
687                                         else _postLis.push(_list[i]);
688                                 }
689                                 var _parent = angular.element(possibleNodes[0].parentNode);
690                                 var newElem = angular.element('<p></p>');
691                                 newElem.html(angular.element(possibleNodes[0]).html());
692                                 if(_preLis.length === 0 || _postLis.length === 0){
693                                         if(_postLis.length === 0) _parent.after(newElem);
694                                         else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]);
695
696                                         if(_preLis.length === 0 && _postLis.length === 0) _parent.remove();
697                                         else angular.element(possibleNodes[0]).remove();
698                                 }else{
699                                         var _firstList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
700                                         var _secondList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
701                                         for(i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i]));
702                                         for(i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i]));
703                                         _parent.after(_secondList);
704                                         _parent.after(newElem);
705                                         _parent.after(_firstList);
706                                         _parent.remove();
707                                 }
708                                 taSelection.setSelectionToElementEnd(newElem[0]);
709                         }
710                         // clear out all class attributes. These do not seem to be cleared via removeFormat
711                         var $editor = this.$editor();
712                         var recursiveRemoveClass = function(node){
713                                 node = angular.element(node);
714                                 if(node[0] !== $editor.displayElements.text[0]) node.removeAttr('class');
715                                 angular.forEach(node.children(), recursiveRemoveClass);
716                         };
717                         angular.forEach(possibleNodes, recursiveRemoveClass);
718                         // check if in list. If not in list then use formatBlock option
719                         if(possibleNodes[0].tagName.toLowerCase() !== 'li' &&
720                                 possibleNodes[0].tagName.toLowerCase() !== 'ol' &&
721                                 possibleNodes[0].tagName.toLowerCase() !== 'ul') this.$editor().wrapSelection("formatBlock", "default");
722                         restoreSelection();
723                 }
724         });
725
726
727         taRegisterTool('insertImage', {
728                 iconclass: 'fa fa-picture-o',
729                 tooltiptext: taTranslations.insertImage.tooltip,
730                 action: function(){
731                         var imageLink;
732                         imageLink = $window.prompt(taTranslations.insertImage.dialogPrompt, 'http://');
733                         if(imageLink && imageLink !== '' && imageLink !== 'http://'){
734                                 return this.$editor().wrapSelection('insertImage', imageLink, true);
735                         }
736                 },
737                 onElementSelect: {
738                         element: 'img',
739                         action: taToolFunctions.imgOnSelectAction
740                 }
741         });
742         taRegisterTool('insertVideo', {
743                 iconclass: 'fa fa-youtube-play',
744                 tooltiptext: taTranslations.insertVideo.tooltip,
745                 action: function(){
746                         var urlPrompt;
747                         urlPrompt = $window.prompt(taTranslations.insertVideo.dialogPrompt, 'https://');
748                         if (urlPrompt && urlPrompt !== '' && urlPrompt !== 'https://') {
749
750                                 videoId = taToolFunctions.extractYoutubeVideoId(urlPrompt);
751
752                                 /* istanbul ignore else: if it's invalid don't worry - though probably should show some kind of error message */
753                                 if(videoId){
754                                         // create the embed link
755                                         var urlLink = "https://www.youtube.com/embed/" + videoId;
756                                         // create the HTML
757                                         // for all options see: http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
758                                         // maxresdefault.jpg seems to be undefined on some.
759                                         var embed = '<img class="ta-insert-video" src="https://img.youtube.com/vi/' + videoId + '/hqdefault.jpg" ta-insert-video="' + urlLink + '" contenteditable="false" allowfullscreen="true" frameborder="0" />';
760                                         // insert
761                                         return this.$editor().wrapSelection('insertHTML', embed, true);
762                                 }
763                         }
764                 },
765                 onElementSelect: {
766                         element: 'img',
767                         onlyWithAttrs: ['ta-insert-video'],
768                         action: taToolFunctions.imgOnSelectAction
769                 }
770         });
771         taRegisterTool('insertLink', {
772                 tooltiptext: taTranslations.insertLink.tooltip,
773                 iconclass: 'fa fa-link',
774                 action: function(){
775                         var urlLink;
776                         urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, 'http://');
777                         if(urlLink && urlLink !== '' && urlLink !== 'http://'){
778                                 return this.$editor().wrapSelection('createLink', urlLink, true);
779                         }
780                 },
781                 activeState: function(commonElement){
782                         if(commonElement) return commonElement[0].tagName === 'A';
783                         return false;
784                 },
785                 onElementSelect: {
786                         element: 'a',
787                         action: taToolFunctions.aOnSelectAction
788                 }
789         });
790         taRegisterTool('wordcount', {
791                 display: '<div id="toolbarWC" style="display:block; min-width:100px;">Words: <span ng-bind="wordcount"></span></div>',
792                 disabled: true,
793                 wordcount: 0,
794                 activeState: function(){ // this fires on keyup
795                         var textElement = this.$editor().displayElements.text;
796                         /* istanbul ignore next: will default to '' when undefined */
797                         var workingHTML = textElement[0].innerHTML || '';
798                         var noOfWords = 0;
799
800                         /* istanbul ignore if: will default to '' when undefined */
801                         if (workingHTML.replace(/\s*<[^>]*?>\s*/g, '') !== '') {
802                                 noOfWords = workingHTML.replace(/<\/?(b|i|em|strong|span|u|strikethrough|a|img|small|sub|sup|label)( [^>*?])?>/gi, '') // remove inline tags without adding spaces
803                                                                                 .replace(/(<[^>]*?>\s*<[^>]*?>)/ig, ' ') // replace adjacent tags with possible space between with a space
804                                                                                 .replace(/(<[^>]*?>)/ig, '') // remove any singular tags
805                                                                                 .replace(/\s+/ig, ' ') // condense spacing
806                                                                                 .match(/\S+/g).length; // count remaining non-space strings
807                         }
808
809                         //Set current scope
810                         this.wordcount = noOfWords;
811                         //Set editor scope
812                         this.$editor().wordcount = noOfWords;
813
814                         return false;
815                 }
816         });
817         taRegisterTool('charcount', {
818                 display: '<div id="toolbarCC" style="display:block; min-width:120px;">Characters: <span ng-bind="charcount"></span></div>',
819                 disabled: true,
820                 charcount: 0,
821                 activeState: function(){ // this fires on keyup
822                         var textElement = this.$editor().displayElements.text;
823                         var sourceText = textElement[0].innerText || textElement[0].textContent; // to cover the non-jquery use case.
824
825                         // Caculate number of chars
826                         var noOfChars = sourceText.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g,' ').replace(/\s+$/g, ' ').length;
827                         //Set current scope
828                         this.charcount = noOfChars;
829                         //Set editor scope
830                         this.$editor().charcount = noOfChars;
831                         return false;
832                 }
833         });
834 }]);