<NME> jquery.flexdatalist.js <BEF> /** * jQuery Flexdatalist. * Autocomplete input fields, with support for datalists. * * Version: * 2.3.1 * * 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(); } // Destroy if already set if (typeof this[0] !== 'undefined' && typeof this[0].fvalue !== 'undefined') { destroy($(this)); } 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, searchEqual: false, searchByWord: false, searchDisabled: false, searchDelay: 200, normalizeString: null, multiple: false, maxShownResults: 100, valueProperty: null, visibleProperties: [], iconProperty: 'thumb', searchIn: ['label'], searchContain: false, searchEqual: false, searchByWord: false, searchDisabled: false, searchDelay: 400, normalizeString: null, return this.each(function () { var $this = $(this), _this = this, _searchTimeout = null, $alias = null, $multiple = null, options = $this.data('flexdatalist'); if (!options) { options = $.extend({}, _options, $this.data(), { multiple: $this.is('[multiple]'), originalValue: $this.val(), _values: [] } ); var $this = $(this), _this = this, _searchTimeout = null, * Initialization */ this.init = function () { this.prepare(); this.chained(); this.fvalue.set($this.attr('value')); $alias // Focus .on('focus', function (event) { // Redo search on focus if not selected yet _this.control.redoSearchFocus(event); _this.control.showAllResults(event); }) // Focusin .on('focusin', function() { .on('focusin', function (event) { _this.action.redoSearchFocus(event); _this.action.showAllResults(event); if ($multiple) { $multiple.addClass('focus'); } if (_this.keyNum(event) === 9) { _this.results.remove(); } _this.control.backSpaceKeyRemove(event); }) // Keyup .on('input keyup', function (event) { _this.control.keypressSearch(event); _this.control.copyValue(event); _this.control.backSpaceKeyRemove(event); }) // Focusout .on('focusout', function() { // Keyup .on('input keyup', function (event) { var ignoreEvent = false; if (_this.action.keypressValue(event, 13)) { ignoreEvent = true; } _this.action.keypressSearch(event); _this.action.copyValue(event); _this.action.backSpaceKeyRemove(event); _this.action.showAllResults(event); _this.action.clearValue(event); } /** * Handle events. */ this.control = { /** * Check if keypress is valid. */ $multiple.removeClass('focus'); } _this.action.clearText(event); _this.action.clearValue(event); }); window.onresize = function (event) { _this.position(); // Ignore Enter and Directional keys } else if (!key || (key !== 13 && (key < 37 || key > 40))) { _searchTimeout = setTimeout(function () { if (length >= options.minLength) { _this.search(function (matches) { _this.results.show(matches); }); } else { this.fvalue._load(options.originalValue, function (values, matches) { _this.fdisabled(options.disabled); }, options.searchDelay); } }, /** * Redo search if input get's back on focus and no value selected. */ redoSearchFocus: function (event) { var val = $this.val(); if (val.length === 0) { this.keypressSearch(event); } }, /** * Check if keypress is valid. */ copyValue: function (event) { if (_this.keyNum(event) !== 13) { && !options.selectionRequired && options.multiple) { event.preventDefault(); event.stopPropagation(); var val = $alias[0].value; _this.fvalue.extract(val); } } }, /** * 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))) { } } }, /** * Show all results if minLength option is 0. */ }); var val = $alias.val(); val = $.trim(val); if (val === '' && options.minLength === 0) { _this.data(function (data) { _this.results.show(data); }); } } } /** * Prepare input replacement. */ this.prepare = function () { $alias = this.alias(); if (options.multiple) { $multiple = this.multipleInput($alias); } else { $alias.insertAfter($this); } // Respect autofocus attribute if ($alias.attr('autofocus')) { $alias.focus(); } } /** * Single value input. */ this.alias = function () { var $alias = $this .clone(false) .attr({ 'list': null, 'name': ($this.attr('name') ? $this.attr('name') + '-flexdatalist' : null), 'id': ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : null) }) .addClass('flexdatalist-alias') .removeClass('flexdatalist'); $this.addClass('flexdatalist flexdatalist-set').prop('type', 'hidden'); return $alias; } /** * Multiple values input/list */ this.multipleInput = function ($alias) { $multiple = $('<ul tabindex="1">') .addClass('flexdatalist-multiple') .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).click(function () { $(this).find('input').focus(); }); $('<li class="input-container">') .addClass('flexdatalist-multiple-value') .append($alias) .appendTo($multiple); return $multiple; } /** * Chained inputs handling. */ this.chained = function () { if (!options.relatives || !options.chainedRelatives) { return; } var toggle = function (init) { options.relatives.each(function () { var disabled = _this.isEmpty($(this).val()), empty = _this.isEmpty($this.val()); // If disabling, clear all values if (!init && (disabled || !empty)) { _this.fvalue.clear(); } if ($multiple) { disabled && $multiple ? $multiple.addClass('disabled') : $multiple.removeClass('disabled'); } else { $alias.prop('disabled', disabled); } }); }; options.relatives.on('change', function () { toggle(); }); toggle(true); } /** /** * Remove results when criteria is met. */ /** * Get value(s). */ get: function () { var val = $this.val(); if (options.multiple) { return this.toObj(val); } return this.toStr(val); }, /** * Parse given value. * Used on input init or when added directly. */ set: function (val, append) { var _fvalue = this; if (!append) { this.clear(true); } if (_this.isEmpty(val)) { return; } this._normalize(val, function (values) { if (!_this.isEmpty(values)) { if ($.isArray(values)) { $.each(values, function (i, value) { _fvalue.extract(value, true); }); } else { _fvalue.extract(values, true); } $this.trigger('change:flexdatalist', [ values, null, options ]).trigger('change'); } }); }, /** * Add value. */ add: function (val) { this.set(val, true); }, /** * Toggle value. */ toggle: function (val) { this.multiple.toggle(val); }, /** * Remove value. */ remove: function (val) { this.multiple.remove(val); }, /** * Normalize value. .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'); }); _searchEqual = options.searchEqual; options.searchIn = options.valueProperty.split(','); options.searchEqual = true; _this.search(function (matches) { if (matches.length > 0) { callback(matches); } options.searchIn = _searchIn; options.searchEqual = _searchEqual; }, data); } else { callback(data); } if (options.relatives && options.chainedRelatives) { var toggle = function (init) { options.relatives.each(function () { var emptyRelative = _this.isEmpty($(this).val()), extract: function (val, parse) { var txt = this.text(val), value = this.value(val), current = $this.val(); if (!_this.isEmpty(val)) { if (!value) { }; 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). if (txt && txt !== $alias.val()) { $alias.val(txt, true); } $this.val(val, true); }, /** * Input with multiple values. 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; }, /** val = _this.fvalue.toObj(val); current.push(val); val = _this.fvalue.toStr(current); $this.val(val, true); }, /** * Toggle value. }, /** * 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(), values.splice(index, 1); values = _this.fvalue.toStr(values); $this.val(values, true).trigger('change:flexdatalist', [ $li.data('value'), $li.data('text'), options 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) { */ removeAll: function (values) { $multiple.find('li:not(.input-container)').remove(); $this.val('', true); options._values = []; }, /** 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 { }); } return $li; } }, /** * @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 false; } val = _this.fvalue.toObj(val); current.push(val); if (options.multiple) { this.multiple.removeAll(); } var current = $this.val(); $this.val('', true); if (current !== '') { $this.trigger('change:flexdatalist', [ null, if (!options.toggleSelected) { return; } var $li = this.findLi(val); $alias.val('', true); } options._values = []; }, /** * Value to object */ toObj: function (val) { if (typeof val !== 'object') { if (_this.isEmpty(val)) { val = []; } else if (this.isCSV()) { val = val.toString().split(options.valuesSeparator); } 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; */ toStr: function (val) { if (typeof val !== 'string') { if (_this.isEmpty(val)) { val = ''; } else if (this.isCSV()) { val = val.join(options.valuesSeparator); * 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(); } /** * Get data. */ this.data = function (callback) { $this.trigger('before:flexdatalist.data'); // Remote data this.url(function (remote) { // Static data _this._data(function (data) { data = data.concat(remote); // Datalist _this.datalist(function (list) { data = list.concat(data); // Check for already set values if (!options.allowDuplicateValues) { var values = options._values; for (var i = 0; i < data.length; i++) { var item = data[i]; if (values && values.indexOf(_this.fvalue.text(item)) > -1) { delete data[i]; } } } $this.trigger('after:flexdatalist.data', [data]); callback(data); }); }); }); } /** * Get static data. */ this._data = function (callback) { // Remote source if (typeof options.data === 'string') { var url = options.data, cache = _this.cache.read(url); if (cache) { callback(cache); return; } _this.remote({ url: url, success: function (data) { var _data = _this.extractRemoteData(data); options.data = _data; callback(options.data); _this.cache.write(url, options.data, options.cacheLifetime); } }); } else { callback(options.data); } } /** * Get datalist values. */ this.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. */ this.url = function (callback) { var keyword = $alias.val(), value = $this.val(), cacheKey = keyword; if (_this.isEmpty(options.url)) { return callback([]); } if (options.cache && options.cache !== 2) { cacheKey = keyword.substring(0, (options.minLength > 0 ? options.minLength : 1)); } var _opts = {}; if (options.requestType === 'post') { $.each(options, function (option, value) { if (option.indexOf('_') == 0) { return; } _opts[option] = value; }); delete _opts.relatives; } this.remote({ url: options.url, data: $.extend( options.params, { keyword: keyword, contain: options.searchContain, selected: value, options: _opts } ), success: function (data) { var _data = _this.extractRemoteData(data), _keyword = $alias.val(); if (_keyword.length >= keyword.length) { callback(_data); } } }); } /** * AJAX request. */ this.remote = function (options) { // Prevent get data when pressing backspace button if ($this.hasClass('flexdatalist-loading')) { return; } $this.addClass('flexdatalist-loading'); options = $.extend({ type: options.requestType, dataType: 'json', complete: function () { $this.removeClass('flexdatalist-loading'); } }, options); $.ajax(options); } /** * Extract remote data from server response. */ this.extractRemoteData = function (data) { var _data = data.results ? data.results : data; if (typeof _data === 'string' && _data.indexOf('[{') === 0) { _data = JSON.parse(_data); } if (_data.options) { options = $.extend({}, options, _data.options); } if (this.isObject(_data)) { return _data; } return []; } /** * Search for keywords in data and return matches. */ this.search = function (callback, keywords) { this.data(function (data) { var matches = []; // If search disabled, return if (options.searchDisabled) { return callback(data); } if (!_this.isDefined(keywords)) { keywords = $alias.val(); } $this.trigger('before:flexdatalist.search', [keywords, data]); keywords = _this.split(keywords); for (var index = 0; index < data.length; index++) { var item = _this.matches(data[index], keywords); if (item) { matches.push(item); } } $this.trigger('after:flexdatalist.search', [keywords, data, matches]); callback(matches); }); } /** * Match against searchable properties. */ this.matches = function (item, keywords) { var hasMatches = false, _item = $.extend({}, item), found = [], 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. */ this.highlight = function (keyword, text) { keyword = keyword.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); if (text) { return text.replace( new RegExp(keyword, (options.searchContain ? "ig" : "i")), '|:|$&|::|' ); } keyword = keyword.split('|:|').join('<span class="highlight">'); return keyword.split('|::|').join('</span>'); } /** * Search for keyword(s) in string. */ this.find = function (keyword, strings) { for (var index = 0; index < strings.length; index++) { var text = strings[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. */ this.split = function (keywords) { if (typeof keywords === 'string') { keywords = [$.trim(keywords)]; } if (options.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. */ this.normalizeString = function (string) { if (typeof string === 'string') { var normalizeString = options.normalizeString; if (typeof normalizeString === 'function') { string = normalizeString(string); } return string.toUpperCase(); } return string; } /** searchContainParamName = options.searchContainParamName, value = _this.fvalue.get(), relatives = this.relativesData(); if (_this.isEmpty(options.url)) { return callback([]); } var _opts = {}; var __this = this; this.remove(true); if (results.length === 0) { this.empty(options.noResultsText); return; } // 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]) { } } /** * Position results below parent element. */ 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(); return _variable; } /** * Simple interface for sessionStorage. */ 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) { if (_this.cache.isSupported()) { var object = { value: value, // Get current UNIX timestamp timestamp: _this.unixtime(), lifetime: (lifetime ? lifetime : false) }; return sessionStorage.setItem(key, JSON.stringify(object)); } return null; }, /** * Read data associated with given key * * @param string key Data key string * @return mixed */ read: function (key) { if (_this.cache.isSupported()) { var data = sessionStorage.getItem(key); if (data) { var object = JSON.parse(data); if (object.lifetime) { var diff = (_this.unixtime() - object.timestamp); if (object.lifetime > diff) { return object.value; } _this.cache.delete(key); return null; } return object.value; } return null; } return null; }, /** * Remove data associated with given key * * @param string key Data key string * @return mixed */ delete: function (key) { if (_this.cache.isSupported()) { return sessionStorage.removeItem(key); } return null; }, /** * Check if browser supports sessionStorage * * @return boolean True if supports, false otherwise */ isSupported: function () { if (!options.cache) { return false; } try { return 'sessionStorage' in window && window['sessionStorage'] !== null; } catch (e) { return false; } } } /** * Get unixtime stamp. * $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 { // Go! this.init(); }); } .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]; jQuery('input.flexdatalist:not(.flexdatalist-set)').flexdatalist(); }); var _defaultValFunc = jQuery.fn.val; jQuery.fn.val = function (value, _flexdatalist) { if (!_flexdatalist && $(this).hasClass('flexdatalist-set') && typeof value !== 'undefined') { $(this)[0].fvalue.set(value); } return _defaultValFunc.apply(this, arguments); }; * 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; } /** * 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; } /** * Is variable an object. */ this.isObject = function (value) { return (value && typeof value === 'object'); } /** * 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, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } /** * 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); <MSG> 2.0.0 WIP <DFF> @@ -75,7 +75,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { } // Destroy if already set - if (typeof this[0] !== 'undefined' && typeof this[0].fvalue !== 'undefined') { + if (this && typeof this[0] !== 'undefined' && typeof this[0].fvalue !== 'undefined') { destroy($(this)); } @@ -100,7 +100,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { searchEqual: false, searchByWord: false, searchDisabled: false, - searchDelay: 200, + searchDelay: 300, normalizeString: null, multiple: false, maxShownResults: 100, @@ -117,18 +117,20 @@ jQuery.fn.flexdatalist = function (_option, _value) { return this.each(function () { var $this = $(this), _this = this, + _init = false, _searchTimeout = null, $alias = null, $multiple = null, options = $this.data('flexdatalist'); if (!options) { + $this.removeData('flexdatalist'); options = $.extend({}, _options, $this.data(), { multiple: $this.is('[multiple]'), - originalValue: $this.val(), + originalValue: $this[0].value, _values: [] } ); @@ -138,16 +140,14 @@ jQuery.fn.flexdatalist = function (_option, _value) { * Initialization */ this.init = function () { - this.prepare(); - this.chained(); + this.set.up(); this.fvalue.set($this.attr('value')); $alias // Focus .on('focus', function (event) { - // Redo search on focus if not selected yet - _this.control.redoSearchFocus(event); - _this.control.showAllResults(event); + _this.action.redoSearchFocus(event); + _this.action.showAllResults(event); }) // Focusin .on('focusin', function() { @@ -160,13 +160,13 @@ jQuery.fn.flexdatalist = function (_option, _value) { if (_this.keyNum(event) === 9) { _this.results.remove(); } - _this.control.backSpaceKeyRemove(event); + _this.action.backSpaceKeyRemove(event); }) // Keyup .on('input keyup', function (event) { - _this.control.keypressSearch(event); - _this.control.copyValue(event); - _this.control.backSpaceKeyRemove(event); + _this.action.keypressSearch(event); + _this.action.copyValue(event); + _this.action.backSpaceKeyRemove(event); }) // Focusout .on('focusout', function() { @@ -184,10 +184,9 @@ jQuery.fn.flexdatalist = function (_option, _value) { } /** - * Handle events. + * Handle user actions. */ - this.control = { - + this.action = { /** * Check if keypress is valid. */ @@ -202,8 +201,8 @@ jQuery.fn.flexdatalist = function (_option, _value) { // Ignore Enter and Directional keys } else if (!key || (key !== 13 && (key < 37 || key > 40))) { _searchTimeout = setTimeout(function () { - if (length >= options.minLength) { - _this.search(function (matches) { + if ((options.minLength === 0 && length > 0) || (options.minLength > 0 && length >= options.minLength)) { + _this.search.get(function (matches) { _this.results.show(matches); }); } else { @@ -212,19 +211,18 @@ jQuery.fn.flexdatalist = function (_option, _value) { }, options.searchDelay); } }, - /** * Redo search if input get's back on focus and no value selected. */ redoSearchFocus: function (event) { - var val = $this.val(); - if (val.length === 0) { + var val = _this.fvalue.get(), + alias = $alias.val(); + if (alias.length > 0) { this.keypressSearch(event); } }, - /** - * Check if keypress is valid. + * Copy value from alias to target input. */ copyValue: function (event) { if (_this.keyNum(event) !== 13) { @@ -238,7 +236,6 @@ jQuery.fn.flexdatalist = function (_option, _value) { } } }, - /** * Check if keypress is valid. */ @@ -254,7 +251,6 @@ jQuery.fn.flexdatalist = function (_option, _value) { } } }, - /** * Show all results if minLength option is 0. */ @@ -262,97 +258,102 @@ jQuery.fn.flexdatalist = function (_option, _value) { var val = $alias.val(); val = $.trim(val); if (val === '' && options.minLength === 0) { - _this.data(function (data) { + _this.data.load(function (data) { _this.results.show(data); }); } } } - /** - * Prepare input replacement. - */ - this.prepare = function () { - $alias = this.alias(); - if (options.multiple) { - $multiple = this.multipleInput($alias); - } else { - $alias.insertAfter($this); - } - // Respect autofocus attribute - if ($alias.attr('autofocus')) { - $alias.focus(); - } - } /** - * Single value input. + * Setup input. */ - this.alias = function () { - var $alias = $this - .clone(false) - .attr({ - 'list': null, - 'name': ($this.attr('name') ? $this.attr('name') + '-flexdatalist' : null), - 'id': ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : null) - }) - .addClass('flexdatalist-alias') - .removeClass('flexdatalist'); - $this.addClass('flexdatalist flexdatalist-set').prop('type', 'hidden'); - return $alias; - } + this.set = { + /** + * Prepare input replacement. + */ + up: function () { + $alias = this.alias(); + if (options.multiple) { + $multiple = this.multipleInput($alias); + } else { + $alias.insertAfter($this); + } + // Respect autofocus attribute + if ($alias.attr('autofocus')) { + $alias.focus(); + } + this.chained(); + }, + /** + * Single value input. + */ + alias: function () { + var $alias = $this + .clone(false) + .attr({ + 'list': null, + 'name': ($this.attr('name') ? $this.attr('name') + '-flexdatalist' : null), + 'id': ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : null), + 'value': '' + }) + .addClass('flexdatalist-alias') + .removeClass('flexdatalist'); + $this.addClass('flexdatalist flexdatalist-set').prop('type', 'hidden'); + return $alias; + }, + /** + * Multiple values input/list + */ + multipleInput: function ($alias) { + $multiple = $('<ul tabindex="1">') + .addClass('flexdatalist-multiple') + .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).click(function () { + $(this).find('input').focus(); + }); + $('<li class="input-container">') + .addClass('flexdatalist-multiple-value') + .append($alias) + .appendTo($multiple); - /** - * Multiple values input/list - */ - this.multipleInput = function ($alias) { - $multiple = $('<ul tabindex="1">') - .addClass('flexdatalist-multiple') - .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).click(function () { - $(this).find('input').focus(); - }); - $('<li class="input-container">') - .addClass('flexdatalist-multiple-value') - .append($alias) - .appendTo($multiple); + return $multiple; + }, + /** + * Chained inputs handling. + */ + chained: function () { + if (!options.relatives || !options.chainedRelatives) { + return; + } + var toggle = function (init) { + options.relatives.each(function () { + var disabled = _this.isEmpty($(this).val()), + empty = _this.isEmpty($this[0].value); - return $multiple; - } + // If disabling, clear all values + if (!init && (disabled || !empty)) { + _this.fvalue.clear(); + } + if ($multiple) { + disabled && $multiple ? $multiple.addClass('disabled') : $multiple.removeClass('disabled'); + } else { + $alias.prop('disabled', disabled); + } + }); + }; - /** - * Chained inputs handling. - */ - this.chained = function () { - if (!options.relatives || !options.chainedRelatives) { - return; - } - var toggle = function (init) { - options.relatives.each(function () { - var disabled = _this.isEmpty($(this).val()), - empty = _this.isEmpty($this.val()); - - // If disabling, clear all values - if (!init && (disabled || !empty)) { - _this.fvalue.clear(); - } - if ($multiple) { - disabled && $multiple ? $multiple.addClass('disabled') : $multiple.removeClass('disabled'); - } else { - $alias.prop('disabled', disabled); - } + options.relatives.on('change', function () { + toggle(); }); - }; - - options.relatives.on('change', function () { - toggle(); - }); - toggle(true); + toggle(true); + } } /** @@ -362,59 +363,66 @@ jQuery.fn.flexdatalist = function (_option, _value) { /** * Get value(s). */ - get: function () { - var val = $this.val(); - if (options.multiple) { + get: function (asString) { + var val = $this[0].value; + if (options.multiple && !asString) { return this.toObj(val); } return this.toStr(val); }, /** - * Parse given value. - * Used on input init or when added directly. + * Set value. + * Parse value if necessary. */ set: function (val, append) { - var _fvalue = this; - if (!append) { - this.clear(true); - } if (_this.isEmpty(val)) { - return; - } - this._normalize(val, function (values) { - if (!_this.isEmpty(values)) { - if ($.isArray(values)) { - $.each(values, function (i, value) { - _fvalue.extract(value, true); - }); - } else { - _fvalue.extract(values, true); - } - $this.trigger('change:flexdatalist', [ - values, - null, - options - ]).trigger('change'); + if (!append) { + this.clear(true); } - }); + } else { + var _fvalue = this; + this._normalize(val, function (values) { + if (!append) { + _fvalue.clear(true); + } + if (!_this.isEmpty(values)) { + if ($.isArray(values)) { + $.each(values, function (i, value) { + _fvalue.extract(value, true); + }); + } else { + _fvalue.extract(values, true); + } + $this.trigger('change:flexdatalist', [ + values, + null, + options + ]).trigger('change'); + } + }); + } + return $this; }, /** * Add value. */ add: function (val) { this.set(val, true); + return $this; }, /** * Toggle value. */ toggle: function (val) { this.multiple.toggle(val); + return $this; }, /** * Remove value. */ remove: function (val) { this.multiple.remove(val); + return $this; }, /** * Normalize value. @@ -432,13 +440,13 @@ jQuery.fn.flexdatalist = function (_option, _value) { _searchEqual = options.searchEqual; options.searchIn = options.valueProperty.split(','); options.searchEqual = true; - _this.search(function (matches) { + _this.search.get(data, function (matches) { if (matches.length > 0) { callback(matches); } options.searchIn = _searchIn; options.searchEqual = _searchEqual; - }, data); + }); } else { callback(data); } @@ -449,7 +457,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { extract: function (val, parse) { var txt = this.text(val), value = this.value(val), - current = $this.val(); + current = $this[0].value; if (!_this.isEmpty(val)) { if (!value) { @@ -484,7 +492,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { if (txt && txt !== $alias.val()) { $alias.val(txt, true); } - $this.val(val, true); + $this[0].value = val; }, /** * Input with multiple values. @@ -516,7 +524,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { val = _this.fvalue.toObj(val); current.push(val); val = _this.fvalue.toStr(current); - $this.val(val, true); + $this[0].value = val; }, /** * Toggle value. @@ -557,7 +565,8 @@ jQuery.fn.flexdatalist = function (_option, _value) { values.splice(index, 1); values = _this.fvalue.toStr(values); - $this.val(values, true).trigger('change:flexdatalist', [ + $this[0].value = values; + $this.trigger('change:flexdatalist', [ $li.data('value'), $li.data('text'), options @@ -573,7 +582,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { */ removeAll: function (values) { $multiple.find('li:not(.input-container)').remove(); - $this.val('', true); + $this[0].value = ''; options._values = []; }, /** @@ -618,6 +627,12 @@ jQuery.fn.flexdatalist = function (_option, _value) { }); } return $li; + }, + /** + * Get li item from value. + */ + isEmpty: function () { + return this.get().length > 0; } }, /** @@ -690,8 +705,8 @@ jQuery.fn.flexdatalist = function (_option, _value) { if (options.multiple) { this.multiple.removeAll(); } - var current = $this.val(); - $this.val('', true); + var current = $this[0].value; + $this[0].value = ''; if (current !== '') { $this.trigger('change:flexdatalist', [ null, @@ -703,13 +718,14 @@ jQuery.fn.flexdatalist = function (_option, _value) { $alias.val('', true); } options._values = []; + return $this; }, /** * Value to object */ toObj: function (val) { if (typeof val !== 'object') { - if (_this.isEmpty(val)) { + if (_this.isEmpty(val) || !_this.isDefined(val)) { val = []; } else if (this.isCSV()) { val = val.toString().split(options.valuesSeparator); @@ -727,7 +743,7 @@ jQuery.fn.flexdatalist = function (_option, _value) { */ toStr: function (val) { if (typeof val !== 'string') { - if (_this.isEmpty(val)) { + if (_this.isEmpty(val) || !_this.isDefined(val)) { val = ''; } else if (this.isCSV()) { val = val.join(options.valuesSeparator); @@ -753,291 +769,348 @@ jQuery.fn.flexdatalist = function (_option, _value) { } /** - * Get data. + * Data. */ - this.data = function (callback) { - $this.trigger('before:flexdatalist.data'); - // Remote data - this.url(function (remote) { - // Static data - _this._data(function (data) { + this.data = { + /** + * Load data from all sources. + */ + load: function (callback) { + var __this = this, + data = []; + $this.trigger('before:flexdatalist.data'); + // Remote data + this.url(function (remote) { data = data.concat(remote); - // Datalist - _this.datalist(function (list) { - data = list.concat(data); - // Check for already set values - if (!options.allowDuplicateValues) { - var values = options._values; - for (var i = 0; i < data.length; i++) { - var item = data[i]; - if (values && values.indexOf(_this.fvalue.text(item)) > -1) { - delete data[i]; + // Static data + __this.static(function (_static) { + data = data.concat(_static); + // Datalist + __this.datalist(function (list) { + data = data.concat(list); + // Check for already set values + if (!options.allowDuplicateValues) { + var values = options._values; + for (var i = 0; i < data.length; i++) { + var item = data[i]; + if (values && values.indexOf(_this.fvalue.text(item)) > -1) { + data.splice(i, 1); + } } } - } - $this.trigger('after:flexdatalist.data', [data]); - callback(data); + $this.trigger('after:flexdatalist.data', [data]); + callback(data); + }); }); }); - }); - } - - /** - * Get static data. - */ - this._data = function (callback) { - // Remote source - if (typeof options.data === 'string') { - var url = options.data, - cache = _this.cache.read(url); - if (cache) { - callback(cache); - return; - } - _this.remote({ - url: url, - success: function (data) { - var _data = _this.extractRemoteData(data); - options.data = _data; - callback(options.data); - _this.cache.write(url, options.data, options.cacheLifetime); + }, + /** + * Get static data. + */ + static: function (callback) { + var __this = this; + // Remote source + if (typeof options.data === 'string') { + var url = options.data, + cache = _this.cache.read(url); + if (cache) { + callback(cache); + return; } - }); - } else { - callback(options.data); - } - } - - /** - * Get datalist values. - */ - this.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 + this.remote({ + url: url, + success: function (data) { + options.data = _data; + callback(options.data); + _this.cache.write(url, options.data, options.cacheLifetime); + } }); - }); - } - callback(datalist); - } + } 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) { + var __this = this, + keyword = $alias.val(), + value = _this.fvalue.get(), + relatives = this.relativesData(); - /** - * Get remote data. - */ - this.url = function (callback) { - var keyword = $alias.val(), - value = $this.val(), - cacheKey = keyword; + if (_this.isEmpty(options.url)) { + return callback([]); + } - 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; + } - if (options.cache && options.cache !== 2) { - cacheKey = keyword.substring(0, (options.minLength > 0 ? options.minLength : 1)); - } + // Cache + var cacheKey = _this.cache.keyGen({ + relative: relatives, + keyword: keyword.substring(0, (options.minLength > 0 ? options.minLength : 1)), + contain: options.searchContain + }, options.url), + cache = _this.cache.read(cacheKey); + if (cache) { + callback(cache); + return; + } - var _opts = {}; - if (options.requestType === 'post') { - $.each(options, function (option, value) { - if (option.indexOf('_') == 0) { - return; + this.remote({ + url: options.url, + data: $.extend( + relatives, + options.params, + { + keyword: keyword, + contain: options.searchContain, + selected: value, + original: options.originalValue, + options: _opts + } + ), + success: function (_data) { + var _keyword = $alias.val(); + if (_keyword.length >= keyword.length) { + callback(_data); + } + _this.cache.write(cacheKey, _data, options.cacheLifetime); } - _opts[option] = value; }); - delete _opts.relatives; - } - - this.remote({ - url: options.url, - data: $.extend( - options.params, + }, + /** + * AJAX request. + */ + remote: function (options) { + var __this = this; + // Prevent get data when pressing backspace button + if ($this.hasClass('flexdatalist-loading')) { + return; + } + $this.addClass('flexdatalist-loading'); + $.ajax($.extend( { - keyword: keyword, - contain: options.searchContain, - selected: value, - options: _opts - } - ), - success: function (data) { - var _data = _this.extractRemoteData(data), - _keyword = $alias.val(); - if (_keyword.length >= keyword.length) { - callback(_data); + type: 'post', + dataType: 'json', + complete: function () { + $this.removeClass('flexdatalist-loading'); + } + }, options, { + success: function (data) { + data = __this.extractRemoteData(data); + options.success(data); + } } + )); + }, + /** + * Extract remote data from server response. + */ + extractRemoteData: function (data) { + var _data = data.results ? data.results : data; + if (typeof _data === 'string' && _data.indexOf('[{') === 0) { + _data = JSON.parse(_data); } - }); - } - - /** - * AJAX request. - */ - this.remote = function (options) { - // Prevent get data when pressing backspace button - if ($this.hasClass('flexdatalist-loading')) { - return; - } - $this.addClass('flexdatalist-loading'); - options = $.extend({ - type: options.requestType, - dataType: 'json', - complete: function () { - $this.removeClass('flexdatalist-loading'); - } - }, options); - $.ajax(options); - } - - /** - * Extract remote data from server response. - */ - this.extractRemoteData = function (data) { - var _data = data.results ? data.results : data; - if (typeof _data === 'string' && _data.indexOf('[{') === 0) { - _data = JSON.parse(_data); - } - if (_data.options) { - options = $.extend({}, options, _data.options); - } -
2.0.0 WIP