diff options
11 files changed, 17123 insertions, 275 deletions
diff --git a/src/main/resources/META-INF/resources/designer/index.html b/src/main/resources/META-INF/resources/designer/index.html index d8b3fedad..5d1e53047 100644 --- a/src/main/resources/META-INF/resources/designer/index.html +++ b/src/main/resources/META-INF/resources/designer/index.html @@ -92,7 +92,10 @@ 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> @@ -172,6 +175,8 @@ <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 index 000000000..2966fac97 --- /dev/null +++ b/src/main/resources/META-INF/resources/designer/lib/jsoneditor.js @@ -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,'&').replace(/</g,'<').replace(/>/g,'>'); + } + // 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 index 000000000..169b28787 --- /dev/null +++ b/src/main/resources/META-INF/resources/designer/lib/query-builder.standalone.js @@ -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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" }, + 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"> </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 diff --git a/src/main/resources/META-INF/resources/designer/partials/menu.html b/src/main/resources/META-INF/resources/designer/partials/menu.html index d3ffe3861..b8db03470 100644 --- a/src/main/resources/META-INF/resources/designer/partials/menu.html +++ b/src/main/resources/META-INF/resources/designer/partials/menu.html @@ -99,14 +99,6 @@ <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" @@ -114,7 +106,7 @@ </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 index b2698a7ff..000000000 --- a/src/main/resources/META-INF/resources/designer/partials/portfolios/clds_create_model_off_Template.html +++ /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">×</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 index 000000000..b053b24ed --- /dev/null +++ b/src/main/resources/META-INF/resources/designer/partials/portfolios/tosca_model_properties.html @@ -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">×</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;"> *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> diff --git a/src/main/resources/META-INF/resources/designer/scripts/CldsModelService.js b/src/main/resources/META-INF/resources/designer/scripts/CldsModelService.js index 98e8443ee..9d4598b8e 100644 --- a/src/main/resources/META-INF/resources/designer/scripts/CldsModelService.js +++ b/src/main/resources/META-INF/resources/designer/scripts/CldsModelService.js @@ -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'); diff --git a/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js b/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js index 2d1eeaa80..a64af7467 100644 --- a/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js +++ b/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js @@ -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 index 000000000..f43161ec0 --- /dev/null +++ b/src/main/resources/META-INF/resources/designer/scripts/ToscaModelCtrl.js @@ -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 index 000000000..c99a4556b --- /dev/null +++ b/src/main/resources/META-INF/resources/designer/scripts/ToscaModelService.js @@ -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; + }); + }; + }]); diff --git a/src/main/resources/META-INF/resources/designer/scripts/app.js b/src/main/resources/META-INF/resources/designer/scripts/app.js index 9dc104b1f..c9bb9e3a4 100644 --- a/src/main/resources/META-INF/resources/designer/scripts/app.js +++ b/src/main/resources/META-INF/resources/designer/scripts/app.js @@ -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( |