diff --git a/modules/backend/assets/css/october.css b/modules/backend/assets/css/october.css index c9cce185f..99f947dce 100644 --- a/modules/backend/assets/css/october.css +++ b/modules/backend/assets/css/october.css @@ -484,6 +484,8 @@ div.control-scrollpad > .scrollpad-scrollbar[data-visible]{opacity:0.7} div.control-scrollpad > .scrollpad-scrollbar[data-hidden]{display:none} div.control-scrollpad[data-direction=horizontal] > .scrollpad-scrollbar{top:auto;left:0;width:auto;height:11px} div.control-scrollpad[data-direction=horizontal] > .scrollpad-scrollbar .drag-handle{right:auto;top:2px;height:7px;min-height:0;min-width:10px;width:auto} +.autocomplete.dropdown-menu{background:white} +.autocomplete.dropdown-menu li a{padding:3px 12px} @font-face{font-family:'Open Sans';src:url('../font/OpenSans-Light.eot');src:url('../font/OpenSans-Light.eot?#iefix') format('embedded-opentype'),url('../font/OpenSans-Light.svg#open_sanslight') format('svg'),url('../font/OpenSans-Light.woff') format('woff'),url('../font/OpenSans-Light.ttf') format('truetype');font-style:normal;font-weight:300} @font-face{font-family:'Open Sans';src:url('../font/OpenSans-Regular.eot');src:url('../font/OpenSans-Regular.eot?#iefix') format('embedded-opentype'),url('../font/OpenSans-Regular.svg#open_sansregular') format('svg'),url('../font/OpenSans-Regular.woff') format('woff'),url('../font/OpenSans-Regular.ttf') format('truetype');font-style:normal;font-weight:400} @font-face{font-family:'Open Sans';src:url('../font/OpenSans-Semibold.eot');src:url('../font/OpenSans-Semibold.eot?#iefix') format('embedded-opentype'),url('../font/OpenSans-Semibold.svg#open_sanssemibold') format('svg'),url('../font/OpenSans-Semibold.woff') format('woff'),url('../font/OpenSans-Semibold.ttf') format('truetype');font-style:normal;font-weight:600} @@ -789,16 +791,16 @@ html.csstransitions body.outer.preload .outer-form-container{-webkit-transform:s .fancy-layout .control-tabs.master-tabs.scroll-after:after,.fancy-layout.control-tabs.master-tabs.scroll-after:after{color:#ffffff} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container{background:#d35400;padding-left:20px;padding-right:20px} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs{margin-left:-8px} -.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li{margin-left:-10px;top:1px} +.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li{margin-left:-8px;top:1px} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close{top:9px;right:-3px;left:auto;z-index:110} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close i,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close i{top:4px;right:1px;color:rgba(255,255,255,0.3) !important;font:14px bold "Helvetica Neue",Helvetica,Arial,sans-serif} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close i:hover,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li span.tab-close i:hover{color:#ffffff !important} -.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a{border-bottom:none;background:transparent;font-size:14px;color:rgba(255,255,255,0.35);padding:6px 0 0 0;overflow:visible} +.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a{border-bottom:none;background:transparent;font-size:14px;color:rgba(255,255,255,0.35);padding:6px 0 0 24px!important;overflow:visible} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title{position:relative;display:inline-block;padding:8px 5px 9px 5px;height:31px;font-size:13px;z-index:100;background-color:#b9530f} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:before,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:before,.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:after,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:after{content:' ';position:absolute;background:transparent url(../images/tab-shape.svg) no-repeat left -80px;width:20px;display:block;height:30px;top:0;z-index:100} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:before,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:before{left:-20px} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:after,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a > span.title:after{right:-20px;background-position:-80px -80px} -.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a:before,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a:before{z-index:110;position:relative;margin-right:-12px} +.fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a:before,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a:before{z-index:110;position:absolute;top:15px;left:22px} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a[class*=icon] > span.title,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li a[class*=icon] > span.title{padding-left:18px} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li.active a,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li.active a{z-index:107;color:#ffffff} .fancy-layout .control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li.active span.tab-close i,.fancy-layout.control-tabs.master-tabs > div > div.tabs-container > ul.nav-tabs > li.active span.tab-close i{color:#ffffff} @@ -856,7 +858,7 @@ html.csstransitions body.outer.preload .outer-form-container{-webkit-transform:s .fancy-layout .control-tabs.primary-tabs > .tab-content > .tab-pane.pane-compact,.fancy-layout.control-tabs.primary-tabs > .tab-content > .tab-pane.pane-compact{padding:0} .fancy-layout .control-tabs.primary-tabs > .tab-content > .tab-pane .form-control,.fancy-layout.control-tabs.primary-tabs > .tab-content > .tab-pane .form-control{border-width:1px} .fancy-layout .control-tabs.primary-tabs.collapsed,.fancy-layout.control-tabs.primary-tabs.collapsed{display:none} -.fancy-layout .control-tabs > div.tab-content,.fancy-layout.control-tabs > div.tab-content{background:#f9f9f9} +.fancy-layout .control-tabs.has-tabs > div.tab-content,.fancy-layout.control-tabs.has-tabs > div.tab-content{background:#f9f9f9} .fancy-layout .control-tabs > div.tab-content > div.tab-pane,.fancy-layout.control-tabs > div.tab-content > div.tab-pane{padding:0} .fancy-layout .control-tabs > div.tab-content > div.tab-pane.padded-pane,.fancy-layout.control-tabs > div.tab-content > div.tab-pane.padded-pane{padding:15px 15px 0 15px} .fancy-layout .form-tabless-fields{position:relative;background:#e67e22;padding:18px 23px 0 23px;-webkit-transition:all 0.5s;transition:all 0.5s} diff --git a/modules/backend/assets/less/controls/autocomplete.less b/modules/backend/assets/less/controls/autocomplete.less new file mode 100644 index 000000000..26ed4d335 --- /dev/null +++ b/modules/backend/assets/less/controls/autocomplete.less @@ -0,0 +1,7 @@ +.autocomplete.dropdown-menu { + background: white; + + li a { + padding: 3px 12px; + } +} \ No newline at end of file diff --git a/modules/backend/assets/less/layout/fancylayout.less b/modules/backend/assets/less/layout/fancylayout.less index d68220b7e..98a49abe2 100644 --- a/modules/backend/assets/less/layout/fancylayout.less +++ b/modules/backend/assets/less/layout/fancylayout.less @@ -54,7 +54,7 @@ > ul.nav-tabs { margin-left: -8px; > li { - margin-left: -10px; + margin-left: -8px; top: 1px; span.tab-close{ @@ -79,7 +79,7 @@ background: transparent; font-size: 14px; color: @color-fancy-master-tabs-inactive-text; - padding: 6px 0 0 0; + padding: 6px 0 0 24px!important; overflow: visible; > span.title { @@ -114,8 +114,9 @@ &:before { z-index: 110; - position: relative; - margin-right: -12px; + position: absolute; + top: 15px; + left: 22px; } &[class*=icon] > span.title { @@ -401,9 +402,13 @@ } } - > div.tab-content { - background: @body-bg; + &.has-tabs { + > div.tab-content { + background: @body-bg; + } + } + > div.tab-content { > div.tab-pane { padding: 0; diff --git a/modules/backend/assets/less/october.less b/modules/backend/assets/less/october.less index 4fa7b625d..4c1f03313 100644 --- a/modules/backend/assets/less/october.less +++ b/modules/backend/assets/less/october.less @@ -35,6 +35,7 @@ @import "controls/tree-path.less"; @import "controls/namevaluelist.less"; @import "controls/scrollpad.less"; +@import "controls/autocomplete.less"; // // October Storm UI diff --git a/modules/backend/traits/InspectableContainer.php b/modules/backend/traits/InspectableContainer.php index 1376cb7e1..a3b5f5c70 100644 --- a/modules/backend/traits/InspectableContainer.php +++ b/modules/backend/traits/InspectableContainer.php @@ -33,7 +33,21 @@ trait InspectableContainer $obj = new $className(null); - $methodName = 'get'.ucfirst($property).'Options'; + // Nested properties have names like object.property. + // Convert them to Object.Property. + $propertyNameParts = explode('.', $property); + $propertyMethodName = ''; + foreach ($propertyNameParts as $part) { + $part = trim($part); + + if (!strlen($part)) { + continue; + } + + $propertyMethodName .= ucfirst($part); + } + + $methodName = 'get'.$propertyMethodName.'Options'; if (method_exists($obj, $methodName)) { $options = $obj->$methodName(); } @@ -53,4 +67,4 @@ trait InspectableContainer 'options' => $optionsArray ]; } -} +} \ No newline at end of file diff --git a/modules/backend/widgets/table/assets/js/build-min.js b/modules/backend/widgets/table/assets/js/build-min.js index 8540ad926..f31c4126d 100644 --- a/modules/backend/widgets/table/assets/js/build-min.js +++ b/modules/backend/widgets/table/assets/js/build-min.js @@ -6,6 +6,7 @@ $.oc.table={} var Table=function(element,options){this.el=element this.$el=$(element) this.options=options +this.disposed=false this.dataSource=null this.cellProcessors={} this.activeCellProcessor=null @@ -24,7 +25,9 @@ if(this.options.postback&&this.options.clientDataSourceClass=='client') this.formSubmitHandler=this.onFormSubmit.bind(this) this.navigation=null this.recordsAddedOrDeleted=0 -this.init()} +this.disposeBound=this.dispose.bind(this) +this.init() +$.oc.foundation.controlUtils.markDisposable(element)} Table.prototype.init=function(){this.createDataSource() this.initCellProcessors() this.navigation=new $.oc.table.helper.navigation(this) @@ -41,6 +44,7 @@ throw new Error('The table client-side data source class "'+dataSourceClass+'" i this.dataSource=new $.oc.table.datasource[dataSourceClass](this)} Table.prototype.registerHandlers=function(){this.el.addEventListener('click',this.clickHandler) this.el.addEventListener('keydown',this.keydownHandler) +this.$el.one('dispose-control',this.disposeBound) document.addEventListener('click',this.documentClickHandler) if(this.options.postback&&this.options.clientDataSourceClass=='client') this.$el.closest('form').bind('oc.beforeRequest',this.formSubmitHandler) @@ -308,7 +312,10 @@ return if(this.activeCellProcessor&&this.activeCellProcessor.elementBelongsToProcessor(target)) return this.unfocusTable()} -Table.prototype.dispose=function(){this.unfocusTable() +Table.prototype.dispose=function(){if(this.disposed){return} +this.disposed=true +this.disposeBound=true +this.unfocusTable() this.dataSource.dispose() this.dataSource=null this.unregisterHandlers() diff --git a/modules/backend/widgets/table/assets/js/table.js b/modules/backend/widgets/table/assets/js/table.js index 086a425c3..946f43d88 100644 --- a/modules/backend/widgets/table/assets/js/table.js +++ b/modules/backend/widgets/table/assets/js/table.js @@ -23,6 +23,7 @@ this.$el = $(element) this.options = options + this.disposed = false // // State properties @@ -77,11 +78,16 @@ // Number of records added or deleted during the session this.recordsAddedOrDeleted = 0 + // Bound reference to dispose() - ideally the class should use the October foundation library base class + this.disposeBound = this.dispose.bind(this) + // // Initialization // this.init() + + $.oc.foundation.controlUtils.markDisposable(element) } // INTERNAL METHODS @@ -136,6 +142,8 @@ Table.prototype.registerHandlers = function() { this.el.addEventListener('click', this.clickHandler) this.el.addEventListener('keydown', this.keydownHandler) + this.$el.one('dispose-control', this.disposeBound) + document.addEventListener('click', this.documentClickHandler) if (this.options.postback && this.options.clientDataSourceClass == 'client') @@ -822,6 +830,16 @@ // ============================ Table.prototype.dispose = function() { + if (this.disposed) { + // Prevent errors when legacy code executes the dispose() method + // directly, bypassing $.oc.foundation.controlUtils.disposeControls(container) + return + } + + this.disposed = true + + this.disposeBound = true + // Remove an editor and commit the data if needed this.unfocusTable() diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index 7647f8cf7..c52c25d7f 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -168,6 +168,7 @@ class Controller MaintenanceSettings::get('is_enabled', false) && !BackendAuth::getUser() ) { + $this->setStatusCode(503); $page = Page::loadCached($this->theme, MaintenanceSettings::get('cms_page')); } diff --git a/modules/system/assets/ui/js/input.trigger.js b/modules/system/assets/ui/js/input.trigger.js index af4f62189..9a760e8d9 100644 --- a/modules/system/assets/ui/js/input.trigger.js +++ b/modules/system/assets/ui/js/input.trigger.js @@ -25,7 +25,7 @@ if (this.options.triggerCondition.indexOf('value') == 0) { var match = this.options.triggerCondition.match(/[^[\]]+(?=])/g) this.triggerCondition = 'value' - this.triggerConditionValue = (match) ? match : "" + this.triggerConditionValue = (match) ? match : [""] } this.triggerParent = this.options.triggerClosestParent !== undefined diff --git a/modules/system/assets/ui/js/inspector.editor.autocomplete.js b/modules/system/assets/ui/js/inspector.editor.autocomplete.js index 574c2ed3c..6e6215c09 100644 --- a/modules/system/assets/ui/js/inspector.editor.autocomplete.js +++ b/modules/system/assets/ui/js/inspector.editor.autocomplete.js @@ -9,6 +9,8 @@ BaseProto = Base.prototype var AutocompleteEditor = function(inspector, propertyDefinition, containerCell, group) { + this.autoUpdateTimeout = null + Base.call(this, inspector, propertyDefinition, containerCell, group) } @@ -16,6 +18,7 @@ AutocompleteEditor.prototype.constructor = Base AutocompleteEditor.prototype.dispose = function() { + this.clearAutoUpdateTimeout() this.removeAutocomplete() BaseProto.dispose.call(this) @@ -63,10 +66,18 @@ items = [] } - $(input).autocomplete({ - source: this.prepareItems(items), - matchWidth: true - }) + var $input = $(input), + autocomplete = $input.data('autocomplete') + + if (!autocomplete) { + $input.autocomplete({ + source: this.prepareItems(items), + matchWidth: true + }) + } + else { + autocomplete.source = this.prepareItems(items) + } } AutocompleteEditor.prototype.removeAutocomplete = function() { @@ -110,6 +121,46 @@ $(this.getInput()).off('change', this.proxy(this.onInputKeyUp)) } + AutocompleteEditor.prototype.saveDependencyValues = function() { + this.prevDependencyValues = this.getDependencyValues() + } + + AutocompleteEditor.prototype.getDependencyValues = function() { + var result = '' + + 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 = ''; + } + + result += property + ':' + value + '-' + } + + return result + } + + AutocompleteEditor.prototype.onInspectorPropertyChanged = function(property, value) { + if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) { + return + } + + this.clearAutoUpdateTimeout() + + if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) { + this.autoUpdateTimeout = setTimeout(this.proxy(this.loadDynamicItems), 200) + } + } + + AutocompleteEditor.prototype.clearAutoUpdateTimeout = function() { + if (this.autoUpdateTimeout !== null) { + clearTimeout(this.autoUpdateTimeout) + this.autoUpdateTimeout = null + } + } + // // Dynamic items // @@ -132,8 +183,14 @@ } AutocompleteEditor.prototype.loadDynamicItems = function() { + if (this.isDisposed()) { + return + } + + this.clearAutoUpdateTimeout() + var container = this.getContainer(), - data = this.inspector.getValues(), + data = this.getRootSurface().getValues(), $form = $(container).closest('form') $.oc.foundation.element.addClass(container, 'loading-indicator-container size-small') @@ -143,7 +200,7 @@ return } - data['inspectorProperty'] = this.propertyDefinition.property + data['inspectorProperty'] = this.getPropertyPath() data['inspectorClassName'] = this.inspector.options.inspectorClass $form.request('onInspectableGetOptions', { diff --git a/modules/system/assets/ui/js/inspector.editor.base.js b/modules/system/assets/ui/js/inspector.editor.base.js index c0f0cd9f0..357404f77 100644 --- a/modules/system/assets/ui/js/inspector.editor.base.js +++ b/modules/system/assets/ui/js/inspector.editor.base.js @@ -81,6 +81,14 @@ BaseEditor.prototype.onInspectorPropertyChanged = function(property, value) { } + BaseEditor.prototype.notifyChildSurfacesPropertyChanged = function(property, value) { + if (!this.hasChildSurface()) { + return + } + + this.childInspector.notifyEditorsPropertyChanged(property, value) + } + BaseEditor.prototype.focus = function() { } @@ -92,6 +100,10 @@ return this.inspector.getRootSurface() } + BaseEditor.prototype.getPropertyPath = function() { + return this.inspector.getPropertyPath(this.propertyDefinition.property) + } + /** * Updates displayed value in the editor UI. The value is already set * in the Inspector and should be loaded from Inspector. diff --git a/modules/system/assets/ui/js/inspector.editor.dictionary.js b/modules/system/assets/ui/js/inspector.editor.dictionary.js index 510820453..755c80e3d 100644 --- a/modules/system/assets/ui/js/inspector.editor.dictionary.js +++ b/modules/system/assets/ui/js/inspector.editor.dictionary.js @@ -1,5 +1,5 @@ /* - * Inspector text editor class. + * Inspector dictionary editor class. */ +function ($) { "use strict"; diff --git a/modules/system/assets/ui/js/inspector.editor.dropdown.js b/modules/system/assets/ui/js/inspector.editor.dropdown.js index f77937371..09cdb29b0 100644 --- a/modules/system/assets/ui/js/inspector.editor.dropdown.js +++ b/modules/system/assets/ui/js/inspector.editor.dropdown.js @@ -306,7 +306,7 @@ DropdownEditor.prototype.loadDynamicOptions = function(initialization) { var currentValue = this.inspector.getPropertyValue(this.propertyDefinition.property), - data = this.inspector.getValues(), + data = this.getRootSurface().getValues(), self = this, $form = $(this.getSelect()).closest('form') @@ -323,7 +323,7 @@ this.saveDependencyValues() } - data['inspectorProperty'] = this.propertyDefinition.property + data['inspectorProperty'] = this.getPropertyPath() data['inspectorClassName'] = this.inspector.options.inspectorClass this.showLoadingIndicator() diff --git a/modules/system/assets/ui/js/inspector.editor.set.js b/modules/system/assets/ui/js/inspector.editor.set.js index 6bc6f759f..ea40eda94 100644 --- a/modules/system/assets/ui/js/inspector.editor.set.js +++ b/modules/system/assets/ui/js/inspector.editor.set.js @@ -164,7 +164,7 @@ $.oc.foundation.element.addClass(link, 'loading-indicator-container size-small') this.showLoadingIndicator() - data['inspectorProperty'] = this.propertyDefinition.property + data['inspectorProperty'] = this.getPropertyPath() data['inspectorClassName'] = this.inspector.options.inspectorClass $form.request('onInspectableGetOptions', { @@ -309,6 +309,10 @@ } SetEditor.prototype.setPropertyValue = function(checkboxValue, isChecked) { + // In this method the Set Editor mimics the Surface. + // It acts as a parent surface for the children checkboxes, + // watching changes in them and updating the link text. + var currentValue = this.getNormalizedValue() if (currentValue === undefined) { @@ -319,8 +323,10 @@ currentValue = [] } - var resultValue = [] - for (var itemValue in this.propertyDefinition.items) { + var resultValue = [], + items = this.getItemsSource() + + for (var itemValue in items) { if (itemValue !== checkboxValue) { if (currentValue.indexOf(itemValue) !== -1) { resultValue.push(itemValue) diff --git a/modules/system/assets/ui/js/inspector.editor.stringlistautocomplete.js b/modules/system/assets/ui/js/inspector.editor.stringlistautocomplete.js new file mode 100644 index 000000000..a48326b61 --- /dev/null +++ b/modules/system/assets/ui/js/inspector.editor.stringlistautocomplete.js @@ -0,0 +1,549 @@ +/* + * Inspector string list with autocompletion editor class. + * + * TODO: validation is not implemented in this editor. See the Dictionary editor for reference. + */ ++function ($) { "use strict"; + + var Base = $.oc.inspector.propertyEditors.popupBase, + BaseProto = Base.prototype + + var StringListAutocomplete = function(inspector, propertyDefinition, containerCell, group) { + this.items = null + + Base.call(this, inspector, propertyDefinition, containerCell, group) + } + + StringListAutocomplete.prototype = Object.create(BaseProto) + StringListAutocomplete.prototype.constructor = Base + + StringListAutocomplete.prototype.dispose = function() { + BaseProto.dispose.call(this) + } + + StringListAutocomplete.prototype.init = function() { + BaseProto.init.call(this) + } + + StringListAutocomplete.prototype.supportsExternalParameterEditor = function() { + return false + } + + StringListAutocomplete.prototype.setLinkText = function(link, value) { + var value = value !== undefined ? value + : this.inspector.getPropertyValue(this.propertyDefinition.property) + + if (value === undefined) { + value = this.propertyDefinition.default + } + + this.checkValueType(value) + + if (!value) { + value = this.propertyDefinition.placeholder + $.oc.foundation.element.addClass(link, 'placeholder') + + if (!value) { + value = '[]' + } + + link.textContent = value + } + else { + $.oc.foundation.element.removeClass(link, 'placeholder') + + link.textContent = '[' + value.join(', ') + ']' + } + } + + StringListAutocomplete.prototype.checkValueType = function(value) { + if (value && Object.prototype.toString.call(value) !== '[object Array]') { + this.throwError('The string list value should be an array.') + } + } + + // + // Popup editor methods + // + + StringListAutocomplete.prototype.getPopupContent = function() { + return '
' + } + + StringListAutocomplete.prototype.configurePopup = function(popup) { + this.initAutocomplete() + + this.buildItemsTable(popup.get(0)) + + this.focusFirstInput() + } + + StringListAutocomplete.prototype.handleSubmit = function($form) { + return this.applyValues() + } + + // + // Building and row management + // + + StringListAutocomplete.prototype.buildItemsTable = function(popup) { + var table = popup.querySelector('table.inspector-dictionary-table'), + tbody = document.createElement('tbody'), + items = this.inspector.getPropertyValue(this.propertyDefinition.property) + + if (items === undefined) { + items = this.propertyDefinition.default + } + + if (items === undefined || this.getValueKeys(items).length === 0) { + var row = this.buildEmptyRow() + + tbody.appendChild(row) + } + else { + for (var key in items) { + var row = this.buildTableRow(items[key]) + + tbody.appendChild(row) + } + } + + table.appendChild(tbody) + this.updateScrollpads() + } + + StringListAutocomplete.prototype.buildTableRow = function(value) { + var row = document.createElement('tr'), + valueCell = document.createElement('td') + + this.createInput(valueCell, value) + + row.appendChild(valueCell) + + return row + } + + StringListAutocomplete.prototype.buildEmptyRow = function() { + return this.buildTableRow(null) + } + + StringListAutocomplete.prototype.createInput = function(container, value) { + var input = document.createElement('input'), + controlContainer = document.createElement('div') + + input.setAttribute('type', 'text') + input.setAttribute('class', 'form-control') + input.value = value + + controlContainer.appendChild(input) + container.appendChild(controlContainer) + } + + StringListAutocomplete.prototype.setActiveCell = function(input) { + var activeCells = this.popup.querySelectorAll('td.active') + + for (var i = activeCells.length-1; i >= 0; i--) { + $.oc.foundation.element.removeClass(activeCells[i], 'active') + } + + var activeCell = input.parentNode.parentNode // input / div / td + $.oc.foundation.element.addClass(activeCell, 'active') + + this.buildAutoComplete(input) + } + + StringListAutocomplete.prototype.createItem = function() { + var activeRow = this.getActiveRow(), + newRow = this.buildEmptyRow(), + tbody = this.getTableBody(), + nextSibling = activeRow ? activeRow.nextElementSibling : null + + tbody.insertBefore(newRow, nextSibling) + + this.focusAndMakeActive(newRow.querySelector('input')) + this.updateScrollpads() + } + + StringListAutocomplete.prototype.deleteItem = function() { + var activeRow = this.getActiveRow(), + tbody = this.getTableBody() + + if (!activeRow) { + return + } + + var nextRow = activeRow.nextElementSibling, + prevRow = activeRow.previousElementSibling, + input = this.getRowInputByIndex(activeRow, 0) + + if (input) { + this.removeAutocomplete(input) + } + + tbody.removeChild(activeRow) + + var newSelectedRow = nextRow ? nextRow : prevRow + + if (!newSelectedRow) { + newSelectedRow = this.buildEmptyRow() + tbody.appendChild(newSelectedRow) + } + + this.focusAndMakeActive(newSelectedRow.querySelector('input')) + this.updateScrollpads() + } + + StringListAutocomplete.prototype.applyValues = function() { + var tbody = this.getTableBody(), + dataRows = tbody.querySelectorAll('tr'), + link = this.getLink(), + result = [] + + for (var i = 0, len = dataRows.length; i < len; i++) { + var dataRow = dataRows[i], + valueInput = this.getRowInputByIndex(dataRow, 0), + value = $.trim(valueInput.value) + + if (value.length == 0) { + continue + } + + result.push(value) + } + + this.inspector.setPropertyValue(this.propertyDefinition.property, result) + this.setLinkText(link, result) + } + + // + // Helpers + // + + StringListAutocomplete.prototype.getValueKeys = function(value) { + var result = [] + + for (var key in value) { + result.push(key) + } + + return result + } + + StringListAutocomplete.prototype.getActiveRow = function() { + var activeCell = this.popup.querySelector('td.active') + + if (!activeCell) { + return null + } + + return activeCell.parentNode + } + + StringListAutocomplete.prototype.getTableBody = function() { + return this.popup.querySelector('table.inspector-dictionary-table tbody') + } + + StringListAutocomplete.prototype.updateScrollpads = function() { + $('.control-scrollpad', this.popup).scrollpad('update') + } + + StringListAutocomplete.prototype.focusFirstInput = function() { + var input = this.popup.querySelector('td input') + + if (input) { + input.focus() + this.setActiveCell(input) + } + } + + StringListAutocomplete.prototype.getEditorCell = function(cell) { + return cell.parentNode.parentNode // cell / div / td + } + + StringListAutocomplete.prototype.getEditorRow = function(cell) { + return cell.parentNode.parentNode.parentNode // cell / div / td / tr + } + + StringListAutocomplete.prototype.focusAndMakeActive = function(input) { + input.focus() + this.setActiveCell(input) + } + + StringListAutocomplete.prototype.getRowInputByIndex = function(row, index) { + return row.cells[index].querySelector('input') + } + + // + // Navigation + // + + StringListAutocomplete.prototype.navigateDown = function(ev) { + var cell = this.getEditorCell(ev.currentTarget), + row = this.getEditorRow(ev.currentTarget), + nextRow = row.nextElementSibling + + if (!nextRow) { + return + } + + var newActiveEditor = nextRow.cells[cell.cellIndex].querySelector('input') + + this.focusAndMakeActive(newActiveEditor) + } + + StringListAutocomplete.prototype.navigateUp = function(ev) { + var cell = this.getEditorCell(ev.currentTarget), + row = this.getEditorRow(ev.currentTarget), + prevRow = row.previousElementSibling + + if (!prevRow) { + return + } + + var newActiveEditor = prevRow.cells[cell.cellIndex].querySelector('input') + + this.focusAndMakeActive(newActiveEditor) + } + + // + // Autocomplete + // + + StringListAutocomplete.prototype.initAutocomplete = function() { + if (this.propertyDefinition.items !== undefined) { + this.items = this.prepareItems(this.propertyDefinition.items) + this.initializeAutocompleteForCurrentInput() + } + else { + this.loadDynamicItems() + } + } + + StringListAutocomplete.prototype.initializeAutocompleteForCurrentInput = function() { + var activeElement = document.activeElement + + if (!activeElement) { + return + } + + var inputs = this.popup.querySelectorAll('td input.form-control') + + if (!inputs) { + return + } + + for (var i=inputs.length-1; i>=0; i--) { + if (inputs[i] === activeElement) { + this.buildAutoComplete(inputs[i]) + return + } + } + } + + StringListAutocomplete.prototype.buildAutoComplete = function(input) { + if (this.items === null) { + return + } + + $(input).autocomplete({ + source: this.items, + matchWidth: true, + menu: '', + bodyContainer: true + }) + } + + StringListAutocomplete.prototype.removeAutocomplete = function(input) { + var $input = $(input) + + if (!$input.data('autocomplete')) { + return + } + + $input.autocomplete('destroy') + } + + StringListAutocomplete.prototype.prepareItems = function(items) { + var result = {} + + if ($.isArray(items)) { + for (var i = 0, len = items.length; i < len; i++) { + result[items[i]] = items[i] + } + } + else { + result = items + } + + return result + } + + StringListAutocomplete.prototype.loadDynamicItems = function() { + if (this.isDisposed()) { + return + } + + var data = this.getRootSurface().getValues(), + $form = $(this.popup).find('form') + + if (this.triggerGetItems(data) === false) { + return + } + + data['inspectorProperty'] = this.getPropertyPath() + data['inspectorClassName'] = this.inspector.options.inspectorClass + + $form.request('onInspectableGetOptions', { + data: data, + }) + .done(this.proxy(this.itemsRequestDone)) + } + + StringListAutocomplete.prototype.triggerGetItems = function(values) { + var $inspectable = this.getInspectableElement() + if (!$inspectable) { + return true + } + + var itemsEvent = $.Event('autocompleteitems.oc.inspector') + + $inspectable.trigger(itemsEvent, [{ + values: values, + callback: this.proxy(this.itemsRequestDone), + property: this.inspector.getPropertyPath(this.propertyDefinition.property), + propertyDefinition: this.propertyDefinition + }]) + + if (itemsEvent.isDefaultPrevented()) { + return false + } + + return true + } + + StringListAutocomplete.prototype.itemsRequestDone = function(data) { + if (this.isDisposed()) { + // Handle the case when the asynchronous request finishes after + // the editor is disposed + return + } + + var loadedItems = {} + + if (data.options) { + for (var i = data.options.length-1; i >= 0; i--) { + loadedItems[data.options[i].value] = data.options[i].title + } + } + + this.items = this.prepareItems(loadedItems) + this.initializeAutocompleteForCurrentInput() + } + + StringListAutocomplete.prototype.removeAutocompleteFromAllRows = function() { + var inputs = this.popup.querySelector('td input.form-control') + + for (var i=inputs.length-1; i>=0; i--) { + this.removeAutocomplete(inputs[i]) + } + } + + // + // Event handlers + // + + StringListAutocomplete.prototype.onPopupShown = function(ev, link, popup) { + BaseProto.onPopupShown.call(this,ev, link, popup) + + popup.on('focus.inspector', 'td input', this.proxy(this.onFocus)) + popup.on('blur.inspector', 'td input', this.proxy(this.onBlur)) + popup.on('keydown.inspector', 'td input', this.proxy(this.onKeyDown)) + popup.on('click.inspector', '[data-cmd]', this.proxy(this.onCommand)) + } + + StringListAutocomplete.prototype.onPopupHidden = function(ev, link, popup) { + popup.off('.inspector', 'td input') + popup.off('.inspector', '[data-cmd]', this.proxy(this.onCommand)) + + this.removeAutocompleteFromAllRows() + this.items = null + + BaseProto.onPopupHidden.call(this, ev, link, popup) + } + + StringListAutocomplete.prototype.onFocus = function(ev) { + this.setActiveCell(ev.currentTarget) + } + + StringListAutocomplete.prototype.onBlur = function(ev) { + if ($(ev.relatedTarget).closest('ul.inspector-autocomplete').length > 0) { + // Do not close the autocomplete results if a drop-down + // menu item was clicked + return + } + + this.removeAutocomplete(ev.currentTarget) + } + + StringListAutocomplete.prototype.onCommand = function(ev) { + var command = ev.currentTarget.getAttribute('data-cmd') + + switch (command) { + case 'create-item' : + this.createItem() + break; + case 'delete-item' : + this.deleteItem() + break; + } + } + + StringListAutocomplete.prototype.onKeyDown = function(ev) { + if (ev.keyCode == 40) { + return this.navigateDown(ev) + } + else if (ev.keyCode == 38) { + return this.navigateUp(ev) + } + } + + $.oc.inspector.propertyEditors.stringListAutocomplete = StringListAutocomplete +}(window.jQuery); \ No newline at end of file diff --git a/modules/system/assets/ui/js/inspector.externalparametereditor.js b/modules/system/assets/ui/js/inspector.externalparametereditor.js index d373a9dcb..bf4e4dfde 100644 --- a/modules/system/assets/ui/js/inspector.externalparametereditor.js +++ b/modules/system/assets/ui/js/inspector.externalparametereditor.js @@ -23,10 +23,11 @@ var Base = $.oc.foundation.base, BaseProto = Base.prototype - var ExternalParameterEditor = function(inspector, propertyDefinition, containerCell) { + var ExternalParameterEditor = function(inspector, propertyDefinition, containerCell, initialValue) { this.inspector = inspector this.propertyDefinition = propertyDefinition this.containerCell = containerCell + this.initialValue = initialValue Base.call(this) @@ -43,6 +44,7 @@ this.inspector = null this.propertyDefinition = null this.containerCell = null + this.initialValue = null BaseProto.dispose.call(this) } @@ -108,18 +110,16 @@ } ExternalParameterEditor.prototype.setInitialValue = function() { - var propertyValue = this.inspector.getPropertyValue(this.propertyDefinition.property) - - if (!propertyValue) { + if (!this.initialValue) { return } - if (typeof propertyValue !== 'string') { + if (typeof this.initialValue !== 'string') { return } var matches = [] - if (matches = propertyValue.match(/^\{\{([^\}]+)\}\}$/)) { + if (matches = this.initialValue.match(/^\{\{([^\}]+)\}\}$/)) { var value = $.trim(matches[1]) if (value.length > 0) { @@ -209,15 +209,21 @@ ExternalParameterEditor.prototype.toggleEditorVisibility = function(show) { var container = this.getContainer(), children = container.children, - height = 0 + height = 19 + + // Fixed value instead of trying to get the container cell height. + // If the editor is contained in initially hidden editor (collapsed group), + // the container cell will be unknown. 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++) { diff --git a/modules/system/assets/ui/js/inspector.surface.js b/modules/system/assets/ui/js/inspector.surface.js index 1c0085bcb..663268a70 100644 --- a/modules/system/assets/ui/js/inspector.surface.js +++ b/modules/system/assets/ui/js/inspector.surface.js @@ -234,6 +234,7 @@ // if (property.property) { row.setAttribute('data-property', property.property) + row.setAttribute('data-property-path', this.getPropertyPath(property.property)) } this.applyGroupIndexAttribute(property, row, group) @@ -363,7 +364,13 @@ var cell = row.querySelector('td'), propertyDefinition = this.findPropertyDefinition(property), - editor = new $.oc.inspector.externalParameterEditor(this, propertyDefinition, cell) + initialValue = this.getPropertyValue(property) + + if (initialValue === undefined) { + initialValue = propertyEditor.getUndefinedValue() + } + + var editor = new $.oc.inspector.externalParameterEditor(this, propertyDefinition, cell, initialValue) this.externalParameterEditors.push(editor) } @@ -536,7 +543,8 @@ this.markPropertyChanged(property, false) } - this.notifyEditorsPropertyChanged(property, value) + var propertyPath = this.getPropertyPath(property) + this.getRootSurface().notifyEditorsPropertyChanged(propertyPath, value) if (this.options.onChange !== null) { this.options.onChange(property, value) @@ -553,11 +561,19 @@ return value } - Surface.prototype.notifyEditorsPropertyChanged = function(property, value) { + Surface.prototype.notifyEditorsPropertyChanged = function(propertyPath, value) { + // Editors use this event to watch changes in properties + // they depend on. All editors should be notified, including + // editors in nested surfaces. The property name is passed as a + // path object.property (if the property is nested), so that + // property depenencies could be defined as + // ['property', 'object.property'] + for (var i = 0, len = this.editors.length; i < len; i++) { var editor = this.editors[i] - editor.onInspectorPropertyChanged(property, value) + editor.onInspectorPropertyChanged(propertyPath, value) + editor.notifyChildSurfacesPropertyChanged(propertyPath, value) } } @@ -573,7 +589,8 @@ } Surface.prototype.markPropertyChanged = function(property, changed) { - var row = this.tableContainer.querySelector('tr[data-property="'+property+'"]') + var propertyPath = this.getPropertyPath(property), + row = this.tableContainer.querySelector('tr[data-property-path="'+propertyPath+'"]') if (changed) { $.oc.foundation.element.addClass(row, 'changed') diff --git a/modules/system/assets/ui/less/inspector.less b/modules/system/assets/ui/less/inspector.less index 63c3a63a6..3dc39be97 100644 --- a/modules/system/assets/ui/less/inspector.less +++ b/modules/system/assets/ui/less/inspector.less @@ -22,6 +22,18 @@ // Inspector // -------------------------------------------------- +.inspector-autocomplete-list() { + background: white; + font-size: 12px; + z-index: 100000; // It's safe to set z-index any high value for drop-downs + + li a { + padding: 5px 12px; + white-space: normal; + word-wrap: break-word; + } +} + .inspector-fields { min-width: 220px; border-collapse: collapse; @@ -131,12 +143,7 @@ } ul.dropdown-menu { - background: white; - font-size: 12px; - - li a { - padding: 5px 12px; - } + .inspector-autocomplete-list(); } .loading-indicator { @@ -578,7 +585,8 @@ div.inspector-dictionary-container { } } - span { + span, a { + text-decoration: none; position: absolute; top: 12px; float: none; @@ -618,6 +626,10 @@ div.inspector-dictionary-container { } } +ul.autocomplete.dropdown-menu.inspector-autocomplete { + .inspector-autocomplete-list(); +} + .select2-dropdown { &.ocInspectorDropdown { font-size: 12px; diff --git a/modules/system/assets/ui/less/popover.less b/modules/system/assets/ui/less/popover.less index e29b81e51..30cadadd1 100644 --- a/modules/system/assets/ui/less/popover.less +++ b/modules/system/assets/ui/less/popover.less @@ -181,6 +181,7 @@ div.control-popover { float: none; color: #ffffff; cursor: pointer; + text-decoration: none; &:hover { .opacity(1); diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js index fc799fe09..65fbea75f 100644 --- a/modules/system/assets/ui/storm-min.js +++ b/modules/system/assets/ui/storm-min.js @@ -2889,7 +2889,7 @@ throw new Error('Trigger action is not specified.') this.triggerCondition=this.options.triggerCondition if(this.options.triggerCondition.indexOf('value')==0){var match=this.options.triggerCondition.match(/[^[\]]+(?=])/g) this.triggerCondition='value' -this.triggerConditionValue=(match)?match:""} +this.triggerConditionValue=(match)?match:[""]} this.triggerParent=this.options.triggerClosestParent!==undefined?$el.closest(this.options.triggerClosestParent):undefined if(this.triggerCondition=='checked'||this.triggerCondition=='unchecked'||this.triggerCondition=='value'){$(document).on('change',this.options.trigger,$.proxy(this.onConditionChanged,this))} var self=this @@ -3413,7 +3413,8 @@ if(!this.parentSurface){this.focusFirstEditor()}} Surface.prototype.moveToContainer=function(newContainer){this.container=newContainer this.container.appendChild(this.tableContainer)} Surface.prototype.buildRow=function(property,group){var row=document.createElement('tr'),th=document.createElement('th'),titleSpan=document.createElement('span'),description=this.buildPropertyDescription(property) -if(property.property){row.setAttribute('data-property',property.property)} +if(property.property){row.setAttribute('data-property',property.property) +row.setAttribute('data-property-path',this.getPropertyPath(property.property))} this.applyGroupIndexAttribute(property,row,group) $.oc.foundation.element.addClass(row,this.getRowCssClass(property,group)) this.applyHeadColspan(th,property) @@ -3459,7 +3460,9 @@ for(var i=0,len=rows.length;i