+ /*
+ * Copyright 2015 brutusin.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "SuperLicense");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @author Ignacio del Valle Alles idelvall@brutusin.org
+ */
+
+/**
+ * @name BrutusinForms
+ * @class
+ * @classdesc This is the main brutusin-form class
+ */
+if (typeof brutusin === "undefined") {
+ window.brutusin = new Object();
+} else if (typeof brutusin !== "object") {
+ throw ("brutusin global variable already exists");
+}
+
+(function () {
+ if (!String.prototype.startsWith) {
+ String.prototype.startsWith = function (searchString, position) {
+ position = position || 0;
+ return this.indexOf(searchString, position) === position;
+ };
+ }
+ if (!String.prototype.endsWith) {
+ String.prototype.endsWith = function (searchString, position) {
+ var subjectString = this.toString();
+ if (position === undefined || position > subjectString.length) {
+ position = subjectString.length;
+ }
+ position -= searchString.length;
+ var lastIndex = subjectString.indexOf(searchString, position);
+ return lastIndex !== -1 && lastIndex === position;
+ };
+ }
+ if (!String.prototype.includes) {
+ String.prototype.includes = function () {
+ 'use strict';
+ return String.prototype.indexOf.apply(this, arguments) !== -1;
+ };
+ }
+ if (!String.prototype.format) {
+ String.prototype.format = function () {
+ var formatted = this;
+ for (var i = 0; i < arguments.length; i++) {
+ var regexp = new RegExp('\\{' + i + '\\}', 'gi');
+ formatted = formatted.replace(regexp, arguments[i]);
+ }
+ return formatted;
+ };
+ }
+
+ /**
+ * Static class of BrutusinForms
+ * @constructor
+ * @type {object}
+ */
+ var BrutusinForms = new Object();
+
+ /**
+ * An array of messages used in the generation of HTML form and validations.
+ * @name BrutusinForms.messages
+ * @type {array}
+ */
+ BrutusinForms.messages = {
+ "validationError": "Validation error",
+ "required": "This field is **required**",
+ "invalidValue": "Invalid field value",
+ "addpropNameExistent": "This property is already present in the object",
+ "addpropNameRequired": "A name is required",
+ "minItems": "At least `{0}` items are required",
+ "maxItems": "At most `{0}` items are allowed",
+ "pattern": "Value does not match pattern: `{0}`",
+ "minLength": "Value must be **at least** `{0}` characters long",
+ "maxLength": "Value must be **at most** `{0}` characters long",
+ "multipleOf": "Value must be **multiple of** `{0}`",
+ "minimum": "Value must be **greater or equal than** `{0}`",
+ "exclusiveMinimum": "Value must be **greater than** `{0}`",
+ "maximum": "Value must be **lower or equal than** `{0}`",
+ "exclusiveMaximum": "Value must be **lower than** `{0}`",
+ "minProperties": "At least `{0}` properties are required",
+ "maxProperties": "At most `{0}` properties are allowed",
+ "email": "The email must at least consists an asterisk (@), following by a domain name with a dot (.)",
+ "url": "The URL provided is not a valid URL.",
+ "uniqueItems": "Array items must be unique",
+ "addItem": "Add item",
+ "true": "True",
+ "false": "False"
+ };
+
+ /**
+ * Callback functions to be notified after an HTML element has been rendered (passed as parameter).
+ * @name BrutusinForms.decorators
+ * @callback
+ * @type {array}
+ */
+ BrutusinForms.decorators = new Array();
+
+ /**
+ * Register a callback function to be notified after an HTML element has been rendered (passed as parameter). See {@link brutusin-json-forms-bootstrap.js} for an example of bootstrap decorator.
+ * @static
+ * @name BrutusinForms.addDecorator
+ * @param {type} f
+ */
+ BrutusinForms.addDecorator = function (f) {
+ BrutusinForms.decorators[BrutusinForms.decorators.length] = f;
+ };
+
+ /**
+ * Register a callback function to be notified when just starting to resolve a schema
+ * @static
+ * @name BrutusinForms.onResolutionStarted
+ * @param {HTMLElement} element
+ */
+ BrutusinForms.onResolutionStarted = function (element) {
+ };
+
+ /**
+ * Register a callback function to be notified when the resolution is completed
+ * @static
+ * @name BrutusinForms.onResolutionFinished
+ * @param {HTMLElement} element
+ */
+ BrutusinForms.onResolutionFinished = function (element) {
+ };
+
+ /**
+ * Calls the onValidationError function when there is a validation error on the form
+ * @static
+ * @name BrutusinForms.onValidationError
+ * @param {HTMLElement} element
+ * @param {string} message
+ */
+ BrutusinForms.onValidationError = function (element, message) {
+ element.focus();
+ if (!element.className.includes(" error")) {
+ element.className += " error";
+ }
+ alert(message);
+ };
+
+ /**
+ * Calls when the validation is passed
+ * @static
+ * @name BrutusinForms.onValidationSuccess
+ * @param {HTMLElement} element
+ */
+ BrutusinForms.onValidationSuccess = function (element) {
+ element.className = element.className.replace(" error", "");
+ };
+
+ /**
+ * Callback function to be notified after a form has been rendered (passed as parameter).
+ * @name BrutusinForms.postRender
+ * @callback
+ * @type type
+ */
+ BrutusinForms.postRender = null;
+ /**
+ * BrutusinForms instances created in the document
+ * @name BrutusinForms.instances
+ * @type {Array}
+ */
+ BrutusinForms.instances = new Array();
+ /**
+ * BrutusinForms factory method
+ * @name BrutusinForms.create
+ * @static
+ * @param {type} schema schema object
+ * @property {object} schemaMap - The schema map extracted from the original schema excluding some fields.
+ * @property {object} dependencyMap - The dependency map that checks which schema is it dependent on.
+ * @property {object} renderInfoMap - The schema that is extracted from the schema resolver.
+ * @property {HTMLElement} container - Container of the rendered form.
+ * @property {object} data - The JSON schema retrieved from the webpage.
+ * @property {string} error - Error message that will be display in the webpage.
+ * @property {(string|number|boolean)} initialValue - The initial value defined in the JSON schema.
+ * @property {number} inputCounter - Counter for the input fields generated, used for input ID generation.
+ * @property {object} root - The `schema` param will be passed into here.
+ * @property {string} formId - The unique form ID will be bind on the `<form id="">`.
+ * @returns {BrutusinForms.create.obj|Object|Object.create.obj}
+ */
+ BrutusinForms.create = function (schema) {
+ var SCHEMA_ANY = {"type": "any"};
+ var obj = new Object();
+
+ var schemaMap = new Object();
+ var dependencyMap = new Object();
+ var renderInfoMap = new Object();
+ var container;
+ var data;
+ var error;
+ var initialValue;
+ var inputCounter = 0;
+ var root = schema;
+ var formId = "BrutusinForms#" + BrutusinForms.instances.length;
+
+ renameRequiredPropeties(schema); // required v4 (array) -> requiredProperties
+ populateSchemaMap("$", schema);
+
+ validateDepencyMapIsAcyclic();
+
+ /**
+ * Use to identify the input type of the field and renders the form
+ * @name renderers
+ * @type {object}
+ */
+ var renderers = new Object();
+
+ renderers["integer"] = function (container, id, parentObject, propertyProvider, value) {
+ renderers["string"](container, id, parentObject, propertyProvider, value);
+ };
+
+ renderers["number"] = function (container, id, parentObject, propertyProvider, value) {
+ renderers["string"](container, id, parentObject, propertyProvider, value);
+ };
+
+ renderers["any"] = function (container, id, parentObject, propertyProvider, value) {
+ renderers["string"](container, id, parentObject, propertyProvider, value);
+ };
+
+ renderers["string"] = function (container, id, parentObject, propertyProvider, value) {
+ /// TODO change the handler for when there is a 'media'
+ /// specifier so it becomes a file element.
+ var schemaId = getSchemaId(id);
+ var parentId = getParentSchemaId(schemaId);
+ var s = getSchema(schemaId);
+ var parentSchema = getSchema(parentId);
+ var input;
+ if (s.type === "any") {
+ input = document.createElement("textarea");
+ if (value) {
+ input.value = JSON.stringify(value, null, 4);
+ if (s.readOnly)
+ input.disabled = true;
+ }
+ } else if (s.media) {
+ input = document.createElement("input");
+ input.type = "file";
+ // XXX TODO, encode the SOB properly.
+ } else if (s.enum) {
+ input = document.createElement("select");
+ if (!s.required) {
+ var option = document.createElement("option");
+ var textNode = document.createTextNode("");
+ option.value = "";
+ appendChild(option, textNode, s);
+ appendChild(input, option, s);
+ }
+ var selectedIndex = 0;
+ for (var i = 0; i < s.enum.length; i++) {
+ var option = document.createElement("option");
+ var textNode = document.createTextNode(s.enum[i]);
+ option.value = s.enum[i];
+ appendChild(option, textNode, s);
+ appendChild(input, option, s);
+ if (value && s.enum[i] === value) {
+ selectedIndex = i;
+ if (!s.required) {
+ selectedIndex++;
+ }
+ if (s.readOnly)
+ input.disabled = true;
+ }
+ }
+ if (s.enum.length === 1)
+ input.selectedIndex = 0;
+ else
+ input.selectedIndex = selectedIndex;
+ } else {
+ input = document.createElement("input");
+ if (s.type === "integer" || s.type === "number") {
+ input.type = "number";
+ input.step = s.step?""+s.step:"any";
+ if (typeof value !== "number") {
+ value = null;
+ }
+ } else if (s.format === "date-time") {
+ try {
+ input.type = "datetime-local";
+ } catch (err) {
+ // #46, problem in IE11. TODO polyfill?
+ input.type = "text";
+ }
+ } else if (s.format === "date") {
+ input.type = "date";
+ } else if (s.format === "time") {
+ input.type = "time";
+ } else if (s.format === "tel") {
+ input.type = "tel";
+ } else if (s.format === "email") {
+ input.type = "email";
+ } else if (s.format === "password") {
+ input.type = "password";
+ } else if (s.format === "url") {
+ input.type = "url";
+ } else if (s.format === "range") {
+ input.type = "range";
+ if (s.minimum) {
+ input.min = s.minimum;
+ }
+ if (s.maximum) {
+ input.max = s.maximum;
+ }
+ if (s.step) {
+ input.step = s.step;
+ }
+ } else if (s.format === "text") {
+ input = document.createElement("textarea");
+ } else {
+ input.type = "text";
+ }
+ if (value !== null && typeof value !== "undefined") {
+ // readOnly?
+ input.value = value;
+ if (s.readOnly)
+ input.disabled = true;
+
+ }
+ }
+ input.schema = schemaId;
+ input.setAttribute("autocorrect", "off");
+
+ input.getValidationError = function () {
+ try {
+ var value = getValue(s, input);
+ if (value === null) {
+ if (s.required) {
+ if (parentSchema && parentSchema.type === "object") {
+ if (parentSchema.required) {
+ return BrutusinForms.messages["required"];
+ } else if (parentSchema.requiredProperties) {
+ for (var i = 0; i < parentSchema.requiredProperties.length; i++) {
+ if (parentSchema.requiredProperties[i] === s.$id.substring(2)) {
+ return BrutusinForms.messages["required"];
+ }
+ }
+ } else {
+ for (var prop in parentObject) {
+ if (parentObject[prop] === null) {
+ return BrutusinForms.messages["required"];
+ }
+ }
+ }
+ } else {
+ return BrutusinForms.messages["required"];
+ }
+ }
+ } else {
+ if (s.pattern && !s.pattern.test(value)) {
+ return BrutusinForms.messages["pattern"].format(s.pattern.source);
+ }
+ if (s.minLength) {
+ if (!value || s.minLength > value.length) {
+ return BrutusinForms.messages["minLength"].format(s.minLength);
+ }
+ }
+ if (s.maxLength) {
+ if (value && s.maxLength < value.length) {
+ return BrutusinForms.messages["maxLength"].format(s.maxLength);
+ }
+ }
+ //Add a default regex pattern matching for email validation, or else user could use
+ //the `pattern` field for their own custom regex pattern
+ if (!s.pattern && s.format === "email") {
+ if (!value.match(/[^@\s]+@[^@\s]+\.[^@\s]+/)) {
+ return BrutusinForms.messages["email"];
+ }
+ }
+
+ if (!s.pattern && s.format === "url") {
+ if (!value.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/)) {
+ return BrutusinForms.messages["url"];
+ }
+ }
+ }
+ if (value !== null && !isNaN(value)) {
+ if (s.multipleOf && value % s.multipleOf !== 0) {
+ return BrutusinForms.messages["multipleOf"].format(s.multipleOf);
+ }
+ if (s.hasOwnProperty("maximum")) {
+ if (s.exclusiveMaximum && value >= s.maximum) {
+ return BrutusinForms.messages["exclusiveMaximum"].format(s.maximum);
+ } else if (!s.exclusiveMaximum && value > s.maximum) {
+ return BrutusinForms.messages["maximum"].format(s.maximum);
+ }
+ }
+ if (s.hasOwnProperty("minimum")) {
+ if (s.exclusiveMinimum && value <= s.minimum) {
+ return BrutusinForms.messages["exclusiveMinimum"].format(s.minimum);
+ } else if (!s.exclusiveMinimum && value < s.minimum) {
+ return BrutusinForms.messages["minimum"].format(s.minimum);
+ }
+ }
+ }
+ } catch (error) {
+ return BrutusinForms.messages["invalidValue"];
+ }
+ };
+
+ input.onchange = function () {
+ var value;
+ try {
+ value = getValue(s, input);
+ } catch (error) {
+ value = null;
+ }
+ if (parentObject) {
+ parentObject[propertyProvider.getValue()] = value;
+ } else {
+ data = value;
+ }
+ onDependencyChanged(schemaId, input);
+ };
+
+ if (s.description) {
+ input.title = s.description;
+ input.placeholder = s.description;
+ }
+ if (s.class) {
+ input.className = s.class;
+ }
+ input.onchange();
+ input.id = getInputId();
+ inputCounter++;
+ appendChild(container, input, s);
+ return parentObject;
+ };
+
+ renderers["boolean"] = function (container, id, parentObject, propertyProvider, value) {
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var input;
+ if (s.format === "radio") {
+ input = document.createElement("div");
+ input.className = "form-check form-check-inline";
+ for (var i = 0; i < s.enum.length; i++) {
+ var radioInput = document.createElement("input");
+ radioInput.type = "radio";
+ radioInput.name = s.$id.substring(2);
+ radioInput.value = s.enum[i];
+ radioInput.id = s.enum[i];
+ radioInput.className = "form-check-input";
+ var label = document.createElement("label");
+ label.htmlFor = s.enum[i];
+ label.className = "form-check-label";
+ var labelText = document.createTextNode(s.enum[i]);
+ appendChild(label, labelText);
+ if (value && s.enum[i] === value) {
+ radioInput.checked = true;
+ }
+ if (s.readOnly) {
+ radioInput.disabled = true;
+ }
+ appendChild(input, label);
+ appendChild(input, radioInput, s);
+ }
+ }
+ else if (s.format === "checkbox") {
+ input = document.createElement("div");
+ input.className = "form-check form-check-inline";
+ for (var i = 0; i < s.enum.length; i++) {
+ checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.name = s.enum[i];
+ checkbox.value = s.enum[i];
+ checkbox.className = "form-check-input";
+
+ var label = document.createElement("label");
+ label.htmlFor = s.enum[i];
+ label.className = "form-check-label";
+ var labelText = document.createTextNode(s.enum[i]);
+ appendChild(label, labelText);
+ if (value) {
+ for (var j = 0; j < value.length; j++) {
+ if (s.enum[i] === value[j]) {
+ checkbox.checked = true;
+ }
+ }
+ }
+ if (s.readOnly) {
+ checkbox.disabled = true;
+ }
+ appendChild(input, label);
+ appendChild(input, checkbox, s);
+ }
+ }
+ else if (s.required) {
+ input = document.createElement("input");
+ input.type = "checkbox";
+ if (value === true || value !== false && s.default) {
+ input.checked = true;
+ }
+ } else {
+ input = document.createElement("select");
+ var emptyOption = document.createElement("option");
+ var textEmpty = document.createTextNode("");
+ textEmpty.value = "";
+ appendChild(emptyOption, textEmpty, s);
+ appendChild(input, emptyOption, s);
+
+ var optionTrue = document.createElement("option");
+ var textTrue = document.createTextNode(BrutusinForms.messages["true"]);
+ optionTrue.value = "true";
+ appendChild(optionTrue, textTrue, s);
+ appendChild(input, optionTrue, s);
+
+ var optionFalse = document.createElement("option");
+ var textFalse = document.createTextNode(BrutusinForms.messages["false"]);
+ optionFalse.value = "false";
+ appendChild(optionFalse, textFalse, s);
+ appendChild(input, optionFalse, s);
+
+ if (value === true) {
+ input.selectedIndex = 1;
+ } else if (value === false) {
+ input.selectedIndex = 2;
+ }
+ }
+
+ input.getValidationError = function () {
+ try {
+ var value = getValue(s, input);
+ if (value === null) {
+ if (s.required) {
+ return BrutusinForms.messages["required"];
+ }
+ }
+ } catch (error) {
+ return BrutusinForms.messages["invalidValue"];
+ }
+ };
+
+ input.onchange = function () {
+ if (parentObject) {
+ parentObject[propertyProvider.getValue()] = getValue(s, input);
+ } else {
+ data = getValue(s, input);
+ }
+ onDependencyChanged(schemaId, input);
+ };
+ input.schema = schemaId;
+ input.id = getInputId();
+ if (s.class) {
+ input.className = s.class;
+ }
+ inputCounter++;
+ if (s.description) {
+ input.title = s.description;
+ }
+ input.onchange();
+ appendChild(container, input, s);
+ };
+
+ renderers["oneOf"] = function (container, id, parentObject, propertyProvider, value) {
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var input = document.createElement("select");
+ var display = document.createElement("div");
+ display.innerHTML = "";
+ input.type = "select";
+ input.schema = schemaId;
+ var noption = document.createElement("option");
+ noption.value = null;
+ appendChild(input, noption, s);
+ for (var i = 0; i < s.oneOf.length; i++) {
+ var option = document.createElement("option");
+ var propId = schemaId + "." + i;
+ var ss = getSchema(propId);
+ var textNode = document.createTextNode(ss.title);
+ option.value = s.oneOf[i];
+ appendChild(option, textNode, s);
+ appendChild(input, option, s);
+ if (value === undefined || value === null)
+ continue;
+ if (s.readOnly)
+ input.disabled = true;
+ if (value.hasOwnProperty("type")) {
+ if (ss.hasOwnProperty("properties")) {
+ if (ss.properties.hasOwnProperty("type")) {
+ var tryit = getSchema(ss.properties.type);
+ if (value.type === tryit.enum[0]) {
+ input.selectedIndex = i + 1;
+ render(null, display, id + "." + (input.selectedIndex - 1), parentObject, propertyProvider, value);
+ }
+ }
+ }
+ }
+ }
+ input.onchange = function () {
+ render(null, display, id + "." + (input.selectedIndex - 1), parentObject, propertyProvider, value);
+ };
+ appendChild(container, input, s);
+ appendChild(container, display, s);
+
+ };
+
+ renderers["object"] = function (container, id, parentObject, propertyProvider, value) {
+
+ function createStaticPropertyProvider(propname) {
+ var ret = new Object();
+ ret.getValue = function () {
+ return propname;
+ };
+ ret.onchange = function (oldName) {
+ };
+ return ret;
+ }
+
+ function addAdditionalProperty(current, table, id, name, value, pattern) {
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var tbody = table.tBodies[0];
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.className = "add-prop-name";
+ var innerTab = document.createElement("table");
+ var innerTr = document.createElement("tr");
+ var innerTd1 = document.createElement("td");
+ var innerTd2 = document.createElement("td");
+ var keyForBlank = "$" + Object.keys(current).length + "$";
+ var td2 = document.createElement("td");
+ td2.className = "prop-value";
+ var nameInput = document.createElement("input");
+ nameInput.type = "text";
+ var regExp;
+ if (pattern) {
+ regExp = RegExp(pattern);
+ }
+ nameInput.getValidationError = function () {
+ if (nameInput.previousValue !== nameInput.value) {
+ if (current.hasOwnProperty(nameInput.value)) {
+ return BrutusinForms.messages["addpropNameExistent"];
+ }
+ }
+ if (!nameInput.value) {
+ return BrutusinForms.messages["addpropNameRequired"];
+ }
+ };
+ var pp = createPropertyProvider(
+ function () {
+ if (nameInput.value) {
+ if (regExp) {
+ if (nameInput.value.search(regExp) !== -1) {
+ return nameInput.value;
+ }
+ } else {
+ return nameInput.value;
+ }
+ }
+ return keyForBlank;
+ },
+ function (oldPropertyName) {
+ if (pp.getValue() === oldPropertyName) {
+ return;
+ }
+ if (!oldPropertyName || !current.hasOwnProperty(oldPropertyName)) {
+ oldPropertyName = keyForBlank;
+ }
+ if (current.hasOwnProperty(oldPropertyName) || regExp && pp.getValue().search(regExp) === -1) {
+ current[pp.getValue()] = current[oldPropertyName];
+ delete current[oldPropertyName];
+ }
+ });
+
+ nameInput.onblur = function () {
+ if (nameInput.previousValue !== nameInput.value) {
+ var name = nameInput.value;
+ var i = 1;
+ while (nameInput.previousValue !== name && current.hasOwnProperty(name)) {
+ name = nameInput.value + "(" + i + ")";
+ i++;
+ }
+ nameInput.value = name;
+ pp.onchange(nameInput.previousValue);
+ nameInput.previousValue = nameInput.value;
+ return;
+ }
+ };
+ var removeButton = document.createElement("button");
+ removeButton.setAttribute('type', 'button');
+ removeButton.className = "remove";
+ appendChild(removeButton, document.createTextNode("x"), s);
+ removeButton.onclick = function () {
+ delete current[nameInput.value];
+ table.deleteRow(tr.rowIndex);
+ nameInput.value = null;
+ pp.onchange(nameInput.previousValue);
+ };
+ appendChild(innerTd1, nameInput, s);
+ appendChild(innerTd2, removeButton, s);
+ appendChild(innerTr, innerTd1, s);
+ appendChild(innerTr, innerTd2, s);
+ appendChild(innerTab, innerTr, s);
+ appendChild(td1, innerTab, s);
+
+ if (pattern !== undefined) {
+ nameInput.placeholder = pattern;
+ }
+
+ appendChild(tr, td1, s);
+ appendChild(tr, td2, s);
+ appendChild(tbody, tr, s);
+ appendChild(table, tbody, s);
+ render(null, td2, id, current, pp, value);
+
+ if (name) {
+ nameInput.value = name;
+ nameInput.onblur();
+ }
+ }
+
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var current = new Object();
+ if (!parentObject) {
+ data = current;
+ } else {
+ if (propertyProvider.getValue() || propertyProvider.getValue() === 0) {
+ parentObject[propertyProvider.getValue()] = current;
+ }
+ }
+ var table = document.createElement("table");
+ table.className = "object";
+ var tbody = document.createElement("tbody");
+ appendChild(table, tbody, s);
+ var propNum = 0;
+ if (s.hasOwnProperty("properties")) {
+ propNum = s.properties.length;
+ for (var prop in s.properties) {
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.className = "form-group row";
+ var divLabel = document.createElement("div");
+ divLabel.className = "col-md-2";
+ var propId = id + "." + prop;
+ var propSchema = getSchema(getSchemaId(propId));
+ var divValue = document.createElement("div");
+ divValue.className = "col-md-10";
+
+ appendChild(tbody, tr, propSchema);
+ appendChild(tr, td1, propSchema);
+ appendChild(td1, divLabel, propSchema);
+ appendChild(td1, divValue, propSchema);
+ var pp = createStaticPropertyProvider(prop);
+ var propInitialValue = null;
+ if (value) {
+ propInitialValue = value[prop];
+ }
+ render(divLabel, divValue, propId, current, pp, propInitialValue);
+ }
+ }
+ var usedProps = [];
+ if (s.patternProperties || s.additionalProperties) {
+ var div = document.createElement("div");
+ appendChild(div, table, s);
+ if (s.patternProperties) {
+ for (var pattern in s.patternProperties) {
+ var patProps = s.patternProperties[pattern];
+ var patdiv = document.createElement("div");
+ patdiv.className = "add-pattern-div";
+ var addButton = document.createElement("button");
+ addButton.setAttribute('type', 'button');
+ addButton.pattern = pattern;
+ addButton.id = id + "[" + pattern + "]";
+ addButton.onclick = function () {
+ addAdditionalProperty(current, table, this.id, undefined, undefined, this.pattern);
+ };
+ if (s.maxProperties || s.minProperties) {
+ addButton.getValidationError = function () {
+ if (s.minProperties && propNum + table.rows.length < s.minProperties) {
+ return BrutusinForms.messages["minProperties"].format(s.minProperties);
+ }
+ if (s.maxProperties && propNum + table.rows.length > s.maxProperties) {
+ return BrutusinForms.messages["maxProperties"].format(s.maxProperties);
+ }
+ };
+ }
+ if (patProps.description) {
+ addButton.title = patProps.description;
+ }
+ appendChild(addButton, document.createTextNode("Add " + pattern), s);
+ appendChild(patdiv, addButton, s);
+ if (value) {
+ for (var p in value) {
+ if (s.properties && s.properties.hasOwnProperty(p)) {
+ continue;
+ }
+ var r = RegExp(pattern);
+ if (p.search(r) === -1) {
+ continue;
+ }
+ if (usedProps.indexOf(p) !== -1) {
+ continue;
+ }
+ addAdditionalProperty(current, table, id + "[" + pattern + "]", p, value[p], pattern);
+ usedProps.push(p);
+ }
+ }
+ appendChild(div, patdiv, s);
+ }
+ }
+ if (s.additionalProperties) {
+ var addPropS = getSchema(s.additionalProperties);
+ var addButton = document.createElement("button");
+ addButton.setAttribute('type', 'button');
+ addButton.onclick = function () {
+ addAdditionalProperty(current, table, id + "[*]", undefined);
+ };
+ if (s.maxProperties || s.minProperties) {
+ addButton.getValidationError = function () {
+ if (s.minProperties && propNum + table.rows.length < s.minProperties) {
+ return BrutusinForms.messages["minProperties"].format(s.minProperties);
+ }
+ if (s.maxProperties && propNum + table.rows.length > s.maxProperties) {
+ return BrutusinForms.messages["maxProperties"].format(s.maxProperties);
+ }
+ };
+ }
+ if (addPropS.description) {
+ addButton.title = addPropS.description;
+ }
+ appendChild(addButton, document.createTextNode("Add"), s);
+ appendChild(div, addButton, s);
+ if (value) {
+ for (var p in value) {
+ if (s.properties && s.properties.hasOwnProperty(p)) {
+ continue;
+ }
+ if (usedProps.indexOf(p) !== -1) {
+ continue;
+ }
+ addAdditionalProperty(current, table, id + "[\"" + prop + "\"]", p, value[p]);
+ }
+ }
+ }
+ appendChild(container, div, s);
+ } else {
+ appendChild(container, table, s);
+ }
+ };
+ // end of object renderer
+ renderers["array"] = function (container, id, parentObject, propertyProvider, value) {
+ function addItem(current, table, id, value, readOnly) {
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var tbody = document.createElement("tbody");
+ var tr = document.createElement("tr");
+ tr.className = "item";
+ var td1 = document.createElement("td");
+ td1.className = "item-index";
+ var td2 = document.createElement("td");
+ td2.className = "item-action";
+ var td3 = document.createElement("td");
+ td3.className = "item-value";
+ var removeButton = document.createElement("button");
+ removeButton.setAttribute('type', 'button');
+ removeButton.className = "remove";
+ if (readOnly === true)
+ removeButton.disabled = true;
+ appendChild(removeButton, document.createTextNode("x"), s);
+ var computRowCount = function () {
+ for (var i = 0; i < table.rows.length; i++) {
+ var row = table.rows[i];
+ row.cells[0].innerHTML = i + 1;
+ }
+ };
+ removeButton.onclick = function () {
+ current.splice(tr.rowIndex, 1);
+ table.deleteRow(tr.rowIndex);
+ computRowCount();
+ };
+ appendChild(td2, removeButton, s);
+ var number = document.createTextNode(table.rows.length + 1);
+ appendChild(td1, number, s);
+ appendChild(tr, td1, s);
+ appendChild(tr, td2, s);
+ appendChild(tr, td3, s);
+ appendChild(tbody, tr, s);
+ appendChild(table, tbody, s);
+ var pp = createPropertyProvider(function () {
+ return tr.rowIndex;
+ });
+ render(null, td3, id, current, pp, value);
+ }
+
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ var itemS = getSchema(s.items);
+ var current = new Array();
+ if (!parentObject) {
+ data = current;
+ } else {
+ if (propertyProvider.getValue() || propertyProvider.getValue() === 0) {
+ parentObject[propertyProvider.getValue()] = current;
+ }
+ }
+ if (propertyProvider) {
+ propertyProvider.onchange = function (oldPropertyName) {
+ delete parentObject[oldPropertyName];
+ parentObject[propertyProvider.getValue()] = current;
+ };
+ }
+ var div = document.createElement("div");
+ var table = document.createElement("table");
+ table.className = "array";
+ appendChild(div, table, s);
+ appendChild(container, div, s);
+ var addButton = document.createElement("button");
+ if (s.readOnly)
+ addButton.disabled = true;
+ addButton.setAttribute('type', 'button');
+ addButton.className = "addItem";
+ addButton.getValidationError = function () {
+ if (s.minItems && s.minItems > table.rows.length) {
+ return BrutusinForms.messages["minItems"].format(s.minItems);
+ }
+ if (s.maxItems && s.maxItems < table.rows.length) {
+ return BrutusinForms.messages["maxItems"].format(s.maxItems);
+ }
+ if (s.uniqueItems) {
+ for (var i = 0; i < current.length; i++) {
+ for (var j = i + 1; j < current.length; j++) {
+ if (JSON.stringify(current[i]) === JSON.stringify(current[j])) {
+ return BrutusinForms.messages["uniqueItems"];
+ }
+ }
+ }
+ }
+ };
+ addButton.onclick = function () {
+ addItem(current, table, id + "[#]", null);
+ };
+ if (itemS.description) {
+ addButton.title = itemS.description;
+ }
+ appendChild(addButton, document.createTextNode(BrutusinForms.messages["addItem"]), s);
+ appendChild(div, table, s);
+ appendChild(div, addButton, s);
+ if (value && value instanceof Array) {
+ for (var i = 0; i < value.length; i++) {
+ addItem(current, table, id + "[" + i + "]", value[i], s.readOnly);
+ }
+ }
+ appendChild(container, div, s);
+ };
+ // end of array render
+
+ /**
+ * Renders the form inside the the container, with the specified data preloaded
+ * @name obj.render
+ * @instance
+ * @param {HTMLElement} c container
+ * @param {type} data json data
+ * @returns {void}
+ */
+ obj.render = function (c, data) {
+ container = c;
+ initialValue = data;
+ var form = document.createElement("form");
+ form.className = "brutusin-form";
+ form.id = "brutusin-form";
+ form.onsubmit = function (event) {
+ return false;
+ };
+ if (container) {
+ appendChild(container, form);
+ } else {
+ appendChild(document.body, form);
+ }
+ if (error) {
+ var errLabel = document.createElement("label");
+ var errNode = document.createTextNode(error);
+ appendChild(errLabel, errNode);
+ errLabel.className = "error-message";
+ appendChild(form, errLabel);
+ } else {
+ render(null, form, "$", null, null);
+ }
+ if (dependencyMap.hasOwnProperty("$")) {
+ onDependencyChanged("$");
+ }
+ if (BrutusinForms.postRender) {
+ BrutusinForms.postRender(obj);
+ }
+ };
+
+ /**
+ * @deprecated
+ * @instance
+ * @name obj.getRenderingContainer
+ * @returns {HTMLElement} The container that consists the form
+ */
+ obj.getRenderingContainer = function () {
+ return container;
+ };
+
+ /**
+ * Validates all the fields inside the form
+ * @instance
+ * @name obj.validate
+ * @returns {boolean} Validation result
+ */
+ obj.validate = function () {
+ return validate(container);
+ };
+
+ /**
+ * Retrieves the field values from the form
+ * @instance
+ * @name obj.getData
+ * @returns {type} json object
+ */
+ obj.getData = function () {
+ function removeEmptiesAndNulls(object, s) {
+ if (s === null) {
+ s = SCHEMA_ANY;
+ }
+ if (s.$ref) {
+ s = getDefinition(s.$ref);
+ }
+ if (object instanceof Array) {
+ if (object.length === 0) {
+ return null;
+ }
+ if (s.format === "checkbox") {
+ return object;
+ }
+ var clone = new Array();
+ for (var i = 0; i < object.length; i++) {
+ clone[i] = removeEmptiesAndNulls(object[i], s.items);
+ }
+ return clone;
+ } else if (object === "") {
+ return null;
+ } else if (object instanceof Object) {
+ var clone = new Object();
+ var nonEmpty = false;
+ for (var prop in object) {
+ if (prop.startsWith("$") && prop.endsWith("$")) {
+ continue;
+ }
+ var ss = null;
+ if (s.hasOwnProperty("properties") && s.properties.hasOwnProperty(prop)) {
+ ss = s.properties[prop];
+ }
+ if (ss === null && s.hasOwnProperty("additionalProperties")) {
+ if (typeof s.additionalProperties === 'object') {
+ ss = s.additionalProperties;
+ }
+ }
+ if (ss === null && s.hasOwnProperty("patternProperties")) {
+ for (var p in s.patternProperties) {
+ var r = RegExp(p);
+ if (prop.search(r) !== -1) {
+ ss = s.patternProperties[p];
+ break;
+ }
+ }
+ }
+ var value = removeEmptiesAndNulls(object[prop], ss);
+ //Check if user assign an empty String in the default field, if true return empty string instead of null
+ if (ss !== null) {
+ if (ss.default == "" && value === null) {
+ value = "";
+ }
+ }
+ if (value !== null) {
+ clone[prop] = value;
+ nonEmpty = true;
+ }
+ }
+ if (nonEmpty || s.required) {
+ return clone;
+ } else {
+ return null;
+ }
+ } else {
+ return object;
+ }
+ }
+
+ if (!container) {
+ return null;
+ } else {
+ return removeEmptiesAndNulls(data, schema);
+ }
+ };
+
+ BrutusinForms.instances[BrutusinForms.instances.length] = obj;
+
+ return obj;
+
+ /**
+ * Check if the dependency map forms a circle
+ * @name validateDepencyMapIsAcyclic
+ * @function
+ */
+ function validateDepencyMapIsAcyclic() {
+ function dfs(visitInfo, stack, id) {
+ if (stack.hasOwnProperty(id)) {
+ error = "Schema dependency graph has cycles";
+ return;
+ }
+ stack[id] = null;
+ if (visitInfo.hasOwnProperty(id)) {
+ return;
+ }
+ visitInfo[id] = null;
+ var arr = dependencyMap[id];
+ if (arr) {
+ for (var i = 0; i < arr.length; i++) {
+ dfs(visitInfo, stack, arr[i]);
+ }
+ }
+ delete stack[id];
+ }
+ var visitInfo = new Object();
+ for (var id in dependencyMap) {
+ if (visitInfo.hasOwnProperty(id)) {
+ continue;
+ }
+ dfs(visitInfo, new Object(), id);
+ }
+ }
+
+ /**
+ * Append the child HTMLElement to the parent HTMLElement, schema is used for the decorators
+ * @see BrutusinForms.addDecorator
+ *
+ * @name appendChild
+ * @function
+ * @param {HTMLElement} parent - The parent HTMLElement to append with
+ * @param {HTMLElement} child - The child HTMLElement that wants to append on the parent
+ * @param {Object} [schema] - The schema object
+ */
+ function appendChild(parent, child, schema) {
+ parent.appendChild(child);
+ for (var i = 0; i < BrutusinForms.decorators.length; i++) {
+ BrutusinForms.decorators[i](child, schema);
+ }
+ }
+
+ /**
+ * Creates a copy of the original schema excluding `items`, `properties` and `additionalProperties` field
+ * @function
+ * @name createPseudoSchema
+ * @param {Object} schema - The original JSON schema object
+ * @returns - The copy of the original schema
+ */
+ function createPseudoSchema(schema) {
+ var pseudoSchema = new Object();
+ for (var p in schema) {
+ if (p === "items" || p === "properties" || p === "additionalProperties") {
+ continue;
+ }
+ if (p === "pattern") {
+ pseudoSchema[p] = new RegExp(schema[p]);
+ } else {
+ pseudoSchema[p] = schema[p];
+ }
+
+ }
+ return pseudoSchema;
+ }
+
+ /**
+ * Use for `$ref` type. Refer to {@link ./index.html#11}. To get the field properties under the `path` provided.
+ * @function
+ * @name getDefinition
+ * @param {string} path - The path to the defined field.
+ * @returns - The properties of the path
+ */
+ function getDefinition(path) {
+ var parts = path.split('/');
+ var def = root;
+ for (var p in parts) {
+ if (p === "0")
+ continue;
+ def = def[parts[p]];
+
+ }
+ return def;
+ }
+
+ /**
+ * To find if a string is exists inside an array
+ * @function
+ * @name containsStr
+ * @param {Array} array - The array to check against
+ * @param {string} string - The string to look for
+ * @returns {boolean} - Result of the string exists in the array or not
+ */
+ function containsStr(array, string) {
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] == string) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Rename the `required` field to `requiredProperties` for certain property type in the schema to support JSON Schema v4.
+ * See example {@link ./index.html#8}
+ * @function
+ * @name renameRequiredPropeties
+ * @param {Object} schema - The JSON schema object
+ */
+ function renameRequiredPropeties(schema) {
+ if (!schema) {
+ return;
+ } else if (schema.hasOwnProperty("oneOf")) {
+ for (var i in schema.oneOf) {
+ renameRequiredPropeties(schema.oneOf[i]);
+ }
+ } else if (schema.hasOwnProperty("$ref")) {
+ var newSchema = getDefinition(schema["$ref"]);
+ renameRequiredPropeties(newSchema);
+ } else if (schema.type === "object") {
+ if (schema.properties) {
+ if (schema.hasOwnProperty("required")) {
+ if (Array.isArray(schema.required)) {
+ schema.requiredProperties = schema.required;
+ delete schema.required;
+ }
+ }
+ for (var prop in schema.properties) {
+ renameRequiredPropeties(schema.properties[prop]);
+ }
+ }
+ if (schema.patternProperties) {
+ for (var pat in schema.patternProperties) {
+ var s = schema.patternProperties[pat];
+ if (s.hasOwnProperty("type") || s.hasOwnProperty("$ref") || s.hasOwnProperty("oneOf")) {
+ renameRequiredPropeties(schema.patternProperties[pat]);
+ }
+ }
+ }
+ if (schema.additionalProperties) {
+ if (schema.additionalProperties.hasOwnProperty("type") || schema.additionalProperties.hasOwnProperty("oneOf")) {
+ renameRequiredPropeties(schema.additionalProperties);
+
+ }
+ }
+ } else if (schema.type === "array") {
+ renameRequiredPropeties(schema.items);
+ }
+ }
+
+ /**
+ * Generates a schema map based on the provided JSON schema
+ * @function
+ * @name populateSchemaMap
+ * @param {string} name - The schema ID
+ * @param {Object} schema - The JSON schema object
+ */
+ function populateSchemaMap(name, schema) {
+ var pseudoSchema = createPseudoSchema(schema);
+ pseudoSchema["$id"] = name;
+ schemaMap[name] = pseudoSchema;
+
+ if (!schema) {
+ return;
+ } else if (schema.hasOwnProperty("oneOf")) {
+ pseudoSchema.oneOf = new Array();
+ pseudoSchema.type = "oneOf";
+ for (var i in schema.oneOf) {
+ var childProp = name + "." + i;
+ pseudoSchema.oneOf[i] = childProp;
+ populateSchemaMap(childProp, schema.oneOf[i]);
+ }
+ } else if (schema.hasOwnProperty("$ref")) {
+ var refSchema = getDefinition(schema["$ref"]);
+ if (refSchema) {
+ if (schema.hasOwnProperty("title") || schema.hasOwnProperty("description")) {
+ var clonedRefSchema = {};
+ for (var prop in refSchema) {
+ clonedRefSchema[prop] = refSchema[prop];
+ }
+ if (schema.hasOwnProperty("title")) {
+ clonedRefSchema.title = schema.title;
+ }
+ if (schema.hasOwnProperty("description")) {
+ clonedRefSchema.description = schema.description;
+ }
+ refSchema = clonedRefSchema;
+ }
+ populateSchemaMap(name, refSchema);
+ }
+ } else if (schema.type === "object") {
+ if (schema.properties) {
+ pseudoSchema.properties = new Object();
+ for (var prop in schema.properties) {
+ var childProp = name + "." + prop;
+ pseudoSchema.properties[prop] = childProp;
+ var subSchema = schema.properties[prop];
+ if (schema.requiredProperties) {
+ if (containsStr(schema.requiredProperties, prop)) {
+ subSchema.required = true;
+ } else {
+ subSchema.required = false;
+ }
+ }
+ populateSchemaMap(childProp, subSchema);
+ }
+ }
+ if (schema.patternProperties) {
+ pseudoSchema.patternProperties = new Object();
+ for (var pat in schema.patternProperties) {
+ var patChildProp = name + "[" + pat + "]";
+ pseudoSchema.patternProperties[pat] = patChildProp;
+ var s = schema.patternProperties[pat];
+
+ if (s.hasOwnProperty("type") || s.hasOwnProperty("$ref") ||
+ s.hasOwnProperty("oneOf")) {
+ populateSchemaMap(patChildProp, schema.patternProperties[pat]);
+ } else {
+ populateSchemaMap(patChildProp, SCHEMA_ANY);
+ }
+ }
+ }
+ if (schema.additionalProperties) {
+ var childProp = name + "[*]";
+ pseudoSchema.additionalProperties = childProp;
+ if (schema.additionalProperties.hasOwnProperty("type") ||
+ schema.additionalProperties.hasOwnProperty("oneOf")) {
+ populateSchemaMap(childProp, schema.additionalProperties);
+ } else {
+ populateSchemaMap(childProp, SCHEMA_ANY);
+ }
+ }
+ } else if (schema.type === "array") {
+ pseudoSchema.items = name + "[#]";
+ populateSchemaMap(pseudoSchema.items, schema.items);
+ }
+ if (schema.hasOwnProperty("dependsOn")) {
+ if (schema.dependsOn === null) {
+ schema.dependsOn = ["$"];
+ }
+ var arr = new Array();
+ for (var i = 0; i < schema.dependsOn.length; i++) {
+ if (!schema.dependsOn[i]) {
+ arr[i] = "$";
+ // Relative cases
+ } else if (schema.dependsOn[i].startsWith("$")) {
+ arr[i] = schema.dependsOn[i];
+ // Relative cases
+ } else if (name.endsWith("]")) {
+ arr[i] = name + "." + schema.dependsOn[i];
+ } else {
+ arr[i] = name.substring(0, name.lastIndexOf(".")) + "." + schema.dependsOn[i];
+ }
+ }
+ schemaMap[name].dependsOn = arr;
+ for (var i = 0; i < arr.length; i++) {
+ var entry = dependencyMap[arr[i]];
+ if (!entry) {
+ entry = new Array();
+ dependencyMap[arr[i]] = entry;
+ }
+ entry[entry.length] = name;
+ }
+ }
+ }
+
+ /**
+ * Renders the label for the input field
+ * @name renderTitle
+ * @function
+ * @param {HTMLElement} container - The container for the label element
+ * @param {string} title - The title for the label
+ * @param {Object} schema - The JSON schema object
+ */
+ function renderTitle(container, title, schema) {
+ if (container) {
+ if (title) {
+ var titleLabel = document.createElement("label");
+ if (schema.type !== "any" && schema.type !== "object" && schema.type !== "array") {
+ titleLabel.htmlFor = getInputId();
+ }
+ var titleNode = document.createTextNode(title);
+ appendChild(titleLabel, titleNode, schema);
+ if (schema.description) {
+ titleLabel.title = schema.description;
+ }
+ if (schema.required) {
+ var sup = document.createElement("sup");
+ appendChild(sup, document.createTextNode("*"), schema);
+ appendChild(titleLabel, sup, schema);
+ titleLabel.className = "required";
+ }
+ appendChild(container, titleLabel, schema);
+ }
+ }
+ }
+
+ /**
+ * Concat the formId and inputCounter to form an unique ID
+ * @name getInputId
+ * @function
+ * @returns {string} The ID for the input field
+ */
+ function getInputId() {
+ return formId + "_" + inputCounter;
+ }
+
+ /**
+ * Validates if the form input fields adhere to all the validations defined
+ * @name validate
+ * @function
+ * @param {HTMLElement} element
+ * @returns {boolean} The result of the validation
+ */
+ function validate(element) {
+ var ret = true;
+ if (element.hasOwnProperty("getValidationError")) {
+ var error = element.getValidationError();
+ if (error) {
+ BrutusinForms.onValidationError(element, error);
+ ret = false;
+ } else {
+ BrutusinForms.onValidationSuccess(element);
+ }
+ }
+ if (element.childNodes) {
+ for (var i = 0; i < element.childNodes.length; i++) {
+ if (!validate(element.childNodes[i])) {
+ ret = false;
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Clears the first child element of the selected container
+ * @function
+ * @name clear
+ * @param {HTMLElement} container
+ */
+ function clear(container) {
+ if (container) {
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+ }
+ }
+
+ /**
+ * Renders the HTML form based on the provided JSON schema
+ * @function
+ * @name render
+ * @param {HTMLElement} titleContainer - The label container that needs to generate the label field in it
+ * @param {HTMLElement} container - The input field container that needs to generate the input field in it
+ * @param {string} id - The schema ID
+ * @param {Object} parentObject - The parent object of the current schema ID
+ * @param {Object} propertyProvider - The properties of the current schema ID
+ * @param {string|number|boolean} value - The initial or default value defined (if exists)
+ */
+ function render(titleContainer, container, id, parentObject, propertyProvider, value) {
+ //console.log(id);
+ var schemaId = getSchemaId(id);
+ var s = getSchema(schemaId);
+ renderInfoMap[schemaId] = new Object();
+ renderInfoMap[schemaId].titleContainer = titleContainer;
+ renderInfoMap[schemaId].container = container;
+ renderInfoMap[schemaId].parentObject = parentObject;
+ renderInfoMap[schemaId].propertyProvider = propertyProvider;
+ renderInfoMap[schemaId].value = value;
+ clear(titleContainer);
+ clear(container);
+ if (s === undefined) {
+ data = new Object();
+ return;
+ }
+ //console.log(id,s,value);
+ var r = renderers[s.type];
+ if (r && !s.dependsOn) {
+ if (s.title) {
+ renderTitle(titleContainer, s.title, s);
+ } else if (propertyProvider) {
+ renderTitle(titleContainer, propertyProvider.getValue(), s);
+ }
+ if (!value) {
+ if (typeof initialValue !== "undefined" && initialValue !== null) {
+ value = getInitialValue(id);
+ if (value === null && typeof s.default !== "undefined") {
+ value = s.default;
+ }
+ } else {
+ value = s.default;
+ }
+ }
+ r(container, id, parentObject, propertyProvider, value);
+ } else if (s.$ref) {
+ if (obj.schemaResolver) {
+ var cb = function (schemas) {
+ if (schemas && schemas.hasOwnProperty(id)) {
+ if (JSON.stringify(schemaMap[id]) !== JSON.stringify(schemas[id])) {
+ cleanSchemaMap(id);
+ cleanData(id);
+ populateSchemaMap(id, schemas[id]);
+ var renderInfo = renderInfoMap[id];
+ if (renderInfo) {
+ render(renderInfo.titleContainer, renderInfo.container, id, renderInfo.parentObject, renderInfo.propertyProvider, renderInfo.value);
+ }
+ }
+ }
+ BrutusinForms.onResolutionFinished(parentObject);
+ };
+ BrutusinForms.onResolutionStarted(parentObject);
+ obj.schemaResolver([id], obj.getData(), cb);
+ }
+ }
+ }
+
+ /**
+ * Used in object additionalProperties and arrays
+ * @function
+ * @name createPropertyProvider
+ * @param {type} getValue
+ * @param {type} onchange
+ * @returns {Object.create.createPropertyProvider.ret}
+ */
+ function createPropertyProvider(getValue, onchange) {
+ var ret = new Object();
+ ret.getValue = getValue;
+ ret.onchange = onchange;
+ return ret;
+ }
+
+ /**
+ * Retrieve the initial value defined in the initial data JSON schema
+ * @name getInitialValue
+ * @function
+ * @param {string} id - The schema ID
+ * @returns {string|number|boolean} The initial value defined in the JSON schema based on the ID
+ */
+ function getInitialValue(id) {
+ var fields = id.substring(2).split('.');
+ var initialValueClone = initialValue;
+ for(var i = 0; i < fields.length; i++) {
+ var field = fields[i];
+ if (field != "") {
+ if (field.substring(field.length - 1) === "]") {
+ //Get the index from the array in the field
+ var arrayIndex = parseInt(field.substring(field.lastIndexOf("[") + 1, field.length - 1));
+ //Substring off the square bracket from the field
+ field = field.substring(0, field.lastIndexOf("["));
+ initialValueClone = initialValueClone[field][arrayIndex];
+ } else {
+ initialValueClone = initialValueClone[field];
+ }
+ }
+ }
+
+ return initialValueClone;
+ }
+
+ /**
+ * Retrieve the values from the input field in the form
+ * @name getValue
+ * @function
+ * @param {Object} schema - The JSON schema
+ * @param {HTMLInputElement} input - The input element retrieves from the form
+ * @returns {string|number|boolean} The values of the input field
+ */
+ function getValue(schema, input) {
+ if (typeof input.getValue === "function") {
+ return input.getValue();
+ }
+ var value;
+
+ if (input.tagName.toLowerCase() === "select") {
+ value = input.options[input.selectedIndex].value;
+ } else {
+ value = input.value;
+ }
+ if (value === "") {
+ return null;
+ }
+ if (schema.type === "integer") {
+ value = parseInt(value);
+ if (!isFinite(value)) {
+ value = null;
+ }
+ } else if (schema.type === "number") {
+ value = parseFloat(value);
+ if (!isFinite(value)) {
+ value = null;
+ }
+ } else if (schema.type === "boolean") {
+ if (input.tagName.toLowerCase() === "input") {
+ value = input.checked;
+ if (!value) {
+ value = false;
+ }
+ } else if (schema.format === "checkbox") {
+ var checkboxValue = [];
+ for (var i = 0; i < input.childElementCount; i++) {
+ if (input.childNodes[i].tagName === "INPUT" && input.childNodes[i].checked) {
+ checkboxValue.push(input.childNodes[i].value);
+ }
+ }
+ if (checkboxValue.length !== 0) {
+ value = checkboxValue;
+ }
+ else {
+ value = null;
+ }
+ } else if (input.tagName.toLowerCase() === "select") {
+ if (input.value === "true") {
+ value = true;
+ } else if (input.value === "false") {
+ value = false;
+ } else {
+ value = null;
+ }
+ } else if (schema.format === "radio") {
+ value = null;
+ for (var i = 0; i < input.childElementCount; i++) {
+ if (input.childNodes[i].tagName === "INPUT" && input.childNodes[i].checked) {
+ value = input.childNodes[i].value;
+ break;
+ }
+ }
+ }
+ } else if (schema.type === "any") {
+ if (value) {
+ eval("value=" + value);
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Replace the ID from string or int to [*] or [#] if found
+ * @name getSchemaId
+ * @function
+ * @param {string} id - The schema id
+ * @returns The ID that has been replaced
+ */
+ function getSchemaId(id) {
+ return id.replace(/\["[^"]*"\]/g, "[*]").replace(/\[\d*\]/g, "[#]");
+ }
+
+ /**
+ * This method is to retrieve the parent schemaId based on the provided schemaId
+ * @function
+ * @name getParentSchemaId
+ * @param {string} id - The schemaId to use for retrieving the parent schemaId
+ * @returns {string} The parent schemaId of the provided id.
+ */
+ function getParentSchemaId(id) {
+ return id.substring(0, id.lastIndexOf("."));
+ }
+
+ /**
+ * This method is to retrieve the JSON schemas under the specified schemaId.
+ * @function
+ * @name getSchema
+ * @param {string} schemaId - The schemaId to use for getting the child arrays.
+ * @returns {Object}
+ */
+ function getSchema(schemaId) {
+ return schemaMap[schemaId];
+ }
+
+ /**
+ * Removes the child schema based on the schema ID provided
+ * @name cleanSchemaMap
+ * @function
+ * @param {string} schemaId - The schema ID used to identify which part of the child needs to be removed
+ */
+ function cleanSchemaMap(schemaId) {
+ for (var prop in schemaMap) {
+ if (prop.startsWith(schemaId)) {
+ delete schemaMap[prop];
+ }
+ }
+ }
+
+ /**
+ * This method is to clean up the DOM Element after resolving the schema (used in dynamic schema resolver)
+ * @function
+ * @name cleanData
+ * @param {string} schemaId - The identifier of the schema to use for cleaning data.
+ */
+ function cleanData(schemaId) {
+ var expression = new Expression(schemaId);
+ expression.visit(data, function (data, parent, property) {
+ delete parent[property];
+ });
+ }
+
+ /**
+ * Used in schema resolver. When it detects the schema depends on another schema based on the output has a change,
+ * it renders the fields based on the schema defined in the resolver.
+ * @function
+ * @name onDependencyChanged
+ * @param {string} name - The schema ID
+ * @param {HTMLElement} source - The HTMLElement of the source
+ */
+ function onDependencyChanged(name, source) {
+
+ var arr = dependencyMap[name];
+ if (!arr || !obj.schemaResolver) {
+ return;
+ }
+ var cb = function (schemas) {
+ if (schemas) {
+ for (var id in schemas) {
+ if (JSON.stringify(schemaMap[id]) !== JSON.stringify(schemas[id])) {
+ cleanSchemaMap(id);
+ cleanData(id);
+ populateSchemaMap(id, schemas[id]);
+ var renderInfo = renderInfoMap[id];
+ if (renderInfo) {
+ render(renderInfo.titleContainer, renderInfo.container, id, renderInfo.parentObject, renderInfo.propertyProvider, renderInfo.value);
+ }
+ }
+ }
+ }
+ BrutusinForms.onResolutionFinished(source);
+ };
+ BrutusinForms.onResolutionStarted(source);
+ obj.schemaResolver(arr, obj.getData(), cb);
+ }
+
+ /**
+ * @function
+ * @name Expression
+ * @constructor
+ * @param {string} exp - The schemaId to be parsed.
+ * @returns {Expression} An Expression object.
+ */
+ function Expression(exp) {
+ if (exp === null || exp.length === 0 || exp === ".") {
+ exp = "$";
+ }
+ var queue = new Array();
+ var tokens = parseTokens(exp);
+ var isInBracket = false;
+ var numInBracket = 0;
+ var sb = "";
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if (token === "[") {
+ if (isInBracket) {
+ throw ("Error parsing expression '" + exp + "': Nested [ found");
+ }
+ isInBracket = true;
+ numInBracket = 0;
+ sb = sb + token;
+ } else if (token === "]") {
+ if (!isInBracket) {
+ throw ("Error parsing expression '" + exp + "': Unbalanced ] found");
+ }
+ isInBracket = false;
+ sb = sb + token;
+ queue[queue.length] = sb;
+ sb = "";
+ } else {
+ if (isInBracket) {
+ if (numInBracket > 0) {
+ throw ("Error parsing expression '" + exp + "': Multiple tokens found inside a bracket");
+ }
+ sb = sb + token;
+ numInBracket++;
+ } else {
+ queue[queue.length] = token;
+ }
+ }
+ if (i === tokens.length - 1) {
+ if (isInBracket) {
+ throw ("Error parsing expression '" + exp + "': Unbalanced [ found");
+ }
+ }
+ }
+ this.exp = exp;
+ this.queue = queue;
+ this.visit = function (data, visitor) {
+ function visit(name, queue, data, parentData, property) {
+ if (data == null) {
+ return;
+ }
+ var currentToken = queue.shift();
+ if (currentToken === "$") {
+ name = "$";
+ var currentToken = queue.shift();
+ }
+ if (!currentToken) {
+ visitor(data, parentData, property);
+ } else if (Array.isArray(data)) {
+ if (!currentToken.startsWith("[")) {
+ throw ("Node '" + name + "' is of type array");
+ }
+ var element = currentToken.substring(1, currentToken.length - 1);
+ if (element.equals("#")) {
+ for (var i = 0; i < data.length; i++) {
+ var child = data[i];
+ visit(name + currentToken, queue.slice(0), child, data, i);
+ visit(name + "[" + i + "]", queue.slice(0), child, data, i);
+ }
+ } else if (element === "$") {
+ var child = data[data.length - 1];
+ visit(name + currentToken, queue.slice(0), child, data, data.length - 1);
+ } else {
+ var index = parseInt(element);
+ if (isNaN(index)) {
+ throw ("Element '" + element + "' of node '" + name + "' is not an integer, or the '$' last element symbol, or the wilcard symbol '#'");
+ }
+ if (index < 0) {
+ throw ("Element '" + element + "' of node '" + name + "' is lower than zero");
+ }
+ var child = data[index];
+ visit(name + currentToken, queue.slice(0), child, data, index);
+ }
+ } else if ("object" === typeof data) {
+ if (currentToken === "[*]") {
+ for (var p in data) {
+ var child = data[p];
+ visit(name + currentToken, queue.slice(0), child, data, p);
+ visit(name + "[\"" + p + "\"]", queue.slice(0), child, data, p);
+ }
+ } else {
+ var child;
+ if (currentToken.startsWith("[")) {
+ var element = currentToken.substring(1, currentToken.length - 1);
+ if (element.startsWith("\"") || element.startsWith("'")) {
+ element = element.substring(1, element.length() - 1);
+ } else {
+ throw ("Element '" + element + "' of node '" + name + "' must be a string expression or wilcard '*'");
+ }
+ name = name + currentToken;
+ child = data[element];
+ } else {
+ if (name.length > 0) {
+ name = name + "." + currentToken;
+ } else {
+ name = currentToken;
+ }
+ child = data[currentToken];
+ }
+ visit(name, queue, child, data, currentToken);
+ }
+ } else if ("boolean" === typeof data
+ || "number" === typeof data
+ || "string" === typeof data) {
+ throw ("Node is leaf but still are tokens remaining: " + currentToken);
+ } else {
+ throw ("Node type '" + typeof data + "' not supported for index field '" + name + "'");
+ }
+ }
+ visit(this.exp, this.queue, data);
+ };
+
+ function parseTokens(exp) {
+ if (exp === null) {
+ return null;
+ }
+ var ret = new Array();
+ var commentChar = null;
+ var start = 0;
+ for (var i = 0; i < exp.length; i++) {
+ if (exp.charAt(i) === '"') {
+ if (commentChar === null) {
+ commentChar = '"';
+ } else if (commentChar === '"') {
+ commentChar = null;
+ ret[ret.length] = exp.substring(start, i + 1).trim();
+ start = i + 1;
+ }
+ } else if (exp.charAt(i) === '\'') {
+ if (commentChar === null) {
+ commentChar = '\'';
+ } else if (commentChar === '\'') {
+ commentChar = null;
+ ret[ret.length] = exp.substring(start, i + 1).trim();
+ start = i + 1;
+ }
+ } else if (exp.charAt(i) === '[') {
+ if (commentChar === null) {
+ if (start !== i) {
+ ret[ret.length] = exp.substring(start, i).trim();
+ }
+ ret[ret.length] = "[";
+ start = i + 1;
+ }
+ } else if (exp.charAt(i) === ']') {
+ if (commentChar === null) {
+ if (start !== i) {
+ ret[ret.length] = exp.substring(start, i).trim();
+ }
+ ret[ret.length] = "]";
+ start = i + 1;
+ }
+ } else if (exp.charAt(i) === '.') {
+ if (commentChar === null) {
+ if (start !== i) {
+ ret[ret.length] = exp.substring(start, i).trim();
+ }
+ start = i + 1;
+ }
+ } else if (i === exp.length - 1) {
+ ret[ret.length] = exp.substring(start, i + 1).trim();
+ }
+ }
+ return ret;
+ }
+ }
+ };
+ brutusin["json-forms"] = BrutusinForms;
+}());
+
+
+