Drop maxHeight and innerHTML, add "side-effect" mode with multipleListener

master
Vitaliy Filippov 2013-03-05 17:55:49 +00:00
parent 1d6b572985
commit 100235c5e4
1 changed files with 106 additions and 90 deletions

196
hinter.js
View File

@ -2,7 +2,7 @@
Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete
License: MPL 2.0+ (http://www.mozilla.org/MPL/2.0/) License: MPL 2.0+ (http://www.mozilla.org/MPL/2.0/)
(c) Vitaliy Filippov 2011-2012 (c) Vitaliy Filippov 2011-2013
Usage: Usage:
Include hinter.css, hinter.js on your page. Then write: Include hinter.css, hinter.js on your page. Then write:
@ -14,17 +14,19 @@
dataLoader(hint, value[, more]) dataLoader(hint, value[, more])
Callback which should load autocomplete options and then call: Callback which should load autocomplete options and then call:
hint.replaceItems(newOptions, append) hint.replaceItems(newOptions, append)
newOptions is [ [ name, value[, disabled ] ], [ name, value ], ... ] newOptions = [ [ name, value, disabled, checked ] ], [ name, value ], ... ]
append=(more>0) name = HTML option name
value = plaintext option value
disabled = only meaningful when multipleListener is set
checked = only meaningful when multipleListener is set
append = (more>0)
Callback parameters: Callback parameters:
hint hint
This SimpleAutocomplete object This SimpleAutocomplete object
value value
The string guess should be done based on The string guess should be done based on
more more
This is optional and means the times user has triggered 'load more items'. The 'page' of autocomplete options to load, 0 = first page.
i.e. if the original list had 10 items (more=0), after first 'more' click
user would expect 20 items (more=1), and etc.
See also moreMarker option below. See also moreMarker option below.
params attribute is an object with optional parameters: params attribute is an object with optional parameters:
@ -35,17 +37,16 @@
dataLoader should handle it's 'value' parameter accordingly in this case, 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 because it will be just the raw value of the input, probably with incomplete
item or items, typed by the user. item or items, typed by the user.
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.
onChangeListener(hint, index) onChangeListener(hint, index)
Callback which is called when input value is changed using this dropdown. Callback which is called when input value is changed using this dropdown.
index is the number of element which selection is changed, starting with 0. index is the number of element which selection is changed, starting with 0.
It must be used instead of normal 'onchange' event. It must be used instead of normal 'onchange' event.
maxHeight
Maximum hint dropdown height in pixels
emptyText emptyText
Text to show when dataLoader returns no options. Text to show when dataLoader returns no options. Empty (default) means 'hide hint'.
If emptyText === false, the hint will be hidden instead of showing text.
allowHTML
If true, HTML code will be allowed in option names.
prompt prompt
HTML text to be displayed before a non-empty option list. Empty by default. HTML text to be displayed before a non-empty option list. Empty by default.
delay delay
@ -53,16 +54,18 @@
1 request in each delay milliseconds. 1 request in each delay milliseconds.
moreMarker moreMarker
The server supplying hint options usually limits their count. The server supplying hint options usually limits their count.
But it's not always convenient having to type additional characters for But it's not always convenient having to type additional characters to
narrowing down the selection. Optionally you can supply additional item narrow down the selection. Optionally you can supply additional item
with special value '#MORE' (or 'moreMarker' option value if it is there) with special value equal to moreMarker value or '#MORE' at the end
at the end of the list, and when it will be clicked, SimpleAutocomplete of the list, and SimpleAutocomplete will issue another request to
will issue another request to dataLoader with incremented 'more' parameter. dataLoader with incremented 'more' parameter when it will be clicked.
You can also set moreMarker to false to disable this feature. You can also set moreMarker to false to disable this feature.
Other methods: Destroy instance:
hint.remove() -- Destroy the instance hint.remove(); hint = null;
hint.enableItem(index, enabled) -- Enable or disable an item
Change multiselect item state:
hint.changeMultiItem(index, name, disabled, checked);
*/ */
// *** Constructor *** // *** Constructor ***
@ -77,11 +80,10 @@ var SimpleAutocomplete = function(input, dataLoader, params)
// Parameters // Parameters
this.input = input; this.input = input;
this.dataLoader = dataLoader; this.dataLoader = dataLoader;
this.multipleDelimiter = params.multipleDelimiter; this.multipleDelimiter = params.multipleDelimiter || params.multipleListener;
this.multipleListener = params.multipleListener;
this.onChangeListener = params.onChangeListener; this.onChangeListener = params.onChangeListener;
this.maxHeight = params.maxHeight;
this.emptyText = params.emptyText; this.emptyText = params.emptyText;
this.allowHTML = params.allowHTML;
this.prompt = params.prompt; this.prompt = params.prompt;
this.delay = params.delay; this.delay = params.delay;
this.moreMarker = params.moreMarker; this.moreMarker = params.moreMarker;
@ -91,8 +93,6 @@ var SimpleAutocomplete = function(input, dataLoader, params)
this.moreMarker = '#MORE'; this.moreMarker = '#MORE';
if (this.delay === undefined) if (this.delay === undefined)
this.delay = 300; this.delay = 300;
if (this.emptyText === undefined)
this.emptyText = false;
// Variables // Variables
this.origAutocomplete = input.autocomplete; this.origAutocomplete = input.autocomplete;
@ -128,14 +128,6 @@ SimpleAutocomplete.prototype.init = function()
t.style.top = (p.top+e.offsetHeight) + 'px'; t.style.top = (p.top+e.offsetHeight) + 'px';
t.style.zIndex = 1000; t.style.zIndex = 1000;
t.style.left = p.left + 'px'; t.style.left = p.left + 'px';
if (this.maxHeight)
{
t.style.overflowY = 'scroll';
try { t.style.overflow = '-moz-scrollbars-vertical'; } catch(exc) {}
t.style.maxHeight = this.maxHeight+'px';
if (!t.style.maxHeight)
this.scriptMaxHeight = true;
}
document.body.appendChild(t); document.body.appendChild(t);
// Remember instance // Remember instance
@ -160,7 +152,7 @@ SimpleAutocomplete.prototype.init = function()
this.onChange(); this.onChange();
}; };
// items = [ [ name, value[, disabled] ], [ name, value ], ... ] // items = [ [ name, value ], [ name, value ], ... ]
SimpleAutocomplete.prototype.replaceItems = function(items, append) SimpleAutocomplete.prototype.replaceItems = function(items, append)
{ {
if (!append) if (!append)
@ -178,20 +170,22 @@ SimpleAutocomplete.prototype.replaceItems = function(items, append)
this.hintLayer.innerHTML = this.prompt ? '<div class="hintPrompt">'+this.prompt+'</div>' : ''; this.hintLayer.innerHTML = this.prompt ? '<div class="hintPrompt">'+this.prompt+'</div>' : '';
this.enable(); this.enable();
} }
var h = {}; if (!this.multipleListener)
if (this.multipleDelimiter) for (var i in items)
items[i][2] = 0;
if (this.multipleDelimiter && !this.multipleListener)
{ {
var h = {};
var old = this.input.value.split(this.multipleDelimiter); var old = this.input.value.split(this.multipleDelimiter);
for (var i = 0; i < old.length; i++) for (var i = 0; i < old.length; i++)
h[old[i].trim()] = true; h[old[i].trim()] = true;
for (var i in items)
items[i][3] = h[items[i][1]];
} }
for (var i in items) for (var i in items)
this.hintLayer.appendChild(this.makeItem(items[i][0], items[i][1], items[i][2], h[items[i][1]]));
if (this.maxHeight)
{ {
this.hintLayer.style.height = this.hintLayer.appendChild(this.makeItem(this.items.length, items[i]));
(this.hintLayer.scrollHeight > this.maxHeight this.items.push(items[i]);
? this.maxHeight : this.hintLayer.scrollHeight) + 'px';
} }
}; };
@ -226,37 +220,49 @@ SimpleAutocomplete.prototype.remove = function()
this.items = null; this.items = null;
}; };
// Enable or disable an item // Check/uncheck/enable/disable/rename multiselect item
SimpleAutocomplete.prototype.enableItem = function(index, enabled) SimpleAutocomplete.prototype.changeMultiItem = function(index, name, disabled, checked)
{ {
if (this.items && this.items[index]) if (this.items && this.items[index] && this.multipleDelimiter)
{ {
this.items[index][2] = enabled; var d = document.getElementById(this.id+'_item_'+index);
document.getElementById(this.id+'_item_'+index).style = enabled ? 'hintItem' : 'hintDisabledItem'; d.className = disabled ? 'hintDisabledItem' : 'hintItem';
if (this.multipleDelimiter) var c = d.children[0];
document.getElementById(this.id+'_check_'+index).disabled = !enabled; if (name && name != this.items[index][0])
{
d.removeChild(c);
d.innerHTML = name;
d.childNodes.length ? d.insertBefore(c, d.childNodes[0]) : d.appendChild(c);
this.items[index][0] = name;
}
c.checked = checked && true;
c.disabled = disabled && true;
this.items[index][2] = disabled;
if ((this.items[index][3] && true) != c.checked && !this.multipleListener)
{
this.items[index][3] = c.checked;
this.toggleValue(index);
this.curValue = this.input.value;
}
} }
}; };
// Create a drop-down list item, include checkbox if this.multipleDelimiter is true // Create a drop-down list item, include checkbox if this.multipleDelimiter is true
SimpleAutocomplete.prototype.makeItem = function(name, value, disabled, checked) SimpleAutocomplete.prototype.makeItem = function(index, item)
{ {
var d = document.createElement('div'); var d = document.createElement('div');
d.id = this.id+'_item_'+this.items.length; d.id = this.id+'_item_'+index;
d.className = disabled ? 'hintDisabledItem' : 'hintItem'; d.className = item[2] ? 'hintDisabledItem' : 'hintItem';
d.title = value; d.title = item[1];
if (this.allowHTML) d.innerHTML = item[0];
d.innerHTML = name;
else
d.appendChild(document.createTextNode(name));
if (this.multipleDelimiter) if (this.multipleDelimiter)
{ {
var c = document.createElement('input'); var c = document.createElement('input');
c.type = 'checkbox'; c.type = 'checkbox';
c.id = this.id+'_check_'+this.items.length; c.id = this.id+'_check_'+index;
c.checked = checked && true; c.checked = item[3] && true;
c.disabled = disabled && true; c.disabled = item[2] && true;
c.value = value; c.value = item[1];
if (d.childNodes.length) if (d.childNodes.length)
d.insertBefore(c, d.firstChild); d.insertBefore(c, d.firstChild);
else else
@ -266,7 +272,6 @@ SimpleAutocomplete.prototype.makeItem = function(name, value, disabled, checked)
var self = this; var self = this;
addListener(d, 'mouseover', function() { return self.onItemMouseOver(this); }); addListener(d, 'mouseover', function() { return self.onItemMouseOver(this); });
addListener(d, 'mousedown', function(ev) { return self.onItemClick(ev, this); }); addListener(d, 'mousedown', function(ev) { return self.onItemClick(ev, this); });
this.items.push([name, value, disabled, checked]);
return d; return d;
}; };
@ -320,43 +325,54 @@ SimpleAutocomplete.prototype.selectItem = function(index)
else else
{ {
document.getElementById(this.id+'_check_'+index).checked = this.items[index][3] = !this.items[index][3]; document.getElementById(this.id+'_check_'+index).checked = this.items[index][3] = !this.items[index][3];
var old = this.input.value.split(this.multipleDelimiter); if (this.multipleListener)
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--) this.multipleListener(this, index, this.items[index]);
if (old[i] == this.items[index][1]) return;
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+' ');
} }
this.toggleValue(index);
} }
this.curValue = this.input.value; this.curValue = this.input.value;
if (this.onChangeListener) if (this.onChangeListener)
this.onChangeListener(this, index); this.onChangeListener(this, index);
}; };
// 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+' ');
}
}
// Hide hinter // Hide hinter
SimpleAutocomplete.prototype.hide = function() SimpleAutocomplete.prototype.hide = function()
{ {