PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/booktype/apps/edit/static/edit/js/aloha/plugins/extra/numerated-headers/lib/numerated-headers-plugin.js

https://gitlab.com/wilane/Booktype
JavaScript | 428 lines | 292 code | 37 blank | 99 comment | 69 complexity | 73432b20bd360db14d248abedc09bdd3 MD5 | raw file
  1. /* numerated-headers-plugin.js is part of Aloha Editor project http://aloha-editor.org
  2. *
  3. * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
  4. * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
  5. * Contributors http://aloha-editor.org/contribution.php
  6. *
  7. * Aloha Editor is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU General Public License
  9. * as published by the Free Software Foundation; either version 2
  10. * of the License, or any later version.
  11. *
  12. * Aloha Editor is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, write to the Free Software
  19. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. *
  21. * As an additional permission to the GNU GPL version 2, you may distribute
  22. * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
  23. * source code without the copy of the GNU GPL normally required,
  24. * provided you include this license notice and a URL through which
  25. * recipients can access the Corresponding Source.
  26. */
  27. define([
  28. 'aloha/core',
  29. 'aloha/ephemera',
  30. 'jquery',
  31. 'aloha/plugin',
  32. 'ui/ui',
  33. 'ui/toggleButton',
  34. 'util/browser',
  35. 'i18n!numerated-headers/nls/i18n',
  36. 'i18n!aloha/nls/i18n'
  37. ], function (
  38. Aloha,
  39. Ephemera,
  40. $,
  41. Plugin,
  42. Ui,
  43. ToggleButton,
  44. Browser,
  45. i18n,
  46. i18nCore
  47. ) {
  48. 'use strict';
  49. /**
  50. * A cache of editable configuration.
  51. * @private
  52. * @type {object<string, object>}
  53. */
  54. var editableConfigurations = {};
  55. Aloha.bind('aloha-editable-destroyed', function (event, editable) {
  56. delete editableConfigurations[editable.getId()];
  57. });
  58. return Plugin.create('numerated-headers', {
  59. config: {
  60. numeratedactive: true,
  61. headingselector: 'h1, h2, h3, h4, h5, h6',
  62. trailingdot: false
  63. },
  64. /**
  65. * Initialize the plugin.
  66. */
  67. init: function () {
  68. var that = this;
  69. this._formatNumeratedHeadersButton = Ui.adopt('formatNumeratedHeaders',
  70. ToggleButton, {
  71. tooltip: i18n.t('button.numeratedHeaders.tooltip'),
  72. icon: 'aloha-icon aloha-icon-numerated-headers',
  73. scope: 'Aloha.continuoustext',
  74. click: function () {
  75. var buttonPressed = that._formatNumeratedHeadersButton.getState();
  76. if (!buttonPressed) {
  77. that.removeNumerations();
  78. } else {
  79. that.createNumeratedHeaders();
  80. }
  81. }
  82. });
  83. // We need to bind to smart-content-changed event to recognize
  84. // backspace and delete interactions.
  85. Aloha.bind('aloha-smart-content-changed', function (event) {
  86. that.cleanNumerations();
  87. if (that.showNumbers()) {
  88. that.createNumeratedHeaders();
  89. }
  90. });
  91. // We need to listen to that event, when a block is formatted to
  92. // header format. smart-content-changed would be not fired in
  93. // that case
  94. Aloha.bind('aloha-format-block', function () {
  95. that.cleanNumerations();
  96. if (that.showNumbers()) {
  97. that.createNumeratedHeaders();
  98. }
  99. });
  100. Aloha.bind('aloha-editable-activated', function (event) {
  101. if (that.isNumeratingOn()) {
  102. that._formatNumeratedHeadersButton.show();
  103. that.initForEditable(Aloha.activeEditable.obj);
  104. } else {
  105. that._formatNumeratedHeadersButton.hide();
  106. }
  107. });
  108. },
  109. /**
  110. * Init the toggle button (and numerating) for the current editable,
  111. * if not yet done.
  112. * If numerating shall be on by default and was not turned on, numbers
  113. * will be created.
  114. */
  115. initForEditable: function ($editable) {
  116. var flag = $editable.attr('aloha-numerated-headers');
  117. if (flag !== 'true' && flag !== 'false') {
  118. flag = (true === this.getCurrentConfig().numeratedactive) ? 'true' : 'false';
  119. $editable.attr('aloha-numerated-headers', flag);
  120. }
  121. if (flag === 'true') {
  122. this.createNumeratedHeaders();
  123. this._formatNumeratedHeadersButton.setState(true);
  124. } else {
  125. this._formatNumeratedHeadersButton.setState(false);
  126. }
  127. },
  128. /**
  129. * Get the config for the current editable
  130. */
  131. getCurrentConfig: function () {
  132. var config = this.getEditableConfig(Aloha.activeEditable.obj);
  133. // normalize config (set default values)
  134. if (config.numeratedactive === true || config.numeratedactive === 'true' || config.numeratedactive === '1') {
  135. config.numeratedactive = true;
  136. } else {
  137. config.numeratedactive = false;
  138. }
  139. if (typeof config.headingselector !== 'string') {
  140. config.headingselector = 'h1, h2, h3, h4, h5, h6';
  141. }
  142. config.headingselector = $.trim(config.headingselector);
  143. if (config.trailingdot === true || config.trailingdot === 'true' || config.trailingdot === '1') {
  144. config.trailingdot = true;
  145. } else {
  146. config.trailingdot = false;
  147. }
  148. return config;
  149. },
  150. /**
  151. * Check whether numerating shall be possible in the current editable
  152. */
  153. isNumeratingOn: function () {
  154. return this.getCurrentConfig().headingselector !== '';
  155. },
  156. /**
  157. * Check whether numbers shall currently be shown in the current
  158. * editable.
  159. */
  160. showNumbers: function () {
  161. return (
  162. Aloha.activeEditable &&
  163. this.isNumeratingOn() &&
  164. (Aloha.activeEditable.obj.attr('aloha-numerated-headers') === 'true')
  165. );
  166. },
  167. /**
  168. * Remove all annotations in the current editable.
  169. */
  170. cleanNumerations: function () {
  171. var that = this;
  172. var active_editable_obj = this.getBaseElement();
  173. if (!active_editable_obj) {
  174. return;
  175. }
  176. $('div.aloha-numerated-headers-annotation-wrapper>span[role=annotation]').unwrap();
  177. this._safeRemoveAnnotations($(active_editable_obj).find('span[role=annotation]'));
  178. },
  179. /**
  180. * Safely removes a jQuery collection of annotations.
  181. * @param annotationcollection the collection of annotations.
  182. */
  183. _safeRemoveAnnotations: function (annotationcollection) {
  184. var that = this;
  185. var range = Aloha.Selection.getRangeObject();
  186. var rangemod = false;
  187. annotationcollection.each(function () {
  188. if (range.startContainer === this || $.inArray(this, $(range.startContainer).parents()) > -1) {
  189. range.startContainer = that._prevNode(this);
  190. range.startOffset = 0;
  191. rangemod = true;
  192. }
  193. if (range.startContainer === this.parentNode && range.startOffset >= $(this).index() && range.startOffset > 0) {
  194. range.startOffset --;
  195. rangemod = true;
  196. }
  197. //Check if the selection ends inside the annotation
  198. if (range.endContainer === this || $.inArray(this, $(range.endContainer).parents()) > -1) {
  199. range.endContainer = that._prevNode(this);
  200. range.endOffset = 0;
  201. rangemod = true;
  202. }
  203. if (range.endContainer === this.parentNode && range.endOffset >= $(this).index() && range.endOffset > 0) {
  204. range.endOffset --;
  205. rangemod = true;
  206. }
  207. $(this).remove();
  208. });
  209. if (rangemod === true) {
  210. range.update();
  211. range.select();
  212. }
  213. },
  214. /**
  215. * Prepends the annotation to the given prependElement.
  216. */
  217. _prependAnnotation: function (annotationcontent, prependElem) {
  218. var range = Aloha.Selection.getRangeObject();
  219. var rangemod = false;
  220. if (range.startContainer === prependElem) {
  221. range.startOffset ++;
  222. rangemod = true;
  223. }
  224. if (range.endContainer === prependElem) {
  225. range.endOffset ++;
  226. rangemod = true;
  227. }
  228. var annotation = $('<span role="annotation">' +
  229. annotationcontent + '</span>');
  230. var displayStyle = Browser.ie7 ? 'inline' : 'inline-block';
  231. var wrappedannotation=$('<div class="aloha-numerated-headers-annotation-wrapper" style="display: '+ displayStyle +';" contenteditable="false"></div>').append(annotation);
  232. Ephemera.markWrapper(wrappedannotation)
  233. $(prependElem).prepend(wrappedannotation);
  234. if (rangemod === true) {
  235. range.update();
  236. range.select();
  237. }
  238. },
  239. /**
  240. * Navigates to the previous node.
  241. */
  242. _prevNode: function (node) {
  243. var prev = node.previousSibling;
  244. if (!prev) {
  245. return node.parentNode;
  246. }
  247. while (prev.lastChild) {
  248. prev = prev.lastChild;
  249. }
  250. return prev;
  251. },
  252. /**
  253. * Removed and disables numeration for the current editable.
  254. */
  255. removeNumerations : function () {
  256. $(Aloha.activeEditable.obj).attr('aloha-numerated-headers', 'false');
  257. this.cleanNumerations();
  258. },
  259. getBaseElement: function () {
  260. if (typeof this.baseobjectSelector !== 'undefined') {
  261. return ($(this.baseobjectSelector).length > 0) ?
  262. $(this.baseobjectSelector) : null;
  263. }
  264. return Aloha.activeEditable ? Aloha.activeEditable.obj : null;
  265. },
  266. /*
  267. * checks if the given Object contains a note Tag that looks like this:
  268. * <span annotation=''>
  269. *
  270. * @param {HTMLElement} obj The DOM object to check.
  271. */
  272. hasNote: function (obj) {
  273. if (!obj || $(obj).length <= 0) {
  274. return false;
  275. }
  276. return $(obj).find('span[role=annotation]').length > 0;
  277. },
  278. /*
  279. * checks if the given Object has textual content.
  280. * A possible "<span annotation=''>" tag will be ignored
  281. *
  282. * @param {HTMLElement} obj The DOM object to check
  283. */
  284. hasContent: function (obj) {
  285. if (!obj || 0 === $(obj).length) {
  286. return false;
  287. }
  288. // we have to check the content of this object without the annotation span
  289. var $objCleaned = $(obj).clone()
  290. .find('span[role=annotation]')
  291. .remove()
  292. .end();
  293. // check for text, also in other possible sub tags
  294. return $.trim($objCleaned.text()).length > 0;
  295. },
  296. createNumeratedHeaders: function () {
  297. var active_editable_obj = this.getBaseElement();
  298. if (!active_editable_obj) {
  299. return;
  300. }
  301. var config = this.getCurrentConfig();
  302. var headingselector = config.headingselector;
  303. var headers = active_editable_obj.find(headingselector);
  304. Aloha.activeEditable.obj.attr('aloha-numerated-headers', 'true');
  305. if (typeof headers === 'undefined' || headers.length === 0) {
  306. return;
  307. }
  308. // base rank is the lowest rank of all selected headers
  309. var base_rank = 7;
  310. var that = this;
  311. headers.each(function () {
  312. if (that.hasContent(this)) {
  313. var current_rank = parseInt(this.nodeName.substr(1), 10);
  314. if (current_rank < base_rank) {
  315. base_rank = current_rank;
  316. }
  317. }
  318. });
  319. if (base_rank > 6) {
  320. return;
  321. }
  322. var prev_rank = null,
  323. current_annotation = [],
  324. annotation_pos = 0,
  325. i;
  326. // initialize the base annotations
  327. for (i = 0; i < (6 - base_rank) + 1; i++) {
  328. current_annotation[i] = 0;
  329. }
  330. headers.each(function () {
  331. // build and count annotation only if there is content in this header
  332. if (that.hasContent(this)) {
  333. var current_rank = parseInt(this.nodeName.substr(1), 10);
  334. if (prev_rank === null && current_rank !== base_rank) {
  335. // when the first found header has a rank
  336. // different from the base rank, we omit it
  337. that._safeRemoveAnnotations($(this).find('span[role=annotation]'));
  338. return;
  339. } else if (prev_rank === null) {
  340. // increment the main annotation
  341. current_annotation[annotation_pos]++;
  342. } else if (current_rank > prev_rank) {
  343. // starts a sub title
  344. current_annotation[++annotation_pos]++;
  345. } else if (current_rank === prev_rank) {
  346. // continues subtitles
  347. current_annotation[annotation_pos]++;
  348. } else if (current_rank < prev_rank) {
  349. //goes back to a main title
  350. var current_pos = current_rank - base_rank;
  351. var j;
  352. for (j = annotation_pos; j > (current_pos); j--) {
  353. current_annotation[j] = 0; //reset current sub-annotation
  354. }
  355. annotation_pos = current_pos;
  356. current_annotation[annotation_pos]++;
  357. }
  358. prev_rank = current_rank;
  359. var annotation_result = '', i;
  360. if (config.trailingdot === true) {
  361. annotation_result = '';
  362. for (i = 0; i < current_annotation.length; i++) {
  363. if (current_annotation[i] !== 0) {
  364. annotation_result += (current_annotation[i] + '.');
  365. }
  366. }
  367. } else {
  368. annotation_result = current_annotation[0];
  369. for (i = 1; i < current_annotation.length; i++) {
  370. if (current_annotation[i] !== 0) {
  371. annotation_result += ('.' + current_annotation[i]);
  372. }
  373. }
  374. }
  375. //We add a trailing non-breakable space to the annotation_result
  376. //to separate the annotation from the heading's text.
  377. annotation_result += '&nbsp;';
  378. if (that.hasNote(this)) {
  379. that._safeRemoveAnnotations($(this).find('span[role=annotation]'));
  380. }
  381. that._prependAnnotation(annotation_result, this);
  382. } else {
  383. // no Content, so remove the Note, if there is one
  384. if (that.hasNote(this)) {
  385. that._safeRemoveAnnotations($(this).find('span[role=annotation]'));
  386. }
  387. }
  388. });
  389. }
  390. });
  391. });