From b005809cbc9d5e96a687d5d0445452fb317981fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michael=20Gro=C3=9Fe?= <grosse@cosmocode.de>
Date: Mon, 26 Mar 2018 18:21:23 +0200
Subject: [PATCH] feat(search): display search tools as lists on click

This functionality is inspired by what other large search engines are
doing.
---
 inc/Ui/Search.php                | 319 +++++++++++++++++++++----------
 lib/scripts/search.js            |   6 +
 lib/tpl/dokuwiki/css/_search.css |  20 +-
 3 files changed, 245 insertions(+), 100 deletions(-)

diff --git a/inc/Ui/Search.php b/inc/Ui/Search.php
index 5a01b6919..4f477f4d6 100644
--- a/inc/Ui/Search.php
+++ b/inc/Ui/Search.php
@@ -16,8 +16,8 @@ class Search extends Ui
     /**
      * Search constructor.
      *
-     * @param array $pageLookupResults
-     * @param array $fullTextResults
+     * @param array  $pageLookupResults
+     * @param array  $fullTextResults
      * @param string $highlight
      */
     public function __construct(array $pageLookupResults, array $fullTextResults, $highlight)
@@ -90,11 +90,6 @@ class Search extends Ui
             $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');
         }
-        if ($INPUT->str('sort') === 'mtime') {
-            $this->searchState->addSearchLinkSort($searchForm, 'sort by hits', '');
-        } else {
-            $this->searchState->addSearchLinkSort($searchForm, 'sort by mtime', 'mtime');
-        }
 
         $searchForm->addFieldsetClose();
 
@@ -103,6 +98,59 @@ class Search extends Ui
         return $searchForm->toHTML();
     }
 
