From aee247727aa1944ebb55f6e1da358a390f218bc6 Mon Sep 17 00:00:00 2001 From: alekseybobkov Date: Wed, 7 Oct 2015 20:16:24 -0700 Subject: [PATCH] Implementing Inspector validation + minor refactoring of the Inspector editors. --- .../assets/ui/js/inspector.editor.base.js | 157 +++++++++++++++--- .../ui/js/inspector.editor.dictionary.js | 2 +- .../assets/ui/js/inspector.editor.object.js | 2 +- .../ui/js/inspector.editor.objectlist.js | 2 +- .../ui/js/inspector.editor.stringlist.js | 2 +- .../js/inspector.externalparametereditor.js | 13 ++ .../system/assets/ui/js/inspector.groups.js | 38 +++++ .../system/assets/ui/js/inspector.surface.js | 64 +++++-- .../assets/ui/js/inspector.validator.base.js | 56 +++++++ .../assets/ui/js/inspector.validator.regex.js | 40 +++++ .../ui/js/inspector.validator.required.js | 33 ++++ modules/system/assets/ui/less/inspector.less | 4 + modules/system/assets/ui/storm.css | 1 + 13 files changed, 373 insertions(+), 41 deletions(-) create mode 100644 modules/system/assets/ui/js/inspector.validator.base.js create mode 100644 modules/system/assets/ui/js/inspector.validator.regex.js create mode 100644 modules/system/assets/ui/js/inspector.validator.required.js diff --git a/modules/system/assets/ui/js/inspector.editor.base.js b/modules/system/assets/ui/js/inspector.editor.base.js index 5424e518d..f0f3c1a38 100644 --- a/modules/system/assets/ui/js/inspector.editor.base.js +++ b/modules/system/assets/ui/js/inspector.editor.base.js @@ -29,6 +29,7 @@ this.parentGroup = group this.group = null // Group created by a grouped editor, for example by the set editor this.childInspector = null + this.validators = [] Base.call(this) @@ -39,6 +40,8 @@ BaseEditor.prototype.constructor = Base BaseEditor.prototype.dispose = function() { + this.disposeValidators() + if (this.childInspector) { this.childInspector.dispose() } @@ -50,6 +53,7 @@ this.childInspector = null this.parentGroup = null this.group = null + this.validators = null BaseProto.dispose.call(this) } @@ -57,6 +61,7 @@ BaseEditor.prototype.init = function() { this.build() this.registerHandlers() + this.createValidators() } BaseEditor.prototype.build = function() { @@ -69,39 +74,13 @@ BaseEditor.prototype.onInspectorPropertyChanged = function(property, value) { } - BaseEditor.prototype.onExternalPropertyEditorHidden = function() { - } - BaseEditor.prototype.focus = function() { } - BaseEditor.prototype.supportsExternalParameterEditor = function() { - return true - } - - BaseEditor.prototype.isGroupedEditor = function() { - return false - } - BaseEditor.prototype.hasChildSurface = function() { return this.childInspector !== null } - BaseEditor.prototype.initControlGroup = function() { - this.group = this.inspector.getGroupManager().createGroup(this.propertyDefinition.property, this.parentGroup) - } - - BaseEditor.prototype.createGroupedRow = function(property) { - var row = this.inspector.buildRow(property, this.group), - groupedClass = this.inspector.getGroupManager().isGroupExpanded(this.group) ? 'expanded' : 'collapsed' - - this.inspector.applyGroupLevelToRow(row, this.group) - - $.oc.foundation.element.addClass(row, 'property') - $.oc.foundation.element.addClass(row, groupedClass) - return row - } - /** * Updates displayed value in the editor UI. The value is already set * in the Inspector and should be loaded from Inspector. @@ -117,5 +96,131 @@ return this.propertyDefinition.default === undefined ? undefined : this.propertyDefinition.default } + BaseEditor.prototype.throwError = function(errorMessage) { + throw new Error(errorMessage + ' Property: ' + this.propertyDefinition.property) + } + + // + // Validation + // + + BaseEditor.prototype.createValidators = function() { + // Handle legacy validation syntax properties: + // + // - required + // - validationPattern + // - validationMessage + + if ((this.propertyDefinition.required !== undefined || + this.propertyDefinition.validationPattern !== undefined || + this.propertyDefinition.validationMessage !== undefined) && + this.propertyDefinition.validation !== undefined) { + this.throwError('Legacy and new validation syntax should not be mixed.') + } + + if (this.propertyDefinition.required !== undefined) { + var validator = new $.oc.inspector.validators.required({ + message: this.propertyDefinition.validationMessage + }) + + this.validators.push(validator) + } + + if (this.propertyDefinition.validationPattern !== undefined) { + var validator = new $.oc.inspector.validators.regex({ + message: this.propertyDefinition.validationMessage, + pattern: this.propertyDefinition.validationPattern + }) + + this.validators.push(validator) + } + + // + // Handle new validation syntax + // + + if (this.propertyDefinition.validation === undefined) { + return + } + + for (var validatorName in this.propertyDefinition.validation) { + if ($.oc.inspector.validators[validatorName] == undefined) { + this.throwError('Inspector validator "' + validatorName + '" is not found in the $.oc.inspector.validators namespace.') + } + + var validator = new $.oc.inspector.validators[validatorName]( + this.propertyDefinition.validation[validatorName] + ) + + this.validators.push(validator) + } + } + + BaseEditor.prototype.disposeValidators = function() { + for (var i = 0, len = this.validators.length; i < len; i++) { + this.validators[i].dispose() + } + } + + BaseEditor.prototype.validate = function() { + for (var i = 0, len = this.validators.length; i < len; i++) { + var validator = this.validators[i], + value = this.inspector.getPropertyValue(this.propertyDefinition.property) + + if (value === undefined) { + value = this.getUndefinedValue() + } + + if (!validator.isValid(value)) { + $.oc.flashMsg({text: validator.getMessage(), 'class': 'error', 'interval': 5}) + return false + } + } + + return true + } + + BaseEditor.prototype.markInvalid = function() { + $.oc.foundation.element.addClass(this.containerRow, 'invalid') + this.inspector.getGroupManager().markGroupRowInvalid(this.parentGroup, this.inspector.getRootTable()) + + this.inspector.getRootSurface().expandGroupParents(this.parentGroup) + this.focus() + } + + // + // External editor + // + + BaseEditor.prototype.supportsExternalParameterEditor = function() { + return true + } + + BaseEditor.prototype.onExternalPropertyEditorHidden = function() { + } + + // + // Grouping + // + + BaseEditor.prototype.isGroupedEditor = function() { + return false + } + + BaseEditor.prototype.initControlGroup = function() { + this.group = this.inspector.getGroupManager().createGroup(this.propertyDefinition.property, this.parentGroup) + } + + BaseEditor.prototype.createGroupedRow = function(property) { + var row = this.inspector.buildRow(property, this.group), + groupedClass = this.inspector.getGroupManager().isGroupExpanded(this.group) ? 'expanded' : 'collapsed' + + this.inspector.applyGroupLevelToRow(row, this.group) + + $.oc.foundation.element.addClass(row, 'property') + $.oc.foundation.element.addClass(row, groupedClass) + return row + } + $.oc.inspector.propertyEditors.base = BaseEditor }(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.editor.dictionary.js b/modules/system/assets/ui/js/inspector.editor.dictionary.js index 31ea21a9a..70d7ccda5 100644 --- a/modules/system/assets/ui/js/inspector.editor.dictionary.js +++ b/modules/system/assets/ui/js/inspector.editor.dictionary.js @@ -40,7 +40,7 @@ var itemCount = 0 if (typeof value !== 'object') { - throw new Error('Object list value should be an object. Property: ' + this.propertyDefinition.property) + this.throwError('Object list value should be an object.') } itemCount = this.getValueKeys(value).length diff --git a/modules/system/assets/ui/js/inspector.editor.object.js b/modules/system/assets/ui/js/inspector.editor.object.js index 1c452bb04..e64b6c893 100644 --- a/modules/system/assets/ui/js/inspector.editor.object.js +++ b/modules/system/assets/ui/js/inspector.editor.object.js @@ -10,7 +10,7 @@ var ObjectEditor = function(inspector, propertyDefinition, containerCell, group) { if (propertyDefinition.properties === undefined) { - throw new Error('The properties property should be specified in the object editor configuration. Property: ' + propertyDefinition.property) + this.throwError('The properties property should be specified in the object editor configuration.') } Base.call(this, inspector, propertyDefinition, containerCell, group) diff --git a/modules/system/assets/ui/js/inspector.editor.objectlist.js b/modules/system/assets/ui/js/inspector.editor.objectlist.js index d90af738e..abefeaba6 100644 --- a/modules/system/assets/ui/js/inspector.editor.objectlist.js +++ b/modules/system/assets/ui/js/inspector.editor.objectlist.js @@ -488,7 +488,7 @@ var form = this.popup.querySelector('form') if (!form) { - throw new Error('Cannot find form element in the popup window.') + this.throwError('Cannot find form element in the popup window.') } return form diff --git a/modules/system/assets/ui/js/inspector.editor.stringlist.js b/modules/system/assets/ui/js/inspector.editor.stringlist.js index 66944bf5e..ee03a74ac 100644 --- a/modules/system/assets/ui/js/inspector.editor.stringlist.js +++ b/modules/system/assets/ui/js/inspector.editor.stringlist.js @@ -42,7 +42,7 @@ StringListEditor.prototype.checkValueType = function(value) { if (value && Object.prototype.toString.call(value) !== '[object Array]') { - throw new Error('The string list value should be an array.') + this.throwError('The string list value should be an array.') } } diff --git a/modules/system/assets/ui/js/inspector.externalparametereditor.js b/modules/system/assets/ui/js/inspector.externalparametereditor.js index 6ac6ed5df..95f1acdc2 100644 --- a/modules/system/assets/ui/js/inspector.externalparametereditor.js +++ b/modules/system/assets/ui/js/inspector.externalparametereditor.js @@ -241,6 +241,19 @@ this.getInput().focus() } + ExternalParameterEditor.prototype.validate = function() { + var value = $.trim(this.getValue()) + + if (value.length === 0) { + $.oc.flashMsg({text: 'Please enter the external parameter name.', 'class': 'error', 'interval': 5}) + this.focus() + + return false + } + + return true + } + // // Event handlers // diff --git a/modules/system/assets/ui/js/inspector.groups.js b/modules/system/assets/ui/js/inspector.groups.js index 5967b7d6b..1fec16adb 100644 --- a/modules/system/assets/ui/js/inspector.groups.js +++ b/modules/system/assets/ui/js/inspector.groups.js @@ -103,6 +103,27 @@ return group.findGroupRows(table, ignoreCollapsedSubgroups, this) } + GroupManager.prototype.markGroupRowInvalid = function(group, table) { + var currentGroup = group + + while (currentGroup) { + var row = currentGroup.findGroupRow(table) + if (row) { + $.oc.foundation.element.addClass(row, 'invalid') + } + + currentGroup = currentGroup.parentGroup + } + } + + GroupManager.prototype.unmarkInvalidGroups = function(table) { + var rows = table.querySelectorAll('tr.invalid') + + for (var i = rows.length-1; i >= 0; i--) { + $.oc.foundation.element.removeClass(rows[i], 'invalid') + } + } + GroupManager.prototype.isRowVisible = function(table, rowGroupIndex) { var group = this.findGroupByIndex(index) @@ -204,6 +225,19 @@ return level } + Group.prototype.getGroupAndAllParents = function() { + var current = this, + result = [] + + while (current) { + result.push(current) + + current = current.parentGroup + } + + return result + } + Group.prototype.findGroupRows = function(table, ignoreCollapsedSubgroups, groupManager) { var groupIndex = this.getGroupIndex(), rows = table.querySelectorAll('tr[data-parent-group-index="'+groupIndex+'"]'), @@ -225,5 +259,9 @@ return result } + Group.prototype.findGroupRow = function(table) { + return table.querySelector('tr[data-group-index="'+this.groupIndex+'"]') + } + $.oc.inspector.groupManager = GroupManager }(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.surface.js b/modules/system/assets/ui/js/inspector.surface.js index ee39c685f..505984f6f 100644 --- a/modules/system/assets/ui/js/inspector.surface.js +++ b/modules/system/assets/ui/js/inspector.surface.js @@ -390,14 +390,14 @@ th.children[0].style.marginLeft = groupLevel*10 + 'px' } - Surface.prototype.toggleGroup = function(row) { + Surface.prototype.toggleGroup = function(row, forceExpand) { var link = row.querySelector('a'), groupIndex = row.getAttribute('data-group-index'), table = this.getRootTable(), groupManager = this.getGroupManager(), collapse = true - if ($.oc.foundation.element.hasClass(link, 'expanded')) { + if ($.oc.foundation.element.hasClass(link, 'expanded') && !forceExpand) { $.oc.foundation.element.removeClass(link, 'expanded') } else { $.oc.foundation.element.addClass(link, 'expanded') @@ -407,21 +407,41 @@ var propertyRows = groupManager.findGroupRows(table, groupIndex, !collapse), duration = Math.round(50 / propertyRows.length) - this.expandOrCollapseRows(propertyRows, collapse, duration) + this.expandOrCollapseRows(propertyRows, collapse, duration, forceExpand) groupManager.setGroupStatus(groupIndex, !collapse) } - Surface.prototype.expandOrCollapseRows = function(rows, collapse, duration) { + Surface.prototype.expandGroupParents = function(group) { + var groups = group.getGroupAndAllParents(), + table = this.getRootTable() + + for (var i = groups.length-1; i >= 0; i--) { + var row = groups[i].findGroupRow(table) + + if (row) { + this.toggleGroup(row, true) + } + } + } + + Surface.prototype.expandOrCollapseRows = function(rows, collapse, duration, noAnimation) { var row = rows.pop(), self = this if (row) { - setTimeout(function toggleRow() { + if (!noAnimation) { + setTimeout(function toggleRow() { + $.oc.foundation.element.toggleClass(row, 'collapsed', collapse) + $.oc.foundation.element.toggleClass(row, 'expanded', !collapse) + + self.expandOrCollapseRows(rows, collapse, duration, noAnimation) + }, duration) + } else { $.oc.foundation.element.toggleClass(row, 'collapsed', collapse) $.oc.foundation.element.toggleClass(row, 'expanded', !collapse) - self.expandOrCollapseRows(rows, collapse, duration) - }, duration) + self.expandOrCollapseRows(rows, collapse, duration, noAnimation) + } } } @@ -708,10 +728,6 @@ Surface.prototype.getValues = function() { var result = {} -// TODO: implement validation in this method. It should be optional, -// as the method is used by other classes internally, but the validation -// is required only for the external callers. - for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) { var property = this.parsedProperties.properties[i] @@ -751,6 +767,32 @@ return result } + Surface.prototype.validate = function() { + this.getGroupManager().unmarkInvalidGroups(this.getRootTable()) + + for (var i = 0, len = this.editors.length; i < len; i++) { + var editor = this.editors[i], + externalEditor = this.findExternalParameterEditor(editor.propertyDefinition.property) + + if (externalEditor && externalEditor.isEditorVisible()) { + if (!externalEditor.validate()) { + editor.markInvalid() + return false + } + else { + continue + } + } + + if (!editor.validate()) { + editor.markInvalid() + return false + } + } + + return true + } + // EVENT HANDLERS // diff --git a/modules/system/assets/ui/js/inspector.validator.base.js b/modules/system/assets/ui/js/inspector.validator.base.js new file mode 100644 index 000000000..9e5ad6b84 --- /dev/null +++ b/modules/system/assets/ui/js/inspector.validator.base.js @@ -0,0 +1,56 @@ +/* + * Inspector validator base class. + */ ++function ($) { "use strict"; + + // NAMESPACES + // ============================ + + if ($.oc.inspector.validators === undefined) + $.oc.inspector.validators = {} + + // CLASS DEFINITION + // ============================ + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var BaseValidator = function(options) { + this.options = options + this.defaultMessage = 'Invalid property value' + } + + BaseValidator.prototype = Object.create(BaseProto) + BaseValidator.prototype.constructor = Base + + BaseValidator.prototype.dispose = function() { + this.defaultMessage = null + + BaseProto.dispose.call(this) + } + + BaseValidator.prototype.getMessage = function() { + if (this.options.message !== undefined) + return this.options.message + + return this.defaultMessage + } + + BaseValidator.prototype.isScalar = function(value) { + if (value === undefined || value === null) { + return true + } + + if (typeof value === 'string' || typeof value == 'number' || typeof value == 'boolean') { + return true + } + + return false + } + + BaseValidator.prototype.isValid = function(value) { + return true + } + + $.oc.inspector.validators.base = BaseValidator +}(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.validator.regex.js b/modules/system/assets/ui/js/inspector.validator.regex.js new file mode 100644 index 000000000..eb0a55e43 --- /dev/null +++ b/modules/system/assets/ui/js/inspector.validator.regex.js @@ -0,0 +1,40 @@ +/* + * Inspector regex validator. + */ ++function ($) { "use strict"; + + var Base = $.oc.inspector.validators.base, + BaseProto = Base.prototype + + var RegexValidator = function(options) { + Base.call(this, options) + } + + RegexValidator.prototype = Object.create(BaseProto) + RegexValidator.prototype.constructor = Base + + RegexValidator.prototype.isValid = function(value) { + if (!this.isScalar(value)) { + this.throwError('The Regex Inspector validator can only be used with string values.') + } + + if (value === undefined || value === null) { + return true + } + + var string = String(value) + + if (string.length == 0) + return + + if (this.options.pattern === undefined) { + this.throwError('The pattern parameter is not defined in the Regex Inspector validator configuration.') + } + + var regexObj = new RegExp(this.options.pattern, this.options.modifiers) + + return regexObj.test(string) + } + + $.oc.inspector.validators.regex = RegexValidator +}(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.validator.required.js b/modules/system/assets/ui/js/inspector.validator.required.js new file mode 100644 index 000000000..c8825128b --- /dev/null +++ b/modules/system/assets/ui/js/inspector.validator.required.js @@ -0,0 +1,33 @@ +/* + * Inspector required validator. + */ ++function ($) { "use strict"; + + var Base = $.oc.inspector.validators.base, + BaseProto = Base.prototype + + var RequiredValidator = function(options) { + Base.call(this, options) + } + + RequiredValidator.prototype = Object.create(BaseProto) + RequiredValidator.prototype.constructor = Base + + RequiredValidator.prototype.isValid = function(value) { + if (value === undefined || value === null) { + return false + } + + if (typeof value === 'boolean') { + return value + } + + if (typeof value === 'object') { + return !$.isEmptyObject(value) + } + + return $.trim(String(value)).length > 0 + } + + $.oc.inspector.validators.required = RequiredValidator +}(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/less/inspector.less b/modules/system/assets/ui/less/inspector.less index fbfc505c8..b00566466 100644 --- a/modules/system/assets/ui/less/inspector.less +++ b/modules/system/assets/ui/less/inspector.less @@ -58,6 +58,10 @@ } } + tr.invalid th { + color: #c03f31!important; + } + tr.control-group { .user-select(none); diff --git a/modules/system/assets/ui/storm.css b/modules/system/assets/ui/storm.css index 464c50865..d2c9169e5 100644 --- a/modules/system/assets/ui/storm.css +++ b/modules/system/assets/ui/storm.css @@ -2429,6 +2429,7 @@ table.table.data tr.list-tree-level-10 td.list-cell-index-1{padding-left:125px} .inspector-fields tr:last-child td,.inspector-fields tr:last-child td input[type=text]{-webkit-border-radius:0 0 2px 0;-moz-border-radius:0 0 2px 0;border-radius:0 0 2px 0} .inspector-fields tr.group{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .inspector-fields tr.group th{background:#e0e4e5;font-weight:600;cursor:pointer} +.inspector-fields tr.invalid th{color:#c03f31 !important} .inspector-fields tr.control-group{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .inspector-fields tr.control-group th,.inspector-fields tr.control-group td{cursor:pointer} .inspector-fields tr.collapsed{display:none}