/***************************************************************************** * * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved. * * This software is distributed under the terms of the Kupu * License. See LICENSE.txt for license text. For a list of Kupu * Contributors see CREDITS.txt. * *****************************************************************************/ // $Id$ //---------------------------------------------------------------------------- // // Toolboxes // // These are addons for Kupu, simple plugins that implement a certain // interface to provide functionality and control view aspects. // //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- // Superclasses //---------------------------------------------------------------------------- function KupuTool() { /* Superclass (or actually more of an interface) for tools Tools must implement at least an initialize method and an updateState method, and can implement other methods to add certain extra functionality (e.g. createContextMenuElements). */ this.toolboxes = {}; // methods this.initialize = function(editor) { /* Initialize the tool. Obviously this can be overriden but it will do for the most simple cases */ this.editor = editor; }; this.registerToolBox = function(id, toolbox) { /* register a ui box Note that this needs to be called *after* the tool has been registered to the KupuEditor */ this.toolboxes[id] = toolbox; toolbox.initialize(this, this.editor); }; this.updateState = function(selNode, event) { /* Is called when user moves cursor to other element Calls the updateState for all toolboxes and may want perform some actions itself */ for (var id in this.toolboxes) { this.toolboxes[id].updateState(selNode, event); }; }; this.enable = function() { // Called when the tool is enabled after a form is dismissed. }; this.disable = function() { // Called when the tool is disabled (e.g. for a modal form) }; // private methods addEventHandler = addEventHandler; } function KupuToolBox() { /* Superclass for a user-interface object that controls a tool */ this.initialize = function(tool, editor) { /* store a reference to the tool and the editor */ this.tool = tool; this.editor = editor; }; this.updateState = function(selNode, event) { /* update the toolbox according to the current iframe's situation */ }; }; function noContextMenu(object) { /* Decorator for a tool to suppress the context menu */ object.createContextMenuElements = function(selNode, event) { return []; }; return object; } // Helper function for enabling/disabling tools function kupuButtonDisable(button) { button = button || this.button; if (button) { button.disabled = "disabled"; button.className += ' disabled'; } } function kupuButtonEnable(button) { button = button || this.button; if (button) { button.disabled = ""; button.className = button.className.replace(/ *\bdisabled\b/g, ''); } } //---------------------------------------------------------------------------- // Implementations //---------------------------------------------------------------------------- function KupuButton(buttonid, commandfunc, tool) { /* Base prototype for kupu button tools */ this.buttonid = buttonid; this.button = getFromSelector(buttonid); this.commandfunc = commandfunc; this.tool = tool; this.disable = kupuButtonDisable; this.enable = kupuButtonEnable; }; KupuButton.prototype = new KupuTool; KupuButton.prototype.initialize = function(editor) { this.editor = editor; if (!this.button) return; addEventHandler(this.button, 'click', this.execCommand, this); }; KupuButton.prototype.execCommand = function() { /* exec this button's command */ this.commandfunc(this, this.editor, this.tool); }; KupuButton.prototype.updateState = function(selNode, event) { /* override this in subclasses to determine whether a button should look 'pressed in' or not */ }; function KupuStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) { /* A button that can have two states (e.g. pressed and not-pressed) based on CSS classes */ this.buttonid = buttonid; this.button = getFromSelector(buttonid); this.commandfunc = commandfunc; this.checkfunc = checkfunc; this.offclass = offclass; this.onclass = onclass; this.pressed = false; this.execCommand = function() { /* exec this button's command */ this.button.className = (this.pressed ? this.offclass : this.onclass); this.pressed = !this.pressed; this.editor.focusDocument(); this.commandfunc(this, this.editor); }; this.updateState = function(selNode, event) { /* check if we need to be clicked or unclicked, and update accordingly if the state of the button should be changed, we set the class */ if (!this.button) return; var currclass = this.button.className; var newclass = null; if (this.checkfunc(selNode, this, this.editor, event)) { newclass = this.onclass; this.pressed = true; } else { newclass = this.offclass; this.pressed = false; }; if (currclass != newclass) { this.button.className = newclass; }; }; }; KupuStateButton.prototype = new KupuButton; /* Same as the state button, but the focusDocument call is delayed. * Mozilla&Firefox have a bug on windows which can cause a crash if you * change CSS positioning styles on an element which has focus. */ function KupuLateFocusStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) { KupuStateButton.apply(this, [buttonid, commandfunc, checkfunc, offclass, onclass]); this.execCommand = function() { /* exec this button's command */ this.button.className = (this.pressed ? this.offclass : this.onclass); this.pressed = !this.pressed; this.commandfunc(this, this.editor); this.editor.focusDocument(); }; } KupuLateFocusStateButton.prototype = new KupuStateButton; function KupuRemoveElementButton(buttonid, element_name, cssclass) { /* A button specialized in removing elements in the current node context. Typical usages include removing links, images, etc. */ this.button = getFromSelector(buttonid); this.onclass = 'invisible'; this.offclass = cssclass; this.pressed = false; this.commandfunc = function(button, editor) { editor.focusDocument(); editor.removeNearestParentOfType(editor.getSelectedNode(), element_name); editor.updateState(); }; this.checkfunc = function(currnode, button, editor, event) { var element = editor.getNearestParentOfType(currnode, element_name); return (element ? false : true); }; }; KupuRemoveElementButton.prototype = new KupuStateButton; function KupuUI(textstyleselectid) { /* View This is the main view, which controls most of the toolbar buttons. Even though this is probably never going to be removed from the view, it was easier to implement this as a plain tool (plugin) as well. */ // attributes this.tsselect = getFromSelector(textstyleselectid); var paraoptions = []; var tableoptions = []; var styleoptions = []; var tableoffset = 0; var styleoffset = 0; var tablegrp = null; this.optionstate = -1; this.otherstyle = null; this.tablestyles = {}; this.charstyles = {}; this.styles = {}; // use an object here so we can use the 'in' operator later on var blocktagre = /^(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center)$/i; var spanre = /^span\b/i; var tblre = /^thead|tbody|table|t[rdh]\b/i; this.initialize = function(editor) { /* initialize the ui like tools */ this.editor = editor; this.cleanStyles(); this.enableOptions(false); if (this.tsselect) { this._selectevent = addEventHandler(this.tsselect, 'change', this.setTextStyleHandler, this); } }; this.getStyles = function() { if (!paraoptions) { this.cleanStyles(); } return [ paraoptions, tableoptions ]; }; this.setTextStyleHandler = function(event) { this.setTextStyle(this.tsselect.options[this.tsselect.selectedIndex].value); }; // event handlers this.basicButtonHandler = function(action) { /* event handler for basic actions (toolbar buttons) */ this.editor.execCommand(action); this.editor.updateState(); }; this.saveButtonHandler = function() { /* handler for the save button */ this.editor.saveDocument(); }; this.saveAndExitButtonHandler = function(redirect_url) { /* save the document and, if successful, redirect */ this.editor.saveDocument(redirect_url); }; this.cutButtonHandler = function() { try { this.editor.execCommand('Cut'); } catch (e) { if (this.editor.getBrowserName() == 'Mozilla') { alert(_('Cutting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html')); } else { throw e; }; }; this.editor.updateState(); }; this.copyButtonHandler = function() { try { this.editor.execCommand('Copy'); } catch (e) { if (this.editor.getBrowserName() == 'Mozilla') { alert(_('Copying from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html')); } else { throw e; }; }; this.editor.updateState(); }; this.pasteButtonHandler = function() { try { this.editor.execCommand('Paste'); } catch (e) { if (this.editor.getBrowserName() == 'Mozilla') { alert(_('Pasting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html')); } else { throw e; }; }; this.editor.updateState(); }; this.cleanStyles = function() { if (!this.tsselect) return; var options = this.tsselect.options; var parastyles = this.styles; var tablestyles = this.tablestyles; var charstyles = this.charstyles; var normal = ['Normal', 'p|']; var td = ['Plain Cell', 'td|']; var nostyle = ['(remove style)', '']; var opts = []; while (options.length) { var opt = options[0]; options[0] = null; var v = opt.value; if (v.indexOf('|') > -1) { var split = v.split('|'); v = split[0].toLowerCase() + "|" + split[1]; } else { v = v.toLowerCase()+"|"; }; var optarray = [opt.text, v]; if (v=='td|') { td = optarray; } else if (v=='p|') { normal = optarray; } else if (v=='') { nostyle = optarray; } else { opts.push([opt.text,v]); } } tableoptions.push(td); tablestyles[td[1]] = 0; paraoptions.push(normal); parastyles[normal[1]] = 0; for (var i = 0; i < opts.length; i++) { optarray = opts[i]; v = optarray[1]; if (spanre.test(v)) { charstyles[v] = styleoptions.length; styleoptions.push(optarray); } else if (tblre.test(v)) { tablestyles[v] = tableoptions.length; tableoptions.push(optarray); } else { parastyles[v] = paraoptions.length; paraoptions.push(optarray); }; }; paraoptions.push(nostyle); styleoffset = paraoptions.length; tableoffset = styleoffset + styleoptions.length; }; // Remove otherstyle and switch to appropriate style set. this.enableOptions = function(inTable) { if (!this.tsselect) return; var select = this.tsselect; var options = select.options; if (this.otherstyle) { options[0] = null; this.otherstyle = null; } if (this.optionstate == inTable) return; /* No change */ // while (select.firstChild) select.removeChild(select.firstChild); function option(info) { return newElement('option', {'value': info[1]}, [info[0]]); } if (this.optionstate==-1) { for (var i = 0; i < paraoptions.length; i++) { select.appendChild(option(paraoptions[i])); } if (styleoptions.length) { var grp = document.createElement('optgroup'); grp.label = 'Character styles'; for (var i = 0; i < styleoptions.length; i++) { grp.appendChild(option(styleoptions[i])); } select.appendChild(grp); } } if (inTable) { var grp = (tablegrp = document.createElement('optgroup')); grp.label = 'Table elements'; for (var i = 0; i < tableoptions.length; i++) { grp.appendChild(option(tableoptions[i])); } select.appendChild(grp); } else { while (select.options[tableoffset]) { select.options[tableoffset] = null; }; if (tablegrp) { select.removeChild(tablegrp); tablegrp = null; }; }; this.optionstate = inTable; }; this.setIndex = function(currnode, tag, index, styles) { var className = currnode.className; this.styletag = tag; this.classname = className; var style = tag+'|'+className; if (style in styles) { return styles[style]; } else if (!className && tag in styles) { return styles[tag]; } return index; }; this.nodeStyle = function(node) { var currnode = node; var index = -1; this.styletag = undefined; this.classname = ''; // Set the table state correctly this.intable = false; while(currnode) { var tag = currnode.nodeName; if (/^body$/i.test(tag)) break; if (tblre.test(tag)) { this.intable = true; break; }; currnode = currnode.parentNode; }; currnode = node; while (currnode) { var tag = currnode.nodeName.toLowerCase(); if (/^body$/.test(tag)) { if (!this.styletag) { // Forced style messes up in Firefox: return -1 to // indicate no style return -1; } break; } if (spanre.test(tag)) { index = this.setIndex(currnode, tag, index, this.charstyles); if (index >= 0) return index+styleoffset; // span takes priority } else if (blocktagre.test(tag)) { index = this.setIndex(currnode, tag, index, this.styles); } else if (tblre.test(tag)) { if (index > 0) return index; // block or span takes priority. index = this.setIndex(currnode, tag, index, this.tablestyles); if (index >= 0 || tag=='table') { return index+tableoffset; // Stop processing if in a table } } currnode = currnode.parentNode; } return index; }; this.updateState = function(selNode) { /* set the text-style pulldown */ // first get the nearest style // search the list of nodes like in the original one, break if we encounter a match, // this method does some more than the original one since it can handle commands in // the form of '