aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--inc/Ui/Search.php174
-rw-r--r--lib/exe/js.php1
-rw-r--r--lib/scripts/search.js67
-rw-r--r--lib/tpl/dokuwiki/css/_search.css16
4 files changed, 254 insertions, 4 deletions
diff --git a/inc/Ui/Search.php b/inc/Ui/Search.php
index bff33cae5..cbc090b52 100644
--- a/inc/Ui/Search.php
+++ b/inc/Ui/Search.php
@@ -62,18 +62,184 @@ class Search extends Ui
{
global $lang;
- $searchForm = (new Form())->attrs(['method' => 'get']);
- $searchForm->setHiddenField('do', 'search');
+ $Indexer = idx_get_indexer();
+ $parsedQuery = ft_queryParser($Indexer, $query);
- $searchForm->addFieldsetOpen();
- $searchForm->addTextInput('id', '')->val($query);
+ $searchForm = (new Form())->attrs(['method' => 'get'])->addClass('search-results-form');
+ $searchForm->setHiddenField('do', 'search');
+ $searchForm->addFieldsetOpen()->addClass('search-results-form__fieldset');
+ $searchForm->addTextInput('id')->val($query);
$searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
+
+ if ($this->isSearchAssistanceAvailable($parsedQuery)) {
+ $this->addSearchAssistanceElements($searchForm, $parsedQuery);
+ } else {
+ $searchForm->addClass('search-results-form--no-assistance');
+ $searchForm->addTagOpen('span')->addClass('search-results-form__no-assistance-message');
+ $searchForm->addHTML('FIXME Your query is too complex. Search assistance is unavailable. See <a href="https://doku.wiki/search">doku.wiki/search</a> for more help.');
+ $searchForm->addTagClose('span');
+ }
+
$searchForm->addFieldsetClose();
return $searchForm->toHTML();
}
/**
+ * Decide if the given query is simple enough to provide search assistance
+ *
+ * @param array $parsedQuery
+ *
+ * @return bool
+ */
+ protected function isSearchAssistanceAvailable(array $parsedQuery)
+ {
+ if (count($parsedQuery['words']) > 1) {
+ return false;
+ }
+ if (!empty($parsedQuery['not'])) {
+ return false;
+ }
+
+ if (!empty($parsedQuery['phrases'])) {
+ return false;
+ }
+
+ if (!empty($parsedQuery['notns'])) {
+ return false;
+ }
+ if (count($parsedQuery['ns']) > 1) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the elements to be used for search assistance
+ *
+ * @param Form $searchForm
+ * @param array $parsedQuery
+ */
+ protected function addSearchAssistanceElements(Form $searchForm, array $parsedQuery)
+ {
+ $matchType = '';
+ $searchTerm = null;
+ if (count($parsedQuery['words']) === 1) {
+ $searchTerm = $parsedQuery['words'][0];
+ $firstChar = $searchTerm[0];
+ $lastChar = substr($searchTerm, -1);
+ $matchType = 'exact';
+
+ if ($firstChar === '*') {
+ $matchType = 'starts';
+ }
+ if ($lastChar === '*') {
+ $matchType = 'ends';
+ }
+ if ($firstChar === '*' && $lastChar === '*') {
+ $matchType = 'contains';
+ }
+ $searchTerm = trim($searchTerm, '*');
+ }
+
+ $searchForm->addTextInput(
+ 'searchTerm',
+ '',
+ $searchForm->findPositionByAttribute('type', 'submit')
+ )
+ ->val($searchTerm)
+ ->attr('style', 'display: none;');
+ $searchForm->addButton('toggleAssistant', 'toggle search assistant')
+ ->attr('type', 'button')
+ ->id('search-results-form__show-assistance-button')
+ ->addClass('search-results-form__show-assistance-button');
+
+ $searchForm->addTagOpen('div')
+ ->addClass('js-advancedSearchOptions')
+ ->attr('style', 'display: none;');
+
+ $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
+ $searchForm->addRadioButton('matchType', 'exact Match FM')->val('exact')->attr('checked',
+ $matchType === 'exact' ?: null);
+ $searchForm->addRadioButton('matchType', 'starts with FM')->val('starts')->attr('checked',
+ $matchType === 'starts' ?: null);
+ $searchForm->addRadioButton('matchType', 'ends with FM')->val('ends')->attr('checked',
+ $matchType === 'ends' ?: null);
+ $searchForm->addRadioButton('matchType', 'contains FM')->val('contains')->attr('checked',
+ $matchType === 'contains' ?: null);
+ $searchForm->addTagClose('div');
+
+ $this->addNamespaceSelector($searchForm, $parsedQuery);
+
+ $searchForm->addTagClose('div');
+ }
+
+ /**
+ * Add the elements for the namespace selector
+ *
+ * @param Form $searchForm
+ * @param array $parsedQuery
+ */
+ protected function addNamespaceSelector(Form $searchForm, array $parsedQuery)
+ {
+ $baseNS = empty($parsedQuery['ns']) ? '' : $parsedQuery['ns'][0];
+ $namespaces = [];
+ $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
+ if ($baseNS) {
+ $searchForm->addRadioButton('namespace', '(no namespace FIXME)')->val('');
+ $parts = [$baseNS => count($this->fullTextResults)];
+ $upperNameSpace = $baseNS;
+ while ($upperNameSpace = getNS($upperNameSpace)) {
+ $parts[$upperNameSpace] = 0;
+ }
+ $namespaces = array_reverse($parts);
+ };
+
+ $namespaces = array_merge($namespaces, $this->getAdditionalNamespacesFromResults($baseNS));
+
+ foreach ($namespaces as $extraNS => $count) {
+ $label = $extraNS . ($count ? " ($count)" : '');
+ $namespaceCB = $searchForm->addRadioButton('namespace', $label)->val($extraNS);
+ if ($extraNS === $baseNS) {
+ $namespaceCB->attr('checked', true);
+ }
+ }
+
+ $searchForm->addTagClose('div');
+ }
+
+ /**
+ * Parse the full text results for their top namespaces below the given base namespace
+ *
+ * @param string $baseNS the namespace within which was searched, empty string for root namespace
+ *
+ * @return array an associative array with namespace => #number of found pages, sorted descending
+ */
+ protected function getAdditionalNamespacesFromResults($baseNS)
+ {
+ $namespaces = [];
+ $baseNSLength = strlen($baseNS);
+ foreach ($this->fullTextResults as $page => $numberOfHits) {
+ $namespace = getNS($page);
+ if (!$namespace) {
+ continue;
+ }
+ if ($namespace === $baseNS) {
+ continue;
+ }
+ $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
+ $subtopNS = substr($namespace, 0, $firstColon);
+ if (empty($namespaces[$subtopNS])) {
+ $namespaces[$subtopNS] = 0;
+ }
+ $namespaces[$subtopNS] += 1;
+ }
+ arsort($namespaces);
+ return $namespaces;
+ }
+
+ /**
* Build the intro text for the search page
*
* @param string $query the search query
diff --git a/lib/exe/js.php b/lib/exe/js.php
index ee017a41e..4c614f080 100644
--- a/lib/exe/js.php
+++ b/lib/exe/js.php
@@ -47,6 +47,7 @@ function js_out(){
DOKU_INC.'lib/scripts/cookie.js',
DOKU_INC.'lib/scripts/script.js',
DOKU_INC.'lib/scripts/qsearch.js',
+ DOKU_INC.'lib/scripts/search.js',
DOKU_INC.'lib/scripts/tree.js',
DOKU_INC.'lib/scripts/index.js',
DOKU_INC.'lib/scripts/textselection.js',
diff --git a/lib/scripts/search.js b/lib/scripts/search.js
new file mode 100644
index 000000000..0c9dca76a
--- /dev/null
+++ b/lib/scripts/search.js
@@ -0,0 +1,67 @@
+jQuery(function () {
+ 'use strict';
+
+ const $searchForm = jQuery('.search-results-form');
+ if (!$searchForm.length) {
+ return;
+ }
+ if (!$searchForm.find('#search-results-form__show-assistance-button').length){
+ return;
+ }
+ const $toggleAssistanceButton = $searchForm.find('#search-results-form__show-assistance-button');
+ const $queryInput = $searchForm.find('[name="id"]');
+ const $termInput = $searchForm.find('[name="searchTerm"]');
+
+ $toggleAssistanceButton.on('click', function () {
+ jQuery('.js-advancedSearchOptions').toggle();
+ $queryInput.toggle();
+ $termInput.toggle();
+ });
+
+
+ const $matchTypeSwitcher = $searchForm.find('[name="matchType"]');
+ const $namespaceSwitcher = $searchForm.find('[name="namespace"]');
+ const $refiningElements = $termInput.add($matchTypeSwitcher).add($namespaceSwitcher);
+ $refiningElements.on('input change', function () {
+ $queryInput.val(
+ rebuildQuery(
+ $termInput.val(),
+ $matchTypeSwitcher.filter(':checked').val(),
+ $namespaceSwitcher.filter(':checked').val()
+ )
+ );
+ });
+
+ /**
+ * Rebuild the search query from the parts
+ *
+ * @param {string} searchTerm the word which is to be searched
+ * @param {enum} matchType the type of matching that is to be done
+ * @param {string} namespace list of namespaces to which to limit the search
+ *
+ * @return {string} the query string for the actual search
+ */
+ function rebuildQuery(searchTerm, matchType, namespace) {
+ let query = '';
+
+ switch (matchType) {
+ case 'contains':
+ query = '*' + searchTerm + '*';
+ break;
+ case 'starts':
+ query = '*' + searchTerm;
+ break;
+ case 'ends':
+ query = searchTerm + '*';
+ break;
+ default:
+ query = searchTerm;
+ }
+
+ if (namespace && namespace.length) {
+ query += ' @' + namespace;
+ }
+
+ return query;
+ }
+});
diff --git a/lib/tpl/dokuwiki/css/_search.css b/lib/tpl/dokuwiki/css/_search.css
index a8972ae72..8da2652a2 100644
--- a/lib/tpl/dokuwiki/css/_search.css
+++ b/lib/tpl/dokuwiki/css/_search.css
@@ -12,6 +12,22 @@
margin-bottom: 1.4em;
}
+.search-results-form .search-results-form__fieldset {
+ width: 80vw;
+}
+
+.search-results-form__show-assistance-button {
+ float: right;
+}
+
+.search-results-form__no-assistance-message {
+ color: grey;
+ float: right;
+ font-size: 80%;
+ margin-top: -0.3em;
+}
+
+
/*____________ matching pagenames ____________*/
.dokuwiki div.search_quickresult {