From d911d8ca63728bfa4cdba6694e1819736fdfa3ff Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Sun, 1 Feb 2015 05:21:18 +0100 Subject: [PATCH] created traits and front end for editing traits --- back-office/Gemfile | 1 + back-office/Gemfile.lock | 3 + back-office/app/assets/images/.DS_Store | Bin 0 -> 6148 bytes .../app/assets/javascripts/application.js | 1 + .../app/assets/javascripts/jsoneditor.js | 6302 +++++++++++++++++ .../app/assets/stylesheets/application.css | 2 + .../app/assets/stylesheets/jsoneditor.min.css | 1 + .../views/items/_traits_form_column.html.erb | 6 + .../app/views/layouts/application.html.erb | 24 + back-office/public/img/jsoneditor-icons.png | Bin 0 -> 14438 bytes .../20150131061353_add_traits_to_item.rb | 5 + front-api/db/schema.rb | 3 +- 12 files changed, 6347 insertions(+), 1 deletion(-) create mode 100644 back-office/app/assets/images/.DS_Store create mode 100644 back-office/app/assets/javascripts/jsoneditor.js create mode 100644 back-office/app/assets/stylesheets/jsoneditor.min.css create mode 100644 back-office/app/views/items/_traits_form_column.html.erb create mode 100644 back-office/public/img/jsoneditor-icons.png create mode 100644 front-api/db/migrate/20150131061353_add_traits_to_item.rb diff --git a/back-office/Gemfile b/back-office/Gemfile index 8e2e0ee..377c6d8 100644 --- a/back-office/Gemfile +++ b/back-office/Gemfile @@ -16,6 +16,7 @@ gem 'coffee-rails', '~> 4.1.0' # Use jquery as the JavaScript library gem 'jquery-rails' +gem 'jquery-ui-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder diff --git a/back-office/Gemfile.lock b/back-office/Gemfile.lock index a22c30c..5af186e 100644 --- a/back-office/Gemfile.lock +++ b/back-office/Gemfile.lock @@ -79,6 +79,8 @@ GEM rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + jquery-ui-rails (5.0.3) + railties (>= 3.2.16) json (1.8.2) loofah (2.0.1) nokogiri (>= 1.5.9) @@ -174,6 +176,7 @@ DEPENDENCIES coffee-rails (~> 4.1.0) jbuilder (~> 2.0) jquery-rails + jquery-ui-rails pg puma rails (= 4.2.0) diff --git a/back-office/app/assets/images/.DS_Store b/back-office/app/assets/images/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + * @version 3.2.0 + * @date 2015-01-25 + */ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define(factory); + else if(typeof exports === 'object') + exports["JSONEditor"] = factory(); + else + root["JSONEditor"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; +/******/ +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.loaded = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(1), __webpack_require__(2), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (treemode, textmode, util) { + + /** + * @constructor JSONEditor + * @param {Element} container Container element + * @param {Object} [options] Object with options. available options: + * {String} mode Editor mode. Available values: + * 'tree' (default), 'view', + * 'form', 'text', and 'code'. + * {function} change Callback method, triggered + * on change of contents + * {Boolean} search Enable search box. + * True by default + * Only applicable for modes + * 'tree', 'view', and 'form' + * {Boolean} history Enable history (undo/redo). + * True by default + * Only applicable for modes + * 'tree', 'view', and 'form' + * {String} name Field name for the root node. + * Only applicable for modes + * 'tree', 'view', and 'form' + * {Number} indentation Number of indentation + * spaces. 4 by default. + * Only applicable for + * modes 'text' and 'code' + * @param {Object | undefined} json JSON object + */ + function JSONEditor (container, options, json) { + if (!(this instanceof JSONEditor)) { + throw new Error('JSONEditor constructor called without "new".'); + } + + // check for unsupported browser (IE8 and older) + var ieVersion = util.getInternetExplorerVersion(); + if (ieVersion != -1 && ieVersion < 9) { + throw new Error('Unsupported browser, IE9 or newer required. ' + + 'Please install the newest version of your browser.'); + } + + if (arguments.length) { + this._create(container, options, json); + } + } + + /** + * Configuration for all registered modes. Example: + * { + * tree: { + * mixin: TreeEditor, + * data: 'json' + * }, + * text: { + * mixin: TextEditor, + * data: 'text' + * } + * } + * + * @type { Object. } + */ + JSONEditor.modes = {}; + + /** + * Create the JSONEditor + * @param {Element} container Container element + * @param {Object} [options] See description in constructor + * @param {Object | undefined} json JSON object + * @private + */ + JSONEditor.prototype._create = function (container, options, json) { + this.container = container; + this.options = options || {}; + this.json = json || {}; + + var mode = this.options.mode || 'tree'; + this.setMode(mode); + }; + + /** + * Detach the editor from the DOM + * @private + */ + JSONEditor.prototype._delete = function () {}; + + /** + * Set JSON object in editor + * @param {Object | undefined} json JSON data + */ + JSONEditor.prototype.set = function (json) { + this.json = json; + }; + + /** + * Get JSON from the editor + * @returns {Object} json + */ + JSONEditor.prototype.get = function () { + return this.json; + }; + + /** + * Set string containing JSON for the editor + * @param {String | undefined} jsonText + */ + JSONEditor.prototype.setText = function (jsonText) { + this.json = util.parse(jsonText); + }; + + /** + * Get stringified JSON contents from the editor + * @returns {String} jsonText + */ + JSONEditor.prototype.getText = function () { + return JSON.stringify(this.json); + }; + + /** + * Set a field name for the root node. + * @param {String | undefined} name + */ + JSONEditor.prototype.setName = function (name) { + if (!this.options) { + this.options = {}; + } + this.options.name = name; + }; + + /** + * Get the field name for the root node. + * @return {String | undefined} name + */ + JSONEditor.prototype.getName = function () { + return this.options && this.options.name; + }; + + /** + * Change the mode of the editor. + * JSONEditor will be extended with all methods needed for the chosen mode. + * @param {String} mode Available modes: 'tree' (default), 'view', 'form', + * 'text', and 'code'. + */ + JSONEditor.prototype.setMode = function (mode) { + var container = this.container, + options = util.extend({}, this.options), + data, + name; + + options.mode = mode; + var config = JSONEditor.modes[mode]; + if (config) { + try { + var asText = (config.data == 'text'); + name = this.getName(); + data = this[asText ? 'getText' : 'get'](); // get text or json + + this._delete(); + util.clear(this); + util.extend(this, config.mixin); + this.create(container, options); + + this.setName(name); + this[asText ? 'setText' : 'set'](data); // set text or json + + if (typeof config.load === 'function') { + try { + config.load.call(this); + } + catch (err) {} + } + } + catch (err) { + this._onError(err); + } + } + else { + throw new Error('Unknown mode "' + options.mode + '"'); + } + }; + + /** + * Throw an error. If an error callback is configured in options.error, this + * callback will be invoked. Else, a regular error is thrown. + * @param {Error} err + * @private + */ + JSONEditor.prototype._onError = function(err) { + // TODO: onError is deprecated since version 2.2.0. cleanup some day + if (typeof this.onError === 'function') { + util.log('WARNING: JSONEditor.onError is deprecated. ' + + 'Use options.error instead.'); + this.onError(err); + } + + if (this.options && typeof this.options.error === 'function') { + this.options.error(err); + } + else { + throw err; + } + }; + + /** + * Register a plugin with one ore multiple modes for the JSON Editor. + * + * A mode is described as an object with properties: + * + * - `mode: String` The name of the mode. + * - `mixin: Object` An object containing the mixin functions which + * will be added to the JSONEditor. Must contain functions + * create, get, getText, set, and setText. May have + * additional functions. + * When the JSONEditor switches to a mixin, all mixin + * functions are added to the JSONEditor, and then + * the function `create(container, options)` is executed. + * - `data: 'text' | 'json'` The type of data that will be used to load the mixin. + * - `[load: function]` An optional function called after the mixin + * has been loaded. + * + * @param {Object | Array} mode A mode object or an array with multiple mode objects. + */ + JSONEditor.registerMode = function (mode) { + var i, prop; + + if (util.isArray(mode)) { + // multiple modes + for (i = 0; i < mode.length; i++) { + JSONEditor.registerMode(mode[i]); + } + } + else { + // validate the new mode + if (!('mode' in mode)) throw new Error('Property "mode" missing'); + if (!('mixin' in mode)) throw new Error('Property "mixin" missing'); + if (!('data' in mode)) throw new Error('Property "data" missing'); + var name = mode.mode; + if (name in JSONEditor.modes) { + throw new Error('Mode "' + name + '" already registered'); + } + + // validate the mixin + if (typeof mode.mixin.create !== 'function') { + throw new Error('Required function "create" missing on mixin'); + } + var reserved = ['setMode', 'registerMode', 'modes']; + for (i = 0; i < reserved.length; i++) { + prop = reserved[i]; + if (prop in mode.mixin) { + throw new Error('Reserved property "' + prop + '" not allowed in mixin'); + } + } + + JSONEditor.modes[name] = mode; + } + }; + + // register tree and text modes + JSONEditor.registerMode(treemode); + JSONEditor.registerMode(textmode); + + return JSONEditor; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(4), __webpack_require__(5), __webpack_require__(6), __webpack_require__(7), __webpack_require__(8), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (Highlighter, History, SearchBox, Node, modeswitcher, util) { + + // create a mixin with the functions for tree mode + var treemode = {}; + + /** + * Create a tree editor + * @param {Element} container Container element + * @param {Object} [options] Object with options. available options: + * {String} mode Editor mode. Available values: + * 'tree' (default), 'view', + * and 'form'. + * {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 + */ + treemode.create = function (container, options) { + if (!container) { + throw new Error('No container element provided.'); + } + this.container = container; + this.dom = {}; + this.highlighter = new Highlighter(); + this.selection = undefined; // will hold the last input selection + + this._setOptions(options); + + if (this.options.history && this.options.mode !== 'view') { + this.history = new History(this); + } + + this._createFrame(); + this._createTable(); + }; + + /** + * Detach the editor from the DOM + * @private + */ + treemode._delete = function () { + if (this.frame && this.container && this.frame.parentNode == this.container) { + this.container.removeChild(this.frame); + } + }; + + /** + * Initialize and set default options + * @param {Object} [options] See description in constructor + * @private + */ + treemode._setOptions = function (options) { + this.options = { + search: true, + history: true, + mode: 'tree', + 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]; + } + } + } + }; + + // node currently being edited + var focusNode = undefined; + + // dom having focus + var domFocus = null; + + /** + * 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). + */ + treemode.set = function (json, name) { + // adjust field name for root node + if (name) { + // TODO: deprecated since version 2.2.0. Cleanup some day. + util.log('Warning: second parameter "name" is deprecated. ' + + 'Use setName(name) instead.'); + 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 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 + */ + treemode.get = function () { + // remove focus from currently edited node + if (focusNode) { + focusNode.blur(); + } + + if (this.node) { + return this.node.getValue(); + } + else { + return undefined; + } + }; + + /** + * Get the text contents of the editor + * @return {String} jsonText + */ + treemode.getText = function() { + return JSON.stringify(this.get()); + }; + + /** + * Set the text contents of the editor + * @param {String} jsonText + */ + treemode.setText = function(jsonText) { + this.set(util.parse(jsonText)); + }; + + /** + * Set a field name for the root node. + * @param {String | undefined} name + */ + treemode.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 + */ + treemode.getName = function () { + return this.options.name; + }; + + /** + * Remove the root node from the editor + */ + treemode.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 {Node} node + * @private + */ + treemode._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: + * - {Node} node, + * - {String} elem the dom element name where + * the result is found ('field' or + * 'value') + */ + treemode.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 + */ + treemode.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 + */ + treemode.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. + * @private + */ + treemode._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) { + util.log('Error in change callback: ', err); + } + } + }; + + /** + * Start autoscrolling when given mouse position is above the top of the + * editor contents, or below the bottom. + * @param {Number} mouseY Absolute mouse position in pixels + */ + treemode.startAutoScroll = function (mouseY) { + var me = this; + var content = this.content; + var top = util.getAbsoluteTop(content); + var height = content.clientHeight; + var bottom = top + height; + var margin = 24; + var interval = 50; // ms + + if ((mouseY < top + margin) && content.scrollTop > 0) { + this.autoScrollStep = ((top + margin) - mouseY) / 3; + } + else if (mouseY > bottom - margin && + height + content.scrollTop < content.scrollHeight) { + this.autoScrollStep = ((bottom - margin) - mouseY) / 3; + } + else { + this.autoScrollStep = undefined; + } + + if (this.autoScrollStep) { + if (!this.autoScrollTimer) { + this.autoScrollTimer = setInterval(function () { + if (me.autoScrollStep) { + content.scrollTop -= me.autoScrollStep; + } + else { + me.stopAutoScroll(); + } + }, interval); + } + } + else { + this.stopAutoScroll(); + } + }; + + /** + * Stop auto scrolling. Only applicable when scrolling + */ + treemode.stopAutoScroll = function () { + if (this.autoScrollTimer) { + clearTimeout(this.autoScrollTimer); + delete this.autoScrollTimer; + } + if (this.autoScrollStep) { + delete this.autoScrollStep; + } + }; + + + /** + * Set the focus to an element in the editor, set text selection, and + * set scroll position. + * @param {Object} selection An object containing fields: + * {Element | undefined} dom The dom element + * which has focus + * {Range | TextRange} range A text selection + * {Number} scrollTop Scroll position + */ + treemode.setSelection = function (selection) { + if (!selection) { + return; + } + + if ('scrollTop' in selection && this.content) { + // TODO: animated scroll + this.content.scrollTop = selection.scrollTop; + } + if (selection.range) { + util.setSelectionOffset(selection.range); + } + if (selection.dom) { + selection.dom.focus(); + } + }; + + /** + * Get the current focus + * @return {Object} selection An object containing fields: + * {Element | undefined} dom The dom element + * which has focus + * {Range | TextRange} range A text selection + * {Number} scrollTop Scroll position + */ + treemode.getSelection = function () { + return { + dom: domFocus, + scrollTop: this.content ? this.content.scrollTop : 0, + range: util.getSelectionOffset() + }; + }; + + /** + * Adjust the scroll position such that given top position is shown at 1/4 + * of the window height. + * @param {Number} top + * @param {function(boolean)} [callback] Callback, executed when animation is + * finished. The callback returns true + * when animation is finished, or false + * when not. + */ + treemode.scrollTo = function (top, callback) { + var content = this.content; + if (content) { + var editor = this; + // cancel any running animation + if (editor.animateTimeout) { + clearTimeout(editor.animateTimeout); + delete editor.animateTimeout; + } + if (editor.animateCallback) { + editor.animateCallback(false); + delete editor.animateCallback; + } + + // 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.animateCallback = callback; + editor.animateTimeout = setTimeout(animate, 50); + } + else { + // finished + if (callback) { + callback(true); + } + content.scrollTop = finalScrollTop; + delete editor.animateTimeout; + delete editor.animateCallback; + } + }; + animate(); + } + else { + if (callback) { + callback(false); + } + } + }; + + /** + * Create main frame + * @private + */ + treemode._createFrame = function () { + // create the frame + this.frame = document.createElement('div'); + this.frame.className = 'jsoneditor'; + this.container.appendChild(this.frame); + + // create one global event listener to handle all events from all nodes + var editor = this; + function onEvent(event) { + editor._onEvent(event); + } + this.frame.onclick = function (event) { + var target = event.target;// || event.srcElement; + + onEvent(event); + + // prevent default submit action of buttons when editor is located + // inside a form + if (target.nodeName == 'BUTTON') { + event.preventDefault(); + } + }; + this.frame.oninput = onEvent; + 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 + util.addEventListener(this.frame, 'focus', onEvent, true); + util.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 = 'menu'; + this.frame.appendChild(this.menu); + + // create expand all button + var expandAll = document.createElement('button'); + expandAll.className = 'expand-all'; + expandAll.title = 'Expand all fields'; + expandAll.onclick = function () { + editor.expandAll(); + }; + this.menu.appendChild(expandAll); + + // create expand all button + var collapseAll = document.createElement('button'); + collapseAll.title = 'Collapse all fields'; + collapseAll.className = 'collapse-all'; + collapseAll.onclick = function () { + editor.collapseAll(); + }; + this.menu.appendChild(collapseAll); + + // create undo/redo buttons + if (this.history) { + // create undo button + var undo = document.createElement('button'); + undo.className = 'undo separator'; + undo.title = 'Undo last action (Ctrl+Z)'; + undo.onclick = function () { + editor._onUndo(); + }; + this.menu.appendChild(undo); + this.dom.undo = undo; + + // create redo button + var redo = document.createElement('button'); + redo.className = 'redo'; + redo.title = 'Redo (Ctrl+Shift+Z)'; + redo.onclick = function () { + editor._onRedo(); + }; + 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 mode box + if (this.options && this.options.modes && this.options.modes.length) { + var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode); + this.menu.appendChild(modeBox); + this.dom.modeBox = modeBox; + } + + // create search box + if (this.options.search) { + this.searchBox = new SearchBox(this, this.menu); + } + }; + + /** + * Perform an undo action + * @private + */ + treemode._onUndo = function () { + if (this.history) { + // undo last action + this.history.undo(); + + // trigger change callback + if (this.options.change) { + this.options.change(); + } + } + }; + + /** + * Perform a redo action + * @private + */ + treemode._onRedo = function () { + if (this.history) { + // redo last action + this.history.redo(); + + // trigger change callback + if (this.options.change) { + this.options.change(); + } + } + }; + + /** + * Event handler + * @param event + * @private + */ + treemode._onEvent = function (event) { + var target = event.target; + + if (event.type == 'keydown') { + this._onKeyDown(event); + } + + if (event.type == 'focus') { + domFocus = target; + } + + var node = Node.getNodeFromTarget(target); + if (node) { + node.onEvent(event); + } + }; + + /** + * Event handler for keydown. Handles shortcut keys + * @param {Event} event + * @private + */ + treemode._onKeyDown = function (event) { + var keynum = event.which || event.keyCode; + var ctrlKey = event.ctrlKey; + var shiftKey = event.shiftKey; + var handled = false; + + if (keynum == 9) { // Tab or Shift+Tab + setTimeout(function () { + // select all text when moving focus to an editable div + util.selectContentEditable(domFocus); + }, 0); + } + + if (this.searchBox) { + if (ctrlKey && keynum == 70) { // Ctrl+F + this.searchBox.dom.search.focus(); + this.searchBox.dom.search.select(); + handled = true; + } + else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G + var focus = true; + if (!shiftKey) { + // select next search result (F3 or Ctrl+G) + this.searchBox.next(focus); + } + else { + // select previous search result (Shift+F3 or Ctrl+Shift+G) + this.searchBox.previous(focus); + } + + handled = true; + } + } + + if (this.history) { + if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z + // undo + this._onUndo(); + handled = true; + } + else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z + // redo + this._onRedo(); + handled = true; + } + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + /** + * Create main table + * @private + */ + treemode._createTable = function () { + var contentOuter = document.createElement('div'); + contentOuter.className = 'outer'; + this.contentOuter = contentOuter; + + this.content = document.createElement('div'); + this.content.className = 'tree'; + contentOuter.appendChild(this.content); + + this.table = document.createElement('table'); + this.table.className = 'tree'; + this.content.appendChild(this.table); + + // 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'); + if (this.options.mode === 'tree') { + 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'); + this.colgroupContent.appendChild(col); + this.table.appendChild(this.colgroupContent); + + this.tbody = document.createElement('tbody'); + this.table.appendChild(this.tbody); + + this.frame.appendChild(contentOuter); + }; + + // define modes + return [ + { + mode: 'tree', + mixin: treemode, + data: 'json' + }, + { + mode: 'view', + mixin: treemode, + data: 'json' + }, + { + mode: 'form', + mixin: treemode, + data: 'json' + } + ]; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(8), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (modeswitcher, util) { + + // create a mixin with the functions for text mode + var textmode = {}; + + /** + * Create a text editor + * @param {Element} container + * @param {Object} [options] Object with options. available options: + * {String} mode Available values: + * "text" (default) + * or "code". + * {Number} indentation Number of indentation + * spaces. 2 by default. + * {function} change Callback method + * triggered on change + * @private + */ + textmode.create = function (container, options) { + // read options + options = options || {}; + this.options = options; + if (options.indentation) { + this.indentation = Number(options.indentation); + } + else { + this.indentation = 2; // number of spaces + } + this.mode = (options.mode == 'code') ? 'code' : 'text'; + if (this.mode == 'code') { + // verify whether Ace editor is available and supported + if (typeof ace === 'undefined') { + this.mode = 'text'; + util.log('WARNING: Cannot load code editor, Ace library not loaded. ' + + 'Falling back to plain text editor'); + } + } + + var me = this; + this.container = container; + this.dom = {}; + this.editor = undefined; // ace code editor + this.textarea = undefined; // plain text editor (fallback when Ace is not available) + + this.width = container.clientWidth; + this.height = container.clientHeight; + + this.frame = document.createElement('div'); + this.frame.className = 'jsoneditor'; + this.frame.onclick = function (event) { + // prevent default submit action when the editor is located inside a form + event.preventDefault(); + }; + this.frame.onkeydown = function (event) { + me._onKeyDown(event); + }; + + // create menu + this.menu = document.createElement('div'); + this.menu.className = 'menu'; + this.frame.appendChild(this.menu); + + // create format button + var buttonFormat = document.createElement('button'); + buttonFormat.className = 'format'; + buttonFormat.title = 'Format JSON data, with proper indentation and line feeds (Ctrl+\\)'; + this.menu.appendChild(buttonFormat); + buttonFormat.onclick = function () { + try { + me.format(); + } + catch (err) { + me._onError(err); + } + }; + + // create compact button + var buttonCompact = document.createElement('button'); + buttonCompact.className = 'compact'; + buttonCompact.title = 'Compact JSON data, remove all whitespaces (Ctrl+Shift+\\)'; + this.menu.appendChild(buttonCompact); + buttonCompact.onclick = function () { + try { + me.compact(); + } + catch (err) { + me._onError(err); + } + }; + + // create mode box + if (this.options && this.options.modes && this.options.modes.length) { + var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode); + this.menu.appendChild(modeBox); + this.dom.modeBox = modeBox; + } + + this.content = document.createElement('div'); + this.content.className = 'outer'; + this.frame.appendChild(this.content); + + this.container.appendChild(this.frame); + + if (this.mode == 'code') { + this.editorDom = document.createElement('div'); + this.editorDom.style.height = '100%'; // TODO: move to css + this.editorDom.style.width = '100%'; // TODO: move to css + this.content.appendChild(this.editorDom); + + var editor = ace.edit(this.editorDom); + editor.setTheme('ace/theme/jsoneditor'); + editor.setShowPrintMargin(false); + editor.setFontSize(13); + editor.getSession().setMode('ace/mode/json'); + editor.getSession().setTabSize(this.indentation); + editor.getSession().setUseSoftTabs(true); + editor.getSession().setUseWrapMode(true); + this.editor = editor; + + var poweredBy = document.createElement('a'); + poweredBy.appendChild(document.createTextNode('powered by ace')); + poweredBy.href = 'http://ace.ajax.org'; + poweredBy.target = '_blank'; + poweredBy.className = 'poweredBy'; + poweredBy.onclick = function () { + // TODO: this anchor falls below the margin of the content, + // therefore the normal a.href does not work. We use a click event + // for now, but this should be fixed. + window.open(poweredBy.href, poweredBy.target); + }; + this.menu.appendChild(poweredBy); + + if (options.change) { + // register onchange event + editor.on('change', function () { + options.change(); + }); + } + } + else { + // load a plain text textarea + var textarea = document.createElement('textarea'); + textarea.className = 'text'; + textarea.spellcheck = false; + this.content.appendChild(textarea); + this.textarea = textarea; + + if (options.change) { + // register onchange event + if (this.textarea.oninput === null) { + this.textarea.oninput = function () { + options.change(); + } + } + else { + // oninput is undefined. For IE8- + this.textarea.onchange = function () { + options.change(); + } + } + } + } + }; + + /** + * Event handler for keydown. Handles shortcut keys + * @param {Event} event + * @private + */ + textmode._onKeyDown = function (event) { + var keynum = event.which || event.keyCode; + var handled = false; + + if (keynum == 220 && event.ctrlKey) { + if (event.shiftKey) { // Ctrl+Shift+\ + this.compact(); + } + else { // Ctrl+\ + this.format(); + } + handled = true; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + /** + * Detach the editor from the DOM + * @private + */ + textmode._delete = function () { + if (this.frame && this.container && this.frame.parentNode == this.container) { + this.container.removeChild(this.frame); + } + }; + + /** + * Throw an error. If an error callback is configured in options.error, this + * callback will be invoked. Else, a regular error is thrown. + * @param {Error} err + * @private + */ + textmode._onError = function(err) { + // TODO: onError is deprecated since version 2.2.0. cleanup some day + if (typeof this.onError === 'function') { + util.log('WARNING: JSONEditor.onError is deprecated. ' + + 'Use options.error instead.'); + this.onError(err); + } + + if (this.options && typeof this.options.error === 'function') { + this.options.error(err); + } + else { + throw err; + } + }; + + /** + * Compact the code in the formatter + */ + textmode.compact = function () { + var json = this.get(); + var text = JSON.stringify(json); + this.setText(text); + }; + + /** + * Format the code in the formatter + */ + textmode.format = function () { + var json = this.get(); + var text = JSON.stringify(json, null, this.indentation); + this.setText(text); + }; + + /** + * Set focus to the formatter + */ + textmode.focus = function () { + if (this.textarea) { + this.textarea.focus(); + } + if (this.editor) { + this.editor.focus(); + } + }; + + /** + * Resize the formatter + */ + textmode.resize = function () { + if (this.editor) { + var force = false; + this.editor.resize(force); + } + }; + + /** + * Set json data in the formatter + * @param {Object} json + */ + textmode.set = function(json) { + this.setText(JSON.stringify(json, null, this.indentation)); + }; + + /** + * Get json data from the formatter + * @return {Object} json + */ + textmode.get = function() { + var text = this.getText(); + var json; + + try { + json = util.parse(text); // this can throw an error + } + catch (err) { + // try to sanitize json, replace JavaScript notation with JSON notation + text = util.sanitize(text); + this.setText(text); + + // try to parse again + json = util.parse(text); // this can throw an error + } + + return json; + }; + + /** + * Get the text contents of the editor + * @return {String} jsonText + */ + textmode.getText = function() { + if (this.textarea) { + return this.textarea.value; + } + if (this.editor) { + return this.editor.getValue(); + } + return ''; + }; + + /** + * Set the text contents of the editor + * @param {String} jsonText + */ + textmode.setText = function(jsonText) { + if (this.textarea) { + this.textarea.value = jsonText; + } + if (this.editor) { + this.editor.setValue(jsonText, -1); + } + }; + + // define modes + return [ + { + mode: 'text', + mixin: textmode, + data: 'text', + load: textmode.format + }, + { + mode: 'code', + mixin: textmode, + data: 'text', + load: textmode.format + } + ]; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { + + // create namespace + var util = {}; + + /** + * 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 + * @return {JSON} json + */ + util.parse = function parse(jsonString) { + try { + return JSON.parse(jsonString); + } + catch (err) { + // try to throw a more detailed error message using validate + util.validate(jsonString); + + // rethrow the original error + throw err; + } + }; + + /** + * Sanitize a JSON-like string containing. For example changes JavaScript + * notation into JSON notation. + * This function for example changes a string like "{a: 2, 'b': {c: 'd'}" + * into '{"a": 2, "b": {"c": "d"}' + * @param {string} jsString + * @returns {string} json + */ + util.sanitize = function (jsString) { + // escape all single and double quotes inside strings + var chars = []; + var inString = false; + var i = 0; + while(i < jsString.length) { + var c = jsString.charAt(i); + var isEscaped = jsString.charAt(i - 1) === '\\'; + + if ((c === '"' || c === '\'') && !isEscaped) { + if (c === inString) { + // end of string + inString = false; + } + else if (!inString) { + // start of string + inString = c; + } + else { + // add escape character + chars.push('\\'); + } + } + + chars.push(c); + i++; + } + var jsonString = chars.join(''); + + // replace unescaped single quotes with double quotes, + // and replace escaped single quotes with unescaped single quotes + // TODO: we could do this step immediately in the previous step + jsonString = jsonString.replace(/(.?)'/g, function ($0, $1) { + return ($1 == '\\') ? '\'' : $1 + '"'; + }); + + // enclose unquoted object keys with double quotes + jsonString = jsonString.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, function ($0, $1, $2, $3) { + return $1 + '"' + $2 + '"' + $3; + }); + + return jsonString; + }; + + /** + * 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 + * @throws Error + */ + util.validate = function validate(jsonString) { + if (typeof(jsonlint) != 'undefined') { + jsonlint.parse(jsonString); + } + else { + JSON.parse(jsonString); + } + }; + + /** + * Extend object a with the properties of object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + util.extend = function extend(a, b) { + for (var prop in b) { + if (b.hasOwnProperty(prop)) { + a[prop] = b[prop]; + } + } + return a; + }; + + /** + * Remove all properties from object a + * @param {Object} a + * @return {Object} a + */ + util.clear = function clear (a) { + for (var prop in a) { + if (a.hasOwnProperty(prop)) { + delete a[prop]; + } + } + return a; + }; + + /** + * Output text to the console, if console is available + * @param {...*} args + */ + util.log = function log (args) { + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + }; + + /** + * Get the type of an object + * @param {*} object + * @return {String} type + */ + util.type = function type (object) { + if (object === null) { + return 'null'; + } + if (object === undefined) { + return 'undefined'; + } + if ((object instanceof Number) || (typeof object === 'number')) { + return 'number'; + } + if ((object instanceof String) || (typeof object === 'string')) { + return 'string'; + } + if ((object instanceof Boolean) || (typeof object === 'boolean')) { + return 'boolean'; + } + if ((object instanceof RegExp) || (typeof object === 'regexp')) { + return 'regexp'; + } + if (util.isArray(object)) { + return 'array'; + } + + return 'object'; + }; + + /** + * Test whether a text contains a url (matches when a string starts + * with 'http://*' or 'https://*' and has no whitespace characters) + * @param {String} text + */ + var isUrlRegex = /^https?:\/\/\S+$/; + util.isUrl = function isUrl (text) { + return (typeof text == 'string' || text instanceof String) && + isUrlRegex.test(text); + }; + + /** + * Tes whether given object is an Array + * @param {*} obj + * @returns {boolean} returns true when obj is an array + */ + util.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + /** + * 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. + */ + util.getAbsoluteLeft = function getAbsoluteLeft(elem) { + var rect = elem.getBoundingClientRect(); + return rect.left + window.pageXOffset || document.scrollLeft || 0; + }; + + /** + * 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. + */ + util.getAbsoluteTop = function getAbsoluteTop(elem) { + var rect = elem.getBoundingClientRect(); + return rect.top + window.pageYOffset || document.scrollTop || 0; + }; + + /** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ + util.addClassName = function addClassName(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 + */ + util.removeClassName = function removeClassName(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 + */ + util.stripFormatting = function stripFormatting(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 + util.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 A content editable div + */ + util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) { + var range, selection; + if(document.createRange) { + 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 + } + }; + + /** + * Select all text of a content editable div. + * http://stackoverflow.com/a/3806004/1262753 + * @param {Element} contentEditableElement A content editable div + */ + util.selectContentEditable = function selectContentEditable(contentEditableElement) { + if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') { + return; + } + + var sel, range; + if (window.getSelection && document.createRange) { + range = document.createRange(); + range.selectNodeContents(contentEditableElement); + sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + }; + + /** + * Get text selection + * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore + * @return {Range | TextRange | null} range + */ + util.getSelection = function getSelection() { + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.getRangeAt && sel.rangeCount) { + return sel.getRangeAt(0); + } + } + return null; + }; + + /** + * Set text selection + * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore + * @param {Range | TextRange | null} range + */ + util.setSelection = function setSelection(range) { + if (range) { + if (window.getSelection) { + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + } + }; + + /** + * Get selected text range + * @return {Object} params object containing parameters: + * {Number} startOffset + * {Number} endOffset + * {Element} container HTML element holding the + * selected text element + * Returns null if no text selection is found + */ + util.getSelectionOffset = function getSelectionOffset() { + var range = util.getSelection(); + + if (range && 'startOffset' in range && 'endOffset' in range && + range.startContainer && (range.startContainer == range.endContainer)) { + return { + startOffset: range.startOffset, + endOffset: range.endOffset, + container: range.startContainer.parentNode + }; + } + + return null; + }; + + /** + * Set selected text range in given element + * @param {Object} params An object containing: + * {Element} container + * {Number} startOffset + * {Number} endOffset + */ + util.setSelectionOffset = function setSelectionOffset(params) { + if (document.createRange && window.getSelection) { + var selection = window.getSelection(); + if(selection) { + var range = document.createRange(); + // TODO: do not suppose that the first child of the container is a textnode, + // but recursively find the textnodes + range.setStart(params.container.firstChild, params.startOffset); + range.setEnd(params.container.firstChild, params.endOffset); + + util.setSelection(range); + } + } + }; + + /** + * Get the inner text of an HTML element (for example a div element) + * @param {Element} element + * @param {Object} [buffer] + * @return {String} innerText + */ + util.getInnerText = function getInnerText(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 += util.getInnerText(child, buffer); + buffer.set('\n'); + } + else if (child.nodeName == 'BR') { + innerText += buffer.flush(); + buffer.set('\n'); + } + else { + innerText += util.getInnerText(child, buffer); + } + } + + return innerText; + } + else { + if (element.nodeName == 'P' && util.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 + */ + util.getInternetExplorerVersion = function getInternetExplorerVersion() { + if (_ieVersion == -1) { + 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 ); + } + } + + _ieVersion = rv; + } + + return _ieVersion; + }; + + /** + * Test whether the current browser is Firefox + * @returns {boolean} isFirefox + */ + util.isFirefox = function isFirefox () { + return (navigator.userAgent.indexOf("Firefox") != -1); + }; + + /** + * cached internet explorer version + * @type {Number} + * @private + */ + var _ieVersion = -1; + + /** + * 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] false by default + * @return {function} the created event listener + */ + util.addEventListener = function addEventListener(element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && util.isFirefox()) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + return listener; + } else if (element.attachEvent) { + // Old 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] false by default + */ + util.removeEventListener = function removeEventListener(element, action, listener, useCapture) { + if (element.removeEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && util.isFirefox()) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else if (element.detachEvent) { + // Old IE browsers + element.detachEvent("on" + action, listener); + } + }; + + return util; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { + + /** + * The highlighter can highlight/unhighlight a node, and + * animate the visibility of a context menu. + * @constructor Highlighter + */ + function Highlighter () { + this.locked = false; + } + + /** + * Hightlight given node and its childs + * @param {Node} node + */ + Highlighter.prototype.highlight = function (node) { + if (this.locked) { + return; + } + + if (this.node != node) { + // unhighlight current node + if (this.node) { + this.node.setHighlight(false); + } + + // highlight new node + this.node = node; + this.node.setHighlight(true); + } + + // cancel any current timeout + this._cancelUnhighlight(); + }; + + /** + * Unhighlight currently highlighted node. + * Will be done after a delay + */ + Highlighter.prototype.unhighlight = function () { + if (this.locked) { + return; + } + + var me = this; + if (this.node) { + this._cancelUnhighlight(); + + // do the unhighlighting after a small delay, to prevent re-highlighting + // the same node when moving from the drag-icon to the contextmenu-icon + // or vice versa. + this.unhighlightTimer = setTimeout(function () { + me.node.setHighlight(false); + me.node = undefined; + me.unhighlightTimer = undefined; + }, 0); + } + }; + + /** + * Cancel an unhighlight action (if before the timeout of the unhighlight action) + * @private + */ + Highlighter.prototype._cancelUnhighlight = function () { + if (this.unhighlightTimer) { + clearTimeout(this.unhighlightTimer); + this.unhighlightTimer = undefined; + } + }; + + /** + * Lock highlighting or unhighlighting nodes. + * methods highlight and unhighlight do not work while locked. + */ + Highlighter.prototype.lock = function () { + this.locked = true; + }; + + /** + * Unlock highlighting or unhighlighting nodes + */ + Highlighter.prototype.unlock = function () { + this.locked = false; + }; + + return Highlighter; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (util) { + + /** + * @constructor History + * Store action history, enables undo and redo + * @param {JSONEditor} editor + */ + function History (editor) { + this.editor = editor; + this.clear(); + + // map with all supported actions + this.actions = { + 'editField': { + 'undo': function (params) { + params.node.updateField(params.oldValue); + }, + 'redo': function (params) { + params.node.updateField(params.newValue); + } + }, + 'editValue': { + 'undo': function (params) { + params.node.updateValue(params.oldValue); + }, + 'redo': function (params) { + params.node.updateValue(params.newValue); + } + }, + 'appendNode': { + 'undo': function (params) { + params.parent.removeChild(params.node); + }, + 'redo': function (params) { + params.parent.appendChild(params.node); + } + }, + 'insertBeforeNode': { + 'undo': function (params) { + params.parent.removeChild(params.node); + }, + 'redo': function (params) { + params.parent.insertBefore(params.node, params.beforeNode); + } + }, + 'insertAfterNode': { + 'undo': function (params) { + params.parent.removeChild(params.node); + }, + 'redo': function (params) { + params.parent.insertAfter(params.node, params.afterNode); + } + }, + 'removeNode': { + 'undo': function (params) { + var parent = params.parent; + var beforeNode = parent.childs[params.index] || parent.append; + parent.insertBefore(params.node, beforeNode); + }, + 'redo': function (params) { + params.parent.removeChild(params.node); + } + }, + 'duplicateNode': { + 'undo': function (params) { + params.parent.removeChild(params.clone); + }, + 'redo': function (params) { + params.parent.insertAfter(params.clone, params.node); + } + }, + 'changeType': { + 'undo': function (params) { + params.node.changeType(params.oldType); + }, + 'redo': function (params) { + params.node.changeType(params.newType); + } + }, + 'moveNode': { + 'undo': function (params) { + params.startParent.moveTo(params.node, params.startIndex); + }, + 'redo': function (params) { + params.endParent.moveTo(params.node, params.endIndex); + } + }, + 'sort': { + 'undo': function (params) { + var node = params.node; + node.hideChilds(); + node.sort = params.oldSort; + node.childs = params.oldChilds; + node.showChilds(); + }, + 'redo': function (params) { + var node = params.node; + node.hideChilds(); + node.sort = params.newSort; + node.childs = params.newChilds; + node.showChilds(); + } + } + + // 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. + */ + 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. + */ + 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 + */ + 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 + */ + History.prototype.canUndo = function () { + return (this.index >= 0); + }; + + /** + * Check if there is an action available for redo + * @return {Boolean} canRedo + */ + History.prototype.canRedo = function () { + return (this.index < this.history.length - 1); + }; + + /** + * Undo the last action + */ + 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.params); + if (obj.params.oldSelection) { + this.editor.setSelection(obj.params.oldSelection); + } + } + else { + util.log('Error: unknown action "' + obj.action + '"'); + } + } + this.index--; + + // fire onchange event + this.onChange(); + } + }; + + /** + * Redo the last action + */ + History.prototype.redo = function () { + if (this.canRedo()) { + this.index++; + + var obj = this.history[this.index]; + if (obj) { + var action = this.actions[obj.action]; + if (action && action.redo) { + action.redo(obj.params); + if (obj.params.newSelection) { + this.editor.setSelection(obj.params.newSelection); + } + } + else { + util.log('Error: unknown action "' + obj.action + '"'); + } + } + + // fire onchange event + this.onChange(); + } + }; + + return History; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { + + /** + * @constructor 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 + */ + function SearchBox (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 = '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'); + tr.appendChild(td); + var results = document.createElement('div'); + this.dom.results = results; + results.className = 'results'; + td.appendChild(results); + + td = document.createElement('td'); + tr.appendChild(td); + var divInput = document.createElement('div'); + this.dom.input = divInput; + divInput.className = 'frame'; + divInput.title = 'Search fields and values'; + td.appendChild(divInput); + + // table to contain the text input and search button + var tableInput = document.createElement('table'); + 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 = 'refresh'; + td = document.createElement('td'); + td.appendChild(refreshSearch); + tr.appendChild(td); + + var search = document.createElement('input'); + this.dom.search = search; + search.oninput = function (event) { + searchBox._onDelayedSearch(event); + }; + search.onchange = function (event) { // For IE 9 + 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 = 'Next result (Enter)'; + searchNext.className = 'next'; + searchNext.onclick = function () { + searchBox.next(); + }; + td = document.createElement('td'); + td.appendChild(searchNext); + tr.appendChild(td); + + var searchPrevious = document.createElement('button'); + searchPrevious.title = 'Previous result (Shift+Enter)'; + searchPrevious.className = 'previous'; + searchPrevious.onclick = function () { + searchBox.previous(); + }; + td = document.createElement('td'); + td.appendChild(searchPrevious); + tr.appendChild(td); + } + + /** + * Go to the next search result + * @param {boolean} [focus] If true, focus will be set to the next result + * focus is false by default. + */ + SearchBox.prototype.next = function(focus) { + if (this.results != undefined) { + var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0; + if (index > this.results.length - 1) { + index = 0; + } + this._setActiveResult(index, focus); + } + }; + + /** + * Go to the prevous search result + * @param {boolean} [focus] If true, focus will be set to the next result + * focus is false by default. + */ + SearchBox.prototype.previous = function(focus) { + 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, focus); + } + }; + + /** + * Set new value for the current active result + * @param {Number} index + * @param {boolean} [focus] If true, focus will be set to the next result. + * focus is false by default. + * @private + */ + SearchBox.prototype._setActiveResult = function(index, focus) { + // 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(); + + // TODO: not so nice that the focus is only set after the animation is finished + node.scrollTo(function () { + if (focus) { + node.focus(elem); + } + }); + }; + + /** + * Cancel any running onDelayedSearch. + * @private + */ + 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 + * @private + */ + 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. + * @private + */ + 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 = 'no results'; break; + case 1: this.dom.results.innerHTML = '1 result'; break; + default: this.dom.results.innerHTML = resultCount + ' results'; break; + } + } + else { + this.dom.results.innerHTML = ''; + } + } + }; + + /** + * Handle onKeyDown event in the input box + * @param {Event} event + * @private + */ + SearchBox.prototype._onKeyDown = function (event) { + var keynum = event.which; + if (keynum == 27) { // ESC + this.dom.search.value = ''; // clear search + this._onSearch(event); + event.preventDefault(); + event.stopPropagation(); + } + 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(); + } + event.preventDefault(); + event.stopPropagation(); + } + }; + + /** + * Handle onKeyUp event in the input box + * @param {Event} event + * @private + */ + SearchBox.prototype._onKeyUp = function (event) { + var keynum = event.keyCode; + if (keynum != 27 && keynum != 13) { // !show and !Enter + this._onDelayedSearch(event); // For IE 9 + } + }; + + return SearchBox; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + + + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(9), __webpack_require__(10), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (ContextMenu, appendNodeFactory, util) { + + /** + * @constructor Node + * Create a new Node + * @param {TreeEditor} editor + * @param {Object} [params] Can contain parameters: + * {string} field + * {boolean} fieldEditable + * {*} value + * {String} type Can have values 'auto', 'array', + * 'object', or 'string'. + */ + function Node (editor, params) { + /** @type {TreeEditor} */ + this.editor = editor; + this.dom = {}; + this.expanded = false; + + if(params && (params instanceof Object)) { + this.setField(params.field, params.fieldEditable); + this.setValue(params.value, params.type); + } + else { + this.setField(''); + this.setValue(null); + } + } + + /** + * Determine whether the field and/or value of this node are editable + * @private + */ + Node.prototype._updateEditability = function () { + this.editable = { + field: true, + value: true + }; + + if (this.editor) { + this.editable.field = this.editor.options.mode === 'tree'; + this.editable.value = this.editor.options.mode !== 'view'; + + if (this.editor.options.mode === 'tree' && (typeof this.editor.options.editable === 'function')) { + var editable = this.editor.options.editable({ + field: this.field, + value: this.value, + path: this.path() + }); + + if (typeof editable === 'boolean') { + this.editable.field = editable; + this.editable.value = editable; + } + else { + if (typeof editable.field === 'boolean') this.editable.field = editable.field; + if (typeof editable.value === 'boolean') this.editable.value = editable.value; + } + } + } + }; + + /** + * Get the path of this node + * @return {String[]} Array containing the path to this node + */ + Node.prototype.path = function () { + var node = this; + var path = []; + while (node) { + var field = node.field != undefined ? node.field : node.index; + if (field !== undefined) { + path.unshift(field); + } + node = node.parent; + } + return path; + }; + + /** + * Set parent node + * @param {Node} parent + */ + Node.prototype.setParent = function(parent) { + this.parent = parent; + }; + + /** + * Set field + * @param {String} field + * @param {boolean} [fieldEditable] + */ + Node.prototype.setField = function(field, fieldEditable) { + this.field = field; + this.fieldEditable = (fieldEditable == true); + }; + + /** + * Get field + * @return {String} + */ + 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 + * @param {String} [type] Specify the type of the value. Can be 'auto', + * 'array', 'object', or 'string' + */ + Node.prototype.setValue = function(value, type) { + 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); + + // check if type corresponds with the provided type + if (type && type != this.type) { + if (type == 'string' && this.type == 'auto') { + this.type = type; + } + else { + throw new Error('Type mismatch: ' + + 'cannot cast value of type "' + this.type + + ' to the specified type "' + type + '"'); + } + } + + 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 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 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); + util.log('check', value, this.value); + } + else { + this.value = value; + } + */ + } + }; + + /** + * Get value. Value is a JSON structure + * @return {*} value + */ + 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 + */ + 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 {Node} clone + */ + Node.prototype.clone = function() { + var clone = new 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 + */ + Node.prototype.expand = function(recurse) { + if (!this.childs) { + return; + } + + // set this node expanded + this.expanded = true; + if (this.dom.expand) { + this.dom.expand.className = 'expanded'; + } + + this.showChilds(); + + if (recurse != false) { + this.childs.forEach(function (child) { + child.expand(recurse); + }); + } + }; + + /** + * Collapse this node and optionally its childs. + * @param {boolean} [recurse] Optional recursion, true by default. When + * true, all childs will be collapsed recursively + */ + 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 = 'collapsed'; + } + this.expanded = false; + }; + + /** + * Recursively show all childs when they are expanded + */ + 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 + */ + 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 + */ + 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 {Node} node + */ + Node.prototype.appendChild = function(node) { + if (this._hasChilds()) { + // 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 {Node} node + * @param {Node} beforeNode + */ + Node.prototype.moveBefore = function(node, beforeNode) { + if (this._hasChilds()) { + // 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); + } + + if (node.parent) { + node.parent.removeChild(node); + } + + if (beforeNode instanceof 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 {Node} node + * @param {Number} index + */ + 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 {Node} node + * @param {Node} beforeNode + */ + Node.prototype.insertBefore = function(node, beforeNode) { + if (this._hasChilds()) { + 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('Node not found'); + } + + // 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}); + } + }; + + /** + * Insert a new child before a given node + * Only applicable when Node value is of type array or object + * @param {Node} node + * @param {Node} afterNode + */ + Node.prototype.insertAfter = function(node, afterNode) { + if (this._hasChilds()) { + var index = this.childs.indexOf(afterNode); + var beforeNode = this.childs[index + 1]; + if (beforeNode) { + this.insertBefore(node, beforeNode); + } + else { + this.appendChild(node); + } + } + }; + + /** + * 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 {Node[]} results Array with nodes containing the search text + */ + 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._hasChilds()) { + // 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 + * @param {function(boolean)} [callback] + */ + Node.prototype.scrollTo = function(callback) { + 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, callback); + } + }; + + + // stores the element name currently having the focus + Node.focusElement = undefined; + + /** + * Set focus to this node + * @param {String} [elementName] The field name of the element to get the + * focus available values: 'drag', 'menu', + * 'expand', 'field', 'value' (default) + */ + Node.prototype.focus = function(elementName) { + Node.focusElement = elementName; + + if (this.dom.tr && this.dom.tr.parentNode) { + var dom = this.dom; + + switch (elementName) { + case 'drag': + if (dom.drag) { + dom.drag.focus(); + } + else { + dom.menu.focus(); + } + break; + + case 'menu': + dom.menu.focus(); + break; + + case 'expand': + if (this._hasChilds()) { + dom.expand.focus(); + } + else if (dom.field && this.fieldEditable) { + dom.field.focus(); + util.selectContentEditable(dom.field); + } + else if (dom.value && !this._hasChilds()) { + dom.value.focus(); + util.selectContentEditable(dom.value); + } + else { + dom.menu.focus(); + } + break; + + case 'field': + if (dom.field && this.fieldEditable) { + dom.field.focus(); + util.selectContentEditable(dom.field); + } + else if (dom.value && !this._hasChilds()) { + dom.value.focus(); + util.selectContentEditable(dom.value); + } + else if (this._hasChilds()) { + dom.expand.focus(); + } + else { + dom.menu.focus(); + } + break; + + case 'value': + default: + if (dom.value && !this._hasChilds()) { + dom.value.focus(); + util.selectContentEditable(dom.value); + } + else if (dom.field && this.fieldEditable) { + dom.field.focus(); + util.selectContentEditable(dom.field); + } + else if (this._hasChilds()) { + dom.expand.focus(); + } + else { + dom.menu.focus(); + } + break; + } + } + }; + + /** + * Select all text in an editable div after a delay of 0 ms + * @param {Element} editableDiv + */ + Node.select = function(editableDiv) { + setTimeout(function () { + util.selectContentEditable(editableDiv); + }, 0); + }; + + /** + * Update the values from the DOM field and value of this node + */ + 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 {Node} node the childNode to be duplicated + * @return {Node} clone the clone of the node + * @private + */ + Node.prototype._duplicate = function(node) { + var clone = node.clone(); + + /* TODO: adjust the field name (to prevent equal field names) + if (this.type == 'object') { + } + */ + + this.insertAfter(clone, node); + + return clone; + }; + + /** + * Check if given node is a child. The method will check recursively to find + * this node. + * @param {Node} node + * @return {boolean} containsNode + */ + Node.prototype.containsNode = function(node) { + if (this == node) { + return true; + } + + var childs = this.childs; + if (childs) { + // TODO: 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 {Node} node the childNode to be moved + * @param {Node} beforeNode node will be inserted before given + * node. If no beforeNode is given, + * the node is appended at the end + * @private + */ + 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('Cannot move a field into a child of itself'); + } + + // 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 {Node} node The child node to be removed; + * @return {Node | undefined} node The removed node on success, + * else undefined + */ + 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 {Node} node + * @private + */ + Node.prototype._remove = function (node) { + this.removeChild(node); + }; + + /** + * Change the type of the value of this Node + * @param {String} newType + */ + Node.prototype.changeType = function (newType) { + var oldType = this.type; + + if (oldType == newType) { + // type is not changed + return; + } + + 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 = ''; + } + }); + + 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 + */ + Node.prototype._getDomValue = function(silent) { + if (this.dom.value && this.type != 'array' && this.type != 'object') { + this.valueInnerText = util.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, + 'oldSelection': this.editor.selection, + 'newSelection': this.editor.getSelection() + }); + } + } + 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 + */ + Node.prototype._updateDomValue = function () { + var domValue = this.dom.value; + if (domValue) { + // set text color depending on value type + // TODO: put colors in css + var v = this.value; + var t = (this.type == 'auto') ? util.type(v) : this.type; + var isUrl = (t == 'string' && util.isUrl(v)); + var color = ''; + if (isUrl && !this.editable.value) { // TODO: when to apply this? + color = ''; + } + else if (t == 'string') { + color = 'green'; + } + else if (t == 'number') { + color = 'red'; + } + else if (t == 'boolean') { + color = 'darkorange'; + } + else if (this._hasChilds()) { + color = ''; + } + else if (v === null) { + color = '#004ED0'; // blue + } + else { + // invalid value + color = 'black'; + } + domValue.style.color = color; + + // make background color light-gray when empty + var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object'); + if (isEmpty) { + util.addClassName(domValue, 'empty'); + } + else { + util.removeClassName(domValue, 'empty'); + } + + // underline url + if (isUrl) { + util.addClassName(domValue, 'url'); + } + else { + util.removeClassName(domValue, 'url'); + } + + // update title + if (t == 'array' || t == 'object') { + var count = this.childs ? this.childs.length : 0; + domValue.title = this.type + ' containing ' + count + ' items'; + } + else if (t == 'string' && util.isUrl(v)) { + if (this.editable.value) { + domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window'; + } + } + else { + domValue.title = ''; + } + + // highlight when there is a search result + if (this.searchValueActive) { + util.addClassName(domValue, 'highlight-active'); + } + else { + util.removeClassName(domValue, 'highlight-active'); + } + if (this.searchValue) { + util.addClassName(domValue, 'highlight'); + } + else { + util.removeClassName(domValue, 'highlight'); + } + + // strip formatting from the contents of the editable div + util.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 + */ + Node.prototype._updateDomField = function () { + var domField = this.dom.field; + if (domField) { + // make backgound color lightgray when empty + var isEmpty = (String(this.field) == '' && this.parent.type != 'array'); + if (isEmpty) { + util.addClassName(domField, 'empty'); + } + else { + util.removeClassName(domField, 'empty'); + } + + // highlight when there is a search result + if (this.searchFieldActive) { + util.addClassName(domField, 'highlight-active'); + } + else { + util.removeClassName(domField, 'highlight-active'); + } + if (this.searchField) { + util.addClassName(domField, 'highlight'); + } + else { + util.removeClassName(domField, 'highlight'); + } + + // strip formatting from the contents of the editable div + util.stripFormatting(domField); + } + }; + + /** + * Retrieve field from DOM + * @param {boolean} [silent] If true (default), no errors will be thrown in + * case of invalid data + * @private + */ + Node.prototype._getDomField = function(silent) { + if (this.dom.field && this.fieldEditable) { + this.fieldInnerText = util.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, + 'oldSelection': this.editor.selection, + 'newSelection': this.editor.getSelection() + }); + } + } + 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 + */ + Node.prototype.clearDom = function() { + // TODO: hide the node first? + //this.hide(); + // TODO: 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 + */ + Node.prototype.getDom = function() { + var dom = this.dom; + if (dom.tr) { + return dom.tr; + } + + this._updateEditability(); + + // create row + dom.tr = document.createElement('tr'); + dom.tr.node = this; + + if (this.editor.options.mode === 'tree') { // note: we take here the global setting + var tdDrag = document.createElement('td'); + if (this.editable.field) { + // create draggable area + if (this.parent) { + var domDrag = document.createElement('button'); + dom.drag = domDrag; + domDrag.className = 'dragarea'; + domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)'; + tdDrag.appendChild(domDrag); + } + } + dom.tr.appendChild(tdDrag); + + // create context menu + var tdMenu = document.createElement('td'); + var menu = document.createElement('button'); + dom.menu = menu; + menu.className = 'contextmenu'; + menu.title = 'Click to open the actions menu (Ctrl+M)'; + tdMenu.appendChild(dom.menu); + dom.tr.appendChild(tdMenu); + } + + // create tree and field + var tdField = document.createElement('td'); + dom.tr.appendChild(tdField); + dom.tree = this._createDomTree(); + tdField.appendChild(dom.tree); + + this.updateDom({'updateIndexes': true}); + + return dom.tr; + }; + + /** + * DragStart event, fired on mousedown on the dragarea at the left side of a Node + * @param {Event} event + * @private + */ + Node.prototype._onDragStart = function (event) { + var node = this; + if (!this.mousemove) { + this.mousemove = util.addEventListener(document, 'mousemove', + function (event) { + node._onDrag(event); + }); + } + + if (!this.mouseup) { + this.mouseup = util.addEventListener(document, 'mouseup', + function (event ) { + node._onDragEnd(event); + }); + } + + this.editor.highlighter.lock(); + this.drag = { + 'oldCursor': document.body.style.cursor, + 'startParent': this.parent, + 'startIndex': this.parent.childs.indexOf(this), + 'mouseX': event.pageX, + 'level': this.getLevel() + }; + document.body.style.cursor = 'move'; + + event.preventDefault(); + }; + + /** + * Drag event, fired when moving the mouse while dragging a Node + * @param {Event} event + * @private + */ + Node.prototype._onDrag = function (event) { + // TODO: this method has grown too large. Split it in a number of methods + var mouseY = event.pageY; + var mouseX = event.pageX; + + var trThis, trPrev, trNext, trFirst, trLast, trRoot; + var nodePrev, nodeNext; + var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext; + var moved = false; + + // TODO: add an ESC option, which resets to the original position + + // move up/down + trThis = this.dom.tr; + topThis = util.getAbsoluteTop(trThis); + heightThis = trThis.offsetHeight; + if (mouseY < topThis) { + // move up + trPrev = trThis; + do { + trPrev = trPrev.previousSibling; + nodePrev = Node.getNodeFromTarget(trPrev); + topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0; + } + while (trPrev && mouseY < topPrev); + + if (nodePrev && !nodePrev.parent) { + nodePrev = undefined; + } + + if (!nodePrev) { + // move to the first node + trRoot = trThis.parentNode.firstChild; + trPrev = trRoot ? trRoot.nextSibling : undefined; + nodePrev = Node.getNodeFromTarget(trPrev); + if (nodePrev == this) { + nodePrev = undefined; + } + } + + if (nodePrev) { + // check if mouseY is really inside the found node + trPrev = nodePrev.dom.tr; + topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0; + if (mouseY > topPrev + heightThis) { + nodePrev = undefined; + } + } + + if (nodePrev) { + nodePrev.parent.moveBefore(this, nodePrev); + moved = true; + } + } + else { + // move down + trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr; + trFirst = trLast ? trLast.nextSibling : undefined; + if (trFirst) { + topFirst = util.getAbsoluteTop(trFirst); + trNext = trFirst; + do { + nodeNext = Node.getNodeFromTarget(trNext); + if (trNext) { + bottomNext = trNext.nextSibling ? + util.getAbsoluteTop(trNext.nextSibling) : 0; + heightNext = trNext ? (bottomNext - topFirst) : 0; + + if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) { + // We are about to remove the last child of this parent, + // which will make the parents appendNode visible. + topThis += 24 - 1; + // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px. + } + } + + trNext = trNext.nextSibling; + } + while (trNext && mouseY > topThis + heightNext); + + if (nodeNext && nodeNext.parent) { + // calculate the desired level + var diffX = (mouseX - this.drag.mouseX); + var diffLevel = Math.round(diffX / 24 / 2); + var level = this.drag.level + diffLevel; // desired level + var levelNext = nodeNext.getLevel(); // level to be + + // find the best fitting level (move upwards over the append nodes) + trPrev = nodeNext.dom.tr.previousSibling; + while (levelNext < level && trPrev) { + nodePrev = Node.getNodeFromTarget(trPrev); + if (nodePrev == this || nodePrev._isChildOf(this)) { + // neglect itself and its childs + } + else if (nodePrev instanceof AppendNode) { + var childs = nodePrev.parent.childs; + if (childs.length > 1 || + (childs.length == 1 && childs[0] != this)) { + // non-visible append node of a list of childs + // consisting of not only this node (else the + // append node will change into a visible "empty" + // text when removing this node). + nodeNext = Node.getNodeFromTarget(trPrev); + levelNext = nodeNext.getLevel(); + } + else { + break; + } + } + else { + break; + } + + trPrev = trPrev.previousSibling; + } + + // move the node when its position is changed + if (trLast.nextSibling != nodeNext.dom.tr) { + nodeNext.parent.moveBefore(this, nodeNext); + moved = true; + } + } + } + } + + if (moved) { + // update the dragging parameters when moved + this.drag.mouseX = mouseX; + this.drag.level = this.getLevel(); + } + + // auto scroll when hovering around the top of the editor + this.editor.startAutoScroll(mouseY); + + event.preventDefault(); + }; + + /** + * Drag event, fired on mouseup after having dragged a node + * @param {Event} event + * @private + */ + Node.prototype._onDragEnd = function (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; + this.editor.highlighter.unlock(); + delete this.drag; + + if (this.mousemove) { + util.removeEventListener(document, 'mousemove', this.mousemove); + delete this.mousemove;} + if (this.mouseup) { + util.removeEventListener(document, 'mouseup', this.mouseup); + delete this.mouseup; + } + + // Stop any running auto scroll + this.editor.stopAutoScroll(); + + event.preventDefault(); + }; + + /** + * Test if this node is a child of an other node + * @param {Node} node + * @return {boolean} isChild + * @private + */ + Node.prototype._isChildOf = function (node) { + var n = this.parent; + while (n) { + if (n == node) { + return true; + } + n = n.parent; + } + + return false; + }; + + /** + * Create an editable field + * @return {Element} domField + * @private + */ + 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 + */ + Node.prototype.setHighlight = function (highlight) { + if (this.dom.tr) { + this.dom.tr.className = (highlight ? '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 + */ + Node.prototype.updateValue = function (value) { + this.value = value; + this.updateDom(); + }; + + /** + * Update the field of the node. + * @param {String} field + */ + 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. + */ + 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) { + // parent is an object + domField.contentEditable = this.editable.field; + domField.spellcheck = false; + domField.className = 'field'; + } + else { + // parent is an array this is the root node + domField.className = 'readonly'; + } + + var field; + if (this.index != undefined) { + field = this.index; + } + else if (this.field != undefined) { + field = this.field; + } + else if (this._hasChilds()) { + field = this.type; + } + else { + 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 + ']'; + } + else if (this.type == 'object') { + domValue.innerHTML = '{' + count + '}'; + } + else { + domValue.innerHTML = this._escapeHTML(this.value); + } + } + + // 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 + */ + 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 = ''; + } + } + }); + } + } + }; + + /** + * Create an editable value + * @private + */ + Node.prototype._createDomValue = function () { + var domValue; + + if (this.type == 'array') { + domValue = document.createElement('div'); + domValue.className = 'readonly'; + domValue.innerHTML = '[...]'; + } + else if (this.type == 'object') { + domValue = document.createElement('div'); + domValue.className = 'readonly'; + domValue.innerHTML = '{...}'; + } + else { + if (!this.editable.value && util.isUrl(this.value)) { + // create a link in case of read-only editor and value containing an url + domValue = document.createElement('a'); + domValue.className = 'value'; + domValue.href = this.value; + domValue.target = '_blank'; + domValue.innerHTML = this._escapeHTML(this.value); + } + else { + // create an editable or read-only div + domValue = document.createElement('div'); + domValue.contentEditable = this.editable.value; + domValue.spellcheck = false; + domValue.className = 'value'; + domValue.innerHTML = this._escapeHTML(this.value); + } + } + + return domValue; + }; + + /** + * Create an expand/collapse button + * @return {Element} expand + * @private + */ + Node.prototype._createDomExpandButton = function () { + // create expand button + var expand = document.createElement('button'); + if (this._hasChilds()) { + expand.className = this.expanded ? 'expanded' : 'collapsed'; + expand.title = + 'Click to expand/collapse this field (Ctrl+E). \n' + + 'Ctrl+Click to expand/collapse including all childs.'; + } + else { + expand.className = 'invisible'; + expand.title = ''; + } + + return expand; + }; + + + /** + * Create a DOM tree element, containing the expand/collapse button + * @return {Element} domTree + * @private + */ + Node.prototype._createDomTree = function () { + var dom = this.dom; + var domTree = document.createElement('table'); + var tbody = document.createElement('tbody'); + domTree.style.borderCollapse = 'collapse'; // TODO: put in css + domTree.className = 'values'; + domTree.appendChild(tbody); + var tr = document.createElement('tr'); + tbody.appendChild(tr); + + // create expand button + var tdExpand = document.createElement('td'); + tdExpand.className = 'tree'; + tr.appendChild(tdExpand); + dom.expand = this._createDomExpandButton(); + tdExpand.appendChild(dom.expand); + dom.tdExpand = tdExpand; + + // create the field + var tdField = document.createElement('td'); + tdField.className = 'tree'; + tr.appendChild(tdField); + dom.field = this._createDomField(); + tdField.appendChild(dom.field); + dom.tdField = tdField; + + // create a separator + var tdSeparator = document.createElement('td'); + tdSeparator.className = 'tree'; + tr.appendChild(tdSeparator); + if (this.type != 'object' && this.type != 'array') { + tdSeparator.appendChild(document.createTextNode(':')); + tdSeparator.className = 'separator'; + } + dom.tdSeparator = tdSeparator; + + // create the value + var tdValue = document.createElement('td'); + tdValue.className = 'tree'; + tr.appendChild(tdValue); + dom.value = this._createDomValue(); + tdValue.appendChild(dom.value); + dom.tdValue = tdValue; + + return domTree; + }; + + /** + * Handle an event. The event is catched centrally by the editor + * @param {Event} event + */ + Node.prototype.onEvent = function (event) { + var type = event.type, + target = event.target || event.srcElement, + dom = this.dom, + node = this, + focusNode, + expandable = this._hasChilds(); + + // check if mouse is on menu or on dragarea. + // If so, highlight current row and its childs + if (target == dom.drag || target == dom.menu) { + if (type == 'mouseover') { + this.editor.highlighter.highlight(this); + } + else if (type == 'mouseout') { + this.editor.highlighter.unhighlight(); + } + } + + // drag events + if (type == 'mousedown' && target == dom.drag) { + this._onDragStart(event); + } + + // context menu events + if (type == 'click' && target == dom.menu) { + var highlighter = node.editor.highlighter; + highlighter.highlight(node); + highlighter.lock(); + util.addClassName(dom.menu, 'selected'); + this.showContextMenu(dom.menu, function () { + util.removeClassName(dom.menu, 'selected'); + highlighter.unlock(); + highlighter.unhighlight(); + }); + } + + // expand events + if (type == 'click' && target == dom.expand) { + if (expandable) { + var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all + this._onExpand(recurse); + } + } + + // value events + var domValue = dom.value; + if (target == domValue) { + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'focus': + focusNode = this; + break; + + case 'blur': + case 'change': + this._getDomValue(true); + this._updateDomValue(); + if (this.value) { + domValue.innerHTML = this._escapeHTML(this.value); + } + break; + + case 'input': + this._getDomValue(true); + this._updateDomValue(); + break; + + case 'keydown': + case 'mousedown': + this.editor.selection = this.editor.getSelection(); + break; + + case 'click': + if (event.ctrlKey || !this.editable.value) { + if (util.isUrl(this.value)) { + window.open(this.value, '_blank'); + } + } + 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': + focusNode = this; + break; + + case 'blur': + case 'change': + this._getDomField(true); + this._updateDomField(); + if (this.field) { + domField.innerHTML = this._escapeHTML(this.field); + } + break; + + case 'input': + this._getDomField(true); + this._updateDomField(); + break; + + case 'keydown': + case 'mousedown': + this.editor.selection = this.editor.getSelection(); + break; + + case 'keyup': + this._getDomField(true); + this._updateDomField(); + break; + + case 'cut': + case 'paste': + setTimeout(function () { + node._getDomField(true); + node._updateDomField(); + }, 1); + 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.pageX < util.getAbsoluteLeft(dom.tdSeparator));// for FF + if (left || expandable) { + // node is expandable when it is an object or array + if (domField) { + util.setEndOfContentEditable(domField); + domField.focus(); + } + } + else { + if (domValue) { + util.setEndOfContentEditable(domValue); + domValue.focus(); + } + } + break; + } + } + if ((target == dom.tdExpand && !expandable) || target == dom.tdField || + target == dom.tdSeparator) { + switch (type) { + case 'click': + if (domField) { + util.setEndOfContentEditable(domField); + domField.focus(); + } + break; + } + } + + if (type == 'keydown') { + this.onKeyDown(event); + } + }; + + /** + * Key down event handler + * @param {Event} event + */ + Node.prototype.onKeyDown = function (event) { + var keynum = event.which || event.keyCode; + var target = event.target || event.srcElement; + var ctrlKey = event.ctrlKey; + var shiftKey = event.shiftKey; + var altKey = event.altKey; + var handled = false; + var prevNode, nextNode, nextDom, nextDom2; + var editable = this.editor.options.mode === 'tree'; + + // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup + if (keynum == 13) { // Enter + if (target == this.dom.value) { + if (!this.editable.value || event.ctrlKey) { + if (util.isUrl(this.value)) { + window.open(this.value, '_blank'); + handled = true; + } + } + } + else if (target == this.dom.expand) { + var expandable = this._hasChilds(); + if (expandable) { + var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all + this._onExpand(recurse); + target.focus(); + handled = true; + } + } + } + else if (keynum == 68) { // D + if (ctrlKey && editable) { // Ctrl+D + this._onDuplicate(); + handled = true; + } + } + else if (keynum == 69) { // E + if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E + this._onExpand(shiftKey); // recurse = shiftKey + target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline) + handled = true; + } + } + else if (keynum == 77 && editable) { // M + if (ctrlKey) { // Ctrl+M + this.showContextMenu(target); + handled = true; + } + } + else if (keynum == 46 && editable) { // Del + if (ctrlKey) { // Ctrl+Del + this._onRemove(); + handled = true; + } + } + else if (keynum == 45 && editable) { // Ins + if (ctrlKey && !shiftKey) { // Ctrl+Ins + this._onInsertBefore(); + handled = true; + } + else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins + this._onInsertAfter(); + handled = true; + } + } + else if (keynum == 35) { // End + if (altKey) { // Alt+End + // find the last node + var lastNode = this._lastNode(); + if (lastNode) { + lastNode.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + } + else if (keynum == 36) { // Home + if (altKey) { // Alt+Home + // find the first node + var firstNode = this._firstNode(); + if (firstNode) { + firstNode.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + } + else if (keynum == 37) { // Arrow Left + if (altKey && !shiftKey) { // Alt + Arrow Left + // move to left element + var prevElement = this._previousElement(target); + if (prevElement) { + this.focus(this._getElementName(prevElement)); + } + handled = true; + } + else if (altKey && shiftKey && editable) { // Alt + Shift Arrow left + if (this.expanded) { + var appendDom = this.getAppend(); + nextDom = appendDom ? appendDom.nextSibling : undefined; + } + else { + var dom = this.getDom(); + nextDom = dom.nextSibling; + } + if (nextDom) { + nextNode = Node.getNodeFromTarget(nextDom); + nextDom2 = nextDom.nextSibling; + nextNode2 = Node.getNodeFromTarget(nextDom2); + if (nextNode && nextNode instanceof AppendNode && + !(this.parent.childs.length == 1) && + nextNode2 && nextNode2.parent) { + nextNode2.parent.moveBefore(this, nextNode2); + this.focus(Node.focusElement || this._getElementName(target)); + } + } + } + } + else if (keynum == 38) { // Arrow Up + if (altKey && !shiftKey) { // Alt + Arrow Up + // find the previous node + prevNode = this._previousNode(); + if (prevNode) { + prevNode.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + else if (altKey && shiftKey) { // Alt + Shift + Arrow Up + // find the previous node + prevNode = this._previousNode(); + if (prevNode && prevNode.parent) { + prevNode.parent.moveBefore(this, prevNode); + this.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + } + else if (keynum == 39) { // Arrow Right + if (altKey && !shiftKey) { // Alt + Arrow Right + // move to right element + var nextElement = this._nextElement(target); + if (nextElement) { + this.focus(this._getElementName(nextElement)); + } + handled = true; + } + else if (altKey && shiftKey) { // Alt + Shift Arrow Right + dom = this.getDom(); + var prevDom = dom.previousSibling; + if (prevDom) { + prevNode = Node.getNodeFromTarget(prevDom); + if (prevNode && prevNode.parent && + (prevNode instanceof AppendNode) + && !prevNode.isVisible()) { + prevNode.parent.moveBefore(this, prevNode); + this.focus(Node.focusElement || this._getElementName(target)); + } + } + } + } + else if (keynum == 40) { // Arrow Down + if (altKey && !shiftKey) { // Alt + Arrow Down + // find the next node + nextNode = this._nextNode(); + if (nextNode) { + nextNode.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Down + // find the 2nd next node and move before that one + if (this.expanded) { + nextNode = this.append ? this.append._nextNode() : undefined; + } + else { + nextNode = this._nextNode(); + } + nextDom = nextNode ? nextNode.getDom() : undefined; + if (this.parent.childs.length == 1) { + nextDom2 = nextDom; + } + else { + nextDom2 = nextDom ? nextDom.nextSibling : undefined; + } + var nextNode2 = Node.getNodeFromTarget(nextDom2); + if (nextNode2 && nextNode2.parent) { + nextNode2.parent.moveBefore(this, nextNode2); + this.focus(Node.focusElement || this._getElementName(target)); + } + handled = true; + } + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + /** + * Handle the expand event, when clicked on the expand button + * @param {boolean} recurse If true, child nodes will be expanded too + * @private + */ + Node.prototype._onExpand = function (recurse) { + 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; + } + }; + + /** + * Remove this node + * @private + */ + Node.prototype._onRemove = function() { + this.editor.highlighter.unhighlight(); + var childs = this.parent.childs; + var index = childs.indexOf(this); + + // adjust the focus + var oldSelection = this.editor.getSelection(); + if (childs[index + 1]) { + childs[index + 1].focus(); + } + else if (childs[index - 1]) { + childs[index - 1].focus(); + } + else { + this.parent.focus(); + } + var newSelection = this.editor.getSelection(); + + // remove the node + this.parent._remove(this); + + // store history action + this.editor._onAction('removeNode', { + node: this, + parent: this.parent, + index: index, + oldSelection: oldSelection, + newSelection: newSelection + }); + }; + + /** + * Duplicate this node + * @private + */ + Node.prototype._onDuplicate = function() { + var oldSelection = this.editor.getSelection(); + var clone = this.parent._duplicate(this); + clone.focus(); + var newSelection = this.editor.getSelection(); + + this.editor._onAction('duplicateNode', { + node: this, + clone: clone, + parent: this.parent, + oldSelection: oldSelection, + newSelection: newSelection + }); + }; + + /** + * Handle insert before event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ + Node.prototype._onInsertBefore = function (field, value, type) { + var oldSelection = this.editor.getSelection(); + + var newNode = new Node(this.editor, { + field: (field != undefined) ? field : '', + value: (value != undefined) ? value : '', + type: type + }); + newNode.expand(true); + this.parent.insertBefore(newNode, this); + this.editor.highlighter.unhighlight(); + newNode.focus('field'); + var newSelection = this.editor.getSelection(); + + this.editor._onAction('insertBeforeNode', { + node: newNode, + beforeNode: this, + parent: this.parent, + oldSelection: oldSelection, + newSelection: newSelection + }); + }; + + /** + * Handle insert after event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ + Node.prototype._onInsertAfter = function (field, value, type) { + var oldSelection = this.editor.getSelection(); + + var newNode = new Node(this.editor, { + field: (field != undefined) ? field : '', + value: (value != undefined) ? value : '', + type: type + }); + newNode.expand(true); + this.parent.insertAfter(newNode, this); + this.editor.highlighter.unhighlight(); + newNode.focus('field'); + var newSelection = this.editor.getSelection(); + + this.editor._onAction('insertAfterNode', { + node: newNode, + afterNode: this, + parent: this.parent, + oldSelection: oldSelection, + newSelection: newSelection + }); + }; + + /** + * Handle append event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ + Node.prototype._onAppend = function (field, value, type) { + var oldSelection = this.editor.getSelection(); + + var newNode = new Node(this.editor, { + field: (field != undefined) ? field : '', + value: (value != undefined) ? value : '', + type: type + }); + newNode.expand(true); + this.parent.appendChild(newNode); + this.editor.highlighter.unhighlight(); + newNode.focus('field'); + var newSelection = this.editor.getSelection(); + + this.editor._onAction('appendNode', { + node: newNode, + parent: this.parent, + oldSelection: oldSelection, + newSelection: newSelection + }); + }; + + /** + * Change the type of the node's value + * @param {String} newType + * @private + */ + Node.prototype._onChangeType = function (newType) { + var oldType = this.type; + if (newType != oldType) { + var oldSelection = this.editor.getSelection(); + this.changeType(newType); + var newSelection = this.editor.getSelection(); + + this.editor._onAction('changeType', { + node: this, + oldType: oldType, + newType: newType, + oldSelection: oldSelection, + newSelection: newSelection + }); + } + }; + + /** + * Sort the childs of the node. Only applicable when the node has type 'object' + * or 'array'. + * @param {String} direction Sorting direction. Available values: "asc", "desc" + * @private + */ + Node.prototype._onSort = function (direction) { + if (this._hasChilds()) { + var order = (direction == 'desc') ? -1 : 1; + var prop = (this.type == 'array') ? 'value': 'field'; + this.hideChilds(); + + var oldChilds = this.childs; + var oldSort = this.sort; + + // copy the array (the old one will be kept for an undo action + this.childs = this.childs.concat(); + + // sort the arrays + this.childs.sort(function (a, b) { + if (a[prop] > b[prop]) return order; + if (a[prop] < b[prop]) return -order; + return 0; + }); + this.sort = (order == 1) ? 'asc' : 'desc'; + + this.editor._onAction('sort', { + node: this, + oldChilds: oldChilds, + oldSort: oldSort, + newChilds: this.childs, + newSort: this.sort + }); + + this.showChilds(); + } + }; + + /** + * Create a table row with an append button. + * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable + */ + Node.prototype.getAppend = function () { + if (!this.append) { + this.append = new AppendNode(this.editor); + this.append.setParent(this); + } + return this.append.getDom(); + }; + + /** + * Find the node from an event target + * @param {Node} target + * @return {Node | undefined} node or undefined when not found + * @static + */ + Node.getNodeFromTarget = function (target) { + while (target) { + if (target.node) { + return target.node; + } + target = target.parentNode; + } + + return undefined; + }; + + /** + * Get the previously rendered node + * @return {Node | null} previousNode + * @private + */ + Node.prototype._previousNode = function () { + var prevNode = null; + var dom = this.getDom(); + if (dom && dom.parentNode) { + // find the previous field + var prevDom = dom; + do { + prevDom = prevDom.previousSibling; + prevNode = Node.getNodeFromTarget(prevDom); + } + while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible())); + } + return prevNode; + }; + + /** + * Get the next rendered node + * @return {Node | null} nextNode + * @private + */ + Node.prototype._nextNode = function () { + var nextNode = null; + var dom = this.getDom(); + if (dom && dom.parentNode) { + // find the previous field + var nextDom = dom; + do { + nextDom = nextDom.nextSibling; + nextNode = Node.getNodeFromTarget(nextDom); + } + while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible())); + } + + return nextNode; + }; + + /** + * Get the first rendered node + * @return {Node | null} firstNode + * @private + */ + Node.prototype._firstNode = function () { + var firstNode = null; + var dom = this.getDom(); + if (dom && dom.parentNode) { + var firstDom = dom.parentNode.firstChild; + firstNode = Node.getNodeFromTarget(firstDom); + } + + return firstNode; + }; + + /** + * Get the last rendered node + * @return {Node | null} lastNode + * @private + */ + Node.prototype._lastNode = function () { + var lastNode = null; + var dom = this.getDom(); + if (dom && dom.parentNode) { + var lastDom = dom.parentNode.lastChild; + lastNode = Node.getNodeFromTarget(lastDom); + while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) { + lastDom = lastDom.previousSibling; + lastNode = Node.getNodeFromTarget(lastDom); + } + } + return lastNode; + }; + + /** + * Get the next element which can have focus. + * @param {Element} elem + * @return {Element | null} nextElem + * @private + */ + Node.prototype._previousElement = function (elem) { + var dom = this.dom; + // noinspection FallthroughInSwitchStatementJS + switch (elem) { + case dom.value: + if (this.fieldEditable) { + return dom.field; + } + // intentional fall through + case dom.field: + if (this._hasChilds()) { + return dom.expand; + } + // intentional fall through + case dom.expand: + return dom.menu; + case dom.menu: + if (dom.drag) { + return dom.drag; + } + // intentional fall through + default: + return null; + } + }; + + /** + * Get the next element which can have focus. + * @param {Element} elem + * @return {Element | null} nextElem + * @private + */ + Node.prototype._nextElement = function (elem) { + var dom = this.dom; + // noinspection FallthroughInSwitchStatementJS + switch (elem) { + case dom.drag: + return dom.menu; + case dom.menu: + if (this._hasChilds()) { + return dom.expand; + } + // intentional fall through + case dom.expand: + if (this.fieldEditable) { + return dom.field; + } + // intentional fall through + case dom.field: + if (!this._hasChilds()) { + return dom.value; + } + default: + return null; + } + }; + + /** + * Get the dom name of given element. returns null if not found. + * For example when element == dom.field, "field" is returned. + * @param {Element} element + * @return {String | null} elementName Available elements with name: 'drag', + * 'menu', 'expand', 'field', 'value' + * @private + */ + Node.prototype._getElementName = function (element) { + var dom = this.dom; + for (var name in dom) { + if (dom.hasOwnProperty(name)) { + if (dom[name] == element) { + return name; + } + } + } + return null; + }; + + /** + * Test if this node has childs. This is the case when the node is an object + * or array. + * @return {boolean} hasChilds + * @private + */ + Node.prototype._hasChilds = function () { + return this.type == 'array' || this.type == 'object'; + }; + + // titles with explanation for the different types + Node.TYPE_TITLES = { + 'auto': 'Field type "auto". ' + + 'The field type is automatically determined from the value ' + + 'and can be a string, number, boolean, or null.', + 'object': 'Field type "object". ' + + 'An object contains an unordered set of key/value pairs.', + 'array': 'Field type "array". ' + + 'An array contains an ordered collection of values.', + 'string': 'Field type "string". ' + + 'Field type is not determined from the value, ' + + 'but always returned as string.' + }; + + /** + * Show a contextmenu for this node + * @param {HTMLElement} anchor Anchor element to attache the context menu to. + * @param {function} [onClose] Callback method called when the context menu + * is being closed. + */ + Node.prototype.showContextMenu = function (anchor, onClose) { + var node = this; + var titles = Node.TYPE_TITLES; + var items = []; + + if (this.editable.value) { + items.push({ + text: 'Type', + title: 'Change the type of this field', + className: 'type-' + this.type, + submenu: [ + { + text: 'Auto', + className: 'type-auto' + + (this.type == 'auto' ? ' selected' : ''), + title: titles.auto, + click: function () { + node._onChangeType('auto'); + } + }, + { + text: 'Array', + className: 'type-array' + + (this.type == 'array' ? ' selected' : ''), + title: titles.array, + click: function () { + node._onChangeType('array'); + } + }, + { + text: 'Object', + className: 'type-object' + + (this.type == 'object' ? ' selected' : ''), + title: titles.object, + click: function () { + node._onChangeType('object'); + } + }, + { + text: 'String', + className: 'type-string' + + (this.type == 'string' ? ' selected' : ''), + title: titles.string, + click: function () { + node._onChangeType('string'); + } + } + ] + }); + } + + if (this._hasChilds()) { + var direction = ((this.sort == 'asc') ? 'desc': 'asc'); + items.push({ + text: 'Sort', + title: 'Sort the childs of this ' + this.type, + className: 'sort-' + direction, + click: function () { + node._onSort(direction); + }, + submenu: [ + { + text: 'Ascending', + className: 'sort-asc', + title: 'Sort the childs of this ' + this.type + ' in ascending order', + click: function () { + node._onSort('asc'); + } + }, + { + text: 'Descending', + className: 'sort-desc', + title: 'Sort the childs of this ' + this.type +' in descending order', + click: function () { + node._onSort('desc'); + } + } + ] + }); + } + + if (this.parent && this.parent._hasChilds()) { + if (items.length) { + // create a separator + items.push({ + 'type': 'separator' + }); + } + + // create append button (for last child node only) + var childs = node.parent.childs; + if (node == childs[childs.length - 1]) { + items.push({ + text: 'Append', + title: 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)', + submenuTitle: 'Select the type of the field to be appended', + className: 'append', + click: function () { + node._onAppend('', '', 'auto'); + }, + submenu: [ + { + text: 'Auto', + className: 'type-auto', + title: titles.auto, + click: function () { + node._onAppend('', '', 'auto'); + } + }, + { + text: 'Array', + className: 'type-array', + title: titles.array, + click: function () { + node._onAppend('', []); + } + }, + { + text: 'Object', + className: 'type-object', + title: titles.object, + click: function () { + node._onAppend('', {}); + } + }, + { + text: 'String', + className: 'type-string', + title: titles.string, + click: function () { + node._onAppend('', '', 'string'); + } + } + ] + }); + } + + // create insert button + items.push({ + text: 'Insert', + title: 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)', + submenuTitle: 'Select the type of the field to be inserted', + className: 'insert', + click: function () { + node._onInsertBefore('', '', 'auto'); + }, + submenu: [ + { + text: 'Auto', + className: 'type-auto', + title: titles.auto, + click: function () { + node._onInsertBefore('', '', 'auto'); + } + }, + { + text: 'Array', + className: 'type-array', + title: titles.array, + click: function () { + node._onInsertBefore('', []); + } + }, + { + text: 'Object', + className: 'type-object', + title: titles.object, + click: function () { + node._onInsertBefore('', {}); + } + }, + { + text: 'String', + className: 'type-string', + title: titles.string, + click: function () { + node._onInsertBefore('', '', 'string'); + } + } + ] + }); + + if (this.editable.field) { + // create duplicate button + items.push({ + text: 'Duplicate', + title: 'Duplicate this field (Ctrl+D)', + className: 'duplicate', + click: function () { + node._onDuplicate(); + } + }); + + // create remove button + items.push({ + text: 'Remove', + title: 'Remove this field (Ctrl+Del)', + className: 'remove', + click: function () { + node._onRemove(); + } + }); + } + } + + var menu = new ContextMenu(items, {close: onClose}); + menu.show(anchor); + }; + + /** + * get the type of a value + * @param {*} value + * @return {String} type Can be 'object', 'array', 'string', 'auto' + * @private + */ + 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 + */ + 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 + */ + 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 + */ + Node.prototype._unescapeHTML = function (escapedText) { + var json = '"' + this._escapeJSON(escapedText) + '"'; + var htmlEscaped = util.parse(json); + return htmlEscaped + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/ |\u00A0/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 + */ + 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; + }; + + // TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode + var AppendNode = appendNodeFactory(Node); + + return Node; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(9)], __WEBPACK_AMD_DEFINE_RESULT__ = function (ContextMenu) { + + /** + * Create a select box to be used in the editor menu's, which allows to switch mode + * @param {Object} editor + * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view' + * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view' + * @returns {HTMLElement} box + */ + function createModeSwitcher(editor, modes, current) { + // TODO: decouple mode switcher from editor + + /** + * Switch the mode of the editor + * @param {String} mode + */ + function switchMode(mode) { + // switch mode + editor.setMode(mode); + + // restore focus on mode box + var modeBox = editor.dom && editor.dom.modeBox; + if (modeBox) { + modeBox.focus(); + } + } + + // available modes + var availableModes = { + code: { + 'text': 'Code', + 'title': 'Switch to code highlighter', + 'click': function () { + switchMode('code') + } + }, + form: { + 'text': 'Form', + 'title': 'Switch to form editor', + 'click': function () { + switchMode('form'); + } + }, + text: { + 'text': 'Text', + 'title': 'Switch to plain text editor', + 'click': function () { + switchMode('text'); + } + }, + tree: { + 'text': 'Tree', + 'title': 'Switch to tree editor', + 'click': function () { + switchMode('tree'); + } + }, + view: { + 'text': 'View', + 'title': 'Switch to tree view', + 'click': function () { + switchMode('view'); + } + } + }; + + // list the selected modes + var items = []; + for (var i = 0; i < modes.length; i++) { + var mode = modes[i]; + var item = availableModes[mode]; + if (!item) { + throw new Error('Unknown mode "' + mode + '"'); + } + + item.className = 'type-modes' + ((current == mode) ? ' selected' : ''); + items.push(item); + } + + // retrieve the title of current mode + var currentMode = availableModes[current]; + if (!currentMode) { + throw new Error('Unknown mode "' + current + '"'); + } + var currentTitle = currentMode.text; + + // create the html element + var box = document.createElement('button'); + box.className = 'modes separator'; + box.innerHTML = currentTitle + ' ▾'; + box.title = 'Switch editor mode'; + box.onclick = function () { + var menu = new ContextMenu(items); + menu.show(box); + }; + + return box; + } + + return { + create: createModeSwitcher + } + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (util) { + + /** + * A context menu + * @param {Object[]} items Array containing the menu structure + * TODO: describe structure + * @param {Object} [options] Object with options. Available options: + * {function} close Callback called when the + * context menu is being closed. + * @constructor + */ + function ContextMenu (items, options) { + this.dom = {}; + + var me = this; + var dom = this.dom; + this.anchor = undefined; + this.items = items; + this.eventListeners = {}; + this.selection = undefined; // holds the selection before the menu was opened + this.visibleSubmenu = undefined; + this.onClose = options ? options.close : undefined; + + // create a container element + var menu = document.createElement('div'); + menu.className = 'jsoneditor-contextmenu'; + dom.menu = menu; + + // create a list to hold the menu items + var list = document.createElement('ul'); + list.className = 'menu'; + menu.appendChild(list); + dom.list = list; + dom.items = []; // list with all buttons + + // create a (non-visible) button to set the focus to the menu + var focusButton = document.createElement('button'); + dom.focusButton = focusButton; + var li = document.createElement('li'); + li.style.overflow = 'hidden'; + li.style.height = '0'; + li.appendChild(focusButton); + list.appendChild(li); + + function createMenuItems (list, domItems, items) { + items.forEach(function (item) { + if (item.type == 'separator') { + // create a separator + var separator = document.createElement('div'); + separator.className = 'separator'; + li = document.createElement('li'); + li.appendChild(separator); + list.appendChild(li); + } + else { + var domItem = {}; + + // create a menu item + var li = document.createElement('li'); + list.appendChild(li); + + // create a button in the menu item + var button = document.createElement('button'); + button.className = item.className; + domItem.button = button; + if (item.title) { + button.title = item.title; + } + if (item.click) { + button.onclick = function () { + me.hide(); + item.click(); + }; + } + li.appendChild(button); + + // create the contents of the button + if (item.submenu) { + // add the icon to the button + var divIcon = document.createElement('div'); + divIcon.className = 'icon'; + button.appendChild(divIcon); + button.appendChild(document.createTextNode(item.text)); + + var buttonSubmenu; + if (item.click) { + // submenu and a button with a click handler + button.className += ' default'; + + var buttonExpand = document.createElement('button'); + domItem.buttonExpand = buttonExpand; + buttonExpand.className = 'expand'; + buttonExpand.innerHTML = '

