From eb4848647d67afa5ab4a6376fce2985576a5bf44 Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Mon, 17 Jul 2017 13:35:43 -0700 Subject: [PATCH 1/7] Implement toolbar plugins --- docs/api.md | 17 +++++++++++++++++ src/js/JSONEditor.js | 7 ++++++- src/js/treemode.js | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 9595848d7..9eb824dff 100644 --- a/docs/api.md +++ b/docs/api.md @@ -180,6 +180,23 @@ Constructs a new JSONEditor. - Can return an object `{startFrom: number, options: string[]}`. Here `startFrom` determines the start character from where the existing text will be replaced. `startFrom` is `0` by default, replacing the whole text. - Can return a `Promise` resolving one of the return types above to support asynchronously retrieving a list with options. +- `{Object[]} toolbarPlugins` + + Adds custom buttons to the toolbar. Contains **subelements**: + + - `{string} title` + + The button's title text, for example `Expand to fullscreen`. + + - `{string} className` + + To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. + To add a spacer on the left, include class `jsoneditor-separator`, for example `jsoneditor-fullscreen jsoneditor-separator`. + + - `{Function} onclick` + + The callback function when the custom button is clicked. Called without parameters. + ### Methods diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index 9b0a33156..ebe20f51b 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -44,6 +44,10 @@ var util = require('./util'); * {boolean} sortObjectKeys If true, object keys are * sorted before display. * false by default. + * {Object[]} toolbarPlugins Array of custom toolbar + * buttons. Must contain + * `title`, `className`, and + * `onclick` properties. * @param {Object | undefined} json JSON object */ function JSONEditor (container, options, json) { @@ -82,7 +86,8 @@ function JSONEditor (container, options, json) { 'ajv', 'schema', 'schemaRefs','templates', 'ace', 'theme','autocomplete', 'onChange', 'onEditable', 'onError', 'onModeChange', - 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys' + 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys', + 'toolbarPlugins' ]; Object.keys(options).forEach(function (option) { diff --git a/src/js/treemode.js b/src/js/treemode.js index bc5ebac09..9b4e98eec 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -753,6 +753,31 @@ treemode._createFrame = function () { }); } + // create custom toolbar buttons + if (this.options && this.options.toolbarPlugins && this.options.toolbarPlugins.length) { + for (var i in this.options.toolbarPlugins) { + var customButtonOpts = this.options.toolbarPlugins[i]; + + // skip this plugin if these properties don't exist + if (!customButtonOpts.title || !customButtonOpts.className || !customButtonOpts.onclick) { + console.error("Toolbar plugin is being skipped for missing mandatory properties (title, className, onclick): " + + JSON.stringify(customButtonOpts)); + continue; + } + + // create the basic button + var customButton = document.createElement('button'); + customButton.type = 'button'; + + // merge the provided options to the button + for (var option in customButtonOpts) { + customButton[option] = customButtonOpts[option]; + } + + this.menu.appendChild(customButton); + } + } + // create search box if (this.options.search) { this.searchBox = new SearchBox(this, this.menu); From 2a6a65e889810551c591665d7eb01afd8d8afc53 Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Mon, 17 Jul 2017 16:12:59 -0700 Subject: [PATCH 2/7] Implement context menu plugins --- docs/api.md | 56 +++++++++++++++++++++++++++++++++++++++++ src/js/JSONEditor.js | 12 ++++++++- src/js/Node.js | 57 ++++++++++++++++++++++++++++++++++++++++++ src/js/treemode.js | 59 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 9eb824dff..745443fad 100644 --- a/docs/api.md +++ b/docs/api.md @@ -197,6 +197,62 @@ Constructs a new JSONEditor. The callback function when the custom button is clicked. Called without parameters. +- `{Object[]} contextMenuPlugins` + + Adds custom actions to the context menu when a single node is selected. Contains **subelements**: + + - `{string} type` + + If type === 'separator', this object represents a separator in the context menu. All other properties will be ignored. + + - `{string} text` + + The action's text, for example `Action Name`. + + - `{string} title` + + The action's title (tooltip) text, for example `A longer description of the action`. + + - `{string} className` + + To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. + + - `{Function} _click (nodes)` + + The callback function when the custom action is clicked. Called with the selected nodes. + + - `{Object[]} submenu` + + Submenu items of the same type as `contextMenuPlugins`. + +- `{Object[]} multiContextMenuPlugins` + + Adds custom actions to the context menu when multiple nodes are selected. Contains **subelements**: + + - `{string} type` + + If type === 'separator', this object represents a separator in the context menu. All other properties will be ignored. + + - `{string} text` + + The action's text, for example `Action Name`. + + - `{string} title` + + The action's title (tooltip) text, for example `A longer description of the action`. + + - `{string} className` + + To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. + + - `{Function} _click (nodes)` + + The callback function when the custom action is clicked. Called with the selected nodes. + + - `{Object[]} submenu` + + Submenu items of the same type as `multiContextMenuPlugins`. + ### Methods diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index ebe20f51b..b749ad69b 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -48,6 +48,16 @@ var util = require('./util'); * buttons. Must contain * `title`, `className`, and * `onclick` properties. + * {Object[]} contextMenuPlugins Array of custom toolbar + * buttons. Must contain + * 'text', `title`, `className`, + * and either `_click` or + * `submenu` properties. + * {Object[]} multiContextMenuPlugins Array of custom toolbar + * buttons. Must contain + * 'text', `title`, `className`, + * and either `_click` or + * `submenu` properties. * @param {Object | undefined} json JSON object */ function JSONEditor (container, options, json) { @@ -87,7 +97,7 @@ function JSONEditor (container, options, json) { 'ace', 'theme','autocomplete', 'onChange', 'onEditable', 'onError', 'onModeChange', 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys', - 'toolbarPlugins' + 'toolbarPlugins', 'contextMenuPlugins', 'multiContextMenuPlugins' ]; Object.keys(options).forEach(function (option) { diff --git a/src/js/Node.js b/src/js/Node.js index 1725439b0..7ea6a1f9b 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -3409,10 +3409,67 @@ Node.prototype.showContextMenu = function (anchor, onClose) { } } + // create custom context menu buttons + if (this.editor.options && this.editor.options.contextMenuPlugins && this.editor.options.contextMenuPlugins.length) { + for (var i in this.editor.options.contextMenuPlugins) { + // recursively validate and process the plugin configurations + var pluginConfig = this._processContextMenuPlugin(this.editor.options.contextMenuPlugins[i], node); + + // add the action + if (pluginConfig) { + items.push(pluginConfig); + } + } + } + var menu = new ContextMenu(items, {close: onClose}); menu.show(anchor, this.editor.content); }; +/** + * Recursively process a Context Menu plugin configuration + * @param {Object} pluginConfig + * @param {Node} node the selected node + * @return {Object} plugin config or null + * @private + */ +Node.prototype._processContextMenuPlugin = function(pluginConfig, node) { + // skip this plugin if these properties don't exist + if (pluginConfig.type == 'separator') { + // separators don't have mandatory properties + } else if (!pluginConfig.text || !pluginConfig.title || !pluginConfig.className) { + console.error("Context Menu plugin is being skipped for missing mandatory properties (text, title, className): " + + JSON.stringify(pluginConfig)); + return null; + } else if (!pluginConfig._click && !pluginConfig.submenu) { + console.error("Context Menu plugin is being skipped for not including at least on of the properties (_click, submenu): " + + JSON.stringify(pluginConfig)); + return null; + } + + // wrap the callback so we can pass the node + if (typeof pluginConfig._click === 'function') { + pluginConfig.click = function() { + this._click(node); + }; + } + + // recursively process submenus + if (pluginConfig.submenu instanceof Array) { + var processedSubMenu = []; + for (var i in pluginConfig.submenu) { + var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i], node); + + if (submenuPlugin) { + processedSubMenu.push(submenuPlugin); + } + } + pluginConfig.submenu = processedSubMenu; + } + + return pluginConfig; +}; + /** * get the type of a value * @param {*} value diff --git a/src/js/treemode.js b/src/js/treemode.js index 9b4e98eec..ab9f7a243 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -1250,10 +1250,69 @@ treemode.showContextMenu = function (anchor, onClose) { } }); + // create custom multi-select context menu buttons + if (this.options && this.options.multiContextMenuPlugins && this.options.multiContextMenuPlugins.length) { + for (var i in this.options.multiContextMenuPlugins) { + var pluginConfig = this.options.multiContextMenuPlugins[i]; + + // recursively validate and process the plugin configurations + pluginConfig = this._processContextMenuPlugin(pluginConfig, editor.multiselection.nodes); + + // add the action + if (pluginConfig) { + items.push(pluginConfig); + } + } + } + var menu = new ContextMenu(items, {close: onClose}); menu.show(anchor, this.content); }; +/** + * Recursively process a Context Menu plugin configuration + * @param {Object} pluginConfig + * @param {Node} nodes the selected nodes + * @return {Object} plugin config or null + * @private + */ +treemode._processContextMenuPlugin = function(pluginConfig) { + // skip this plugin if these properties don't exist + if (pluginConfig.type == 'separator') { + // separators don't have mandatory properties + } else if (!pluginConfig.text || !pluginConfig.title || !pluginConfig.className) { + console.error("Context Menu plugin is being skipped for missing mandatory properties (text, title, className): " + + JSON.stringify(pluginConfig)); + return null; + } else if (!pluginConfig._click && !pluginConfig.submenu) { + console.error("Context Menu plugin is being skipped for not including at least on of the properties (_click, submenu): " + + JSON.stringify(pluginConfig)); + return null; + } + + // wrap the callback so we can pass the node + if (typeof pluginConfig._click === 'function') { + pluginConfig.click = function() { + this._click(nodes); + }; + } + + // recursively process submenus + if (pluginConfig.submenu instanceof Array) { + var processedSubMenu = []; + for (var i in pluginConfig.submenu) { + var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i], nodes); + + if (submenuPlugin) { + processedSubMenu.push(submenuPlugin); + } + } + pluginConfig.submenu = processedSubMenu; + } + + return pluginConfig; +}; + // define modes module.exports = [ From 2dd41d75c36337a1d6e806f4640c5b073c751c60 Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Tue, 18 Jul 2017 09:18:51 -0700 Subject: [PATCH 3/7] Create test for toolbar and context menu plugin support --- test/test_plugins.html | 192 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 test/test_plugins.html diff --git a/test/test_plugins.html b/test/test_plugins.html new file mode 100644 index 000000000..6135a339a --- /dev/null +++ b/test/test_plugins.html @@ -0,0 +1,192 @@ + + + + + + + + + + + + +

