diff --git a/examples/25_custom_buttons.html b/examples/25_custom_buttons.html
new file mode 100644
index 000000000..3a8fee904
--- /dev/null
+++ b/examples/25_custom_buttons.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ JSONEditor | Custom buttons
+
+
+
+
+
+
+
+
+
+
+ Custom buttons
+
+ This example demonstrates how to append custom buttons to the head menu
+ bar. The custom buttons is available in text mode.
+
+
+ This example use custom buttons to load/save local files. Powered by
+ FileReader.js and
+ FileSaver.js .
+ Only supported on modern browsers (Chrome, FireFox, IE10+, Safari 6.1+,
+ Opera 15+).
+
+
+
+
+
+
+
diff --git a/examples/image/load.svg b/examples/image/load.svg
new file mode 100644
index 000000000..c9ea02627
--- /dev/null
+++ b/examples/image/load.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/image/save.svg b/examples/image/save.svg
new file mode 100644
index 000000000..6dba329f8
--- /dev/null
+++ b/examples/image/save.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js
index b7fb38556..6b5ff4c1c 100644
--- a/src/js/JSONEditor.js
+++ b/src/js/JSONEditor.js
@@ -2,6 +2,7 @@
const ace = require('./ace') // may be undefined in case of minimalist bundle
const VanillaPicker = require('./vanilla-picker') // may be undefined in case of minimalist bundle
+const { ModeSwitcher } = require('./ModeSwitcher')
const { treeModeMixins } = require('./treemode')
const { textModeMixins } = require('./textmode')
const { previewModeMixins } = require('./previewmode')
@@ -9,6 +10,7 @@ const { clear, extend, getInnerText, getInternetExplorerVersion, parse } = requi
const { tryRequireAjv } = require('./tryRequireAjv')
const { showTransformModal } = require('./showTransformModal')
const { showSortModal } = require('./showSortModal')
+const { defaultOptions } = require('./defaultOptions')
const Ajv = tryRequireAjv()
@@ -187,7 +189,7 @@ JSONEditor.VALID_OPTIONS = [
'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform', 'limitDragging',
'maxVisibleChilds', 'onValidationError',
'modalAnchor', 'popupAnchor',
- 'createQuery', 'executeQuery', 'queryDescription'
+ 'createQuery', 'executeQuery', 'queryDescription', 'buttons', 'disableButtons'
]
/**
@@ -203,6 +205,7 @@ JSONEditor.prototype._create = function (container, options, json) {
this.json = json || {}
const mode = this.options.mode || (this.options.modes && this.options.modes[0]) || 'tree'
+ this._initButtons()
this.setMode(mode)
}
@@ -471,6 +474,134 @@ JSONEditor.registerMode = mode => {
}
}
+/**
+ * Create menu buttons by options.buttons
+ */
+JSONEditor.prototype._createButtons = function () {
+ if (this.options && this.options.buttons) {
+ this.options.buttons.forEach((buttonCustomConf) => {
+ // Check mode setting
+ let shouldCreateByModeConf = false
+ if (buttonCustomConf.mode === undefined) {
+ shouldCreateByModeConf = true
+ } else if (
+ buttonCustomConf.mode === this.options.mode ||
+ buttonCustomConf.mode.indexOf(this.options.mode) !== -1
+ ) {
+ shouldCreateByModeConf = true
+ }
+
+ // Check enable condition
+ let shouldCreateByEnableKey = false
+ if (buttonCustomConf.checkEnableKey) {
+ if (this.options[buttonCustomConf.checkEnableKey]) {
+ shouldCreateByEnableKey = true
+ } else {
+ shouldCreateByEnableKey = false
+ }
+ } else {
+ shouldCreateByEnableKey = true
+ }
+
+ // Check the key in editor condition
+ let shouldCreateByInEditorKey = false
+ if (buttonCustomConf.checkInEditorKey) {
+ if (this[buttonCustomConf.checkInEditorKey]) {
+ shouldCreateByInEditorKey = true
+ } else {
+ shouldCreateByInEditorKey = false
+ }
+ } else {
+ shouldCreateByInEditorKey = true
+ }
+
+ if (
+ shouldCreateByModeConf &&
+ shouldCreateByEnableKey &&
+ shouldCreateByInEditorKey
+ ) {
+ // If the config is for children
+ if (buttonCustomConf.children) {
+ const createdButtonMap = {}
+ buttonCustomConf.children.forEach((childConf) => {
+ const buttonCustom = this._createButtonElement(childConf)
+ createdButtonMap[childConf.name] = buttonCustom
+ })
+ if (buttonCustomConf.afterCreate) {
+ buttonCustomConf.afterCreate(this, createdButtonMap)
+ }
+ } else {
+ this._createButtonElement(buttonCustomConf)
+ }
+ }
+ })
+ }
+ if (this.options && this.options.modes && this.options.modes.length) {
+ const me = this
+ this.modeSwitcher = new ModeSwitcher(
+ this.menu,
+ this.options.modes,
+ this.options.mode,
+ function onSwitch (mode) {
+ // switch mode and restore focus
+ me.setMode(mode)
+ me.modeSwitcher.focus()
+ }
+ )
+ }
+}
+
+/**
+ * Create dom element by the button configuration
+ * @param {Object} buttonCustomConf The configuration of a button.
+ * @return {Element} The button dom element created
+ */
+JSONEditor.prototype._createButtonElement = function (buttonCustomConf) {
+ const buttonCustom = document.createElement('button')
+ buttonCustom.type = 'button'
+ buttonCustom.className = buttonCustomConf.className
+ buttonCustom.title = buttonCustomConf.title
+ buttonCustom.name = buttonCustomConf.name
+ this.menu.appendChild(buttonCustom)
+ buttonCustom.onclick = () => {
+ buttonCustomConf.target(this, buttonCustom)
+ }
+ if (buttonCustomConf.afterCreate) {
+ buttonCustomConf.afterCreate(this, buttonCustom)
+ }
+ return buttonCustom
+}
+
+/**
+ * Initialize options.buttons array
+ */
+JSONEditor.prototype._initButtons = function () {
+ // Merge,filter,sort the custom buttons and built-in buttons
+ if (this.options) {
+ // Filter the default button name in the options.disableButtons
+ let deaultButtons = defaultOptions.buttons
+ let customButtons = this.options.buttons || []
+ if (this.options.disableButtons) {
+ deaultButtons = deaultButtons.filter(
+ (buttonOption) =>
+ this.options.disableButtons.indexOf(buttonOption.name) < 0
+ )
+ customButtons = customButtons.filter(
+ (buttonOption) =>
+ this.options.disableButtons.indexOf(buttonOption.name) < 0
+ )
+ }
+ // Insert the custom buttons into deault buttons by the index of the custom button option
+ if (customButtons) {
+ customButtons.forEach((element) => {
+ deaultButtons.splice(element.index, 0, element)
+ })
+ }
+ console.log(deaultButtons)
+ this.options.buttons = deaultButtons
+ }
+}
+
// register tree, text, and preview modes
JSONEditor.registerMode(treeModeMixins)
JSONEditor.registerMode(textModeMixins)
diff --git a/src/js/defaultOptions.js b/src/js/defaultOptions.js
new file mode 100644
index 000000000..5cf57bb16
--- /dev/null
+++ b/src/js/defaultOptions.js
@@ -0,0 +1,214 @@
+import { translate } from './i18n'
+
+let index = 1
+
+const expandAllButton = {
+ index: index++,
+ name: 'expandAll',
+ align: 'left',
+ type: 'button',
+ title: translate('expandAll'),
+ className: 'jsoneditor-expand-all',
+ mode: ['tree', 'view', 'form'],
+ target: function (editor, button) {
+ editor.expandAll()
+ }
+}
+
+const collapseAllButton = {
+ index: index++,
+ name: 'collapseAll',
+ align: 'left',
+ type: 'button',
+ title: translate('collapseAll'),
+ className: 'jsoneditor-collapse-all',
+ mode: ['tree', 'view', 'form'],
+ target: function (editor) {
+ editor.collapseAll()
+ }
+}
+
+const formatButton = {
+ index: index++,
+ name: 'format',
+ align: 'left',
+ type: 'button',
+ title: translate('formatTitle'),
+ className: 'jsoneditor-format',
+ mode: ['code', 'text'],
+ target: function (editor) {
+ try {
+ editor.format()
+ editor._onChange()
+ } catch (err) {
+ editor._onError(err)
+ }
+ }
+}
+
+const compactButton = {
+ index: index++,
+ name: 'compact',
+ align: 'left',
+ type: 'button',
+ title: translate('compactTitle'),
+ className: 'jsoneditor-compact',
+ mode: ['code', 'text'],
+ target: function (editor) {
+ try {
+ editor.compact()
+ editor._onChange()
+ } catch (err) {
+ editor._onError(err)
+ }
+ }
+}
+
+const sortButton = {
+ index: index++,
+ name: 'sort',
+ align: 'left',
+ type: 'button',
+ title: translate('sortTitleShort'),
+ className: 'jsoneditor-sort',
+ mode: ['code', 'text', 'tree', 'view', 'form'],
+ target: function (editor) {
+ if (['code', 'text'].indexOf(editor.options.mode) !== -1) {
+ editor._showSortModal()
+ } else if (['tree', 'view', 'form'].indexOf(editor.options.mode) !== -1) {
+ editor.node.showSortModal()
+ } else {
+ console.warn('Sort unsupport for mode ' + editor.options.mode)
+ }
+ },
+ checkEnableKey: 'enableSort'
+}
+
+const transformButton = {
+ index: index++,
+ name: 'transform',
+ align: 'left',
+ type: 'button',
+ title: translate('transformTitleShort'),
+ className: 'jsoneditor-transform',
+ mode: ['code', 'text', 'tree', 'view', 'form'],
+ target: function (editor) {
+ if (['code', 'text'].indexOf(editor.options.mode) !== -1) {
+ editor._showTransformModal()
+ } else if (['tree', 'view', 'form'].indexOf(editor.options.mode) !== -1) {
+ editor.node.showTransformModal()
+ } else {
+ console.warn('Transform unsupport for mode ' + editor.options.mode)
+ }
+ },
+ checkEnableKey: 'enableTransform'
+}
+
+const repairButton = {
+ index: index++,
+ name: 'repair',
+ align: 'left',
+ type: 'button',
+ title: translate('repairTitle'),
+ className: 'jsoneditor-repair',
+ mode: ['code', 'text'],
+ target: function (editor) {
+ try {
+ editor.repair()
+ editor._onChange()
+ } catch (err) {
+ editor._onError(err)
+ }
+ }
+}
+
+const undoButtonForCodeMode = {
+ index: index++,
+ name: 'undo',
+ align: 'left',
+ type: 'button',
+ title: translate('undo'),
+ className: 'jsoneditor-undo jsoneditor-separator',
+ mode: ['code'],
+ target: function (editor) {
+ editor.aceEditor.getSession().getUndoManager().undo()
+ },
+ afterCreate: function (editor, buttonElement) {
+ editor.dom.undo = buttonElement
+ }
+}
+
+const redoButtonForCodeMode = {
+ index: index++,
+ name: 'redo',
+ align: 'left',
+ type: 'button',
+ title: translate('redo'),
+ className: 'jsoneditor-redo',
+ mode: ['code'],
+ target: function (editor) {
+ editor.aceEditor.getSession().getUndoManager().redo()
+ },
+ afterCreate: function (editor, buttonElement) {
+ editor.dom.redo = buttonElement
+ }
+}
+
+const undoRedoButtonPairForTreeMode = {
+ index: index++,
+ name: 'undoRedoPair',
+ checkInEditorKey: 'history',
+ mode: ['tree', 'view', 'form'],
+ afterCreate: function (editor, createdButtonMap) {
+ editor.history.onChange = () => {
+ createdButtonMap.undo.disabled = !editor.history.canUndo()
+ createdButtonMap.redo.disabled = !editor.history.canRedo()
+ }
+ editor.history.onChange()
+ },
+ children: [
+ {
+ name: 'undo',
+ align: 'left',
+ type: 'button',
+ title: translate('undo'),
+ className: 'jsoneditor-undo jsoneditor-separator',
+ target: function (editor) {
+ editor._onUndo()
+ },
+ afterCreate: function (editor, buttonElement) {
+ editor.dom.undo = buttonElement
+ }
+ },
+ {
+ name: 'redo',
+ align: 'left',
+ type: 'button',
+ title: translate('redo'),
+ className: 'jsoneditor-redo',
+ target: function (editor) {
+ editor._onRedo()
+ },
+ afterCreate: function (editor, buttonElement) {
+ editor.dom.redo = buttonElement
+ }
+ }
+ ]
+}
+
+const _buttons = [
+ formatButton,
+ compactButton,
+ sortButton,
+ transformButton,
+ repairButton,
+ undoButtonForCodeMode,
+ redoButtonForCodeMode,
+ expandAllButton,
+ collapseAllButton,
+ undoRedoButtonPairForTreeMode
+]
+
+export const defaultOptions = {
+ buttons: _buttons
+}
diff --git a/src/js/textmode.js b/src/js/textmode.js
index c77875c8f..f269c1301 100644
--- a/src/js/textmode.js
+++ b/src/js/textmode.js
@@ -5,9 +5,8 @@ import ace from './ace'
import { DEFAULT_MODAL_ANCHOR } from './constants'
import { ErrorTable } from './ErrorTable'
import { FocusTracker } from './FocusTracker'
-import { setLanguage, setLanguages, translate } from './i18n'
+import { setLanguage, setLanguages } from './i18n'
import { createQuery, executeQuery } from './jmespathQuery'
-import { ModeSwitcher } from './ModeSwitcher'
import { showSortModal } from './showSortModal'
import { showTransformModal } from './showTransformModal'
import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor'
@@ -131,108 +130,8 @@ textmode.create = function (container, options = {}) {
this.menu.className = 'jsoneditor-menu'
this.frame.appendChild(this.menu)
- // create format button
- const buttonFormat = document.createElement('button')
- buttonFormat.type = 'button'
- buttonFormat.className = 'jsoneditor-format'
- buttonFormat.title = translate('formatTitle')
- this.menu.appendChild(buttonFormat)
- buttonFormat.onclick = () => {
- try {
- me.format()
- me._onChange()
- } catch (err) {
- me._onError(err)
- }
- }
-
- // create compact button
- const buttonCompact = document.createElement('button')
- buttonCompact.type = 'button'
- buttonCompact.className = 'jsoneditor-compact'
- buttonCompact.title = translate('compactTitle')
- this.menu.appendChild(buttonCompact)
- buttonCompact.onclick = () => {
- try {
- me.compact()
- me._onChange()
- } catch (err) {
- me._onError(err)
- }
- }
-
- // create sort button
- if (this.options.enableSort) {
- const sort = document.createElement('button')
- sort.type = 'button'
- sort.className = 'jsoneditor-sort'
- sort.title = translate('sortTitleShort')
- sort.onclick = () => {
- me._showSortModal()
- }
- this.menu.appendChild(sort)
- }
-
- // create transform button
- if (this.options.enableTransform) {
- const transform = document.createElement('button')
- transform.type = 'button'
- transform.title = translate('transformTitleShort')
- transform.className = 'jsoneditor-transform'
- transform.onclick = () => {
- me._showTransformModal()
- }
- this.menu.appendChild(transform)
- }
-
- // create repair button
- const buttonRepair = document.createElement('button')
- buttonRepair.type = 'button'
- buttonRepair.className = 'jsoneditor-repair'
- buttonRepair.title = translate('repairTitle')
- this.menu.appendChild(buttonRepair)
- buttonRepair.onclick = () => {
- try {
- me.repair()
- me._onChange()
- } catch (err) {
- me._onError(err)
- }
- }
-
- // create undo/redo buttons
- if (this.mode === 'code') {
- // create undo button
- const undo = document.createElement('button')
- undo.type = 'button'
- undo.className = 'jsoneditor-undo jsoneditor-separator'
- undo.title = translate('undo')
- undo.onclick = () => {
- this.aceEditor.getSession().getUndoManager().undo()
- }
- this.menu.appendChild(undo)
- this.dom.undo = undo
-
- // create redo button
- const redo = document.createElement('button')
- redo.type = 'button'
- redo.className = 'jsoneditor-redo'
- redo.title = translate('redo')
- redo.onclick = () => {
- this.aceEditor.getSession().getUndoManager().redo()
- }
- this.menu.appendChild(redo)
- this.dom.redo = redo
- }
-
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) {
- // switch mode and restore focus
- me.setMode(mode)
- me.modeSwitcher.focus()
- })
- }
+ // create buttons defined in options.buttons
+ this._createButtons()
if (this.mode === 'code') {
const poweredBy = document.createElement('a')
diff --git a/src/js/treemode.js b/src/js/treemode.js
index 9849e4224..f6f2656de 100644
--- a/src/js/treemode.js
+++ b/src/js/treemode.js
@@ -6,7 +6,6 @@ import { FocusTracker } from './FocusTracker'
import { Highlighter } from './Highlighter'
import { setLanguage, setLanguages, translate } from './i18n'
import { createQuery, executeQuery } from './jmespathQuery'
-import { ModeSwitcher } from './ModeSwitcher'
import { Node } from './Node'
import { NodeHistory } from './NodeHistory'
import { SearchBox } from './SearchBox'
@@ -974,91 +973,8 @@ treemode._createFrame = function () {
this.menu.className = 'jsoneditor-menu'
this.frame.appendChild(this.menu)
- // create expand all button
- const expandAll = document.createElement('button')
- expandAll.type = 'button'
- expandAll.className = 'jsoneditor-expand-all'
- expandAll.title = translate('expandAll')
- expandAll.onclick = () => {
- editor.expandAll()
- }
- this.menu.appendChild(expandAll)
-
- // create collapse all button
- const collapseAll = document.createElement('button')
- collapseAll.type = 'button'
- collapseAll.title = translate('collapseAll')
- collapseAll.className = 'jsoneditor-collapse-all'
- collapseAll.onclick = () => {
- editor.collapseAll()
- }
- this.menu.appendChild(collapseAll)
-
- // create sort button
- if (this.options.enableSort) {
- const sort = document.createElement('button')
- sort.type = 'button'
- sort.className = 'jsoneditor-sort'
- sort.title = translate('sortTitleShort')
- sort.onclick = () => {
- editor.node.showSortModal()
- }
- this.menu.appendChild(sort)
- }
-
- // create transform button
- if (this.options.enableTransform) {
- const transform = document.createElement('button')
- transform.type = 'button'
- transform.title = translate('transformTitleShort')
- transform.className = 'jsoneditor-transform'
- transform.onclick = () => {
- editor.node.showTransformModal()
- }
- this.menu.appendChild(transform)
- }
-
- // create undo/redo buttons
- if (this.history) {
- // create undo button
- const undo = document.createElement('button')
- undo.type = 'button'
- undo.className = 'jsoneditor-undo jsoneditor-separator'
- undo.title = translate('undo')
- undo.onclick = () => {
- editor._onUndo()
- }
- this.menu.appendChild(undo)
- this.dom.undo = undo
-
- // create redo button
- const redo = document.createElement('button')
- redo.type = 'button'
- redo.className = 'jsoneditor-redo'
- redo.title = translate('redo')
- redo.onclick = () => {
- editor._onRedo()
- }
- this.menu.appendChild(redo)
- this.dom.redo = redo
-
- // register handler for onchange of history
- this.history.onChange = () => {
- undo.disabled = !editor.history.canUndo()
- redo.disabled = !editor.history.canRedo()
- }
- this.history.onChange()
- }
-
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- const me = this
- this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) {
- // switch mode and restore focus
- me.setMode(mode)
- me.modeSwitcher.focus()
- })
- }
+ // create buttons defined in options.buttons
+ this._createButtons()
// create search box
if (this.options.search) {