2011-11-09 20:51:07 +04:00
|
|
|
/* Simple autocomplete for text inputs, with the support for multiple selection.
|
2012-10-13 18:29:46 +04:00
|
|
|
|
2011-11-09 21:11:16 +04:00
|
|
|
Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete
|
2012-10-13 18:32:01 +04:00
|
|
|
License: MPL 2.0+ (http://www.mozilla.org/MPL/2.0/)
|
2018-06-01 16:34:34 +03:00
|
|
|
Version: 2018-06-01
|
|
|
|
(c) Vitaliy Filippov 2011-2018
|
2012-10-13 18:29:46 +04:00
|
|
|
|
2011-11-08 18:14:01 +04:00
|
|
|
Usage:
|
2011-11-09 20:51:07 +04:00
|
|
|
Include hinter.css, hinter.js on your page. Then write:
|
2013-01-09 19:30:44 +04:00
|
|
|
var hint = new SimpleAutocomplete(input, dataLoader, params);
|
2012-10-13 18:29:46 +04:00
|
|
|
|
2011-11-08 18:14:01 +04:00
|
|
|
Parameters:
|
2011-11-09 20:51:07 +04:00
|
|
|
input
|
2011-11-09 21:11:16 +04:00
|
|
|
The input, either id or DOM element reference (the input must have an id anyway).
|
2013-01-17 22:59:14 +04:00
|
|
|
dataLoader(hint, value[, more])
|
2013-03-05 20:26:55 +04:00
|
|
|
Callback which should load autocomplete options and then call:
|
|
|
|
hint.replaceItems(newOptions, append)
|
2015-03-25 15:47:27 +03:00
|
|
|
newOptions = [ [ name, value, disabled, checked OR id ] ], [ name, value ], ... ]
|
2013-03-05 21:55:49 +04:00
|
|
|
name = HTML option name
|
|
|
|
value = plaintext option value
|
2013-11-13 15:43:35 +04:00
|
|
|
disabled = prevent selection of this option
|
2015-03-25 15:47:27 +03:00
|
|
|
checked = (when multipleListener is set) is the item checked initially
|
|
|
|
id = (when idField is set) the value ID for idField
|
2013-03-05 22:21:50 +04:00
|
|
|
append = 'more' parameter should be passed here
|
2013-01-17 22:59:14 +04:00
|
|
|
Callback parameters:
|
|
|
|
hint
|
|
|
|
This SimpleAutocomplete object
|
|
|
|
value
|
|
|
|
The string guess should be done based on
|
|
|
|
more
|
2013-03-05 21:55:49 +04:00
|
|
|
The 'page' of autocomplete options to load, 0 = first page.
|
2013-01-17 22:59:14 +04:00
|
|
|
See also moreMarker option below.
|
2012-10-13 18:29:46 +04:00
|
|
|
|
2013-01-09 19:30:44 +04:00
|
|
|
params attribute is an object with optional parameters:
|
2011-11-09 20:51:07 +04:00
|
|
|
multipleDelimiter
|
|
|
|
Pass a delimiter string (for example ',' or ';') to enable multiple selection.
|
|
|
|
Item values cannot have leading or trailing whitespace. Input value will consist
|
|
|
|
of selected item values separated by this delimiter plus single space.
|
|
|
|
dataLoader should handle it's 'value' parameter accordingly in this case,
|
|
|
|
because it will be just the raw value of the input, probably with incomplete
|
|
|
|
item or items, typed by the user.
|
2013-03-05 21:55:49 +04:00
|
|
|
multipleListener(hint, index, item)
|
|
|
|
If you don't want to touch the input value, but want to use multi-select for
|
|
|
|
your own purposes, specify a callback that will handle item clicks here.
|
|
|
|
Also you can disable and check/uncheck items during loading in this mode.
|
2014-09-04 19:18:55 +04:00
|
|
|
onChangeListener(hint, index, item)
|
2011-11-09 20:51:07 +04:00
|
|
|
Callback which is called when input value is changed using this dropdown.
|
2011-11-09 21:11:16 +04:00
|
|
|
index is the number of element which selection is changed, starting with 0.
|
2011-11-09 20:51:07 +04:00
|
|
|
It must be used instead of normal 'onchange' event.
|
|
|
|
emptyText
|
2013-03-05 21:55:49 +04:00
|
|
|
Text to show when dataLoader returns no options. Empty (default) means 'hide hint'.
|
2013-03-05 20:26:55 +04:00
|
|
|
prompt
|
|
|
|
HTML text to be displayed before a non-empty option list. Empty by default.
|
2013-01-09 19:30:44 +04:00
|
|
|
delay
|
|
|
|
If this is set to a non-zero value, the autocompleter does no more than
|
|
|
|
1 request in each delay milliseconds.
|
2013-01-17 22:59:14 +04:00
|
|
|
moreMarker
|
|
|
|
The server supplying hint options usually limits their count.
|
2013-03-05 21:55:49 +04:00
|
|
|
But it's not always convenient having to type additional characters to
|
|
|
|
narrow down the selection. Optionally you can supply additional item
|
|
|
|
with special value equal to moreMarker value or '#MORE' at the end
|
|
|
|
of the list, and SimpleAutocomplete will issue another request to
|
|
|
|
dataLoader with incremented 'more' parameter when it will be clicked.
|
2013-01-17 22:59:14 +04:00
|
|
|
You can also set moreMarker to false to disable this feature.
|
2015-03-25 15:47:27 +03:00
|
|
|
idField
|
|
|
|
If you specify an ID here, the selected value ID will be put into
|
|
|
|
a hidden field with this ID, while the original hinted input will
|
|
|
|
just contain the name of that value.
|
2014-09-04 15:15:16 +04:00
|
|
|
persist
|
|
|
|
If true, the hint layer will never be hidden. You can use it to create
|
|
|
|
multiselect-like controls (see example at the homepage).
|
2018-06-01 16:34:34 +03:00
|
|
|
useTab
|
|
|
|
Select entry on Tab key press
|
2014-09-04 15:15:16 +04:00
|
|
|
className
|
|
|
|
CSS class name for the hint layer. Default is 'hintLayer'.
|
2013-03-05 20:26:55 +04:00
|
|
|
|
2013-03-05 21:55:49 +04:00
|
|
|
Destroy instance:
|
|
|
|
hint.remove(); hint = null;
|
2011-11-08 18:14:01 +04:00
|
|
|
*/
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** Constructor ***
|
|
|
|
|
2013-01-09 19:30:44 +04:00
|
|
|
var SimpleAutocomplete = function(input, dataLoader, params)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
|
|
|
if (typeof(input) == 'string')
|
|
|
|
input = document.getElementById(input);
|
2013-01-09 19:30:44 +04:00
|
|
|
if (!params)
|
|
|
|
params = {};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
|
|
|
// Parameters
|
2015-09-06 19:23:26 +03:00
|
|
|
this.options = params;
|
2012-10-13 18:29:46 +04:00
|
|
|
this.input = input;
|
|
|
|
this.dataLoader = dataLoader;
|
2013-04-11 00:19:21 +04:00
|
|
|
this.multipleDelimiter = params.multipleDelimiter;
|
2013-03-05 21:55:49 +04:00
|
|
|
this.multipleListener = params.multipleListener;
|
2013-01-09 19:30:44 +04:00
|
|
|
this.onChangeListener = params.onChangeListener;
|
|
|
|
this.emptyText = params.emptyText;
|
2013-03-05 20:26:55 +04:00
|
|
|
this.prompt = params.prompt;
|
2013-01-09 19:30:44 +04:00
|
|
|
this.delay = params.delay;
|
2013-01-17 22:59:14 +04:00
|
|
|
this.moreMarker = params.moreMarker;
|
2015-03-25 15:47:27 +03:00
|
|
|
this.idField = params.idField;
|
2014-09-04 15:15:16 +04:00
|
|
|
this.persist = params.persist;
|
2018-06-01 16:34:34 +03:00
|
|
|
this.useTab = params.useTab && true;
|
2014-09-04 15:15:16 +04:00
|
|
|
this.className = params.className || 'hintLayer';
|
2015-03-25 15:47:27 +03:00
|
|
|
if (this.idField && typeof(this.idField) == 'string')
|
|
|
|
this.idField = document.getElementById(this.idField);
|
2013-01-17 22:59:14 +04:00
|
|
|
|
|
|
|
// Default values
|
|
|
|
if (this.moreMarker === undefined)
|
|
|
|
this.moreMarker = '#MORE';
|
2013-01-09 19:30:44 +04:00
|
|
|
if (this.delay === undefined)
|
|
|
|
this.delay = 300;
|
2011-11-08 18:14:01 +04:00
|
|
|
|
|
|
|
// Variables
|
2013-01-17 22:59:14 +04:00
|
|
|
this.more = 0;
|
2013-01-09 19:30:44 +04:00
|
|
|
this.timer = null;
|
2012-10-14 02:15:54 +04:00
|
|
|
this.closure = [];
|
2012-10-13 18:29:46 +04:00
|
|
|
this.items = [];
|
|
|
|
this.skipHideCounter = 0;
|
|
|
|
this.selectedIndex = -1;
|
|
|
|
this.disabled = false;
|
2015-03-25 16:02:38 +03:00
|
|
|
this.curValue = null;
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** Call initialise ***
|
|
|
|
this.init();
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** Instance methods ***
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Initialiser
|
|
|
|
SimpleAutocomplete.prototype.init = function()
|
|
|
|
{
|
|
|
|
var e = this.input;
|
2013-01-17 22:59:14 +04:00
|
|
|
var l = SimpleAutocomplete.SimpleAutocompletes;
|
|
|
|
this.id = this.input.id + l.length;
|
|
|
|
l.push(this);
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Create hint layer
|
|
|
|
var t = this.hintLayer = document.createElement('div');
|
2014-09-04 15:15:16 +04:00
|
|
|
t.className = this.className;
|
|
|
|
if (!this.persist)
|
|
|
|
{
|
|
|
|
t.style.display = 'none';
|
|
|
|
t.style.position = 'absolute';
|
|
|
|
t.style.zIndex = 1000;
|
2015-01-16 17:47:32 +03:00
|
|
|
document.body.insertBefore(t, document.body.childNodes[0]);
|
2014-09-04 15:15:16 +04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
e.nextSibling ? e.parentNode.insertBefore(t, e.nextSibling) : e.parentNode.appendChild(t);
|
|
|
|
}
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Remember instance
|
2012-10-14 03:40:47 +04:00
|
|
|
e.SimpleAutocomplete_input = this;
|
|
|
|
t.SimpleAutocomplete_layer = this;
|
2012-10-13 18:29:46 +04:00
|
|
|
|
2013-10-18 18:14:59 +04:00
|
|
|
// Set autocomplete to off and reenable before unload
|
|
|
|
if (typeof e.autocomplete !== 'undefined')
|
|
|
|
{
|
|
|
|
e.autocomplete = 'off';
|
|
|
|
addListener(window, 'beforeunload', function() { e.autocomplete = 'on'; });
|
|
|
|
}
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Set event listeners
|
|
|
|
var self = this;
|
2013-04-02 16:35:37 +04:00
|
|
|
this.addRmListener('keydown', function(ev) { return self.onKeyDown(ev); });
|
2012-10-14 02:15:54 +04:00
|
|
|
this.addRmListener('keyup', function(ev) { return self.onKeyUp(ev); });
|
|
|
|
this.addRmListener('change', function() { return self.onChange(); });
|
|
|
|
this.addRmListener('focus', function() { return self.onInputFocus(); });
|
|
|
|
this.addRmListener('blur', function() { return self.onInputBlur(); });
|
2012-10-14 03:40:47 +04:00
|
|
|
addListener(t, 'mousedown', function(ev) { return self.cancelBubbleOnHint(ev); });
|
2014-09-04 15:15:16 +04:00
|
|
|
this.onChange(true);
|
2012-10-13 18:29:46 +04:00
|
|
|
};
|
|
|
|
|
2013-03-05 21:55:49 +04:00
|
|
|
// items = [ [ name, value ], [ name, value ], ... ]
|
2013-03-05 20:26:55 +04:00
|
|
|
SimpleAutocomplete.prototype.replaceItems = function(items, append)
|
2012-10-13 18:29:46 +04:00
|
|
|
{
|
2013-03-05 20:26:55 +04:00
|
|
|
if (!append)
|
2013-01-17 22:59:14 +04:00
|
|
|
{
|
|
|
|
this.hintLayer.scrollTop = 0;
|
2013-04-02 16:35:37 +04:00
|
|
|
this.selectedIndex = 0;
|
2013-03-05 20:26:55 +04:00
|
|
|
this.items = [];
|
|
|
|
if (!items || items.length == 0)
|
2011-11-09 20:51:07 +04:00
|
|
|
{
|
2013-03-05 20:26:55 +04:00
|
|
|
if (this.emptyText)
|
|
|
|
this.hintLayer.innerHTML = '<div class="hintEmptyText">'+this.emptyText+'</div>';
|
|
|
|
else
|
|
|
|
this.disable();
|
|
|
|
return;
|
2011-11-09 20:51:07 +04:00
|
|
|
}
|
2013-11-13 16:46:23 +04:00
|
|
|
while (this.selectedIndex < items.length && items[this.selectedIndex][2])
|
|
|
|
this.selectedIndex++;
|
2013-03-05 20:26:55 +04:00
|
|
|
this.hintLayer.innerHTML = this.prompt ? '<div class="hintPrompt">'+this.prompt+'</div>' : '';
|
|
|
|
this.enable();
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
2013-04-11 00:19:21 +04:00
|
|
|
if (this.multipleDelimiter)
|
2011-11-09 20:51:07 +04:00
|
|
|
{
|
2013-03-05 21:55:49 +04:00
|
|
|
var h = {};
|
2012-10-13 18:29:46 +04:00
|
|
|
var old = this.input.value.split(this.multipleDelimiter);
|
|
|
|
for (var i = 0; i < old.length; i++)
|
|
|
|
h[old[i].trim()] = true;
|
2013-03-05 21:55:49 +04:00
|
|
|
for (var i in items)
|
|
|
|
items[i][3] = h[items[i][1]];
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
|
|
|
for (var i in items)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2013-03-05 21:55:49 +04:00
|
|
|
this.hintLayer.appendChild(this.makeItem(this.items.length, items[i]));
|
|
|
|
this.items.push(items[i]);
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2013-11-13 15:43:35 +04:00
|
|
|
// Add removable listener on this.input (remember the function)
|
2012-10-14 02:15:54 +04:00
|
|
|
SimpleAutocomplete.prototype.addRmListener = function(n, f)
|
|
|
|
{
|
|
|
|
this.closure[n] = f;
|
|
|
|
addListener(this.input, n, f);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Remove instance ("destructor")
|
|
|
|
SimpleAutocomplete.prototype.remove = function()
|
|
|
|
{
|
|
|
|
if (!this.hintLayer)
|
|
|
|
return;
|
|
|
|
this.hintLayer.parentNode.removeChild(this.hintLayer);
|
|
|
|
for (var i in this.closure)
|
|
|
|
{
|
|
|
|
removeListener(this.input, i, this.closure[i]);
|
|
|
|
}
|
|
|
|
for (var i = 0; i < SimpleAutocomplete.SimpleAutocompletes.length; i++)
|
|
|
|
{
|
|
|
|
if (SimpleAutocomplete.SimpleAutocompletes[i] == this)
|
|
|
|
{
|
|
|
|
SimpleAutocomplete.SimpleAutocompletes.splice(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.closure = {};
|
|
|
|
this.input = null;
|
|
|
|
this.hintLayer = null;
|
|
|
|
this.items = null;
|
|
|
|
};
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Create a drop-down list item, include checkbox if this.multipleDelimiter is true
|
2013-03-05 21:55:49 +04:00
|
|
|
SimpleAutocomplete.prototype.makeItem = function(index, item)
|
2012-10-13 18:29:46 +04:00
|
|
|
{
|
|
|
|
var d = document.createElement('div');
|
2013-03-05 21:55:49 +04:00
|
|
|
d.id = this.id+'_item_'+index;
|
2013-03-05 22:12:05 +04:00
|
|
|
d.className = item[2] ? 'hintDisabledItem' : (this.selectedIndex == index ? 'hintActiveItem' : 'hintItem');
|
2013-03-05 21:55:49 +04:00
|
|
|
d.title = item[1];
|
2013-04-11 00:19:21 +04:00
|
|
|
if (this.multipleDelimiter || this.multipleListener)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
var c = document.createElement('input');
|
|
|
|
c.type = 'checkbox';
|
2013-03-05 21:55:49 +04:00
|
|
|
c.id = this.id+'_check_'+index;
|
|
|
|
c.checked = item[3] && true;
|
|
|
|
c.disabled = item[2] && true;
|
|
|
|
c.value = item[1];
|
2013-04-11 00:19:21 +04:00
|
|
|
d.appendChild(c);
|
|
|
|
var l = document.createElement('label');
|
|
|
|
l.htmlFor = c.id;
|
|
|
|
l.innerHTML = item[0];
|
|
|
|
d.appendChild(l);
|
2014-09-04 17:25:17 +04:00
|
|
|
addListener(l, 'click', this.preventCheck);
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
2013-04-11 00:19:21 +04:00
|
|
|
else
|
|
|
|
d.innerHTML = item[0];
|
2012-10-13 18:29:46 +04:00
|
|
|
var self = this;
|
|
|
|
addListener(d, 'mouseover', function() { return self.onItemMouseOver(this); });
|
2013-10-18 18:14:59 +04:00
|
|
|
addListener(d, 'click', function(ev) { return self.onItemClick(ev, this); });
|
2012-10-13 18:29:46 +04:00
|
|
|
return d;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Move highlight forward or back by 'by' items (integer)
|
|
|
|
SimpleAutocomplete.prototype.moveHighlight = function(by)
|
|
|
|
{
|
|
|
|
var n = this.selectedIndex+by;
|
|
|
|
if (n < 0)
|
|
|
|
n = 0;
|
2013-11-13 15:43:35 +04:00
|
|
|
while (this.items[n] && this.items[n][2])
|
|
|
|
n += by;
|
2012-10-13 18:29:46 +04:00
|
|
|
var elem = document.getElementById(this.id+'_item_'+n);
|
|
|
|
if (!elem)
|
2011-11-08 18:14:01 +04:00
|
|
|
return true;
|
2012-10-13 18:29:46 +04:00
|
|
|
return this.highlightItem(elem);
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Make item 'elem' active (highlighted)
|
|
|
|
SimpleAutocomplete.prototype.highlightItem = function(elem)
|
|
|
|
{
|
2013-03-05 20:26:55 +04:00
|
|
|
var ni = parseInt(elem.id.substr(this.id.length+6));
|
|
|
|
if (this.items[ni][2])
|
|
|
|
return false;
|
2012-10-13 18:29:46 +04:00
|
|
|
if (this.selectedIndex >= 0)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
var c = this.getItem();
|
|
|
|
if (c)
|
2013-10-18 18:27:07 +04:00
|
|
|
{
|
|
|
|
c.className = this.items[this.selectedIndex][2] ? 'hintDisabledItem' : 'hintItem';
|
|
|
|
}
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
2013-03-05 20:26:55 +04:00
|
|
|
this.selectedIndex = ni;
|
2012-10-13 18:29:46 +04:00
|
|
|
elem.className = 'hintActiveItem';
|
|
|
|
return false;
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Get index'th item, or current when index is null
|
|
|
|
SimpleAutocomplete.prototype.getItem = function(index)
|
|
|
|
{
|
|
|
|
if (index == null)
|
|
|
|
index = this.selectedIndex;
|
|
|
|
if (index < 0)
|
|
|
|
return null;
|
|
|
|
return document.getElementById(this.id+'_item_'+this.selectedIndex);
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Select index'th item - change the input value and hide the hint if not a multi-select
|
|
|
|
SimpleAutocomplete.prototype.selectItem = function(index)
|
|
|
|
{
|
2013-10-18 18:22:02 +04:00
|
|
|
if (this.items[index][2])
|
|
|
|
return false;
|
|
|
|
if (this.moreMarker && this.items[index][1] == this.moreMarker)
|
|
|
|
{
|
|
|
|
// User clicked 'more'. Load more items without delay.
|
|
|
|
this.items.splice(index, 1);
|
2015-05-08 17:49:40 +03:00
|
|
|
var elm = document.getElementById(this.id+'_item_'+index);
|
2013-10-18 18:22:02 +04:00
|
|
|
elm.parentNode.removeChild(elm);
|
|
|
|
this.more++;
|
|
|
|
this.onChange(true);
|
|
|
|
return;
|
|
|
|
}
|
2013-04-11 00:19:21 +04:00
|
|
|
if (!this.multipleDelimiter && !this.multipleListener)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
this.input.value = this.items[index][1];
|
2015-03-25 15:47:27 +03:00
|
|
|
if (this.idField)
|
|
|
|
this.idField.value = this.items[index][3];
|
2012-10-13 18:29:46 +04:00
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
else
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2013-03-05 20:26:55 +04:00
|
|
|
document.getElementById(this.id+'_check_'+index).checked = this.items[index][3] = !this.items[index][3];
|
2013-04-11 00:19:21 +04:00
|
|
|
if (this.multipleListener && !this.multipleListener(this, index, this.items[index]))
|
2013-03-05 21:55:49 +04:00
|
|
|
return;
|
|
|
|
this.toggleValue(index);
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
|
|
|
this.curValue = this.input.value;
|
|
|
|
if (this.onChangeListener)
|
2014-09-04 19:18:55 +04:00
|
|
|
this.onChangeListener(this, index, this.items[index]);
|
2012-10-13 18:29:46 +04:00
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2013-03-05 21:55:49 +04:00
|
|
|
// Change input value so it will respect index'th item state in a multi-select
|
|
|
|
SimpleAutocomplete.prototype.toggleValue = function(index)
|
|
|
|
{
|
|
|
|
var old = this.input.value.split(this.multipleDelimiter);
|
|
|
|
for (var i = 0; i < old.length; i++)
|
|
|
|
old[i] = old[i].trim();
|
|
|
|
// Turn the clicked item on or off, preserving order
|
|
|
|
if (!this.items[index][3])
|
|
|
|
{
|
|
|
|
for (var i = old.length-1; i >= 0; i--)
|
|
|
|
if (old[i] == this.items[index][1])
|
|
|
|
old.splice(i, 1);
|
|
|
|
this.input.value = old.join(this.multipleDelimiter+' ');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var h = {};
|
|
|
|
for (var i = 0; i < this.items.length; i++)
|
|
|
|
if (this.items[i][3])
|
|
|
|
h[this.items[i][1]] = true;
|
|
|
|
var nl = [];
|
|
|
|
for (var i = 0; i < old.length; i++)
|
|
|
|
{
|
|
|
|
if (h[old[i]])
|
|
|
|
{
|
|
|
|
delete h[old[i]];
|
|
|
|
nl.push(old[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (var i = 0; i < this.items.length; i++)
|
|
|
|
if (this.items[i][3] && h[this.items[i][1]])
|
|
|
|
nl.push(this.items[i][1]);
|
|
|
|
this.input.value = nl.join(this.multipleDelimiter+' ');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Hide hinter
|
|
|
|
SimpleAutocomplete.prototype.hide = function()
|
|
|
|
{
|
2014-09-04 15:15:16 +04:00
|
|
|
if (!this.persist)
|
|
|
|
{
|
|
|
|
if (!this.skipHideCounter)
|
2014-09-05 14:32:57 +04:00
|
|
|
{
|
2014-09-04 15:15:16 +04:00
|
|
|
this.hintLayer.style.display = 'none';
|
2014-09-05 14:32:57 +04:00
|
|
|
return true;
|
|
|
|
}
|
2014-09-04 15:15:16 +04:00
|
|
|
else
|
|
|
|
this.skipHideCounter = 0;
|
|
|
|
}
|
2012-10-13 18:29:46 +04:00
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Show hinter
|
|
|
|
SimpleAutocomplete.prototype.show = function()
|
|
|
|
{
|
2014-09-05 14:35:28 +04:00
|
|
|
if (!this.disabled && !this.persist && this.hintLayer.style.display == 'none')
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
var p = getOffset(this.input);
|
|
|
|
this.hintLayer.style.top = (p.top+this.input.offsetHeight) + 'px';
|
|
|
|
this.hintLayer.style.left = p.left + 'px';
|
|
|
|
this.hintLayer.style.display = '';
|
2014-09-22 17:51:06 +04:00
|
|
|
var sw = document.clientWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
|
|
|
if (p.left + this.hintLayer.offsetWidth > sw)
|
|
|
|
{
|
|
|
|
this.hintLayer.style.right = (sw-p.left-this.input.offsetWidth)+'px';
|
|
|
|
this.hintLayer.style.left = '';
|
|
|
|
}
|
2014-09-05 14:32:57 +04:00
|
|
|
return true;
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Disable hinter, for the case when there is no items and no empty text
|
|
|
|
SimpleAutocomplete.prototype.disable = function()
|
|
|
|
{
|
|
|
|
this.disabled = true;
|
|
|
|
this.hide();
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Enable hinter
|
|
|
|
SimpleAutocomplete.prototype.enable = function()
|
|
|
|
{
|
|
|
|
this.disabled = false;
|
2013-03-14 00:51:24 +04:00
|
|
|
if (this.hasFocus)
|
2012-10-13 18:29:46 +04:00
|
|
|
this.show();
|
|
|
|
}
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** Event handlers ***
|
|
|
|
|
2014-09-04 17:25:17 +04:00
|
|
|
// Prevent propagating label click to checkbox
|
2012-10-13 18:29:46 +04:00
|
|
|
SimpleAutocomplete.prototype.preventCheck = function(ev)
|
|
|
|
{
|
2014-09-04 17:25:17 +04:00
|
|
|
return stopEvent(ev||window.event, false, true);
|
2012-10-13 18:29:46 +04:00
|
|
|
};
|
|
|
|
|
2012-10-14 03:40:47 +04:00
|
|
|
// Cancel event propagation
|
|
|
|
SimpleAutocomplete.prototype.cancelBubbleOnHint = function(ev)
|
|
|
|
{
|
|
|
|
ev = ev||window.event;
|
|
|
|
if (this.hasFocus)
|
|
|
|
this.skipHideCounter++;
|
|
|
|
return stopEvent(ev, true, false);
|
|
|
|
};
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Handle item mouse over
|
|
|
|
SimpleAutocomplete.prototype.onItemMouseOver = function(elm)
|
|
|
|
{
|
|
|
|
return this.highlightItem(elm);
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Handle item clicks
|
2012-10-14 01:48:31 +04:00
|
|
|
SimpleAutocomplete.prototype.onItemClick = function(ev, elm)
|
2012-10-13 18:29:46 +04:00
|
|
|
{
|
2013-01-17 22:59:14 +04:00
|
|
|
var index = parseInt(elm.id.substr(this.id.length+6));
|
|
|
|
this.selectItem(index);
|
2012-10-13 18:29:46 +04:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Handle user input, load new items
|
2013-03-05 20:26:55 +04:00
|
|
|
SimpleAutocomplete.prototype.onChange = function(force)
|
2012-10-13 18:29:46 +04:00
|
|
|
{
|
|
|
|
var v = this.input.value.trim();
|
2013-03-05 20:26:55 +04:00
|
|
|
if (!force)
|
2013-01-17 22:59:14 +04:00
|
|
|
this.more = 0;
|
2013-03-05 20:26:55 +04:00
|
|
|
if (v != this.curValue || force)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2015-03-25 16:02:38 +03:00
|
|
|
if (this.curValue !== null && this.idField)
|
2015-03-25 15:47:27 +03:00
|
|
|
this.idField.value = '';
|
2012-10-13 18:29:46 +04:00
|
|
|
this.curValue = v;
|
2013-03-05 20:26:55 +04:00
|
|
|
if (!this.delay || force)
|
2013-01-17 22:59:14 +04:00
|
|
|
this.dataLoader(this, v, this.more);
|
2013-01-09 19:30:44 +04:00
|
|
|
else if (!this.timer)
|
|
|
|
{
|
|
|
|
var self = this;
|
|
|
|
this.timer = setTimeout(function() {
|
2013-01-17 22:59:14 +04:00
|
|
|
self.dataLoader(self, self.curValue, self.more);
|
2013-01-09 19:30:44 +04:00
|
|
|
self.timer = null;
|
|
|
|
}, this.delay);
|
|
|
|
}
|
2012-10-13 18:29:46 +04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2018-06-01 16:34:34 +03:00
|
|
|
// Handle Enter/Tab key presses, cancel handling of arrow keys
|
2012-10-13 18:29:46 +04:00
|
|
|
SimpleAutocomplete.prototype.onKeyUp = function(ev)
|
|
|
|
{
|
|
|
|
ev = ev||window.event;
|
2012-10-14 02:27:40 +04:00
|
|
|
if (ev.keyCode == 38 || ev.keyCode == 40)
|
2012-10-13 18:29:46 +04:00
|
|
|
this.show();
|
2018-06-01 16:34:34 +03:00
|
|
|
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 10 || ev.keyCode == 13 || ev.keyCode == 9 && this.useTab)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
if (this.hintLayer.style.display == '')
|
|
|
|
return stopEvent(ev, true, true);
|
2011-11-08 18:14:01 +04:00
|
|
|
else
|
2012-10-13 18:29:46 +04:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
this.onChange();
|
|
|
|
return true;
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2018-06-01 16:34:34 +03:00
|
|
|
// Handle arrow keys and Enter/Tab
|
2013-04-02 16:35:37 +04:00
|
|
|
SimpleAutocomplete.prototype.onKeyDown = function(ev)
|
2012-10-13 18:29:46 +04:00
|
|
|
{
|
|
|
|
if (this.hintLayer.style.display == 'none')
|
|
|
|
return true;
|
|
|
|
ev = ev||window.event;
|
|
|
|
if (ev.keyCode == 38) // up
|
|
|
|
this.moveHighlight(-1);
|
|
|
|
else if (ev.keyCode == 40) // down
|
|
|
|
this.moveHighlight(1);
|
2018-06-01 16:34:34 +03:00
|
|
|
else if (ev.keyCode == 10 || ev.keyCode == 13 || ev.keyCode == 9 && this.useTab) // enter / tab
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
if (this.selectedIndex >= 0)
|
|
|
|
this.selectItem(this.selectedIndex);
|
|
|
|
return stopEvent(ev, true, true);
|
|
|
|
}
|
2012-10-14 02:27:40 +04:00
|
|
|
else if (ev.keyCode == 27) // escape
|
|
|
|
{
|
|
|
|
this.hide();
|
|
|
|
return stopEvent(ev, true, true);
|
|
|
|
}
|
2012-10-13 18:29:46 +04:00
|
|
|
else
|
|
|
|
return true;
|
|
|
|
// scrolling
|
|
|
|
if (this.selectedIndex >= 0)
|
2011-11-08 18:14:01 +04:00
|
|
|
{
|
2012-10-13 18:29:46 +04:00
|
|
|
var c = this.getItem();
|
|
|
|
var t = this.hintLayer;
|
|
|
|
var ct = getOffset(c).top + t.scrollTop - t.style.top.substr(0, t.style.top.length-2);
|
|
|
|
var ch = c.scrollHeight;
|
|
|
|
if (ct+ch-t.offsetHeight > t.scrollTop)
|
|
|
|
t.scrollTop = ct+ch-t.offsetHeight;
|
|
|
|
else if (ct < t.scrollTop)
|
|
|
|
t.scrollTop = ct;
|
2011-11-08 18:14:01 +04:00
|
|
|
}
|
2012-10-13 18:29:46 +04:00
|
|
|
return stopEvent(ev, true, true);
|
|
|
|
};
|
2011-11-08 18:14:01 +04:00
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Called when input receives focus
|
|
|
|
SimpleAutocomplete.prototype.onInputFocus = function()
|
|
|
|
{
|
|
|
|
this.show();
|
|
|
|
this.hasFocus = true;
|
|
|
|
return true;
|
2011-11-08 18:14:01 +04:00
|
|
|
};
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// Called when input loses focus
|
|
|
|
SimpleAutocomplete.prototype.onInputBlur = function()
|
|
|
|
{
|
2015-07-17 01:09:32 +03:00
|
|
|
if (!this.skipHideCounter && this.idField && !this.idField.value)
|
|
|
|
this.input.value = '';
|
2012-10-13 18:29:46 +04:00
|
|
|
this.hide();
|
|
|
|
this.hasFocus = false;
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
// *** Global variables ***
|
|
|
|
|
|
|
|
// List of all instances
|
2011-11-08 18:14:01 +04:00
|
|
|
SimpleAutocomplete.SimpleAutocompletes = [];
|
|
|
|
|
|
|
|
// Global mousedown handler, hides dropdowns when clicked outside
|
|
|
|
SimpleAutocomplete.GlobalMouseDown = function(ev)
|
|
|
|
{
|
|
|
|
var target = ev.target || ev.srcElement;
|
|
|
|
var esh;
|
|
|
|
while (target)
|
|
|
|
{
|
|
|
|
esh = target.SimpleAutocomplete_input;
|
|
|
|
if (esh)
|
|
|
|
break;
|
|
|
|
else if (target.SimpleAutocomplete_layer)
|
|
|
|
return true;
|
|
|
|
target = target.parentNode;
|
|
|
|
}
|
|
|
|
for (var i in SimpleAutocomplete.SimpleAutocompletes)
|
|
|
|
if (SimpleAutocomplete.SimpleAutocompletes[i] != esh)
|
|
|
|
SimpleAutocomplete.SimpleAutocompletes[i].hide();
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** UTILITY FUNCTIONS ***
|
2012-10-14 02:15:54 +04:00
|
|
|
// Remove this section if you already have these functions defined somewhere else
|
2011-11-09 20:51:07 +04:00
|
|
|
|
2014-09-22 17:52:20 +04:00
|
|
|
// Cross-browser add/remove event listeners
|
|
|
|
var addListener = function()
|
|
|
|
{
|
|
|
|
return window.addEventListener
|
|
|
|
? function(el, type, fn) { el.addEventListener(type, fn, false); }
|
|
|
|
: function(el, type, fn) { el.attachEvent('on'+type, fn); };
|
|
|
|
}();
|
|
|
|
|
|
|
|
var removeListener = function()
|
|
|
|
{
|
|
|
|
return window.removeEventListener
|
|
|
|
? function(el, type, fn) { el.removeEventListener(type, fn, false); }
|
|
|
|
: function(el, type, fn) { el.detachEvent('on'+type, fn); };
|
|
|
|
}();
|
|
|
|
|
2011-11-08 18:14:01 +04:00
|
|
|
// Cancel event bubbling and/or default action
|
|
|
|
var stopEvent = function(ev, cancelBubble, preventDefault)
|
|
|
|
{
|
|
|
|
if (cancelBubble)
|
|
|
|
{
|
|
|
|
if (ev.stopPropagation)
|
|
|
|
ev.stopPropagation();
|
|
|
|
else
|
|
|
|
ev.cancelBubble = true;
|
|
|
|
}
|
|
|
|
if (preventDefault && ev.preventDefault)
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.returnValue = !preventDefault;
|
|
|
|
return !preventDefault;
|
|
|
|
};
|
|
|
|
|
2014-09-22 17:52:20 +04:00
|
|
|
// Remove leading and trailing whitespace
|
|
|
|
if (!String.prototype.trim)
|
|
|
|
{
|
|
|
|
String.prototype.trim = function()
|
|
|
|
{
|
|
|
|
return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2011-11-09 20:51:07 +04:00
|
|
|
// Get element position, relative to the top-left corner of page
|
|
|
|
var getOffset = function(elem)
|
|
|
|
{
|
|
|
|
if (elem.getBoundingClientRect)
|
|
|
|
return getOffsetRect(elem);
|
|
|
|
else
|
|
|
|
return getOffsetSum(elem);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Get element position using getBoundingClientRect()
|
|
|
|
var getOffsetRect = function(elem)
|
|
|
|
{
|
|
|
|
var box = elem.getBoundingClientRect();
|
|
|
|
|
|
|
|
var body = document.body;
|
|
|
|
var docElem = document.documentElement;
|
|
|
|
|
|
|
|
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
|
|
|
|
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
|
|
|
|
var clientTop = docElem.clientTop || body.clientTop || 0;
|
|
|
|
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
|
|
|
|
var top = box.top + scrollTop - clientTop;
|
|
|
|
var left = box.left + scrollLeft - clientLeft;
|
|
|
|
|
|
|
|
return { top: Math.round(top), left: Math.round(left) };
|
|
|
|
};
|
|
|
|
|
|
|
|
// Get element position using sum of offsetTop/offsetLeft
|
|
|
|
var getOffsetSum = function(elem)
|
|
|
|
{
|
|
|
|
var top = 0, left = 0;
|
|
|
|
while(elem)
|
|
|
|
{
|
|
|
|
top = top + parseInt(elem.offsetTop);
|
|
|
|
left = left + parseInt(elem.offsetLeft);
|
|
|
|
elem = elem.offsetParent;
|
|
|
|
}
|
|
|
|
return { top: top, left: left };
|
|
|
|
};
|
|
|
|
|
2012-10-13 18:29:46 +04:00
|
|
|
// *** END UTILITY FUNCTIONS ***
|
2011-11-09 20:51:07 +04:00
|
|
|
|
2011-11-08 18:14:01 +04:00
|
|
|
// Set global mousedown listener
|
|
|
|
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });
|