Index: modules/help/help.css =================================================================== RCS file: /cvs/drupal/drupal/modules/help/help.css,v retrieving revision 1.1 diff -u -p -r1.1 help.css --- modules/help/help.css 14 Aug 2006 07:14:49 -0000 1.1 +++ modules/help/help.css 10 Dec 2006 09:18:12 -0000 @@ -8,3 +8,22 @@ .help-items-last { padding-right: 0; } + +#help_guide_form { + position: absolute; + top: 5em; + right: 1em; + z-index: 10001; +} + +#help_guide_form * { + display: inline; +} + +#autocomplete.guide-popup { + z-index: 10002; +} +#autocomplete.guide-popup .path { + color: #777; + font-size: 0.9em; +} Index: modules/help/help.info =================================================================== RCS file: /cvs/drupal/drupal/modules/help/help.info,v retrieving revision 1.3 diff -u -p -r1.3 help.info --- modules/help/help.info 21 Nov 2006 20:55:34 -0000 1.3 +++ modules/help/help.info 10 Dec 2006 09:18:12 -0000 @@ -3,3 +3,4 @@ name = Help description = Manages the display of online help. package = Core - optional version = VERSION +dependencies = search Index: modules/help/help.js =================================================================== RCS file: modules/help/help.js diff -N modules/help/help.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/help/help.js 10 Dec 2006 09:18:12 -0000 @@ -0,0 +1,269 @@ +// $Id$ + +/** + * Attaches the guide behaviour to all required fields + */ +Drupal.guideAutoAttach = function () { + var gdb = new Drupal.ACDB(Drupal.settings.guide.url); + $('input.guide').each(function () { + var uri = this.value; + var input = $(this) + .attr('autocomplete', 'OFF')[0]; + $(input.form).submit(Drupal.guideSubmit); + new Drupal.guide(input, gdb); + }); + + // Tweak IE settings so that $('body').css('height') works + if (jQuery.browser.msie) { + $('html').add('body').css('height', '100%').css('left', '0'); + } +} + +/** + * Prevents the form from submitting if the suggestions popup is open + * and closes the suggestions popup when doing so. + */ +Drupal.guideSubmit = function () { + $('#autocomplete').each(function () { + this.owner.hidePopup(); + }); + return false; +} + +/** + * A guide object + */ +Drupal.guide = function (input, db) { + var guide = this; + this.input = input; + this.db = db; + + $(this.input) + .keydown(function (event) { return guide.onkeydown(this, event); }) + .keyup(function (event) { guide.onkeyup(this, event) }) + .blur(function () { guide.hidePopup(); guide.db.cancel(); }); + +}; + +/** + * Inherited methods from autocomplete. + */ +Drupal.guide.prototype.onkeydown = Drupal.jsAC.prototype.onkeydown; +Drupal.guide.prototype.onkeyup = Drupal.jsAC.prototype.onkeyup; +Drupal.guide.prototype.selectDown = Drupal.jsAC.prototype.selectDown; +Drupal.guide.prototype.selectUp = Drupal.jsAC.prototype.selectUp; +Drupal.guide.prototype.highlight = Drupal.jsAC.prototype.highlight; +Drupal.guide.prototype.unhighlight = Drupal.jsAC.prototype.unhighlight; +Drupal.guide.prototype.setStatus = Drupal.jsAC.prototype.setStatus; + +/** + * Puts the currently highlighted suggestion into the guide field and + * visits the target URL. + */ +Drupal.guide.prototype.select = function (node) { + this.input.value = node.guideValue; + location.href = node.guideUrl; +} + +/** + * Hides the guide suggestions + */ +Drupal.guide.prototype.hidePopup = function (keycode) { + // Hide popup + var popup = this.popup; + if (popup) { + this.popup = null; + $(popup).fadeOut('fast', function() { $(popup).remove(); }); + } + + // Unexpose all items + this.unexpose(); + + // Select item if the right key or mousebutton was pressed + if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { + this.select(this.selected); + } + + // Remove selection + this.selected = false; +} + +/** + * Positions the suggestions popup and starts a search + */ +Drupal.guide.prototype.populatePopup = function () { + // Show popup + if (this.popup) { + $(this.popup).remove(); + } + var pos = Drupal.absolutePosition(this.input); + this.selected = false; + this.popup = document.createElement('div'); + this.popup.id = 'autocomplete'; + this.popup.className = 'guide-popup'; + this.popup.owner = this; + $(this.popup).css({ + top: (pos.y + this.input.offsetHeight) +'px', + left: pos.x +'px', + width: (this.input.offsetWidth - 4) +'px', + display: 'none' + }); + $('body').append(this.popup); + + // Do search + this.db.owner = this; + this.db.search(this.input.value); +} + +/** + * Fills the suggestion popup with any matches received + */ +Drupal.guide.prototype.found = function (matches) { + // Unhighlight everything in the page. + this.unexpose(); + + // Prepare matches + var ul = document.createElement('ul'); + var ac = this; + var i = 0; + for (key in matches) { + var li = document.createElement('li'); + $(li) + .html('
'+ matches[key][0] +'
') + .mousedown(function () { ac.select(this); }) + .mouseover(function () { ac.highlight(this); }) + .mouseout(function () { ac.unhighlight(this); }); + li.guideValue = key; + li.guideUrl = matches[key][1]; + $(ul).append(li); + + // Highlight this link in the page. + this.expose(key); + i++; + } + + // Show popup with matches, if any + if (ul.childNodes.length > 0) { + $(this.popup).empty().append(ul).show(); + } + else { + $(this.popup).css({visibility: 'hidden'}); + this.hidePopup(); + } +} + +/** + * Unexpose all items on the page. + */ +Drupal.guide.prototype.unexpose = function () { + // Remove shade. + $('#guide-shade').remove(); + + // Remove bubbles. + if (this.exposed && this.exposed.length > 0) { + $(this.marked).removeClass('exposed'); + $(this.exposed).remove(); + this.exposed = []; + this.marked = []; + } +} + +/** + * Expose links to this + */ +Drupal.guide.prototype.expose = function (path) { + var guide = this; + // Dim the page. + if ($('#guide-shade').size() == 0) { + $('
').appendTo($('body')).css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'right': '0px', + 'height': parseInt($('body').css('height')) + 30 + 'px', + 'margin-bottom': '-30px', + 'background': '#000', + 'opacity': 0.0, + 'z-index': 10000 + }) + .animate({ 'opacity': 0.5 }); + this.exposed = []; + this.marked = [] + } + + var z = 10001; + /** + * Highlight a DOM node. + */ + var _expose = function (node, opacity) { + // Check if it's been exposed already. + if ($(node).is('.exposed') || $(node.parentNode.parentNode).css('float') != 'none') { + return; + } + $(node).addClass('exposed'); + guide.marked.push(node); + + // Ensure we're positioning relative to the parent (prevents + // shrink-wrapping of content). + var p = node.parentNode; + if ($(p).css('position') == 'static') { + $(p).css('position', 'relative'); + } + + // Duplicate highlighted item. + var clone = node.cloneNode(true); + $(node).before(clone); + var glow = node.cloneNode(true); + $(node).before(glow); + + // Place above all else. + $([glow, clone]).css({ + 'position': 'absolute', + 'opacity': 0 + }); + + // Style and animate (hardcoded, to override). + $(glow).css({ + 'z-index': z++, + 'background': '#000', + 'padding': '12px 27px', + 'margin': '-10px -27px', + 'border-radius': '18px', + '-moz-border-radius': '18px' + }) + .animate({ 'opacity': opacity * 0.25 }); + $(clone).css({ + 'z-index': z++, + 'background': '#fff', + 'border': '1px solid #ccc', + 'padding': '7px 22px', + 'margin': '-8px -23px', + 'border-radius': '14px', + '-moz-border-radius': '14px' + }) + .animate({ 'opacity': opacity }); + + // Remember clone elements for later. + guide.exposed.push(glow); + guide.exposed.push(clone); + } + + // Highlight exact matches. + $('a[@href$='+ path +']').each(function () { _expose(this, 1) }); + + // Highlight partial matches from 3 or more path components. + var path = path.split('/'); + var part = ''; + for (i in path) { + part += (part == '' ? '' : '/') + path[i]; + if (i >= 2) { + $('a[@href$='+ part +']').each(function () { _expose(this, 0.5) }); + } + } +} + + +// Global Killswitch +if (Drupal.jsEnabled) { + $(document).ready(Drupal.guideAutoAttach); +} Index: modules/help/help.module =================================================================== RCS file: /cvs/drupal/drupal/modules/help/help.module,v retrieving revision 1.66 diff -u -p -r1.66 help.module --- modules/help/help.module 1 Dec 2006 22:47:53 -0000 1.66 +++ modules/help/help.module 10 Dec 2006 09:18:12 -0000 @@ -12,8 +12,19 @@ function help_menu($may_cache) { $items = array(); + $admin_access = user_access('access administration pages'); + if ($may_cache) { - $admin_access = user_access('access administration pages'); + $items[] = array('path' => 'admin/help/guide/page', + 'callback' => 'help_guide_page', + 'access' => $admin_access, + 'type' => MENU_CALLBACK, + ); + + $items[] = array('path' => 'admin/help/guide/js', + 'callback' => 'help_guide_js', + 'access' => $admin_access, + 'type' => MENU_CALLBACK); $items[] = array('path' => 'admin/help', 'title' => t('Help'), 'callback' => 'help_main', @@ -27,6 +38,15 @@ function help_menu($may_cache) { 'type' => MENU_CALLBACK, 'access' => $admin_access); } + + } + else { + // Add guide search to all top-level admin pages. + if (arg(0) == 'admin' && arg(2) == '' && $admin_access) { + drupal_add_js('misc/autocomplete.js'); + drupal_add_js(drupal_get_path('module', 'help') .'/help.js'); + drupal_set_content('content', drupal_get_form('help_guide_form')); + } } return $items; @@ -134,3 +154,149 @@ function help_page() { } return $output; } + +/** + * Form callback: return the form for querying the guide. + */ +function help_guide_form() { + // Add CSS. + drupal_add_css(drupal_get_path('module', 'help') .'/help.css'); + + // Pass data to JS. + drupal_add_js(array('guide' => array('url' => url('admin/help/guide/js', NULL, NULL, TRUE))), 'setting'); + + $form['query'] = array( + '#type' => 'textfield', + '#title' => t('Find'), + '#size' => 30, + '#attributes' => array('class' => 'guide form-autocomplete'), + ); + return $form; +} + +/** + * Menu callback: print a page with results for a guide query. + */ +function help_guide_page($query) { + // Query database + $results = help_guide_query($query); + + // Fetch menu + $menu = menu_get_menu(); + + // Format results + $list = array(); + foreach ($results as $path) { + $list[] = l($menu['items'][$menu['path index'][$path]]['title'], $path); + } + drupal_set_title(t('Find pages related to %search', array('%search' => $query))); + return theme('item_list', $list, t('The following pages were found:')); +} + +/** + * Menu callback: return JSON results for a guide query. + */ +function help_guide_js($query) { + // Query database + $results = help_guide_query($query); + drupal_get_messages(NULL, TRUE); + + // Fetch menu + $menu = menu_get_menu(); + + // Format results + $output = array(); + foreach ($results as $path) { + $output[$path] = array(check_plain($menu['items'][$menu['path index'][$path]]['title']) . ' ('. check_plain($path) .')', check_url(url($path, NULL, NULL, TRUE))); + } + + print drupal_to_js($output); + exit(); +} + +/** + * Perform a guide query. + */ +function help_guide_query($query) { + // Check if the index is available. + $path_map = help_guide_map(); + if (count($path_map) == 0) { + return array(); + } + + // OR search + $query = implode(' OR ', explode(' ', $query)); + + // Query search index + $results = array(); + $result = do_search($query, 'help'); + foreach ($result as $item) { + $results[] = $path_map[$item->sid]; + } + + return $results; +} + +/** + * Return the guide path map. + */ +function help_guide_map() { + // We use the menu cache, so we know the menu has rebuilt when it's been wiped. + if ($path_map = cache_get('help:path_map', 'cache_menu')) { + return unserialize($path_map->data); + } + else { + // Avoid multiple index runs at the same time. + cache_set('help:path_map', 'cache_menu', serialize(array()), CACHE_PERMANENT); + + // Rebuild the index and path map. + return help_guide_index(); + } +} + +/** + * Re-index the guide. + */ +function help_guide_index() { + // Clean search index. + db_query("DELETE FROM {search_index} WHERE type = 'help'"); + + // Prepare path map. + $path_map = array(); + $i = 0; + + $menu = menu_get_menu(); + foreach ($menu['path index'] as $path => $mid) { + // Fetch next item. + $item = $menu['items'][$mid]; + if ($item['pid'] == 0 || !($item['type'] & (MENU_VISIBLE_IN_TREE | MENU_IS_LOCAL_TASK))) { + continue; + } + + // Prepare snippet to index. + $output = '

