? misc/autocomplete.js ? misc/drupal.js Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.442 diff -u -r1.442 common.inc --- includes/common.inc 20 May 2005 11:31:16 -0000 1.442 +++ includes/common.inc 21 May 2005 16:39:55 -0000 @@ -1230,6 +1230,48 @@ } /** + * Format a single-line text field that has uses AJAX for autocomplete. + * + * @param $title + * The label for the text field. + * @param $name + * The internal name used to refer to the field. + * @param $value + * The initial value for the field at page load time. + * @param $size + * A measure of the visible size of the field (passed directly to HTML). + * @param $maxlength + * The maximum number of characters that may be entered in the field. + * @param $callback_path + * A drupal path for the AJAX autocomplete callback. + * @param $description + * Explanatory text to display after the form item. + * @param $attributes + * An associative array of HTML attributes to add to the form item. + * @param $required + * Whether the user must enter some text in the field. + * @return + * A themed HTML string representing the field. + */ +function form_autocomplete($title, $name, $value, $size, $maxlength, $callback_path, $description = NULL, $attributes = NULL, $required = FALSE) { + static $header_sent = false; + + if (!$header_sent) { + theme_add_style('modules/ajax/autocomplete.css'); + drupal_set_html_head(''); + drupal_set_html_head(''); + $header_sent = true; + } + + $size = $size ? ' size="'. $size .'"' : ''; + + $output = theme('form_element', $title, '', $description, 'edit-'. $name, $required, _form_get_error($name)); + $output .= ''; + + return $output; +} + +/** * Format a single-line text field that does not display its contents visibly. * * @param $title Index: misc/drupal.css =================================================================== RCS file: /cvs/drupal/drupal/misc/drupal.css,v retrieving revision 1.103 diff -u -r1.103 drupal.css --- misc/drupal.css 21 May 2005 11:53:01 -0000 1.103 +++ misc/drupal.css 21 May 2005 14:25:27 -0000 @@ -551,3 +551,26 @@ ul.secondary a.active { border-bottom: 4px solid #999; } + +/* +** Autocomplete styles +*/ +#autocomplete { + position: absolute; + margin: 0; + padding: 0; + border: 1px solid #000; + background: #e1f1ff; + color: #000; + overflow: hidden; + white-space: pre; +} +#autocomplete p { + margin: 0; + padding: 2px; + cursor: pointer; +} +#autocomplete p.selected { + background: #a4d7ff; + color: #000; +} \ No newline at end of file Index: modules/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node.module,v retrieving revision 1.488 diff -u -r1.488 node.module --- modules/node.module 17 May 2005 20:24:33 -0000 1.488 +++ modules/node.module 21 May 2005 14:46:29 -0000 @@ -1317,7 +1317,7 @@ if (user_access('administer nodes')) { $output .= '
'; - $author = form_textfield(t('Authored by'), 'name', $edit->name, 20, 60); + $author = form_autocomplete(t('Authored by'), 'name', $edit->name, 20, 60, 'user/autocomplete'); $author .= form_textfield(t('Authored on'), 'date', $edit->date, 20, 25, NULL, NULL, TRUE); $output .= '
'; Index: modules/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user.module,v retrieving revision 1.473 diff -u -r1.473 user.module --- modules/user.module 21 May 2005 11:57:59 -0000 1.473 +++ modules/user.module 21 May 2005 15:03:43 -0000 @@ -638,6 +638,9 @@ $items[] = array('path' => 'user', 'title' => t('user account'), 'callback' => 'user_page', 'access' => TRUE, 'type' => MENU_CALLBACK); + $items[] = array('path' => 'user/autocomplete', 'title' => t('user autocomplete'), + 'callback' => 'user_autocomplete', 'access' => true, 'type' => MENU_CALLBACK); + //registration and login pages. $items[] = array('path' => 'user/login', 'title' => t('log in'), 'type' => MENU_DEFAULT_LOCAL_TASK); @@ -1851,4 +1854,16 @@ return $output; } +/** + * Retrieve a pipe delimited string of autocomplete suggestions for existing users + */ +function user_autocomplete($string, $limit = 10) { + $matches = array(); + $rs = db_query_range('SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER("%%%s%%")', $string, 0, $limit); + while ($r = db_fetch_object($rs)) { + $matches[] = $r->name; + } + echo implode('|', $matches); +} + ?> --- misc/autocomplete.js +++ misc/autocomplete.js @@ -0,0 +1,280 @@ +addLoadEvent(autocomplete_auto_attach); + +function AbsolutePosition(el) { + var SL = 0, ST = 0; + var is_div = /^div$/i.test(el.tagName); + if (is_div && el.scrollLeft) + SL = el.scrollLeft; + if (is_div && el.scrollTop) + ST = el.scrollTop; + var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST }; + if (el.offsetParent) { + var tmp = AbsolutePosition(el.offsetParent); + r.x += tmp.x; + r.y += tmp.y; + } + return r; +}; + +function autocomplete_auto_attach() { + var acdb = []; + var inputs = document.getElementsByTagName('input'); + for (i=0;input=inputs[i];i++) { + if (input && input.className == 'autocomplete') { + var uri = input.value; + if (!acdb[uri]) { + acdb[uri] = new ACDB_Remote(uri); + } + var id = input.id.substr(0,input.id.length - 13); + input = document.getElementById(id); + input.setAttribute('autocomplete','OFF'); + input.form.onsubmit = autocomplete_submit; + NewAutoComplete(input, acdb[uri]); + } + } +} + +function autocomplete_submit() { + var popup = document.getElementById('autocomplete'); + if (popup) { + popup.owner.hidePopup(); + return false; + } + return true; +} + +function NewAutoComplete(input, db) { + if (!input.parentNode) + input = document.getElementById(input); + var ac = new jsAC(input, db); +} + +/* === jsAC Class (javascript AutoComplete) === */ + +function jsAC(input, db) { + this.input = input; + this.init(); + this.db = db; +}; + +jsAC.prototype.init = function() { + var ac = this; + this.input.onkeydown = function (event) { return ac.onkeydown(this, event); } + this.input.onkeyup = function (event) { ac.onkeyup(this, event) } + this.input.onblur = function () { ac.onblur(this) } + this.popup = document.createElement('div'); + this.popup.id = 'autocomplete'; + this.popup.owner = this; +} + +jsAC.prototype.hidePopup = function (keycode) { + if ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode) + if (this.selected) + this.input.value = this.selected.innerHTML; + if (this.popup.parentNode && this.popup.parentNode.tagName) + this.popup.parentNode.removeChild(this.popup); + this.selected = false; +} + +jsAC.prototype.onkeydown = function (input, e) { + if (!e) e = window.event; + switch (e.keyCode) { + case 40: + this.selectDown(); + return false; + case 38: + this.selectUp(); + return false; + case 13: + return false; + default: + return true; + } +} + +jsAC.prototype.onkeyup = function (input, e) { + if (!e) e = window.event; + switch (e.keyCode) { + case 16: // shift + case 17: // ctrl + case 18: // alt + case 20: // caps lock + case 33: // page up + case 34: // page down + case 35: // end + case 36: // home + case 37: // left arrow + case 38: // up arrow + case 39: // right arrow + case 40: // down arrow + return true; + + case 9: // tab + case 13: // enter + case 27: // esc + this.hidePopup(e.keyCode); + return true; + + default: // all other keys + if (input.value.length > 0) + this.populatePopup(); + else + this.hidePopup(e.keyCode); + return true; + } +} + +jsAC.prototype.onblur = function (input) { + this.hidePopup(); +} + +jsAC.prototype.select = function (node) { + this.input.value = node.innerHTML; +} + +jsAC.prototype.selectDown = function () { + if (this.selected) { + if (this.selected.nextSibling && this.selected.nextSibling.tagName) + this.highlight(this.selected.nextSibling); + } else { + var ps = this.popup.getElementsByTagName('p'); + if (ps.length > 0) + this.highlight(ps[0]); + } +} + +jsAC.prototype.selectUp = function () { + if (this.selected && this.selected.previousSibling && this.selected.previousSibling.tagName) { + this.highlight(this.selected.previousSibling); + } +} + +jsAC.prototype.highlight = function (node) { + if (this.selected) + this.selected.className = ''; + node.className = 'selected'; + this.selected = node; + //this.input.value = node.innerHTML; +} + +jsAC.prototype.unhighlight = function (node) { + node.className = ''; + this.selected = false; +} + +jsAC.prototype.populatePopup = function () { + var ac = this; + this.selected = false; + var pos = AbsolutePosition(this.input); + this.popup.style.top = (pos.y + this.input.offsetHeight) + 'px'; + this.popup.style.left = pos.x + 'px'; + this.popup.style.width = (this.input.offsetWidth - 4) + 'px'; + this.db.onmatch = function(matches) { ac.found(matches); } + this.db.search(this.input.value); +} + +jsAC.prototype.found = function (matches) { + + while (this.popup.hasChildNodes()) + this.popup.removeChild(this.popup.childNodes[0]); + if (!this.popup.parentNode || !this.popup.parentNode.tagName) + document.getElementsByTagName('body')[0].appendChild(this.popup); + + var div = document.createElement('div'); + var ac = this; + if (matches.length > 0) { + for (var i in matches) { + var p = document.createElement('p'); + p.appendChild(document.createTextNode(matches[i])); + p.onmousedown = function() { ac.select(this); } + p.onmouseover = function() { ac.highlight(this); } + p.onmouseout = function() { ac.unhighlight(this); } + div.appendChild(p); + } + this.popup.appendChild(div); + } else { + this.hidePopup(); + } +} + + + + +/* === ACDB Base class (AutoComplete DataBase) ===*/ + +function ACDB() { + this.max = 15; + this.delay = 300; + this.docache = false; + this.cache = {}; +} + +ACDB.prototype.search = function(search_string) { + if (!this.dosearch) + return false; + if (this.docache) { + this.search_string = search_string; + if (this.cache[search_string]) + return this.match(this.cache[search_string]); + } + var db = this; + if (this.timer) + clearTimeout(this.timer); + this.timer = setTimeout(function() { db.dosearch(search_string) }, this.delay); +} + +ACDB.prototype.match = function (matches) { + if (this.docache) + this.cache[this.search_string] = matches; + if (this.onmatch) + this.onmatch(matches); +} + + + + +/* === class ACDB_JS extends ACDB === */ + +function ACDB_JS(array_of_strings) { + this.strings = array_of_strings; + this.delay = 30; +} + +ACDB_JS.prototype = new ACDB; + +ACDB_JS.prototype.dosearch = function(search_string) { + var search_length = search_string.length; + var matches = []; + var test_string = ''; + + for (var i = 0; test_string = this.strings[i]; i++) { + if (test_string.substr(0,search_length).toLowerCase() == search_string.toLowerCase()) + matches[matches.length] = test_string; + if (matches.length >= this.max) + break; + } + this.match(matches); +} + + + + +/* === class ACDB_Remote extends ACDB === */ + +function ACDB_Remote(uri) { + this.uri = uri; + this.delay = 300; + this.docache = true; +} + +ACDB_Remote.prototype = new ACDB; + +ACDB_Remote.prototype.dosearch = function(search_string) { + HTTPGet(this.uri + '/' + search_string + '/' + this.max, this.receive, this); +} + +ACDB_Remote.prototype.receive = function(string, xmlhttp, acdb) { + if (xmlhttp.status != 200) + return alert('An HTTP error ' + xmlhttp.status + ' occured.\n' + acdb.uri); + acdb.match(string.length > 0 ? string.split('|') : []); +} --- misc/drupal.js +++ misc/drupal.js @@ -0,0 +1,42 @@ +if (typeof XMLHttpRequest == 'undefined') { + XMLHttpRequest = function () { + var msxmls = ['MSXML3', 'MSXML2', 'Microsoft'] + for (var i=0; i < msxmls.length; i++) { + try { + return new ActiveXObject(msxmls[i]+'.XMLHTTP') + } catch (e) { } + } + throw new Error("No XML component installed!") + } +} + +function HTTPGet(uri, callback_function, callback_parameter) { + var xmlhttp = new XMLHttpRequest(); + var bAsync = true; + if (!callback_function) + bAsync = false; + xmlhttp.open('GET', uri, bAsync); + xmlhttp.send(null); + + if (bAsync) { + if (callback_function) + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) + callback_function(xmlhttp.responseText, xmlhttp, callback_parameter) + } + return true; + } else + return xmlhttp.responseText; +} + +function addLoadEvent(func) { + var oldonload = window.onload; + if (typeof window.onload != 'function') { + window.onload = func; + } else { + window.onload = function() { + oldonload(); + func(); + } + } +}