text:
'use strict';

var naturalSort = require('javascript-natural-sort');
var ContextMenu = require('./ContextMenu');
var appendNodeFactory = require('./appendNodeFactory');
var util = require('./util');

/**
 * @constructor Node
 * Create a new Node
 * @param {./treemode} 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 {./treemode} */
  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);
  }

  this._debouncedOnChangeValue = util.debounce(this._onChangeValue.bind(this), Node.prototype.DEBOUNCE_INTERVAL);
  this._debouncedOnChangeField = util.debounce(this._onChangeField.bind(this), Node.prototype.DEBOUNCE_INTERVAL);
}

// debounce interval for keyboard input in milliseconds
Node.prototype.DEBOUNCE_INTERVAL = 150;

/**
 * Determine whether the field and/or value of this node are editable
 * @private
 */
Node.prototype._updateEditability = function () {
  this.editable = {
    field: true,
    value: true
  };

  if (this.editor) {
    this.editable.field = this.editor.options.mode === 'tree';
    this.editable.value = this.editor.options.mode !== 'view';

    if ((this.editor.options.mode === 'tree' || this.editor.options.mode === 'form') &&
        (typeof this.editor.options.onEditable === 'function')) {
      var editable = this.editor.options.onEditable({
        field: this.field,
        value: this.value,
        path: this.getPath()
      });

      if (typeof editable === 'boolean') {
        this.editable.field = editable;
        this.editable.value = editable;
      }
      else {
        if (typeof editable.field === 'boolean') this.editable.field = editable.field;
        if (typeof editable.value === 'boolean') this.editable.value = editable.value;
      }
    }
  }
};

/**
 * Get the path of this node
 * @return {String[]} Array containing the path to this node
 */
Node.prototype.getPath = function () {
  var node = this;
  var path = [];
  while (node) {
    var field = !node.parent
        ? undefined  // do not add an (optional) field name of the root node
        :  (node.parent.type != 'array')
            ? node.field
            : node.index;

    if (field !== undefined) {
      path.unshift(field);
    }
    node = node.parent;
  }
  return path;
};

/**
 * Find a Node from a JSON path like '.items[3].name'
 * @param {string} jsonPath
 * @return {Node | null} Returns the Node when found, returns null if not found
 */
Node.prototype.findNode = function (jsonPath) {
  var path = util.parsePath(jsonPath);
  var node = this;
  while (node && path.length > 0) {
    var prop = path.shift();
    if (typeof prop === 'number') {
      if (node.type !== 'array') {
        throw new Error('Cannot get child node at index ' + prop + ': node is no array');
      }
      node = node.childs[prop];
    }
    else { // string
      if (node.type !== 'object') {
        throw new Error('Cannot get child node ' + prop + ': node is no object');
      }
      node = node.childs.filter(function (child) {
        return child.field === prop;
      })[0];
    }
  }

  return node;
};

/**
 * Find all parents of this node. The parents are ordered from root node towards
 * the original node.
 * @return {Array.<Node>}
 */
Node.prototype.findParents = function () {
  var parents = [];
  var parent = this.parent;
  while (parent) {
    parents.unshift(parent);
    parent = parent.parent;
  }
  return parents;
};

/**
 *
 * @param {{dataPath: string, keyword: string, message: string, params: Object, schemaPath: string} | null} error
 * @param {Node} [child]  When this is the error of a parent node, pointing
 *                        to an invalid child node, the child node itself
 *                        can be provided. If provided, clicking the error
 *                        icon will set focus to the invalid child node.
 */
Node.prototype.setError = function (error, child) {
  // ensure the dom exists
  this.getDom();

  this.error = error;
  var tdError = this.dom.tdError;
  if (error) {
    if (!tdError) {
      tdError = document.createElement('td');
      this.dom.tdError = tdError;
      this.dom.tdValue.parentNode.appendChild(tdError);
    }

    var popover = document.createElement('div');
    popover.className = 'jsoneditor-popover jsoneditor-right';
    popover.appendChild(document.createTextNode(error.message));

    var button = document.createElement('button');
    button.type = 'button';
    button.className = 'jsoneditor-schema-error';
    button.appendChild(popover);

    // update the direction of the popover
    button.onmouseover = button.onfocus = function updateDirection() {
      var directions = ['right', 'above', 'below', 'left'];
      for (var i = 0; i < directions.length; i++) {
        var direction = directions[i];
        popover.className = 'jsoneditor-popover jsoneditor-' + direction;

        var contentRect = this.editor.content.getBoundingClientRect();
        var popoverRect = popover.getBoundingClientRect();
        var margin = 20; // account for a scroll bar
        var fit = util.insideRect(contentRect, popoverRect, margin);

        if (fit) {
          break;
        }
      }
    }.bind(this);

    // when clicking the error icon, expand all nodes towards the invalid
    // child node, and set focus to the child node
    if (child) {
      button.onclick = function showInvalidNode() {
        child.findParents().forEach(function (parent) {
          parent.expand(false);
        });

        child.scrollTo(function () {
          child.focus();
        });
      };
    }

    // apply the error message to the node
    while (tdError.firstChild) {
      tdError.removeChild(tdError.firstChild);
    }
    tdError.appendChild(button);
  }
  else {
    if (tdError) {
      this.dom.tdError.parentNode.removeChild(this.dom.tdError);
      delete this.dom.tdError;
    }
  }
};

/**
 * Get the index of this node: the index in the list of childs where this
 * node is part of
 * @return {number} Returns the index, or -1 if this is the root node
 */
Node.prototype.getIndex = function () {
  return this.parent ? this.parent.childs.indexOf(this) : -1;
};

/**
 * 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.previousField = 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 = '';

    // sort object keys
    if (this.editor.options.sortObjectKeys === true) {
      this.sort('asc');
    }
  }
  else {
    // value
    this.childs = undefined;
    this.value = value;
  }

  this.previousValue = this.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);
};

/**
 * Get jsonpath of the current node
 * @return {Node[]} Returns an array with nodes
 */
Node.prototype.getNodePath = function () {
  var path = this.parent ? this.parent.getNodePath() : [];
  path.push(this);
  return path;
};

/**
 * 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 = 'jsoneditor-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 = 'jsoneditor-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();
  });
};

/**
 * Goes through the path from the node to the root and ensures that it is expanded
 */
Node.prototype.expandTo = function() {
  var currentNode = this.parent;
  while (currentNode) {
    if (!currentNode.expanded) {
      currentNode.expand();
    }
    currentNode = currentNode.parent;
  }
};

