Multiple selection support, add utility functions from getOffset.js
parent
78a2118554
commit
62f3b28e15
|
@ -15,6 +15,12 @@
|
||||||
color: black;
|
color: black;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.hintItem input {
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
.hintActiveItem {
|
.hintActiveItem {
|
||||||
color: white;
|
color: white;
|
||||||
|
|
185
hinter.js
185
hinter.js
|
@ -1,21 +1,36 @@
|
||||||
/* Simple autocomplete for text inputs.
|
/* Simple autocomplete for text inputs, with the support for multiple selection.
|
||||||
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
|
|
||||||
Homepage: http://yourcmc.ru/wiki/SHint_JS
|
Homepage: http://yourcmc.ru/wiki/SHint_JS
|
||||||
(c) Vitaliy Filippov 2011
|
(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')
|
if (typeof(input) == 'string')
|
||||||
input = document.getElementById(input);
|
input = document.getElementById(input);
|
||||||
|
@ -25,6 +40,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
// Parameters
|
// Parameters
|
||||||
var self = this;
|
var self = this;
|
||||||
self.input = input;
|
self.input = input;
|
||||||
|
self.multipleDelimiter = multipleDelimiter;
|
||||||
self.dataLoader = dataLoader;
|
self.dataLoader = dataLoader;
|
||||||
self.onChangeListener = onChangeListener;
|
self.onChangeListener = onChangeListener;
|
||||||
self.maxHeight = maxHeight;
|
self.maxHeight = maxHeight;
|
||||||
|
@ -37,8 +53,8 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
self.id = input.id;
|
self.id = input.id;
|
||||||
self.disabled = false;
|
self.disabled = false;
|
||||||
|
|
||||||
// Initialise hinter
|
// Initialiser
|
||||||
self.init = function()
|
var init = function()
|
||||||
{
|
{
|
||||||
var e = self.input;
|
var e = self.input;
|
||||||
var p = getOffset(e);
|
var p = getOffset(e);
|
||||||
|
@ -102,8 +118,15 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.enable();
|
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)
|
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)
|
if (self.maxHeight)
|
||||||
{
|
{
|
||||||
self.hintLayer.style.height =
|
self.hintLayer.style.height =
|
||||||
|
@ -112,20 +135,37 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a drop-down list item
|
// Create a drop-down list item, include checkbox if self.multipleDelimiter is true
|
||||||
self.makeItem = function(name, value)
|
self.makeItem = function(name, value, checked)
|
||||||
{
|
{
|
||||||
var d = document.createElement('div');
|
var d = document.createElement('div');
|
||||||
d.id = self.id+'_item_'+self.items.length;
|
d.id = self.id+'_item_'+self.items.length;
|
||||||
d.className = 'hintItem';
|
d.className = 'hintItem';
|
||||||
d.title = value;
|
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));
|
d.appendChild(document.createTextNode(name));
|
||||||
addListener(d, 'mouseover', self.onItemMouseOver);
|
addListener(d, 'mouseover', self.onItemMouseOver);
|
||||||
addListener(d, 'mousedown', self.onItemClick);
|
addListener(d, 'mousedown', self.onItemClick);
|
||||||
self.items.push([name, value]);
|
self.items.push([name, value, checked]);
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prevent default action on checkbox
|
||||||
|
self.preventCheck = function(ev)
|
||||||
|
{
|
||||||
|
ev = ev||window.event;
|
||||||
|
return stopEvent(ev, false, true);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle item mouse over
|
// Handle item mouse over
|
||||||
self.onItemMouseOver = function()
|
self.onItemMouseOver = function()
|
||||||
{
|
{
|
||||||
|
@ -133,7 +173,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle item clicks
|
// Handle item clicks
|
||||||
self.onItemClick = function()
|
self.onItemClick = function(ev)
|
||||||
{
|
{
|
||||||
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
|
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
|
||||||
return true;
|
return true;
|
||||||
|
@ -175,11 +215,48 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
return document.getElementById(self.id+'_item_'+self.selectedIndex);
|
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.selectItem = function(index)
|
||||||
{
|
{
|
||||||
self.input.value = self.items[index][1];
|
if (!self.multipleDelimiter)
|
||||||
self.hide();
|
{
|
||||||
|
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)
|
if (self.onChangeListener)
|
||||||
self.onChangeListener(self, index);
|
self.onChangeListener(self, index);
|
||||||
};
|
};
|
||||||
|
@ -252,6 +329,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
self.onInputFocus = function()
|
self.onInputFocus = function()
|
||||||
{
|
{
|
||||||
self.show();
|
self.show();
|
||||||
|
self.hasFocus = true;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -259,6 +337,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
self.onInputBlur = function()
|
self.onInputBlur = function()
|
||||||
{
|
{
|
||||||
self.hide();
|
self.hide();
|
||||||
|
self.hasFocus = false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -295,7 +374,7 @@ var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// *** Call initialise ***
|
// *** Call initialise ***
|
||||||
self.init();
|
init();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global variable
|
// Global variable
|
||||||
|
@ -313,7 +392,8 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
|
||||||
break;
|
break;
|
||||||
else if (target.SimpleAutocomplete_layer)
|
else if (target.SimpleAutocomplete_layer)
|
||||||
{
|
{
|
||||||
target.SimpleAutocomplete_layer.skipHideCounter++;
|
if (target.SimpleAutocomplete_layer.hasFocus)
|
||||||
|
target.SimpleAutocomplete_layer.skipHideCounter++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
target = target.parentNode;
|
target = target.parentNode;
|
||||||
|
@ -324,7 +404,10 @@ SimpleAutocomplete.GlobalMouseDown = function(ev)
|
||||||
return true;
|
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()
|
var addListener = function()
|
||||||
{
|
{
|
||||||
if (window.addEventListener)
|
if (window.addEventListener)
|
||||||
|
@ -360,5 +443,53 @@ var stopEvent = function(ev, cancelBubble, preventDefault)
|
||||||
return !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
|
// Set global mousedown listener
|
||||||
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });
|
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });
|
||||||
|
|
Loading…
Reference in New Issue