cvs diff: Diffing . cvs diff: Diffing database cvs diff: Diffing includes 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 24 May 2005 05:03:28 -0000 @@ -1257,6 +1257,41 @@ } /** + * 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) { + drupal_add_js('misc/autocomplete.js'); + + $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 @@ -1917,6 +1952,42 @@ '); } +/** + * Add a JavaScript file to the output. + * + * The first time this function is invoked per page request, + * it adds "misc/drupal.js" to the output. Other scripts + * depends on the 'killswitch' inside it. + */ +function drupal_add_js($file) { + static $sent = array(); + if (!$sent['misc/drupal.js']) { + drupal_set_html_head(''); + } + if (!$sent[$file]) { + drupal_set_html_head(''); + $sent[$file] = true; + } +} + +/** + * Implode a PHP array into a string that can be decoded by the autocomplete JS routines. + * + * Items are separated by double pipes. Each item consists of a key-value pair + * separated by single pipes. Entities are used to ensure pipes in the strings + * pass unharmed. + * + * The key is what is filled in in the text-box (plain-text), the value is what + * is displayed in the suggestion list (HTML). + */ +function drupal_implode_autocomplete($array) { + $output = array(); + foreach ($array as $k => $v) { + $output[] = str_replace('|', '|', $k) .'|'. str_replace('|', '|', $v); + } + return implode('||', $output); +} + // Set the Drupal custom error handler. set_error_handler('error_handler'); cvs diff: Diffing misc Index: misc/autocomplete.js =================================================================== RCS file: misc/autocomplete.js diff -N misc/autocomplete.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/autocomplete.js 24 May 2005 05:03:28 -0000 @@ -0,0 +1,263 @@ +// Global Killswitch +if (isJSEnabled()) { + addLoadEvent(autocomplete_auto_attach); +} + +/** + * Attaches the autocomplete behaviour to all required fields + */ +function autocomplete_auto_attach() { + 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.autocompleteValue; + } + 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.autocompleteValue; +} + +/** + * 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'; + addClass(this.input, 'throbbing'); + 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'); + div = document.createElement('div'); + div.innerHTML = matches[i][1]; + li.appendChild(div); + li.autocompleteValue = matches[i][0]; + 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(); + } + removeClass(this.input, 'throbbing'); +} + +/** + * 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); + } + // Split into array of key->value pairs + var matches = string.length > 0 ? string.split('||') : []; + for (i in matches) { + matches[i] = matches[i].length > 0 ? matches[i].split('|') : []; + // Decode textfield pipes back to plain-text + matches[i][0] = ereg_replace('|', '|', matches[i][0]); + } + acdb.cache[acdb.search_string] = matches; + acdb.owner.found(matches); +} 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 24 May 2005 05:03:28 -0000 @@ -551,3 +551,28 @@ ul.secondary a.active { border-bottom: 4px solid #999; } + +/* +** Autocomplete styles +*/ +#autocomplete { + position: absolute; + border: 1px solid; + overflow: hidden; +} +#autocomplete ul { + margin: 0; + padding: 0; + list-style: none; +} +#autocomplete li { + background: #fff; + color: #000; + white-space: pre; + cursor: default; +} +#autocomplete li.selected { + background: #0072b9; + color: #fff; +} + Index: misc/drupal.js =================================================================== RCS file: misc/drupal.js diff -N misc/drupal.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/drupal.js 24 May 2005 05:03:28 -0000 @@ -0,0 +1,165 @@ +/** + * Only enable Javascript functionality if all required features are supported. + */ +function isJSEnabled() { + if (document.jsEnabled == undefined) { + // Note: ! casts to boolean implicitly. + document.jsEnabled = !( + !document.getElementsByTagName || + !document.createElement || + !document.createTextNode || + !document.getElementById); + } + return document.jsEnabled; +} + +// Global Killswitch +if (isJSEnabled()) { + +} + +/** + * 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('(^| )'+ className +'($| )') + 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 = ereg_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 ereg_replace function in javascript + */ +function ereg_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; + } +} cvs diff: Diffing modules 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 24 May 2005 05:03:33 -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/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy.module,v retrieving revision 1.204 diff -u -r1.204 taxonomy.module --- modules/taxonomy.module 21 May 2005 21:32:54 -0000 1.204 +++ modules/taxonomy.module 24 May 2005 05:03:37 -0000 @@ -82,6 +82,11 @@ 'callback' => 'taxonomy_term_page', 'access' => user_access('access content'), 'type' => MENU_CALLBACK); + + $items[] = array('path' => 'taxonomy/autocomplete', 'title' => t('autocomplete taxonomy'), + 'callback' => 'taxonomy_autocomplete', + 'access' => user_access('access content'), + 'type' => MENU_CALLBACK); } else { if (is_numeric(arg(2))) { @@ -519,7 +524,7 @@ } } $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL); - $result[] = form_textfield($vocabulary->name, "$name][tags][". $vocabulary->vid, $typed_string, 50, 100, t('A comma-separated list of terms describing this content (Example: funny, bungie jumping, "Company, Inc.").'), NULL, ($vocabulary->required ? TRUE : FALSE)); + $result[] = form_autocomplete($vocabulary->name, "$name][tags][". $vocabulary->vid, $typed_string, 50, 100, 'taxonomy/autocomplete/'. $vocabulary->vid, t('A comma-separated list of terms describing this content (Example: funny, bungie jumping, "Company, Inc.").'), NULL, ($vocabulary->required ? TRUE : FALSE)); } else { $ntterms = array_key_exists('taxonomy', $node) ? $terms : array_keys($terms); @@ -1259,4 +1264,36 @@ return $term->tid; } +/** + * Helper function for autocompletion + */ +function taxonomy_autocomplete($vid, $string = '', $limit = 10) { + // The user enters a comma-separated list of tags. We only autocomplete the last tag. + // This regexp allows the following types of user input: + // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar + $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; + preg_match_all($regexp, $string, $matches); + $array = $matches[1]; + + // Fetch last tag + $last_string = trim(array_pop($array)); + if ($last_string != '') { + $result = db_query_range("SELECT name FROM {term_data} WHERE vid = %d AND LOWER(name) LIKE LOWER('%%%s%%')", $vid, $last_string, 0, $limit); + + $prefix = count($array) ? implode(', ', $array) .', ' : ''; + + $matches = array(); + while ($tag = db_fetch_object($result)) { + $n = $tag->name; + // Commas and quotes in terms are special cases, so encode 'em. + if (preg_match('/,/', $tag->name) || preg_match('/"/', $tag->name)) { + $n = '"'. preg_replace('/"/', '""', $tag->name) .'"'; + } + $matches[$prefix . $n] = check_plain($tag->name); + } + print drupal_implode_autocomplete($matches); + exit(); + } +} + ?> 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 24 May 2005 05:03:44 -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) { + $matches = array(); + $result = db_query_range('SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER("%%%s%%")', $string, 0, 10); + while ($user = db_fetch_object($result)) { + $matches[$user->name] = check_plain($user->name); + } + print drupal_implode_autocomplete($matches); + exit(); +} + ?>