? misc/autocomplete.js
? misc/drupal.js
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.443
diff -u -r1.443 common.inc
--- includes/common.inc 21 May 2005 18:33:59 -0000 1.443
+++ includes/common.inc 22 May 2005 21:38:43 -0000
@@ -1257,6 +1257,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;
+ global $base_url;
+
+ if (!$header_sent) {
+ 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 22 May 2005 00:13:44 -0000
@@ -551,3 +551,28 @@
ul.secondary a.active {
border-bottom: 4px solid #999;
}
+
+/*
+** Autocomplete styles
+*/
+#autocomplete {
+ position: absolute;
+ border-width: 1px;
+ border-style: solid;
+ overflow: hidden;
+ white-space: pre;
+}
+#autocomplete ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+#autocomplete li {
+ cursor: pointer;
+ background: #e1f1ff;
+ color: #000;
+}
+#autocomplete li.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 22 May 2005 10:38:21 -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' => $admin_access, 'type' => MENU_CALLBACK);
+
//registration and login pages.
$items[] = array('path' => 'user/login', 'title' => t('log in'),
'type' => MENU_DEFAULT_LOCAL_TASK);
@@ -1851,4 +1854,17 @@
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[] = htmlspecialchars($r->name);
+ }
+ echo implode('|', $matches);
+ exit();
+}
+
?>
--- misc/autocomplete.js
+++ misc/autocomplete.js
@@ -0,0 +1,252 @@
+addLoadEvent(autocomplete_auto_attach);
+
+/**
+ * Attaches the autocomplete behaviour to all required fields
+ */
+function autocomplete_auto_attach() {
+ // Only continue if the browser supports several essential functions
+ if (!document.getElementsByTagName || !document.createElement || !document.createTextNode || !document.getElementById) {
+ return false;
+ }
+ var acdb = [];
+ var inputs = document.getElementsByTagName('input');
+ for (i=0;input=inputs[i];i++) {
+ if (input && hasClass(input, 'autocomplete')) {
+ uri = input.value;
+ if (!acdb[uri]) {
+ acdb[uri] = new ACDB(uri);
+ }
+ id = input.id.substr(0,input.id.length - 13);
+ input = document.getElementById(id);
+ input.setAttribute('autocomplete','OFF');
+ input.form.onsubmit = autocomplete_submit;
+ new jsAC(input, acdb[uri]);
+ }
+ }
+}
+
+/**
+ * Prevents the form from submitting if the suggestions popup is open
+ */
+function autocomplete_submit() {
+ var popup = document.getElementById('autocomplete');
+ if (popup) {
+ popup.owner.hidePopup();
+ return false;
+ }
+ return true;
+}
+
+
+/**
+ * An AutoComplete object
+ */
+function jsAC(input, db) {
+ var ac = this;
+ this.input = input;
+ this.db = db;
+ this.db.owner = 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.hidePopup() };
+ this.popup = document.createElement('div');
+ this.popup.id = 'autocomplete';
+ this.popup.owner = this;
+};
+
+/**
+ * Hides the autocomplete suggestions
+ */
+jsAC.prototype.hidePopup = function (keycode) {
+ if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
+ this.input.value = this.selected.innerHTML;
+ }
+ if (this.popup.parentNode && this.popup.parentNode.tagName) {
+ removeNode(this.popup);
+ }
+ this.selected = false;
+}
+
+/**
+ * Handler for the "keydown" event
+ */
+jsAC.prototype.onkeydown = function (input, e) {
+ if (!e) {
+ e = window.event;
+ }
+ switch (e.keyCode) {
+ case 40: // down arrow
+ this.selectDown();
+ return false;
+ case 38: // up arrow
+ this.selectUp();
+ return false;
+ default: // all other keys
+ return true;
+ }
+}
+
+/**
+ * Handler for the "keyup" event
+ */
+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;
+ }
+}
+
+/**
+ * Puts the currently highlighted suggestion into the autocomplete field
+ */
+jsAC.prototype.select = function (node) {
+ this.input.value = node.innerHTML;
+}
+
+/**
+ * Highlights the next suggestion
+ */
+jsAC.prototype.selectDown = function () {
+ if (this.selected && this.selected.nextSibling) {
+ this.highlight(this.selected.nextSibling);
+ }
+ else {
+ var lis = this.popup.getElementsByTagName('li');
+ if (lis.length > 0) {
+ this.highlight(lis[0]);
+ }
+ }
+}
+
+/**
+ * Highlights the previous suggestion
+ */
+jsAC.prototype.selectUp = function () {
+ if (this.selected && this.selected.previousSibling) {
+ this.highlight(this.selected.previousSibling);
+ }
+}
+
+/**
+ * Highlights a suggestion
+ */
+jsAC.prototype.highlight = function (node) {
+ removeClass(this.selected, 'selected');
+ addClass(node, 'selected');
+ this.selected = node;
+}
+
+/**
+ * Unhighlights a suggestion
+ */
+jsAC.prototype.unhighlight = function (node) {
+ removeClass(node, 'selected');
+ this.selected = false;
+}
+
+/**
+ * Positions the suggestions popup and starts a search
+ */
+jsAC.prototype.populatePopup = function () {
+ var ac = this;
+ var pos = AbsolutePosition(this.input);
+ this.selected = false;
+ 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.search(this.input.value);
+}
+
+/**
+ * Fills the suggestion popup with any matches received
+ */
+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 ul = document.createElement('ul');
+ var ac = this;
+ if (matches.length > 0) {
+ for (i in matches) {
+ li = document.createElement('li');
+ li.appendChild(document.createTextNode(matches[i]));
+ li.onmousedown = function() { ac.select(this); };
+ li.onmouseover = function() { ac.highlight(this); };
+ li.onmouseout = function() { ac.unhighlight(this); };
+ ul.appendChild(li);
+ }
+ this.popup.appendChild(ul);
+ }
+ else {
+ this.hidePopup();
+ }
+}
+
+/**
+ * An AutoComplete DataBase object
+ */
+function ACDB(uri) {
+ this.uri = uri;
+ this.max = 15;
+ this.delay = 300;
+ this.cache = {};
+}
+
+/**
+ * Performs a cached and delayed search
+ */
+ACDB.prototype.search = function(search_string) {
+ if (this.docache) {
+ this.search_string = search_string;
+ if (this.cache[search_string]) {
+ return this.match(this.cache[search_string]);
+ }
+ }
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ var db = this;
+ this.timer = setTimeout(function() { HTTPGet(db.uri + '/' + search_string + '/' + db.max, db.receive, db); }, this.delay);
+}
+
+/**
+ * HTTP callback function. Passes suggestions to the autocomplete object
+ */
+ACDB.prototype.receive = function(string, xmlhttp, acdb) {
+ if (xmlhttp.status != 200) {
+ return alert('An HTTP error ' + xmlhttp.status + ' occured.\n' + acdb.uri);
+ }
+ var matches = string.length > 0 ? string.split('|') : [];
+ acdb.cache[acdb.search_string] = matches;
+ acdb.owner.found(matches);
+}
--- misc/drupal.js
+++ misc/drupal.js
@@ -0,0 +1,145 @@
+/**
+ * Make IE's XMLHTTP object accessible through XMLHttpRequest()
+ */
+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!")
+ }
+}
+
+/**
+ * Creates an HTTP request and sends the response to the callback function
+ */
+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;
+ }
+}
+
+/**
+ * Adds a function to the window onload event
+ */
+function addLoadEvent(func) {
+ var oldonload = window.onload;
+ if (typeof window.onload != 'function') {
+ window.onload = func;
+ }
+ else {
+ window.onload = function() {
+ oldonload();
+ func();
+ }
+ }
+}
+
+/**
+ * Retrieves the absolute position of an element on the screen
+ */
+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;
+};
+
+/**
+ * Returns true if an element has a specified class name
+ */
+function hasClass(node, className) {
+ if (node.className == className) {
+ return true;
+ }
+ var reg = new RegExp('\\b'+className+'\\b')
+ if (reg.test(node.className)) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Adds a class name to an element
+ */
+function addClass (node, className) {
+ if (hasClass(node, className)) {
+ return false;
+ }
+ node.className += ' ' + className;
+ return true;
+}
+
+/**
+ * Removes a class name from an element
+ */
+function removeClass (node, className) {
+ if (!hasClass(node, className)) {
+ return false;
+ }
+ node.className = str_replace(className, '', node.className);
+ return true;
+}
+
+/**
+ * Toggles a class name on or off for an element
+ */
+function toggleClass(node, className) {
+ if (!removeClass(node, className) && !addClass(node, className)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Emulate PHP's str_replace function in javascript
+ */
+function str_replace(search, replace, subject) {
+ return subject.replace(new RegExp(search,'g'), replace);
+}
+
+/**
+ * Removes an element from the page
+ */
+function removeNode(node) {
+ if (typeof node == 'string') {
+ node = document.getElementById(node);
+ }
+ if (node && node.parentNode) {
+ return node.parentNode.removeChild(node);
+ }
+ else {
+ return false;
+ }
+}