+    protected function addSortTool(Form $searchForm)
+    {
+        global $INPUT, $lang;
+
+        $options = [
+            'hits' => [
+                'label' => $lang['search_sort_by_hits'],
+                'sort' => '',
+            ],
+            'mtime' => [
+                'label' => $lang['search_sort_by_mtime'],
+                'sort' => 'mtime',
+            ],
+        ];
+        $activeOption = 'hits';
+
+        if ($INPUT->str('sort') === 'mtime') {
+            $activeOption = 'mtime';
+        }
+
+        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
+        // render current
+        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
+        if ($activeOption !== 'hits') {
+            $currentWrapper->addClass('search-tool__current--changed');
+        }
+        $searchForm->addHTML($options[$activeOption]['label']);
+        $searchForm->addTagClose('div');
+
+        // render options list
+        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
+
+        foreach ($options as $key => $option) {
+            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
+
+            if ($key === $activeOption) {
+                $listItem->addClass('search-tool__options-list-item--active');
+                $searchForm->addHTML($option['label']);
+            } else {
+                $this->searchState->addSearchLinkSort(
+                    $searchForm,
+                    $option['label'],
+                    $option['sort']
+                );
+            }
+            $searchForm->addTagClose('li');
+        }
+        $searchForm->addTagClose('ul');
+
+        $searchForm->addTagClose('div');
+
+    }
+
     /**
      * Decide if the given query is simple enough to provide search assistance
      *
@@ -136,7 +184,7 @@ class Search extends Ui
     /**
      * Add the elements to be used for search assistance
      *
-     * @param Form  $searchForm
+     * @param Form $searchForm
      */
     protected function addSearchAssistanceElements(Form $searchForm)
     {
@@ -152,81 +200,143 @@ class Search extends Ui
         $this->addFragmentBehaviorLinks($searchForm);
         $this->addNamespaceSelector($searchForm);
         $this->addDateSelector($searchForm);
+        $this->addSortTool($searchForm);
 
         $searchForm->addTagClose('div');
     }
 
     protected function addFragmentBehaviorLinks(Form $searchForm)
     {
-        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
-        $searchForm->addHTML('fragment behavior: ');
-
-        $this->searchState->addSearchLinkFragment(
-            $searchForm,
-            'exact match',
-            array_map(function($term){return trim($term, '*');},$this->parsedQuery['and'])
-        );
-
-        $searchForm->addHTML(', ');
+        global $lang;
 
-        $this->searchState->addSearchLinkFragment(
-            $searchForm,
-            'starts with',
-            array_map(function($term){return trim($term, '*') . '*';},$this->parsedQuery['and'])
-        );
+        $options = [
+            'exact' => [
+                'label' => $lang['search_exact_match'],
+                'and' => array_map(function ($term) {
+                    return trim($term, '*');
+                }, $this->parsedQuery['and']),
+            ],
+            'starts' => [
+                'label' => $lang['search_starts_with'],
+                'and' => array_map(function ($term) {
+                    return trim($term, '*') . '*';
+                }, $this->parsedQuery['and'])
+            ],
+            'ends' => [
+                'label' => $lang['search_ends_with'],
+                'and' => array_map(function ($term) {
+                    return '*' . trim($term, '*');
+                }, $this->parsedQuery['and'])
+            ],
+            'contains' => [
+                'label' => $lang['search_contains'],
+                'and' => array_map(function ($term) {
+                    return '*' . trim($term, '*') . '*';
+                }, $this->parsedQuery['and'])
+            ]
+        ];
+
+        // detect current
+        $activeOption = 'exact';
+        foreach ($options as $key => $option) {
+            if ($this->parsedQuery['and'] === $option['and']) {
+                $activeOption = $key;
+            }
+        }
 
-        $searchForm->addHTML(', ');
+        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
+        // render current
+        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
+        if ($activeOption !== 'exact') {
+            $currentWrapper->addClass('search-tool__current--changed');
+        }
+        $searchForm->addHTML($options[$activeOption]['label']);
+        $searchForm->addTagClose('div');
 
-        $this->searchState->addSearchLinkFragment(
-            $searchForm,
-            'ends with',
-            array_map(function($term){return '*' . trim($term, '*');},$this->parsedQuery['and'])
-        );
+        // render options list
+        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
 
-        $searchForm->addHTML(', ');
+        foreach ($options as $key => $option) {
+            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
 
-        $this->searchState->addSearchLinkFragment(
-            $searchForm,
-            'contains',
-            array_map(function($term){return '*' . trim($term, '*') . '*';},$this->parsedQuery['and'])
-        );
+            if ($key === $activeOption) {
+                $listItem->addClass('search-tool__options-list-item--active');
+                $searchForm->addHTML($option['label']);
+            } else {
+                $this->searchState->addSearchLinkFragment(
+                    $searchForm,
+                    $option['label'],
+                    $option['and']
+                );
+            }
+            $searchForm->addTagClose('li');
+        }
+        $searchForm->addTagClose('ul');
 
         $searchForm->addTagClose('div');
+
+        // render options list
     }
 
     /**
      * Add the elements for the namespace selector
      *
-     * @param Form  $searchForm
+     * @param Form $searchForm
      */
     protected function addNamespaceSelector(Form $searchForm)
     {
-        $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
-        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
+        global $lang;
 
+        $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
         $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
-        if (!empty($extraNS) || $baseNS) {
-            $searchForm->addTagOpen('div');
-            $searchForm->addHTML('limit to namespace: ');
 
-            if ($baseNS) {
+        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
+        // render current
+        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
+        if ($baseNS) {
+            $currentWrapper->addClass('search-tool__current--changed');
+            $searchForm->addHTML('@' . $baseNS);
+        } else {
+            $searchForm->addHTML($lang['search_any_ns']);
+        }
+        $searchForm->addTagClose('div');
+
+        // render options list
+        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
+
+        $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
+        if ($baseNS) {
+            $listItem->addClass('search-tool__options-list-item--active');
+            $this->searchState->addSeachLinkNS(
+                $searchForm,
+                $lang['search_any_ns'],
+                ''
+            );
+        } else {
+            $searchForm->addHTML($lang['search_any_ns']);
+        }
+        $searchForm->addTagClose('li');
+
+        foreach ($extraNS as $ns => $count) {
+            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
+            $label = $ns . ($count ? " ($count)" : '');
+
+            if ($ns === $baseNS) {
+                $listItem->addClass('search-tool__options-list-item--active');
+                $searchForm->addHTML($label);
+            } else {
                 $this->searchState->addSeachLinkNS(
                     $searchForm,
-                    '(remove limit)',
-                    ''
+                    $label,
+                    $ns
                 );
             }
-
-            foreach ($extraNS as $ns => $count) {
-                $searchForm->addHTML(' ');
-                $label = $ns . ($count ? " ($count)" : '');
-
-                $this->searchState->addSeachLinkNS($searchForm, $label, $ns);
-            }
-            $searchForm->addTagClose('div');
+            $searchForm->addTagClose('li');
         }
+        $searchForm->addTagClose('ul');
 
         $searchForm->addTagClose('div');
+
     }
 
     /**
@@ -264,58 +374,69 @@ class Search extends Ui
      *
      * @param Form $searchForm
      */
-    protected function addDateSelector(Form $searchForm) {
-        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
-        $searchForm->addHTML('limit by date: ');
-
-        global $INPUT;
-        if ($INPUT->has('before') || $INPUT->has('after')) {
-            $this->searchState->addSearchLinkTime(
-                $searchForm,
-                '(remove limit)',
-                false,
-                false
-            );
-
-            $searchForm->addHTML(', ');
+    protected function addDateSelector(Form $searchForm)
+    {
+        global $INPUT, $lang;
+
+        $options = [
+            'any' => [
+                'before' => false,
+                'after' => false,
+                'label' => $lang['search_any_time'],
+            ],
+            'week' => [
+                'before' => false,
+                'after' => '1 week ago',
+                'label' => $lang['search_past_7_days'],
+            ],
+            'month' => [
+                'before' => false,
+                'after' => '1 month ago',
+                'label' => $lang['search_past_month'],
+            ],
+            'year' => [
+                'before' => false,
+                'after' => '1 year ago',
+                'label' => $lang['search_past_year'],
+            ],
+        ];
+        $activeOption = 'any';
+        foreach ($options as $key => $option) {
+            if ($INPUT->str('after') === $option['after']) {
+                $activeOption = $key;
+                break;
+            }
         }
 
-        if ($INPUT->str('after') === '1 week ago') {
-            $searchForm->addHTML('<span class="active">past 7 days</span>');
-        } else {
-            $this->searchState->addSearchLinkTime(
-                $searchForm,
-                'past 7 days',
-                '1 week ago',
-                false
-            );
+        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
+        // render current
+        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
+        if ($INPUT->has('before') || $INPUT->has('after')) {
+            $currentWrapper->addClass('search-tool__current--changed');
         }
+        $searchForm->addHTML($options[$activeOption]['label']);
+        $searchForm->addTagClose('div');
 
-        $searchForm->addHTML(', ');
-
-        if ($INPUT->str('after') === '1 month ago') {
-            $searchForm->addHTML('<span class="active">past month</span>');
-        } else {
-            $this->searchState->addSearchLinkTime(
-                $searchForm,
-                'past month',
-                '1 month ago',
-                false
-            );
-        }
+        // render options list
+        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
 
-        $searchForm->addHTML(', ');
+        foreach ($options as $key => $option) {
+            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
 
-        if ($INPUT->str('after') === '1 year ago') {
-            $searchForm->addHTML('<span class="active">past year</span>');
-        } else {
-            $this->searchState->addSearchLinkTime(
-                $searchForm,
-                'past year',
-                '1 year ago',
-                false
-            );
+            if ($key === $activeOption) {
+                $listItem->addClass('search-tool__options-list-item--active');
+                $searchForm->addHTML($option['label']);
+            } else {
+                $this->searchState->addSearchLinkTime(
+                    $searchForm,
+                    $option['label'],
+                    $option['after'],
+                    $option['before']
+                );
+            }
+            $searchForm->addTagClose('li');
         }
+        $searchForm->addTagClose('ul');
 
         $searchForm->addTagClose('div');
     }
@@ -416,8 +537,8 @@ class Search extends Ui
                 $resultHeader[] = $cnt . ' ' . $lang['hits'];
                 if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
                     $snippet = '<dd>' . ft_snippet($id, $highlight) . '</dd>';
-                    $lastMod = '<span class="search_results__lastmod">'. $lang['lastmod'] . ' ';
-                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">'. dformat($mtime) . '</time>';
+                    $lastMod = '<span class="search_results__lastmod">' . $lang['lastmod'] . ' ';
+                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">' . dformat($mtime) . '</time>';
                     $lastMod .= '</span>';
                 }
                 $num++;
diff --git a/lib/scripts/search.js b/lib/scripts/search.js
index bfde5490c..9bea9d1d7 100644
--- a/lib/scripts/search.js
+++ b/lib/scripts/search.js
@@ -19,5 +19,11 @@ jQuery(function () {
         $toggleAssistanceButton.click();
     }
 
+    $searchForm.find('.js-search-tool .js-current').on('click', function() {
+        $searchForm.find('.js-current').show();
+        $searchForm.find('.js-optionsList').hide();
+        jQuery(this).hide();
+        jQuery(this).next('.js-optionsList').show();
+    });
 
 });
diff --git a/lib/tpl/dokuwiki/css/_search.css b/lib/tpl/dokuwiki/css/_search.css
index 8da2652a2..7e9b6c20a 100644
--- a/lib/tpl/dokuwiki/css/_search.css
+++ b/lib/tpl/dokuwiki/css/_search.css
@@ -13,7 +13,7 @@
 }
 
 .search-results-form .search-results-form__fieldset {
-    width: 80vw;
+    width: 80%;
 }
 
 .search-results-form__show-assistance-button {
@@ -27,6 +27,24 @@
     margin-top: -0.3em;
 }
 
+.search-tool__current {
+    cursor: pointer;
+}
+
+.search-tool__current--changed {
+    font-weight: bold;
+}
+
+.search-tool__options-list {
+    display: none;
+
+    border: 1px solid @ini_border;
+}
+
+.search-tool__options-list-item {
+    list-style: none;
+}
+
 
 /*____________ matching pagenames ____________*/
 
-- 
GitLab