From 8199e25215157730191518c42053c55e5e3c438a Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 28 Mar 2023 20:40:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=87=8D=E6=A7=8B=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=90=9C=E7=B4=A2=20feat:=20Algolia=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=EF=BC=8C=E9=BB=9E=E6=93=8A=E6=96=87=E7=AB=A0=E5=85=A7?= =?UTF-8?q?=E5=AE=B9=E4=B9=9F=E6=9C=83=E8=B7=B3=E8=BD=89=E5=88=B0=E7=9B=B8?= =?UTF-8?q?=E6=87=89=E9=A0=81=E9=9D=A2=20fix:=20=E4=BF=AE=E5=BE=A9=20fullp?= =?UTF-8?q?age=20loading=20=E9=A1=AF=E7=A4=BA=E6=BB=BE=E5=8B=95=E6=A2=9D?= =?UTF-8?q?=E7=9A=84=20bug=20close=20#1235=20fix:=20=E4=BF=AE=E5=BE=A9=20s?= =?UTF-8?q?afari=20=E4=B8=8B=EF=BC=8C=E6=90=9C=E7=B4=A2=E5=85=A7=E5=AE=B9?= =?UTF-8?q?=E8=A2=AB=E7=B3=BB=E7=B5=B1=E6=90=9C=E7=B4=A2=E6=A1=86=E9=81=AE?= =?UTF-8?q?=E6=93=8B=E7=9A=84=20bug=20improvement:=20=E6=89=8B=E6=A9=9F=20?= =?UTF-8?q?safari=20=E6=A9=AB=E5=B1=8F=E6=99=82=EF=BC=8C=E7=B6=B2=E9=A0=81?= =?UTF-8?q?=E5=85=A8=E5=B1=8F=E5=B9=95=E9=A1=AF=E7=A4=BA=20improvement:=20?= =?UTF-8?q?=E7=95=B6=E6=B2=92=E6=9C=89=E9=96=8B=E5=95=9F=20beautify=20?= =?UTF-8?q?=E6=99=82=EF=BC=8C=20hr=20=E9=A1=AF=E7=A4=BA=E9=BB=98=E8=AA=8D?= =?UTF-8?q?=E7=9A=84=E6=A8=A3=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _config.yml | 8 +- languages/default.yml | 1 + languages/en.yml | 1 + languages/zh-CN.yml | 1 + languages/zh-TW.yml | 3 +- layout/includes/additional-js.pug | 2 + layout/includes/head.pug | 2 +- layout/includes/head/config.pug | 3 + layout/includes/layout.pug | 3 +- layout/includes/loading/fullpage-loading.pug | 7 +- .../third-party/search/local-search.pug | 4 +- package.json | 2 +- plugins.yml | 18 +- source/css/_layout/post.styl | 15 +- source/css/_layout/third-party.styl | 6 +- source/css/_search/algolia.styl | 26 +- source/css/_search/index.styl | 1 + source/css/_search/local-search.styl | 20 +- source/js/search/algolia.js | 36 +- source/js/search/local-search.js | 458 ++++++++++++------ source/js/utils.js | 35 +- 21 files changed, 426 insertions(+), 226 deletions(-) diff --git a/_config.yml b/_config.yml index db17670..72373bc 100644 --- a/_config.yml +++ b/_config.yml @@ -56,11 +56,15 @@ algolia_search: # Local search local_search: enable: false + # Preload the search data when the page loads. preload: false + # Show top n results per article, show all results by setting to -1 + top_n_per_article: 1 + # Unescape html strings to the readable one. + unescape: false CDN: # Docsearch -# https://docsearch.algolia.com/ docsearch: enable: false appId: @@ -777,7 +781,7 @@ rightside_item_order: medium_zoom: false # fancybox -# http://fancyapps.com/fancybox/3/ +# https://fancyapps.com/fancybox/ fancybox: true # Tag Plugins settings (標籤外掛) diff --git a/languages/default.yml b/languages/default.yml index a491719..fea921e 100644 --- a/languages/default.yml +++ b/languages/default.yml @@ -45,6 +45,7 @@ search: local_search: input_placeholder: Search for Posts hits_empty: "We didn't find any results for the search: ${query}" + hits_stats: '${hits} results found' pagination: prev: Previous Post diff --git a/languages/en.yml b/languages/en.yml index 49120c2..1cc937c 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -45,6 +45,7 @@ search: local_search: input_placeholder: Search for Posts hits_empty: "We didn't find any results for the search: ${query}" + hits_stats: '${hits} results found' pagination: prev: Previous Post diff --git a/languages/zh-CN.yml b/languages/zh-CN.yml index 9c2919a..94f85cd 100644 --- a/languages/zh-CN.yml +++ b/languages/zh-CN.yml @@ -46,6 +46,7 @@ search: local_search: input_placeholder: 搜索文章 hits_empty: '找不到您查询的内容:${query}' + hits_stats: '共找到 ${hits} 篇文章' pagination: prev: 上一篇 diff --git a/languages/zh-TW.yml b/languages/zh-TW.yml index 86623c0..9801d16 100644 --- a/languages/zh-TW.yml +++ b/languages/zh-TW.yml @@ -46,7 +46,8 @@ search: local_search: input_placeholder: 搜尋文章 hits_empty: '找不到您查詢的內容:${query}' - + hits_stats: '共找到 ${hits} 篇文章' + pagination: prev: 上一篇 next: 下一篇 diff --git a/layout/includes/additional-js.pug b/layout/includes/additional-js.pug index 3cb29f4..d5d556d 100644 --- a/layout/includes/additional-js.pug +++ b/layout/includes/additional-js.pug @@ -59,3 +59,5 @@ div if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js') + + !=partial('includes/third-party/search/index', {}, {cache: true}) \ No newline at end of file diff --git a/layout/includes/head.pug b/layout/includes/head.pug index 5cb3c61..5748040 100644 --- a/layout/includes/head.pug +++ b/layout/includes/head.pug @@ -15,7 +15,7 @@ meta(charset='UTF-8') meta(http-equiv="X-UA-Compatible" content="IE=edge") -meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0") +meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,viewport-fit=cover") title= tabTitle meta(name="author" content=pageAuthor) meta(name="copyright" content=pageCopyright) diff --git a/layout/includes/head/config.pug b/layout/includes/head/config.pug index ed81219..16eab2f 100644 --- a/layout/includes/head/config.pug +++ b/layout/includes/head/config.pug @@ -21,9 +21,12 @@ localSearch = JSON.stringify({ path: theme.local_search.CDN ? theme.local_search.CDN : config.root + config.search.path, preload: theme.local_search.preload, + top_n_per_article: theme.local_search.top_n_per_article, + unescape: theme.local_search.unescape, languages: { // search languages hits_empty: _p("search.local_search.hits_empty"), + hits_stats: _p("search.local_search.hits_stats"), } }) } diff --git a/layout/includes/layout.pug b/layout/includes/layout.pug index 6055d1e..dde47c5 100644 --- a/layout/includes/layout.pug +++ b/layout/includes/layout.pug @@ -44,5 +44,4 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside include ./404.pug include ./rightside.pug - include ./additional-js.pug - !=partial('includes/third-party/search/index', {}, {cache: true}) \ No newline at end of file + include ./additional-js.pug \ No newline at end of file diff --git a/layout/includes/loading/fullpage-loading.pug b/layout/includes/loading/fullpage-loading.pug index 75a2041..5ca3d0d 100644 --- a/layout/includes/loading/fullpage-loading.pug +++ b/layout/includes/loading/fullpage-loading.pug @@ -11,15 +11,16 @@ script. const preloader = { endLoading: () => { - document.body.style.overflow = 'auto'; + document.body.style.overflow = ''; document.getElementById('loading-box').classList.add("loaded") }, initLoading: () => { - document.body.style.overflow = ''; + document.body.style.overflow = 'hidden'; document.getElementById('loading-box').classList.remove("loaded") - } } + + preloader.initLoading() window.addEventListener('load',()=> { preloader.endLoading() }) if (!{theme.pjax && theme.pjax.enable}) { diff --git a/layout/includes/third-party/search/local-search.pug b/layout/includes/third-party/search/local-search.pug index c3a4777..0c7537b 100644 --- a/layout/includes/third-party/search/local-search.pug +++ b/layout/includes/third-party/search/local-search.pug @@ -15,8 +15,8 @@ .local-search-box input(placeholder=_p("search.local_search.input_placeholder") type="text").local-search-box--input hr - #local-search-results - + #local-search-results.no-result + #local-search-stats-wrap #search-mask script(src=url_for(theme.asset.local_search)) \ No newline at end of file diff --git a/package.json b/package.json index ca6f00d..424a615 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "4.8.0-b1", + "version": "4.8.0-b2", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/plugins.yml b/plugins.yml index b672299..648077e 100644 --- a/plugins.yml +++ b/plugins.yml @@ -1,11 +1,11 @@ algolia_search: name: algoliasearch file: dist/algoliasearch-lite.umd.js - version: 4.14.3 + version: 4.15.0 instantsearch: name: instantsearch.js file: dist/instantsearch.production.min.js - version: 4.51.1 + version: 4.52.0 pjax: name: pjax file: pjax.min.js @@ -42,12 +42,12 @@ waline_js: name: '@waline/client' file: dist/waline.js other_name: waline - version: 2.14.7 + version: 2.14.8 waline_css: name: '@waline/client' file: dist/waline.css other_name: waline - version: 2.14.7 + version: 2.14.8 sharejs: name: butterfly-extsrc file: sharejs/dist/js/social-share.min.js @@ -73,7 +73,7 @@ katex_copytex: mermaid: name: mermaid file: dist/mermaid.min.js - version: 9.4.0 + version: 9.4.3 canvas_ribbon: name: butterfly-extsrc file: dist/canvas-ribbon.min.js @@ -121,12 +121,12 @@ pangu: fancybox_css: name: '@fancyapps/ui' file: dist/fancybox/fancybox.css - version: 5.0.3 + version: 5.0.7 other_name: fancyapps-ui fancybox: name: '@fancyapps/ui' file: dist/fancybox/fancybox.umd.js - version: 5.0.3 + version: 5.0.7 other_name: fancyapps-ui medium_zoom: name: medium-zoom @@ -183,11 +183,11 @@ prismjs_autoloader: artalk_js: name: artalk file: dist/Artalk.js - version: 2.4.4 + version: 2.5.0 artalk_css: name: artalk file: dist/Artalk.css - version: 2.4.4 + version: 2.5.0 pace_js: name: pace-js other_name: pace diff --git a/source/css/_layout/post.styl b/source/css/_layout/post.styl index 4915ee6..a6c59cc 100644 --- a/source/css/_layout/post.styl +++ b/source/css/_layout/post.styl @@ -144,15 +144,26 @@ beautify() p margin: 0 0 8px + > :last-child + margin-bottom: 0 !important + + hr + margin: 20px 0 + if hexo-config('beautify.enable') if hexo-config('beautify.field') == 'site' beautify() else if hexo-config('beautify.field') == 'post' &.post-content beautify() + else + hr + margin: 20px 0 + border: 1px inset + width 100% - > :last-child - margin-bottom: 0 !important + &:before + content: none #post .tag_share diff --git a/source/css/_layout/third-party.styl b/source/css/_layout/third-party.styl index bc2bd51..a0a5b2d 100644 --- a/source/css/_layout/third-party.styl +++ b/source/css/_layout/third-party.styl @@ -113,4 +113,8 @@ span.mathjax-overflow content: none .snackbar-css - border-radius: 5px !important \ No newline at end of file + border-radius: 5px !important + ++maxWidth768() + .fancybox__toolbar__column.is-middle + display: none \ No newline at end of file diff --git a/source/css/_search/algolia.styl b/source/css/_search/algolia.styl index 31ba9df..b2f4067 100644 --- a/source/css/_search/algolia.styl +++ b/source/css/_search/algolia.styl @@ -26,12 +26,15 @@ color: $search-keyword-highlight font-weight: bold + .algolia-hits-item-title + font-weight: 600 + .algolia-hit-item-content margin: 0 0 8px word-break: break-word .ais-Pagination - margin: 20px 0 0 + margin: 15px 0 0 padding: 0 text-align: center @@ -61,22 +64,16 @@ .ais-Pagination-item--disabled visibility: hidden - .algolia-logo - padding-top: 2px - width: 80px - height: 30px - #algolia-hits > div overflow-y: scroll margin: 0 -20px padding: 0 22px - - +minWidth768() - max-height: calc(80vh - 240px) + max-height: calc(80vh - 240px) +maxWidth768() - height: calc(100vh - 260px) + max-height: none + height: calc(var(--search-height) - 265px) #algolia-info div @@ -84,10 +81,7 @@ .algolia-poweredBy float: right + vertical-align: text-top -.apple - #algolia-search - #algolia-hits - > div - +maxWidth768() - height: calc(90vh - 260px) + svg + height: 1.1em \ No newline at end of file diff --git a/source/css/_search/index.styl b/source/css/_search/index.styl index bc5c5b1..93d6ad8 100644 --- a/source/css/_search/index.styl +++ b/source/css/_search/index.styl @@ -9,6 +9,7 @@ width: 600px border-radius: 8px background: var(--search-bg) + --search-height: 100vh +maxWidth768() top: 0 diff --git a/source/css/_search/local-search.styl b/source/css/_search/local-search.styl index 69e8c0d..f5c5fa9 100644 --- a/source/css/_search/local-search.styl +++ b/source/css/_search/local-search.styl @@ -18,7 +18,7 @@ .search-wrap display: none - .local-search__hit-item + .local-search-hit-item position: relative padding-left: 24px line-height: 1.7 @@ -55,16 +55,20 @@ margin: 0 0 8px word-break: break-word - .search-keyword - color: $search-keyword-highlight - font-weight: bold - .search-result-list overflow-y: overlay margin: 0 -20px padding: 0 22px - max-height: calc(80vh - 130px) + max-height: calc(80vh - 200px) +maxWidth768() - padding-bottom: 40px - max-height: 75vh !important + max-height: calc(var(--search-height) - 220px) !important + + .no-result + & + #local-search-stats-wrap + display: none + +.search-keyword + background: transparent + color: $search-keyword-highlight + font-weight: bold \ No newline at end of file diff --git a/source/js/search/algolia.js b/source/js/search/algolia.js index 338c617..9ac9f94 100644 --- a/source/js/search/algolia.js +++ b/source/js/search/algolia.js @@ -1,10 +1,13 @@ window.addEventListener('load', () => { + const $searchMask = document.getElementById('search-mask') + const $searchDialog = document.querySelector('#algolia-search .search-dialog') + const openSearch = () => { const bodyStyle = document.body.style bodyStyle.width = '100%' bodyStyle.overflow = 'hidden' - btf.animateIn(document.getElementById('search-mask'), 'to_show 0.5s') - btf.animateIn(document.querySelector('#algolia-search .search-dialog'), 'titleScale 0.5s') + btf.animateIn($searchMask, 'to_show 0.5s') + btf.animateIn($searchDialog, 'titleScale 0.5s') setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100) // shortcut: ESC @@ -14,22 +17,33 @@ window.addEventListener('load', () => { document.removeEventListener('keydown', f) } }) + + fixSafariHeight() + window.addEventListener('resize', fixSafariHeight) } const closeSearch = () => { const bodyStyle = document.body.style bodyStyle.width = '' bodyStyle.overflow = '' - btf.animateOut(document.querySelector('#algolia-search .search-dialog'), 'search_close .5s') - btf.animateOut(document.getElementById('search-mask'), 'to_hide 0.5s') + btf.animateOut($searchDialog, 'search_close .5s') + btf.animateOut($searchMask, 'to_hide 0.5s') + window.removeEventListener('resize', fixSafariHeight) + } + + // fix safari + const fixSafariHeight = () => { + if (window.innerWidth < 768) { + $searchDialog.style.setProperty('--search-height', window.innerHeight + 'px') + } } const searchClickFn = () => { document.querySelector('#search-button > .search').addEventListener('click', openSearch) } - const searchClickFnOnce = () => { - document.getElementById('search-mask').addEventListener('click', closeSearch) + const searchFnOnce = () => { + $searchMask.addEventListener('click', closeSearch) document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch) } @@ -102,9 +116,9 @@ window.addEventListener('load', () => { : '' return ` - ${result.title.value || 'no-title'} - -

