Move SimpleAutocomplete functions to the prototype, remove obsolete SHint

master
Vitaliy Filippov 2012-10-13 14:29:46 +00:00
parent f2320f0aa4
commit c16781a238
1 changed files with 383 additions and 369 deletions

752
hinter.js
View File

@ -1,9 +1,13 @@
/* Simple autocomplete for text inputs, with the support for multiple selection. /* Simple autocomplete for text inputs, with the support for multiple selection.
Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete
(c) Vitaliy Filippov 2011 License: FSF LGPL v3 or later (http://www.gnu.org/licenses/lgpl.html)
(c) Vitaliy Filippov 2011-2012
Usage: Usage:
Include hinter.css, hinter.js on your page. Then write: Include hinter.css, hinter.js on your page. Then write:
var hint = new SimpleAutocomplete(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML); var hint = new SimpleAutocomplete(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML);
Parameters: Parameters:
input input
The input, either id or DOM element reference (the input must have an id anyway). The input, either id or DOM element reference (the input must have an id anyway).
@ -12,6 +16,7 @@
hint.replaceItems([ [ name, value ], [ name, value ], ... ]) hint.replaceItems([ [ name, value ], [ name, value ], ... ])
'hint' parameter will be this autocompleter object, and the guess 'hint' parameter will be this autocompleter object, and the guess
should be done based on 'value' parameter (string). should be done based on 'value' parameter (string).
Optional parameters: Optional parameters:
multipleDelimiter multipleDelimiter
Pass a delimiter string (for example ',' or ';') to enable multiple selection. Pass a delimiter string (for example ',' or ';') to enable multiple selection.
@ -33,6 +38,8 @@
If true, HTML code will be allowed in option names. If true, HTML code will be allowed in option names.
*/ */
// *** Constructor ***
var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML) var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML)
{ {
if (typeof(input) == 'string') if (typeof(input) == 'string')
@ -41,377 +48,384 @@ var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChange
emptyText = 'No items found'; emptyText = 'No items found';
// Parameters // Parameters
var self = this; this.input = input;
self.input = input; this.multipleDelimiter = multipleDelimiter;
self.multipleDelimiter = multipleDelimiter; this.dataLoader = dataLoader;
self.dataLoader = dataLoader; this.onChangeListener = onChangeListener;
self.onChangeListener = onChangeListener; this.maxHeight = maxHeight;
self.maxHeight = maxHeight; this.emptyText = emptyText;
self.emptyText = emptyText; this.allowHTML = allowHTML;
self.allowHTML = allowHTML;
// Variables // Variables
self.items = []; this.items = [];
self.skipHideCounter = 0; this.skipHideCounter = 0;
self.selectedIndex = -1; this.selectedIndex = -1;
self.id = input.id; this.id = input.id;
self.disabled = false; this.disabled = false;
// Initialiser
var init = function()
{
var e = self.input;
var p = getOffset(e);
// Create hint layer
var t = self.hintLayer = document.createElement('div');
t.className = 'hintLayer';
t.style.display = 'none';
t.style.position = 'absolute';
t.style.top = (p.top+e.offsetHeight) + 'px';
t.style.zIndex = 1000;
t.style.left = p.left + 'px';
if (self.maxHeight)
{
t.style.overflowY = 'scroll';
try { t.style.overflow = '-moz-scrollbars-vertical'; } catch(exc) {}
t.style.maxHeight = self.maxHeight+'px';
if (!t.style.maxHeight)
self.scriptMaxHeight = true;
}
document.body.appendChild(t);
// Remember instance
e.SimpleAutocomplete_input = self;
t.SimpleAutocomplete_layer = self;
SimpleAutocomplete.SimpleAutocompletes.push(self);
// Set event listeners
var ie_opera = navigator.userAgent.match('MSIE') || navigator.userAgent.match('Opera');
if (ie_opera)
addListener(e, 'keydown', self.onKeyPress);
else
{
addListener(e, 'keydown', self.onKeyDown);
addListener(e, 'keypress', self.onKeyPress);
}
addListener(e, 'keyup', self.onKeyUp);
addListener(e, 'change', self.onChange);
addListener(e, 'focus', self.onInputFocus);
addListener(e, 'blur', self.onInputBlur);
self.onChange();
};
// obj = [ [ name, value, disabled ], [ name, value ], ... ]
self.replaceItems = function(items)
{
self.hintLayer.innerHTML = '';
self.hintLayer.scrollTop = 0;
self.items = [];
if (!items || items.length == 0)
{
if (self.emptyText)
{
var d = document.createElement('div');
d.className = 'hintEmptyText';
d.innerHTML = self.emptyText;
self.hintLayer.appendChild(d);
}
else
self.disable();
return;
}
self.enable();
var h = {};
if (self.multipleDelimiter)
{
var old = self.input.value.split(self.multipleDelimiter);
for (var i = 0; i < old.length; i++)
h[old[i].trim()] = true;
}
for (var i in items)
self.hintLayer.appendChild(self.makeItem(items[i][0], items[i][1], h[items[i][1]]));
if (self.maxHeight)
{
self.hintLayer.style.height =
(self.hintLayer.scrollHeight > self.maxHeight
? self.maxHeight : self.hintLayer.scrollHeight) + 'px';
}
};
// Create a drop-down list item, include checkbox if self.multipleDelimiter is true
self.makeItem = function(name, value, checked)
{
var d = document.createElement('div');
d.id = self.id+'_item_'+self.items.length;
d.className = 'hintItem';
d.title = value;
if (self.allowHTML)
d.innerHTML = name;
if (self.multipleDelimiter)
{
var c = document.createElement('input');
c.type = 'checkbox';
c.id = self.id+'_check_'+self.items.length;
c.checked = checked && true;
c.value = value;
if (d.childNodes.length)
d.insertBefore(c, d.firstChild);
else
d.appendChild(c);
addListener(c, 'click', self.preventCheck);
}
if (!self.allowHTML)
d.appendChild(document.createTextNode(name));
addListener(d, 'mouseover', self.onItemMouseOver);
addListener(d, 'mousedown', self.onItemClick);
self.items.push([name, value, checked]);
return d;
};
// Prevent default action on checkbox
self.preventCheck = function(ev)
{
ev = ev||window.event;
return stopEvent(ev, false, true);
};
// Handle item mouse over
self.onItemMouseOver = function()
{
return self.highlightItem(this);
};
// Handle item clicks
self.onItemClick = function(ev)
{
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
return true;
};
// Move highlight forward or back by 'by' items (integer)
self.moveHighlight = function(by)
{
var n = self.selectedIndex+by;
if (n < 0)
n = 0;
var elem = document.getElementById(self.id+'_item_'+n);
if (!elem)
return true;
return self.highlightItem(elem);
};
// Make item 'elem' active (highlighted)
self.highlightItem = function(elem)
{
if (self.selectedIndex >= 0)
{
var c = self.getItem();
if (c)
c.className = 'hintItem';
}
self.selectedIndex = parseInt(elem.id.substr(self.id.length+6));
elem.className = 'hintActiveItem';
return false;
};
// Get index'th item, or current when index is null
self.getItem = function(index)
{
if (index == null)
index = self.selectedIndex;
if (index < 0)
return null;
return document.getElementById(self.id+'_item_'+self.selectedIndex);
};
// Select index'th item - change the input value and hide the hint if not a multi-select
self.selectItem = function(index)
{
if (!self.multipleDelimiter)
{
self.input.value = self.items[index][1];
self.hide();
}
else
{
document.getElementById(self.id+'_check_'+index).checked = self.items[index][2] = !self.items[index][2];
var old = self.input.value.split(self.multipleDelimiter);
for (var i = 0; i < old.length; i++)
old[i] = old[i].trim();
if (!self.items[index][2])
{
for (var i = old.length-1; i >= 0; i--)
if (old[i] == self.items[index][1])
old.splice(i, 1);
self.input.value = old.join(self.multipleDelimiter+' ');
}
else
{
var h = {};
for (var i = 0; i < self.items.length; i++)
if (self.items[i][2])
h[self.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 < self.items.length; i++)
if (self.items[i][2] && h[self.items[i][1]])
nl.push(self.items[i][1]);
self.input.value = nl.join(self.multipleDelimiter+' ');
}
}
self.curValue = self.input.value;
if (self.onChangeListener)
self.onChangeListener(self, index);
};
// Handle user input, load new items
self.onChange = function()
{
var v = self.input.value.trim();
if (v != self.curValue)
{
self.curValue = v;
self.dataLoader(self, v);
}
return true;
};
// Handle Enter key presses, cancel handling of arrow keys
self.onKeyUp = function(ev)
{
ev = ev||window.event;
if (ev.keyCode != 10 && ev.keyCode != 13)
self.show();
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 10 || ev.keyCode == 13)
{
if (self.hintLayer.style.display == '')
return stopEvent(ev, true, true);
else
return true;
}
self.onChange();
return true;
};
// Cancel handling of Enter key
self.onKeyDown = function(ev)
{
ev = ev||window.event;
if (ev.keyCode == 10 || ev.keyCode == 13)
{
if (self.hintLayer.style.display == '')
return stopEvent(ev, true, true);
else
return true;
}
return true;
};
// Handle arrow keys and Enter
self.onKeyPress = function(ev)
{
if (self.hintLayer.style.display == 'none')
return true;
ev = ev||window.event;
if (ev.keyCode == 38) // up
self.moveHighlight(-1);
else if (ev.keyCode == 40) // down
self.moveHighlight(1);
else if (ev.keyCode == 10 || ev.keyCode == 13) // enter
{
if (self.selectedIndex >= 0)
self.selectItem(self.selectedIndex);
return stopEvent(ev, true, true);
}
else
return true;
// scrolling
if (self.selectedIndex >= 0)
{
var c = self.getItem();
var t = self.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;
}
return stopEvent(ev, true, true);
};
// Called when input receives focus
self.onInputFocus = function()
{
self.show();
self.input.autocomplete = 'off';
self.hasFocus = true;
return true;
};
// Called when input loses focus
self.onInputBlur = function()
{
self.hide();
self.input.autocomplete = 'on';
self.hasFocus = false;
return true;
};
// Hide hinter
self.hide = function()
{
if (!self.skipHideCounter)
{
self.hintLayer.style.display = 'none';
self.input.autocomplete = 'on';
}
else
self.skipHideCounter = 0;
};
// Show hinter
self.show = function()
{
if (!self.disabled)
{
var p = getOffset(self.input);
self.hintLayer.style.top = (p.top+self.input.offsetHeight) + 'px';
self.hintLayer.style.left = p.left + 'px';
self.hintLayer.style.display = '';
self.input.autocomplete = 'off';
}
};
// Disable hinter, for the case when there is no items and no empty text
self.disable = function()
{
self.disabled = true;
self.hide();
};
// Enable hinter
self.enable = function()
{
var show = self.disabled;
self.disabled = false;
if (show)
self.show();
}
// *** Call initialise *** // *** Call initialise ***
init(); this.init();
}; };
// Global variable // *** Instance methods ***
// Initialiser
SimpleAutocomplete.prototype.init = function()
{
var e = this.input;
var p = getOffset(e);
// Create hint layer
var t = this.hintLayer = document.createElement('div');
t.className = 'hintLayer';
t.style.display = 'none';
t.style.position = 'absolute';
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
e.SimpleAutocomplete_input = self;
t.SimpleAutocomplete_layer = self;
SimpleAutocomplete.SimpleAutocompletes.push(self);
// Set event listeners
var self = this;
var ie_opera = navigator.userAgent.match('MSIE') || navigator.userAgent.match('Opera');
if (ie_opera)
addListener(e, 'keydown', function(ev) { return self.onKeyPress(ev); });
else
{
addListener(e, 'keydown', function(ev) { return self.onKeyDown(ev); });
addListener(e, 'keypress', function(ev) { return self.onKeyPress(ev); });
}
addListener(e, 'keyup', function(ev) { return self.onKeyUp(ev); });
addListener(e, 'change', function() { return self.onChange(); });
addListener(e, 'focus', function() { return self.onInputFocus(); });
addListener(e, 'blur', function() { return self.onInputBlur(); });
this.onChange();
};
// obj = [ [ name, value, disabled ], [ name, value ], ... ]
SimpleAutocomplete.prototype.replaceItems = function(items)
{
this.hintLayer.innerHTML = '';
this.hintLayer.scrollTop = 0;
this.items = [];
if (!items || items.length == 0)
{
if (this.emptyText)
{
var d = document.createElement('div');
d.className = 'hintEmptyText';
d.innerHTML = this.emptyText;
this.hintLayer.appendChild(d);
}
else
this.disable();
return;
}
this.enable();
var h = {};
if (this.multipleDelimiter)
{
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)
this.hintLayer.appendChild(this.makeItem(items[i][0], items[i][1], h[items[i][1]]));
if (this.maxHeight)
{
this.hintLayer.style.height =
(this.hintLayer.scrollHeight > this.maxHeight
? this.maxHeight : this.hintLayer.scrollHeight) + 'px';
}
};
// Create a drop-down list item, include checkbox if this.multipleDelimiter is true
SimpleAutocomplete.prototype.makeItem = function(name, value, checked)
{
var d = document.createElement('div');
d.id = this.id+'_item_'+this.items.length;
d.className = 'hintItem';
d.title = value;
if (this.allowHTML)
d.innerHTML = name;
if (this.multipleDelimiter)
{
var c = document.createElement('input');
c.type = 'checkbox';
c.id = this.id+'_check_'+this.items.length;
c.checked = checked && true;
c.value = value;
if (d.childNodes.length)
d.insertBefore(c, d.firstChild);
else
d.appendChild(c);
addListener(c, 'click', this.preventCheck);
}
if (!this.allowHTML)
d.appendChild(document.createTextNode(name));
var self = this;
addListener(d, 'mouseover', function() { return self.onItemMouseOver(this); });
addListener(d, 'mousedown', function() { return self.onItemClick(this); });
this.items.push([name, value, checked]);
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;
var elem = document.getElementById(this.id+'_item_'+n);
if (!elem)
return true;
return this.highlightItem(elem);
};
// Make item 'elem' active (highlighted)
SimpleAutocomplete.prototype.highlightItem = function(elem)
{
if (this.selectedIndex >= 0)
{
var c = this.getItem();
if (c)
c.className = 'hintItem';
}
this.selectedIndex = parseInt(elem.id.substr(this.id.length+6));
elem.className = 'hintActiveItem';
return false;
};
// 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);
};
// Select index'th item - change the input value and hide the hint if not a multi-select
SimpleAutocomplete.prototype.selectItem = function(index)
{
if (!this.multipleDelimiter)
{
this.input.value = this.items[index][1];
this.hide();
}
else
{
document.getElementById(this.id+'_check_'+index).checked = this.items[index][2] = !this.items[index][2];
var old = this.input.value.split(this.multipleDelimiter);
for (var i = 0; i < old.length; i++)
old[i] = old[i].trim();
if (!this.items[index][2])
{
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][2])
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][2] && h[this.items[i][1]])
nl.push(this.items[i][1]);
this.input.value = nl.join(this.multipleDelimiter+' ');
}
}
this.curValue = this.input.value;
if (this.onChangeListener)
this.onChangeListener(self, index);
};
// Hide hinter
SimpleAutocomplete.prototype.hide = function()
{
if (!this.skipHideCounter)
{
this.hintLayer.style.display = 'none';
this.input.autocomplete = 'on';
}
else
this.skipHideCounter = 0;
};
// Show hinter
SimpleAutocomplete.prototype.show = function()
{
if (!this.disabled)
{
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 = '';
this.input.autocomplete = 'off';
}
};
// Disable hinter, for the case when there is no items and no empty text
SimpleAutocomplete.prototype.disable = function()
{
this.disabled = true;
this.hide();
};
// Enable hinter
SimpleAutocomplete.prototype.enable = function()
{
var show = this.disabled;
this.disabled = false;
if (show)
this.show();
}
// *** Event handlers ***
// Prevent default action on checkbox
SimpleAutocomplete.prototype.preventCheck = function(ev)
{
ev = ev||window.event;
return stopEvent(ev, false, true);
};
// Handle item mouse over
SimpleAutocomplete.prototype.onItemMouseOver = function(elm)
{
return this.highlightItem(elm);
};
// Handle item clicks
SimpleAutocomplete.prototype.onItemClick = function(elm)
{
this.selectItem(parseInt(elm.id.substr(this.id.length+6)));
return true;
};
// Handle user input, load new items
SimpleAutocomplete.prototype.onChange = function()
{
var v = this.input.value.trim();
if (v != this.curValue)
{
this.curValue = v;
this.dataLoader(this, v);
}
return true;
};
// Handle Enter key presses, cancel handling of arrow keys
SimpleAutocomplete.prototype.onKeyUp = function(ev)
{
ev = ev||window.event;
if (ev.keyCode != 10 && ev.keyCode != 13)
this.show();
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 10 || ev.keyCode == 13)
{
if (this.hintLayer.style.display == '')
return stopEvent(ev, true, true);
else
return true;
}
this.onChange();
return true;
};
// Cancel handling of Enter key
SimpleAutocomplete.prototype.onKeyDown = function(ev)
{
ev = ev||window.event;
if (ev.keyCode == 10 || ev.keyCode == 13)
{
if (this.hintLayer.style.display == '')
return stopEvent(ev, true, true);
else
return true;
}
return true;
};
// Handle arrow keys and Enter
SimpleAutocomplete.prototype.onKeyPress = function(ev)
{
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);
else if (ev.keyCode == 10 || ev.keyCode == 13) // enter
{
if (this.selectedIndex >= 0)
this.selectItem(this.selectedIndex);
return stopEvent(ev, true, true);
}
else
return true;
// scrolling
if (this.selectedIndex >= 0)
{
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;
}
return stopEvent(ev, true, true);
};
// Called when input receives focus
SimpleAutocomplete.prototype.onInputFocus = function()
{
this.show();
this.input.autocomplete = 'off';
this.hasFocus = true;
return true;
};
// Called when input loses focus
SimpleAutocomplete.prototype.onInputBlur = function()
{
this.hide();
this.input.autocomplete = 'on';
this.hasFocus = false;
return true;
};
// *** Global variables ***
// List of all instances
SimpleAutocomplete.SimpleAutocompletes = []; SimpleAutocomplete.SimpleAutocompletes = [];
// Global mousedown handler, hides dropdowns when clicked outside // Global mousedown handler, hides dropdowns when clicked outside
@ -438,8 +452,8 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
return true; return true;
}; };
//// UTILITY FUNCTIONS //// // *** UTILITY FUNCTIONS ***
// You can delete this section if you already have them somewhere in your scripts // // Remove this section if you already have these functions somewhere else included
// Cross-browser adding of event listeners // Cross-browser adding of event listeners
var addListener = function() var addListener = function()
@ -526,7 +540,7 @@ var getOffsetSum = function(elem)
return { top: top, left: left }; return { top: top, left: left };
}; };
//// END UTILITY FUNCTIONS //// // *** END UTILITY FUNCTIONS ***
// Set global mousedown listener // Set global mousedown listener
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) }); addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });