/** * @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, * @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; }; /** * Create an expand/collapse button * @return {Element} expand * @private */ JSONEditor.Node.prototype._createDomExpandButton = function() { // create expand button var expand = document.createElement("button"); var expandable = this.type == "array" || this.type == "object"; if (expandable) { expand.className = this.expanded ? "jsoneditor-expanded" : "jsoneditor-collapsed"; expand.title = "Click to expand/collapse this field. \n" + "Ctrl+Click to expand/collapse including all childs."; } else { expand.className = "jsoneditor-invisible"; expand.title = ""; } return expand; }; /** * Create a DOM tree element, containing the expand/collapse button * @param {Element} domExpand * @param {Element} domField * @param {Element} domValue * @return {Element} domTree * @private */ JSONEditor.Node.prototype._createDomTree = function( domExpand, domField, domValue ) { var dom = this.dom; var domTree = document.createElement("table"); var tbody = document.createElement("tbody"); domTree.style.borderCollapse = "collapse"; // TODO: put in css domTree.appendChild(tbody); var tr = document.createElement("tr"); tbody.appendChild(tr); // create expand button var tdExpand = document.createElement("td"); tdExpand.className = "jsoneditor-td-tree"; tr.appendChild(tdExpand); tdExpand.appendChild(domExpand); dom.tdExpand = tdExpand; // add the field var tdField = document.createElement("td"); tdField.className = "jsoneditor-td-tree"; tr.appendChild(tdField); tdField.appendChild(domField); dom.tdField = tdField; // add a separator var tdSeparator = document.createElement("td"); tdSeparator.className = "jsoneditor-td-tree"; tr.appendChild(tdSeparator); if (this.type != "object" && this.type != "array") { tdSeparator.appendChild(document.createTextNode(":")); tdSeparator.className = "jsoneditor-separator"; } dom.tdSeparator = tdSeparator; // add the value var tdValue = document.createElement("td"); tdValue.className = "jsoneditor-td-tree"; tr.appendChild(tdValue); tdValue.appendChild(domValue); dom.tdValue = tdValue; return domTree; }; /** * Handle an event. The event is catched centrally by the editor * @param {Event} event */ JSONEditor.Node.prototype.onEvent = function(event) { var type = event.type; var target = event.target || event.srcElement; var dom = this.dom; var node = this; var expandable = this.type == "array" || this.type == "object"; // value events var domValue = dom.value; if (target == domValue) { switch (type) { case "focus": JSONEditor.focusNode = this; break; case "blur": case "change": this._getDomValue(true); this._updateDomValue(); if (this.value) { domValue.innerHTML = this._escapeHTML(this.value); } break; case "keyup": this._getDomValue(true); this._updateDomValue(); break; case "cut": case "paste": setTimeout(function() { node._getDomValue(true); node._updateDomValue(); }, 1); break; } } // field events var domField = dom.field; if (target == domField) { switch (type) { case "focus": JSONEditor.focusNode = this; break; case "change": case "blur": this._getDomField(true); this._updateDomField(); if (this.field) { domField.innerHTML = this._escapeHTML(this.field); } break; case "keyup": this._getDomField(true); this._updateDomField(); break; case "cut": case "paste": setTimeout(function() { node._getDomField(true); node._updateDomField(); }, 1); break; } } // drag events var domDrag = dom.drag; if (target == domDrag) { switch (type) { case "mousedown": this._onDragStart(event); break; case "mouseover": this.setHighlight(true); break; case "mouseout": this.setHighlight(false); break; } } // expand events var domExpand = dom.expand; if (target == domExpand) { if (type == "click") { if (expandable) { this._onExpand(event); } } } // duplicate button var domDuplicate = dom.duplicate; if (target == domDuplicate) { switch (type) { case "click": var clone = this.parent._duplicate(this); this.editor.onAction("duplicateNode", { node: this, clone: clone, parent: this.parent }); break; case "mouseover": this.setHighlight(true); break; case "mouseout": this.setHighlight(false); break; } } // remove button var domRemove = dom.remove; if (target == domRemove) { switch (type) { case "click": this._onRemove(); break; case "mouseover": this.setHighlight(true); break; case "mouseout": this.setHighlight(false); break; } } // type button var domType = dom.type; if (target == domType) { switch (type) { case "click": this._onChangeType(event); break; case "mouseover": this.setHighlight(true); break; case "mouseout": this.setHighlight(false); break; } } // focus // when clicked in whitespace left or right from the field or value, set focus var domTree = dom.tree; if (target == domTree.parentNode) { switch (type) { case "click": var left = event.offsetX != undefined ? event.offsetX < (this.getLevel() + 1) * 24 : event.clientX < JSONEditor.getAbsoluteLeft(dom.tdSeparator); // for FF if (left || expandable) { // node is expandable when it is an object or array if (domField) { JSONEditor.setEndOfContentEditable(domField); domField.focus(); } } else { if (domValue) { JSONEditor.setEndOfContentEditable(domValue); domValue.focus(); } } break; } } if ( (target == dom.tdExpand && !expandable) || target == dom.tdField || target == dom.tdSeparator ) { switch (type) { case "click": if (domField) { JSONEditor.setEndOfContentEditable(domField); domField.focus(); } break; } } }; /** * Handle the expand event, when clicked on the expand button * @param {Event} event * @private */ JSONEditor.Node.prototype._onExpand = function(event) { event = event || window.event; var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all if (recurse) { // Take the table offline var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this var frame = table.parentNode; var scrollTop = frame.scrollTop; frame.removeChild(table); } if (this.expanded) { this.collapse(recurse); } else { this.expand(recurse); } if (recurse) { // Put the table online again frame.appendChild(table); frame.scrollTop = scrollTop; } }; JSONEditor.Node.types = [ { value: "array", className: "jsoneditor-option-array", title: '"array" 类型: 包含了有序值集合的数组.' }, { value: "auto", className: "jsoneditor-option-auto", title: '"auto" 类型: 节点类型将自动从值中获取, 可以是: string, number, boolean, 或 null.' }, { value: "object", className: "jsoneditor-option-object", title: '"object" 类型: 对象包含了一些无序的键/值对.' }, { value: "string", className: "jsoneditor-option-string", title: '"string" 类型: 节点类型不从值中自动获取, 但永远返回string.' } ]; /** * Create a DOM select box containing the node type * @return {Element} domType * @private */ JSONEditor.Node.prototype._createDomTypeButton = function() { var node = this; var domType = document.createElement("button"); domType.className = "jsoneditor-type-" + node.type; domType.title = "改变节点类型"; return domType; }; /** * Remove this node * @private */ JSONEditor.Node.prototype._onRemove = function() { this.setHighlight(false); var index = this.parent.childs.indexOf(this); this.parent._remove(this); this.editor.onAction("removeNode", { node: this, parent: this.parent, index: index }); }; /** * Handle a click on the Type-button * @param {Event} event * @private */ JSONEditor.Node.prototype._onChangeType = function(event) { JSONEditor.Events.stopPropagation(event); var domType = this.dom.type; var node = this; var x = JSONEditor.getAbsoluteLeft(domType); var y = JSONEditor.getAbsoluteTop(domType) + domType.clientHeight; var callback = function(newType) { var oldType = node.type; node.changeType(newType); node.editor.onAction("changeType", { node: node, oldType: oldType, newType: newType }); domType.className = "jsoneditor-type-" + node.type; }; JSONEditor.showDropDownList({ x: x, y: y, node: node, value: node.type, values: JSONEditor.Node.types, className: "jsoneditor-select", optionSelectedClassName: "jsoneditor-option-selected", optionClassName: "jsoneditor-option", callback: callback }); }; /** * Show a dropdown list * @param {Object} params Available parameters: * {Number} x The absolute horizontal position * {Number} y The absolute vertical position * {JSONEditor.Node} node node used for highlighting * {String} value current selected value * {Object[]} values the available values. Each object * contains a value, title, and * className * {String} optionSelectedClassName * {String} optionClassName * {function} callback Callback method, called when * the selected value changed. */ JSONEditor.showDropDownList = function(params) { var select = document.createElement("div"); select.className = params.className || ""; select.style.position = "absolute"; select.style.left = (params.x || 0) + "px"; select.style.top = (params.y || 0) + "px"; params.values.forEach(function(v) { var text = v.value || String(v); var className = "jsoneditor-option"; var selected = text == params.value; if (selected) { className += " " + params.optionSelectedClassName; } var option = document.createElement("div"); option.className = className; if (v.title) { option.title = v.title; } var divIcon = document.createElement("div"); divIcon.className = v.className || ""; option.appendChild(divIcon); var divText = document.createElement("div"); divText.className = "jsoneditor-option-text"; divText.innerHTML = "
" + text + "
"; option.appendChild(divText); option.onmousedown = (function(value) { return function() { params.callback(value); }; })(v.value); select.appendChild(option); }); document.body.appendChild(select); params.node.setHighlight(true); JSONEditor.freezeHighlight = true; // TODO: change to onclick? -> but be sure to remove existing dropdown first var onmousedown = JSONEditor.Events.addEventListener( document, "mousedown", function() { JSONEditor.freezeHighlight = false; params.node.setHighlight(false); if (select && select.parentNode) { select.parentNode.removeChild(select); } JSONEditor.Events.removeEventListener(document, "mousedown", onmousedown); } ); var onmousewheel = JSONEditor.Events.addEventListener( document, "mousewheel", function() { JSONEditor.freezeHighlight = false; params.node.setHighlight(false); if (select && select.parentNode) { select.parentNode.removeChild(select); } JSONEditor.Events.removeEventListener( document, "mousewheel", onmousewheel ); } ); }; /** * Create a table row with an append button. * @return {Element | undefined} buttonAppend or undefined when inapplicable */ JSONEditor.Node.prototype.getAppend = function() { if (!this.append) { this.append = new JSONEditor.AppendNode(this.editor); this.append.setParent(this); } return this.append.getDom(); }; /** * Create a remove button. Returns undefined when the structure cannot * be removed * @return {Element | undefined} removeButton, or undefined when inapplicable * @private */ JSONEditor.Node.prototype._createDomRemoveButton = function() { if ( this.parent && (this.parent.type == "array" || this.parent.type == "object") ) { var buttonRemove = document.createElement("button"); buttonRemove.className = "jsoneditor-remove"; buttonRemove.title = "删除节点 (包括所有子节点)"; return buttonRemove; } else { return undefined; } }; /** * Create a duplicate button. * If the Node is the root node, no duplicate button is available and undefined * will be returned * @return {Element | undefined} buttonDuplicate * @private */ JSONEditor.Node.prototype._createDomDuplicateButton = function() { if ( this.parent && (this.parent.type == "array" || this.parent.type == "object") ) { var buttonDupliate = document.createElement("button"); buttonDupliate.className = "jsoneditor-duplicate"; buttonDupliate.title = "复制节点 (包括所有子节点)"; return buttonDupliate; } else { return undefined; } }; /** * get the type of a value * @param {*} value * @return {String} type Can be 'object', 'array', 'string', 'auto' * @private */ JSONEditor.Node.prototype._getType = function(value) { if (value instanceof Array) { return "array"; } if (value instanceof Object) { return "object"; } if (typeof value == "string" && typeof this._stringCast(value) != "string") { return "string"; } return "auto"; }; /** * cast contents of a string to the correct type. This can be a string, * a number, a boolean, etc * @param {String} str * @return {*} castedStr * @private */ JSONEditor.Node.prototype._stringCast = function(str) { var lower = str.toLowerCase(), num = Number(str), // will nicely fail with '123ab' numFloat = parseFloat(str); // will nicely fail with ' ' if (str == "") { return ""; } else if (lower == "null") { return null; } else if (lower == "true") { return true; } else if (lower == "false") { return false; } else if (!isNaN(num) && !isNaN(numFloat)) { return num; } else { return str; } }; /** * escape a text, such that it can be displayed safely in an HTML element * @param {String} text * @return {String} escapedText * @private */ JSONEditor.Node.prototype._escapeHTML = function(text) { var htmlEscaped = String(text) .replace(//g, ">") .replace(/ /g, "  ") // replace double space with an nbsp and space .replace(/^ /, " ") // space at start .replace(/ $/, " "); // space at end var json = JSON.stringify(htmlEscaped); return json.substring(1, json.length - 1); }; /** * unescape a string. * @param {String} escapedText * @return {String} text * @private */ JSONEditor.Node.prototype._unescapeHTML = function(escapedText) { var json = '"' + this._escapeJSON(escapedText) + '"'; var htmlEscaped = JSONEditor.parse(json); return htmlEscaped .replace(/</g, "<") .replace(/>/g, ">") .replace(/ /g, " "); }; /** * escape a text to make it a valid JSON string. The method will: * - replace unescaped double quotes with '\"' * - replace unescaped backslash with '\\' * - replace returns with '\n' * @param {String} text * @return {String} escapedText * @private */ JSONEditor.Node.prototype._escapeJSON = function(text) { // TODO: replace with some smart regex (only when a new solution is faster!) var escaped = ""; var i = 0, iMax = text.length; while (i < iMax) { var c = text.charAt(i); if (c == "\n") { escaped += "\\n"; } else if (c == "\\") { escaped += c; i++; c = text.charAt(i); if ('"\\/bfnrtu'.indexOf(c) == -1) { escaped += "\\"; // no valid escape character } escaped += c; } else if (c == '"') { escaped += '\\"'; } else { escaped += c; } i++; } return escaped; }; /** * @constructor JSONEditor.AppendNode * @extends JSONEditor.Node * @param {JSONEditor} editor * Create a new AppendNode. This is a special node which is created at the * end of the list with childs for an object or array */ JSONEditor.AppendNode = function(editor) { this.editor = editor; this.dom = {}; }; JSONEditor.AppendNode.prototype = new JSONEditor.Node(); /** * Return a table row with an append button. * @return {Element} dom TR element */ JSONEditor.AppendNode.prototype.getDom = function() { if (this.dom.tr) { return this.dom.tr; } /** * Create a TD element, and give it the provided class name (if any) * @param {String} [className] * @return {Element} td */ function newTd(className) { var td = document.createElement("td"); td.className = className || ""; return td; } // a row for the append button var trAppend = document.createElement("tr"); trAppend.node = this; // TODO: do not create an appendNode at all when in viewer mode if (!this.editor.editable) { return trAppend; } // a cell for the drag area column trAppend.appendChild(newTd("jsoneditor-td")); // a cell for the append button var tdAppend = document.createElement("td"); trAppend.appendChild(tdAppend); tdAppend.className = "jsoneditor-td"; // create the append button var buttonAppend = document.createElement("button"); buttonAppend.className = "jsoneditor-append"; buttonAppend.title = "添加"; this.dom.append = buttonAppend; tdAppend.appendChild(buttonAppend); trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit")); trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit")); trAppend.appendChild(newTd("jsoneditor-td jsoneditor-td-edit")); this.dom.tr = trAppend; this.dom.td = tdAppend; this.updateDom(); return trAppend; }; /** * Update the HTML dom of the Node */ JSONEditor.AppendNode.prototype.updateDom = function() { var tdAppend = this.dom.td; if (tdAppend) { tdAppend.style.paddingLeft = this.getLevel() * 24 + 26 + "px"; // TODO: not so nice hard coded offset } }; /** * Handle an event. The event is catched centrally by the editor * @param {Event} event */ JSONEditor.AppendNode.prototype.onEvent = function(event) { var type = event.type; var target = event.target || event.srcElement; var dom = this.dom; var domAppend = dom.append; if (target == domAppend) { switch (type) { case "click": this._onAppend(); break; case "mouseover": this.parent.setHighlight(true); break; case "mouseout": this.parent.setHighlight(false); } } }; /** * Handle append event * @private */ JSONEditor.AppendNode.prototype._onAppend = function() { var newNode = new JSONEditor.Node(this.editor, { field: "field", value: "value" }); this.parent.appendChild(newNode); this.parent.setHighlight(false); newNode.focus(); this.editor.onAction("appendNode", { node: newNode, parent: this.parent }); }; /** * Create main frame * @private */ JSONEditor.prototype._createFrame = function() { // create the frame this.container.innerHTML = ""; this.frame = document.createElement("div"); this.frame.className = "jsoneditor-frame"; this.container.appendChild(this.frame); // create one global event listener to handle all events from all nodes var editor = this; // TODO: move this onEvent to JSONEditor.prototype.onEvent var onEvent = function(event) { event = event || window.event; var target = event.target || event.srcElement; /* TODO: Enable quickkeys Ctrl+F and F3. // Requires knowing whether the JSONEditor has focus or not // (use a global event listener for that?) // Check for search quickkeys, Ctrl+F and F3 if (editor.options.search) { if (event.type == 'keydown') { var keynum = event.which || event.keyCode; if (keynum == 70 && event.ctrlKey) { // Ctrl+F if (editor.searchBox) { editor.searchBox.dom.search.focus(); editor.searchBox.dom.search.select(); JSONEditor.Events.preventDefault(event); JSONEditor.Events.stopPropagation(event); } } else if (keynum == 114) { // F3 if (!event.shiftKey) { // select next search result editor.searchBox.next(); } else { // select previous search result editor.searchBox.previous(); } editor.searchBox.focusActiveResult(); // set selection to the current JSONEditor.Events.preventDefault(event); JSONEditor.Events.stopPropagation(event); } } } */ var node = JSONEditor.getNodeFromTarget(target); if (node) { node.onEvent(event); } }; this.frame.onclick = function(event) { onEvent(event); // prevent default submit action when JSONEditor is located inside a form JSONEditor.Events.preventDefault(event); }; this.frame.onchange = onEvent; this.frame.onkeydown = onEvent; this.frame.onkeyup = onEvent; this.frame.oncut = onEvent; this.frame.onpaste = onEvent; this.frame.onmousedown = onEvent; this.frame.onmouseup = onEvent; this.frame.onmouseover = onEvent; this.frame.onmouseout = onEvent; // Note: focus and blur events do not propagate, therefore they defined // using an eventListener with useCapture=true // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html JSONEditor.Events.addEventListener(this.frame, "focus", onEvent, true); JSONEditor.Events.addEventListener(this.frame, "blur", onEvent, true); this.frame.onfocusin = onEvent; // for IE this.frame.onfocusout = onEvent; // for IE // create menu this.menu = document.createElement("div"); this.menu.className = "jsoneditor-menu"; this.frame.appendChild(this.menu); // create expand all button var expandAll = document.createElement("button"); expandAll.className = "jsoneditor-menu jsoneditor-expand-all"; expandAll.title = "展开"; expandAll.onclick = function() { editor.expandAll(); }; this.menu.appendChild(expandAll); // create expand all button var collapseAll = document.createElement("button"); collapseAll.title = "折叠"; collapseAll.className = "jsoneditor-menu jsoneditor-collapse-all"; collapseAll.onclick = function() { editor.collapseAll(); }; this.menu.appendChild(collapseAll); // create expand/collapse buttons if (this.history) { // create separator var separator = document.createElement("span"); separator.innerHTML = " "; this.menu.appendChild(separator); // create undo button var undo = document.createElement("button"); undo.className = "jsoneditor-menu jsoneditor-undo"; undo.title = "撤销"; undo.onclick = function() { // undo last action editor.history.undo(); // trigger change callback if (editor.options.change) { editor.options.change(); } }; this.menu.appendChild(undo); this.dom.undo = undo; // create redo button var redo = document.createElement("button"); redo.className = "jsoneditor-menu jsoneditor-redo"; redo.title = "重做"; redo.onclick = function() { // redo last action editor.history.redo(); // trigger change callback if (editor.options.change) { editor.options.change(); } }; this.menu.appendChild(redo); this.dom.redo = redo; // register handler for onchange of history this.history.onChange = function() { undo.disabled = !editor.history.canUndo(); redo.disabled = !editor.history.canRedo(); }; this.history.onChange(); } // create search box if (this.options.search) { this.searchBox = new JSONEditor.SearchBox(this, this.menu); } }; /** * Create main table * @private */ JSONEditor.prototype._createTable = function() { var contentOuter = document.createElement("div"); contentOuter.className = "jsoneditor-content-outer"; this.contentOuter = contentOuter; this.content = document.createElement("div"); this.content.className = "jsoneditor-content"; contentOuter.appendChild(this.content); this.table = document.createElement("table"); this.table.className = "jsoneditor-table"; this.content.appendChild(this.table); // IE8 does not handle overflow='auto' correctly. // Therefore, set overflow to 'scroll' var ieVersion = JSONEditor.getInternetExplorerVersion(); if (ieVersion == 8) { this.content.style.overflow = "scroll"; } // create colgroup where the first two columns don't have a fixed // width, and the edit columns do have a fixed width var col; this.colgroupContent = document.createElement("colgroup"); col = document.createElement("col"); col.width = "24px"; this.colgroupContent.appendChild(col); col = document.createElement("col"); this.colgroupContent.appendChild(col); col = document.createElement("col"); col.width = "24px"; this.colgroupContent.appendChild(col); col = document.createElement("col"); col.width = "24px"; this.colgroupContent.appendChild(col); col = document.createElement("col"); col.width = "24px"; this.colgroupContent.appendChild(col); this.table.appendChild(this.colgroupContent); this.tbody = document.createElement("tbody"); this.table.appendChild(this.tbody); this.frame.appendChild(contentOuter); }; /** * Find the node from an event target * @param {Element} target * @return {JSONEditor.Node | undefined} node or undefined when not found */ JSONEditor.getNodeFromTarget = function(target) { while (target) { if (target.node) { return target.node; } target = target.parentNode; } return undefined; }; /** * Create a JSONFormatter and attach it to given container * @constructor JSONFormatter * @param {Element} container * @param {Object} [options] Object with options. available options: * {Number} indentation Number of indentation * spaces. 4 by default. * {function} change Callback method * triggered on change * @param {JSON | String} [json] initial contents of the formatter */ JSONFormatter = 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)." ); } this.container = container; this.indentation = 4; // number of spaces this.width = container.clientWidth; this.height = container.clientHeight; this.frame = document.createElement("div"); this.frame.className = "jsoneditor-frame"; this.frame.onclick = function(event) { // prevent default submit action when JSONFormatter is located inside a form JSONEditor.Events.preventDefault(event); }; // create menu this.menu = document.createElement("div"); this.menu.className = "jsoneditor-menu"; this.frame.appendChild(this.menu); // create format button var buttonFormat = document.createElement("button"); //buttonFormat.innerHTML = 'Format'; buttonFormat.className = "jsoneditor-menu jsoneditor-format"; buttonFormat.title = "格式化JSON数据"; //buttonFormat.className = 'jsoneditor-button'; this.menu.appendChild(buttonFormat); // create compact button var buttonCompact = document.createElement("button"); //buttonCompact.innerHTML = 'Compact'; buttonCompact.className = "jsoneditor-menu jsoneditor-compact"; buttonCompact.title = "压缩JSON数据, 清除所有空白字符"; //buttonCompact.className = 'jsoneditor-button'; this.menu.appendChild(buttonCompact); this.content = document.createElement("div"); this.content.className = "jsonformatter-content"; this.frame.appendChild(this.content); this.textarea = document.createElement("textarea"); this.textarea.className = "jsonformatter-textarea"; this.textarea.spellcheck = false; this.content.appendChild(this.textarea); var textarea = this.textarea; // read the options if (options) { if (options.change) { // register on change event if (this.textarea.oninput === null) { this.textarea.oninput = function() { options.change(); }; } else { // oninput is undefined. For IE8- this.textarea.onchange = function() { options.change(); }; } } if (options.indentation) { this.indentation = Number(options.indentation); } } var me = this; buttonFormat.onclick = function() { try { var json = JSONEditor.parse(textarea.value); textarea.value = JSON.stringify(json, null, me.indentation); } catch (err) { me.onError(err); } }; buttonCompact.onclick = function() { try { var json = JSONEditor.parse(textarea.value); textarea.value = JSON.stringify(json); } catch (err) { me.onError(err); } }; this.container.appendChild(this.frame); // load initial json object or string if (typeof json == "string") { this.setText(json); } else { this.set(json); } }; /** * This method is executed on error. * It can be overwritten for each instance of the JSONFormatter * @param {String} err */ JSONFormatter.prototype.onError = function(err) { // action should be implemented for the instance }; /** * Set json data in the formatter * @param {Object} json */ JSONFormatter.prototype.set = function(json) { this.textarea.value = JSON.stringify(json, null, this.indentation); }; /** * Get json data from the formatter * @return {Object} json */ JSONFormatter.prototype.get = function() { return JSONEditor.parse(this.textarea.value); }; /** * Get the text contents of the JSONFormatter * @return {String} text */ JSONFormatter.prototype.getText = function() { return this.textarea.value; }; /** * Set the text contents of the JSONFormatter * @param {String} text */ JSONFormatter.prototype.setText = function(text) { this.textarea.value = text; }; /** * @constructor JSONEditor.SearchBox * Create a search box in given HTML container * @param {JSONEditor} editor The JSON Editor to attach to * @param {Element} container HTML container element of where to create the * search box */ JSONEditor.SearchBox = function(editor, container) { var searchBox = this; this.editor = editor; this.timeout = undefined; this.delay = 200; // ms this.lastText = undefined; this.dom = {}; this.dom.container = container; var table = document.createElement("table"); this.dom.table = table; table.className = "jsoneditor-search"; container.appendChild(table); var tbody = document.createElement("tbody"); this.dom.tbody = tbody; table.appendChild(tbody); var tr = document.createElement("tr"); tbody.appendChild(tr); var td = document.createElement("td"); td.className = "jsoneditor-search"; tr.appendChild(td); var results = document.createElement("div"); this.dom.results = results; results.className = "jsoneditor-search-results"; td.appendChild(results); td = document.createElement("td"); td.className = "jsoneditor-search"; tr.appendChild(td); var divInput = document.createElement("div"); this.dom.input = divInput; divInput.className = "jsoneditor-search"; divInput.title = "查找区块"; td.appendChild(divInput); // table to contain the text input and search button var tableInput = document.createElement("table"); tableInput.className = "jsoneditor-search-input"; divInput.appendChild(tableInput); var tbodySearch = document.createElement("tbody"); tableInput.appendChild(tbodySearch); tr = document.createElement("tr"); tbodySearch.appendChild(tr); var refreshSearch = document.createElement("button"); refreshSearch.className = "jsoneditor-search-refresh"; td = document.createElement("td"); td.appendChild(refreshSearch); tr.appendChild(td); var search = document.createElement("input"); this.dom.search = search; search.className = "jsoneditor-search"; search.oninput = function(event) { searchBox.onDelayedSearch(event); }; search.onchange = function(event) { // For IE 8 searchBox.onSearch(event); }; search.onkeydown = function(event) { searchBox.onKeyDown(event); }; search.onkeyup = function(event) { searchBox.onKeyUp(event); }; refreshSearch.onclick = function(event) { search.select(); }; // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819 td = document.createElement("td"); td.appendChild(search); tr.appendChild(td); var searchNext = document.createElement("button"); searchNext.title = "下一个 (Enter)"; searchNext.className = "jsoneditor-search-next"; searchNext.onclick = function() { searchBox.next(); }; td = document.createElement("td"); td.appendChild(searchNext); tr.appendChild(td); var searchPrevious = document.createElement("button"); searchPrevious.title = "上一个 (Shift+Enter)"; searchPrevious.className = "jsoneditor-search-previous"; searchPrevious.onclick = function() { searchBox.previous(); }; td = document.createElement("td"); td.appendChild(searchPrevious); tr.appendChild(td); }; /** * Go to the next search result */ JSONEditor.SearchBox.prototype.next = function() { if (this.results != undefined) { var index = this.resultIndex != undefined ? this.resultIndex + 1 : 0; if (index > this.results.length - 1) { index = 0; } this.setActiveResult(index); } }; /** * Go to the prevous search result */ JSONEditor.SearchBox.prototype.previous = function() { if (this.results != undefined) { var max = this.results.length - 1; var index = this.resultIndex != undefined ? this.resultIndex - 1 : max; if (index < 0) { index = max; } this.setActiveResult(index); } }; /** * Set new value for the current active result * @param {Number} index */ JSONEditor.SearchBox.prototype.setActiveResult = function(index) { // de-activate current active result if (this.activeResult) { var prevNode = this.activeResult.node; var prevElem = this.activeResult.elem; if (prevElem == "field") { delete prevNode.searchFieldActive; } else { delete prevNode.searchValueActive; } prevNode.updateDom(); } if (!this.results || !this.results[index]) { // out of range, set to undefined this.resultIndex = undefined; this.activeResult = undefined; return; } this.resultIndex = index; // set new node active var node = this.results[this.resultIndex].node; var elem = this.results[this.resultIndex].elem; if (elem == "field") { node.searchFieldActive = true; } else { node.searchValueActive = true; } this.activeResult = this.results[this.resultIndex]; node.updateDom(); node.scrollTo(); }; /** * Set the focus to the currently active result. If there is no currently * active result, the next search result will get focus */ JSONEditor.SearchBox.prototype.focusActiveResult = function() { if (!this.activeResult) { this.next(); } if (this.activeResult) { this.activeResult.node.focus(this.activeResult.elem); } }; /** * Cancel any running onDelayedSearch. */ JSONEditor.SearchBox.prototype.clearDelay = function() { if (this.timeout != undefined) { clearTimeout(this.timeout); delete this.timeout; } }; /** * Start a timer to execute a search after a short delay. * Used for reducing the number of searches while typing. * @param {Event} event */ JSONEditor.SearchBox.prototype.onDelayedSearch = function(event) { // execute the search after a short delay (reduces the number of // search actions while typing in the search text box) this.clearDelay(); var searchBox = this; this.timeout = setTimeout(function(event) { searchBox.onSearch(event); }, this.delay); }; /** * Handle onSearch event * @param {Event} event * @param {boolean} [forceSearch] If true, search will be executed again even * when the search text is not changed. * Default is false. */ JSONEditor.SearchBox.prototype.onSearch = function(event, forceSearch) { this.clearDelay(); var value = this.dom.search.value; var text = value.length > 0 ? value : undefined; if (text != this.lastText || forceSearch) { // only search again when changed this.lastText = text; this.results = this.editor.search(text); this.setActiveResult(undefined); // display search results if (text != undefined) { var resultCount = this.results.length; switch (resultCount) { case 0: this.dom.results.innerHTML = "区块/值未找到"; break; default: this.dom.results.innerHTML = "找到 " + resultCount + " 个节点"; break; } } else { this.dom.results.innerHTML = ""; } } }; /** * Handle onKeyDown event in the input box * @param {Event} event */ JSONEditor.SearchBox.prototype.onKeyDown = function(event) { event = event || window.event; var keynum = event.which || event.keyCode; if (keynum == 27) { // ESC this.dom.search.value = ""; // clear search this.onSearch(event); JSONEditor.Events.preventDefault(event); JSONEditor.Events.stopPropagation(event); } else if (keynum == 13) { // Enter if (event.ctrlKey) { // force to search again this.onSearch(event, true); } else if (event.shiftKey) { // move to the previous search result this.previous(); } else { // move to the next search result this.next(); } JSONEditor.Events.preventDefault(event); JSONEditor.Events.stopPropagation(event); } }; /** * Handle onKeyUp event in the input box * @param {Event} event */ JSONEditor.SearchBox.prototype.onKeyUp = function(event) { event = event || window.event; var keynum = event.which || event.keyCode; if (keynum != 27 && keynum != 13) { // !ESC and !Enter this.onDelayedSearch(event); // For IE 8 } }; // create namespace for event methods JSONEditor.Events = {}; /** * Add and event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" * @param {function} listener The callback function to be executed * @param {boolean} useCapture * @return {function} the created event listener */ JSONEditor.Events.addEventListener = function( element, action, listener, useCapture ) { if (element.addEventListener) { if (useCapture === undefined) useCapture = false; if ( action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0 ) { action = "DOMMouseScroll"; // For Firefox } element.addEventListener(action, listener, useCapture); return listener; } else { // IE browsers var f = function() { return listener.call(element, window.event); }; element.attachEvent("on" + action, f); return f; } }; /** * Remove an event listener from an element * @param {Element} element An html dom element * @param {string} action The name of the event, for example "mousedown" * @param {function} listener The listener function * @param {boolean} useCapture */ JSONEditor.Events.removeEventListener = function( element, action, listener, useCapture ) { if (element.removeEventListener) { // non-IE browsers if (useCapture === undefined) useCapture = false; if ( action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0 ) { action = "DOMMouseScroll"; // For Firefox } element.removeEventListener(action, listener, useCapture); } else { // IE browsers element.detachEvent("on" + action, listener); } }; /** * Stop event propagation * @param {Event} event */ JSONEditor.Events.stopPropagation = function(event) { if (!event) event = window.event; if (event.stopPropagation) { event.stopPropagation(); // non-IE browsers } else { event.cancelBubble = true; // IE browsers } }; /** * Cancels the event if it is cancelable, without stopping further propagation of the event. * @param {Event} event */ JSONEditor.Events.preventDefault = function(event) { if (!event) event = window.event; if (event.preventDefault) { event.preventDefault(); // non-IE browsers } else { event.returnValue = false; // IE browsers } }; /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} left The absolute left position of this element * in the browser page. */ JSONEditor.getAbsoluteLeft = function(elem) { var left = 0; var body = document.body; while (elem != null && elem != body) { left += elem.offsetLeft; left -= elem.scrollLeft; elem = elem.offsetParent; } return left; }; /** * Retrieve the absolute top value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} top The absolute top position of this element * in the browser page. */ JSONEditor.getAbsoluteTop = function(elem) { var top = 0; var body = document.body; while (elem != null && elem != body) { top += elem.offsetTop; top -= elem.scrollTop; elem = elem.offsetParent; } return top; }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ JSONEditor.addClassName = function(elem, className) { var classes = elem.className.split(" "); if (classes.indexOf(className) == -1) { classes.push(className); // add the class to the array elem.className = classes.join(" "); } }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ JSONEditor.removeClassName = function(elem, className) { var classes = elem.className.split(" "); var index = classes.indexOf(className); if (index != -1) { classes.splice(index, 1); // remove the class from the array elem.className = classes.join(" "); } }; /** * Strip the formatting from the contents of a div * the formatting from the div itself is not stripped, only from its childs. * @param {Element} divElement */ JSONEditor.stripFormatting = function(divElement) { var childs = divElement.childNodes; for (var i = 0, iMax = childs.length; i < iMax; i++) { var child = childs[i]; // remove the style if (child.style) { // TODO: test if child.attributes does contain style child.removeAttribute("style"); } // remove all attributes var attributes = child.attributes; if (attributes) { for (var j = attributes.length - 1; j >= 0; j--) { var attribute = attributes[j]; if (attribute.specified == true) { child.removeAttribute(attribute.name); } } } // recursively strip childs JSONEditor.stripFormatting(child); } }; /** * Set focus to the end of an editable div * code from Nico Burns * http://stackoverflow.com/users/140293/nico-burns * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity * @param {Element} contentEditableElement */ JSONEditor.setEndOfContentEditable = function(contentEditableElement) { var range, selection; if (document.createRange) { //Firefox, Chrome, Opera, Safari, IE 9+ range = document.createRange(); //Create a range (a range is a like the selection but invisible) range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start selection = window.getSelection(); //get the selection object (allows you to change selection) selection.removeAllRanges(); //remove any selections already made selection.addRange(range); //make the range you have just created the visible selection } else if (document.selection) { //IE 8 and lower range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible) range.moveToElementText(contentEditableElement); //Select the entire contents of the element with the range range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start range.select(); //Select the range (make it the visible selection } }; /** * Get the inner text of an HTML element (for example a div element) * @param {Element} element * @param {Object} [buffer] * @return {String} innerText */ JSONEditor.getInnerText = function(element, buffer) { var first = buffer == undefined; if (first) { buffer = { text: "", flush: function() { var text = this.text; this.text = ""; return text; }, set: function(text) { this.text = text; } }; } // text node if (element.nodeValue) { return buffer.flush() + element.nodeValue; } // divs or other HTML elements if (element.hasChildNodes()) { var childNodes = element.childNodes; var innerText = ""; for (var i = 0, iMax = childNodes.length; i < iMax; i++) { var child = childNodes[i]; if (child.nodeName == "DIV" || child.nodeName == "P") { var prevChild = childNodes[i - 1]; var prevName = prevChild ? prevChild.nodeName : undefined; if ( prevName && prevName != "DIV" && prevName != "P" && prevName != "BR" ) { innerText += "\n"; buffer.flush(); } innerText += JSONEditor.getInnerText(child, buffer); buffer.set("\n"); } else if (child.nodeName == "BR") { innerText += buffer.flush(); buffer.set("\n"); } else { innerText += JSONEditor.getInnerText(child, buffer); } } return innerText; } else { if ( element.nodeName == "P" && JSONEditor.getInternetExplorerVersion() != -1 ) { // On Internet Explorer, a

with hasChildNodes()==false is // rendered with a new line. Note that a

with // hasChildNodes()==true is rendered without a new line // Other browsers always ensure there is a
inside the

, // and if not, the

does not render a new line return buffer.flush(); } } // br or unknown return ""; }; /** * Returns the version of Internet Explorer or a -1 * (indicating the use of another browser). * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx * @return {Number} Internet Explorer version, or -1 in case of an other browser */ JSONEditor._ieVersion = undefined; JSONEditor.getInternetExplorerVersion = function() { if (JSONEditor._ieVersion == undefined) { var rv = -1; // Return value assumes failure. if (navigator.appName == "Microsoft Internet Explorer") { var ua = navigator.userAgent; var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})"); if (re.exec(ua) != null) { rv = parseFloat(RegExp.$1); } } JSONEditor._ieVersion = rv; } return JSONEditor._ieVersion; }; JSONEditor.ieVersion = JSONEditor.getInternetExplorerVersion(); /** * Parse JSON using the parser built-in in the browser. * On exception, the jsonString is validated and a detailed error is thrown. * @param {String} jsonString */ JSONEditor.parse = function(jsonString) { try { return JSON.parse(jsonString); } catch (err) { // get a detailed error message using validate var message = JSONEditor.validate(jsonString) || err; throw new Error(message); } }; /** * Validate a string containing a JSON object * This method uses JSONLint to validate the String. If JSONLint is not * available, the built-in JSON parser of the browser is used. * @param {String} jsonString String with an (invalid) JSON object * @return {String | undefined} Returns undefined when the string is valid JSON, * returns a string with an error message when * the data is invalid */ JSONEditor.validate = function(jsonString) { var message = undefined; try { if (window.jsonlint) { window.jsonlint.parse(jsonString); } else { JSON.parse(jsonString); } } catch (err) { message = '

