2084 lines
78 KiB
JavaScript
Executable File
2084 lines
78 KiB
JavaScript
Executable File
/**
|
|
* jQuery Flexdatalist.
|
|
* Autocomplete input fields, with support for datalists.
|
|
*
|
|
* Version:
|
|
* 2.3.0
|
|
*
|
|
* Depends:
|
|
* jquery.js > 1.8.3
|
|
*
|
|
* Demo and Documentation:
|
|
* http://projects.sergiodinislopes.pt/flexdatalist/
|
|
*
|
|
* Github:
|
|
* https://github.com/sergiodlopes/jquery-flexdatalist/
|
|
*
|
|
*/
|
|
|
|
jQuery.fn.flexdatalist = function (_option, _value) {
|
|
'use strict';
|
|
|
|
var destroy = function ($flex, clear) {
|
|
$flex.each(function () {
|
|
var $this = $(this),
|
|
data = $this.data(),
|
|
options = data.flexdatalist,
|
|
$aliascontainer = data.aliascontainer;
|
|
|
|
if ($aliascontainer) {
|
|
$this.removeClass('flexdatalist-set')
|
|
.attr({'style': null, 'tabindex': null})
|
|
.val((options && options.originalValue && !clear ? options.originalValue : ''))
|
|
.removeData('flexdatalist')
|
|
.removeData('aliascontainer')
|
|
.off();
|
|
$aliascontainer.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Callable stuff
|
|
if (typeof _option === 'string' && _option !== 'reset') {
|
|
if (typeof this[0] === 'object' && typeof this[0].fvalue !== 'undefined') {
|
|
var target = this[0];
|
|
if (_option === 'destroy') {
|
|
destroy(this, _value);
|
|
// Get/Set value
|
|
} else if (_option === 'value') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.fvalue.get();
|
|
}
|
|
target.fvalue.set(_value);
|
|
// Add value
|
|
} else if (_option === 'add') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.debug('Missing value to add!');
|
|
}
|
|
target.fvalue.add(_value);
|
|
// Toggle value
|
|
} else if (_option === 'toggle') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.debug('Missing value to toggle!');
|
|
}
|
|
target.fvalue.toggle(_value);
|
|
// Remove value
|
|
} else if (_option === 'remove') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.debug('Missing value to remove!');
|
|
}
|
|
target.fvalue.remove(_value);
|
|
// Disabled/enabled
|
|
} else if (_option === 'disabled') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.fdisabled();
|
|
}
|
|
target.fdisabled(_value);
|
|
// Option(s)
|
|
} else if (typeof _option === 'string') {
|
|
if (typeof _value === 'undefined') {
|
|
return target.options.get(_option);
|
|
}
|
|
target.options.set(_option, _value);
|
|
}
|
|
return this;
|
|
}
|
|
_option = {_option: _value};
|
|
}
|
|
|
|
// Destroy if already set
|
|
if (this.length > 0 && typeof this[0].fvalue !== 'undefined') {
|
|
destroy(this);
|
|
}
|
|
|
|
var _options = $.extend({
|
|
url: null,
|
|
data: [],
|
|
params: {},
|
|
relatives: null,
|
|
chainedRelatives: false,
|
|
cache: true,
|
|
cacheLifetime: 60,
|
|
minLength: 3,
|
|
groupBy: false,
|
|
selectionRequired: false,
|
|
focusFirstResult: false,
|
|
textProperty: null,
|
|
valueProperty: null,
|
|
visibleProperties: [],
|
|
iconProperty: 'thumb',
|
|
searchIn: ['label'],
|
|
searchContain: false,
|
|
searchEqual: false,
|
|
searchByWord: false,
|
|
searchDisabled: false,
|
|
searchDelay: 400,
|
|
normalizeString: null,
|
|
multiple: null,
|
|
disabled: null,
|
|
maxShownResults: 100,
|
|
removeOnBackspace: true,
|
|
noResultsText: 'No results found for "{keyword}"',
|
|
toggleSelected: false,
|
|
allowDuplicateValues: false,
|
|
redoSearchOnFocus: true,
|
|
requestType: 'get',
|
|
requestContentType: 'x-www-form-urlencoded',
|
|
requestHeaders: null,
|
|
resultsProperty: 'results',
|
|
keywordParamName: 'keyword',
|
|
searchContainParamName: 'contain',
|
|
limitOfValues: 0,
|
|
valuesSeparator: ',',
|
|
debug: true
|
|
}, _option);
|
|
|
|
return this.each(function (id) {
|
|
var $this = $(this),
|
|
_this = this,
|
|
_searchTimeout = null,
|
|
_values = [],
|
|
fid = 'flex' + id,
|
|
$alias = null,
|
|
$multiple = null;
|
|
|
|
/**
|
|
* Initialization
|
|
*/
|
|
this.init = function () {
|
|
var options = this.options.init();
|
|
this.set.up();
|
|
|
|
$alias
|
|
// Focusin
|
|
.on('focusin', function (event) {
|
|
_this.action.redoSearchFocus(event);
|
|
_this.action.showAllResults(event);
|
|
if ($multiple) {
|
|
$multiple.addClass('focus');
|
|
}
|
|
})
|
|
// Keydown
|
|
.on('input keydown', function (event) {
|
|
if (_this.keyNum(event) === 9) {
|
|
_this.results.remove();
|
|
}
|
|
_this.action.keypressValue(event, 188);
|
|
_this.action.backSpaceKeyRemove(event);
|
|
})
|
|
// Keyup
|
|
.on('input keyup', function (event) {
|
|
_this.action.keypressValue(event, 13);
|
|
_this.action.keypressSearch(event);
|
|
_this.action.copyValue(event);
|
|
_this.action.backSpaceKeyRemove(event);
|
|
_this.action.showAllResults(event);
|
|
_this.action.clearValue(event);
|
|
_this.action.removeResults(event);
|
|
_this.action.inputWidth(event);
|
|
})
|
|
// Focusout
|
|
.on('focusout', function (event) {
|
|
if ($multiple) {
|
|
$multiple.removeClass('focus');
|
|
}
|
|
_this.action.clearText(event);
|
|
_this.action.clearValue(event);
|
|
});
|
|
|
|
window.onresize = function (event) {
|
|
_this.position();
|
|
};
|
|
|
|
// Run garbage collector
|
|
this.cache.gc();
|
|
|
|
if (options.selectionRequired) {
|
|
_this.fvalue.clear(true, true);
|
|
}
|
|
this.fvalue._load(options.originalValue, function (values, matches) {
|
|
_this.fdisabled(options.disabled);
|
|
$this.trigger('init:flexdatalist', [options]);
|
|
}, true);
|
|
}
|
|
|
|
/**
|
|
* Handle user actions.
|
|
*/
|
|
this.action = {
|
|
/**
|
|
* Add value on comma or enter keypress.
|
|
*/
|
|
keypressValue: function (event, keyCode) {
|
|
var key = _this.keyNum(event),
|
|
val = $alias[0].value,
|
|
options = _this.options.get();
|
|
|
|
if (val.length > 0
|
|
&& key === keyCode
|
|
&& !options.selectionRequired
|
|
&& options.multiple) {
|
|
var val = $alias[0].value;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
_this.fvalue.extract(val);
|
|
_this.results.remove();
|
|
}
|
|
},
|
|
/**
|
|
* Check if keypress is valid.
|
|
*/
|
|
keypressSearch: function (event) {
|
|
var key = _this.keyNum(event),
|
|
keyword = $alias.val(),
|
|
length = keyword.length,
|
|
options = _this.options.get();
|
|
|
|
clearTimeout(_searchTimeout);
|
|
if (!key || (key !== 13 && (key < 37 || key > 40))) {
|
|
_searchTimeout = setTimeout(function () {
|
|
if ((options.minLength === 0 && length > 0) || (options.minLength > 0 && length >= options.minLength)) {
|
|
_this.data.load(function (data) {
|
|
_this.search.get(keyword, data, function (matches) {
|
|
_this.results.show(matches);
|
|
});
|
|
});
|
|
}
|
|
}, options.searchDelay);
|
|
}
|
|
},
|
|
/**
|
|
* Redo search if input get's back on focus and no value selected.
|
|
*/
|
|
redoSearchFocus: function (event) {
|
|
var val = _this.fvalue.get(),
|
|
options = _this.options.get(),
|
|
alias = $alias.val();
|
|
if (options.redoSearchOnFocus && ((alias.length > 0 && options.multiple) || (alias.length > 0 && val.length === 0))) {
|
|
this.keypressSearch(event);
|
|
}
|
|
},
|
|
/**
|
|
* Copy value from alias to target input.
|
|
*/
|
|
copyValue: function (event) {
|
|
if (_this.keyNum(event) !== 13) {
|
|
var keyword = $alias.val(),
|
|
val = _this.fvalue.get(true),
|
|
options = _this.options.get();
|
|
if (!options.multiple && !options.selectionRequired && keyword.length !== val.length) {
|
|
_this.fvalue.extract(keyword);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Remove value on backspace key (multiple input only).
|
|
*/
|
|
backSpaceKeyRemove: function (event) {
|
|
var options = _this.options.get();
|
|
if (options.removeOnBackspace && options.multiple) {
|
|
var val = $alias.val(),
|
|
$remove = $alias.data('_remove');
|
|
if (_this.keyNum(event) === 8) {
|
|
if (val.length === 0) {
|
|
if ($remove) {
|
|
_this.fvalue.remove($remove);
|
|
$alias.data('_remove', null);
|
|
} else {
|
|
$alias.data('_remove', $alias.parents('li:eq(0)').prev());
|
|
}
|
|
} else {
|
|
$alias.data('_remove', null);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Show all results if minLength option is 0.
|
|
*/
|
|
showAllResults: function (event) {
|
|
var val = $alias.val();
|
|
val = $.trim(val);
|
|
if (val === '' && _this.options.get('minLength') === 0) {
|
|
_this.data.load(function (data) {
|
|
_this.results.show(data);
|
|
});
|
|
}
|
|
},
|
|
/**
|
|
* Calculate input width by number of characters.
|
|
*/
|
|
inputWidth: function (event) {
|
|
var options = _this.options.get();
|
|
if (options.multiple) {
|
|
var keyword = $alias.val(),
|
|
fontSize = parseInt($alias.css('fontSize').replace('px', '')),
|
|
minWidth = 40,
|
|
maxWidth = $this.innerWidth(),
|
|
width = ((keyword.length + 1) * fontSize);
|
|
|
|
if (width >= minWidth && width <= maxWidth) {
|
|
$alias[0].style.width = width + 'px';
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Clear text/alias input when criteria is met.
|
|
*/
|
|
clearText: function (event) {
|
|
var val = _this.fvalue.get(),
|
|
options = _this.options.get();
|
|
|
|
if (!options.multiple && options.selectionRequired && val.length === 0) {
|
|
$alias[0].value = '';
|
|
}
|
|
},
|
|
/**
|
|
* Clear value when criteria is met.
|
|
*/
|
|
clearValue: function (event) {
|
|
var val = _this.fvalue.get(),
|
|
keyword = $alias.val(),
|
|
options = _this.options.get();
|
|
|
|
if (!options.multiple && options.selectionRequired && keyword.length <= options.minLength) {
|
|
_this.fvalue.clear();
|
|
}
|
|
},
|
|
/**
|
|
* Remove results when criteria is met.
|
|
*/
|
|
removeResults: function (event) {
|
|
var keyword = $alias.val(),
|
|
options = _this.options.get();
|
|
if (options.minLength > 0 && keyword.length < options.minLength) {
|
|
_this.results.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup flex.
|
|
*/
|
|
this.set = {
|
|
/**
|
|
* Prepare input replacement.
|
|
*/
|
|
up: function () {
|
|
$alias = this.getAlias();
|
|
if (_this.options.get('multiple')) {
|
|
$multiple = this.multipleInput($alias);
|
|
} else {
|
|
$alias.insertAfter($this);
|
|
}
|
|
|
|
this.accessibility($alias);
|
|
|
|
// Respect autofocus attribute
|
|
if ($this.attr('autofocus')) {
|
|
$alias.trigger('focus');
|
|
}
|
|
|
|
$this.data('aliascontainer', ($multiple ? $multiple : $alias)).addClass('flexdatalist flexdatalist-set').css({
|
|
'position': 'absolute',
|
|
'top': -14000,
|
|
'left': -14000
|
|
}).attr('tabindex', -1);
|
|
|
|
// update input label with alias id
|
|
var inputId = $this.attr('id'),
|
|
aliasId = $alias.attr('id');
|
|
$('label[for="' + inputId + '"]').attr('for', aliasId);
|
|
|
|
this.chained();
|
|
},
|
|
/**
|
|
* Create and return the alias input field for single value input.
|
|
*/
|
|
getAlias: function () {
|
|
var aliasid = ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : fid);
|
|
var $alias = $('<input type="text">')
|
|
.attr({
|
|
'class': $this.attr('class'),
|
|
'name': ($this.attr('name') ? 'flexdatalist-' + $this.attr('name') : null),
|
|
'id': aliasid,
|
|
'placeholder': $this.attr('placeholder')
|
|
})
|
|
.addClass('flexdatalist-alias ' + aliasid)
|
|
.removeClass('flexdatalist')
|
|
.attr('autocomplete', 'off');
|
|
return $alias;
|
|
},
|
|
/**
|
|
* Multiple values input/list
|
|
*/
|
|
multipleInput: function ($alias) {
|
|
$multiple = $('<ul tabindex="1">')
|
|
.addClass('flexdatalist-multiple ' + fid)
|
|
.css({
|
|
'border-color': $this.css('border-left-color'),
|
|
'border-width': $this.css('border-left-width'),
|
|
'border-style': $this.css('border-left-style'),
|
|
'border-radius': $this.css('border-top-left-radius'),
|
|
'background-color': $this.css('background-color')
|
|
})
|
|
.insertAfter($this).on('click', function () {
|
|
$(this).find('input').trigger('focus');
|
|
});
|
|
|
|
$('<li class="input-container">')
|
|
.addClass('flexdatalist-multiple-value')
|
|
.append($alias)
|
|
.appendTo($multiple);
|
|
|
|
return $multiple;
|
|
},
|
|
/**
|
|
* Chained inputs handling.
|
|
*/
|
|
chained: function () {
|
|
var options = _this.options.get();
|
|
if (options.relatives && options.chainedRelatives) {
|
|
var toggle = function (init) {
|
|
options.relatives.each(function () {
|
|
var emptyRelative = _this.isEmpty($(this).val()),
|
|
empty = _this.isEmpty(_this.value);
|
|
// If disabling, clear all values
|
|
if (emptyRelative || !empty) {
|
|
_this.fvalue.clear();
|
|
}
|
|
_this.fdisabled(emptyRelative);
|
|
});
|
|
};
|
|
options.relatives.on('change', function () {
|
|
toggle();
|
|
});
|
|
toggle(true);
|
|
}
|
|
},
|
|
/**
|
|
* Accessibility.
|
|
*/
|
|
accessibility: function ($input) {
|
|
var aliasid = ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : fid);
|
|
var scrReaderAttr = {
|
|
'aria-autocomplete': 'list',
|
|
'aria-expanded': 'false',
|
|
'aria-owns': aliasid + '-results',
|
|
};
|
|
|
|
$input.attr(scrReaderAttr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process input value(s) (where the magic happens).
|
|
*/
|
|
this.fvalue = {
|
|
/**
|
|
* Get value(s).
|
|
*/
|
|
get: function (asString) {
|
|
var val = _this.value,
|
|
options = _this.options.get();
|
|
if ((options.multiple || this.isJSON()) && !asString) {
|
|
return this.toObj(val);
|
|
}
|
|
return val;
|
|
},
|
|
/**
|
|
* Set value.
|
|
* Parse value if necessary.
|
|
*/
|
|
set: function (val, append) {
|
|
if (!_this.fdisabled()) {
|
|
if (!append) {
|
|
this.clear(true);
|
|
}
|
|
this._load(val);
|
|
}
|
|
return $this;
|
|
},
|
|
/**
|
|
* Add value.
|
|
*/
|
|
add: function (val) {
|
|
if (_this.options.get('multiple')) {
|
|
this.set(val, true);
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Toggle value.
|
|
*/
|
|
toggle: function (val) {
|
|
if (!_this.fdisabled()) {
|
|
this.multiple.toggle(val);
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Remove value.
|
|
*/
|
|
remove: function (val) {
|
|
if (!_this.fdisabled()) {
|
|
val = this.toObj(val);
|
|
$this.trigger('before:flexdatalist.remove', [val]);
|
|
var result = [];
|
|
if (_this.isArray(val)) {
|
|
$.each(val, function (i, value) {
|
|
var removed = _this.fvalue.multiple.remove(value);
|
|
if (removed) {
|
|
result.push(removed);
|
|
}
|
|
});
|
|
} else {
|
|
var _result = this.multiple.remove(val);
|
|
if (_result) {
|
|
result.push(_result);
|
|
}
|
|
}
|
|
$this
|
|
.trigger('after:flexdatalist.remove', [val, result])
|
|
.trigger('change:flexdatalist', [result, _this.options.get()])
|
|
.trigger('change');
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Load (remote?) value(s).
|
|
*/
|
|
_load: function (values, callback, init) {
|
|
var options = _this.options.get(),
|
|
valueProp = options.valueProperty,
|
|
_values = this.toStr(values),
|
|
_val = this.get(true);
|
|
|
|
callback = (callback ? callback : $.noop);
|
|
// If nothing changes, return
|
|
if (_values.length == 0 && _val.length == 0) {
|
|
callback(values);
|
|
return;
|
|
}
|
|
values = this.toObj(values);
|
|
if (!_this.isEmpty(values) && !_this.isEmpty(valueProp) && valueProp !== '*') {
|
|
if (!_this.isObject(valueProp)) {
|
|
valueProp = valueProp.split(',');
|
|
}
|
|
// Load data
|
|
_this.data.load(function (data) {
|
|
if (!_this.isObject(values)) {
|
|
values = values.split(',');
|
|
} else if (!_this.isArray(values)) {
|
|
values = [values];
|
|
}
|
|
var found = [];
|
|
for (var idxv = 0; idxv < values.length; idxv++) {
|
|
var value = values[idxv];
|
|
for (var i = 0; i < data.length; i++) {
|
|
var item = data[i];
|
|
for (var idx = 0; idx < valueProp.length; idx++) {
|
|
var prop = valueProp[idx],
|
|
value = _this.isDefined(value, prop) ? value[prop] : value;
|
|
if (_this.isDefined(item, prop) && value === item[prop]) {
|
|
found.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (found.length > 0) {
|
|
_this.fvalue.extract(found, true);
|
|
}
|
|
callback(values);
|
|
}, values);
|
|
return;
|
|
}
|
|
callback(values);
|
|
_this.fvalue.extract(values, init);
|
|
},
|
|
/**
|
|
* Extract value and text.
|
|
*/
|
|
extract: function (values, init) {
|
|
var options = _this.options.get(),
|
|
result = [];
|
|
|
|
if (!init) {
|
|
$this.trigger('before:flexdatalist.value', [values, options]);
|
|
}
|
|
|
|
if (_this.isArray(values)) {
|
|
$.each(values, function (i, value) {
|
|
result.push(_this.fvalue._extract(value));
|
|
});
|
|
} else {
|
|
result = _this.fvalue._extract(values);
|
|
}
|
|
|
|
if (!init) {
|
|
$this
|
|
.data('result_selected', values)
|
|
.trigger('after:flexdatalist.value', [result, options])
|
|
.trigger('change:flexdatalist', [result, options])
|
|
.trigger('change');
|
|
}
|
|
},
|
|
/**
|
|
* @inherited.
|
|
*/
|
|
_extract: function (val) {
|
|
var txt = this.text(val),
|
|
value = this.value(val),
|
|
options = _this.options.get();
|
|
|
|
if (options.multiple) {
|
|
// For allowDuplicateValues
|
|
if (!_this.isEmpty(txt)) {
|
|
if (_this.isDup(txt)) {
|
|
return;
|
|
}
|
|
_values.push(txt);
|
|
this.multiple.add(value, txt);
|
|
}
|
|
} else {
|
|
this.single(value, txt);
|
|
}
|
|
|
|
return {value: value, text: txt};
|
|
},
|
|
/**
|
|
* Default input value.
|
|
*/
|
|
single: function (val, txt) {
|
|
if (txt && txt !== $alias.val()) {
|
|
$alias[0].value = txt;
|
|
}
|
|
_this.value = val;
|
|
},
|
|
/**
|
|
* Input with multiple values.
|
|
*/
|
|
multiple: {
|
|
/**
|
|
* Add value and item on list.
|
|
*/
|
|
add: function (val, txt) {
|
|
var _multiple = this,
|
|
$li = this.li(val, txt);
|
|
|
|
// Toggle
|
|
$li.on('click', function () {
|
|
_multiple.toggle($(this));
|
|
// Remove
|
|
}).find('.fdl-remove').on('click', function () {
|
|
_this.fvalue.remove($(this).parent());
|
|
});
|
|
|
|
this.push(val);
|
|
$alias[0].value = '';
|
|
this.handleLimit();
|
|
},
|
|
/**
|
|
* Push value to input.
|
|
*/
|
|
push: function (val, index) {
|
|
var current = _this.fvalue.get();
|
|
if (current.includes(val)) {
|
|
return;
|
|
}
|
|
val = _this.fvalue.toObj(val);
|
|
current.push(val);
|
|
val = _this.fvalue.toStr(current);
|
|
_this.value = val;
|
|
},
|
|
/**
|
|
* Toggle value.
|
|
*/
|
|
toggle: function (val) {
|
|
var options = _this.options.get();
|
|
if (!options.toggleSelected) {
|
|
return;
|
|
}
|
|
|
|
var $li = this.findLi(val);
|
|
if (!$li) {
|
|
return;
|
|
}
|
|
|
|
var data = $li.data(),
|
|
action = $li.hasClass('disabled') ? 'enable' : 'disable',
|
|
eventArgs = [{value: data.value, text: data.text, action: action}, options];
|
|
|
|
$this.trigger('before:flexdatalist.toggle', eventArgs);
|
|
|
|
if (action === 'enable') {
|
|
$li.removeClass('disabled');
|
|
} else {
|
|
$li.addClass('disabled');
|
|
}
|
|
|
|
var current = [];
|
|
$multiple.find('li.toggle:not(.disabled)').each(function () {
|
|
var $item = $(this);
|
|
current.push($item.data('value'));
|
|
});
|
|
|
|
current = _this.fvalue.toStr(current);
|
|
_this.value = current;
|
|
|
|
$this
|
|
.trigger('after:flexdatalist.toggle', eventArgs)
|
|
.trigger('change:flexdatalist', eventArgs)
|
|
.trigger('change');
|
|
|
|
},
|
|
/**
|
|
* Remove value from input.
|
|
*/
|
|
remove: function (val) {
|
|
var $li = this.findLi(val);
|
|
if (!$li) {
|
|
return;
|
|
}
|
|
|
|
var values = _this.fvalue.get(),
|
|
index = $li.index(),
|
|
data = $li.data(),
|
|
arg = {value: data.value, text: data.text};
|
|
|
|
values.splice(index, 1);
|
|
values = _this.fvalue.toStr(values);
|
|
_this.value = values;
|
|
$li.remove();
|
|
_this.fvalue.multiple.handleLimit();
|
|
|
|
// For allowDuplicateValues
|
|
_values.splice(index, 1);
|
|
|
|
this.handleLimit();
|
|
|
|
return arg;
|
|
},
|
|
/**
|
|
* Remove all.
|
|
*/
|
|
removeAll: function () {
|
|
var values = _this.fvalue.get(),
|
|
options = _this.options.get();
|
|
|
|
$this.trigger('before:flexdatalist.remove.all', [values, options]);
|
|
|
|
$multiple.find('li:not(.input-container)').remove();
|
|
|
|
_this.value = '';
|
|
_values = [];
|
|
|
|
this.handleLimit();
|
|
|
|
$this.trigger('after:flexdatalist.remove.all', [values, options]);
|
|
},
|
|
/**
|
|
* Create new item and return it.
|
|
*/
|
|
li: function (val, txt) {
|
|
var $inputContainer = $multiple.find('li.input-container');
|
|
var options = _this.options.get();
|
|
return $('<li>')
|
|
.addClass('value' + (options.toggleSelected ? ' toggle' : ''))
|
|
.append('<span class="text">' + txt + '</span>')
|
|
.append('<span class="fdl-remove">×</span>')
|
|
.data({
|
|
'text': txt,
|
|
'value': _this.fvalue.toStr(val)
|
|
})
|
|
.insertBefore($inputContainer);
|
|
},
|
|
/**
|
|
* Handle the limit.
|
|
*
|
|
* @return void
|
|
*/
|
|
handleLimit: function () {
|
|
var isAtLimit = this.isAtLimit();
|
|
var $input = $multiple.find('li.input-container');
|
|
isAtLimit ? $input.hide() : $input.show();
|
|
},
|
|
/**
|
|
* Check the limit of items.
|
|
*
|
|
* @return bool True if reached the limit, false otherwise
|
|
*/
|
|
isAtLimit: function () {
|
|
var limit = _this.options.get('limitOfValues');
|
|
if (!(limit > 0)) {
|
|
return false;
|
|
}
|
|
return limit == _values.length;
|
|
},
|
|
/**
|
|
* Get li item from value.
|
|
*/
|
|
findLi: function ($li) {
|
|
if (!($li instanceof jQuery)) {
|
|
var val = $li;
|
|
$li = null;
|
|
$multiple.find('li:not(.input-container)').each(function () {
|
|
var $_li = $(this);
|
|
if ($_li.data('value') === val) {
|
|
$li = $_li;
|
|
return false;
|
|
}
|
|
});
|
|
} else if ($li.length === 0) {
|
|
$li = null;
|
|
}
|
|
return $li;
|
|
},
|
|
/**
|
|
* Get li item from value.
|
|
*/
|
|
isEmpty: function () {
|
|
return this.get().length > 0;
|
|
}
|
|
},
|
|
/**
|
|
* Get value that will be set on input field.
|
|
*/
|
|
value: function (item) {
|
|
var value = item,
|
|
options = _this.options.get(),
|
|
valueProperty = options.valueProperty;
|
|
|
|
if (_this.isObject(item)) {
|
|
if (this.isJSON() || this.isMixed()) {
|
|
delete item.name_highlight;
|
|
if (_this.isArray(valueProperty)) {
|
|
var _value = {};
|
|
for (var i = 0; i < valueProperty.length; i++) {
|
|
var propValue = _this.getPropertyValue(item, valueProperty[i]);
|
|
if (propValue) {
|
|
_value[valueProperty[i]] = propValue;
|
|
}
|
|
}
|
|
value = this.toStr(_value);
|
|
} else {
|
|
value = this.toStr(item);
|
|
}
|
|
} else if (_this.isDefined(item, valueProperty)) {
|
|
value = _this.getPropertyValue(item, valueProperty);
|
|
} else if (_this.isDefined(item, options.searchIn[0])) {
|
|
value = _this.getPropertyValue(item, options.searchIn[0]);
|
|
} else {
|
|
value = null;
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
/**
|
|
* Get text that will be shown to user on the alias input field.
|
|
*/
|
|
text: function (item) {
|
|
var text = item,
|
|
options = _this.options.get();
|
|
|
|
if (_this.isObject(item)) {
|
|
text = _this.getPropertyValue(item, options.searchIn[0]);
|
|
if (_this.isDefined(item, options.textProperty)) {
|
|
text = _this.getPropertyValue(item, options.textProperty);
|
|
} else {
|
|
text = this.placeholders.replace(item, options.textProperty, text);
|
|
}
|
|
}
|
|
|
|
text = _this.escapeHtml(text);
|
|
|
|
return text;
|
|
},
|
|
/**
|
|
* Process text placeholders.
|
|
*/
|
|
placeholders: {
|
|
replace: function (item, pattern, fallback) {
|
|
if (_this.isObject(item) && typeof pattern === 'string') {
|
|
var properties = this.parse(pattern);
|
|
if (!_this.isEmpty(item) && properties) {
|
|
$.each(properties, function (string, property) {
|
|
if (_this.isDefined(item, property)) {
|
|
pattern = pattern.replace(string, _this.getPropertyValue(item, property));
|
|
}
|
|
});
|
|
return pattern;
|
|
}
|
|
}
|
|
return fallback;
|
|
},
|
|
parse: function (pattern) {
|
|
var matches = pattern.match(/\{.+?\}/g);
|
|
if (!matches) {
|
|
return false;
|
|
}
|
|
var properties = {};
|
|
matches.map(function (string) {
|
|
properties[string] = string.slice(1, -1);
|
|
});
|
|
return properties;
|
|
}
|
|
},
|
|
/**
|
|
* Clear input value(s).
|
|
*/
|
|
clear: function (alias, init) {
|
|
var current = _this.value,
|
|
options = _this.options.get();
|
|
|
|
if (options.multiple) {
|
|
this.multiple.removeAll();
|
|
}
|
|
|
|
_this.value = '';
|
|
if (alias) {
|
|
$alias[0].value = '';
|
|
}
|
|
if (current !== '' && !init) {
|
|
$this
|
|
.trigger('change:flexdatalist', [{value: '', text: ''}, options])
|
|
.trigger('clear:flexdatalist', [{value: '', text: ''}, options])
|
|
.trigger('change');
|
|
}
|
|
_values = [];
|
|
return this;
|
|
},
|
|
/**
|
|
* Value to object.
|
|
*/
|
|
toObj: function (val) {
|
|
if (typeof val !== 'object') {
|
|
var options = _this.options.get();
|
|
if (_this.isEmpty(val) || !_this.isDefined(val)) {
|
|
val = options.multiple ? [] : (this.isJSON() ? {} : '');
|
|
} else if (this.isCSV()) {
|
|
val = val.toString().split(options.valuesSeparator);
|
|
val = val.map(function (v) {
|
|
return v.trim();
|
|
});
|
|
} else if ((this.isMixed() || this.isJSON()) && this.isJSON(val)) {
|
|
val = JSON.parse(val);
|
|
} else if (typeof val === 'number') {
|
|
val = val.toString();
|
|
}
|
|
}
|
|
return val;
|
|
},
|
|
/**
|
|
* Is value expected to be JSON (either object or string).
|
|
*/
|
|
toStr: function (val) {
|
|
if (typeof val !== 'string') {
|
|
if (_this.isEmpty(val) || !_this.isDefined(val)) {
|
|
val = '';
|
|
} else if (typeof val === 'number') {
|
|
val = val.toString();
|
|
} else if (this.isCSV()) {
|
|
val = val.join(_this.options.get('valuesSeparator'));
|
|
} else if (this.isJSON() || this.isMixed()) {
|
|
val = JSON.stringify(val);
|
|
}
|
|
}
|
|
return $.trim(val);
|
|
},
|
|
/**
|
|
* If argument is passed, will check if is a valid JSON object/string.
|
|
* otherwise will check if JSON is the value expected for input
|
|
*/
|
|
isJSON: function (str) {
|
|
if (typeof str !== 'undefined') {
|
|
if (_this.isObject(str)) {
|
|
str = JSON.stringify(str);
|
|
} else if (typeof str !== 'string') {
|
|
return false;
|
|
}
|
|
return (str.indexOf('{') === 0 || str.indexOf('[{') === 0);
|
|
}
|
|
var options = _this.options.get(),
|
|
prop = options.valueProperty;
|
|
return (_this.isObject(prop) || prop === '*');
|
|
},
|
|
/**
|
|
* Is value expected to be JSON (either object or string).
|
|
*/
|
|
isMixed: function () {
|
|
var options = _this.options.get();
|
|
return !options.selectionRequired && options.valueProperty === '*';
|
|
},
|
|
/**
|
|
* Is value expected to be CSV?
|
|
*/
|
|
isCSV: function () {
|
|
return (!this.isJSON() && _this.options.get('multiple'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Data.
|
|
*/
|
|
this.data = {
|
|
/**
|
|
* Load data from all sources.
|
|
*/
|
|
load: function (callback, load) {
|
|
var __this = this,
|
|
data = [];
|
|
$this.trigger('before:flexdatalist.data');
|
|
// Remote data
|
|
this.url(function (remote) {
|
|
data = data.concat(remote);
|
|
// Static data
|
|
__this.static(function (_static) {
|
|
data = data.concat(_static);
|
|
// Datalist
|
|
__this.datalist(function (list) {
|
|
data = data.concat(list);
|
|
|
|
$this.trigger('after:flexdatalist.data', [data]);
|
|
callback(data);
|
|
});
|
|
});
|
|
}, load);
|
|
},
|
|
/**
|
|
* Get static data.
|
|
*/
|
|
static: function (callback) {
|
|
var __this = this,
|
|
options = _this.options.get();
|
|
// Remote source
|
|
if (typeof options.data === 'string') {
|
|
var url = options.data,
|
|
cache = _this.cache.read(url, true);
|
|
if (cache) {
|
|
callback(cache);
|
|
return;
|
|
}
|
|
this.remote({
|
|
url: url,
|
|
success: function (data) {
|
|
options.data = data;
|
|
callback(data);
|
|
_this.cache.write(url, data, options.cacheLifetime, true);
|
|
}
|
|
});
|
|
} else {
|
|
if (typeof options.data !== 'object') {
|
|
options.data = [];
|
|
}
|
|
callback(options.data);
|
|
}
|
|
},
|
|
/**
|
|
* Get datalist values.
|
|
*/
|
|
datalist: function (callback) {
|
|
var list = $this.attr('list'),
|
|
datalist = [];
|
|
if (!_this.isEmpty(list)) {
|
|
$('#' + list).find('option').each(function () {
|
|
var $option = $(this),
|
|
val = $option.val(),
|
|
label = $option.text();
|
|
datalist.push({
|
|
label: (label.length > 0 ? label : val),
|
|
value: val
|
|
});
|
|
});
|
|
}
|
|
callback(datalist);
|
|
},
|
|
/**
|
|
* Get remote data.
|
|
*/
|
|
url: function (callback, load) {
|
|
var keyword = $alias.val(),
|
|
options = _this.options.get(),
|
|
keywordParamName = options.keywordParamName,
|
|
searchContainParamName = options.searchContainParamName,
|
|
value = _this.fvalue.get(),
|
|
relatives = this.relativesData();
|
|
|
|
if (_this.isEmpty(options.url)) {
|
|
return callback([]);
|
|
}
|
|
|
|
var _opts = {};
|
|
if (options.requestType === 'post') {
|
|
$.each(options, function (option, value) {
|
|
if (option.indexOf('_') == 0 || option == 'data') {
|
|
return;
|
|
}
|
|
_opts[option] = value;
|
|
});
|
|
delete _opts.relatives;
|
|
}
|
|
|
|
// Cache
|
|
var cacheKey = _this.cache.keyGen({
|
|
relative: relatives,
|
|
load: load,
|
|
keyword: keyword,
|
|
contain: options.searchContain
|
|
}, options.url),
|
|
cache = _this.cache.read(cacheKey, true);
|
|
|
|
if (cache) {
|
|
callback(cache);
|
|
return;
|
|
}
|
|
|
|
var params = typeof(options.params) == 'function' ?
|
|
options.params.call($this[0], keyword) :
|
|
options.params;
|
|
|
|
var data = $.extend(
|
|
relatives,
|
|
params,
|
|
{
|
|
load: load,
|
|
selected: value,
|
|
original: options.originalValue,
|
|
options: _opts
|
|
}
|
|
);
|
|
|
|
data[keywordParamName] = keyword;
|
|
data[searchContainParamName] = options.searchContain;
|
|
|
|
this.remote({
|
|
url: options.url,
|
|
data: data,
|
|
success: function (_data) {
|
|
var _keyword = $alias.val();
|
|
// Is this really needed?
|
|
if (_keyword.length >= keyword.length) {
|
|
callback(_data);
|
|
}
|
|
_this.cache.write(cacheKey, _data, options.cacheLifetime, true);
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* AJAX request.
|
|
*/
|
|
remote: function (settings) {
|
|
var __this = this,
|
|
options = _this.options.get();
|
|
|
|
// Prevent get data when pressing backspace button
|
|
if ($this.hasClass('flexdatalist-loading')) {
|
|
return;
|
|
}
|
|
$this.addClass('flexdatalist-loading');
|
|
|
|
if (options.requestContentType === 'json') {
|
|
settings.data = JSON.stringify(settings.data);
|
|
}
|
|
|
|
$.ajax($.extend(
|
|
{
|
|
type: options.requestType,
|
|
dataType: 'json',
|
|
headers: options.requestHeaders,
|
|
contentType: 'application/' + options.requestContentType + '; charset=UTF-8',
|
|
complete: function () {
|
|
$this.removeClass('flexdatalist-loading');
|
|
}
|
|
}, settings, {
|
|
success: function (data) {
|
|
data = __this.extractRemoteData(data);
|
|
settings.success(data);
|
|
}
|
|
}
|
|
));
|
|
},
|
|
/**
|
|
* Extract remote data from server response.
|
|
*/
|
|
extractRemoteData: function (data) {
|
|
var options = _this.options.get(),
|
|
_data = _this.isDefined(data, options.resultsProperty) ? data[options.resultsProperty] : data;
|
|
|
|
if (typeof _data === 'string' && _data.indexOf('[{') === 0) {
|
|
_data = JSON.parse(_data);
|
|
}
|
|
if (_data && _data.options) {
|
|
_this.options.set($.extend({}, options, _data.options));
|
|
}
|
|
if (_this.isObject(_data)) {
|
|
return _data;
|
|
}
|
|
return [];
|
|
},
|
|
/**
|
|
* Extract remote data from server response.
|
|
*/
|
|
relativesData: function () {
|
|
var relatives = _this.options.get('relatives'),
|
|
data = {};
|
|
if (relatives) {
|
|
data['relatives'] = {};
|
|
relatives.each(function () {
|
|
var $_input = $(this),
|
|
name = $_input.attr('name')
|
|
.split('][').join('-')
|
|
.split(']').join('-')
|
|
.split('[').join('-')
|
|
.replace(/^\|+|\-+$/g, '');
|
|
data['relatives'][name] = $_input.val();
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search.
|
|
*/
|
|
this.search = {
|
|
/**
|
|
* Search for keywords in data and return matches to given callback.
|
|
*/
|
|
get: function (keywords, data, callback) {
|
|
var __this = this,
|
|
options = _this.options.get(),
|
|
matches = data;
|
|
|
|
if (!options.searchDisabled) {
|
|
var matches = _this.cache.read(keywords);
|
|
if (!matches) {
|
|
$this.trigger('before:flexdatalist.search', [keywords, data]);
|
|
if (!_this.isEmpty(keywords)) {
|
|
matches = [];
|
|
var words = __this.split(keywords);
|
|
for (var index = 0; index < data.length; index++) {
|
|
var item = data[index];
|
|
if (_this.isDup(item)) {
|
|
continue;
|
|
}
|
|
item = __this.matches(item, words);
|
|
if (item) {
|
|
matches.push(item);
|
|
}
|
|
}
|
|
}
|
|
_this.cache.write(keywords, matches, 2);
|
|
$this.trigger('after:flexdatalist.search', [keywords, data, matches]);
|
|
}
|
|
}
|
|
|
|
callback(matches);
|
|
},
|
|
/**
|
|
* Match against searchable properties.
|
|
*/
|
|
matches: function (item, keywords) {
|
|
var _item = $.extend({}, item),
|
|
found = [],
|
|
options = _this.options.get(),
|
|
searchIn = options.searchIn;
|
|
|
|
if (keywords.length > 0) {
|
|
for (var index = 0; index < searchIn.length; index++) {
|
|
var searchProperty = searchIn[index];
|
|
if (!_this.isDefined(item, searchProperty) || !item[searchProperty]) {
|
|
continue;
|
|
}
|
|
|
|
var text = item[searchProperty].toString(),
|
|
highlight = text,
|
|
strings = this.split(text);
|
|
|
|
for (var kwindex = 0; kwindex < keywords.length; kwindex++) {
|
|
var keyword = keywords[kwindex];
|
|
if (this.find(keyword, strings)) {
|
|
found.push(keyword);
|
|
highlight = this.highlight(keyword, highlight);
|
|
}
|
|
}
|
|
|
|
if (highlight !== text) {
|
|
_item[searchProperty + '_highlight'] = this.highlight(highlight);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (found.length === 0 || (options.searchByWord && found.length < (keywords.length - 1))) {
|
|
return false;
|
|
}
|
|
|
|
return _item;
|
|
},
|
|
/**
|
|
* Wrap found keyword with span.highlight.
|
|
*/
|
|
highlight: function (keyword, text) {
|
|
if (text) {
|
|
// Fix by https://github.com/antunesl
|
|
keyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
return text.replace(
|
|
new RegExp(keyword, (_this.options.get('searchContain') ? "ig" : "i")),
|
|
'|:|$&|::|'
|
|
);
|
|
}
|
|
keyword = keyword.split('|:|').join('<span class="highlight">');
|
|
return keyword.split('|::|').join('</span>');
|
|
},
|
|
/**
|
|
* Search for keyword(s) in string.
|
|
*/
|
|
find: function (keyword, splitted) {
|
|
var options = _this.options.get();
|
|
for (var index = 0; index < splitted.length; index++) {
|
|
var text = splitted[index];
|
|
text = this.normalizeString(text),
|
|
keyword = this.normalizeString(keyword);
|
|
if (options.searchEqual) {
|
|
return text == keyword;
|
|
}
|
|
if ((options.searchContain ? (text.indexOf(keyword) >= 0) : (text.indexOf(keyword) === 0))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
/**
|
|
* Split string by words if needed.
|
|
*/
|
|
split: function (keywords) {
|
|
if (typeof keywords === 'string') {
|
|
keywords = [$.trim(keywords)];
|
|
}
|
|
if (_this.options.get('searchByWord')) {
|
|
for (var index = 0; index < keywords.length; index++) {
|
|
var keyword = $.trim(keywords[index]);
|
|
if (keyword.indexOf(' ') > 0) {
|
|
var words = keyword.split(' ');
|
|
$.merge(keywords, words);
|
|
}
|
|
}
|
|
}
|
|
return keywords;
|
|
},
|
|
/**
|
|
* Normalize string to a consistent one to perform the search/match.
|
|
*/
|
|
normalizeString: function (string) {
|
|
if (typeof string === 'string') {
|
|
var normalizeString = _this.options.get('normalizeString');
|
|
if (typeof normalizeString === 'function') {
|
|
string = normalizeString(string);
|
|
}
|
|
string = string.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
return string.toUpperCase();
|
|
}
|
|
return string;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle results.
|
|
*/
|
|
this.results = {
|
|
/**
|
|
* Save key = value data in local storage (if supported)
|
|
*
|
|
* @param string key Data key string
|
|
*/
|
|
show: function (results) {
|
|
var __this = this,
|
|
options = _this.options.get();
|
|
|
|
this.remove(true);
|
|
|
|
if (!results) {
|
|
return;
|
|
} else if(results.length === 0) {
|
|
this.empty(options.noResultsText);
|
|
return;
|
|
}
|
|
|
|
var $ul = this.container();
|
|
if (!options.groupBy) {
|
|
this.items(results, $ul);
|
|
} else {
|
|
results = this.group(results);
|
|
Object.keys(results).forEach(function (groupName, index) {
|
|
var items = results[groupName],
|
|
property = options.groupBy,
|
|
groupText = _this.results.highlight(items[0], property, groupName);
|
|
|
|
var $li = $('<li>')
|
|
.addClass('group')
|
|
.append($('<span>')
|
|
.addClass('group-name')
|
|
.html(groupText)
|
|
)
|
|
.append($('<span>')
|
|
.addClass('group-item-count')
|
|
.text(' ' + items.length)
|
|
)
|
|
.appendTo($ul);
|
|
|
|
__this.items(items, $ul);
|
|
});
|
|
}
|
|
|
|
var $li = $ul.find('li:not(.group)');
|
|
|
|
// Listen to result's item events
|
|
$li.on('click', function (event) {
|
|
var item = $(this).data('item');
|
|
if (item) {
|
|
_this.fvalue.extract(item);
|
|
__this.remove();
|
|
$this.trigger('select:flexdatalist', [item, options]);
|
|
}
|
|
}).on('hover', function () {
|
|
$li.removeClass('active');
|
|
$(this).addClass('active').trigger('active:flexdatalist.results', [$(this).data('item')]);
|
|
}, function () {
|
|
$(this).removeClass('active');
|
|
});
|
|
|
|
if (options.focusFirstResult) {
|
|
$li.filter(':first').addClass('active');
|
|
}
|
|
},
|
|
/**
|
|
* Remove results container.
|
|
*/
|
|
empty: function (text) {
|
|
if (_this.isEmpty(text)) {
|
|
return;
|
|
}
|
|
var $container = this.container(),
|
|
keyword = $alias.val();
|
|
|
|
text = text.split('{keyword}').join(keyword);
|
|
$('<li>')
|
|
.addClass('item no-results')
|
|
.append(text)
|
|
.appendTo($container)
|
|
|
|
$this.trigger('empty:flexdatalist.results', [text]);
|
|
},
|
|
/**
|
|
* Items iteration.
|
|
*/
|
|
items: function (items, $resultsContainer) {
|
|
var max = _this.options.get('maxShownResults');
|
|
|
|
$this.trigger('show:flexdatalist.results', [items]);
|
|
|
|
for (var index = 0; index < items.length; index++) {
|
|
if (max > 0 && max === index) {
|
|
break;
|
|
}
|
|
this.item(items[index], index, items.length).appendTo($resultsContainer);
|
|
}
|
|
|
|
$this.trigger('shown:flexdatalist.results', [items]);
|
|
},
|
|
/**
|
|
* Result item creation.
|
|
*/
|
|
item: function (item, index, total) {
|
|
var $li = $('<li>')
|
|
.attr({
|
|
'role': 'option',
|
|
'tabindex': '-1',
|
|
'aria-posinset': index + 1,
|
|
'aria-setsize': total
|
|
})
|
|
.data('item', item)
|
|
.addClass('item'),
|
|
options = _this.options.get(),
|
|
visibleProperties = options.visibleProperties;
|
|
|
|
for (var index = 0; index < visibleProperties.length; index++) {
|
|
var visibleProperty = visibleProperties[index];
|
|
|
|
if (visibleProperty.indexOf('{') > -1) {
|
|
var str = _this.fvalue.placeholders.replace(item, visibleProperty),
|
|
parsed = _this.fvalue.placeholders.parse(visibleProperty);
|
|
$item = $('<span>')
|
|
.addClass('item item-' + Object.values(parsed).join('-'))
|
|
.html(str + ' ');
|
|
} else {
|
|
if (options.groupBy && options.groupBy === visibleProperty || !_this.isDefined(item, visibleProperty)) {
|
|
continue;
|
|
}
|
|
var $item = {};
|
|
if (visibleProperty === options.iconProperty) {
|
|
// Icon
|
|
$item = $('<img>')
|
|
.addClass('item item-' + visibleProperty)
|
|
.attr('src', item[visibleProperty]);
|
|
} else {
|
|
var propertyText = _this.results.highlight(item, visibleProperty);
|
|
// Other text properties
|
|
$item = $('<span>')
|
|
.addClass('item item-' + visibleProperty)
|
|
.html(propertyText + ' ');
|
|
}
|
|
}
|
|
|
|
$item.appendTo($li);
|
|
}
|
|
|
|
$this.trigger('item:flexdatalist.results', [$li, item]);
|
|
|
|
return $li;
|
|
},
|
|
/**
|
|
* Results container
|
|
*/
|
|
container: function () {
|
|
var $target = $this;
|
|
|
|
if ($multiple) {
|
|
$target = $multiple;
|
|
}
|
|
|
|
var $container = $('ul.flexdatalist-results');
|
|
|
|
if ($container.length === 0) {
|
|
$container = $('<ul>')
|
|
.addClass('flexdatalist-results ')
|
|
.appendTo('body')
|
|
.attr({
|
|
'id': $alias.attr('id') + '-results',
|
|
'role': 'listbox'
|
|
})
|
|
.css({
|
|
'border-color': $target.css("border-left-color"),
|
|
'border-width': '1px',
|
|
'border-bottom-left-radius': $target.css("border-bottom-left-radius"),
|
|
'border-bottom-right-radius': $target.css("border-bottom-right-radius")
|
|
}).data({
|
|
target: ($multiple ? $multiple : $alias),
|
|
input: $this
|
|
});
|
|
_this.position($alias);
|
|
}
|
|
|
|
return $container;
|
|
},
|
|
/**
|
|
* Results container
|
|
*/
|
|
group: function (results) {
|
|
var data = [],
|
|
groupProperty = _this.options.get('groupBy');
|
|
|
|
for (var index = 0; index < results.length; index++) {
|
|
var _data = results[index];
|
|
if (_this.isDefined(_data, groupProperty)) {
|
|
var propertyValue = _data[groupProperty];
|
|
if (!_this.isDefined(data, propertyValue)) {
|
|
data[propertyValue] = [];
|
|
}
|
|
data[propertyValue].push(_data);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
},
|
|
/**
|
|
* Check if highlighted property value exists,
|
|
* if true, return it, if not, fallback to given string
|
|
*/
|
|
highlight: function (item, property, fallback) {
|
|
if (_this.isDefined(item, property + '_highlight')) {
|
|
return item[property + '_highlight'];
|
|
}
|
|
return (_this.isDefined(item, property) ? item[property] : fallback);
|
|
},
|
|
/**
|
|
* Set given item as active
|
|
*/
|
|
active: function ($item) {
|
|
|
|
},
|
|
/**
|
|
* Remove results
|
|
*/
|
|
remove: function (itemsOnly) {
|
|
var selector = 'ul.flexdatalist-results';
|
|
if (itemsOnly) {
|
|
selector = 'ul.flexdatalist-results li';
|
|
}
|
|
$this.trigger('remove:flexdatalist.results');
|
|
$(selector).remove();
|
|
$this.trigger('removed:flexdatalist.results');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface for localStorage.
|
|
*/
|
|
this.cache = {
|
|
/**
|
|
* Save key = value data in local storage (if supported)
|
|
*
|
|
* @param string key Data key string
|
|
* @param mixed value Value to be saved
|
|
* @param int lifetime In Seconds
|
|
* @return mixed
|
|
*/
|
|
write: function (key, value, lifetime, global) {
|
|
if (_this.cache.isSupported()) {
|
|
key = this.keyGen(key, undefined, global);
|
|
var object = {
|
|
value: value,
|
|
// Get current UNIX timestamp
|
|
timestamp: _this.unixtime(),
|
|
lifetime: (lifetime ? lifetime : false)
|
|
};
|
|
localStorage.setItem(key, JSON.stringify(object));
|
|
}
|
|
},
|
|
/**
|
|
* Read data associated with given key
|
|
*
|
|
* @param string key Data key string
|
|
* @return mixed
|
|
*/
|
|
read: function (key, global) {
|
|
if (_this.cache.isSupported()) {
|
|
key = this.keyGen(key, undefined, global);
|
|
var data = localStorage.getItem(key);
|
|
if (data) {
|
|
var object = JSON.parse(data);
|
|
if (!this.expired(object)) {
|
|
return object.value;
|
|
}
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
/**
|
|
* Remove data associated with given key.
|
|
*
|
|
* @param string key Data key string
|
|
*/
|
|
delete: function (key, global) {
|
|
if (_this.cache.isSupported()) {
|
|
key = this.keyGen(key, undefined, global);
|
|
localStorage.removeItem(key);
|
|
}
|
|
},
|
|
/**
|
|
* Clear all data.
|
|
*/
|
|
clear: function () {
|
|
if (_this.cache.isSupported()) {
|
|
for (var key in localStorage){
|
|
if (key.indexOf(fid) > -1 || key.indexOf('global') > -1) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
localStorage.clear();
|
|
}
|
|
},
|
|
/**
|
|
* Run cache garbage collector to prevent using all localStorage's
|
|
* available space.
|
|
*/
|
|
gc: function () {
|
|
if (_this.cache.isSupported()) {
|
|
for (var key in localStorage){
|
|
if (key.indexOf(fid) > -1 || key.indexOf('global') > -1) {
|
|
var data = localStorage.getItem(key);
|
|
data = JSON.parse(data);
|
|
if (this.expired(data)) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Check if browser supports localtorage.
|
|
*
|
|
* @return boolean True if supports, false otherwise
|
|
*/
|
|
isSupported: function () {
|
|
if (_this.options.get('cache')) {
|
|
try {
|
|
return 'localStorage' in window && window['localStorage'] !== null;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
/**
|
|
* Check if cache data as expired.
|
|
*
|
|
* @param object object Data to check
|
|
* @return boolean True if expired, false otherwise
|
|
*/
|
|
expired: function (object) {
|
|
if (object.lifetime) {
|
|
var diff = (_this.unixtime() - object.timestamp);
|
|
return object.lifetime <= diff;
|
|
}
|
|
return false;
|
|
},
|
|
/**
|
|
* Generate cache key from object or string.
|
|
*
|
|
* @return string Cache key
|
|
*/
|
|
keyGen: function (str, seed, global) {
|
|
if (typeof str === 'object') {
|
|
str = JSON.stringify(str);
|
|
}
|
|
var i, l,
|
|
hval = (seed === undefined) ? 0x811c9dc5 : seed;
|
|
|
|
for (i = 0, l = str.length; i < l; i++) {
|
|
hval ^= str.charCodeAt(i);
|
|
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
|
|
}
|
|
return (global ? 'global' : fid) + ("0000000" + (hval >>> 0).toString(16)).substr(-8);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Options handler.
|
|
*/
|
|
this.options = {
|
|
init: function () {
|
|
var options = $.extend({},
|
|
_options,
|
|
$this.data(),
|
|
{
|
|
multiple: (_options.multiple === null ? $this.is('[multiple]') : _options.multiple),
|
|
disabled: (_options.disabled === null ? $this.is('[disabled]') : _options.disabled),
|
|
originalValue: _this.value
|
|
}
|
|
);
|
|
this.set(options);
|
|
return options;
|
|
},
|
|
get: function (option) {
|
|
var options = $this.data('flexdatalist');
|
|
if (!option) {
|
|
return options ? options : {};
|
|
}
|
|
return _this.isDefined(options, option) ? options[option] : null;
|
|
},
|
|
set: function (option, value) {
|
|
var options = this.get();
|
|
if (_this.isDefined(options, option) && _this.isDefined(value)) {
|
|
options[option] = value;
|
|
} else if (_this.isObject(option)) {
|
|
options = this._normalize(option);
|
|
}
|
|
$this.data('flexdatalist', options);
|
|
return $this;
|
|
},
|
|
_normalize: function (options) {
|
|
options.searchIn = _this.csvToArray(options.searchIn);
|
|
options.relatives = options.relatives && $(options.relatives).length > 0 ? $(options.relatives) : null;
|
|
options.textProperty = options.textProperty === null ? options.searchIn[0] : options.textProperty;
|
|
options.visibleProperties = _this.csvToArray(options.visibleProperties, options.searchIn);
|
|
if (options.valueProperty === '*' && options.multiple && !options.selectionRequired) {
|
|
throw new Error('Selection must be required for multiple, JSON fields!');
|
|
}
|
|
return options;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Position results below parent element.
|
|
*/
|
|
this.position = function () {
|
|
var $results = $('ul.flexdatalist-results'),
|
|
$target = $results.data('target');
|
|
if ($results.length > 0) {
|
|
// Set some required CSS properties
|
|
$results.css({
|
|
'width': $target.outerWidth() + 'px',
|
|
'top': (($target.offset().top + $target.outerHeight())) + 'px',
|
|
'left': $target.offset().left + 'px'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle disabled state.
|
|
*/
|
|
this.fdisabled = function (disabled) {
|
|
if (this.isDefined(disabled)) {
|
|
$this.prop('disabled', disabled);
|
|
$alias.prop('disabled', disabled);
|
|
if ($multiple) {
|
|
$multiple.css('background-color', $this.css('background-color'));
|
|
var $btns = $multiple.find('li .fdl-remove'),
|
|
$input = $multiple.find('li.input-container');
|
|
if (disabled) {
|
|
$multiple.addClass('disabled');
|
|
if ($btns.length > 0) {
|
|
$input.hide();
|
|
}
|
|
$btns.hide();
|
|
} else {
|
|
$multiple.removeClass('disabled');
|
|
$input.show();
|
|
$btns.show();
|
|
}
|
|
}
|
|
this.options.set('disabled', disabled);
|
|
}
|
|
return this.options.get('disabled');
|
|
}
|
|
|
|
/**
|
|
* Check for dup values.
|
|
*/
|
|
this.isDup = function (val) {
|
|
if (!this.options.get('allowDuplicateValues')) {
|
|
return _values.length > 0 && _values.indexOf(this.fvalue.text(val)) > -1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get key code from event.
|
|
*/
|
|
this.keyNum = function (event) {
|
|
return event.which || event.keyCode;
|
|
}
|
|
|
|
/**
|
|
* Is variable empty.
|
|
*/
|
|
this.isEmpty = function (value) {
|
|
if (!_this.isDefined(value)) {
|
|
return true;
|
|
} else if (value === null) {
|
|
return true;
|
|
} else if (value === true) {
|
|
return false;
|
|
} else if (this.length(value) === 0) {
|
|
return true;
|
|
} else if ($.trim(value) === '') {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Is variable an object.
|
|
*/
|
|
this.isObject = function (value) {
|
|
return (value && typeof value === 'object');
|
|
}
|
|
|
|
/**
|
|
* Get length of variable.
|
|
*/
|
|
this.length = function (value) {
|
|
if (this.isObject(value)) {
|
|
return Object.keys(value).length;
|
|
} else if (typeof value === 'number' || typeof value.length === 'number') {
|
|
return value.toString().length;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if variable (and optionally property) is defined.
|
|
*/
|
|
this.isDefined = function (variable, property) {
|
|
var _variable = (typeof variable !== 'undefined');
|
|
if (_variable && typeof property !== 'undefined') {
|
|
return (typeof this.getPropertyValue(variable, property) !== 'undefined');
|
|
}
|
|
return _variable;
|
|
}
|
|
|
|
/**
|
|
* Check if variable is an array.
|
|
*/
|
|
this.isArray = function (variable) {
|
|
return Object.prototype.toString.call(variable) === '[object Array]';
|
|
}
|
|
|
|
/**
|
|
* Get unixtime stamp.
|
|
*
|
|
* @return boolean True if supports, false otherwise
|
|
*/
|
|
this.unixtime = function (time) {
|
|
var date = new Date();
|
|
if (time) {
|
|
date = new Date(time);
|
|
}
|
|
return Math.round(date.getTime() / 1000);
|
|
}
|
|
|
|
/**
|
|
* To array.
|
|
*/
|
|
this.csvToArray = function (value, _default) {
|
|
if (value.length === 0) {
|
|
return _default;
|
|
}
|
|
return typeof value === 'string' ? value.split(_this.options.get('valuesSeparator')) : value;
|
|
},
|
|
/**
|
|
* A function to take a string written in dot notation style, and use it to
|
|
* find a nested object property inside of an object.
|
|
*
|
|
* Useful in a plugin or module that accepts a JSON array of objects, but
|
|
* you want to let the user specify where to find various bits of data
|
|
* inside of each custom object instead of forcing a standardized
|
|
* property list.
|
|
*
|
|
* Thanks to https://github.com/sylvainblot for the PR.
|
|
*
|
|
* @param object object (optional) The object to search
|
|
* @param string path A dot notation style path to the value (ie "urls.small")
|
|
* @return the value of the property in question
|
|
* @see https://github.com/sergiodlopes/jquery-flexdatalist/pull/195
|
|
*/
|
|
this.getPropertyValue = function (obj, path) {
|
|
if (!obj || typeof path !== 'string') {
|
|
return undefined;
|
|
}
|
|
|
|
var parts = path.split('.');
|
|
while (parts.length && (obj = obj[parts.shift()]));
|
|
return obj;
|
|
}
|
|
/**
|
|
* Escape HTML special characters.
|
|
*
|
|
* @param string str String to escape HTML
|
|
* @return string String with HTML special characters escaped
|
|
*/
|
|
this.escapeHtml = function (str) {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Plugin warnings for debug.
|
|
*/
|
|
this.debug = function (msg, data) {
|
|
var options = _this.options.get();
|
|
if (!options.debug) {
|
|
return;
|
|
}
|
|
if (!data) {
|
|
data = {};
|
|
}
|
|
msg = 'Flexdatalist: ' + msg;
|
|
console.warn(msg);
|
|
console.log($.extend({
|
|
inputName: $this.attr('name'),
|
|
options: options
|
|
}, data));
|
|
console.log('--- /flexdatalist ---');
|
|
}
|
|
|
|
// Go!
|
|
this.init();
|
|
});
|
|
}
|
|
|
|
jQuery(function ($) {
|
|
var $document = $(document);
|
|
// Handle results selection list keyboard shortcuts and events.
|
|
if (!$document.data('flexdatalist')) {
|
|
// Remove results on outside click
|
|
$(document).on('mouseup', function (event) {
|
|
var $container = $('.flexdatalist-results'),
|
|
$target = $container.data('target');
|
|
if ((!$target || !$target.is(':focus')) && !$container.is(event.target) && $container.has(event.target).length === 0) {
|
|
$container.remove();
|
|
}
|
|
// Keyboard navigation
|
|
}).on('keydown', function (event) {
|
|
var $ul = $('.flexdatalist-results'),
|
|
$li = $ul.find('li'),
|
|
$active = $li.filter('.active'),
|
|
index = $active.index(),
|
|
length = $li.length,
|
|
keynum = event.which || event.keyCode;
|
|
|
|
if (length === 0) {
|
|
return;
|
|
}
|
|
|
|
// on escape key, remove results
|
|
if (keynum === 27) {
|
|
$ul.remove();
|
|
return;
|
|
}
|
|
|
|
// Enter/tab key
|
|
if (keynum === 13) {
|
|
event.preventDefault();
|
|
$active.trigger('click');
|
|
// Up/Down key
|
|
} else if (keynum === 40 || keynum === 38) {
|
|
event.preventDefault();
|
|
// Down key
|
|
if (keynum === 40) {
|
|
if (index < length && $active.nextAll('.item').first().length > 0) {
|
|
$active = $active.removeClass('active').nextAll('.item').first().addClass('active');
|
|
} else {
|
|
$active = $li.removeClass('active').filter('.item:first').addClass('active');
|
|
}
|
|
// Up key
|
|
} else if (keynum === 38) {
|
|
if (index > 0 && $active.prevAll('.item').first().length > 0) {
|
|
$active = $active.removeClass('active').prevAll('.item').first().addClass('active');
|
|
} else {
|
|
$active = $li.removeClass('active').filter('.item:last').addClass('active');
|
|
}
|
|
}
|
|
|
|
$active.trigger('active:flexdatalist.results', [$active.data('item')]);
|
|
|
|
// Scroll to
|
|
var position = ($active.prev().length === 0 ? $active : $active.prev()).position().top;
|
|
$ul.animate({
|
|
scrollTop: position + $ul.scrollTop()
|
|
}, 100);
|
|
}
|
|
}).data('flexdatalist', true);
|
|
}
|
|
|
|
jQuery('input.flexdatalist:not(.flexdatalist-set):not(.autodiscover-disabled)').flexdatalist();
|
|
});
|
|
|
|
(function ($) {
|
|
var jVal = $.fn.val;
|
|
$.fn.val = function (value) {
|
|
var isFlex = this.length > 0 && typeof this[0].fvalue !== 'undefined';
|
|
if (typeof value === 'undefined') {
|
|
return isFlex ? this[0].fvalue.get(true) : jVal.call(this);
|
|
}
|
|
return isFlex ? this[0].fvalue.set(value) : jVal.call(this, value);
|
|
};
|
|
})(jQuery);
|