aboutsummaryrefslogtreecommitdiff
path: root/app/genealogy/dllib/jquery.flexdatalist.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/genealogy/dllib/jquery.flexdatalist.js')
-rw-r--r--app/genealogy/dllib/jquery.flexdatalist.js2083
1 files changed, 2083 insertions, 0 deletions
diff --git a/app/genealogy/dllib/jquery.flexdatalist.js b/app/genealogy/dllib/jquery.flexdatalist.js
new file mode 100644
index 0000000..6fef98b
--- /dev/null
+++ b/app/genealogy/dllib/jquery.flexdatalist.js
@@ -0,0 +1,2083 @@
+/**
+ * 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">&times;</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, "&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);