' + err.toString() + "
"; if (window.jsonlint) { message += '
' + "JSONLint" + " 提供验证.
"; } } return message; }; function jsonArea(ob) { let inputEle = ob.el; let insert = ob.insert; let nth = ob.nth; let change = ob.change; let thisare = new Object(); if (!inputEle) { throw new Error("没有提供数据."); } // if target to multi dom if (nth) { thisare.data = document.querySelectorAll(inputEle)[nth - 1]; } else { let all = document.querySelectorAll(inputEle); for (var i = 0; i < all.length; i++) { jsonArea({ el: inputEle, insert: insert, nth: i + 1, change: change }); } // thisare.data = document.querySelector(inputEle); return; } // create container thisare.container = document.createElement("div"); thisare.container.style.width = "100%"; thisare.container.style.height = "auto"; thisare.data.parentElement.insertBefore(thisare.container, thisare.data); // check wether is a json thisare.isJson = function(str) { if (!isNaN(str)) return false; if (str == "") return false; if (str == '""') return false; if (typeof str == "string") { try { JSON.parse(str); return true; } catch (e) { // console.log(e); return false; } } console.log("不是丢�个stringify"); }; // json data thisare.jsonval = "{}"; // callback when change thisare.syn = function() {}; // whether is textarea or input if (thisare.data.tagName == "TEXTAREA") { thisare.jsonval = thisare.data.innerHTML; thisare.syn = function() { thisare.data.innerHTML = JSON.stringify(thisare.jsonEditor.get()); change(thisare.jsonEditor.get()); }; } else if (thisare.data.tagName == "INPUT") { thisare.jsonval = thisare.data.value; thisare.syn = function() { thisare.data.value = JSON.stringify(thisare.jsonEditor.get()); change(thisare.jsonEditor.get()); }; } // if not json, and not insert ,then json editer thisare.hashJson = thisare.isJson(thisare.jsonval); if (!thisare.hashJson && !insert) return; thisare.data.style.display = "none"; // create jsoneditor for father thisare.jsonEditor = new JSONEditor(thisare.container, { change: function() { thisare.lastChanged = thisare.jsonEditor; thisare.syn(); } }); if (thisare.hashJson) { thisare.jsonEditor.set(JSON.parse(thisare.jsonval)); } console.log("in jsonArea"); return thisare; }