Hugo:增加搜索功能

Posted by Nefelibata on Sun 2024-03-31 | | about 4 mins
Last Modified on Thu 2024-05-23

1 显示效果

image-20240331221906100

可以看到在右上角有一个搜索图标,原本是这个主题带的,原本是点击以后会进入一个新的 search页面进行搜索,但是我用的时候发现这个功能有问题,点击进去以后什么都没有出现,加上又会跳转到新页面,遂放弃,根据我的需求进行改进。

2 改进方向

总结起来我有几个想要的效果,便朝着这几个方向去改:

  • 在同一个页面进行搜索,省去跳转的麻烦
  • 搜索框和结果居中固定显示,并且有滚轮
  • 为了可以和背景区分开,又不至于太僵硬,做成了毛玻璃效果
  • 同步适应手机端的摆放位置优化
  • 其他一些美化

那么就朝着这个方向开始做起来吧(并没有,其实都是做好以后再总结的😑

3 解决方案

2.1 添加搜索

layouts\partials\nav.html 中插入如下代码,在你想添加搜索的地方

1<li>
2    <a id="search-btn"  href="javascript:void(0);">
3        <i class="fa fa-search"></i>
4    </a>
5</li>
6<script src="/js/fuse.min.js"></script>
7<script src="/js/fastsearch.js"></script>
8<link rel="stylesheet" type="text/css" href="/css/custom.css">

fuse.min.js 需要在github上下载一下。

2.2 添加 index.json

然后,添加 layouts\_default\index.json 文件:

1{{- $.Scratch.Add "index" slice -}}
2{{- range .Site.RegularPages -}}
3    {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink "date" .Date "section" .Section) -}}
4{{- end -}}
5{{- $.Scratch.Get "index" | jsonify -}}

2.3 添加 css 文件

css\custom.css 中添加如下代码, css和js文件都需要进行引用

  1#fastSearch {
  2  visibility: hidden;
  3  position: absolute;
  4  top: 200%;
  5  left: 50%;
  6  transform: translate(-50%, -50%);
  7  display: inline-block;
  8  width: 600px;
  9  margin: 0 10px 0 0;
 10  padding: 0;
 11
 12 }
 13 
 14
 15#fastSearch input {
 16  /* padding: 4px; */
 17  width: 100%;
 18  height: 100%;
 19  font-size: 1em;
 20  color: #000000;
 21  font-weight: bold;
 22  /* background-color: #fffffffc; */
 23  border-radius: 5px 5px 5px 5px;
 24  border: none; 
 25  outline: none;
 26  text-align: left;
 27  display: inline-block;
 28  padding: 10px;
 29  backdrop-filter: blur(10px); /* 设置模糊效果,值越大模糊效果越明显 */
 30 background-color: rgba(242, 242, 242, 0.472); /* 设置半透明的背景色 */
 31}
 32
 33#searchResults li {
 34  list-style: none;
 35  margin-left: 0em;
 36  /* background-color: #fffffffc; */
 37  /* border-bottom: 1px dotted #000; */
 38}
 39
 40#searchResults li .title {
 41  font-size: 1em;
 42  margin: 0;
 43  color: #000;
 44  display: inline-block;
 45}
 46
 47
 48
 49#searchResults {
 50    position: absolute; /* 使ul元素相对于#fastSearch进行定位 */
 51    top: 100%; /* 调整ul元素的位置 */
 52    left: 0; /* 调整ul元素的位置 */
 53    width: 100%; /* 确保ul元素填充整个#fastSearch元素 */
 54    list-style: none; /* 移除列表样式 */
 55    padding: 0; /* 移除默认的ul元素内边距 */
 56  border-radius:  5px 5px 5px 5px;
 57  /* background-color: #fffffffc; */
 58  max-height: 600px; /* 根据需要调整最大高度 */
 59  overflow-y: auto; /* 当内容超过最大高度时显示滚动条 */
 60  backdrop-filter: blur(10px); /* 设置模糊效果,值越大模糊效果越明显 */
 61  background-color: rgba(242, 242, 242, 0.472); /* 设置半透明的背景色 */
 62}
 63
 64
 65#searchResults a {
 66  text-decoration: none !important;
 67  padding: 10px;
 68  display: inline-block;
 69  width: 100%;
 70  
 71}
 72
 73#searchResults a:hover, #searchResults a:focus {
 74  outline: 0;
 75  background-color: rgba(243, 243, 243, 0.627);
 76  
 77  color: #0085a1;
 78}
 79
 80#searchResults li:hover a span {
 81  
 82  color: #0085a1;
 83 }
 84 
 85
 86#search-btn {
 87  /* position: absolute;
 88  top: 0px;
 89  right: 0px; */
 90  /* font-size: 24px; */
 91}
 92
 93@media (max-width:683px) {
 94  #search-btn {
 95    top: 0px;
 96  }
 97  #fastSearch{
 98    top: 100px;
 99    width: 300px;
