From c16781a238d6d6d1e2aa27f146e758cf517a7bab Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sat, 13 Oct 2012 14:29:46 +0000 Subject: [PATCH] Move SimpleAutocomplete functions to the prototype, remove obsolete SHint --- hinter.js | 752 +++++++++++++++++++++++++++--------------------------- 1 file changed, 383 insertions(+), 369 deletions(-) diff --git a/hinter.js b/hinter.js index cdc9702..0c0cc60 100644 --- a/hinter.js +++ b/hinter.js @@ -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) });