/**
 * 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.select) {
          // enum select box
          dom.select.focus();
        }
        else 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);
};

/**
 * 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];
      removedNode.parent = null;

      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 fire 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) {
        this.value = value;
        this._debouncedOnChangeValue();
      }
    }
    catch (err) {
      this.value = undefined;
      // TODO: sent an action with the new, invalid value?
      if (silent !== true) {
        throw err;
      }
    }
  }
};

/**
 * Handle a changed value
 * @private
 */
Node.prototype._onChangeValue = function () {
  // get current selection, then override the range such that we can select
  // the added/removed text on undo/redo
  var oldSelection = this.editor.getSelection();
  if (oldSelection.range) {
    var undoDiff = util.textDiff(String(this.value), String(this.previousValue));
    oldSelection.range.startOffset = undoDiff.start;
    oldSelection.range.endOffset = undoDiff.end;
  }
  var newSelection = this.editor.getSelection();
  if (newSelection.range) {
    var redoDiff = util.textDiff(String(this.previousValue), String(this.value));
    newSelection.range.startOffset = redoDiff.start;
    newSelection.range.endOffset = redoDiff.end;
  }

  this.editor._onAction('editValue', {
    node: this,
    oldValue: this.previousValue,
    newValue: this.value,
    oldSelection: oldSelection,
    newSelection: newSelection
  });

  this.previousValue = this.value;
};

/**
 * Handle a changed field
 * @private
 */
Node.prototype._onChangeField = function () {
  // get current selection, then override the range such that we can select
  // the added/removed text on undo/redo
  var oldSelection = this.editor.getSelection();
  if (oldSelection.range) {
    var undoDiff = util.textDiff(this.field, this.previousField);
    oldSelection.range.startOffset = undoDiff.start;
    oldSelection.range.endOffset = undoDiff.end;
  }
  var newSelection = this.editor.getSelection();
  if (newSelection.range) {
    var redoDiff = util.textDiff(this.previousField, this.field);
    newSelection.range.startOffset = redoDiff.start;
    newSelection.range.endOffset = redoDiff.end;
  }

  this.editor._onAction('editField', {
    node: this,
    oldValue: this.previousField,
    newValue: this.field,
    oldSelection: oldSelection,
    newSelection: newSelection
  });

  this.previousField = this.field;
};

/**
 * 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) {
    var classNames = ['jsoneditor-value'];

    // set text color depending on value type
    var value = this.value;
    var type = (this.type == 'auto') ? util.type(value) : this.type;
    var isUrl = type == 'string' && util.isUrl(value);
    classNames.push('jsoneditor-' + type);
    if (isUrl) {
      classNames.push('jsoneditor-url');
    }

    // visual styling when empty
    var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
    if (isEmpty) {
      classNames.push('jsoneditor-empty');
    }

    // highlight when there is a search result
    if (this.searchValueActive) {
      classNames.push('jsoneditor-highlight-active');
    }
    if (this.searchValue) {
      classNames.push('jsoneditor-highlight');
    }

    domValue.className = classNames.join(' ');

    // update title
    if (type == 'array' || type == 'object') {
      var count = this.childs ? this.childs.length : 0;
      domValue.title = this.type + ' containing ' + count + ' items';
    }
    else if (isUrl && this.editable.value) {
      domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
    }
    else {
      domValue.title = '';
    }

    // show checkbox when the value is a boolean
    if (type === 'boolean' && this.editable.value) {
      if (!this.dom.checkbox) {
        this.dom.checkbox = document.createElement('input');
        this.dom.checkbox.type = 'checkbox';
        this.dom.tdCheckbox = document.createElement('td');
        this.dom.tdCheckbox.className = 'jsoneditor-tree';
        this.dom.tdCheckbox.appendChild(this.dom.checkbox);

        this.dom.tdValue.parentNode.insertBefore(this.dom.tdCheckbox, this.dom.tdValue);
      }

      this.dom.checkbox.checked = this.value;
    }
    else {
      // cleanup checkbox when displayed
      if (this.dom.tdCheckbox) {
        this.dom.tdCheckbox.parentNode.removeChild(this.dom.tdCheckbox);
        delete this.dom.tdCheckbox;
        delete this.dom.checkbox;
      }
    }

    if (this.enum && this.editable.value) {
      // create select box when this node has an enum object
      if (!this.dom.select) {
        this.dom.select = document.createElement('select');
        this.id = this.field + "_" + new Date().getUTCMilliseconds();
        this.dom.select.id = this.id;
        this.dom.select.name = this.dom.select.id;

        //Create the default empty option
        this.dom.select.option = document.createElement('option');
        this.dom.select.option.value = '';
        this.dom.select.option.innerHTML = '--';
        this.dom.select.appendChild(this.dom.select.option);

        //Iterate all enum values and add them as options
        for(var i = 0; i < this.enum.length; i++) {
          this.dom.select.option = document.createElement('option');
          this.dom.select.option.value = this.enum[i];
          this.dom.select.option.innerHTML = this.enum[i];
          if(this.dom.select.option.value == this.value){
            this.dom.select.option.selected = true;
          }
          this.dom.select.appendChild(this.dom.select.option);
        }

        this.dom.tdSelect = document.createElement('td');
        this.dom.tdSelect.className = 'jsoneditor-tree';
        this.dom.tdSelect.appendChild(this.dom.select);
        this.dom.tdValue.parentNode.insertBefore(this.dom.tdSelect, this.dom.tdValue);
      }

      // If the enum is inside a composite type display
      // both the simple input and the dropdown field
      if(this.schema && (
          !this.schema.hasOwnProperty("oneOf") &&
          !this.schema.hasOwnProperty("anyOf") &&
          !this.schema.hasOwnProperty("allOf"))
      ) {
        this.valueFieldHTML = this.dom.tdValue.innerHTML;
        this.dom.tdValue.style.visibility = 'hidden';
        this.dom.tdValue.innerHTML = '';
      } else {
        delete this.valueFieldHTML;
      }
    }
    else {
      // cleanup select box when displayed
      if (this.dom.tdSelect) {
        this.dom.tdSelect.parentNode.removeChild(this.dom.tdSelect);
        delete this.dom.tdSelect;
        delete this.dom.select;
        this.dom.tdValue.innerHTML = this.valueFieldHTML;
        this.dom.tdValue.style.visibility = '';
        delete this.valueFieldHTML;
      }
    }

    // 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, 'jsoneditor-empty');
    }
    else {
      util.removeClassName(domField, 'jsoneditor-empty');
    }

    // highlight when there is a search result
    if (this.searchFieldActive) {
      util.addClassName(domField, 'jsoneditor-highlight-active');
    }
    else {
      util.removeClassName(domField, 'jsoneditor-highlight-active');
    }
    if (this.searchField) {
      util.addClassName(domField, 'jsoneditor-highlight');
    }
    else {
      util.removeClassName(domField, 'jsoneditor-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) {
        this.field = field;
        this._debouncedOnChangeField();
      }
    }
    catch (err) {
      this.field = undefined;
      // TODO: sent an action here, with the new, invalid value?
      if (silent !== true) {
        throw err;
      }
    }
  }
};

/**
 * Validate this node and all it's childs
 * @return {Array.<{node: Node, error: {message: string}}>} Returns a list with duplicates
 */
