Automatic Config Policy Ui generation 31/78031/4
authorxg353y <xg353y@intl.att.com>
Wed, 6 Feb 2019 15:21:40 +0000 (16:21 +0100)
committerxg353y <xg353y@intl.att.com>
Thu, 7 Feb 2019 08:47:46 +0000 (09:47 +0100)
Automatic GUI generation based on json output of policy-model. And
remove the Create CL related code.

Change-Id: I42f7e8da46052e01bda33593434f8794f0e430c5
Signed-off-by: xg353y <xg353y@intl.att.com>
Issue-ID: CLAMP-264

src/main/resources/META-INF/resources/designer/index.html
src/main/resources/META-INF/resources/designer/lib/jsoneditor.js [new file with mode: 0644]
src/main/resources/META-INF/resources/designer/lib/query-builder.standalone.js [new file with mode: 0644]
src/main/resources/META-INF/resources/designer/partials/menu.html
src/main/resources/META-INF/resources/designer/partials/portfolios/clds_create_model_off_Template.html [deleted file]
src/main/resources/META-INF/resources/designer/partials/portfolios/tosca_model_properties.html [new file with mode: 0644]
src/main/resources/META-INF/resources/designer/scripts/CldsModelService.js
src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js
src/main/resources/META-INF/resources/designer/scripts/ToscaModelCtrl.js [new file with mode: 0644]
src/main/resources/META-INF/resources/designer/scripts/ToscaModelService.js [new file with mode: 0644]
src/main/resources/META-INF/resources/designer/scripts/app.js

index d8b3fed..5d1e530 100644 (file)
                style="width: 100%; height: 100%"></div>
 
        <script src="lib/jquery.min.js"></script>
-       
+
+       <!-- TOSCA Model Driven Dymamic UI Support -->
+       <script src="lib/jsoneditor.js"></script>
+       <script src="lib/query-builder.standalone.js"></script>
        
        <script src="lib/angular.min.js"></script>
        <script src="lib/angular-cookies.min.js"></script>
      <script src="scripts/CldsTemplateService.js"></script>
      <script src="scripts/GlobalPropertiesCtrl.js"></script>
      <script src="scripts/AlertService.js"></script>
+     <script src="scripts/ToscaModelCtrl.js"></script>
+     <script src="scripts/ToscaModelService.js"></script>
 
     <!--    dialog box ctl end -->
     <script src="scripts/aOnBoot.js"></script>
diff --git a/src/main/resources/META-INF/resources/designer/lib/jsoneditor.js b/src/main/resources/META-INF/resources/designer/lib/jsoneditor.js
new file mode 100644 (file)
index 0000000..2966fac
--- /dev/null
@@ -0,0 +1,10235 @@
+/**
+ * @name JSON Editor
+ * @description JSON Schema Based Editor
+ * Deprecation notice
+ * This repo is no longer maintained (see also https://github.com/jdorn/json-editor/issues/800)
+ * Development is continued at https://github.com/json-editor/json-editor
+ * For details please visit https://github.com/json-editor/json-editor/issues/5
+ * @version 1.1.0-beta.2
+ * @author Jeremy Dorn
+ * @see https://github.com/jdorn/json-editor/
+ * @see https://github.com/json-editor/json-editor
+ * @license MIT
+ * @example see README.md and docs/ for requirements, examples and usage info
+ */
+
+(function() {
+
+/*jshint loopfunc: true */
+/* Simple JavaScript Inheritance
+ * By John Resig http://ejohn.org/
+ * MIT Licensed.
+ */
+// Inspired by base2 and Prototype
+var Class;
+(function(){
+  var initializing = false, fnTest = /xyz/.test(function(){window.postMessage("xyz");}) ? /\b_super\b/ : /.*/;
+  // The base Class implementation (does nothing)
+  Class = function(){};
+  // Create a new Class that inherits from this class
+  Class.extend = function extend(prop) {
+    var _super = this.prototype;
+   
+    // Instantiate a base class (but only create the instance,
+    // don't run the init constructor)
+    initializing = true;
+    var prototype = new this();
+    initializing = false;
+   
+    // Copy the properties over onto the new prototype
+    for (var name in prop) {
+      // Check if we're overwriting an existing function
+      prototype[name] = typeof prop[name] == "function" &&
+        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
+        (function(name, fn){
+          return function() {
+            var tmp = this._super;
+           
+            // Add a new ._super() method that is the same method
+            // but on the super-class
+            this._super = _super[name];
+           
+            // The method only need to be bound temporarily, so we
+            // remove it when we're done executing
+            var ret = fn.apply(this, arguments);        
+            this._super = tmp;
+           
+            return ret;
+          };
+        })(name, prop[name]) :
+        prop[name];
+    }
+   
+    // The dummy class constructor
+    function Class() {
+      // All construction is actually done in the init method
+      if ( !initializing && this.init )
+        this.init.apply(this, arguments);
+    }
+   
+    // Populate our constructed prototype object
+    Class.prototype = prototype;
+   
+    // Enforce the constructor to be what we expect
+    Class.prototype.constructor = Class;
+    // And make this class extendable
+    Class.extend = extend;
+   
+    return Class;
+  };
+  
+  return Class;
+})();
+
+// CustomEvent constructor polyfill
+// From MDN
+(function () {
+  function CustomEvent ( event, params ) {
+    params = params || { bubbles: false, cancelable: false, detail: undefined };
+    var evt = document.createEvent( 'CustomEvent' );
+    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
+    return evt;
+  }
+
+  CustomEvent.prototype = window.Event.prototype;
+
+  window.CustomEvent = CustomEvent;
+})();
+
+// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
+// MIT license
+(function() {
+    var lastTime = 0;
+    var vendors = ['ms', 'moz', 'webkit', 'o'];
+    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
+        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || 
+                                      window[vendors[x]+'CancelRequestAnimationFrame'];
+    }
+    if (!window.requestAnimationFrame)
+        window.requestAnimationFrame = function(callback, element) {
+            var currTime = new Date().getTime();
+            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
+              timeToCall);
+            lastTime = currTime + timeToCall;
+            return id;
+        };
+    if (!window.cancelAnimationFrame)
+        window.cancelAnimationFrame = function(id) {
+            clearTimeout(id);
+        };
+}());
+
+// Array.isArray polyfill
+// From MDN
+(function() {
+       if(!Array.isArray) {
+         Array.isArray = function(arg) {
+               return Object.prototype.toString.call(arg) === '[object Array]';
+         };
+       }
+}());
+/**
+ * Taken from jQuery 2.1.3
+ *
+ * @param obj
+ * @returns {boolean}
+ */
+var $isplainobject = function( obj ) {
+  // Not plain objects:
+  // - Any object or value whose internal [[Class]] property is not "[object Object]"
+  // - DOM nodes
+  // - window
+  if (typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) {
+    return false;
+  }
+
+  if (obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
+    return false;
+  }
+
+  // If the function hasn't returned already, we're confident that
+  // |obj| is a plain object, created by {} or constructed with new Object
+  return true;
+};
+
+var $extend = function(destination) {
+  var source, i,property;
+  for(i=1; i<arguments.length; i++) {
+    source = arguments[i];
+    for (property in source) {
+      if(!source.hasOwnProperty(property)) continue;
+      if(source[property] && $isplainobject(source[property])) {
+        if(!destination.hasOwnProperty(property)) destination[property] = {};
+        $extend(destination[property], source[property]);
+      }
+      else {
+        destination[property] = source[property];
+      }
+    }
+  }
+  return destination;
+};
+
+var $each = function(obj,callback) {
+  if(!obj || typeof obj !== "object") return;
+  var i;
+  if(Array.isArray(obj) || (typeof obj.length === 'number' && obj.length > 0 && (obj.length - 1) in obj)) {
+    for(i=0; i<obj.length; i++) {
+      if(callback(i,obj[i])===false) return;
+    }
+  }
+  else {
+    if (Object.keys) {
+      var keys = Object.keys(obj);
+      for(i=0; i<keys.length; i++) {
+        if(callback(keys[i],obj[keys[i]])===false) return;
+      }
+    }
+    else {
+      for(i in obj) {
+        if(!obj.hasOwnProperty(i)) continue;
+        if(callback(i,obj[i])===false) return;
+      }
+    }
+  }
+};
+
+var $trigger = function(el,event) {
+  var e = document.createEvent('HTMLEvents');
+  e.initEvent(event, true, true);
+  el.dispatchEvent(e);
+};
+var $triggerc = function(el,event) {
+  var e = new CustomEvent(event,{
+    bubbles: true,
+    cancelable: true
+  });
+
+  el.dispatchEvent(e);
+};
+
+var JSONEditor = function(element,options) {
+  if (!(element instanceof Element)) {
+    throw new Error('element should be an instance of Element');
+  }
+  options = $extend({},JSONEditor.defaults.options,options||{});
+  this.element = element;
+  this.options = options;
+  this.init();
+};
+JSONEditor.prototype = {
+  // necessary since we remove the ctor property by doing a literal assignment. Without this
+  // the $isplainobject function will think that this is a plain object.
+  constructor: JSONEditor,
+  init: function() {
+    var self = this;
+    
+    this.ready = false;
+    this.copyClipboard = null;
+
+    var theme_class = JSONEditor.defaults.themes[this.options.theme || JSONEditor.defaults.theme];
+    if(!theme_class) throw "Unknown theme " + (this.options.theme || JSONEditor.defaults.theme);
+    
+    this.schema = this.options.schema;
+    this.theme = new theme_class();
+    this.template = this.options.template;
+    this.refs = this.options.refs || {};
+    this.uuid = 0;
+    this.__data = {};
+    
+    var icon_class = JSONEditor.defaults.iconlibs[this.options.iconlib || JSONEditor.defaults.iconlib];
+    if(icon_class) this.iconlib = new icon_class();
+
+    this.root_container = this.theme.getContainer();
+    this.element.appendChild(this.root_container);
+    
+    this.translate = this.options.translate || JSONEditor.defaults.translate;
+
+    // Fetch all external refs via ajax
+    this._loadExternalRefs(this.schema, function() {
+      self._getDefinitions(self.schema);
+      
+      // Validator options
+      var validator_options = {};
+      if(self.options.custom_validators) {
+        validator_options.custom_validators = self.options.custom_validators;
+      }
+      self.validator = new JSONEditor.Validator(self,null,validator_options);
+      
+      // Create the root editor
+      var schema = self.expandRefs(self.schema);
+      var editor_class = self.getEditorClass(schema);
+      self.root = self.createEditor(editor_class, {
+        jsoneditor: self,
+        schema: schema,
+        required: true,
+        container: self.root_container
+      });
+      
+      self.root.preBuild();
+      self.root.build();
+      self.root.postBuild();
+
+      // Starting data
+      if(self.options.hasOwnProperty('startval')) self.root.setValue(self.options.startval, true);
+
+      self.validation_results = self.validator.validate(self.root.getValue());
+      self.root.showValidationErrors(self.validation_results);
+      self.ready = true;
+
+      // Fire ready event asynchronously
+      window.requestAnimationFrame(function() {
+        if(!self.ready) return;
+        self.validation_results = self.validator.validate(self.root.getValue());
+        self.root.showValidationErrors(self.validation_results);
+        self.trigger('ready');
+        self.trigger('change');
+      });
+    });
+  },
+  getValue: function() {
+    if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before getting the value";
+
+    return this.root.getValue();
+  },
+  setValue: function(value) {
+    if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before setting the value";
+
+    this.root.setValue(value);
+    return this;
+  },
+  validate: function(value) {
+    if(!this.ready) throw "JSON Editor not ready yet.  Listen for 'ready' event before validating";
+    
+    // Custom value
+    if(arguments.length === 1) {
+      return this.validator.validate(value);
+    }
+    // Current value (use cached result)
+    else {
+      return this.validation_results;
+    }
+  },
+  destroy: function() {
+    if(this.destroyed) return;
+    if(!this.ready) return;
+    
+    this.schema = null;
+    this.options = null;
+    this.root.destroy();
+    this.root = null;
+    this.root_container = null;
+    this.validator = null;
+    this.validation_results = null;
+    this.theme = null;
+    this.iconlib = null;
+    this.template = null;
+    this.__data = null;
+    this.ready = false;
+    this.element.innerHTML = '';
+    
+    this.destroyed = true;
+  },
+  on: function(event, callback) {
+    this.callbacks = this.callbacks || {};
+    this.callbacks[event] = this.callbacks[event] || [];
+    this.callbacks[event].push(callback);
+    
+    return this;
+  },
+  off: function(event, callback) {
+    // Specific callback
+    if(event && callback) {
+      this.callbacks = this.callbacks || {};
+      this.callbacks[event] = this.callbacks[event] || [];
+      var newcallbacks = [];
+      for(var i=0; i<this.callbacks[event].length; i++) {
+        if(this.callbacks[event][i]===callback) continue;
+        newcallbacks.push(this.callbacks[event][i]);
+      }
+      this.callbacks[event] = newcallbacks;
+    }
+    // All callbacks for a specific event
+    else if(event) {
+      this.callbacks = this.callbacks || {};
+      this.callbacks[event] = [];
+    }
+    // All callbacks for all events
+    else {
+      this.callbacks = {};
+    }
+    
+    return this;
+  },
+  trigger: function(event) {
+    if(this.callbacks && this.callbacks[event] && this.callbacks[event].length) {
+      for(var i=0; i<this.callbacks[event].length; i++) {
+        this.callbacks[event][i].apply(this, []);
+      }
+    }
+    
+    return this;
+  },
+  setOption: function(option, value) {
+    if(option === "show_errors") {
+      this.options.show_errors = value;
+      this.onChange();
+    }
+    // Only the `show_errors` option is supported for now
+    else {
+      throw "Option "+option+" must be set during instantiation and cannot be changed later";
+    }
+    
+    return this;
+  },
+  getEditorClass: function(schema) {
+    var classname;
+
+    schema = this.expandSchema(schema);
+
+    $each(JSONEditor.defaults.resolvers,function(i,resolver) {
+      var tmp = resolver(schema);
+      if(tmp) {
+        if(JSONEditor.defaults.editors[tmp]) {
+          classname = tmp;
+          return false;
+        }
+      }
+    });
+
+    if(!classname) throw "Unknown editor for schema "+JSON.stringify(schema);
+    if(!JSONEditor.defaults.editors[classname]) throw "Unknown editor "+classname;
+
+    return JSONEditor.defaults.editors[classname];
+  },
+  createEditor: function(editor_class, options) {
+    options = $extend({},editor_class.options||{},options);
+    return new editor_class(options);
+  },
+  onChange: function() {
+    if(!this.ready) return;
+    
+    if(this.firing_change) return;
+    this.firing_change = true;
+    
+    var self = this;
+    
+    window.requestAnimationFrame(function() {
+      self.firing_change = false;
+      if(!self.ready) return;
+
+      // Validate and cache results
+      self.validation_results = self.validator.validate(self.root.getValue());
+      
+      if(self.options.show_errors !== "never") {
+        self.root.showValidationErrors(self.validation_results);
+      }
+      else {
+        self.root.showValidationErrors([]);
+      }
+      
+      // Fire change event
+      self.trigger('change');
+    });
+    
+    return this;
+  },
+  compileTemplate: function(template, name) {
+    name = name || JSONEditor.defaults.template;
+
+    var engine;
+
+    // Specifying a preset engine
+    if(typeof name === 'string') {
+      if(!JSONEditor.defaults.templates[name]) throw "Unknown template engine "+name;
+      engine = JSONEditor.defaults.templates[name]();
+
+      if(!engine) throw "Template engine "+name+" missing required library.";
+    }
+    // Specifying a custom engine
+    else {
+      engine = name;
+    }
+
+    if(!engine) throw "No template engine set";
+    if(!engine.compile) throw "Invalid template engine set";
+
+    return engine.compile(template);
+  },
+  _data: function(el,key,value) {
+    // Setting data
+    if(arguments.length === 3) {
+      var uuid;
+      if(el.hasAttribute('data-jsoneditor-'+key)) {
+        uuid = el.getAttribute('data-jsoneditor-'+key);
+      }
+      else {
+        uuid = this.uuid++;
+        el.setAttribute('data-jsoneditor-'+key,uuid);
+      }
+
+      this.__data[uuid] = value;
+    }
+    // Getting data
+    else {
+      // No data stored
+      if(!el.hasAttribute('data-jsoneditor-'+key)) return null;
+      
+      return this.__data[el.getAttribute('data-jsoneditor-'+key)];
+    }
+  },
+  registerEditor: function(editor) {
+    this.editors = this.editors || {};
+    this.editors[editor.path] = editor;
+    return this;
+  },
+  unregisterEditor: function(editor) {
+    this.editors = this.editors || {};
+    this.editors[editor.path] = null;
+    return this;
+  },
+  getEditor: function(path) {
+    if(!this.editors) return;
+    return this.editors[path];
+  },
+  watch: function(path,callback) {
+    this.watchlist = this.watchlist || {};
+    this.watchlist[path] = this.watchlist[path] || [];
+    this.watchlist[path].push(callback);
+    
+    return this;
+  },
+  unwatch: function(path,callback) {
+    if(!this.watchlist || !this.watchlist[path]) return this;
+    // If removing all callbacks for a path
+    if(!callback) {
+      this.watchlist[path] = null;
+      return this;
+    }
+    
+    var newlist = [];
+    for(var i=0; i<this.watchlist[path].length; i++) {
+      if(this.watchlist[path][i] === callback) continue;
+      else newlist.push(this.watchlist[path][i]);
+    }
+    this.watchlist[path] = newlist.length? newlist : null;
+    return this;
+  },
+  notifyWatchers: function(path) {
+    if(!this.watchlist || !this.watchlist[path]) return this;
+    for(var i=0; i<this.watchlist[path].length; i++) {
+      this.watchlist[path][i]();
+    }
+  },
+  isEnabled: function() {
+    return !this.root || this.root.isEnabled();
+  },
+  enable: function() {
+    this.root.enable();
+  },
+  disable: function() {
+    this.root.disable();
+  },
+  _getDefinitions: function(schema,path) {
+    path = path || '#/definitions/';
+    if(schema.definitions) {
+      for(var i in schema.definitions) {
+        if(!schema.definitions.hasOwnProperty(i)) continue;
+        this.refs[path+i] = schema.definitions[i];
+        if(schema.definitions[i].definitions) {
+          this._getDefinitions(schema.definitions[i],path+i+'/definitions/');
+        }
+      }
+    }
+  },
+  _getExternalRefs: function(schema) {
+    var refs = {};
+    var merge_refs = function(newrefs) {
+      for(var i in newrefs) {
+        if(newrefs.hasOwnProperty(i)) {
+          refs[i] = true;
+        }
+      }
+    };
+    
+    if(schema.$ref && typeof schema.$ref !== "object" && schema.$ref.substr(0,1) !== "#" && !this.refs[schema.$ref]) {
+      refs[schema.$ref] = true;
+    }
+    
+    for(var i in schema) {
+      if(!schema.hasOwnProperty(i)) continue;
+      if(schema[i] && typeof schema[i] === "object" && Array.isArray(schema[i])) {
+        for(var j=0; j<schema[i].length; j++) {
+          if(schema[i][j] && typeof schema[i][j]==="object") {
+            merge_refs(this._getExternalRefs(schema[i][j]));
+          }
+        }
+      }
+      else if(schema[i] && typeof schema[i] === "object") {
+        merge_refs(this._getExternalRefs(schema[i]));
+      }
+    }
+    
+    return refs;
+  },
+  _loadExternalRefs: function(schema, callback) {
+    var self = this;
+    var refs = this._getExternalRefs(schema);
+    
+    var done = 0, waiting = 0, callback_fired = false;
+    
+    $each(refs,function(url) {
+      if(self.refs[url]) return;
+      if(!self.options.ajax) throw "Must set ajax option to true to load external ref "+url;
+      self.refs[url] = 'loading';
+      waiting++;
+
+      var fetchUrl=url;
+      if( self.options.ajaxBase && self.options.ajaxBase!=url.substr(0,self.options.ajaxBase.length) && "http"!=url.substr(0,4)) fetchUrl=self.options.ajaxBase+url;
+
+      var r = new XMLHttpRequest(); 
+      r.open("GET", fetchUrl, true);
+      if(self.options.ajaxCredentials) r.withCredentials=self.options.ajaxCredentials;
+      r.onreadystatechange = function () {
+        if (r.readyState != 4) return; 
+        // Request succeeded
+        if(r.status === 200) {
+          var response;
+          try {
+            response = JSON.parse(r.responseText);
+          }
+          catch(e) {
+            window.console.log(e);
+            throw "Failed to parse external ref "+fetchUrl;
+          }
+          if(!response || typeof response !== "object") throw "External ref does not contain a valid schema - "+fetchUrl;
+          
+          self.refs[url] = response;
+          self._loadExternalRefs(response,function() {
+            done++;
+            if(done >= waiting && !callback_fired) {
+              callback_fired = true;
+              callback();
+            }
+          });
+        }
+        // Request failed
+        else {
+          window.console.log(r);
+          throw "Failed to fetch ref via ajax- "+url;
+        }
+      };
+      r.send();
+    });
+    
+    if(!waiting) {
+      callback();
+    }
+  },
+  expandRefs: function(schema) {
+    schema = $extend({},schema);
+    
+    while (schema.$ref) {
+      var ref = schema.$ref;
+      delete schema.$ref;
+      
+      if(!this.refs[ref]) ref = decodeURIComponent(ref);
+      
+      schema = this.extendSchemas(schema,this.refs[ref]);
+    }
+    return schema;
+  },
+  expandSchema: function(schema) {
+    var self = this;
+    var extended = $extend({},schema);
+    var i;
+
+    // Version 3 `type`
+    if(typeof schema.type === 'object') {
+      // Array of types
+      if(Array.isArray(schema.type)) {
+        $each(schema.type, function(key,value) {
+          // Schema
+          if(typeof value === 'object') {
+            schema.type[key] = self.expandSchema(value);
+          }
+        });
+      }
+      // Schema
+      else {
+        schema.type = self.expandSchema(schema.type);
+      }
+    }
+    // Version 3 `disallow`
+    if(typeof schema.disallow === 'object') {
+      // Array of types
+      if(Array.isArray(schema.disallow)) {
+        $each(schema.disallow, function(key,value) {
+          // Schema
+          if(typeof value === 'object') {
+            schema.disallow[key] = self.expandSchema(value);
+          }
+        });
+      }
+      // Schema
+      else {
+        schema.disallow = self.expandSchema(schema.disallow);
+      }
+    }
+    // Version 4 `anyOf`
+    if(schema.anyOf) {
+      $each(schema.anyOf, function(key,value) {
+        schema.anyOf[key] = self.expandSchema(value);
+      });
+    }
+    // Version 4 `dependencies` (schema dependencies)
+    if(schema.dependencies) {
+      $each(schema.dependencies,function(key,value) {
+        if(typeof value === "object" && !(Array.isArray(value))) {
+          schema.dependencies[key] = self.expandSchema(value);
+        }
+      });
+    }
+    // Version 4 `not`
+    if(schema.not) {
+      schema.not = this.expandSchema(schema.not);
+    }
+    
+    // allOf schemas should be merged into the parent
+    if(schema.allOf) {
+      for(i=0; i<schema.allOf.length; i++) {
+        extended = this.extendSchemas(extended,this.expandSchema(schema.allOf[i]));
+      }
+      delete extended.allOf;
+    }
+    // extends schemas should be merged into parent
+    if(schema["extends"]) {
+      // If extends is a schema
+      if(!(Array.isArray(schema["extends"]))) {
+        extended = this.extendSchemas(extended,this.expandSchema(schema["extends"]));
+      }
+      // If extends is an array of schemas
+      else {
+        for(i=0; i<schema["extends"].length; i++) {
+          extended = this.extendSchemas(extended,this.expandSchema(schema["extends"][i]));
+        }
+      }
+      delete extended["extends"];
+    }
+    // parent should be merged into oneOf schemas
+    if(schema.oneOf) {
+      var tmp = $extend({},extended);
+      delete tmp.oneOf;
+      for(i=0; i<schema.oneOf.length; i++) {
+        extended.oneOf[i] = this.extendSchemas(this.expandSchema(schema.oneOf[i]),tmp);
+      }
+    }
+    
+    return this.expandRefs(extended);
+  },
+  extendSchemas: function(obj1, obj2) {
+    obj1 = $extend({},obj1);
+    obj2 = $extend({},obj2);
+
+    var self = this;
+    var extended = {};
+    $each(obj1, function(prop,val) {
+      // If this key is also defined in obj2, merge them
+      if(typeof obj2[prop] !== "undefined") {
+        // Required and defaultProperties arrays should be unioned together
+        if((prop === 'required'||prop === 'defaultProperties') && typeof val === "object" && Array.isArray(val)) {
+          // Union arrays and unique
+          extended[prop] = val.concat(obj2[prop]).reduce(function(p, c) {
+            if (p.indexOf(c) < 0) p.push(c);
+            return p;
+          }, []);
+        }
+        // Type should be intersected and is either an array or string
+        else if(prop === 'type' && (typeof val === "string" || Array.isArray(val))) {
+          // Make sure we're dealing with arrays
+          if(typeof val === "string") val = [val];
+          if(typeof obj2.type === "string") obj2.type = [obj2.type];
+
+          // If type is only defined in the first schema, keep it
+          if(!obj2.type || !obj2.type.length) {
+            extended.type = val;
+          }
+          // If type is defined in both schemas, do an intersect
+          else {
+            extended.type = val.filter(function(n) {
+              return obj2.type.indexOf(n) !== -1;
+            });
+          }
+
+          // If there's only 1 type and it's a primitive, use a string instead of array
+          if(extended.type.length === 1 && typeof extended.type[0] === "string") {
+            extended.type = extended.type[0];
+          }
+          // Remove the type property if it's empty
+          else if(extended.type.length === 0) {
+            delete extended.type;
+          }
+        }
+        // All other arrays should be intersected (enum, etc.)
+        else if(typeof val === "object" && Array.isArray(val)){
+          extended[prop] = val.filter(function(n) {
+            return obj2[prop].indexOf(n) !== -1;
+          });
+        }
+        // Objects should be recursively merged
+        else if(typeof val === "object" && val !== null) {
+          extended[prop] = self.extendSchemas(val,obj2[prop]);
+        }
+        // Otherwise, use the first value
+        else {
+          extended[prop] = val;
+        }
+      }
+      // Otherwise, just use the one in obj1
+      else {
+        extended[prop] = val;
+      }
+    });
+    // Properties in obj2 that aren't in obj1
+    $each(obj2, function(prop,val) {
+      if(typeof obj1[prop] === "undefined") {
+        extended[prop] = val;
+      }
+    });
+
+    return extended;
+  },
+  setCopyClipboardContents: function(value) {
+    this.copyClipboard = value;
+  },
+  getCopyClipboardContents: function() {
+    return this.copyClipboard;
+  }
+};
+
+JSONEditor.defaults = {
+  themes: {},
+  templates: {},
+  iconlibs: {},
+  editors: {},
+  languages: {},
+  resolvers: [],
+  custom_validators: []
+};
+
+JSONEditor.Validator = Class.extend({
+  init: function(jsoneditor,schema,options) {
+    this.jsoneditor = jsoneditor;
+    this.schema = schema || this.jsoneditor.schema;
+    this.options = options || {};
+    this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
+  },
+  validate: function(value) {
+    return this._validateSchema(this.schema, value);
+  },
+  _validateSchema: function(schema,value,path) {
+    var self = this;
+    var errors = [];
+    var valid, i, j;
+    var stringified = JSON.stringify(value);
+
+    path = path || 'root';
+
+    // Work on a copy of the schema
+    schema = $extend({},this.jsoneditor.expandRefs(schema));
+
+    /*
+     * Type Agnostic Validation
+     */
+
+    // Version 3 `required` and `required_by_default`
+    if(typeof value === "undefined" || value === null) {
+      if((typeof schema.required !== "undefined" && schema.required === true) || (typeof schema.required === "undefined" && this.jsoneditor.options.required_by_default === true)) {
+        errors.push({
+          path: path,
+          property: 'required',
+          message: this.translate("error_notset", [schema.title ? schema.title : path.split('-').pop().trim()])
+        });
+      }
+
+      return errors;
+    }
+
+    // `enum`
+    if(schema["enum"]) {
+      valid = false;
+      for(i=0; i<schema["enum"].length; i++) {
+        if(stringified === JSON.stringify(schema["enum"][i])) valid = true;
+      }
+      if(!valid) {
+        errors.push({
+          path: path,
+          property: 'enum',
+          message: this.translate("error_enum", [schema.title ? schema.title : path.split('-').pop().trim()])
+        });
+      }
+    }
+
+    // `extends` (version 3)
+    if(schema["extends"]) {
+      for(i=0; i<schema["extends"].length; i++) {
+        errors = errors.concat(this._validateSchema(schema["extends"][i],value,path));
+      }
+    }
+
+    // `allOf`
+    if(schema.allOf) {
+      for(i=0; i<schema.allOf.length; i++) {
+        errors = errors.concat(this._validateSchema(schema.allOf[i],value,path));
+      }
+    }
+
+    // `anyOf`
+    if(schema.anyOf) {
+      valid = false;
+      for(i=0; i<schema.anyOf.length; i++) {
+        if(!this._validateSchema(schema.anyOf[i],value,path).length) {
+          valid = true;
+          break;
+        }
+      }
+      if(!valid) {
+        errors.push({
+          path: path,
+          property: 'anyOf',
+          message: this.translate('error_anyOf')
+        });
+      }
+    }
+
+    // `oneOf`
+    if(schema.oneOf) {
+      valid = 0;
+      var oneof_errors = [];
+      for(i=0; i<schema.oneOf.length; i++) {
+        // Set the error paths to be path.oneOf[i].rest.of.path
+        var tmp = this._validateSchema(schema.oneOf[i],value,path);
+        if(!tmp.length) {
+          valid++;
+        }
+
+        for(j=0; j<tmp.length; j++) {
+          tmp[j].path = path+'.oneOf['+i+']'+tmp[j].path.substr(path.length);
+        }
+        oneof_errors = oneof_errors.concat(tmp);
+
+      }
+      if(valid !== 1) {
+        errors.push({
+          path: path,
+          property: 'oneOf',
+          message: this.translate('error_oneOf', [valid])
+        });
+        errors = errors.concat(oneof_errors);
+      }
+    }
+
+    // `not`
+    if(schema.not) {
+      if(!this._validateSchema(schema.not,value,path).length) {
+        errors.push({
+          path: path,
+          property: 'not',
+          message: this.translate('error_not')
+        });
+      }
+    }
+
+    // `type` (both Version 3 and Version 4 support)
+    if(schema.type) {
+      // Union type
+      if(Array.isArray(schema.type)) {
+        valid = false;
+        for(i=0;i<schema.type.length;i++) {
+          if(this._checkType(schema.type[i], value)) {
+            valid = true;
+            break;
+          }
+        }
+        if(!valid) {
+          errors.push({
+            path: path,
+            property: 'type',
+            message: this.translate('error_type_union')
+          });
+        }
+      }
+      // Simple type
+      else {
+        if(!this._checkType(schema.type, value)) {
+          errors.push({
+            path: path,
+            property: 'type',
+            message: this.translate('error_type', [schema.type])
+          });
+        }
+      }
+    }
+
+
+    // `disallow` (version 3)
+    if(schema.disallow) {
+      // Union type
+      if(Array.isArray(schema.disallow)) {
+        valid = true;
+        for(i=0;i<schema.disallow.length;i++) {
+          if(this._checkType(schema.disallow[i], value)) {
+            valid = false;
+            break;
+          }
+        }
+        if(!valid) {
+          errors.push({
+            path: path,
+            property: 'disallow',
+            message: this.translate('error_disallow_union')
+          });
+        }
+      }
+      // Simple type
+      else {
+        if(this._checkType(schema.disallow, value)) {
+          errors.push({
+            path: path,
+            property: 'disallow',
+            message: this.translate('error_disallow', [schema.disallow])
+          });
+        }
+      }
+    }
+
+    /*
+     * Type Specific Validation
+     */
+
+    // Number Specific Validation
+    if(typeof value === "number") {
+      // `multipleOf` and `divisibleBy`
+      if(schema.multipleOf || schema.divisibleBy) {
+        var divisor = schema.multipleOf || schema.divisibleBy;
+        // Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999)
+        valid = (value/divisor === Math.floor(value/divisor));
+
+        // Use math.js is available
+        if(window.math) {
+          valid = window.math.mod(window.math.bignumber(value), window.math.bignumber(divisor)).equals(0);
+        }
+        // Use decimal.js is available
+        else if(window.Decimal) {
+          valid = (new window.Decimal(value)).mod(new window.Decimal(divisor)).equals(0);
+        }
+
+        if(!valid) {
+          errors.push({
+            path: path,
+            property: schema.multipleOf? 'multipleOf' : 'divisibleBy',
+            message: this.translate('error_multipleOf', [divisor])
+          });
+        }
+      }
+
+      // `maximum`
+      if(schema.hasOwnProperty('maximum')) {
+        // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
+        valid = schema.exclusiveMaximum? (value < schema.maximum) : (value <= schema.maximum);
+
+        // Use math.js is available
+        if(window.math) {
+          valid = window.math[schema.exclusiveMaximum?'smaller':'smallerEq'](
+            window.math.bignumber(value),
+            window.math.bignumber(schema.maximum)
+          );
+        }
+        // Use Decimal.js if available
+        else if(window.Decimal) {
+          valid = (new window.Decimal(value))[schema.exclusiveMaximum?'lt':'lte'](new window.Decimal(schema.maximum));
+        }
+
+        if(!valid) {
+          errors.push({
+            path: path,
+            property: 'maximum',
+            message: this.translate(
+              (schema.exclusiveMaximum?'error_maximum_excl':'error_maximum_incl'),
+              [schema.title ? schema.title : path.split('-').pop().trim(), schema.maximum]
+            )
+          });
+        }
+      }
+
+      // `minimum`
+      if(schema.hasOwnProperty('minimum')) {
+        // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
+        valid = schema.exclusiveMinimum? (value > schema.minimum) : (value >= schema.minimum);
+
+        // Use math.js is available
+        if(window.math) {
+          valid = window.math[schema.exclusiveMinimum?'larger':'largerEq'](
+            window.math.bignumber(value),
+            window.math.bignumber(schema.minimum)
+          );
+        }
+        // Use Decimal.js if available
+        else if(window.Decimal) {
+          valid = (new window.Decimal(value))[schema.exclusiveMinimum?'gt':'gte'](new window.Decimal(schema.minimum));
+        }
+
+        if(!valid) {
+          errors.push({
+            path: path,
+            property: 'minimum',
+            message: this.translate(
+              (schema.exclusiveMinimum?'error_minimum_excl':'error_minimum_incl'),
+              [schema.title ? schema.title : path.split('-').pop().trim(), schema.minimum]
+            )
+          });
+        }
+      }
+    }
+    // String specific validation
+    else if(typeof value === "string") {
+      // `maxLength`
+      if(schema.maxLength) {
+        if((value+"").length > schema.maxLength) {
+          errors.push({
+            path: path,
+            property: 'maxLength',
+            message: this.translate('error_maxLength', 
+                       [schema.title ? schema.title : path.split('-').pop().trim(), schema.maxLength])
+          });
+        }
+      }
+
+      // `minLength` -- Commented because we are validating required field. 
+      if(schema.minLength) {
+        if((value+"").length < schema.minLength) {
+          errors.push({
+            path: path,
+            property: 'minLength',
+            message: this.translate((schema.minLength===1?'error_notempty':'error_minLength'), 
+                       [schema.title ? schema.title : path.split('-').pop().trim(), schema.minLength])
+          });
+        }
+      }
+
+      // `pattern`
+      if(schema.pattern) {
+        if(!(new RegExp(schema.pattern)).test(value)) {
+          errors.push({
+            path: path,
+            property: 'pattern',
+            message: this.translate('error_pattern', 
+                       [schema.title ? schema.title : path.split('-').pop().trim(), schema.pattern])
+          });
+        }
+      }
+    }
+    // Array specific validation
+    else if(typeof value === "object" && value !== null && Array.isArray(value)) {
+      // `items` and `additionalItems`
+      if(schema.items) {
+        // `items` is an array
+        if(Array.isArray(schema.items)) {
+          for(i=0; i<value.length; i++) {
+            // If this item has a specific schema tied to it
+            // Validate against it
+            if(schema.items[i]) {
+              errors = errors.concat(this._validateSchema(schema.items[i],value[i],path+'.'+i));
+            }
+            // If all additional items are allowed
+            else if(schema.additionalItems === true) {
+              break;
+            }
+            // If additional items is a schema
+            // TODO: Incompatibility between version 3 and 4 of the spec
+            else if(schema.additionalItems) {
+              errors = errors.concat(this._validateSchema(schema.additionalItems,value[i],path+'.'+i));
+            }
+            // If no additional items are allowed
+            else if(schema.additionalItems === false) {
+              errors.push({
+                path: path,
+                property: 'additionalItems',
+                message: this.translate('error_additionalItems')
+              });
+              break;
+            }
+            // Default for `additionalItems` is an empty schema
+            else {
+              break;
+            }
+          }
+        }
+        // `items` is a schema
+        else {
+          // Each item in the array must validate against the schema
+          for(i=0; i<value.length; i++) {
+            errors = errors.concat(this._validateSchema(schema.items,value[i],path+'.'+i));
+          }
+        }
+      }
+
+      // `maxItems`
+      if(schema.maxItems) {
+        if(value.length > schema.maxItems) {
+          errors.push({
+            path: path,
+            property: 'maxItems',
+            message: this.translate('error_maxItems', [schema.maxItems])
+          });
+        }
+      }
+
+      // `minItems`
+      if(schema.minItems) {
+        if(value.length < schema.minItems) {
+          errors.push({
+            path: path,
+            property: 'minItems',
+            message: this.translate('error_minItems', [schema.minItems])
+          });
+        }
+      }
+
+      // `uniqueItems`
+      if(schema.uniqueItems) {
+        var seen = {};
+        for(i=0; i<value.length; i++) {
+          valid = JSON.stringify(value[i]);
+          if(seen[valid]) {
+            errors.push({
+              path: path,
+              property: 'uniqueItems',
+              message: this.translate('error_uniqueItems', 
+                               [schema.title ? schema.title : path.split('-').pop().trim()])
+            });
+            break;
+          }
+          seen[valid] = true;
+        }
+      }
+    }
+    // Object specific validation
+    else if(typeof value === "object" && value !== null) {
+      // `maxProperties`
+      if(schema.maxProperties) {
+        valid = 0;
+        for(i in value) {
+          if(!value.hasOwnProperty(i)) continue;
+          valid++;
+        }
+        if(valid > schema.maxProperties) {
+          errors.push({
+            path: path,
+            property: 'maxProperties',
+            message: this.translate('error_maxProperties', [schema.maxProperties])
+          });
+        }
+      }
+
+      // `minProperties`
+      if(schema.minProperties) {
+        valid = 0;
+        for(i in value) {
+          if(!value.hasOwnProperty(i)) continue;
+          valid++;
+        }
+        if(valid < schema.minProperties) {
+          errors.push({
+            path: path,
+            property: 'minProperties',
+            message: this.translate('error_minProperties', [schema.minProperties])
+          });
+        }
+      }
+
+      // Version 4 `required`
+      if(typeof schema.required !== "undefined" && Array.isArray(schema.required)) {
+        for(i=0; i<schema.required.length; i++) {
+          // Arrays are the only missing "required" thing we report in the "object" 
+          // level control group error message area; all others appear in their own form control 
+          // control message area.
+          if((typeof value[schema.required[i]] === "undefined") ||
+                (Array.isArray(value[schema.required[i]]) && value[schema.required[i]].length == 0)) {
+            var parm_name;
+            if(typeof schema.properties[schema.required[i]].title !== "undefined") {
+              parm_name = schema.properties[schema.required[i]].title;
+            } 
+            else {
+              parm_name = schema.required[i];
+            }
+            errors.push({
+              path: path,
+              property: 'required',
+              message: this.translate('error_required', [parm_name])
+            });
+          }
+        }
+      }
+
+      // `properties`
+      var validated_properties = {};
+      if(schema.properties) {
+        if(typeof schema.required !== "undefined" && Array.isArray(schema.required)) {
+          for(i=0; i<schema.required.length; i++) {
+            var property = schema.required[i];
+            validated_properties[property] = true;
+            errors = errors.concat(this._validateSchema(schema.properties[property],value[property],path+'.'+property));
+          }
+        }
+
+        // If an optional property is not an object and is not empty, we must run validation
+        // on it as the user may have entered some data into it.
+
+        for(i in schema.properties) {
+          if(!schema.properties.hasOwnProperty(i) || validated_properties[i] === true) continue;
+          if((typeof value[i] !== "object" && typeof value[i] !== "undefined" && value[i] !== null) ||
+             (schema.properties[i].type === "array" && Array.isArray(value[i]) && value[i].length > 0)) {
+                 
+            errors = errors.concat(this._validateSchema(schema.properties[i],value[i],path+'.'+i));
+          }
+          validated_properties[i] = true;
+        }
+      }
+
+      // `patternProperties`
+      if(schema.patternProperties) {
+        for(i in schema.patternProperties) {
+          if(!schema.patternProperties.hasOwnProperty(i)) continue;
+          var regex = new RegExp(i);
+
+          // Check which properties match
+          for(j in value) {
+            if(!value.hasOwnProperty(j)) continue;
+            if(regex.test(j)) {
+              validated_properties[j] = true;
+              errors = errors.concat(this._validateSchema(schema.patternProperties[i],value[j],path+'.'+j));
+            }
+          }
+        }
+      }
+
+      // The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf
+      if(typeof schema.additionalProperties === "undefined" && this.jsoneditor.options.no_additional_properties && !schema.oneOf && !schema.anyOf) {
+        schema.additionalProperties = false;
+      }
+
+      // `additionalProperties`
+      if(typeof schema.additionalProperties !== "undefined") {
+        for(i in value) {
+          if(!value.hasOwnProperty(i)) continue;
+          if(!validated_properties[i]) {
+            // No extra properties allowed
+            if(!schema.additionalProperties) {
+              errors.push({
+                path: path,
+                property: 'additionalProperties',
+                message: this.translate('error_additional_properties', [i])
+              });
+              break;
+            }
+            // Allowed
+            else if(schema.additionalProperties === true) {
+              break;
+            }
+            // Must match schema
+            // TODO: incompatibility between version 3 and 4 of the spec
+            else {
+              errors = errors.concat(this._validateSchema(schema.additionalProperties,value[i],path+'.'+i));
+            }
+          }
+        }
+      }
+
+      // `dependencies`
+      if(schema.dependencies) {
+        for(i in schema.dependencies) {
+          if(!schema.dependencies.hasOwnProperty(i)) continue;
+
+          // Doesn't need to meet the dependency
+          if(typeof value[i] === "undefined") continue;
+
+          // Property dependency
+          if(Array.isArray(schema.dependencies[i])) {
+            for(j=0; j<schema.dependencies[i].length; j++) {
+              if(typeof value[schema.dependencies[i][j]] === "undefined") {
+                errors.push({
+                  path: path,
+                  property: 'dependencies',
+                  message: this.translate('error_dependency', [schema.dependencies[i][j]])
+                });
+              }
+            }
+          }
+          // Schema dependency
+          else {
+            errors = errors.concat(this._validateSchema(schema.dependencies[i],value,path));
+          }
+        }
+      }
+    }
+
+    // Custom type validation (global)
+    $each(JSONEditor.defaults.custom_validators,function(i,validator) {
+      errors = errors.concat(validator.call(self,schema,value,path));
+    });
+    // Custom type validation (instance specific)
+    if(this.options.custom_validators) {
+      $each(this.options.custom_validators,function(i,validator) {
+        errors = errors.concat(validator.call(self,schema,value,path));
+      });
+    }
+
+    return errors;
+  },
+  _checkType: function(type, value) {
+    // Simple types
+    if(typeof type === "string") {
+      if(type==="string") return typeof value === "string";
+      else if(type==="number") return typeof value === "number";
+      else if(type==="qbldr") return typeof value === "string";
+      else if(type==="integer") return typeof value === "number" && value === Math.floor(value);
+      else if(type==="boolean") return typeof value === "boolean";
+      else if(type==="array") return Array.isArray(value);
+      else if(type === "object") return value !== null && !(Array.isArray(value)) && typeof value === "object";
+      else if(type === "null") return value === null;
+      else return true;
+    }
+    // Schema
+    else {
+      return !this._validateSchema(type,value).length;
+    }
+  }
+});
+
+/**
+ * All editors should extend from this class
+ */
+JSONEditor.AbstractEditor = Class.extend({
+  onChildEditorChange: function(editor) {
+    this.onChange(true);
+  },
+  notify: function() {
+    if(this.path) this.jsoneditor.notifyWatchers(this.path);
+  },
+  change: function() {
+    if(this.parent) this.parent.onChildEditorChange(this);
+    else if(this.jsoneditor) this.jsoneditor.onChange();
+  },
+  onChange: function(bubble) {
+    this.notify();
+    if(this.watch_listener) this.watch_listener();
+    if(bubble) this.change();
+  },
+  register: function() {
+    this.jsoneditor.registerEditor(this);
+    this.onChange();
+  },
+  unregister: function() {
+    if(!this.jsoneditor) return;
+    this.jsoneditor.unregisterEditor(this);
+  },
+  getNumColumns: function() {
+    return 12;
+  },
+  init: function(options) {
+    this.jsoneditor = options.jsoneditor;
+    
+    this.theme = this.jsoneditor.theme;
+    this.template_engine = this.jsoneditor.template;
+    this.iconlib = this.jsoneditor.iconlib;
+    
+    this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
+
+    this.original_schema = options.schema;
+    this.schema = this.jsoneditor.expandSchema(this.original_schema);
+
+    this.options = $extend({}, (this.options || {}), (this.schema.options || {}), (options.schema.options || {}), options);
+
+    if(!options.path && !this.schema.id) this.schema.id = 'root';
+    this.path = options.path || 'root';
+    this.formname = options.formname || this.path.replace(/\.([^.]+)/g,'[$1]');
+    if(this.jsoneditor.options.form_name_root) this.formname = this.formname.replace(/^root\[/,this.jsoneditor.options.form_name_root+'[');
+    this.key = this.path.split('.').pop();
+    this.parent = options.parent;
+    
+    this.link_watchers = [];
+    
+    if(options.container) this.setContainer(options.container);
+    this.registerDependencies();
+  },
+  registerDependencies: function() {
+    this.dependenciesFulfilled = true;
+    var deps = this.options.dependencies;
+    if (!deps) {
+      return;
+    }
+    
+    var self = this;
+    Object.keys(deps).forEach(function(dependency) {
+      var path = self.path.split('.');
+      path[path.length - 1] = dependency;
+      path = path.join('.');
+      var choices = deps[dependency];
+      self.jsoneditor.watch(path, function() {
+        self.checkDependency(path, choices);
+      });
+    });
+  },
+  checkDependency: function(path, choices) {
+    var wrapper = this.control || this.container;
+    if (this.path === path || !wrapper) {
+      return;
+    }
+    
+    var self = this;
+    var editor = this.jsoneditor.getEditor(path);
+    var value = editor ? editor.getValue() : undefined;
+    var previousStatus = this.dependenciesFulfilled;
+    this.dependenciesFulfilled = false;
+    
+    if (!editor || !editor.dependenciesFulfilled) {
+      this.dependenciesFulfilled = false;
+    } else if (Array.isArray(choices)) {
+      choices.some(function(choice) {
+        if (value === choice) {
+          self.dependenciesFulfilled = true;
+          return true;
+        }
+      });
+    } else if (typeof choices === 'object') {
+      if (typeof value !== 'object') {
+        this.dependenciesFulfilled = choices === value;
+      } else {
+        Object.keys(choices).some(function(key) {
+          if (!choices.hasOwnProperty(key)) {
+            return false;
+          }
+          if (!value.hasOwnProperty(key) || choices[key] !== value[key]) {
+            self.dependenciesFulfilled = false;
+            return true;
+          }
+          self.dependenciesFulfilled = true;
+        });
+      }
+    } else if (typeof choices === 'string' || typeof choices === 'number') {
+      this.dependenciesFulfilled = value === choices;
+    } else if (typeof choices === 'boolean') {
+      if (choices) {
+        this.dependenciesFulfilled = value && value.length > 0;
+      } else {
+        this.dependenciesFulfilled = !value || value.length === 0;
+      }
+    }
+    
+    if (this.dependenciesFulfilled !== previousStatus) {
+      this.notify();
+    }
+    
+    if (this.dependenciesFulfilled) {
+      wrapper.style.display = 'block';
+    } else {
+      wrapper.style.display = 'none';
+    }
+  },
+  setContainer: function(container) {
+    this.container = container;
+    if(this.schema.id) this.container.setAttribute('data-schemaid',this.schema.id);
+    if(this.schema.type && typeof this.schema.type === "string") this.container.setAttribute('data-schematype',this.schema.type);
+    this.container.setAttribute('data-schemapath',this.path);
+    this.container.style.padding = '4px';
+  },
+  
+  preBuild: function() {
+
+  },
+  build: function() {
+    
+  },
+  postBuild: function() {
+    this.setupWatchListeners();
+    this.addLinks();
+    this.setValue(this.getDefault(), true);
+    this.updateHeaderText();
+    this.register();
+    this.onWatchedFieldChange();
+  },
+  
+  setupWatchListeners: function() {
+    var self = this;
+    
+    // Watched fields
+    this.watched = {};
+    if(this.schema.vars) this.schema.watch = this.schema.vars;
+    this.watched_values = {};
+    this.watch_listener = function() {
+      if(self.refreshWatchedFieldValues()) {
+        self.onWatchedFieldChange();
+      }
+    };
+    
+    if(this.schema.hasOwnProperty('watch')) {
+      var path,path_parts,first,root,adjusted_path;
+
+      for(var name in this.schema.watch) {
+        if(!this.schema.watch.hasOwnProperty(name)) continue;
+        path = this.schema.watch[name];
+
+        if(Array.isArray(path)) {
+          if(path.length<2) continue;
+          path_parts = [path[0]].concat(path[1].split('.'));
+        }
+        else {
+          path_parts = path.split('.');
+          if(!self.theme.closest(self.container,'[data-schemaid="'+path_parts[0]+'"]')) path_parts.unshift('#');
+        }
+        first = path_parts.shift();
+
+        if(first === '#') first = self.jsoneditor.schema.id || 'root';
+
+        // Find the root node for this template variable
+        root = self.theme.closest(self.container,'[data-schemaid="'+first+'"]');
+        if(!root) throw "Could not find ancestor node with id "+first;
+
+        // Keep track of the root node and path for use when rendering the template
+        adjusted_path = root.getAttribute('data-schemapath') + '.' + path_parts.join('.');
+        
+        self.jsoneditor.watch(adjusted_path,self.watch_listener);
+        
+        self.watched[name] = adjusted_path;
+      }
+    }
+    
+    // Dynamic header
+    if(this.schema.headerTemplate) {
+      this.header_template = this.jsoneditor.compileTemplate(this.schema.headerTemplate, this.template_engine);
+    }
+  },
+  
+  addLinks: function() {
+    // Add links
+    if(!this.no_link_holder) {
+      this.link_holder = this.theme.getLinksHolder();
+      this.container.appendChild(this.link_holder);
+      if(this.schema.links) {
+        for(var i=0; i<this.schema.links.length; i++) {
+          this.addLink(this.getLink(this.schema.links[i]));
+        }
+      }
+    }
+  },
+  
+  
+  getButton: function(text, icon, title) {
+    var btnClass = 'json-editor-btn-'+icon;
+    if(!this.iconlib) icon = null;
+    else icon = this.iconlib.getIcon(icon);
+    
+    if(!icon && title) {
+      text = title;
+      title = null;
+    }
+    
+    var btn = this.theme.getButton(text, icon, title);
+    btn.className += ' ' + btnClass + ' ';
+    return btn;
+  },
+  setButtonText: function(button, text, icon, title) {
+    if(!this.iconlib) icon = null;
+    else icon = this.iconlib.getIcon(icon);
+    
+    if(!icon && title) {
+      text = title;
+      title = null;
+    }
+    
+    return this.theme.setButtonText(button, text, icon, title);
+  },
+  addLink: function(link) {
+    if(this.link_holder) this.link_holder.appendChild(link);
+  },
+  getLink: function(data) {
+    var holder, link;
+        
+    // Get mime type of the link
+    var mime = data.mediaType || 'application/javascript';
+    var type = mime.split('/')[0];
+    
+    // Template to generate the link href
+    var href = this.jsoneditor.compileTemplate(data.href,this.template_engine);
+    var relTemplate = this.jsoneditor.compileTemplate(data.rel ? data.rel : data.href,this.template_engine);
+    
+    // Template to generate the link's download attribute
+    var download = null;
+    if(data.download) download = data.download;
+
+    if(download && download !== true) {
+      download = this.jsoneditor.compileTemplate(download, this.template_engine);
+    }
+
+    // Image links
+    if(type === 'image') {
+      holder = this.theme.getBlockLinkHolder();
+      link = document.createElement('a');
+      link.setAttribute('target','_blank');
+      var image = document.createElement('img');
+      
+      this.theme.createImageLink(holder,link,image);
+    
+      // When a watched field changes, update the url  
+      this.link_watchers.push(function(vars) {
+        var url = href(vars);
+        var rel = relTemplate(vars);
+        link.setAttribute('href',url);
+        link.setAttribute('title',rel || url);
+        image.setAttribute('src',url);
+      });
+    }
+    // Audio/Video links
+    else if(['audio','video'].indexOf(type) >=0) {
+      holder = this.theme.getBlockLinkHolder();
+      
+      link = this.theme.getBlockLink();
+      link.setAttribute('target','_blank');
+      
+      var media = document.createElement(type);
+      media.setAttribute('controls','controls');
+      
+      this.theme.createMediaLink(holder,link,media);
+      
+      // When a watched field changes, update the url  
+      this.link_watchers.push(function(vars) {
+        var url = href(vars);
+        var rel = relTemplate(vars);
+        link.setAttribute('href',url);
+        link.textContent = rel || url;
+        media.setAttribute('src',url);
+      });
+    }
+    // Text links
+    else {
+      link = holder = this.theme.getBlockLink();
+      holder.setAttribute('target','_blank');
+      holder.textContent = data.rel;
+
+      // When a watched field changes, update the url
+      this.link_watchers.push(function(vars) {
+        var url = href(vars);
+        var rel = relTemplate(vars);
+        holder.setAttribute('href',url);
+        holder.textContent = rel || url;
+      });
+    }
+
+    if(download && link) {
+      if(download === true) {
+        link.setAttribute('download','');
+      }
+      else {
+        this.link_watchers.push(function(vars) {
+          link.setAttribute('download',download(vars));
+        });
+      }
+    }
+    
+    if(data.class) link.className = link.className + ' ' + data.class;
+
+    return holder;
+  },
+  refreshWatchedFieldValues: function() {
+    if(!this.watched_values) return;
+    var watched = {};
+    var changed = false;
+    var self = this;
+    
+    if(this.watched) {
+      var val,editor;
+      for(var name in this.watched) {
+        if(!this.watched.hasOwnProperty(name)) continue;
+        editor = self.jsoneditor.getEditor(this.watched[name]);
+        val = editor? editor.getValue() : null;
+        if(self.watched_values[name] !== val) changed = true;
+        watched[name] = val;
+      }
+    }
+    
+    watched.self = this.getValue();
+    if(this.watched_values.self !== watched.self) changed = true;
+    
+    this.watched_values = watched;
+    
+    return changed;
+  },
+  getWatchedFieldValues: function() {
+    return this.watched_values;
+  },
+  updateHeaderText: function() {
+    if(this.header) {
+      // If the header has children, only update the text node's value
+      if(this.header.children.length) {
+        for(var i=0; i<this.header.childNodes.length; i++) {
+          if(this.header.childNodes[i].nodeType===3) {
+            this.header.childNodes[i].nodeValue = this.getHeaderText();
+            break;
+          }
+        }
+      }
+      // Otherwise, just update the entire node
+      else {
+        this.header.textContent = this.getHeaderText();
+      }
+    }
+  },
+  getHeaderText: function(title_only) {
+    if(this.header_text) return this.header_text;
+    else if(title_only) return this.schema.title;
+    else return this.getTitle();
+  },
+  onWatchedFieldChange: function() {
+    var vars;
+    if(this.header_template) {      
+      vars = $extend(this.getWatchedFieldValues(),{
+        key: this.key,
+        i: this.key,
+        i0: (this.key*1),
+        i1: (this.key*1+1),
+        title: this.getTitle()
+      });
+      var header_text = this.header_template(vars);
+      
+      if(header_text !== this.header_text) {
+        this.header_text = header_text;
+        this.updateHeaderText();
+        this.notify();
+        //this.fireChangeHeaderEvent();
+      }
+    }
+    if(this.link_watchers.length) {
+      vars = this.getWatchedFieldValues();
+      for(var i=0; i<this.link_watchers.length; i++) {
+        this.link_watchers[i](vars);
+      }
+    }
+  },
+  setValue: function(value) {
+    this.value = value;
+  },
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    return this.value;
+  },
+  refreshValue: function() {
+
+  },
+  getChildEditors: function() {
+    return false;
+  },
+  destroy: function() {
+    var self = this;
+    this.unregister(this);
+    $each(this.watched,function(name,adjusted_path) {
+      self.jsoneditor.unwatch(adjusted_path,self.watch_listener);
+    });
+    this.watched = null;
+    this.watched_values = null;
+    this.watch_listener = null;
+    this.header_text = null;
+    this.header_template = null;
+    this.value = null;
+    if(this.container && this.container.parentNode) this.container.parentNode.removeChild(this.container);
+    this.container = null;
+    this.jsoneditor = null;
+    this.schema = null;
+    this.path = null;
+    this.key = null;
+    this.parent = null;
+  },
+  getDefault: function() {
+    if (typeof this.schema["default"] !== 'undefined') {
+      return this.schema["default"];
+    }
+
+    if (typeof this.schema["enum"] !== 'undefined') {
+      return this.schema["enum"][0];
+    }
+    
+    var type = this.schema.type || this.schema.oneOf;
+    if(type && Array.isArray(type)) type = type[0];
+    if(type && typeof type === "object") type = type.type;
+    if(type && Array.isArray(type)) type = type[0];
+    
+    if(typeof type === "string") {
+      if(type === "number") return 0.0;
+      if(type === "boolean") return false;
+      if(type === "integer") return 0;
+      if(type === "string") return "";
+      if(type === "object") return {};
+      if(type === "array") return [];
+    }
+    
+    return null;
+  },
+  getTitle: function() {
+    return this.schema.title || this.key;
+  },
+  enable: function() {
+    this.disabled = false;
+  },
+  disable: function() {
+    this.disabled = true;
+  },
+  isEnabled: function() {
+    return !this.disabled;
+  },
+  isRequired: function() {
+    if(typeof this.schema.required === "boolean") return this.schema.required;
+    else if(this.parent && this.parent.schema && Array.isArray(this.parent.schema.required)) return this.parent.schema.required.indexOf(this.key) > -1;
+    else if(this.jsoneditor.options.required_by_default) return true;
+    else return false;
+  },  
+  getDisplayText: function(arr) {
+    var disp = [];
+    var used = {};
+    
+    // Determine how many times each attribute name is used.
+    // This helps us pick the most distinct display text for the schemas.
+    $each(arr,function(i,el) {
+      if(el.title) {
+        used[el.title] = used[el.title] || 0;
+        used[el.title]++;
+      }
+      if(el.description) {
+        used[el.description] = used[el.description] || 0;
+        used[el.description]++;
+      }
+      if(el.format) {
+        used[el.format] = used[el.format] || 0;
+        used[el.format]++;
+      }
+      if(el.type) {
+        used[el.type] = used[el.type] || 0;
+        used[el.type]++;
+      }
+    });
+    
+    // Determine display text for each element of the array
+    $each(arr,function(i,el)  {
+      var name;
+      
+      // If it's a simple string
+      if(typeof el === "string") name = el;
+      // Object
+      else if(el.title && used[el.title]<=1) name = el.title;
+      else if(el.format && used[el.format]<=1) name = el.format;
+      else if(el.type && used[el.type]<=1) name = el.type;
+      else if(el.description && used[el.description]<=1) name = el.descripton;
+      else if(el.title) name = el.title;
+      else if(el.format) name = el.format;
+      else if(el.type) name = el.type;
+      else if(el.description) name = el.description;
+      else if(JSON.stringify(el).length < 50) name = JSON.stringify(el);
+      else name = "type";
+      
+      disp.push(name);
+    });
+    
+    // Replace identical display text with "text 1", "text 2", etc.
+    var inc = {};
+    $each(disp,function(i,name) {
+      inc[name] = inc[name] || 0;
+      inc[name]++;
+      
+      if(used[name] > 1) disp[i] = name + " " + inc[name];
+    });
+    
+    return disp;
+  },
+  getOption: function(key) {
+    try {
+      throw "getOption is deprecated";
+    }
+    catch(e) {
+      window.console.error(e);
+    }
+    
+    return this.options[key];
+  },
+  showValidationErrors: function(errors) {
+
+  }
+});
+
+JSONEditor.defaults.editors["null"] = JSONEditor.AbstractEditor.extend({
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    return null;
+  },
+  setValue: function() {
+    this.onChange();
+  },
+  getNumColumns: function() {
+    return 2;
+  }
+});
+
+JSONEditor.defaults.editors.qbldr = JSONEditor.AbstractEditor.extend({
+  register: function() {
+       this._super();
+       if(!this.input) return;
+       this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+       this._super();
+       if(!this.input) return;
+       this.input.removeAttribute('name');
+  },
+  setValue: function(value, initial) {
+       var self = this;
+           
+       if(typeof value === "undefined" || typeof this.jqbldrId === "undefined" || value === this.value) {
+         return;
+       }
+
+    if ((initial === true) && (value !== "") && (value !== null)) {
+         $(this.jqbldrId).queryBuilder('off','rulesChanged');
+         $(this.jqbldrId).queryBuilder('setRulesFromSQL', value);
+         var filter_result = $(this.jqbldrId).queryBuilder('getSQL');
+         value = filter_result === null ? null : filter_result.sql;
+         $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
+    }
+    
+       this.input.value = value;  
+       this.value = value;
+
+       // Bubble this setValue to parents if the value changed
+       this.onChange(true);
+  },
+  getValue: function() {
+       var self = this;
+
+       if (this.value === "" || this.value === null) { 
+         return undefined; 
+       } else { 
+         return this.value;
+       }
+  },
+  
+  getNumColumns: function() {   
+    return 12;
+  },
+  
+  qbldrRulesChangedCb: function(eventObj) {
+    var self = this;
+
+    $(this.jqbldrId).queryBuilder('off','rulesChanged');
+
+    var filter_result = $(this.jqbldrId).queryBuilder('getSQL');
+    
+    if (filter_result !== null) {
+       this.setValue(filter_result.sql);
+    }
+
+    $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
+
+    return;
+  },
+  preBuild: function() {
+    var self = this;
+    this._super();
+  },
+  build: function() {
+    var self = this;
+    
+    this.qschema = this.schema.qschema;
+    this.qbldrId = this.path;
+    this.jqbldrId = '#' + this.qbldrId;
+    this.jqbldrId = this.jqbldrId.replace(/\./g,'\\.');
+
+    this.qgrid = this.theme.getGridContainer();
+    this.qgrid.style.padding = '4px';
+    this.qgrid.style.border = '1px solid #e3e3e3';
+
+    this.gridrow1 = this.theme.getGridRow();
+    this.gridrow1.style.padding = '4px';
+    
+    this.gridrow2 = this.theme.getGridRow();
+    this.gridrow2.style.padding = '4px';
+    
+    this.title = this.getTitle();
+    this.label = this.theme.getFormInputLabel(this.title);
+
+    this.input = this.theme.getTextareaInput();
+    this.input.disabled = 'true';
+
+    this.control = this.theme.getFormControl(this.label, this.input, this.description);
+    
+    this.gridrow2.setAttribute('id',this.qbldrId);
+    
+    this.container.appendChild(this.qgrid);  // attach the grid to container
+
+    this.qgrid.appendChild(this.gridrow1);   // attach gridrow1 to grid
+    this.gridrow1.appendChild(this.control); // attach control form to gridrow1
+    
+    this.qgrid.appendChild(this.gridrow2);
+    
+    var options = { conditions: [ 'AND', 'OR'], sort_filters: true };
+    
+    $.extend(this.qschema, options);
+    
+    $(this.jqbldrId).queryBuilder(this.qschema);
+    
+    //$(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
+    //$(this.jqbldrId).queryBuilder('on', 'afterUpdateRuleValue', this.qbldrRulesChangedCb.bind(this));
+    $(this.jqbldrId).queryBuilder('on', 'rulesChanged', this.qbldrRulesChangedCb.bind(this));
+  },
+  enable: function() {
+    this._super();
+  },
+  disable: function() {
+    this._super();
+  },
+  afterInputReady: function() {
+    var self = this, options;
+    self.theme.afterInputReady(self.input);
+  },
+  refreshValue: function() {
+    this.value = this.input.value;
+    if(typeof this.value !== "string") this.value = '';
+  },
+  destroy: function() {
+       var self = this;
+    this._super();
+  },
+  /**
+   * This is overridden in derivative editors
+   */
+  sanitize: function(value) {
+    return value;
+  },
+  /**
+   * Re-calculates the value if needed
+   */
+  onWatchedFieldChange: function() {    
+    var self = this, vars, j;
+    
+    this._super();
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+
+    if(this.jsoneditor.options.show_errors === "always") {}
+    else if(this.previous_error_setting===this.jsoneditor.options.show_errors) return;
+    
+    this.previous_error_setting = this.jsoneditor.options.show_errors;
+
+    var messages = [];
+    $each(errors,function(i,error) {
+      if(error.path === self.path) {
+        messages.push(error.message);
+      }
+    });
+
+    this.input.controlgroup = this.control;
+
+    if(messages.length) {
+      this.theme.addInputError(this.input, messages.join('. ')+'.');
+    }
+    else {
+      this.theme.removeInputError(this.input);
+    }
+  }
+});
+
+JSONEditor.defaults.editors.string = JSONEditor.AbstractEditor.extend({
+  register: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  setValue: function(value,initial,from_template) {
+    var self = this;
+    
+    if(this.template && !from_template) {
+      return;
+    }
+    
+    if(value === null || typeof value === 'undefined') value = "";
+    else if(typeof value === "object") value = JSON.stringify(value);
+    else if(typeof value !== "string") value = ""+value;
+    
+    if(value === this.serialized) return;
+
+    // Sanitize value before setting it
+    var sanitized = this.sanitize(value);
+
+    if(this.input.value === sanitized) {
+      return;
+    }
+
+    this.input.value = sanitized;
+    
+    // If using SCEditor, update the WYSIWYG
+    if(this.sceditor_instance) {
+      this.sceditor_instance.val(sanitized);
+    }
+    else if(this.SimpleMDE) {
+      this.SimpleMDE.value(sanitized);
+    }
+    else if(this.ace_editor) {
+      this.ace_editor.setValue(sanitized);
+    }
+    
+    var changed = from_template || this.getValue() !== value;
+    
+    this.refreshValue();
+    
+    if(initial) this.is_dirty = false;
+    else if(this.jsoneditor.options.show_errors === "change") this.is_dirty = true;
+    
+    if(this.adjust_height) this.adjust_height(this.input);
+
+    // Bubble this setValue to parents if the value changed
+    this.onChange(changed);
+  },
+  getNumColumns: function() {
+    var min = Math.ceil(Math.max(this.getTitle().length,this.schema.maxLength||0,this.schema.minLength||0)/5);
+    var num;
+    
+    if(this.input_type === 'textarea') num = 6;
+    else if(['text','email'].indexOf(this.input_type) >= 0) num = 4;
+    else num = 2;
+    
+    return Math.min(12,Math.max(min,num));
+  },
+  build: function() {
+    var self = this, i;
+    if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+    if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
+
+    this.format = this.schema.format;
+    if(!this.format && this.schema.media && this.schema.media.type) {
+      this.format = this.schema.media.type.replace(/(^(application|text)\/(x-)?(script\.)?)|(-source$)/g,'');
+    }
+    if(!this.format && this.options.default_format) {
+      this.format = this.options.default_format;
+    }
+    if(this.options.format) {
+      this.format = this.options.format;
+    }
+
+    // Specific format
+    if(this.format) {
+      // Text Area
+      if(this.format === 'textarea') {
+        this.input_type = 'textarea';
+        this.input = this.theme.getTextareaInput();
+      }
+      // Range Input
+      else if(this.format === 'range') {
+        this.input_type = 'range';
+        var min = this.schema.minimum || 0;
+        var max = this.schema.maximum || Math.max(100,min+1);
+        var step = 1;
+        if(this.schema.multipleOf) {
+          if(min%this.schema.multipleOf) min = Math.ceil(min/this.schema.multipleOf)*this.schema.multipleOf;
+          if(max%this.schema.multipleOf) max = Math.floor(max/this.schema.multipleOf)*this.schema.multipleOf;
+          step = this.schema.multipleOf;
+        }
+
+        this.input = this.theme.getRangeInput(min,max,step);
+      }
+      // Source Code
+      else if([
+          'actionscript',
+          'batchfile',
+          'bbcode',
+          'c',
+          'c++',
+          'cpp',
+          'coffee',
+          'csharp',
+          'css',
+          'dart',
+          'django',
+          'ejs',
+          'erlang',
+          'golang',
+          'groovy',
+          'handlebars',
+          'haskell',
+          'haxe',
+          'html',
+          'ini',
+          'jade',
+          'java',
+          'javascript',
+          'json',
+          'less',
+          'lisp',
+          'lua',
+          'makefile',
+          'markdown',
+          'matlab',
+          'mysql',
+          'objectivec',
+          'pascal',
+          'perl',
+          'pgsql',
+          'php',
+          'python',
+          'r',
+          'ruby',
+          'sass',
+          'scala',
+          'scss',
+          'smarty',
+          'sql',
+          'stylus',
+          'svg',
+          'twig',
+          'vbscript',
+          'xml',
+          'yaml'
+        ].indexOf(this.format) >= 0
+      ) {
+        this.input_type = this.format;
+        this.source_code = true;
+        
+        this.input = this.theme.getTextareaInput();
+      }
+      // HTML5 Input type
+      else {
+        this.input_type = this.format;
+        this.input = this.theme.getFormInputField(this.input_type);
+      }
+    }
+    // Normal text input
+    else {
+      this.input_type = 'text';
+      this.input = this.theme.getFormInputField(this.input_type);
+    }
+    
+    // minLength, maxLength, and pattern
+    if(typeof this.schema.maxLength !== "undefined") this.input.setAttribute('maxlength',this.schema.maxLength);
+    if(typeof this.schema.pattern !== "undefined") this.input.setAttribute('pattern',this.schema.pattern);
+    else if(typeof this.schema.minLength !== "undefined") this.input.setAttribute('pattern','.{'+this.schema.minLength+',}');
+
+    if(this.options.compact) {
+      this.container.className += ' compact';
+    }
+    else {
+      if(this.options.input_width) this.input.style.width = this.options.input_width;
+    }
+
+    if(this.schema.readOnly || this.schema.readonly || this.schema.template) {
+      this.always_disabled = true;
+      this.input.disabled = true;
+    }
+
+    this.input
+      .addEventListener('change',function(e) {        
+        e.preventDefault();
+        e.stopPropagation();
+        
+        // Don't allow changing if this field is a template
+        if(self.schema.template) {
+          this.value = self.value;
+          return;
+        }
+
+        var val = this.value;
+        
+        // sanitize value
+        var sanitized = self.sanitize(val);
+        if(val !== sanitized) {
+          this.value = sanitized;
+        }
+        
+        self.is_dirty = true;
+
+        self.refreshValue();
+        self.onChange(true);
+      });
+      
+    if(this.options.input_height) this.input.style.height = this.options.input_height;
+    if(this.options.expand_height) {
+      this.adjust_height = function(el) {
+        if(!el) return;
+        var i, ch=el.offsetHeight;
+        // Input too short
+        if(el.offsetHeight < el.scrollHeight) {
+          i=0;
+          while(el.offsetHeight < el.scrollHeight+3) {
+            if(i>100) break;
+            i++;
+            ch++;
+            el.style.height = ch+'px';
+          }
+        }
+        else {
+          i=0;
+          while(el.offsetHeight >= el.scrollHeight+3) {
+            if(i>100) break;
+            i++;
+            ch--;
+            el.style.height = ch+'px';
+          }
+          el.style.height = (ch+1)+'px';
+        }
+      };
+      
+      this.input.addEventListener('keyup',function(e) {
+        self.adjust_height(this);
+      });
+      this.input.addEventListener('change',function(e) {
+        self.adjust_height(this);
+      });
+      this.adjust_height();
+    }
+
+    if(this.format) this.input.setAttribute('data-schemaformat',this.format);
+
+    this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
+    this.container.appendChild(this.control);
+
+    // Any special formatting that needs to happen after the input is added to the dom
+    window.requestAnimationFrame(function() {
+      // Skip in case the input is only a temporary editor,
+      // otherwise, in the case of an ace_editor creation,
+      // it will generate an error trying to append it to the missing parentNode
+      if(self.input.parentNode) self.afterInputReady();
+      if(self.adjust_height) self.adjust_height(self.input);
+    });
+
+    // Compile and store the template
+    if(this.schema.template) {
+      this.template = this.jsoneditor.compileTemplate(this.schema.template, this.template_engine);
+      this.refreshValue();
+    }
+    else {
+      this.refreshValue();
+    }
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      this.input.disabled = false;
+      // TODO: WYSIWYG and Markdown editors
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    this.input.disabled = true;
+    // TODO: WYSIWYG and Markdown editors
+    this._super();
+  },
+  afterInputReady: function() {
+    var self = this, options;
+    
+    // Code editor
+    if(this.source_code) {      
+      // WYSIWYG html and bbcode editor
+      if(this.options.wysiwyg && 
+        ['html','bbcode'].indexOf(this.input_type) >= 0 && 
+        window.jQuery && window.jQuery.fn && window.jQuery.fn.sceditor
+      ) {
+        options = $extend({},{
+          plugins: self.input_type==='html'? 'xhtml' : 'bbcode',
+          emoticonsEnabled: false,
+          width: '100%',
+          height: 300
+        },JSONEditor.plugins.sceditor,self.options.sceditor_options||{});
+        
+        window.jQuery(self.input).sceditor(options);
+        
+        self.sceditor_instance = window.jQuery(self.input).sceditor('instance');
+        
+        self.sceditor_instance.blur(function() {
+          // Get editor's value
+          var val = window.jQuery("<div>"+self.sceditor_instance.val()+"</div>");
+          // Remove sceditor spans/divs
+          window.jQuery('#sceditor-start-marker,#sceditor-end-marker,.sceditor-nlf',val).remove();
+          // Set the value and update
+          self.input.value = val.html();
+          self.value = self.input.value;
+          self.is_dirty = true;
+          self.onChange(true);
+        });
+      }
+      // SimpleMDE for markdown (if it's loaded)
+      else if (this.input_type === 'markdown' && window.SimpleMDE) {
+        options = $extend({},JSONEditor.plugins.SimpleMDE,{
+          element: this.input
+        });
+
+        this.SimpleMDE = new window.SimpleMDE((options));
+
+        this.SimpleMDE.codemirror.on("change",function() {
+          self.value = self.SimpleMDE.value();
+          self.is_dirty = true;
+          self.onChange(true);
+        });
+      }
+      // ACE editor for everything else
+      else if(window.ace) {
+        var mode = this.input_type;
+        // aliases for c/cpp
+        if(mode === 'cpp' || mode === 'c++' || mode === 'c') {
+          mode = 'c_cpp';
+        }
+        
+        this.ace_container = document.createElement('div');
+        this.ace_container.style.width = '100%';
+        this.ace_container.style.position = 'relative';
+        this.ace_container.style.height = '400px';
+        this.input.parentNode.insertBefore(this.ace_container,this.input);
+        this.input.style.display = 'none';
+        this.ace_editor = window.ace.edit(this.ace_container);
+        
+        this.ace_editor.setValue(this.getValue());
+
+        // The theme
+        if(JSONEditor.plugins.ace.theme) this.ace_editor.setTheme('ace/theme/'+JSONEditor.plugins.ace.theme);
+        // The mode
+        this.ace_editor.getSession().setMode('ace/mode/' + this.schema.format);
+
+        // Listen for changes
+        this.ace_editor.on('change',function() {
+          var val = self.ace_editor.getValue();
+          self.input.value = val;
+          self.refreshValue();
+          self.is_dirty = true;
+          self.onChange(true);
+        });
+      }
+    }
+    
+    self.theme.afterInputReady(self.input);
+  },
+  refreshValue: function() {
+    this.value = this.input.value;
+    if(typeof this.value !== "string") this.value = '';
+    this.serialized = this.value;
+  },
+  destroy: function() {
+    // If using SCEditor, destroy the editor instance
+    if(this.sceditor_instance) {
+      this.sceditor_instance.destroy();
+    }
+    else if(this.SimpleMDE) {
+      this.SimpleMDE.destroy();
+    }
+    else if(this.ace_editor) {
+      this.ace_editor.destroy();
+    }
+    
+    
+    this.template = null;
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+
+    this._super();
+  },
+  /**
+   * This is overridden in derivative editors
+   */
+  sanitize: function(value) {
+    return value;
+  },
+  /**
+   * Re-calculates the value if needed
+   */
+  onWatchedFieldChange: function() {    
+    var self = this, vars, j;
+    
+    // If this editor needs to be rendered by a macro template
+    if(this.template) {
+      vars = this.getWatchedFieldValues();
+      this.setValue(this.template(vars),false,true);
+    }
+    
+    this._super();
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+    
+    if(this.jsoneditor.options.show_errors === "always") {}
+    else if(!this.is_dirty && this.previous_error_setting===this.jsoneditor.options.show_errors) return;
+    
+    this.previous_error_setting = this.jsoneditor.options.show_errors;
+
+    var messages = [];
+    $each(errors,function(i,error) {
+      if(error.path === self.path) {
+        messages.push(error.message);
+      }
+    });
+
+    this.input.controlgroup = this.control;
+    
+    if(messages.length) {
+      this.theme.addInputError(this.input, messages.join('. ')+'.');
+    }
+    else {
+      this.theme.removeInputError(this.input);
+    }
+  }
+});
+
+/**
+ * Created by Mehmet Baker on 12.04.2017
+ */
+JSONEditor.defaults.editors.hidden = JSONEditor.AbstractEditor.extend({
+  register: function () {
+    this._super();
+    if (!this.input) return;
+    this.input.setAttribute('name', this.formname);
+  },
+  unregister: function () {
+    this._super();
+    if (!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  setValue: function (value, initial, from_template) {
+    var self = this;
+
+    if(this.template && !from_template) {
+      return;
+    }
+
+    if(value === null || typeof value === 'undefined') value = "";
+    else if(typeof value === "object") value = JSON.stringify(value);
+    else if(typeof value !== "string") value = ""+value;
+
+    if(value === this.serialized) return;
+
+    // Sanitize value before setting it
+    var sanitized = this.sanitize(value);
+
+    if(this.input.value === sanitized) {
+      return;
+    }
+
+    this.input.value = sanitized;
+
+    var changed = from_template || this.getValue() !== value;
+
+    this.refreshValue();
+
+    if(initial) this.is_dirty = false;
+    else if(this.jsoneditor.options.show_errors === "change") this.is_dirty = true;
+
+    if(this.adjust_height) this.adjust_height(this.input);
+
+    // Bubble this setValue to parents if the value changed
+    this.onChange(changed);
+  },
+  getNumColumns: function () {
+    return 2;
+  },
+  enable: function () {
+    this._super();
+  },
+  disable: function () {
+    this._super();
+  },
+  refreshValue: function () {
+    this.value = this.input.value;
+    if (typeof this.value !== "string") this.value = '';
+    this.serialized = this.value;
+  },
+  destroy: function () {
+    this.template = null;
+    if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
+    if (this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+
+    this._super();
+  },
+  /**
+   * This is overridden in derivative editors
+   */
+  sanitize: function (value) {
+    return value;
+  },
+  /**
+   * Re-calculates the value if needed
+   */
+  onWatchedFieldChange: function () {
+    var self = this, vars, j;
+
+    // If this editor needs to be rendered by a macro template
+    if (this.template) {
+      vars = this.getWatchedFieldValues();
+      this.setValue(this.template(vars), false, true);
+    }
+
+    this._super();
+  },
+  build: function () {
+    var self = this;
+
+    this.format = this.schema.format;
+    if (!this.format && this.options.default_format) {
+      this.format = this.options.default_format;
+    }
+    if (this.options.format) {
+      this.format = this.options.format;
+    }
+
+    this.input_type = 'hidden';
+    this.input = this.theme.getFormInputField(this.input_type);
+
+    if (this.format) this.input.setAttribute('data-schemaformat', this.format);
+
+    this.container.appendChild(this.input);
+
+    // Compile and store the template
+    if (this.schema.template) {
+      this.template = this.jsoneditor.compileTemplate(this.schema.template, this.template_engine);
+      this.refreshValue();
+    }
+    else {
+      this.refreshValue();
+    }
+  }
+});
+JSONEditor.defaults.editors.number = JSONEditor.defaults.editors.string.extend({
+  build: function() {
+    this._super();
+
+    if (typeof this.schema.minimum !== "undefined") {
+      var minimum = this.schema.minimum;
+
+      if (typeof this.schema.exclusiveMinimum !== "undefined") {
+        minimum += 1;
+      }
+
+      this.input.setAttribute("min", minimum);
+    }
+
+    if (typeof this.schema.maximum !== "undefined") {
+      var maximum = this.schema.maximum;
+
+      if (typeof this.schema.exclusiveMaximum !== "undefined") {
+        maximum -= 1;
+      }
+
+      this.input.setAttribute("max", maximum);
+    }
+
+    if (typeof this.schema.step !== "undefined") {
+      var step = this.schema.step || 1;
+      this.input.setAttribute("step", step);
+    }
+
+  },
+  sanitize: function(value) {
+    return (value+"").replace(/[^0-9\.\-eE]/g,'');
+  },
+  getNumColumns: function() {
+    return 2;
+  },
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    return this.value===''?undefined:this.value*1;
+  }
+});
+
+JSONEditor.defaults.editors.integer = JSONEditor.defaults.editors.number.extend({
+  sanitize: function(value) {
+    value = value + "";
+    return value.replace(/[^0-9\-]/g,'');
+  },
+  getNumColumns: function() {
+    return 2;
+  }
+});
+
+JSONEditor.defaults.editors.rating = JSONEditor.defaults.editors.integer.extend({
+  build: function() {
+    var self = this, i;
+    if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+
+    // Dynamically add the required CSS the first time this editor is used
+    var styleId = 'json-editor-style-rating';
+    var styles = document.getElementById(styleId);
+    if (!styles) {
+      var style = document.createElement('style');
+      style.id = styleId;
+      style.type = 'text/css';
+      style.innerHTML =
+        '      .rating-container {' +
+        '        display: inline-block;' +
+        '        clear: both;' +
+        '      }' +
+        '      ' +
+        '      .rating {' +
+        '        float:left;' +
+        '      }' +
+        '      ' +
+        '      /* :not(:checked) is a filter, so that browsers that don’t support :checked don’t' +
+        '         follow these rules. Every browser that supports :checked also supports :not(), so' +
+        '         it doesn’t make the test unnecessarily selective */' +
+        '      .rating:not(:checked) > input {' +
+        '        position:absolute;' +
+        '        top:-9999px;' +
+        '        clip:rect(0,0,0,0);' +
+        '      }' +
+        '      ' +
+        '      .rating:not(:checked) > label {' +
+        '        float:right;' +
+        '        width:1em;' +
+        '        padding:0 .1em;' +
+        '        overflow:hidden;' +
+        '        white-space:nowrap;' +
+        '        cursor:pointer;' +
+        '        color:#ddd;' +
+        '      }' +
+        '      ' +
+        '      .rating:not(:checked) > label:before {' +
+        '        content: \'★ \';' +
+        '      }' +
+        '      ' +
+        '      .rating > input:checked ~ label {' +
+        '        color: #FFB200;' +
+        '      }' +
+        '      ' +
+        '      .rating:not([readOnly]):not(:checked) > label:hover,' +
+        '      .rating:not([readOnly]):not(:checked) > label:hover ~ label {' +
+        '        color: #FFDA00;' +
+        '      }' +
+        '      ' +
+        '      .rating:not([readOnly]) > input:checked + label:hover,' +
+        '      .rating:not([readOnly]) > input:checked + label:hover ~ label,' +
+        '      .rating:not([readOnly]) > input:checked ~ label:hover,' +
+        '      .rating:not([readOnly]) > input:checked ~ label:hover ~ label,' +
+        '      .rating:not([readOnly]) > label:hover ~ input:checked ~ label {' +
+        '        color: #FF8C0D;' +
+        '      }' +
+        '      ' +
+        '      .rating:not([readOnly])  > label:active {' +
+        '        position:relative;' +
+        '        top:2px;' +
+        '        left:2px;' +
+        '      }';
+      document.getElementsByTagName('head')[0].appendChild(style);
+    }
+
+    this.input = this.theme.getFormInputField('hidden');
+    this.container.appendChild(this.input);
+
+    // Required to keep height
+    var ratingContainer = document.createElement('div');
+    ratingContainer.className = 'rating-container';
+
+    // Contains options for rating
+    var group = document.createElement('div');
+    group.setAttribute('name', this.formname);
+    group.className = 'rating';
+    ratingContainer.appendChild(group);
+
+    if(this.options.compact) this.container.setAttribute('class',this.container.getAttribute('class')+' compact');
+
+    var max = this.schema.maximum ? this.schema.maximum : 5;
+    if (this.schema.exclusiveMaximum) max--;
+
+    this.inputs = [];
+    for(i=max; i>0; i--) {
+      var id = this.formname + i;
+      var radioInput = this.theme.getFormInputField('radio');
+      radioInput.setAttribute('id', id);
+      radioInput.setAttribute('value', i);
+      radioInput.setAttribute('name', this.formname);
+      group.appendChild(radioInput);
+      this.inputs.push(radioInput);
+
+      var label = document.createElement('label');
+      label.setAttribute('for', id);
+      label.appendChild(document.createTextNode(i + (i == 1 ? ' star' : ' stars')));
+      group.appendChild(label);
+    }
+
+    if(this.schema.readOnly || this.schema.readonly) {
+      this.always_disabled = true;
+      $each(this.inputs,function(i,input) {
+        group.setAttribute("readOnly", "readOnly");
+        input.disabled = true;
+      });
+    }
+
+    ratingContainer
+      .addEventListener('change',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        self.input.value = e.srcElement.value;
+
+        self.is_dirty = true;
+
+        self.refreshValue();
+        self.watch_listener();
+        self.jsoneditor.notifyWatchers(self.path);
+        if(self.parent) self.parent.onChildEditorChange(self);
+        else self.jsoneditor.onChange();
+      });
+
+    this.control = this.theme.getFormControl(this.label, ratingContainer, this.description);
+    this.container.appendChild(this.control);
+
+    this.refreshValue();
+  },
+  setValue: function(val) {
+    var sanitized = this.sanitize(val);
+    if(this.value === sanitized) {
+      return;
+    }
+    var self = this;
+    $each(this.inputs,function(i,input) {
+      if (input.value === sanitized) {
+        input.checked = true;
+        self.value = sanitized;
+        self.input.value = self.value;
+        self.watch_listener();
+        self.jsoneditor.notifyWatchers(self.path);
+        return false;
+      }
+    });
+  }
+});
+
+JSONEditor.defaults.editors.object = JSONEditor.AbstractEditor.extend({
+  getDefault: function() {
+    return $extend({},this.schema["default"] || {});
+  },
+  getChildEditors: function() {
+    return this.editors;
+  },
+  register: function() {
+    this._super();
+    if(this.editors) {
+      for(var i in this.editors) {
+        if(!this.editors.hasOwnProperty(i)) continue;
+        this.editors[i].register();
+      }
+    }
+  },
+  unregister: function() {
+    this._super();
+    if(this.editors) {
+      for(var i in this.editors) {
+        if(!this.editors.hasOwnProperty(i)) continue;
+        this.editors[i].unregister();
+      }
+    }
+  },
+  getNumColumns: function() {
+    return Math.max(Math.min(12,this.maxwidth),3);
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.editjson_button) this.editjson_button.disabled = false;
+      if(this.addproperty_button) this.addproperty_button.disabled = false;
+
+      this._super();
+      if(this.editors) {
+        for(var i in this.editors) {
+          if(!this.editors.hasOwnProperty(i)) continue;
+          this.editors[i].enable();
+        }
+      }
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.editjson_button) this.editjson_button.disabled = true;
+    if(this.addproperty_button) this.addproperty_button.disabled = true;
+    this.hideEditJSON();
+
+    this._super();
+    if(this.editors) {
+      for(var i in this.editors) {
+        if(!this.editors.hasOwnProperty(i)) continue;
+        this.editors[i].disable(always_disabled);
+      }
+    }
+  },
+  layoutEditors: function() {
+    var self = this, i, j;
+
+    if(!this.row_container) return;
+
+    // Sort editors by propertyOrder
+    this.property_order = Object.keys(this.editors);
+    this.property_order = this.property_order.sort(function(a,b) {
+      var ordera = self.editors[a].schema.propertyOrder;
+      var orderb = self.editors[b].schema.propertyOrder;
+      if(typeof ordera !== "number") ordera = 1000;
+      if(typeof orderb !== "number") orderb = 1000;
+
+      return ordera - orderb;
+    });
+
+    var container = document.createElement('div');
+    var isCategoriesFormat = (this.format === 'categories');
+
+    if(this.format === 'grid') {
+      var rows = [];
+      $each(this.property_order, function(j,key) {
+        var editor = self.editors[key];
+        if(editor.property_removed) return;
+        var found = false;
+        var width = editor.options.hidden? 0 : (editor.options.grid_columns || editor.getNumColumns());
+        var height = editor.options.hidden? 0 : editor.container.offsetHeight;
+        // See if the editor will fit in any of the existing rows first
+        for(var i=0; i<rows.length; i++) {
+          // If the editor will fit in the row horizontally
+          if(rows[i].width + width <= 12) {
+            // If the editor is close to the other elements in height
+            // i.e. Don't put a really tall editor in an otherwise short row or vice versa
+            if(!height || (rows[i].minh*0.5 < height && rows[i].maxh*2 > height)) {
+              found = i;
+            }
+          }
+        }
+
+        // If there isn't a spot in any of the existing rows, start a new row
+        if(found === false) {
+          rows.push({
+            width: 0,
+            minh: 999999,
+            maxh: 0,
+            editors: []
+          });
+          found = rows.length-1;
+        }
+
+        rows[found].editors.push({
+          key: key,
+          //editor: editor,
+          width: width,
+          height: height
+        });
+        rows[found].width += width;
+        rows[found].minh = Math.min(rows[found].minh,height);
+        rows[found].maxh = Math.max(rows[found].maxh,height);
+      });
+
+      // Make almost full rows width 12
+      // Do this by increasing all editors' sizes proprotionately
+      // Any left over space goes to the biggest editor
+      // Don't touch rows with a width of 6 or less
+      for(i=0; i<rows.length; i++) {
+        if(rows[i].width < 12) {
+          var biggest = false;
+          var new_width = 0;
+          for(j=0; j<rows[i].editors.length; j++) {
+            if(biggest === false) biggest = j;
+            else if(rows[i].editors[j].width > rows[i].editors[biggest].width) biggest = j;
+            rows[i].editors[j].width *= 12/rows[i].width;
+            rows[i].editors[j].width = Math.floor(rows[i].editors[j].width);
+            new_width += rows[i].editors[j].width;
+          }
+          if(new_width < 12) rows[i].editors[biggest].width += 12-new_width;
+          rows[i].width = 12;
+        }
+      }
+
+      // layout hasn't changed
+      if(this.layout === JSON.stringify(rows)) return false;
+      this.layout = JSON.stringify(rows);
+
+      // Layout the form
+      for(i=0; i<rows.length; i++) {
+        var row = this.theme.getGridRow();
+        container.appendChild(row);
+        for(j=0; j<rows[i].editors.length; j++) {
+          var key = rows[i].editors[j].key;
+          var editor = this.editors[key];
+
+          if(editor.options.hidden) editor.container.style.display = 'none';
+          else this.theme.setGridColumnSize(editor.container,rows[i].editors[j].width);
+          row.appendChild(editor.container);
+        }
+      }
+    }
+    // Normal layout
+    else if(isCategoriesFormat) {
+      //A container for properties not object nor arrays
+      var containerSimple = document.createElement('div');
+      //This will be the place to (re)build tabs and panes
+      //tabs_holder has 2 childs, [0]: ul.nav.nav-tabs and [1]: div.tab-content
+      var newTabs_holder = this.theme.getTopTabHolder(this.schema.title);
+      //child [1] of previous, stores panes
+      var newTabPanesContainer = this.theme.getTopTabContentHolder(newTabs_holder);
+
+      $each(this.property_order, function(i,key){
+        var editor = self.editors[key];
+        if(editor.property_removed) return;
+        var aPane = self.theme.getTabContent();
+        var isObjOrArray = editor.schema && (editor.schema.type === "object" || editor.schema.type === "array");
+        //mark the pane
+        aPane.isObjOrArray = isObjOrArray;
+        var gridRow = self.theme.getGridRow();
+
+        //this happens with added properties, they don't have a tab
+        if(!editor.tab){
+          //Pass the pane which holds the editor
+          if(typeof self.basicPane === 'undefined'){
+            //There is no basicPane yet, so aPane will be it
+            self.addRow(editor,newTabs_holder, aPane);
+          }
+          else {
+            self.addRow(editor,newTabs_holder, self.basicPane);
+          }
+        }
+
+        aPane.id = editor.tab_text.textContent;
+
+        //For simple properties, add them on the same panel (Basic)
+        if(!isObjOrArray){
+          containerSimple.appendChild(gridRow);
+          //There is already some panes
+          if(newTabPanesContainer.childElementCount > 0){
+            //If first pane is object or array, insert before a simple pane
+            if(newTabPanesContainer.firstChild.isObjOrArray){
+              //Append pane for simple properties
+              aPane.appendChild(containerSimple);
+              newTabPanesContainer.insertBefore(aPane,newTabPanesContainer.firstChild);
+              //Add "Basic" tab
+              self.theme.insertBasicTopTab(editor.tab,newTabs_holder);
+              //newTabs_holder.firstChild.insertBefore(editor.tab,newTabs_holder.firstChild.firstChild);
+              //Update the basicPane
+              editor.basicPane = aPane;
+            }
+            else {
+              //We already have a first "Basic" pane, just add the new property to it, so
+              //do nothing;
+            }
+          }
+          //There is no pane, so add the first (simple) pane
+          else {
+            //Append pane for simple properties
+            aPane.appendChild(containerSimple);
+            newTabPanesContainer.appendChild(aPane);
+            //Add "Basic" tab
+            //newTabs_holder.firstChild.appendChild(editor.tab);
+            self.theme.addTopTab(newTabs_holder,editor.tab);
+            //Update the basicPane
+            editor.basicPane = aPane;
+          }
+        }
+        //Objects and arrays earn it's own panes
+        else {
+          aPane.appendChild(gridRow);
+          newTabPanesContainer.appendChild(aPane);
+          //newTabs_holder.firstChild.appendChild(editor.tab);
+          self.theme.addTopTab(newTabs_holder,editor.tab);
+        }
+
+        if(editor.options.hidden) editor.container.style.display = 'none';
+        else self.theme.setGridColumnSize(editor.container,12);
+        //Now, add the property editor to the row
+        gridRow.appendChild(editor.container);
+        //Update the container (same as self.rows[x].container)
+        editor.container = aPane;
+
+      });
+
+      //Erase old panes
+      while (this.tabPanesContainer.firstChild) {
+        this.tabPanesContainer.removeChild(this.tabPanesContainer.firstChild);
+      }
+
+      //Erase old tabs and set the new ones
+      var parentTabs_holder = this.tabs_holder.parentNode;
+      parentTabs_holder.removeChild(parentTabs_holder.firstChild);
+      parentTabs_holder.appendChild(newTabs_holder);
+
+      this.tabPanesContainer = newTabPanesContainer;
+      this.tabs_holder = newTabs_holder;
+
+      //Activate the first tab
+      var firstTab = this.theme.getFirstTab(this.tabs_holder);
+      if(firstTab){
+        $trigger(firstTab,'click');
+      }
+      return;
+    }
+    // !isCategoriesFormat
+    else {
+      $each(this.property_order, function(i,key) {
+        var editor = self.editors[key];
+        if(editor.property_removed) return;
+        var row = self.theme.getGridRow();
+        container.appendChild(row);
+
+        if(editor.options.hidden) editor.container.style.display = 'none';
+        else self.theme.setGridColumnSize(editor.container,12);
+        row.appendChild(editor.container);
+      });
+    }
+    //for grid and normal layout
+    while (this.row_container.firstChild) {
+      this.row_container.removeChild(this.row_container.firstChild);
+    }
+    this.row_container.appendChild(container);
+  },
+  getPropertySchema: function(key) {
+    // Schema declared directly in properties
+    var schema = this.schema.properties[key] || {};
+    schema = $extend({},schema);
+    var matched = this.schema.properties[key]? true : false;
+
+    // Any matching patternProperties should be merged in
+    if(this.schema.patternProperties) {
+      for(var i in this.schema.patternProperties) {
+        if(!this.schema.patternProperties.hasOwnProperty(i)) continue;
+        var regex = new RegExp(i);
+        if(regex.test(key)) {
+          schema.allOf = schema.allOf || [];
+          schema.allOf.push(this.schema.patternProperties[i]);
+          matched = true;
+        }
+      }
+    }
+
+    // Hasn't matched other rules, use additionalProperties schema
+    if(!matched && this.schema.additionalProperties && typeof this.schema.additionalProperties === "object") {
+      schema = $extend({},this.schema.additionalProperties);
+    }
+
+    return schema;
+  },
+  preBuild: function() {
+    this._super();
+
+    this.editors = {};
+    this.cached_editors = {};
+    var self = this;
+
+    this.format = this.options.layout || this.options.object_layout || this.schema.format || this.jsoneditor.options.object_layout || 'normal';
+
+    this.schema.properties = this.schema.properties || {};
+
+    this.minwidth = 0;
+    this.maxwidth = 0;
+
+    // If the object should be rendered as a table row
+    if(this.options.table_row) {
+      $each(this.schema.properties, function(key,schema) {
+        var editor = self.jsoneditor.getEditorClass(schema);
+        self.editors[key] = self.jsoneditor.createEditor(editor,{
+          jsoneditor: self.jsoneditor,
+          schema: schema,
+          path: self.path+'.'+key,
+          parent: self,
+          compact: true,
+          required: true
+        });
+        self.editors[key].preBuild();
+
+        var width = self.editors[key].options.hidden? 0 : (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
+
+        self.minwidth += width;
+        self.maxwidth += width;
+      });
+      this.no_link_holder = true;
+    }
+    // If the object should be rendered as a table
+    else if(this.options.table) {
+      // TODO: table display format
+      throw "Not supported yet";
+    }
+    // If the object should be rendered as a div
+    else {
+      if(!this.schema.defaultProperties) {
+        if(this.jsoneditor.options.display_required_only || this.options.display_required_only) {
+          this.schema.defaultProperties = [];
+          $each(this.schema.properties, function(k,s) {
+            if(self.isRequired({key: k, schema: s})) {
+              self.schema.defaultProperties.push(k);
+            }
+          });
+        }
+        else {
+          self.schema.defaultProperties = Object.keys(self.schema.properties);
+        }
+      }
+
+      // Increase the grid width to account for padding
+      self.maxwidth += 1;
+
+      $each(this.schema.defaultProperties, function(i,key) {
+        self.addObjectProperty(key, true);
+
+        if(self.editors[key]) {
+          self.minwidth = Math.max(self.minwidth,(self.editors[key].options.grid_columns || self.editors[key].getNumColumns()));
+          self.maxwidth += (self.editors[key].options.grid_columns || self.editors[key].getNumColumns());
+        }
+      });
+    }
+
+    // Sort editors by propertyOrder
+    this.property_order = Object.keys(this.editors);
+    this.property_order = this.property_order.sort(function(a,b) {
+      var ordera = self.editors[a].schema.propertyOrder;
+      var orderb = self.editors[b].schema.propertyOrder;
+      if(typeof ordera !== "number") ordera = 1000;
+      if(typeof orderb !== "number") orderb = 1000;
+
+      return ordera - orderb;
+    });
+  },
+  //"Borrow" from arrays code
+  addTab: function(idx){
+      var self = this;
+      var isObjOrArray = self.rows[idx].schema && (self.rows[idx].schema.type === "object" || self.rows[idx].schema.type === "array");
+      if(self.tabs_holder) {
+        self.rows[idx].tab_text = document.createElement('span');
+
+        if(!isObjOrArray){
+          self.rows[idx].tab_text.textContent = (typeof self.schema.basicCategoryTitle === 'undefined') ? "Basic" : self.schema.basicCategoryTitle;
+        } else {
+          self.rows[idx].tab_text.textContent = self.rows[idx].getHeaderText();
+        }
+        self.rows[idx].tab = self.theme.getTopTab(self.rows[idx].tab_text,self.rows[idx].tab_text.textContent);
+        self.rows[idx].tab.addEventListener('click', function(e) {
+          self.active_tab = self.rows[idx].tab;
+          self.refreshTabs();
+          e.preventDefault();
+          e.stopPropagation();
+        });
+
+      }
+
+    },
+  addRow: function(editor, tabHolder, holder) {
+    var self = this;
+    var rowsLen = this.rows.length;
+    var isObjOrArray = editor.schema.type === "object" || editor.schema.type === "array";
+
+    //Add a row
+    self.rows[rowsLen] = editor;
+    //container stores the editor corresponding pane to set the display style when refreshing Tabs
+    self.rows[rowsLen].container = holder;
+
+    if(!isObjOrArray){
+
+      //This is the first simple property to be added,
+      //add a ("Basic") tab for it and save it's row number
+      if(typeof self.basicTab === "undefined"){
+        self.addTab(rowsLen);
+        //Store the index row of the first simple property added
+        self.basicTab = rowsLen;
+        self.basicPane = holder;
+        self.theme.addTopTab(tabHolder, self.rows[rowsLen].tab);
+      }
+
+      else {
+        //Any other simple property gets the same tab (and the same pane) as the first one,
+        //so, when 'click' event is fired from a row, it gets the correct ("Basic") tab
+        self.rows[rowsLen].tab = self.rows[self.basicTab].tab;
+        self.rows[rowsLen].tab_text = self.rows[self.basicTab].tab_text;
+        self.rows[rowsLen].container = self.rows[self.basicTab].container;
+      }
+    }
+    else {
+      self.addTab(rowsLen);
+      self.theme.addTopTab(tabHolder, self.rows[rowsLen].tab);
+    }
+  },
+  //Mark the active tab and make visible the corresponding pane, hide others
+  refreshTabs: function(refresh_headers) {
+    var self = this;
+    var basicTabPresent = typeof self.basicTab !== 'undefined';
+    var basicTabRefreshed = false;
+
+    $each(this.rows, function(i,row) {
+      //If it's an orphan row (some property which has been deleted), return
+      if(!row.tab || !row.container || !row.container.parentNode) return;
+
+      if(basicTabPresent && row.tab == self.rows[self.basicTab].tab && basicTabRefreshed) return;
+
+      if(refresh_headers) {
+        row.tab_text.textContent = row.getHeaderText();
+      }
+      else {
+        //All rows of simple properties point to the same tab, so refresh just once
+        if(basicTabPresent && row.tab == self.rows[self.basicTab].tab) basicTabRefreshed = true;
+
+        if(row.tab === self.active_tab) {
+          self.theme.markTabActive(row);
+        }
+        else {
+          self.theme.markTabInactive(row);
+        }
+      }
+    });
+  },
+  build: function() {
+    var self = this;
+
+    var isCategoriesFormat = (this.format === 'categories');
+    this.rows=[];
+    this.active_tab = null;
+
+    // If the object should be rendered as a table row
+    if(this.options.table_row) {
+      this.editor_holder = this.container;
+      $each(this.editors, function(key,editor) {
+        var holder = self.theme.getTableCell();
+        self.editor_holder.appendChild(holder);
+
+        editor.setContainer(holder);
+        editor.build();
+        editor.postBuild();
+
+        if(self.editors[key].options.hidden) {
+          holder.style.display = 'none';
+        }
+        if(self.editors[key].options.input_width) {
+          holder.style.width = self.editors[key].options.input_width;
+        }
+      });
+    }
+    // If the object should be rendered as a table
+    else if(this.options.table) {
+      // TODO: table display format
+      throw "Not supported yet";
+    }
+    // If the object should be rendered as a div
+    else {
+      this.header = document.createElement('span');
+      this.header.textContent = this.getTitle();
+      this.title = this.theme.getHeader(this.header);
+      this.container.appendChild(this.title);
+      this.container.style.position = 'relative';
+
+      // Edit JSON modal
+      this.editjson_holder = this.theme.getModal();
+      this.editjson_textarea = this.theme.getTextareaInput();
+      this.editjson_textarea.style.height = '170px';
+      this.editjson_textarea.style.width = '300px';
+      this.editjson_textarea.style.display = 'block';
+      this.editjson_save = this.getButton('Save','save','Save');
+      this.editjson_save.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        self.saveJSON();
+      });
+      this.editjson_cancel = this.getButton('Cancel','cancel','Cancel');
+      this.editjson_cancel.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        self.hideEditJSON();
+      });
+      this.editjson_holder.appendChild(this.editjson_textarea);
+      this.editjson_holder.appendChild(this.editjson_save);
+      this.editjson_holder.appendChild(this.editjson_cancel);
+
+      // Manage Properties modal
+      this.addproperty_holder = this.theme.getModal();
+      this.addproperty_list = document.createElement('div');
+      this.addproperty_list.style.width = '295px';
+      this.addproperty_list.style.maxHeight = '160px';
+      this.addproperty_list.style.padding = '5px 0';
+      this.addproperty_list.style.overflowY = 'auto';
+      this.addproperty_list.style.overflowX = 'hidden';
+      this.addproperty_list.style.paddingLeft = '5px';
+      this.addproperty_list.setAttribute('class', 'property-selector');
+      this.addproperty_add = this.getButton('add','add','add');
+      this.addproperty_input = this.theme.getFormInputField('text');
+      this.addproperty_input.setAttribute('placeholder','Property name...');
+      this.addproperty_input.style.width = '220px';
+      this.addproperty_input.style.marginBottom = '0';
+      this.addproperty_input.style.display = 'inline-block';
+      this.addproperty_add.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        if(self.addproperty_input.value) {
+          if(self.editors[self.addproperty_input.value]) {
+            window.alert('there is already a property with that name');
+            return;
+          }
+
+          self.addObjectProperty(self.addproperty_input.value);
+          if(self.editors[self.addproperty_input.value]) {
+            self.editors[self.addproperty_input.value].disable();
+          }
+          self.onChange(true);
+        }
+      });
+      this.addproperty_holder.appendChild(this.addproperty_list);
+      this.addproperty_holder.appendChild(this.addproperty_input);
+      this.addproperty_holder.appendChild(this.addproperty_add);
+      var spacer = document.createElement('div');
+      spacer.style.clear = 'both';
+      this.addproperty_holder.appendChild(spacer);
+
+
+      // Description
+      if(this.schema.description) {
+        this.description = this.theme.getDescription(this.schema.description);
+        this.container.appendChild(this.description);
+      }
+
+      // Validation error placeholder area
+      this.error_holder = document.createElement('div');
+      this.container.appendChild(this.error_holder);
+
+      // Container for child editor area
+      this.editor_holder = this.theme.getIndentedPanel();
+      this.container.appendChild(this.editor_holder);
+
+      // Container for rows of child editors
+      this.row_container = this.theme.getGridContainer();
+
+      if(isCategoriesFormat) {
+        this.tabs_holder = this.theme.getTopTabHolder(this.schema.title);
+        this.tabPanesContainer = this.theme.getTopTabContentHolder(this.tabs_holder);
+        this.editor_holder.appendChild(this.tabs_holder);
+      }
+      else {
+        this.tabs_holder = this.theme.getTabHolder(this.schema.title);
+        this.tabPanesContainer = this.theme.getTabContentHolder(this.tabs_holder);
+        this.editor_holder.appendChild(this.row_container);
+      }
+
+      $each(this.editors, function(key,editor) {
+        var aPane = self.theme.getTabContent();
+        var holder = self.theme.getGridColumn();
+        var isObjOrArray = (editor.schema && (editor.schema.type === 'object' || editor.schema.type === 'array')) ? true : false;
+        aPane.isObjOrArray = isObjOrArray;
+
+        if(isCategoriesFormat){
+          if(isObjOrArray) {
+            var single_row_container = self.theme.getGridContainer();
+            single_row_container.appendChild(holder);
+            aPane.appendChild(single_row_container);
+            self.tabPanesContainer.appendChild(aPane);
+            self.row_container = single_row_container;
+          }
+          else {
+            if(typeof self.row_container_basic === 'undefined'){
+              self.row_container_basic = self.theme.getGridContainer();
+              aPane.appendChild(self.row_container_basic);
+              if(self.tabPanesContainer.childElementCount == 0){
+                self.tabPanesContainer.appendChild(aPane);
+              }
+              else {
+                self.tabPanesContainer.insertBefore(aPane,self.tabPanesContainer.childNodes[1]);
+              }
+            }
+            self.row_container_basic.appendChild(holder);
+          }
+
+          self.addRow(editor,self.tabs_holder,aPane);
+
+          aPane.id = editor.schema.title; //editor.schema.path//tab_text.textContent
+
+        }
+        else {
+          self.row_container.appendChild(holder);
+        }
+
+        editor.setContainer(holder);
+        editor.build();
+        editor.postBuild();
+      });
+
+      if(this.rows[0]){
+        $trigger(this.rows[0].tab,'click');
+      }
+
+      // Control buttons
+      this.title_controls = this.theme.getHeaderButtonHolder();
+      this.editjson_controls = this.theme.getHeaderButtonHolder();
+      this.addproperty_controls = this.theme.getHeaderButtonHolder();
+      this.title.appendChild(this.title_controls);
+      this.title.appendChild(this.editjson_controls);
+      this.title.appendChild(this.addproperty_controls);
+
+      // Show/Hide button
+      this.collapsed = false;
+      this.toggle_button = this.getButton('', 'collapse', this.translate('button_collapse'));
+      this.title_controls.appendChild(this.toggle_button);
+      this.toggle_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        if(self.collapsed) {
+          self.editor_holder.style.display = '';
+          self.collapsed = false;
+          self.setButtonText(self.toggle_button,'','collapse',self.translate('button_collapse'));
+        }
+        else {
+          self.editor_holder.style.display = 'none';
+          self.collapsed = true;
+          self.setButtonText(self.toggle_button,'','expand',self.translate('button_expand'));
+        }
+      });
+
+      // If it should start collapsed
+      if(this.options.collapsed) {
+        $trigger(this.toggle_button,'click');
+      }
+
+      // Collapse button disabled
+      if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
+        if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
+      }
+      else if(this.jsoneditor.options.disable_collapse) {
+        this.toggle_button.style.display = 'none';
+      }
+
+      // Edit JSON Button
+      this.editjson_button = this.getButton('JSON','edit','Edit JSON');
+      this.editjson_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        self.toggleEditJSON();
+      });
+      this.editjson_controls.appendChild(this.editjson_button);
+      this.editjson_controls.appendChild(this.editjson_holder);
+
+      // Edit JSON Buttton disabled
+      if(this.schema.options && typeof this.schema.options.disable_edit_json !== "undefined") {
+        if(this.schema.options.disable_edit_json) this.editjson_button.style.display = 'none';
+      }
+      else if(this.jsoneditor.options.disable_edit_json) {
+        this.editjson_button.style.display = 'none';
+      }
+
+      // Object Properties Button
+      this.addproperty_button = this.getButton('Properties','edit','Object Properties');
+      this.addproperty_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        self.toggleAddProperty();
+      });
+      this.addproperty_controls.appendChild(this.addproperty_button);
+      this.addproperty_controls.appendChild(this.addproperty_holder);
+      this.refreshAddProperties();
+    }
+
+    // Fix table cell ordering
+    if(this.options.table_row) {
+      this.editor_holder = this.container;
+      $each(this.property_order,function(i,key) {
+        self.editor_holder.appendChild(self.editors[key].container);
+      });
+    }
+    // Layout object editors in grid if needed
+    else {
+      // Initial layout
+      this.layoutEditors();
+      // Do it again now that we know the approximate heights of elements
+      this.layoutEditors();
+    }
+  },
+  showEditJSON: function() {
+    if(!this.editjson_holder) return;
+    this.hideAddProperty();
+
+    // Position the form directly beneath the button
+    // TODO: edge detection
+    this.editjson_holder.style.left = this.editjson_button.offsetLeft+"px";
+    this.editjson_holder.style.top = this.editjson_button.offsetTop + this.editjson_button.offsetHeight+"px";
+
+    // Start the textarea with the current value
+    this.editjson_textarea.value = JSON.stringify(this.getValue(),null,2);
+
+    // Disable the rest of the form while editing JSON
+    this.disable();
+
+    this.editjson_holder.style.display = '';
+    this.editjson_button.disabled = false;
+    this.editing_json = true;
+  },
+  hideEditJSON: function() {
+    if(!this.editjson_holder) return;
+    if(!this.editing_json) return;
+
+    this.editjson_holder.style.display = 'none';
+    this.enable();
+    this.editing_json = false;
+  },
+  saveJSON: function() {
+    if(!this.editjson_holder) return;
+
+    try {
+      var json = JSON.parse(this.editjson_textarea.value);
+      this.setValue(json);
+      this.hideEditJSON();
+    }
+    catch(e) {
+      window.alert('invalid JSON');
+      throw e;
+    }
+  },
+  toggleEditJSON: function() {
+    if(this.editing_json) this.hideEditJSON();
+    else this.showEditJSON();
+  },
+  insertPropertyControlUsingPropertyOrder: function (property, control, container) {
+    var propertyOrder;
+    if (this.schema.properties[property])
+      propertyOrder = this.schema.properties[property].propertyOrder;
+    if (typeof propertyOrder !== "number") propertyOrder = 1000;
+    control.propertyOrder = propertyOrder;
+
+    for (var i = 0; i < container.childNodes.length; i++) {
+      var child = container.childNodes[i];
+      if (control.propertyOrder < child.propertyOrder) {
+        this.addproperty_list.insertBefore(control, child);
+        control = null;
+        break;
+      }
+    }
+    if (control) {
+      this.addproperty_list.appendChild(control);
+    }
+  },
+  addPropertyCheckbox: function(key) {
+    var self = this;
+    var checkbox, label, labelText, control;
+
+    checkbox = self.theme.getCheckbox();
+    checkbox.style.width = 'auto';
+
+    if (this.schema.properties[key] && this.schema.properties[key].title)
+      labelText = this.schema.properties[key].title;
+    else
+      labelText = key;
+
+    label = self.theme.getCheckboxLabel(labelText);
+
+    control = self.theme.getFormControl(label,checkbox);
+    control.style.paddingBottom = control.style.marginBottom = control.style.paddingTop = control.style.marginTop = 0;
+    control.style.height = 'auto';
+    //control.style.overflowY = 'hidden';
+
+    this.insertPropertyControlUsingPropertyOrder(key, control, this.addproperty_list);
+
+    checkbox.checked = key in this.editors;
+    checkbox.addEventListener('change',function() {
+      if(checkbox.checked) {
+        self.addObjectProperty(key);
+      }
+      else {
+        self.removeObjectProperty(key);
+      }
+      self.onChange(true);
+    });
+    self.addproperty_checkboxes[key] = checkbox;
+
+    return checkbox;
+  },
+  showAddProperty: function() {
+    if(!this.addproperty_holder) return;
+    this.hideEditJSON();
+
+    // Position the form directly beneath the button
+    // TODO: edge detection
+    this.addproperty_holder.style.left = this.addproperty_button.offsetLeft+"px";
+    this.addproperty_holder.style.top = this.addproperty_button.offsetTop + this.addproperty_button.offsetHeight+"px";
+
+    // Disable the rest of the form while editing JSON
+    this.disable();
+
+    this.adding_property = true;
+    this.addproperty_button.disabled = false;
+    this.addproperty_holder.style.display = '';
+    this.refreshAddProperties();
+  },
+  hideAddProperty: function() {
+    if(!this.addproperty_holder) return;
+    if(!this.adding_property) return;
+
+    this.addproperty_holder.style.display = 'none';
+    this.enable();
+
+    this.adding_property = false;
+  },
+  toggleAddProperty: function() {
+    if(this.adding_property) this.hideAddProperty();
+    else this.showAddProperty();
+  },
+  removeObjectProperty: function(property) {
+    if(this.editors[property]) {
+      this.editors[property].unregister();
+      delete this.editors[property];
+
+      this.refreshValue();
+      this.layoutEditors();
+    }
+  },
+  addObjectProperty: function(name, prebuild_only) {
+    var self = this;
+
+    // Property is already added
+    if(this.editors[name]) return;
+
+    // Property was added before and is cached
+    if(this.cached_editors[name]) {
+      this.editors[name] = this.cached_editors[name];
+      if(prebuild_only) return;
+      this.editors[name].register();
+    }
+    // New property
+    else {
+      if(!this.canHaveAdditionalProperties() && (!this.schema.properties || !this.schema.properties[name])) {
+        return;
+      }
+
+      var schema = self.getPropertySchema(name);
+      if(typeof schema.propertyOrder !== 'number'){
+        // if the propertyOrder undefined, then set a smart default value.
+        schema.propertyOrder = Object.keys(self.editors).length + 1000;
+      }
+
+
+      // Add the property
+      var editor = self.jsoneditor.getEditorClass(schema);
+
+      self.editors[name] = self.jsoneditor.createEditor(editor,{
+        jsoneditor: self.jsoneditor,
+        schema: schema,
+        path: self.path+'.'+name,
+        parent: self
+      });
+      self.editors[name].preBuild();
+
+      if(!prebuild_only) {
+        var holder = self.theme.getChildEditorHolder();
+        self.editor_holder.appendChild(holder);
+        self.editors[name].setContainer(holder);
+        self.editors[name].build();
+        self.editors[name].postBuild();
+      }
+
+      self.cached_editors[name] = self.editors[name];
+    }
+
+    // If we're only prebuilding the editors, don't refresh values
+    if(!prebuild_only) {
+      self.refreshValue();
+      self.layoutEditors();
+    }
+  },
+  onChildEditorChange: function(editor) {
+    this.refreshValue();
+    this._super(editor);
+  },
+  canHaveAdditionalProperties: function() {
+    if (typeof this.schema.additionalProperties === "boolean") {//# sourceMappingURL=jsoneditor.js.map
+      return this.schema.additionalProperties;
+    }
+    return !this.jsoneditor.options.no_additional_properties;
+  },
+  destroy: function() {
+    $each(this.cached_editors, function(i,el) {
+      el.destroy();
+    });
+    if(this.editor_holder) this.editor_holder.innerHTML = '';
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.error_holder && this.error_holder.parentNode) this.error_holder.parentNode.removeChild(this.error_holder);
+
+    this.editors = null;
+    this.cached_editors = null;
+    if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
+    this.editor_holder = null;
+
+    this._super();
+  },
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    var result = this._super();
+    if(this.jsoneditor.options.remove_empty_properties || this.options.remove_empty_properties) {
+      for (var i in result) {
+        if (result.hasOwnProperty(i)) {
+          if ((typeof result[i] === 'undefined' || result[i] === '') || 
+              (Object.keys(result[i]).length == 0 && result[i].constructor == Object) ||
+              (Array.isArray(result[i]) && result[i].length == 0)) {
+              delete result[i];
+          }
+        }
+      }
+    }
+
+    return result;
+  },
+  refreshValue: function() {
+    this.value = {};
+    var self = this;
+
+    for(var i in this.editors) {
+      if(!this.editors.hasOwnProperty(i)) continue;
+      this.value[i] = this.editors[i].getValue();
+    }
+
+    if(this.adding_property) this.refreshAddProperties();
+  },
+  refreshAddProperties: function() {
+    if(this.options.disable_properties || (this.options.disable_properties !== false && this.jsoneditor.options.disable_properties)) {
+      this.addproperty_controls.style.display = 'none';
+      return;
+    }
+
+    var can_add = false, can_remove = false, num_props = 0, i, show_modal = false;
+
+    // Get number of editors
+    for(i in this.editors) {
+      if(!this.editors.hasOwnProperty(i)) continue;
+      num_props++;
+    }
+
+    // Determine if we can add back removed properties
+    can_add = this.canHaveAdditionalProperties() && !(typeof this.schema.maxProperties !== "undefined" && num_props >= this.schema.maxProperties);
+
+    if(this.addproperty_checkboxes) {
+      this.addproperty_list.innerHTML = '';
+    }
+    this.addproperty_checkboxes = {};
+
+    // Check for which editors can't be removed or added back
+    for(i in this.cached_editors) {
+      if(!this.cached_editors.hasOwnProperty(i)) continue;
+
+      this.addPropertyCheckbox(i);
+
+      if(this.isRequired(this.cached_editors[i]) && i in this.editors) {
+        this.addproperty_checkboxes[i].disabled = true;
+      }
+
+      if(typeof this.schema.minProperties !== "undefined" && num_props <= this.schema.minProperties) {
+        this.addproperty_checkboxes[i].disabled = this.addproperty_checkboxes[i].checked;
+        if(!this.addproperty_checkboxes[i].checked) show_modal = true;
+      }
+      else if(!(i in this.editors)) {
+        if(!can_add  && !this.schema.properties.hasOwnProperty(i)) {
+          this.addproperty_checkboxes[i].disabled = true;
+        }
+        else {
+          this.addproperty_checkboxes[i].disabled = false;
+          show_modal = true;
+        }
+      }
+      else {
+        show_modal = true;
+        can_remove = true;
+      }
+    }
+
+    if(this.canHaveAdditionalProperties()) {
+      show_modal = true;
+    }
+
+    // Additional addproperty checkboxes not tied to a current editor
+    for(i in this.schema.properties) {
+      if(!this.schema.properties.hasOwnProperty(i)) continue;
+      if(this.cached_editors[i]) continue;
+      show_modal = true;
+      this.addPropertyCheckbox(i);
+    }
+
+    // If no editors can be added or removed, hide the modal button
+    if(!show_modal) {
+      this.hideAddProperty();
+      this.addproperty_controls.style.display = 'none';
+    }
+    // If additional properties are disabled
+    else if(!this.canHaveAdditionalProperties()) {
+      this.addproperty_add.style.display = 'none';
+      this.addproperty_input.style.display = 'none';
+    }
+    // If no new properties can be added
+    else if(!can_add) {
+      this.addproperty_add.disabled = true;
+    }
+    // If new properties can be added
+    else {
+      this.addproperty_add.disabled = false;
+    }
+  },
+  isRequired: function(editor) {
+    if(typeof editor.schema.required === "boolean") return editor.schema.required;
+    else if(Array.isArray(this.schema.required)) return this.schema.required.indexOf(editor.key) > -1;
+    else if(this.jsoneditor.options.required_by_default) return true;
+    else return false;
+  },
+  setValue: function(value, initial) {
+    var self = this;
+    value = value || {};
+
+    if(typeof value !== "object" || Array.isArray(value)) value = {};
+
+    // First, set the values for all of the defined properties
+    $each(this.cached_editors, function(i,editor) {
+      // Value explicitly set
+      if(typeof value[i] !== "undefined") {
+        self.addObjectProperty(i);
+        editor.setValue(value[i],initial);
+      }
+      // Otherwise, remove value unless this is the initial set or it's required
+      else if(!initial && !self.isRequired(editor)) {
+        self.removeObjectProperty(i);
+      }
+      // Otherwise, set the value to the default
+      else {
+        editor.setValue(editor.getDefault(),initial);
+      }
+    });
+
+    $each(value, function(i,val) {
+      if(!self.cached_editors[i]) {
+        self.addObjectProperty(i);
+        if(self.editors[i]) self.editors[i].setValue(val,initial);
+      }
+    });
+
+    this.refreshValue();
+    this.layoutEditors();
+    this.onChange();
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+
+    // Get all the errors that pertain to this editor
+    var my_errors = [];
+    var other_errors = [];
+    $each(errors, function(i,error) {
+      if(error.path === self.path) {
+        my_errors.push(error);
+      }
+      else {
+        other_errors.push(error);
+      }
+    });
+
+    // Show errors for this editor
+    if(this.error_holder) {
+      if(my_errors.length) {
+        var message = [];
+        this.error_holder.innerHTML = '';
+        this.error_holder.style.display = '';
+        $each(my_errors, function(i,error) {
+          self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
+        });
+      }
+      // Hide error area
+      else {
+        this.error_holder.style.display = 'none';
+      }
+    }
+
+    // Show error for the table row if this is inside a table
+    if(this.options.table_row) {
+      if(my_errors.length) {
+        this.theme.addTableRowError(this.container);
+      }
+      else {
+        this.theme.removeTableRowError(this.container);
+      }
+    }
+
+    // Show errors for child editors
+    $each(this.editors, function(i,editor) {
+      editor.showValidationErrors(other_errors);
+    });
+  }
+});
+
+JSONEditor.defaults.editors.array = JSONEditor.AbstractEditor.extend({
+  getDefault: function() {
+    return this.schema["default"] || [];
+  },
+  register: function() {
+    this._super();
+    if(this.rows) {
+      for(var i=0; i<this.rows.length; i++) {
+        this.rows[i].register();
+      }
+    }
+  },
+  unregister: function() {
+    this._super();
+    if(this.rows) {
+      for(var i=0; i<this.rows.length; i++) {
+        this.rows[i].unregister();
+      }
+    }
+  },
+  getNumColumns: function() {
+    var info = this.getItemInfo(0);
+    // Tabs require extra horizontal space
+    if(this.tabs_holder && this.schema.format !== 'tabs-top') {
+      return Math.max(Math.min(12,info.width+2),4);
+    }
+    else {
+      return info.width;
+    }
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.add_row_button) this.add_row_button.disabled = false;
+      if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = false;
+      if(this.delete_last_row_button) this.delete_last_row_button.disabled = false;
+
+      if(this.rows) {
+        for(var i=0; i<this.rows.length; i++) {
+          this.rows[i].enable();
+
+          if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = false;
+          if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = false;
+          if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = false;
+        }
+      }
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.add_row_button) this.add_row_button.disabled = true;
+    if(this.remove_all_rows_button) this.remove_all_rows_button.disabled = true;
+    if(this.delete_last_row_button) this.delete_last_row_button.disabled = true;
+
+    if(this.rows) {
+      for(var i=0; i<this.rows.length; i++) {
+        this.rows[i].disable(always_disabled);
+
+        if(this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = true;
+        if(this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = true;
+        if(this.rows[i].delete_button) this.rows[i].delete_button.disabled = true;
+      }
+    }
+    this._super();
+  },
+  preBuild: function() {
+    this._super();
+
+    this.rows = [];
+    this.row_cache = [];
+
+    this.hide_delete_buttons = this.options.disable_array_delete || this.jsoneditor.options.disable_array_delete;
+    this.hide_delete_all_rows_buttons = this.hide_delete_buttons || this.options.disable_array_delete_all_rows || this.jsoneditor.options.disable_array_delete_all_rows;
+    this.hide_delete_last_row_buttons = this.hide_delete_buttons || this.options.disable_array_delete_last_row || this.jsoneditor.options.disable_array_delete_last_row;
+    this.hide_move_buttons = this.options.disable_array_reorder || this.jsoneditor.options.disable_array_reorder;
+    this.hide_add_button = this.options.disable_array_add || this.jsoneditor.options.disable_array_add;
+       this.show_copy_button = this.options.enable_array_copy || this.jsoneditor.options.enable_array_copy;
+  },
+  build: function() {
+    var self = this;
+
+    if(!this.options.compact) {
+      this.header = document.createElement('span');
+      this.header.textContent = this.getTitle();
+      this.title = this.theme.getHeader(this.header);
+      this.container.appendChild(this.title);
+      this.title_controls = this.theme.getHeaderButtonHolder();
+      this.title.appendChild(this.title_controls);
+      if(this.schema.description) {
+        this.description = this.theme.getDescription(this.schema.description);
+        this.container.appendChild(this.description);
+      }
+      this.error_holder = document.createElement('div');
+      this.container.appendChild(this.error_holder);
+
+      if(this.schema.format === 'tabs-top') {
+        this.controls = this.theme.getHeaderButtonHolder();
+        this.title.appendChild(this.controls);
+        this.tabs_holder = this.theme.getTopTabHolder(this.getItemTitle());
+        this.container.appendChild(this.tabs_holder);
+        this.row_holder = this.theme.getTopTabContentHolder(this.tabs_holder);
+
+        this.active_tab = null;
+      }
+      else if(this.schema.format === 'tabs') {
+        this.controls = this.theme.getHeaderButtonHolder();
+        this.title.appendChild(this.controls);
+        this.tabs_holder = this.theme.getTabHolder(this.getItemTitle());
+        this.container.appendChild(this.tabs_holder);
+        this.row_holder = this.theme.getTabContentHolder(this.tabs_holder);
+
+        this.active_tab = null;
+      }
+      else {
+        this.panel = this.theme.getIndentedPanel();
+        this.container.appendChild(this.panel);
+        this.row_holder = document.createElement('div');
+        this.panel.appendChild(this.row_holder);
+        this.controls = this.theme.getButtonHolder();
+        this.panel.appendChild(this.controls);
+      }
+    }
+    else {
+        this.panel = this.theme.getIndentedPanel();
+        this.container.appendChild(this.panel);
+        this.controls = this.theme.getButtonHolder();
+        this.panel.appendChild(this.controls);
+        this.row_holder = document.createElement('div');
+        this.panel.appendChild(this.row_holder);
+    }
+
+    // Add controls
+    this.addControls();
+  },
+  onChildEditorChange: function(editor) {
+    this.refreshValue();
+    this.refreshTabs(true);
+    this._super(editor);
+  },
+  getItemTitle: function() {
+    if(!this.item_title) {
+      if(this.schema.items && !Array.isArray(this.schema.items)) {
+        var tmp = this.jsoneditor.expandRefs(this.schema.items);
+        this.item_title = tmp.title || 'item';
+      }
+      else {
+        this.item_title = 'item';
+      }
+    }
+    return this.item_title;
+  },
+  getItemSchema: function(i) {
+    if(Array.isArray(this.schema.items)) {
+      if(i >= this.schema.items.length) {
+        if(this.schema.additionalItems===true) {
+          return {};
+        }
+        else if(this.schema.additionalItems) {
+          return $extend({},this.schema.additionalItems);
+        }
+      }
+      else {
+        return $extend({},this.schema.items[i]);
+      }
+    }
+    else if(this.schema.items) {
+      return $extend({},this.schema.items);
+    }
+    else {
+      return {};
+    }
+  },
+  getItemInfo: function(i) {
+    var schema = this.getItemSchema(i);
+
+    // Check if it's cached
+    this.item_info = this.item_info || {};
+    var stringified = JSON.stringify(schema);
+    if(typeof this.item_info[stringified] !== "undefined") return this.item_info[stringified];
+
+    // Get the schema for this item
+    schema = this.jsoneditor.expandRefs(schema);
+
+    this.item_info[stringified] = {
+      title: schema.title || "item",
+      'default': schema["default"],
+      width: 12,
+      child_editors: schema.properties || schema.items
+    };
+
+    return this.item_info[stringified];
+  },
+  getElementEditor: function(i) {
+    var item_info = this.getItemInfo(i);
+    var schema = this.getItemSchema(i);
+    schema = this.jsoneditor.expandRefs(schema);
+    schema.title = item_info.title+' '+(i+1);
+
+    var editor = this.jsoneditor.getEditorClass(schema);
+
+    var holder;
+    if(this.tabs_holder) {
+      if(this.schema.format === 'tabs-top') {
+        holder = this.theme.getTopTabContent();
+      }
+      else {
+        holder = this.theme.getTabContent();
+      }
+      holder.id = this.path+'.'+i;
+    }
+    else if(item_info.child_editors) {
+      holder = this.theme.getChildEditorHolder();
+    }
+    else {
+      holder = this.theme.getIndentedPanel();
+    }
+
+    this.row_holder.appendChild(holder);
+
+    var ret = this.jsoneditor.createEditor(editor,{
+      jsoneditor: this.jsoneditor,
+      schema: schema,
+      container: holder,
+      path: this.path+'.'+i,
+      parent: this,
+      required: true
+    });
+    ret.preBuild();
+    ret.build();
+    ret.postBuild();
+
+    if(!ret.title_controls) {
+      ret.array_controls = this.theme.getButtonHolder();
+      holder.appendChild(ret.array_controls);
+    }
+
+    return ret;
+  },
+  destroy: function() {
+    this.empty(true);
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
+    if(this.controls && this.controls.parentNode) this.controls.parentNode.removeChild(this.controls);
+    if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
+
+    this.rows = this.row_cache = this.title = this.description = this.row_holder = this.panel = this.controls = null;
+
+    this._super();
+  },
+  empty: function(hard) {
+    if(!this.rows) return;
+    var self = this;
+    $each(this.rows,function(i,row) {
+      if(hard) {
+        if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
+        self.destroyRow(row,true);
+        self.row_cache[i] = null;
+      }
+      self.rows[i] = null;
+    });
+    self.rows = [];
+    if(hard) self.row_cache = [];
+  },
+  destroyRow: function(row,hard) {
+    var holder = row.container;
+    if(hard) {
+      row.destroy();
+      if(holder.parentNode) holder.parentNode.removeChild(holder);
+      if(row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
+    }
+    else {
+      if(row.tab) row.tab.style.display = 'none';
+      holder.style.display = 'none';
+      row.unregister();
+    }
+  },
+  getMax: function() {
+    if((Array.isArray(this.schema.items)) && this.schema.additionalItems === false) {
+      return Math.min(this.schema.items.length,this.schema.maxItems || Infinity);
+    }
+    else {
+      return this.schema.maxItems || Infinity;
+    }
+  },
+  refreshTabs: function(refresh_headers) {
+    var self = this;
+    $each(this.rows, function(i,row) {
+      if(!row.tab) return;
+
+      if(refresh_headers) {
+        row.tab_text.textContent = row.getHeaderText();
+      }
+      else {
+        if(row.tab === self.active_tab) {
+          self.theme.markTabActive(row);
+        }
+        else {
+          self.theme.markTabInactive(row);
+        }
+      }
+    });
+  },
+  setValue: function(value, initial) {
+    // Update the array's value, adding/removing rows when necessary
+    value = value || [];
+
+    if(!(Array.isArray(value))) value = [value];
+
+    var serialized = JSON.stringify(value);
+    if(serialized === this.serialized) return;
+
+    // Make sure value has between minItems and maxItems items in it
+    if(this.schema.minItems) {
+      while(value.length < this.schema.minItems) {
+        value.push(this.getItemInfo(value.length)["default"]);
+      }
+    }
+    if(this.getMax() && value.length > this.getMax()) {
+      value = value.slice(0,this.getMax());
+    }
+
+    var self = this;
+    $each(value,function(i,val) {
+      if(self.rows[i]) {
+        // TODO: don't set the row's value if it hasn't changed
+        self.rows[i].setValue(val,initial);
+      }
+      else if(self.row_cache[i]) {
+        self.rows[i] = self.row_cache[i];
+        self.rows[i].setValue(val,initial);
+        self.rows[i].container.style.display = '';
+        if(self.rows[i].tab) self.rows[i].tab.style.display = '';
+        self.rows[i].register();
+      }
+      else {
+        self.addRow(val,initial);
+      }
+    });
+
+    for(var j=value.length; j<self.rows.length; j++) {
+      self.destroyRow(self.rows[j]);
+      self.rows[j] = null;
+    }
+    self.rows = self.rows.slice(0,value.length);
+
+    // Set the active tab
+    var new_active_tab = null;
+    $each(self.rows, function(i,row) {
+      if(row.tab === self.active_tab) {
+        new_active_tab = row.tab;
+        return false;
+      }
+    });
+    if(!new_active_tab && self.rows.length) new_active_tab = self.rows[0].tab;
+
+    self.active_tab = new_active_tab;
+
+    self.refreshValue(initial);
+    self.refreshTabs(true);
+    self.refreshTabs();
+
+    self.onChange();
+
+    // TODO: sortable
+  },
+  refreshValue: function(force) {
+    var self = this;
+    var oldi = this.value? this.value.length : 0;
+    this.value = [];
+
+    $each(this.rows,function(i,editor) {
+      // Get the value for this editor
+      self.value[i] = editor.getValue();
+    });
+
+    if(oldi !== this.value.length || force) {
+      // If we currently have minItems items in the array
+      var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
+
+      $each(this.rows,function(i,editor) {
+        // Hide the move down button for the last row
+        if(editor.movedown_button) {
+          if(i === self.rows.length - 1) {
+            editor.movedown_button.style.display = 'none';
+          }
+          else {
+            editor.movedown_button.style.display = '';
+          }
+        }
+
+        // Hide the delete button if we have minItems items
+        if(editor.delete_button) {
+          if(minItems) {
+            editor.delete_button.style.display = 'none';
+          }
+          else {
+            editor.delete_button.style.display = '';
+          }
+        }
+
+        // Get the value for this editor
+        self.value[i] = editor.getValue();
+      });
+
+      var controls_needed = false;
+
+      if(!this.value.length) {
+        this.delete_last_row_button.style.display = 'none';
+        this.remove_all_rows_button.style.display = 'none';
+      }
+      else if(this.value.length === 1) {
+        this.remove_all_rows_button.style.display = 'none';
+
+        // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
+        if(minItems || this.hide_delete_last_row_buttons) {
+          this.delete_last_row_button.style.display = 'none';
+        }
+        else {
+          this.delete_last_row_button.style.display = '';
+          controls_needed = true;
+        }
+      }
+      else {
+        if(minItems || this.hide_delete_last_row_buttons) {
+          this.delete_last_row_button.style.display = 'none';
+        }
+        else {
+          this.delete_last_row_button.style.display = '';
+          controls_needed = true;
+        }
+
+        if(minItems || this.hide_delete_all_rows_buttons) {
+          this.remove_all_rows_button.style.display = 'none';
+        }
+        else {
+          this.remove_all_rows_button.style.display = '';
+          controls_needed = true;
+        }
+      }
+
+      // If there are maxItems in the array, hide the add button beneath the rows
+      if((this.getMax() && this.getMax() <= this.rows.length) || this.hide_add_button){
+        this.add_row_button.style.display = 'none';
+      }
+      else {
+        this.add_row_button.style.display = '';
+        controls_needed = true;
+      }
+
+      if(!this.collapsed && controls_needed) {
+        this.controls.style.display = 'inline-block';
+      }
+      else {
+        this.controls.style.display = 'none';
+      }
+    }
+  },
+  addRow: function(value, initial) {
+    var self = this;
+    var i = this.rows.length;
+
+    self.rows[i] = this.getElementEditor(i);
+    self.row_cache[i] = self.rows[i];
+
+    if(self.tabs_holder) {
+      self.rows[i].tab_text = document.createElement('span');
+      self.rows[i].tab_text.textContent = self.rows[i].getHeaderText();
+      if(self.schema.format === 'tabs-top'){
+        self.rows[i].tab = self.theme.getTopTab(self.rows[i].tab_text,self.rows[i].path);
+        self.theme.addTopTab(self.tabs_holder, self.rows[i].tab);
+      }
+      else {
+        self.rows[i].tab = self.theme.getTab(self.rows[i].tab_text,self.rows[i].path);
+        self.theme.addTab(self.tabs_holder, self.rows[i].tab);
+      }
+      self.rows[i].tab.addEventListener('click', function(e) {
+        self.active_tab = self.rows[i].tab;
+        self.refreshTabs();
+        e.preventDefault();
+        e.stopPropagation();
+      });
+
+    }
+
+    var controls_holder = self.rows[i].title_controls || self.rows[i].array_controls;
+
+    // Buttons to delete row, move row up, and move row down
+    if(!self.hide_delete_buttons) {
+      self.rows[i].delete_button = this.getButton(self.getItemTitle(),'delete',this.translate('button_delete_row_title',[self.getItemTitle()]));
+      self.rows[i].delete_button.className += ' delete';
+      self.rows[i].delete_button.setAttribute('data-i',i);
+      self.rows[i].delete_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        if (self.jsoneditor.options.prompt_before_delete === true) {
+          if (confirm("Confirm to remove.") === false) {
+            return false;
+          }
+        }
+
+        var i = this.getAttribute('data-i')*1;
+
+        var value = self.getValue();
+
+        var newval = [];
+        var new_active_tab = null;
+        $each(value,function(j,row) {
+          if(j===i) {
+            // If the one we're deleting is the active tab
+            if(self.rows[j].tab === self.active_tab) {
+              // Make the next tab active if there is one
+              // Note: the next tab is going to be the current tab after deletion
+              if(self.rows[j+1]) new_active_tab = self.rows[j].tab;
+              // Otherwise, make the previous tab active if there is one
+              else if(j) new_active_tab = self.rows[j-1].tab;
+            }
+
+            return; // If this is the one we're deleting
+          }
+          newval.push(row);
+        });
+        self.setValue(newval);
+        if(new_active_tab) {
+          self.active_tab = new_active_tab;
+          self.refreshTabs();
+        }
+
+        self.onChange(true);
+      });
+
+      if(controls_holder) {
+        controls_holder.appendChild(self.rows[i].delete_button);
+      }
+    }
+
+       //Button to copy an array element and add it as last element
+       if(self.show_copy_button){
+        self.rows[i].copy_button = this.getButton(self.getItemTitle(),'copy','Copy '+self.getItemTitle());
+        self.rows[i].copy_button.className += ' copy';
+        self.rows[i].copy_button.setAttribute('data-i',i);
+        self.rows[i].copy_button.addEventListener('click',function(e) {
+            var value = self.getValue();
+            e.preventDefault();
+            e.stopPropagation();
+            var i = this.getAttribute('data-i')*1;
+
+            $each(value,function(j,row) {
+              if(j===i) {
+                value.push(row);
+                return;
+              }
+            });
+
+            self.setValue(value);
+            self.refreshValue(true);
+            self.onChange(true);
+
+        });
+
+        controls_holder.appendChild(self.rows[i].copy_button);
+    }
+
+
+    if(i && !self.hide_move_buttons) {
+      self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
+      self.rows[i].moveup_button.className += ' moveup';
+      self.rows[i].moveup_button.setAttribute('data-i',i);
+      self.rows[i].moveup_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        var i = this.getAttribute('data-i')*1;
+
+        if(i<=0) return;
+        var rows = self.getValue();
+        var tmp = rows[i-1];
+        rows[i-1] = rows[i];
+        rows[i] = tmp;
+
+        self.setValue(rows);
+        self.active_tab = self.rows[i-1].tab;
+        self.refreshTabs();
+
+        self.onChange(true);
+      });
+
+      if(controls_holder) {
+        controls_holder.appendChild(self.rows[i].moveup_button);
+      }
+    }
+
+    if(!self.hide_move_buttons) {
+      self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
+      self.rows[i].movedown_button.className += ' movedown';
+      self.rows[i].movedown_button.setAttribute('data-i',i);
+      self.rows[i].movedown_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        var i = this.getAttribute('data-i')*1;
+
+        var rows = self.getValue();
+        if(i>=rows.length-1) return;
+        var tmp = rows[i+1];
+        rows[i+1] = rows[i];
+        rows[i] = tmp;
+
+        self.setValue(rows);
+        self.active_tab = self.rows[i+1].tab;
+        self.refreshTabs();
+        self.onChange(true);
+      });
+
+      if(controls_holder) {
+        controls_holder.appendChild(self.rows[i].movedown_button);
+      }
+    }
+
+    if(value) self.rows[i].setValue(value, initial);
+    self.refreshTabs();
+  },
+  addControls: function() {
+    var self = this;
+
+    this.collapsed = false;
+    this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
+    this.title_controls.appendChild(this.toggle_button);
+    var row_holder_display = self.row_holder.style.display;
+    var controls_display = self.controls.style.display;
+    this.toggle_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      if(self.collapsed) {
+        self.collapsed = false;
+        if(self.panel) self.panel.style.display = '';
+        self.row_holder.style.display = row_holder_display;
+        if(self.tabs_holder) self.tabs_holder.style.display = '';
+        self.controls.style.display = controls_display;
+        self.setButtonText(this,'','collapse',self.translate('button_collapse'));
+      }
+      else {
+        self.collapsed = true;
+        self.row_holder.style.display = 'none';
+        if(self.tabs_holder) self.tabs_holder.style.display = 'none';
+        self.controls.style.display = 'none';
+        if(self.panel) self.panel.style.display = 'none';
+        self.setButtonText(this,'','expand',self.translate('button_expand'));
+      }
+    });
+
+    // If it should start collapsed
+    if(this.options.collapsed) {
+      $trigger(this.toggle_button,'click');
+    }
+
+    // Collapse button disabled
+    if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
+      if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
+    }
+    else if(this.jsoneditor.options.disable_collapse) {
+      this.toggle_button.style.display = 'none';
+    }
+
+    // Add "new row" and "delete last" buttons below editor
+    this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
+
+    this.add_row_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      var i = self.rows.length;
+      if(self.row_cache[i]) {
+        self.rows[i] = self.row_cache[i];
+        self.rows[i].setValue(self.rows[i].getDefault(), true);
+        self.rows[i].container.style.display = '';
+        if(self.rows[i].tab) self.rows[i].tab.style.display = '';
+        self.rows[i].register();
+      }
+      else {
+        self.addRow();
+      }
+      self.active_tab = self.rows[i].tab;
+      self.refreshTabs();
+      self.refreshValue();
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.add_row_button);
+
+    this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
+    this.delete_last_row_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      if (self.jsoneditor.options.prompt_before_delete === true) {
+        if (confirm("Confirm to remove.") === false) {
+          return false;
+        }
+      }
+
+      var rows = self.getValue();
+
+      var new_active_tab = null;
+      if(self.rows.length > 1 && self.rows[self.rows.length-1].tab === self.active_tab) new_active_tab = self.rows[self.rows.length-2].tab;
+
+      rows.pop();
+      self.setValue(rows);
+      if(new_active_tab) {
+        self.active_tab = new_active_tab;
+        self.refreshTabs();
+      }
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.delete_last_row_button);
+
+    this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
+    this.remove_all_rows_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      if (self.jsoneditor.options.prompt_before_delete === true) {
+        if (confirm("Confirm to remove.") === false) {
+          return false;
+        }
+      }
+
+      self.setValue([]);
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.remove_all_rows_button);
+
+    if(self.tabs) {
+      this.add_row_button.style.width = '100%';
+      this.add_row_button.style.textAlign = 'left';
+      this.add_row_button.style.marginBottom = '3px';
+
+      this.delete_last_row_button.style.width = '100%';
+      this.delete_last_row_button.style.textAlign = 'left';
+      this.delete_last_row_button.style.marginBottom = '3px';
+
+      this.remove_all_rows_button.style.width = '100%';
+      this.remove_all_rows_button.style.textAlign = 'left';
+      this.remove_all_rows_button.style.marginBottom = '3px';
+    }
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+
+    // Get all the errors that pertain to this editor
+    var my_errors = [];
+    var other_errors = [];
+    $each(errors, function(i,error) {
+      if(error.path === self.path) {
+        my_errors.push(error);
+      }
+      else {
+        other_errors.push(error);
+      }
+    });
+
+    // Show errors for this editor
+    if(this.error_holder) {
+      if(my_errors.length) {
+        var message = [];
+        this.error_holder.innerHTML = '';
+        this.error_holder.style.display = '';
+        $each(my_errors, function(i,error) {
+          self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
+        });
+      }
+      // Hide error area
+      else {
+        this.error_holder.style.display = 'none';
+      }
+    }
+
+    // Show errors for child editors
+    $each(this.rows, function(i,row) {
+      row.showValidationErrors(other_errors);
+    });
+  }
+});
+
+JSONEditor.defaults.editors.table = JSONEditor.defaults.editors.array.extend({
+  register: function() {
+    this._super();
+    if(this.rows) {
+      for(var i=0; i<this.rows.length; i++) {
+        this.rows[i].register();
+      }
+    }
+  },
+  unregister: function() {
+    this._super();
+    if(this.rows) {
+      for(var i=0; i<this.rows.length; i++) {
+        this.rows[i].unregister();
+      }
+    }
+  },
+  getNumColumns: function() {
+    return Math.max(Math.min(12,this.width),3);
+  },
+  preBuild: function() {
+    var item_schema = this.jsoneditor.expandRefs(this.schema.items || {});
+
+    this.item_title = item_schema.title || 'row';
+    this.item_default = item_schema["default"] || null;
+    this.item_has_child_editors = item_schema.properties || item_schema.items;
+    this.width = 12;
+    this._super();
+  },
+  build: function() {
+    var self = this;
+    this.table = this.theme.getTable();
+    this.container.appendChild(this.table);
+    this.thead = this.theme.getTableHead();
+    this.table.appendChild(this.thead);
+    this.header_row = this.theme.getTableRow();
+    this.thead.appendChild(this.header_row);
+    this.row_holder = this.theme.getTableBody();
+    this.table.appendChild(this.row_holder);
+
+    // Determine the default value of array element
+    var tmp = this.getElementEditor(0,true);
+    this.item_default = tmp.getDefault();
+    this.width = tmp.getNumColumns() + 2;
+
+    if(!this.options.compact) {
+      this.title = this.theme.getHeader(this.getTitle());
+      this.container.appendChild(this.title);
+      this.title_controls = this.theme.getHeaderButtonHolder();
+      this.title.appendChild(this.title_controls);
+      if(this.schema.description) {
+        this.description = this.theme.getDescription(this.schema.description);
+        this.container.appendChild(this.description);
+      }
+      this.panel = this.theme.getIndentedPanel();
+      this.container.appendChild(this.panel);
+      this.error_holder = document.createElement('div');
+      this.panel.appendChild(this.error_holder);
+    }
+    else {
+      this.panel = document.createElement('div');
+      this.container.appendChild(this.panel);
+    }
+
+    this.panel.appendChild(this.table);
+    this.controls = this.theme.getButtonHolder();
+    this.panel.appendChild(this.controls);
+
+    if(this.item_has_child_editors) {
+      var ce = tmp.getChildEditors();
+      var order = tmp.property_order || Object.keys(ce);
+      for(var i=0; i<order.length; i++) {
+        var th = self.theme.getTableHeaderCell(ce[order[i]].getTitle());
+        if(ce[order[i]].options.hidden) th.style.display = 'none';
+        self.header_row.appendChild(th);
+      }
+    }
+    else {
+      self.header_row.appendChild(self.theme.getTableHeaderCell(this.item_title));
+    }
+
+    tmp.destroy();
+    this.row_holder.innerHTML = '';
+
+    // Row Controls column
+    this.controls_header_cell = self.theme.getTableHeaderCell(" ");
+    self.header_row.appendChild(this.controls_header_cell);
+
+    // Add controls
+    this.addControls();
+  },
+  onChildEditorChange: function(editor) {
+    this.refreshValue();
+    this._super();
+  },
+  getItemDefault: function() {
+    return $extend({},{"default":this.item_default})["default"];
+  },
+  getItemTitle: function() {
+    return this.item_title;
+  },
+  getElementEditor: function(i,ignore) {
+    var schema_copy = $extend({},this.schema.items);
+    var editor = this.jsoneditor.getEditorClass(schema_copy, this.jsoneditor);
+    var row = this.row_holder.appendChild(this.theme.getTableRow());
+    var holder = row;
+    if(!this.item_has_child_editors) {
+      holder = this.theme.getTableCell();
+      row.appendChild(holder);
+    }
+
+    var ret = this.jsoneditor.createEditor(editor,{
+      jsoneditor: this.jsoneditor,
+      schema: schema_copy,
+      container: holder,
+      path: this.path+'.'+i,
+      parent: this,
+      compact: true,
+      table_row: true
+    });
+
+    ret.preBuild();
+    if(!ignore) {
+      ret.build();
+      ret.postBuild();
+
+      ret.controls_cell = row.appendChild(this.theme.getTableCell());
+      ret.row = row;
+      ret.table_controls = this.theme.getButtonHolder();
+      ret.controls_cell.appendChild(ret.table_controls);
+      ret.table_controls.style.margin = 0;
+      ret.table_controls.style.padding = 0;
+    }
+
+    return ret;
+  },
+  destroy: function() {
+    this.innerHTML = '';
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.row_holder && this.row_holder.parentNode) this.row_holder.parentNode.removeChild(this.row_holder);
+    if(this.table && this.table.parentNode) this.table.parentNode.removeChild(this.table);
+    if(this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
+
+    this.rows = this.title = this.description = this.row_holder = this.table = this.panel = null;
+
+    this._super();
+  },
+  setValue: function(value, initial) {
+    // Update the array's value, adding/removing rows when necessary
+    value = value || [];
+
+    // Make sure value has between minItems and maxItems items in it
+    if(this.schema.minItems) {
+      while(value.length < this.schema.minItems) {
+        value.push(this.getItemDefault());
+      }
+    }
+    if(this.schema.maxItems && value.length > this.schema.maxItems) {
+      value = value.slice(0,this.schema.maxItems);
+    }
+
+    var serialized = JSON.stringify(value);
+    if(serialized === this.serialized) return;
+
+    var numrows_changed = false;
+
+    var self = this;
+    $each(value,function(i,val) {
+      if(self.rows[i]) {
+        // TODO: don't set the row's value if it hasn't changed
+        self.rows[i].setValue(val);
+      }
+      else {
+        self.addRow(val);
+        numrows_changed = true;
+      }
+    });
+
+    for(var j=value.length; j<self.rows.length; j++) {
+      var holder = self.rows[j].container;
+      if(!self.item_has_child_editors) {
+        self.rows[j].row.parentNode.removeChild(self.rows[j].row);
+      }
+      self.rows[j].destroy();
+      if(holder.parentNode) holder.parentNode.removeChild(holder);
+      self.rows[j] = null;
+      numrows_changed = true;
+    }
+    self.rows = self.rows.slice(0,value.length);
+
+    self.refreshValue();
+    if(numrows_changed || initial) self.refreshRowButtons();
+
+    self.onChange();
+
+    // TODO: sortable
+  },
+  refreshRowButtons: function() {
+    var self = this;
+
+    // If we currently have minItems items in the array
+    var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
+
+    var need_row_buttons = false;
+    $each(this.rows,function(i,editor) {
+      // Hide the move down button for the last row
+      if(editor.movedown_button) {
+        if(i === self.rows.length - 1) {
+          editor.movedown_button.style.display = 'none';
+        }
+        else {
+          need_row_buttons = true;
+          editor.movedown_button.style.display = '';
+        }
+      }
+
+      // Hide the delete button if we have minItems items
+      if(editor.delete_button) {
+        if(minItems) {
+          editor.delete_button.style.display = 'none';
+        }
+        else {
+          need_row_buttons = true;
+          editor.delete_button.style.display = '';
+        }
+      }
+
+      if(editor.moveup_button) {
+        need_row_buttons = true;
+      }
+    });
+
+    // Show/hide controls column in table
+    $each(this.rows,function(i,editor) {
+      if(need_row_buttons) {
+        editor.controls_cell.style.display = '';
+      }
+      else {
+        editor.controls_cell.style.display = 'none';
+      }
+    });
+    if(need_row_buttons) {
+      this.controls_header_cell.style.display = '';
+    }
+    else {
+      this.controls_header_cell.style.display = 'none';
+    }
+
+    var controls_needed = false;
+
+    if(!this.value.length) {
+      this.delete_last_row_button.style.display = 'none';
+      this.remove_all_rows_button.style.display = 'none';
+      this.table.style.display = 'none';
+    }
+    else if(this.value.length === 1) {
+      this.table.style.display = '';
+      this.remove_all_rows_button.style.display = 'none';
+
+      // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
+      if(minItems || this.hide_delete_last_row_buttons) {
+        this.delete_last_row_button.style.display = 'none';
+      }
+      else {
+        this.delete_last_row_button.style.display = '';
+        controls_needed = true;
+      }
+    }
+    else {
+      this.table.style.display = '';
+
+      if(minItems || this.hide_delete_last_row_buttons) {
+        this.delete_last_row_button.style.display = 'none';
+      }
+      else {
+        this.delete_last_row_button.style.display = '';
+        controls_needed = true;
+      }
+
+      if(minItems || this.hide_delete_all_rows_buttons) {
+        this.remove_all_rows_button.style.display = 'none';
+      }
+      else {
+        this.remove_all_rows_button.style.display = '';
+        controls_needed = true;
+      }
+    }
+
+    // If there are maxItems in the array, hide the add button beneath the rows
+    if((this.schema.maxItems && this.schema.maxItems <= this.rows.length) || this.hide_add_button) {
+      this.add_row_button.style.display = 'none';
+    }
+    else {
+      this.add_row_button.style.display = '';
+      controls_needed = true;
+    }
+
+    if(!controls_needed) {
+      this.controls.style.display = 'none';
+    }
+    else {
+      this.controls.style.display = '';
+    }
+  },
+  refreshValue: function() {
+    var self = this;
+    this.value = [];
+
+    $each(this.rows,function(i,editor) {
+      // Get the value for this editor
+      self.value[i] = editor.getValue();
+    });
+    this.serialized = JSON.stringify(this.value);
+  },
+  addRow: function(value) {
+    var self = this;
+    var i = this.rows.length;
+
+    self.rows[i] = this.getElementEditor(i);
+
+    var controls_holder = self.rows[i].table_controls;
+
+    // Buttons to delete row, move row up, and move row down
+    if(!this.hide_delete_buttons) {
+      self.rows[i].delete_button = this.getButton('','delete',this.translate('button_delete_row_title_short'));
+      self.rows[i].delete_button.className += ' delete';
+      self.rows[i].delete_button.setAttribute('data-i',i);
+      self.rows[i].delete_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        var i = this.getAttribute('data-i')*1;
+
+        var value = self.getValue();
+
+        var newval = [];
+        $each(value,function(j,row) {
+          if(j===i) return; // If this is the one we're deleting
+          newval.push(row);
+        });
+        self.setValue(newval);
+        self.onChange(true);
+      });
+      controls_holder.appendChild(self.rows[i].delete_button);
+    }
+
+
+    if(i && !this.hide_move_buttons) {
+      self.rows[i].moveup_button = this.getButton('','moveup',this.translate('button_move_up_title'));
+      self.rows[i].moveup_button.className += ' moveup';
+      self.rows[i].moveup_button.setAttribute('data-i',i);
+      self.rows[i].moveup_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        var i = this.getAttribute('data-i')*1;
+
+        if(i<=0) return;
+        var rows = self.getValue();
+        var tmp = rows[i-1];
+        rows[i-1] = rows[i];
+        rows[i] = tmp;
+
+        self.setValue(rows);
+        self.onChange(true);
+      });
+      controls_holder.appendChild(self.rows[i].moveup_button);
+    }
+
+    if(!this.hide_move_buttons) {
+      self.rows[i].movedown_button = this.getButton('','movedown',this.translate('button_move_down_title'));
+      self.rows[i].movedown_button.className += ' movedown';
+      self.rows[i].movedown_button.setAttribute('data-i',i);
+      self.rows[i].movedown_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        var i = this.getAttribute('data-i')*1;
+        var rows = self.getValue();
+        if(i>=rows.length-1) return;
+        var tmp = rows[i+1];
+        rows[i+1] = rows[i];
+        rows[i] = tmp;
+
+        self.setValue(rows);
+        self.onChange(true);
+      });
+      controls_holder.appendChild(self.rows[i].movedown_button);
+    }
+
+    if(value) self.rows[i].setValue(value);
+  },
+  addControls: function() {
+    var self = this;
+
+    this.collapsed = false;
+    this.toggle_button = this.getButton('','collapse',this.translate('button_collapse'));
+    if(this.title_controls) {
+      this.title_controls.appendChild(this.toggle_button);
+      this.toggle_button.addEventListener('click',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        if(self.collapsed) {
+          self.collapsed = false;
+          self.panel.style.display = '';
+          self.setButtonText(this,'','collapse',self.translate('button_collapse'));
+        }
+        else {
+          self.collapsed = true;
+          self.panel.style.display = 'none';
+          self.setButtonText(this,'','expand',self.translate('button_expand'));
+        }
+      });
+
+      // If it should start collapsed
+      if(this.options.collapsed) {
+        $trigger(this.toggle_button,'click');
+      }
+
+      // Collapse button disabled
+      if(this.schema.options && typeof this.schema.options.disable_collapse !== "undefined") {
+        if(this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
+      }
+      else if(this.jsoneditor.options.disable_collapse) {
+        this.toggle_button.style.display = 'none';
+      }
+    }
+
+    // Add "new row" and "delete last" buttons below editor
+    this.add_row_button = this.getButton(this.getItemTitle(),'add',this.translate('button_add_row_title',[this.getItemTitle()]));
+    this.add_row_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      self.addRow();
+      self.refreshValue();
+      self.refreshRowButtons();
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.add_row_button);
+
+    this.delete_last_row_button = this.getButton(this.translate('button_delete_last',[this.getItemTitle()]),'delete',this.translate('button_delete_last_title',[this.getItemTitle()]));
+    this.delete_last_row_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      var rows = self.getValue();
+      rows.pop();
+      self.setValue(rows);
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.delete_last_row_button);
+
+    this.remove_all_rows_button = this.getButton(this.translate('button_delete_all'),'delete',this.translate('button_delete_all_title'));
+    this.remove_all_rows_button.addEventListener('click',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      self.setValue([]);
+      self.onChange(true);
+    });
+    self.controls.appendChild(this.remove_all_rows_button);
+  }
+});
+
+// Multiple Editor (for when `type` is an array, also when `oneOf` is present)
+JSONEditor.defaults.editors.multiple = JSONEditor.AbstractEditor.extend({
+  register: function() {
+    if(this.editors) {
+      for(var i=0; i<this.editors.length; i++) {
+        if(!this.editors[i]) continue;
+        this.editors[i].unregister();
+      }
+      if(this.editors[this.type]) this.editors[this.type].register();
+    }
+    this._super();
+  },
+  unregister: function() {
+    this._super();
+    if(this.editors) {
+      for(var i=0; i<this.editors.length; i++) {
+        if(!this.editors[i]) continue;
+        this.editors[i].unregister();
+      }
+    }
+  },
+  getNumColumns: function() {
+    if(!this.editors[this.type]) return 4;
+    return Math.max(this.editors[this.type].getNumColumns(),4);
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.editors) {
+        for(var i=0; i<this.editors.length; i++) {
+          if(!this.editors[i]) continue;
+          this.editors[i].enable();
+        }
+      }
+      this.switcher.disabled = false;
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.editors) {
+      for(var i=0; i<this.editors.length; i++) {
+        if(!this.editors[i]) continue;
+        this.editors[i].disable(always_disabled);
+      }
+    }
+    this.switcher.disabled = true;
+    this._super();
+  },
+  switchEditor: function(i) {
+    var self = this;
+
+    if(!this.editors[i]) {
+      this.buildChildEditor(i);
+    }
+    
+    var current_value = self.getValue();
+
+    self.type = i;
+
+    self.register();
+
+    $each(self.editors,function(type,editor) {
+      if(!editor) return;
+      if(self.type === type) {
+        if(self.keep_values) editor.setValue(current_value,true);
+        editor.container.style.display = '';
+      }
+      else editor.container.style.display = 'none';
+    });
+    self.refreshValue();
+    self.refreshHeaderText();
+  },
+  buildChildEditor: function(i) {
+    var self = this;
+    var type = this.types[i];
+    var holder = self.theme.getChildEditorHolder();
+    self.editor_holder.appendChild(holder);
+
+    var schema;
+
+    if(typeof type === "string") {
+      schema = $extend({},self.schema);
+      schema.type = type;
+    }
+    else {
+      schema = $extend({},self.schema,type);
+      schema = self.jsoneditor.expandRefs(schema);
+
+      // If we need to merge `required` arrays
+      if(type && type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
+        schema.required = self.schema.required.concat(type.required);
+      }
+    }
+
+    var editor = self.jsoneditor.getEditorClass(schema);
+
+    self.editors[i] = self.jsoneditor.createEditor(editor,{
+      jsoneditor: self.jsoneditor,
+      schema: schema,
+      container: holder,
+      path: self.path,
+      parent: self,
+      required: true
+    });
+    self.editors[i].preBuild();
+    self.editors[i].build();
+    self.editors[i].postBuild();
+
+    if(self.editors[i].header) self.editors[i].header.style.display = 'none';
+
+    self.editors[i].option = self.switcher_options[i];
+
+    holder.addEventListener('change_header_text',function() {
+      self.refreshHeaderText();
+    });
+
+    if(i !== self.type) holder.style.display = 'none';
+  },
+  preBuild: function() {
+    var self = this;
+
+    this.types = [];
+    this.type = 0;
+    this.editors = [];
+    this.validators = [];
+
+    this.keep_values = true;
+    if(typeof this.jsoneditor.options.keep_oneof_values !== "undefined") this.keep_values = this.jsoneditor.options.keep_oneof_values;
+    if(typeof this.options.keep_oneof_values !== "undefined") this.keep_values = this.options.keep_oneof_values;
+
+    if(this.schema.oneOf) {
+      this.oneOf = true;
+      this.types = this.schema.oneOf;
+      delete this.schema.oneOf;
+    }
+    else if(this.schema.anyOf) {
+      this.anyOf = true;
+      this.types = this.schema.anyOf;
+      delete this.schema.anyOf;
+    }
+    else {
+      if(!this.schema.type || this.schema.type === "any") {
+        this.types = ['string','number','integer','boolean','object','array','null'];
+
+        // If any of these primitive types are disallowed
+        if(this.schema.disallow) {
+          var disallow = this.schema.disallow;
+          if(typeof disallow !== 'object' || !(Array.isArray(disallow))) {
+            disallow = [disallow];
+          }
+          var allowed_types = [];
+          $each(this.types,function(i,type) {
+            if(disallow.indexOf(type) === -1) allowed_types.push(type);
+          });
+          this.types = allowed_types;
+        }
+      }
+      else if(Array.isArray(this.schema.type)) {
+        this.types = this.schema.type;
+      }
+      else {
+        this.types = [this.schema.type];
+      }
+      delete this.schema.type;
+    }
+
+    this.display_text = this.getDisplayText(this.types);
+  },
+  build: function() {
+    var self = this;
+    var container = this.container;
+
+    this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    this.container.appendChild(this.header);
+
+    this.switcher = this.theme.getSwitcher(this.display_text);
+    container.appendChild(this.switcher);
+    this.switcher.addEventListener('change',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      self.switchEditor(self.display_text.indexOf(this.value));
+      self.onChange(true);
+    });
+
+    this.editor_holder = document.createElement('div');
+    container.appendChild(this.editor_holder);
+    
+      
+    var validator_options = {};
+    if(self.jsoneditor.options.custom_validators) {
+      validator_options.custom_validators = self.jsoneditor.options.custom_validators;
+    }
+
+    this.switcher_options = this.theme.getSwitcherOptions(this.switcher);
+    $each(this.types,function(i,type) {
+      self.editors[i] = false;
+
+      var schema;
+
+      if(typeof type === "string") {
+        schema = $extend({},self.schema);
+        schema.type = type;
+      }
+      else {
+        schema = $extend({},self.schema,type);
+
+        // If we need to merge `required` arrays
+        if(type.required && Array.isArray(type.required) && self.schema.required && Array.isArray(self.schema.required)) {
+          schema.required = self.schema.required.concat(type.required);
+        }
+      }
+
+      self.validators[i] = new JSONEditor.Validator(self.jsoneditor,schema,validator_options);
+    });
+
+    this.switchEditor(0);
+  },
+  onChildEditorChange: function(editor) {
+    if(this.editors[this.type]) {
+      this.refreshValue();
+      this.refreshHeaderText();
+    }
+
+    this._super();
+  },
+  refreshHeaderText: function() {
+    var display_text = this.getDisplayText(this.types);
+    $each(this.switcher_options, function(i,option) {
+      option.textContent = display_text[i];
+    });
+  },
+  refreshValue: function() {
+    this.value = this.editors[this.type].getValue();
+  },
+  setValue: function(val,initial) {
+    // Determine type by getting the first one that validates
+    var self = this;
+    var prev_type = this.type;
+    $each(this.validators, function(i,validator) {
+      if(!validator.validate(val).length) {
+        self.type = i;
+        self.switcher.value = self.display_text[i];
+        return false;
+      }
+    });
+
+    var type_changed = this.type != prev_type;
+    if (type_changed) {
+       this.switchEditor(this.type);
+    }
+
+    this.editors[this.type].setValue(val,initial);
+
+    this.refreshValue();
+    self.onChange(type_changed);
+  },
+  destroy: function() {
+    $each(this.editors, function(type,editor) {
+      if(editor) editor.destroy();
+    });
+    if(this.editor_holder && this.editor_holder.parentNode) this.editor_holder.parentNode.removeChild(this.editor_holder);
+    if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
+    this._super();
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+
+    // oneOf and anyOf error paths need to remove the oneOf[i] part before passing to child editors
+    if(this.oneOf || this.anyOf) {
+      var check_part = this.oneOf? 'oneOf' : 'anyOf';
+      $each(this.editors,function(i,editor) {
+        if(!editor) return;
+        var check = self.path+'.'+check_part+'['+i+']';
+        var new_errors = [];
+        $each(errors, function(j,error) {
+          if(error.path.substr(0,check.length)===check) {
+            var new_error = $extend({},error);
+            new_error.path = self.path+new_error.path.substr(check.length);
+            new_errors.push(new_error);
+          }
+        });
+
+        editor.showValidationErrors(new_errors);
+      });
+    }
+    else {
+      $each(this.editors,function(type,editor) {
+        if(!editor) return;
+        editor.showValidationErrors(errors);
+      });
+    }
+  }
+});
+
+// Enum Editor (used for objects and arrays with enumerated values)
+JSONEditor.defaults.editors["enum"] = JSONEditor.AbstractEditor.extend({
+  getNumColumns: function() {
+    return 4;
+  },
+  build: function() {
+    var container = this.container;
+    this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    this.container.appendChild(this.title);
+
+    this.options.enum_titles = this.options.enum_titles || [];
+
+    this["enum"] = this.schema["enum"];
+    this.selected = 0;
+    this.select_options = [];
+    this.html_values = [];
+
+    var self = this;
+    for(var i=0; i<this["enum"].length; i++) {
+      this.select_options[i] = this.options.enum_titles[i] || "Value "+(i+1);
+      this.html_values[i] = this.getHTML(this["enum"][i]);
+    }
+
+    // Switcher
+    this.switcher = this.theme.getSwitcher(this.select_options);
+    this.container.appendChild(this.switcher);
+
+    // Display area
+    this.display_area = this.theme.getIndentedPanel();
+    this.container.appendChild(this.display_area);
+
+    if(this.options.hide_display) this.display_area.style.display = "none";
+
+    this.switcher.addEventListener('change',function() {
+      self.selected = self.select_options.indexOf(this.value);
+      self.value = self["enum"][self.selected];
+      self.refreshValue();
+      self.onChange(true);
+    });
+    this.value = this["enum"][0];
+    this.refreshValue();
+
+    if(this["enum"].length === 1) this.switcher.style.display = 'none';
+  },
+  refreshValue: function() {
+    var self = this;
+    self.selected = -1;
+    var stringified = JSON.stringify(this.value);
+    $each(this["enum"], function(i, el) {
+      if(stringified === JSON.stringify(el)) {
+        self.selected = i;
+        return false;
+      }
+    });
+
+    if(self.selected<0) {
+      self.setValue(self["enum"][0]);
+      return;
+    }
+
+    this.switcher.value = this.select_options[this.selected];
+    this.display_area.innerHTML = this.html_values[this.selected];
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      this.switcher.disabled = false;
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    this.switcher.disabled = true;
+    this._super();
+  },
+  getHTML: function(el) {
+    var self = this;
+
+    if(el === null) {
+      return '<em>null</em>';
+    }
+    // Array or Object
+    else if(typeof el === "object") {
+      // TODO: use theme
+      var ret = '';
+
+      $each(el,function(i,child) {
+        var html = self.getHTML(child);
+
+        // Add the keys to object children
+        if(!(Array.isArray(el))) {
+          // TODO: use theme
+          html = '<div><em>'+i+'</em>: '+html+'</div>';
+        }
+
+        // TODO: use theme
+        ret += '<li>'+html+'</li>';
+      });
+
+      if(Array.isArray(el)) ret = '<ol>'+ret+'</ol>';
+      else ret = "<ul style='margin-top:0;margin-bottom:0;padding-top:0;padding-bottom:0;'>"+ret+'</ul>';
+
+      return ret;
+    }
+    // Boolean
+    else if(typeof el === "boolean") {
+      return el? 'true' : 'false';
+    }
+    // String
+    else if(typeof el === "string") {
+      return el.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+    }
+    // Number
+    else {
+      return el;
+    }
+  },
+  setValue: function(val) {
+    if(this.value !== val) {
+      this.value = val;
+      this.refreshValue();
+      this.onChange();
+    }
+  },
+  destroy: function() {
+    if(this.display_area && this.display_area.parentNode) this.display_area.parentNode.removeChild(this.display_area);
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.switcher && this.switcher.parentNode) this.switcher.parentNode.removeChild(this.switcher);
+
+    this._super();
+  }
+});
+
+JSONEditor.defaults.editors.select = JSONEditor.AbstractEditor.extend({
+  setValue: function(value,initial) {
+    value = this.typecast(value||'');
+
+    // Sanitize value before setting it
+    var sanitized = value;
+    if(this.enum_values.indexOf(sanitized) < 0) {
+      sanitized = this.enum_values[0];
+    }
+
+    if(this.value === sanitized) {
+      return;
+    }
+
+    this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
+    if(this.select2) {
+      if(this.select2v4)
+        this.select2.val(this.input.value).trigger("change"); 
+      else
+        this.select2.select2('val',this.input.value);
+    }
+    this.value = sanitized;
+    this.onChange();
+    this.change();
+  },
+  register: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  getNumColumns: function() {
+    if(!this.enum_options) return 3;
+    var longest_text = this.getTitle().length;
+    for(var i=0; i<this.enum_options.length; i++) {
+      longest_text = Math.max(longest_text,this.enum_options[i].length+4);
+    }
+    return Math.min(12,Math.max(longest_text/7,2));
+  },
+  typecast: function(value) {
+    if(this.schema.type === "boolean") {
+      return !!value;
+    }
+    else if(this.schema.type === "number") {
+      return 1*value;
+    }
+    else if(this.schema.type === "integer") {
+      return Math.floor(value*1);
+    }
+    else {
+      return ""+value;
+    }
+  },
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    return this.typecast(this.value);
+  },
+  preBuild: function() {
+    var self = this;
+    this.input_type = 'select';
+    this.enum_options = [];
+    this.enum_values = [];
+    this.enum_display = [];
+    var i;
+
+    // Enum options enumerated
+    if(this.schema["enum"]) {
+      var display = this.schema.options && this.schema.options.enum_titles || [];
+
+      $each(this.schema["enum"],function(i,option) {
+        self.enum_options[i] = ""+option;
+        self.enum_display[i] = ""+(display[i] || option);
+        self.enum_values[i] = self.typecast(option);
+      });
+
+      if(!this.isRequired()){
+        self.enum_display.unshift(' ');
+        self.enum_options.unshift('undefined');
+        self.enum_values.unshift(undefined);
+      }
+
+    }
+    // Boolean
+    else if(this.schema.type === "boolean") {
+      self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
+      self.enum_options = ['1',''];
+      self.enum_values = [true,false];
+
+      if(!this.isRequired()){
+        self.enum_display.unshift(' ');
+        self.enum_options.unshift('undefined');
+        self.enum_values.unshift(undefined);
+      }
+
+    }
+    // Dynamic Enum
+    else if(this.schema.enumSource) {
+      this.enumSource = [];
+      this.enum_display = [];
+      this.enum_options = [];
+      this.enum_values = [];
+
+      // Shortcut declaration for using a single array
+      if(!(Array.isArray(this.schema.enumSource))) {
+        if(this.schema.enumValue) {
+          this.enumSource = [
+            {
+              source: this.schema.enumSource,
+              value: this.schema.enumValue
+            }
+          ];
+        }
+        else {
+          this.enumSource = [
+            {
+              source: this.schema.enumSource
+            }
+          ];
+        }
+      }
+      else {
+        for(i=0; i<this.schema.enumSource.length; i++) {
+          // Shorthand for watched variable
+          if(typeof this.schema.enumSource[i] === "string") {
+            this.enumSource[i] = {
+              source: this.schema.enumSource[i]
+            };
+          }
+          // Make a copy of the schema
+          else if(!(Array.isArray(this.schema.enumSource[i]))) {
+            this.enumSource[i] = $extend({},this.schema.enumSource[i]);
+          }
+          else {
+            this.enumSource[i] = this.schema.enumSource[i];
+          }
+        }
+      }
+
+      // Now, enumSource is an array of sources
+      // Walk through this array and fix up the values
+      for(i=0; i<this.enumSource.length; i++) {
+        if(this.enumSource[i].value) {
+          this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
+        }
+        if(this.enumSource[i].title) {
+          this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
+        }
+        if(this.enumSource[i].filter) {
+          this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
+        }
+      }
+    }
+    // Other, not supported
+    else {
+      throw "'select' editor requires the enum property to be set.";
+    }
+  },
+  build: function() {
+    var self = this;
+    if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+    if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
+    if(this.options.compact) this.container.className += ' compact';
+
+    this.input = this.theme.getSelectInput(this.enum_options);
+    this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
+
+    if(this.schema.readOnly || this.schema.readonly) {
+      this.always_disabled = true;
+      this.input.disabled = true;
+    }
+
+    this.input.addEventListener('change',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      self.onInputChange();
+    });
+
+    this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
+    this.input.controlgroup = this.control;
+    this.container.appendChild(this.control);
+
+    this.value = this.enum_values[0];
+  },
+  onInputChange: function() {
+    var val = this.typecast(this.input.value);
+
+    var new_val;
+    // Invalid option, use first option instead
+    if(this.enum_options.indexOf(val) === -1) {
+      new_val = this.enum_values[0];
+    }
+    else {
+      new_val = this.enum_values[this.enum_options.indexOf(val)];
+    }
+
+    // If valid hasn't changed
+    if(new_val === this.value) return;
+
+    // Store new value and propogate change event
+    this.value = new_val;
+    this.onChange(true);
+  },
+  setupSelect2: function() {
+    // If the Select2 library is loaded use it when we have lots of items
+    if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2 && (this.enum_options.length > 2 || (this.enum_options.length && this.enumSource))) {
+      var options = $extend({},JSONEditor.plugins.select2);
+      if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
+      this.select2 = window.jQuery(this.input).select2(options);
+      this.select2v4 = this.select2.select2.hasOwnProperty("amd");
+
+      var self = this;
+      this.select2.on('select2-blur',function() {
+        if(self.select2v4)
+          self.input.value = self.select2.val();
+        else
+          self.input.value = self.select2.select2('val');
+
+        self.onInputChange();
+      });
+
+      this.select2.on('change',function() {
+        if(self.select2v4)
+          self.input.value = self.select2.val();
+        else
+          self.input.value = self.select2.select2('val');
+
+        self.onInputChange();
+      });
+    }
+    else {
+      this.select2 = null;
+    }
+  },
+  postBuild: function() {
+    this._super();
+    this.theme.afterInputReady(this.input);
+    this.setupSelect2();
+  },
+  onWatchedFieldChange: function() {
+    var self = this, vars, j;
+
+    // If this editor uses a dynamic select box
+    if(this.enumSource) {
+      vars = this.getWatchedFieldValues();
+      var select_options = [];
+      var select_titles = [];
+
+      for(var i=0; i<this.enumSource.length; i++) {
+        // Constant values
+        if(Array.isArray(this.enumSource[i])) {
+          select_options = select_options.concat(this.enumSource[i]);
+          select_titles = select_titles.concat(this.enumSource[i]);
+        }
+        else {
+          var items = [];
+          // Static list of items
+          if(Array.isArray(this.enumSource[i].source)) {
+            items = this.enumSource[i].source;
+          // A watched field
+          } else {
+            items = vars[this.enumSource[i].source];
+          }
+
+          if(items) {
+            // Only use a predefined part of the array
+            if(this.enumSource[i].slice) {
+              items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
+            }
+            // Filter the items
+            if(this.enumSource[i].filter) {
+              var new_items = [];
+              for(j=0; j<items.length; j++) {
+                if(this.enumSource[i].filter({i:j,item:items[j],watched:vars})) new_items.push(items[j]);
+              }
+              items = new_items;
+            }
+
+            var item_titles = [];
+            var item_values = [];
+            for(j=0; j<items.length; j++) {
+              var item = items[j];
+
+              // Rendered value
+              if(this.enumSource[i].value) {
+                item_values[j] = this.enumSource[i].value({
+                  i: j,
+                  item: item
+                });
+              }
+              // Use value directly
+              else {
+                item_values[j] = items[j];
+              }
+
+              // Rendered title
+              if(this.enumSource[i].title) {
+                item_titles[j] = this.enumSource[i].title({
+                  i: j,
+                  item: item
+                });
+              }
+              // Use value as the title also
+              else {
+                item_titles[j] = item_values[j];
+              }
+            }
+
+            // TODO: sort
+
+            select_options = select_options.concat(item_values);
+            select_titles = select_titles.concat(item_titles);
+          }
+        }
+      }
+
+      var prev_value = this.value;
+
+      this.theme.setSelectOptions(this.input, select_options, select_titles);
+      this.enum_options = select_options;
+      this.enum_display = select_titles;
+      this.enum_values = select_options;
+
+      if(this.select2) {
+        this.select2.select2('destroy');
+      }
+
+      // If the previous value is still in the new select options, stick with it
+      if(select_options.indexOf(prev_value) !== -1) {
+        this.input.value = prev_value;
+        this.value = prev_value;
+      }
+      // Otherwise, set the value to the first select option
+      else {
+        this.input.value = select_options[0];
+        this.value = this.typecast(select_options[0] || "");  
+        if(this.parent) this.parent.onChildEditorChange(this);
+        else this.jsoneditor.onChange();
+        this.jsoneditor.notifyWatchers(this.path);
+      }
+
+      this.setupSelect2();
+    }
+
+    this._super();
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      this.input.disabled = false;
+      if(this.select2) {
+        if(this.select2v4)
+          this.select2.prop("disabled",false);
+        else
+          this.select2.select2("enable",true);
+      }
+    }
+    this._super();
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    this.input.disabled = true;
+    if(this.select2) {
+      if(this.select2v4)
+        this.select2.prop("disabled",true);
+      else
+        this.select2.select2("enable",false);
+    }
+    this._super();
+  },
+  destroy: function() {
+    if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if(this.select2) {
+      this.select2.select2('destroy');
+      this.select2 = null;
+    }
+
+    this._super();
+  },
+  showValidationErrors: function (errors) {
+           var self = this;
+
+           if (this.jsoneditor.options.show_errors === "always") {}
+           else if (!this.is_dirty && this.previous_error_setting === this.jsoneditor.options.show_errors) {
+             return;
+           }
+
+           this.previous_error_setting = this.jsoneditor.options.show_errors;
+
+           var messages = [];
+           $each(errors, function (i, error) {
+             if (error.path === self.path) {
+               messages.push(error.message);
+             }
+           });
+
+           this.input.controlgroup = this.control;
+
+           if (messages.length) {
+             this.theme.addInputError(this.input, messages.join('. ') + '.');
+           }
+           else {
+             this.theme.removeInputError(this.input);
+           }
+         }
+});
+
+JSONEditor.defaults.editors.selectize = JSONEditor.AbstractEditor.extend({
+  setValue: function(value,initial) {
+    value = this.typecast(value||'');
+
+    // Sanitize value before setting it
+    var sanitized = value;
+    if(this.enum_values.indexOf(sanitized) < 0) {
+      sanitized = this.enum_values[0];
+    }
+
+    if(this.value === sanitized) {
+      return;
+    }
+
+    this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
+
+    if(this.selectize) {
+      this.selectize[0].selectize.addItem(sanitized);
+    }
+
+    this.value = sanitized;
+    this.onChange();
+  },
+  register: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  getNumColumns: function() {
+    if(!this.enum_options) return 3;
+    var longest_text = this.getTitle().length;
+    for(var i=0; i<this.enum_options.length; i++) {
+      longest_text = Math.max(longest_text,this.enum_options[i].length+4);
+    }
+    return Math.min(12,Math.max(longest_text/7,2));
+  },
+  typecast: function(value) {
+    if(this.schema.type === "boolean") {
+      return !!value;
+    }
+    else if(this.schema.type === "number") {
+      return 1*value;
+    }
+    else if(this.schema.type === "integer") {
+      return Math.floor(value*1);
+    }
+    else {
+      return ""+value;
+    }
+  },
+  getValue: function() {
+    if (!this.dependenciesFulfilled) {
+      return undefined;
+    }
+    return this.value;
+  },
+  preBuild: function() {
+    var self = this;
+    this.input_type = 'select';
+    this.enum_options = [];
+    this.enum_values = [];
+    this.enum_display = [];
+    var i;
+
+    // Enum options enumerated
+    if(this.schema.enum) {
+      var display = this.schema.options && this.schema.options.enum_titles || [];
+
+      $each(this.schema.enum,function(i,option) {
+        self.enum_options[i] = ""+option;
+        self.enum_display[i] = ""+(display[i] || option);
+        self.enum_values[i] = self.typecast(option);
+      });
+    }
+    // Boolean
+    else if(this.schema.type === "boolean") {
+      self.enum_display = this.schema.options && this.schema.options.enum_titles || ['true','false'];
+      self.enum_options = ['1','0'];
+      self.enum_values = [true,false];
+    }
+    // Dynamic Enum
+    else if(this.schema.enumSource) {
+      this.enumSource = [];
+      this.enum_display = [];
+      this.enum_options = [];
+      this.enum_values = [];
+
+      // Shortcut declaration for using a single array
+      if(!(Array.isArray(this.schema.enumSource))) {
+        if(this.schema.enumValue) {
+          this.enumSource = [
+            {
+              source: this.schema.enumSource,
+              value: this.schema.enumValue
+            }
+          ];
+        }
+        else {
+          this.enumSource = [
+            {
+              source: this.schema.enumSource
+            }
+          ];
+        }
+      }
+      else {
+        for(i=0; i<this.schema.enumSource.length; i++) {
+          // Shorthand for watched variable
+          if(typeof this.schema.enumSource[i] === "string") {
+            this.enumSource[i] = {
+              source: this.schema.enumSource[i]
+            };
+          }
+          // Make a copy of the schema
+          else if(!(Array.isArray(this.schema.enumSource[i]))) {
+            this.enumSource[i] = $extend({},this.schema.enumSource[i]);
+          }
+          else {
+            this.enumSource[i] = this.schema.enumSource[i];
+          }
+        }
+      }
+
+      // Now, enumSource is an array of sources
+      // Walk through this array and fix up the values
+      for(i=0; i<this.enumSource.length; i++) {
+        if(this.enumSource[i].value) {
+          this.enumSource[i].value = this.jsoneditor.compileTemplate(this.enumSource[i].value, this.template_engine);
+        }
+        if(this.enumSource[i].title) {
+          this.enumSource[i].title = this.jsoneditor.compileTemplate(this.enumSource[i].title, this.template_engine);
+        }
+        if(this.enumSource[i].filter) {
+          this.enumSource[i].filter = this.jsoneditor.compileTemplate(this.enumSource[i].filter, this.template_engine);
+        }
+      }
+    }
+    // Other, not supported
+    else {
+      throw "'select' editor requires the enum property to be set.";
+    }
+  },
+  build: function() {
+    var self = this;
+    if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+    if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
+
+    if(this.options.compact) this.container.className += ' compact';
+
+    this.input = this.theme.getSelectInput(this.enum_options);
+    this.theme.setSelectOptions(this.input,this.enum_options,this.enum_display);
+
+    if(this.schema.readOnly || this.schema.readonly) {
+      this.always_disabled = true;
+      this.input.disabled = true;
+    }
+
+    this.input.addEventListener('change',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      self.onInputChange();
+    });
+
+    this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
+    this.container.appendChild(this.control);
+
+    this.value = this.enum_values[0];
+  },
+  onInputChange: function() {
+    //console.log("onInputChange");
+    var val = this.input.value;
+
+    var sanitized = val;
+    if(this.enum_options.indexOf(val) === -1) {
+      sanitized = this.enum_options[0];
+    }
+
+    //this.value = this.enum_values[this.enum_options.indexOf(val)];
+    this.value = val;
+    this.onChange(true);
+  },
+  setupSelectize: function() {
+    // If the Selectize library is loaded use it when we have lots of items
+    var self = this;
+    if(window.jQuery && window.jQuery.fn && window.jQuery.fn.selectize && (this.enum_options.length >= 2 || (this.enum_options.length && this.enumSource))) {
+      var options = $extend({},JSONEditor.plugins.selectize);
+      if(this.schema.options && this.schema.options.selectize_options) options = $extend(options,this.schema.options.selectize_options);
+      this.selectize = window.jQuery(this.input).selectize($extend(options,
+      {
+        // set the create option to true by default, or to the user specified value if defined
+        create: ( options.create === undefined ? true : options.create),
+        onChange : function() {
+          self.onInputChange();
+        }
+      }));
+    }
+    else {
+      this.selectize = null;
+    }
+  },
+  postBuild: function() {
+    this._super();
+    this.theme.afterInputReady(this.input);
+    this.setupSelectize();
+  },
+  onWatchedFieldChange: function() {
+    var self = this, vars, j;
+
+    // If this editor uses a dynamic select box
+    if(this.enumSource) {
+      vars = this.getWatchedFieldValues();
+      var select_options = [];
+      var select_titles = [];
+
+      for(var i=0; i<this.enumSource.length; i++) {
+        // Constant values
+        if(Array.isArray(this.enumSource[i])) {
+          select_options = select_options.concat(this.enumSource[i]);
+          select_titles = select_titles.concat(this.enumSource[i]);
+        }
+        // A watched field
+        else if(vars[this.enumSource[i].source]) {
+          var items = vars[this.enumSource[i].source];
+
+          // Only use a predefined part of the array
+          if(this.enumSource[i].slice) {
+            items = Array.prototype.slice.apply(items,this.enumSource[i].slice);
+          }
+          // Filter the items
+          if(this.enumSource[i].filter) {
+            var new_items = [];
+            for(j=0; j<items.length; j++) {
+              if(this.enumSource[i].filter({i:j,item:items[j]})) new_items.push(items[j]);
+            }
+            items = new_items;
+          }
+
+          var item_titles = [];
+          var item_values = [];
+          for(j=0; j<items.length; j++) {
+            var item = items[j];
+
+            // Rendered value
+            if(this.enumSource[i].value) {
+              item_values[j] = this.enumSource[i].value({
+                i: j,
+                item: item
+              });
+            }
+            // Use value directly
+            else {
+              item_values[j] = items[j];
+            }
+
+            // Rendered title
+            if(this.enumSource[i].title) {
+              item_titles[j] = this.enumSource[i].title({
+                i: j,
+                item: item
+              });
+            }
+            // Use value as the title also
+            else {
+              item_titles[j] = item_values[j];
+            }
+          }
+
+          // TODO: sort
+
+          select_options = select_options.concat(item_values);
+          select_titles = select_titles.concat(item_titles);
+        }
+      }
+
+      var prev_value = this.value;
+
+      // Check to see if this item is in the list
+      // Note: We have to skip empty string for watch lists to work properly
+      if ((prev_value !== undefined) && (prev_value !== "") && (select_options.indexOf(prev_value) === -1)) {
+        // item is not in the list. Add it.
+        select_options = select_options.concat(prev_value);
+        select_titles = select_titles.concat(prev_value);
+      }
+
+      this.theme.setSelectOptions(this.input, select_options, select_titles);
+      this.enum_options = select_options;
+      this.enum_display = select_titles;
+      this.enum_values = select_options;
+
+      // If the previous value is still in the new select options, stick with it
+      if(select_options.indexOf(prev_value) !== -1) {
+        this.input.value = prev_value;
+        this.value = prev_value;
+      }
+
+      // Otherwise, set the value to the first select option
+      else {
+        this.input.value = select_options[0];
+        this.value = select_options[0] || "";
+        if(this.parent) this.parent.onChildEditorChange(this);
+        else this.jsoneditor.onChange();
+        this.jsoneditor.notifyWatchers(this.path);
+      }
+
+      if(this.selectize) {
+        // Update the Selectize options
+        this.updateSelectizeOptions(select_options);
+      }
+      else {
+        this.setupSelectize();
+      }
+
+      this._super();
+    }
+  },
+  updateSelectizeOptions: function(select_options) {
+    var selectized = this.selectize[0].selectize,
+        self = this;
+
+    selectized.off();
+    selectized.clearOptions();
+    for(var n in select_options) {
+      selectized.addOption({value:select_options[n],text:select_options[n]});
+    }
+    selectized.addItem(this.value);
+    selectized.on('change',function() {
+      self.onInputChange();
+    });
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      this.input.disabled = false;
+      if(this.selectize) {
+        this.selectize[0].selectize.unlock();
+      }
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    this.input.disabled = true;
+    if(this.selectize) {
+      this.selectize[0].selectize.lock();
+    }
+    this._super();
+  },
+  destroy: function() {
+    if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if(this.selectize) {
+      this.selectize[0].selectize.destroy();
+      this.selectize = null;
+    }
+    this._super();
+  }
+});
+
+JSONEditor.defaults.editors.multiselect = JSONEditor.AbstractEditor.extend({
+  preBuild: function() {
+    this._super();
+    var i;
+
+    this.select_options = {};
+    this.select_values = {};
+
+    var items_schema = this.jsoneditor.expandRefs(this.schema.items || {});
+
+    var e = items_schema["enum"] || [];
+    var t = items_schema.options? items_schema.options.enum_titles || [] : [];
+    this.option_keys = [];
+    this.option_titles = [];
+    for(i=0; i<e.length; i++) {
+      // If the sanitized value is different from the enum value, don't include it
+      if(this.sanitize(e[i]) !== e[i]) continue;
+
+      this.option_keys.push(e[i]+"");
+      this.option_titles.push((t[i]||e[i])+"");
+      this.select_values[e[i]+""] = e[i];
+    }
+  },
+  build: function() {
+    var self = this, i;
+    if(!this.options.compact) this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+
+    if((!this.schema.format && this.option_keys.length < 8) || this.schema.format === "checkbox") {
+      this.input_type = 'checkboxes';
+
+      this.inputs = {};
+      this.controls = {};
+      for(i=0; i<this.option_keys.length; i++) {
+        this.inputs[this.option_keys[i]] = this.theme.getCheckbox();
+        this.select_options[this.option_keys[i]] = this.inputs[this.option_keys[i]];
+        var label = this.theme.getCheckboxLabel(this.option_titles[i]);
+        this.controls[this.option_keys[i]] = this.theme.getFormControl(label, this.inputs[this.option_keys[i]]);
+      }
+
+      this.control = this.theme.getMultiCheckboxHolder(this.controls,this.label,this.description);
+    }
+    else {
+      this.input_type = 'select';
+      this.input = this.theme.getSelectInput(this.option_keys);
+      this.theme.setSelectOptions(this.input,this.option_keys,this.option_titles);
+      this.input.multiple = true;
+      this.input.size = Math.min(10,this.option_keys.length);
+
+      for(i=0; i<this.option_keys.length; i++) {
+        this.select_options[this.option_keys[i]] = this.input.children[i];
+      }
+
+      if(this.schema.readOnly || this.schema.readonly) {
+        this.always_disabled = true;
+        this.input.disabled = true;
+      }
+
+      this.control = this.theme.getFormControl(this.label, this.input, this.description);
+    }
+
+    this.container.appendChild(this.control);
+    this.control.addEventListener('change',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      var new_value = [];
+      for(i = 0; i<self.option_keys.length; i++) {
+        if(self.select_options[self.option_keys[i]].selected || self.select_options[self.option_keys[i]].checked) new_value.push(self.select_values[self.option_keys[i]]);
+      }
+
+      self.updateValue(new_value);
+      self.onChange(true);
+    });
+  },
+  setValue: function(value, initial) {
+    var i;
+    value = value || [];
+    if(typeof value !== "object") value = [value];
+    else if(!(Array.isArray(value))) value = [];
+
+    // Make sure we are dealing with an array of strings so we can check for strict equality
+    for(i=0; i<value.length; i++) {
+      if(typeof value[i] !== "string") value[i] += "";
+    }
+
+    // Update selected status of options
+    for(i in this.select_options) {
+      if(!this.select_options.hasOwnProperty(i)) continue;
+
+      this.select_options[i][this.input_type === "select"? "selected" : "checked"] = (value.indexOf(i) !== -1);
+    }
+
+    this.updateValue(value);
+    this.onChange();
+  },
+  setupSelect2: function() {
+    if(window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
+        var options = window.jQuery.extend({},JSONEditor.plugins.select2);
+        if(this.schema.options && this.schema.options.select2_options) options = $extend(options,this.schema.options.select2_options);
+        this.select2 = window.jQuery(this.input).select2(options);
+        this.select2v4 = this.select2.select2.hasOwnProperty("amd");
+
+        var self = this;
+        this.select2.on('select2-blur',function() {
+          if(self.select2v4)
+            self.value = self.select2.val();
+          else
+            self.value = self.select2.select2('val');
+
+          self.onChange(true);
+        });
+
+        this.select2.on('change',function() {
+          if(self.select2v4)
+            self.value = self.select2.val();
+          else
+            self.value = self.select2.select2('val');
+
+          self.onChange(true);
+        });
+    }
+    else {
+        this.select2 = null;
+    }
+  },
+  onInputChange: function() {
+      this.value = this.input.value;
+      this.onChange(true);
+  },
+  postBuild: function() {
+      this._super();
+      this.setupSelect2();
+  },
+  register: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  getNumColumns: function() {
+    var longest_text = this.getTitle().length;
+    for(var i in this.select_values) {
+      if(!this.select_values.hasOwnProperty(i)) continue;
+      longest_text = Math.max(longest_text,(this.select_values[i]+"").length+4);
+    }
+
+    return Math.min(12,Math.max(longest_text/7,2));
+  },
+  updateValue: function(value) {
+    var changed = false;
+    var new_value = [];
+    for(var i=0; i<value.length; i++) {
+      if(!this.select_options[value[i]+""]) {
+        changed = true;
+        continue;
+      }
+      var sanitized = this.sanitize(this.select_values[value[i]]);
+      new_value.push(sanitized);
+      if(sanitized !== value[i]) changed = true;
+    }
+    this.value = new_value;
+
+    if(this.select2) {
+      if(this.select2v4)
+        this.select2.val(this.value).trigger("change"); 
+      else
+        this.select2.select2('val',this.value);
+    }
+
+    return changed;
+  },
+  sanitize: function(value) {
+    if(this.schema.items.type === "number") {
+      return 1*value;
+    }
+    else if(this.schema.items.type === "integer") {
+      return Math.floor(value*1);
+    }
+    else {
+      return ""+value;
+    }
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.input) {
+        this.input.disabled = false;
+      }
+      else if(this.inputs) {
+        for(var i in this.inputs) {
+          if(!this.inputs.hasOwnProperty(i)) continue;
+          this.inputs[i].disabled = false;
+        }
+      }
+      if(this.select2) {
+        if(this.select2v4)
+          this.select2.prop("disabled",false);
+        else
+          this.select2.select2("enable",true);
+      }
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.input) {
+      this.input.disabled = true;
+    }
+    else if(this.inputs) {
+      for(var i in this.inputs) {
+        if(!this.inputs.hasOwnProperty(i)) continue;
+        this.inputs[i].disabled = true;
+      }
+    }
+    if(this.select2) {
+      if(this.select2v4)
+        this.select2.prop("disabled",true);
+      else
+        this.select2.select2("enable",false);
+    }
+    this._super();
+  },
+  destroy: function() {
+    if(this.select2) {
+        this.select2.select2('destroy');
+        this.select2 = null;
+    }
+    this._super();
+  }
+});
+
+JSONEditor.defaults.editors.base64 = JSONEditor.AbstractEditor.extend({
+  getNumColumns: function() {
+    return 4;
+  },
+  build: function() {    
+    var self = this;
+    this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+    if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
+
+    // Input that holds the base64 string
+    this.input = this.theme.getFormInputField('hidden');
+    this.container.appendChild(this.input);
+    
+    // Don't show uploader if this is readonly
+    if(!this.schema.readOnly && !this.schema.readonly) {
+      if(!window.FileReader) throw "FileReader required for base64 editor";
+      
+      // File uploader
+      this.uploader = this.theme.getFormInputField('file');
+      
+      this.uploader.addEventListener('change',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        
+        if(this.files && this.files.length) {
+          var fr = new FileReader();
+          fr.onload = function(evt) {
+            self.value = evt.target.result;
+            self.refreshPreview();
+            self.onChange(true);
+            fr = null;
+          };
+          fr.readAsDataURL(this.files[0]);
+        }
+      });
+    }
+
+    this.preview = this.theme.getFormInputDescription(this.schema.description);
+    this.container.appendChild(this.preview);
+
+    this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview, this.infoButton);
+    this.container.appendChild(this.control);
+  },
+  refreshPreview: function() {
+    if(this.last_preview === this.value) return;
+    this.last_preview = this.value;
+    
+    this.preview.innerHTML = '';
+    
+    if(!this.value) return;
+    
+    var mime = this.value.match(/^data:([^;,]+)[;,]/);
+    if(mime) mime = mime[1];
+    
+    if(!mime) {
+      this.preview.innerHTML = '<em>Invalid data URI</em>';
+    }
+    else {
+      this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+Math.floor((this.value.length-this.value.split(',')[0].length-1)/1.33333)+' bytes';
+      if(mime.substr(0,5)==="image") {
+        this.preview.innerHTML += '<br>';
+        var img = document.createElement('img');
+        img.style.maxWidth = '100%';
+        img.style.maxHeight = '100px';
+        img.src = this.value;
+        this.preview.appendChild(img);
+      }
+    }
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.uploader) this.uploader.disabled = false;
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.uploader) this.uploader.disabled = true;
+    this._super();
+  },
+  setValue: function(val) {
+    if(this.value !== val) {
+      this.value = val;
+      this.input.value = this.value;
+      this.refreshPreview();
+      this.onChange();
+    }
+  },
+  destroy: function() {
+    if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
+
+    this._super();
+  }
+});
+
+JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
+  getNumColumns: function() {
+    return 4;
+  },
+  build: function() {    
+    var self = this;
+    this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
+
+    // Input that holds the base64 string
+    this.input = this.theme.getFormInputField('hidden');
+    this.container.appendChild(this.input);
+    
+    // Don't show uploader if this is readonly
+    if(!this.schema.readOnly && !this.schema.readonly) {
+
+      if(!this.jsoneditor.options.upload) throw "Upload handler required for upload editor";
+
+      // File uploader
+      this.uploader = this.theme.getFormInputField('file');
+      
+      this.uploader.addEventListener('change',function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        
+        if(this.files && this.files.length) {
+          var fr = new FileReader();
+          fr.onload = function(evt) {
+            self.preview_value = evt.target.result;
+            self.refreshPreview();
+            self.onChange(true);
+            fr = null;
+          };
+          fr.readAsDataURL(this.files[0]);
+        }
+      });
+    }
+
+    var description = this.schema.description;
+    if (!description) description = '';
+
+    this.preview = this.theme.getFormInputDescription(description);
+    this.container.appendChild(this.preview);
+
+    this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
+    this.container.appendChild(this.control);
+  },
+  refreshPreview: function() {
+    if(this.last_preview === this.preview_value) return;
+    this.last_preview = this.preview_value;
+
+    this.preview.innerHTML = '';
+    
+    if(!this.preview_value) return;
+
+    var self = this;
+
+    var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
+    if(mime) mime = mime[1];
+    if(!mime) mime = 'unknown';
+
+    var file = this.uploader.files[0];
+
+    this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+file.size+' bytes';
+    if(mime.substr(0,5)==="image") {
+      this.preview.innerHTML += '<br>';
+      var img = document.createElement('img');
+      img.style.maxWidth = '100%';
+      img.style.maxHeight = '100px';
+      img.src = this.preview_value;
+      this.preview.appendChild(img);
+    }
+
+    this.preview.innerHTML += '<br>';
+    var uploadButton = this.getButton('Upload', 'upload', 'Upload');
+    this.preview.appendChild(uploadButton);
+    uploadButton.addEventListener('click',function(event) {
+      event.preventDefault();
+
+      uploadButton.setAttribute("disabled", "disabled");
+      self.theme.removeInputError(self.uploader);
+
+      if (self.theme.getProgressBar) {
+        self.progressBar = self.theme.getProgressBar();
+        self.preview.appendChild(self.progressBar);
+      }
+
+      self.jsoneditor.options.upload(self.path, file, {
+        success: function(url) {
+          self.setValue(url);
+
+          if(self.parent) self.parent.onChildEditorChange(self);
+          else self.jsoneditor.onChange();
+
+          if (self.progressBar) self.preview.removeChild(self.progressBar);
+          uploadButton.removeAttribute("disabled");
+        },
+        failure: function(error) {
+          self.theme.addInputError(self.uploader, error);
+          if (self.progressBar) self.preview.removeChild(self.progressBar);
+          uploadButton.removeAttribute("disabled");
+        },
+        updateProgress: function(progress) {
+          if (self.progressBar) {
+            if (progress) self.theme.updateProgressBar(self.progressBar, progress);
+            else self.theme.updateProgressBarUnknown(self.progressBar);
+          }
+        }
+      });
+    });
+
+    if(this.jsoneditor.options.auto_upload || this.schema.options.auto_upload) {
+      uploadButton.dispatchEvent(new MouseEvent('click'));
+      this.preview.removeChild(uploadButton);
+    }
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      if(this.uploader) this.uploader.disabled = false;
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    if(this.uploader) this.uploader.disabled = true;
+    this._super();
+  },
+  setValue: function(val) {
+    if(this.value !== val) {
+      this.value = val;
+      this.input.value = this.value;
+      this.onChange();
+    }
+  },
+  destroy: function() {
+    if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
+
+    this._super();
+  }
+});
+
+JSONEditor.defaults.editors.checkbox = JSONEditor.AbstractEditor.extend({
+  setValue: function(value,initial) {
+    this.value = !!value;
+    this.input.checked = this.value;
+    this.onChange();
+  },
+  register: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.setAttribute('name',this.formname);
+  },
+  unregister: function() {
+    this._super();
+    if(!this.input) return;
+    this.input.removeAttribute('name');
+  },
+  getNumColumns: function() {
+    return Math.min(12,Math.max(this.getTitle().length/7,2));
+  },
+  build: function() {
+    var self = this;
+    if(!this.options.compact) {
+      this.label = this.header = this.theme.getCheckboxLabel(this.getTitle());
+    }
+    if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
+    if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
+    if(this.options.compact) this.container.className += ' compact';
+
+    this.input = this.theme.getCheckbox();
+    this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
+
+    if(this.schema.readOnly || this.schema.readonly) {
+      this.always_disabled = true;
+      this.input.disabled = true;
+    }
+
+    this.input.addEventListener('change',function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      self.value = this.checked;
+      self.onChange(true);
+    });
+
+    this.container.appendChild(this.control);
+  },
+  enable: function() {
+    if(!this.always_disabled) {
+      this.input.disabled = false;
+      this._super();
+    }
+  },
+  disable: function(always_disabled) {
+    if(always_disabled) this.always_disabled = true;
+    this.input.disabled = true;
+    this._super();
+  },
+  destroy: function() {
+    if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+    this._super();
+  },
+  showValidationErrors: function (errors) {
+    var self = this;
+
+    if (this.jsoneditor.options.show_errors === "always") {}
+
+    else if (!this.is_dirty && this.previous_error_setting === this.jsoneditor.options.show_errors) {
+      return;
+    }
+
+    this.previous_error_setting = this.jsoneditor.options.show_errors;
+
+    var messages = [];
+    $each(errors, function (i, error) {
+      if (error.path === self.path) {
+        messages.push(error.message);
+      }
+    });
+
+    this.input.controlgroup = this.control;
+
+    if (messages.length) {
+      this.theme.addInputError(this.input, messages.join('. ') + '.');
+    }
+    else {
+      this.theme.removeInputError(this.input);
+    }
+  }
+});
+
+JSONEditor.defaults.editors.arraySelectize = JSONEditor.AbstractEditor.extend({
+  build: function() {
+    this.title = this.theme.getFormInputLabel(this.getTitle());
+
+    this.title_controls = this.theme.getHeaderButtonHolder();
+    this.title.appendChild(this.title_controls);
+    this.error_holder = document.createElement('div');
+
+    if(this.schema.description) {
+      this.description = this.theme.getDescription(this.schema.description);
+    }
+
+    this.input = document.createElement('select');
+    this.input.setAttribute('multiple', 'multiple');
+
+    var group = this.theme.getFormControl(this.title, this.input, this.description);
+
+    this.container.appendChild(group);
+    this.container.appendChild(this.error_holder);
+
+    window.jQuery(this.input).selectize({
+      delimiter: false,
+      createOnBlur: true,
+      create: true
+    });
+  },
+  postBuild: function() {
+      var self = this;
+      this.input.selectize.on('change', function(event) {
+          self.refreshValue();
+          self.onChange(true);
+      });
+  },
+  destroy: function() {
+    this.empty(true);
+    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
+    if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
+    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
+
+    this._super();
+  },
+  empty: function(hard) {},
+  setValue: function(value, initial) {
+    var self = this;
+    // Update the array's value, adding/removing rows when necessary
+    value = value || [];
+    if(!(Array.isArray(value))) value = [value];
+
+    this.input.selectize.clearOptions();
+    this.input.selectize.clear(true);
+
+    value.forEach(function(item) {
+      self.input.selectize.addOption({text: item, value: item});
+    });
+    this.input.selectize.setValue(value);
+
+    this.refreshValue(initial);
+  },
+  refreshValue: function(force) {
+    this.value = this.input.selectize.getValue();
+  },
+  showValidationErrors: function(errors) {
+    var self = this;
+
+    // Get all the errors that pertain to this editor
+    var my_errors = [];
+    var other_errors = [];
+    $each(errors, function(i,error) {
+      if(error.path === self.path) {
+        my_errors.push(error);
+      }
+      else {
+        other_errors.push(error);
+      }
+    });
+
+    // Show errors for this editor
+    if(this.error_holder) {
+
+      if(my_errors.length) {
+        var message = [];
+        this.error_holder.innerHTML = '';
+        this.error_holder.style.display = '';
+        $each(my_errors, function(i,error) {
+          self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
+        });
+      }
+      // Hide error area
+      else {
+        this.error_holder.style.display = 'none';
+      }
+    }
+  }
+});
+
+var matchKey = (function () {
+  var elem = document.documentElement;
+
+  if (elem.matches) return 'matches';
+  else if (elem.webkitMatchesSelector) return 'webkitMatchesSelector';
+  else if (elem.mozMatchesSelector) return 'mozMatchesSelector';
+  else if (elem.msMatchesSelector) return 'msMatchesSelector';
+  else if (elem.oMatchesSelector) return 'oMatchesSelector';
+})();
+
+JSONEditor.AbstractTheme = Class.extend({
+  getContainer: function() {
+    return document.createElement('div');
+  },
+  getFloatRightLinkHolder: function() {
+    var el = document.createElement('div');
+    el.style = el.style || {};
+    el.style.cssFloat = 'right';
+    el.style.marginLeft = '10px';
+    return el;
+  },
+  getModal: function() {
+    var el = document.createElement('div');
+    el.style.backgroundColor = 'white';
+    el.style.border = '1px solid black';
+    el.style.boxShadow = '3px 3px black';
+    el.style.position = 'absolute';
+    el.style.zIndex = '10';
+    el.style.display = 'none';
+    return el;
+  },
+  getGridContainer: function() {
+    var el = document.createElement('div');
+    return el;
+  },
+  getGridRow: function() {
+    var el = document.createElement('div');
+    el.className = 'row';
+    return el;
+  },
+  getGridColumn: function() {
+    var el = document.createElement('div');
+    return el;
+  },
+  setGridColumnSize: function(el,size) {
+
+  },
+  getLink: function(text) {
+    var el = document.createElement('a');
+    el.setAttribute('href','#');
+    el.appendChild(document.createTextNode(text));
+    return el;
+  },
+  disableHeader: function(header) {
+    header.style.color = '#ccc';
+  },
+  disableLabel: function(label) {
+    label.style.color = '#ccc';
+  },
+  enableHeader: function(header) {
+    header.style.color = '';
+  },
+  enableLabel: function(label) {
+    label.style.color = '';
+  },
+  getInfoButton: function(text) {
+    var icon = document.createElement('span');
+    icon.innerText = "ⓘ";
+    icon.style.fontSize = "16px";
+    icon.style.fontWeight = "bold";
+    icon.style.padding = ".25rem";
+    icon.style.position = "relative";
+    icon.style.display = "inline-block";
+
+    var tooltip = document.createElement('span');
+    tooltip.style.fontSize = "12px";
+    icon.style.fontWeight = "normal";
+    tooltip.style["font-family"] = "sans-serif";
+    tooltip.style.visibility = "hidden";
+    tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
+    tooltip.style.margin = "0 .25rem";
+    tooltip.style.color = "#FAFAFA";
+    tooltip.style.padding = ".5rem 1rem";
+    tooltip.style["border-radius"] = ".25rem";
+    tooltip.style.width = "20rem";
+    tooltip.style.position = "absolute";
+    tooltip.innerText = text;
+    icon.onmouseover = function() {
+      tooltip.style.visibility = "visible";
+    };
+    icon.onmouseleave = function() {
+      tooltip.style.visibility = "hidden";
+    };
+
+    icon.appendChild(tooltip);
+
+    return icon;
+  },
+  getFormInputLabel: function(text) {
+    var el = document.createElement('label');
+    el.appendChild(document.createTextNode(text));
+    return el;
+  },
+  getCheckboxLabel: function(text) {
+    var el = this.getFormInputLabel(text);
+    el.style.fontWeight = 'normal';
+    return el;
+  },
+  getHeader: function(text) {
+    var el = document.createElement('h3');
+    if(typeof text === "string") {
+      el.textContent = text;
+      el.style.fontWeight = 'bold';
+      el.style.fontSize = '12px';
+      el.style.padding = '4px';
+    }
+    else {
+      el.appendChild(text);
+    }
+
+    return el;
+  },
+  getCheckbox: function() {
+    var el = this.getFormInputField('checkbox');
+    el.style.display = 'inline-block';
+    el.style.width = 'auto';
+    return el;
+  },
+  getMultiCheckboxHolder: function(controls,label,description) {
+    var el = document.createElement('div');
+
+    if(label) {
+      label.style.display = 'block';
+      el.appendChild(label);
+    }
+
+    for(var i in controls) {
+      if(!controls.hasOwnProperty(i)) continue;
+      controls[i].style.display = 'inline-block';
+      controls[i].style.marginRight = '20px';
+      el.appendChild(controls[i]);
+    }
+
+    if(description) el.appendChild(description);
+
+    return el;
+  },
+  getSelectInput: function(options) {
+    var select = document.createElement('select');
+    if(options) this.setSelectOptions(select, options);
+    return select;
+  },
+  getSwitcher: function(options) {
+    var switcher = this.getSelectInput(options);
+    switcher.style.backgroundColor = 'transparent';
+    switcher.style.display = 'inline-block';
+    switcher.style.fontStyle = 'italic';
+    switcher.style.fontWeight = 'normal';
+    switcher.style.height = 'auto';
+    switcher.style.marginBottom = 0;
+    switcher.style.marginLeft = '5px';
+    switcher.style.padding = '0 0 0 3px';
+    switcher.style.width = 'auto';
+    return switcher;
+  },
+  getSwitcherOptions: function(switcher) {
+    return switcher.getElementsByTagName('option');
+  },
+  setSwitcherOptions: function(switcher, options, titles) {
+    this.setSelectOptions(switcher, options, titles);
+  },
+  setSelectOptions: function(select, options, titles) {
+    titles = titles || [];
+    select.innerHTML = '';
+    for(var i=0; i<options.length; i++) {
+      var option = document.createElement('option');
+      option.setAttribute('value',options[i]);
+      option.textContent = titles[i] || options[i];
+      select.appendChild(option);
+    }
+  },
+  getTextareaInput: function(rows, cols) {
+    var el = document.createElement('textarea');
+    el.style = el.style || {};
+    el.style.width = '100%';
+    el.style.height = '50px';
+    el.style.fontWeight = 'bold';
+    el.style.fontSize = '1em';
+    el.style.boxSizing = 'border-box';
+    if(typeof rows === undefined) { rows = 1 };
+    if(typeof cols === undefined) { cols = 80 };
+    el.rows = rows;
+    el.cols = cols;
+    el.wrap = 'soft';
+    el.readonly = 'true';
+    return el;
+  },
+  getRangeInput: function(min,max,step) {
+    var el = this.getFormInputField('range');
+    el.setAttribute('min',min);
+    el.setAttribute('max',max);
+    el.setAttribute('step',step);
+    return el;
+  },
+  getFormInputField: function(type) {
+    var el = document.createElement('input');
+    el.setAttribute('type',type);
+    return el;
+  },
+  afterInputReady: function(input) {
+
+  },
+  getFormControl: function(label, input, description, infoText) {
+    var el = document.createElement('div');
+    el.className = 'form-control';
+    if(label) el.appendChild(label);
+    if(input.type === 'checkbox' && label) {
+      label.insertBefore(input,label.firstChild);
+      if(infoText) label.appendChild(infoText);
+    }
+    else {
+      if(infoText) label.appendChild(infoText);
+      el.appendChild(input);
+    }
+
+    if(description) el.appendChild(description);
+    return el;
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.style = el.style || {};
+    el.style.paddingLeft = '10px';
+    el.style.marginLeft = '10px';
+    el.style.borderLeft = '1px solid #ccc';
+    return el;
+  },
+  getTopIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.style = el.style || {};
+    el.style.paddingLeft = '10px';
+    el.style.marginLeft = '10px';
+    return el;
+  },
+  getChildEditorHolder: function() {
+    return document.createElement('div');
+  },
+  getDescription: function(text) {
+    var el = document.createElement('p');
+    el.innerHTML = text;
+    return el;
+  },
+  getCheckboxDescription: function(text) {
+    return this.getDescription(text);
+  },
+  getFormInputDescription: function(text) {
+    return this.getDescription(text);
+  },
+  getHeaderButtonHolder: function() {
+    return this.getButtonHolder();
+  },
+  getButtonHolder: function() {
+    return document.createElement('div');
+  },
+  getButton: function(text, icon, title) {
+    var el = document.createElement('button');
+    el.type = 'button';
+    this.setButtonText(el,text,icon,title);
+    return el;
+  },
+  setButtonText: function(button, text, icon, title) {
+    button.innerHTML = '';
+    if(icon) {
+      button.appendChild(icon);
+      button.innerHTML += ' ';
+    }
+    button.appendChild(document.createTextNode(text));
+    if(title) button.setAttribute('title',title);
+  },
+  getTable: function() {
+    return document.createElement('table');
+  },
+  getTableRow: function() {
+    return document.createElement('tr');
+  },
+  getTableHead: function() {
+    return document.createElement('thead');
+  },
+  getTableBody: function() {
+    return document.createElement('tbody');
+  },
+  getTableHeaderCell: function(text) {
+    var el = document.createElement('th');
+    el.textContent = text;
+    return el;
+  },
+  getTableCell: function() {
+    var el = document.createElement('td');
+    return el;
+  },
+  getErrorMessage: function(text) {
+    var el = document.createElement('p');
+    el.style = el.style || {};
+    el.style.color = 'red';
+    el.appendChild(document.createTextNode(text));
+    return el;
+  },
+  addInputError: function(input, text) {
+  },
+  removeInputError: function(input) {
+  },
+  addTableRowError: function(row) {
+  },
+  removeTableRowError: function(row) {
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = "<div style='float: left; width: 130px;' class='tabs' id='" + pName + "'></div><div class='content' style='margin-left: 120px;' id='" + pName + "'></div><div style='clear:both;'></div>";
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = "<div class='tabs' style='margin-left: 10px;' id='" + pName + "'></div><div style='clear:both;'></div><div class='content' id='" + pName + "'></div>";
+    return el;
+  },
+  applyStyles: function(el,styles) {
+    for(var i in styles) {
+      if(!styles.hasOwnProperty(i)) continue;
+      el.style[i] = styles[i];
+    }
+  },
+  closest: function(elem, selector) {
+    while (elem && elem !== document) {
+      if (elem[matchKey]) {
+        if (elem[matchKey](selector)) {
+          return elem;
+        } else {
+          elem = elem.parentNode;
+        }
+      }
+      else {
+        return false;
+      }
+    }
+    return false;
+  },
+  insertBasicTopTab: function(tab, newTabs_holder ) {
+    newTabs_holder.firstChild.insertBefore(tab,newTabs_holder.firstChild.firstChild);
+  },
+  getTab: function(span, tabId) {
+    var el = document.createElement('div');
+    el.appendChild(span);
+    el.id = tabId;
+    el.style = el.style || {};
+    this.applyStyles(el,{
+      border: '1px solid #ccc',
+      borderWidth: '1px 0 1px 1px',
+      textAlign: 'center',
+      lineHeight: '30px',
+      borderRadius: '5px',
+      borderBottomRightRadius: 0,
+      borderTopRightRadius: 0,
+      fontWeight: 'bold',
+      cursor: 'pointer'
+    });
+    return el;
+  },
+  getTopTab: function(span, tabId) {
+    var el = document.createElement('div');
+    el.id = tabId;
+    el.appendChild(span);
+    el.style = el.style || {};
+    this.applyStyles(el,{
+      float: 'left',
+      border: '1px solid #ccc',
+      borderWidth: '1px 1px 0px 1px',
+      textAlign: 'center',
+      lineHeight: '30px',
+      borderRadius: '5px',
+      paddingLeft:'5px',
+      paddingRight:'5px',
+      borderBottomRightRadius: 0,
+      borderBottomLeftRadius: 0,
+      fontWeight: 'bold',
+      cursor: 'pointer'
+    });
+    return el;
+  },
+  getTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTopTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTabContent: function() {
+    return this.getIndentedPanel();
+  },
+  getTopTabContent: function() {
+    return this.getTopIndentedPanel();
+  },
+  markTabActive: function(row) {
+    this.applyStyles(row.tab,{
+      opacity: 1,
+      background: 'white'
+    });
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    this.applyStyles(row.tab,{
+      opacity:0.5,
+      background: ''
+    });
+    row.container.style.display = 'none';
+  },
+  addTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  addTopTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  getBlockLink: function() {
+    var link = document.createElement('a');
+    link.style.display = 'block';
+    return link;
+  },
+  getBlockLinkHolder: function() {
+    var el = document.createElement('div');
+    return el;
+  },
+  getLinksHolder: function() {
+    var el = document.createElement('div');
+    return el;
+  },
+  createMediaLink: function(holder,link,media) {
+    holder.appendChild(link);
+    media.style.width='100%';
+    holder.appendChild(media);
+  },
+  createImageLink: function(holder,link,image) {
+    holder.appendChild(link);
+    link.appendChild(image);
+  },
+  getFirstTab: function(holder){
+    return holder.firstChild.firstChild;
+  }
+});
+
+JSONEditor.defaults.themes.bootstrap2 = JSONEditor.AbstractTheme.extend({
+  getRangeInput: function(min, max, step) {
+    // TODO: use bootstrap slider
+    return this._super(min, max, step);
+  },
+  getGridContainer: function() {
+    var el = document.createElement('div');
+    el.className = 'container-fluid';
+    el.style.padding = '4px';
+    return el;
+  },
+  getGridRow: function() {
+    var el = document.createElement('div');
+    el.className = 'row-fluid';
+    return el;
+  },
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.display = 'inline-block';
+    el.style.fontWeight = 'bold';
+    return el;
+  },
+  setGridColumnSize: function(el,size) {
+    el.className = 'span'+size;
+  },
+  getSelectInput: function(options) {
+    var input = this._super(options);
+    input.style.width = 'auto';
+    input.style.maxWidth = '98%';
+    return input;
+  },
+  getFormInputField: function(type) {
+    var el = this._super(type);
+    el.style.width = '98%';
+    return el;
+  },
+  afterInputReady: function(input) {
+    if(input.controlgroup) return;
+    input.controlgroup = this.closest(input,'.control-group');
+    input.controls = this.closest(input,'.controls');
+    if(this.closest(input,'.compact')) {
+      input.controlgroup.className = input.controlgroup.className.replace(/control-group/g,'').replace(/[ ]{2,}/g,' ');
+      input.controls.className = input.controlgroup.className.replace(/controls/g,'').replace(/[ ]{2,}/g,' ');
+      input.style.marginBottom = 0;
+    }
+    if (this.queuedInputErrorText) {
+        var text = this.queuedInputErrorText;
+        delete this.queuedInputErrorText;
+        this.addInputError(input,text);
+    }
+
+    // TODO: use bootstrap slider
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.className = 'well well-small';
+    el.style.padding = '4px';
+    return el;
+  },
+  getInfoButton: function(text) {
+    var icon = document.createElement('span');
+    icon.className = "icon-info-sign pull-right";
+    icon.style.padding = ".25rem";
+    icon.style.position = "relative";
+    icon.style.display = "inline-block";
+
+    var tooltip = document.createElement('span');
+    tooltip.style["font-family"] = "sans-serif";
+    tooltip.style.visibility = "hidden";
+    tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
+    tooltip.style.margin = "0 .25rem";
+    tooltip.style.color = "#FAFAFA";
+    tooltip.style.padding = ".5rem 1rem";
+    tooltip.style["border-radius"] = ".25rem";
+    tooltip.style.width = "25rem";
+    tooltip.style.transform = "translateX(-27rem) translateY(-.5rem)";
+    tooltip.style.position = "absolute";
+    tooltip.innerText = text;
+    icon.onmouseover = function() {
+      tooltip.style.visibility = "visible";
+    };
+    icon.onmouseleave = function() {
+      tooltip.style.visibility = "hidden";
+    };
+
+    icon.appendChild(tooltip);
+
+    return icon;
+  },
+  getFormInputDescription: function(text) {
+    var el = document.createElement('p');
+    el.className = 'help-inline';
+    el.textContent = text;
+    return el;
+  },
+  getFormControl: function(label, input, description, infoText) {
+    var ret = document.createElement('div');
+    ret.className = 'control-group';
+
+    var controls = document.createElement('div');
+    controls.className = 'controls';
+
+    if(label && input.getAttribute('type') === 'checkbox') {
+      ret.appendChild(controls);
+      label.className += ' checkbox';
+      label.appendChild(input);
+      controls.appendChild(label);
+      if(infoText) controls.appendChild(infoText);
+      controls.style.height = '30px';
+    }
+    else {
+      if(label) {
+        label.className += ' control-label';
+        ret.appendChild(label);
+      }
+      if(infoText) controls.appendChild(infoText);
+      controls.appendChild(input);
+      ret.appendChild(controls);
+    }
+
+    if(description) controls.appendChild(description);
+
+    return ret;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.marginLeft = '10px';
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement('div');
+    el.className = 'btn-group';
+    return el;
+  },
+  getButton: function(text, icon, title) {
+    var el =  this._super(text, icon, title);
+    el.className += ' btn btn-default';
+    el.style.backgroundColor = '#f2bfab';
+    el.style.border = '1px solid #ddd';
+    return el;
+  },
+  getTable: function() {
+    var el = document.createElement('table');
+    el.className = 'table table-bordered';
+    el.style.width = 'auto';
+    el.style.maxWidth = 'none';
+    return el;
+  },
+  addInputError: function(input,text) {
+    if(!input.controlgroup) {
+        this.queuedInputErrorText = text;
+        return;
+    }
+    if(!input.controlgroup || !input.controls) return;
+    input.controlgroup.className += ' error';
+    if(!input.errmsg) {
+      input.errmsg = document.createElement('p');
+      input.errmsg.className = 'help-block errormsg';
+      input.controls.appendChild(input.errmsg);
+    }
+    else {
+      input.errmsg.style.display = '';
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if(!input.controlgroup) {
+        delete this.queuedInputErrorText;
+    }
+    if(!input.errmsg) return;
+    input.errmsg.style.display = 'none';
+    input.controlgroup.className = input.controlgroup.className.replace(/\s?error/g,'');
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'tabbable tabs-left';
+    el.innerHTML = "<ul class='nav nav-tabs'  id='" + pName + "'></ul><div class='tab-content well well-small' id='" + pName + "'></div>";
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'tabbable tabs-over';
+    el.innerHTML = "<ul class='nav nav-tabs' id='" + pName + "'></ul><div class='tab-content well well-small'  id='" + pName + "'></div>";
+    return el;
+  },
+  getTab: function(text,tabId) {
+    var el = document.createElement('li');
+    el.className = 'nav-item';
+    var a = document.createElement('a');
+    a.setAttribute('href','#' + tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTopTab: function(text,tabId) {
+    var el = document.createElement('li');
+    el.className = 'nav-item';
+    var a = document.createElement('a');
+    a.setAttribute('href','#' + tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTopTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tab-pane';
+    return el;
+  },
+  getTopTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tab-pane';
+    return el;
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.tab.className += ' active';
+    row.container.className = row.container.className.replace(/\s?active/g,'');
+    row.container.className += ' active';
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.container.className = row.container.className.replace(/\s?active/g,'');
+  },
+  addTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  addTopTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  getProgressBar: function() {
+    var container = document.createElement('div');
+    container.className = 'progress';
+
+    var bar = document.createElement('div');
+    bar.className = 'bar';
+    bar.style.width = '0%';
+    container.appendChild(bar);
+
+    return container;
+  },
+  updateProgressBar: function(progressBar, progress) {
+    if (!progressBar) return;
+
+    progressBar.firstChild.style.width = progress + "%";
+  },
+  updateProgressBarUnknown: function(progressBar) {
+    if (!progressBar) return;
+
+    progressBar.className = 'progress progress-striped active';
+    progressBar.firstChild.style.width = '100%';
+  }
+});
+
+JSONEditor.defaults.themes.bootstrap3 = JSONEditor.AbstractTheme.extend({
+  getSelectInput: function(options) {
+    var el = this._super(options);
+    el.className += 'form-control';
+    //el.style.width = 'auto';
+    return el;
+  },
+  getGridContainer: function() {
+           var el = document.createElement('div');
+           el.className = 'container-fluid';
+           el.style.padding = '4px';
+           return el;
+  },
+  getGridRow: function() {
+           var el = document.createElement('div');
+           el.className = 'row-fluid';
+           el.style.padding = '4px';
+           return el;
+  },
+  setGridColumnSize: function(el,size) {
+    el.className = 'col-md-'+size;
+  },
+  afterInputReady: function(input) {
+    if(input.controlgroup) return;
+    input.controlgroup = this.closest(input,'.form-group');
+    if(this.closest(input,'.compact')) {
+      input.controlgroup.style.marginBottom = 0;
+    }
+    if (this.queuedInputErrorText) {
+        var text = this.queuedInputErrorText;
+        delete this.queuedInputErrorText;
+        this.addInputError(input,text);
+    }
+
+    // TODO: use bootstrap slider
+  },
+  getRangeInput: function(min, max, step) {
+    // TODO: use better slider
+    return this._super(min, max, step);
+  },
+  getFormInputField: function(type) {
+    var el = this._super(type);
+    if(type !== 'checkbox') {
+      el.className += 'form-control';
+    }
+    return el;
+  },
+  getFormControl: function(label, input, description, infoText) {
+    var group = document.createElement('div');
+
+    if(label && input.type === 'checkbox') {
+      group.className += ' checkbox';
+      label.appendChild(input);
+      label.style.fontSize = '12px';
+      group.style.marginTop = '0';
+      if(infoText) group.appendChild(infoText);
+      group.appendChild(label);
+      input.style.position = 'relative';
+      input.style.cssFloat = 'left';
+    }
+    else {
+      group.className += ' form-group';
+      if(label) {
+        label.className += ' control-label';
+        group.appendChild(label);
+      }
+
+      if(infoText) group.appendChild(infoText);
+      group.appendChild(input);
+    }
+
+    if(description) group.appendChild(description);
+
+    return group;
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.className = 'well well-sm';
+    el.style.padding = '4px';
+    return el;
+  },
+  getInfoButton: function(text) {
+    var icon = document.createElement('span');
+    icon.className = "glyphicon glyphicon-info-sign pull-right";
+    icon.style.padding = ".25rem";
+    icon.style.position = "relative";
+    icon.style.display = "inline-block";
+
+    var tooltip = document.createElement('span');
+    tooltip.style["font-family"] = "sans-serif";
+    tooltip.style.visibility = "hidden";
+    tooltip.style["background-color"] = "rgba(50, 50, 50, .75)";
+    tooltip.style.margin = "0 .25rem";
+    tooltip.style.color = "#FAFAFA";
+    tooltip.style.padding = ".5rem 1rem";
+    tooltip.style["border-radius"] = ".25rem";
+    tooltip.style.width = "25rem";
+    tooltip.style.transform = "translateX(-27rem) translateY(-.5rem)";
+    tooltip.style.position = "absolute";
+    tooltip.innerText = text;
+    icon.onmouseover = function() {
+      tooltip.style.visibility = "visible";
+    };
+    icon.onmouseleave = function() {
+      tooltip.style.visibility = "hidden";
+    };
+
+    icon.appendChild(tooltip);
+
+    return icon;
+  },
+  getFormInputDescription: function(text) {
+    var el = document.createElement('p');
+    el.className = 'help-block';
+    el.innerHTML = text;
+    return el;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.marginLeft = '5px';
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement('div');
+    el.className = 'btn-group';
+    return el;
+  },
+  getButton: function(text, icon, title) {
+       var el =  this._super(text, icon, title);
+    el.className += ' btn btn-default';
+       el.style.backgroundColor = '#f2bfab';
+       el.style.border = '1px solid #ddd';
+       return el;
+  },
+  getTable: function() {
+    var el = document.createElement('table');
+    el.className = 'table table-bordered';
+    el.style.width = 'auto';
+    el.style.maxWidth = 'none';
+    return el;
+  },
+
+  addInputError: function(input,text) {
+    if(!input.controlgroup) {
+        this.queuedInputErrorText = text;
+        return;
+    }
+    input.controlgroup.className = input.controlgroup.className.replace(/\s?has-error/g,'');
+    input.controlgroup.className += ' has-error';
+    if(!input.errmsg) {
+      input.errmsg = document.createElement('p');
+      input.errmsg.className = 'help-block errormsg';
+      input.controlgroup.appendChild(input.errmsg);
+    }
+    else {
+      input.errmsg.style.display = '';
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if(!input.controlgroup) {
+        delete this.queuedInputErrorText;
+    }
+    if(!input.errmsg) return;
+    input.errmsg.style.display = 'none';
+    input.controlgroup.className = input.controlgroup.className.replace(/\s?has-error/g,'');
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = "<div class='list-group pull-left' id='" + pName + "'></div><div class='col-sm-10 pull-left' id='" + pName + "'></div>";
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = "<ul class='nav nav-tabs' style='padding: 4px;' id='" + pName + "'></ul><div class='tab-content' style='overflow:visible;' id='" + pName + "'></div>";
+    return el;
+  },
+  getTab: function(text, tabId) {
+    var el = document.createElement('a');
+    el.className = 'list-group-item';
+    el.setAttribute('href','#'+tabId);
+    el.appendChild(text);
+    return el;
+  },
+  getTopTab: function(text, tabId) {
+    var el = document.createElement('li');
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.tab.className += ' active';
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.container.style.display = 'none';
+  },
+  getProgressBar: function() {
+    var min = 0, max = 100, start = 0;
+
+    var container = document.createElement('div');
+    container.className = 'progress';
+
+    var bar = document.createElement('div');
+    bar.className = 'progress-bar';
+    bar.setAttribute('role', 'progressbar');
+    bar.setAttribute('aria-valuenow', start);
+    bar.setAttribute('aria-valuemin', min);
+    bar.setAttribute('aria-valuenax', max);
+    bar.innerHTML = start + "%";
+    container.appendChild(bar);
+
+    return container;
+  },
+  updateProgressBar: function(progressBar, progress) {
+    if (!progressBar) return;
+
+    var bar = progressBar.firstChild;
+    var percentage = progress + "%";
+    bar.setAttribute('aria-valuenow', progress);
+    bar.style.width = percentage;
+    bar.innerHTML = percentage;
+  },
+  updateProgressBarUnknown: function(progressBar) {
+    if (!progressBar) return;
+
+    var bar = progressBar.firstChild;
+    progressBar.className = 'progress progress-striped active';
+    bar.removeAttribute('aria-valuenow');
+    bar.style.width = '100%';
+    bar.innerHTML = '';
+  }
+});
+
+JSONEditor.defaults.themes.bootstrap4 = JSONEditor.AbstractTheme.extend({
+  getSelectInput: function(options) {
+    var el = this._super(options);
+    el.className += "form-control";
+    //el.style.width = 'auto';
+    return el;
+  },
+  setGridColumnSize: function(el, size) {
+    el.className = "col-md-" + size;
+  },
+  afterInputReady: function(input) {
+    if (input.controlgroup) return;
+    input.controlgroup = this.closest(input, ".form-group");
+    if (this.closest(input, ".compact")) {
+      input.controlgroup.style.marginBottom = 0;
+    }
+
+    // TODO: use bootstrap slider
+  },
+  getTextareaInput: function() {
+    var el = document.createElement("textarea");
+    el.className = "form-control";
+    return el;
+  },
+  getRangeInput: function(min, max, step) {
+    // TODO: use better slider
+    return this._super(min, max, step);
+  },
+  getFormInputField: function(type) {
+    var el = this._super(type);
+    if (type !== "checkbox") {
+      el.className += "form-control";
+    }
+    return el;
+  },
+  getFormControl: function(label, input, description) {
+    var group = document.createElement("div");
+
+    if (label && input.type === "checkbox") {
+      group.className += " checkbox";
+      label.appendChild(input);
+      label.style.fontSize = "12px";
+      group.style.marginTop = "0";
+      group.appendChild(label);
+      input.style.position = "relative";
+      input.style.cssFloat = "left";
+    } else {
+      group.className += " form-group";
+      if (label) {
+        label.className += " form-control-label";
+        group.appendChild(label);
+      }
+      group.appendChild(input);
+    }
+
+    if (description) group.appendChild(description);
+
+    return group;
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement("div");
+    el.className = "card card-body bg-light";
+    return el;
+  },
+  getFormInputDescription: function(text) {
+    var el = document.createElement("p");
+    el.className = "form-text";
+    el.innerHTML = text;
+    return el;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.marginLeft = "10px";
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement("div");
+    el.className = "btn-group";
+    return el;
+  },
+  getButton: function(text, icon, title) {
+    var el = this._super(text, icon, title);
+    el.className += "btn btn-secondary";
+    return el;
+  },
+  getTable: function() {
+    var el = document.createElement("table");
+    el.className = "table-bordered table-sm";
+    el.style.width = "auto";
+    el.style.maxWidth = "none";
+    return el;
+  },
+
+  addInputError: function(input, text) {
+    if (!input.controlgroup) return;
+    input.controlgroup.className += " has-error";
+    if (!input.errmsg) {
+      input.errmsg = document.createElement("p");
+      input.errmsg.className = "form-text errormsg";
+      input.controlgroup.appendChild(input.errmsg);
+    } else {
+      input.errmsg.style.display = "";
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if (!input.errmsg) return;
+    input.errmsg.style.display = "none";
+    input.controlgroup.className = input.controlgroup.className.replace(
+      /\s?has-error/g,
+      ""
+    );
+  },
+  getTabHolder: function(propertyName) {
+    var el = document.createElement("div");
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    el.innerHTML =
+      "<ul class='nav flex-column nav-pills col-md-2' style='padding: 0px;' id='" + pName + "'></ul><div class='tab-content col-md-10' style='padding:5px;' id='" + pName + "'></div>";
+el.className = "row";
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = "<ul class='nav nav-tabs' id='" + pName + "'></ul><div class='card-body' id='" + pName + "'></div>";
+    return el;
+  },
+  getTab: function(text,tabId) {
+    var liel = document.createElement('li');
+    liel.className = 'nav-item';
+    var ael = document.createElement("a");
+    ael.className = "nav-link";
+    ael.setAttribute("style",'padding:10px;');
+    ael.setAttribute("href", "#" + tabId);
+    ael.appendChild(text);
+    liel.appendChild(ael);
+    return liel;
+  },
+  getTopTab: function(text, tabId) {
+    var el = document.createElement('li');
+    el.className = 'nav-item';
+    var a = document.createElement('a');
+    a.className = 'nav-link';
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  markTabActive: function(row) {
+    var el = row.tab.firstChild;
+    el.className = el.className.replace(/\s?active/g,'');
+    el.className += " active";
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    var el = row.tab.firstChild;
+    el.className = el.className.replace(/\s?active/g,'');
+    row.container.style.display = 'none';
+  },
+  getProgressBar: function() {
+    var min = 0,
+      max = 100,
+      start = 0;
+
+    var container = document.createElement("div");
+    container.className = "progress";
+
+    var bar = document.createElement("div");
+    bar.className = "progress-bar";
+    bar.setAttribute("role", "progressbar");
+    bar.setAttribute("aria-valuenow", start);
+    bar.setAttribute("aria-valuemin", min);
+    bar.setAttribute("aria-valuenax", max);
+    bar.innerHTML = start + "%";
+    container.appendChild(bar);
+
+    return container;
+  },
+  updateProgressBar: function(progressBar, progress) {
+    if (!progressBar) return;
+
+    var bar = progressBar.firstChild;
+    var percentage = progress + "%";
+    bar.setAttribute("aria-valuenow", progress);
+    bar.style.width = percentage;
+    bar.innerHTML = percentage;
+  },
+  updateProgressBarUnknown: function(progressBar) {
+    if (!progressBar) return;
+
+    var bar = progressBar.firstChild;
+    progressBar.className = "progress progress-striped active";
+    bar.removeAttribute("aria-valuenow");
+    bar.style.width = "100%";
+    bar.innerHTML = "";
+  }
+});
+
+// Base Foundation theme
+JSONEditor.defaults.themes.foundation = JSONEditor.AbstractTheme.extend({
+  getChildEditorHolder: function() {
+    var el = document.createElement('div');
+    el.style.marginBottom = '15px';
+    return el;
+  },
+  getSelectInput: function(options) {
+    var el = this._super(options);
+    el.style.minWidth = 'none';
+    el.style.padding = '5px';
+    el.style.marginTop = '3px';
+    return el;
+  },
+  getSwitcher: function(options) {
+    var el = this._super(options);
+    el.style.paddingRight = '8px';
+    return el;
+  },
+  afterInputReady: function(input) {
+    if(input.group) return;
+    if(this.closest(input,'.compact')) {
+      input.style.marginBottom = 0;
+    }
+    input.group = this.closest(input,'.form-control');
+    if (this.queuedInputErrorText) {
+        var text = this.queuedInputErrorText;
+        delete this.queuedInputErrorText;
+        this.addInputError(input,text);
+    }
+  },
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.display = 'inline-block';
+    return el;
+  },
+  getFormInputField: function(type) {
+    var el = this._super(type);
+    el.style.width = '100%';
+    el.style.marginBottom = type==='checkbox'? '0' : '12px';
+    return el;
+  },
+  getFormInputDescription: function(text) {
+    var el = document.createElement('p');
+    el.textContent = text;
+    el.style.marginTop = '-10px';
+    el.style.fontStyle = 'italic';
+    return el;
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.className = 'panel';
+    el.style.paddingBottom = 0;
+    return el;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.display = 'inline-block';
+    el.style.marginLeft = '10px';
+    el.style.verticalAlign = 'middle';
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement('div');
+    el.className = 'button-group';
+    return el;
+  },
+  getButton: function(text, icon, title) {
+    var el = this._super(text, icon, title);
+    el.className += ' small button';
+    return el;
+  },
+  addInputError: function(input,text) {
+    if(!input.group) {
+        this.queuedInputErrorText = text;
+        return;
+    }
+    input.group.className += ' error';
+
+    if(!input.errmsg) {
+      input.insertAdjacentHTML('afterend','<small class="error"></small>');
+      input.errmsg = input.parentNode.getElementsByClassName('error')[0];
+    }
+    else {
+      input.errmsg.style.display = '';
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if(!input.group) {
+        delete this.queuedInputErrorText;
+    }
+    if(!input.errmsg) return;
+    input.group.className = input.group.className.replace(/ error/g,'');
+    input.errmsg.style.display = 'none';
+  },
+  getProgressBar: function() {
+    var progressBar = document.createElement('div');
+    progressBar.className = 'progress';
+
+    var meter = document.createElement('span');
+    meter.className = 'meter';
+    meter.style.width = '0%';
+    progressBar.appendChild(meter);
+    return progressBar;
+  },
+  updateProgressBar: function(progressBar, progress) {
+    if (!progressBar) return;
+    progressBar.firstChild.style.width = progress + '%';
+  },
+  updateProgressBarUnknown: function(progressBar) {
+    if (!progressBar) return;
+    progressBar.firstChild.style.width = '100%';
+  }
+});
+
+// Foundation 3 Specific Theme
+JSONEditor.defaults.themes.foundation3 = JSONEditor.defaults.themes.foundation.extend({
+  getHeaderButtonHolder: function() {
+    var el = this._super();
+    el.style.fontSize = '.6em';
+    return el;
+  },
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.fontWeight = 'bold';
+    return el;
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'row';
+    el.innerHTML = '<dl class="tabs vertical two columns" id="' + pName + '"></dl><div class="tabs-content ten columns" id="' + pName + '"></div>';
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'row';
+    el.innerHTML = '<dl class="tabs horizontal" style="padding-left: 10px; margin-left: 10px;" id="' + pName + '"></dl><div class="tabs-content twelve columns" style="padding: 10px; margin-left: 10px;" id="' + pName + '"></div>';
+    return el;
+  },
+  setGridColumnSize: function(el,size) {
+    var sizes = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve'];
+    el.className = 'columns '+sizes[size];
+  },
+  getTab: function(text, tabId) {
+    var el = document.createElement('dd');
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTopTab: function(text, tabId) {
+    var el = document.createElement('dd');
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTopTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'content active';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  getTopTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'content active';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.tab.className += ' active';
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.container.style.display = 'none';
+  },
+  addTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  addTopTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  }
+});
+
+// Foundation 4 Specific Theme
+JSONEditor.defaults.themes.foundation4 = JSONEditor.defaults.themes.foundation.extend({
+  getHeaderButtonHolder: function() {
+    var el = this._super();
+    el.style.fontSize = '.6em';
+    return el;
+  },
+  setGridColumnSize: function(el,size) {
+    el.className = 'columns large-'+size;
+  },
+  getFormInputDescription: function(text) {
+    var el = this._super(text);
+    el.style.fontSize = '.8rem';
+    return el;
+  },
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.fontWeight = 'bold';
+    return el;
+  }
+});
+
+// Foundation 5 Specific Theme
+JSONEditor.defaults.themes.foundation5 = JSONEditor.defaults.themes.foundation.extend({
+  getFormInputDescription: function(text) {
+    var el = this._super(text);
+    el.style.fontSize = '.8rem';
+    return el;
+  },
+  setGridColumnSize: function(el,size) {
+    el.className = 'columns medium-'+size;
+  },
+  getButton: function(text, icon, title) {
+    var el = this._super(text,icon,title);
+    el.className = el.className.replace(/\s*small/g,'') + ' tiny';
+    return el;
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.innerHTML = '<dl class="tabs vertical" id="' + pName + '"></dl><div class="tabs-content vertical" id="' + pName + '"></div>';
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'row';
+    el.innerHTML = '<dl class="tabs horizontal" style="padding-left: 10px;" id="' + pName + '"></dl><div class="tabs-content horizontal" style="padding: 10px;" id="' + pName + '"></div>';
+    return el;
+  },
+  getTab: function(text, tabId) {
+    var el = document.createElement('dd');
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTopTab: function(text, tabId) {
+    var el = document.createElement('dd');
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTopTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1];
+  },
+  getTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tab-content active';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  getTopTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tab-content active';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.tab.className += ' active';
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?active/g,'');
+    row.container.style.display = 'none';
+  },
+  addTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  },
+  addTopTab: function(holder, tab) {
+    holder.children[0].appendChild(tab);
+  }
+
+});
+
+JSONEditor.defaults.themes.foundation6 = JSONEditor.defaults.themes.foundation5.extend({
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.className = 'callout secondary';
+    el.className.style = 'padding-left: 10px; margin-left: 10px;';
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement('div');
+    el.className = 'button-group tiny';
+    el.style.marginBottom = 0;
+    return el;
+  },
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.display = 'block';
+    return el;
+  },
+  getFormControl: function(label, input, description, infoText) {
+    var el = document.createElement('div');
+    el.className = 'form-control';
+    if(label) el.appendChild(label);
+    if(input.type === 'checkbox') {
+      label.insertBefore(input,label.firstChild);
+    }
+    else if (label) {
+      if(infoText) label.appendChild(infoText);
+      label.appendChild(input);
+    } else {
+      if(infoText) el.appendChild(infoText);
+      el.appendChild(input);
+    }
+
+    if(description) label.appendChild(description);
+    return el;
+  },
+  addInputError: function(input,text) {
+    if(!input.group) return;
+    input.group.className += ' error';
+
+    if(!input.errmsg) {
+      var errorEl = document.createElement('span');
+      errorEl.className = 'form-error is-visible';
+      input.group.getElementsByTagName('label')[0].appendChild(errorEl);
+
+      input.className = input.className + ' is-invalid-input';
+
+      input.errmsg = errorEl;
+    }
+    else {
+      input.errmsg.style.display = '';
+      input.className = '';
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if(!input.errmsg) return;
+    input.className = input.className.replace(/ is-invalid-input/g,'');
+    if(input.errmsg.parentNode) {
+      input.errmsg.parentNode.removeChild(input.errmsg);
+    }
+  },
+  getTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'grid-x';
+    el.innerHTML = '<div class="medium-2 cell" style="float: left;"><ul class="vertical tabs" data-tabs id="' + pName + '"></ul></div><div class="medium-10 cell" style="float: left;"><div class="tabs-content" data-tabs-content="'+pName+'"></div></div>';
+    return el;
+  },
+  getTopTabHolder: function(propertyName) {
+    var pName = (typeof propertyName === 'undefined')? "" : propertyName;
+    var el = document.createElement('div');
+    el.className = 'grid-y';
+    el.innerHTML = '<div className="cell"><ul class="tabs" data-tabs id="' + pName + '"></ul><div class="tabs-content" data-tabs-content="' + pName + '"></div></div>';
+    return el;
+
+
+  },
+  insertBasicTopTab: function(tab, newTabs_holder ) {
+    newTabs_holder.firstChild.firstChild.insertBefore(tab,newTabs_holder.firstChild.firstChild.firstChild);
+  },
+  getTab: function(text, tabId) {
+    var el = document.createElement('li');
+    el.className = 'tabs-title';
+    var a = document.createElement('a');
+    a.setAttribute('href','#'+tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTopTab: function(text, tabId) {
+    var el = document.createElement('li');
+    el.className = 'tabs-title';
+    var a = document.createElement('a');
+    a.setAttribute('href','#' + tabId);
+    a.appendChild(text);
+    el.appendChild(a);
+    return el;
+  },
+  getTabContentHolder: function(tab_holder) {
+    return tab_holder.children[1].firstChild;
+  },
+  getTopTabContentHolder: function(tab_holder) {
+    return tab_holder.firstChild.children[1];
+  },
+  getTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tabs-panel';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  getTopTabContent: function() {
+    var el = document.createElement('div');
+    el.className = 'tabs-panel';
+    el.style.paddingLeft = '5px';
+    return el;
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?is-active/g,'');
+    row.tab.className += ' is-active';
+    row.tab.firstChild.setAttribute('aria-selected', 'true');
+
+    row.container.className  = row.container.className.replace(/\s?is-active/g,'');
+    row.container.className += ' is-active';
+    row.container.setAttribute('aria-selected', 'true');
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?is-active/g,'');
+    row.tab.firstChild.removeAttribute('aria-selected');
+
+    row.container.className = row.container.className.replace(/\s?is-active/g,'');
+    row.container.removeAttribute('aria-selected');
+  },
+  addTab: function(holder, tab) {
+    holder.children[0].firstChild.appendChild(tab);
+  },
+  addTopTab: function(holder, tab) {
+    holder.firstChild.children[0].appendChild(tab);
+  },
+  getFirstTab: function(holder){
+    return holder.firstChild.firstChild.firstChild;
+  }
+});
+
+JSONEditor.defaults.themes.html = JSONEditor.AbstractTheme.extend({
+  getFormInputLabel: function(text) {
+    var el = this._super(text);
+    el.style.display = 'block';
+    el.style.marginBottom = '3px';
+    el.style.fontWeight = 'bold';
+    return el;
+  },
+  getFormInputDescription: function(text) {
+    var el = this._super(text);
+    el.style.fontSize = '.8em';
+    el.style.margin = 0;
+    el.style.display = 'inline-block';
+    el.style.fontStyle = 'italic';
+    return el;
+  },
+  getIndentedPanel: function() {
+    var el = this._super();
+    el.style.border = '1px solid #ddd';
+    el.style.padding = '5px';
+    el.style.margin = '10px';
+    el.style.borderRadius = '3px';
+    return el;
+  },
+  getTopIndentedPanel: function() {
+    return this.getIndentedPanel();
+  },
+  getChildEditorHolder: function() {
+    var el = this._super();
+    el.style.marginBottom = '8px';
+    return el;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.display = 'inline-block';
+    el.style.marginLeft = '10px';
+    el.style.fontSize = '.8em';
+    el.style.verticalAlign = 'middle';
+    return el;
+  },
+  getTable: function() {
+    var el = this._super();
+    el.style.borderBottom = '1px solid #ccc';
+    el.style.marginBottom = '5px';
+    return el;
+  },
+  addInputError: function(input, text) {
+    input.style.borderColor = 'red';
+    
+    if(!input.errmsg) {
+      var group = this.closest(input,'.form-control');
+      input.errmsg = document.createElement('div');
+      input.errmsg.setAttribute('class','errmsg');
+      input.errmsg.style = input.errmsg.style || {};
+      input.errmsg.style.color = 'red';
+      group.appendChild(input.errmsg);
+    }
+    else {
+      input.errmsg.style.display = 'block';
+    }
+    
+    input.errmsg.innerHTML = '';
+    input.errmsg.appendChild(document.createTextNode(text));
+  },
+  removeInputError: function(input) {
+    input.style.borderColor = '';
+    if(input.errmsg) input.errmsg.style.display = 'none';
+  },
+  getProgressBar: function() {
+    var max = 100, start = 0;
+
+    var progressBar = document.createElement('progress');
+    progressBar.setAttribute('max', max);
+    progressBar.setAttribute('value', start);
+    return progressBar;
+  },
+  updateProgressBar: function(progressBar, progress) {
+    if (!progressBar) return;
+    progressBar.setAttribute('value', progress);
+  },
+  updateProgressBarUnknown: function(progressBar) {
+    if (!progressBar) return;
+    progressBar.removeAttribute('value');
+  }
+});
+
+JSONEditor.defaults.themes.jqueryui = JSONEditor.AbstractTheme.extend({
+  getTable: function() {
+    var el = this._super();
+    el.setAttribute('cellpadding',5);
+    el.setAttribute('cellspacing',0);
+    return el;
+  },
+  getTableHeaderCell: function(text) {
+    var el = this._super(text);
+    el.className = 'ui-state-active';
+    el.style.fontWeight = 'bold';
+    return el;
+  },
+  getTableCell: function() {
+    var el = this._super();
+    el.className = 'ui-widget-content';
+    return el;
+  },
+  getHeaderButtonHolder: function() {
+    var el = this.getButtonHolder();
+    el.style.marginLeft = '10px';
+    el.style.fontSize = '.6em';
+    el.style.display = 'inline-block';
+    return el;
+  },
+  getFormInputDescription: function(text) {
+    var el = this.getDescription(text);
+    el.style.marginLeft = '10px';
+    el.style.display = 'inline-block';
+    return el;
+  },
+  getFormControl: function(label, input, description, infoText) {
+    var el = this._super(label,input,description, infoText);
+    if(input.type === 'checkbox') {
+      el.style.lineHeight = '25px';
+
+      el.style.padding = '3px 0';
+    }
+    else {
+      el.style.padding = '4px';
+    }
+    return el;
+  },
+  getDescription: function(text) {
+    var el = document.createElement('span');
+    el.style.fontSize = '.8em';
+    el.style.fontStyle = 'italic';
+    el.textContent = text;
+    return el;
+  },
+  getButtonHolder: function() {
+    var el = document.createElement('div');
+    el.className = 'ui-buttonset';
+    el.style.fontSize = '.7em';
+    return el;
+  },
+  getFormInputLabel: function(text) {
+    var el = document.createElement('label');
+    el.style.fontWeight = 'bold';
+    el.style.display = 'block';
+    el.textContent = text;
+    return el;
+  },
+  getButton: function(text, icon, title) {
+    var button = document.createElement("button");
+    button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
+
+    // Icon only
+    if(icon && !text) {
+      button.className += ' ui-button-icon-only';
+      icon.className += ' ui-button-icon-primary ui-icon-primary';
+      button.appendChild(icon);
+    }
+    // Icon and Text
+    else if(icon) {
+      button.className += ' ui-button-text-icon-primary';
+      icon.className += ' ui-button-icon-primary ui-icon-primary';
+      button.appendChild(icon);
+    }
+    // Text only
+    else {
+      button.className += ' ui-button-text-only';
+    }
+
+    var el = document.createElement('span');
+    el.className = 'ui-button-text';
+    el.textContent = text||title||".";
+    button.appendChild(el);
+
+    button.setAttribute('title',title);
+
+    return button;
+  },
+  setButtonText: function(button,text, icon, title) {
+    button.innerHTML = '';
+    button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
+
+    // Icon only
+    if(icon && !text) {
+      button.className += ' ui-button-icon-only';
+      icon.className += ' ui-button-icon-primary ui-icon-primary';
+      button.appendChild(icon);
+    }
+    // Icon and Text
+    else if(icon) {
+      button.className += ' ui-button-text-icon-primary';
+      icon.className += ' ui-button-icon-primary ui-icon-primary';
+      button.appendChild(icon);
+    }
+    // Text only
+    else {
+      button.className += ' ui-button-text-only';
+    }
+
+    var el = document.createElement('span');
+    el.className = 'ui-button-text';
+    el.textContent = text||title||".";
+    button.appendChild(el);
+
+    button.setAttribute('title',title);
+  },
+  getIndentedPanel: function() {
+    var el = document.createElement('div');
+    el.className = 'ui-widget-content ui-corner-all';
+    el.style.padding = '1em 1.4em';
+    el.style.marginBottom = '20px';
+    return el;
+  },
+  afterInputReady: function(input) {
+    if(input.controls) return;
+    input.controls = this.closest(input,'.form-control');
+    if (this.queuedInputErrorText) {
+        var text = this.queuedInputErrorText;
+        delete this.queuedInputErrorText;
+        this.addInputError(input,text);
+    }
+  },
+  addInputError: function(input,text) {
+    if(!input.controls) {
+        this.queuedInputErrorText = text;
+        return;
+    }
+    if(!input.errmsg) {
+      input.errmsg = document.createElement('div');
+      input.errmsg.className = 'ui-state-error';
+      input.controls.appendChild(input.errmsg);
+    }
+    else {
+      input.errmsg.style.display = '';
+    }
+
+    input.errmsg.textContent = text;
+  },
+  removeInputError: function(input) {
+    if(!input.controls) {
+        delete this.queuedInputErrorText;
+    }
+    if(!input.errmsg) return;
+    input.errmsg.style.display = 'none';
+  },
+  markTabActive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?ui-widget-header/g,'').replace(/\s?ui-state-active/g,'')+' ui-state-active';
+    row.container.style.display = '';
+  },
+  markTabInactive: function(row) {
+    row.tab.className = row.tab.className.replace(/\s?ui-state-active/g,'').replace(/\s?ui-widget-header/g,'')+' ui-widget-header';
+    row.container.style.display = 'none';
+  }
+});
+
+JSONEditor.defaults.themes.barebones = JSONEditor.AbstractTheme.extend({
+    getFormInputLabel: function (text) {
+        var el = this._super(text);
+        return el;
+    },
+    getFormInputDescription: function (text) {
+        var el = this._super(text);
+        return el;
+    },
+    getIndentedPanel: function () {
+        var el = this._super();
+        return el;
+    },
+    getChildEditorHolder: function () {
+        var el = this._super();
+        return el;
+    },
+    getHeaderButtonHolder: function () {
+        var el = this.getButtonHolder();
+        return el;
+    },
+    getTable: function () {
+        var el = this._super();
+        return el;
+    },
+    addInputError: function (input, text) {
+        if (!input.errmsg) {
+            var group = this.closest(input, '.form-control');
+            input.errmsg = document.createElement('div');
+            input.errmsg.setAttribute('class', 'errmsg');
+            group.appendChild(input.errmsg);
+        }
+        else {
+            input.errmsg.style.display = 'block';
+        }
+
+        input.errmsg.innerHTML = '';
+        input.errmsg.appendChild(document.createTextNode(text));
+    },
+    removeInputError: function (input) {
+        input.style.borderColor = '';
+        if (input.errmsg) input.errmsg.style.display = 'none';
+    },
+    getProgressBar: function () {
+        var max = 100, start = 0;
+
+        var progressBar = document.createElement('progress');
+        progressBar.setAttribute('max', max);
+        progressBar.setAttribute('value', start);
+        return progressBar;
+    },
+    updateProgressBar: function (progressBar, progress) {
+        if (!progressBar) return;
+        progressBar.setAttribute('value', progress);
+    },
+    updateProgressBarUnknown: function (progressBar) {
+        if (!progressBar) return;
+        progressBar.removeAttribute('value');
+    }
+});
+
+JSONEditor.defaults.themes.materialize = JSONEditor.AbstractTheme.extend({
+
+    /**
+     * Applies grid size to specified element.
+     * 
+     * @param {HTMLElement} el The DOM element to have specified size applied.
+     * @param {int} size The grid column size.
+     * @see http://materializecss.com/grid.html
+     */
+    setGridColumnSize: function(el, size) {
+        el.className = 'col s' + size;
+    },
+
+    /**
+     * Gets a wrapped button element for a header.
+     * 
+     * @returns {HTMLElement} The wrapped button element.
+     */
+    getHeaderButtonHolder: function() {
+        return this.getButtonHolder();
+    },
+
+    /**
+     * Gets a wrapped button element.
+     * 
+     * @returns {HTMLElement} The wrapped button element.
+     */
+    getButtonHolder: function() {
+        return document.createElement('span');
+    },
+
+    /**
+     * Gets a single button element.
+     * 
+     * @param {string} text The button text.
+     * @param {HTMLElement} icon The icon object.
+     * @param {string} title The button title.
+     * @returns {HTMLElement} The button object.
+     * @see http://materializecss.com/buttons.html
+     */
+    getButton: function(text, icon, title) {
+
+        // Prepare icon.
+        if (text) {
+            icon.className += ' left';
+            icon.style.marginRight = '5px';
+        }
+
+        // Create and return button.
+        var el = this._super(text, icon, title);
+        el.className = 'waves-effect waves-light btn';
+        el.style.fontSize = '0.75rem';
+        el.style.height = '20px';
+        el.style.lineHeight = '20px';
+        el.style.marginLeft = '4px';
+        el.style.padding = '0 0.5rem';
+        return el;
+
+    },
+
+    /**
+     * Gets a form control object consisiting of several sub objects.
+     * 
+     * @param {HTMLElement} label The label element.
+     * @param {HTMLElement} input The input element.
+     * @param {string} description The element description.
+     * @param {string} infoText The element information text.
+     * @returns {HTMLElement} The assembled DOM element.
+     * @see http://materializecss.com/forms.html
+     */
+    getFormControl: function(label, input, description, infoText) {
+
+        var ctrl,
+            type = input.type;
+
+        // Checkboxes get wrapped in p elements.
+        if (type && type === 'checkbox') {
+
+            ctrl = document.createElement('p');
+            ctrl.appendChild(input);
+            if (label) {
+                label.setAttribute('for', input.id);
+                ctrl.appendChild(label);
+            }
+            return ctrl;
+
+        }
+
+        // Anything else gets wrapped in divs.
+        ctrl = this._super(label, input, description, infoText);
+
+        // Not .input-field for select wrappers.
+        if (!type || !type.startsWith('select'))
+            ctrl.className = 'input-field';
+
+        // Color needs special attention.
+        if (type && type === 'color') {
+            input.style.height = '3rem';
+            input.style.width = '100%';
+            input.style.margin = '5px 0 20px 0';
+            input.style.padding = '3px';
+
+            if (label) {
+                label.style.transform = 'translateY(-14px) scale(0.8)';
+                label.style['-webkit-transform'] = 'translateY(-14px) scale(0.8)';
+                label.style['-webkit-transform-origin'] = '0 0';
+                label.style['transform-origin'] = '0 0';
+            }
+        }
+
+        return ctrl;
+
+    },
+
+    getDescription: function(text) {
+        var el = document.createElement('div');
+        el.className = 'grey-text';
+        el.style.marginTop = '-15px';
+        el.innerHTML = text;
+        return el;
+    },
+
+    /**
+     * Gets a header element.
+     * 
+     * @param {string|HTMLElement} text The header text or element.
+     * @returns {HTMLElement} The header element.
+     */
+    getHeader: function(text) {
+
+        var el = document.createElement('h5');
+
+        if (typeof text === 'string') {
+          el.textContent = text;
+        } else {
+          el.appendChild(text);
+        }
+    
+        return el;
+
+    },
+
+    getChildEditorHolder: function() {
+
+        var el = document.createElement('div');
+        el.marginBottom = '10px';
+        return el;
+
+    },
+
+    getIndentedPanel: function() {
+        var el = document.createElement("div");
+        el.className = "card-panel";
+        return el;
+    },
+
+    getTable: function() {
+
+        var el = document.createElement('table');
+        el.className = 'striped bordered';
+        el.style.marginBottom = '10px';
+        return el;
+
+    },
+
+    getTableRow: function() {
+        return document.createElement('tr');
+    },
+
+    getTableHead: function() {
+        return document.createElement('thead');
+    },
+
+    getTableBody: function() {
+        return document.createElement('tbody');
+    },
+
+    getTableHeaderCell: function(text) {
+
+        var el = document.createElement('th');
+        el.textContent = text;
+        return el;
+
+    },
+
+    getTableCell: function() {
+
+        var el = document.createElement('td');
+        return el;
+
+    },
+
+    /**
+     * Gets the tab holder element.
+     * 
+     * @returns {HTMLElement} The tab holder component.
+     * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
+     */
+    getTabHolder: function() {
+
+        var html = [
+            '<div class="col s2">',
+            '   <ul class="tabs" style="height: auto; margin-top: 0.82rem; -ms-flex-direction: column; -webkit-flex-direction: column; flex-direction: column; display: -webkit-flex; display: flex;">',
+            '   </ul>',
+            '</div>',
+            '<div class="col s10">',
+            '<div>'
+        ].join("\n");
+
+        var el = document.createElement('div');
+        el.className = 'row card-panel';
+        el.innerHTML = html;
+        return el;
+
+    },
+
+    /**
+     * Add specified tab to specified holder element.
+     * 
+     * @param {HTMLElement} holder The tab holder element.
+     * @param {HTMLElement} tab The tab to add.
+     */
+    addTab: function(holder, tab) {
+        holder.children[0].children[0].appendChild(tab);
+    },
+
+    /**
+     * Gets a single tab element.
+     * 
+     * @param {HTMLElement} span The tab's content.
+     * @returns {HTMLElement} The tab element.
+     * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
+     */
+    getTab: function(span) {
+
+        var el = document.createElement('li');
+        el.className = 'tab';
+        this.applyStyles(el, {
+            width: '100%',
+            textAlign: 'left',
+            lineHeight: '24px',
+            height: '24px',
+            fontSize: '14px',
+            cursor: 'pointer'
+        });
+        el.appendChild(span);
+        return el;
+    },
+
+    /**
+     * Marks specified tab as active.
+     * 
+     * @returns {HTMLElement} The tab element.
+     * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
+     */
+    markTabActive: function(tab) {
+
+        this.applyStyles(tab, {
+            width: '100%',
+            textAlign: 'left',
+            lineHeight: '24px',
+            height: '24px',
+            fontSize: '14px',
+            cursor: 'pointer',
+            color: 'rgba(238,110,115,1)',
+            transition: 'border-color .5s ease',
+            borderRight: '3px solid #424242'
+        });
+
+    },
+
+    /**
+     * Marks specified tab as inactive.
+     * 
+     * @returns {HTMLElement} The tab element.
+     * @see https://github.com/Dogfalo/materialize/issues/2542#issuecomment-233458602
+     */
+    markTabInactive: function(tab) {
+
+        this.applyStyles(tab, {
+            width: '100%',
+            textAlign: 'left',
+            lineHeight: '24px',
+            height: '24px',
+            fontSize: '14px',
+            cursor: 'pointer',
+            color: 'rgba(238,110,115,0.7)'
+        });
+
+    },
+
+    /**
+     * Returns the element that holds the tab contents.
+     * 
+     * @param {HTMLElement} tabHolder The full tab holder element.
+     * @returns {HTMLElement} The content element inside specified tab holder.
+     */
+    getTabContentHolder: function(tabHolder) {
+        return tabHolder.children[1];
+    },
+
+    /**
+     * Creates and returns a tab content element.
+     * 
+     * @returns {HTMLElement} The new tab content element.
+     */
+    getTabContent: function() {
+        return document.createElement('div');
+    },
+
+    /**
+     * Adds an error message to the specified input element.
+     * 
+     * @param {HTMLElement} input The input element that caused the error.
+     * @param {string} text The error message.
+     */
+    addInputError: function(input, text) {
+
+        // Get the parent element. Should most likely be a <div class="input-field" ... />.
+        var parent = input.parentNode,
+            el;
+
+        if (!parent) return;
+
+        // Remove any previous error.
+        this.removeInputError(input);
+
+        // Append an error message div.
+        el = document.createElement('div');
+        el.className = 'error-text red-text';
+        el.textContent = text;
+        parent.appendChild(el);
+
+    },
+
+    /**
+     * Removes any error message from the specified input element.
+     * 
+     * @param {HTMLElement} input The input element that previously caused the error.
+     */
+    removeInputError: function(input) {
+
+        // Get the parent element. Should most likely be a <div class="input-field" ... />.
+        var parent = input.parentElement,
+            els;
+
+        if (!parent) return;
+
+        // Remove all elements having class .error-text.
+        els = parent.getElementsByClassName('error-text');
+        for (var i = 0; i < els.length; i++)
+            parent.removeChild(els[i]);
+
+    },
+
+    addTableRowError: function(row) {
+    },
+
+    removeTableRowError: function(row) {
+    },
+
+    /**
+     * Gets a select DOM element.
+     * 
+     * @param {object} options The option values.
+     * @return {HTMLElement} The DOM element.
+     * @see http://materializecss.com/forms.html#select
+     */
+    getSelectInput: function(options) {
+
+        var select = this._super(options);
+        select.className = 'browser-default';
+        return select;
+
+    },
+
+    /**
+     * Gets a textarea DOM element.
+     * 
+     * @returns {HTMLElement} The DOM element.
+     * @see http://materializecss.com/forms.html#textarea
+     */
+    getTextareaInput: function() {
+        var el = document.createElement('textarea');
+        el.style.marginBottom = '5px';
+        el.style.fontSize = '1rem';
+        el.style.fontFamily = 'monospace';
+        return el;
+    },
+
+    getCheckbox: function() {
+
+        var el = this.getFormInputField('checkbox');
+        el.id = this.createUuid();
+        return el;
+
+    },
+
+    /**
+     * Gets the modal element for displaying Edit JSON and Properties dialogs.
+     * 
+     * @returns {HTMLElement} The modal DOM element.
+     * @see http://materializecss.com/cards.html
+     */
+    getModal: function() {
+
+        var el = document.createElement('div');
+        el.className = 'card-panel z-depth-3';
+        el.style.padding = '5px';
+        el.style.position = 'absolute';
+        el.style.zIndex = '10';
+        el.style.display = 'none';
+        return el;
+
+    },
+
+    /**
+     * Creates and returns a RFC4122 version 4 compliant unique id.
+     * 
+     * @returns {string} A GUID.
+     * @see https://stackoverflow.com/a/2117523
+     */
+    createUuid: function() {
+
+        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+            return v.toString(16);
+        });
+
+    }
+
+});
+
+JSONEditor.AbstractIconLib = Class.extend({
+  mapping: {
+    collapse: '',
+    expand: '',
+    "delete": '',
+    edit: '',
+    add: '',
+    cancel: '',
+    save: '',
+    moveup: '',
+    movedown: ''
+  },
+  icon_prefix: '',
+  getIconClass: function(key) {
+    if(this.mapping[key]) return this.icon_prefix+this.mapping[key];
+    else return null;
+  },
+  getIcon: function(key) {
+    var iconclass = this.getIconClass(key);
+    
+    if(!iconclass) return null;
+    
+    var i = document.createElement('i');
+    i.className = iconclass;
+    return i;
+  }
+});
+
+JSONEditor.defaults.iconlibs.bootstrap2 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'chevron-down',
+    expand: 'chevron-up',
+    "delete": 'trash',
+    edit: 'pencil',
+    add: 'plus',
+    cancel: 'ban-circle',
+    save: 'ok',
+    moveup: 'arrow-up',
+    movedown: 'arrow-down'
+  },
+  icon_prefix: 'glyphicon glyphicon-'
+});
+
+JSONEditor.defaults.iconlibs.bootstrap3 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'chevron-down',
+    expand: 'chevron-right',
+    "delete": 'remove',
+    edit: 'pencil',
+    add: 'plus',
+    cancel: 'floppy-remove',
+    save: 'floppy-saved',
+    moveup: 'arrow-up',
+    movedown: 'arrow-down'
+  },
+  icon_prefix: 'glyphicon glyphicon-'
+});
+
+JSONEditor.defaults.iconlibs.fontawesome3 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'chevron-down',
+    expand: 'chevron-right',
+    "delete": 'remove',
+    edit: 'pencil',
+    add: 'plus',
+    cancel: 'ban-circle',
+    save: 'save',
+    moveup: 'arrow-up',
+    movedown: 'arrow-down'
+  },
+  icon_prefix: 'icon-'
+});
+
+JSONEditor.defaults.iconlibs.fontawesome4 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'caret-square-o-down',
+    expand: 'caret-square-o-right',
+    "delete": 'times',
+    edit: 'pencil',
+    add: 'plus',
+    cancel: 'ban',
+    save: 'save',
+    moveup: 'arrow-up',
+    movedown: 'arrow-down',
+    copy: 'files-o'
+  },
+  icon_prefix: 'fa fa-'
+});
+
+JSONEditor.defaults.iconlibs.foundation2 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'minus',
+    expand: 'plus',
+    "delete": 'remove',
+    edit: 'edit',
+    add: 'add-doc',
+    cancel: 'error',
+    save: 'checkmark',
+    moveup: 'up-arrow',
+    movedown: 'down-arrow'
+  },
+  icon_prefix: 'foundicon-'
+});
+
+JSONEditor.defaults.iconlibs.foundation3 = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'minus',
+    expand: 'plus',
+    "delete": 'x',
+    edit: 'pencil',
+    add: 'page-add',
+    cancel: 'x-circle',
+    save: 'save',
+    moveup: 'arrow-up',
+    movedown: 'arrow-down'
+  },
+  icon_prefix: 'fi-'
+});
+
+JSONEditor.defaults.iconlibs.jqueryui = JSONEditor.AbstractIconLib.extend({
+  mapping: {
+    collapse: 'triangle-1-s',
+    expand: 'triangle-1-e',
+    "delete": 'trash',
+    edit: 'pencil',
+    add: 'plusthick',
+    cancel: 'closethick',
+    save: 'disk',
+    moveup: 'arrowthick-1-n',
+    movedown: 'arrowthick-1-s'
+  },
+  icon_prefix: 'ui-icon ui-icon-'
+});
+
+JSONEditor.defaults.iconlibs.materialicons = JSONEditor.AbstractIconLib.extend({
+
+    mapping: {
+        collapse: 'arrow_drop_up',
+        expand: 'arrow_drop_down',
+        "delete": 'delete',
+        edit: 'edit',
+        add: 'add',
+        cancel: 'cancel',
+        save: 'save',
+        moveup: 'arrow_upward',
+        movedown: 'arrow_downward',
+        copy: 'content_copy'
+    },
+
+    icon_class: 'material-icons',
+    icon_prefix: '',
+
+    getIconClass: function(key) {
+
+        // This method is unused.
+
+        return this.icon_class;
+    },
+
+    getIcon: function(key) {
+
+        // Get the mapping.
+        var mapping = this.mapping[key];
+        if (!mapping) return null;
+
+        // @see http://materializecss.com/icons.html
+        var i = document.createElement('i');
+        i.className = this.icon_class;
+        var t = document.createTextNode(mapping);
+        i.appendChild(t);
+        return i;
+
+    }
+});
+
+JSONEditor.defaults.templates["default"] = function() {
+  return {
+    compile: function(template) {
+      var matches = template.match(/{{\s*([a-zA-Z0-9\-_ \.]+)\s*}}/g);
+      var l = matches && matches.length;
+
+      // Shortcut if the template contains no variables
+      if(!l) return function() { return template; };
+
+      // Pre-compute the search/replace functions
+      // This drastically speeds up template execution
+      var replacements = [];
+      var get_replacement = function(i) {
+        var p = matches[i].replace(/[{}]+/g,'').trim().split('.');
+        var n = p.length;
+        var func;
+        
+        if(n > 1) {
+          var cur;
+          func = function(vars) {
+            cur = vars;
+            for(i=0; i<n; i++) {
+              cur = cur[p[i]];
+              if(!cur) break;
+            }
+            return cur;
+          };
+        }
+        else {
+          p = p[0];
+          func = function(vars) {
+            return vars[p];
+          };
+        }
+        
+        replacements.push({
+          s: matches[i],
+          r: func
+        });
+      };
+      for(var i=0; i<l; i++) {
+        get_replacement(i);
+      }
+
+      // The compiled function
+      return function(vars) {
+        var ret = template+"";
+        var r;
+        for(i=0; i<l; i++) {
+          r = replacements[i];
+          ret = ret.replace(r.s, r.r(vars));
+        }
+        return ret;
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.ejs = function() {
+  if(!window.EJS) return false;
+
+  return {
+    compile: function(template) {
+      var compiled = new window.EJS({
+        text: template
+      });
+
+      return function(context) {
+        return compiled.render(context);
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.handlebars = function() {
+  return window.Handlebars;
+};
+
+JSONEditor.defaults.templates.hogan = function() {
+  if(!window.Hogan) return false;
+
+  return {
+    compile: function(template) {
+      var compiled = window.Hogan.compile(template);
+      return function(context) {
+        return compiled.render(context);
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.lodash = function() {
+  if(!window._) return false;
+
+  return {
+    compile: function(template) {
+      return function(context) {
+        return window._.template(template)(context);
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.markup = function() {
+  if(!window.Mark || !window.Mark.up) return false;
+
+  return {
+    compile: function(template) {
+      return function(context) {
+        return window.Mark.up(template,context);
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.mustache = function() {
+  if(!window.Mustache) return false;
+
+  return {
+    compile: function(template) {
+      return function(view) {
+        return window.Mustache.render(template, view);
+      };
+    }
+  };
+};
+
+JSONEditor.defaults.templates.swig = function() {
+  return window.swig;
+};
+
+JSONEditor.defaults.templates.underscore = function() {
+  if(!window._) return false;
+
+  return {
+    compile: function(template) {
+      return function(context) {
+        return window._.template(template, context);
+      };
+    }
+  };
+};
+
+// Set the default theme
+JSONEditor.defaults.theme = 'html';
+
+// Set the default template engine
+JSONEditor.defaults.template = 'default';
+
+// Default options when initializing JSON Editor
+JSONEditor.defaults.options = {};
+
+JSONEditor.defaults.options.prompt_before_delete = true;
+
+// String translate function
+JSONEditor.defaults.translate = function(key, variables) {
+  var lang = JSONEditor.defaults.languages[JSONEditor.defaults.language];
+  if(!lang) throw "Unknown language "+JSONEditor.defaults.language;
+  
+  var string = lang[key] || JSONEditor.defaults.languages[JSONEditor.defaults.default_language][key];
+  
+  if(typeof string === "undefined") throw "Unknown translate string "+key;
+  
+  if(variables) {
+    for(var i=0; i<variables.length; i++) {
+      string = string.replace(new RegExp('\\{\\{'+i+'}}','g'),variables[i]);
+    }
+  }
+  
+  return string;
+};
+
+// Translation strings and default languages
+JSONEditor.defaults.default_language = 'en';
+JSONEditor.defaults.language = JSONEditor.defaults.default_language;
+JSONEditor.defaults.languages.en = {
+  /**
+   * When a property is not set
+   */
+  error_notset: 'Please populate the required property "{{0}}"',
+  /**
+   * When a string must not be empty
+   */
+  error_notempty: 'Please populate the required property "{{0}}"',
+  /**
+   * When a value is not one of the enumerated values
+   */
+  error_enum: "{{0}} must be one of the enumerated values",
+  /**
+   * When a value doesn't validate any schema of a 'anyOf' combination
+   */
+  error_anyOf: "Value must validate against at least one of the provided schemas",
+  /**
+   * When a value doesn't validate
+   * @variables This key takes one variable: The number of schemas the value does not validate
+   */
+  error_oneOf: 'Value must validate against exactly one of the provided schemas. It currently validates against {{0}} of the schemas.',
+  /**
+   * When a value does not validate a 'not' schema
+   */
+  error_not: "Value must not validate against the provided schema",
+  /**
+   * When a value does not match any of the provided types
+   */
+  error_type_union: "Value must be one of the provided types",
+  /**
+   * When a value does not match the given type
+   * @variables This key takes one variable: The type the value should be of
+   */
+  error_type: "Value must be of type {{0}}",
+  /**
+   *  When the value validates one of the disallowed types
+   */
+  error_disallow_union: "Value must not be one of the provided disallowed types",
+  /**
+   *  When the value validates a disallowed type
+   * @variables This key takes one variable: The type the value should not be of
+   */
+  error_disallow: "Value must not be of type {{0}}",
+  /**
+   * When a value is not a multiple of or divisible by a given number
+   * @variables This key takes one variable: The number mentioned above
+   */
+  error_multipleOf: "Value must be a multiple of {{0}}",
+  /**
+   * When a value is greater than it's supposed to be (exclusive)
+   * @variables This key takes one variable: The maximum
+   */
+  error_maximum_excl: "{{0}} must be less than {{1}}",
+  /**
+   * When a value is greater than it's supposed to be (inclusive)
+   * @variables This key takes one variable: The maximum
+   */
+  error_maximum_incl: "{{0}} must be at most {{1}}",
+  /**
+   * When a value is lesser than it's supposed to be (exclusive)
+   * @variables This key takes one variable: The minimum
+   */
+  error_minimum_excl: "{{0}} must be greater than {{1}}",
+  /**
+   * When a value is lesser than it's supposed to be (inclusive)
+   * @variables This key takes one variable: The minimum
+   */
+  error_minimum_incl: "{{0}} must be at least {{1}}",
+  /**
+   * When a value have too many characters
+   * @variables This key takes one variable: The maximum character count
+   */
+  error_maxLength: "{{0}} must be at most {{1}} characters long",
+  /**
+   * When a value does not have enough characters
+   * @variables This key takes one variable: The minimum character count
+   */
+  error_minLength: "{{0}} must be at least {{1}} characters long",
+  /**
+   * When a value does not match a given pattern
+   */
+  error_pattern: "{{0}} must match the pattern {{1}}",
+  /**
+   * When an array has additional items whereas it is not supposed to
+   */
+  error_additionalItems: "No additional items allowed in this array",
+  /**
+   * When there are to many items in an array
+   * @variables This key takes one variable: The maximum item count
+   */
+  error_maxItems: "{{0}} must have at most {{1}} items",
+  /**
+   * When there are not enough items in an array
+   * @variables This key takes one variable: The minimum item count
+   */
+  error_minItems: "{{0}} must have at least {{1}} items",
+  /**
+   * When an array is supposed to have unique items but has duplicates
+   */
+  error_uniqueItems: "Each tab of {{0}} must specify a unique combination of parameters",
+  /**
+   * When there are too many properties in an object
+   * @variables This key takes one variable: The maximum property count
+   */
+  error_maxProperties: "Object must have at most {{0}} properties",
+  /**
+   * When there are not enough properties in an object
+   * @variables This key takes one variable: The minimum property count
+   */
+  error_minProperties: "Object must have at least {{0}} properties",
+  /**
+   * When a required property is not defined
+   * @variables This key takes one variable: The name of the missing property
+   */
+  error_required: 'Please populate the required property "{{0}}"',
+  /**
+   * When there is an additional property is set whereas there should be none
+   * @variables This key takes one variable: The name of the additional property
+   */
+  error_additional_properties: "No additional properties allowed, but property {{0}} is set",
+  /**
+   * When a dependency is not resolved
+   * @variables This key takes one variable: The name of the missing property for the dependency
+   */
+  error_dependency: "Must have property {{0}}",
+  /**
+   * Text on Delete All buttons
+   */
+  button_delete_all: "All",
+  /**
+   * Title on Delete All buttons
+   */
+  button_delete_all_title: "Delete All",
+  /**
+    * Text on Delete Last buttons
+    * @variable This key takes one variable: The title of object to delete
+    */
+  button_delete_last: "Last {{0}}",
+  /**
+    * Title on Delete Last buttons
+    * @variable This key takes one variable: The title of object to delete
+    */
+  button_delete_last_title: "Delete Last {{0}}",
+  /**
+    * Title on Add Row buttons
+    * @variable This key takes one variable: The title of object to add
+    */
+  button_add_row_title: "Add {{0}}",
+  /**
+    * Title on Move Down buttons
+    */
+  button_move_down_title: "Move down",
+  /**
+    * Title on Move Up buttons
+    */
+  button_move_up_title: "Move up",
+  /**
+    * Title on Delete Row buttons
+    * @variable This key takes one variable: The title of object to delete
+    */
+  button_delete_row_title: "Delete {{0}}",
+  /**
+    * Title on Delete Row buttons, short version (no parameter with the object title)
+    */
+  button_delete_row_title_short: "Delete",
+  /**
+    * Title on Collapse buttons
+    */
+  button_collapse: "Collapse",
+  /**
+    * Title on Expand buttons
+    */
+  button_expand: "Expand"
+};
+
+// Miscellaneous Plugin Settings
+JSONEditor.plugins = {
+  ace: {
+    theme: ''
+  },
+  SimpleMDE: {
+
+  },
+  sceditor: {
+
+  },
+  select2: {
+    
+  },
+  selectize: {
+  }
+};
+
+// Default per-editor options
+$each(JSONEditor.defaults.editors, function(i,editor) {
+  JSONEditor.defaults.editors[i].options = editor.options || {};
+});
+
+// Set the default resolvers
+// Use "multiple" as a fall back for everything
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.type === "qbldr") return "qbldr";
+});
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(typeof schema.type !== "string") return "multiple";
+});
+// If the type is not set but properties are defined, we can infer the type is actually object
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // If the schema is a simple type
+  if(!schema.type && schema.properties ) return "object";
+});
+// If the type is set and it's a basic type, use the primitive editor
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // If the schema is a simple type
+  if(typeof schema.type === "string") return schema.type;
+});
+// Use a specialized editor for ratings
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.type === "integer" && schema.format === "rating") return "rating";
+});
+// Use the select editor for all boolean values
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.type === 'boolean') {
+    // If explicitly set to 'checkbox', use that
+    if(schema.format === "checkbox" || (schema.options && schema.options.checkbox)) {
+      return "checkbox";
+    }
+    // Otherwise, default to select menu
+    return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
+  }
+});
+// Use the multiple editor for schemas where the `type` is set to "any"
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // If the schema can be of any type
+  if(schema.type === "any") return "multiple";
+});
+// Editor for base64 encoded files
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // If the schema can be of any type
+  if(schema.type === "string" && schema.media && schema.media.binaryEncoding==="base64") {
+    return "base64";
+  }
+});
+// Editor for uploading files
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.type === "string" && schema.format === "url" && schema.options && schema.options.upload === true) {
+    if(window.FileReader) return "upload";
+  }
+});
+// Use the table editor for arrays with the format set to `table`
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // Type `array` with format set to `table`
+  if(schema.type === "array" && schema.format === "table") {
+    return "table";
+  }
+});
+// Use the `select` editor for dynamic enumSource enums
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.enumSource) return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
+});
+// Use the `enum` or `select` editors for schemas with enumerated properties
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema["enum"]) {
+    if(schema.type === "array" || schema.type === "object") {
+      return "enum";
+    }
+    else if(schema.type === "number" || schema.type === "integer" || schema.type === "string") {
+      return (JSONEditor.plugins.selectize.enable) ? 'selectize' : 'select';
+    }
+  }
+});
+// Specialized editors for arrays of strings
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  if(schema.type === "array" && schema.items && !(Array.isArray(schema.items)) && schema.uniqueItems && ['string','number','integer'].indexOf(schema.items.type) >= 0) {
+    // For enumerated strings, number, or integers
+    if(schema.items.enum) {
+      return 'multiselect';
+    }
+    // For non-enumerated strings (tag editor)
+    else if(JSONEditor.plugins.selectize.enable && schema.items.type === "string") {
+      return 'arraySelectize';
+    }
+  }
+});
+// Use the multiple editor for schemas with `oneOf` set
+JSONEditor.defaults.resolvers.unshift(function(schema) {
+  // If this schema uses `oneOf` or `anyOf`
+  if(schema.oneOf || schema.anyOf) return "multiple";
+});
+
+/**
+ * This is a small wrapper for using JSON Editor like a typical jQuery plugin.
+ */
+(function() {
+  if(window.jQuery || window.Zepto) {
+    var $ = window.jQuery || window.Zepto;
+    $.jsoneditor = JSONEditor.defaults;
+    
+    $.fn.jsoneditor = function(options) {
+      var self = this;
+      var editor = this.data('jsoneditor');
+      if(options === 'value') {
+        if(!editor) throw "Must initialize jsoneditor before getting/setting the value";
+        
+        // Set value
+        if(arguments.length > 1) {
+          editor.setValue(arguments[1]);
+        }
+        // Get value
+        else {
+          return editor.getValue();
+        }
+      }
+      else if(options === 'validate') {
+        if(!editor) throw "Must initialize jsoneditor before validating";
+        
+        // Validate a specific value
+        if(arguments.length > 1) {
+          return editor.validate(arguments[1]);
+        }
+        // Validate current value
+        else {
+          return editor.validate();
+        }
+      }
+      else if(options === 'destroy') {
+        if(editor) {
+          editor.destroy();
+          this.data('jsoneditor',null);
+        }
+      }
+      else {
+        // Destroy first
+        if(editor) {
+          editor.destroy();
+        }
+        
+        // Create editor
+        editor = new JSONEditor(this.get(0),options);
+        this.data('jsoneditor',editor);
+        
+        // Setup event listeners
+        editor.on('change',function() {
+          self.trigger('change');
+        });
+        editor.on('ready',function() {
+          self.trigger('ready');
+        });
+      }
+      
+      return this;
+    };
+  }
+})();
+
+  window.JSONEditor = JSONEditor;
+})();
diff --git a/src/main/resources/META-INF/resources/designer/lib/query-builder.standalone.js b/src/main/resources/META-INF/resources/designer/lib/query-builder.standalone.js
new file mode 100644 (file)
index 0000000..169b287
--- /dev/null
@@ -0,0 +1,6541 @@
+/*!
+ * jQuery.extendext 0.1.2
+ *
+ * Copyright 2014-2016 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (http://opensource.org/licenses/MIT)
+ * 
+ * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors
+ */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define('jQuery.extendext', ['jquery'], factory);
+    }
+    else if (typeof module === 'object' && module.exports) {
+        module.exports = factory(require('jquery'));
+    }
+    else {
+        factory(root.jQuery);
+    }
+}(this, function ($) {
+    "use strict";
+
+    $.extendext = function () {
+        var options, name, src, copy, copyIsArray, clone,
+            target = arguments[0] || {},
+            i = 1,
+            length = arguments.length,
+            deep = false,
+            arrayMode = 'default';
+
+        // Handle a deep copy situation
+        if (typeof target === "boolean") {
+            deep = target;
+
+            // Skip the boolean and the target
+            target = arguments[i++] || {};
+        }
+
+        // Handle array mode parameter
+        if (typeof target === "string") {
+            arrayMode = target.toLowerCase();
+            if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') {
+                arrayMode = 'default';
+            }
+
+            // Skip the string param
+            target = arguments[i++] || {};
+        }
+
+        // Handle case when target is a string or something (possible in deep copy)
+        if (typeof target !== "object" && !$.isFunction(target)) {
+            target = {};
+        }
+
+        // Extend jQuery itself if only one argument is passed
+        if (i === length) {
+            target = this;
+            i--;
+        }
+
+        for (; i < length; i++) {
+            // Only deal with non-null/undefined values
+            if ((options = arguments[i]) !== null) {
+                // Special operations for arrays
+                if ($.isArray(options) && arrayMode !== 'default') {
+                    clone = target && $.isArray(target) ? target : [];
+
+                    switch (arrayMode) {
+                    case 'concat':
+                        target = clone.concat($.extend(deep, [], options));
+                        break;
+
+                    case 'replace':
+                        target = $.extend(deep, [], options);
+                        break;
+
+                    case 'extend':
+                        options.forEach(function (e, i) {
+                            if (typeof e === 'object') {
+                                var type = $.isArray(e) ? [] : {};
+                                clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e);
+
+                            } else if (clone.indexOf(e) === -1) {
+                                clone.push(e);
+                            }
+                        });
+
+                        target = clone;
+                        break;
+                    }
+
+                } else {
+                    // Extend the base object
+                    for (name in options) {
+                        src = target[name];
+                        copy = options[name];
+
+                        // Prevent never-ending loop
+                        if (target === copy) {
+                            continue;
+                        }
+
+                        // Recurse if we're merging plain objects or arrays
+                        if (deep && copy && ( $.isPlainObject(copy) ||
+                            (copyIsArray = $.isArray(copy)) )) {
+
+                            if (copyIsArray) {
+                                copyIsArray = false;
+                                clone = src && $.isArray(src) ? src : [];
+
+                            } else {
+                                clone = src && $.isPlainObject(src) ? src : {};
+                            }
+
+                            // Never move original objects, clone them
+                            target[name] = $.extendext(deep, arrayMode, clone, copy);
+
+                            // Don't bring in undefined values
+                        } else if (copy !== undefined) {
+                            target[name] = copy;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Return the modified object
+        return target;
+    };
+}));
+
+// doT.js
+// 2011-2014, Laura Doktorova, https://github.com/olado/doT
+// Licensed under the MIT license.
+
+(function () {
+       "use strict";
+
+       var doT = {
+               name: "doT",
+               version: "1.1.1",
+               templateSettings: {
+                       evaluate:    /\{\{([\s\S]+?(\}?)+)\}\}/g,
+                       interpolate: /\{\{=([\s\S]+?)\}\}/g,
+                       encode:      /\{\{!([\s\S]+?)\}\}/g,
+                       use:         /\{\{#([\s\S]+?)\}\}/g,
+                       useParams:   /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
+                       define:      /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
+                       defineParams:/^\s*([\w$]+):([\s\S]+)/,
+                       conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
+                       iterate:     /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
+                       varname:        "it",
+                       strip:          true,
+                       append:         true,
+                       selfcontained: false,
+                       doNotSkipEncoded: false
+               },
+               template: undefined, //fn, compile template
+               compile:  undefined, //fn, for express
+               log: true
+       }, _globals;
+
+       doT.encodeHTMLSource = function(doNotSkipEncoded) {
+               var encodeHTMLRules = { "&": "&#38;", "<": "&#60;", ">": "&#62;", '"': "&#34;", "'": "&#39;", "/": "&#47;" },
+                       matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
+               return function(code) {
+                       return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : "";
+               };
+       };
+
+       _globals = (function(){ return this || (0,eval)("this"); }());
+
+       /* istanbul ignore else */
+       if (typeof module !== "undefined" && module.exports) {
+               module.exports = doT;
+       } else if (typeof define === "function" && define.amd) {
+               define('doT', function(){return doT;});
+       } else {
+               _globals.doT = doT;
+       }
+
+       var startend = {
+               append: { start: "'+(",      end: ")+'",      startencode: "'+encodeHTML(" },
+               split:  { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" }
+       }, skip = /$^/;
+
+       function resolveDefs(c, block, def) {
+               return ((typeof block === "string") ? block : block.toString())
+               .replace(c.define || skip, function(m, code, assign, value) {
+                       if (code.indexOf("def.") === 0) {
+                               code = code.substring(4);
+                       }
+                       if (!(code in def)) {
+                               if (assign === ":") {
+                                       if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
+                                               def[code] = {arg: param, text: v};
+                                       });
+                                       if (!(code in def)) def[code]= value;
+                               } else {
+                                       new Function("def", "def['"+code+"']=" + value)(def);
+                               }
+                       }
+                       return "";
+               })
+               .replace(c.use || skip, function(m, code) {
+                       if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
+                               if (def[d] && def[d].arg && param) {
+                                       var rw = (d+":"+param).replace(/'|\\/g, "_");
+                                       def.__exp = def.__exp || {};
+                                       def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
+                                       return s + "def.__exp['"+rw+"']";
+                               }
+                       });
+                       var v = new Function("def", "return " + code)(def);
+                       return v ? resolveDefs(c, v, def) : v;
+               });
+       }
+
+       function unescape(code) {
+               return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ");
+       }
+
+       doT.template = function(tmpl, c, def) {
+               c = c || doT.templateSettings;
+               var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
+                       str  = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
+
+               str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
+                                       .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str)
+                       .replace(/'|\\/g, "\\$&")
+                       .replace(c.interpolate || skip, function(m, code) {
+                               return cse.start + unescape(code) + cse.end;
+                       })
+                       .replace(c.encode || skip, function(m, code) {
+                               needhtmlencode = true;
+                               return cse.startencode + unescape(code) + cse.end;
+                       })
+                       .replace(c.conditional || skip, function(m, elsecase, code) {
+                               return elsecase ?
+                                       (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
+                                       (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
+                       })
+                       .replace(c.iterate || skip, function(m, iterate, vname, iname) {
+                               if (!iterate) return "';} } out+='";
+                               sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
+                               return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
+                                       +vname+"=arr"+sid+"["+indv+"+=1];out+='";
+                       })
+                       .replace(c.evaluate || skip, function(m, code) {
+                               return "';" + unescape(code) + "out+='";
+                       })
+                       + "';return out;")
+                       .replace(/\n/g, "\\n").replace(/\t/g, '\\t').replace(/\r/g, "\\r")
+                       .replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, "");
+                       //.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');
+
+               if (needhtmlencode) {
+                       if (!c.selfcontained && _globals && !_globals._encodeHTML) _globals._encodeHTML = doT.encodeHTMLSource(c.doNotSkipEncoded);
+                       str = "var encodeHTML = typeof _encodeHTML !== 'undefined' ? _encodeHTML : ("
+                               + doT.encodeHTMLSource.toString() + "(" + (c.doNotSkipEncoded || '') + "));"
+                               + str;
+               }
+               try {
+                       return new Function(c.varname, str);
+               } catch (e) {
+                       /* istanbul ignore else */
+                       if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
+                       throw e;
+               }
+       };
+
+       doT.compile = function(tmpl, def) {
+               return doT.template(tmpl, null, def);
+       };
+}());
+
+
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+(function(root, factory) {
+    if (typeof define == 'function' && define.amd) {
+        define('query-builder', ['jquery', 'dot/doT', 'jquery-extendext'], factory);
+    }
+    else if (typeof module === 'object' && module.exports) {
+        module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
+    }
+    else {
+        factory(root.jQuery, root.doT);
+    }
+}(this, function($, doT) {
+"use strict";
+
+/**
+ * @typedef {object} Filter
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#filters}
+ */
+
+/**
+ * @typedef {object} Operator
+ * @memberof QueryBuilder
+ * @description See {@link http://querybuilder.js.org/index.html#operators}
+ */
+
+/**
+ * @param {jQuery} $el
+ * @param {object} options - see {@link http://querybuilder.js.org/#options}
+ * @constructor
+ */
+var QueryBuilder = function($el, options) {
+    $el[0].queryBuilder = this;
+
+    /**
+     * Element container
+     * @member {jQuery}
+     * @readonly
+     */
+    this.$el = $el;
+
+    /**
+     * Configuration object
+     * @member {object}
+     * @readonly
+     */
+    this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
+
+    /**
+     * Internal model
+     * @member {Model}
+     * @readonly
+     */
+    this.model = new Model();
+
+    /**
+     * Internal status
+     * @member {object}
+     * @property {string} id - id of the container
+     * @property {boolean} generated_id - if the container id has been generated
+     * @property {int} group_id - current group id
+     * @property {int} rule_id - current rule id
+     * @property {boolean} has_optgroup - if filters have optgroups
+     * @property {boolean} has_operator_optgroup - if operators have optgroups
+     * @readonly
+     * @private
+     */
+    this.status = {
+        id: null,
+        generated_id: false,
+        group_id: 0,
+        rule_id: 0,
+        has_optgroup: false,
+        has_operator_optgroup: false
+    };
+
+    /**
+     * List of filters
+     * @member {QueryBuilder.Filter[]}
+     * @readonly
+     */
+    this.filters = this.settings.filters;
+
+    /**
+     * List of icons
+     * @member {object.<string, string>}
+     * @readonly
+     */
+    this.icons = this.settings.icons;
+
+    /**
+     * List of operators
+     * @member {QueryBuilder.Operator[]}
+     * @readonly
+     */
+    this.operators = this.settings.operators;
+
+    /**
+     * List of templates
+     * @member {object.<string, function>}
+     * @readonly
+     */
+    this.templates = this.settings.templates;
+
+    /**
+     * Plugins configuration
+     * @member {object.<string, object>}
+     * @readonly
+     */
+    this.plugins = this.settings.plugins;
+
+    /**
+     * Translations object
+     * @member {object}
+     * @readonly
+     */
+    this.lang = null;
+
+    // translations : english << 'lang_code' << custom
+    if (QueryBuilder.regional['en'] === undefined) {
+        Utils.error('Config', '"i18n/en.js" not loaded.');
+    }
+    this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
+
+    // "allow_groups" can be boolean or int
+    if (this.settings.allow_groups === false) {
+        this.settings.allow_groups = 0;
+    }
+    else if (this.settings.allow_groups === true) {
+        this.settings.allow_groups = -1;
+    }
+
+    // init templates
+    Object.keys(this.templates).forEach(function(tpl) {
+        if (!this.templates[tpl]) {
+            this.templates[tpl] = QueryBuilder.templates[tpl];
+        }
+        if (typeof this.templates[tpl] == 'string') {
+            this.templates[tpl] = doT.template(this.templates[tpl]);
+        }
+    }, this);
+
+    // ensure we have a container id
+    if (!this.$el.attr('id')) {
+        this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
+        this.status.generated_id = true;
+    }
+    this.status.id = this.$el.attr('id');
+
+    // INIT
+    this.$el.addClass('query-builder form-inline');
+
+    this.filters = this.checkFilters(this.filters);
+    this.operators = this.checkOperators(this.operators);
+    this.bindEvents();
+    this.initPlugins();
+};
+
+$.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
+    /**
+     * Triggers an event on the builder container
+     * @param {string} type
+     * @returns {$.Event}
+     */
+    trigger: function(type) {
+        var event = new $.Event(this._tojQueryEvent(type), {
+            builder: this
+        });
+
+        this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+
+        return event;
+    },
+
+    /**
+     * Triggers an event on the builder container and returns the modified value
+     * @param {string} type
+     * @param {*} value
+     * @returns {*}
+     */
+    change: function(type, value) {
+        var event = new $.Event(this._tojQueryEvent(type, true), {
+            builder: this,
+            value: value
+        });
+
+        this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
+
+        return event.value;
+    },
+
+    /**
+     * Attaches an event listener on the builder container
+     * @param {string} type
+     * @param {function} cb
+     * @returns {QueryBuilder}
+     */
+    on: function(type, cb) {
+        this.$el.on(this._tojQueryEvent(type), cb);
+        return this;
+    },
+
+    /**
+     * Removes an event listener from the builder container
+     * @param {string} type
+     * @param {function} [cb]
+     * @returns {QueryBuilder}
+     */
+    off: function(type, cb) {
+        this.$el.off(this._tojQueryEvent(type), cb);
+        return this;
+    },
+
+    /**
+     * Attaches an event listener called once on the builder container
+     * @param {string} type
+     * @param {function} cb
+     * @returns {QueryBuilder}
+     */
+    once: function(type, cb) {
+        this.$el.one(this._tojQueryEvent(type), cb);
+        return this;
+    },
+
+    /**
+     * Appends `.queryBuilder` and optionally `.filter` to the events names
+     * @param {string} name
+     * @param {boolean} [filter=false]
+     * @returns {string}
+     * @private
+     */
+    _tojQueryEvent: function(name, filter) {
+        return name.split(' ').map(function(type) {
+            return type + '.queryBuilder' + (filter ? '.filter' : '');
+        }).join(' ');
+    }
+});
+
+
+/**
+ * Allowed types and their internal representation
+ * @type {object.<string, string>}
+ * @readonly
+ * @private
+ */
+QueryBuilder.types = {
+    'string':   'string',
+    'integer':  'number',
+    'double':   'number',
+    'date':     'datetime',
+    'time':     'datetime',
+    'datetime': 'datetime',
+    'boolean':  'boolean',
+    'map': 'map'
+};
+
+/**
+ * Allowed inputs
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.inputs = [
+    'text',
+    'number',
+    'textarea',
+    'radio',
+    'checkbox',
+    'select'
+];
+
+/**
+ * Runtime modifiable options with `setOptions` method
+ * @type {string[]}
+ * @readonly
+ * @private
+ */
+QueryBuilder.modifiable_options = [
+    'display_errors',
+    'allow_groups',
+    'allow_empty',
+    'default_condition',
+    'default_filter'
+];
+
+/**
+ * CSS selectors for common components
+ * @type {object.<string, string>}
+ * @readonly
+ */
+QueryBuilder.selectors = {
+    group_container:      '.rules-group-container',
+    rule_container:       '.rule-container',
+    filter_container:     '.rule-filter-container',
+    operator_container:   '.rule-operator-container',
+    value_container:      '.rule-value-container',
+    error_container:      '.error-container',
+    condition_container:  '.rules-group-header .group-conditions',
+
+    rule_header:          '.rule-header',
+    group_header:         '.rules-group-header',
+    group_actions:        '.group-actions',
+    rule_actions:         '.rule-actions',
+
+    rules_list:           '.rules-group-body>.rules-list',
+
+    group_condition:      '.rules-group-header [name$=_cond]',
+    rule_filter:          '.rule-filter-container [name$=_filter]',
+    rule_operator:        '.rule-operator-container [name$=_operator]',
+    rule_value:           '.rule-value-container [name*=_value_]',
+
+    add_rule:             '[data-add=rule]',
+    delete_rule:          '[data-delete=rule]',
+    add_group:            '[data-add=group]',
+    delete_group:         '[data-delete=group]'
+};
+
+/**
+ * Template strings (see template.js)
+ * @type {object.<string, string>}
+ * @readonly
+ */
+QueryBuilder.templates = {};
+
+/**
+ * Localized strings (see i18n/)
+ * @type {object.<string, object>}
+ * @readonly
+ */
+QueryBuilder.regional = {};
+
+/**
+ * Default operators
+ * @type {object.<string, object>}
+ * @readonly
+ */
+QueryBuilder.OPERATORS = {
+    equal:            { type: 'equal',            nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
+    not_equal:        { type: 'not_equal',        nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
+    in:               { type: 'in',               nb_inputs: 1, multiple: true,  apply_to: ['string', 'number', 'datetime', 'map'] },
+    not_in:           { type: 'not_in',           nb_inputs: 1, multiple: true,  apply_to: ['string', 'number', 'datetime', 'map'] },
+    less:             { type: 'less',             nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+    less_or_equal:    { type: 'less_or_equal',    nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+    greater:          { type: 'greater',          nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+    greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
+    between:          { type: 'between',          nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+    not_between:      { type: 'not_between',      nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
+    begins_with:      { type: 'begins_with',      nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    not_begins_with:  { type: 'not_begins_with',  nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    contains:         { type: 'contains',         nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    not_contains:     { type: 'not_contains',     nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    ends_with:        { type: 'ends_with',        nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    not_ends_with:    { type: 'not_ends_with',    nb_inputs: 1, multiple: false, apply_to: ['string', 'map'] },
+    is_empty:         { type: 'is_empty',         nb_inputs: 0, multiple: false, apply_to: ['string', 'map'] },
+    is_not_empty:     { type: 'is_not_empty',     nb_inputs: 0, multiple: false, apply_to: ['string', 'map'] },
+    is_null:          { type: 'is_null',          nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] },
+    is_not_null:      { type: 'is_not_null',      nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean', 'map'] }
+};
+
+/**
+ * Default configuration
+ * @type {object}
+ * @readonly
+ */
+QueryBuilder.DEFAULTS = {
+    filters: [],
+    plugins: [],
+
+    sort_filters: false,
+    display_errors: true,
+    allow_groups: -1,
+    allow_empty: true,
+    conditions: ['AND', 'OR'],
+    default_condition: 'AND',
+    inputs_separator: ' , ',
+    select_placeholder: '------',
+    display_empty_filter: true,
+    default_filter: null,
+    optgroups: {},
+
+    default_rule_flags: {
+        filter_readonly: false,
+        operator_readonly: false,
+        value_readonly: false,
+        no_delete: false
+    },
+
+    default_group_flags: {
+        condition_readonly: false,
+        no_add_rule: false,
+        no_add_group: false,
+        no_delete: false
+    },
+
+    templates: {
+        group: null,
+        rule: null,
+        filterSelect: null,
+        operatorSelect: null,
+        ruleValueSelect: null
+    },
+
+    lang_code: 'en',
+    lang: {},
+
+    operators: [
+        'equal',
+        'not_equal',
+        'in',
+        'not_in',
+        'less',
+        'less_or_equal',
+        'greater',
+        'greater_or_equal',
+        'between',
+        'not_between',
+        'begins_with',
+        'not_begins_with',
+        'contains',
+        'not_contains',
+        'ends_with',
+        'not_ends_with',
+        'is_empty',
+        'is_not_empty',
+        'is_null',
+        'is_not_null'
+    ],
+
+    icons: {
+        add_group:    'glyphicon glyphicon-plus-sign',
+        add_rule:     'glyphicon glyphicon-plus',
+        remove_group: 'glyphicon glyphicon-trash',
+        remove_rule:  'glyphicon glyphicon-trash',
+        error:        'glyphicon glyphicon-warning-sign'
+    }
+};
+
+
+/**
+ * @module plugins
+ */
+
+/**
+ * Definition of available plugins
+ * @type {object.<String, object>}
+ */
+QueryBuilder.plugins = {};
+
+/**
+ * Gets or extends the default configuration
+ * @param {object} [options] - new configuration
+ * @returns {undefined|object} nothing or configuration object (copy)
+ */
+QueryBuilder.defaults = function(options) {
+    if (typeof options == 'object') {
+        $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
+    }
+    else if (typeof options == 'string') {
+        if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
+            return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
+        }
+        else {
+            return QueryBuilder.DEFAULTS[options];
+        }
+    }
+    else {
+        return $.extend(true, {}, QueryBuilder.DEFAULTS);
+    }
+};
+
+/**
+ * Registers a new plugin
+ * @param {string} name
+ * @param {function} fct - init function
+ * @param {object} [def] - default options
+ */
+QueryBuilder.define = function(name, fct, def) {
+    QueryBuilder.plugins[name] = {
+        fct: fct,
+        def: def || {}
+    };
+};
+
+/**
+ * Adds new methods to QueryBuilder prototype
+ * @param {object.<string, function>} methods
+ */
+QueryBuilder.extend = function(methods) {
+    $.extend(QueryBuilder.prototype, methods);
+};
+
+/**
+ * Initializes plugins for an instance
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype.initPlugins = function() {
+    if (!this.plugins) {
+        return;
+    }
+
+    if ($.isArray(this.plugins)) {
+        var tmp = {};
+        this.plugins.forEach(function(plugin) {
+            tmp[plugin] = null;
+        });
+        this.plugins = tmp;
+    }
+
+    Object.keys(this.plugins).forEach(function(plugin) {
+        if (plugin in QueryBuilder.plugins) {
+            this.plugins[plugin] = $.extend(true, {},
+                QueryBuilder.plugins[plugin].def,
+                this.plugins[plugin] || {}
+            );
+
+            QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
+        }
+        else {
+            Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
+        }
+    }, this);
+};
+
+/**
+ * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
+ * @param {string} name
+ * @param {string} [property]
+ * @throws ConfigError
+ * @returns {*}
+ */
+QueryBuilder.prototype.getPluginOptions = function(name, property) {
+    var plugin;
+    if (this.plugins && this.plugins[name]) {
+        plugin = this.plugins[name];
+    }
+    else if (QueryBuilder.plugins[name]) {
+        plugin = QueryBuilder.plugins[name].def;
+    }
+
+    if (plugin) {
+        if (property) {
+            return plugin[property];
+        }
+        else {
+            return plugin;
+        }
+    }
+    else {
+        Utils.error('Config', 'Unable to find plugin "{0}"', name);
+    }
+};
+
+
+/**
+ * Final initialisation of the builder
+ * @param {object} [rules]
+ * @fires QueryBuilder.afterInit
+ * @private
+ */
+QueryBuilder.prototype.init = function(rules) {
+    /**
+     * When the initilization is done, just before creating the root group
+     * @event afterInit
+     * @memberof QueryBuilder
+     */
+    this.trigger('afterInit');
+
+    if (rules) {
+        this.setRules(rules);
+        delete this.settings.rules;
+    }
+    else {
+        this.setRoot(true);
+    }
+};
+
+/**
+ * Checks the configuration of each filter
+ * @param {QueryBuilder.Filter[]} filters
+ * @returns {QueryBuilder.Filter[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkFilters = function(filters) {
+    var definedFilters = [];
+
+    if (!filters || filters.length === 0) {
+        Utils.error('Config', 'Missing filters list');
+    }
+
+    filters.forEach(function(filter, i) {
+        if (!filter.id) {
+            Utils.error('Config', 'Missing filter {0} id', i);
+        }
+        if (definedFilters.indexOf(filter.id) != -1) {
+            Utils.error('Config', 'Filter "{0}" already defined', filter.id);
+        }
+        definedFilters.push(filter.id);
+
+        if (!filter.type) {
+            filter.type = 'string';
+        }
+        else if (!QueryBuilder.types[filter.type]) {
+            Utils.error('Config', 'Invalid type "{0}"', filter.type);
+        }
+
+        if (!filter.input) {
+            filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
+        }
+        else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
+            Utils.error('Config', 'Invalid input "{0}"', filter.input);
+        }
+
+        if (filter.operators) {
+            filter.operators.forEach(function(operator) {
+                if (typeof operator != 'string') {
+                    Utils.error('Config', 'Filter operators must be global operators types (string)');
+                }
+            });
+        }
+
+        if (!filter.field) {
+            filter.field = filter.id;
+        }
+        if (!filter.label) {
+            filter.label = filter.field;
+        }
+
+        if (!filter.optgroup) {
+            filter.optgroup = null;
+        }
+        else {
+            this.status.has_optgroup = true;
+
+            // register optgroup if needed
+            if (!this.settings.optgroups[filter.optgroup]) {
+                this.settings.optgroups[filter.optgroup] = filter.optgroup;
+            }
+        }
+
+        switch (filter.input) {
+            case 'radio':
+            case 'checkbox':
+                if (!filter.values || filter.values.length < 1) {
+                    Utils.error('Config', 'Missing filter "{0}" values', filter.id);
+                }
+                break;
+
+            case 'select':
+                var cleanValues = [];
+                filter.has_optgroup = false;
+
+                Utils.iterateOptions(filter.values, function(value, label, optgroup) {
+                    cleanValues.push({
+                        value: value,
+                        label: label,
+                        optgroup: optgroup || null
+                    });
+
+                    if (optgroup) {
+                        filter.has_optgroup = true;
+
+                        // register optgroup if needed
+                        if (!this.settings.optgroups[optgroup]) {
+                            this.settings.optgroups[optgroup] = optgroup;
+                        }
+                    }
+                }.bind(this));
+
+                if (filter.has_optgroup) {
+                    filter.values = Utils.groupSort(cleanValues, 'optgroup');
+                }
+                else {
+                    filter.values = cleanValues;
+                }
+
+                if (filter.placeholder) {
+                    if (filter.placeholder_value === undefined) {
+                        filter.placeholder_value = -1;
+                    }
+
+                    filter.values.forEach(function(entry) {
+                        if (entry.value == filter.placeholder_value) {
+                            Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
+                        }
+                    });
+                }
+                break;
+        }
+    }, this);
+
+    if (this.settings.sort_filters) {
+        if (typeof this.settings.sort_filters == 'function') {
+            filters.sort(this.settings.sort_filters);
+        }
+        else {
+            var self = this;
+            filters.sort(function(a, b) {
+                return self.translate(a.label).localeCompare(self.translate(b.label));
+            });
+        }
+    }
+
+    if (this.status.has_optgroup) {
+        filters = Utils.groupSort(filters, 'optgroup');
+    }
+
+    return filters;
+};
+
+/**
+ * Checks the configuration of each operator
+ * @param {QueryBuilder.Operator[]} operators
+ * @returns {QueryBuilder.Operator[]}
+ * @throws ConfigError
+ */
+QueryBuilder.prototype.checkOperators = function(operators) {
+    var definedOperators = [];
+
+    operators.forEach(function(operator, i) {
+        if (typeof operator == 'string') {
+            if (!QueryBuilder.OPERATORS[operator]) {
+                Utils.error('Config', 'Unknown operator "{0}"', operator);
+            }
+
+            operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
+        }
+        else {
+            if (!operator.type) {
+                Utils.error('Config', 'Missing "type" for operator {0}', i);
+            }
+
+            if (QueryBuilder.OPERATORS[operator.type]) {
+                operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
+            }
+
+            if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
+                Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
+            }
+        }
+
+        if (definedOperators.indexOf(operator.type) != -1) {
+            Utils.error('Config', 'Operator "{0}" already defined', operator.type);
+        }
+        definedOperators.push(operator.type);
+
+        if (!operator.optgroup) {
+            operator.optgroup = null;
+        }
+        else {
+            this.status.has_operator_optgroup = true;
+
+            // register optgroup if needed
+            if (!this.settings.optgroups[operator.optgroup]) {
+                this.settings.optgroups[operator.optgroup] = operator.optgroup;
+            }
+        }
+    }, this);
+
+    if (this.status.has_operator_optgroup) {
+        operators = Utils.groupSort(operators, 'optgroup');
+    }
+
+    return operators;
+};
+
+/**
+ * Adds all events listeners to the builder
+ * @private
+ */
+QueryBuilder.prototype.bindEvents = function() {
+    var self = this;
+    var Selectors = QueryBuilder.selectors;
+
+    // group condition change
+    this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
+        if ($(this).is(':checked')) {
+            var $group = $(this).closest(Selectors.group_container);
+            self.getModel($group).condition = $(this).val();
+        }
+    });
+
+    // rule filter change
+    this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
+        var $rule = $(this).closest(Selectors.rule_container);
+        self.getModel($rule).filter = self.getFilterById($(this).val());
+    });
+
+    // rule operator change
+    this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
+        var $rule = $(this).closest(Selectors.rule_container);
+        self.getModel($rule).operator = self.getOperatorByType($(this).val());
+    });
+
+    // add rule button
+    this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
+        var $group = $(this).closest(Selectors.group_container);
+        self.addRule(self.getModel($group));
+    });
+
+    // delete rule button
+    this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
+        var $rule = $(this).closest(Selectors.rule_container);
+        self.deleteRule(self.getModel($rule));
+    });
+
+    if (this.settings.allow_groups !== 0) {
+        // add group button
+        this.$el.on('click.queryBuilder', Selectors.add_group, function() {
+            var $group = $(this).closest(Selectors.group_container);
+            self.addGroup(self.getModel($group));
+        });
+
+        // delete group button
+        this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
+            var $group = $(this).closest(Selectors.group_container);
+            self.deleteGroup(self.getModel($group));
+        });
+    }
+
+    // model events
+    this.model.on({
+        'drop': function(e, node) {
+            node.$el.remove();
+            self.refreshGroupsConditions();
+        },
+        'add': function(e, parent, node, index) {
+            if (index === 0) {
+                node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
+            }
+            else {
+                node.$el.insertAfter(parent.rules[index - 1].$el);
+            }
+            self.refreshGroupsConditions();
+        },
+        'move': function(e, node, group, index) {
+            node.$el.detach();
+
+            if (index === 0) {
+                node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
+            }
+            else {
+                node.$el.insertAfter(group.rules[index - 1].$el);
+            }
+            self.refreshGroupsConditions();
+        },
+        'update': function(e, node, field, value, oldValue) {
+            if (node instanceof Rule) {
+                switch (field) {
+                    case 'error':
+                        self.updateError(node);
+                        break;
+
+                    case 'flags':
+                        self.applyRuleFlags(node);
+                        break;
+
+                    case 'filter':
+                        self.updateRuleFilter(node, oldValue);
+                        break;
+
+                    case 'operator':
+                        self.updateRuleOperator(node, oldValue);
+                        break;
+
+                    case 'value':
+                        self.updateRuleValue(node, oldValue);
+                        break;
+                }
+            }
+            else {
+                switch (field) {
+                    case 'error':
+                        self.updateError(node);
+                        break;
+
+                    case 'flags':
+                        self.applyGroupFlags(node);
+                        break;
+
+                    case 'condition':
+                        self.updateGroupCondition(node, oldValue);
+                        break;
+                }
+            }
+        }
+    });
+};
+
+/**
+ * Creates the root group
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group} root group
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
+    addRule = (addRule === undefined || addRule === true);
+
+    var group_id = this.nextGroupId();
+    var $group = $(this.getGroupTemplate(group_id, 1));
+
+    this.$el.append($group);
+    this.model.root = new Group(null, $group);
+    this.model.root.model = this.model;
+
+    this.model.root.data = data;
+    this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
+    this.model.root.condition = this.settings.default_condition;
+
+    this.trigger('afterAddGroup', this.model.root);
+
+    if (addRule) {
+        this.addRule(this.model.root);
+    }
+
+    return this.model.root;
+};
+
+/**
+ * Adds a new group
+ * @param {Group} parent
+ * @param {boolean} [addRule=true] - adds a default empty rule
+ * @param {object} [data] - group custom data
+ * @param {object} [flags] - flags to apply to the group
+ * @returns {Group}
+ * @fires QueryBuilder.beforeAddGroup
+ * @fires QueryBuilder.afterAddGroup
+ */
+QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
+    addRule = (addRule === undefined || addRule === true);
+
+    var level = parent.level + 1;
+
+    /**
+     * Just before adding a group, can be prevented.
+     * @event beforeAddGroup
+     * @memberof QueryBuilder
+     * @param {Group} parent
+     * @param {boolean} addRule - if an empty rule will be added in the group
+     * @param {int} level - nesting level of the group, 1 is the root group
+     */
+    var e = this.trigger('beforeAddGroup', parent, addRule, level);
+    if (e.isDefaultPrevented()) {
+        return null;
+    }
+
+    var group_id = this.nextGroupId();
+    var $group = $(this.getGroupTemplate(group_id, level));
+    var model = parent.addGroup($group);
+
+    model.data = data;
+    model.flags = $.extend({}, this.settings.default_group_flags, flags);
+    model.condition = this.settings.default_condition;
+
+    /**
+     * Just after adding a group
+     * @event afterAddGroup
+     * @memberof QueryBuilder
+     * @param {Group} group
+     */
+    this.trigger('afterAddGroup', model);
+
+    /**
+     * After any change in the rules
+     * @event rulesChanged
+     * @memberof QueryBuilder
+     */
+    this.trigger('rulesChanged');
+
+    if (addRule) {
+        this.addRule(model);
+    }
+
+    return model;
+};
+
+/**
+ * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
+ * @param {Group} group
+ * @returns {boolean} if the group has been deleted
+ * @fires QueryBuilder.beforeDeleteGroup
+ * @fires QueryBuilder.afterDeleteGroup
+ */
+QueryBuilder.prototype.deleteGroup = function(group) {
+    if (group.isRoot()) {
+        return false;
+    }
+
+    /**
+     * Just before deleting a group, can be prevented
+     * @event beforeDeleteGroup
+     * @memberof QueryBuilder
+     * @param {Group} parent
+     */
+    var e = this.trigger('beforeDeleteGroup', group);
+    if (e.isDefaultPrevented()) {
+        return false;
+    }
+
+    var del = true;
+
+    group.each('reverse', function(rule) {
+        del &= this.deleteRule(rule);
+    }, function(group) {
+        del &= this.deleteGroup(group);
+    }, this);
+
+    if (del) {
+        group.drop();
+
+        /**
+         * Just after deleting a group
+         * @event afterDeleteGroup
+         * @memberof QueryBuilder
+         */
+        this.trigger('afterDeleteGroup');
+
+        this.trigger('rulesChanged');
+    }
+
+    return del;
+};
+
+/**
+ * Performs actions when a group's condition changes
+ * @param {Group} group
+ * @param {object} previousCondition
+ * @fires QueryBuilder.afterUpdateGroupCondition
+ * @private
+ */
+QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
+    group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
+        var $this = $(this);
+        $this.prop('checked', $this.val() === group.condition);
+        $this.parent().toggleClass('active', $this.val() === group.condition);
+    });
+
+    /**
+     * After the group condition has been modified
+     * @event afterUpdateGroupCondition
+     * @memberof QueryBuilder
+     * @param {Group} group
+     * @param {object} previousCondition
+     */
+    this.trigger('afterUpdateGroupCondition', group, previousCondition);
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Updates the visibility of conditions based on number of rules inside each group
+ * @private
+ */
+QueryBuilder.prototype.refreshGroupsConditions = function() {
+    (function walk(group) {
+        if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
+            group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
+                .parent().toggleClass('disabled', group.rules.length <= 1);
+        }
+
+        group.each(null, function(group) {
+            walk(group);
+        }, this);
+    }(this.model.root));
+};
+
+/**
+ * Adds a new rule
+ * @param {Group} parent
+ * @param {object} [data] - rule custom data
+ * @param {object} [flags] - flags to apply to the rule
+ * @returns {Rule}
+ * @fires QueryBuilder.beforeAddRule
+ * @fires QueryBuilder.afterAddRule
+ * @fires QueryBuilder.changer:getDefaultFilter
+ */
+QueryBuilder.prototype.addRule = function(parent, data, flags) {
+    /**
+     * Just before adding a rule, can be prevented
+     * @event beforeAddRule
+     * @memberof QueryBuilder
+     * @param {Group} parent
+     */
+    var e = this.trigger('beforeAddRule', parent);
+    if (e.isDefaultPrevented()) {
+        return null;
+    }
+
+    var rule_id = this.nextRuleId();
+    var $rule = $(this.getRuleTemplate(rule_id));
+    var model = parent.addRule($rule);
+
+    model.data = data;
+    model.flags = $.extend({}, this.settings.default_rule_flags, flags);
+
+    /**
+     * Just after adding a rule
+     * @event afterAddRule
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     */
+    this.trigger('afterAddRule', model);
+
+    this.trigger('rulesChanged');
+
+    this.createRuleFilters(model);
+
+    if (this.settings.default_filter || !this.settings.display_empty_filter) {
+        /**
+         * Modifies the default filter for a rule
+         * @event changer:getDefaultFilter
+         * @memberof QueryBuilder
+         * @param {QueryBuilder.Filter} filter
+         * @param {Rule} rule
+         * @returns {QueryBuilder.Filter}
+         */
+        model.filter = this.change('getDefaultFilter',
+            this.getFilterById(this.settings.default_filter || this.filters[0].id),
+            model
+        );
+    }
+
+    return model;
+};
+
+/**
+ * Tries to delete a rule
+ * @param {Rule} rule
+ * @returns {boolean} if the rule has been deleted
+ * @fires QueryBuilder.beforeDeleteRule
+ * @fires QueryBuilder.afterDeleteRule
+ */
+QueryBuilder.prototype.deleteRule = function(rule) {
+    if (rule.flags.no_delete) {
+        return false;
+    }
+
+    /**
+     * Just before deleting a rule, can be prevented
+     * @event beforeDeleteRule
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     */
+    var e = this.trigger('beforeDeleteRule', rule);
+    if (e.isDefaultPrevented()) {
+        return false;
+    }
+
+    rule.drop();
+
+    /**
+     * Just after deleting a rule
+     * @event afterDeleteRule
+     * @memberof QueryBuilder
+     */
+    this.trigger('afterDeleteRule');
+
+    this.trigger('rulesChanged');
+
+    return true;
+};
+
+/**
+ * Creates the filters for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.changer:getRuleFilters
+ * @fires QueryBuilder.afterCreateRuleFilters
+ * @private
+ */
+QueryBuilder.prototype.createRuleFilters = function(rule) {
+    /**
+     * Modifies the list a filters available for a rule
+     * @event changer:getRuleFilters
+     * @memberof QueryBuilder
+     * @param {QueryBuilder.Filter[]} filters
+     * @param {Rule} rule
+     * @returns {QueryBuilder.Filter[]}
+     */
+    var filters = this.change('getRuleFilters', this.filters, rule);
+    var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
+
+    rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
+
+    /**
+     * After creating the dropdown for filters
+     * @event afterCreateRuleFilters
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     */
+    this.trigger('afterCreateRuleFilters', rule);
+
+    this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the operators for a rule and init the rule operator
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleOperators
+ * @private
+ */
+QueryBuilder.prototype.createRuleOperators = function(rule) {
+    var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
+
+    if (!rule.filter) {
+        return;
+    }
+
+    var operators = this.getOperators(rule.filter);
+    var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
+
+    $operatorContainer.html($operatorSelect);
+
+    // set the operator without triggering update event
+    if (rule.filter.default_operator) {
+        rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
+    }
+    else {
+        rule.__.operator = operators[0];
+    }
+
+    rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+    /**
+     * After creating the dropdown for operators
+     * @event afterCreateRuleOperators
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
+     */
+    this.trigger('afterCreateRuleOperators', rule, operators);
+
+    this.applyRuleFlags(rule);
+};
+
+/**
+ * Creates the main input for a rule
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterCreateRuleInput
+ * @private
+ */
+QueryBuilder.prototype.createRuleInput = function(rule) {
+    var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
+
+    rule.__.value = undefined;
+
+    if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
+        return;
+    }
+
+    var self = this;
+    var $inputs = $();
+    var filter = rule.filter;
+
+    if(filter.type === 'map') {
+       for (var i = 0; i < 2; i++) {
+            var $ruleInput = $(this.getRuleInput(rule, i));
+            if (i > 0) $valueContainer.append('|');
+            $valueContainer.append($ruleInput);
+            $inputs = $inputs.add($ruleInput);
+        }
+    } else {
+       for (var i = 0; i < rule.operator.nb_inputs; i++) {
+            var $ruleInput = $(this.getRuleInput(rule, i));
+            if (i > 0) $valueContainer.append(this.settings.inputs_separator);
+            $valueContainer.append($ruleInput);
+            $inputs = $inputs.add($ruleInput);
+       }
+    }
+
+    $valueContainer.css('display', '');
+
+    $inputs.on('change ' + (filter.input_event || ''), function() {
+        if (!rule._updating_input) {
+            rule._updating_value = true;
+            rule.value = self.getRuleInputValue(rule);
+            rule._updating_value = false;
+        }
+    });
+
+    if (filter.plugin) {
+        $inputs[filter.plugin](filter.plugin_config || {});
+    }
+
+    /**
+     * After creating the input for a rule and initializing optional plugin
+     * @event afterCreateRuleInput
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     */
+    this.trigger('afterCreateRuleInput', rule);
+
+    if (filter.default_value !== undefined) {
+        rule.value = filter.default_value;
+    }
+    else {
+        rule._updating_value = true;
+        rule.value = self.getRuleInputValue(rule);
+        rule._updating_value = false;
+    }
+
+    this.applyRuleFlags(rule);
+};
+
+/**
+ * Performs action when a rule's filter changes
+ * @param {Rule} rule
+ * @param {object} previousFilter
+ * @fires QueryBuilder.afterUpdateRuleFilter
+ * @private
+ */
+QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
+    this.createRuleOperators(rule);
+    this.createRuleInput(rule);
+
+    rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+
+    // clear rule data if the filter changed
+    if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
+        rule.data = undefined;
+    }
+
+    /**
+     * After the filter has been updated and the operators and input re-created
+     * @event afterUpdateRuleFilter
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     * @param {object} previousFilter
+     */
+    this.trigger('afterUpdateRuleFilter', rule, previousFilter);
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when a rule's operator changes
+ * @param {Rule} rule
+ * @param {object} previousOperator
+ * @fires QueryBuilder.afterUpdateRuleOperator
+ * @private
+ */
+QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
+    var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
+
+    if (!rule.operator || rule.operator.nb_inputs === 0) {
+        $valueContainer.hide();
+
+        rule.__.value = undefined;
+    }
+    else {
+        $valueContainer.css('display', '');
+
+        if ($valueContainer.is(':empty') || !previousOperator ||
+            rule.operator.nb_inputs !== previousOperator.nb_inputs ||
+            rule.operator.optgroup !== previousOperator.optgroup
+        ) {
+            this.createRuleInput(rule);
+        }
+    }
+
+    if (rule.operator) {
+        rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
+
+        // refresh value if the format changed for this operator
+        rule.__.value = this.getRuleInputValue(rule);
+    }
+
+    /**
+     *  After the operator has been updated and the input optionally re-created
+     * @event afterUpdateRuleOperator
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     * @param {object} previousOperator
+     */
+    this.trigger('afterUpdateRuleOperator', rule, previousOperator);
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Performs actions when rule's value changes
+ * @param {Rule} rule
+ * @param {object} previousValue
+ * @fires QueryBuilder.afterUpdateRuleValue
+ * @private
+ */
+QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
+    if (!rule._updating_value) {
+        this.setRuleInputValue(rule, rule.value);
+    }
+
+    /**
+     * After the rule value has been modified
+     * @event afterUpdateRuleValue
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     * @param {*} previousValue
+     */
+    this.trigger('afterUpdateRuleValue', rule, previousValue);
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Changes a rule's properties depending on its flags
+ * @param {Rule} rule
+ * @fires QueryBuilder.afterApplyRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.applyRuleFlags = function(rule) {
+    var flags = rule.flags;
+    var Selectors = QueryBuilder.selectors;
+
+    rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
+    rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
+    rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
+
+    if (flags.no_delete) {
+        rule.$el.find(Selectors.delete_rule).remove();
+    }
+
+    /**
+     * After rule's flags has been applied
+     * @event afterApplyRuleFlags
+     * @memberof QueryBuilder
+     * @param {Rule} rule
+     */
+    this.trigger('afterApplyRuleFlags', rule);
+};
+
+/**
+ * Changes group's properties depending on its flags
+ * @param {Group} group
+ * @fires QueryBuilder.afterApplyGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.applyGroupFlags = function(group) {
+    var flags = group.flags;
+    var Selectors = QueryBuilder.selectors;
+
+    group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
+        .parent().toggleClass('readonly', flags.condition_readonly);
+
+    if (flags.no_add_rule) {
+        group.$el.find(Selectors.add_rule).remove();
+    }
+    if (flags.no_add_group) {
+        group.$el.find(Selectors.add_group).remove();
+    }
+    if (flags.no_delete) {
+        group.$el.find(Selectors.delete_group).remove();
+    }
+
+    /**
+     * After group's flags has been applied
+     * @event afterApplyGroupFlags
+     * @memberof QueryBuilder
+     * @param {Group} group
+     */
+    this.trigger('afterApplyGroupFlags', group);
+};
+
+/**
+ * Clears all errors markers
+ * @param {Node} [node] default is root Group
+ */
+QueryBuilder.prototype.clearErrors = function(node) {
+    node = node || this.model.root;
+
+    if (!node) {
+        return;
+    }
+
+    node.error = null;
+
+    if (node instanceof Group) {
+        node.each(function(rule) {
+            rule.error = null;
+        }, function(group) {
+            this.clearErrors(group);
+        }, this);
+    }
+};
+
+/**
+ * Adds/Removes error on a Rule or Group
+ * @param {Node} node
+ * @fires QueryBuilder.changer:displayError
+ * @private
+ */
+QueryBuilder.prototype.updateError = function(node) {
+    if (this.settings.display_errors) {
+        if (node.error === null) {
+            node.$el.removeClass('has-error');
+        }
+        else {
+            var errorMessage = this.translate('errors', node.error[0]);
+            errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
+
+            /**
+             * Modifies an error message before display
+             * @event changer:displayError
+             * @memberof QueryBuilder
+             * @param {string} errorMessage - the error message (translated and formatted)
+             * @param {array} error - the raw error array (error code and optional arguments)
+             * @param {Node} node
+             * @returns {string}
+             */
+            errorMessage = this.change('displayError', errorMessage, node.error, node);
+
+            node.$el.addClass('has-error')
+                .find(QueryBuilder.selectors.error_container).eq(0)
+                .attr('title', errorMessage);
+        }
+    }
+};
+
+/**
+ * Triggers a validation error event
+ * @param {Node} node
+ * @param {string|array} error
+ * @param {*} value
+ * @fires QueryBuilder.validationError
+ * @private
+ */
+QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
+    if (!$.isArray(error)) {
+        error = [error];
+    }
+
+    /**
+     * Fired when a validation error occurred, can be prevented
+     * @event validationError
+     * @memberof QueryBuilder
+     * @param {Node} node
+     * @param {string} error
+     * @param {*} value
+     */
+    var e = this.trigger('validationError', node, error, value);
+    if (!e.isDefaultPrevented()) {
+        node.error = error;
+    }
+};
+
+
+/**
+ * Destroys the builder
+ * @fires QueryBuilder.beforeDestroy
+ */
+QueryBuilder.prototype.destroy = function() {
+    /**
+     * Before the {@link QueryBuilder#destroy} method
+     * @event beforeDestroy
+     * @memberof QueryBuilder
+     */
+    this.trigger('beforeDestroy');
+
+    if (this.status.generated_id) {
+        this.$el.removeAttr('id');
+    }
+
+    this.clear();
+    this.model = null;
+
+    this.$el
+        .off('.queryBuilder')
+        .removeClass('query-builder')
+        .removeData('queryBuilder');
+
+    delete this.$el[0].queryBuilder;
+};
+
+/**
+ * Clear all rules and resets the root group
+ * @fires QueryBuilder.beforeReset
+ * @fires QueryBuilder.afterReset
+ */
+QueryBuilder.prototype.reset = function() {
+    /**
+     * Before the {@link QueryBuilder#reset} method, can be prevented
+     * @event beforeReset
+     * @memberof QueryBuilder
+     */
+    var e = this.trigger('beforeReset');
+    if (e.isDefaultPrevented()) {
+        return;
+    }
+
+    this.status.group_id = 1;
+    this.status.rule_id = 0;
+
+    this.model.root.empty();
+
+    this.model.root.data = undefined;
+    this.model.root.flags = $.extend({}, this.settings.default_group_flags);
+    this.model.root.condition = this.settings.default_condition;
+
+    this.addRule(this.model.root);
+
+    /**
+     * After the {@link QueryBuilder#reset} method
+     * @event afterReset
+     * @memberof QueryBuilder
+     */
+    this.trigger('afterReset');
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Clears all rules and removes the root group
+ * @fires QueryBuilder.beforeClear
+ * @fires QueryBuilder.afterClear
+ */
+QueryBuilder.prototype.clear = function() {
+    /**
+     * Before the {@link QueryBuilder#clear} method, can be prevented
+     * @event beforeClear
+     * @memberof QueryBuilder
+     */
+    var e = this.trigger('beforeClear');
+    if (e.isDefaultPrevented()) {
+        return;
+    }
+
+    this.status.group_id = 0;
+    this.status.rule_id = 0;
+
+    if (this.model.root) {
+        this.model.root.drop();
+        this.model.root = null;
+    }
+
+    /**
+     * After the {@link QueryBuilder#clear} method
+     * @event afterClear
+     * @memberof QueryBuilder
+     */
+    this.trigger('afterClear');
+
+    this.trigger('rulesChanged');
+};
+
+/**
+ * Modifies the builder configuration.<br>
+ * Only options defined in QueryBuilder.modifiable_options are modifiable
+ * @param {object} options
+ */
+QueryBuilder.prototype.setOptions = function(options) {
+    $.each(options, function(opt, value) {
+        if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
+            this.settings[opt] = value;
+        }
+    }.bind(this));
+};
+
+/**
+ * Returns the model associated to a DOM object, or the root model
+ * @param {jQuery} [target]
+ * @returns {Node}
+ */
+QueryBuilder.prototype.getModel = function(target) {
+    if (!target) {
+        return this.model.root;
+    }
+    else if (target instanceof Node) {
+        return target;
+    }
+    else {
+        return $(target).data('queryBuilderModel');
+    }
+};
+
+/**
+ * Validates the whole builder
+ * @param {object} [options]
+ * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
+ * @returns {boolean}
+ * @fires QueryBuilder.changer:validate
+ */
+QueryBuilder.prototype.validate = function(options) {
+    options = $.extend({
+        skip_empty: false
+    }, options);
+
+    this.clearErrors();
+
+    var self = this;
+
+    var valid = (function parse(group) {
+        var done = 0;
+        var errors = 0;
+
+        group.each(function(rule) {
+            if (!rule.filter && options.skip_empty) {
+                return;
+            }
+
+            if (!rule.filter) {
+                self.triggerValidationError(rule, 'no_filter', null);
+                errors++;
+                return;
+            }
+
+            if (!rule.operator) {
+                self.triggerValidationError(rule, 'no_operator', null);
+                errors++;
+                return;
+            }
+
+            if (rule.operator.nb_inputs !== 0) {
+                var valid = self.validateValue(rule, rule.value);
+
+                if (valid !== true) {
+                    self.triggerValidationError(rule, valid, rule.value);
+                    errors++;
+                    return;
+                }
+            }
+
+            done++;
+
+        }, function(group) {
+            var res = parse(group);
+            if (res === true) {
+                done++;
+            }
+            else if (res === false) {
+                errors++;
+            }
+        });
+
+        if (errors > 0) {
+            return false;
+        }
+        else if (done === 0 && !group.isRoot() && options.skip_empty) {
+            return null;
+        }
+        else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
+            self.triggerValidationError(group, 'empty_group', null);
+            return false;
+        }
+
+        return true;
+
+    }(this.model.root));
+
+    /**
+     * Modifies the result of the {@link QueryBuilder#validate} method
+     * @event changer:validate
+     * @memberof QueryBuilder
+     * @param {boolean} valid
+     * @returns {boolean}
+     */
+    return this.change('validate', valid);
+};
+
+/**
+ * Gets an object representing current rules
+ * @param {object} [options]
+ * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
+ * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
+ * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
+ * @returns {object}
+ * @fires QueryBuilder.changer:ruleToJson
+ * @fires QueryBuilder.changer:groupToJson
+ * @fires QueryBuilder.changer:getRules
+ */
+QueryBuilder.prototype.getRules = function(options) {
+    options = $.extend({
+        get_flags: false,
+        allow_invalid: false,
+        skip_empty: false
+    }, options);
+
+    var valid = this.validate(options);
+    if (!valid && !options.allow_invalid) {
+        return null;
+    }
+
+    var self = this;
+
+    var out = (function parse(group) {
+        var groupData = {
+            condition: group.condition,
+            rules: []
+        };
+
+        if (group.data) {
+            groupData.data = $.extendext(true, 'replace', {}, group.data);
+        }
+
+        if (options.get_flags) {
+            var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
+            if (!$.isEmptyObject(flags)) {
+                groupData.flags = flags;
+            }
+        }
+
+        group.each(function(rule) {
+            if (!rule.filter && options.skip_empty) {
+                return;
+            }
+
+            var value = null;
+            if (!rule.operator || rule.operator.nb_inputs !== 0) {
+                value = rule.value;
+            }
+
+            var ruleData = {
+                id: rule.filter ? rule.filter.id : null,
+                field: rule.filter ? rule.filter.field : null,
+                type: rule.filter ? rule.filter.type : null,
+                input: rule.filter ? rule.filter.input : null,
+                operator: rule.operator ? rule.operator.type : null,
+                value: value
+            };
+
+            if (rule.filter && rule.filter.data || rule.data) {
+                ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
+            }
+
+            if (options.get_flags) {
+                var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
+                if (!$.isEmptyObject(flags)) {
+                    ruleData.flags = flags;
+                }
+            }
+
+            /**
+             * Modifies the JSON generated from a Rule object
+             * @event changer:ruleToJson
+             * @memberof QueryBuilder
+             * @param {object} json
+             * @param {Rule} rule
+             * @returns {object}
+             */
+            groupData.rules.push(self.change('ruleToJson', ruleData, rule));
+
+        }, function(model) {
+            var data = parse(model);
+            if (data.rules.length !== 0 || !options.skip_empty) {
+                groupData.rules.push(data);
+            }
+        }, this);
+
+        /**
+         * Modifies the JSON generated from a Group object
+         * @event changer:groupToJson
+         * @memberof QueryBuilder
+         * @param {object} json
+         * @param {Group} group
+         * @returns {object}
+         */
+        return self.change('groupToJson', groupData, group);
+
+    }(this.model.root));
+
+    out.valid = valid;
+
+    /**
+     * Modifies the result of the {@link QueryBuilder#getRules} method
+     * @event changer:getRules
+     * @memberof QueryBuilder
+     * @param {object} json
+     * @returns {object}
+     */
+    return this.change('getRules', out);
+};
+
+/**
+ * Sets rules from object
+ * @param {object} data
+ * @param {object} [options]
+ * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
+ * @throws RulesError, UndefinedConditionError
+ * @fires QueryBuilder.changer:setRules
+ * @fires QueryBuilder.changer:jsonToRule
+ * @fires QueryBuilder.changer:jsonToGroup
+ * @fires QueryBuilder.afterSetRules
+ */
+QueryBuilder.prototype.setRules = function(data, options) {
+    options = $.extend({
+        allow_invalid: false
+    }, options);
+
+    if ($.isArray(data)) {
+        data = {
+            condition: this.settings.default_condition,
+            rules: data
+        };
+    }
+
+    if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
+        Utils.error('RulesParse', 'Incorrect data object passed');
+    }
+
+    this.clear();
+    this.setRoot(false, data.data, this.parseGroupFlags(data));
+
+    /**
+     * Modifies data before the {@link QueryBuilder#setRules} method
+     * @event changer:setRules
+     * @memberof QueryBuilder
+     * @param {object} json
+     * @param {object} options
+     * @returns {object}
+     */
+    data = this.change('setRules', data, options);
+
+    var self = this;
+
+    (function add(data, group) {
+        if (group === null) {
+            return;
+        }
+
+        if (data.condition === undefined) {
+            data.condition = self.settings.default_condition;
+        }
+        else if (self.settings.conditions.indexOf(data.condition) == -1) {
+            Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
+            data.condition = self.settings.default_condition;
+        }
+
+        group.condition = data.condition;
+
+        data.rules.forEach(function(item) {
+            var model;
+
+            if (item.rules !== undefined) {
+                if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
+                    Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
+                    self.reset();
+                }
+                else {
+                    model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
+                    if (model === null) {
+                        return;
+                    }
+
+                    add(item, model);
+                }
+            }
+            else {
+                if (!item.empty) {
+                    if (item.id === undefined) {
+                        Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
+                        item.empty = true;
+                    }
+                    if (item.operator === undefined) {
+                        item.operator = 'equal';
+                    }
+                }
+
+                model = self.addRule(group, item.data, self.parseRuleFlags(item));
+                if (model === null) {
+                    return;
+                }
+
+                if (!item.empty) {
+                    model.filter = self.getFilterById(item.id, !options.allow_invalid);
+                }
+
+                if (model.filter) {
+                    model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
+
+                    if (!model.operator) {
+                        model.operator = self.getOperators(model.filter)[0];
+                    }
+                }
+
+                if (model.operator && model.operator.nb_inputs !== 0) {
+                    if (item.value !== undefined) {
+                       if(model.filter.type === 'map') {
+                               model.value = item.value.split('|')
+                       } else if (model.filter.type === 'datetime') {
+                               if (!('moment' in window)) {
+                                       Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+                               }
+                               model.value = moment(item.value * 1000).format('YYYY/MM/DD HH:mm:ss');
+                       } else {
+                               model.value = item.value;
+                       }
+                    }
+                    else if (model.filter.default_value !== undefined) {
+                        model.value = model.filter.default_value;
+                    }
+                }
+
+                /**
+                 * Modifies the Rule object generated from the JSON
+                 * @event changer:jsonToRule
+                 * @memberof QueryBuilder
+                 * @param {Rule} rule
+                 * @param {object} json
+                 * @returns {Rule} the same rule
+                 */
+                if (self.change('jsonToRule', model, item) != model) {
+                    Utils.error('RulesParse', 'Plugin tried to change rule reference');
+                }
+            }
+        });
+
+        /**
+         * Modifies the Group object generated from the JSON
+         * @event changer:jsonToGroup
+         * @memberof QueryBuilder
+         * @param {Group} group
+         * @param {object} json
+         * @returns {Group} the same group
+         */
+        if (self.change('jsonToGroup', group, data) != group) {
+            Utils.error('RulesParse', 'Plugin tried to change group reference');
+        }
+
+    }(data, this.model.root));
+
+    /**
+     * After the {@link QueryBuilder#setRules} method
+     * @event afterSetRules
+     * @memberof QueryBuilder
+     */
+    this.trigger('afterSetRules');
+};
+
+
+/**
+ * Performs value validation
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @fires QueryBuilder.changer:validateValue
+ */
+QueryBuilder.prototype.validateValue = function(rule, value) {
+    var validation = rule.filter.validation || {};
+    var result = true;
+
+    if (validation.callback) {
+        result = validation.callback.call(this, value, rule);
+    }
+    else {
+        result = this._validateValue(rule, value);
+    }
+
+    /**
+     * Modifies the result of the rule validation method
+     * @event changer:validateValue
+     * @memberof QueryBuilder
+     * @param {array|boolean} result - true or an error array
+     * @param {*} value
+     * @param {Rule} rule
+     * @returns {array|boolean}
+     */
+    return this.change('validateValue', result, value, rule);
+};
+
+/**
+ * Default validation function
+ * @param {Rule} rule
+ * @param {string|string[]} value
+ * @returns {array|boolean} true or error array
+ * @throws ConfigError
+ * @private
+ */
+QueryBuilder.prototype._validateValue = function(rule, value) {
+    var filter = rule.filter;
+    var operator = rule.operator;
+    var validation = filter.validation || {};
+    var result = true;
+    var tmp, tempValue;
+    var numOfInputs = operator.nb_inputs;
+    if(filter.type === 'map') {
+       numOfInputs = 2;
+    }
+
+    if (numOfInputs === 1) {
+        value = [value];
+    }
+
+    for (var i = 0; i < numOfInputs; i++) {
+        if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
+            result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
+            break;
+        }
+
+        switch (filter.input) {
+            case 'radio':
+                if (value[i] === undefined || value[i].length === 0) {
+                    if (!validation.allow_empty_value) {
+                        result = ['radio_empty'];
+                    }
+                    break;
+                }
+                break;
+
+            case 'checkbox':
+                if (value[i] === undefined || value[i].length === 0) {
+                    if (!validation.allow_empty_value) {
+                        result = ['checkbox_empty'];
+                    }
+                    break;
+                }
+                break;
+
+            case 'select':
+                if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
+                    if (!validation.allow_empty_value) {
+                        result = ['select_empty'];
+                    }
+                    break;
+                }
+                break;
+
+            default:
+                tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
+
+                for (var j = 0; j < tempValue.length; j++) {
+                    switch (QueryBuilder.types[filter.type]) {
+                        case 'string':
+                        case 'map':
+                            if (tempValue[j] === undefined || tempValue[j].length === 0) {
+                                if (!validation.allow_empty_value) {
+                                    result = ['string_empty'];
+                                }
+                                break;
+                            }
+                            if (validation.min !== undefined) {
+                                if (tempValue[j].length < parseInt(validation.min)) {
+                                    result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
+                                    break;
+                                }
+                            }
+                            if (validation.max !== undefined) {
+                                if (tempValue[j].length > parseInt(validation.max)) {
+                                    result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
+                                    break;
+                                }
+                            }
+                            if (validation.format) {
+                                if (typeof validation.format == 'string') {
+                                    validation.format = new RegExp(validation.format);
+                                }
+                                if (!validation.format.test(tempValue[j])) {
+                                    result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
+                                    break;
+                                }
+                            }
+                            break;
+
+                        case 'number':
+                            if (tempValue[j] === undefined || tempValue[j].length === 0) {
+                                if (!validation.allow_empty_value) {
+                                    result = ['number_nan'];
+                                }
+                                break;
+                            }
+                            if (isNaN(tempValue[j])) {
+                                result = ['number_nan'];
+                                break;
+                            }
+                            if (filter.type == 'integer') {
+                                if (parseInt(tempValue[j]) != tempValue[j]) {
+                                    result = ['number_not_integer'];
+                                    break;
+                                }
+                            }
+                            else {
+                                if (parseFloat(tempValue[j]) != tempValue[j]) {
+                                    result = ['number_not_double'];
+                                    break;
+                                }
+                            }
+                            if (validation.min !== undefined) {
+                                if (tempValue[j] < parseFloat(validation.min)) {
+                                    result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
+                                    break;
+                                }
+                            }
+                            if (validation.max !== undefined) {
+                                if (tempValue[j] > parseFloat(validation.max)) {
+                                    result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
+                                    break;
+                                }
+                            }
+                            if (validation.step !== undefined && validation.step !== 'any') {
+                                var v = (tempValue[j] / validation.step).toPrecision(14);
+                                if (parseInt(v) != v) {
+                                    result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
+                                    break;
+                                }
+                            }
+                            break;
+
+                        case 'datetime':
+                            if (tempValue[j] === undefined || tempValue[j].length === 0) {
+                                if (!validation.allow_empty_value) {
+                                    result = ['datetime_empty'];
+                                }
+                                break;
+                            }
+
+                            // we need MomentJS
+                            if (validation.format) {
+                                if (!('moment' in window)) {
+                                    Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+                                }
+
+                                var datetime = moment.utc(tempValue[j], validation.format, true);
+                                if (!datetime.isValid()) {
+                                    result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
+                                    break;
+                                }
+                                else {
+                                    if (validation.min) {
+                                        if (datetime < moment.utc(validation.min, validation.format, true)) {
+                                            result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
+                                            break;
+                                        }
+                                    }
+                                    if (validation.max) {
+                                        if (datetime > moment.utc(validation.max, validation.format, true)) {
+                                            result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                            break;
+
+                        case 'boolean':
+                            if (tempValue[j] === undefined || tempValue[j].length === 0) {
+                                if (!validation.allow_empty_value) {
+                                    result = ['boolean_not_valid'];
+                                }
+                                break;
+                            }
+                            tmp = ('' + tempValue[j]).trim().toLowerCase();
+                            if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
+                                result = ['boolean_not_valid'];
+                                break;
+                            }
+                    }
+
+                    if (result !== true) {
+                        break;
+                    }
+                }
+        }
+
+        if (result !== true) {
+            break;
+        }
+    }
+
+    if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
+        switch (QueryBuilder.types[filter.type]) {
+            case 'number':
+                if (value[0] > value[1]) {
+                    result = ['number_between_invalid', value[0], value[1]];
+                }
+                break;
+
+            case 'datetime':
+                // we need MomentJS
+                if (validation.format) {
+                    if (!('moment' in window)) {
+                        Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+                    }
+
+                    if (moment.utc(value[0], validation.format, true).isAfter(moment.utc(value[1], validation.format, true))) {
+                        result = ['datetime_between_invalid', value[0], value[1]];
+                    }
+                }
+                break;
+        }
+    }
+
+    return result;
+};
+
+/**
+ * Returns an incremented group ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextGroupId = function() {
+    return this.status.id + '_group_' + (this.status.group_id++);
+};
+
+/**
+ * Returns an incremented rule ID
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.nextRuleId = function() {
+    return this.status.id + '_rule_' + (this.status.rule_id++);
+};
+
+/**
+ * Returns the operators for a filter
+ * @param {string|object} filter - filter id or filter object
+ * @returns {object[]}
+ * @fires QueryBuilder.changer:getOperators
+ * @private
+ */
+QueryBuilder.prototype.getOperators = function(filter) {
+    if (typeof filter == 'string') {
+        filter = this.getFilterById(filter);
+    }
+
+    var result = [];
+
+    for (var i = 0, l = this.operators.length; i < l; i++) {
+        // filter operators check
+        if (filter.operators) {
+            if (filter.operators.indexOf(this.operators[i].type) == -1) {
+                continue;
+            }
+        }
+        // type check
+        else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
+            continue;
+        }
+
+        result.push(this.operators[i]);
+    }
+
+    // keep sort order defined for the filter
+    if (filter.operators) {
+        result.sort(function(a, b) {
+            return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
+        });
+    }
+
+    /**
+     * Modifies the operators available for a filter
+     * @event changer:getOperators
+     * @memberof QueryBuilder
+     * @param {QueryBuilder.Operator[]} operators
+     * @param {QueryBuilder.Filter} filter
+     * @returns {QueryBuilder.Operator[]}
+     */
+    return this.change('getOperators', result, filter);
+};
+
+/**
+ * Returns a particular filter by its id
+ * @param {string} id
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedFilterError
+ * @private
+ */
+QueryBuilder.prototype.getFilterById = function(id, doThrow) {
+    if (id == '-1') {
+        return null;
+    }
+
+    for (var i = 0, l = this.filters.length; i < l; i++) {
+        if (this.filters[i].id == id) {
+            return this.filters[i];
+        }
+    }
+
+    Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
+
+    return null;
+};
+
+/**
+ * Returns a particular operator by its type
+ * @param {string} type
+ * @param {boolean} [doThrow=true]
+ * @returns {object|null}
+ * @throws UndefinedOperatorError
+ * @private
+ */
+QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
+    if (type == '-1') {
+        return null;
+    }
+
+    for (var i = 0, l = this.operators.length; i < l; i++) {
+        if (this.operators[i].type == type) {
+            return this.operators[i];
+        }
+    }
+
+    Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
+
+    return null;
+};
+
+/**
+ * Returns rule's current input value
+ * @param {Rule} rule
+ * @returns {*}
+ * @fires QueryBuilder.changer:getRuleValue
+ * @private
+ */
+QueryBuilder.prototype.getRuleInputValue = function(rule) {
+    var filter = rule.filter;
+    var operator = rule.operator;
+    var numOfInputs = operator.nb_inputs;
+    if(filter.type === 'map') {
+       numOfInputs = 2;
+    }
+    var value = [];
+    
+    if (filter.valueGetter) {
+        value = filter.valueGetter.call(this, rule);
+    }
+    else {
+        var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+        for (var i = 0; i < numOfInputs; i++) {
+            var name = Utils.escapeElementId(rule.id + '_value_' + i);
+            var tmp;
+
+            switch (filter.input) {
+                case 'radio':
+                    value.push($value.find('[name=' + name + ']:checked').val());
+                    break;
+
+                case 'checkbox':
+                    tmp = [];
+                    // jshint loopfunc:true
+                    $value.find('[name=' + name + ']:checked').each(function() {
+                        tmp.push($(this).val());
+                    });
+                    // jshint loopfunc:false
+                    value.push(tmp);
+                    break;
+
+                case 'select':
+                    if (filter.multiple) {
+                        tmp = [];
+                        // jshint loopfunc:true
+                        $value.find('[name=' + name + '] option:selected').each(function() {
+                            tmp.push($(this).val());
+                        });
+                        // jshint loopfunc:false
+                        value.push(tmp);
+                    }
+                    else {
+                        value.push($value.find('[name=' + name + '] option:selected').val());
+                    }
+                    break;
+
+                default:
+                    value.push($value.find('[name=' + name + ']').val());
+            }
+        }
+
+        value = value.map(function(val) {
+            if (operator.multiple && filter.value_separator && typeof val == 'string') {
+                val = val.split(filter.value_separator);
+            }
+            
+            if ($.isArray(val)) {
+                return val.map(function(subval) {
+                    return Utils.changeType(subval, filter.type);
+                });
+            }
+            else {
+                return Utils.changeType(val, filter.type);
+            }
+        });
+
+        if (numOfInputs === 1) {
+            value = value[0];
+        }
+
+        // @deprecated
+        if (filter.valueParser) {
+            value = filter.valueParser.call(this, rule, value);
+        }
+    }
+
+    /**
+     * Modifies the rule's value grabbed from the DOM
+     * @event changer:getRuleValue
+     * @memberof QueryBuilder
+     * @param {*} value
+     * @param {Rule} rule
+     * @returns {*}
+     */
+    return this.change('getRuleValue', value, rule);
+};
+
+/**
+ * Sets the value of a rule's input
+ * @param {Rule} rule
+ * @param {*} value
+ * @private
+ */
+QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
+    var filter = rule.filter;
+    var operator = rule.operator;
+    var numOfInputs = operator.nb_inputs;
+    if(filter.type === 'map') {
+       numOfInputs = 2;
+    }
+
+    if (!filter || !operator) {
+        return;
+    }
+
+    rule._updating_input = true;
+
+    if (filter.valueSetter) {
+        filter.valueSetter.call(this, rule, value);
+    }
+    else {
+        var $value = rule.$el.find(QueryBuilder.selectors.value_container);
+
+        if (numOfInputs == 1) {
+            value = [value];
+        }
+
+        for (var i = 0; i < numOfInputs; i++) {
+            var name = Utils.escapeElementId(rule.id + '_value_' + i);
+
+            switch (filter.input) {
+                case 'radio':
+                    $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
+                    break;
+
+                case 'checkbox':
+                    if (!$.isArray(value[i])) {
+                        value[i] = [value[i]];
+                    }
+                    // jshint loopfunc:true
+                    value[i].forEach(function(value) {
+                        $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
+                    });
+                    // jshint loopfunc:false
+                    break;
+
+                default:
+                    if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
+                        value[i] = value[i].join(filter.value_separator);
+                    }
+                    $value.find('[name=' + name + ']').val(value[i]).trigger('change');
+                    break;
+            }
+        }
+    }
+
+    rule._updating_input = false;
+};
+
+/**
+ * Parses rule flags
+ * @param {object} rule
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseRuleFlags
+ * @private
+ */
+QueryBuilder.prototype.parseRuleFlags = function(rule) {
+    var flags = $.extend({}, this.settings.default_rule_flags);
+
+    if (rule.readonly) {
+        $.extend(flags, {
+            filter_readonly: true,
+            operator_readonly: true,
+            value_readonly: true,
+            no_delete: true
+        });
+    }
+
+    if (rule.flags) {
+        $.extend(flags, rule.flags);
+    }
+
+    /**
+     * Modifies the consolidated rule's flags
+     * @event changer:parseRuleFlags
+     * @memberof QueryBuilder
+     * @param {object} flags
+     * @param {object} rule - <b>not</b> a Rule object
+     * @returns {object}
+     */
+    return this.change('parseRuleFlags', flags, rule);
+};
+
+/**
+ * Gets a copy of flags of a rule
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getRuleFlags = function(flags, all) {
+    if (all) {
+        return $.extend({}, flags);
+    }
+    else {
+        var ret = {};
+        $.each(this.settings.default_rule_flags, function(key, value) {
+            if (flags[key] !== value) {
+                ret[key] = flags[key];
+            }
+        });
+        return ret;
+    }
+};
+
+/**
+ * Parses group flags
+ * @param {object} group
+ * @returns {object}
+ * @fires QueryBuilder.changer:parseGroupFlags
+ * @private
+ */
+QueryBuilder.prototype.parseGroupFlags = function(group) {
+    var flags = $.extend({}, this.settings.default_group_flags);
+
+    if (group.readonly) {
+        $.extend(flags, {
+            condition_readonly: true,
+            no_add_rule: true,
+            no_add_group: true,
+            no_delete: true
+        });
+    }
+
+    if (group.flags) {
+        $.extend(flags, group.flags);
+    }
+
+    /**
+     * Modifies the consolidated group's flags
+     * @event changer:parseGroupFlags
+     * @memberof QueryBuilder
+     * @param {object} flags
+     * @param {object} group - <b>not</b> a Group object
+     * @returns {object}
+     */
+    return this.change('parseGroupFlags', flags, group);
+};
+
+/**
+ * Gets a copy of flags of a group
+ * @param {object} flags
+ * @param {boolean} [all=false] - return all flags or only changes from default flags
+ * @returns {object}
+ * @private
+ */
+QueryBuilder.prototype.getGroupFlags = function(flags, all) {
+    if (all) {
+        return $.extend({}, flags);
+    }
+    else {
+        var ret = {};
+        $.each(this.settings.default_group_flags, function(key, value) {
+            if (flags[key] !== value) {
+                ret[key] = flags[key];
+            }
+        });
+        return ret;
+    }
+};
+
+/**
+ * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
+ * @param {string} [category]
+ * @param {string|object} key
+ * @returns {string}
+ * @fires QueryBuilder.changer:translate
+ */
+QueryBuilder.prototype.translate = function(category, key) {
+    if (!key) {
+        key = category;
+        category = undefined;
+    }
+
+    var translation;
+    if (typeof key === 'object') {
+        translation = key[this.settings.lang_code] || key['en'];
+    }
+    else {
+        translation = (category ? this.lang[category] : this.lang)[key] || key;
+    }
+
+    /**
+     * Modifies the translated label
+     * @event changer:translate
+     * @memberof QueryBuilder
+     * @param {string} translation
+     * @param {string|object} key
+     * @param {string} [category]
+     * @returns {string}
+     */
+    return this.change('translate', translation, key, category);
+};
+
+/**
+ * Returns a validation message
+ * @param {object} validation
+ * @param {string} type
+ * @param {string} def
+ * @returns {string}
+ * @private
+ */
+QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
+    return validation.messages && validation.messages[type] || def;
+};
+
+
+QueryBuilder.templates.group = '\
+<div id="{{= it.group_id }}" class="rules-group-container"> \
+  <div class="rules-group-header"> \
+    <div class="btn-group pull-right group-actions"> \
+      <button type="button" class="btn btn-xs btn-clamp" data-add="rule"> \
+        <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
+      </button> \
+      {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
+        <button type="button" class="btn btn-xs btn-clamp" data-add="group"> \
+          <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
+        </button> \
+      {{?}} \
+      {{? it.level>1 }} \
+        <button type="button" class="btn btn-xs btn-clamp" data-delete="group"> \
+          <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
+        </button> \
+      {{?}} \
+    </div> \
+    <div class="btn-group group-conditions"> \
+      {{~ it.conditions: condition }} \
+        <label class="btn btn-xs btn-primary"> \
+          <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
+        </label> \
+      {{~}} \
+    </div> \
+    {{? it.settings.display_errors }} \
+      <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
+    {{?}} \
+  </div> \
+  <div class=rules-group-body> \
+    <div class=rules-list></div> \
+  </div> \
+</div>';
+
+QueryBuilder.templates.rule = '\
+<div id="{{= it.rule_id }}" class="rule-container"> \
+  <div class="rule-header"> \
+    <div class="btn-group pull-right rule-actions"> \
+      <button type="button" class="btn btn-xs btn-clamp" data-delete="rule"> \
+        <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
+      </button> \
+    </div> \
+  </div> \
+  {{? it.settings.display_errors }} \
+    <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
+  {{?}} \
+  <div class="rule-filter-container"></div> \
+  <div class="rule-operator-container"></div> \
+  <div class="rule-value-container"></div> \
+</div>';
+
+QueryBuilder.templates.filterSelect = '\
+{{ var optgroup = null; }} \
+<select class="form-control" name="{{= it.rule.id }}_filter"> \
+  {{? it.settings.display_empty_filter }} \
+    <option value="-1">{{= it.settings.select_placeholder }}</option> \
+  {{?}} \
+  {{~ it.filters: filter }} \
+    {{? optgroup !== filter.optgroup }} \
+      {{? optgroup !== null }}</optgroup>{{?}} \
+      {{? (optgroup = filter.optgroup) !== null }} \
+        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
+      {{?}} \
+    {{?}} \
+    <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
+  {{~}} \
+  {{? optgroup !== null }}</optgroup>{{?}} \
+</select>';
+
+QueryBuilder.templates.operatorSelect = '\
+{{? it.operators.length === 1 }} \
+<span> \
+{{= it.translate("operators", it.operators[0].type) }} \
+</span> \
+{{?}} \
+{{ var optgroup = null; }} \
+<select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
+  {{~ it.operators: operator }} \
+    {{? optgroup !== operator.optgroup }} \
+      {{? optgroup !== null }}</optgroup>{{?}} \
+      {{? (optgroup = operator.optgroup) !== null }} \
+        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
+      {{?}} \
+    {{?}} \
+    <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
+  {{~}} \
+  {{? optgroup !== null }}</optgroup>{{?}} \
+</select>';
+
+QueryBuilder.templates.ruleValueSelect = '\
+{{ var optgroup = null; }} \
+<select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
+  {{? it.rule.filter.placeholder }} \
+    <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
+  {{?}} \
+  {{~ it.rule.filter.values: entry }} \
+    {{? optgroup !== entry.optgroup }} \
+      {{? optgroup !== null }}</optgroup>{{?}} \
+      {{? (optgroup = entry.optgroup) !== null }} \
+        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
+      {{?}} \
+    {{?}} \
+    <option value="{{= entry.value }}">{{= entry.label }}</option> \
+  {{~}} \
+  {{? optgroup !== null }}</optgroup>{{?}} \
+</select>';
+
+/**
+ * Returns group's HTML
+ * @param {string} group_id
+ * @param {int} level
+ * @returns {string}
+ * @fires QueryBuilder.changer:getGroupTemplate
+ * @private
+ */
+QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
+    var h = this.templates.group({
+        builder: this,
+        group_id: group_id,
+        level: level,
+        conditions: this.settings.conditions,
+        icons: this.icons,
+        settings: this.settings,
+        translate: this.translate.bind(this)
+    });
+
+    /**
+     * Modifies the raw HTML of a group
+     * @event changer:getGroupTemplate
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @param {int} level
+     * @returns {string}
+     */
+    return this.change('getGroupTemplate', h, level);
+};
+
+/**
+ * Returns rule's HTML
+ * @param {string} rule_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
+    var h = this.templates.rule({
+        builder: this,
+        rule_id: rule_id,
+        icons: this.icons,
+        settings: this.settings,
+        translate: this.translate.bind(this)
+    });
+
+    /**
+     * Modifies the raw HTML of a rule
+     * @event changer:getRuleTemplate
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @returns {string}
+     */
+    return this.change('getRuleTemplate', h);
+};
+
+/**
+ * Returns rule's filter HTML
+ * @param {Rule} rule
+ * @param {object[]} filters
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleFilterTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
+    var h = this.templates.filterSelect({
+        builder: this,
+        rule: rule,
+        filters: filters,
+        icons: this.icons,
+        settings: this.settings,
+        translate: this.translate.bind(this)
+    });
+
+    /**
+     * Modifies the raw HTML of the rule's filter dropdown
+     * @event changer:getRuleFilterSelect
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @param {Rule} rule
+     * @param {QueryBuilder.Filter[]} filters
+     * @returns {string}
+     */
+    return this.change('getRuleFilterSelect', h, rule, filters);
+};
+
+/**
+ * Returns rule's operator HTML
+ * @param {Rule} rule
+ * @param {object[]} operators
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleOperatorTemplate
+ * @private
+ */
+QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
+    var h = this.templates.operatorSelect({
+        builder: this,
+        rule: rule,
+        operators: operators,
+        icons: this.icons,
+        settings: this.settings,
+        translate: this.translate.bind(this)
+    });
+
+    /**
+     * Modifies the raw HTML of the rule's operator dropdown
+     * @event changer:getRuleOperatorSelect
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @param {Rule} rule
+     * @param {QueryBuilder.Operator[]} operators
+     * @returns {string}
+     */
+    return this.change('getRuleOperatorSelect', h, rule, operators);
+};
+
+/**
+ * Returns the rule's value select HTML
+ * @param {string} name
+ * @param {Rule} rule
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleValueSelect
+ * @private
+ */
+QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
+    var h = this.templates.ruleValueSelect({
+        builder: this,
+        name: name,
+        rule: rule,
+        icons: this.icons,
+        settings: this.settings,
+        translate: this.translate.bind(this)
+    });
+
+    /**
+     * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
+     * @event changer:getRuleValueSelect
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @param [string} name
+     * @param {Rule} rule
+     * @returns {string}
+     */
+    return this.change('getRuleValueSelect', h, name, rule);
+};
+
+/**
+ * Returns the rule's value HTML
+ * @param {Rule} rule
+ * @param {int} value_id
+ * @returns {string}
+ * @fires QueryBuilder.changer:getRuleInput
+ * @private
+ */
+QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
+    var filter = rule.filter;
+    var validation = rule.filter.validation || {};
+    var name = rule.id + '_value_' + value_id;
+    var c = filter.vertical ? ' class=block' : '';
+    var h = '';
+
+    if (typeof filter.input == 'function') {
+        h = filter.input.call(this, rule, name);
+    }
+    else {
+        switch (filter.input) {
+            case 'radio':
+            case 'checkbox':
+                Utils.iterateOptions(filter.values, function(key, val) {
+                    h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
+                });
+                break;
+
+            case 'select':
+                h = this.getRuleValueSelect(name, rule);
+                break;
+
+            case 'textarea':
+                h += '<textarea class="form-control" name="' + name + '"';
+                if (filter.size) h += ' cols="' + filter.size + '"';
+                if (filter.rows) h += ' rows="' + filter.rows + '"';
+                if (validation.min !== undefined) h += ' minlength="' + validation.min + '"';
+                if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
+                if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
+                h += '></textarea>';
+                break;
+
+            case 'number':
+                h += '<input class="form-control" type="number" name="' + name + '"';
+                if (validation.step !== undefined) h += ' step="' + validation.step + '"';
+                if (validation.min !== undefined) h += ' min="' + validation.min + '"';
+                if (validation.max !== undefined) h += ' max="' + validation.max + '"';
+                if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
+                if (filter.size) h += ' size="' + filter.size + '"';
+                h += '>';
+                break;
+
+            default:
+                h += '<input class="form-control" type="text" name="' + name + '"';
+                if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
+                if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"';
+                if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
+                if (filter.size) h += ' size="' + filter.size + '"';
+                h += '>';
+        }
+    }
+
+    /**
+     * Modifies the raw HTML of the rule's input
+     * @event changer:getRuleInput
+     * @memberof QueryBuilder
+     * @param {string} html
+     * @param {Rule} rule
+     * @param {string} name - the name that the input must have
+     * @returns {string}
+     */
+    return this.change('getRuleInput', h, rule, name);
+};
+
+
+/**
+ * @namespace
+ */
+var Utils = {};
+
+/**
+ * @member {object}
+ * @memberof QueryBuilder
+ * @see Utils
+ */
+QueryBuilder.utils = Utils;
+
+/**
+ * @callback Utils#OptionsIteratee
+ * @param {string} key
+ * @param {string} value
+ * @param {string} [optgroup]
+ */
+
+/**
+ * Iterates over radio/checkbox/selection options, it accept four formats
+ *
+ * @example
+ * // array of values
+ * options = ['one', 'two', 'three']
+ * @example
+ * // simple key-value map
+ * options = {1: 'one', 2: 'two', 3: 'three'}
+ * @example
+ * // array of 1-element maps
+ * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
+ * @example
+ * // array of elements
+ * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
+ *
+ * @param {object|array} options
+ * @param {Utils#OptionsIteratee} tpl
+ */
+Utils.iterateOptions = function(options, tpl) {
+    if (options) {
+        if ($.isArray(options)) {
+            options.forEach(function(entry) {
+                if ($.isPlainObject(entry)) {
+                    // array of elements
+                    if ('value' in entry) {
+                        tpl(entry.value, entry.label || entry.value, entry.optgroup);
+                    }
+                    // array of one-element maps
+                    else {
+                        $.each(entry, function(key, val) {
+                            tpl(key, val);
+                            return false; // break after first entry
+                        });
+                    }
+                }
+                // array of values
+                else {
+                    tpl(entry, entry);
+                }
+            });
+        }
+        // unordered map
+        else {
+            $.each(options, function(key, val) {
+                tpl(key, val);
+            });
+        }
+    }
+};
+
+/**
+ * Replaces {0}, {1}, ... in a string
+ * @param {string} str
+ * @param {...*} args
+ * @returns {string}
+ */
+Utils.fmt = function(str, args) {
+    if (!Array.isArray(args)) {
+        args = Array.prototype.slice.call(arguments, 1);
+    }
+
+    return str.replace(/{([0-9]+)}/g, function(m, i) {
+        return args[parseInt(i)];
+    });
+};
+
+/**
+ * Throws an Error object with custom name or logs an error
+ * @param {boolean} [doThrow=true]
+ * @param {string} type
+ * @param {string} message
+ * @param {...*} args
+ */
+Utils.error = function() {
+    var i = 0;
+    var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
+    var type = arguments[i++];
+    var message = arguments[i++];
+    var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i);
+
+    if (doThrow) {
+        var err = new Error(Utils.fmt(message, args));
+        err.name = type + 'Error';
+        err.args = args;
+        throw err;
+    }
+    else {
+        console.error(type + 'Error: ' + Utils.fmt(message, args));
+    }
+};
+
+/**
+ * Changes the type of a value to int, float or bool
+ * @param {*} value
+ * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
+ * @returns {*}
+ */
+Utils.changeType = function(value, type) {
+    if (value === '' || value === undefined) {
+        return undefined;
+    }
+
+    switch (type) {
+        // @formatter:off
+        case 'integer':
+            if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
+                return value;
+            }
+            return parseInt(value);
+        case 'double':
+            if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
+                return value;
+            }
+            return parseFloat(value);
+        case 'boolean':
+            if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
+                return value;
+            }
+            return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
+        default: return value;
+        // @formatter:on
+    }
+};
+
+/**
+ * Escapes a string like PHP's mysql_real_escape_string does
+ * @param {string} value
+ * @returns {string}
+ */
+Utils.escapeString = function(value) {
+    if (typeof value != 'string') {
+        return value;
+    }
+
+    return value
+        .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
+            switch (s) {
+                // @formatter:off
+                case '\0': return '\\0';
+                case '\n': return '\\n';
+                case '\r': return '\\r';
+                case '\b': return '\\b';
+                default:   return '\\' + s;
+                // @formatter:off
+            }
+        })
+        // uglify compliant
+        .replace(/\t/g, '\\t')
+        .replace(/\x1a/g, '\\Z');
+};
+
+/**
+ * Escapes a string for use in regex
+ * @param {string} str
+ * @returns {string}
+ */
+Utils.escapeRegExp = function(str) {
+    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
+};
+
+/**
+ * Escapes a string for use in HTML element id
+ * @param {string} str
+ * @returns {string}
+ */
+Utils.escapeElementId = function(str) {
+    // Regex based on that suggested by:
+    // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
+    // - escapes : . [ ] ,
+    // - avoids escaping already escaped values
+    return (str) ? str.replace(/(\\)?([:.\[\],])/g,
+            function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
+};
+
+/**
+ * Sorts objects by grouping them by `key`, preserving initial order when possible
+ * @param {object[]} items
+ * @param {string} key
+ * @returns {object[]}
+ */
+Utils.groupSort = function(items, key) {
+    var optgroups = [];
+    var newItems = [];
+
+    items.forEach(function(item) {
+        var idx;
+
+        if (item[key]) {
+            idx = optgroups.lastIndexOf(item[key]);
+
+            if (idx == -1) {
+                idx = optgroups.length;
+            }
+            else {
+                idx++;
+            }
+        }
+        else {
+            idx = optgroups.length;
+        }
+
+        optgroups.splice(idx, 0, item[key]);
+        newItems.splice(idx, 0, item);
+    });
+
+    return newItems;
+};
+
+/**
+ * Defines properties on an Node prototype with getter and setter.<br>
+ *     Update events are emitted in the setter through root Model (if any).<br>
+ *     The object must have a `__` object, non enumerable property to store values.
+ * @param {function} obj
+ * @param {string[]} fields
+ */
+Utils.defineModelProperties = function(obj, fields) {
+    fields.forEach(function(field) {
+        Object.defineProperty(obj.prototype, field, {
+            enumerable: true,
+            get: function() {
+                return this.__[field];
+            },
+            set: function(value) {
+                var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
+                    $.extend({}, this.__[field]) :
+                    this.__[field];
+
+                this.__[field] = value;
+
+                if (this.model !== null) {
+                    /**
+                     * After a value of the model changed
+                     * @event model:update
+                     * @memberof Model
+                     * @param {Node} node
+                     * @param {string} field
+                     * @param {*} value
+                     * @param {*} previousValue
+                     */
+                    this.model.trigger('update', this, field, value, previousValue);
+                }
+            }
+        });
+    });
+};
+
+
+/**
+ * Main object storing data model and emitting model events
+ * @constructor
+ */
+function Model() {
+    /**
+     * @member {Group}
+     * @readonly
+     */
+    this.root = null;
+
+    /**
+     * Base for event emitting
+     * @member {jQuery}
+     * @readonly
+     * @private
+     */
+    this.$ = $(this);
+}
+
+$.extend(Model.prototype, /** @lends Model.prototype */ {
+    /**
+     * Triggers an event on the model
+     * @param {string} type
+     * @returns {$.Event}
+     */
+    trigger: function(type) {
+        var event = new $.Event(type);
+        this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
+        return event;
+    },
+
+    /**
+     * Attaches an event listener on the model
+     * @param {string} type
+     * @param {function} cb
+     * @returns {Model}
+     */
+    on: function() {
+        this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
+        return this;
+    },
+
+    /**
+     * Removes an event listener from the model
+     * @param {string} type
+     * @param {function} [cb]
+     * @returns {Model}
+     */
+    off: function() {
+        this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
+        return this;
+    },
+
+    /**
+     * Attaches an event listener called once on the model
+     * @param {string} type
+     * @param {function} cb
+     * @returns {Model}
+     */
+    once: function() {
+        this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
+        return this;
+    }
+});
+
+
+/**
+ * Root abstract object
+ * @constructor
+ * @param {Node} [parent]
+ * @param {jQuery} $el
+ */
+var Node = function(parent, $el) {
+    if (!(this instanceof Node)) {
+        return new Node(parent, $el);
+    }
+
+    Object.defineProperty(this, '__', { value: {} });
+
+    $el.data('queryBuilderModel', this);
+
+    /**
+     * @name level
+     * @member {int}
+     * @memberof Node
+     * @instance
+     * @readonly
+     */
+    this.__.level = 1;
+
+    /**
+     * @name error
+     * @member {string}
+     * @memberof Node
+     * @instance
+     */
+    this.__.error = null;
+
+    /**
+     * @name flags
+     * @member {object}
+     * @memberof Node
+     * @instance
+     * @readonly
+     */
+    this.__.flags = {};
+
+    /**
+     * @name data
+     * @member {object}
+     * @memberof Node
+     * @instance
+     */
+    this.__.data = undefined;
+
+    /**
+     * @member {jQuery}
+     * @readonly
+     */
+    this.$el = $el;
+
+    /**
+     * @member {string}
+     * @readonly
+     */
+    this.id = $el[0].id;
+
+    /**
+     * @member {Model}
+     * @readonly
+     */
+    this.model = null;
+
+    /**
+     * @member {Group}
+     * @readonly
+     */
+    this.parent = parent;
+};
+
+Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
+
+Object.defineProperty(Node.prototype, 'parent', {
+    enumerable: true,
+    get: function() {
+        return this.__.parent;
+    },
+    set: function(value) {
+        this.__.parent = value;
+        this.level = value === null ? 1 : value.level + 1;
+        this.model = value === null ? null : value.model;
+    }
+});
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean}
+ */
+Node.prototype.isRoot = function() {
+    return (this.level === 1);
+};
+
+/**
+ * Returns the node position inside its parent
+ * @returns {int}
+ */
+Node.prototype.getPos = function() {
+    if (this.isRoot()) {
+        return -1;
+    }
+    else {
+        return this.parent.getNodePos(this);
+    }
+};
+
+/**
+ * Deletes self
+ * @fires Model.model:drop
+ */
+Node.prototype.drop = function() {
+    var model = this.model;
+
+    if (!!this.parent) {
+        this.parent.removeNode(this);
+    }
+
+    this.$el.removeData('queryBuilderModel');
+
+    if (model !== null) {
+        /**
+         * After a node of the model has been removed
+         * @event model:drop
+         * @memberof Model
+         * @param {Node} node
+         */
+        model.trigger('drop', this);
+    }
+};
+
+/**
+ * Moves itself after another Node
+ * @param {Node} target
+ * @fires Model.model:move
+ */
+Node.prototype.moveAfter = function(target) {
+    if (!this.isRoot()) {
+        this.move(target.parent, target.getPos() + 1);
+    }
+};
+
+/**
+ * Moves itself at the beginning of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtBegin = function(target) {
+    if (!this.isRoot()) {
+        if (target === undefined) {
+            target = this.parent;
+        }
+
+        this.move(target, 0);
+    }
+};
+
+/**
+ * Moves itself at the end of parent or another Group
+ * @param {Group} [target]
+ * @fires Model.model:move
+ */
+Node.prototype.moveAtEnd = function(target) {
+    if (!this.isRoot()) {
+        if (target === undefined) {
+            target = this.parent;
+        }
+
+        this.move(target, target.length() === 0 ? 0 : target.length() - 1);
+    }
+};
+
+/**
+ * Moves itself at specific position of Group
+ * @param {Group} target
+ * @param {int} index
+ * @fires Model.model:move
+ */
+Node.prototype.move = function(target, index) {
+    if (!this.isRoot()) {
+        if (typeof target === 'number') {
+            index = target;
+            target = this.parent;
+        }
+
+        this.parent.removeNode(this);
+        target.insertNode(this, index, false);
+
+        if (this.model !== null) {
+            /**
+             * After a node of the model has been moved
+             * @event model:move
+             * @memberof Model
+             * @param {Node} node
+             * @param {Node} target
+             * @param {int} index
+             */
+            this.model.trigger('move', this, target, index);
+        }
+    }
+};
+
+
+/**
+ * Group object
+ * @constructor
+ * @extends Node
+ * @param {Group} [parent]
+ * @param {jQuery} $el
+ */
+var Group = function(parent, $el) {
+    if (!(this instanceof Group)) {
+        return new Group(parent, $el);
+    }
+
+    Node.call(this, parent, $el);
+
+    /**
+     * @member {object[]}
+     * @readonly
+     */
+    this.rules = [];
+
+    /**
+     * @name condition
+     * @member {string}
+     * @memberof Group
+     * @instance
+     */
+    this.__.condition = null;
+};
+
+Group.prototype = Object.create(Node.prototype);
+Group.prototype.constructor = Group;
+
+Utils.defineModelProperties(Group, ['condition']);
+
+/**
+ * Removes group's content
+ */
+Group.prototype.empty = function() {
+    this.each('reverse', function(rule) {
+        rule.drop();
+    }, function(group) {
+        group.drop();
+    });
+};
+
+/**
+ * Deletes self
+ */
+Group.prototype.drop = function() {
+    this.empty();
+    Node.prototype.drop.call(this);
+};
+
+/**
+ * Returns the number of children
+ * @returns {int}
+ */
+Group.prototype.length = function() {
+    return this.rules.length;
+};
+
+/**
+ * Adds a Node at specified index
+ * @param {Node} node
+ * @param {int} [index=end]
+ * @param {boolean} [trigger=false] - fire 'add' event
+ * @returns {Node} the inserted node
+ * @fires Model.model:add
+ */
+Group.prototype.insertNode = function(node, index, trigger) {
+    if (index === undefined) {
+        index = this.length();
+    }
+
+    this.rules.splice(index, 0, node);
+    node.parent = this;
+
+    if (trigger && this.model !== null) {
+        /**
+         * After a node of the model has been added
+         * @event model:add
+         * @memberof Model
+         * @param {Node} parent
+         * @param {Node} node
+         * @param {int} index
+         */
+        this.model.trigger('add', this, node, index);
+    }
+
+    return node;
+};
+
+/**
+ * Adds a new Group at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Group}
+ * @fires Model.model:add
+ */
+Group.prototype.addGroup = function($el, index) {
+    return this.insertNode(new Group(this, $el), index, true);
+};
+
+/**
+ * Adds a new Rule at specified index
+ * @param {jQuery} $el
+ * @param {int} [index=end]
+ * @returns {Rule}
+ * @fires Model.model:add
+ */
+Group.prototype.addRule = function($el, index) {
+    return this.insertNode(new Rule(this, $el), index, true);
+};
+
+/**
+ * Deletes a specific Node
+ * @param {Node} node
+ */
+Group.prototype.removeNode = function(node) {
+    var index = this.getNodePos(node);
+    if (index !== -1) {
+        node.parent = null;
+        this.rules.splice(index, 1);
+    }
+};
+
+/**
+ * Returns the position of a child Node
+ * @param {Node} node
+ * @returns {int}
+ */
+Group.prototype.getNodePos = function(node) {
+    return this.rules.indexOf(node);
+};
+
+/**
+ * @callback Model#GroupIteratee
+ * @param {Node} node
+ * @returns {boolean} stop the iteration
+ */
+
+/**
+ * Iterate over all Nodes
+ * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
+ * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
+ * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
+ * @param {object} [context] - context for callbacks
+ * @returns {boolean} if the iteration has been stopped by a callback
+ */
+Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
+    if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
+        context = cbGroup;
+        cbGroup = cbRule;
+        cbRule = reverse;
+        reverse = false;
+    }
+    context = context === undefined ? null : context;
+
+    var i = reverse ? this.rules.length - 1 : 0;
+    var l = reverse ? 0 : this.rules.length - 1;
+    var c = reverse ? -1 : 1;
+    var next = function() {
+        return reverse ? i >= l : i <= l;
+    };
+    var stop = false;
+
+    for (; next(); i += c) {
+        if (this.rules[i] instanceof Group) {
+            if (!!cbGroup) {
+                stop = cbGroup.call(context, this.rules[i]) === false;
+            }
+        }
+        else if (!!cbRule) {
+            stop = cbRule.call(context, this.rules[i]) === false;
+        }
+
+        if (stop) {
+            break;
+        }
+    }
+
+    return !stop;
+};
+
+/**
+ * Checks if the group contains a particular Node
+ * @param {Node} node
+ * @param {boolean} [recursive=false]
+ * @returns {boolean}
+ */
+Group.prototype.contains = function(node, recursive) {
+    if (this.getNodePos(node) !== -1) {
+        return true;
+    }
+    else if (!recursive) {
+        return false;
+    }
+    else {
+        // the loop will return with false as soon as the Node is found
+        return !this.each(function() {
+            return true;
+        }, function(group) {
+            return !group.contains(node, true);
+        });
+    }
+};
+
+
+/**
+ * Rule object
+ * @constructor
+ * @extends Node
+ * @param {Group} parent
+ * @param {jQuery} $el
+ */
+var Rule = function(parent, $el) {
+    if (!(this instanceof Rule)) {
+        return new Rule(parent, $el);
+    }
+
+    Node.call(this, parent, $el);
+
+    this._updating_value = false;
+    this._updating_input = false;
+
+    /**
+     * @name filter
+     * @member {QueryBuilder.Filter}
+     * @memberof Rule
+     * @instance
+     */
+    this.__.filter = null;
+
+    /**
+     * @name operator
+     * @member {QueryBuilder.Operator}
+     * @memberof Rule
+     * @instance
+     */
+    this.__.operator = null;
+
+    /**
+     * @name value
+     * @member {*}
+     * @memberof Rule
+     * @instance
+     */
+    this.__.value = undefined;
+};
+
+Rule.prototype = Object.create(Node.prototype);
+Rule.prototype.constructor = Rule;
+
+Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
+
+/**
+ * Checks if this Node is the root
+ * @returns {boolean} always false
+ */
+Rule.prototype.isRoot = function() {
+    return false;
+};
+
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Group
+ */
+QueryBuilder.Group = Group;
+
+/**
+ * @member {function}
+ * @memberof QueryBuilder
+ * @see Rule
+ */
+QueryBuilder.Rule = Rule;
+
+
+/**
+ * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
+ * @external "jQuery.fn"
+ */
+
+/**
+ * Instanciates or accesses the {@link QueryBuilder} on an element
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @param {*} option - initial configuration or method name
+ * @param {...*} args - method arguments
+ *
+ * @example
+ * $('#builder').queryBuilder({ /** configuration object *\/ });
+ * @example
+ * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
+ */
+$.fn.queryBuilder = function(option) {
+    if (this.length === 0) {
+        Utils.error('Config', 'No target defined');
+    }
+    if (this.length > 1) {
+        Utils.error('Config', 'Unable to initialize on multiple target');
+    }
+
+    var data = this.data('queryBuilder');
+    var options = (typeof option == 'object' && option) || {};
+
+    if (!data && option == 'destroy') {
+        return this;
+    }
+    if (!data) {
+        var builder = new QueryBuilder(this, options);
+        this.data('queryBuilder', builder);
+        builder.init(options.rules);
+    }
+    if (typeof option == 'string') {
+        return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
+    }
+
+    return this;
+};
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder
+ */
+$.fn.queryBuilder.constructor = QueryBuilder;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.defaults = QueryBuilder.defaults;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.defaults
+ */
+$.fn.queryBuilder.extend = QueryBuilder.extend;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.define
+ */
+$.fn.queryBuilder.define = QueryBuilder.define;
+
+/**
+ * @function
+ * @memberof external:"jQuery.fn"
+ * @see QueryBuilder.regional
+ */
+$.fn.queryBuilder.regional = QueryBuilder.regional;
+
+
+/**
+ * @class BtCheckbox
+ * @memberof module:plugins
+ * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
+ * @param {object} [options]
+ * @param {string} [options.font='glyphicons']
+ * @param {string} [options.color='default']
+ */
+QueryBuilder.define('bt-checkbox', function(options) {
+    if (options.font == 'glyphicons') {
+        this.$el.addClass('bt-checkbox-glyphicons');
+    }
+
+    this.on('getRuleInput.filter', function(h, rule, name) {
+        var filter = rule.filter;
+
+        if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
+            h.value = '';
+
+            if (!filter.colors) {
+                filter.colors = {};
+            }
+            if (filter.color) {
+                filter.colors._def_ = filter.color;
+            }
+
+            var style = filter.vertical ? ' style="display:block"' : '';
+            var i = 0;
+
+            Utils.iterateOptions(filter.values, function(key, val) {
+                var color = filter.colors[key] || filter.colors._def_ || options.color;
+                var id = name + '_' + (i++);
+
+                h.value+= '\
+<div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
+  <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
+  <label for="' + id + '">' + val + '</label> \
+</div>';
+            });
+        }
+    });
+}, {
+    font: 'glyphicons',
+    color: 'default'
+});
+
+
+/**
+ * @class BtSelectpicker
+ * @memberof module:plugins
+ * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
+ * @param {object} [options]
+ * @param {string} [options.container='body']
+ * @param {string} [options.style='btn-inverse btn-xs']
+ * @param {int|string} [options.width='auto']
+ * @param {boolean} [options.showIcon=false]
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('bt-selectpicker', function(options) {
+    if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
+        Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
+    }
+
+    var Selectors = QueryBuilder.selectors;
+
+    // init selectpicker
+    this.on('afterCreateRuleFilters', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
+    });
+
+    this.on('afterCreateRuleOperators', function(e, rule) {
+        rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
+    });
+
+    // update selectpicker on change
+    this.on('afterUpdateRuleFilter', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).selectpicker('render');
+    });
+
+    this.on('afterUpdateRuleOperator', function(e, rule) {
+        rule.$el.find(Selectors.rule_operator).selectpicker('render');
+    });
+
+    this.on('beforeDeleteRule', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
+        rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
+    });
+}, {
+    container: 'body',
+    style: 'btn-inverse btn-xs',
+    width: 'auto',
+    showIcon: false
+});
+
+
+/**
+ * @class BtTooltipErrors
+ * @memberof module:plugins
+ * @description Applies Bootstrap Tooltips on validation error messages.
+ * @param {object} [options]
+ * @param {string} [options.placement='right']
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('bt-tooltip-errors', function(options) {
+    if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
+        Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
+    }
+
+    var self = this;
+
+    // add BT Tooltip data
+    this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
+        var $h = $(h.value);
+        $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip');
+        h.value = $h.prop('outerHTML');
+    });
+
+    // init/refresh tooltip when title changes
+    this.model.on('update', function(e, node, field) {
+        if (field == 'error' && self.settings.display_errors) {
+            node.$el.find(QueryBuilder.selectors.error_container).eq(0)
+                .tooltip(options)
+                .tooltip('hide')
+                .tooltip('fixTitle');
+        }
+    });
+}, {
+    placement: 'right'
+});
+
+
+/**
+ * @class ChangeFilters
+ * @memberof module:plugins
+ * @description Allows to change available filters after plugin initialization.
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
+    /**
+     * Change the filters of the builder
+     * @param {boolean} [deleteOrphans=false] - delete rules using old filters
+     * @param {QueryBuilder[]} filters
+     * @fires module:plugins.ChangeFilters.changer:setFilters
+     * @fires module:plugins.ChangeFilters.afterSetFilters
+     * @throws ChangeFilterError
+     */
+    setFilters: function(deleteOrphans, filters) {
+        var self = this;
+
+        if (filters === undefined) {
+            filters = deleteOrphans;
+            deleteOrphans = false;
+        }
+
+        filters = this.checkFilters(filters);
+
+        /**
+         * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
+         * @event changer:setFilters
+         * @memberof module:plugins.ChangeFilters
+         * @param {QueryBuilder.Filter[]} filters
+         * @returns {QueryBuilder.Filter[]}
+         */
+        filters = this.change('setFilters', filters);
+
+        var filtersIds = filters.map(function(filter) {
+            return filter.id;
+        });
+
+        // check for orphans
+        if (!deleteOrphans) {
+            (function checkOrphans(node) {
+                node.each(
+                    function(rule) {
+                        if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+                            Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
+                        }
+                    },
+                    checkOrphans
+                );
+            }(this.model.root));
+        }
+
+        // replace filters
+        this.filters = filters;
+
+        // apply on existing DOM
+        (function updateBuilder(node) {
+            node.each(true,
+                function(rule) {
+                    if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
+                        rule.drop();
+
+                        self.trigger('rulesChanged');
+                    }
+                    else {
+                        self.createRuleFilters(rule);
+
+                        rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
+                        self.trigger('afterUpdateRuleFilter', rule);
+                    }
+                },
+                updateBuilder
+            );
+        }(this.model.root));
+
+        // update plugins
+        if (this.settings.plugins) {
+            if (this.settings.plugins['unique-filter']) {
+                this.updateDisabledFilters();
+            }
+            if (this.settings.plugins['bt-selectpicker']) {
+                this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+            }
+        }
+
+        // reset the default_filter if does not exist anymore
+        if (this.settings.default_filter) {
+            try {
+                this.getFilterById(this.settings.default_filter);
+            }
+            catch (e) {
+                this.settings.default_filter = null;
+            }
+        }
+
+        /**
+         * After {@link module:plugins.ChangeFilters.setFilters} method
+         * @event afterSetFilters
+         * @memberof module:plugins.ChangeFilters
+         * @param {QueryBuilder.Filter[]} filters
+         */
+        this.trigger('afterSetFilters', filters);
+    },
+
+    /**
+     * Adds a new filter to the builder
+     * @param {QueryBuilder.Filter|Filter[]} newFilters
+     * @param {int|string} [position=#end] - index or '#start' or '#end'
+     * @fires module:plugins.ChangeFilters.changer:setFilters
+     * @fires module:plugins.ChangeFilters.afterSetFilters
+     * @throws ChangeFilterError
+     */
+    addFilter: function(newFilters, position) {
+        if (position === undefined || position == '#end') {
+            position = this.filters.length;
+        }
+        else if (position == '#start') {
+            position = 0;
+        }
+
+        if (!$.isArray(newFilters)) {
+            newFilters = [newFilters];
+        }
+
+        var filters = $.extend(true, [], this.filters);
+
+        // numeric position
+        if (parseInt(position) == position) {
+            Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+        }
+        else {
+            // after filter by its id
+            if (this.filters.some(function(filter, index) {
+                    if (filter.id == position) {
+                        position = index + 1;
+                        return true;
+                    }
+                })
+            ) {
+                Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
+            }
+            // defaults to end of list
+            else {
+                Array.prototype.push.apply(filters, newFilters);
+            }
+        }
+
+        this.setFilters(filters);
+    },
+
+    /**
+     * Removes a filter from the builder
+     * @param {string|string[]} filterIds
+     * @param {boolean} [deleteOrphans=false] delete rules using old filters
+     * @fires module:plugins.ChangeFilters.changer:setFilters
+     * @fires module:plugins.ChangeFilters.afterSetFilters
+     * @throws ChangeFilterError
+     */
+    removeFilter: function(filterIds, deleteOrphans) {
+        var filters = $.extend(true, [], this.filters);
+        if (typeof filterIds === 'string') {
+            filterIds = [filterIds];
+        }
+
+        filters = filters.filter(function(filter) {
+            return filterIds.indexOf(filter.id) === -1;
+        });
+
+        this.setFilters(deleteOrphans, filters);
+    }
+});
+
+
+/**
+ * @class ChosenSelectpicker
+ * @memberof module:plugins
+ * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
+ * @param {object} [options] Supports all the options for chosen
+ * @throws MissingLibraryError
+ */
+QueryBuilder.define('chosen-selectpicker', function(options) {
+
+    if (!$.fn.chosen) {
+        Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
+    }
+
+    if (this.settings.plugins['bt-selectpicker']) {
+        Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
+    }
+
+    var Selectors = QueryBuilder.selectors;
+
+    // init selectpicker
+    this.on('afterCreateRuleFilters', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
+    });
+
+    this.on('afterCreateRuleOperators', function(e, rule) {
+        rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
+    });
+
+    // update selectpicker on change
+    this.on('afterUpdateRuleFilter', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
+    });
+
+    this.on('afterUpdateRuleOperator', function(e, rule) {
+        rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
+    });
+
+    this.on('beforeDeleteRule', function(e, rule) {
+        rule.$el.find(Selectors.rule_filter).chosen('destroy');
+        rule.$el.find(Selectors.rule_operator).chosen('destroy');
+    });
+});
+
+
+/**
+ * @class FilterDescription
+ * @memberof module:plugins
+ * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
+ * @param {object} [options]
+ * @param {string} [options.icon='glyphicon glyphicon-info-sign']
+ * @param {string} [options.mode='popover'] - inline, popover or bootbox
+ * @throws ConfigError
+ */
+QueryBuilder.define('filter-description', function(options) {
+    // INLINE
+    if (options.mode === 'inline') {
+        this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+            var $p = rule.$el.find('p.filter-description');
+            var description = e.builder.getFilterDescription(rule.filter, rule);
+
+            if (!description) {
+                $p.hide();
+            }
+            else {
+                if ($p.length === 0) {
+                    $p = $('<p class="filter-description"></p>');
+                    $p.appendTo(rule.$el);
+                }
+                else {
+                    $p.css('display', '');
+                }
+
+                $p.html('<i class="' + options.icon + '"></i> ' + description);
+            }
+        });
+    }
+    // POPOVER
+    else if (options.mode === 'popover') {
+        if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
+            Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
+        }
+
+        this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+            var $b = rule.$el.find('button.filter-description');
+            var description = e.builder.getFilterDescription(rule.filter, rule);
+
+            if (!description) {
+                $b.hide();
+
+                if ($b.data('bs.popover')) {
+                    $b.popover('hide');
+                }
+            }
+            else {
+                if ($b.length === 0) {
+                    $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
+                    $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+
+                    $b.popover({
+                        placement: 'left',
+                        container: 'body',
+                        html: true
+                    });
+
+                    $b.on('mouseout', function() {
+                        $b.popover('hide');
+                    });
+                }
+                else {
+                    $b.css('display', '');
+                }
+
+                $b.data('bs.popover').options.content = description;
+
+                if ($b.attr('aria-describedby')) {
+                    $b.popover('show');
+                }
+            }
+        });
+    }
+    // BOOTBOX
+    else if (options.mode === 'bootbox') {
+        if (!('bootbox' in window)) {
+            Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
+        }
+
+        this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
+            var $b = rule.$el.find('button.filter-description');
+            var description = e.builder.getFilterDescription(rule.filter, rule);
+
+            if (!description) {
+                $b.hide();
+            }
+            else {
+                if ($b.length === 0) {
+                    $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
+                    $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
+
+                    $b.on('click', function() {
+                        bootbox.alert($b.data('description'));
+                    });
+                }
+                else {
+                    $b.css('display', '');
+                }
+
+                $b.data('description', description);
+            }
+        });
+    }
+}, {
+    icon: 'glyphicon glyphicon-info-sign',
+    mode: 'popover'
+});
+
+QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
+    /**
+     * Returns the description of a filter for a particular rule (if present)
+     * @param {object} filter
+     * @param {Rule} [rule]
+     * @returns {string}
+     * @private
+     */
+    getFilterDescription: function(filter, rule) {
+        if (!filter) {
+            return undefined;
+        }
+        else if (typeof filter.description == 'function') {
+            return filter.description.call(this, rule);
+        }
+        else {
+            return filter.description;
+        }
+    }
+});
+
+
+/**
+ * @class Invert
+ * @memberof module:plugins
+ * @description Allows to invert a rule operator, a group condition or the entire builder.
+ * @param {object} [options]
+ * @param {string} [options.icon='glyphicon glyphicon-random']
+ * @param {boolean} [options.recursive=true]
+ * @param {boolean} [options.invert_rules=true]
+ * @param {boolean} [options.display_rules_button=false]
+ * @param {boolean} [options.silent_fail=false]
+ */
+QueryBuilder.define('invert', function(options) {
+    var self = this;
+    var Selectors = QueryBuilder.selectors;
+
+    // Bind events
+    this.on('afterInit', function() {
+        self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
+            var $group = $(this).closest(Selectors.group_container);
+            self.invert(self.getModel($group), options);
+        });
+
+        if (options.display_rules_button && options.invert_rules) {
+            self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
+                var $rule = $(this).closest(Selectors.rule_container);
+                self.invert(self.getModel($rule), options);
+            });
+        }
+    });
+
+    // Modify templates
+    if (!options.disable_template) {
+        this.on('getGroupTemplate.filter', function(h) {
+            var $h = $(h.value);
+            $h.find(Selectors.condition_container).after(
+                '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
+                '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
+                '</button>'
+            );
+            h.value = $h.prop('outerHTML');
+        });
+
+        if (options.display_rules_button && options.invert_rules) {
+            this.on('getRuleTemplate.filter', function(h) {
+                var $h = $(h.value);
+                $h.find(Selectors.rule_actions).prepend(
+                    '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
+                    '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
+                    '</button>'
+                );
+                h.value = $h.prop('outerHTML');
+            });
+        }
+    }
+}, {
+    icon: 'glyphicon glyphicon-random',
+    recursive: true,
+    invert_rules: true,
+    display_rules_button: false,
+    silent_fail: false,
+    disable_template: false
+});
+
+QueryBuilder.defaults({
+    operatorOpposites: {
+        'equal':            'not_equal',
+        'not_equal':        'equal',
+        'in':               'not_in',
+        'not_in':           'in',
+        'less':             'greater_or_equal',
+        'less_or_equal':    'greater',
+        'greater':          'less_or_equal',
+        'greater_or_equal': 'less',
+        'between':          'not_between',
+        'not_between':      'between',
+        'begins_with':      'not_begins_with',
+        'not_begins_with':  'begins_with',
+        'contains':         'not_contains',
+        'not_contains':     'contains',
+        'ends_with':        'not_ends_with',
+        'not_ends_with':    'ends_with',
+        'is_empty':         'is_not_empty',
+        'is_not_empty':     'is_empty',
+        'is_null':          'is_not_null',
+        'is_not_null':      'is_null'
+    },
+
+    conditionOpposites: {
+        'AND': 'OR',
+        'OR': 'AND'
+    }
+});
+
+QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
+    /**
+     * Invert a Group, a Rule or the whole builder
+     * @param {Node} [node]
+     * @param {object} [options] {@link module:plugins.Invert}
+     * @fires module:plugins.Invert.afterInvert
+     * @throws InvertConditionError, InvertOperatorError
+     */
+    invert: function(node, options) {
+        if (!(node instanceof Node)) {
+            if (!this.model.root) return;
+            options = node;
+            node = this.model.root;
+        }
+
+        if (typeof options != 'object') options = {};
+        if (options.recursive === undefined) options.recursive = true;
+        if (options.invert_rules === undefined) options.invert_rules = true;
+        if (options.silent_fail === undefined) options.silent_fail = false;
+        if (options.trigger === undefined) options.trigger = true;
+
+        if (node instanceof Group) {
+            // invert group condition
+            if (this.settings.conditionOpposites[node.condition]) {
+                node.condition = this.settings.conditionOpposites[node.condition];
+            }
+            else if (!options.silent_fail) {
+                Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
+            }
+
+            // recursive call
+            if (options.recursive) {
+                var tempOpts = $.extend({}, options, { trigger: false });
+                node.each(function(rule) {
+                    if (options.invert_rules) {
+                        this.invert(rule, tempOpts);
+                    }
+                }, function(group) {
+                    this.invert(group, tempOpts);
+                }, this);
+            }
+        }
+        else if (node instanceof Rule) {
+            if (node.operator && !node.filter.no_invert) {
+                // invert rule operator
+                if (this.settings.operatorOpposites[node.operator.type]) {
+                    var invert = this.settings.operatorOpposites[node.operator.type];
+                    // check if the invert is "authorized"
+                    if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
+                        node.operator = this.getOperatorByType(invert);
+                    }
+                }
+                else if (!options.silent_fail) {
+                    Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
+                }
+            }
+        }
+
+        if (options.trigger) {
+            /**
+             * After {@link module:plugins.Invert.invert} method
+             * @event afterInvert
+             * @memberof module:plugins.Invert
+             * @param {Node} node - the main group or rule that has been modified
+             * @param {object} options
+             */
+            this.trigger('afterInvert', node, options);
+
+            this.trigger('rulesChanged');
+        }
+    }
+});
+
+
+/**
+ * @class MongoDbSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
+ */
+
+QueryBuilder.defaults({
+    mongoOperators: {
+        // @formatter:off
+        equal:            function(v) { return v[0]; },
+        not_equal:        function(v) { return { '$ne': v[0] }; },
+        in:               function(v) { return { '$in': v }; },
+        not_in:           function(v) { return { '$nin': v }; },
+        less:             function(v) { return { '$lt': v[0] }; },
+        less_or_equal:    function(v) { return { '$lte': v[0] }; },
+        greater:          function(v) { return { '$gt': v[0] }; },
+        greater_or_equal: function(v) { return { '$gte': v[0] }; },
+        between:          function(v) { return { '$gte': v[0], '$lte': v[1] }; },
+        not_between:      function(v) { return { '$lt': v[0], '$gt': v[1] }; },
+        begins_with:      function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
+        not_begins_with:  function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
+        contains:         function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
+        not_contains:     function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
+        ends_with:        function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
+        not_ends_with:    function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
+        is_empty:         function(v) { return ''; },
+        is_not_empty:     function(v) { return { '$ne': '' }; },
+        is_null:          function(v) { return null; },
+        is_not_null:      function(v) { return { '$ne': null }; }
+        // @formatter:on
+    },
+
+    mongoRuleOperators: {
+        $eq: function(v) {
+            return {
+                'val': v,
+                'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
+            };
+        },
+        $ne: function(v) {
+            v = v.$ne;
+            return {
+                'val': v,
+                'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
+            };
+        },
+        $regex: function(v) {
+            v = v.$regex;
+            if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
+                return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
+            }
+            else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
+                return { 'val': v.slice(5, -5), 'op': 'not_contains' };
+            }
+            else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
+                return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
+            }
+            else if (v.slice(-1) == '$') {
+                return { 'val': v.slice(0, -1), 'op': 'ends_with' };
+            }
+            else if (v.slice(0, 1) == '^') {
+                return { 'val': v.slice(1), 'op': 'begins_with' };
+            }
+            else {
+                return { 'val': v, 'op': 'contains' };
+            }
+        },
+        between: function(v) {
+            return { 'val': [v.$gte, v.$lte], 'op': 'between' };
+        },
+        not_between: function(v) {
+            return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
+        },
+        $in: function(v) {
+            return { 'val': v.$in, 'op': 'in' };
+        },
+        $nin: function(v) {
+            return { 'val': v.$nin, 'op': 'not_in' };
+        },
+        $lt: function(v) {
+            return { 'val': v.$lt, 'op': 'less' };
+        },
+        $lte: function(v) {
+            return { 'val': v.$lte, 'op': 'less_or_equal' };
+        },
+        $gt: function(v) {
+            return { 'val': v.$gt, 'op': 'greater' };
+        },
+        $gte: function(v) {
+            return { 'val': v.$gte, 'op': 'greater_or_equal' };
+        }
+    }
+});
+
+QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
+    /**
+     * Returns rules as a MongoDB query
+     * @param {object} [data] - current rules by default
+     * @returns {object}
+     * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
+     * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
+     * @fires module:plugins.MongoDbSupport.changer:groupToMongo
+     * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
+     */
+    getMongo: function(data) {
+        data = (data === undefined) ? this.getRules() : data;
+
+        if (!data) {
+            return null;
+        }
+
+        var self = this;
+
+        return (function parse(group) {
+            if (!group.condition) {
+                group.condition = self.settings.default_condition;
+            }
+            if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
+                Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
+            }
+
+            if (!group.rules) {
+                return {};
+            }
+
+            var parts = [];
+
+            group.rules.forEach(function(rule) {
+                if (rule.rules && rule.rules.length > 0) {
+                    parts.push(parse(rule));
+                }
+                else {
+                    var mdb = self.settings.mongoOperators[rule.operator];
+                    var ope = self.getOperatorByType(rule.operator);
+
+                    if (mdb === undefined) {
+                        Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
+                    }
+
+                    if (ope.nb_inputs !== 0) {
+                        if (!(rule.value instanceof Array)) {
+                            rule.value = [rule.value];
+                        }
+                    }
+
+                    /**
+                     * Modifies the MongoDB field used by a rule
+                     * @event changer:getMongoDBField
+                     * @memberof module:plugins.MongoDbSupport
+                     * @param {string} field
+                     * @param {Rule} rule
+                     * @returns {string}
+                     */
+                    var field = self.change('getMongoDBField', rule.field, rule);
+
+                    var ruleExpression = {};
+                    ruleExpression[field] = mdb.call(self, rule.value);
+
+                    /**
+                     * Modifies the MongoDB expression generated for a rul
+                     * @event changer:ruleToMongo
+                     * @memberof module:plugins.MongoDbSupport
+                     * @param {object} expression
+                     * @param {Rule} rule
+                     * @param {*} value
+                     * @param {function} valueWrapper - function that takes the value and adds the operator
+                     * @returns {object}
+                     */
+                    parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
+                }
+            });
+
+            var groupExpression = {};
+            groupExpression['$' + group.condition.toLowerCase()] = parts;
+
+            /**
+             * Modifies the MongoDB expression generated for a group
+             * @event changer:groupToMongo
+             * @memberof module:plugins.MongoDbSupport
+             * @param {object} expression
+             * @param {Group} group
+             * @returns {object}
+             */
+            return self.change('groupToMongo', groupExpression, group);
+        }(data));
+    },
+
+    /**
+     * Converts a MongoDB query to rules
+     * @param {object} query
+     * @returns {object}
+     * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
+     * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
+     * @fires module:plugins.MongoDbSupport.changer:mongoToRule
+     * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
+     * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
+     */
+    getRulesFromMongo: function(query) {
+        if (query === undefined || query === null) {
+            return null;
+        }
+
+        var self = this;
+
+        /**
+         * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
+         * @event changer:parseMongoNode
+         * @memberof module:plugins.MongoDbSupport
+         * @param {object} expression
+         * @returns {object} expression, rule or group
+         */
+        query = self.change('parseMongoNode', query);
+
+        // a plugin returned a group
+        if ('rules' in query && 'condition' in query) {
+            return query;
+        }
+
+        // a plugin returned a rule
+        if ('id' in query && 'operator' in query && 'value' in query) {
+            return {
+                condition: this.settings.default_condition,
+                rules: [query]
+            };
+        }
+
+        var key = self.getMongoCondition(query);
+        if (!key) {
+            Utils.error('MongoParse', 'Invalid MongoDB query format');
+        }
+
+        return (function parse(data, topKey) {
+            var rules = data[topKey];
+            var parts = [];
+
+            rules.forEach(function(data) {
+                // allow plugins to manually parse or handle special cases
+                data = self.change('parseMongoNode', data);
+
+                // a plugin returned a group
+                if ('rules' in data && 'condition' in data) {
+                    parts.push(data);
+                    return;
+                }
+
+                // a plugin returned a rule
+                if ('id' in data && 'operator' in data && 'value' in data) {
+                    parts.push(data);
+                    return;
+                }
+
+                var key = self.getMongoCondition(data);
+                if (key) {
+                    parts.push(parse(data, key));
+                }
+                else {
+                    var field = Object.keys(data)[0];
+                    var value = data[field];
+
+                    var operator = self.getMongoOperator(value);
+                    if (operator === undefined) {
+                        Utils.error('MongoParse', 'Invalid MongoDB query format');
+                    }
+
+                    var mdbrl = self.settings.mongoRuleOperators[operator];
+                    if (mdbrl === undefined) {
+                        Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
+                    }
+
+                    var opVal = mdbrl.call(self, value);
+
+                    var id = self.getMongoDBFieldID(field, value);
+
+                    /**
+                     * Modifies the rule generated from the MongoDB expression
+                     * @event changer:mongoToRule
+                     * @memberof module:plugins.MongoDbSupport
+                     * @param {object} rule
+                     * @param {object} expression
+                     * @returns {object}
+                     */
+                    var rule = self.change('mongoToRule', {
+                        id: id,
+                        field: field,
+                        operator: opVal.op,
+                        value: opVal.val
+                    }, data);
+
+                    parts.push(rule);
+                }
+            });
+
+            /**
+             * Modifies the group generated from the MongoDB expression
+             * @event changer:mongoToGroup
+             * @memberof module:plugins.MongoDbSupport
+             * @param {object} group
+             * @param {object} expression
+             * @returns {object}
+             */
+            return self.change('mongoToGroup', {
+                condition: topKey.replace('$', '').toUpperCase(),
+                rules: parts
+            }, data);
+        }(query, key));
+    },
+
+    /**
+     * Sets rules a from MongoDB query
+     * @see module:plugins.MongoDbSupport.getRulesFromMongo
+     */
+    setRulesFromMongo: function(query) {
+        this.setRules(this.getRulesFromMongo(query));
+    },
+
+    /**
+     * Returns a filter identifier from the MongoDB field.
+     * Automatically use the only one filter with a matching field, fires a changer otherwise.
+     * @param {string} field
+     * @param {*} value
+     * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
+     * @returns {string}
+     * @private
+     */
+    getMongoDBFieldID: function(field, value) {
+        var matchingFilters = this.filters.filter(function(filter) {
+            return filter.field === field;
+        });
+
+        var id;
+        if (matchingFilters.length === 1) {
+            id = matchingFilters[0].id;
+        }
+        else {
+            /**
+             * Returns a filter identifier from the MongoDB field
+             * @event changer:getMongoDBFieldID
+             * @memberof module:plugins.MongoDbSupport
+             * @param {string} field
+             * @param {*} value
+             * @returns {string}
+             */
+            id = this.change('getMongoDBFieldID', field, value);
+        }
+
+        return id;
+    },
+
+    /**
+     * Finds which operator is used in a MongoDB sub-object
+     * @param {*} data
+     * @returns {string|undefined}
+     * @private
+     */
+    getMongoOperator: function(data) {
+        if (data !== null && typeof data === 'object') {
+            if (data.$gte !== undefined && data.$lte !== undefined) {
+                return 'between';
+            }
+            if (data.$lt !== undefined && data.$gt !== undefined) {
+                return 'not_between';
+            }
+
+            var knownKeys = Object.keys(data).filter(function(key) {
+                return !!this.settings.mongoRuleOperators[key];
+            }.bind(this));
+
+            if (knownKeys.length === 1) {
+                return knownKeys[0];
+            }
+        }
+        else {
+            return '$eq';
+        }
+    },
+
+
+    /**
+     * Returns the key corresponding to "$or" or "$and"
+     * @param {object} data
+     * @returns {string|undefined}
+     * @private
+     */
+    getMongoCondition: function(data) {
+        var keys = Object.keys(data);
+
+        for (var i = 0, l = keys.length; i < l; i++) {
+            if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
+                return keys[i];
+            }
+        }
+    }
+});
+
+
+/**
+ * @class NotGroup
+ * @memberof module:plugins
+ * @description Adds a "Not" checkbox in front of group conditions.
+ * @param {object} [options]
+ * @param {string} [options.icon_checked='glyphicon glyphicon-checked']
+ * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
+ */
+QueryBuilder.define('not-group', function(options) {
+    var self = this;
+
+    // Bind events
+    this.on('afterInit', function() {
+        self.$el.on('click.queryBuilder', '[data-not=group]', function() {
+            var $group = $(this).closest(QueryBuilder.selectors.group_container);
+            var group = self.getModel($group);
+            group.not = !group.not;
+        });
+
+        self.model.on('update', function(e, node, field) {
+            if (node instanceof Group && field === 'not') {
+                self.updateGroupNot(node);
+            }
+        });
+    });
+
+    // Init "not" property
+    this.on('afterAddGroup', function(e, group) {
+        group.__.not = false;
+    });
+
+    // Modify templates
+    if (!options.disable_template) {
+        this.on('getGroupTemplate.filter', function(h) {
+            var $h = $(h.value);
+            $h.find(QueryBuilder.selectors.condition_container).prepend(
+                '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
+                '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
+                '</button>'
+            );
+            h.value = $h.prop('outerHTML');
+        });
+    }
+
+    // Export "not" to JSON
+    this.on('groupToJson.filter', function(e, group) {
+        e.value.not = group.not;
+    });
+
+    // Read "not" from JSON
+    this.on('jsonToGroup.filter', function(e, json) {
+        e.value.not = !!json.not;
+    });
+
+    // Export "not" to SQL
+    this.on('groupToSQL.filter', function(e, group) {
+        if (group.not) {
+            e.value = 'NOT ( ' + e.value + ' )';
+        }
+    });
+
+    // Parse "NOT" function from sqlparser
+    this.on('parseSQLNode.filter', function(e) {
+        if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
+            e.value = e.value.arguments.value[0];
+
+            // if the there is no sub-group, create one
+            if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
+                e.value = new SQLParser.nodes.Op(
+                    self.settings.default_condition,
+                    e.value,
+                    null
+                );
+            }
+
+            e.value.not = true;
+        }
+    });
+
+    // Request to create sub-group if the "not" flag is set
+    this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
+        if (data.not && i > 0) {
+            e.value = true;
+        }
+    });
+
+    // Read "not" from parsed SQL
+    this.on('sqlToGroup.filter', function(e, data) {
+        e.value.not = !!data.not;
+    });
+
+    // Export "not" to Mongo
+    this.on('groupToMongo.filter', function(e, group) {
+        var key = '$' + group.condition.toLowerCase();
+        if (group.not && e.value[key]) {
+            e.value = { '$nor': [e.value] };
+        }
+    });
+
+    // Parse "$nor" operator from Mongo
+    this.on('parseMongoNode.filter', function(e) {
+        var keys = Object.keys(e.value);
+
+        if (keys[0] == '$nor') {
+            e.value = e.value[keys[0]][0];
+            e.value.not = true;
+        }
+    });
+
+    // Read "not" from parsed Mongo
+    this.on('mongoToGroup.filter', function(e, data) {
+        e.value.not = !!data.not;
+    });
+}, {
+    icon_unchecked: 'glyphicon glyphicon-unchecked',
+    icon_checked: 'glyphicon glyphicon-check',
+    disable_template: false
+});
+
+/**
+ * From {@link module:plugins.NotGroup}
+ * @name not
+ * @member {boolean}
+ * @memberof Group
+ * @instance
+ */
+Utils.defineModelProperties(Group, ['not']);
+
+QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
+
+QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
+    /**
+     * Performs actions when a group's not changes
+     * @param {Group} group
+     * @fires module:plugins.NotGroup.afterUpdateGroupNot
+     * @private
+     */
+    updateGroupNot: function(group) {
+        var options = this.plugins['not-group'];
+        group.$el.find('>' + QueryBuilder.selectors.group_not)
+            .toggleClass('active', group.not)
+            .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
+
+        /**
+         * After the group's not flag has been modified
+         * @event afterUpdateGroupNot
+         * @memberof module:plugins.NotGroup
+         * @param {Group} group
+         */
+        this.trigger('afterUpdateGroupNot', group);
+
+        this.trigger('rulesChanged');
+    }
+});
+
+
+/**
+ * @class Sortable
+ * @memberof module:plugins
+ * @description Enables drag & drop sort of rules.
+ * @param {object} [options]
+ * @param {boolean} [options.inherit_no_drop=true]
+ * @param {boolean} [options.inherit_no_sortable=true]
+ * @param {string} [options.icon='glyphicon glyphicon-sort']
+ * @throws MissingLibraryError, ConfigError
+ */
+QueryBuilder.define('sortable', function(options) {
+    if (!('interact' in window)) {
+        Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
+    }
+
+    if (options.default_no_sortable !== undefined) {
+        Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
+        this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
+    }
+
+    // recompute drop-zones during drag (when a rule is hidden)
+    interact.dynamicDrop(true);
+
+    // set move threshold to 10px
+    interact.pointerMoveTolerance(10);
+
+    var placeholder;
+    var ghost;
+    var src;
+    var moved;
+
+    // Init drag and drop
+    this.on('afterAddRule afterAddGroup', function(e, node) {
+        if (node == placeholder) {
+            return;
+        }
+
+        var self = e.builder;
+
+        // Inherit flags
+        if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
+            node.flags.no_sortable = true;
+        }
+        if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
+            node.flags.no_drop = true;
+        }
+
+        // Configure drag
+        if (!node.flags.no_sortable) {
+            interact(node.$el[0])
+                .draggable({
+                    allowFrom: QueryBuilder.selectors.drag_handle,
+                    onstart: function(event) {
+                        moved = false;
+
+                        // get model of dragged element
+                        src = self.getModel(event.target);
+
+                        // create ghost
+                        ghost = src.$el.clone()
+                            .appendTo(src.$el.parent())
+                            .width(src.$el.outerWidth())
+                            .addClass('dragging');
+
+                        // create drop placeholder
+                        var ph = $('<div class="rule-placeholder">&nbsp;</div>')
+                            .height(src.$el.outerHeight());
+
+                        placeholder = src.parent.addRule(ph, src.getPos());
+
+                        // hide dragged element
+                        src.$el.hide();
+                    },
+                    onmove: function(event) {
+                        // make the ghost follow the cursor
+                        ghost[0].style.top = event.clientY - 15 + 'px';
+                        ghost[0].style.left = event.clientX - 15 + 'px';
+                    },
+                    onend: function(event) {
+                        // starting from Interact 1.3.3, onend is called before ondrop
+                        if (event.dropzone) {
+                            moveSortableToTarget(src, $(event.relatedTarget), self);
+                            moved = true;
+                        }
+
+                        // remove ghost
+                        ghost.remove();
+                        ghost = undefined;
+
+                        // remove placeholder
+                        placeholder.drop();
+                        placeholder = undefined;
+
+                        // show element
+                        src.$el.css('display', '');
+
+                        /**
+                         * After a node has been moved with {@link module:plugins.Sortable}
+                         * @event afterMove
+                         * @memberof module:plugins.Sortable
+                         * @param {Node} node
+                         */
+                        self.trigger('afterMove', src);
+
+                        self.trigger('rulesChanged');
+                    }
+                });
+        }
+
+        if (!node.flags.no_drop) {
+            //  Configure drop on groups and rules
+            interact(node.$el[0])
+                .dropzone({
+                    accept: QueryBuilder.selectors.rule_and_group_containers,
+                    ondragenter: function(event) {
+                        moveSortableToTarget(placeholder, $(event.target), self);
+                    },
+                    ondrop: function(event) {
+                        if (!moved) {
+                            moveSortableToTarget(src, $(event.target), self);
+                        }
+                    }
+                });
+
+            // Configure drop on group headers
+            if (node instanceof Group) {
+                interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
+                    .dropzone({
+                        accept: QueryBuilder.selectors.rule_and_group_containers,
+                        ondragenter: function(event) {
+                            moveSortableToTarget(placeholder, $(event.target), self);
+                        },
+                        ondrop: function(event) {
+                            if (!moved) {
+                                moveSortableToTarget(src, $(event.target), self);
+                            }
+                        }
+                    });
+            }
+        }
+    });
+
+    // Detach interactables
+    this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
+        if (!e.isDefaultPrevented()) {
+            interact(node.$el[0]).unset();
+
+            if (node instanceof Group) {
+                interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
+            }
+        }
+    });
+
+    // Remove drag handle from non-sortable items
+    this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
+        if (node.flags.no_sortable) {
+            node.$el.find('.drag-handle').remove();
+        }
+    });
+
+    // Modify templates
+    if (!options.disable_template) {
+        this.on('getGroupTemplate.filter', function(h, level) {
+            if (level > 1) {
+                var $h = $(h.value);
+                $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
+                h.value = $h.prop('outerHTML');
+            }
+        });
+
+        this.on('getRuleTemplate.filter', function(h) {
+            var $h = $(h.value);
+            $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
+            h.value = $h.prop('outerHTML');
+        });
+    }
+}, {
+    inherit_no_sortable: true,
+    inherit_no_drop: true,
+    icon: 'glyphicon glyphicon-sort',
+    disable_template: false
+});
+
+QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
+QueryBuilder.selectors.drag_handle = '.drag-handle';
+
+QueryBuilder.defaults({
+    default_rule_flags: {
+        no_sortable: false,
+        no_drop: false
+    },
+    default_group_flags: {
+        no_sortable: false,
+        no_drop: false
+    }
+});
+
+/**
+ * Moves an element (placeholder or actual object) depending on active target
+ * @memberof module:plugins.Sortable
+ * @param {Node} node
+ * @param {jQuery} target
+ * @param {QueryBuilder} [builder]
+ * @private
+ */
+function moveSortableToTarget(node, target, builder) {
+    var parent, method;
+    var Selectors = QueryBuilder.selectors;
+
+    // on rule
+    parent = target.closest(Selectors.rule_container);
+    if (parent.length) {
+        method = 'moveAfter';
+    }
+
+    // on group header
+    if (!method) {
+        parent = target.closest(Selectors.group_header);
+        if (parent.length) {
+            parent = target.closest(Selectors.group_container);
+            method = 'moveAtBegin';
+        }
+    }
+
+    // on group
+    if (!method) {
+        parent = target.closest(Selectors.group_container);
+        if (parent.length) {
+            method = 'moveAtEnd';
+        }
+    }
+
+    if (method) {
+        node[method](builder.getModel(parent));
+
+        // refresh radio value
+        if (builder && node instanceof Rule) {
+            builder.setRuleInputValue(node, node.value);
+        }
+    }
+}
+
+
+/**
+ * @class SqlSupport
+ * @memberof module:plugins
+ * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
+ * @param {object} [options]
+ * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
+ */
+QueryBuilder.define('sql-support', function(options) {
+
+}, {
+    boolean_as_integer: true
+});
+
+QueryBuilder.defaults({
+    // operators for internal -> SQL conversion
+    sqlOperators: {
+        equal: { op: '= ?' },
+        not_equal: { op: '!= ?' },
+        in: { op: 'IN(?)', sep: ', ' },
+        not_in: { op: 'NOT IN(?)', sep: ', ' },
+        less: { op: '< ?' },
+        less_or_equal: { op: '<= ?' },
+        greater: { op: '> ?' },
+        greater_or_equal: { op: '>= ?' },
+        between: { op: 'BETWEEN ?', sep: ' AND ' },
+        not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
+        begins_with: { op: 'LIKE(?)', mod: '{0}%' },
+        not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
+        contains: { op: 'LIKE(?)', mod: '%{0}%' },
+        not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
+        ends_with: { op: 'LIKE(?)', mod: '%{0}' },
+        not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
+        is_empty: { op: '= \'\'' },
+        is_not_empty: { op: '!= \'\'' },
+        is_null: { op: 'IS NULL' },
+        is_not_null: { op: 'IS NOT NULL' }
+    },
+
+    // operators for SQL -> internal conversion
+    sqlRuleOperator: {
+        '=': function(v) {
+            return {
+                val: v,
+                op: v === '' ? 'is_empty' : 'equal'
+            };
+        },
+        '!=': function(v) {
+            return {
+                val: v,
+                op: v === '' ? 'is_not_empty' : 'not_equal'
+            };
+        },
+        'LIKE': function(v) {
+            if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+                return {
+                    val: v.slice(1, -1),
+                    op: 'contains'
+                };
+            }
+            else if (v.slice(0, 1) == '%') {
+                return {
+                    val: v.slice(1),
+                    op: 'ends_with'
+                };
+            }
+            else if (v.slice(-1) == '%') {
+                return {
+                    val: v.slice(0, -1),
+                    op: 'begins_with'
+                };
+            }
+            else {
+                Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
+            }
+        },
+        'NOT LIKE': function(v) {
+            if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
+                return {
+                    val: v.slice(1, -1),
+                    op: 'not_contains'
+                };
+            }
+            else if (v.slice(0, 1) == '%') {
+                return {
+                    val: v.slice(1),
+                    op: 'not_ends_with'
+                };
+            }
+            else if (v.slice(-1) == '%') {
+                return {
+                    val: v.slice(0, -1),
+                    op: 'not_begins_with'
+                };
+            }
+            else {
+                Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
+            }
+        },
+        'IN': function(v) {
+            return { val: v, op: 'in' };
+        },
+        'NOT IN': function(v) {
+            return { val: v, op: 'not_in' };
+        },
+        '<': function(v) {
+            return { val: v, op: 'less' };
+        },
+        '<=': function(v) {
+            return { val: v, op: 'less_or_equal' };
+        },
+        '>': function(v) {
+            return { val: v, op: 'greater' };
+        },
+        '>=': function(v) {
+            return { val: v, op: 'greater_or_equal' };
+        },
+        'BETWEEN': function(v) {
+            return { val: v, op: 'between' };
+        },
+        'NOT BETWEEN': function(v) {
+            return { val: v, op: 'not_between' };
+        },
+        'IS': function(v) {
+            if (v !== null) {
+                Utils.error('SQLParse', 'Invalid value for IS operator');
+            }
+            return { val: null, op: 'is_null' };
+        },
+        'IS NOT': function(v) {
+            if (v !== null) {
+                Utils.error('SQLParse', 'Invalid value for IS operator');
+            }
+            return { val: null, op: 'is_not_null' };
+        }
+    },
+
+    // statements for internal -> SQL conversion
+    sqlStatements: {
+        'question_mark': function() {
+            var params = [];
+            return {
+                add: function(rule, value) {
+                    params.push(value);
+                    return '?';
+                },
+                run: function() {
+                    return params;
+                }
+            };
+        },
+
+        'numbered': function(char) {
+            if (!char || char.length > 1) char = '$';
+            var index = 0;
+            var params = [];
+            return {
+                add: function(rule, value) {
+                    params.push(value);
+                    index++;
+                    return char + index;
+                },
+                run: function() {
+                    return params;
+                }
+            };
+        },
+
+        'named': function(char) {
+            if (!char || char.length > 1) char = ':';
+            var indexes = {};
+            var params = {};
+            return {
+                add: function(rule, value) {
+                    if (!indexes[rule.field]) indexes[rule.field] = 1;
+                    var key = rule.field + '_' + (indexes[rule.field]++);
+                    params[key] = value;
+                    return char + key;
+                },
+                run: function() {
+                    return params;
+                }
+            };
+        }
+    },
+
+    // statements for SQL -> internal conversion
+    sqlRuleStatement: {
+        'question_mark': function(values) {
+            var index = 0;
+            return {
+                parse: function(v) {
+                    return v == '?' ? values[index++] : v;
+                },
+                esc: function(sql) {
+                    return sql.replace(/\?/g, '\'?\'');
+                }
+            };
+        },
+
+        'numbered': function(values, char) {
+            if (!char || char.length > 1) char = '$';
+            var regex1 = new RegExp('^\\' + char + '[0-9]+$');
+            var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
+            return {
+                parse: function(v) {
+                    return regex1.test(v) ? values[v.slice(1) - 1] : v;
+                },
+                esc: function(sql) {
+                    return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+                }
+            };
+        },
+
+        'named': function(values, char) {
+            if (!char || char.length > 1) char = ':';
+            var regex1 = new RegExp('^\\' + char);
+            var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
+            return {
+                parse: function(v) {
+                    return regex1.test(v) ? values[v.slice(1)] : v;
+                },
+                esc: function(sql) {
+                    return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
+                }
+            };
+        }
+    }
+});
+
+/**
+ * @typedef {object} SqlQuery
+ * @memberof module:plugins.SqlSupport
+ * @property {string} sql
+ * @property {object} params
+ */
+
+QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
+    /**
+     * Returns rules as a SQL query
+     * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
+     * @param {boolean} [nl=false] output with new lines
+     * @param {object} [data] - current rules by default
+     * @returns {module:plugins.SqlSupport.SqlQuery}
+     * @fires module:plugins.SqlSupport.changer:getSQLField
+     * @fires module:plugins.SqlSupport.changer:ruleToSQL
+     * @fires module:plugins.SqlSupport.changer:groupToSQL
+     * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
+     */
+    getSQL: function(stmt, nl, data) {
+        data = (data === undefined) ? this.getRules() : data;
+
+        if (!data) {
+            return null;
+        }
+
+        nl = !!nl ? '\n' : ' ';
+        var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
+
+        if (stmt === true) {
+            stmt = 'question_mark';
+        }
+        if (typeof stmt == 'string') {
+            var config = getStmtConfig(stmt);
+            stmt = this.settings.sqlStatements[config[1]](config[2]);
+        }
+
+        var self = this;
+
+        var sql = (function parse(group) {
+            if (!group.condition) {
+                group.condition = self.settings.default_condition;
+            }
+            if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
+                Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
+            }
+
+            if (!group.rules) {
+                return '';
+            }
+
+            var parts = [];
+
+            group.rules.forEach(function(rule) {
+                if (rule.rules && rule.rules.length > 0) {
+                    parts.push('(' + nl + parse(rule) + nl + ')' + nl);
+                }
+                else {
+                    var sql = self.settings.sqlOperators[rule.operator];
+                    var ope = self.getOperatorByType(rule.operator);
+                    var value = '';
+
+                    if (sql === undefined) {
+                        Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
+                    }
+
+                    if (ope.nb_inputs !== 0) {
+                        if (!(rule.value instanceof Array)) {
+                            rule.value = [rule.value];
+                        }
+
+                        rule.value.forEach(function(v, i) {
+                            if (i > 0) {
+                               if (rule.type === 'map') {
+                                       value += "|";
+                               } else {
+                                       value += sql.sep;
+                               }
+                            }
+
+                            if (rule.type == 'boolean' && boolean_as_integer) {
+                                v = v ? 1 : 0;
+                            }
+                            else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
+                                v = Utils.escapeString(v);
+                            }
+                            
+                            if (rule.type == 'datetime') {
+                               if (!('moment' in window)) {
+                                       Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
+                               }
+                               v = moment(v, 'YYYY/MM/DD HH:mm:ss').utc().unix();
+                            }
+                            
+                            if (sql.mod) {
+                               if ((rule.type !== 'map') || (rule.type === 'map' && i > 0)) {
+                                       v = Utils.fmt(sql.mod, v);
+                               }
+                            }
+
+                            if (stmt) {
+                                value += stmt.add(rule, v);
+                            }
+                            else {
+                               if ( rule.type === 'map') {
+                                       if(i > 0) {
+                                               value += v;
+                                               value = '\'' + value + '\'';
+                                       } else{
+                                               value += v;
+                                       }
+                               } else {
+                                       if (typeof v == 'string') {
+                                               v = '\'' + v + '\'';
+                                       }
+                                       value += v;
+                               }
+                            }
+                        });
+                    }
+
+                    var sqlFn = function(v) {
+                        return sql.op.replace('?', function() {
+                            return v;
+                        });
+                    };
+
+                    /**
+                     * Modifies the SQL field used by a rule
+                     * @event changer:getSQLField
+                     * @memberof module:plugins.SqlSupport
+                     * @param {string} field
+                     * @param {Rule} rule
+                     * @returns {string}
+                     */
+                    var field = self.change('getSQLField', rule.field, rule);
+
+                    var ruleExpression = field + ' ' + sqlFn(value);
+
+                    /**
+                     * Modifies the SQL generated for a rule
+                     * @event changer:ruleToSQL
+                     * @memberof module:plugins.SqlSupport
+                     * @param {string} expression
+                     * @param {Rule} rule
+                     * @param {*} value
+                     * @param {function} valueWrapper - function that takes the value and adds the operator
+                     * @returns {string}
+                     */
+                    parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
+                }
+            });
+
+            var groupExpression = parts.join(' ' + group.condition + nl);
+
+            /**
+             * Modifies the SQL generated for a group
+             * @event changer:groupToSQL
+             * @memberof module:plugins.SqlSupport
+             * @param {string} expression
+             * @param {Group} group
+             * @returns {string}
+             */
+            return self.change('groupToSQL', groupExpression, group);
+        }(data));
+
+        if (stmt) {
+            return {
+                sql: sql,
+                params: stmt.run()
+            };
+        }
+        else {
+            return {
+                sql: sql
+            };
+        }
+    },
+
+    /**
+     * Convert a SQL query to rules
+     * @param {string|module:plugins.SqlSupport.SqlQuery} query
+     * @param {boolean|string} stmt
+     * @returns {object}
+     * @fires module:plugins.SqlSupport.changer:parseSQLNode
+     * @fires module:plugins.SqlSupport.changer:getSQLFieldID
+     * @fires module:plugins.SqlSupport.changer:sqlToRule
+     * @fires module:plugins.SqlSupport.changer:sqlToGroup
+     * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
+     */
+    getRulesFromSQL: function(query, stmt) {
+        if (!('SQLParser' in window)) {
+            Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
+        }
+
+        var self = this;
+
+        if (typeof query == 'string') {
+            query = { sql: query };
+        }
+
+        if (stmt === true) stmt = 'question_mark';
+        if (typeof stmt == 'string') {
+            var config = getStmtConfig(stmt);
+            stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
+        }
+
+        if (stmt) {
+            query.sql = stmt.esc(query.sql);
+        }
+
+        if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
+            query.sql = 'SELECT * FROM table WHERE ' + query.sql;
+        }
+
+        var parsed = SQLParser.parse(query.sql);
+
+        if (!parsed.where) {
+            Utils.error('SQLParse', 'No WHERE clause found');
+        }
+
+        /**
+         * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
+         * @event changer:parseSQLNode
+         * @memberof module:plugins.SqlSupport
+         * @param {object} AST node
+         * @returns {object} tree, rule or group
+         */
+        var data = self.change('parseSQLNode', parsed.where.conditions);
+
+        // a plugin returned a group
+        if ('rules' in data && 'condition' in data) {
+            return data;
+        }
+
+        // a plugin returned a rule
+        if ('id' in data && 'operator' in data && 'value' in data) {
+            return {
+                condition: this.settings.default_condition,
+                rules: [data]
+            };
+        }
+
+        // create root group
+        var out = self.change('sqlToGroup', {
+            condition: this.settings.default_condition,
+            rules: []
+        }, data);
+
+        // keep track of current group
+        var curr = out;
+
+        (function flatten(data, i) {
+            if (data === null) {
+                return;
+            }
+
+            // allow plugins to manually parse or handle special cases
+            data = self.change('parseSQLNode', data);
+
+            // a plugin returned a group
+            if ('rules' in data && 'condition' in data) {
+                curr.rules.push(data);
+                return;
+            }
+
+            // a plugin returned a rule
+            if ('id' in data && 'operator' in data && 'value' in data) {
+                curr.rules.push(data);
+                return;
+            }
+
+            // data must be a SQL parser node
+            if (!('left' in data) || !('right' in data) || !('operation' in data)) {
+                Utils.error('SQLParse', 'Unable to parse WHERE clause');
+            }
+
+            // it's a node
+            if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
+                // create a sub-group if the condition is not the same and it's not the first level
+
+                /**
+                 * Given an existing group and an AST node, determines if a sub-group must be created
+                 * @event changer:sqlGroupsDistinct
+                 * @memberof module:plugins.SqlSupport
+                 * @param {boolean} create - true by default if the group condition is different
+                 * @param {object} group
+                 * @param {object} AST
+                 * @param {int} current group level
+                 * @returns {boolean}
+                 */
+                var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
+
+                if (createGroup) {
+                    /**
+                     * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
+                     * @event changer:sqlToGroup
+                     * @memberof module:plugins.SqlSupport
+                     * @param {object} group
+                     * @param {object} AST
+                     * @returns {object}
+                     */
+                    var group = self.change('sqlToGroup', {
+                        condition: self.settings.default_condition,
+                        rules: []
+                    }, data);
+
+                    curr.rules.push(group);
+                    curr = group;
+                }
+
+                curr.condition = data.operation.toUpperCase();
+                i++;
+
+                // some magic !
+                var next = curr;
+                flatten(data.left, i);
+
+                curr = next;
+                flatten(data.right, i);
+            }
+            // it's a leaf
+            else {
+                if ($.isPlainObject(data.right.value)) {
+                    Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
+                }
+
+                // convert array
+                var value;
+                if ($.isArray(data.right.value)) {
+                    value = data.right.value.map(function(v) {
+                        return v.value;
+                    });
+                }
+                else {
+                    value = data.right.value;
+                }
+
+                // get actual values
+                if (stmt) {
+                    if ($.isArray(value)) {
+                        value = value.map(stmt.parse);
+                    }
+                    else {
+                        value = stmt.parse(value);
+                    }
+                }
+
+                // convert operator
+                var operator = data.operation.toUpperCase();
+                if (operator == '<>') {
+                    operator = '!=';
+                }
+
+                var sqlrl = self.settings.sqlRuleOperator[operator];
+                if (sqlrl === undefined) {
+                    Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
+                }
+                
+                // find field name
+                var field;
+                if ('values' in data.left) {
+                    field = data.left.values.join('.');
+                }
+                else if ('value' in data.left) {
+                    field = data.left.value;
+                }
+                else {
+                    Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
+                }
+
+                var matchingFilter = self.filters.filter(function(filter) {
+                    return filter.field.toLowerCase() === field.toLowerCase();
+                });
+                
+                var opVal;
+                if(matchingFilter && matchingFilter[0].type === 'map') {
+                       var tempVal = value.split('|')
+                    opVal = sqlrl.call(this, tempVal[1], data.operation);
+                    opVal.val = tempVal[0] + '|' + opVal.val;
+                } else {
+                    opVal = sqlrl.call(this, value, data.operation);
+                }
+                
+                var id = self.getSQLFieldID(field, value);
+
+                /**
+                 * Modifies the rule generated from the SQL expression
+                 * @event changer:sqlToRule
+                 * @memberof module:plugins.SqlSupport
+                 * @param {object} rule
+                 * @param {object} AST
+                 * @returns {object}
+                 */
+                var rule = self.change('sqlToRule', {
+                    id: id,
+                    field: field,
+                    operator: opVal.op,
+                    value: opVal.val
+                }, data);
+
+                curr.rules.push(rule);
+            }
+        }(data, 0));
+
+        return out;
+    },
+
+    /**
+     * Sets the builder's rules from a SQL query
+     * @see module:plugins.SqlSupport.getRulesFromSQL
+     */
+    setRulesFromSQL: function(query, stmt) {
+        this.setRules(this.getRulesFromSQL(query, stmt));
+    },
+
+    /**
+     * Returns a filter identifier from the SQL field.
+     * Automatically use the only one filter with a matching field, fires a changer otherwise.
+     * @param {string} field
+     * @param {*} value
+     * @fires module:plugins.SqlSupport:changer:getSQLFieldID
+     * @returns {string}
+     * @private
+     */
+    getSQLFieldID: function(field, value) {
+        var matchingFilters = this.filters.filter(function(filter) {
+            return filter.field.toLowerCase() === field.toLowerCase();
+        });
+
+        var id;
+        if (matchingFilters.length === 1) {
+            id = matchingFilters[0].id;
+        }
+        else {
+            /**
+             * Returns a filter identifier from the SQL field
+             * @event changer:getSQLFieldID
+             * @memberof module:plugins.SqlSupport
+             * @param {string} field
+             * @param {*} value
+             * @returns {string}
+             */
+            id = this.change('getSQLFieldID', field, value);
+        }
+
+        return id;
+    }
+});
+
+/**
+ * Parses the statement configuration
+ * @memberof module:plugins.SqlSupport
+ * @param {string} stmt
+ * @returns {Array} null, mode, option
+ * @private
+ */
+function getStmtConfig(stmt) {
+    var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
+    if (!config) config = [null, 'question_mark', undefined];
+    return config;
+}
+
+
+/**
+ * @class UniqueFilter
+ * @memberof module:plugins
+ * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
+ */
+QueryBuilder.define('unique-filter', function() {
+    this.status.used_filters = {};
+
+    this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
+    this.on('afterDeleteRule', this.updateDisabledFilters);
+    this.on('afterCreateRuleFilters', this.applyDisabledFilters);
+    this.on('afterReset', this.clearDisabledFilters);
+    this.on('afterClear', this.clearDisabledFilters);
+
+    // Ensure that the default filter is not already used if unique
+    this.on('getDefaultFilter.filter', function(e, model) {
+        var self = e.builder;
+
+        self.updateDisabledFilters();
+
+        if (e.value.id in self.status.used_filters) {
+            var found = self.filters.some(function(filter) {
+                if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
+                    e.value = filter;
+                    return true;
+                }
+            });
+
+            if (!found) {
+                Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
+                e.value = undefined;
+            }
+        }
+    });
+});
+
+QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
+    /**
+     * Updates the list of used filters
+     * @param {$.Event} [e]
+     * @private
+     */
+    updateDisabledFilters: function(e) {
+        var self = e ? e.builder : this;
+
+        self.status.used_filters = {};
+
+        if (!self.model) {
+            return;
+        }
+
+        // get used filters
+        (function walk(group) {
+            group.each(function(rule) {
+                if (rule.filter && rule.filter.unique) {
+                    if (!self.status.used_filters[rule.filter.id]) {
+                        self.status.used_filters[rule.filter.id] = [];
+                    }
+                    if (rule.filter.unique == 'group') {
+                        self.status.used_filters[rule.filter.id].push(rule.parent);
+                    }
+                }
+            }, function(group) {
+                walk(group);
+            });
+        }(self.model.root));
+
+        self.applyDisabledFilters(e);
+    },
+
+    /**
+     * Clear the list of used filters
+     * @param {$.Event} [e]
+     * @private
+     */
+    clearDisabledFilters: function(e) {
+        var self = e ? e.builder : this;
+
+        self.status.used_filters = {};
+
+        self.applyDisabledFilters(e);
+    },
+
+    /**
+     * Disabled filters depending on the list of used ones
+     * @param {$.Event} [e]
+     * @private
+     */
+    applyDisabledFilters: function(e) {
+        var self = e ? e.builder : this;
+
+        // re-enable everything
+        self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
+
+        // disable some
+        $.each(self.status.used_filters, function(filterId, groups) {
+            if (groups.length === 0) {
+                self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+            }
+            else {
+                groups.forEach(function(group) {
+                    group.each(function(rule) {
+                        rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
+                    });
+                });
+            }
+        });
+
+        // update Selectpicker
+        if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
+            self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
+        }
+    }
+});
+
+
+/*!
+ * jQuery QueryBuilder 2.5.2
+ * Locale: English (en)
+ * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
+ * Licensed under MIT (https://opensource.org/licenses/MIT)
+ */
+
+QueryBuilder.regional['en'] = {
+  "__locale": "English (en)",
+  "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
+  "add_rule": "Add rule",
+  "add_group": "Add group",
+  "delete_rule": "Delete",
+  "delete_group": "Delete",
+  "conditions": {
+    "AND": "AND",
+    "OR": "OR"
+  },
+  "operators": {
+    "equal": "equal",
+    "not_equal": "not equal",
+    "in": "in",
+    "not_in": "not in",
+    "less": "less",
+    "less_or_equal": "less or equal",
+    "greater": "greater",
+    "greater_or_equal": "greater or equal",
+    "between": "between",
+    "not_between": "not between",
+    "begins_with": "begins with",
+    "not_begins_with": "doesn't begin with",
+    "contains": "contains",
+    "not_contains": "doesn't contain",
+    "ends_with": "ends with",
+    "not_ends_with": "doesn't end with",
+    "is_empty": "is empty",
+    "is_not_empty": "is not empty",
+    "is_null": "is null",
+    "is_not_null": "is not null"
+  },
+  "errors": {
+    "no_filter": "No filter selected",
+    "empty_group": "The group is empty",
+    "radio_empty": "No value selected",
+    "checkbox_empty": "No value selected",
+    "select_empty": "No value selected",
+    "string_empty": "Empty value",
+    "string_exceed_min_length": "Must contain at least {0} characters",
+    "string_exceed_max_length": "Must not contain more than {0} characters",
+    "string_invalid_format": "Invalid format ({0})",
+    "number_nan": "Not a number",
+    "number_not_integer": "Not an integer",
+    "number_not_double": "Not a real number",
+    "number_exceed_min": "Must be greater than {0}",
+    "number_exceed_max": "Must be lower than {0}",
+    "number_wrong_step": "Must be a multiple of {0}",
+    "number_between_invalid": "Invalid values, {0} is greater than {1}",
+    "datetime_empty": "Empty value",
+    "datetime_invalid": "Invalid date format ({0})",
+    "datetime_exceed_min": "Must be after {0}",
+    "datetime_exceed_max": "Must be before {0}",
+    "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
+    "boolean_not_valid": "Not a boolean",
+    "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
+  },
+  "invert": "Invert",
+  "NOT": "NOT"
+};
+
+QueryBuilder.defaults({ lang_code: 'en' });
+return QueryBuilder;
+
+}));
\ No newline at end of file
index d3ffe38..b8db034 100644 (file)
                                                <span class="caret"></span>
                                </a>
                                        <ul class="dropdown-menu" role="menu">
-                                               
-                                               <li ng-repeat="section in tabs[dropDownName]"
-                                                       ng-if="section.name==='Create CL'"><a
-                                                       id="{{section.name}}" role="presentation"
-                                                       ng-click="emptyMenuClick(section.link,section.name)"
-                                                       ng-class="{true:'ThisLink', false:''}[!(userInfo['permissionUpdateCl'])]">{{section.name}}</a>
-                                               </li>
-                                               
                                                <li ng-repeat="section in tabs[dropDownName]"
                                                        ng-if="section.name==='Open CL'"><a
                                                        id="{{section.name}}" role="presentation"
                                                </li>
 
                                                <li ng-repeat="section in tabs[dropDownName]"
-                                                       ng-if="section.name != 'Create CL' && section.name != 'Open CL' && section.name != 'ECOMP User Guide - Design Overview' && section.name != 'ECOMP User Guide - Closed Loop Design' && section.name != 'ECOMP User Guide - CLAMP' && section.name != 'User Info'"><a
+                                                       ng-if="section.name != 'Open CL' && section.name != 'ECOMP User Guide - Design Overview' && section.name != 'ECOMP User Guide - Closed Loop Design' && section.name != 'ECOMP User Guide - CLAMP' && section.name != 'User Info'"><a
                                                        id="{{section.name}}" role="presentation"
                                                        ng-click="emptyMenuClick(section.link,section.name)"
                                                        class="ThisLink">{{section.name}}</a>
diff --git a/src/main/resources/META-INF/resources/designer/partials/portfolios/clds_create_model_off_Template.html b/src/main/resources/META-INF/resources/designer/partials/portfolios/clds_create_model_off_Template.html
deleted file mode 100644 (file)
index b2698a7..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<!--
-  ============LICENSE_START=======================================================
-  ONAP CLAMP
-  ================================================================================
-  Copyright (C) 2017 AT&T Intellectual Property. All rights
-                              reserved.
-  ================================================================================
-  Licensed under the Apache License, Version 2.0 (the "License"); 
-  you may not use this file except in compliance with the License. 
-  You may obtain a copy of the License at
-  
-  http://www.apache.org/licenses/LICENSE-2.0
-  
-  Unless required by applicable law or agreed to in writing, software 
-  distributed under the License is distributed on an "AS IS" BASIS, 
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
-  See the License for the specific language governing permissions and 
-  limitations under the License.
-  ============LICENSE_END============================================
-  ===================================================================
-  
-  -->
-
-<div attribute-test="cldsmodelofftemplate" id="configure-widgets">
-    <div  attribute-test="cldsmodelofftemplate" class="modal-header">
-        <button type="button" class="close" ng-click="close(false)" aria-hidden="true" style="margin-top: -3px">&times;</button>
-        <h4>Model Creation</h4>
-    </div>
-    <div attribute-test="cldsmodelofftemplate" class="modal-body" >
-
-        <ul style="margin-bottom:15px;" class="nav nav-tabs">
-                   <li ng-class="{active : typeModel == 'template'}" ng-click="setTypeModel('template');"><a href="#">Template</a></li>
-                   <li ng-class="{active : typeModel == 'clone'}" ng-click="setTypeModel('clone');"><a href="#">Clone</a></li>
-               </ul>        
-               <div ng-show="error.flag">{{error.message}} </div>
-        <div ng-show="(typeModel=='template')">
-            <form name="model" class="form-horizontal" novalidate>
-                <div class="form-group">
-                    <label for="modelName" class="col-sm-3 control-label">Model Name</label>
-                    <div class="col-sm-8">
-                        <input type="text" class="form-control" id="modelName" name="modelName" ng-model="modelName" placeholder="Model Name" ng-change="checkExisting();" autofocus="autofocus" ng-pattern="/^\s*[\w\-]*\s*$/" required ng-trim="true">
-                        <div role="alert"><span ng-show="model.modelName.$error.pattern" style="color: red">Special Characters are not allowed in Model name.</span> <span ng-show="nameinUse" style="color: red"> Model Name Already In Use</span></div>
-                    </div>
-                </div>
-                <div class="form-group">
-                    <label for="modelName" class="col-sm-3 control-label">Templates</label>
-                    <div class="col-sm-8">
-                        <select class="form-control" id="templateName" name="templateName" autofocus="autofocus" required ng-trim="true">
-                            <option ng-repeat="x in templateNamel" value="{{x}}">{{x}}</option>
-                        </select>
-                    </div>
-                </div>
-            </form> 
-        </div>
-        <div ng-show="(typeModel=='clone')">
-            <form name="model" class="form-horizontal" novalidate>
-                <div class="form-group">
-                    <label for="modelName" class="col-sm-3 control-label">Model Name</label>
-                    <div class="col-sm-8">
-                        <input type="text" class="form-control" id="modelName" name="modelName" ng-model="modelName" placeholder="Model Name" ng-change="checkExisting()" autofocus="autofocus" ng-pattern="/^\s*[\w\-]*\s*$/" required ng-trim="true">
-                        <div role="alert"><span ng-show="model.modelName.$error.pattern" style="color: red">Special Characters are not allowed in Model name.</span> <span ng-show="nameinUse" style="color: red"> Model Name Already In Use</span></div>
-                    </div>
-                </div>
-                <div class="form-group">
-                    <label for="modelName" class="col-sm-3 control-label">Clone</label>
-                    <div class="col-sm-8">
-                        <select class="form-control" id="modelList" name="modelList" autofocus="autofocus" required ng-trim="true">
-                            <option ng-repeat="x in modelNamel" value="{{x}}">{{x}}</option>
-                        </select>
-                    </div>
-                </div>
-            </form>
-        </div>
-    </div>
-    <div ng-show="(typeModel=='template')">
-        <div class="modal-footer">
-            <button ng-click="createNewModelOffTemplate(model)" class="btn btn-primary" ng-disabled="spcl || nameinUse" class="btn btn-primary">Create</button>
-            <button ng-click="close(true)" class="btn btn-primary">Cancel</button>
-        </div>
-    </div>
-    <div ng-show="(typeModel=='clone')">
-        <div class="modal-footer">
-            <button ng-click="cloneModel()" class="btn btn-primary" ng-disabled="model.modelName.$error.pattern || nameinUse" class="btn btn-primary">Clone</button>
-            <button ng-click="close(true)" class="btn btn-primary">Cancel</button>
-        </div>
-    </div>
-</div>
\ No newline at end of file
diff --git a/src/main/resources/META-INF/resources/designer/partials/portfolios/tosca_model_properties.html b/src/main/resources/META-INF/resources/designer/partials/portfolios/tosca_model_properties.html
new file mode 100644 (file)
index 0000000..b053b24
--- /dev/null
@@ -0,0 +1,80 @@
+<!--
+  ============LICENSE_START=======================================================
+  ONAP CLAMP
+  ================================================================================
+  Copyright (C) 2017 AT&T Intellectual Property. All rights
+                              reserved.
+  ================================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END=========================================================
+-->
+
+<style>
+#paramsWarn {
+       display: none;
+}
+
+.modal-dialog {
+       width: 1100px;
+}
+
+</style>
+
+<div id="configure-widgets">
+       <div class="modal-header">
+               <button type="button" class="close" data-ng-click="close(false)"
+                       aria-hidden="true" style="margin-top: -3px">&times;</button>
+               <h4>{{ toscaModelName }}</h4>
+       </div>
+
+       <div class="modal-body" style="display:block; height:600px; overflow:auto;">
+               <i hidden id="ridinSpinners" class="fa fa-spinner fa-spin"
+                       style="display: none; margin-bottom: 10px; width: 100%; text-align: center; font-size: 24px; color: black;"></i>
+               <form id="saveProps">
+                       <!-- <div ng-if="(simpleModel!==true)">  -->
+                       <div class="alert alert-danger" role="alert" id='paramsWarn'>
+                               <strong>Ooops!</strong> Unable to load properties for <span
+                                       id='servName'>. Would you like to</span> <a
+                                       href="javascript:void(0)" class="btn-link" id='paramsRetry'>Retry
+                               </a> / <a href="javascript:void(0)" class="btn-link" id='paramsCancel'>Cancel</a>
+                       </div>
+                       <div class="form-group clearfix" data-ng-if="policytypes">
+                               <label for="policytypes" class="col-sm-4 control-label">
+                                       Policy Types<span id="locationErr"
+                                       style="display: none; color: red;">&nbsp;*Required*</span>
+                               </label>
+
+                               <div class="col-sm-8">
+                                        <select class="form-control" id="policytype" data-ng-change = "jsonByPolicyType(selectedHPPolicy, '{{selectedHPPolicy}}', '')" data-ng-model ="$parent.selectedHPPolicy">
+                                               <option data-ng-repeat="pt in policytypes" value="{{pt}}">{{pt}}</option>
+                                        </select>
+                               </div>
+                       </div>
+               </form>
+               <div class="alert alert-warning propChangeWarn" style="display: none;">
+                       <strong>Warning!</strong> Property changes will reset all associated
+                       GUI fields.
+               </div>
+               <div class="modal-body" id="form1" style="display: none">
+                       <div class="container-fluid">
+                               <div class="row">
+                                       <div id="editor"></div>
+                               </div>
+                       </div>
+               </div>
+       </div>
+</div>
+<div class="modal-footer">
+       <button data-ng-click="saveToscaProps()" id="savePropsBtn" class="btn btn-primary">Done</button>
+       <button data-ng-click="close(true)" id="close_button" class="btn btn-primary">Cancel</button>
+</div>
index 98e8443..9d4598b 100644 (file)
@@ -27,24 +27,36 @@ app
     'alertService',
     '$http',
     '$q',
-    function(alertService, $http, $q) {
-
-           function checkIfElementType(name) {
-
-                   // This will open the methods located in the app.js
-                   if (undefined == name) {
-                           return;
-                   }
-                   mapping = {
-                       'tca' : TCAWindow,
-                       'policy' : PolicyWindow,
-                       'vescollector' : VesCollectorWindow,
-                       'holmes' : HolmesWindow,
-                   };
-                   key = name.split('_')[0].toLowerCase()
-                   if (key in mapping) {
-                           mapping[key]();
-                   }
+    '$rootScope',
+    function(alertService, $http, $q, $rootScope) {
+
+           function checkIfElementType(name, isSimple) {
+
+        //This will open the methods located in the app.js
+                 if (isSimple){
+                         if (undefined == name) {
+                                 return;
+                         }else if (name.toLowerCase().indexOf("policy") >= 0){
+                                         PolicyWindow();
+                         } else {
+                                 $rootScope.selectedBoxName = name.toLowerCase();
+                                 ToscaModelWindow();
+                         }
+                 } else {
+                         if (undefined == name) {
+                                 return;
+                         }
+                       mapping = {
+                               'tca' : TCAWindow,
+                               'policy' : PolicyWindow,
+                               'vescollector' : VesCollectorWindow,
+                               'holmes' : HolmesWindow,
+                           };
+                           key = name.split('_')[0].toLowerCase()
+                           if (key in mapping) {
+                                   mapping[key]();
+                           }
+                 };
            }
            function handleQueryToBackend(def, svcAction, svcUrl, svcPayload) {
 
@@ -242,7 +254,7 @@ app
                            document.getElementById(menuText).classList.add('ThisLink');
                    }
            };
-           this.processActionResponse = function(modelName, pars) {
+           this.processActionResponse = function(modelName, pars, simple) {
 
                    // populate control name (prefix and uuid here)
                    var controlNamePrefix = pars.controlNamePrefix;
@@ -260,7 +272,7 @@ app
                    document.getElementById("modeler_name").textContent = headerText;
                    document.getElementById("templa_name").textContent = ("Template Used - " + selected_template);
                    setStatus(pars)
-                   addSVG(pars);
+                   disableBPMNAddSVG(pars, simple);
                    this.enableDisableMenuOptions(pars);
            };
            this.processRefresh = function(pars) {
@@ -309,7 +321,7 @@ app
                    '<span id="status_clds" style="position: absolute;  left: 61%;top: 151px; font-size:20px;">Status: '
                    + statusMsg + '</span>');
            }
-           function addSVG(pars) {
+           function disableBPMNAddSVG(pars, simple) {
 
                    var svg = pars.imageText.substring(pars.imageText.indexOf("<svg"))
                    if ($("#svgContainer").length > 0)
@@ -330,7 +342,7 @@ app
                            var name = $($(event.target).parent()).attr("data-element-id")
                            lastElementSelected = $($(event.target).parent()).attr(
                            "data-element-id")
-                           checkIfElementType(name)
+                           checkIfElementType(name, simple)
                    });
            }
            this.enableDisableMenuOptions = function(pars) {
@@ -345,14 +357,11 @@ app
                            document.getElementById('Close Model').classList
                            .remove('ThisLink');
                            // disable models options
-                           document.getElementById('Create CL').classList.add('ThisLink');
                            document.getElementById('Save CL').classList.add('ThisLink');
                            document.getElementById('Revert Model Changes').classList
                            .add('ThisLink');
                    } else {
                            // enable menu options
-                           document.getElementById('Create CL').classList
-                           .remove('ThisLink');
                            document.getElementById('Save CL').classList.remove('ThisLink');
                            document.getElementById('Properties CL').classList
                            .remove('ThisLink');
index 2d1eeaa..a64af74 100644 (file)
@@ -26,12 +26,14 @@ app
 [
 '$scope',
 '$rootScope',
+'$modalInstance',
+'$window',
 '$uibModalInstance',
 'cldsModelService',
 '$location',
 'dialogs',
 'cldsTemplateService',
-function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
+function($scope, $rootScope, $modalInstance, $window, $uibModalInstance, cldsModelService, $location,
          dialogs, cldsTemplateService) {
        $scope.typeModel = 'template';
        $scope.error = {
@@ -92,14 +94,20 @@ function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
                }
                return false;
        }
-       $scope.checkExisting = function() {
-               var name = $('#modelName').val();
-               if (contains($scope.modelNamel, name)) {
-                       $scope.nameinUse = true;
+       $scope.checkExisting=function(checkVal, errPatt, num){
+               var name = checkVal;
+               if (!errPatt && (checkVal!== undefined)){
+                       if(contains($scope.modelNamel,name)){
+                               $scope["nameinUse"+num]=true;
+                               return true;
+                       }else{
+                               $scope["nameinUse"+num]=false;
+                               return false;
+                       }
                } else {
-                       $scope.nameinUse = false;
+                       $scope["nameinUse"+num]=false;
+                       return false;
                }
-               specialCharacters();
        }
        function specialCharacters() {
                $scope.spcl = false;
@@ -117,126 +125,8 @@ function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
                $rootScope.isNewClosed = false;
                $uibModalInstance.close("closed");
        };
-       $scope.createNewModelOffTemplate = function(formModel) {
-               reloadDefaultVariables(false)
-               var modelName = document.getElementById("modelName").value;
-               var templateName = document.getElementById("templateName").value;
-               if (!modelName) {
-                       $scope.error.flag = true;
-                       $scope.error.message = "Please enter any closed template name for proceeding";
-                       return false;
-               }
-               // init UTM items
-               $scope.utmModelsArray = [];
-               $scope.selectedParent = {};
-               $scope.currentUTMModel = {};
-               $scope.currentUTMModel.selectedParent = {};
-               $rootScope.oldUTMModels = [];
-               $rootScope.projectName = "clds_default_project";
-               var utmModels = {};
-               utmModels.name = modelName;
-               utmModels.subModels = [];
-               $rootScope.utmModels = utmModels;
-               cldsTemplateService.getTemplate(templateName).then(function(pars) {
-                       var tempImageText = pars.imageText;
-                       var authorizedToUp = pars.userAuthorizedToUpdate;
-                       pars = {}
-                       pars.imageText = tempImageText
-                       pars.status = "DESIGN";
-                       if (readMOnly) {
-                               pars.permittedActionCd = [ "" ];
-                       } else {
-                               pars.permittedActionCd = [ "TEST", "SUBMIT" ];
-                       }
-                       selected_template = templateName
-                       selected_model = modelName;
-                       cldsModelService.processActionResponse(modelName, pars);
-                       // set model bpmn and open diagram
-                       $rootScope.isPalette = true;
-               }, function(data) {
-                       // alert("getModel failed");
-               });
-               allPolicies = {};
-               elementMap = {};
-               $uibModalInstance.close("closed");
-       }
-       $scope.cloneModel = function() {
-               reloadDefaultVariables(false)
-               var modelName = document.getElementById("modelName").value;
-               var originalModel = document.getElementById("modelList").value;
-               if (!modelName) {
-                       $scope.error.flag = true;
-                       $scope.error.message = "Please enter any name for proceeding";
-                       return false;
-               }
-               // init UTM items
-               $scope.utmModelsArray = [];
-               $scope.selectedParent = {};
-               $scope.currentUTMModel = {};
-               $scope.currentUTMModel.selectedParent = {};
-               $rootScope.oldUTMModels = [];
-               $rootScope.projectName = "clds_default_project";
-               var utmModels = {};
-               utmModels.name = modelName;
-               utmModels.subModels = [];
-               $rootScope.utmModels = utmModels;
-               cldsModelService.getModel(originalModel).then(function(pars) {
-                       // process data returned
-                       var propText = pars.propText;
-                       var status = pars.status;
-                       var controlNamePrefix = pars.controlNamePrefix;
-                       var controlNameUuid = pars.controlNameUuid;
-                       selected_template = pars.templateName;
-                       typeID = pars.typeId;
-                       pars.status = "DESIGN";
-                       if (readMOnly) {
-                               pars.permittedActionCd = [ "" ];
-                       } else {
-                               pars.permittedActionCd = [ "TEST", "SUBMIT" ];
-                       }
-                       pars.controlNameUuid = "";
-                       modelEventService = pars.event;
-                       // actionCd = pars.event.actionCd;
-                       actionStateCd = pars.event.actionStateCd;
-                       deploymentId = pars.deploymentId;
-                       var authorizedToUp = pars.userAuthorizedToUpdate;
-                       cldsModelService.processActionResponse(modelName, pars);
-                       // deserialize model properties
-                       if (propText == null) {
-                       } else {
-                               elementMap = JSON.parse(propText);
-                       }
-                       selected_model = modelName;
-                       // set model bpmn and open diagram
-                       $rootScope.isPalette = true;
-               }, function(data) {
-               });
-               $uibModalInstance.close("closed");
-       }
-       $scope.createNewModel = function() {
-               reloadDefaultVariables(false)
-               var modelName = document.getElementById("modelName").value;
-               // BEGIN env
-               // init UTM items
-               $scope.utmModelsArray = [];
-               $scope.selectedParent = {};
-               $scope.currentUTMModel = {};
-               $scope.currentUTMModel.selectedParent = {};
-               $rootScope.oldUTMModels = [];
-               $rootScope.projectName = "clds_default_project";
-               var utmModels = {};
-               utmModels.name = modelName;
-               utmModels.subModels = [];
-               $rootScope.utmModels = utmModels;
-               // enable appropriate menu options
-               var pars = {
-                       status : "DESIGN"
-               };
-               cldsModelService.processActionResponse(modelName, pars);
-               selected_model = modelName;
-               // set model bpmn and open diagram
-               $rootScope.isPalette = true;
-               $uibModalInstance.close("closed");
+       $scope.closeDiagram=function(){
+               $window.location.reload();
        }
        $scope.revertChanges = function() {
                $scope.openModel();
@@ -257,6 +147,7 @@ function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
                var utmModels = {};
                utmModels.name = modelName;
                utmModels.subModels = [];
+               utmModels.type = 'Model';
                $rootScope.utmModels = utmModels;
                cldsModelService.getModel(modelName).then(function(pars) {
                        // process data returned
@@ -273,13 +164,16 @@ function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
                        if (readMOnly) {
                                pars.permittedActionCd = [ "" ];
                        }
-                       cldsModelService.processActionResponse(modelName, pars);
+
                        // deserialize model properties
                        if (propText == null) {
                        } else {
                                elementMap = JSON.parse(propText);
                        }
+                       var simple = elementMap.simpleModel;
+                       $rootScope.isSimpleModel = simple;
                        selected_model = modelName;
+                       cldsModelService.processActionResponse(modelName, pars, simple);
                        // set model bpmn and open diagram
                        $rootScope.isPalette = true;
                }, function(data) {
diff --git a/src/main/resources/META-INF/resources/designer/scripts/ToscaModelCtrl.js b/src/main/resources/META-INF/resources/designer/scripts/ToscaModelCtrl.js
new file mode 100644 (file)
index 0000000..f43161e
--- /dev/null
@@ -0,0 +1,156 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights
+ *                             reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * ===================================================================
+ *
+ */
+app.controller('ToscaModelCtrl',
+    ['$scope', '$rootScope', '$modalInstance', '$location', 'dialogs', 'toscaModelService',
+    function($scope, $rootScope, $modalInstance, $location, dialogs, toscaModelService) {
+
+        $scope.jsonByPolicyType = function(selectedPolicy, oldSelectedPolicy, editorData){
+               if (selectedPolicy && selectedPolicy != '') {
+                       toscaModelService.getHpModelJsonByPolicyType(selectedPolicy).then(function(response) {
+                               $('#editor').empty();
+                               // get the list of available policies
+                               $scope.getPolicyList();
+                               var toscaModel = JSON.parse(response.body.toscaModelJson);
+                               if($scope.policyList && toscaModel.schema.properties && toscaModel.schema.properties.policyList){
+                                       toscaModel.schema.properties.policyList.enum = $scope.policyList;
+                               }
+
+                               JSONEditor.defaults.options.theme = 'bootstrap3';
+                               JSONEditor.defaults.options.iconlib = 'bootstrap2';
+                               JSONEditor.defaults.options.object_layout = 'grid';
+                               JSONEditor.defaults.options.disable_properties = true;
+                               JSONEditor.defaults.options.disable_edit_json = true;
+                               JSONEditor.defaults.options.disable_array_reorder = true;
+                               JSONEditor.defaults.options.disable_array_delete_last_row = true;
+                               JSONEditor.defaults.options.disable_array_delete_all_rows = false;
+                               JSONEditor.defaults.options.show_errors = 'always';
+
+                               if($scope.editor) { $scope.editor.destroy(); }
+                               $scope.editor = new JSONEditor(document.getElementById("editor"),
+                                                     { schema: toscaModel.schema, startval: editorData });
+                               $scope.editor.watch('root.policy.recipe',function() {
+
+                               });
+                               $('#form1').show();
+                       });
+               } else {
+                               $('#editor').empty();
+                               $('#form1').hide();
+                       }
+        }
+
+        $scope.getPolicyList = function(){
+                       var policyNameList = [];
+                       if (typeof elementMap !== 'undefined'){
+                               for (key in elementMap){
+                                       if (key.indexOf('Policy')>-1){
+                                               angular.forEach(Object.keys(elementMap[key]), function(text, val){
+                                                       for (policyKey in elementMap[key][text]){
+                                                               if(elementMap[key][text][policyKey].name == 'pname'){
+                                                                       policyNameList.push(elementMap[key][text][policyKey].value);
+                                                               }
+                                                       }
+                                               });
+                                       }
+                               }
+                       };
+                       $scope.policyList = policyNameList;
+               }
+
+        if($rootScope.selectedBoxName) {
+               var policyType = $rootScope.selectedBoxName.split('_')[0].toLowerCase();
+               $scope.toscaModelName = policyType.toUpperCase() + " Microservice";
+               if(elementMap[lastElementSelected]) {
+                       $scope.jsonByPolicyType(policyType, '', elementMap[lastElementSelected][policyType]);
+               }else{
+                       $scope.jsonByPolicyType(policyType, '', '');
+               }
+           }
+
+        $scope.getEditorData = function(){
+               if(!$scope.editor){
+                       return null;
+               }
+               var errors = $scope.editor.validate();
+               var editorData = $scope.editor.getValue();
+
+               if(errors.length) {
+                       $scope.displayErrorMessage(errors);
+                       return null;
+               }
+               else{
+                       console.log("there are NO validation errors........");
+               }
+               return editorData;
+        }
+
+        $scope.saveToscaProps = function(){
+               var policyType = $rootScope.selectedBoxName.split('_')[0].toLowerCase();
+               var data = $scope.getEditorData();
+
+            if(data !== null) {
+               data = {[policyType]: data};
+                       saveProperties(data);
+                       if($scope.editor) { $scope.editor.destroy(); $scope.editor = null; }
+                       $modalInstance.close('closed');
+               }
+        }
+
+        $scope.displayErrorMessage = function(errors){
+               console.log("there are validation errors.....");
+               var all_errs = "Please address the following issues before selecting 'Done' or 'Policy Types':\n";
+               for (var i = 0; i < errors.length; i++) {
+                 if(all_errs.indexOf(errors[i].message) < 0) {
+                       all_errs += '\n' + errors[i].message;
+                 }
+               }
+            window.alert(all_errs);
+        };
+
+        $scope.close = function(){
+               angular.copy(elementMap[lastElementSelected], $scope.hpPolicyList);
+                       $modalInstance.close('closed');
+                       if($scope.editor) { $scope.editor.destroy(); $scope.editor = null; }
+        }
+
+        $scope.checkDuplicateInObject = function(propertyName, inputArray) {
+                 var seenDuplicate = false,
+                     testObject = {};
+
+                 inputArray.map(function(item) {
+                   var itemPropertyName = item[propertyName];
+                   if (itemPropertyName in testObject) {
+                     testObject[itemPropertyName].duplicate = true;
+                     item.duplicate = true;
+                     seenDuplicate = true;
+                   }
+                   else {
+                     testObject[itemPropertyName] = item;
+                     delete item.duplicate;
+                   }
+                 });
+
+                 return seenDuplicate;
+               }
+}
+]);
\ No newline at end of file
diff --git a/src/main/resources/META-INF/resources/designer/scripts/ToscaModelService.js b/src/main/resources/META-INF/resources/designer/scripts/ToscaModelService.js
new file mode 100644 (file)
index 0000000..c99a455
--- /dev/null
@@ -0,0 +1,38 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights
+ *                             reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * ===================================================================
+ *
+ */
+app.service('toscaModelService', ['alertService','$http', '$q', '$rootScope', function (alertService,$http, $q, $rootScope) {
+
+       this.getHpModelJsonByPolicyType = function(policyType) {
+               var sets = [];
+               var svcUrl = "/restservices/clds/v1/tosca/models/policyType/" + policyType;
+               return $http({
+                       method : "GET",
+                       url : svcUrl
+               }).then(function successCallback(response) {
+                       return response.data;
+               }, function errorCallback(response) {
+                       //Open Model Unsuccessful
+                       return response.data;
+               });
+       };
+ }]);
index 9dc104b..c9bb9e3 100644 (file)
@@ -266,9 +266,6 @@ function($scope, $rootScope, $timeout, dialogs) {
                                    $scope.cldsClose();
                            } else if (name == "Refresh ASDC") {
                                    $scope.cldsRefreshASDC();
-                           } else if (name == "Create CL") {
-                                   $rootScope.isNewClosed = true;
-                                   $scope.cldsCreateModel();
                            } else if (name == "Open CL") {
                                    $scope.cldsOpenModel();
                            } else if (name == "Save CL") {
@@ -308,9 +305,6 @@ function($scope, $rootScope, $timeout, dialogs) {
            };
            $scope.tabs = {
                "Closed Loop" : [ {
-                   link : "/cldsCreateModel",
-                   name : "Create CL"
-               }, {
                    link : "/cldsOpenModel",
                    name : "Open CL"
                }, {
@@ -597,25 +591,6 @@ function($scope, $rootScope, $timeout, dialogs) {
 
                    });
            };
-           $scope.cldsCreateModel = function() {
-
-                   var dlg = dialogs.create(
-                   'partials/portfolios/clds_create_model_off_Template.html',
-                   'CldsOpenModelCtrl', {
-                       closable : true,
-                       draggable : true
-                   }, {
-                       size : 'lg',
-                       keyboard : true,
-                       backdrop : 'static',
-                       windowClass : 'my-class'
-                   });
-                   dlg.result.then(function(name) {
-
-                   }, function() {
-
-                   });
-           };
            $scope.extraUserInfo = function() {
 
                    var dlg = dialogs.create(
@@ -807,6 +782,13 @@ function($scope, $rootScope, $timeout, dialogs) {
 
                    });
            };
+           $scope.ToscaModelWindow = function (tosca_model) {
+
+               var dlg = dialogs.create('partials/portfolios/tosca_model_properties.html','ToscaModelCtrl',{closable:true,draggable:true},{size:'lg',keyboard: true,backdrop: 'static',windowClass: 'my-class'});
+               dlg.result.then(function(name){
+               },function(){
+               });
+           };
            $scope.PolicyWindow = function(policy) {
 
                    var dlg = dialogs.create(
@@ -935,6 +917,9 @@ function GOCWindow() {
 
        angular.element(document.getElementById('navbar')).scope().GOCWindow();
 }
+function ToscaModelWindow() {
+    angular.element(document.getElementById('navbar')).scope().ToscaModelWindow();
+};
 function PolicyWindow(PolicyWin) {
 
        angular.element(document.getElementById('navbar')).scope().PolicyWindow(