/*!
 * jsoneditor.js
 *
 * @brief
 * JSONEditor is a web-based tool to view, edit, and format JSON.
 * It shows data a clear, editable treeview.
 *
 * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
 *
 * @license
 * This json editor is open sourced with the intention to use the editor as
 * a component in your own application. Not to just copy and monetize the editor
 * as it is.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org
 *
 * @author  Jos de Jong, <wjosdejong@gmail.com>
 * @version 2.3.6
 * @date    2014-01-03
 */
(function () {

/**
 * @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: {
 *         editor: TreeEditor,
 *         data: 'json'
 *     },
 *     text: {
 *         editor: TextEditor,
 *         data: 'text'
 *     }
 * }
 *
 * @type { Object.<String, {editor: Object, data: String} > }
 */
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 {
      if (config.data == 'text') {
        // text
        name = this.getName();
        data = this.getText();

        this._delete();
        util.clear(this);
        util.extend(this, config.editor.prototype);
        this._create(container, options);

        this.setName(name);
        this.setText(data);
      }
      else {
        // json
        name = this.getName();
        data = this.get();

        this._delete();
        util.clear(this);
        util.extend(this, config.editor.prototype);
        this._create(container, options);

        this.setName(name);
        this.set(data);
      }

      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;
  }
};

/**
 * @constructor TreeEditor
 * @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.
 * @param {Object | undefined} json JSON object
 */
function TreeEditor(container, options, json) {
  if (!(this instanceof TreeEditor)) {
    throw new Error('TreeEditor constructor called without "new".');
  }

  this._create(container, options, json);
}

/**
 * Create the TreeEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    See description in constructor
 * @param {Object | undefined} json JSON object
 * @private
 */
TreeEditor.prototype._create = function (container, options, json) {
  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.mode.view) {
    this.history = new History(this);
  }

  this._createFrame();
  this._createTable();

  this.set(json || {});
};

/**
 * Detach the editor from the DOM
 * @private
 */
TreeEditor.prototype._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
 */
TreeEditor.prototype._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];
      }
    }

    // check for deprecated options
    if (options['enableSearch']) {
      // deprecated since version 1.6.0, 2012-11-03
      this.options.search = options['enableSearch'];
      util.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
    }
    if (options['enableHistory']) {
      // deprecated since version 1.6.0, 2012-11-03
      this.options.history = options['enableHistory'];
      util.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
    }
    if (options['mode'] == 'editor') {
      // deprecated since version 2.2.0, 2013-04-30
      this.options.mode = 'tree';
      util.log('WARNING: Mode "editor" is deprecated. Use "tree" instead.');
    }
    if (options['mode'] == 'viewer') {
      // deprecated since version 2.2.0, 2013-04-30
      this.options.mode = 'view';
      util.log('WARNING: Mode "viewer" is deprecated. Use "view" instead.');
    }
  }

  // interpret the mode options
  this.mode = {
    edit: (this.options.mode != 'view' && this.options.mode != 'form'),
    view: (this.options.mode == 'view'),
    form: (this.options.mode == 'form')
  };
};

// node currently being edited
TreeEditor.focusNode = undefined;

/**
 * Set JSON object in editor
 * @param {Object | undefined} json      JSON data
 * @param {String}             [name]    Optional field name for the root node.
 *                                       Can also be set using setName(name).
 */
TreeEditor.prototype.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
 */
TreeEditor.prototype.get = function () {
  // remove focus from currently edited node
  if (TreeEditor.focusNode) {
    TreeEditor.focusNode.blur();
  }

  if (this.node) {
    return this.node.getValue();
  }
  else {
    return undefined;
  }
};

/**
 * Get the text contents of the TreeEditor
 * @return {String} jsonText
 */
TreeEditor.prototype.getText = function() {
  return JSON.stringify(this.get());
};

/**
 * Set the text contents of the TreeEditor
 * @param {String} jsonText
 */
TreeEditor.prototype.setText = function(jsonText) {
  this.set(util.parse(jsonText));
};

/**
 * Set a field name for the root node.
 * @param {String | undefined} name
 */
