2 // tests against the current jqLite/jquery implementation if this can be an element
3 function validElementString(string){
5 return angular.element(string).length !== 0;
10 // setup the global contstant functions for setting up the toolbar
12 // all tool definitions
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.
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
27 Optional, if set will override the taOptions.classes.toolbarButton class.
29 if this is defined it will replace the contents of the element contained in the `display` element
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:
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
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
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');
59 (toolDefinition.display && (toolDefinition.display === '' || !validElementString(toolDefinition.display))) ||
60 (!toolDefinition.display && !toolDefinition.buttontext && !toolDefinition.iconclass)
62 throw('textAngular Error: Tool Definition for "' + name + '" does not have a valid display/iconclass/buttontext value');
63 taTools[name] = toolDefinition;
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.
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 ///////////////////////////////////////////////////////////////////////////////////////
78 // allow customizable keyMappings for specialized key boards or languages
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'
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;
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.
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; } },
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']
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'
118 defaultTagAttributes : {
123 textEditorSetup: function($element){ /* Do some processing here */ },
125 htmlEditorSetup: function($element){ /* Do some processing here */ }
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);
136 reader.readAsDataURL(file);
137 // NOTE: For async procedures return a promise and resolve it when the editor should update the model.
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'])
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', [
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
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);
167 iframe.attr('src', iframe.attr('ta-insert-video'));
168 element.replaceWith(iframe);
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",
180 tooltip: 'Toggle html / Rich Text'
182 // tooltip for heading - might be worth splitting
190 tooltip: 'Preformatted text'
193 tooltip: 'Unordered List'
196 tooltip: 'Ordered List'
199 tooltip: 'Quote/unquote selection or paragraph'
217 tooltip: 'Strikethrough'
220 tooltip: 'Align text left'
223 tooltip: 'Align text right'
226 tooltip: 'Justify text'
232 tooltip: 'Increase indent'
235 tooltip: 'Decrease indent'
238 tooltip: 'Clear formatting'
241 dialogPrompt: 'Please enter an image URL to insert',
242 tooltip: 'Insert image',
243 hotkey: 'the - possibly language dependent hotkey ... for some future implementation'
246 tooltip: 'Insert video',
247 dialogPrompt: 'Please enter a youtube URL to embed'
250 tooltip: 'Insert / edit link',
251 dialogPrompt: "Please enter a URL to insert"
261 buttontext: "Open in New Window"
265 tooltip: 'Display words Count'
268 tooltip: 'Display characters Count'
271 .factory('taToolFunctions', ['$window','taTranslations', function($window, taTranslations) {
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();
280 event.preventDefault();
281 editorScope.displayElements.popover.css('width', '375px');
282 var container = editorScope.displayElements.popoverContainer;
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();
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();
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();
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();
321 buttonGroup.append(fullButton);
322 buttonGroup.append(halfButton);
323 buttonGroup.append(quartButton);
324 buttonGroup.append(resetButton);
325 container.append(buttonGroup);
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();
332 $element.css('float', 'left');
334 $element.css('cssFloat', 'left');
336 $element.css('styleFloat', 'left');
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();
343 $element.css('float', 'right');
345 $element.css('cssFloat', 'right');
347 $element.css('styleFloat', 'right');
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();
354 $element.css('float', '');
356 $element.css('cssFloat', '');
358 $element.css('styleFloat', '');
361 buttonGroup.append(floatLeft);
362 buttonGroup.append(floatNone);
363 buttonGroup.append(floatRight);
364 container.append(buttonGroup);
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();
373 buttonGroup.append(remove);
374 container.append(buttonGroup);
376 editorScope.showPopover($element);
377 editorScope.showResizeOverlay($element);
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;
386 container.css('line-height', '28px');
387 var link = angular.element('<a href="' + $element.attr('href') + '" target="_blank">' + $element.attr('href') + '</a>');
389 'display': 'inline-block',
390 'max-width': '200px',
391 'overflow': 'hidden',
392 'text-overflow': 'ellipsis',
393 'white-space': 'nowrap',
394 'vertical-align': 'middle'
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();
406 editorScope.hidePopover();
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();
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');
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();
428 buttonGroup.append(targetToggle);
429 container.append(buttonGroup);
430 editorScope.showPopover($element);
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;
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?");
447 taRegisterTool("html", {
448 iconclass: 'fa fa-code',
449 tooltiptext: taTranslations.html.tooltip,
451 this.$editor().switchView();
453 activeState: function(){
454 return this.$editor().showHtml;
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); };
462 var headerAction = function(){
463 return this.$editor().wrapSelection("formatBlock", "<" + this.name.toUpperCase() +">");
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())
473 taRegisterTool('p', {
475 tooltiptext: taTranslations.p.tooltip,
477 return this.$editor().wrapSelection("formatBlock", "<P>");
479 activeState: function(){ return this.$editor().queryFormatBlockState('p'); }
481 // key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext
482 taRegisterTool('pre', {
484 tooltiptext: taTranslations.pre.tooltip,
486 return this.$editor().wrapSelection("formatBlock", "<PRE>");
488 activeState: function(){ return this.$editor().queryFormatBlockState('pre'); }
490 taRegisterTool('ul', {
491 iconclass: 'fa fa-list-ul',
492 tooltiptext: taTranslations.ul.tooltip,
494 return this.$editor().wrapSelection("insertUnorderedList", null);
496 activeState: function(){ return this.$editor().queryCommandState('insertUnorderedList'); }
498 taRegisterTool('ol', {
499 iconclass: 'fa fa-list-ol',
500 tooltiptext: taTranslations.ol.tooltip,
502 return this.$editor().wrapSelection("insertOrderedList", null);
504 activeState: function(){ return this.$editor().queryCommandState('insertOrderedList'); }
506 taRegisterTool('quote', {
507 iconclass: 'fa fa-quote-right',
508 tooltiptext: taTranslations.quote.tooltip,
510 return this.$editor().wrapSelection("formatBlock", "<BLOCKQUOTE>");
512 activeState: function(){ return this.$editor().queryFormatBlockState('blockquote'); }
514 taRegisterTool('undo', {
515 iconclass: 'fa fa-undo',
516 tooltiptext: taTranslations.undo.tooltip,
518 return this.$editor().wrapSelection("undo", null);
521 taRegisterTool('redo', {
522 iconclass: 'fa fa-repeat',
523 tooltiptext: taTranslations.redo.tooltip,
525 return this.$editor().wrapSelection("redo", null);
528 taRegisterTool('bold', {
529 iconclass: 'fa fa-bold',
530 tooltiptext: taTranslations.bold.tooltip,
532 return this.$editor().wrapSelection("bold", null);
534 activeState: function(){
535 return this.$editor().queryCommandState('bold');
539 taRegisterTool('justifyLeft', {
540 iconclass: 'fa fa-align-left',
541 tooltiptext: taTranslations.justifyLeft.tooltip,
543 return this.$editor().wrapSelection("justifyLeft", null);
545 activeState: function(commonElement){
546 /* istanbul ignore next: */
547 if (commonElement && commonElement.nodeName === '#document') return false;
551 commonElement.css('text-align') === 'left' ||
552 commonElement.attr('align') === 'left' ||
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');
562 taRegisterTool('justifyRight', {
563 iconclass: 'fa fa-align-right',
564 tooltiptext: taTranslations.justifyRight.tooltip,
566 return this.$editor().wrapSelection("justifyRight", null);
568 activeState: function(commonElement){
569 /* istanbul ignore next: */
570 if (commonElement && commonElement.nodeName === '#document') return false;
572 if(commonElement) result = commonElement.css('text-align') === 'right';
573 result = result || this.$editor().queryCommandState('justifyRight');
577 taRegisterTool('justifyFull', {
578 iconclass: 'fa fa-align-justify',
579 tooltiptext: taTranslations.justifyFull.tooltip,
581 return this.$editor().wrapSelection("justifyFull", null);
583 activeState: function(commonElement){
585 if(commonElement) result = commonElement.css('text-align') === 'justify';
586 result = result || this.$editor().queryCommandState('justifyFull');
590 taRegisterTool('justifyCenter', {
591 iconclass: 'fa fa-align-center',
592 tooltiptext: taTranslations.justifyCenter.tooltip,
594 return this.$editor().wrapSelection("justifyCenter", null);
596 activeState: function(commonElement){
597 /* istanbul ignore next: */
598 if (commonElement && commonElement.nodeName === '#document') return false;
600 if(commonElement) result = commonElement.css('text-align') === 'center';
601 result = result || this.$editor().queryCommandState('justifyCenter');
605 taRegisterTool('indent', {
606 iconclass: 'fa fa-indent',
607 tooltiptext: taTranslations.indent.tooltip,
609 return this.$editor().wrapSelection("indent", null);
611 activeState: function(){
612 return this.$editor().queryFormatBlockState('blockquote');
614 commandKeyCode: 'TabKey'
616 taRegisterTool('outdent', {
617 iconclass: 'fa fa-outdent',
618 tooltiptext: taTranslations.outdent.tooltip,
620 return this.$editor().wrapSelection("outdent", null);
622 activeState: function(){
625 commandKeyCode: 'ShiftTabKey'
627 taRegisterTool('italics', {
628 iconclass: 'fa fa-italic',
629 tooltiptext: taTranslations.italic.tooltip,
631 return this.$editor().wrapSelection("italic", null);
633 activeState: function(){
634 return this.$editor().queryCommandState('italic');
638 taRegisterTool('underline', {
639 iconclass: 'fa fa-underline',
640 tooltiptext: taTranslations.underline.tooltip,
642 return this.$editor().wrapSelection("underline", null);
644 activeState: function(){
645 return this.$editor().queryCommandState('underline');
649 taRegisterTool('strikeThrough', {
650 iconclass: 'fa fa-strikethrough',
651 tooltiptext: taTranslations.strikeThrough.tooltip,
653 return this.$editor().wrapSelection("strikeThrough", null);
655 activeState: function(){
656 return document.queryCommandState('strikeThrough');
659 taRegisterTool('clear', {
660 iconclass: 'fa fa-ban',
661 tooltiptext: taTranslations.clear.tooltip,
662 action: function(deferred, restoreSelection){
664 this.$editor().wrapSelection("removeFormat", null);
665 var possibleNodes = angular.element(taSelection.getSelectionElement());
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;
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]){
686 }else if(!_found) _preLis.push(_list[i]);
687 else _postLis.push(_list[i]);
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]);
696 if(_preLis.length === 0 && _postLis.length === 0) _parent.remove();
697 else angular.element(possibleNodes[0]).remove();
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);
708 taSelection.setSelectionToElementEnd(newElem[0]);
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);
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");
727 taRegisterTool('insertImage', {
728 iconclass: 'fa fa-picture-o',
729 tooltiptext: taTranslations.insertImage.tooltip,
732 imageLink = $window.prompt(taTranslations.insertImage.dialogPrompt, 'http://');
733 if(imageLink && imageLink !== '' && imageLink !== 'http://'){
734 return this.$editor().wrapSelection('insertImage', imageLink, true);
739 action: taToolFunctions.imgOnSelectAction
742 taRegisterTool('insertVideo', {
743 iconclass: 'fa fa-youtube-play',
744 tooltiptext: taTranslations.insertVideo.tooltip,
747 urlPrompt = $window.prompt(taTranslations.insertVideo.dialogPrompt, 'https://');
748 if (urlPrompt && urlPrompt !== '' && urlPrompt !== 'https://') {
750 videoId = taToolFunctions.extractYoutubeVideoId(urlPrompt);
752 /* istanbul ignore else: if it's invalid don't worry - though probably should show some kind of error message */
754 // create the embed link
755 var urlLink = "https://www.youtube.com/embed/" + videoId;
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" />';
761 return this.$editor().wrapSelection('insertHTML', embed, true);
767 onlyWithAttrs: ['ta-insert-video'],
768 action: taToolFunctions.imgOnSelectAction
771 taRegisterTool('insertLink', {
772 tooltiptext: taTranslations.insertLink.tooltip,
773 iconclass: 'fa fa-link',
776 urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, 'http://');
777 if(urlLink && urlLink !== '' && urlLink !== 'http://'){
778 return this.$editor().wrapSelection('createLink', urlLink, true);
781 activeState: function(commonElement){
782 if(commonElement) return commonElement[0].tagName === 'A';
787 action: taToolFunctions.aOnSelectAction
790 taRegisterTool('wordcount', {
791 display: '<div id="toolbarWC" style="display:block; min-width:100px;">Words: <span ng-bind="wordcount"></span></div>',
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 || '';
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
810 this.wordcount = noOfWords;
812 this.$editor().wordcount = noOfWords;
817 taRegisterTool('charcount', {
818 display: '<div id="toolbarCC" style="display:block; min-width:120px;">Characters: <span ng-bind="charcount"></span></div>',
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.
825 // Caculate number of chars
826 var noOfChars = sourceText.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g,' ').replace(/\s+$/g, ' ').length;
828 this.charcount = noOfChars;
830 this.$editor().charcount = noOfChars;