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