Implementing Inspector validation + minor refactoring of the Inspector editors.

This commit is contained in:
alekseybobkov 2015-10-07 20:16:24 -07:00
parent 8d574e93d9
commit aee247727a
13 changed files with 373 additions and 41 deletions

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.')
}
}

View File

@ -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
//

View File

@ -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);

View File

@ -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
//

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -58,6 +58,10 @@
}
}
tr.invalid th {
color: #c03f31!important;
}
tr.control-group {
.user-select(none);

View File

@ -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}