100  }
101
102}
103
104/* 对于WebKit浏览器(如Chrome和Safari) */
105::-webkit-input-placeholder {
106  color: rgba(73, 73, 73, 0.761);
107}
108
109/* 对于Mozilla Firefox 4至18 */
110:-moz-placeholder {
111  color: rgba(73, 73, 73, 0.761);
112  opacity: 1; /* Firefox默认给placeholder文字添加了透明度,所以需要设置opacity为1 */
113}
114
115/* 对于Mozilla Firefox 19+ */
116::-moz-placeholder {
117  color: rgba(73, 73, 73, 0.761);
118  opacity: 1;
119}
120
121/* 对于Internet Explorer 10-11 */
122:-ms-input-placeholder {
123  color: rgba(73, 73, 73, 0.761);
124}
125
126/* 对于Microsoft Edge */
127::-ms-input-placeholder {
128  color: rgba(73, 73, 73, 0.761);
129}

2.1 添加 js 文件

添加js文件 static\js\fastsearch.js ,并添加如下代码就大功告成了。

  1var fuse; // holds our search engine
  2var fuseIndex;
  3var searchVisible = false; 
  4var firstRun = true; // allow us to delay loading json data unless search activated
  5var list = document.getElementById('searchResults'); // targets the <ul>
  6var first = list.firstChild; // first child of search list
  7var last = list.lastChild; // last child of search list
  8var maininput = document.getElementById('searchInput'); // input box for search
  9var resultsAvailable = false; // Did we get any search results?
 10
 11// ==========================================
 12// The main keyboard event listener running the show
 13//
 14document.addEventListener('keydown', function(event) {
 15
 16  // CMD-/ to show / hide Search
 17  if (event.altKey && event.which === 191) {
 18      // Load json search index if first time invoking search
 19      // Means we don't load json unless searches are going to happen; keep user payload small unless needed
 20      doSearch(event)
 21  }
 22
 23  // Allow ESC (27) to close search box
 24  if (event.keyCode == 27) {
 25    if (searchVisible) {
 26      document.getElementById("fastSearch").style.visibility = "hidden";
 27      document.activeElement.blur();
 28      searchVisible = false;
 29    }
 30  }
 31
 32  // DOWN (40) arrow
 33  if (event.keyCode == 40) {
 34    if (searchVisible && resultsAvailable) {
 35      console.log("down");
 36      event.preventDefault(); // stop window from scrolling
 37      if ( document.activeElement == maininput) { first.focus(); } // if the currently focused element is the main input --> focus the first <li>
 38      else if ( document.activeElement == last ) { last.focus(); } // if we're at the bottom, stay there
 39      else { document.activeElement.parentElement.nextSibling.firstElementChild.focus(); } // otherwise select the next search result
 40    }
 41  }
 42
 43  // UP (38) arrow
 44  if (event.keyCode == 38) {
 45    if (searchVisible && resultsAvailable) {
 46      event.preventDefault(); // stop window from scrolling
 47      if ( document.activeElement == maininput) { maininput.focus(); } // If we're in the input box, do nothing
 48      else if ( document.activeElement == first) { maininput.focus(); } // If we're at the first item, go to input box
 49      else { document.activeElement.parentElement.previousSibling.firstElementChild.focus(); } // Otherwise, select the search result above the current active one
 50    }
 51  }
 52});
 53
 54
 55// ==========================================
 56// execute search as each character is typed
 57//
 58document.getElementById("searchInput").onkeyup = function(e) { 
 59  executeSearch(this.value);
 60}
 61
 62document.querySelector("body").onclick = function(e) { 
 63    if (e.target.tagName === 'BODY' || e.target.tagName === 'DIV') {
 64        hideSearch()
 65    }
 66}
 67
 68document.querySelector("#search-btn").onclick = function(e) { 
 69    doSearch(e)
 70}
 71  
 72function doSearch(e) {
 73    e.stopPropagation();
 74    if (firstRun) {
 75        loadSearch() // loads our json data and builds fuse.js search index
 76        firstRun = false // let's never do this again
 77    }
 78    // Toggle visibility of search box
 79    if (!searchVisible) {
 80        showSearch() // search visible
 81    }
 82    else {
 83        hideSearch()
 84    }
 85}
 86
 87function hideSearch() {
 88    document.getElementById("fastSearch").style.visibility = "hidden" // hide search box
 89    document.activeElement.blur() // remove focus from search box 
 90    searchVisible = false
 91}
 92
 93function showSearch() {
 94    document.getElementById("fastSearch").style.visibility = "visible" // show search box
 95    document.getElementById("searchInput").focus() // put focus in input box so you can just start typing
 96    searchVisible = true
 97}
 98
 99// ==========================================
