diff --git a/settings.json.template b/settings.json.template index 43aa1613d..e9d35dcd7 100644 --- a/settings.json.template +++ b/settings.json.template @@ -103,6 +103,22 @@ // restrict socket.io transport methods "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"], + /* The toolbar buttons configuration. + "toolbar": { + "left": [ + ["bold", "italic", "underline", "strikethrough"], + ["orderedlist", "unorderedlist", "indent", "outdent"], + ["undo", "redo"], + ["clearauthorship"] + ], + "right": [ + ["importexport", "timeslider", "savedrevision"], + ["settings", "embed"], + ["showusers"] + ] + }, + */ + /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ "loglevel": "INFO", diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 7d0519659..8609d6579 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,5 +1,7 @@ var path = require('path'); var eejs = require('ep_etherpad-lite/node/eejs'); +var toolbar = require("ep_etherpad-lite/node/utils/toolbar"); +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); exports.expressCreateServer = function (hook_name, args, cb) { // expose current stats @@ -30,8 +32,15 @@ exports.expressCreateServer = function (hook_name, args, cb) { //serve pad.html under /p args.app.get('/p/:pad', function(req, res, next) - { - res.send(eejs.require("ep_etherpad-lite/templates/pad.html", {req: req})); + { + hooks.callAll("padInitToolbar", { + toolbar: toolbar + }); + + res.send(eejs.require("ep_etherpad-lite/templates/pad.html", { + req: req, + toolbar: toolbar + })); }); //serve timeslider.html under /p/$padname/timeslider diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index bc13a211e..9bfcae4b3 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -79,6 +79,23 @@ exports.dbSettings = { "filename" : path.join(exports.root, "dirty.db") }; */ exports.defaultPadText = "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad on Github: http:\/\/j.mp/ep-lite\n"; +/** + * The toolbar buttons and order. + */ +exports.toolbar = { + left: [ + ["bold", "italic", "underline", "strikethrough"], + ["orderedlist", "unorderedlist", "indent", "outdent"], + ["undo", "redo"], + ["clearauthorship"] + ], + right: [ + ["importexport", "timeslider", "savedrevision"], + ["settings", "embed"], + ["showusers"] + ] +} + /** * A flag that requires any user to have a valid session (via the api) before accessing a pad */ diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js new file mode 100644 index 000000000..a0a4fe013 --- /dev/null +++ b/src/node/utils/toolbar.js @@ -0,0 +1,227 @@ +/** + * The Toolbar Module creates and renders the toolbars and buttons + */ +var _ = require("underscore") + , tagAttributes + , tag + , defaultButtons + , Button + , ButtonsGroup + , Separator + , defaultButtonAttributes; + +defaultButtonAttributes = function (name, overrides) { + return { + key: name, + localizationId: "pad.toolbar." + name + ".title", + icon: "buttonicon-" + name + }; +}; + +tag = function (name, attributes, contents) { + var aStr = tagAttributes(attributes); + + if (_.isString(contents) && contents.length > 0) { + return '<' + name + aStr + '>' + contents + ''; + } + else { + return '<' + name + aStr + '>'; + } +}; + +tagAttributes = function (attributes) { + attributes = _.reduce(attributes || {}, function (o, val, name) { + if (!_.isUndefined(val)) { + o[name] = val; + } + return o; + }, {}); + + return " " + _.map(attributes, function (val, name) { + return "" + name + '="' + _.escape(val) + '"'; + }).join(" "); +}; + +ButtonsGroup = function () { + this.buttons = []; +}; + +ButtonsGroup.fromArray = function (array) { + var btnGroup = new this; + _.each(array, function (btnName) { + btnGroup.addButton(Button.load(btnName)); + }); + return btnGroup; +}; + +ButtonsGroup.prototype.addButton = function (button) { + this.buttons.push(button); + return this; +}; + +ButtonsGroup.prototype.render = function () { + if (this.buttons.length == 1) { + this.buttons[0].grouping = ""; + } + else { + _.first(this.buttons).grouping = "grouped-left"; + _.last(this.buttons).grouping = "grouped-right"; + _.each(this.buttons.slice(1, -1), function (btn) { + btn.grouping = "grouped-middle" + }); + } + + return _.map(this.buttons, function (btn) { + return btn.render(); + }).join("\n"); +}; + +Button = function (attributes) { + this.attributes = attributes; +}; + +Button.load = function (btnName) { + var button = module.exports.availableButtons[btnName]; + if (button.constructor === Button || button.constructor === SelectButton) { + return button; + } + else { + return new Button(button); + } +}; + +_.extend(Button.prototype, { + grouping: "", + + render: function () { + var liAttributes = { + "data-type": "button", + "data-key": this.attributes.key, + }; + return tag("li", liAttributes, + tag("a", { "class": this.grouping, "data-l10n-id": this.attributes.localizationId }, + tag("span", { "class": "buttonicon " + this.attributes.icon }) + ) + ); + } +}); + +SelectButton = function (attributes) { + this.attributes = attributes; + this.options = []; +}; + +_.extend(SelectButton.prototype, Button.prototype, { + addOption: function (value, text, attributes) { + this.options.push({ + value: value, + text: text, + attributes: attributes + }); + return this; + }, + + select: function (attributes) { + var self = this + , options = []; + + _.each(this.options, function (opt) { + var a = _.extend({ + value: opt.value + }, opt.attributes); + + options.push( tag("option", a, opt.text) ); + }); + return tag("select", attributes, options.join("")); + }, + + render: function () { + var attributes = { + id: this.attributes.id, + "data-key": this.attributes.command, + "data-type": "select" + }; + return tag("li", attributes, + this.select({ id: this.attributes.selectId }) + ); + } +}); + +Separator = function () {}; +Separator.prototype.render = function () { + return tag("li", { "class": "separator" }); +}; + +module.exports = { + availableButtons: { + bold: defaultButtonAttributes("bold"), + italic: defaultButtonAttributes("italic"), + underline: defaultButtonAttributes("underline"), + strikethrough: defaultButtonAttributes("strikethrough"), + + orderedlist: { + key: "insertorderedlist", + localizationId: "pad.toolbar.ol.title", + icon: "buttonicon-insertorderedlist" + }, + + unorderedlist: { + key: "insertunorderedlist", + localizationId: "pad.toolbar.ul.title", + icon: "buttonicon-insertunorderedlist" + }, + + indent: defaultButtonAttributes("indent"), + outdent: { + key: "outdent", + localizationId: "pad.toolbar.unindent.title", + icon: "buttonicon-outdent" + }, + + undo: defaultButtonAttributes("undo"), + redo: defaultButtonAttributes("redo"), + + clearauthorship: { + key: "clearauthorship", + localizationId: "pad.toolbar.clearAuthorship.title", + icon: "buttonicon-clearauthorship" + }, + + importexport: { + key: "import_export", + localizationId: "pad.toolbar.import_export.title", + icon: "buttonicon-import_export" + }, + + timeslider: { + key: "showTimeSlider", + localizationId: "pad.toolbar.timeslider.title", + icon: "buttonicon-history" + }, + + savedrevision: defaultButtonAttributes("savedRevision"), + settings: defaultButtonAttributes("settings"), + embed: defaultButtonAttributes("embed"), + showusers: defaultButtonAttributes("showusers") + }, + + registerButton: function (buttonName, buttonInfo) { + this.availableButtons[buttonName] = buttonInfo; + }, + + button: function (attributes) { + return new Button(attributes); + }, + separator: function () { + return (new Separator).render(); + }, + selectButton: function (attributes) { + return new SelectButton(attributes); + }, + menu: function (buttons) { + var groups = _.map(buttons, function (group) { + return ButtonsGroup.fromArray(group).render(); + }); + return groups.join(this.separator()); + } +}; diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 1b824031f..f4fda27c5 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -20,18 +20,60 @@ * limitations under the License. */ +var hooks = require('./pluginfw/hooks'); var padutils = require('./pad_utils').padutils; var padeditor = require('./pad_editor').padeditor; var padsavedrevs = require('./pad_savedrevs'); -function indexOf(array, value) { - for (var i = 0, ii = array.length; i < ii; i++) { - if (array[i] == value) { - return i; - } +var ToolbarItem = function (element) { + this.$el = element; +}; + +ToolbarItem.prototype.getCommand = function () { + return this.$el.attr("data-key"); +}; + +ToolbarItem.prototype.getValue = function () { + if (this.isSelect()) { + return this.$el.find("select").val(); } - return -1; -} +}; + +ToolbarItem.prototype.setValue = function (val) { + if (this.isSelect()) { + return this.$el.find("select").val(val); + } +}; + + +ToolbarItem.prototype.getType = function () { + return this.$el.attr("data-type"); +}; + +ToolbarItem.prototype.isSelect = function () { + return this.getType() == "select"; +}; + +ToolbarItem.prototype.isButton = function () { + return this.getType() == "button"; +}; + +ToolbarItem.prototype.bind = function (callback) { + var self = this; + + if (self.isButton()) { + self.$el.click(function (event) { + callback(self.getCommand(), self); + event.preventDefault(); + }); + } + else if (self.isSelect()) { + self.$el.find("select").change(function () { + callback(self.getCommand(), self); + }); + } +}; + var padeditbar = (function() { @@ -95,17 +137,22 @@ var padeditbar = (function() }()); var self = { - init: function() - { + init: function() { var self = this; $("#editbar .editbarbutton").attr("unselectable", "on"); // for IE $("#editbar").removeClass("disabledtoolbar").addClass("enabledtoolbar"); - $("#editbar [data-key]").each(function (i, e) { - $(e).click(function (event) { - self.toolbarClick($(e).attr('data-key')); - event.preventDefault(); + $("#editbar [data-key]").each(function () { + (new ToolbarItem($(this))).bind(function (command, item) { + self.triggerCommand(command, item); }); }); + + registerDefaultCommands(self); + + hooks.callAll("postToolbarInit", { + toolbar: self, + ace: padeditor.ace + }); }, isEnabled: function() { @@ -116,77 +163,34 @@ var padeditbar = (function() { $("#editbar").addClass('disabledtoolbar').removeClass("enabledtoolbar"); }, - toolbarClick: function(cmd) - { - if (self.isEnabled()) - { - if(cmd == "showusers") - { - self.toggleDropDown("users"); - } - else if (cmd == 'settings') - { - self.toggleDropDown("settings"); - } - else if (cmd == 'connectivity') - { - self.toggleDropDown("connectivity"); - } - else if (cmd == 'embed') - { - self.setEmbedLinks(); - $('#linkinput').focus().select(); - self.toggleDropDown("embed"); - } - else if (cmd == 'import_export') - { - self.toggleDropDown("importexport"); - } - else if (cmd == 'savedRevision') - { - padsavedrevs.saveNow(); - } - else - { - padeditor.ace.callWithAce(function(ace) - { - if (cmd == 'bold' || cmd == 'italic' || cmd == 'underline' || cmd == 'strikethrough') ace.ace_toggleAttributeOnSelection(cmd); - else if (cmd == 'undo' || cmd == 'redo') ace.ace_doUndoRedo(cmd); - else if (cmd == 'insertunorderedlist') ace.ace_doInsertUnorderedList(); - else if (cmd == 'insertorderedlist') ace.ace_doInsertOrderedList(); - else if (cmd == 'indent') - { - ace.ace_doIndentOutdent(false); - } - else if (cmd == 'outdent') - { - ace.ace_doIndentOutdent(true); - } - else if (cmd == 'clearauthorship') - { - if ((!(ace.ace_getRep().selStart && ace.ace_getRep().selEnd)) || ace.ace_isCaret()) - { - if (window.confirm(html10n.get("pad.editbar.clearcolors"))) - { - ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [ - ['author', ''] - ]); - } - } - else - { - ace.ace_setAttributeOnSelection('author', ''); - } - } - }, cmd, true); - } + commands: {}, + registerCommand: function (cmd, callback) { + this.commands[cmd] = callback; + return this; + }, + registerDropdownCommand: function (cmd, dropdown) { + dropdown = dropdown || cmd; + this.registerCommand(cmd, function () { + self.toggleDropDown(dropdown); + }); + }, + registerAceCommand: function (cmd, callback) { + this.registerCommand(cmd, function (cmd, ace) { + ace.callWithAce(function (ace) { + callback(cmd, ace); + }, cmd, true); + }); + }, + triggerCommand: function (cmd, item) { + if (self.isEnabled() && this.commands[cmd]) { + this.commands[cmd](cmd, padeditor.ace, item); } if(padeditor.ace) padeditor.ace.focus(); }, toggleDropDown: function(moduleName, cb) { var modules = ["settings", "connectivity", "importexport", "embed", "users"]; - + // hide all modules and remove highlighting of all buttons if(moduleName == "none") { @@ -196,9 +200,9 @@ var padeditbar = (function() //skip the userlist if(modules[i] == "users") continue; - + var module = $("#" + modules[i]); - + if(module.css('display') != "none") { $("#" + modules[i] + "link").removeClass("selected"); @@ -208,14 +212,14 @@ var padeditbar = (function() } if(!returned && cb) return cb(); } - else + else { // hide all modules that are not selected and remove highlighting // respectively add highlighting to the corresponding button for(var i=0;i <% e.begin_block("htmlHead"); %> @@ -54,103 +56,15 @@
+ @@ -295,7 +209,7 @@ <% e.end_block(); %> - +