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.
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:
Include hinter.css, hinter.js on your page. Then write:
var hint = new SimpleAutocomplete(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML);
Parameters:
input
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' parameter will be this autocompleter object, and the guess
should be done based on 'value' parameter (string).
Optional parameters:
multipleDelimiter
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.
*/
// *** Constructor ***
var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML)
{
if (typeof(input) == 'string')
@ -41,377 +48,384 @@ var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChange
emptyText = 'No items found';
// Parameters
var self = this;
self.input = input;
self.multipleDelimiter = multipleDelimiter;
self.dataLoader = dataLoader;
self.onChangeListener = onChangeListener;
self.maxHeight = maxHeight;
self.emptyText = emptyText;
self.allowHTML = allowHTML;
this.input = input;
this.multipleDelimiter = multipleDelimiter;
this.dataLoader = dataLoader;
this.onChangeListener = onChangeListener;
this.maxHeight = maxHeight;
this.emptyText = emptyText;
this.allowHTML = allowHTML;
// Variables
self.items = [];
self.skipHideCounter = 0;
self.selectedIndex = -1;
self.id = input.id;
self.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();
}
this.items = [];
this.skipHideCounter = 0;
this.selectedIndex = -1;
this.id = input.id;
this.disabled = false;
// *** 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 = [];
// Global mousedown handler, hides dropdowns when clicked outside
@ -438,8 +452,8 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
return true;
};
//// UTILITY FUNCTIONS ////
// You can delete this section if you already have them somewhere in your scripts //
// *** UTILITY FUNCTIONS ***
// Remove this section if you already have these functions somewhere else included
// Cross-browser adding of event listeners
var addListener = function()
@ -526,7 +540,7 @@ var getOffsetSum = function(elem)
return { top: top, left: left };
};
//// END UTILITY FUNCTIONS ////
// *** END UTILITY FUNCTIONS ***
// Set global mousedown listener
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });