diff --git a/hinter.js b/hinter.js index 9f9517a..68a5f1c 100644 --- a/hinter.js +++ b/hinter.js @@ -2,7 +2,7 @@ Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete License: MPL 2.0+ (http://www.mozilla.org/MPL/2.0/) - (c) Vitaliy Filippov 2011-2012 + (c) Vitaliy Filippov 2011-2013 Usage: Include hinter.css, hinter.js on your page. Then write: @@ -14,17 +14,19 @@ dataLoader(hint, value[, more]) Callback which should load autocomplete options and then call: hint.replaceItems(newOptions, append) - newOptions is [ [ name, value[, disabled ] ], [ name, value ], ... ] - append=(more>0) + newOptions = [ [ name, value, disabled, checked ] ], [ name, value ], ... ] + name = HTML option name + value = plaintext option value + disabled = only meaningful when multipleListener is set + checked = only meaningful when multipleListener is set + append = (more>0) Callback parameters: hint This SimpleAutocomplete object value The string guess should be done based on more - This is optional and means the times user has triggered 'load more items'. - i.e. if the original list had 10 items (more=0), after first 'more' click - user would expect 20 items (more=1), and etc. + The 'page' of autocomplete options to load, 0 = first page. See also moreMarker option below. params attribute is an object with optional parameters: @@ -35,17 +37,16 @@ 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. + multipleListener(hint, index, item) + If you don't want to touch the input value, but want to use multi-select for + your own purposes, specify a callback that will handle item clicks here. + Also you can disable and check/uncheck items during loading in this mode. onChangeListener(hint, index) Callback which is called when input value is changed using this dropdown. index is the number of element which selection is changed, starting with 0. 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. - allowHTML - If true, HTML code will be allowed in option names. + Text to show when dataLoader returns no options. Empty (default) means 'hide hint'. prompt HTML text to be displayed before a non-empty option list. Empty by default. delay @@ -53,16 +54,18 @@ 1 request in each delay milliseconds. moreMarker The server supplying hint options usually limits their count. - But it's not always convenient having to type additional characters for - narrowing down the selection. Optionally you can supply additional item - with special value '#MORE' (or 'moreMarker' option value if it is there) - at the end of the list, and when it will be clicked, SimpleAutocomplete - will issue another request to dataLoader with incremented 'more' parameter. + But it's not always convenient having to type additional characters to + narrow down the selection. Optionally you can supply additional item + with special value equal to moreMarker value or '#MORE' at the end + of the list, and SimpleAutocomplete will issue another request to + dataLoader with incremented 'more' parameter when it will be clicked. You can also set moreMarker to false to disable this feature. - Other methods: - hint.remove() -- Destroy the instance - hint.enableItem(index, enabled) -- Enable or disable an item + Destroy instance: + hint.remove(); hint = null; + + Change multiselect item state: + hint.changeMultiItem(index, name, disabled, checked); */ // *** Constructor *** @@ -77,11 +80,10 @@ var SimpleAutocomplete = function(input, dataLoader, params) // Parameters this.input = input; this.dataLoader = dataLoader; - this.multipleDelimiter = params.multipleDelimiter; + this.multipleDelimiter = params.multipleDelimiter || params.multipleListener; + this.multipleListener = params.multipleListener; this.onChangeListener = params.onChangeListener; - this.maxHeight = params.maxHeight; this.emptyText = params.emptyText; - this.allowHTML = params.allowHTML; this.prompt = params.prompt; this.delay = params.delay; this.moreMarker = params.moreMarker; @@ -91,8 +93,6 @@ var SimpleAutocomplete = function(input, dataLoader, params) this.moreMarker = '#MORE'; if (this.delay === undefined) this.delay = 300; - if (this.emptyText === undefined) - this.emptyText = false; // Variables this.origAutocomplete = input.autocomplete; @@ -128,14 +128,6 @@ SimpleAutocomplete.prototype.init = function() 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 @@ -160,7 +152,7 @@ SimpleAutocomplete.prototype.init = function() this.onChange(); }; -// items = [ [ name, value[, disabled] ], [ name, value ], ... ] +// items = [ [ name, value ], [ name, value ], ... ] SimpleAutocomplete.prototype.replaceItems = function(items, append) { if (!append) @@ -178,20 +170,22 @@ SimpleAutocomplete.prototype.replaceItems = function(items, append) this.hintLayer.innerHTML = this.prompt ? '
'+this.prompt+'
' : ''; this.enable(); } - var h = {}; - if (this.multipleDelimiter) + if (!this.multipleListener) + for (var i in items) + items[i][2] = 0; + if (this.multipleDelimiter && !this.multipleListener) { + var h = {}; 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) + items[i][3] = h[items[i][1]]; } for (var i in items) - this.hintLayer.appendChild(this.makeItem(items[i][0], items[i][1], items[i][2], h[items[i][1]])); - if (this.maxHeight) { - this.hintLayer.style.height = - (this.hintLayer.scrollHeight > this.maxHeight - ? this.maxHeight : this.hintLayer.scrollHeight) + 'px'; + this.hintLayer.appendChild(this.makeItem(this.items.length, items[i])); + this.items.push(items[i]); } }; @@ -226,37 +220,49 @@ SimpleAutocomplete.prototype.remove = function() this.items = null; }; -// Enable or disable an item -SimpleAutocomplete.prototype.enableItem = function(index, enabled) +// Check/uncheck/enable/disable/rename multiselect item +SimpleAutocomplete.prototype.changeMultiItem = function(index, name, disabled, checked) { - if (this.items && this.items[index]) + if (this.items && this.items[index] && this.multipleDelimiter) { - this.items[index][2] = enabled; - document.getElementById(this.id+'_item_'+index).style = enabled ? 'hintItem' : 'hintDisabledItem'; - if (this.multipleDelimiter) - document.getElementById(this.id+'_check_'+index).disabled = !enabled; + var d = document.getElementById(this.id+'_item_'+index); + d.className = disabled ? 'hintDisabledItem' : 'hintItem'; + var c = d.children[0]; + if (name && name != this.items[index][0]) + { + d.removeChild(c); + d.innerHTML = name; + d.childNodes.length ? d.insertBefore(c, d.childNodes[0]) : d.appendChild(c); + this.items[index][0] = name; + } + c.checked = checked && true; + c.disabled = disabled && true; + this.items[index][2] = disabled; + if ((this.items[index][3] && true) != c.checked && !this.multipleListener) + { + this.items[index][3] = c.checked; + this.toggleValue(index); + this.curValue = this.input.value; + } } }; // Create a drop-down list item, include checkbox if this.multipleDelimiter is true -SimpleAutocomplete.prototype.makeItem = function(name, value, disabled, checked) +SimpleAutocomplete.prototype.makeItem = function(index, item) { var d = document.createElement('div'); - d.id = this.id+'_item_'+this.items.length; - d.className = disabled ? 'hintDisabledItem' : 'hintItem'; - d.title = value; - if (this.allowHTML) - d.innerHTML = name; - else - d.appendChild(document.createTextNode(name)); + d.id = this.id+'_item_'+index; + d.className = item[2] ? 'hintDisabledItem' : 'hintItem'; + d.title = item[1]; + d.innerHTML = item[0]; if (this.multipleDelimiter) { var c = document.createElement('input'); c.type = 'checkbox'; - c.id = this.id+'_check_'+this.items.length; - c.checked = checked && true; - c.disabled = disabled && true; - c.value = value; + c.id = this.id+'_check_'+index; + c.checked = item[3] && true; + c.disabled = item[2] && true; + c.value = item[1]; if (d.childNodes.length) d.insertBefore(c, d.firstChild); else @@ -266,7 +272,6 @@ SimpleAutocomplete.prototype.makeItem = function(name, value, disabled, checked) var self = this; addListener(d, 'mouseover', function() { return self.onItemMouseOver(this); }); addListener(d, 'mousedown', function(ev) { return self.onItemClick(ev, this); }); - this.items.push([name, value, disabled, checked]); return d; }; @@ -320,43 +325,54 @@ SimpleAutocomplete.prototype.selectItem = function(index) else { document.getElementById(this.id+'_check_'+index).checked = this.items[index][3] = !this.items[index][3]; - var old = this.input.value.split(this.multipleDelimiter); - for (var i = 0; i < old.length; i++) - old[i] = old[i].trim(); - // Turn the clicked item on or off, preserving order - if (!this.items[index][3]) + if (this.multipleListener) { - 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][3]) - 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][3] && h[this.items[i][1]]) - nl.push(this.items[i][1]); - this.input.value = nl.join(this.multipleDelimiter+' '); + this.multipleListener(this, index, this.items[index]); + return; } + this.toggleValue(index); } this.curValue = this.input.value; if (this.onChangeListener) this.onChangeListener(this, index); }; +// Change input value so it will respect index'th item state in a multi-select +SimpleAutocomplete.prototype.toggleValue = function(index) +{ + var old = this.input.value.split(this.multipleDelimiter); + for (var i = 0; i < old.length; i++) + old[i] = old[i].trim(); + // Turn the clicked item on or off, preserving order + if (!this.items[index][3]) + { + 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][3]) + 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][3] && h[this.items[i][1]]) + nl.push(this.items[i][1]); + this.input.value = nl.join(this.multipleDelimiter+' '); + } +} + // Hide hinter SimpleAutocomplete.prototype.hide = function() {