TreeEditor.prototype.setName = function (name) {
  this.options.name = name;
  if (this.node) {
    this.node.updateField(this.options.name);
  }
};

/**
 * Get the field name for the root node.
 * @return {String | undefined} name
 */
TreeEditor.prototype.getName = function () {
  return this.options.name;
};

/**
 * Remove the root node from the editor
 */
TreeEditor.prototype.clear = function () {
  if (this.node) {
    this.node.collapse();
    this.tbody.removeChild(this.node.getDom());
    delete this.node;
  }
};

/**
 * Set the root node for the json editor
 * @param {Node} node
 * @private
 */
TreeEditor.prototype._setRoot = function (node) {
  this.clear();

  this.node = node;

  // append to the dom
  this.tbody.appendChild(node.getDom());
};

/**
 * Search text in all nodes
 * The nodes will be expanded when the text is found one of its childs,
 * else it will be collapsed. Searches are case insensitive.
 * @param {String} text
 * @return {Object[]} results  Array with nodes containing the search results
 *                             The result objects contains fields:
 *                             - {Node} node,
 *                             - {String} elem  the dom element name where
 *                                              the result is found ('field' or
 *                                              'value')
 */
TreeEditor.prototype.search = function (text) {
  var results;
  if (this.node) {
    this.content.removeChild(this.table);  // Take the table offline
    results = this.node.search(text);
    this.content.appendChild(this.table);  // Put the table online again
  }
  else {
    results = [];
  }

  return results;
};

/**
 * Expand all nodes
 */
TreeEditor.prototype.expandAll = function () {
  if (this.node) {
    this.content.removeChild(this.table);  // Take the table offline
    this.node.expand();
    this.content.appendChild(this.table);  // Put the table online again
  }
};

/**
 * Collapse all nodes
 */
TreeEditor.prototype.collapseAll = function () {
  if (this.node) {
    this.content.removeChild(this.table);  // Take the table offline
    this.node.collapse();
    this.content.appendChild(this.table);  // Put the table online again
  }
};

/**
 * The method onChange is called whenever a field or value is changed, created,
 * deleted, duplicated, etc.
 * @param {String} action  Change action. Available values: "editField",
 *                         "editValue", "changeType", "appendNode",
 *                         "removeNode", "duplicateNode", "moveNode", "expand",
 *                         "collapse".
 * @param {Object} params  Object containing parameters describing the change.
 *                         The parameters in params depend on the action (for
 *                         example for "editValue" the Node, old value, and new
 *                         value are provided). params contains all information
 *                         needed to undo or redo the action.
 * @private
 */
