Multiple selection support, add utility functions from getOffset.js

master
Vitaliy Filippov 2011-11-09 16:51:07 +00:00
parent 78a2118554
commit 62f3b28e15
2 changed files with 164 additions and 27 deletions

View File

@ -15,6 +15,12 @@
color: black;
cursor: pointer;
padding: 1px 3px;
vertical-align: middle;
}
.hintItem input {
cursor: pointer;
vertical-align: middle;
margin-right: 3px;
}
.hintActiveItem {
color: white;

185
hinter.js
View File

@ -1,21 +1,36 @@
/* Simple autocomplete for text inputs.
Usage:
1) include hinter.css, hinter.js, offsetRect.js
2) var hint = new SimpleAutocomplete(input, dataLoader, onChangeListener, maxHeight, emptyText);
Parameters:
input - the input, either id or DOM element
dataLoader(hint, value) - callback which should load autocomplete options
and call hint.replaceItems([ [ name, value ], [ name, value ], ... ])
Optional parameters:
onChangeListener - callback which is called when an item is selected through the drop-down list
maxHeight - maximum hint dropdown height in pixels
emptyText - text to show when dataLoader returns no options
if emptyText === false, the hint will be hidden
/* Simple autocomplete for text inputs, with the support for multiple selection.
Homepage: http://yourcmc.ru/wiki/SHint_JS
(c) Vitaliy Filippov 2011
Usage:
Include hinter.css, hinter.js on your page. Then write:
var hint = new SimpleAutocomplete(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText);
Parameters:
input
The input, either id or DOM element reference.
dataLoader(hint, value)
Callback which should load autocomplete options and then call
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.
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.
onChangeListener
Callback which is called when input value is changed using this dropdown.
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.
*/
var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight, emptyText)
var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText)
{
if (typeof(input) == 'string')
input = document.getElementById(input);
@ -25,6 +40,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
// Parameters
var self = this;
self.input = input;
self.multipleDelimiter = multipleDelimiter;
self.dataLoader = dataLoader;
self.onChangeListener = onChangeListener;
self.maxHeight = maxHeight;
@ -37,8 +53,8 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
self.id = input.id;
self.disabled = false;
// Initialise hinter
self.init = function()
// Initialiser
var init = function()
{
var e = self.input;
var p = getOffset(e);
@ -102,8 +118,15 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
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]));
self.hintLayer.appendChild(self.makeItem(items[i][0], items[i][1], h[items[i][1]]));
if (self.maxHeight)
{
self.hintLayer.style.height =
@ -112,20 +135,37 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
}
};
// Create a drop-down list item
self.makeItem = function(name, value)
// 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.multipleDelimiter)
{
var c = document.createElement('input');
c.type = 'checkbox';
c.id = self.id+'_check_'+self.items.length;
c.checked = checked && true;
c.value = value;
d.appendChild(c);
addListener(c, 'click', self.preventCheck);
}
d.appendChild(document.createTextNode(name));
addListener(d, 'mouseover', self.onItemMouseOver);
addListener(d, 'mousedown', self.onItemClick);
self.items.push([name, value]);
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()
{
@ -133,7 +173,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
};
// Handle item clicks
self.onItemClick = function()
self.onItemClick = function(ev)
{
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
return true;
@ -175,11 +215,48 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
return document.getElementById(self.id+'_item_'+self.selectedIndex);
};
// Select index'th item - change the input value and hide the hint
// Select index'th item - change the input value and hide the hint if not a multi-select
self.selectItem = function(index)
{
self.input.value = self.items[index][1];
self.hide();
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+' ');
}
}
if (self.onChangeListener)
self.onChangeListener(self, index);
};
@ -252,6 +329,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
self.onInputFocus = function()
{
self.show();
self.hasFocus = true;
return true;
};
@ -259,6 +337,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
self.onInputBlur = function()
{
self.hide();
self.hasFocus = false;
return true;
};
@ -295,7 +374,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
}
// *** Call initialise ***
self.init();
init();
};
// Global variable
@ -313,7 +392,8 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
break;
else if (target.SimpleAutocomplete_layer)
{
target.SimpleAutocomplete_layer.skipHideCounter++;
if (target.SimpleAutocomplete_layer.hasFocus)
target.SimpleAutocomplete_layer.skipHideCounter++;
return true;
}
target = target.parentNode;
@ -324,7 +404,10 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
return true;
};
// Cross-browser adding of event listeners (remove if you already have it)
//// UTILITY FUNCTIONS ////
// You can delete this section if you already have them somewhere in your scripts //
// Cross-browser adding of event listeners
var addListener = function()
{
if (window.addEventListener)
@ -360,5 +443,53 @@ var stopEvent = function(ev, cancelBubble, preventDefault)
return !preventDefault;
};
// Remove leading and trailing whitespace
String.prototype.trim = function()
{
return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
// 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 };
};
//// END UTILITY FUNCTIONS ////
// Set global mousedown listener
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });