/* numerated-headers-plugin.js is part of Aloha Editor project http://aloha-editor.org * * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria. * Contributors http://aloha-editor.org/contribution.php * * Aloha Editor is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * Aloha Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * As an additional permission to the GNU GPL version 2, you may distribute * non-source (e.g., minimized or compacted) forms of the Aloha-Editor * source code without the copy of the GNU GPL normally required, * provided you include this license notice and a URL through which * recipients can access the Corresponding Source. */ define([ 'aloha/core', 'jquery', 'aloha/plugin', 'ui/ui', 'ui/toggleButton', 'i18n!numerated-headers/nls/i18n', 'i18n!aloha/nls/i18n' ], function ( Aloha, $, Plugin, Ui, ToggleButton, i18n, i18nCore ) { 'use strict'; /** * A cache of editable configuration. * @private * @type {object} */ var editableConfigurations = {}; Aloha.bind('aloha-editable-destroyed', function (event, editable) { delete editableConfigurations[editable.getId()]; }); return Plugin.create('numerated-headers', { config: { numeratedactive: true, headingselector: 'h1, h2, h3, h4, h5, h6', trailingdot: false }, /** * Initialize the plugin. */ init: function () { var that = this; this._formatNumeratedHeadersButton = Ui.adopt('formatNumeratedHeaders', ToggleButton, { tooltip: i18n.t('button.numeratedHeaders.tooltip'), icon: 'aloha-icon aloha-icon-numerated-headers', scope: 'Aloha.continuoustext', click: function () { var buttonPressed = that._formatNumeratedHeadersButton.getState(); if (!buttonPressed) { that.removeNumerations(); } else { that.createNumeratedHeaders(); } } }); // We need to bind to smart-content-changed event to recognize // backspace and delete interactions. Aloha.bind('aloha-smart-content-changed', function (event) { that.cleanNumerations(); if (that.showNumbers()) { that.createNumeratedHeaders(); } }); // We need to listen to that event, when a block is formatted to // header format. smart-content-changed would be not fired in // that case Aloha.bind('aloha-format-block', function () { that.cleanNumerations(); if (that.showNumbers()) { that.createNumeratedHeaders(); } }); Aloha.bind('aloha-editable-activated', function (event) { if (that.isNumeratingOn()) { that._formatNumeratedHeadersButton.show(); that.initForEditable(Aloha.activeEditable.obj); } else { that._formatNumeratedHeadersButton.hide(); } }); }, /** * Init the toggle button (and numerating) for the current editable, * if not yet done. * If numerating shall be on by default and was not turned on, numbers * will be created. */ initForEditable: function ($editable) { var flag = $editable.attr('aloha-numerated-headers'); if (flag !== 'true' && flag !== 'false') { flag = (true === this.getCurrentConfig().numeratedactive) ? 'true' : 'false'; $editable.attr('aloha-numerated-headers', flag); } if (flag === 'true') { this.createNumeratedHeaders(); this._formatNumeratedHeadersButton.setState(true); } else { this._formatNumeratedHeadersButton.setState(false); } }, /** * Get the config for the current editable */ getCurrentConfig: function () { var config = this.getEditableConfig(Aloha.activeEditable.obj); // normalize config (set default values) if (config.numeratedactive === true || config.numeratedactive === 'true' || config.numeratedactive === '1') { config.numeratedactive = true; } else { config.numeratedactive = false; } if (typeof config.headingselector !== 'string') { config.headingselector = 'h1, h2, h3, h4, h5, h6'; } config.headingselector = $.trim(config.headingselector); if (config.trailingdot === true || config.trailingdot === 'true' || config.trailingdot === '1') { config.trailingdot = true; } else { config.trailingdot = false; } return config; }, /** * Check whether numerating shall be possible in the current editable */ isNumeratingOn: function () { return this.getCurrentConfig().headingselector !== ''; }, /** * Check whether numbers shall currently be shown in the current * editable. */ showNumbers: function () { return ( Aloha.activeEditable && this.isNumeratingOn() && (Aloha.activeEditable.obj.attr('aloha-numerated-headers') === 'true') ); }, /** * Remove all annotations in the current editable. */ cleanNumerations: function () { var that = this; var active_editable_obj = this.getBaseElement(); if (!active_editable_obj) { return; } this._saveRemoveAnnotations($(active_editable_obj).find('span[role=annotation]')); }, /** * Savely removes a jQuery collection of annotations. * @param annotationcollection the collection of annotations. */ _saveRemoveAnnotations: function (annotationcollection) { var that = this; var range = Aloha.Selection.getRangeObject(); var rangemod = false; annotationcollection.each(function () { if (range.startContainer === this || $.inArray(this, $(range.startContainer).parents()) > -1) { range.startContainer = that._prevNode(this); range.startOffset = 0; rangemod = true; } if (range.startContainer === this.parentNode && range.startOffset >= $(this).index() && range.startOffset > 0) { range.startOffset --; rangemod = true; } //Check if the selection ends inside the annotation if (range.endContainer === this || $.inArray(this, $(range.endContainer).parents()) > -1) { range.endContainer = that._prevNode(this); range.endOffset = 0; rangemod = true; } if (range.endContainer === this.parentNode && range.endOffset >= $(this).index() && range.endOffset > 0) { range.endOffset --; rangemod = true; } $(this).remove(); }); if (rangemod === true) { range.update(); range.select(); } }, /** * Prepends the annotation to the given prependElement. */ _prependAnnotation: function (annotationcontent, prependElem) { var range = Aloha.Selection.getRangeObject(); var rangemod = false; if (range.startContainer === prependElem) { range.startOffset ++; rangemod = true; } if (range.endContainer === prependElem) { range.endOffset ++; rangemod = true; } $(prependElem).prepend('' + annotationcontent + ''); if (rangemod === true) { range.update(); range.select(); } }, /** * Navigates to the previous node. */ _prevNode: function (node) { var prev = node.previousSibling; if (!prev) { return node.parentNode; } while (prev.lastChild) { prev = prev.lastChild; } return prev; }, /** * Removed and disables numeration for the current editable. */ removeNumerations : function () { $(Aloha.activeEditable.obj).attr('aloha-numerated-headers', 'false'); this.cleanNumerations(); }, getBaseElement: function () { if (typeof this.baseobjectSelector !== 'undefined') { return ($(this.baseobjectSelector).length > 0) ? $(this.baseobjectSelector) : null; } return Aloha.activeEditable ? Aloha.activeEditable.obj : null; }, /* * checks if the given Object contains a note Tag that looks like this: * * * @param {HTMLElement} obj The DOM object to check. */ hasNote: function (obj) { if (!obj || $(obj).length <= 0) { return false; } return $(obj).find('span[role=annotation]').length > 0; }, /* * checks if the given Object has textual content. * A possible "" tag will be ignored * * @param {HTMLElement} obj The DOM object to check */ hasContent: function (obj) { if (!obj || 0 === $(obj).length) { return false; } // we have to check the content of this object without the annotation span var $objCleaned = $(obj).clone() .find('span[role=annotation]') .remove() .end(); // check for text, also in other possible sub tags return $.trim($objCleaned.text()).length > 0; }, createNumeratedHeaders: function () { var active_editable_obj = this.getBaseElement(); if (!active_editable_obj) { return; } var config = this.getCurrentConfig(); var headingselector = config.headingselector; var headers = active_editable_obj.find(headingselector); Aloha.activeEditable.obj.attr('aloha-numerated-headers', 'true'); if (typeof headers === 'undefined' || headers.length === 0) { return; } // base rank is the lowest rank of all selected headers var base_rank = 7; var that = this; headers.each(function () { if (that.hasContent(this)) { var current_rank = parseInt(this.nodeName.substr(1), 10); if (current_rank < base_rank) { base_rank = current_rank; } } }); if (base_rank > 6) { return; } var prev_rank = null, current_annotation = [], annotation_pos = 0, i; // initialize the base annotations for (i = 0; i < (6 - base_rank) + 1; i++) { current_annotation[i] = 0; } headers.each(function () { // build and count annotation only if there is content in this header if (that.hasContent(this)) { var current_rank = parseInt(this.nodeName.substr(1), 10); if (prev_rank === null && current_rank !== base_rank) { // when the first found header has a rank // different from the base rank, we omit it that._saveRemoveAnnotations($(this).find('span[role=annotation]')); return; } else if (prev_rank === null) { // increment the main annotation current_annotation[annotation_pos]++; } else if (current_rank > prev_rank) { // starts a sub title current_annotation[++annotation_pos]++; } else if (current_rank === prev_rank) { // continues subtitles current_annotation[annotation_pos]++; } else if (current_rank < prev_rank) { //goes back to a main title var current_pos = current_rank - base_rank; var j; for (j = annotation_pos; j > (current_pos); j--) { current_annotation[j] = 0; //reset current sub-annotation } annotation_pos = current_pos; current_annotation[annotation_pos]++; } prev_rank = current_rank; var annotation_result = '', i; if (config.trailingdot === true) { annotation_result = ''; for (i = 0; i < current_annotation.length; i++) { if (current_annotation[i] !== 0) { annotation_result += (current_annotation[i] + '.'); } } } else { annotation_result = current_annotation[0]; for (i = 1; i < current_annotation.length; i++) { if (current_annotation[i] !== 0) { annotation_result += ('.' + current_annotation[i]); } } } //We add a trailing non-breakable space to the annotation_result //to separate the annotation from the heading's text. annotation_result += ' '; if (that.hasNote(this)) { $(this).find('span[role=annotation]').html(annotation_result); } else { that._prependAnnotation(annotation_result, this); } } else { // no Content, so remove the Note, if there is one if (that.hasNote(this)) { that._saveRemoveAnnotations($(this).find('span[role=annotation]')); } } }); } }); });