diff --git a/modules/system/assets/ui/js/inspector.editor.dictionary.js b/modules/system/assets/ui/js/inspector.editor.dictionary.js index 4e0352897..ed8534cbb 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.stringlistautocomplete.js b/modules/system/assets/ui/js/inspector.editor.stringlistautocomplete.js new file mode 100644 index 000000000..d363d7a3e --- /dev/null +++ b/modules/system/assets/ui/js/inspector.editor.stringlistautocomplete.js @@ -0,0 +1,543 @@ +/* + * 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.propertyDefinition.property + 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) { + 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/less/inspector.less b/modules/system/assets/ui/less/inspector.less index 799edd806..9872f0565 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,14 +143,7 @@ } ul.dropdown-menu { - background: white; - font-size: 12px; - - li a { - padding: 5px 12px; - white-space: normal; - word-wrap: break-word; - } + .inspector-autocomplete-list(); } .loading-indicator { @@ -620,6 +625,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/storm-min.js b/modules/system/assets/ui/storm-min.js index de97156dc..a40760eed 100644 --- a/modules/system/assets/ui/storm-min.js +++ b/modules/system/assets/ui/storm-min.js @@ -4806,6 +4806,190 @@ for(var i=0,len=arrayValue.length;i0){resultValue.push(currentValue)}}} this.inspector.setPropertyValue(this.propertyDefinition.property,resultValue)} $.oc.inspector.propertyEditors.stringList=StringListEditor}(window.jQuery);+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.')}} +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()} +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 +$.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=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=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])}} +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){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);+function($){"use strict";var Base=$.oc.inspector.propertyEditors.popupBase,BaseProto=Base.prototype var DictionaryEditor=function(inspector,propertyDefinition,containerCell,group){this.keyValidationSet=null this.valueValidationSet=null Base.call(this,inspector,propertyDefinition,containerCell,group)} diff --git a/modules/system/assets/ui/storm.css b/modules/system/assets/ui/storm.css index f20bebe58..a90497b63 100644 --- a/modules/system/assets/ui/storm.css +++ b/modules/system/assets/ui/storm.css @@ -2460,7 +2460,7 @@ table.table.data tr.list-tree-level-10 td.list-cell-index-1{padding-left:125px} .inspector-fields td.text.active{background:#ffffff;border-left:1px solid #c8cccd} .inspector-fields td.autocomplete{padding:0} .inspector-fields td.autocomplete .autocomplete-container input[type=text]{padding:5px 12px} -.inspector-fields td.autocomplete .autocomplete-container ul.dropdown-menu{background:white;font-size:12px} +.inspector-fields td.autocomplete .autocomplete-container ul.dropdown-menu{background:white;font-size:12px;z-index:100000} .inspector-fields td.autocomplete .autocomplete-container ul.dropdown-menu li a{padding:5px 12px;white-space:normal;word-wrap:break-word} .inspector-fields td.autocomplete .autocomplete-container .loading-indicator span{margin-top:-12px;right:10px;left:auto} .inspector-fields td.trigger-cell{padding:0 !important} @@ -2538,6 +2538,8 @@ div.inspector-dictionary-container table.inspector-dictionary-table tbody tr:las .inspector-container:empty{display:none} .inspector-container .control-scrollpad{position:absolute} .inspector-field-comment:empty{display:none} +ul.autocomplete.dropdown-menu.inspector-autocomplete{background:white;font-size:12px;z-index:100000} +ul.autocomplete.dropdown-menu.inspector-autocomplete li a{padding:5px 12px;white-space:normal;word-wrap:break-word} .select2-dropdown.ocInspectorDropdown{font-size:12px;-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;border:none !important} .select2-dropdown.ocInspectorDropdown > .select2-results > .select2-results__options{font-size:12px} .select2-dropdown.ocInspectorDropdown > .select2-results > li > div{padding:5px 12px 5px} diff --git a/modules/system/assets/ui/storm.js b/modules/system/assets/ui/storm.js index 8c8ed3866..ad1dc8b12 100644 --- a/modules/system/assets/ui/storm.js +++ b/modules/system/assets/ui/storm.js @@ -68,6 +68,7 @@ =require js/inspector.editor.objectlist.js =require js/inspector.editor.object.js =require js/inspector.editor.stringlist.js +=require js/inspector.editor.stringlistautocomplete.js =require js/inspector.editor.dictionary.js =require js/inspector.editor.autocomplete.js =require js/inspector.helpers.js