From 6ebc7c0fb9a042a384bc3bfdae3cd222096fcdb0 Mon Sep 17 00:00:00 2001 From: xg353y Date: Wed, 6 Feb 2019 16:21:40 +0100 Subject: Automatic Config Policy Ui generation Automatic GUI generation based on json output of policy-model. And remove the Create CL related code. Change-Id: I42f7e8da46052e01bda33593434f8794f0e430c5 Signed-off-by: xg353y Issue-ID: CLAMP-264 --- .../META-INF/resources/designer/index.html | 7 +- .../META-INF/resources/designer/lib/jsoneditor.js | 10235 +++++++++++++++++++ .../designer/lib/query-builder.standalone.js | 6541 ++++++++++++ .../META-INF/resources/designer/partials/menu.html | 10 +- .../portfolios/clds_create_model_off_Template.html | 87 - .../portfolios/tosca_model_properties.html | 80 + .../resources/designer/scripts/CldsModelService.js | 59 +- .../designer/scripts/CldsOpenModelCtrl.js | 150 +- .../resources/designer/scripts/ToscaModelCtrl.js | 156 + .../designer/scripts/ToscaModelService.js | 38 + .../META-INF/resources/designer/scripts/app.js | 35 +- 11 files changed, 17123 insertions(+), 275 deletions(-) create mode 100644 src/main/resources/META-INF/resources/designer/lib/jsoneditor.js create mode 100644 src/main/resources/META-INF/resources/designer/lib/query-builder.standalone.js delete mode 100644 src/main/resources/META-INF/resources/designer/partials/portfolios/clds_create_model_off_Template.html create mode 100644 src/main/resources/META-INF/resources/designer/partials/portfolios/tosca_model_properties.html create mode 100644 src/main/resources/META-INF/resources/designer/scripts/ToscaModelCtrl.js create mode 100644 src/main/resources/META-INF/resources/designer/scripts/ToscaModelService.js (limited to 'src/main') 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%"> - + + + + @@ -172,6 +175,8 @@ + + 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 0 && (obj.length - 1) in obj)) { + for(i=0; i= 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.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 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 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 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 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=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 -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("
"+self.sceditor_instance.val()+"
"); + // 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 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[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 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.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= 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.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= 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; inull'; + } + // 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 = '
'+i+': '+html+'
'; + } + + // TODO: use theme + ret += '
  • '+html+'
  • '; + }); + + if(Array.isArray(el)) ret = '
      '+ret+'
    '; + else ret = "
      "+ret+'
    '; + + return ret; + } + // Boolean + else if(typeof el === "boolean") { + return el? 'true' : 'false'; + } + // String + else if(typeof el === "string") { + return el.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 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= 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; iSize: '+Math.floor((this.value.length-this.value.split(',')[0].length-1)/1.33333)+' bytes'; + if(mime.substr(0,5)==="image") { + this.preview.innerHTML += '
    '; + 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 = 'Type: '+mime+', Size: '+file.size+' bytes'; + if(mime.substr(0,5)==="image") { + this.preview.innerHTML += '
    '; + 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 += '
    '; + 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
    "; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.innerHTML = "
    "; + 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 = "
    "; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.className = 'tabbable tabs-over'; + el.innerHTML = "
    "; + 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 = "
    "; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.innerHTML = "
    "; + 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 = + "
    "; +el.className = "row"; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.innerHTML = "
    "; + 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',''); + 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 = '
    '; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.className = 'row'; + el.innerHTML = '
    '; + 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 = '
    '; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.className = 'row'; + el.innerHTML = '
    '; + 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 = '
      '; + return el; + }, + getTopTabHolder: function(propertyName) { + var pName = (typeof propertyName === 'undefined')? "" : propertyName; + var el = document.createElement('div'); + el.className = 'grid-y'; + el.innerHTML = '
        '; + 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 = [ + '
        ', + '
          ', + '
        ', + '
        ', + '
        ', + '
        ' + ].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
        . + 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
        . + 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= 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+"} + * @readonly + */ + this.icons = this.settings.icons; + + /** + * List of operators + * @member {QueryBuilder.Operator[]} + * @readonly + */ + this.operators = this.settings.operators; + + /** + * List of templates + * @member {object.} + * @readonly + */ + this.templates = this.settings.templates; + + /** + * Plugins configuration + * @member {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.} + * @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.} + * @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.} + * @readonly + */ +QueryBuilder.templates = {}; + +/** + * Localized strings (see i18n/) + * @type {object.} + * @readonly + */ +QueryBuilder.regional = {}; + +/** + * Default operators + * @type {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.} + */ +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.} 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.
        + * 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 - not 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 - not 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 = '\ +
        \ +
        \ +
        \ + \ + {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \ + \ + {{?}} \ + {{? it.level>1 }} \ + \ + {{?}} \ +
        \ +
        \ + {{~ it.conditions: condition }} \ + \ + {{~}} \ +
        \ + {{? it.settings.display_errors }} \ +
        \ + {{?}} \ +
        \ +
        \ +
        \ +
        \ +
        '; + +QueryBuilder.templates.rule = '\ +
        \ +
        \ +
        \ + \ +
        \ +
        \ + {{? it.settings.display_errors }} \ +
        \ + {{?}} \ +
        \ +
        \ +
        \ +
        '; + +QueryBuilder.templates.filterSelect = '\ +{{ var optgroup = null; }} \ +'; + +QueryBuilder.templates.operatorSelect = '\ +{{? it.operators.length === 1 }} \ + \ +{{= it.translate("operators", it.operators[0].type) }} \ + \ +{{?}} \ +{{ var optgroup = null; }} \ +'; + +QueryBuilder.templates.ruleValueSelect = '\ +{{ var optgroup = null; }} \ +'; + +/** + * 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 += ' ' + val + ' '; + }); + break; + + case 'select': + h = this.getRuleValueSelect(name, rule); + break; + + case 'textarea': + h += '