diff --git a/_config.yml b/_config.yml index fd637a6..10c3130 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/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 4e0d749..e6c9e88 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 2c8503c..97eface 100644 --- a/source/css/_layout/third-party.styl +++ b/source/css/_layout/third-party.styl @@ -121,4 +121,8 @@ span.mathjax-overflow transition: opacity .3s &.abcjs-container - opacity: 1 \ No newline at end of file + opacity: 1 + ++maxWidth768() + .fancybox__toolbar__column.is-middle + display: none 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 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(content, slice)}...
` + }) + + resultItem += '