textAngularSetup.js 31 KB

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