Node.prototype.validate = function () {
  var errors = [];

  // find duplicate keys
  if (this.type === 'object') {
    var keys = {};
    var duplicateKeys = [];
    for (var i = 0; i < this.childs.length; i++) {
      var child = this.childs[i];
      if (keys.hasOwnProperty(child.field)) {
        duplicateKeys.push(child.field);
      }
      keys[child.field] = true;
    }

    if (duplicateKeys.length > 0) {
      errors = this.childs
          .filter(function (node) {
            return duplicateKeys.indexOf(node.field) !== -1;
          })
          .map(function (node) {
            return {
              node: node,
              error: {
                message: 'duplicate key "' + node.field + '"'
              }
            }
          });
    }
  }

  // recurse over the childs
  if (this.childs) {
    for (var i = 0; i < this.childs.length; i++) {
      var e = this.childs[i].validate();
      if (e.length > 0) {
        errors = errors.concat(e);
      }
    }
  }

  return errors;
};

/**
 * Clear the dom of the node
 */
Node.prototype.clearDom = function() {
  // TODO: hide the node first?
  //this.hide();
  // TODO: recursively clear dom?

  this.dom = {};
};

/**
 * Get the HTML DOM TR element of the node.
 * The dom will be generated when not yet created
 * @return {Element} tr    HTML DOM TR Element
 */
Node.prototype.getDom = function() {
  var dom = this.dom;
  if (dom.tr) {
    return dom.tr;
  }

  this._updateEditability();

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

  if (this.editor.options.mode === 'tree') { // note: we take here the global setting
    var tdDrag = document.createElement('td');
    if (this.editable.field) {
      // create draggable area
      if (this.parent) {
        var domDrag = document.createElement('button');
        domDrag.type = 'button';
        dom.drag = domDrag;
        domDrag.className = 'jsoneditor-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');
    menu.type = 'button';
    dom.menu = menu;
    menu.className = 'jsoneditor-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 {Node[] | Node} nodes
 * @param {Event} event
 */
Node.onDragStart = function (nodes, event) {
  if (!Array.isArray(nodes)) {
    return Node.onDragStart([nodes], event);
  }
  if (nodes.length === 0) {
    return;
  }

  var firstNode = nodes[0];
  var lastNode = nodes[nodes.length - 1];
  var draggedNode = Node.getNodeFromTarget(event.target);
  var beforeNode = lastNode._nextSibling();
  var editor = firstNode.editor;

  // in case of multiple selected nodes, offsetY prevents the selection from
  // jumping when you start dragging one of the lower down nodes in the selection
  var offsetY = util.getAbsoluteTop(draggedNode.dom.tr) - util.getAbsoluteTop(firstNode.dom.tr);

  if (!editor.mousemove) {
    editor.mousemove = util.addEventListener(window, 'mousemove', function (event) {
      Node.onDrag(nodes, event);
    });
  }

  if (!editor.mouseup) {
    editor.mouseup = util.addEventListener(window, 'mouseup',function (event ) {
      Node.onDragEnd(nodes, event);
    });
  }

  editor.highlighter.lock();
  editor.drag = {
    oldCursor: document.body.style.cursor,
    oldSelection: editor.getSelection(),
    oldBeforeNode: beforeNode,
    mouseX: event.pageX,
    offsetY: offsetY,
    level: firstNode.getLevel()
  };
  document.body.style.cursor = 'move';

  event.preventDefault();
};

/**
 * Drag event, fired when moving the mouse while dragging a Node
 * @param {Node[] | Node} nodes
 * @param {Event} event
 */
Node.onDrag = function (nodes, event) {
  if (!Array.isArray(nodes)) {
    return Node.onDrag([nodes], event);
  }
  if (nodes.length === 0) {
    return;
  }

  // TODO: this method has grown too large. Split it in a number of methods
  var editor = nodes[0].editor;
  var mouseY = event.pageY - editor.drag.offsetY;
  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
  var firstNode = nodes[0];
  trThis = firstNode.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 == firstNode) {
        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) {
      nodes.forEach(function (node) {
        nodePrev.parent.moveBefore(node, nodePrev);
      });
      moved = true;
    }
  }
  else {
    // move down
    var lastNode = nodes[nodes.length - 1];
    trLast = (lastNode.expanded && lastNode.append) ? lastNode.append.getDom() : lastNode.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 == nodes.length &&
              nodeNext.parent.childs[nodes.length - 1] == lastNode) {
            // We are about to remove the last child of this parent,
            // which will make the parents appendNode visible.
            topThis += 27;
            // TODO: dangerous to suppose the height of the appendNode a constant of 27 px.
          }
        }

        trNext = trNext.nextSibling;
      }
      while (trNext && mouseY > topThis + heightNext);

      if (nodeNext && nodeNext.parent) {
        // calculate the desired level
        var diffX = (mouseX - editor.drag.mouseX);
        var diffLevel = Math.round(diffX / 24 / 2);
        var level = editor.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);

          var isDraggedNode = nodes.some(function (node) {
            return node === nodePrev || nodePrev._isChildOf(node);
          });

          if (isDraggedNode) {
            // neglect the dragged nodes themselves and their childs
          }
          else if (nodePrev instanceof AppendNode) {
            var childs = nodePrev.parent.childs;
            if (childs.length != nodes.length || childs[nodes.length - 1] != lastNode) {
              // 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) {
          nodes.forEach(function (node) {
            nodeNext.parent.moveBefore(node, nodeNext);
          });
          moved = true;
        }
      }
    }
  }

  if (moved) {
    // update the dragging parameters when moved
    editor.drag.mouseX = mouseX;
    editor.drag.level = firstNode.getLevel();
  }

  // auto scroll when hovering around the top of the editor
  editor.startAutoScroll(mouseY);

  event.preventDefault();
};

/**
 * Drag event, fired on mouseup after having dragged a node
 * @param {Node[] | Node} nodes
 * @param {Event} event
 */