100// fetch some json without jquery
101//
102function fetchJSONFile(path, callback) {
103  var httpRequest = new XMLHttpRequest();
104  httpRequest.onreadystatechange = function() {
105    if (httpRequest.readyState === 4) {
106      if (httpRequest.status === 200) {
107        var data = JSON.parse(httpRequest.responseText);
108          if (callback) callback(data);
109      }
110    }
111  };
112  httpRequest.open('GET', path);
113  httpRequest.send(); 
114}
115
116
117// ==========================================
118// load our search index, only executed once
119// on first call of search box (CMD-/)
120//
121function loadSearch() { 
122  console.log('loadSearch()')
123  fetchJSONFile('/index.json', function(data){
124
125    var options = { // fuse.js options; check fuse.js website for details
126      shouldSort: true,
127      location: 0,
128      distance: 100,
129      threshold: 0.4,
130      minMatchCharLength: 1,
131      keys: [
132        'permalink',
133        'title',
134        'tags',
135        'contents'
136        ]
137    };
138    // Create the Fuse index
139    fuseIndex = Fuse.createIndex(options.keys, data)
140    fuse = new Fuse(data, options, fuseIndex); // build the index from the json file
141  });
142}
143
144
145// ==========================================
146// using the index we loaded on CMD-/, run 
147// a search query (for "term") every time a letter is typed
148// in the search box
149//
150function executeSearch(term) {
151  let results = fuse.search(term); // the actual query being run using fuse.js
152  let searchitems = ''; // our results bucket
153
154  if (results.length === 0) { // no results based on what was typed into the input box
155    resultsAvailable = false;
156    searchitems = '';
157  } else { // build our html
158    // console.log(results)
159    permalinks = [];
160    numLimit = 100;
161    for (let item in results) { // only show first 5 results
162        if (item > numLimit) {
163            break;
164        }
165        if (permalinks.includes(results[item].item.permalink)) {
166            continue;
167        }
168    //   console.log('item: %d, title: %s', item, results[item].item.title)
169      searchitems = searchitems + '<li><a href="' + results[item].item.permalink + '" tabindex="0">' + '<span class="title">' + results[item].item.title + '</span></a></li>';
170      permalinks.push(results[item].item.permalink);
171    }
172    resultsAvailable = true;
173  }
174
175  document.getElementById("searchResults").innerHTML = searchitems;
176  if (results.length > 0) {
177    first = list.firstChild.firstElementChild; // first result container  used for checking against keyboard up/down location
178    last = list.lastChild.firstElementChild; // last result container  used for checking against keyboard up/down location
179  }
180}