${content}

` + ${result.title.value || 'no-title'} +

${content}

+ ` }, empty: function (data) { return ( @@ -150,10 +164,10 @@ window.addEventListener('load', () => { search.start() searchClickFn() - searchClickFnOnce() + searchFnOnce() window.addEventListener('pjax:complete', () => { - getComputedStyle(document.querySelector('#algolia-search .search-dialog')).display === 'block' && closeSearch() + !btf.isHidden($searchMask) && closeSearch() searchClickFn() }) diff --git a/source/js/search/local-search.js b/source/js/search/local-search.js index 6a495f4..73dfe6b 100644 --- a/source/js/search/local-search.js +++ b/source/js/search/local-search.js @@ -1,17 +1,311 @@ +/** + * Refer to hexo-generator-searchdb + * https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js + * Modified by hexo-theme-butterfly + */ + +class LocalSearch { + constructor ({ + path = '', + unescape = false, + top_n_per_article = 1 + }) { + this.path = path + this.unescape = unescape + this.top_n_per_article = top_n_per_article + this.isfetched = false + this.datas = null + } + + getIndexByWord (words, text, caseSensitive = false) { + const index = [] + const included = new Set() + + if (!caseSensitive) { + text = text.toLowerCase() + } + words.forEach(word => { + if (this.unescape) { + const div = document.createElement('div') + div.innerText = word + word = div.innerHTML + } + const wordLen = word.length + if (wordLen === 0) return + let startPosition = 0 + let position = -1 + if (!caseSensitive) { + word = word.toLowerCase() + } + while ((position = text.indexOf(word, startPosition)) > -1) { + index.push({ position, word }) + included.add(word) + startPosition = position + wordLen + } + }) + // Sort index by position of keyword + index.sort((left, right) => { + if (left.position !== right.position) { + return left.position - right.position + } + return right.word.length - left.word.length + }) + return [index, included] + } + + // Merge hits into slices + mergeIntoSlice (start, end, index) { + let item = index[0] + let { position, word } = item + const hits = [] + const count = new Set() + while (position + word.length <= end && index.length !== 0) { + count.add(word) + hits.push({ + position, + length: word.length + }) + const wordEnd = position + word.length + + // Move to next position of hit + index.shift() + while (index.length !== 0) { + item = index[0] + position = item.position + word = item.word + if (wordEnd > position) { + index.shift() + } else { + break + } + } + } + return { + hits, + start, + end, + count: count.size + } + } + + // Highlight title and content + highlightKeyword (val, slice) { + let result = '' + let index = slice.start + for (const { position, length } of slice.hits) { + result += val.substring(index, position) + index = position + length + result += `${val.substr(position, length)}` + } + result += val.substring(index, slice.end) + return result + } + + getResultItems (keywords) { + const resultItems = [] + this.datas.forEach(({ title, content, url }) => { + // The number of different keywords included in the article. + const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title) + const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content) + const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size + + // Show search results + const hitCount = indexOfTitle.length + indexOfContent.length + if (hitCount === 0) return + + const slicesOfTitle = [] + if (indexOfTitle.length !== 0) { + slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle)) + } + + let slicesOfContent = [] + while (indexOfContent.length !== 0) { + const item = indexOfContent[0] + const { position } = item + // Cut out 120 characters. The maxlength of .search-input is 80. + const start = Math.max(0, position - 20) + const end = Math.min(content.length, position + 100) + slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent)) + } + + // Sort slices in content by included keywords' count and hits' count + slicesOfContent.sort((left, right) => { + if (left.count !== right.count) { + return right.count - left.count + } else if (left.hits.length !== right.hits.length) { + return right.hits.length - left.hits.length + } + return left.start - right.start + }) + + // Select top N slices in content + const upperBound = parseInt(this.top_n_per_article, 10) + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound) + } + + let resultItem = '' + + url = new URL(url, location.origin) + url.searchParams.append('highlight', keywords.join(' ')) + + if (slicesOfTitle.length !== 0) { + resultItem += `
${this.highlightKeyword(title, slicesOfTitle[0])}` + } else { + resultItem += `' + resultItems.push({ + item: resultItem, + id: resultItems.length, + hitCount, + includedCount + }) + }) + return resultItems + } + + fetchData () { + const isXml = !this.path.endsWith('json') + fetch(this.path) + .then(response => response.text()) + .then(res => { + // Get the contents from search data + this.isfetched = true + this.datas = isXml + ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({ + title: element.querySelector('title').textContent, + content: element.querySelector('content').textContent, + url: element.querySelector('url').textContent + })) + : JSON.parse(res) + // Only match articles with non-empty titles + this.datas = this.datas.filter(data => data.title).map(data => { + data.title = data.title.trim() + data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '' + data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/') + return data + }) + // Remove loading animation + window.dispatchEvent(new Event('search:loaded')) + }) + } + + // Highlight by wrapping node in mark elements with the given class name + highlightText (node, slice, className) { + const val = node.nodeValue + let index = slice.start + const children = [] + for (const { position, length } of slice.hits) { + const text = document.createTextNode(val.substring(index, position)) + index = position + length + const mark = document.createElement('mark') + mark.className = className + mark.appendChild(document.createTextNode(val.substr(position, length))) + children.push(text, mark) + } + node.nodeValue = val.substring(index, slice.end) + children.forEach(element => { + node.parentNode.insertBefore(element, node) + }) + } + + // Highlight the search words provided in the url in the text + highlightSearchWords (body) { + const params = new URL(location.href).searchParams.get('highlight') + const keywords = params ? params.split(' ') : [] + if (!keywords.length || !body) return + const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null) + const allNodes = [] + while (walk.nextNode()) { + if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode) + } + allNodes.forEach(node => { + const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue) + if (!indexOfNode.length) return + const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode) + this.highlightText(node, slice, 'search-keyword') + }) + } +} + window.addEventListener('load', () => { +// Search + const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch + const localSearch = new LocalSearch({ + path, + top_n_per_article, + unescape + }) + + const input = document.querySelector('#local-search-input input') + const statsItem = document.getElementById('local-search-stats-wrap') + const $loadingStatus = document.getElementById('loading-status') + + const inputEventFunction = () => { + if (!localSearch.isfetched) return + const searchText = input.value.trim().toLowerCase() + if (searchText !== '') $loadingStatus.innerHTML = '' + const keywords = searchText.split(/[-\s]+/) + const container = document.getElementById('local-search-results') + let resultItems = [] + if (searchText.length > 0) { + // Perform local searching + resultItems = localSearch.getResultItems(keywords) + } + if (keywords.length === 1 && keywords[0] === '') { + container.classList.add('no-result') + container.textContent = '' + } else if (resultItems.length === 0) { + container.textContent = '' + statsItem.innerHTML = `
${languages.hits_empty.replace(/\$\{query}/, searchText)}
` + } else { + resultItems.sort((left, right) => { + if (left.includedCount !== right.includedCount) { + return right.includedCount - left.includedCount + } else if (left.hitCount !== right.hitCount) { + return right.hitCount - left.hitCount + } + return right.id - left.id + }) + + const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length) + + container.classList.remove('no-result') + container.innerHTML = `
${resultItems.map(result => result.item).join('')}
` + statsItem.innerHTML = `
${stats}
` + window.pjax && window.pjax.refresh(container) + } + + $loadingStatus.innerHTML = '' + } + let loadFlag = false - let dataObj = [] const $searchMask = document.getElementById('search-mask') + const $searchDialog = document.querySelector('#local-search .search-dialog') + + // fix safari + const fixSafariHeight = () => { + if (window.innerWidth < 768) { + $searchDialog.style.setProperty('--search-height', window.innerHeight + 'px') + } + } const openSearch = () => { const bodyStyle = document.body.style bodyStyle.width = '100%' bodyStyle.overflow = 'hidden' btf.animateIn($searchMask, 'to_show 0.5s') - btf.animateIn(document.querySelector('#local-search .search-dialog'), 'titleScale 0.5s') - setTimeout(() => { document.querySelector('#local-search-input input').focus() }, 100) + btf.animateIn($searchDialog, 'titleScale 0.5s') + setTimeout(() => { input.focus() }, 300) if (!loadFlag) { - search() + !localSearch.isfetched && localSearch.fetchData() + input.addEventListener('input', inputEventFunction) loadFlag = true } // shortcut: ESC @@ -21,170 +315,46 @@ window.addEventListener('load', () => { document.removeEventListener('keydown', f) } }) + + fixSafariHeight() + window.addEventListener('resize', fixSafariHeight) } const closeSearch = () => { const bodyStyle = document.body.style bodyStyle.width = '' bodyStyle.overflow = '' - btf.animateOut(document.querySelector('#local-search .search-dialog'), 'search_close .5s') + btf.animateOut($searchDialog, 'search_close .5s') btf.animateOut($searchMask, 'to_hide 0.5s') + window.removeEventListener('resize', fixSafariHeight) } const searchClickFn = () => { document.querySelector('#search-button > .search').addEventListener('click', openSearch) } - const searchClickFnOnce = () => { + const searchFnOnce = () => { document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch) $searchMask.addEventListener('click', closeSearch) - if (GLOBAL_CONFIG.localSearch.preload) dataObj = fetchData(GLOBAL_CONFIG.localSearch.path) - } - - // check url is json or not - const isJson = url => { - const reg = /\.json$/ - return reg.test(url) - } - - const fetchData = async (path) => { - let data = [] - const response = await fetch(path) - if (isJson(path)) { - data = await response.json() - } else { - const res = await response.text() - const t = await new window.DOMParser().parseFromString(res, 'text/xml') - const a = await t - data = [...a.querySelectorAll('entry')].map(item => { - return { - title: item.querySelector('title').textContent, - content: item.querySelector('content') && item.querySelector('content').textContent, - url: item.querySelector('url').textContent - } - }) + if (GLOBAL_CONFIG.localSearch.preload) { + localSearch.fetchData() } - if (response.ok) { - const $loadDataItem = document.getElementById('loading-database') - $loadDataItem.nextElementSibling.style.display = 'block' - $loadDataItem.remove() - } - return data + localSearch.highlightSearchWords(document.getElementById('article-container')) } - const search = () => { - if (!GLOBAL_CONFIG.localSearch.preload) { - dataObj = fetchData(GLOBAL_CONFIG.localSearch.path) - } - - const $input = document.querySelector('#local-search-input input') - const $resultContent = document.getElementById('local-search-results') - const $loadingStatus = document.getElementById('loading-status') - - $input.addEventListener('input', function () { - const keywords = this.value.trim().toLowerCase().split(/[\s]+/) - if (keywords[0] !== '') $loadingStatus.innerHTML = '' - else { - $resultContent.innerHTML = '' - return - } - - let str = '
' - if (keywords.length <= 0) return - let count = 0 - // perform local searching - dataObj.then(data => { - data.forEach(data => { - let isMatch = true - let dataTitle = data.title ? data.title.trim().toLowerCase() : '' - const dataContent = data.content ? data.content.trim().replace(/<[^>]+>/g, '').toLowerCase() : '' - const dataUrl = data.url.startsWith('/') ? data.url : GLOBAL_CONFIG.root + data.url - let indexTitle = -1 - let indexContent = -1 - let firstOccur = -1 - // only match articles with not empty titles and contents - if (dataTitle !== '' || dataContent !== '') { - keywords.forEach((keyword, i) => { - indexTitle = dataTitle.indexOf(keyword) - indexContent = dataContent.indexOf(keyword) - if (indexTitle < 0 && indexContent < 0) { - isMatch = false - } else { - if (indexContent < 0) { - indexContent = 0 - } - if (i === 0) { - firstOccur = indexContent - } - } - }) - } else { - isMatch = false - } - - // show search results - if (isMatch) { - if (firstOccur >= 0) { - // cut out 130 characters - // let start = firstOccur - 30 < 0 ? 0 : firstOccur - 30 - // let end = firstOccur + 50 > dataContent.length ? dataContent.length : firstOccur + 50 - let start = firstOccur - 30 - let end = firstOccur + 100 - let pre = '' - let post = '' - - if (start < 0) { - start = 0 - } - - if (start === 0) { - end = 100 - } else { - pre = '...' - } - - if (end > dataContent.length) { - end = dataContent.length - } else { - post = '...' - } - - let matchContent = dataContent.substring(start, end) - - // highlight all keywords - keywords.forEach(keyword => { - matchContent = matchContent.replaceAll(keyword, '' + keyword + '') - dataTitle = dataTitle.replaceAll(keyword, '' + keyword + '') - }) - - str += '' - } - }) - if (count === 0) { - str += '
' + GLOBAL_CONFIG.localSearch.languages.hits_empty.replace(/\$\{query}/, this.value.trim()) + - '
' - } - str += '
' - $resultContent.innerHTML = str - if (keywords[0] !== '') $loadingStatus.innerHTML = '' - window.pjax && window.pjax.refresh($resultContent) - }) - }) - } + window.addEventListener('search:loaded', () => { + const $loadDataItem = document.getElementById('loading-database') + $loadDataItem.nextElementSibling.style.display = 'block' + $loadDataItem.remove() + }) searchClickFn() - searchClickFnOnce() + searchFnOnce() // pjax window.addEventListener('pjax:complete', () => { !btf.isHidden($searchMask) && closeSearch() + localSearch.highlightSearchWords(document.getElementById('article-container')) searchClickFn() }) }) diff --git a/source/js/utils.js b/source/js/utils.js index 1ee51d1..3debebf 100644 --- a/source/js/utils.js +++ b/source/js/utils.js @@ -80,30 +80,19 @@ const btf = { const day = hour * 24 const month = day * 30 - let result - if (more) { - const monthCount = dateDiff / month - const dayCount = dateDiff / day - const hourCount = dateDiff / hour - const minuteCount = dateDiff / minute + if (!more) return parseInt(dateDiff / day) - if (monthCount > 12) { - result = datePost.toISOString().slice(0, 10) - } else if (monthCount >= 1) { - result = parseInt(monthCount) + ' ' + GLOBAL_CONFIG.date_suffix.month - } else if (dayCount >= 1) { - result = parseInt(dayCount) + ' ' + GLOBAL_CONFIG.date_suffix.day - } else if (hourCount >= 1) { - result = parseInt(hourCount) + ' ' + GLOBAL_CONFIG.date_suffix.hour - } else if (minuteCount >= 1) { - result = parseInt(minuteCount) + ' ' + GLOBAL_CONFIG.date_suffix.min - } else { - result = GLOBAL_CONFIG.date_suffix.just - } - } else { - result = parseInt(dateDiff / day) - } - return result + const monthCount = dateDiff / month + const dayCount = dateDiff / day + const hourCount = dateDiff / hour + const minuteCount = dateDiff / minute + + if (monthCount > 12) return datePost.toISOString().slice(0, 10) + if (monthCount >= 1) return parseInt(monthCount) + ' ' + GLOBAL_CONFIG.date_suffix.month + if (dayCount >= 1) return parseInt(dayCount) + ' ' + GLOBAL_CONFIG.date_suffix.day + if (hourCount >= 1) return parseInt(hourCount) + ' ' + GLOBAL_CONFIG.date_suffix.hour + if (minuteCount >= 1) return parseInt(minuteCount) + ' ' + GLOBAL_CONFIG.date_suffix.min + return GLOBAL_CONFIG.date_suffix.just }, loadComment: (dom, callback) => { From 93f7461d2815a03f5e6c8d539c04fa159ef51fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B4=9B=E7=AB=B9?= Date: Mon, 3 Apr 2023 10:43:55 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E5=AE=BD=E5=B1=8F=201500px=20?= =?UTF-8?q?=E6=9C=89=E4=BA=9B=E6=B5=AA=E8=B4=B9=E7=A9=BA=E9=97=B4=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=B0=201700px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/css/_page/common.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/css/_page/common.styl b/source/css/_page/common.styl index 3356d17..0be8e49 100644 --- a/source/css/_page/common.styl +++ b/source/css/_page/common.styl @@ -18,7 +18,7 @@ padding: 20px 5px +minWidth2000() - max-width: 1500px + max-width: 1700px & > div:first-child:not(.recent-posts) @extend .cardHover