+ Switch editor mode using the mode box. + Note that the mode can be changed programmatically as well using the method + editor.setMode(mode), try it in the console of your browser. +

+ +
+ + + + From a8e3ae99e74aca50c9f4c0d08d65a4ba869ddd74 Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Tue, 18 Jul 2017 13:36:23 -0700 Subject: [PATCH 4/7] Add a selectedElements parameter to ContextMenu to pass to actions' click handlers. This allows us to remove the ugly callback wrapping when processing the contextMenuPlugins. Also updates test and documentation. --- docs/api.md | 6 +++--- src/js/ContextMenu.js | 13 ++++++++----- src/js/JSONEditor.js | 4 ++-- src/js/Node.js | 20 ++++++-------------- src/js/appendNodeFactory.js | 13 ++++++------- src/js/treemode.js | 26 +++++++++----------------- test/test_plugins.html | 24 ++++++++++++------------ 7 files changed, 46 insertions(+), 60 deletions(-) diff --git a/docs/api.md b/docs/api.md index 745443fad..d580c3e45 100644 --- a/docs/api.md +++ b/docs/api.md @@ -217,9 +217,9 @@ Constructs a new JSONEditor. To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. - - `{Function} _click (nodes)` + - `{Function} click (node)` - The callback function when the custom action is clicked. Called with the selected nodes. + The callback function when the custom action is clicked. Called with the selected node. - `{Object[]} submenu` @@ -245,7 +245,7 @@ Constructs a new JSONEditor. To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. - - `{Function} _click (nodes)` + - `{Function} click (nodes)` The callback function when the custom action is clicked. Called with the selected nodes. diff --git a/src/js/ContextMenu.js b/src/js/ContextMenu.js index 6bb1b1ffb..d0af3bf34 100644 --- a/src/js/ContextMenu.js +++ b/src/js/ContextMenu.js @@ -9,9 +9,12 @@ var util = require('./util'); * @param {Object} [options] Object with options. Available options: * {function} close Callback called when the * context menu is being closed. + * @param {Object or Object[]} [selectedElements] + * the selected element or elements to which + * this ContextMenu applies * @constructor */ -function ContextMenu (items, options) { +function ContextMenu (items, options, selectedElements) { this.dom = {}; var me = this; @@ -50,7 +53,7 @@ function ContextMenu (items, options) { li.appendChild(focusButton); list.appendChild(li); - function createMenuItems (list, domItems, items) { + function createMenuItems (list, domItems, items, selectedElements) { items.forEach(function (item) { if (item.type == 'separator') { // create a separator @@ -79,7 +82,7 @@ function ContextMenu (items, options) { button.onclick = function (event) { event.preventDefault(); me.hide(); - item.click(); + item.click(selectedElements); }; } li.appendChild(button); @@ -133,7 +136,7 @@ function ContextMenu (items, options) { ul.className = 'jsoneditor-menu'; ul.style.height = '0'; li.appendChild(ul); - createMenuItems(ul, domSubItems, item.submenu); + createMenuItems(ul, domSubItems, item.submenu, selectedElements); } else { // no submenu, just a button with clickhandler @@ -144,7 +147,7 @@ function ContextMenu (items, options) { } }); } - createMenuItems(list, this.dom.items, items); + createMenuItems(list, this.dom.items, items, selectedElements); // TODO: when the editor is small, show the submenu on the right instead of inline? diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index b749ad69b..9b02a0b33 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -51,12 +51,12 @@ var util = require('./util'); * {Object[]} contextMenuPlugins Array of custom toolbar * buttons. Must contain * 'text', `title`, `className`, - * and either `_click` or + * and either `click` or * `submenu` properties. * {Object[]} multiContextMenuPlugins Array of custom toolbar * buttons. Must contain * 'text', `title`, `className`, - * and either `_click` or + * and either `click` or * `submenu` properties. * @param {Object | undefined} json JSON object */ diff --git a/src/js/Node.js b/src/js/Node.js index 7ea6a1f9b..085f6a7e4 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -3413,7 +3413,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) { if (this.editor.options && this.editor.options.contextMenuPlugins && this.editor.options.contextMenuPlugins.length) { for (var i in this.editor.options.contextMenuPlugins) { // recursively validate and process the plugin configurations - var pluginConfig = this._processContextMenuPlugin(this.editor.options.contextMenuPlugins[i], node); + var pluginConfig = this._processContextMenuPlugin(this.editor.options.contextMenuPlugins[i]); // add the action if (pluginConfig) { @@ -3422,18 +3422,17 @@ Node.prototype.showContextMenu = function (anchor, onClose) { } } - var menu = new ContextMenu(items, {close: onClose}); + var menu = new ContextMenu(items, {close: onClose}, this); menu.show(anchor, this.editor.content); }; /** * Recursively process a Context Menu plugin configuration * @param {Object} pluginConfig - * @param {Node} node the selected node * @return {Object} plugin config or null * @private */ -Node.prototype._processContextMenuPlugin = function(pluginConfig, node) { +Node.prototype._processContextMenuPlugin = function(pluginConfig) { // skip this plugin if these properties don't exist if (pluginConfig.type == 'separator') { // separators don't have mandatory properties @@ -3441,24 +3440,17 @@ Node.prototype._processContextMenuPlugin = function(pluginConfig, node) { console.error("Context Menu plugin is being skipped for missing mandatory properties (text, title, className): " + JSON.stringify(pluginConfig)); return null; - } else if (!pluginConfig._click && !pluginConfig.submenu) { - console.error("Context Menu plugin is being skipped for not including at least on of the properties (_click, submenu): " + + } else if (!pluginConfig.click && !pluginConfig.submenu) { + console.error("Context Menu plugin is being skipped for not including at least on of the properties (click, submenu): " + JSON.stringify(pluginConfig)); return null; } - // wrap the callback so we can pass the node - if (typeof pluginConfig._click === 'function') { - pluginConfig.click = function() { - this._click(node); - }; - } - // recursively process submenus if (pluginConfig.submenu instanceof Array) { var processedSubMenu = []; for (var i in pluginConfig.submenu) { - var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i], node); + var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i]); if (submenuPlugin) { processedSubMenu.push(submenuPlugin); diff --git a/src/js/appendNodeFactory.js b/src/js/appendNodeFactory.js index bb900cc43..fa8cb9d1d 100644 --- a/src/js/appendNodeFactory.js +++ b/src/js/appendNodeFactory.js @@ -132,14 +132,13 @@ function appendNodeFactory(Node) { * is being closed. */ AppendNode.prototype.showContextMenu = function (anchor, onClose) { - var node = this; var titles = Node.TYPE_TITLES; var appendSubmenu = [ { text: 'Auto', className: 'jsoneditor-type-auto', title: titles.auto, - click: function () { + click: function (node) { node._onAppend('', '', 'auto'); } }, @@ -147,7 +146,7 @@ function appendNodeFactory(Node) { text: 'Array', className: 'jsoneditor-type-array', title: titles.array, - click: function () { + click: function (node) { node._onAppend('', []); } }, @@ -155,7 +154,7 @@ function appendNodeFactory(Node) { text: 'Object', className: 'jsoneditor-type-object', title: titles.object, - click: function () { + click: function (node) { node._onAppend('', {}); } }, @@ -163,7 +162,7 @@ function appendNodeFactory(Node) { text: 'String', className: 'jsoneditor-type-string', title: titles.string, - click: function () { + click: function (node) { node._onAppend('', '', 'string'); } } @@ -176,14 +175,14 @@ function appendNodeFactory(Node) { 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)', 'submenuTitle': 'Select the type of the field to be appended', 'className': 'jsoneditor-insert', - 'click': function () { + 'click': function (node) { node._onAppend('', '', 'auto'); }, 'submenu': appendSubmenu } ]; - var menu = new ContextMenu(items, {close: onClose}); + var menu = new ContextMenu(items, {close: onClose}, this); menu.show(anchor, this.editor.content); }; diff --git a/src/js/treemode.js b/src/js/treemode.js index ab9f7a243..813f6333c 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -1235,8 +1235,8 @@ treemode.showContextMenu = function (anchor, onClose) { text: 'Duplicate', title: 'Duplicate selected fields (Ctrl+D)', className: 'jsoneditor-duplicate', - click: function () { - Node.onDuplicate(editor.multiselection.nodes); + click: function (nodes) { + Node.onDuplicate(); } }); @@ -1245,8 +1245,8 @@ treemode.showContextMenu = function (anchor, onClose) { text: 'Remove', title: 'Remove selected fields (Ctrl+Del)', className: 'jsoneditor-remove', - click: function () { - Node.onRemove(editor.multiselection.nodes); + click: function (nodes) { + Node.onRemove(nodes); } }); @@ -1256,7 +1256,7 @@ treemode.showContextMenu = function (anchor, onClose) { var pluginConfig = this.options.multiContextMenuPlugins[i]; // recursively validate and process the plugin configurations - pluginConfig = this._processContextMenuPlugin(pluginConfig, editor.multiselection.nodes); + pluginConfig = this._processContextMenuPlugin(pluginConfig); // add the action if (pluginConfig) { @@ -1265,14 +1265,13 @@ treemode.showContextMenu = function (anchor, onClose) { } } - var menu = new ContextMenu(items, {close: onClose}); + var menu = new ContextMenu(items, {close: onClose}, editor.multiselection.nodes); menu.show(anchor, this.content); }; /** * Recursively process a Context Menu plugin configuration * @param {Object} pluginConfig - * @param {Node} nodes the selected nodes * @return {Object} plugin config or null * @private */ @@ -1284,24 +1283,17 @@ treemode._processContextMenuPlugin = function(pluginConfig) { console.error("Context Menu plugin is being skipped for missing mandatory properties (text, title, className): " + JSON.stringify(pluginConfig)); return null; - } else if (!pluginConfig._click && !pluginConfig.submenu) { - console.error("Context Menu plugin is being skipped for not including at least on of the properties (_click, submenu): " + + } else if (!pluginConfig.click && !pluginConfig.submenu) { + console.error("Context Menu plugin is being skipped for not including at least on of the properties (click, submenu): " + JSON.stringify(pluginConfig)); return null; } - // wrap the callback so we can pass the node - if (typeof pluginConfig._click === 'function') { - pluginConfig.click = function() { - this._click(nodes); - }; - } - // recursively process submenus if (pluginConfig.submenu instanceof Array) { var processedSubMenu = []; for (var i in pluginConfig.submenu) { - var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i], nodes); + var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i]); if (submenuPlugin) { processedSubMenu.push(submenuPlugin); diff --git a/test/test_plugins.html b/test/test_plugins.html index 6135a339a..90e16ecee 100644 --- a/test/test_plugins.html +++ b/test/test_plugins.html @@ -76,7 +76,7 @@ text: 'Click', title: 'Action with click', className: 'jsoneditor-action1', - _click: function(node) { + click: function(node) { alert('Action 1'); alert(node); } @@ -90,22 +90,22 @@ text: 'Submenu 1', title: 'New subaction 1', className: 'jsoneditor-sa1', - _click: function(node) { alert('subaction 1'); } + click: function(node) { alert('subaction 1'); } }, { title: 'Invalid Submenu 1', className: 'jsoneditor-invalidsa1', - _click: function(node) { alert('subaction 1'); } + click: function(node) { alert('subaction 1'); } }, { text: 'submenu 2', className: 'jsoneditor-invalidsa2', - _click: function(node) { alert('subaction 2'); } + click: function(node) { alert('subaction 2'); } }, { text: 'submenu 3', title: 'Invalid Submenu 3', - _click: function(node) { alert('subaction 3'); } + click: function(node) { alert('subaction 3'); } }, { text: 'submenu 4', @@ -118,7 +118,7 @@ text: 'Submenu+Click', title: 'Action with submenu and click', className: 'jsoneditor-action3', - _click: function(node) { + click: function(node) { alert('Action 3'); alert(node); }, @@ -127,14 +127,14 @@ text: 'Submenu 1', title: 'New subaction 1', className: 'jsoneditor-sa1', - _click: function(node) { alert('subaction 1'); } + click: function(node) { alert('subaction 1'); } } ] }, { text: 'Custom C', title: 'New action 3', - _click: function(node) { + click: function(node) { alert('Action 3'); } } @@ -144,22 +144,22 @@ text: 'Multi1', title: 'New multiselect action 1', className: 'jsoneditor-msaction1', - _click: function(nodes) { alert('Action 1'); } + click: function(nodes) { alert('Action 1'); } }, { title: 'New multiselect action 2', className: 'jsoneditor-msaction2', - _click: function(nodes) { alert('Action 2'); } + click: function(nodes) { alert('Action 2'); } }, { text: 'Multi3', className: 'jsoneditor-msaction3', - _click: function(nodes) { alert('Action 3'); } + click: function(nodes) { alert('Action 3'); } }, { text: 'Multi4', title: 'New multiselect action 4', - _click: function(nodes) { alert('Action 4'); } + click: function(nodes) { alert('Action 4'); } }, { text: 'Multi5', From fbd082de1313ab01b05c89851050bd7a1993a3f1 Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Tue, 18 Jul 2017 14:01:12 -0700 Subject: [PATCH 5/7] Add contextMenuPlugin.submenuTitle to docs and test. I noticed that the arrow only renders properly in some cases. For this reason, the submenuTitle only works sometimes. --- docs/api.md | 8 ++++++++ test/test_plugins.html | 2 ++ 2 files changed, 10 insertions(+) diff --git a/docs/api.md b/docs/api.md index d580c3e45..8c7a6e71a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -221,6 +221,10 @@ Constructs a new JSONEditor. The callback function when the custom action is clicked. Called with the selected node. + - `{string} submenuTitle` + + The submenu expander's title (tooltip) text, for example `A longer description of the submenu`. + - `{Object[]} submenu` Submenu items of the same type as `contextMenuPlugins`. @@ -249,6 +253,10 @@ Constructs a new JSONEditor. The callback function when the custom action is clicked. Called with the selected nodes. + - `{string} submenuTitle` + + The submenu expander's title (tooltip) text, for example `A longer description of the submenu`. + - `{Object[]} submenu` Submenu items of the same type as `multiContextMenuPlugins`. diff --git a/test/test_plugins.html b/test/test_plugins.html index 90e16ecee..f2b72059f 100644 --- a/test/test_plugins.html +++ b/test/test_plugins.html @@ -85,6 +85,7 @@ text: 'Submenu', title: 'Action with submenu', className: 'jsoneditor-action2', + submenuTitle: 'A title for the submenu expand button', submenu: [ { text: 'Submenu 1', @@ -122,6 +123,7 @@ alert('Action 3'); alert(node); }, + submenuTitle: 'Another title for a different submenu expand button', submenu: [ { text: 'Submenu 1', From f33365d2f03e439b0d2a86dcb2a7245e2c4d6a0b Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Tue, 18 Jul 2017 16:15:24 -0700 Subject: [PATCH 6/7] Add `contextMenuPlugin.enabled` property so plugins can enable/disable themselves. Includes docs and tests. In the future, it may be preferable for disabled actions to be constructed and appear in the context menu, appearing as disabled (grayed out). --- docs/api.md | 8 ++++++++ src/js/ContextMenu.js | 5 +++++ test/test_plugins.html | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/docs/api.md b/docs/api.md index 8c7a6e71a..04aca4656 100644 --- a/docs/api.md +++ b/docs/api.md @@ -217,6 +217,10 @@ Constructs a new JSONEditor. To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. + - `{Function} enabled (node)` + + The optional callback function to enable/disable this plugin (enabled by default). Called with the selected node. + - `{Function} click (node)` The callback function when the custom action is clicked. Called with the selected node. @@ -249,6 +253,10 @@ Constructs a new JSONEditor. To be consistent with standard buttons, use `jsoneditor-`, for example `jsoneditor-fullscreen`. + - `{Function} enabled (nodes)` + + The optional callback function to enable/disable this plugin (enabled by default). Called with the selected nodes. + - `{Function} click (nodes)` The callback function when the custom action is clicked. Called with the selected nodes. diff --git a/src/js/ContextMenu.js b/src/js/ContextMenu.js index d0af3bf34..e3a515ea9 100644 --- a/src/js/ContextMenu.js +++ b/src/js/ContextMenu.js @@ -64,6 +64,11 @@ function ContextMenu (items, options, selectedElements) { list.appendChild(li); } else { + // check if the item should be enabled + if (item.enabled && item.enabled instanceof Function && !item.enabled(selectedElements)) { + return false; + } + var domItem = {}; // create a menu item diff --git a/test/test_plugins.html b/test/test_plugins.html index f2b72059f..0c9cbd092 100644 --- a/test/test_plugins.html +++ b/test/test_plugins.html @@ -72,6 +72,18 @@ { type: 'separator' }, + { + text: 'Disabled', + title: 'This action should be disabled', + className: 'jsoneditor-disabledaction1', + enabled: function(node) { + return false; + }, + click: function(node) { + alert('This action should be disabled'); + console.error('This action should be disabled'); + } + }, { text: 'Click', title: 'Action with click', From bde68cf33330a87e8c247e2ee5014260c895c71d Mon Sep 17 00:00:00 2001 From: Dave Hughes Date: Sat, 22 Jul 2017 09:12:20 -0700 Subject: [PATCH 7/7] Add class to plugin actions for setting default icons --- src/css/contextmenu.css | 4 ++++ src/css/menu.css | 4 ++++ src/js/Node.js | 5 +++++ src/js/treemode.js | 8 ++++++++ test/test_plugins.html | 16 ++++++++++++++-- 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/css/contextmenu.css b/src/css/contextmenu.css index 568c20dbc..7d256ec73 100644 --- a/src/css/contextmenu.css +++ b/src/css/contextmenu.css @@ -86,6 +86,10 @@ div.jsoneditor-contextmenu div.jsoneditor-icon { background-image: url('img/jsoneditor-icons.svg'); } +div.jsoneditor-contextmenu button.jsoneditor-plugin div.jsoneditor-icon { + background: transparent; +} + div.jsoneditor-contextmenu ul li button div.jsoneditor-expand { float: right; width: 24px; diff --git a/src/css/menu.css b/src/css/menu.css index 6a4f44c99..a9aab1356 100644 --- a/src/css/menu.css +++ b/src/css/menu.css @@ -31,6 +31,10 @@ div.jsoneditor-menu > div.jsoneditor-modes > button { float: left; } +div.jsoneditor-menu > button.jsoneditor-plugin { + background: #d3d3d3; +} + div.jsoneditor-menu > button:hover, div.jsoneditor-menu > div.jsoneditor-modes > button:hover { background-color: rgba(255,255,255,0.2); diff --git a/src/js/Node.js b/src/js/Node.js index 085f6a7e4..94d17dfe8 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -3446,6 +3446,11 @@ Node.prototype._processContextMenuPlugin = function(pluginConfig) { return null; } + // add a plugin class to hide the icon + if (!pluginConfig.className || pluginConfig.className.indexOf('jsoneditor-plugin ') === -1) { + pluginConfig.className = 'jsoneditor-plugin ' + (pluginConfig.className || ''); + } + // recursively process submenus if (pluginConfig.submenu instanceof Array) { var processedSubMenu = []; diff --git a/src/js/treemode.js b/src/js/treemode.js index 813f6333c..5771580d7 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -774,6 +774,9 @@ treemode._createFrame = function () { customButton[option] = customButtonOpts[option]; } + // add a plugin class to hide the icon + customButton.className = 'jsoneditor-plugin ' + (customButton.className || ''); + this.menu.appendChild(customButton); } } @@ -1289,6 +1292,11 @@ treemode._processContextMenuPlugin = function(pluginConfig) { return null; } + // add a plugin class to hide the icon + if (!pluginConfig.className || pluginConfig.className.indexOf('jsoneditor-plugin ') === -1) { + pluginConfig.className = 'jsoneditor-plugin ' + (pluginConfig.className || ''); + } + // recursively process submenus if (pluginConfig.submenu instanceof Array) { var processedSubMenu = []; diff --git a/test/test_plugins.html b/test/test_plugins.html index 0c9cbd092..ef714d27b 100644 --- a/test/test_plugins.html +++ b/test/test_plugins.html @@ -23,6 +23,18 @@ width: 500px; height: 500px; } + + div.jsoneditor-menu > button.jsoneditor-button1 { + background: white; + } + + div.jsoneditor-contextmenu button.jsoneditor-action1 div.jsoneditor-icon { + background: black; + } + + div.jsoneditor-contextmenu button.jsoneditor-sa1 div.jsoneditor-icon { + background: red; + } @@ -140,8 +152,8 @@ { text: 'Submenu 1', title: 'New subaction 1', - className: 'jsoneditor-sa1', - click: function(node) { alert('subaction 1'); } + className: 'jsoneditor-sa21', + click: function(node) { alert('subaction 2-1'); } } ] },