Node.onDragEnd = function (nodes, event) {
  if (!Array.isArray(nodes)) {
    return Node.onDrag([nodes], event);
  }
  if (nodes.length === 0) {
    return;
  }

  var firstNode = nodes[0];
  var editor = firstNode.editor;
  var parent = firstNode.parent;
  var firstIndex = parent.childs.indexOf(firstNode);
  var beforeNode = parent.childs[firstIndex + nodes.length] || parent.append;

  // set focus to the context menu button of the first node
  if (nodes[0]) {
    nodes[0].dom.menu.focus();
  }

  var params = {
    nodes: nodes,
    oldSelection: editor.drag.oldSelection,
    newSelection: editor.getSelection(),
    oldBeforeNode: editor.drag.oldBeforeNode,
    newBeforeNode: beforeNode
  };

  if (params.oldBeforeNode != params.newBeforeNode) {
    // only register this action if the node is actually moved to another place
    editor._onAction('moveNodes', params);
  }

  document.body.style.cursor = editor.drag.oldCursor;
  editor.highlighter.unlock();
  nodes.forEach(function (node) {
    if (event.target !== node.dom.drag && event.target !== node.dom.menu) {
      editor.highlighter.unhighlight();
    }
  });
  delete editor.drag;

  if (editor.mousemove) {
    util.removeEventListener(window, 'mousemove', editor.mousemove);
    delete editor.mousemove;
  }
  if (editor.mouseup) {
    util.removeEventListener(window, 'mouseup', editor.mouseup);
    delete editor.mouseup;
  }

  // Stop any running auto scroll
  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) {
    if (highlight) {
      util.addClassName(this.dom.tr, 'jsoneditor-highlight');
    }
    else {
      util.removeClassName(this.dom.tr, 'jsoneditor-highlight');
    }

    if (this.append) {
      this.append.setHighlight(highlight);
    }

    if (this.childs) {
      this.childs.forEach(function (child) {
        child.setHighlight(highlight);
      });
    }
  }
};

/**
 * Select or deselect a node
 * @param {boolean} selected
 * @param {boolean} [isFirst]
 */
Node.prototype.setSelected = function (selected, isFirst) {
  this.selected = selected;

  if (this.dom.tr) {
    if (selected) {
      util.addClassName(this.dom.tr, 'jsoneditor-selected');
    }
    else {
      util.removeClassName(this.dom.tr, 'jsoneditor-selected');
    }

    if (isFirst) {
      util.addClassName(this.dom.tr, 'jsoneditor-first');
    }
    else {
      util.removeClassName(this.dom.tr, 'jsoneditor-first');
    }

    if (this.append) {
      this.append.setSelected(selected);
    }

    if (this.childs) {
      this.childs.forEach(function (child) {
        child.setSelected(selected);
      });
    }
  }
};

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

  // apply field to DOM
  var domField = this.dom.field;
  if (domField) {
    if (this.fieldEditable) {
      // parent is an object
      domField.contentEditable = this.editable.field;
      domField.spellcheck = false;
      domField.className = 'jsoneditor-field';
    }
    else {
      // parent is an array this is the root node
      domField.className = 'jsoneditor-readonly';
    }

    var fieldText;
    if (this.index != undefined) {
      fieldText = this.index;
    }
    else if (this.field != undefined) {
      fieldText = this.field;
    }
    else if (this._hasChilds()) {
      fieldText = this.type;
    }
    else {
      fieldText = '';
    }
    domField.innerHTML = this._escapeHTML(fieldText);

    this._updateSchema();
  }

  // apply value to DOM
  var domValue = this.dom.value;
  if (domValue) {
    var count = this.childs ? this.childs.length : 0;
    if (this.type == 'array') {
      domValue.innerHTML = '[' + count + ']';
      util.addClassName(this.dom.tr, 'jsoneditor-expandable');
    }
    else if (this.type == 'object') {
      domValue.innerHTML = '{' + count + '}';
      util.addClassName(this.dom.tr, 'jsoneditor-expandable');
    }
    else {
      domValue.innerHTML = this._escapeHTML(this.value);
      util.removeClassName(this.dom.tr, 'jsoneditor-expandable');
    }
  }

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

/**
 * Locate the JSON schema of the node and check for any enum type
 * @private
 */
Node.prototype._updateSchema = function () {
  //Locating the schema of the node and checking for any enum type
  if(this.editor && this.editor.options) {
    // find the part of the json schema matching this nodes path
    this.schema = this.editor.options.schema
        ? Node._findSchema(this.editor.options.schema, this.getPath())
        : null;
    if (this.schema) {
      this.enum = Node._findEnum(this.schema);
    }
    else {
      delete this.enum;
    }
  }
};

/**
 * find an enum definition in a JSON schema, as property `enum` or inside
 * one of the schemas composites (`oneOf`, `anyOf`, `allOf`)
 * @param  {Object} schema
 * @return {Array | null} Returns the enum when found, null otherwise.
 * @private
 */
Node._findEnum = function (schema) {
  if (schema.enum) {
    return schema.enum;
  }

  var composite = schema.oneOf || schema.anyOf || schema.allOf;
  if (composite) {
    var match = composite.filter(function (entry) {return entry.enum});
    if (match.length > 0) {
      return match[0].enum;
    }
  }

  return null
};

/**
 * Return the part of a JSON schema matching given path.
 * @param {Object} schema
 * @param {Array.<string | number>} path
 * @return {Object | null}
 * @private
 */
Node._findSchema = function (schema, path) {
  var childSchema = schema;
  var foundSchema = childSchema;

  var allSchemas = schema.oneOf || schema.anyOf || schema.allOf;
  if (!allSchemas) {
    allSchemas = [schema];
  }

  for (var j = 0; j < allSchemas.length; j++) {
    childSchema = allSchemas[j];

    for (var i = 0; i < path.length && childSchema; i++) {
      var key = path[i];

      if (typeof key === 'string' && childSchema.patternProperties && i == path.length - 1) {
        for (var prop in childSchema.patternProperties) {
          foundSchema = Node._findSchema(childSchema.patternProperties[prop], path.slice(i, path.length));
        }
      }
      else if (childSchema.items && childSchema.items.properties) {
        childSchema = childSchema.items.properties[key];
        if (childSchema) {
          foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
        }
      }
      else if (typeof key === 'string' && childSchema.properties) {
        childSchema = childSchema.properties[key] || null;
        if (childSchema) {
          foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
        }
      }
      else if (typeof key === 'number' && childSchema.items) {
        childSchema = childSchema.items;
        if (childSchema) {
          foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
        }
      }
    }

  }
  return foundSchema
};

