diff --git a/_config.yml b/_config.yml index 68b25a1..4ac6107 100644 --- a/_config.yml +++ b/_config.yml @@ -51,6 +51,8 @@ algolia_search: # Local search local_search: enable: false + preload: false + CDN: # Math (數學) # -------------------------------------- diff --git a/layout/includes/head/config.pug b/layout/includes/head/config.pug index 3cc2808..61f62d0 100644 --- a/layout/includes/head/config.pug +++ b/layout/includes/head/config.pug @@ -19,7 +19,8 @@ let localSearch = 'undefined'; if (theme.local_search && theme.local_search.enable) { localSearch = JSON.stringify({ - path: config.search.path, + path: theme.local_search.CDN ? theme.local_search.CDN : config.root + config.search.path, + preload: theme.local_search.preload, languages: { // search languages hits_empty: _p("search.local_search.hits_empty"), diff --git a/package.json b/package.json index ff4c103..4e31934 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "4.2.0-b1", + "version": "4.2.0-b2", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/scripts/helpers/related_post.js b/scripts/helpers/related_post.js index 2f8371a..6809ac7 100644 --- a/scripts/helpers/related_post.js +++ b/scripts/helpers/related_post.js @@ -54,14 +54,15 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) { relatedPosts[i].cover === false ? relatedPosts[i].randomcover : relatedPosts[i].cover - result += `
` + const title = this.escape_html(relatedPosts[i].title) + result += `
` result += `cover` if (dateType === 'created') { result += `' } diff --git a/source/css/_layout/third-party.styl b/source/css/_layout/third-party.styl index 8f52548..cf9483c 100644 --- a/source/css/_layout/third-party.styl +++ b/source/css/_layout/third-party.styl @@ -89,6 +89,7 @@ mjx-container[display], .has-jax overflow-x: auto overflow-y: hidden + line-height: normal !important .aplayer color: $font-black diff --git a/source/css/_search/algolia.styl b/source/css/_search/algolia.styl index c90c0a0..db57b02 100644 --- a/source/css/_search/algolia.styl +++ b/source/css/_search/algolia.styl @@ -11,7 +11,8 @@ color: var(--search-input-color) .ais-Hits-list - padding-left: 24px + margin: 0 + padding: 0 @extend .list-beauty a @@ -25,12 +26,17 @@ color: $search-keyword-highlight font-weight: bold + .algolia-hit-item-content + margin: 0 0 8px + word-break: break-all + .ais-Pagination - margin: 16px 0 0 + margin: 20px 0 0 padding: 0 text-align: center .ais-Pagination-list + margin: 0 padding: 0 list-style: none @@ -52,7 +58,27 @@ color: #eee cursor: default + .ais-Pagination-item--disabled + visibility: hidden + .algolia-logo padding-top: 2px width: 80px - height: 30px \ No newline at end of file + height: 30px + + #algolia-hits + > div + overflow-y: scroll + + +minWidth768() + max-height: calc(80vh - 240px) + + +maxWidth768() + height: calc(100vh - 260px) + +.apple + #algolia-search + #algolia-hits + > div + +maxWidth768() + height: calc(90vh - 260px) diff --git a/source/css/_search/index.styl b/source/css/_search/index.styl index 2e4e874..bc5c5b1 100644 --- a/source/css/_search/index.styl +++ b/source/css/_search/index.styl @@ -1,6 +1,6 @@ .search-dialog position: fixed - top: 100px + top: 10% left: 50% z-index: 1001 display: none diff --git a/source/css/_search/local-search.styl b/source/css/_search/local-search.styl index f67d87a..bf99945 100644 --- a/source/css/_search/local-search.styl +++ b/source/css/_search/local-search.styl @@ -60,7 +60,7 @@ .search-result-list overflow-y: auto - max-height: 210px + max-height: calc(80vh - 130px) +maxWidth768() padding-bottom: 40px diff --git a/source/js/search/algolia.js b/source/js/search/algolia.js index 13373e9..1554cc6 100644 --- a/source/js/search/algolia.js +++ b/source/js/search/algolia.js @@ -26,16 +26,39 @@ window.addEventListener('load', () => { const searchClickFn = () => { document.querySelector('#search-button > .search').addEventListener('click', openSearch) + } + + const searchClickFnOnce = () => { document.getElementById('search-mask').addEventListener('click', closeSearch) document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch) } - searchClickFn() + const cutContent = content => { + if (content === '') return '' - window.addEventListener('pjax:complete', function () { - getComputedStyle(document.querySelector('#algolia-search .search-dialog')).display === 'block' && closeSearch() - searchClickFn() - }) + const firstOccur = content.indexOf('') + + let start = firstOccur - 30 + let end = firstOccur + 120 + let pre = '' + let post = '' + + if (start <= 0) { + start = 0 + end = 140 + } else { + pre = '...' + } + + if (end > content.length) { + end = content.length + } else { + post = '...' + } + + let matchContent = pre + content.substring(start, end) + post + return matchContent + } const algolia = GLOBAL_CONFIG.algolia const isAlgoliaValid = algolia.appId && algolia.apiKey && algolia.indexName @@ -43,81 +66,87 @@ window.addEventListener('load', () => { return console.error('Algolia setting is invalid!') } - const searchClient = window.algoliasearch(algolia.appId, algolia.apiKey) const search = instantsearch({ indexName: algolia.indexName, - searchClient + searchClient: algoliasearch(algolia.appId, algolia.apiKey), + searchFunction(helper) { + helper.state.query && helper.search() + }, }) - search.addWidgets([ - instantsearch.widgets.configure({ - hitsPerPage: 5 - }) - ]) + const configure = instantsearch.widgets.configure({ + hitsPerPage: 5 + }) - search.addWidgets([ - instantsearch.widgets.searchBox({ - container: '#algolia-search-input', - showReset: false, - showSubmit: false, - placeholder: GLOBAL_CONFIG.algolia.languages.input_placeholder, - showLoadingIndicator: true - }) - ]) + const searchBox = instantsearch.widgets.searchBox({ + container: '#algolia-search-input', + showReset: false, + showSubmit: false, + placeholder: GLOBAL_CONFIG.algolia.languages.input_placeholder, + showLoadingIndicator: true + }) - search.addWidgets([ - instantsearch.widgets.hits({ - container: '#algolia-hits', - templates: { - item: function (data) { - const link = data.permalink ? data.permalink : (GLOBAL_CONFIG.root + data.path) - return ` - - ${data._highlightResult.title.value || 'no-title'} - ` - }, - empty: function (data) { - return ( - '
' + - GLOBAL_CONFIG.algolia.languages.hits_empty.replace(/\$\{query}/, data.query) + - '
' - ) - } + const hits = instantsearch.widgets.hits({ + container: '#algolia-hits', + templates: { + item(data) { + console.log(data); + const link = data.permalink ? data.permalink : (GLOBAL_CONFIG.root + data.path) + return ` + + ${data._highlightResult.title.value || 'no-title'} + +

