diff --git a/README.md b/README.md
index d8d9a16..1e0bd1c 100644
--- a/README.md
+++ b/README.md
@@ -4,4 +4,5 @@ Dokuwiki template specifically designed for luxtools plugins.
This is a fork of the original default dokuwiki template.
## Changes from the original template
-- Default font changed to "Perfect DOS VGA 437 Win"
\ No newline at end of file
+- Default font changed to "Perfect DOS VGA 437 Win"
+- Enhanced header search with DokuWiki quicksearch suggestions, keyboard navigation, and disabled browser autofill
\ No newline at end of file
diff --git a/css/_search.less b/css/_search.less
index 59400f9..eaf1481 100755
--- a/css/_search.less
+++ b/css/_search.less
@@ -179,6 +179,44 @@
text-align: left;
display: none;
+ &.is-open {
+ display: block;
+ }
+
+ &.is-loading {
+ opacity: .9;
+ }
+
+ ul.search-suggestions {
+ list-style: none;
+ margin: 0 !important;
+ padding: 0 !important;
+ }
+
+ ul.search-suggestions li {
+ margin: 0;
+ padding: 0;
+ display: block !important;
+ }
+
+ ul.search-suggestions li a {
+ display: block;
+ padding: .15em .25em;
+ }
+
+ ul.search-suggestions li.is-active > a,
+ ul.search-suggestions li a:focus {
+ background-color: __highlight__;
+ color: @ini_text;
+ outline: none;
+ }
+
+ ul.search-suggestions li.is-loading,
+ ul.search-suggestions li.is-empty {
+ padding: .15em .25em;
+ color: @ini_text_alt;
+ }
+
strong {
display: block;
margin-bottom: .3em;
diff --git a/css/mobile.less b/css/mobile.less
index 31657d1..7f98717 100755
--- a/css/mobile.less
+++ b/css/mobile.less
@@ -243,6 +243,10 @@ body {
display: none !important;
}
+#dokuwiki__sitetools form.search div.ajax_qsearch.is-open {
+ display: block !important;
+}
+
/* action dropdown is alternative for all hidden tools */
#dokuwiki__header .mobileTools {
display: block;
diff --git a/script.js b/script.js
index 88dae90..1c068a1 100755
--- a/script.js
+++ b/script.js
@@ -86,4 +86,310 @@ jQuery(function(){
jQuery('#dokuwiki__pagetools div.tools>ul>li>a').on('click', function(){
this.blur();
});
+
+ // enhance header search with suggestions and keyboard navigation
+ (function enhanceSearch(){
+ var $input = jQuery('#qsearch__in');
+ var $output = jQuery('#qsearch__out');
+
+ if ($input.length === 0 || $output.length === 0) {
+ return;
+ }
+
+ var $form = $input.closest('form');
+ var debounceMs = 150;
+ var minChars = 2;
+ var maxSuggestions = 10;
+ var requestTimer = null;
+ var curRequest = null;
+ var items = [];
+ var activeIndex = -1;
+ var listId = 'qsearch__listbox';
+
+ $form
+ .addClass('search-enhanced')
+ .attr('autocomplete', 'off');
+
+ $input
+ .attr('autocomplete', 'off')
+ .attr('spellcheck', 'false')
+ .attr('aria-autocomplete', 'list')
+ .attr('aria-expanded', 'false')
+ .attr('aria-controls', listId)
+ .attr('aria-haspopup', 'listbox');
+
+ $output
+ .attr('aria-hidden', 'true');
+
+ // remove default quicksearch handlers
+ $input.off('keyup');
+ $output.off('click');
+
+ function clearActive() {
+ activeIndex = -1;
+ $input.removeAttr('aria-activedescendant');
+ }
+
+ function closeList() {
+ if (curRequest) {
+ curRequest.abort();
+ curRequest = null;
+ }
+ if (requestTimer) {
+ window.clearTimeout(requestTimer);
+ requestTimer = null;
+ }
+
+ items = [];
+ clearActive();
+ $output
+ .removeClass('is-open is-loading')
+ .attr('aria-hidden', 'true')
+ .hide()
+ .empty();
+ $form.removeClass('is-searching');
+ $input.attr('aria-expanded', 'false');
+ }
+
+ function openList() {
+ $output
+ .addClass('is-open')
+ .attr('aria-hidden', 'false')
+ .show();
+ $input.attr('aria-expanded', 'true');
+ }
+
+ function renderList(content) {
+ $output.empty().append(content);
+ openList();
+ }
+
+ function renderLoading() {
+ var $list = jQuery('
', {
+ 'class': 'search-suggestions',
+ 'id': listId,
+ 'role': 'listbox',
+ 'aria-busy': 'true'
+ });
+ $list.append(jQuery('', {
+ 'class': 'is-loading',
+ 'role': 'option',
+ 'aria-disabled': 'true',
+ 'text': 'Loading…'
+ }));
+ $output.addClass('is-loading');
+ renderList($list);
+ }
+
+ function renderNoMatches() {
+ var $list = jQuery('', {
+ 'class': 'search-suggestions',
+ 'id': listId,
+ 'role': 'listbox',
+ 'aria-busy': 'false'
+ });
+ $list.append(jQuery('', {
+ 'class': 'is-empty',
+ 'role': 'option',
+ 'aria-disabled': 'true',
+ 'text': 'No Matches'
+ }));
+ $output.removeClass('is-loading');
+ renderList($list);
+ }
+
+ function updateActive() {
+ var $options = $output.find('li[data-index]');
+ $options.removeClass('is-active').attr('aria-selected', 'false');
+
+ if (activeIndex < 0 || activeIndex >= items.length) {
+ clearActive();
+ return;
+ }
+
+ var $active = $options.filter('[data-index="' + activeIndex + '"]');
+ $active.addClass('is-active').attr('aria-selected', 'true');
+ $input.attr('aria-activedescendant', $active.attr('id'));
+ }
+
+ function moveActive(delta) {
+ if (!items.length) {
+ return;
+ }
+ if (activeIndex < 0) {
+ activeIndex = delta > 0 ? 0 : items.length - 1;
+ } else {
+ activeIndex = (activeIndex + delta + items.length) % items.length;
+ }
+ updateActive();
+ }
+
+ function selectActive() {
+ if (activeIndex < 0 || activeIndex >= items.length) {
+ return false;
+ }
+ window.location.assign(items[activeIndex].href);
+ return true;
+ }
+
+ function renderResults(results) {
+ var $list = jQuery('', {
+ 'class': 'search-suggestions',
+ 'id': listId,
+ 'role': 'listbox',
+ 'aria-busy': 'false'
+ });
+
+ results.forEach(function (item, index) {
+ var optionId = 'qsearch__option_' + index;
+ var $link = jQuery('', {
+ 'href': item.href,
+ 'text': item.text
+ });
+ var $option = jQuery('', {
+ 'id': optionId,
+ 'role': 'option',
+ 'data-index': index,
+ 'aria-selected': 'false'
+ });
+ $option.append($link);
+ $list.append($option);
+ });
+
+ $output.removeClass('is-loading');
+ renderList($list);
+ clearActive();
+ }
+
+ function handleResults(data) {
+ $form.removeClass('is-searching');
+ curRequest = null;
+
+ var $wrapper = jQuery('').html(data || '');
+ var $links = $wrapper.find('a');
+ items = [];
+
+ $links.each(function () {
+ if (items.length >= maxSuggestions) {
+ return false;
+ }
+ items.push({
+ href: this.href,
+ text: jQuery(this).text()
+ });
+ return true;
+ });
+
+ if (!items.length) {
+ renderNoMatches();
+ return;
+ }
+
+ renderResults(items);
+ }
+
+ function scheduleSearch() {
+ if (requestTimer) {
+ window.clearTimeout(requestTimer);
+ requestTimer = null;
+ }
+ requestTimer = window.setTimeout(runSearch, debounceMs);
+ }
+
+ function runSearch() {
+ var term = jQuery.trim($input.val());
+
+ if (term.length < minChars) {
+ closeList();
+ return;
+ }
+
+ if (curRequest) {
+ curRequest.abort();
+ }
+
+ $form.addClass('is-searching');
+ renderLoading();
+
+ var base = (typeof DOKU_BASE !== 'undefined') ? DOKU_BASE : '/';
+ curRequest = jQuery.post(
+ base + 'lib/exe/ajax.php',
+ {
+ call: 'qsearch',
+ q: encodeURI(term)
+ },
+ handleResults,
+ 'html'
+ ).fail(function () {
+ $form.removeClass('is-searching');
+ curRequest = null;
+ renderNoMatches();
+ });
+ }
+
+ $input.on('input', scheduleSearch);
+
+ $input.on('keydown', function (event) {
+ var key = event.key;
+ var isOpen = $output.hasClass('is-open');
+
+ if (!isOpen) {
+ return;
+ }
+
+ if (key === 'Escape') {
+ event.preventDefault();
+ closeList();
+ return;
+ }
+
+ if (!items.length) {
+ return;
+ }
+
+ if (key === 'ArrowDown') {
+ event.preventDefault();
+ moveActive(1);
+ return;
+ }
+
+ if (key === 'ArrowUp') {
+ event.preventDefault();
+ moveActive(-1);
+ return;
+ }
+
+ if (key === 'Tab') {
+ event.preventDefault();
+ moveActive(event.shiftKey ? -1 : 1);
+ return;
+ }
+
+ if (key === 'Enter') {
+ if (selectActive()) {
+ event.preventDefault();
+ }
+ }
+ });
+
+ $output.on('mouseenter', 'li[data-index]', function () {
+ activeIndex = parseInt(jQuery(this).attr('data-index'), 10);
+ updateActive();
+ });
+
+ $output.on('mouseleave', 'li[data-index]', function () {
+ clearActive();
+ });
+
+ $output.on('mousedown', 'li[data-index] > a', function (event) {
+ event.preventDefault();
+ window.location.assign(this.href);
+ });
+
+ jQuery(document).on('click', function (event) {
+ if (!jQuery(event.target).closest($form).length) {
+ closeList();
+ }
+ });
+ })();
});
diff --git a/tpl_header.php b/tpl_header.php
index 9eb6088..3b390ee 100755
--- a/tpl_header.php
+++ b/tpl_header.php
@@ -61,7 +61,7 @@ if (!defined('DOKU_INC')) die();