/**
 * 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.innerHTML = '[...]';
  }
  else if (this.type == 'object') {
    domValue = document.createElement('div');
    domValue.innerHTML = '{...}';
  }
  else {
    if (!this.editable.value && util.isUrl(this.value)) {
      // create a link in case of read-only editor and value containing an url
      domValue = document.createElement('a');
      domValue.href = this.value;
      domValue.innerHTML = this._escapeHTML(this.value);
    }
    else {
      // create an editable or read-only div
      domValue = document.createElement('div');
      domValue.contentEditable = this.editable.value;
      domValue.spellcheck = false;
      domValue.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');
  expand.type = 'button';
  if (this._hasChilds()) {
    expand.className = this.expanded ? 'jsoneditor-expanded' : 'jsoneditor-collapsed';
    expand.title =
        'Click to expand/collapse this field (Ctrl+E). \n' +
        'Ctrl+Click to expand/collapse including all childs.';
  }
  else {
    expand.className = 'jsoneditor-invisible';
    expand.title = '';
  }

  return expand;
};

/**
 * Create a DOM tree element, containing the expand/collapse button
 * @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 = 'jsoneditor-values';
  domTree.appendChild(tbody);
  var tr = document.createElement('tr');
  tbody.appendChild(tr);

  // create expand button
  var tdExpand = document.createElement('td');
  tdExpand.className = 'jsoneditor-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 = 'jsoneditor-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 = 'jsoneditor-tree';
  tr.appendChild(tdSeparator);
  if (this.type != 'object' && this.type != 'array') {
    tdSeparator.appendChild(document.createTextNode(':'));
    tdSeparator.className = 'jsoneditor-separator';
  }
  dom.tdSeparator = tdSeparator;

  // create the value
  var tdValue = document.createElement('td');
  tdValue.className = 'jsoneditor-tree';
  tr.appendChild(tdValue);
  dom.value = this._createDomValue();
  tdValue.appendChild(dom.value);
  dom.tdValue = tdValue;

  return domTree;
};

/**
 * Handle an event. The event is caught 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,
      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();
    }
  }

  // context menu events
  if (type == 'click' && target == dom.menu) {
    var highlighter = node.editor.highlighter;
    highlighter.highlight(node);
    highlighter.lock();
    util.addClassName(dom.menu, 'jsoneditor-selected');
    this.showContextMenu(dom.menu, function () {
      util.removeClassName(dom.menu, 'jsoneditor-selected');
      highlighter.unlock();
      highlighter.unhighlight();
    });
  }

  // expand events
  if (type == 'click') {
    if (target == dom.expand ||
        ((node.editor.options.mode === 'view' || node.editor.options.mode === 'form') && target.nodeName === 'DIV')) {
      if (expandable) {
        var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
        this._onExpand(recurse);
      }
    }
  }

  // swap the value of a boolean when the checkbox displayed left is clicked
  if (type == 'change' && target == dom.checkbox) {
    this.dom.value.innerHTML = !this.value;
    this._getDomValue();
  }

  // update the value of the node based on the selected option
  if (type == 'change' && target == dom.select) {
    this.dom.value.innerHTML = dom.select.value;
    this._getDomValue();
    this._updateDomValue();
  }

  // value events
  var domValue = dom.value;
  if (target == domValue) {
    //noinspection FallthroughInSwitchStatementJS
    switch (type) {
      case 'blur':
      case 'change':
        this._getDomValue(true);
        this._updateDomValue();
        if (this.value) {
          domValue.innerHTML = this._escapeHTML(this.value);
        }
        break;

      case 'input':
        //this._debouncedGetDomValue(true); // TODO
        this._getDomValue(true);
        this._updateDomValue();
        break;

      case 'keydown':
      case 'mousedown':
          // TODO: cleanup
        this.editor.selection = this.editor.getSelection();
        break;

      case 'click':
        if (event.ctrlKey && this.editable.value) {
          // if read-only, we use the regular click behavior of an anchor
          if (util.isUrl(this.value)) {
            event.preventDefault();
            window.open(this.value, '_blank');
          }
        }
        break;

      case 'keyup':
        //this._debouncedGetDomValue(true); // TODO
        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 'blur':
      case 'change':
        this._getDomField(true);
        this._updateDomField();
        if (this.field) {
          domField.innerHTML = this._escapeHTML(this.field);
        }
        break;

      case 'input':
        this._getDomField(true);
        this._updateSchema();
        this._updateDomField();
        this._updateDomValue();
        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 && type == 'click' && !event.hasMoved) {
    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 && !this.enum) {
        util.setEndOfContentEditable(domValue);
        domValue.focus();
      }
    }
  }
  if (((target == dom.tdExpand && !expandable) || target == dom.tdField || target == dom.tdSeparator) &&
      (type == 'click' && !event.hasMoved)) {
    if (domField) {
      util.setEndOfContentEditable(domField);
      domField.focus();
    }
  }

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

/**
 * Key down event handler
 * @param {Event} event
 */
