Added autocomplete column type to the Table widget

This commit is contained in:
alekseybobkov 2015-11-17 21:26:03 -08:00
parent e7a51a75f4
commit 0e2e18af7f
10 changed files with 357 additions and 20 deletions

View File

@ -1260,9 +1260,12 @@ this.shown=false
this.listen()}
Autocomplete.prototype={constructor:Autocomplete,select:function(){var val=this.$menu.find('.active').attr('data-value')
this.$element.val(this.updater(val)).change()
return this.hide()},updater:function(item){return item},show:function(){var pos=$.extend({},this.$element.position(),{height:this.$element[0].offsetHeight}),cssOptions={top:pos.top+pos.height,left:pos.left}
return this.hide()},updater:function(item){return item},show:function(){var offset=this.options.bodyContainer?this.$element.offset():this.$element.position(),pos=$.extend({},offset,{height:this.$element[0].offsetHeight}),cssOptions={top:pos.top+pos.height,left:pos.left}
if(this.options.matchWidth){cssOptions.width=this.$element[0].offsetWidth}
this.$menu.insertAfter(this.$element).css(cssOptions).show()
this.$menu.css(cssOptions)
if(this.options.bodyContainer){$(document.body).append(this.$menu)}
else{this.$menu.insertAfter(this.$element)}
this.$menu.show()
this.shown=true
return this},hide:function(){this.$menu.hide()
this.shown=false
@ -1339,7 +1342,7 @@ var old=$.fn.autocomplete
$.fn.autocomplete=function(option){return this.each(function(){var $this=$(this),data=$this.data('autocomplete'),options=typeof option=='object'&&option
if(!data)$this.data('autocomplete',(data=new Autocomplete(this,options)))
if(typeof option=='string')data[option]()})}
$.fn.autocomplete.defaults={source:[],items:8,menu:'<ul class="autocomplete dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1}
$.fn.autocomplete.defaults={source:[],items:8,menu:'<ul class="autocomplete dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1,bodyContainer:false}
$.fn.autocomplete.Constructor=Autocomplete
$.fn.autocomplete.noConflict=function(){$.fn.autocomplete=old
return this}

View File

@ -51,7 +51,8 @@
},
show: function () {
var pos = $.extend({}, this.$element.position(), {
var offset = this.options.bodyContainer ? this.$element.offset() : this.$element.position(),
pos = $.extend({}, offset, {
height: this.$element[0].offsetHeight
}),
cssOptions = {
@ -63,10 +64,16 @@
cssOptions.width = this.$element[0].offsetWidth
}
this.$menu
.insertAfter(this.$element)
.css(cssOptions)
.show()
this.$menu.css(cssOptions)
if (this.options.bodyContainer) {
$(document.body).append(this.$menu)
}
else {
this.$menu.insertAfter(this.$element)
}
this.$menu.show()
this.shown = true
return this
@ -347,7 +354,8 @@
items: 8,
menu: '<ul class="autocomplete dropdown-menu"></ul>',
item: '<li><a href="#"></a></li>',
minLength: 1
minLength: 1,
bodyContainer: false
}
$.fn.autocomplete.Constructor = Autocomplete

View File

@ -197,4 +197,21 @@ class Table extends WidgetBase
'options' => $options
];
}
public function onGetAutocompleteOptions()
{
$columnName = Input::get('column');
$rowData = Input::get('rowData');
$eventResults = $this->fireEvent('table.getAutocompleteOptions', [$columnName, $rowData]);
$options = [];
if (count($eventResults)) {
$options = $eventResults[0];
}
return [
'options' => $options
];
}
}

View File

@ -161,6 +161,24 @@ Multiple fields are allowed as well:
**Note:** Dependent drop-down should always be defined after their master columns.
### Autocomplete cell processor
The autocomplete column type can load options from the column configuration or with AJAX. Example column configuration:
color:
title: Color
type: autocomplete
options:
red: Red
green: Green
blue: Blue
If the `options` element is not presented in the configuration, the options will be loaded with AJAX.
**TODO:** Document the AJAX interface
The editor can have the `dependsOn` property similar to the drop-down editor.
# Server-side table widget (Backend\Widgets\Table)
## Configuration

View File

@ -72,16 +72,16 @@
padding: 1px;
}
.control-table table.data td.active {
border-color: #5fb6f5 !important;
border-color: #4da7e8 !important;
}
.control-table table.data td.active .content-container {
padding: 0;
border: 1px solid #5fb6f5;
border: 1px solid #4da7e8;
}
.control-table table.data td.active .content-container:before,
.control-table table.data td.active .content-container:after {
content: ' ';
background: #5fb6f5;
background: #4da7e8;
position: absolute;
left: -2px;
top: -2px;
@ -189,7 +189,7 @@
color: #95a5a6;
}
.control-table .pagination ul li.active {
background: #5fb6f5;
background: #4da7e8;
}
.control-table .pagination ul li.active a {
color: #ffffff;
@ -207,9 +207,10 @@
}
}
/*
* String editor
* String and autocomplete editors
*/
.control-table td[data-column-type=string] input[type=text] {
.control-table td[data-column-type=string] input[type=text],
.control-table td[data-column-type=autocomplete] input[type=text] {
width: 100%;
height: 100%;
display: block;
@ -217,6 +218,18 @@
border: none;
padding: 5px 10px;
}
ul.table-widget-autocomplete {
background: white;
font-size: 13px;
margin-top: 0;
border: 1px solid #808c8d;
border-top: 1px solid #ecf0f1;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
ul.table-widget-autocomplete li a {
padding: 5px 10px;
}
/*
* Checkbox editor
*/
@ -254,7 +267,7 @@
top: -4px;
}
.control-table td[data-column-type=checkbox] div[data-checkbox-element]:focus {
border-color: #5fb6f5;
border-color: #4da7e8;
outline: none;
}
/*
@ -366,6 +379,6 @@ html.cssanimations .control-table td[data-column-type=dropdown] [data-view-conta
.table-control-dropdown-list li:hover,
.table-control-dropdown-list li:focus,
.table-control-dropdown-list li.selected {
background: #5fb6f5;
background: #4da7e8;
color: white;
}

View File

@ -863,6 +863,54 @@ DropdownProcessor.prototype.elementBelongsToProcessor=function(element){if(!this
return false
return this.tableObj.parentContainsElement(this.itemListElement,element)}
$.oc.table.processor.dropdown=DropdownProcessor;}(window.jQuery);+function($){"use strict";if($.oc.table===undefined)
throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded.");if($.oc.table.processor===undefined)
throw new Error("The $.oc.table.processor namespace is not defined. Make sure that the table.processor.base.js script is loaded.");var Base=$.oc.table.processor.string,BaseProto=Base.prototype
var AutocompleteProcessor=function(tableObj,columnName,columnConfiguration){this.cachedOptionPromises={}
Base.call(this,tableObj,columnName,columnConfiguration)}
AutocompleteProcessor.prototype=Object.create(BaseProto)
AutocompleteProcessor.prototype.constructor=AutocompleteProcessor
AutocompleteProcessor.prototype.dispose=function(){this.cachedOptionPromises=null
BaseProto.dispose.call(this)}
AutocompleteProcessor.prototype.onUnfocus=function(){if(!this.activeCell)
return
this.removeAutocomplete()
BaseProto.onUnfocus.call(this)}
AutocompleteProcessor.prototype.renderCell=function(value,cellContentContainer){BaseProto.renderCell.call(this,value,cellContentContainer)}
AutocompleteProcessor.prototype.buildEditor=function(cellElement,cellContentContainer,isClick){BaseProto.buildEditor.call(this,cellElement,cellContentContainer,isClick)
var self=this
this.fetchOptions(cellElement,function autocompleteFetchOptions(options){self.buildAutoComplete(options)
self=null})}
AutocompleteProcessor.prototype.fetchOptions=function(cellElement,onSuccess){if(this.columnConfiguration.options){if(onSuccess!==undefined){onSuccess(this.columnConfiguration.options)}}else{if(this.triggerGetOptions(onSuccess)===false){return}
var row=cellElement.parentNode,cachingKey=this.createOptionsCachingKey(row),viewContainer=this.getViewContainer(cellElement)
$.oc.foundation.element.addClass(viewContainer,'loading')
if(!this.cachedOptionPromises[cachingKey]){var requestData={column:this.columnName,rowData:this.tableObj.getRowData(row)},handlerName=this.tableObj.getAlias()+'::onGetAutocompleteOptions'
this.cachedOptionPromises[cachingKey]=this.tableObj.$el.request(handlerName,{data:requestData})}
this.cachedOptionPromises[cachingKey].done(function onAutocompleteLoadOptionsSuccess(data){if(onSuccess!==undefined){onSuccess(data.options)}}).always(function onAutocompleteLoadOptionsAlways(){$.oc.foundation.element.removeClass(viewContainer,'loading')})}}
AutocompleteProcessor.prototype.createOptionsCachingKey=function(row){var cachingKey='non-dependent',dependsOn=this.columnConfiguration.dependsOn
if(dependsOn){if(typeof dependsOn=='object'){for(var i=0,len=dependsOn.length;i<len;i++)
cachingKey+=dependsOn[i]+this.tableObj.getRowCellValueByColumnName(row,dependsOn[i])}else
cachingKey=dependsOn+this.tableObj.getRowCellValueByColumnName(row,dependsOn)}
return cachingKey}
AutocompleteProcessor.prototype.triggerGetOptions=function(callback){var tableElement=this.tableObj.getElement()
if(!tableElement){return}
var optionsEvent=$.Event('autocompleteitems.oc.table'),values={}
$(tableElement).trigger(optionsEvent,[{values:values,callback:callback,column:this.columnName,columnConfiguration:this.columnConfiguration}])
if(optionsEvent.isDefaultPrevented()){return false}
return true}
AutocompleteProcessor.prototype.getInput=function(){if(!this.activeCell){return null}
return this.activeCell.querySelector('.string-input')}
AutocompleteProcessor.prototype.buildAutoComplete=function(items){if(!this.activeCell){return}
var input=this.getInput()
if(!input){return}
if(items===undefined){items=[]}
$(input).autocomplete({source:this.prepareItems(items),matchWidth:true,menu:'<ul class="autocomplete dropdown-menu table-widget-autocomplete"></ul>',bodyContainer:true})}
AutocompleteProcessor.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}
AutocompleteProcessor.prototype.removeAutocomplete=function(){var input=this.getInput()
$(input).autocomplete('destroy')}
$.oc.table.processor.autocomplete=AutocompleteProcessor;}(window.jQuery);+function($){"use strict";if($.oc.table===undefined)
throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded.");if($.oc.table.validator===undefined)
$.oc.table.validator={}
var Base=function(options){this.options=options}

View File

@ -15,6 +15,7 @@
=require table.processor.string.js
=require table.processor.checkbox.js
=require table.processor.dropdown.js
=require table.processor.autocomplete.js
=require table.validator.base.js
=require table.validator.required.js
=require table.validator.basenumber.js

View File

@ -0,0 +1,215 @@
/*
* Autocomplete cell processor for the table control.
*/
+function ($) { "use strict";
// NAMESPACE CHECK
// ============================
if ($.oc.table === undefined)
throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded.");
if ($.oc.table.processor === undefined)
throw new Error("The $.oc.table.processor namespace is not defined. Make sure that the table.processor.base.js script is loaded.");
// CLASS DEFINITION
// ============================
var Base = $.oc.table.processor.string,
BaseProto = Base.prototype
var AutocompleteProcessor = function(tableObj, columnName, columnConfiguration) {
//
// State properties
//
this.cachedOptionPromises = {}
//
// Parent constructor
//
Base.call(this, tableObj, columnName, columnConfiguration)
}
AutocompleteProcessor.prototype = Object.create(BaseProto)
AutocompleteProcessor.prototype.constructor = AutocompleteProcessor
AutocompleteProcessor.prototype.dispose = function() {
this.cachedOptionPromises = null
BaseProto.dispose.call(this)
}
/*
* Forces the processor to hide the editor when the user navigates
* away from the cell. Processors can update the sell value in this method.
* Processors must clear the reference to the active cell in this method.
*/
AutocompleteProcessor.prototype.onUnfocus = function() {
if (!this.activeCell)
return
this.removeAutocomplete()
BaseProto.onUnfocus.call(this)
}
/*
* Renders the cell in the normal (no edit) mode
*/
AutocompleteProcessor.prototype.renderCell = function(value, cellContentContainer) {
BaseProto.renderCell.call(this, value, cellContentContainer)
// this.fetchOptions(cellContentContainer.parentNode)
}
AutocompleteProcessor.prototype.buildEditor = function(cellElement, cellContentContainer, isClick) {
BaseProto.buildEditor.call(this, cellElement, cellContentContainer, isClick)
var self = this
this.fetchOptions(cellElement, function autocompleteFetchOptions(options) {
self.buildAutoComplete(options)
self = null
})
}
AutocompleteProcessor.prototype.fetchOptions = function(cellElement, onSuccess) {
if (this.columnConfiguration.options) {
if (onSuccess !== undefined) {
onSuccess(this.columnConfiguration.options)
}
} else {
// If options are not provided and not found in the cache,
// request them from the server. For dependent autocomplete editors
// the caching key contains the master column values.
if (this.triggerGetOptions(onSuccess) === false) {
return
}
var row = cellElement.parentNode,
cachingKey = this.createOptionsCachingKey(row),
viewContainer = this.getViewContainer(cellElement)
// Request options from the server. When the table widget builds,
// multiple cells in the column could require loading the options.
// The AJAX promises are cached here so that we have a single
// request per caching key.
$.oc.foundation.element.addClass(viewContainer, 'loading')
if (!this.cachedOptionPromises[cachingKey]) {
var requestData = {
column: this.columnName,
rowData: this.tableObj.getRowData(row)
},
handlerName = this.tableObj.getAlias()+'::onGetAutocompleteOptions'
this.cachedOptionPromises[cachingKey] = this.tableObj.$el.request(handlerName, {data: requestData})
}
this.cachedOptionPromises[cachingKey].done(function onAutocompleteLoadOptionsSuccess(data){
if (onSuccess !== undefined) {
onSuccess(data.options)
}
}).always(function onAutocompleteLoadOptionsAlways(){
$.oc.foundation.element.removeClass(viewContainer, 'loading')
})
}
}
AutocompleteProcessor.prototype.createOptionsCachingKey = function(row) {
var cachingKey = 'non-dependent',
dependsOn = this.columnConfiguration.dependsOn
if (dependsOn) {
if (typeof dependsOn == 'object') {
for (var i = 0, len = dependsOn.length; i < len; i++ )
cachingKey += dependsOn[i] + this.tableObj.getRowCellValueByColumnName(row, dependsOn[i])
} else
cachingKey = dependsOn + this.tableObj.getRowCellValueByColumnName(row, dependsOn)
}
return cachingKey
}
AutocompleteProcessor.prototype.triggerGetOptions = function(callback) {
var tableElement = this.tableObj.getElement()
if (!tableElement) {
return
}
var optionsEvent = $.Event('autocompleteitems.oc.table'),
values = {} // TODO - implement loading values from the current row.
$(tableElement).trigger(optionsEvent, [{
values: values,
callback: callback,
column: this.columnName,
columnConfiguration: this.columnConfiguration
}])
if (optionsEvent.isDefaultPrevented()) {
return false
}
return true
}
AutocompleteProcessor.prototype.getInput = function() {
if (!this.activeCell) {
return null
}
return this.activeCell.querySelector('.string-input')
}
AutocompleteProcessor.prototype.buildAutoComplete = function(items) {
if (!this.activeCell) {
return
}
var input = this.getInput()
if (!input) {
return
}
if (items === undefined) {
items = []
}
$(input).autocomplete({
source: this.prepareItems(items),
matchWidth: true,
menu: '<ul class="autocomplete dropdown-menu table-widget-autocomplete"></ul>',
bodyContainer: true
})
}
AutocompleteProcessor.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
}
AutocompleteProcessor.prototype.removeAutocomplete = function() {
var input = this.getInput()
$(input).autocomplete('destroy')
}
$.oc.table.processor.autocomplete = AutocompleteProcessor;
}(window.jQuery);

View File

@ -212,7 +212,7 @@
if (!this.cachedOptionPromises[cachingKey]) {
var requestData = {
column: this.columnName,
column: this.columnName,
rowData: this.tableObj.getRowData(row)
},
handlerName = this.tableObj.getAlias()+'::onGetDropdownOptions'

View File

@ -265,11 +265,12 @@
}
/*
* String editor
* String and autocomplete editors
*/
.control-table {
td[data-column-type=string] {
td[data-column-type=string],
td[data-column-type=autocomplete] {
input[type=text] {
width: 100%;
height: 100%;
@ -281,6 +282,19 @@
}
}
ul.table-widget-autocomplete {
background: white;
font-size: 13px;
margin-top: 0;
border: 1px solid #808c8d;
border-top: 1px solid #ecf0f1;
.border-bottom-radius(4px);
li a {
padding: 5px 10px;
}
}
/*
* Checkbox editor
*/