/* format-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('format/format-plugin', [ 'aloha', 'aloha/plugin', 'aloha/state-override', 'jquery', 'util/arrays', 'util/maps', 'ui/ui', 'ui/toggleButton', 'ui/port-helper-multi-split', 'PubSub', 'i18n!format/nls/i18n', 'i18n!aloha/nls/i18n', 'aloha/selection' ], function ( Aloha, Plugin, StateOverride, jQuery, Arrays, Maps, Ui, ToggleButton, MultiSplitButton, PubSub, i18n, i18nCore ) { 'use strict'; var GENTICS = window.GENTICS; var pluginNamespace = 'aloha-format'; var commandsByElement = { 'b': 'bold', 'strong': 'bold', 'i': 'italic', 'em': 'italic', 'del': 'strikethrough', 'sub': 'subscript', 'sup': 'superscript', 'u': 'underline', 's': 'strikethrough' }; var componentNameByElement = { 'strong': 'strong', 'em': 'emphasis', 's': 'strikethrough2' }; var textLevelSemantics = { 'u': true, 'em': true, 'strong': true, 'b': true, 'i': true, 'cite': true, 'q': true, 'code': true, 'abbr': true, 'del': true, 's': true, 'sub': true, 'sup': true }; var blockLevelSemantics = { 'p': true, 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, 'pre': true }; var interchangeableNodeNames = { "B": ["STRONG", "B"], "I": ["EM", "I"], "STRONG": ["STRONG", "B"], "EM": ["EM", "I"] }; function formatInsideTableWorkaround(button) { var selectedCells = jQuery('.aloha-cell-selected'); if (selectedCells.length > 0) { var cellMarkupCounter = 0; selectedCells.each(function () { var cellContent = jQuery(this).find('div'), cellMarkup = cellContent.find(button); if (cellMarkup.length > 0) { // unwrap all found markup text // text foo bar // and wrap the whole contents of the into tags // text foo bar cellMarkup.contents().unwrap(); cellMarkupCounter++; } cellContent.contents().wrap('<'+button+'>'); }); // remove all markup if all cells have markup if (cellMarkupCounter === selectedCells.length) { selectedCells.find(button).contents().unwrap(); } return true; } return false; } function textLevelButtonClickHandler(formatPlugin, button) { if (formatInsideTableWorkaround(button)) { return false; } formatPlugin.addMarkup( button ); return false; } function blockLevelButtonClickHandler(formatPlugin, button) { if (formatInsideTableWorkaround(button)) { return false; } formatPlugin.changeMarkup( button ); // setting the focus is needed for mozilla to have a working rangeObject.select() if (Aloha.activeEditable && jQuery.browser.mozilla) { Aloha.activeEditable.obj.focus(); } // triggered for numerated-headers plugin if (Aloha.activeEditable) { Aloha.trigger( 'aloha-format-block' ); } } function makeTextLevelButton(formatPlugin, button) { var command = commandsByElement[button]; var componentName = command; if (componentNameByElement.hasOwnProperty(button)) { componentName = componentNameByElement[button]; } var component = Ui.adopt(componentName, ToggleButton, { tooltip : i18n.t('button.' + button + '.tooltip'), icon: 'aloha-icon aloha-icon-' + componentName, scope: 'Aloha.continuoustext', click: function () { return textLevelButtonClickHandler(formatPlugin, button); } }); return component; } function makeBlockLevelButton(formatPlugin, button) { return { name: button, tooltip: i18n.t('button.' + button + '.tooltip'), iconClass: 'aloha-icon ' + i18n.t('aloha-large-icon-' + button), markup: jQuery('<' + button + '>'), click: function () { return blockLevelButtonClickHandler(formatPlugin, button); } }; } function makeRemoveFormatButton(formatPlugin, button) { return { name: button, text: i18n.t('button.' + button + '.text'), tooltip: i18n.t('button.' + button + '.tooltip'), wide: true, cls: 'aloha-ui-multisplit-fullwidth', click: function () { formatPlugin.removeFormat(); } }; } function changeMarkup(button) { Aloha.Selection.changeMarkupOnSelection(jQuery('<' + button + '>')); } function updateUiAfterMutation(formatPlugin, rangeObject) { // select the modified range rangeObject.select(); // update Button toggle state. We take 'Aloha.Selection.getRangeObject()' // because rangeObject is not up-to-date onSelectionChanged(formatPlugin, Aloha.Selection.getRangeObject()); } function format(formatPlugin, rangeObject, markup) { GENTICS.Utils.Dom.addMarkup(rangeObject, markup); updateUiAfterMutation(formatPlugin, rangeObject); } function addMarkup(button) { var formatPlugin = this; var markup = jQuery('<'+button+'>'); var rangeObject = Aloha.Selection.rangeObject; if ( typeof button === "undefined" || button == "" ) { return; } // check whether the markup is found in the range (at the start of the range) var nodeNames = interchangeableNodeNames[markup[0].nodeName] || [markup[0].nodeName]; var foundMarkup = rangeObject.findMarkup(function() { return -1 !== Arrays.indexOf(nodeNames, this.nodeName); }, Aloha.activeEditable.obj); if (foundMarkup) { // remove the markup if (rangeObject.isCollapsed()) { // when the range is collapsed, we remove exactly the one DOM element GENTICS.Utils.Dom.removeFromDOM(foundMarkup, rangeObject, true); } else { // the range is not collapsed, so we remove the markup from the range GENTICS.Utils.Dom.removeMarkup(rangeObject, jQuery(foundMarkup), Aloha.activeEditable.obj); } updateUiAfterMutation(formatPlugin, rangeObject); } else { // when the range is collapsed, extend it to a word if (rangeObject.isCollapsed()) { GENTICS.Utils.Dom.extendToWord(rangeObject); if (rangeObject.isCollapsed()) { if (StateOverride.enabled()) { StateOverride.setWithRangeObject( commandsByElement[button], rangeObject, function (command, rangeObject) { format(formatPlugin, rangeObject, markup); } ); return; } } } format(formatPlugin, rangeObject, markup); } } function onSelectionChanged(formatPlugin, rangeObject) { var effectiveMarkup, foundMultiSplit, i, j, multiSplitItem; jQuery.each(formatPlugin.buttons, function (index, button) { var statusWasSet = false; var nodeNames = interchangeableNodeNames[button.markup[0].nodeName] || [button.markup[0].nodeName]; for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) { effectiveMarkup = rangeObject.markupEffectiveAtStart[i]; for (j = 0; j < nodeNames.length; j++) { if (Aloha.Selection.standardTextLevelSemanticsComparator(effectiveMarkup, jQuery('<' + nodeNames[j] + '>'))) { button.handle.setState(true); statusWasSet = true; } } } if (!statusWasSet) { button.handle.setState(false); } }); if (formatPlugin.multiSplitItems.length > 0) { foundMultiSplit = false; // iterate over the markup elements for (i = 0; i < rangeObject.markupEffectiveAtStart.length && !foundMultiSplit; i++) { effectiveMarkup = rangeObject.markupEffectiveAtStart[i]; for (j = 0; j < formatPlugin.multiSplitItems.length && !foundMultiSplit; j++) { multiSplitItem = formatPlugin.multiSplitItems[j]; if (!multiSplitItem.markup) { continue; } // now check whether one of the multiSplitItems fits to the effective markup if (Aloha.Selection.standardTextLevelSemanticsComparator(effectiveMarkup, multiSplitItem.markup)) { formatPlugin.multiSplitButton.setActiveItem(multiSplitItem.name); foundMultiSplit = true; } } } if (!foundMultiSplit) { formatPlugin.multiSplitButton.setActiveItem(null); } } } /** * register the plugin with unique name */ return Plugin.create('format', { /** * default button configuration */ config: [ 'b', 'i', 'sub', 'sup', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'removeFormat' ], /** * available options / buttons * * @todo new buttons needed for 'code' */ availableButtons: [ 'u', 'strong', 'del', 'em', 'b', 'i', 's', 'sub', 'sup', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'removeFormat' ], /** * HotKeys used for special actions */ hotKey: { formatBold: 'ctrl+b', formatItalic: 'ctrl+i', formatParagraph: 'alt+ctrl+0', formatH1: 'alt+ctrl+1', formatH2: 'alt+ctrl+2', formatH3: 'alt+ctrl+3', formatH4: 'alt+ctrl+4', formatH5: 'alt+ctrl+5', formatH6: 'alt+ctrl+6', formatPre: 'ctrl+p', formatDel: 'ctrl+d', formatSub: 'alt+shift+s', formatSup: 'ctrl+shift+s' }, /** * Initialize the plugin and set initialize flag on true */ init: function () { // Prepare var me = this; if (typeof this.settings.hotKey !== 'undefined') { jQuery.extend(true, this.hotKey, this.settings.hotKey); } this.initButtons(); Aloha.bind('aloha-plugins-loaded', function () { // @todo add config option for sidebar panel me.initSidebar(Aloha.Sidebar.right); }); // apply specific configuration if an editable has been activated Aloha.bind('aloha-editable-activated',function (e, params) { me.applyButtonConfig(params.editable.obj); // handle hotKeys params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatBold, function() { me.addMarkup( 'b' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatItalic, function() { me.addMarkup( 'i' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatParagraph, function() { me.changeMarkup( 'p' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH1, function() { me.changeMarkup( 'h1' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH2, function() { me.changeMarkup( 'h2' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH3, function() { me.changeMarkup( 'h3' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH4, function() { me.changeMarkup( 'h4' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH5, function() { me.changeMarkup( 'h5' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatH6, function() { me.changeMarkup( 'h6' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatPre, function() { me.changeMarkup( 'pre' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatDel, function() { me.addMarkup( 'del' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatSub, function() { me.addMarkup( 'sub' ); return false; }); params.editable.obj.bind( 'keydown.aloha.format', me.hotKey.formatSup, function() { me.addMarkup( 'sup' ); return false; }); }); Aloha.bind('aloha-editable-deactivated',function (e, params) { params.editable.obj.unbind('keydown.aloha.format'); }); }, /** * applys a configuration specific for an editable * buttons not available in this configuration are hidden * @param {Object} id of the activated editable * @return void */ applyButtonConfig: function (obj) { var config = this.getEditableConfig(obj), button, i, len; if ( typeof config === 'object' ) { var config_old = []; jQuery.each(config, function(j, button) { if ( !(typeof j === 'number' && typeof button === 'string') ) { config_old.push(j); } }); if ( config_old.length > 0 ) { config = config_old; } } this.formatOptions = config; // now iterate all buttons and show/hide them according to the config for ( button in this.buttons) { if (this.buttons.hasOwnProperty(button)) { if (jQuery.inArray(button, config) !== -1) { this.buttons[button].handle.show(); } else { this.buttons[button].handle.hide(); } } } // and the same for multisplit items len = this.multiSplitItems.length; for (i = 0; i < len; i++) { if (jQuery.inArray(this.multiSplitItems[i].name, config) !== -1) { this.multiSplitButton.showItem(this.multiSplitItems[i].name); } else { this.multiSplitButton.hideItem(this.multiSplitItems[i].name); } } }, /** * initialize the buttons and register them on floating menu * @param event event object * @param editable current editable object */ initButtons: function () { var that = this; this.buttons = {}; this.multiSplitItems = []; jQuery.each(this.availableButtons, function(j, button) { var button_config = false; if (typeof j !== 'number' && typeof button !== 'string') { button_config = button; button = j; } if (textLevelSemantics[button]) { that.buttons[button] = { handle: makeTextLevelButton(that, button), markup: jQuery('<'+button+'>', {'class': button_config || ''}) }; } else if (blockLevelSemantics[button]) { that.multiSplitItems.push(makeBlockLevelButton(that, button)); } else if ('removeFormat' === button) { that.multiSplitItems.push(makeRemoveFormatButton(that, button)); } else { Aloha.log('warn', that, 'Button "' + button + '" is not defined'); } }); this.multiSplitButton = MultiSplitButton({ name: 'formatBlock', items: this.multiSplitItems, hideIfEmpty: true, scope: 'Aloha.continuoustext' }); PubSub.sub('aloha.selection.context-change', function(message) { onSelectionChanged(that, message.range); }); }, initSidebar: function ( sidebar ) { var pl = this; pl.sidebar = sidebar; sidebar.addPanel( { id : pl.nsClass( 'sidebar-panel-class' ), title : i18n.t( 'floatingmenu.tab.format' ), content : '', expanded : true, activeOn : this.formatOptions || false, onInit: function () { }, onActivate: function ( effective ) { var that = this; that.effective = effective; if ( !effective[0] ) { return; } that.format = effective[0].nodeName.toLowerCase(); var dom = jQuery('
').attr('class', pl.nsClass( 'target-container' )); var fieldset = jQuery('
'); fieldset.append(jQuery('' + that.format + ' ' + i18n.t( 'format.class.legend' )).append(jQuery('' + ''; if ( pl.config[that.format] && pl.config[that.format]['class'] ) { jQuery.each(pl.config[that.format]['class'], function(i ,v) { html += ''; }); } html += '
'; var that = this, content = this.setContent(html).content; jQuery( pl.nsSel( 'framename' ) ).live( 'keyup', function () { jQuery( that.effective ).attr( 'target', jQuery( this ).val().replace( '\"', '"' ).replace( "'", "'" ) ); } ); var that = this; that.effective = effective; jQuery( pl.nsSel( 'linkTitle' ) ).val( jQuery( that.effective ).attr( 'title' ) ); } } ); sidebar.show(); }, // duplicated code from link-plugin //Creates string with this component's namepsace prefixed the each classname nsClass: function () { var stringBuilder = [], prefix = pluginNamespace; jQuery.each( arguments, function () { stringBuilder.push( this == '' ? prefix : prefix + '-' + this ); } ); return jQuery.trim(stringBuilder.join(' ')); }, // duplicated code from link-plugin nsSel: function () { var stringBuilder = [], prefix = pluginNamespace; jQuery.each( arguments, function () { stringBuilder.push( '.' + ( this == '' ? prefix : prefix + '-' + this ) ); } ); return jQuery.trim(stringBuilder.join(' ')); }, addMarkup: addMarkup, changeMarkup: changeMarkup, /** * Removes all formatting from the current selection. */ removeFormat: function() { var formats = [ 'u', 'strong', 'em', 'b', 'i', 'q', 'del', 's', 'code', 'sub', 'sup', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'quote', 'blockquote' ], rangeObject = Aloha.Selection.rangeObject, i; // formats to be removed by the removeFormat button may now be configured using Aloha.settings.plugins.format.removeFormats = ['b', 'strong', ...] if (this.settings.removeFormats) { formats = this.settings.removeFormats; } if (rangeObject.isCollapsed()) { return; } for (i = 0; i < formats.length; i++) { GENTICS.Utils.Dom.removeMarkup(rangeObject, jQuery('<' + formats[i] + '>'), Aloha.activeEditable.obj); } // select the modified range rangeObject.select(); // TODO: trigger event - removed Format }, /** * toString method * @return string */ toString: function () { return 'format'; } }); });