Node.prototype.onKeyDown = function (event) {
  var keynum = event.which || event.keyCode;
  var target = event.target || event.srcElement;
  var ctrlKey = event.ctrlKey;
  var shiftKey = event.shiftKey;
  var altKey = event.altKey;
  var handled = false;
  var prevNode, nextNode, nextDom, nextDom2;
  var editable = this.editor.options.mode === 'tree';
  var oldSelection;
  var oldBeforeNode;
  var nodes;
  var multiselection;
  var selectedNodes = this.editor.multiselection.nodes.length > 0
      ? this.editor.multiselection.nodes
      : [this];
  var firstNode = selectedNodes[0];
  var lastNode = selectedNodes[selectedNodes.length - 1];

  // console.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
  if (keynum == 13) { // Enter
    if (target == this.dom.value) {
      if (!this.editable.value || event.ctrlKey) {
        if (util.isUrl(this.value)) {
          window.open(this.value, '_blank');
          handled = true;
        }
      }
    }
    else if (target == this.dom.expand) {
      var expandable = this._hasChilds();
      if (expandable) {
        var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
        this._onExpand(recurse);
        target.focus();
        handled = true;
      }
    }
  }
  else if (keynum == 68) {  // D
    if (ctrlKey && editable) {   // Ctrl+D
      Node.onDuplicate(selectedNodes);
      handled = true;
    }
  }
  else if (keynum == 69) { // E
    if (ctrlKey) {       // Ctrl+E and Ctrl+Shift+E
      this._onExpand(shiftKey);  // recurse = shiftKey
      target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
      handled = true;
    }
  }
  else if (keynum == 77 && editable) { // M
    if (ctrlKey) { // Ctrl+M
      this.showContextMenu(target);
      handled = true;
    }
  }
  else if (keynum == 46 && editable) { // Del
    if (ctrlKey) {       // Ctrl+Del
      Node.onRemove(selectedNodes);
      handled = true;
    }
  }
  else if (keynum == 45 && editable) { // Ins
    if (ctrlKey && !shiftKey) {       // Ctrl+Ins
      this._onInsertBefore();
      handled = true;
    }
    else if (ctrlKey && shiftKey) {   // Ctrl+Shift+Ins
      this._onInsertAfter();
      handled = true;
    }
  }
  else if (keynum == 35) { // End
    if (altKey) { // Alt+End
      // find the last node
      var endNode = this._lastNode();
      if (endNode) {
        endNode.focus(Node.focusElement || this._getElementName(target));
      }
      handled = true;
    }
  }
  else if (keynum == 36) { // Home
    if (altKey) { // Alt+Home
      // find the first node
      var homeNode = this._firstNode();
      if (homeNode) {
        homeNode.focus(Node.focusElement || this._getElementName(target));
      }
      handled = true;
    }
  }
  else if (keynum == 37) {        // Arrow Left
    if (altKey && !shiftKey) {  // Alt + Arrow Left
      // move to left element
      var prevElement = this._previousElement(target);
      if (prevElement) {
        this.focus(this._getElementName(prevElement));
      }
      handled = true;
    }
    else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow left
      if (lastNode.expanded) {
        var appendDom = lastNode.getAppend();
        nextDom = appendDom ? appendDom.nextSibling : undefined;
      }
      else {
        var dom = lastNode.getDom();
        nextDom = dom.nextSibling;
      }
      if (nextDom) {
        nextNode = Node.getNodeFromTarget(nextDom);
        nextDom2 = nextDom.nextSibling;
        nextNode2 = Node.getNodeFromTarget(nextDom2);
        if (nextNode && nextNode instanceof AppendNode &&
            !(lastNode.parent.childs.length == 1) &&
            nextNode2 && nextNode2.parent) {
          oldSelection = this.editor.getSelection();
          oldBeforeNode = lastNode._nextSibling();

          selectedNodes.forEach(function (node) {
            nextNode2.parent.moveBefore(node, nextNode2);
          });
          this.focus(Node.focusElement || this._getElementName(target));

          this.editor._onAction('moveNodes', {
            nodes: selectedNodes,
            oldBeforeNode: oldBeforeNode,
            newBeforeNode: nextNode2,
            oldSelection: oldSelection,
            newSelection: this.editor.getSelection()
          });
        }
      }
    }
  }
  else if (keynum == 38) {        // Arrow Up
    if (altKey && !shiftKey) {  // Alt + Arrow Up
      // find the previous node
      prevNode = this._previousNode();
      if (prevNode) {
        this.editor.deselect(true);
        prevNode.focus(Node.focusElement || this._getElementName(target));
      }
      handled = true;
    }
    else if (!altKey && ctrlKey && shiftKey && editable) { // Ctrl + Shift + Arrow Up
      // select multiple nodes
      prevNode = this._previousNode();
      if (prevNode) {
        multiselection = this.editor.multiselection;
        multiselection.start = multiselection.start || this;
        multiselection.end = prevNode;
        nodes = this.editor._findTopLevelNodes(multiselection.start, multiselection.end);

        this.editor.select(nodes);
        prevNode.focus('field'); // select field as we know this always exists
      }
      handled = true;
    }
    else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Up
      // find the previous node
      prevNode = firstNode._previousNode();
      if (prevNode && prevNode.parent) {
        oldSelection = this.editor.getSelection();
        oldBeforeNode = lastNode._nextSibling();

        selectedNodes.forEach(function (node) {
          prevNode.parent.moveBefore(node, prevNode);
        });
        this.focus(Node.focusElement || this._getElementName(target));

        this.editor._onAction('moveNodes', {
          nodes: selectedNodes,
          oldBeforeNode: oldBeforeNode,
          newBeforeNode: prevNode,
          oldSelection: oldSelection,
          newSelection: this.editor.getSelection()
        });
      }
      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 && editable) { // Alt + Shift + Arrow Right
      dom = firstNode.getDom();
      var prevDom = dom.previousSibling;
      if (prevDom) {
        prevNode = Node.getNodeFromTarget(prevDom);
        if (prevNode && prevNode.parent &&
            (prevNode instanceof AppendNode)
            && !prevNode.isVisible()) {
          oldSelection = this.editor.getSelection();
          oldBeforeNode = lastNode._nextSibling();

          selectedNodes.forEach(function (node) {
            prevNode.parent.moveBefore(node, prevNode);
          });
          this.focus(Node.focusElement || this._getElementName(target));

          this.editor._onAction('moveNodes', {
            nodes: selectedNodes,
            oldBeforeNode: oldBeforeNode,
            newBeforeNode: prevNode,
            oldSelection: oldSelection,
            newSelection: this.editor.getSelection()
          });
        }
      }
    }
  }
  else if (keynum == 40) {        // Arrow Down
    if (altKey && !shiftKey) {  // Alt + Arrow Down
      // find the next node
      nextNode = this._nextNode();
      if (nextNode) {
        this.editor.deselect(true);
        nextNode.focus(Node.focusElement || this._getElementName(target));
      }
      handled = true;
    }
    else if (!altKey && ctrlKey && shiftKey && editable) { // Ctrl + Shift + Arrow Down
      // select multiple nodes
      nextNode = this._nextNode();
      if (nextNode) {
        multiselection = this.editor.multiselection;
        multiselection.start = multiselection.start || this;
        multiselection.end = nextNode;
        nodes = this.editor._findTopLevelNodes(multiselection.start, multiselection.end);

        this.editor.select(nodes);
        nextNode.focus('field'); // select field as we know this always exists
      }
      handled = true;
    }
    else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Down
      // find the 2nd next node and move before that one
      if (lastNode.expanded) {
        nextNode = lastNode.append ? lastNode.append._nextNode() : undefined;
      }
      else {
        nextNode = lastNode._nextNode();
      }
      var nextNode2 = nextNode && (nextNode._nextNode() || nextNode.parent.append);
      if (nextNode2 && nextNode2.parent) {
        oldSelection = this.editor.getSelection();
        oldBeforeNode = lastNode._nextSibling();

        selectedNodes.forEach(function (node) {
          nextNode2.parent.moveBefore(node, nextNode2);
        });
        this.focus(Node.focusElement || this._getElementName(target));

        this.editor._onAction('moveNodes', {
          nodes: selectedNodes,
          oldBeforeNode: oldBeforeNode,
          newBeforeNode: nextNode2,
          oldSelection: oldSelection,
          newSelection: this.editor.getSelection()
        });
      }
      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 nodes
 * @param {Node[] | Node} nodes
 */
Node.onRemove = function(nodes) {
  if (!Array.isArray(nodes)) {
    return Node.onRemove([nodes]);
  }

  if (nodes && nodes.length > 0) {
    var firstNode = nodes[0];
    var parent = firstNode.parent;
    var editor = firstNode.editor;
    var firstIndex = firstNode.getIndex();
    editor.highlighter.unhighlight();

    // adjust the focus
    var oldSelection = editor.getSelection();
    Node.blurNodes(nodes);
    var newSelection = editor.getSelection();

    // remove the nodes
    nodes.forEach(function (node) {
      node.parent._remove(node);
    });

    // store history action
    editor._onAction('removeNodes', {
      nodes: nodes.slice(0), // store a copy of the array!
      parent: parent,
      index: firstIndex,
      oldSelection: oldSelection,
      newSelection: newSelection
    });
  }
};

/**
 * Duplicate nodes
 * duplicated nodes will be added right after the original nodes
 * @param {Node[] | Node} nodes
 */
Node.onDuplicate = function(nodes) {
  if (!Array.isArray(nodes)) {
    return Node.onDuplicate([nodes]);
  }

  if (nodes && nodes.length > 0) {
    var lastNode = nodes[nodes.length - 1];
    var parent = lastNode.parent;
    var editor = lastNode.editor;

    editor.deselect(editor.multiselection.nodes);

    // duplicate the nodes
    var oldSelection = editor.getSelection();
    var afterNode = lastNode;
    var clones = nodes.map(function (node) {
      var clone = node.clone();
      parent.insertAfter(clone, afterNode);
      afterNode = clone;
      return clone;
    });

    // set selection to the duplicated nodes
    if (nodes.length === 1) {
      clones[0].focus();
    }
    else {
      editor.select(clones);
    }
    var newSelection = editor.getSelection();

    editor._onAction('duplicateNodes', {
      afterNode: lastNode,
      nodes: clones,
      parent: 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('insertBeforeNodes', {
    nodes: [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('insertAfterNodes', {
    nodes: [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('appendNodes', {
    nodes: [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 child's 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.sort = function (direction) {
  if (!this._hasChilds()) {
    return;
  }

  var order = (direction == 'desc') ? -1 : 1;
  var prop = (this.type == 'array') ? 'value': 'field';
  this.hideChilds();

  var oldChilds = this.childs;
  var oldSortOrder = this.sortOrder;

  // 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) {
    return order * naturalSort(a[prop], b[prop]);
  });
  this.sortOrder = (order == 1) ? 'asc' : 'desc';

  this.editor._onAction('sort', {
    node: this,
    oldChilds: oldChilds,
    oldSort: oldSortOrder,
    newChilds: this.childs,
    newSort: this.sortOrder
  });

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

/**
 * Remove the focus of given nodes, and move the focus to the (a) node before,
 * (b) the node after, or (c) the parent node.
 * @param {Array.<Node> | Node} nodes
 */
Node.blurNodes = function (nodes) {
  if (!Array.isArray(nodes)) {
    Node.blurNodes([nodes]);
    return;
  }

  var firstNode = nodes[0];
  var parent = firstNode.parent;
  var firstIndex = firstNode.getIndex();

  if (parent.childs[firstIndex + nodes.length]) {
    parent.childs[firstIndex + nodes.length].focus();
  }
  else if (parent.childs[firstIndex - 1]) {
    parent.childs[firstIndex - 1].focus();
  }
  else {
    parent.focus();
  }
};

/**
 * Get the next sibling of current node
 * @return {Node} nextSibling
 * @private
 */
Node.prototype._nextSibling = function () {
  var index = this.parent.childs.indexOf(this);
  return this.parent.childs[index + 1] || this.parent.append;
};

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

Node.prototype.addTemplates = function (menu, append) {
    var node = this;
    var templates = node.editor.options.templates;
    if (templates == null) return;
    if (templates.length) {
        // create a separator
        menu.push({
            'type': 'separator'
        });
    }
    var appendData = function (name, data) {
        node._onAppend(name, data);
    };
    var insertData = function (name, data) {
        node._onInsertBefore(name, data);
    };
    templates.forEach(function (template) {
        menu.push({
            text: template.text,
            className: (template.className || 'jsoneditor-type-object'),
            title: template.title,
            click: (append ? appendData.bind(this, template.field, template.value) : insertData.bind(this, template.field, template.value))
        });
    });
};

/**
 * Show a contextmenu for this node
 * @param {HTMLElement} anchor   Anchor element to attach the context menu to
 *                               as sibling.
 * @param {function} [onClose]   Callback method called when the context menu
 *                               is being closed.
 */
Node.prototype.showContextMenu = function (anchor, onClose) {
  var node = this;
  var titles = Node.TYPE_TITLES;
  var items = [];

  if (this.editable.value) {
    items.push({
      text: 'Type',
      title: 'Change the type of this field',
      className: 'jsoneditor-type-' + this.type,
      submenu: [
        {
          text: 'Auto',
          className: 'jsoneditor-type-auto' +
              (this.type == 'auto' ? ' jsoneditor-selected' : ''),
          title: titles.auto,
          click: function () {
            node._onChangeType('auto');
          }
        },
        {
          text: 'Array',
          className: 'jsoneditor-type-array' +
              (this.type == 'array' ? ' jsoneditor-selected' : ''),
          title: titles.array,
          click: function () {
            node._onChangeType('array');
          }
        },
        {
          text: 'Object',
          className: 'jsoneditor-type-object' +
              (this.type == 'object' ? ' jsoneditor-selected' : ''),
          title: titles.object,
          click: function () {
            node._onChangeType('object');
          }
        },
        {
          text: 'String',
          className: 'jsoneditor-type-string' +
              (this.type == 'string' ? ' jsoneditor-selected' : ''),
          title: titles.string,
          click: function () {
            node._onChangeType('string');
          }
        }
      ]
    });
  }

  if (this._hasChilds()) {
    var direction = ((this.sortOrder == 'asc') ? 'desc': 'asc');
    items.push({
      text: 'Sort',
      title: 'Sort the childs of this ' + this.type,
      className: 'jsoneditor-sort-' + direction,
      click: function () {
        node.sort(direction);
      },
      submenu: [
        {
          text: 'Ascending',
          className: 'jsoneditor-sort-asc',
          title: 'Sort the childs of this ' + this.type + ' in ascending order',
          click: function () {
            node.sort('asc');
          }
        },
        {
          text: 'Descending',
          className: 'jsoneditor-sort-desc',
          title: 'Sort the childs of this ' + this.type +' in descending order',
          click: function () {
            node.sort('desc');
          }
        }
      ]
    });
  }

  if (this.parent && this.parent._hasChilds()) {
    if (items.length) {
      // create a separator
      items.push({
        'type': 'separator'
      });
    }

    // create append button (for last child node only)
    var childs = node.parent.childs;
    if (node == childs[childs.length - 1]) {
        var appendSubmenu = [
            {
                text: 'Auto',
                className: 'jsoneditor-type-auto',
                title: titles.auto,
                click: function () {
                    node._onAppend('', '', 'auto');
                }
            },
            {
                text: 'Array',
                className: 'jsoneditor-type-array',
                title: titles.array,
                click: function () {
                    node._onAppend('', []);
                }
            },
            {
                text: 'Object',
                className: 'jsoneditor-type-object',
                title: titles.object,
                click: function () {
                    node._onAppend('', {});
                }
            },
            {
                text: 'String',
                className: 'jsoneditor-type-string',
                title: titles.string,
                click: function () {
                    node._onAppend('', '', 'string');
                }
            }
        ];
        node.addTemplates(appendSubmenu, true);
        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: 'jsoneditor-append',
            click: function () {
                node._onAppend('', '', 'auto');
            },
            submenu: appendSubmenu
        });
    }

    // create insert button
    var insertSubmenu = [
        {
            text: 'Auto',
            className: 'jsoneditor-type-auto',
            title: titles.auto,
            click: function () {
                node._onInsertBefore('', '', 'auto');
            }
        },
        {
            text: 'Array',
            className: 'jsoneditor-type-array',
            title: titles.array,
            click: function () {
                node._onInsertBefore('', []);
            }
        },
        {
            text: 'Object',
            className: 'jsoneditor-type-object',
            title: titles.object,
            click: function () {
                node._onInsertBefore('', {});
            }
        },
        {
            text: 'String',
            className: 'jsoneditor-type-string',
            title: titles.string,
            click: function () {
                node._onInsertBefore('', '', 'string');
            }
        }
    ];
    node.addTemplates(insertSubmenu, false);
    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: 'jsoneditor-insert',
      click: function () {
        node._onInsertBefore('', '', 'auto');
      },
      submenu: insertSubmenu
    });

    if (this.editable.field) {
      // create duplicate button
      items.push({
        text: 'Duplicate',
        title: 'Duplicate this field (Ctrl+D)',
        className: 'jsoneditor-duplicate',
        click: function () {
          Node.onDuplicate(node);
        }
      });

      // create remove button
      items.push({
        text: 'Remove',
        title: 'Remove this field (Ctrl+Del)',
        className: 'jsoneditor-remove',
        click: function () {
          Node.onRemove(node);
        }
      });
    }
  }

  var menu = new ContextMenu(items, {close: onClose});
  menu.show(anchor, this.editor.content);
};

/**
 * 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) {
  if (typeof text !== 'string') {
    return String(text);
  }
  else {
    var htmlEscaped = String(text)
        .replace(/&/g, '&')    // must be replaced first!
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/  /g, '  ') // replace double space with an nbsp and space
        .replace(/^ /, ' ')   // space at start
        .replace(/ $/, ' ');  // space at end

    var json = JSON.stringify(htmlEscaped);
    var html = json.substring(1, json.length - 1);
    if (this.editor.options.escapeUnicode === true) {
      html = util.escapeUnicodeChars(html);
    }
    return html;
  }
};

/**
 * unescape a string.
 * @param {String} escapedText
 * @return {String} text
 * @private
 */
Node.prototype._unescapeHTML = function (escapedText) {
  var json = '"' + this._escapeJSON(escapedText) + '"';
  var htmlEscaped = util.parse(json);

  return htmlEscaped
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/ |\u00A0/g, ' ')
      .replace(/&/g, '&');   // must be replaced last
};

/**
 * 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;
  while (i < text.length) {
    var c = text.charAt(i);
    if (c == '\n') {
      escaped += '\\n';
    }
    else if (c == '\\') {
      escaped += c;
      i++;

      c = text.charAt(i);
      if (c === '' || '"\\/bfnrtu'.indexOf(c) == -1) {
        escaped += '\\';  // no valid escape character
      }
      escaped += c;
    }
    else if (c == '"') {
      escaped += '\\"';
    }
    else {
      escaped += c;
    }
    i++;
  }

  return escaped;
};

// TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode
var AppendNode = appendNodeFactory(Node);

module.exports = Node;


text:
import Component from '@glimmer/component';
import { action } from '@ember/object';
import scrollIntoView from 'scroll-into-view';
import { inject as service } from '@ember/service';

export default class IliosCourseDetailsComponent extends Component {
  @service router;

  @action
  collapse() {
    //when the button is clicked to collapse, animate the focus to the top of the page
    scrollIntoView(this.topSection);
    this.args.setShowDetails(false);
  }

  get notRolloverRoute() {
    return this.router.currentRouteName !== 'course.rollover';
  }
}

redpajama_set_name: RedPajamaGithub


text:
module NavigatorRails
  class Item
    include ActiveModel::Model
    class << self
      def level_of path
        path.count('/')
      end
      def normalize_path path
        path = path.to_s
        path = "/#{path}" unless path =~ /^\//
        path
      end
    end
    attr_accessor :path, :order, :type, :active_on, :active_controller
    attr_writer :content, :constraint
    def save
      self.type ||= Decorator.at level: self.level
      Store.add self
    end
    def level
      Item.level_of self.path
    end
    def relative_path
      File.basename path
    end
    def decorator
      Decorator.for self.type
    end
    def content
      Decorator.process(@content.to_s) rescue Store.delete self
    end
    def constraint
      Constraint.process(@constraint.to_s) rescue Store.delete self
    end
    def children params={}
      children = Store.children_of self
      children.collect do |child|
        params.each do |attribute,value|
          child=nil and break unless child and child.send(attribute) == value
        end
        child
      end.compact
    end
    def children_except params={}
      children = Store.children_of self
      children.collect do |child|
        params.each do |attribute,value|
          child=nil and break if child and child.send(attribute) == value
        end
        child
      end.compact
    end
    def active
      return unless @active_on
      return unless Constraint.process('controller').class == @active_controller
      return "active" if @active_on == :all
      return unless Constraint.process('params')['action'].to_sym == @active_on
      "active"
    end
  end
end


text:
package org.kohsuke.eiie_logger.transform;

import java.util.Map;
import java.util.HashMap;

/**
 * Specifies how we transform a class.
 *
 * @author Kohsuke Kawaguchi
 */
public final class ClassTransformSpec {
    public final String name;
    /*package*/ Map<String,MethodTransformSpec> methodSpecs = new HashMap<String,MethodTransformSpec>();

    public ClassTransformSpec(Class clazz, MethodTransformSpec... methodSpecs) {
        this(clazz.getName().replace('.', '/'),methodSpecs);
    }

    public ClassTransformSpec(String name, MethodTransformSpec... methodSpecs) {
        this.name = name;
        for (MethodTransformSpec s : methodSpecs)
            this.methodSpecs.put(s.name+s.desc,s);
    }
}