/* toc-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', 'aloha/plugin', 'jquery', 'ui/ui', 'ui/button', 'i18n!toc/nls/i18n', 'i18n!aloha/nls/i18n', 'aloha/console' ], function( Aloha, Plugin, jQuery, Ui, Button, i18n, i18nCore, console ) { 'use strict'; var GENTICS = window.GENTICS, namespace = 'toc', $containers = null, allTocs = []; /* helper functions */ function last(a) { return a[a.length - 1]; } function head(a) { return a[0]; } function tail(a) { return a.slice(1); } function indexOf(a, item) { return detect(a, function(cmp){ return cmp === item; }); } function detect(a, f) { for (var i = 0; i < a.length; i++) { if (f(a[i])) { return a[i]; } } return null; } function map(a, f) { var result = []; for (var i = 0; i < a.length; i++) { result.push(f(a[i])); } return result; } function each(a, f) { map(a, f); } /** * register the plugin with unique name */ return Plugin.create(namespace, { minEntries: 0, updateInterval: 5000, config: ['toc'], init: function () { var that = this; if ( typeof this.settings.minEntries === 'undefined' ) { this.settings.minEntries = this.minEntries; } if ( typeof this.settings.updateInterval === 'undefined' ) { this.settings.updateInterval = this.updateInterval; } Aloha.bind( 'aloha-editable-activated', function ( event, rangeObject ) { if (Aloha.activeEditable) { that.cfg = that.getEditableConfig( Aloha.activeEditable.obj ); if ( jQuery.inArray( 'toc', that.cfg ) != -1 ) { that._insertTocButton.show(true); } else { that._insertTocButton.show(false); return; } } }); this.initButtons(); jQuery(document).ready(function(){ that.spawn(); }); }, initButtons: function () { var that = this; this._insertTocButton = Ui.adopt("insertToc", Button, { tooltip: i18n.t('button.addtoc.tooltip'), icon: 'aloha-icon aloha-icon-orderedlist', scope: 'Aloha.continuoustext', click: function () { that.insertAtSelection($containers); } }); }, register: function ($c) { $containers = $c; }, /** * inserts a new TOC at the current selection */ insertAtSelection: function($containers){ $containers = $containers || editableContainers(); var id = generateId('toc'); // we start out with an empty ordered list var $tocElement = jQuery("
    "). attr('id', id).attr('contentEditable', 'false'); var range = Aloha.Selection.getRangeObject(); var tocEditable = Aloha.activeEditable; var $tocContainer = jQuery(document.getElementById(tocEditable.getId())); GENTICS.Utils.Dom.insertIntoDOM($tocElement, range, $tocContainer); this.create(id).register($containers).update().tickTock(); }, /** * Spawn containers for all ols with the toc_root class. */ spawn: function ($ctx, $containers) { $ctx = $ctx || jQuery('body'); $containers = $containers || editableContainers(); $ctx.find('ol.toc_root').each(function(){ var id = jQuery(this).attr('id'); if (!id) { id = generateId('toc'); jQuery(this).attr('id', id); } that.create(id).register($containers).tickTock(); }); }, create: function (id) { allTocs.push(this); return { 'id': id, '$containers': jQuery(), 'settings': this.settings, /** * find the TOC root element for this instance */ root: function(){ return jQuery(document.getElementById(this.id)); }, /** * registers the given containers with the TOC. a * container is an element that may begin or contain * sections. Note: use .live on all [contenteditable=true] * to catch dynamically added editables. * the same containers can be passed in multiple times. they will * be registered only once. */ register: function ($containers){ var self = this; // the .add() method ensures that the $containers will be in // document order (required for correct TOC order) self.$containers = self.$containers.add($containers); self.$containers.filter(function(){ return !jQuery(this).data(namespace + '.' + self.id + '.listening'); }).each(function(){ var $container = jQuery(this); $container.data(namespace + '.' + self.id + '.listening', true); $container.bind('blur', function(){ self.cleanupIds($container.get(0)); self.update($container); }); }); return self; }, tickTock: function (interval) { var self = this; interval = interval || this.settings.updateInterval; if (!interval) { return; } window.setInterval(function(){ self.register(editableContainers()); // TODO: use the active editable instead of rebuilding // the entire TOC self.update(); }, interval); return self; }, /** * there are various ways which can cause duplicate ids on targets * (e.g. pressing enter in a heading and writing in a new line, or * copy&pasting). Passing a ctx updates only those elements * either inside or equal to it. * TODO: to be correct this should do * a $.contains(documentElement... */ cleanupIds: function (ctx) { var ids = [], that = this; this.headings(this.$containers).each(function(){ var id = jQuery(this).attr('id'); if ( (id && -1 != jQuery.inArray(id, ids)) || ( ctx && (jQuery.contains(ctx, this) || ctx === this))) { jQuery(this).attr('id', generateId(this)); } ids.push(id); }); return this; }, /** * Updates the TOC from the sections in the given context, or in * all containers that have been registered with this TOC, if no * context is given. */ update: function ($ctx) { var self = this; $ctx = $ctx || self.$containers; var outline = this.outline(self.$containers); var ancestors = [self.root()]; var prevSiblings = []; //TODO: handle TOC rebuilding more intelligently. currently, //the TOC is always rebuilt from scratch. last(ancestors).empty(); (function descend(outline) { var prevSiblings = []; each(outline, function (node) { var $section = head(node); var $entry = self.linkSection($section, ancestors, prevSiblings); ancestors.push($entry); descend(tail(node)); ancestors.pop(); prevSiblings.push($entry); }); })(tail(outline)); // count number of li's in the TOC, if less than minEntries, hide the TOC var minEntries = self.root().attr('data-TOC-minEntries') || this.settings.minEntries; if (self.root().find('li').length >= minEntries) { self.root().show(); } else { self.root().hide(); } return this; }, /** * updates or creates an entry in the TOC for the given section. */ linkSection: function ($section, ancestors, prevSiblings) { var linkId = $section.eq(0).attr('id'); if (!linkId) { linkId = generateId($section.get(0)); $section.eq(0).attr('id', linkId); } var $root = this.root(); var $entry = anchorFromLinkId($root, linkId); if (!$entry.length) { $entry = jQuery('
  1. '); } $entry.find('a'). attr('href', '#' + linkId). text($section.eq(0).text()); if (last(prevSiblings)) { last(prevSiblings).after($entry); } else { if (last(ancestors).get(0) == $root.get(0)) { $root.append($entry); } else { var $subToc = jQuery('
      ').append($entry); last(ancestors).append($subToc); } } return $entry; }, /** * returns a tree of sections in the given context. if the context * element(s) begin a section, they will be included. First element * of each branch in the tree is a $(section) or $() for the * root node. * TODO: http://www.w3.org/TR/html5/sections.html#outline */ outline: function (ctx) { var rootNode = [jQuery()]; var potentialParents = [rootNode]; this.headings(ctx).each(function(){ var $heading = jQuery(this); var nodeName = this.nodeName.toLowerCase(); var hLevels = ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']; var currLevel = jQuery.inArray(nodeName, hLevels); var higherEq = hLevels.slice(currLevel).join(','); var $section = $heading.nextUntil(higherEq).andSelf(); var node = [$section]; var parent = detect(potentialParents, function (parent) { var parentSection = parent[0]; return !parentSection.length || //top-level contains everything detect(parentSection, function (sectionElem) { return $heading.get(0) === sectionElem || jQuery.contains(sectionElem, $heading.get(0)); }); }); parent.push(node); potentialParents.splice(0, indexOf(potentialParents, parent), node); }); return rootNode; }, headings: function ($ctx) { return $ctx.find(':header').add($ctx.filter(':header')); } } } }); //-------------- module methods ----------------- function editableContainers () { return jQuery(map(Aloha.editables, function (editable) { return document.getElementById(editable.getId()); })); } function anchorFromLinkId ($ctx, linkId) { return linkId ? $ctx.find('a[href $= "#' + linkId + '"]') : jQuery(); } function linkIdFromAnchor ($anchor){ var href = $anchor.attr('href'); return href ? href.match(/#(.*?)$/)[1] : null; } function generateId (elemOrText) { var validId; if (typeof elemOrText == "object") { validId = jQuery(elemOrText).text(). replace(/[^a-zA-Z-]+/g, '-'). replace(/^[^a-zA-Z]+/, ''); } else if (elemOrText) { validId = elemOrText; } for (var uniquifier = 0;; uniquifier++) { var uniqueId = validId; if (uniquifier) { uniqueId += '-' + uniquifier; } var conflict = document.getElementById(uniqueId); if ( !conflict || ( typeof elemOrText == "object" && conflict === elemOrText)) { return uniqueId; } } //unreachable } });