From f823e6f0ba67885c1c1a477aca7b1cd22f09f92e Mon Sep 17 00:00:00 2001 From: alekseybobkov Date: Sat, 19 Sep 2015 12:57:31 -0700 Subject: [PATCH] Rewriting the Inspector, in progress. --- .../assets/ui/js/inspector.editor.base.js | 20 ++ .../assets/ui/js/inspector.editor.checkbox.js | 8 + .../assets/ui/js/inspector.editor.dropdown.js | 34 +- .../assets/ui/js/inspector.editor.string.js | 9 + .../js/inspector.externalparametereditor.js | 313 ++++++++++++++++++ .../system/assets/ui/js/inspector.surface.js | 144 +++++++- 6 files changed, 519 insertions(+), 9 deletions(-) create mode 100644 modules/system/assets/ui/js/inspector.externalparametereditor.js diff --git a/modules/system/assets/ui/js/inspector.editor.base.js b/modules/system/assets/ui/js/inspector.editor.base.js index 5274a2893..5bd9e1e53 100644 --- a/modules/system/assets/ui/js/inspector.editor.base.js +++ b/modules/system/assets/ui/js/inspector.editor.base.js @@ -54,5 +54,25 @@ BaseEditor.prototype.registerHandlers = function() { } + BaseEditor.prototype.onInspectorPropertyChanged = function(property, value) { + } + + BaseEditor.prototype.onExternalPropertyEditorHidden = function() { + } + + BaseEditor.prototype.focus = function() { + } + + /** + * Updates displayed value in the editor UI. The value is already set + * in the Inspector and should be loaded from Inspector. + */ + BaseEditor.prototype.updateDisplayedValue = function(value) { + } + + BaseEditor.prototype.getPropertyName = function() { + return this.propertyDefinition.property + } + $.oc.inspector.propertyEditors.base = BaseEditor }(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.editor.checkbox.js b/modules/system/assets/ui/js/inspector.editor.checkbox.js index 0590c1f18..b20d46414 100644 --- a/modules/system/assets/ui/js/inspector.editor.checkbox.js +++ b/modules/system/assets/ui/js/inspector.editor.checkbox.js @@ -66,6 +66,14 @@ return this.containerCell.querySelector('input') } + CheckboxEditor.prototype.focus = function() { + this.getInput().parentNode.focus() + } + + CheckboxEditor.prototype.updateDisplayedValue = function(value) { + this.getInput().checked = this.normalizeCheckedValue(value) + } + CheckboxEditor.prototype.registerHandlers = function() { var input = this.getInput() diff --git a/modules/system/assets/ui/js/inspector.editor.dropdown.js b/modules/system/assets/ui/js/inspector.editor.dropdown.js index 8bcaa8257..e31f4bac1 100644 --- a/modules/system/assets/ui/js/inspector.editor.dropdown.js +++ b/modules/system/assets/ui/js/inspector.editor.dropdown.js @@ -151,6 +151,31 @@ this.inspector.setPropertyValue(this.propertyDefinition.property, select.value, this.initialization) } + DropdownEditor.prototype.onInspectorPropertyChanged = function(property, value) { + if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) { + return + } + + var dependencyValues = this.getDependencyValues() + + if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) + this.loadDynamicOptions() + } + + DropdownEditor.prototype.onExternalPropertyEditorHidden = function() { + this.loadDynamicOptions(false) + } + + // + // Editor API methods + // + + DropdownEditor.prototype.updateDisplayedValue = function(value) { + var select = this.getSelect() + + select.value = value + } + // // Disposing // @@ -224,8 +249,9 @@ DropdownEditor.prototype.getDependencyValues = function() { var result = '' - for (var property in this.propertyDefinition.depends) { - var value = this.inspector.getPropertyValue(property) + for (var i = 0, len = this.propertyDefinition.depends.length; i < len; i++) { + var property = this.propertyDefinition.depends[i], + value = this.inspector.getPropertyValue(property) if (value === undefined) { value = ''; @@ -252,7 +278,11 @@ DropdownEditor.prototype.optionsRequestDone = function(data, currentValue, initialization) { var select = this.getSelect() + // Without destroying and recreating the custom select + // there could be detached DOM nodes. + this.destroyCustomSelect() this.clearOptions(select) + this.initCustomSelect() this.createPlaceholder(select) diff --git a/modules/system/assets/ui/js/inspector.editor.string.js b/modules/system/assets/ui/js/inspector.editor.string.js index 43be4cf41..ce3580276 100644 --- a/modules/system/assets/ui/js/inspector.editor.string.js +++ b/modules/system/assets/ui/js/inspector.editor.string.js @@ -39,10 +39,19 @@ this.containerCell.appendChild(editor) } + StringEditor.prototype.updateDisplayedValue = function(value) { + this.getInput().value = value + } + StringEditor.prototype.getInput = function() { return this.containerCell.querySelector('input') } + StringEditor.prototype.focus = function() { + this.getInput().focus() + this.onInputFocus() + } + StringEditor.prototype.registerHandlers = function() { var input = this.getInput() diff --git a/modules/system/assets/ui/js/inspector.externalparametereditor.js b/modules/system/assets/ui/js/inspector.externalparametereditor.js new file mode 100644 index 000000000..6ac6ed5df --- /dev/null +++ b/modules/system/assets/ui/js/inspector.externalparametereditor.js @@ -0,0 +1,313 @@ +/* + * External parameter editor for Inspector. + * + * The external parameter editor allows to use URL and + * other external parameters as values for the inspectable + * properties. + * + */ ++function ($) { "use strict"; + + // NAMESPACES + // ============================ + + if ($.oc === undefined) + $.oc = {} + + if ($.oc.inspector === undefined) + $.oc.inspector = {} + + // CLASS DEFINITION + // ============================ + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var ExternalParameterEditor = function(inspector, propertyDefinition, containerCell) { + this.inspector = inspector + this.propertyDefinition = propertyDefinition + this.containerCell = containerCell + + Base.call(this) + + this.init() + } + + ExternalParameterEditor.prototype = Object.create(BaseProto) + ExternalParameterEditor.prototype.constructor = Base + + ExternalParameterEditor.prototype.dispose = function() { + this.disposeControls() + this.unregisterHandlers() + + this.inspector = null + this.propertyDefinition = null + this.containerCell = null + + BaseProto.dispose.call(this) + } + + ExternalParameterEditor.prototype.init = function() { + this.tooltipText = 'Click to enter the external parameter name to load the property value from' + + this.build() + this.registerHandlers() + this.setInitialValue() + } + + /** + * Builds the external parameter editor markup: + * + *
+ * <-- original property editing input/markup + *
+ *
+ * + * + * + * + *
+ *
+ *
+ */ + ExternalParameterEditor.prototype.build = function() { + var container = document.createElement('div'), + editor = document.createElement('div'), + controls = document.createElement('div'), + input = document.createElement('input'), + link = document.createElement('a'), + icon = document.createElement('i') + + container.setAttribute('class', 'external-param-editor-container') + editor.setAttribute('class', 'external-editor') + controls.setAttribute('class', 'controls') + input.setAttribute('type', 'text') + input.setAttribute('tabindex', '-1') + + link.setAttribute('href', '#') + link.setAttribute('class', 'external-editor-link') + link.setAttribute('tabindex', '-1') + link.setAttribute('title', this.tooltipText) + $(link).tooltip({'container': 'body', delay: 500}) + + icon.setAttribute('class', 'oc-icon-terminal') + + link.appendChild(icon) + controls.appendChild(input) + controls.appendChild(link) + editor.appendChild(controls) + + while (this.containerCell.firstChild) { + var child = this.containerCell.firstChild + + container.appendChild(child) + } + + container.appendChild(editor) + this.containerCell.appendChild(container) + } + + ExternalParameterEditor.prototype.setInitialValue = function() { + var propertyValue = this.inspector.getPropertyValue(this.propertyDefinition.property) + + if (!propertyValue) { + return + } + + if (typeof propertyValue !== 'string') { + return + } + + var matches = [] + if (matches = propertyValue.match(/^\{\{([^\}]+)\}\}$/)) { + var value = $.trim(matches[1]) + + if (value.length > 0) { + this.showEditor(true) + this.getInput().value = value + this.inspector.setPropertyValue(this.propertyDefinition.property, null, true, true) + } + } + } + + ExternalParameterEditor.prototype.showEditor = function(building) { + var editor = this.getEditor(), + input = this.getInput(), + container = this.getContainer(), + link = this.getLink() + + var position = $(editor).position() + + if (!building) { + editor.style.right = 0 + editor.style.left = position.left + 'px' + } + else { + editor.style.right = 0 + } + + setTimeout(this.proxy(this.repositionEditor), 0) + + $.oc.foundation.element.addClass(container, 'editor-visible') + link.setAttribute('data-original-title', 'Click to enter the property value') + + this.toggleEditorVisibility(false) + input.setAttribute('tabindex', 0) + + if (!building) { + input.focus() + } + } + + ExternalParameterEditor.prototype.repositionEditor = function() { + this.getEditor().style.left = 0 + this.containerCell.scrollTop = 0 + } + + ExternalParameterEditor.prototype.hideEditor = function() { + var editor = this.getEditor(), + container = this.getContainer() + + editor.style.left = 'auto' + editor.style.right = '30px' + + $.oc.foundation.element.removeClass(container, 'editor-visible') + $.oc.foundation.element.removeClass(this.containerCell, 'active') + + var propertyEditor = this.inspector.findPropertyEditor(this.propertyDefinition.property) + + if (propertyEditor) { + propertyEditor.onExternalPropertyEditorHidden() + } + } + + ExternalParameterEditor.prototype.toggleEditor = function(ev) { + $.oc.foundation.event.stop(ev) + + var link = this.getLink(), + container = this.getContainer(), + editor = this.getEditor() + + $(link).tooltip('hide') + + if (!this.isEditorVisible()) { + this.showEditor() + return + } + + var left = container.offsetWidth + + editor.style.left = left + 'px' + link.setAttribute('data-original-title', this.tooltipText) + this.getInput().setAttribute('tabindex', '-1') + + this.toggleEditorVisibility(true) + + setTimeout(this.proxy(this.hideEditor), 200) + } + + ExternalParameterEditor.prototype.toggleEditorVisibility = function(show) { + var container = this.getContainer(), + children = container.children, + height = 0 + + if (!show) { + height = this.containerCell.getAttribute('data-inspector-cell-height') + + if (!height) { + height = $(this.containerCell).height() + this.containerCell.setAttribute('data-inspector-cell-height', height) + } + } + + for (var i = 0, len = children.length; i < len; i++) { + var element = children[i] + + if ($.oc.foundation.element.hasClass(element, 'external-editor')) { + continue + } + + if (show) { + $.oc.foundation.element.removeClass(element, 'hide') + } + else { + container.style.height = height + 'px' + $.oc.foundation.element.addClass(element, 'hide') + } + } + } + + ExternalParameterEditor.prototype.focus = function() { + this.getInput().focus() + } + + // + // Event handlers + // + + ExternalParameterEditor.prototype.registerHandlers = function() { + var input = this.getInput() + + this.getLink().addEventListener('click', this.proxy(this.toggleEditor)) + input.addEventListener('focus', this.proxy(this.onInputFocus)) + input.addEventListener('change', this.proxy(this.onInputChange)) + } + + ExternalParameterEditor.prototype.onInputFocus = function() { + this.inspector.makeCellActive(this.containerCell) + } + + ExternalParameterEditor.prototype.onInputChange = function() { + this.inspector.markPropertyChanged(this.propertyDefinition.property, true) + } + + // + // Disposing + // + + ExternalParameterEditor.prototype.unregisterHandlers = function() { + var input = this.getInput() + + this.getLink().removeEventListener('click', this.proxy(this.toggleEditor)) + input.removeEventListener('focus', this.proxy(this.onInputFocus)) + input.removeEventListener('change', this.proxy(this.onInputChange)) + } + + ExternalParameterEditor.prototype.disposeControls = function() { + $(this.getLink()).tooltip('destroy') + } + + // + // Helpers + // + + ExternalParameterEditor.prototype.getInput = function() { + return this.containerCell.querySelector('div.external-editor input') + } + + ExternalParameterEditor.prototype.getValue = function() { + return this.getInput().value + } + + ExternalParameterEditor.prototype.getLink = function() { + return this.containerCell.querySelector('a.external-editor-link') + } + + ExternalParameterEditor.prototype.getContainer = function() { + return this.containerCell.querySelector('div.external-param-editor-container') + } + + ExternalParameterEditor.prototype.getEditor = function() { + return this.containerCell.querySelector('div.external-editor') + } + + ExternalParameterEditor.prototype.getPropertyName = function() { + return this.propertyDefinition.property + } + + ExternalParameterEditor.prototype.isEditorVisible = function() { + return $.oc.foundation.element.hasClass(this.getContainer(), 'editor-visible') + } + + $.oc.inspector.externalParameterEditor = ExternalParameterEditor +}(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 b6213284e..5c29f0cbc 100644 --- a/modules/system/assets/ui/js/inspector.surface.js +++ b/modules/system/assets/ui/js/inspector.surface.js @@ -40,7 +40,7 @@ throw new Error('Inspector surface unique ID should be defined.') } - this.options = $.extend({}, Surface.DEFAULTS, typeof option == 'object' && option) + this.options = $.extend({}, Surface.DEFAULTS, typeof options == 'object' && options) this.rawProperties = properties this.parsedProperties = $.oc.inspector.engine.processPropertyGroups(properties) this.container = containerElement @@ -50,6 +50,7 @@ this.idCounter = 1 this.editors = [] + this.externalParameterEditors = [] this.tableContainer = null Base.call(this) @@ -62,7 +63,9 @@ Surface.prototype.dispose = function() { this.unregisterHandlers() + this.disposeControls() this.removeElements() + this.disposeExternalParameterEditors() this.disposeEditors() this.container = null @@ -70,6 +73,7 @@ this.rawProperties = null this.parsedProperties = null this.editors = null + this.externalParameterEditors = null this.values = null this.originalValues = null @@ -187,6 +191,37 @@ this.tableContainer.appendChild(dataTable) this.container.appendChild(this.tableContainer) + + if (this.options.enableExternalParameterEditor) { + this.buildExternalParameterEditor(tbody) + } + + this.focusFirstEditor() + } + + Surface.prototype.focusFirstEditor = function() { + if (this.editors.length == 0) { + return + } + + for (var i = 0, len = this.editors.length; i < len; i++) { + var editor = this.editors[i], + group = editor.propertyDefinition.group + + if (group && !this.isGroupExpanded(group)) { + continue + } + + var externalParameterEditor = this.findExternalParameterEditor(editor.getPropertyName()) + + if (externalParameterEditor && externalParameterEditor.isEditorVisible()) { + externalParameterEditor.focus() + return + } + + editor.focus() + return + } } Surface.prototype.applyGroupIndexAttribute = function(property, row) { @@ -241,9 +276,30 @@ span.setAttribute('title', this.escapeJavascriptString(property.description)) span.setAttribute('class', 'info oc-icon-info with-tooltip') + $(span).tooltip({ placement: 'auto right', container: 'body', delay: 500 }) + return span } + Surface.prototype.buildExternalParameterEditor = function(tbody) { + var rows = tbody.children + + for (var i = 0, len = rows.length; i < len; i++) { + var row = rows[i], + property = row.getAttribute('data-property') + + if ($.oc.foundation.element.hasClass(row, 'no-external-parameter') || !property) { + continue + } + + var cell = row.querySelector('td'), + propertyDefinition = this.findPropertyDefinition(property), + editor = new $.oc.inspector.externalParameterEditor(this, propertyDefinition, cell) + + this.externalParameterEditors.push(editor) + } + } + // // Field grouping // @@ -365,6 +421,10 @@ 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] @@ -372,10 +432,19 @@ continue } - var value = this.getPropertyValue(property.property) + var value = null, + externalParameterEditor = this.findExternalParameterEditor(property.property) - if (value === undefined) { - value = property.default + if (!externalParameterEditor || !externalParameterEditor.isEditorVisible()) { + value = this.getPropertyValue(property.property) + + if (value === undefined) { + value = property.default + } + } + else { + value = externalParameterEditor.getValue() + value = '{{ ' + value + ' }}' } result[property.property] = value @@ -384,7 +453,7 @@ return result } - Surface.prototype.setPropertyValue = function(property, value, supressChangeEvents) { + Surface.prototype.setPropertyValue = function(property, value, supressChangeEvents, forceEditorUpdate) { this.values[property] = value if (!supressChangeEvents) { @@ -395,12 +464,27 @@ this.markPropertyChanged(property, false) } -// TODO: here we should force dependent editors to update + this.notifyEditorsPropertyChanged(property, value) + } + + if (forceEditorUpdate) { + var editor = this.findPropertyEditor(property) + if (editor) { + editor.updateDisplayedValue(value) + } } return value } + Surface.prototype.notifyEditorsPropertyChanged = function(property, value) { + for (var i = 0, len = this.editors.length; i < len; i++) { + var editor = this.editors[i] + + editor.onInspectorPropertyChanged(property, value) + } + } + Surface.prototype.makeCellActive = function(cell) { var tbody = cell.parentNode.parentNode.parentNode, // cell / row / tbody cells = tbody.querySelectorAll('tr td') @@ -423,6 +507,36 @@ } } + Surface.prototype.findPropertyEditor = function(property) { + for (var i = 0, len = this.editors.length; i < len; i++) { + if (this.editors[i].getPropertyName() == property) + return this.editors[i] + } + + return null + } + + Surface.prototype.findExternalParameterEditor = function(property) { + for (var i = 0, len = this.externalParameterEditors.length; i < len; i++) { + if (this.externalParameterEditors[i].getPropertyName() == property) + return this.externalParameterEditors[i] + } + + return null + } + + Surface.prototype.findPropertyDefinition = function(property) { + for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) { + var definition = this.parsedProperties.properties[i] + + if (definition.property == property) { + return definition + } + } + + return null + } + // // Disposing // @@ -439,6 +553,22 @@ } } + Surface.prototype.disposeExternalParameterEditors = function() { + for (var i = 0, len = this.externalParameterEditors.length; i < len; i++) { + var editor = this.externalParameterEditors[i] + + editor.dispose() + } + } + + Surface.prototype.disposeControls = function() { + var tooltipControls = this.tableContainer.querySelectorAll('.with-tooltip') + + for (var i = 0, len = tooltipControls.length; i < len; i++) { + $(tooltipControls[i]).tooltip('destroy') + } + } + // // Helpers // @@ -462,7 +592,7 @@ // ============================ Surface.DEFAULTS = { - showExternalParam: false + enableExternalParameterEditor: false } // REGISTRATION