3898 lines
114 KiB
JavaScript
3898 lines
114 KiB
JavaScript
/**
|
||
* @file jsoneditor.js
|
||
*
|
||
* @brief
|
||
* JSONEditor is a web-based tool to view, edit, and format JSON.
|
||
* It shows data a clear, editable treeview.
|
||
*
|
||
* Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
|
||
*
|
||
* @license
|
||
* This json editor is open sourced with the intention to use the editor as
|
||
* a component in your own application. Not to just copy and monetize the editor
|
||
* as it is.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||
* use this file except in compliance with the License. You may obtain a copy
|
||
* of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
* License for the specific language governing permissions and limitations under
|
||
* the License.
|
||
*
|
||
* Copyright (c) 2011-2012 Jos de Jong, http://jsoneditoronline.org
|
||
*
|
||
* @author Jos de Jong, <wjosdejong@gmail.com>
|
||
* @date 2012-12-08
|
||
*/
|
||
// Internet Explorer 8 and older does not support Array.indexOf,
|
||
// so we define it here in that case
|
||
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
|
||
if (!Array.prototype.indexOf) {
|
||
Array.prototype.indexOf = function(obj) {
|
||
for (var i = 0; i < this.length; i++) {
|
||
if (this[i] == obj) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
};
|
||
}
|
||
|
||
// Internet Explorer 8 and older does not support Array.forEach,
|
||
// so we define it here in that case
|
||
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
|
||
if (!Array.prototype.forEach) {
|
||
Array.prototype.forEach = function(fn, scope) {
|
||
for (var i = 0, len = this.length; i < len; ++i) {
|
||
fn.call(scope || this, this[i], i, this);
|
||
}
|
||
};
|
||
}
|
||
|
||
// define variable JSON, needed for correct error handling on IE7 and older
|
||
var JSON;
|
||
|
||
/**
|
||
* JSONEditor
|
||
* @param {Element} container Container element
|
||
* @param {Object} [options] Object with options. available options:
|
||
* {String} mode Editor mode. Available values:
|
||
* 'editor' (default), 'viewer'.
|
||
* {Boolean} search Enable search box.
|
||
* True by default
|
||
* {Boolean} history Enable history (undo/redo).
|
||
* True by default
|
||
* {function} change Callback method, triggered
|
||
* on change of contents
|
||
* {String} name Field name for the root node.
|
||
* @param {Object | undefined} json JSON object
|
||
*/
|
||
JSONEditor = function(container, options, json) {
|
||
// check availability of JSON parser (not available in IE7 and older)
|
||
if (!JSON) {
|
||
throw new Error(
|
||
"您当前使用的浏览器不支持 JSON. \n\n" +
|
||
"请下载安装最新版本的浏览, 本站推荐Google Chrome.\n" +
|
||
"(PS: 当前主流浏览器都支持JSON)."
|
||
);
|
||
}
|
||
|
||
if (!container) {
|
||
throw new Error("没有提供容器元素.");
|
||
}
|
||
this.container = container;
|
||
this.dom = {};
|
||
|
||
this._setOptions(options);
|
||
|
||
if (this.options.history && this.editable) {
|
||
this.history = new JSONEditor.History(this);
|
||
}
|
||
|
||
this._createFrame();
|
||
this._createTable();
|
||
|
||
this.set(json || {});
|
||
};
|
||
|
||
/**
|
||
* Initialize and set default options
|
||
* @param {Object} [options] Object with options. available options:
|
||
* {String} mode Editor mode. Available values:
|
||
* 'editor' (default), 'viewer'.
|
||
* {Boolean} search Enable search box.
|
||
* True by default.
|
||
* {Boolean} history Enable history (undo/redo).
|
||
* True by default.
|
||
* {function} change Callback method, triggered
|
||
* on change of contents.
|
||
* {String} name Field name for the root node.
|
||
* @private
|
||
*/
|
||
JSONEditor.prototype._setOptions = function(options) {
|
||
this.options = {
|
||
search: true,
|
||
history: true,
|
||
mode: "editor",
|
||
name: undefined // field name of root node
|
||
};
|
||
|
||
// copy all options
|
||
if (options) {
|
||
for (var prop in options) {
|
||
if (options.hasOwnProperty(prop)) {
|
||
this.options[prop] = options[prop];
|
||
}
|
||
}
|
||
|
||
// check for deprecated options
|
||
if (options.enableSearch) {
|
||
// deprecated since version 1.6.0, 2012-11-03
|
||
this.options.search = options.enableSearch;
|
||
// console.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
|
||
}
|
||
if (options.enableHistory) {
|
||
// deprecated since version 1.6.0, 2012-11-03
|
||
this.options.search = options.enableHistory;
|
||
// console.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
|
||
}
|
||
}
|
||
|
||
// interpret the options
|
||
this.editable = this.options.mode != "viewer";
|
||
};
|
||
|
||
// node currently being edited
|
||
JSONEditor.focusNode = undefined;
|
||
|
||
/**
|
||
* Set JSON object in editor
|
||
* @param {Object | undefined} json JSON data
|
||
* @param {String} [name] Optional field name for the root node.
|
||
* Can also be set using setName(name).
|
||
*/
|
||
JSONEditor.prototype.set = function(json, name) {
|
||
// adjust field name for root node
|
||
if (name) {
|
||
this.options.name = name;
|
||
}
|
||
|
||
// verify if json is valid JSON, ignore when a function
|
||
if (json instanceof Function || json === undefined) {
|
||
this.clear();
|
||
} else {
|
||
this.content.removeChild(this.table); // Take the table offline
|
||
|
||
// replace the root node
|
||
var params = {
|
||
field: this.options.name,
|
||
value: json
|
||
};
|
||
var node = new JSONEditor.Node(this, params);
|
||
this._setRoot(node);
|
||
|
||
// expand
|
||
var recurse = false;
|
||
this.node.expand(recurse);
|
||
|
||
this.content.appendChild(this.table); // Put the table online again
|
||
}
|
||
|
||
// TODO: maintain history, store last state and previous document
|
||
if (this.history) {
|
||
this.history.clear();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get JSON object from editor
|
||
* @return {Object | undefined} json
|
||
*/
|
||
JSONEditor.prototype.get = function() {
|
||
// remove focus from currently edited node
|
||
if (JSONEditor.focusNode) {
|
||
JSONEditor.focusNode.blur();
|
||
}
|
||
|
||
if (this.node) {
|
||
return this.node.getValue();
|
||
} else {
|
||
return undefined;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Set a field name for the root node.
|
||
* @param {String | undefined} name
|
||
*/
|
||
JSONEditor.prototype.setName = function(name) {
|
||
this.options.name = name;
|
||
if (this.node) {
|
||
this.node.updateField(this.options.name);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get the field name for the root node.
|
||
* @return {String | undefined} name
|
||
*/
|
||
JSONEditor.prototype.getName = function() {
|
||
return this.options.name;
|
||
};
|
||
|
||
/**
|
||
* Remove the root node from the editor
|
||
*/
|
||
JSONEditor.prototype.clear = function() {
|
||
if (this.node) {
|
||
this.node.collapse();
|
||
this.tbody.removeChild(this.node.getDom());
|
||
delete this.node;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Set the root node for the json editor
|
||
* @param {JSONEditor.Node} node
|
||
* @private
|
||
*/
|
||
JSONEditor.prototype._setRoot = function(node) {
|
||
this.clear();
|
||
|
||
this.node = node;
|
||
|
||
// append to the dom
|
||
this.tbody.appendChild(node.getDom());
|
||
};
|
||
|
||
/**
|
||
* Search text in all nodes
|
||
* The nodes will be expanded when the text is found one of its childs,
|
||
* else it will be collapsed. Searches are case insensitive.
|
||
* @param {String} text
|
||
* @return {Object[]} results Array with nodes containing the search results
|
||
* The result objects contains fields:
|
||
* - {JSONEditor.Node} node,
|
||
* - {String} elem the dom element name where
|
||
* the result is found ('field' or
|
||
* 'value')
|
||
*/
|
||
JSONEditor.prototype.search = function(text) {
|
||
var results;
|
||
if (this.node) {
|
||
this.content.removeChild(this.table); // Take the table offline
|
||
results = this.node.search(text);
|
||
this.content.appendChild(this.table); // Put the table online again
|
||
} else {
|
||
results = [];
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
/**
|
||
* Expand all nodes
|
||
*/
|
||
JSONEditor.prototype.expandAll = function() {
|
||
if (this.node) {
|
||
this.content.removeChild(this.table); // Take the table offline
|
||
this.node.expand();
|
||
this.content.appendChild(this.table); // Put the table online again
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Collapse all nodes
|
||
*/
|
||
JSONEditor.prototype.collapseAll = function() {
|
||
if (this.node) {
|
||
this.content.removeChild(this.table); // Take the table offline
|
||
this.node.collapse();
|
||
this.content.appendChild(this.table); // Put the table online again
|
||
}
|
||
};
|
||
|
||
/**
|
||
* The method onChange is called whenever a field or value is changed, created,
|
||
* deleted, duplicated, etc.
|
||
* @param {String} action Change action. Available values: "editField",
|
||
* "editValue", "changeType", "appendNode",
|
||
* "removeNode", "duplicateNode", "moveNode", "expand",
|
||
* "collapse".
|
||
* @param {Object} params Object containing parameters describing the change.
|
||
* The parameters in params depend on the action (for
|
||
* example for "editValue" the Node, old value, and new
|
||
* value are provided). params contains all information
|
||
* needed to undo or redo the action.
|
||
*/
|
||
JSONEditor.prototype.onAction = function(action, params) {
|
||
// add an action to the history
|
||
if (this.history) {
|
||
this.history.add(action, params);
|
||
}
|
||
|
||
// trigger the onChange callback
|
||
if (this.options.change) {
|
||
try {
|
||
this.options.change();
|
||
} catch (err) {
|
||
//console.log('Error in change callback: ', err);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Set the focus to the JSONEditor. A hidden input field will be created
|
||
* which captures key events
|
||
*/
|
||
// TODO: use the focus method?
|
||
JSONEditor.prototype.focus = function() {
|
||
/*
|
||
if (!this.dom.focus) {
|
||
this.dom.focus = document.createElement('input');
|
||
this.dom.focus.className = 'jsoneditor-hidden-focus';
|
||
|
||
var editor = this;
|
||
this.dom.focus.onblur = function () {
|
||
// remove itself
|
||
if (editor.dom.focus) {
|
||
var focus = editor.dom.focus;
|
||
delete editor.dom.focus;
|
||
editor.frame.removeChild(focus);
|
||
}
|
||
};
|
||
|
||
// attach the hidden input box to the DOM
|
||
if (this.frame.firstChild) {
|
||
this.frame.insertBefore(this.dom.focus, this.frame.firstChild);
|
||
}
|
||
else {
|
||
this.frame.appendChild(this.dom.focus);
|
||
}
|
||
}
|
||
this.dom.focus.focus();
|
||
*/
|
||
};
|
||
|
||
/**
|
||
* Adjust the scroll position such that given top position is shown at 1/4
|
||
* of the window height.
|
||
* @param {Number} top
|
||
*/
|
||
JSONEditor.prototype.scrollTo = function(top) {
|
||
var content = this.content;
|
||
if (content) {
|
||
// cancel any running animation
|
||
var editor = this;
|
||
if (editor.animateTimeout) {
|
||
clearTimeout(editor.animateTimeout);
|
||
delete editor.animateTimeout;
|
||
}
|
||
|
||
// calculate final scroll position
|
||
var height = content.clientHeight;
|
||
var bottom = content.scrollHeight - height;
|
||
var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);
|
||
|
||
// animate towards the new scroll position
|
||
var animate = function() {
|
||
var scrollTop = content.scrollTop;
|
||
var diff = finalScrollTop - scrollTop;
|
||
if (Math.abs(diff) > 3) {
|
||
content.scrollTop += diff / 3;
|
||
editor.animateTimeout = setTimeout(animate, 50);
|
||
}
|
||
};
|
||
animate();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @constructor JSONEditor.History
|
||
* Store action history, enables undo and redo
|
||
* @param {JSONEditor} editor
|
||
*/
|
||
JSONEditor.History = function(editor) {
|
||
this.editor = editor;
|
||
this.clear();
|
||
|
||
// map with all supported actions
|
||
this.actions = {
|
||
editField: {
|
||
undo: function(obj) {
|
||
obj.params.node.updateField(obj.params.oldValue);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.node.updateField(obj.params.newValue);
|
||
}
|
||
},
|
||
editValue: {
|
||
undo: function(obj) {
|
||
obj.params.node.updateValue(obj.params.oldValue);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.node.updateValue(obj.params.newValue);
|
||
}
|
||
},
|
||
appendNode: {
|
||
undo: function(obj) {
|
||
obj.params.parent.removeChild(obj.params.node);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.parent.appendChild(obj.params.node);
|
||
}
|
||
},
|
||
removeNode: {
|
||
undo: function(obj) {
|
||
var parent = obj.params.parent;
|
||
var beforeNode = parent.childs[obj.params.index] || parent.append;
|
||
parent.insertBefore(obj.params.node, beforeNode);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.parent.removeChild(obj.params.node);
|
||
}
|
||
},
|
||
duplicateNode: {
|
||
undo: function(obj) {
|
||
obj.params.parent.removeChild(obj.params.clone);
|
||
},
|
||
redo: function(obj) {
|
||
// TODO: insert after instead of insert before
|
||
obj.params.parent.insertBefore(obj.params.clone, obj.params.node);
|
||
}
|
||
},
|
||
changeType: {
|
||
undo: function(obj) {
|
||
obj.params.node.changeType(obj.params.oldType);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.node.changeType(obj.params.newType);
|
||
}
|
||
},
|
||
moveNode: {
|
||
undo: function(obj) {
|
||
obj.params.startParent.moveTo(obj.params.node, obj.params.startIndex);
|
||
},
|
||
redo: function(obj) {
|
||
obj.params.endParent.moveTo(obj.params.node, obj.params.endIndex);
|
||
}
|
||
}
|
||
|
||
// TODO: restore the original caret position and selection with each undo
|
||
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
|
||
};
|
||
};
|
||
|
||
/**
|
||
* The method onChange is executed when the History is changed, and can
|
||
* be overloaded.
|
||
*/
|
||
JSONEditor.History.prototype.onChange = function() {};
|
||
|
||
/**
|
||
* Add a new action to the history
|
||
* @param {String} action The executed action. Available actions: "editField",
|
||
* "editValue", "changeType", "appendNode",
|
||
* "removeNode", "duplicateNode", "moveNode"
|
||
* @param {Object} params Object containing parameters describing the change.
|
||
* The parameters in params depend on the action (for
|
||
* example for "editValue" the Node, old value, and new
|
||
* value are provided). params contains all information
|
||
* needed to undo or redo the action.
|
||
*/
|
||
JSONEditor.History.prototype.add = function(action, params) {
|
||
this.index++;
|
||
this.history[this.index] = {
|
||
action: action,
|
||
params: params,
|
||
timestamp: new Date()
|
||
};
|
||
|
||
// remove redo actions which are invalid now
|
||
if (this.index < this.history.length - 1) {
|
||
this.history.splice(this.index + 1, this.history.length - this.index - 1);
|
||
}
|
||
|
||
// fire onchange event
|
||
this.onChange();
|
||
};
|
||
|
||
/**
|
||
* Clear history
|
||
*/
|
||
JSONEditor.History.prototype.clear = function() {
|
||
this.history = [];
|
||
this.index = -1;
|
||
|
||
// fire onchange event
|
||
this.onChange();
|
||
};
|
||
|
||
/**
|
||
* Check if there is an action available for undo
|
||
* @return {Boolean} canUndo
|
||
*/
|
||
JSONEditor.History.prototype.canUndo = function() {
|
||
return this.index >= 0;
|
||
};
|
||
|
||
/**
|
||
* Check if there is an action available for redo
|
||
* @return {Boolean} canRedo
|
||
*/
|
||
JSONEditor.History.prototype.canRedo = function() {
|
||
return this.index < this.history.length - 1;
|
||
};
|
||
|
||
/**
|
||
* Undo the last action
|
||
*/
|
||
JSONEditor.History.prototype.undo = function() {
|
||
if (this.canUndo()) {
|
||
var obj = this.history[this.index];
|
||
if (obj) {
|
||
var action = this.actions[obj.action];
|
||
if (action && action.undo) {
|
||
action.undo(obj);
|
||
} else {
|
||
//console.log('Error: unknown action "' + obj.action + '"');
|
||
}
|
||
}
|
||
this.index--;
|
||
|
||
// fire onchange event
|
||
this.onChange();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Redo the last action
|
||
*/
|
||
JSONEditor.History.prototype.redo = function() {
|
||
if (this.canRedo()) {
|
||
this.index++;
|
||
|
||
var obj = this.history[this.index];
|
||
if (obj) {
|
||
if (obj) {
|
||
var action = this.actions[obj.action];
|
||
if (action && action.redo) {
|
||
action.redo(obj);
|
||
} else {
|
||
//console.log('Error: unknown action "' + obj.action + '"');
|
||
}
|
||
}
|
||
}
|
||
|
||
// fire onchange event
|
||
this.onChange();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @constructor JSONEditor.Node
|
||
* Create a new Node
|
||
* @param {JSONEditor} editor
|
||
* @param {Object} params Can contain parameters: field, fieldEditable, value.
|
||
*/
|
||
JSONEditor.Node = function(editor, params) {
|
||
this.editor = editor;
|
||
this.dom = {};
|
||
this.expanded = false;
|
||
|
||
if (params && params instanceof Object) {
|
||
this.setField(params.field, params.fieldEditable);
|
||
this.setValue(params.value);
|
||
} else {
|
||
this.setField();
|
||
this.setValue();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Set parent node
|
||
* @param {JSONEditor.Node} parent
|
||
*/
|
||
JSONEditor.Node.prototype.setParent = function(parent) {
|
||
this.parent = parent;
|
||
};
|
||
|
||
/**
|
||
* Get parent node. Returns undefined when no parent node is set.
|
||
* @return {JSONEditor.Node} parent
|
||
*/
|
||
JSONEditor.Node.prototype.getParent = function() {
|
||
return this.parent;
|
||
};
|
||
|
||
/**
|
||
* Set field
|
||
* @param {String} field
|
||
* @param {boolean} fieldEditable
|
||
*/
|
||
JSONEditor.Node.prototype.setField = function(field, fieldEditable) {
|
||
this.field = field;
|
||
this.fieldEditable = fieldEditable == true;
|
||
};
|
||
|
||
/**
|
||
* Get field
|
||
* @return {String}
|
||
*/
|
||
JSONEditor.Node.prototype.getField = function() {
|
||
if (this.field === undefined) {
|
||
this._getDomField();
|
||
}
|
||
|
||
return this.field;
|
||
};
|
||
|
||
/**
|
||
* Set value. Value is a JSON structure or an element String, Boolean, etc.
|
||
* @param {*} value
|
||
*/
|
||
JSONEditor.Node.prototype.setValue = function(value) {
|
||
var childValue, child;
|
||
|
||
// first clear all current childs (if any)
|
||
var childs = this.childs;
|
||
if (childs) {
|
||
while (childs.length) {
|
||
this.removeChild(childs[0]);
|
||
}
|
||
}
|
||
|
||
// TODO: remove the DOM of this Node
|
||
|
||
this.type = this._getType(value);
|
||
if (this.type == "array") {
|
||
// array
|
||
this.childs = [];
|
||
for (var i = 0, iMax = value.length; i < iMax; i++) {
|
||
childValue = value[i];
|
||
if (childValue !== undefined && !(childValue instanceof Function)) {
|
||
// ignore undefined and functions
|
||
child = new JSONEditor.Node(this.editor, {
|
||
value: childValue
|
||
});
|
||
this.appendChild(child);
|
||
}
|
||
}
|
||
this.value = "";
|
||
} else if (this.type == "object") {
|
||
// object
|
||
this.childs = [];
|
||
for (var childField in value) {
|
||
if (value.hasOwnProperty(childField)) {
|
||
childValue = value[childField];
|
||
if (childValue !== undefined && !(childValue instanceof Function)) {
|
||
// ignore undefined and functions
|
||
child = new JSONEditor.Node(this.editor, {
|
||
field: childField,
|
||
value: childValue
|
||
});
|
||
this.appendChild(child);
|
||
}
|
||
}
|
||
}
|
||
this.value = "";
|
||
} else {
|
||
// value
|
||
this.childs = undefined;
|
||
this.value = value;
|
||
/* TODO
|
||
if (typeof(value) == 'string') {
|
||
var escValue = JSON.stringify(value);
|
||
this.value = escValue.substring(1, escValue.length - 1);
|
||
console.log('check', value, this.value);
|
||
}
|
||
else {
|
||
this.value = value;
|
||
}
|
||
*/
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get value. Value is a JSON structure
|
||
* @return {*} value
|
||
*/
|
||
JSONEditor.Node.prototype.getValue = function() {
|
||
//var childs, i, iMax;
|
||
|
||
if (this.type == "array") {
|
||
var arr = [];
|
||
this.childs.forEach(function(child) {
|
||
arr.push(child.getValue());
|
||
});
|
||
return arr;
|
||
} else if (this.type == "object") {
|
||
var obj = {};
|
||
this.childs.forEach(function(child) {
|
||
obj[child.getField()] = child.getValue();
|
||
});
|
||
return obj;
|
||
} else {
|
||
if (this.value === undefined) {
|
||
this._getDomValue();
|
||
}
|
||
|
||
return this.value;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get the nesting level of this node
|
||
* @return {Number} level
|
||
*/
|
||
JSONEditor.Node.prototype.getLevel = function() {
|
||
return this.parent ? this.parent.getLevel() + 1 : 0;
|
||
};
|
||
|
||
/**
|
||
* Create a clone of a node
|
||
* The complete state of a clone is copied, including whether it is expanded or
|
||
* not. The DOM elements are not cloned.
|
||
* @return {JSONEditor.Node} clone
|
||
*/
|
||
JSONEditor.Node.prototype.clone = function() {
|
||
var clone = new JSONEditor.Node(this.editor);
|
||
clone.type = this.type;
|
||
clone.field = this.field;
|
||
clone.fieldInnerText = this.fieldInnerText;
|
||
clone.fieldEditable = this.fieldEditable;
|
||
clone.value = this.value;
|
||
clone.valueInnerText = this.valueInnerText;
|
||
clone.expanded = this.expanded;
|
||
|
||
if (this.childs) {
|
||
// an object or array
|
||
var cloneChilds = [];
|
||
this.childs.forEach(function(child) {
|
||
var childClone = child.clone();
|
||
childClone.setParent(clone);
|
||
cloneChilds.push(childClone);
|
||
});
|
||
clone.childs = cloneChilds;
|
||
} else {
|
||
// a value
|
||
clone.childs = undefined;
|
||
}
|
||
|
||
return clone;
|
||
};
|
||
|
||
/**
|
||
* Expand this node and optionally its childs.
|
||
* @param {boolean} recurse Optional recursion, true by default. When
|
||
* true, all childs will be expanded recursively
|
||
*/
|
||
JSONEditor.Node.prototype.expand = function(recurse) {
|
||
if (!this.childs) {
|
||
return;
|
||
}
|
||
|
||
// set this node expanded
|
||
this.expanded = true;
|
||
if (this.dom.expand) {
|
||
this.dom.expand.className = "jsoneditor-expanded";
|
||
}
|
||
|
||
this.showChilds();
|
||
|
||
if (recurse != false) {
|
||
this.childs.forEach(function(child) {
|
||
child.expand(recurse);
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Collapse this node and optionally its childs.
|
||
* @param {Number} recurse Optional recursion, true by default. When
|
||
* true, all childs will be collapsed recursively
|
||
*/
|
||
JSONEditor.Node.prototype.collapse = function(recurse) {
|
||
if (!this.childs) {
|
||
return;
|
||
}
|
||
|
||
this.hideChilds();
|
||
|
||
// collapse childs in case of recurse
|
||
if (recurse != false) {
|
||
this.childs.forEach(function(child) {
|
||
child.collapse(recurse);
|
||
});
|
||
}
|
||
|
||
// make this node collapsed
|
||
if (this.dom.expand) {
|
||
this.dom.expand.className = "jsoneditor-collapsed";
|
||
}
|
||
this.expanded = false;
|
||
};
|
||
|
||
/**
|
||
* Recursively show all childs when they are expanded
|
||
*/
|
||
JSONEditor.Node.prototype.showChilds = function() {
|
||
var childs = this.childs;
|
||
if (!childs) {
|
||
return;
|
||
}
|
||
if (!this.expanded) {
|
||
return;
|
||
}
|
||
|
||
var tr = this.dom.tr;
|
||
var table = tr ? tr.parentNode : undefined;
|
||
if (table) {
|
||
// show row with append button
|
||
var append = this.getAppend();
|
||
var nextTr = tr.nextSibling;
|
||
if (nextTr) {
|
||
table.insertBefore(append, nextTr);
|
||
} else {
|
||
table.appendChild(append);
|
||
}
|
||
|
||
// show childs
|
||
this.childs.forEach(function(child) {
|
||
table.insertBefore(child.getDom(), append);
|
||
child.showChilds();
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Hide the node with all its childs
|
||
*/
|
||
JSONEditor.Node.prototype.hide = function() {
|
||
var tr = this.dom.tr;
|
||
var table = tr ? tr.parentNode : undefined;
|
||
if (table) {
|
||
table.removeChild(tr);
|
||
}
|
||
this.hideChilds();
|
||
};
|
||
|
||
/**
|
||
* Recursively hide all childs
|
||
*/
|
||
JSONEditor.Node.prototype.hideChilds = function() {
|
||
var childs = this.childs;
|
||
if (!childs) {
|
||
return;
|
||
}
|
||
if (!this.expanded) {
|
||
return;
|
||
}
|
||
|
||
// hide append row
|
||
var append = this.getAppend();
|
||
if (append.parentNode) {
|
||
append.parentNode.removeChild(append);
|
||
}
|
||
|
||
// hide childs
|
||
this.childs.forEach(function(child) {
|
||
child.hide();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Add a new child to the node.
|
||
* Only applicable when Node value is of type array or object
|
||
* @param {JSONEditor.Node} node
|
||
*/
|
||
JSONEditor.Node.prototype.appendChild = function(node) {
|
||
if (this.type == "array" || this.type == "object") {
|
||
// adjust the link to the parent
|
||
node.setParent(this);
|
||
node.fieldEditable = this.type == "object";
|
||
if (this.type == "array") {
|
||
node.index = this.childs.length;
|
||
}
|
||
this.childs.push(node);
|
||
|
||
if (this.expanded) {
|
||
// insert into the DOM, before the appendRow
|
||
var newtr = node.getDom();
|
||
var appendTr = this.getAppend();
|
||
var table = appendTr ? appendTr.parentNode : undefined;
|
||
if (appendTr && table) {
|
||
table.insertBefore(newtr, appendTr);
|
||
}
|
||
|
||
node.showChilds();
|
||
}
|
||
|
||
this.updateDom({
|
||
updateIndexes: true
|
||
});
|
||
node.updateDom({
|
||
recurse: true
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Move a node from its current parent to this node
|
||
* Only applicable when Node value is of type array or object
|
||
* @param {JSONEditor.Node} node
|
||
* @param {JSONEditor.Node} beforeNode
|
||
*/
|
||
JSONEditor.Node.prototype.moveBefore = function(node, beforeNode) {
|
||
if (this.type == "array" || this.type == "object") {
|
||
// create a temporary row, to prevent the scroll position from jumping
|
||
// when removing the node
|
||
var tbody = this.dom.tr ? this.dom.tr.parentNode : undefined;
|
||
if (tbody) {
|
||
var trTemp = document.createElement("tr");
|
||
trTemp.style.height = tbody.clientHeight + "px";
|
||
tbody.appendChild(trTemp);
|
||
}
|
||
|
||
var parent = node.getParent();
|
||
if (parent) {
|
||
parent.removeChild(node);
|
||
}
|
||
if (beforeNode instanceof JSONEditor.AppendNode) {
|
||
this.appendChild(node);
|
||
} else {
|
||
this.insertBefore(node, beforeNode);
|
||
}
|
||
|
||
if (tbody) {
|
||
tbody.removeChild(trTemp);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Move a node from its current parent to this node
|
||
* Only applicable when Node value is of type array or object.
|
||
* If index is out of range, the node will be appended to the end
|
||
* @param {JSONEditor.Node} node
|
||
* @param {Number} index
|
||
*/
|
||
JSONEditor.Node.prototype.moveTo = function(node, index) {
|
||
if (node.parent == this) {
|
||
// same parent
|
||
var currentIndex = this.childs.indexOf(node);
|
||
if (currentIndex < index) {
|
||
// compensate the index for removal of the node itself
|
||
index++;
|
||
}
|
||
}
|
||
|
||
var beforeNode = this.childs[index] || this.append;
|
||
this.moveBefore(node, beforeNode);
|
||
};
|
||
|
||
/**
|
||
* Insert a new child before a given node
|
||
* Only applicable when Node value is of type array or object
|
||
* @param {JSONEditor.Node} node
|
||
* @param {JSONEditor.Node} beforeNode
|
||
*/
|
||
JSONEditor.Node.prototype.insertBefore = function(node, beforeNode) {
|
||
if (this.type == "array" || this.type == "object") {
|
||
if (beforeNode == this.append) {
|
||
// append to the child nodes
|
||
|
||
// adjust the link to the parent
|
||
node.setParent(this);
|
||
node.fieldEditable = this.type == "object";
|
||
this.childs.push(node);
|
||
} else {
|
||
// insert before a child node
|
||
var index = this.childs.indexOf(beforeNode);
|
||
if (index == -1) {
|
||
throw new Error("节点未找到.");
|
||
}
|
||
|
||
// adjust the link to the parent
|
||
node.setParent(this);
|
||
node.fieldEditable = this.type == "object";
|
||
this.childs.splice(index, 0, node);
|
||
}
|
||
|
||
if (this.expanded) {
|
||
// insert into the DOM
|
||
var newTr = node.getDom();
|
||
var nextTr = beforeNode.getDom();
|
||
var table = nextTr ? nextTr.parentNode : undefined;
|
||
if (nextTr && table) {
|
||
table.insertBefore(newTr, nextTr);
|
||
}
|
||
|
||
node.showChilds();
|
||
}
|
||
|
||
this.updateDom({
|
||
updateIndexes: true
|
||
});
|
||
node.updateDom({
|
||
recurse: true
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Search in this node
|
||
* The node will be expanded when the text is found one of its childs, else
|
||
* it will be collapsed. Searches are case insensitive.
|
||
* @param {String} text
|
||
* @return {JSONEditor.Node[]} results Array with nodes containing the search text
|
||
*/
|
||
JSONEditor.Node.prototype.search = function(text) {
|
||
var results = [];
|
||
var index;
|
||
var search = text ? text.toLowerCase() : undefined;
|
||
|
||
// delete old search data
|
||
delete this.searchField;
|
||
delete this.searchValue;
|
||
|
||
// search in field
|
||
if (this.field != undefined) {
|
||
var field = String(this.field).toLowerCase();
|
||
index = field.indexOf(search);
|
||
if (index != -1) {
|
||
this.searchField = true;
|
||
results.push({
|
||
node: this,
|
||
elem: "field"
|
||
});
|
||
}
|
||
|
||
// update dom
|
||
this._updateDomField();
|
||
}
|
||
|
||
// search in value
|
||
if (this.type == "array" || this.type == "object") {
|
||
// array, object
|
||
|
||
// search the nodes childs
|
||
if (this.childs) {
|
||
var childResults = [];
|
||
this.childs.forEach(function(child) {
|
||
childResults = childResults.concat(child.search(text));
|
||
});
|
||
results = results.concat(childResults);
|
||
}
|
||
|
||
// update dom
|
||
if (search != undefined) {
|
||
var recurse = false;
|
||
if (childResults.length == 0) {
|
||
this.collapse(recurse);
|
||
} else {
|
||
this.expand(recurse);
|
||
}
|
||
}
|
||
} else {
|
||
// string, auto
|
||
if (this.value != undefined) {
|
||
var value = String(this.value).toLowerCase();
|
||
index = value.indexOf(search);
|
||
if (index != -1) {
|
||
this.searchValue = true;
|
||
results.push({
|
||
node: this,
|
||
elem: "value"
|
||
});
|
||
}
|
||
}
|
||
|
||
// update dom
|
||
this._updateDomValue();
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
/**
|
||
* Move the scroll position such that this node is in the visible area.
|
||
* The node will not get the focus
|
||
*/
|
||
JSONEditor.Node.prototype.scrollTo = function() {
|
||
if (!this.dom.tr || !this.dom.tr.parentNode) {
|
||
// if the node is not visible, expand its parents
|
||
var parent = this.parent;
|
||
var recurse = false;
|
||
while (parent) {
|
||
parent.expand(recurse);
|
||
parent = parent.parent;
|
||
}
|
||
}
|
||
|
||
if (this.dom.tr && this.dom.tr.parentNode) {
|
||
this.editor.scrollTo(this.dom.tr.offsetTop);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Set focus to the value of this node
|
||
* @param {String} [field] The field name of the element to get the focus
|
||
* available values: 'field', 'value'
|
||
*/
|
||
JSONEditor.Node.prototype.focus = function(field) {
|
||
if (this.dom.tr && this.dom.tr.parentNode) {
|
||
if (field != "value" && this.fieldEditable) {
|
||
var domField = this.dom.field;
|
||
if (domField) {
|
||
domField.focus();
|
||
}
|
||
} else {
|
||
var domValue = this.dom.value;
|
||
if (domValue) {
|
||
domValue.focus();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update the values from the DOM field and value of this node
|
||
*/
|
||
JSONEditor.Node.prototype.blur = function() {
|
||
// retrieve the actual field and value from the DOM.
|
||
this._getDomValue(false);
|
||
this._getDomField(false);
|
||
};
|
||
|
||
/**
|
||
* Duplicate given child node
|
||
* new structure will be added right before the cloned node
|
||
* @param {JSONEditor.Node} node the childNode to be duplicated
|
||
* @return {JSONEditor.Node} clone the clone of the node
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._duplicate = function(node) {
|
||
var clone = node.clone();
|
||
|
||
/* TODO: adjust the field name (to prevent equal field names)
|
||
if (this.type == 'object') {
|
||
}
|
||
*/
|
||
|
||
// TODO: insert after instead of insert before
|
||
this.insertBefore(clone, node);
|
||
|
||
return clone;
|
||
};
|
||
|
||
/**
|
||
* Check if given node is a child. The method will check recursively to find
|
||
* this node.
|
||
* @param {JSONEditor.Node} node
|
||
* @return {boolean} containsNode
|
||
*/
|
||
JSONEditor.Node.prototype.containsNode = function(node) {
|
||
if (this == node) {
|
||
return true;
|
||
}
|
||
|
||
var childs = this.childs;
|
||
if (childs) {
|
||
// TOOD: use the js5 Array.some() here?
|
||
for (var i = 0, iMax = childs.length; i < iMax; i++) {
|
||
if (childs[i].containsNode(node)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Move given node into this node
|
||
* @param {JSONEditor.Node} node the childNode to be moved
|
||
* @param {JSONEditor.Node} beforeNode node will be inserted before given
|
||
* node. If no beforeNode is given,
|
||
* the node is appended at the end
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._move = function(node, beforeNode) {
|
||
if (node == beforeNode) {
|
||
// nothing to do...
|
||
return;
|
||
}
|
||
|
||
// check if this node is not a child of the node to be moved here
|
||
if (node.containsNode(this)) {
|
||
throw new Error("不能把区域移动到自身的子节点.");
|
||
}
|
||
|
||
// remove the original node
|
||
if (node.parent) {
|
||
node.parent.removeChild(node);
|
||
}
|
||
|
||
// create a clone of the node
|
||
var clone = node.clone();
|
||
node.clearDom();
|
||
|
||
// insert or append the node
|
||
if (beforeNode) {
|
||
this.insertBefore(clone, beforeNode);
|
||
} else {
|
||
this.appendChild(clone);
|
||
}
|
||
|
||
/* TODO: adjust the field name (to prevent equal field names)
|
||
if (this.type == 'object') {
|
||
}
|
||
*/
|
||
};
|
||
|
||
/**
|
||
* Remove a child from the node.
|
||
* Only applicable when Node value is of type array or object
|
||
* @param {JSONEditor.Node} node The child node to be removed;
|
||
* @return {JSONEditor.Node | undefined} node The removed node on success,
|
||
* else undefined
|
||
*/
|
||
JSONEditor.Node.prototype.removeChild = function(node) {
|
||
if (this.childs) {
|
||
var index = this.childs.indexOf(node);
|
||
|
||
if (index != -1) {
|
||
node.hide();
|
||
|
||
// delete old search results
|
||
delete node.searchField;
|
||
delete node.searchValue;
|
||
|
||
var removedNode = this.childs.splice(index, 1)[0];
|
||
|
||
this.updateDom({
|
||
updateIndexes: true
|
||
});
|
||
|
||
return removedNode;
|
||
}
|
||
}
|
||
|
||
return undefined;
|
||
};
|
||
|
||
/**
|
||
* Remove a child node node from this node
|
||
* This method is equal to Node.removeChild, except that _remove firex an
|
||
* onChange event.
|
||
* @param {JSONEditor.Node} node
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._remove = function(node) {
|
||
this.removeChild(node);
|
||
};
|
||
|
||
/**
|
||
* Change the type of the value of this Node
|
||
* @param {String} newType
|
||
*/
|
||
JSONEditor.Node.prototype.changeType = function(newType) {
|
||
var oldType = this.type;
|
||
|
||
if (
|
||
(newType == "string" || newType == "auto") &&
|
||
(oldType == "string" || oldType == "auto")
|
||
) {
|
||
// this is an easy change
|
||
this.type = newType;
|
||
} else {
|
||
// change from array to object, or from string/auto to object/array
|
||
|
||
var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
|
||
var lastTr;
|
||
if (this.expanded) {
|
||
lastTr = this.getAppend();
|
||
} else {
|
||
lastTr = this.getDom();
|
||
}
|
||
var nextTr = lastTr && lastTr.parentNode ? lastTr.nextSibling : undefined;
|
||
|
||
// hide current field and all its childs
|
||
this.hide();
|
||
this.clearDom();
|
||
|
||
// adjust the field and the value
|
||
this.type = newType;
|
||
|
||
// adjust childs
|
||
if (newType == "object") {
|
||
if (!this.childs) {
|
||
this.childs = [];
|
||
}
|
||
|
||
this.childs.forEach(function(child, index) {
|
||
child.clearDom();
|
||
delete child.index;
|
||
child.fieldEditable = true;
|
||
if (child.field == undefined) {
|
||
child.field = index;
|
||
}
|
||
});
|
||
|
||
if (oldType == "string" || oldType == "auto") {
|
||
this.expanded = true;
|
||
}
|
||
} else if (newType == "array") {
|
||
if (!this.childs) {
|
||
this.childs = [];
|
||
}
|
||
|
||
this.childs.forEach(function(child, index) {
|
||
child.clearDom();
|
||
child.fieldEditable = false;
|
||
child.index = index;
|
||
});
|
||
|
||
if (oldType == "string" || oldType == "auto") {
|
||
this.expanded = true;
|
||
}
|
||
} else {
|
||
this.expanded = false;
|
||
}
|
||
|
||
// create new DOM
|
||
if (table) {
|
||
if (nextTr) {
|
||
table.insertBefore(this.getDom(), nextTr);
|
||
} else {
|
||
table.appendChild(this.getDom());
|
||
}
|
||
}
|
||
this.showChilds();
|
||
}
|
||
|
||
if (newType == "auto" || newType == "string") {
|
||
// cast value to the correct type
|
||
if (newType == "string") {
|
||
this.value = String(this.value);
|
||
} else {
|
||
this.value = this._stringCast(String(this.value));
|
||
}
|
||
|
||
this.focus();
|
||
}
|
||
|
||
this.updateDom({
|
||
updateIndexes: true
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Retrieve value from DOM
|
||
* @param {boolean} silent. If true (default), no errors will be thrown in
|
||
* case of invalid data
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._getDomValue = function(silent) {
|
||
if (this.dom.value && this.type != "array" && this.type != "object") {
|
||
this.valueInnerText = JSONEditor.getInnerText(this.dom.value);
|
||
}
|
||
|
||
if (this.valueInnerText != undefined) {
|
||
try {
|
||
// retrieve the value
|
||
var value;
|
||
if (this.type == "string") {
|
||
value = this._unescapeHTML(this.valueInnerText);
|
||
} else {
|
||
var str = this._unescapeHTML(this.valueInnerText);
|
||
value = this._stringCast(str);
|
||
}
|
||
if (value !== this.value) {
|
||
var oldValue = this.value;
|
||
this.value = value;
|
||
this.editor.onAction("editValue", {
|
||
node: this,
|
||
oldValue: oldValue,
|
||
newValue: value
|
||
});
|
||
}
|
||
} catch (err) {
|
||
this.value = undefined;
|
||
// TODO: sent an action with the new, invalid value?
|
||
if (silent != true) {
|
||
throw err;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update dom value:
|
||
* - the text color of the value, depending on the type of the value
|
||
* - the height of the field, depending on the width
|
||
* - background color in case it is empty
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._updateDomValue = function() {
|
||
var domValue = this.dom.value;
|
||
if (domValue) {
|
||
// set text color depending on value type
|
||
var v = this.value;
|
||
var t = this.type == "auto" ? typeof v : this.type;
|
||
var color = "";
|
||
if (t == "string") {
|
||
color = "green";
|
||
} else if (t == "number") {
|
||
color = "red";
|
||
} else if (t == "boolean") {
|
||
color = "blue";
|
||
} else if (this.type == "object" || this.type == "array") {
|
||
// note: typeof(null)=="object", therefore check this.type instead of t
|
||
color = "";
|
||
} else if (v === null) {
|
||
color = "purple";
|
||
} else if (v === undefined) {
|
||
// invalid value
|
||
color = "green";
|
||
}
|
||
domValue.style.color = color;
|
||
|
||
// make backgound color lightgray when empty
|
||
var isEmpty =
|
||
String(this.value) == "" && this.type != "array" && this.type != "object";
|
||
if (isEmpty) {
|
||
JSONEditor.addClassName(domValue, "jsoneditor-empty");
|
||
} else {
|
||
JSONEditor.removeClassName(domValue, "jsoneditor-empty");
|
||
}
|
||
|
||
// highlight when there is a search result
|
||
if (this.searchValueActive) {
|
||
JSONEditor.addClassName(domValue, "jsoneditor-search-highlight-active");
|
||
} else {
|
||
JSONEditor.removeClassName(
|
||
domValue,
|
||
"jsoneditor-search-highlight-active"
|
||
);
|
||
}
|
||
if (this.searchValue) {
|
||
JSONEditor.addClassName(domValue, "jsoneditor-search-highlight");
|
||
} else {
|
||
JSONEditor.removeClassName(domValue, "jsoneditor-search-highlight");
|
||
}
|
||
|
||
// strip formatting from the contents of the editable div
|
||
JSONEditor.stripFormatting(domValue);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update dom field:
|
||
* - the text color of the field, depending on the text
|
||
* - the height of the field, depending on the width
|
||
* - background color in case it is empty
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._updateDomField = function() {
|
||
var domField = this.dom.field;
|
||
if (domField) {
|
||
// make backgound color lightgray when empty
|
||
var isEmpty = String(this.field) == "";
|
||
if (isEmpty) {
|
||
JSONEditor.addClassName(domField, "jsoneditor-empty");
|
||
} else {
|
||
JSONEditor.removeClassName(domField, "jsoneditor-empty");
|
||
}
|
||
|
||
// highlight when there is a search result
|
||
if (this.searchFieldActive) {
|
||
JSONEditor.addClassName(domField, "jsoneditor-search-highlight-active");
|
||
} else {
|
||
JSONEditor.removeClassName(
|
||
domField,
|
||
"jsoneditor-search-highlight-active"
|
||
);
|
||
}
|
||
if (this.searchField) {
|
||
JSONEditor.addClassName(domField, "jsoneditor-search-highlight");
|
||
} else {
|
||
JSONEditor.removeClassName(domField, "jsoneditor-search-highlight");
|
||
}
|
||
|
||
// strip formatting from the contents of the editable div
|
||
JSONEditor.stripFormatting(domField);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Retrieve field from DOM
|
||
* @param {boolean} silent. If true (default), no errors will be thrown in
|
||
* case of invalid data
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._getDomField = function(silent) {
|
||
if (this.dom.field && this.fieldEditable) {
|
||
this.fieldInnerText = JSONEditor.getInnerText(this.dom.field);
|
||
}
|
||
|
||
if (this.fieldInnerText != undefined) {
|
||
try {
|
||
var field = this._unescapeHTML(this.fieldInnerText);
|
||
|
||
if (field !== this.field) {
|
||
var oldField = this.field;
|
||
this.field = field;
|
||
this.editor.onAction("editField", {
|
||
node: this,
|
||
oldValue: oldField,
|
||
newValue: field
|
||
});
|
||
}
|
||
} catch (err) {
|
||
this.field = undefined;
|
||
// TODO: sent an action here, with the new, invalid value?
|
||
if (silent != true) {
|
||
throw err;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Clear the dom of the node
|
||
*/
|
||
JSONEditor.Node.prototype.clearDom = function() {
|
||
// TODO: hide the node first?
|
||
//this.hide();
|
||
// TOOD: recursively clear dom?
|
||
|
||
this.dom = {};
|
||
};
|
||
|
||
/**
|
||
* Get the HTML DOM TR element of the node.
|
||
* The dom will be generated when not yet created
|
||
* @return {Element} tr HTML DOM TR Element
|
||
*/
|
||
JSONEditor.Node.prototype.getDom = function() {
|
||
var dom = this.dom;
|
||
if (dom.tr) {
|
||
return dom.tr;
|
||
}
|
||
|
||
// create row
|
||
dom.tr = document.createElement("tr");
|
||
dom.tr.className = "jsoneditor-tr";
|
||
dom.tr.node = this;
|
||
|
||
if (this.editor.editable) {
|
||
// create draggable area
|
||
var tdDrag = document.createElement("td");
|
||
tdDrag.className = "jsoneditor-td";
|
||
dom.drag = this._createDomDragArea();
|
||
if (dom.drag) {
|
||
tdDrag.appendChild(dom.drag);
|
||
}
|
||
dom.tr.appendChild(tdDrag);
|
||
}
|
||
|
||
// create tree and field
|
||
var tdField = document.createElement("td");
|
||
tdField.className = "jsoneditor-td";
|
||
dom.tr.appendChild(tdField);
|
||
dom.expand = this._createDomExpandButton();
|
||
dom.field = this._createDomField();
|
||
dom.value = this._createDomValue();
|
||
dom.tree = this._createDomTree(dom.expand, dom.field, dom.value);
|
||
tdField.appendChild(dom.tree);
|
||
|
||
if (this.editor.editable) {
|
||
// create type select box
|
||
var tdType = document.createElement("td");
|
||
tdType.className = "jsoneditor-td jsoneditor-td-edit";
|
||
dom.tr.appendChild(tdType);
|
||
dom.type = this._createDomTypeButton();
|
||
tdType.appendChild(dom.type);
|
||
|
||
// create duplicate button
|
||
var tdDuplicate = document.createElement("td");
|
||
tdDuplicate.className = "jsoneditor-td jsoneditor-td-edit";
|
||
dom.tr.appendChild(tdDuplicate);
|
||
dom.duplicate = this._createDomDuplicateButton();
|
||
if (dom.duplicate) {
|
||
tdDuplicate.appendChild(dom.duplicate);
|
||
}
|
||
|
||
// create remove button
|
||
var tdRemove = document.createElement("td");
|
||
tdRemove.className = "jsoneditor-td jsoneditor-td-edit";
|
||
dom.tr.appendChild(tdRemove);
|
||
dom.remove = this._createDomRemoveButton();
|
||
if (dom.remove) {
|
||
tdRemove.appendChild(dom.remove);
|
||
}
|
||
}
|
||
|
||
this.updateDom(); // TODO: recurse here?
|
||
|
||
return dom.tr;
|
||
};
|
||
|
||
/**
|
||
* DragStart event, fired on mousedown on the dragarea at the left side of a Node
|
||
* @param {Event} event
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._onDragStart = function(event) {
|
||
event = event || window.event;
|
||
|
||
var node = this;
|
||
if (!this.mousemove) {
|
||
this.mousemove = JSONEditor.Events.addEventListener(
|
||
document,
|
||
"mousemove",
|
||
|
||
function(event) {
|
||
node._onDrag(event);
|
||
}
|
||
);
|
||
}
|
||
|
||
if (!this.mouseup) {
|
||
this.mouseup = JSONEditor.Events.addEventListener(
|
||
document,
|
||
"mouseup",
|
||
|
||
function(event) {
|
||
node._onDragEnd(event);
|
||
}
|
||
);
|
||
}
|
||
|
||
/* TODO: correct highlighting when the TypeDropDown is visible (And has highlighting locked)
|
||
if (JSONEditor.freezeHighlight) {
|
||
console.log('heee');
|
||
JSONEditor.freezeHighlight = false;
|
||
this.setHighlight(true);
|
||
}
|
||
*/
|
||
JSONEditor.freezeHighlight = true;
|
||
this.drag = {
|
||
oldCursor: document.body.style.cursor,
|
||
startParent: this.parent,
|
||
startIndex: this.parent.childs.indexOf(this)
|
||
};
|
||
document.body.style.cursor = "move";
|
||
|
||
JSONEditor.Events.preventDefault(event);
|
||
};
|
||
|
||
/**
|
||
* Drag event, fired when moving the mouse while dragging a Node
|
||
* @param {Event} event
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._onDrag = function(event) {
|
||
event = event || window.event;
|
||
var trThis = this.dom.tr;
|
||
|
||
// TODO: add an ESC option, which resets to the original position
|
||
|
||
var topThis = JSONEditor.getAbsoluteTop(trThis);
|
||
var heightThis = trThis.offsetHeight;
|
||
var mouseY = event.pageY || event.clientY + document.body.scrollTop;
|
||
if (mouseY < topThis) {
|
||
// move up
|
||
var trPrev = trThis.previousSibling;
|
||
var topPrev = JSONEditor.getAbsoluteTop(trPrev);
|
||
var nodePrev = JSONEditor.getNodeFromTarget(trPrev);
|
||
while (trPrev && mouseY < topPrev) {
|
||
nodePrev = JSONEditor.getNodeFromTarget(trPrev);
|
||
trPrev = trPrev.previousSibling;
|
||
topPrev = JSONEditor.getAbsoluteTop(trPrev);
|
||
}
|
||
|
||
if (nodePrev) {
|
||
trPrev = nodePrev.dom.tr;
|
||
topPrev = JSONEditor.getAbsoluteTop(trPrev);
|
||
if (mouseY > topPrev + heightThis) {
|
||
nodePrev = undefined;
|
||
}
|
||
}
|
||
|
||
if (nodePrev && nodePrev.parent) {
|
||
nodePrev.parent.moveBefore(this, nodePrev);
|
||
}
|
||
} else {
|
||
// move down
|
||
var trLast =
|
||
this.expanded && this.append ? this.append.getDom() : this.dom.tr;
|
||
var trFirst = trLast ? trLast.nextSibling : undefined;
|
||
if (trFirst) {
|
||
var topFirst = JSONEditor.getAbsoluteTop(trFirst);
|
||
|
||
var nodeNext = undefined;
|
||
var trNext = trFirst.nextSibling;
|
||
var topNext = JSONEditor.getAbsoluteTop(trNext);
|
||
var heightNext = trNext ? topNext - topFirst : 0;
|
||
while (trNext && mouseY > topThis + heightNext) {
|
||
nodeNext = JSONEditor.getNodeFromTarget(trNext);
|
||
trNext = trNext.nextSibling;
|
||
topNext = JSONEditor.getAbsoluteTop(trNext);
|
||
heightNext = trNext ? topNext - topFirst : 0;
|
||
}
|
||
|
||
if (nodeNext && nodeNext.parent) {
|
||
nodeNext.parent.moveBefore(this, nodeNext);
|
||
}
|
||
}
|
||
}
|
||
JSONEditor.Events.preventDefault(event);
|
||
};
|
||
|
||
/**
|
||
* Drag event, fired on mouseup after having dragged a node
|
||
* @param {Event} event
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._onDragEnd = function(event) {
|
||
event = event || window.event;
|
||
|
||
var params = {
|
||
node: this,
|
||
startParent: this.drag.startParent,
|
||
startIndex: this.drag.startIndex,
|
||
endParent: this.parent,
|
||
endIndex: this.parent.childs.indexOf(this)
|
||
};
|
||
if (
|
||
params.startParent != params.endParent ||
|
||
params.startIndex != params.endIndex
|
||
) {
|
||
// only register this action if the node is actually moved to another place
|
||
this.editor.onAction("moveNode", params);
|
||
}
|
||
|
||
document.body.style.cursor = this.drag.oldCursor;
|
||
delete JSONEditor.freezeHighlight;
|
||
delete this.drag;
|
||
this.setHighlight(false);
|
||
|
||
if (this.mousemove) {
|
||
JSONEditor.Events.removeEventListener(
|
||
document,
|
||
"mousemove",
|
||
this.mousemove
|
||
);
|
||
delete this.mousemove;
|
||
}
|
||
if (this.mouseup) {
|
||
JSONEditor.Events.removeEventListener(document, "mouseup", this.mouseup);
|
||
delete this.mouseup;
|
||
}
|
||
|
||
JSONEditor.Events.preventDefault(event);
|
||
};
|
||
|
||
/**
|
||
* Create a drag area, displayed at the left side of the node
|
||
* @return {Element | undefined} domDrag
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._createDomDragArea = function() {
|
||
if (!this.parent) {
|
||
return undefined;
|
||
}
|
||
|
||
var domDrag = document.createElement("button");
|
||
domDrag.className = "jsoneditor-dragarea";
|
||
domDrag.title = "Move field (drag and drop)";
|
||
|
||
return domDrag;
|
||
};
|
||
|
||
/**
|
||
* Create an editable field
|
||
* @return {Element} domField
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._createDomField = function() {
|
||
return document.createElement("div");
|
||
};
|
||
|
||
/**
|
||
* Set highlighting for this node and all its childs.
|
||
* Only applied to the currently visible (expanded childs)
|
||
* @param {boolean} highlight
|
||
*/
|
||
JSONEditor.Node.prototype.setHighlight = function(highlight) {
|
||
if (JSONEditor.freezeHighlight) {
|
||
return;
|
||
}
|
||
|
||
if (this.dom.tr) {
|
||
this.dom.tr.className =
|
||
"jsoneditor-tr" + (highlight ? " jsoneditor-tr-highlight" : "");
|
||
|
||
if (this.append) {
|
||
this.append.setHighlight(highlight);
|
||
}
|
||
|
||
if (this.childs) {
|
||
this.childs.forEach(function(child) {
|
||
child.setHighlight(highlight);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update the value of the node. Only primitive types are allowed, no Object
|
||
* or Array is allowed.
|
||
* @param {String | Number | Boolean | null} value
|
||
*/
|
||
JSONEditor.Node.prototype.updateValue = function(value) {
|
||
this.value = value;
|
||
this.updateDom();
|
||
};
|
||
|
||
/**
|
||
* Update the field of the node.
|
||
* @param {String} field
|
||
*/
|
||
JSONEditor.Node.prototype.updateField = function(field) {
|
||
this.field = field;
|
||
this.updateDom();
|
||
};
|
||
|
||
/**
|
||
* Update the HTML DOM, optionally recursing through the childs
|
||
* @param {Object} [options] Available parameters:
|
||
* {boolean} [recurse] If true, the
|
||
* DOM of the childs will be updated recursively.
|
||
* False by default.
|
||
* {boolean} [updateIndexes] If true, the childs
|
||
* indexes of the node will be updated too. False by
|
||
* default.
|
||
*/
|
||
JSONEditor.Node.prototype.updateDom = function(options) {
|
||
// update level indentation
|
||
var domTree = this.dom.tree;
|
||
if (domTree) {
|
||
domTree.style.marginLeft = this.getLevel() * 24 + "px";
|
||
}
|
||
|
||
// update field
|
||
var domField = this.dom.field;
|
||
if (domField) {
|
||
if (this.fieldEditable == true) {
|
||
// parent is an object
|
||
domField.contentEditable = this.editor.editable;
|
||
domField.spellcheck = false;
|
||
domField.className = "jsoneditor-field";
|
||
} else {
|
||
// parent is an array this is the root node
|
||
domField.className = "jsoneditor-readonly";
|
||
}
|
||
|
||
var field;
|
||
if (this.index != undefined) {
|
||
field = this.index;
|
||
} else if (this.field != undefined) {
|
||
field = this.field;
|
||
} else if (this.type == "array" || this.type == "object") {
|
||
field = this.type;
|
||
} else {
|
||
field = "field";
|
||
}
|
||
domField.innerHTML = this._escapeHTML(field);
|
||
}
|
||
|
||
// update value
|
||
var domValue = this.dom.value;
|
||
if (domValue) {
|
||
var count = this.childs ? this.childs.length : 0;
|
||
if (this.type == "array") {
|
||
domValue.innerHTML = "[" + count + "]";
|
||
domValue.title = this.type + " containing " + count + " items";
|
||
} else if (this.type == "object") {
|
||
domValue.innerHTML = "{" + count + "}";
|
||
domValue.title = this.type + " containing " + count + " items";
|
||
} else {
|
||
domValue.innerHTML = this._escapeHTML(this.value);
|
||
delete domValue.title;
|
||
}
|
||
}
|
||
|
||
// update field and value
|
||
this._updateDomField();
|
||
this._updateDomValue();
|
||
|
||
// update childs indexes
|
||
if (options && options.updateIndexes == true) {
|
||
// updateIndexes is true or undefined
|
||
this._updateDomIndexes();
|
||
}
|
||
|
||
if (options && options.recurse == true) {
|
||
// recurse is true or undefined. update childs recursively
|
||
if (this.childs) {
|
||
this.childs.forEach(function(child) {
|
||
child.updateDom(options);
|
||
});
|
||
}
|
||
|
||
// update row with append button
|
||
if (this.append) {
|
||
this.append.updateDom();
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update the DOM of the childs of a node: update indexes and undefined field
|
||
* names.
|
||
* Only applicable when structure is an array or object
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._updateDomIndexes = function() {
|
||
var domValue = this.dom.value;
|
||
var childs = this.childs;
|
||
if (domValue && childs) {
|
||
if (this.type == "array") {
|
||
childs.forEach(function(child, index) {
|
||
child.index = index;
|
||
var childField = child.dom.field;
|
||
if (childField) {
|
||
childField.innerHTML = index;
|
||
}
|
||
});
|
||
} else if (this.type == "object") {
|
||
childs.forEach(function(child) {
|
||
if (child.index != undefined) {
|
||
delete child.index;
|
||
|
||
if (child.field == undefined) {
|
||
child.field = "field";
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Create an editable value
|
||
* @private
|
||
*/
|
||
JSONEditor.Node.prototype._createDomValue = function() {
|
||
var domValue;
|
||
|
||
if (this.type == "array") {
|
||
domValue = document.createElement("div");
|
||
domValue.className = "jsoneditor-readonly";
|
||
domValue.innerHTML = "[...]";
|
||
} else if (this.type == "object") {
|
||
domValue = document.createElement("div");
|
||
domValue.className = "jsoneditor-readonly";
|
||
domValue.innerHTML = "{...}";
|
||
} else if (this.type == "string") {
|
||
domValue = document.createElement("div");
|
||
domValue.contentEditable = this.editor.editable;
|
||
domValue.spellcheck = false;
|
||
domValue.className = "jsoneditor-value";
|
||
domValue.innerHTML = this._escapeHTML(this.value);
|
||
} else {
|
||
domValue = document.createElement("div");
|
||
domValue.contentEditable = this.editor.editable;
|
||
domValue.spellcheck = false;
|
||
domValue.className = "jsoneditor-value";
|
||
domValue.innerHTML = this._escapeHTML(this.value);
|
||
}
|
||
|
||
// TODO: in FF spel/check of editable divs is done via the body. quite ugly
|
||
// document.body.spellcheck = false;
|
||
|
||
return domValue;
|
||
};
|
||