Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for Toolbar and Context Menu plugins #438

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
89 changes: 89 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,95 @@ 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-<name>`, 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.

- `{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-<name>`, 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.

- `{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`.

- `{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-<name>`, 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.

- `{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`.


### Methods

Expand Down
4 changes: 4 additions & 0 deletions src/css/contextmenu.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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 ul div.jsoneditor-icon {
margin-left: 24px;
}
Expand Down
4 changes: 4 additions & 0 deletions src/css/menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 13 additions & 5 deletions src/js/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -61,6 +64,11 @@ function ContextMenu (items, options) {
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
Expand All @@ -79,7 +87,7 @@ function ContextMenu (items, options) {
button.onclick = function (event) {
event.preventDefault();
me.hide();
item.click();
item.click(selectedElements);
};
}
li.appendChild(button);
Expand Down Expand Up @@ -137,7 +145,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
Expand All @@ -149,7 +157,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?

Expand Down
17 changes: 16 additions & 1 deletion src/js/JSONEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ var util = require('./util');
* {boolean} sortObjectKeys If true, object keys are
* sorted before display.
* false by default.
* {Object[]} toolbarPlugins Array of custom toolbar
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docblock is outdated and should be removed entirely. API Doc should be sufficient for documenting this.

* 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) {
Expand Down Expand Up @@ -82,7 +96,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', 'contextMenuPlugins', 'multiContextMenuPlugins'
];

Object.keys(options).forEach(function (option) {
Expand Down
56 changes: 55 additions & 1 deletion src/js/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3419,10 +3419,64 @@ Node.prototype.showContextMenu = function (anchor, onClose) {
}
}

var menu = new ContextMenu(items, {close: 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]);

// add the action
if (pluginConfig) {
items.push(pluginConfig);
}
}
}

var menu = new ContextMenu(items, {close: onClose}, this);
menu.show(anchor, this.editor.content);
};

/**
* Recursively process a Context Menu plugin configuration
* @param {Object} pluginConfig
* @return {Object} plugin config or null
* @private
*/
Node.prototype._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;
}

// 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 = [];
for (var i in pluginConfig.submenu) {
var submenuPlugin = this._processContextMenuPlugin(pluginConfig.submenu[i]);

if (submenuPlugin) {
processedSubMenu.push(submenuPlugin);
}
}
pluginConfig.submenu = processedSubMenu;
}

return pluginConfig;
};

/**
* get the type of a value
* @param {*} value
Expand Down
13 changes: 6 additions & 7 deletions src/js/appendNodeFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,38 +132,37 @@ 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');
}
},
{
text: 'Array',
className: 'jsoneditor-type-array',
title: titles.array,
click: function () {
click: function (node) {
node._onAppend('', []);
}
},
{
text: 'Object',
className: 'jsoneditor-type-object',
title: titles.object,
click: function () {
click: function (node) {
node._onAppend('', {});
}
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: titles.string,
click: function () {
click: function (node) {
node._onAppend('', '', 'string');
}
}
Expand All @@ -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);
};

Expand Down
Loading