'. $item['title'] .'

'; + $output .= '

'. str_replace('/', ' ', $path) .'

'; + $output .= '

'. $item['description'] .'

'; + + // Fetch help. + foreach (module_list() as $name) { + $output .= module_invoke($name, 'help', $path); + } + + // Fetch sid. + if (!isset($path_map[$path])) { + $path_map[$path] = ++$i; + } + + // Add to index. + search_index($path_map[$path], 'help', $output); + } + + // Update search totals. + search_update_totals(); + + // Update and return path map. + $path_map = array_flip($path_map); + cache_set('help:path_map', 'cache_menu', serialize($path_map), CACHE_PERMANENT); + return $path_map; +} Index: profiles/default/default.profile =================================================================== RCS file: /cvs/drupal/drupal/profiles/default/default.profile,v retrieving revision 1.2 diff -u -p -r1.2 default.profile --- profiles/default/default.profile 29 Oct 2006 13:17:38 -0000 1.2 +++ profiles/default/default.profile 10 Dec 2006 09:18:12 -0000 @@ -8,7 +8,7 @@ * An array of modules to be enabled. */ function default_profile_modules() { - return array('block', 'color', 'comment', 'filter', 'help', 'menu', 'node', 'system', 'taxonomy', 'user', 'watchdog'); + return array('block', 'color', 'comment', 'filter', 'help', 'menu', 'node', 'search', 'system', 'taxonomy', 'user', 'watchdog'); } /** Index: themes/garland/style.css =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/style.css,v retrieving revision 1.10 diff -u -p -r1.10 style.css --- themes/garland/style.css 4 Dec 2006 23:15:40 -0000 1.10 +++ themes/garland/style.css 10 Dec 2006 09:18:12 -0000 @@ -878,7 +878,7 @@ table.system-status-report th { color: #fff; } -tr.selected td a:link, tr.selected td a:visited, tr.selected td a:active { +tr.selected td a:link, tr.selected td a:visited, tr.selected td a:active, #autocomplete.guide-popup li.selected span.path { color: #d3e7f4; }