TreeEditor.prototype._onAction = function (action, params) {
  // add an action to the history
  if (this.history) {
    this.history.add(action, params);
  }

  // trigger the onChange callback
  if (this.options.change) {
    try {
      this.options.change();
    }
    catch (err) {
      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
 */
TreeEditor.prototype.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
 */
TreeEditor.prototype.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 TreeEditor, 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
 */
TreeEditor.prototype.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
 */
TreeEditor.prototype.getSelection = function () {
  return {
    dom: TreeEditor.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.
 */
TreeEditor.prototype.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
 */
TreeEditor.prototype._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;
  var onEvent = function (event) {
    editor._onEvent(event);
  };
  this.frame.onclick = function (event) {
    var target = event.target;// || event.srcElement;

    onEvent(event);

    // prevent default submit action of buttons when TreeEditor 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 = createModeBox(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
 */
TreeEditor.prototype._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
 */
TreeEditor.prototype._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
 */
TreeEditor.prototype._onEvent = function (event) {
  var target = event.target;

  if (event.type == 'keydown') {
    this._onKeyDown(event);
  }

  if (event.type == 'focus') {
    TreeEditor.domFocus = target;
  }

  var node = Node.getNodeFromTarget(target);
  if (node) {
    node.onEvent(event);
  }
};

/**
 * Event handler for keydown. Handles shortcut keys
 * @param {Event} event
 * @private
 */
TreeEditor.prototype._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(TreeEditor.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
 */
TreeEditor.prototype._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.mode.edit) {
    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);
};

// register modes at the JSONEditor
JSONEditor.modes.tree = {
  editor: TreeEditor,
  data: 'json'
};
JSONEditor.modes.view = {
  editor: TreeEditor,
  data: 'json'
};
JSONEditor.modes.form = {
  editor: TreeEditor,
  data: 'json'
};
// Deprecated modes (deprecated since version 2.2.0)
JSONEditor.modes.editor = {
  editor: TreeEditor,
  data: 'json'
};
JSONEditor.modes.viewer = {
  editor: TreeEditor,
  data: 'json'
};

/**
 * Create a TextEditor and attach it to given container
 * @constructor TextEditor
 * @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. 4 by default.
 *                                   {function} change     Callback method
 *                                                         triggered on change
 * @param {JSON | String} [json]     initial contents of the formatter
 */
function TextEditor(container, options, json) {
  if (!(this instanceof TextEditor)) {
    throw new Error('TextEditor constructor called without "new".');
  }

  this._create(container, options, json);
}

/**
 * Create a TextEditor and attach it to given container
 * @constructor TextEditor
 * @param {Element} container
 * @param {Object} [options]         See description in constructor
 * @param {JSON | String} [json]     initial contents of the formatter
 * @private
 */
TextEditor.prototype._create = function (container, options, json) {
  // 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 TextEditor is located inside a form
    event.preventDefault();
  };

  // 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';
  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';
  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 = createModeBox(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().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();
        }
      }
    }
  }

  // load initial json object or string
  if (typeof(json) == 'string') {
    this.setText(json);
  }
  else {
    this.set(json);
  }
};

/**
 * Detach the editor from the DOM
 * @private
 */
TextEditor.prototype._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
 */
TextEditor.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;
  }
};

/**
 * Compact the code in the formatter
 */
TextEditor.prototype.compact = function () {
  var json = util.parse(this.getText());
  this.setText(JSON.stringify(json));
};

/**
 * Format the code in the formatter
 */
TextEditor.prototype.format = function () {
  var json = util.parse(this.getText());
  this.setText(JSON.stringify(json, null, this.indentation));
};

/**
 * Set focus to the formatter
 */
TextEditor.prototype.focus = function () {
  if (this.textarea) {
    this.textarea.focus();
  }
  if (this.editor) {
    this.editor.focus();
  }
};

/**
 * Resize the formatter
 */
TextEditor.prototype.resize = function () {
  if (this.editor) {
    var force = false;
    this.editor.resize(force);
  }
};

/**
 * Set json data in the formatter
 * @param {Object} json
 */
TextEditor.prototype.set = function(json) {
  this.setText(JSON.stringify(json, null, this.indentation));
};

/**
 * Get json data from the formatter
 * @return {Object} json
 */
TextEditor.prototype.get = function() {
  return util.parse(this.getText());
};

/**
 * Get the text contents of the TextEditor
 * @return {String} jsonText
 */
TextEditor.prototype.getText = function() {
  if (this.textarea) {
    return this.textarea.value;
  }
  if (this.editor) {
    return this.editor.getValue();
  }
  return '';
};

/**
 * Set the text contents of the TextEditor
 * @param {String} jsonText
 */
TextEditor.prototype.setText = function(jsonText) {
  if (this.textarea) {
    this.textarea.value = jsonText;
  }
  if (this.editor) {
    this.editor.setValue(jsonText, -1);
  }
};

// register modes at the JSONEditor
JSONEditor.modes.text = {
  editor: TextEditor,
  data: 'text',
  load: TextEditor.prototype.format
};
JSONEditor.modes.code = {
  editor: TextEditor,
  data: 'text',
  load: TextEditor.prototype.format
};

