562 lines
17 KiB
JavaScript
562 lines
17 KiB
JavaScript
|
webshims.register('form-datalist-lazy', function($, webshims, window, document, undefined, options){
|
||
|
|
||
|
var optionID = 0;
|
||
|
var formsCFG = $.webshims.cfg.forms;
|
||
|
var globStoredOptions = {};
|
||
|
var getStoredOptions = function(name){
|
||
|
if(!name){return [];}
|
||
|
if(globStoredOptions[name]){
|
||
|
return globStoredOptions[name];
|
||
|
}
|
||
|
var data;
|
||
|
try {
|
||
|
data = JSON.parse(localStorage.getItem('storedDatalistOptions'+name));
|
||
|
} catch(e){}
|
||
|
globStoredOptions[name] = data || [];
|
||
|
return data || [];
|
||
|
};
|
||
|
var storeOptions = function(name, val){
|
||
|
if(!name){return;}
|
||
|
val = val || [];
|
||
|
try {
|
||
|
localStorage.setItem( 'storedDatalistOptions'+name, JSON.stringify(val) );
|
||
|
} catch(e){}
|
||
|
};
|
||
|
|
||
|
var lReg = /</g;
|
||
|
var gReg = />/g;
|
||
|
var splitReg = /\s*,\s*/g;
|
||
|
|
||
|
webshims.getDataListVal = function(element){
|
||
|
var datalist = $.data(element, 'datalistWidget');
|
||
|
return (datalist) ? datalist.getPartialValue() : $.prop(element, 'value');
|
||
|
};
|
||
|
|
||
|
$.extend(options.shadowListProto, {
|
||
|
_lazyCreate: function(opts){
|
||
|
var that = this;
|
||
|
this.hideList = $.proxy(that, 'hideList');
|
||
|
|
||
|
this.index = -1;
|
||
|
this.input = opts.input;
|
||
|
this.arrayOptions = [];
|
||
|
|
||
|
this._updateOptions();
|
||
|
|
||
|
this.popover = webshims.objectCreate(webshims.wsPopover, {}, this.options.popover);
|
||
|
this.shadowList = this.popover.element.addClass('datalist-polyfill');
|
||
|
|
||
|
this.shadowList
|
||
|
.on('mouseenter.datalistWidget mousedown.datalistWidget click.datalistWidget', 'li', function(e){
|
||
|
if(that._stopMouseOver && e && e.type == 'mouseenter'){return;}
|
||
|
var items = $('li:not(.hidden-item)', that.shadowList);
|
||
|
var select = (e.type == 'mousedown' || e.type == 'click');
|
||
|
if(select && $(opts.input).getNativeElement().triggerHandler('beforeselect', [$(e.currentTarget).find('.option-value').text()]) === false){
|
||
|
return (e.type != 'mousedown');
|
||
|
}
|
||
|
that.markItem(items.index(e.currentTarget), select, items);
|
||
|
if(e.type == 'click'){
|
||
|
that.hideList();
|
||
|
if(formsCFG.customDatalist){
|
||
|
$(opts.input).getNativeElement().trigger('datalistselect');
|
||
|
}
|
||
|
}
|
||
|
return (e.type != 'mousedown');
|
||
|
})
|
||
|
;
|
||
|
|
||
|
opts.input.setAttribute('autocomplete', 'off');
|
||
|
this.lastCompletedValue = "";
|
||
|
|
||
|
$(opts.input)
|
||
|
.attr({
|
||
|
//role: 'combobox',
|
||
|
'aria-haspopup': 'true',
|
||
|
'aria-autocomplete': 'both' //support only list?
|
||
|
})
|
||
|
.on({
|
||
|
'input.datalistWidget': function(){
|
||
|
if(!that.triggeredByDatalist){
|
||
|
that.changedValue = false;
|
||
|
that.showHideOptions();
|
||
|
}
|
||
|
},
|
||
|
'keydown.datalistWidget': function(e){
|
||
|
var keyCode = e.keyCode;
|
||
|
var activeItem;
|
||
|
var items;
|
||
|
var doValue = !!that.options.inlineValue || 'onlyScroll';
|
||
|
|
||
|
if(keyCode == 40 && !that.showList()){
|
||
|
that.markItem(that.index + 1, doValue);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(!that.popover.isVisible){return;}
|
||
|
|
||
|
|
||
|
if(keyCode == 38){
|
||
|
that.markItem(that.index - 1, doValue);
|
||
|
return false;
|
||
|
}
|
||
|
if(!e.shiftKey && (keyCode == 33 || keyCode == 36)){
|
||
|
that.markItem(0, doValue);
|
||
|
return false;
|
||
|
}
|
||
|
if(!e.shiftKey && (keyCode == 34 || keyCode == 35)){
|
||
|
items = $('li:not(.hidden-item)', that.shadowList);
|
||
|
that.markItem(items.length - 1, true, items);
|
||
|
return false;
|
||
|
}
|
||
|
if(keyCode == 13 || keyCode == 27){
|
||
|
if (keyCode == 13){
|
||
|
activeItem = $('li.active-item:not(.hidden-item)', that.shadowList);
|
||
|
if(that.isCompleted){
|
||
|
$.prop(opts.input, 'selectionStart', $.prop(opts.input, 'value').length);
|
||
|
if(that.lastCompletedValue && !activeItem[0]){
|
||
|
that.lastCompletedValue = "";
|
||
|
that.isCompleted = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if($(opts.input).getNativeElement().triggerHandler('beforeselect', [activeItem.find('.option-value').text()]) === false){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
that.changeValue( activeItem );
|
||
|
}
|
||
|
that.hideList();
|
||
|
if(formsCFG.customDatalist && activeItem && activeItem[0]){
|
||
|
$(opts.input).getNativeElement().trigger('datalistselect');
|
||
|
}
|
||
|
if(e.keyCode != 13 || (activeItem && activeItem[0])){
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
},
|
||
|
'focus.datalistWidget': function(){
|
||
|
that.lastCompletedValue = "";
|
||
|
if(that.options.focus){
|
||
|
that.showList();
|
||
|
}
|
||
|
},
|
||
|
'mousedown.datalistWidget': function(){
|
||
|
if($(this).is(':focus')){
|
||
|
that.showList();
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
;
|
||
|
|
||
|
$(this.datalist)
|
||
|
.off('updateDatalist.datalistWidget')
|
||
|
.on('updateDatalist.datalistWidget', $.proxy(this, '_resetListCached'))
|
||
|
.on('remove', function(e){
|
||
|
if(!e.originalEvent){
|
||
|
that.destroy();
|
||
|
}
|
||
|
})
|
||
|
;
|
||
|
|
||
|
this._resetListCached();
|
||
|
|
||
|
if(opts.input.form && (opts.input.name || opts.input.id)){
|
||
|
$(opts.input.form).on('submit.datalistWidget'+opts.input.id, function(){
|
||
|
if(!$(opts.input).hasClass('no-datalist-cache') && that._autocomplete != 'off'){
|
||
|
var val = $.prop(opts.input, 'value');
|
||
|
var name = (opts.input.name || opts.input.id) + $.prop(opts.input, 'type');
|
||
|
if(!that.storedOptions){
|
||
|
that.storedOptions = getStoredOptions( name );
|
||
|
}
|
||
|
if(val && that.storedOptions.indexOf(val) == -1){
|
||
|
that.storedOptions.push(val);
|
||
|
storeOptions(name, that.storedOptions );
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
$(window).on('unload.datalist'+this.id +' beforeunload.datalist'+this.id, function(e){
|
||
|
that.destroy(e);
|
||
|
});
|
||
|
},
|
||
|
_resetListCached: function(e){
|
||
|
var that = this;
|
||
|
var forceShow;
|
||
|
this.needsUpdate = true;
|
||
|
this.lastUpdatedValue = false;
|
||
|
this.lastUnfoundValue = '';
|
||
|
|
||
|
if(!this.updateTimer){
|
||
|
if(window.QUnit || (forceShow = ($(that.input).is(':focus') && (that.options.focus || $.prop(that.input, 'value'))) )){
|
||
|
that.updateListOptions(forceShow);
|
||
|
} else {
|
||
|
that.updateTimer = setTimeout(function(){
|
||
|
that.updateListOptions();
|
||
|
that = null;
|
||
|
}, 200);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
_updateOptions: function(){
|
||
|
this.options = webshims.getOptions(this.input, 'list', options.list);
|
||
|
|
||
|
if($(this.input).prop('multiple')){
|
||
|
if($(this.input).prop('type') != 'email'){
|
||
|
webshims.warn('multiple only used on email and file type. Use data-list-multiple instead.');
|
||
|
} else {
|
||
|
this.options.multiple = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!this.options.inlineValue && !this.options.valueCompletion){
|
||
|
$.attr(this.input, 'aria-autocomplete', 'list');
|
||
|
$.attr(this.input, 'aria-expanded', 'false');
|
||
|
}
|
||
|
|
||
|
|
||
|
if( this.options.getOptionContent && !$.isFunction(this.options.getOptionContent) ){
|
||
|
this.options.getOptionContent = false;
|
||
|
}
|
||
|
|
||
|
},
|
||
|
updateListOptions: function(_forceShow){
|
||
|
this.needsUpdate = false;
|
||
|
clearTimeout(this.updateTimer);
|
||
|
this.updateTimer = false;
|
||
|
|
||
|
|
||
|
var list = [];
|
||
|
|
||
|
var values = [];
|
||
|
var allOptions = [];
|
||
|
var rElem, rItem, rOptions, rI, rLen, item, value;
|
||
|
for(rOptions = $.prop(this.datalist, 'options'), rI = 0, rLen = rOptions.length; rI < rLen; rI++){
|
||
|
rElem = rOptions[rI];
|
||
|
if(!rElem.disabled && (value = $(rElem).val())){
|
||
|
rItem = {
|
||
|
value: this.options.noHtmlEscape ? value : value.replace(lReg, '<').replace(gReg, '>'),
|
||
|
label: $.trim($.attr(rElem, 'label')) || '',
|
||
|
className: rElem.className || '',
|
||
|
elem: rElem
|
||
|
};
|
||
|
|
||
|
if(rItem.label){
|
||
|
rItem.className += ' has-option-label';
|
||
|
}
|
||
|
values.push(rItem.value);
|
||
|
allOptions.push(rItem);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!this.storedOptions){
|
||
|
this.storedOptions = ($(this.input).hasClass('no-datalist-cache') || this._autocomplete == 'off') ? [] : getStoredOptions((this.input.name || this.input.id) + $.prop(this.input, 'type'));
|
||
|
}
|
||
|
|
||
|
this.storedOptions.forEach(function(val, i){
|
||
|
if(values.indexOf(val) == -1){
|
||
|
allOptions.push({value: val, label: '', className: 'stored-suggest', style: ''});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
for(rI = 0, rLen = allOptions.length; rI < rLen; rI++){
|
||
|
item = allOptions[rI];
|
||
|
list[rI] = '<li class="'+ item.className +'" tabindex="-1" role="listitem" id="wsoption-'+ (optionID++) +'">'+ this.getOptionContent(item) +'</li>';
|
||
|
}
|
||
|
|
||
|
this.arrayOptions = allOptions;
|
||
|
this.popover.contentElement.html('<div class="datalist-box"><ul role="listbox">'+ list.join("\n") +'</ul></div>');
|
||
|
|
||
|
$(this.input).removeAttr('aria-activedescendant').triggerHandler('datalistcreated', [{instance: this}]);
|
||
|
|
||
|
if(_forceShow || this.popover.isVisible){
|
||
|
if(this.options.valueCompletion && this.lastCompletedValue && !$.prop(this.input, 'value').indexOf(this.lastCompletedValue)){
|
||
|
$.prop(this.input, 'value', this.lastCompletedValue);
|
||
|
$(this.input).triggerHandler('updateInput');
|
||
|
}
|
||
|
this.lastCompletedValue = "";
|
||
|
this.showHideOptions();
|
||
|
} else {
|
||
|
this.lastCompletedValue = "";
|
||
|
this.isCompleted = false;
|
||
|
}
|
||
|
},
|
||
|
getOptionContent: function(item){
|
||
|
var content;
|
||
|
var args = [{instance: this, item: item}];
|
||
|
if( ( content = $(this.input).triggerHandler('getoptioncontent', args) ) && content.indexOf && content.indexOf('option-value') == -1 ){
|
||
|
content += '<span class="option-value" style="display: none;">'+ item.value +'</span>';
|
||
|
}
|
||
|
if(content == null){
|
||
|
content = '<span class="option-value">'+ item.value +'</span>';
|
||
|
if(item.label){
|
||
|
content += ' <span class="option-label">'+ item.label +'</span>';
|
||
|
}
|
||
|
}
|
||
|
return content || '';
|
||
|
},
|
||
|
setCompletedValue: function(value, foundItem, length){
|
||
|
this.isCompleted = false;
|
||
|
|
||
|
if(!this.options.valueCompletion || !foundItem || this.lastCompletedValue.length >= value.length ){
|
||
|
this.lastCompletedValue = value;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var newValue;
|
||
|
var input = this.input;
|
||
|
var end = $.prop(input, 'selectionEnd');
|
||
|
|
||
|
this.lastCompletedValue = value;
|
||
|
|
||
|
if(value.length == end){
|
||
|
|
||
|
newValue = value + foundItem.value.substr(length.length);
|
||
|
|
||
|
$(input).triggerHandler('triggerinput');
|
||
|
$.prop(input, 'value', newValue);
|
||
|
$(input).triggerHandler('updateInput');
|
||
|
$(input).callProp('setSelectionRange', [value.length, newValue.length]);
|
||
|
|
||
|
//safari workaround || needs more investigation
|
||
|
setTimeout(function(){
|
||
|
if(newValue == $.prop(input, 'value') && $.prop(input, 'selectionEnd') != newValue.length){
|
||
|
$.prop(input, 'selectionEnd', newValue.length);
|
||
|
}
|
||
|
}, 0);
|
||
|
this.isCompleted = true;
|
||
|
}
|
||
|
},
|
||
|
getPartialValue: function(){
|
||
|
var value = $.prop(this.input, 'value');
|
||
|
if(this.options.valueCompletion && this.lastCompletedValue && !$.prop(this.input, 'value').indexOf(this.lastCompletedValue)){
|
||
|
value = this.lastCompletedValue;
|
||
|
}
|
||
|
if(this.options.multiple){
|
||
|
value = value.split(splitReg);
|
||
|
value = value[value.length - 1] || '';
|
||
|
}
|
||
|
return value;
|
||
|
},
|
||
|
showHideOptions: function(_fromShowList){
|
||
|
var lis, firstFoundValue;
|
||
|
var inputValue = $.prop(this.input, 'value');
|
||
|
var value = inputValue.toLowerCase();
|
||
|
var found = false;
|
||
|
var startSearch = this.options.filter == '^';
|
||
|
var that = this;
|
||
|
|
||
|
//first check prevent infinite loop, second creates simple lazy optimization
|
||
|
if(value === this.lastUpdatedValue){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(this.options.multiple){
|
||
|
value = value.split(splitReg);
|
||
|
value = value[value.length - 1] || '';
|
||
|
}
|
||
|
|
||
|
if(this.lastUnfoundValue && value.indexOf(this.lastUnfoundValue) === 0){
|
||
|
this.hideList();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
this.lastUpdatedValue = value;
|
||
|
lis = $('li', this.shadowList);
|
||
|
|
||
|
|
||
|
|
||
|
if(value && this.options.filter != '!'){
|
||
|
|
||
|
|
||
|
this.arrayOptions.forEach(function(item, i){
|
||
|
var search, searchIndex, foundName;
|
||
|
if(!('lowerValue' in item)){
|
||
|
item.lowerValue = item.value.toLowerCase();
|
||
|
if(item.label && item.label != item.value ){
|
||
|
item.lowerLabel = item.label.toLowerCase();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(value != item.lowerValue && item.lowerLabel != value){
|
||
|
searchIndex = item.lowerValue.indexOf(value);
|
||
|
search = startSearch ? !searchIndex : searchIndex !== -1;
|
||
|
if(search){
|
||
|
foundName = 'value';
|
||
|
if(!firstFoundValue && !searchIndex){
|
||
|
firstFoundValue = item;
|
||
|
}
|
||
|
} else if(item.lowerLabel){
|
||
|
searchIndex = item.lowerLabel.indexOf(value);
|
||
|
search = startSearch ? !searchIndex : searchIndex !== -1;
|
||
|
foundName = 'label';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(search){
|
||
|
that.addMark($(lis[i]).removeClass('hidden-item'), item, foundName, searchIndex, value.length);
|
||
|
found = true;
|
||
|
} else {
|
||
|
$(lis[i]).addClass('hidden-item');
|
||
|
}
|
||
|
});
|
||
|
} else if(lis.length) {
|
||
|
this.removeMark(lis.removeClass('hidden-item'));
|
||
|
found = true;
|
||
|
}
|
||
|
|
||
|
this.hasViewableData = found;
|
||
|
if(!_fromShowList && found){
|
||
|
if(this.popover.isVisible && this.popover.element.attr('data-vertical') == 'bottom'){
|
||
|
that._stopMouseOver = true;
|
||
|
this.popover.element.triggerHandler('pospopover');
|
||
|
setTimeout(function(){
|
||
|
that._stopMouseOver = false;
|
||
|
}, 9);
|
||
|
}
|
||
|
this.showList();
|
||
|
}
|
||
|
|
||
|
if(!found){
|
||
|
this.lastUnfoundValue = value;
|
||
|
this.hideList();
|
||
|
} else {
|
||
|
$(this.input).removeAttr('aria-activedescendant');
|
||
|
this.setCompletedValue(inputValue, firstFoundValue, value);
|
||
|
this.lastUnfoundValue = false;
|
||
|
}
|
||
|
},
|
||
|
otherType: {
|
||
|
value: 'label',
|
||
|
label: 'value'
|
||
|
},
|
||
|
addMark: function(elem, item, prop, start, length){
|
||
|
if(this.options.highlight){
|
||
|
var text = item[prop].substr(start, length);
|
||
|
text = item[prop].replace(text ,'<mark>'+ text +'</mark>');
|
||
|
$('.option-'+ this.otherType[prop] +' > mark', elem).each(this._replaceMark);
|
||
|
$('.option-'+prop, elem).html(text);
|
||
|
|
||
|
}
|
||
|
},
|
||
|
_replaceMark: function(){
|
||
|
var content = $(this).html();
|
||
|
$(this).replaceWith(content);
|
||
|
},
|
||
|
removeMark: function(lis){
|
||
|
if(this.options.highlight){
|
||
|
$('mark', lis).each(this._replaceMark);
|
||
|
}
|
||
|
},
|
||
|
showList: function(){
|
||
|
if(this.popover.isVisible){return false;}
|
||
|
if(this.needsUpdate){
|
||
|
this.updateListOptions();
|
||
|
}
|
||
|
this.showHideOptions(true);
|
||
|
if(!this.hasViewableData){return false;}
|
||
|
var that = this;
|
||
|
|
||
|
that.shadowList.find('li.active-item').removeClass('active-item');
|
||
|
that.popover.show(this.input);
|
||
|
$(this.input)
|
||
|
.attr({'aria-expanded': 'true'})
|
||
|
;
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
hideList: function(){
|
||
|
if(!this.popover.isVisible){return false;}
|
||
|
var that = this;
|
||
|
$(this.input)
|
||
|
.attr({'aria-expanded': 'false'})
|
||
|
.removeAttr('aria-activedescendant')
|
||
|
;
|
||
|
|
||
|
this.popover.hide();
|
||
|
that.shadowList.removeClass('datalist-visible list-item-active');
|
||
|
that.index = -1;
|
||
|
if(that.changedValue){
|
||
|
that.triggeredByDatalist = true;
|
||
|
$(that.input).trigger('input').trigger('change');
|
||
|
that.changedValue = false;
|
||
|
that.triggeredByDatalist = false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
scrollIntoView: function(elem){
|
||
|
var ul = $('ul', this.shadowList);
|
||
|
var div = $('div.datalist-box', this.shadowList);
|
||
|
var elemPos = elem.position();
|
||
|
var containerHeight;
|
||
|
elemPos.top -= (parseInt(ul.css('paddingTop'), 10) || 0) + (parseInt(ul.css('marginTop'), 10) || 0) + (parseInt(ul.css('borderTopWidth'), 10) || 0);
|
||
|
if(elemPos.top < 0){
|
||
|
div.scrollTop( div.scrollTop() + elemPos.top - 2);
|
||
|
return;
|
||
|
}
|
||
|
elemPos.top += elem.outerHeight();
|
||
|
containerHeight = div.height();
|
||
|
if(elemPos.top > containerHeight){
|
||
|
div.scrollTop( div.scrollTop() + (elemPos.top - containerHeight) + 2);
|
||
|
}
|
||
|
},
|
||
|
changeValue: function(activeItem){
|
||
|
if(!activeItem[0]){return;}
|
||
|
var spinner, tmpValue;
|
||
|
var newValue = $('span.option-value', activeItem).text();
|
||
|
var oldValue = $.prop(this.input, 'value');
|
||
|
|
||
|
if(this.options.multiple){
|
||
|
tmpValue = oldValue.split(splitReg);
|
||
|
tmpValue[tmpValue.length - 1] = newValue;
|
||
|
|
||
|
newValue = tmpValue.join(', ');
|
||
|
}
|
||
|
|
||
|
if(this.options.valueCompletion && this.lastCompletedValue && !oldValue.indexOf(this.lastCompletedValue)){
|
||
|
oldValue = this.lastCompletedValue;
|
||
|
this.lastCompletedValue = "";
|
||
|
}
|
||
|
|
||
|
if(newValue != oldValue){
|
||
|
|
||
|
$(this.input)
|
||
|
.prop('value', newValue)
|
||
|
.triggerHandler('updateInput')
|
||
|
;
|
||
|
this.changedValue = true;
|
||
|
if((spinner = $.data(this.input, 'wsspinner')) && spinner.setInput){
|
||
|
spinner.setInput(newValue);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
markItem: function(index, doValue, items){
|
||
|
var activeItem;
|
||
|
var goesUp;
|
||
|
|
||
|
items = items || $('li:not(.hidden-item)', this.shadowList);
|
||
|
if(!items.length){return;}
|
||
|
if(index < 0){
|
||
|
index = items.length - 1;
|
||
|
} else if(index >= items.length){
|
||
|
index = 0;
|
||
|
}
|
||
|
items.removeClass('active-item');
|
||
|
this.shadowList.addClass('list-item-active');
|
||
|
activeItem = items.filter(':eq('+ index +')').addClass('active-item');
|
||
|
|
||
|
if(doValue){
|
||
|
if(doValue != 'onlyScroll'){
|
||
|
this.changeValue(activeItem);
|
||
|
} else {
|
||
|
$(this.input).attr('aria-activedescendant', activeItem.prop('id'));
|
||
|
}
|
||
|
this.scrollIntoView(activeItem);
|
||
|
}
|
||
|
this.index = index;
|
||
|
}
|
||
|
});
|
||
|
});
|