'; + li.appendChild(buttonExpand); + if (item.submenuTitle) { + buttonExpand.title = item.submenuTitle; + } + + buttonSubmenu = buttonExpand; + } + else { + // submenu and a button without a click handler + var divExpand = document.createElement('div'); + divExpand.className = 'expand'; + button.appendChild(divExpand); + + buttonSubmenu = button; + } + + // attach a handler to expand/collapse the submenu + buttonSubmenu.onclick = function () { + me._onExpandItem(domItem); + buttonSubmenu.focus(); + }; + + // create the submenu + var domSubItems = []; + domItem.subItems = domSubItems; + var ul = document.createElement('ul'); + domItem.ul = ul; + ul.className = 'menu'; + ul.style.height = '0'; + li.appendChild(ul); + createMenuItems(ul, domSubItems, item.submenu); + } + else { + // no submenu, just a button with clickhandler + button.innerHTML = '
' + item.text; + } + + domItems.push(domItem); + } + }); + } + createMenuItems(list, this.dom.items, items); + + // TODO: when the editor is small, show the submenu on the right instead of inline? + + // calculate the max height of the menu with one submenu expanded + this.maxHeight = 0; // height in pixels + items.forEach(function (item) { + var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24; + me.maxHeight = Math.max(me.maxHeight, height); + }); + } + + /** + * Get the currently visible buttons + * @return {Array.} buttons + * @private + */ + ContextMenu.prototype._getVisibleButtons = function () { + var buttons = []; + var me = this; + this.dom.items.forEach(function (item) { + buttons.push(item.button); + if (item.buttonExpand) { + buttons.push(item.buttonExpand); + } + if (item.subItems && item == me.expandedItem) { + item.subItems.forEach(function (subItem) { + buttons.push(subItem.button); + if (subItem.buttonExpand) { + buttons.push(subItem.buttonExpand); + } + // TODO: change to fully recursive method + }); + } + }); + + return buttons; + }; + + // currently displayed context menu, a singleton. We may only have one visible context menu + ContextMenu.visibleMenu = undefined; + + /** + * Attach the menu to an anchor + * @param {HTMLElement} anchor + */ + ContextMenu.prototype.show = function (anchor) { + this.hide(); + + // calculate whether the menu fits below the anchor + var windowHeight = window.innerHeight, + windowScroll = (window.pageYOffset || document.scrollTop || 0), + windowBottom = windowHeight + windowScroll, + anchorHeight = anchor.offsetHeight, + menuHeight = this.maxHeight; + + // position the menu + var left = util.getAbsoluteLeft(anchor); + var top = util.getAbsoluteTop(anchor); + if (top + anchorHeight + menuHeight < windowBottom) { + // display the menu below the anchor + this.dom.menu.style.left = left + 'px'; + this.dom.menu.style.top = (top + anchorHeight) + 'px'; + this.dom.menu.style.bottom = ''; + } + else { + // display the menu above the anchor + this.dom.menu.style.left = left + 'px'; + this.dom.menu.style.top = ''; + this.dom.menu.style.bottom = (windowHeight - top) + 'px'; + } + + // attach the menu to the document + document.body.appendChild(this.dom.menu); + + // create and attach event listeners + var me = this; + var list = this.dom.list; + this.eventListeners.mousedown = util.addEventListener( + document, 'mousedown', function (event) { + // hide menu on click outside of the menu + var target = event.target; + if ((target != list) && !me._isChildOf(target, list)) { + me.hide(); + event.stopPropagation(); + event.preventDefault(); + } + }); + this.eventListeners.mousewheel = util.addEventListener( + document, 'mousewheel', function (event) { + // block scrolling when context menu is visible + event.stopPropagation(); + event.preventDefault(); + }); + this.eventListeners.keydown = util.addEventListener( + document, 'keydown', function (event) { + me._onKeyDown(event); + }); + + // move focus to the first button in the context menu + this.selection = util.getSelection(); + this.anchor = anchor; + setTimeout(function () { + me.dom.focusButton.focus(); + }, 0); + + if (ContextMenu.visibleMenu) { + ContextMenu.visibleMenu.hide(); + } + ContextMenu.visibleMenu = this; + }; + + /** + * Hide the context menu if visible + */ + ContextMenu.prototype.hide = function () { + // remove the menu from the DOM + if (this.dom.menu.parentNode) { + this.dom.menu.parentNode.removeChild(this.dom.menu); + if (this.onClose) { + this.onClose(); + } + } + + // remove all event listeners + // all event listeners are supposed to be attached to document. + for (var name in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(name)) { + var fn = this.eventListeners[name]; + if (fn) { + util.removeEventListener(document, name, fn); + } + delete this.eventListeners[name]; + } + } + + if (ContextMenu.visibleMenu == this) { + ContextMenu.visibleMenu = undefined; + } + }; + + /** + * Expand a submenu + * Any currently expanded submenu will be hided. + * @param {Object} domItem + * @private + */ + ContextMenu.prototype._onExpandItem = function (domItem) { + var me = this; + var alreadyVisible = (domItem == this.expandedItem); + + // hide the currently visible submenu + var expandedItem = this.expandedItem; + if (expandedItem) { + //var ul = expandedItem.ul; + expandedItem.ul.style.height = '0'; + expandedItem.ul.style.padding = ''; + setTimeout(function () { + if (me.expandedItem != expandedItem) { + expandedItem.ul.style.display = ''; + util.removeClassName(expandedItem.ul.parentNode, 'selected'); + } + }, 300); // timeout duration must match the css transition duration + this.expandedItem = undefined; + } + + if (!alreadyVisible) { + var ul = domItem.ul; + ul.style.display = 'block'; + var height = ul.clientHeight; // force a reflow in Firefox + setTimeout(function () { + if (me.expandedItem == domItem) { + ul.style.height = (ul.childNodes.length * 24) + 'px'; + ul.style.padding = '5px 10px'; + } + }, 0); + util.addClassName(ul.parentNode, 'selected'); + this.expandedItem = domItem; + } + }; + + /** + * Handle onkeydown event + * @param {Event} event + * @private + */ + ContextMenu.prototype._onKeyDown = function (event) { + var target = event.target; + var keynum = event.which; + var handled = false; + var buttons, targetIndex, prevButton, nextButton; + + if (keynum == 27) { // ESC + // hide the menu on ESC key + + // restore previous selection and focus + if (this.selection) { + util.setSelection(this.selection); + } + if (this.anchor) { + this.anchor.focus(); + } + + this.hide(); + + handled = true; + } + else if (keynum == 9) { // Tab + if (!event.shiftKey) { // Tab + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + if (targetIndex == buttons.length - 1) { + // move to first button + buttons[0].focus(); + handled = true; + } + } + else { // Shift+Tab + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + if (targetIndex == 0) { + // move to last button + buttons[buttons.length - 1].focus(); + handled = true; + } + } + } + else if (keynum == 37) { // Arrow Left + if (target.className == 'expand') { + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + prevButton = buttons[targetIndex - 1]; + if (prevButton) { + prevButton.focus(); + } + } + handled = true; + } + else if (keynum == 38) { // Arrow Up + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + prevButton = buttons[targetIndex - 1]; + if (prevButton && prevButton.className == 'expand') { + // skip expand button + prevButton = buttons[targetIndex - 2]; + } + if (!prevButton) { + // move to last button + prevButton = buttons[buttons.length - 1]; + } + if (prevButton) { + prevButton.focus(); + } + handled = true; + } + else if (keynum == 39) { // Arrow Right + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + nextButton = buttons[targetIndex + 1]; + if (nextButton && nextButton.className == 'expand') { + nextButton.focus(); + } + handled = true; + } + else if (keynum == 40) { // Arrow Down + buttons = this._getVisibleButtons(); + targetIndex = buttons.indexOf(target); + nextButton = buttons[targetIndex + 1]; + if (nextButton && nextButton.className == 'expand') { + // skip expand button + nextButton = buttons[targetIndex + 2]; + } + if (!nextButton) { + // move to first button + nextButton = buttons[0]; + } + if (nextButton) { + nextButton.focus(); + handled = true; + } + handled = true; + } + // TODO: arrow left and right + + if (handled) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + /** + * Test if an element is a child of a parent element. + * @param {Element} child + * @param {Element} parent + * @return {boolean} isChild + */ + ContextMenu.prototype._isChildOf = function (child, parent) { + var e = child.parentNode; + while (e) { + if (e == parent) { + return true; + } + e = e.parentNode; + } + + return false; + }; + + return ContextMenu; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(9), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = function (ContextMenu, util) { + + /** + * A factory function to create an AppendNode, which depends on a Node + * @param {Node} Node + */ + function appendNodeFactory(Node) { + /** + * @constructor AppendNode + * @extends Node + * @param {TreeEditor} 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 + */ + function AppendNode (editor) { + /** @type {TreeEditor} */ + this.editor = editor; + this.dom = {}; + } + + AppendNode.prototype = new Node(); + + /** + * Return a table row with an append button. + * @return {Element} dom TR element + */ + AppendNode.prototype.getDom = function () { + // TODO: implement a new solution for the append node + var dom = this.dom; + + if (dom.tr) { + return dom.tr; + } + + this._updateEditability(); + + // a row for the append button + var trAppend = document.createElement('tr'); + trAppend.node = this; + dom.tr = trAppend; + + // TODO: consistent naming + + if (this.editable.field) { + // a cell for the dragarea column + dom.tdDrag = document.createElement('td'); + + // create context menu + var tdMenu = document.createElement('td'); + dom.tdMenu = tdMenu; + var menu = document.createElement('button'); + menu.className = 'contextmenu'; + menu.title = 'Click to open the actions menu (Ctrl+M)'; + dom.menu = menu; + tdMenu.appendChild(dom.menu); + } + + // a cell for the contents (showing text 'empty') + var tdAppend = document.createElement('td'); + var domText = document.createElement('div'); + domText.innerHTML = '(empty)'; + domText.className = 'readonly'; + tdAppend.appendChild(domText); + dom.td = tdAppend; + dom.text = domText; + + this.updateDom(); + + return trAppend; + }; + + /** + * Update the HTML dom of the Node + */ + AppendNode.prototype.updateDom = function () { + var dom = this.dom; + var tdAppend = dom.td; + if (tdAppend) { + tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px'; + // TODO: not so nice hard coded offset + } + + var domText = dom.text; + if (domText) { + domText.innerHTML = '(empty ' + this.parent.type + ')'; + } + + // attach or detach the contents of the append node: + // hide when the parent has childs, show when the parent has no childs + var trAppend = dom.tr; + if (!this.isVisible()) { + if (dom.tr.firstChild) { + if (dom.tdDrag) { + trAppend.removeChild(dom.tdDrag); + } + if (dom.tdMenu) { + trAppend.removeChild(dom.tdMenu); + } + trAppend.removeChild(tdAppend); + } + } + else { + if (!dom.tr.firstChild) { + if (dom.tdDrag) { + trAppend.appendChild(dom.tdDrag); + } + if (dom.tdMenu) { + trAppend.appendChild(dom.tdMenu); + } + trAppend.appendChild(tdAppend); + } + } + }; + + /** + * Check whether the AppendNode is currently visible. + * the AppendNode is visible when its parent has no childs (i.e. is empty). + * @return {boolean} isVisible + */ + AppendNode.prototype.isVisible = function () { + return (this.parent.childs.length == 0); + }; + + /** + * Show a contextmenu for this node + * @param {HTMLElement} anchor The element to attach the menu to. + * @param {function} [onClose] Callback method called when the context menu + * is being closed. + */ + AppendNode.prototype.showContextMenu = function (anchor, onClose) { + var node = this; + var titles = Node.TYPE_TITLES; + var items = [ + // create append button + { + 'text': 'Append', + 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)', + 'submenuTitle': 'Select the type of the field to be appended', + 'className': 'insert', + 'click': function () { + node._onAppend('', '', 'auto'); + }, + 'submenu': [ + { + 'text': 'Auto', + 'className': 'type-auto', + 'title': titles.auto, + 'click': function () { + node._onAppend('', '', 'auto'); + } + }, + { + 'text': 'Array', + 'className': 'type-array', + 'title': titles.array, + 'click': function () { + node._onAppend('', []); + } + }, + { + 'text': 'Object', + 'className': 'type-object', + 'title': titles.object, + 'click': function () { + node._onAppend('', {}); + } + }, + { + 'text': 'String', + 'className': 'type-string', + 'title': titles.string, + 'click': function () { + node._onAppend('', '', 'string'); + } + } + ] + } + ]; + + var menu = new ContextMenu(items, {close: onClose}); + menu.show(anchor); + }; + + /** + * Handle an event. The event is catched centrally by the editor + * @param {Event} event + */ + AppendNode.prototype.onEvent = function (event) { + var type = event.type; + var target = event.target || event.srcElement; + var dom = this.dom; + + // highlight the append nodes parent + var menu = dom.menu; + if (target == menu) { + if (type == 'mouseover') { + this.editor.highlighter.highlight(this.parent); + } + else if (type == 'mouseout') { + this.editor.highlighter.unhighlight(); + } + } + + // context menu events + if (type == 'click' && target == dom.menu) { + var highlighter = this.editor.highlighter; + highlighter.highlight(this.parent); + highlighter.lock(); + util.addClassName(dom.menu, 'selected'); + this.showContextMenu(dom.menu, function () { + util.removeClassName(dom.menu, 'selected'); + highlighter.unlock(); + highlighter.unhighlight(); + }); + } + + if (type == 'keydown') { + this.onKeyDown(event); + } + }; + + return AppendNode; + } + + // return the factory function + return appendNodeFactory; + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ } +/******/ ]) +}); diff --git a/back-office/app/assets/stylesheets/application.css b/back-office/app/assets/stylesheets/application.css index d77f573..81efefa 100644 --- a/back-office/app/assets/stylesheets/application.css +++ b/back-office/app/assets/stylesheets/application.css @@ -12,5 +12,7 @@ * *= require_tree . *= require_self + *= require jquery-ui *= require active_scaffold */ + \ No newline at end of file diff --git a/back-office/app/assets/stylesheets/jsoneditor.min.css b/back-office/app/assets/stylesheets/jsoneditor.min.css new file mode 100644 index 0000000..aa8fe64 --- /dev/null +++ b/back-office/app/assets/stylesheets/jsoneditor.min.css @@ -0,0 +1 @@ +.jsoneditor .field,.jsoneditor .readonly,.jsoneditor .value{border:1px solid transparent;min-height:16px;min-width:32px;padding:2px;margin:1px;word-wrap:break-word;float:left}.jsoneditor .field p,.jsoneditor .value p{margin:0}.jsoneditor .value{word-break:break-word}.jsoneditor .readonly{min-width:16px;color:gray}.jsoneditor .empty{border-color:#d3d3d3;border-style:dashed;border-radius:2px}.jsoneditor .field.empty{background-image:url(../../../img/jsoneditor-icons.png);background-position:0 -144px}.jsoneditor .value.empty{background-image:url(../img/jsoneditor-icons.png);background-position:-48px -144px}.jsoneditor .value.url{color:green;text-decoration:underline}.jsoneditor a.value.url:focus,.jsoneditor a.value.url:hover{color:red}.jsoneditor .separator{padding:3px 0;vertical-align:top;color:gray}.jsoneditor .field.highlight,.jsoneditor .field[contenteditable=true]:focus,.jsoneditor .field[contenteditable=true]:hover,.jsoneditor .value.highlight,.jsoneditor .value[contenteditable=true]:focus,.jsoneditor .value[contenteditable=true]:hover{background-color:#FFFFAB;border:1px solid #ff0;border-radius:2px}.jsoneditor .field.highlight-active,.jsoneditor .field.highlight-active:focus,.jsoneditor .field.highlight-active:hover,.jsoneditor .value.highlight-active,.jsoneditor .value.highlight-active:focus,.jsoneditor .value.highlight-active:hover{background-color:#fe0;border:1px solid #ffc700;border-radius:2px}.jsoneditor div.tree button{width:24px;height:24px;padding:0;margin:0;border:none;cursor:pointer;background:url(../img/jsoneditor-icons.png)}.jsoneditor div.tree button.collapsed{background-position:0 -48px}.jsoneditor div.tree button.expanded{background-position:0 -72px}.jsoneditor div.tree button.contextmenu{background-position:-48px -72px}.jsoneditor div.tree button.contextmenu.selected,.jsoneditor div.tree button.contextmenu:focus,.jsoneditor div.tree button.contextmenu:hover{background-position:-48px -48px}.jsoneditor div.tree :focus{outline:0}.jsoneditor div.tree button:focus{background-color:#f5f5f5;outline:#e5e5e5 solid 1px}.jsoneditor div.tree button.invisible{visibility:hidden;background:0 0}.jsoneditor{color:#1A1A1A;border:1px solid #97B0F8;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;overflow:auto;position:relative;padding:0;line-height:100%}.jsoneditor div.tree table.tree{border-collapse:collapse;border-spacing:0;width:100%;margin:0}.jsoneditor div.outer{width:100%;height:100%;margin:-35px 0 0;padding:35px 0 0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.jsoneditor div.tree{width:100%;height:100%;position:relative;overflow:auto}.jsoneditor textarea.text{width:100%;height:100%;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;border:none;background-color:#fff;resize:none}.jsoneditor tr.highlight{background-color:#FFFFAB}.jsoneditor div.tree button.dragarea{background:url(../img/jsoneditor-icons.png) -72px -72px;cursor:move}.jsoneditor div.tree button.dragarea:focus,.jsoneditor div.tree button.dragarea:hover{background-position:-72px -48px}.jsoneditor td,.jsoneditor th,.jsoneditor tr{padding:0;margin:0}.jsoneditor td,.jsoneditor td.tree{vertical-align:top}.jsoneditor .field,.jsoneditor .value,.jsoneditor td,.jsoneditor textarea,.jsoneditor th{font-family:droid sans mono,monospace,courier new,courier,sans-serif;font-size:10pt;color:#1A1A1A}.jsoneditor-contextmenu{position:absolute;z-index:99999}.jsoneditor-contextmenu ul{position:relative;left:0;top:0;width:124px;background:#fff;border:1px solid #d3d3d3;box-shadow:2px 2px 12px rgba(128,128,128,.3);list-style:none;margin:0;padding:0}.jsoneditor-contextmenu ul li button{padding:0;margin:0;width:124px;height:24px;border:none;cursor:pointer;color:#4d4d4d;background:0 0;line-height:26px;text-align:left}.jsoneditor-contextmenu ul li button::-moz-focus-inner{padding:0;border:0}.jsoneditor-contextmenu ul li button:focus,.jsoneditor-contextmenu ul li button:hover{color:#1a1a1a;background-color:#f5f5f5;outline:0}.jsoneditor-contextmenu ul li button.default{width:92px}.jsoneditor-contextmenu ul li button.expand{float:right;width:32px;height:24px;border-left:1px solid #e5e5e5}.jsoneditor-contextmenu div.icon{float:left;width:24px;height:24px;border:none;padding:0;margin:0;background-image:url(../img/jsoneditor-icons.png)}.jsoneditor-contextmenu ul li button div.expand{float:right;width:24px;height:24px;padding:0;margin:0 4px 0 0;background:url(../img/jsoneditor-icons.png) 0 -72px;opacity:.4}.jsoneditor-contextmenu ul li button.expand:focus div.expand,.jsoneditor-contextmenu ul li button.expand:hover div.expand,.jsoneditor-contextmenu ul li button:focus div.expand,.jsoneditor-contextmenu ul li button:hover div.expand,.jsoneditor-contextmenu ul li.selected div.expand{opacity:1}.jsoneditor-contextmenu .separator{height:0;border-top:1px solid #e5e5e5;padding-top:5px;margin-top:5px}.jsoneditor-contextmenu button.remove>.icon{background-position:-24px -24px}.jsoneditor-contextmenu button.remove:focus>.icon,.jsoneditor-contextmenu button.remove:hover>.icon{background-position:-24px 0}.jsoneditor-contextmenu button.append>.icon{background-position:0 -24px}.jsoneditor-contextmenu button.append:focus>.icon,.jsoneditor-contextmenu button.append:hover>.icon{background-position:0 0}.jsoneditor-contextmenu button.insert>.icon{background-position:0 -24px}.jsoneditor-contextmenu button.insert:focus>.icon,.jsoneditor-contextmenu button.insert:hover>.icon{background-position:0 0}.jsoneditor-contextmenu button.duplicate>.icon{background-position:-48px -24px}.jsoneditor-contextmenu button.duplicate:focus>.icon,.jsoneditor-contextmenu button.duplicate:hover>.icon{background-position:-48px 0}.jsoneditor-contextmenu button.sort-asc>.icon{background-position:-168px -24px}.jsoneditor-contextmenu button.sort-asc:focus>.icon,.jsoneditor-contextmenu button.sort-asc:hover>.icon{background-position:-168px 0}.jsoneditor-contextmenu button.sort-desc>.icon{background-position:-192px -24px}.jsoneditor-contextmenu button.sort-desc:focus>.icon,.jsoneditor-contextmenu button.sort-desc:hover>.icon{background-position:-192px 0}.jsoneditor-contextmenu ul li .selected{background-color:#D5DDF6}.jsoneditor-contextmenu ul li{overflow:hidden}.jsoneditor-contextmenu ul li ul{display:none;position:relative;left:-10px;top:0;border:none;box-shadow:inset 0 0 10px rgba(128,128,128,.5);padding:0 10px;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.jsoneditor-contextmenu ul li ul li button{padding-left:24px}.jsoneditor-contextmenu ul li ul li button:focus,.jsoneditor-contextmenu ul li ul li button:hover{background-color:#f5f5f5}.jsoneditor-contextmenu button.type-string>.icon{background-position:-144px -24px}.jsoneditor-contextmenu button.type-string.selected>.icon,.jsoneditor-contextmenu button.type-string:focus>.icon,.jsoneditor-contextmenu button.type-string:hover>.icon{background-position:-144px 0}.jsoneditor-contextmenu button.type-auto>.icon{background-position:-120px -24px}.jsoneditor-contextmenu button.type-auto.selected>.icon,.jsoneditor-contextmenu button.type-auto:focus>.icon,.jsoneditor-contextmenu button.type-auto:hover>.icon{background-position:-120px 0}.jsoneditor-contextmenu button.type-object>.icon{background-position:-72px -24px}.jsoneditor-contextmenu button.type-object.selected>.icon,.jsoneditor-contextmenu button.type-object:focus>.icon,.jsoneditor-contextmenu button.type-object:hover>.icon{background-position:-72px 0}.jsoneditor-contextmenu button.type-array>.icon{background-position:-96px -24px}.jsoneditor-contextmenu button.type-array.selected>.icon,.jsoneditor-contextmenu button.type-array:focus>.icon,.jsoneditor-contextmenu button.type-array:hover>.icon{background-position:-96px 0}.jsoneditor-contextmenu button.type-modes>.icon{background-image:none;width:6px}.jsoneditor .menu{width:100%;height:35px;padding:2px;margin:0;overflow:hidden;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:#1A1A1A;background-color:#D5DDF6;border-bottom:1px solid #97B0F8}.jsoneditor .menu button{width:26px;height:26px;margin:2px;padding:0;border-radius:2px;border:1px solid #aec0f8;background:url(../img/jsoneditor-icons.png) #e3eaf6;color:#4D4D4D;opacity:.8;font-family:arial,sans-serif;font-size:10pt;float:left}.jsoneditor .menu button:hover{background-color:#f0f2f5}.jsoneditor .menu button:active,.jsoneditor .menu button:focus{background-color:#fff}.jsoneditor .menu button:disabled{background-color:#e3eaf6}.jsoneditor .menu button.collapse-all{background-position:0 -96px}.jsoneditor .menu button.expand-all{background-position:0 -120px}.jsoneditor .menu button.undo{background-position:-24px -96px}.jsoneditor .menu button.undo:disabled{background-position:-24px -120px}.jsoneditor .menu button.redo{background-position:-48px -96px}.jsoneditor .menu button.redo:disabled{background-position:-48px -120px}.jsoneditor .menu button.compact{background-position:-72px -96px}.jsoneditor .menu button.format{background-position:-72px -120px}.jsoneditor .menu button.modes{background-image:none;width:auto;padding-left:6px;padding-right:6px}.jsoneditor .menu button.separator{margin-left:10px}.jsoneditor .menu a{font-family:arial,sans-serif;font-size:10pt;color:#97B0F8;vertical-align:middle}.jsoneditor .menu a:hover{color:red}.jsoneditor .menu a.poweredBy{font-size:8pt;position:absolute;right:0;top:0;padding:10px}.jsoneditor .search .results,.jsoneditor .search input{font-family:arial,sans-serif;font-size:10pt;color:#1A1A1A;background:0 0}.jsoneditor .search{position:absolute;right:2px;top:2px}.jsoneditor .search .frame{border:1px solid #97B0F8;background-color:#fff;padding:0 2px;margin:0}.jsoneditor .search .frame table{border-collapse:collapse}.jsoneditor .search input{width:120px;border:none;outline:0;margin:1px}.jsoneditor .search .results{color:#4d4d4d;padding-right:5px;line-height:24px}.jsoneditor .search button{width:16px;height:24px;padding:0;margin:0;border:none;background:url(../img/jsoneditor-icons.png);vertical-align:top}.jsoneditor .search button:hover{background-color:transparent}.jsoneditor .search button.refresh{width:18px;background-position:-99px -73px}.jsoneditor .search button.next{cursor:pointer;background-position:-124px -73px}.jsoneditor .search button.next:hover{background-position:-124px -49px}.jsoneditor .search button.previous{cursor:pointer;background-position:-148px -73px;margin-right:2px}.jsoneditor .search button.previous:hover{background-position:-148px -49px} \ No newline at end of file diff --git a/back-office/app/views/items/_traits_form_column.html.erb b/back-office/app/views/items/_traits_form_column.html.erb new file mode 100644 index 0000000..ebf8090 --- /dev/null +++ b/back-office/app/views/items/_traits_form_column.html.erb @@ -0,0 +1,6 @@ + +<%= hidden_field :record, :traits %> + +
+
Click to view / edit
+ diff --git a/back-office/app/views/layouts/application.html.erb b/back-office/app/views/layouts/application.html.erb index cb7c922..0c31d13 100644 --- a/back-office/app/views/layouts/application.html.erb +++ b/back-office/app/views/layouts/application.html.erb @@ -16,7 +16,31 @@ + <%= yield %> + diff --git a/back-office/public/img/jsoneditor-icons.png b/back-office/public/img/jsoneditor-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..7120088f9475d17ba3bd5f716bdf8bf4eeb1f32a GIT binary patch literal 14438 zcmajm1yodFxG($xq@+XXl5UXh25AHYq`SL22M_@%=@OCdF6jp8?(UQr8ooWA^PO1h z-gW=K1)CWdW)HLW`#x_Fp`s*%_L}fD2n0ft{UE6d9Or?LLQDkU^JM&^18{g{A*Lt> z0#(JJJQ~3Rw_#jVWyC?{qr?Zm4RVtYs)`_xHw_3B2nK=ffx80tK_E9a5a`el1maHt zf$$tNnm-Bx-#{>ymyraHK<*@30)PXu-E;9|#YivRbg{RwfxrOrXCGkuU;uiW~{(#8hp*MuuKc(Oy3eMTF(dpRHYU`mjC~$ds(>ROA_U6jfeU}%Ms&L=o@a?4ko5VUh<_m6jOdA|Kh9C}NJjE?7~99bQmiiRR^{7b!XY!PUXyB1Ac0 zt{fd542mKXaV>O2euMjVwW<3Or`o13Y(GIr8(_Kl>Z2!@WX zu$((wzDS)R4g`8Uad>`qlQ@aMNlmS*+jW8KynXQTk*5Mlgx}xKDxPHt$;4=A!g>0% zG|5tdLJ)6G7`GM9Pa488G&SLto7D8}(c2NH-`l^Fj2$u$0sr$ zg?BJ?Oiavqxa+$kKi_2`?yngX9yn(3eXke$G}E1~|5@>4K}os4mX}GwNiIw}=Eew* z$P+H;hr^m!2nZy6Z#l;T3Y3cM>H>l6mj^}>K=X}%AI@Mvy1HP|-|i=;DfjEPHbpek>id!pp|=s|Avq-7sRHGc@%a2+yCrabFXXjprhv#i-t_dSYwL68Q`EzEmEp=Xyp|4w zwv6G^-OspAc#$2~nw zpn)mX;jtXw9f>}B85MYXsL{Re-9~rC z3LexXN9V)oh}f`ng(Vbu3kg4$nVGJWyr!#_x_S)RW5U>`y!Mg%4t`QZL|R%9NI*cQ zKfKl6%*+nW&{ABInIg^+!*>q}ix4J#5qAKx<+V{gI7*u+N2P^h)AsyCMqKK2x;#^Y z(K2pfuHVvk8AeEOgd_1mfw?^BWZr2<&b4-G zBOf=evB;&&#hQW`jKG|j5_voe3kpOpFHl&TW(=tx&?A*Fw7YLOmYRQ!XZCec;d_MyzdTXXnKyYGtK3 zk||(p(NaDzFlT@Wa&(8BgV;FfM>8LeYs80!hd`)`kt;1Xhwvaao7t8#SkU8RjWisn zH8cz9u|9`Sq;p{ z`17avp-Q;gZ20+-3>5gO1T5SFLDubylG&=e>NKy~zs3k7(V({fa zCC{3m-~Egx+KWM}_Q$w{DSrYRe(tZ1*N4pqWvr}ci(H_R-pW*_-@lia4CwYv-@F zqK_o)7#ZbLsy3_o!GYU{i+L#h$ar1NevAs)!YeH<85?(X$H&XMMpr-C7(UucghP&K z6JL+1{S^$9b9?k$nTQn=>kb<<1t}?Y?aNoqFMR$CgMW`cmW?Nk$352@*|pa%9Z=mw zF_F#DApGlFmcOmIuVm#qB?R}QVy|=!HO(_bM=?5wH9x2J+O7L$maJ&`=G{`(N2b#n8Yu`rcja3@4*4tt>+WVq#`yK4OuH zOMnJW{n1LEX<&RS#I$-%i>d1icBjQflX=omyMUD28XpaKcaTFvR)Q8|9=?~5$1qK?NM}h z9~#TT)XFm{uYY-Z`rKiR_iHp$z~gjzp|Uk+Yf;__8g`(;faYn=VfDAy<*uSc^Frv{ za^+YruU~1OsyZ}O?hFDf0nogJ8H^1;gDH^6tQ=s&#q+}4816v0MOLgplYo2~osKt) z+jDw;@-Ihc{ihiV{>SLVby0tBf>zcm1^i6@<9x2+z@h&#-^4ym?8cvYYQ*@xDvbc; z)$Y*Xzr^a7X_U-ZqqUoe|FJna3$q5>`j}MYemh}Z0}rIRYl&W5LG!B7|LOkvXbugJ zhV@^%Lz7SX)0%tiO>2AZS$r3I2AEyc*= z(JXj22;AvHjm21&5SoE!k^uy+$?XXGHuhlXqN02d{+AJv=f}Ity|HYEb^*1mwjcRP zxlSOCWI`>Oerx^X%XXie)0Gwvv}}}z%<88<)PYq;2qjHKh%|3I2)#+%Y?;vwOIf=b zD@rK(GE4vbdmEc5^(_P+#0{F9cZL!fff5s%FqBfK^=9{z#X6h0!uwQ+#zTYMeKvo4 ziSr-bZglJFs`gSil{?zN^5mMipCJhvih7@-4QdN0-Nry)#%MXb3n8zz)*+N+Zt`Pl zfLouR4wDLj2~S$7w0U`m%D91iq>bTmH?iT*EPZxJEH+7f;ZhdQx@GPXuT+m2yuXM1!DJc9pqRO`_Uk|*fmOj%#ct|8+ms_V8 zdGh4@rp2*(qV%|HC=EB7%+fq4a)xt(?JWPBzOBvZzl7y;Y2+GdWoOT0bP(N;)L(fF z35#q^H1Ch4>X50*D2w>-kg}8Ph#3G+Z(SQbqwuetnxyV_GTuwzVkZ)Lo=}Od{Ezqx zFSVEj!}~)T^0d$h5y!~cjX)CSIaQrPyWF?oT=X^hwSDWBpZ?bv_y5={Jk8V^!qb2i zWO7vKaKGNyoEObl*J<0O)Hf78L@*b6sGR*+O-fSbDN3KVd-jbDA>N+K7Zcu=@Q)$A z@z6yWHe9H)=@MD!HZ_(WR^nbFA1kX-SnET?QP)~JB*ZO}2?0%G+M%W(*%Fwwqo*y(~V$D4~h;_|)qug*TEMJr=} zTJfOx7nyI4M@`qu_iO$kJnLwlTYvk_R-kf!k`q-^_;e#wT}H(6z5fhSv(*HwRfnc` zj;x#e-sYTlw|aj%Mop8WL(9vV9*$h=JX`m8yv#mKg{rZL%w(ZrIaJ6jx|C;yfq#t) zGVfAIX|i3&Mq~*A|BHZ!xG>rs=k}D@Fz1YS=}({{C#;3wd{hrLd#vdQ-fdIPn#dPfRNF_gs%=TD^!8gE+c(zOo6 z?7B^1qWD#14tbs4ISlGtB;n8u2n+&F7U9D;*ogI8G;YQ5@FFue^5Y8z)VAsMTDtJ@ z;Y^a4ur972FRyg&nV)8TtznJlemo!U<6WWLhgPtrO6jZN&mQ6g#&I|?WcbOLgJ05v?3}td9lcy_;;*%;w zY@Ke3sI{%8K~{O$xUnk3QgeoZ=SCA50dG;qW)YuLIFHK4I8* z2#~LAab{BN%9CiE3)o6wXTwM6UN6%LZ3km=eZ=9Ss14Q_|LV~8v6+75yZu&Q(S^EK z@$aqvC4tO=+D7hxaLX@YmWYU+gjrz)FLz_V8=-ztZ75kS^H!k*=7r=%=3V+XM?YZ| z_A2qb$N7ti@i&aC(#>Iib-b0`-PT39o7xdsH+X}$Il}&Dx^{KFJLO<>43LWApWO$J z{JKS8Q6z9jX{mYkJ~=Ts1dmltKkoNCFKVV^IzG8?*qNi#2X;~KKc|)|K>lLEYWV&> zpX>U$`pr2uWO*^|?Zi9n+JKj*EtPQdYLK*2EY7wh4$CoF2jV$QKXQ=xqtw8GA%6;b z(%@+UO3p4_$a}gL=bh)5B_jdbms^vZvFVAbs_sxR;!FV-Y^5$);pf{t>z=`Ya6AR~@Q!p_9R|S|~O~b}Hk-YkqAHz7A{l-Przg8+}u~p=~m2 zxo0H2AhPcMbEjmMXj1%FVwl_8J>O9NadERdyP}-3d_Zw$>JdLaS?Wpf#2%xTAKFbE zVNB<7{&gP@m<)n5#tWp;Oeh&tR4_d-J}^DtUTq%?BIH80f#rN&ntWP}>@wJ>I0sI@ z649qJChGT#I1H|+SiAqD+4K8=yB_2mRxT!m#;t}1ys&+vHxeOhNi6 z;o~K|cyi=;rR-UeiU9qWM?8aCH(^NGQiJ_k^Kfzz-t|xYjg3ms+m7ISE_y-<6U!)# zVUJCLznS9V)0Z=VqgSWy?^*107aKWsbQp55873?{-r9D~F10Ms-f!{BvO-1E7(4fD ze;IUlKglGU=BH@^A&E@uG=RxO718bzALaa`BdXF&xdyE#^A)>L;XKa10&M_x+c-LU zvk=tfF0vwv%WkZ1yz)h!s@peynM`0VX!YRzDav~{vXuFt&fi$?)>r_~cYCI+3-XZB zbc<@W3UFfOd#*(FI0jYf)sR1Op{hCRl9J~JP{)f>_3>q+9UTt4i*JYnc|zR}ez4I~~2BCb-4 zSVW}w+6tBJN5hH5P2o5riZ-1gAd{UpHfiC;?;NU2q51fpPoBB36j*D@O;El~wb;^}uZpMm(iU7*JJ6E&%PP;lk*;@3;`l?T_MQq~*mf6TQ1AFB`wa4dVq1WW{j5HzjNf8N0S zOBjJnT66ZiQVg%q=G7Pb6{-hnKr$;Wq$g`WJS_eLw(L6}$gib+?qw)SKb2UGY?<>R>#ww$=^zRy`4*#O3N_*wwox5G&(Eo4DKV)RZ5 z-db<2rzn4E5uazTe^tTG*Vpr}(ha9dKO661P=FP}W+dm=X5R~E&)*$cg3~m8AD^6k zg`e)O_B_ts$|c*l_FhJL5nR6CMI)wr)%Er3Rpx=fl*h$m)8HOWcKTxv4)oW!01Y<6 zD+{P7Qf3lbfTmvAklIes#WylM0lfi1B$&R@Z`lLN+q4Y0R8io#%0=x#W|5Cg^w9{(=UB34(EG?RD7YxlAumdIRD2s;)a2!B1o7G_iE8 z9Y?%5EV0vIqmPB9Pv=4WgPKCX=5o5s?UUViK@^J>i&g1J;TPQ|ZNSKikwV1AV|ZF` zXPk#v7p;NF=Ji!jDhnHr_L0X}G65q4zQ%C2$|Q{gYD|RHU5pJYfs#f>p}umQ`XJs_ zmdt-3du#0Pp0zb>HkvQ@`GQ5ZqEMnY!}WJh#$CeU{~RU12@tyXhgw}V#V3#s`>fq1 z)QEU9IptU43%IyQ^AX0DHz;(?vYp8yex!p&BM zyM73FmE#D8l9zwNfx0=hd-TPQVpLPZV#!6zxPwm^M3M z%*Oi5o2qvjXTK@kVeqGz=K$I45|*$R7s1e`2XvEq%CTb6b`_7=QE&f7ATz2Rh*8e{ zE{*`meOipT_N&GOgB&uZ5!JK`{%Z;3pmawZ`}%e8k|B|8NkWW?BkH%*a@ph=q=4(& znJvv2tEppoD0GsavjNKv&LE9 zj=A1;FL#}O$TQ=xuwaY_;>dXbpW-xo9Kcc$=*mied1RQADGCjZP51>sMM#m>%mvj9k0 zLyr8d= z@teTdiSJZ=I^1W+v7J9V~Y%|`}6zj5F$r7`^PIB&tO_lIVv<#;8q7(tNumAHqIa| z4$ZOhYW8mtK8~8!Nt@;G=7%(rE?(tBx_UX9`){nx@}j5uthP3{Ix!wQlvyeoawpil z{qaSj$dXbAX)gzTtD~c9)wLOz(a`)H@moXSbi;-ny^dxBE1@7n(yY1zGEwU*ULLDY z1=)t&f5H|tG;TcT!i?!=t&4Enek$ckqv5j|b_Bu{6`klkTSZR3&10^v+xgi@$8tD; zH6DcSDiKzDer%^ z3Rt*0=!xUS?mx6*c4vy)I$`K9fN?|XpP`Iewwi=uQb-9C70%36t zugEWtDjGbV!6+Wi7YnsDRu)tG-Vw)C0)wM^Egy&@D4hhA9ma{S`o7b{6(g!+6xzRZ zyupKpHqP;TF7ZlTNnKE6j?6xmHx(NucL%~eCSI~6aHV`=kVybuZc&-tneWeCs~Q12 zsi2>tAKC0P&U-70Cs8K=vPy3Sx=Bc8I~MU!bh)0a?`N*mihu&DV?!AhZwupjJ>aVF zT(mkI5iG$A7T~K&9(-LLTzY%LbC2uyRrvZW0Hu2r2fJsKl@irmt6CDN(&O~Df@*)35@vOxh?KnuN(qb^4+b1`%5?kYWPXxvX7V_%Y>V*h-TF^7+(9n^g2qNn+ zbVlD^@OZjzcvrkrl2q}04(AJR(5cxS#^t?k#ETlf<}YAq)X^0fO6E7Mzw>3>AZ`%B ziTsLguqob!dhe@-ygn6cJRYloy`=~Q#NipjK>6R>3jR;U&VX*+iZg>OX9Ueis>@5XH1K}7Dn#+CkQAavMOam! z3QJ_?jHwh&Z!~s0g>TKy7Gpu}>>OXK5Xwmu<>vZFL8&4)#iBIjN{hj(i-v|H3MpVr zb*mOtb^v=9&XIj#vmYVpmB!`TBX6&4D<1LHgo}Jt?@Z5DeF+TulBFa&=FYi%pQhEi zUbh3?+S##DCNfEglV= ztru=k^z?S0NTVm1W&!q3)kH`Xms3L^=`X(JfYWfYE|YV7L2iHF1dJ7gCBvmGp)>`> ze43gQ8WA*lWWeWYur1}%K>fgn525NGTAks!yQJs$ci;sK5sxjOmV>v+`97fM7tEPM z$w(n{CHvecB*t`f28F;O4x5&z>+!VMuQtX#D)`fgG(30x>2OJv3<+~*o4j;<~ z>Z!IY==cT>mdYreO?JN83={q%W}S6nk@Mz#`B6*x8H?7q;ksPls}H>1>dsOCYL;?s zZRWD1ERiw&q|hO8vz!ow370EkAs!c+R#}J1bU1|bAYXDaEfvty53;Mvi{|X?{(Y0H zDhi2hfgOOIE%3uv=-sJ|+dMHn85%0Cq<+(N1K@3Sy@ESqQ zmuoq9^rvo*H@Jf=-5A{FTE#I@4i8ila7-H%x2p$uo$5wjVv!P)60g?K?hQ3LmpeI$ zE6!Cqb4LIC$Z7p_|EdM*<+;JiB2jL~V7NUg;Hei+1u0a8*U?bI2U>D6B{O{-(*%h4 z&To_9iX+v<4koF|MkxUPjA_y^CL|6HCtu3k zNg8J$A+Wq}3O3a#WxB9EBBvz%FU&7&YQ@jH&ejUWC#g)Fx`-POd#%)X9){}he ze=fFJ)}s^Az@le;=oRdZc#VFzQlut7CALa&7VDAaU0n5KX|R|KVWtmaXG7@I`b^Kw z0Dv4XOyA_lAE-9FK0v(B0+N@hJ<=7atkWL>FRd$T5E-?M{1U*? zc#fiwEimm->tr=Lnxw2PY<2fb8cs^(l@uOpJUO;x@Z_YY&uUhKh>J9StH*BCc!Qem z^5XW8$ai2``uyRn6Pu~C+D0?U|A-DySdjH~jRZRT?dDc9_E7eybJrm9)RSiVxudGO z7Cbb`H+K=Z*E16n;mBMo2Z?K^l+|MPV|K5{kkl zn?|Lms;zA^40J5oC2=&8a#HdOpBi6lqK(;b@ku270=V+GhObJTVPQLWoCdDIwEX-D zYz7g3N0r9Xv@OV|JbEceB8fO51}dfh=pzltf0Ij`3lyTr(B^opb?$oZqDvY?QA>ID-FRMPbE%OB2b%f<%B^&ul zT>h~s={<^@G^sxhxKO*ukGEyFMwYehnH$^}Ijb&XFvX48KcgKt^^P(7+gH`NTn`+^ z%}}=Ah3URa9>`DSQ zO8*{*RYA8(el3#6C5JMfY*HccO+-w^jd01Jb+valf?Gthq~7F}7zq-^^h6zhU{z7H zt9GVF!P6!RMARV!@!xE~X79xzl%~GbQBksX~*Sm{=lE*y_PYL z?6Z?4<77&4q*Uz^IIY&Aw6y!amI0P(`8?HvLbYl7m;2;-_z6r3$DaNg0ztw*W7^e8 zrLA5GjO(u(g9$jmvd;4Iiizml^i?aeEeYh#rFx8y4en)4in~j*^V3@5{>iMGI9wmo*&Nx$73=CX3jeZb{aDHc zT+Zs$egP(QyWysdlM#W-5HDe*=Wiip6;)Et?6l|`b+MVV_Dj6G$j9DRADj=cvgE+t zB~5-(->lIv4WPYvzJE=G(y1V&e*t#*3P4vbp4=;p^)wDp{%sDn9;6c2`sqi7)3r9L=at z#9PK$;NeYa7i8Vd&0i1aZSU5*2@`g`J^wCiik z#v_{gKez%c0tf4P#fYB|oze z;CW@Kv$xj?x|~+az}V})yS?SS5!`0uQ$SZ^V0ui=%Bmx`5t|L3DtOSiD%;m#cY3!X z*DzW3`Lj4^_>ZmA-B=<($f?Wot)CjbfqGk^??i%fby2;7pFcSE&idNhFp(YVU*r$DVgoAFn`2q!$%Lz4AtCOS07uDJ+u|(S=tQ%d9lf@} zij_qc$`Ab~WDNnjECG)2CR0XSvJYmE;p1d@_&QF<71m@C>!e9vt2WMn^$!2F>xc3+ z54HSQ(|Cn=JzYcUQgEV>PZ~cN5``%P%^4g=!=SIkZ>R!AIFWNWk?!v*;vLjrgb8v& zNhD-)7^)wIU!rKY25+YdN;EYq%JubB$TO(`>N<=f zF}qW~X#qQO#@2vCWsy_{GO_;PZ}22OhxGPn4PiP{?E$SM&xOdZxiX_rg~1pb5?>>! zTc`k9fv1aaHYrFq5z)8xyfjy5RxICw~Lo5mTZrUtq=F>{GQW^W}NoD9#3qR(xM zFH3ts<+m91q-`%!8!48MG+@Sg3o|_on`%*7scD z1g`LzL*dH$i}n4ET}nM!&B;2Mm_A6Zkd^XcP3?F49G(57;$jX*gi%qBRSzttU@0rB zzVvI^dn&*9aG4SmV|J#n-s`b$zfOSDy{npXxA{!PQ!Qj_QQ5{umSvE-iJID$O(tZA zBSPtCf}qd~-HZ$*43T2`FQ>EEJIO{$VArs_@&alK31w~CcFH25n=DsZx)9wqmlWxa(+r(Y;&!zJpH=34Aq0(hJ- z*t)s--L9RrEB|N7>0svtG=Mf-lZdnDnO^BFuQfNUy}Vp7Y99Ywa14)~t{g$P^NePi zn3%ja1{_{l5!~3N^6^KZ=XN7e;0?e#cV6uUuvJ!fm?Ek4jfF<&ks?v_7$&GNR^{_G z2bcd~%vUs70($jdNuNGN_~6{ooJL369o&UPLUnf!4sV-Xyf2}EL1GjSxXC=v(D)cP zzca3V%pO#1^r^czER%O)6Q>pBtgA(kby_9cQ~cN$7!iG2fCRbW{))wo?}CAgYH%sf zVBA~0m0PJ*6+705eT4T^a!|4`&t}$#|vo$TpZTs%Wcsv1nbap$0ZkQ=G*WTQ?Qz{(rS+Szq+AdgWA)&MK~JK$2XC zk}MWu0Ff9`q$}optEA%fQDCMJqkA88LMI$s`T!mgC81AR@=GS^|9kVye^2)0$SBwB ziK8BpObZk?WM`Wu1LoGArTY^8n*KIwrT429)+Wo$V)2r`h}tH|qRpZD4sLEy3piAM za#;THk@KrG8kxN>ai*fh)Iwe!CJYv4Tw?D5FG<1ClBSlXeBOJdaN{I^#>;cxUM}2$ zD1R(wb>#88meGuZeHT`S{~Wh^r|Rb?*VKIW-=@F7_4*neT{(xsinMcm(ok;D}ob^ut!dyeYvF+sBrXc_g7@LfG!snBqv*k0;I%)&l&oWQBp?JJ_+Qx z%bk4JYx$K6B`8Gnkilic37EF78pflTcB=Cg8}|GXej5z?uPj#G6URl4!yGi1t!UpC zO6~x540w4JV}FV$2J0cg7(!PTKo>|}V8NdT?fEp_x4vRL!S=6K_NQg*wE&(MrbbDR z=|DMj*F8dJpY;8U-ug%{*+$H{%zKWe&&y2q8lr)f*TgBC3;|HjN~V#Htq{26_lBvVyqHe3*^{|oP6XDjz+?&0W(gCn#RdBf~?RPx<} zyfqb9d>V&E2SG+BpTLrH@Xo7YVndxkQnq5SVmM2Q)W*M1HTBf`i5E2kr;0H9xAjxz#lUDgX8%4mgEuLzrXO~BmFaOz|e4H&2A~* zQJWv-e@^$M-#=f0ghVmaHAB$m1EX7?G8SCS$5NRSLMMUwNC7wFhfn5E1aB{FkH!dq zVd^S)Ui{|l4-XwT9k)XEX#G^ED}~+wOQ4n2?>7#;I(z~(0v`2b&{4p9+HhF-*Liu@ zqdbJt-Ijri`%r2H*nT(whqbpYKXguTZeIxx{sH4{NO?}Ik|I!ZXEh2tZUjyOBrByPSuSoE{Qm)- CdkcvG literal 0 HcmV?d00001 diff --git a/front-api/db/migrate/20150131061353_add_traits_to_item.rb b/front-api/db/migrate/20150131061353_add_traits_to_item.rb new file mode 100644 index 0000000..1546d99 --- /dev/null +++ b/front-api/db/migrate/20150131061353_add_traits_to_item.rb @@ -0,0 +1,5 @@ +class AddTraitsToItem < ActiveRecord::Migration + def change + add_column :items, :traits, :json + end +end diff --git a/front-api/db/schema.rb b/front-api/db/schema.rb index 564e36b..adb7080 100644 --- a/front-api/db/schema.rb +++ b/front-api/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150130041842) do +ActiveRecord::Schema.define(version: 20150131061353) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -35,6 +35,7 @@ ActiveRecord::Schema.define(version: 20150130041842) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "tags" + t.json "traits" end create_table "media_types", force: :cascade do |t|