${cutContent(data._highlightResult.contentStripTruncate.value)}

` + }, + empty: function (data) { + return ( + '
' + + GLOBAL_CONFIG.algolia.languages.hits_empty.replace(/\$\{query}/, data.query) + + '
' + ) } - }) - ]) + } + }) - search.addWidgets([ - instantsearch.widgets.stats({ - container: '#algolia-stats', - templates: { - text: function (data) { - const icon = '' - const stats = GLOBAL_CONFIG.algolia.languages.hits_stats - .replace(/\$\{hits}/, data.nbHits) - .replace(/\$\{time}/, data.processingTimeMS) - return ( - `
${stats}` - ) - } + const stats = instantsearch.widgets.stats({ + container: '#algolia-stats', + templates: { + text: function (data) { + const icon = '' + const stats = GLOBAL_CONFIG.algolia.languages.hits_stats + .replace(/\$\{hits}/, data.nbHits) + .replace(/\$\{time}/, data.processingTimeMS) + return ( + `
${stats}` + ) } - }) - ]) + } + }) + + const pagination = instantsearch.widgets.pagination({ + container: '#algolia-pagination', + totalPages: 5, + templates: { + first: '', + last: '', + previous: '', + next: '' + } + }) + + + search.addWidgets([configure,searchBox,hits,stats,pagination]) // add the widgets to the instantsearch instance - search.addWidgets([ - instantsearch.widgets.pagination({ - container: '#algolia-pagination', - totalPages: 5, - templates: { - first: '', - last: '', - previous: '', - next: '' - } - }) - ]) search.start() + searchClickFn() + searchClickFnOnce() + + window.addEventListener('pjax:complete', () => { + getComputedStyle(document.querySelector('#algolia-search .search-dialog')).display === 'block' && closeSearch() + searchClickFn() + }) + window.pjax && search.on('render', () => { window.pjax.refresh(document.getElementById('algolia-hits')) }) diff --git a/source/js/search/local-search.js b/source/js/search/local-search.js index 2d207d5..6c1ca7d 100644 --- a/source/js/search/local-search.js +++ b/source/js/search/local-search.js @@ -1,14 +1,17 @@ window.addEventListener('load', () => { let loadFlag = false + let dataObj = [] + const $searchMask = document.getElementById('search-mask') + 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($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) if (!loadFlag) { - search(GLOBAL_CONFIG.localSearch.path) + search() loadFlag = true } // shortcut: ESC @@ -25,35 +28,35 @@ window.addEventListener('load', () => { bodyStyle.width = '' bodyStyle.overflow = '' btf.animateOut(document.querySelector('#local-search .search-dialog'), 'search_close .5s') - btf.animateOut(document.getElementById('search-mask'), 'to_hide 0.5s') + btf.animateOut($searchMask, 'to_hide 0.5s') } - // click function const searchClickFn = () => { document.querySelector('#search-button > .search').addEventListener('click', openSearch) - document.getElementById('search-mask').addEventListener('click', closeSearch) - document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch) } - searchClickFn() + const searchClickFnOnce = () => { + 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) + } - // pjax - window.addEventListener('pjax:complete', function () { - getComputedStyle(document.querySelector('#local-search .search-dialog')).display === 'block' && closeSearch() - searchClickFn() - }) + // check url is json or not + const isJson = url => { + const reg = /\.json$/ + return reg.test(url) + } - async function search (path) { - let datas = [] - const typeF = path.split('.')[1] - const response = await fetch(GLOBAL_CONFIG.root + path) - if (typeF === 'json') { - datas = await response.json() - } else if (typeF === 'xml') { + 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 - datas = [...a.querySelectorAll('entry')].map(function (item) { + data = [...a.querySelectorAll('entry')].map(item =>{ return { title: item.querySelector('title').textContent, content: item.querySelector('content') && item.querySelector('content').textContent, @@ -66,10 +69,18 @@ window.addEventListener('load', () => { $loadDataItem.nextElementSibling.style.display = 'block' $loadDataItem.remove() } + return data + } + + 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 = '' @@ -79,82 +90,99 @@ window.addEventListener('load', () => { if (keywords.length <= 0) return let count = 0 // perform local searching - datas.forEach(function (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 artiles with not empty titles and contents - if (dataTitle !== '' || dataContent !== '') { - keywords.forEach(function (keyword, i) { - indexTitle = dataTitle.indexOf(keyword) - indexContent = dataContent.indexOf(keyword) - if (indexTitle < 0 && indexContent < 0) { - isMatch = false - } else { - if (indexContent < 0) { - indexContent = 0 + 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 + } } - 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 - - if (start < 0) { - start = 0 - } - - if (start === 0) { - end = 100 - } - - if (end > dataContent.length) { - end = dataContent.length - } - - let matchContent = dataContent.substring(start, end) - - // highlight all keywords - keywords.forEach(function (keyword) { - const regS = new RegExp(keyword, 'gi') - matchContent = matchContent.replace(regS, '' + keyword + '') - dataTitle = dataTitle.replace(regS, '' + keyword + '') }) - - str += '
' + dataTitle + '' - count += 1 - - if (dataContent !== '') { - str += '

' + matchContent + '...

' - } + } else { + isMatch = false } - str += '
' + + // 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 => { + const regS = new RegExp(keyword, 'gi') + matchContent = matchContent.replace(regS, '' + keyword + '') + dataTitle = dataTitle.replace(regS, '' + keyword + '') + }) + + str += '
' + dataTitle + '' + count += 1 + + if (dataContent !== '') { + str += '

' + pre + matchContent + post + '

' + } + } + 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) }) - 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) }) } + + searchClickFn() + searchClickFnOnce() + + // pjax + window.addEventListener('pjax:complete', () => { + !btf.isHidden($searchMask) && closeSearch() + searchClickFn() + }) })