/**
 * @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);
  }
};

/**
 * 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.editor.mode.edit) {
      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.editor.mode.edit) {
        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;
  }

  // create row
  dom.tr = document.createElement('tr');
  dom.tr.node = this;

  if (this.editor.mode.edit) {
    // create draggable area
    var tdDrag = document.createElement('td');
    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 == true) {
      // parent is an object
      domField.contentEditable = this.editor.mode.edit;
      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.editor.mode.edit && 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 and editable or read-only div
      domValue = document.createElement('div');
      domValue.contentEditable = !this.editor.mode.view;
      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.editor.mode.edit) {
          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;

  // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
  if (keynum == 13) { // Enter
    if (target == this.dom.value) {
      if (!this.editor.mode.edit || 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) {   // 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) { // M
    if (ctrlKey) { // Ctrl+M
      this.showContextMenu(target);
      handled = true;
    }
  }
  else if (keynum == 46) { // Del
    if (ctrlKey) {       // Ctrl+Del
      this._onRemove();
      handled = true;
    }
  }
  else if (keynum == 45) { // 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) { // 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) { // 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 = [];

  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()) {
    // 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');
          }
        }
      ]
    });

    // 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, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/  /g, ' &nbsp;') // replace double space with an nbsp and space
      .replace(/^ /, '&nbsp;')   // space at start
      .replace(/ $/, '&nbsp;');  // 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(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&nbsp;|\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;
};

/**
 * @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;
  }

  // a row for the append button
  var trAppend = document.createElement('tr');
  trAppend.node = this;
  dom.tr = trAppend;

  // TODO: consistent naming

  if (this.editor.mode.edit) {
    // 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);
  }
};

/**
 * 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 = '<div class="expand"></div>';
            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 = '<div class="icon"></div>' + 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.<HTMLElement>} 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;
};


/**
 * @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();
  }
};

/**
 * create a mode box to be used in the editor menu's
 * @param {JSONEditor} 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 createModeBox(editor, modes, current) {
  /**
   * 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 + ' &#x25BE;';
  box.title = 'Switch editor mode';
  box.onclick = function () {
    var menu = new ContextMenu(items);
    menu.show(box);
  };

  return box;
}

/**
 * @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&nbsp;results'; break;
        case 1: this.dom.results.innerHTML = '1&nbsp;result'; break;
        default: this.dom.results.innerHTML = resultCount + '&nbsp;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
  }
};

/**
 * 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;
};

// create namespace
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
 */
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);
    throw err;
  }
};

/**
 * 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 (Array.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);
};

/**
 * 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 <p> with hasChildNodes()==false is
      // rendered with a new line. Note that a <p> with
      // hasChildNodes()==true is rendered without a new line
      // Other browsers always ensure there is a <br> inside the <p>,
      // and if not, the <p> 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);
  }
};


// module exports
var jsoneditor = {
  'JSONEditor': JSONEditor,
  'JSONFormatter': function () {
    throw new Error('JSONFormatter is deprecated. ' +
        'Use JSONEditor with mode "text" or "code" instead');
  },
  'util': util
};

/**
 * load jsoneditor.css
 */
var loadCss = function () {
  // find the script named 'jsoneditor.js' or 'jsoneditor-min.js' or
  // 'jsoneditor.min.js', and use its path to find the css file to be
  // loaded.
  var scripts = document.getElementsByTagName('script');
  for (var s = 0; s < scripts.length; s++) {
    var src = scripts[s].src;
    if (/(^|\/)jsoneditor([-\.]min)?.js$/.test(src)) {
      var jsFile = src.split('?')[0];
      var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';

      // load css file
      var link = document.createElement('link');
      link.type = 'text/css';
      link.rel = 'stylesheet';
      link.href = cssFile;
      document.getElementsByTagName('head')[0].appendChild(link);

      break;
    }
  }
};

/**
 * CommonJS module exports
 */
if (typeof(module) != 'undefined' && typeof(exports) != 'undefined') {
  loadCss();
  module.exports = exports = jsoneditor;
}

/**
 * AMD module exports
 */
if (typeof(require) != 'undefined' && typeof(define) != 'undefined') {
  loadCss();
  define(function () {
    return jsoneditor;
  });
}
else {
  // attach the module to the window, load as a regular javascript file
  window['jsoneditor'] = jsoneditor;
}


})();
