From 62f3b28e15a04314ada6d5b0cde5ed809f9e880e Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Wed, 9 Nov 2011 16:51:07 +0000 Subject: [PATCH] Multiple selection support, add utility functions from getOffset.js --- hinter.css | 6 ++ hinter.js | 185 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 164 insertions(+), 27 deletions(-) diff --git a/hinter.css b/hinter.css index eb6ea4f..d0cad11 100644 --- a/hinter.css +++ b/hinter.css @@ -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; diff --git a/hinter.js b/hinter.js index 25bdc8f..691da19 100644 --- a/hinter.js +++ b/hinter.js @@ -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) });