Skip to content

Instantly share code, notes, and snippets.

@bassmanitram
Last active July 13, 2022 14:22
Show Gist options
  • Save bassmanitram/9cdf406007b2ad7103aa1f1bc1b3eb5e to your computer and use it in GitHub Desktop.
Save bassmanitram/9cdf406007b2ad7103aa1f1bc1b3eb5e to your computer and use it in GitHub Desktop.
JSON Schema Editor example. See https://github.com/json-editor/json-editor
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>JSON Schema Editor Extreme Example</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/css/jsoneditor.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/spectre.css@latest/dist/spectre-icons.min.css">
</head>
<body>
<div class='container'>
<div class='row'>
<div class='col-md-12'>
<h1>Very Advanced Recursive JSON Editor Example</h1>
<p>
This example demonstrates the use of a recursive schema - a Menu model, along with a slew
of other JSON Schema Editor and other desirable features:
</p>
<ul>
<li>Tabbed and table-formatted categories</li>
<li>Dynamic Labels</li>
<li>Editor-specific configuration (in the JSON Schema options objects) - e.g. no properties selector for the root node</li>
<li>Undo, Redo and Save functionality bound to buttons added to the root editor title</li>
<li>Hidden constant properties</li>
<li>Informational links</li>
<li>Various JSON Schema features around value validation ...</li>
<li>Commented-out code that shows how to load the schema and startval from a remote server (the example, of course, has those values hard-coded)</li>
</ul>
</div>
</div>
<div class='row'>
<div class='col-md-12'>
<div id='editor_holder'></div>
</div>
</div>
</div>
<script>
var startval = {
"actions": [
{
"type": "command",
"label": "Run in node",
"mimetypes": [
"application/javascript"
],
"command_line": "gnome-terminal -t \"Running Node %b\" --profile \"No Close\" -- node %b",
"cwd": "%d",
"max_items": 1
},
{
"type": "command",
"label": "Open in gvim",
"filetypes": [
"!directory",
"standard"
],
"command_line": "gvim %F"
},
{
"type": "menu",
"label": "Folder Actions",
"actions": [
{
"type": "command",
"label": "Clone GIT Repo Here",
"mimetypes": [
"inode/directory"
],
"command_line": "zenity-git-clone.sh %f",
"use_shell": true,
"max_items": 1
},
{
"type": "command",
"label": "Execute Command Here",
"mimetypes": [
"inode/directory"
],
"command_line": "zenity-exec-in-dir.sh %f",
"use_shell": true,
"max_items": 1
},
{
"type": "command",
"label": "Start HTTP Server Here",
"mimetypes": [
"inode/directory"
],
"command_line": "gnome-terminal -t \"HTTP Server on %f\" -- python -m http.server --bind 127.0.0.1",
"cwd": "%f",
"max_items": 1
}
]
},
{
"type": "menu",
"label": "Copy Details",
"actions": [
{
"type": "command",
"label": "Copy Name",
"use_shell": true,
"command_line": "arg1-to-clipboard.sh %B"
},
{
"type": "command",
"label": "Copy Path",
"use_shell": true,
"command_line": "arg1-to-clipboard.sh %F"
},
{
"type": "command",
"label": "Copy URI",
"use_shell": true,
"command_line": "arg1-to-clipboard.sh %U"
}
]
}
],
"debug": false
}
var schema = {
"$schema": "http://json-schema.org/draft-06/schema#",
"$ref": "#/definitions/MenuModeler",
"definitions": {
"MimeType": {
"type": "string",
"pattern": "^(!?[A-Za-z0-9-]+\\/(([A-Za-z0-9-]+)|\\*))|(\\*)|(\\*\\/\\*)$",
"title": "MimeType"
},
"FileType": {
"type": "string",
"enum": [
"unknown",
"file",
"directory",
"symbolic-link",
"special",
"shortcut",
"mountable",
"standard",
"!unknown",
"!file",
"!directory",
"!symbolic-link",
"!special",
"!shortcut",
"!mountable",
"!standard"
],
"title": "File type",
"options": {
"infoText": "A standard Nautilus file type that, when matched, allows the associated command action to appear in its assigned menu. If the file type is prefixed with '!' character then the selected items must NOT be of this type."
}
},
"Action": {
"type": "object",
"format": "categories",
"headerTemplate": "{{ self.type }}: {{ self.label }}",
"oneOf": [
{
"title": "Command",
"$ref": "#/definitions/Command"
},
{
"title": "Menu",
"$ref": "#/definitions/Menu"
}
],
"title": "Action",
"options": {
"infoText": "An entry that will appear in the Nautilus context menu. Such an entry can be a Command or a nested Menu."
}
},
"MenuModeler": {
"type": "object",
"additionalProperties": false,
"properties": {
"actions": {
"type": "array",
"format": "tabs",
"items": {
"$ref": "#/definitions/Action"
},
"title": "Top Level Actions",
"options": {
"infoText": "The list of command configurations and/or submenus that will be added to the Nautilus context menu."
}
},
"debug": {
"title": "Enable debugging output",
"type": "boolean",
"format": "checkbox",
"options": {
"infoText": "When set to true, extra debugging information is sent to the Nautilus stdout/stderr destinations."
}
}
},
"required": [
"actions"
],
"options": {
"disable_properties": true
},
"title": "Actions For Nautilus configuration"
},
"Menu": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"const": "menu",
"default": "menu",
"title": "Menu",
"options": {
"hidden": true
}
},
"label": {
"type": "string",
"minLength": 1,
"title": "Label for the menu",
"options": {
"infoText": "The label that will appear in the context menu for this sub menu."
}
},
"actions": {
"type": "array",
"format": "tabs",
"items": {
"$ref": "#/definitions/Action"
},
"title": "Submenu Actions",
"options": {
"infoText": "The list of command and/or menu actions that will be displayed when this menu action is clicked on."
}
}
},
"required": [
"label",
"type",
"actions"
],
"title": "Submenu",
"options": {
"infoText": "An entry in the Nautilus context menu or sub menu that, when clicked on, results in a sub menu being displayed."
}
},
"Command": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"const": "command",
"default": "command",
"title": "Command",
"options": {
"hidden": true
}
},
"label": {
"type": "string",
"minLength": 1,
"title": "Label for the command",
"options": {
"infoText": "The label that will appear in the context menu for this command."
}
},
"command_line": {
"type": "string",
"minLength": 1,
"title": "Command line to execute",
"options": {
"infoText": "The command line to execute when the action is clicked on. Placeholders are allowed and affect the semantics of the execution."
},
"links": [
{
"rel": "Placeholder help",
"href": "https://example.com",
"mediaType": "text/html"
}
]
},
"cwd": {
"type": "string",
"minLength": 1,
"title": "Current working directory",
"options": {
"infoText": "The Current working directory to set when executing the command. Placeholders are allowed."
}
},
"use_shell": {
"type": "boolean",
"format": "checkbox",
"title": "Use the system shell to run the command",
"options": {
"infoText": "Instead of directly executing the command, execute it using the default shell command."
}
},
"max_items": {
"type": "integer",
"minimum": 1,
"title": "Maximum number of selected files",
"options": {
"infoText": "The maximum number of files in the selection for which this action will be displayed."
}
},
"mimetypes": {
"type": "array",
"uniqueItems": true,
"format": "table",
"items": {
"$ref": "#/definitions/MimeType"
},
"title": "Mimetype rules",
"options": {
"infoText": "A list of standard mimetype specifications that, when matched, allow the associated command action to appear in its assigned menu. If a mimetype is prefixed with '!' character then the selected items must NOT match that type."
}
},
"filetypes": {
"type": "array",
"uniqueItems": true,
"format": "table",
"items": {
"$ref": "#/definitions/FileType"
},
"title": "Filetype rules",
"options": {
"infoText": "A list of file types for which the action will be displayed, or not be displayed in the event of a '!' prefix."
}
}
},
"required": [
"label",
"type",
"command_line"
],
"title": "Command",
"options": {
"infoText": "An entry in the Nautilus context menu or sub menu that, when clicked on, results in a command being executed."
}
}
}
}
var editorConfig;
/*
* Because this is a local UI tightly related to
* backend server that it is using, unload should
* stop that server :)
*/
function onUnload(event) {
console.log("Running onUnload hook");
//
// Uncomment and adjust to send a "done" command
// to the backend server
//
// $.ajax({
// url: '/terminate',
// type: 'post',
// data: ""
// });
}
addEventListener("unload", onUnload);
var previous_values = [];
var saved_value;
var current_value_index = 0;
var undo_button;
var redo_button;
var save_button;
var restart_button;
var editor;
function setUndoRedoButtonStates() {
undo_button.disabled = (current_value_index == 0);
redo_button.disabled = (current_value_index == (previous_values.length - 1));
}
function setEditorValueFromPreviousValues(editor) {
editor.setValue(JSON.parse(previous_values[current_value_index]));
setUndoRedoButtonStates();
}
function undo(e) {
if (current_value_index > 0) {
current_value_index--;
setEditorValueFromPreviousValues(editor);
}
}
function redo(e) {
if (current_value_index < (previous_values.length - 1)) {
current_value_index++;
setEditorValueFromPreviousValues(editor);
}
}
function saveConfig(e) {
e.preventDefault();
editor.disable();
save_button.disabled = true;
var errors = editor.validate();
if (errors.length > 0) {
alert("There are validation errors in the data");
return;
}
var data = JSON.stringify(editor.getValue());
console.log("saving config", data)
//
// Uncomment and adjust to send the modified data to the
// backend server
//
// $.ajax({
// url: '/config',
// type: 'post',
// data: data,
// headers: {
// "Content-Type": "application/json"
// },
// error: function (xh, msg, e) {
// console.error(msg, e);
// save_button.disabled = false;
// editor.enable();
// alert("An error ocurred when trying to save the configuration");
// },
// success: function (response) {
saved_value = data;
editor.enable();
// }
// });
}
function configChanged(e) {
new_value = JSON.stringify(editor.getValue());
if (previous_values[current_value_index] != new_value) {
//console.log("Editor really changed!")
/*
* The editor changed outside of undo-redo
* Everything after the current index is lost
* then this change is pushed.
*/
previous_values = previous_values.slice(0, current_value_index + 1);
current_value_index = previous_values.push(new_value) - 1;
setUndoRedoButtonStates();
} else {
//console.log("Editor didn't really change!")
}
if (editor.validation_results.length > 0) {
console.log(editor.validation_results);
save_button.disabled = true
} else {
save_button.disabled = (new_value == saved_value)
}
}
function finalizeEditorConfig(e) {
console.log(editor)
/*
* Create an undo button - icon only
*/
undo_button = editor.root.getButton('Undo', 'arrow-left', 'Undo');
undo_button.lastChild.style = "display: none;";
undo_button.addEventListener('click', undo);
/*
* Create a redo button - icon only
*/
redo_button = editor.root.getButton('Redo', 'arrow-right', 'Redo');
redo_button.lastChild.style = "display: none;";
redo_button.addEventListener('click', redo);
/*
* Set undo/redo button state (disabled at this point)
*/
setUndoRedoButtonStates();
/*
* Create a save config button and disable it
*/
save_button = editor.root.getButton('Save', 'save', 'Save');
save_button.addEventListener('click', saveConfig, false);
save_button.disabled = true
/*
* Add the buttons to the top of the page
*/
var button_holder = editor.root.theme.getHeaderButtonHolder();
button_holder.appendChild(undo_button);
button_holder.appendChild(redo_button);
button_holder.appendChild(save_button);
editor.root.header.parentNode.insertBefore(button_holder, editor.root.header.nextSibling);
/*
* Wire up the change watcher
*/
editor.on('change', configChanged);
}
/*
* Kick off page initialization from server-side objects
*/
//
// Uncomment and adjust to obtain the initial schema and
// start val from the backend server
//
// fetch("/config")
// .then(response => {
// return response.json()
// })
// .then(config => {
// startval = config;
// return fetch("/schema");
// })
// .then(response => {
// return response.json()
// })
// .then(schema => {
// Initialize the editor
editorConfig = {
"ajax": true,
"theme": "bootstrap3",
"iconlib": "spectre",
"object_layout": "normal",
"template": "default",
"show_errors": "interaction",
"required_by_default": 0,
"no_additional_properties": 1,
"display_required_only": 0,
"remove_empty_properties": 0,
"keep_oneof_values": 0,
"ajaxCredentials": 0,
"show_opt_in": 0,
"disable_edit_json": 1,
"disable_collapse": 1,
"disable_properties": 0,
"disable_array_add": 0,
"disable_array_reorder": 0,
"disable_array_delete": 0,
"enable_array_copy": 0,
"array_controls_top": 1,
"disable_array_delete_all_rows": 0,
"disable_array_delete_last_row": 1,
"prompt_before_delete": 1,
"lib_aceeditor": 0,
"lib_autocomplete": 0,
"lib_sceditor": 0,
"lib_simplemde": 0,
"lib_select2": 0,
"lib_selectize": 0,
"lib_choices": 0,
"lib_flatpickr": 0,
"lib_signaturepad": 0,
"lib_mathjs": 0,
"lib_cleavejs": 0,
"lib_jodit": 0,
"lib_jquery": 0,
"lib_dompurify": 0,
"remove_button_labels": 0,
"schema": schema,
"startval": startval,
"use_default_values": true
}
saved_value = JSON.stringify(startval);
previous_values[0] = saved_value;
editor = new window.JSONEditor(document.querySelector("#editor_holder"), editorConfig);
editor.on('ready', finalizeEditorConfig);
// })
// .catch(e => {
// console.log("Page initialization failed", e);
// alert("Failed to initialize the editor page");
// });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment