feat(search): improve insight and add algolia
parent
c84710bc3d
commit
e49b77d7f2
|
@ -41,7 +41,8 @@
|
|||
"class",
|
||||
"onclick",
|
||||
"onload",
|
||||
"onsubmit"
|
||||
"onsubmit",
|
||||
"crossorigin"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
},
|
||||
{
|
||||
"$ref": "/search/google_cse.json"
|
||||
},
|
||||
{
|
||||
"$ref": "/search/algolia.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "/search/algolia.json",
|
||||
"description": "Enable Algolia search\nhttps://www.algolia.com",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "algolia"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
|
@ -8,6 +8,9 @@ common:
|
|||
tag:
|
||||
one: 'Tag'
|
||||
other: 'Tags'
|
||||
page:
|
||||
one: 'Page'
|
||||
other: 'Pages'
|
||||
post:
|
||||
one: 'Post'
|
||||
other: 'Posts'
|
||||
|
@ -42,10 +45,6 @@ plugin:
|
|||
search:
|
||||
search: 'Search'
|
||||
hint: 'Type something...'
|
||||
insight:
|
||||
hint: 'Type something...'
|
||||
posts: 'Posts'
|
||||
pages: 'Pages'
|
||||
categories: 'Categories'
|
||||
tags: 'Tags'
|
||||
no_result: 'No results for'
|
||||
untitled: '(Untitled)'
|
||||
empty_preview: '(No preview)'
|
||||
|
|
|
@ -11,6 +11,9 @@ common:
|
|||
post:
|
||||
one: '文章'
|
||||
other: '文章'
|
||||
page:
|
||||
one: '页面'
|
||||
other: '页面'
|
||||
prev: '上一页'
|
||||
next: '下一页'
|
||||
widget:
|
||||
|
@ -32,6 +35,8 @@ donate:
|
|||
title: '喜欢这篇文章?打赏一下作者吧'
|
||||
alipay: '支付宝'
|
||||
wechat: '微信'
|
||||
paypal: 'Paypal'
|
||||
patreon: 'Patreon'
|
||||
buymeacoffee: '送我杯咖啡'
|
||||
plugin:
|
||||
backtotop: '回到顶端'
|
||||
|
@ -40,10 +45,6 @@ plugin:
|
|||
search:
|
||||
search: '搜索'
|
||||
hint: '想要查找什么...'
|
||||
insight:
|
||||
hint: '想要查找什么...'
|
||||
posts: '文章'
|
||||
pages: '页面'
|
||||
categories: '分类'
|
||||
tags: '标签'
|
||||
no_result: '未找到搜索结果'
|
||||
untitled: '(无标题)'
|
||||
empty_preview: '(无内容预览)'
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
const { Component, Fragment } = require('inferno');
|
||||
const { cacheComponent } = require('hexo-component-inferno/lib/util/cache');
|
||||
|
||||
class Algolia extends Component {
|
||||
render() {
|
||||
const {
|
||||
translation,
|
||||
applicationId,
|
||||
apiKey,
|
||||
indexName,
|
||||
jsUrl,
|
||||
algoliaSearchUrl,
|
||||
instantSearchUrl
|
||||
} = this.props;
|
||||
|
||||
if (!applicationId || !apiKey || !indexName) {
|
||||
return <div class="notification is-danger">
|
||||
It seems that you forget to set the <code>applicationId</code>, <code>apiKey</code>,
|
||||
or <code>indexName</code> for the Aloglia.
|
||||
Please set it in <code>_config.yml</code>.
|
||||
</div>;
|
||||
}
|
||||
|
||||
const js = `document.addEventListener('DOMContentLoaded', function () {
|
||||
loadAlgolia(${JSON.stringify({ applicationId, apiKey, indexName })}, ${JSON.stringify(translation)});
|
||||
});`;
|
||||
|
||||
return <Fragment>
|
||||
<div class="searchbox">
|
||||
<div class="searchbox-container">
|
||||
<div class="searchbox-header">
|
||||
<div class="searchbox-input-container" id="algolia-input">
|
||||
</div>
|
||||
<div class="is-flex" id="algolia-poweredby" style="align-items:center;line-height:0"></div>
|
||||
<a class="searchbox-close" href="javascript:;">×</a>
|
||||
</div>
|
||||
<div class="searchbox-body"></div>
|
||||
<div class="searchbox-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src={algoliaSearchUrl} crossorigin="anonymous" defer={true}></script>
|
||||
<script src={instantSearchUrl} crossorigin="anonymous" defer={true}></script>
|
||||
<script src={jsUrl} defer={true}></script>
|
||||
<script dangerouslySetInnerHTML={{ __html: js }}></script>
|
||||
</Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
Algolia.Cacheable = cacheComponent(Algolia, 'search.algolia', props => {
|
||||
const { config, helper } = props;
|
||||
const { algolia } = config;
|
||||
|
||||
return {
|
||||
translation: {
|
||||
hint: helper.__('search.hint'),
|
||||
no_result: helper.__('search.no_result'),
|
||||
untitled: helper.__('search.untitled'),
|
||||
empty_preview: helper.__('search.empty_preview')
|
||||
},
|
||||
applicationId: algolia ? algolia.applicationID : null,
|
||||
apiKey: algolia ? algolia.apiKey : null,
|
||||
indexName: algolia ? algolia.indexName : null,
|
||||
algoliaSearchUrl: helper.cdn('algoliasearch', '4.0.3', 'dist/algoliasearch-lite.umd.js'),
|
||||
instantSearchUrl: helper.cdn('instantsearch.js', '4.3.1', 'dist/instantsearch.production.min.js'),
|
||||
jsUrl: helper.url_for('/js/algolia.js')
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = Algolia;
|
|
@ -3,37 +3,26 @@ const { cacheComponent } = require('hexo-component-inferno/lib/util/cache');
|
|||
|
||||
class Insight extends Component {
|
||||
render() {
|
||||
const { hint, translation, contentUrl, jsUrl, cssUrl } = this.props;
|
||||
const { translation, contentUrl, jsUrl } = this.props;
|
||||
|
||||
const js = `(function (window) {
|
||||
var INSIGHT_CONFIG = {
|
||||
TRANSLATION: {
|
||||
POSTS: '${translation.posts}',
|
||||
PAGES: '${translation.pages}',
|
||||
CATEGORIES: '${translation.categories}',
|
||||
TAGS: '${translation.tags}',
|
||||
UNTITLED: '${translation.untitled}',
|
||||
},
|
||||
CONTENT_URL: '${contentUrl}',
|
||||
};
|
||||
window.INSIGHT_CONFIG = INSIGHT_CONFIG;
|
||||
})(window);`;
|
||||
const js = `document.addEventListener('DOMContentLoaded', function () {
|
||||
loadInsight(${JSON.stringify({ contentUrl })}, ${JSON.stringify(translation)});
|
||||
});`;
|
||||
|
||||
return <Fragment>
|
||||
<link rel="stylesheet" href={cssUrl} />
|
||||
<div class="searchbox ins-search">
|
||||
<div class="searchbox-container ins-search-container">
|
||||
<div class="searchbox-input-wrapper">
|
||||
<input type="text" class="searchbox-input ins-search-input" placeholder={hint} />
|
||||
<span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
|
||||
</div>
|
||||
<div class="searchbox-result-wrapper ins-section-wrapper">
|
||||
<div class="ins-section-container"></div>
|
||||
<div class="searchbox">
|
||||
<div class="searchbox-container">
|
||||
<div class="searchbox-header">
|
||||
<div class="searchbox-input-container">
|
||||
<input type="text" class="searchbox-input" placeholder={translation.hint}/>
|
||||
</div>
|
||||
<a class="searchbox-close" href="javascript:;">×</a>
|
||||
</div>
|
||||
<div class="searchbox-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script dangerouslySetInnerHTML={{ __html: js }}></script>
|
||||
<script src={jsUrl} defer={true}></script>
|
||||
<script dangerouslySetInnerHTML={{ __html: js }}></script>
|
||||
</Fragment>;
|
||||
}
|
||||
}
|
||||
|
@ -42,17 +31,16 @@ Insight.Cacheable = cacheComponent(Insight, 'search.insight', props => {
|
|||
const { helper } = props;
|
||||
|
||||
return {
|
||||
hint: helper.__('search.hint'),
|
||||
translation: {
|
||||
posts: helper.__('insight.posts'),
|
||||
pages: helper.__('insight.pages'),
|
||||
categories: helper.__('insight.categories'),
|
||||
tags: helper.__('insight.tags'),
|
||||
untitled: helper.__('insight.untitled')
|
||||
hint: helper.__('search.hint'),
|
||||
untitled: helper.__('search.untitled'),
|
||||
posts: helper._p('common.post', Infinity),
|
||||
pages: helper._p('common.page', Infinity),
|
||||
categories: helper._p('common.category', Infinity),
|
||||
tags: helper._p('common.tag', Infinity)
|
||||
},
|
||||
contentUrl: helper.url_for('/content.json'),
|
||||
jsUrl: helper.url_for('/js/insight.js'),
|
||||
cssUrl: helper.url_for('/css/insight.css')
|
||||
jsUrl: helper.url_for('/js/insight.js')
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -350,75 +350,6 @@ footer.footer
|
|||
.level-item
|
||||
margin-bottom: 0
|
||||
|
||||
.searchbox
|
||||
display: none
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
z-index: 100
|
||||
background: rgba(0, 0, 0, 0.7)
|
||||
&.show
|
||||
display: block
|
||||
&,
|
||||
.searchbox-container
|
||||
position: fixed
|
||||
.searchbox-container
|
||||
overflow: hidden
|
||||
.searchbox-selectable
|
||||
cursor: pointer
|
||||
.searchbox-input-wrapper
|
||||
position: relative
|
||||
.searchbox-input
|
||||
width: 100%
|
||||
border: none
|
||||
outline: none
|
||||
font-size: 16px
|
||||
box-shadow: none
|
||||
font-weight: 200
|
||||
border-radius: 0
|
||||
background: #fff
|
||||
line-height: 20px
|
||||
box-sizing: border-box
|
||||
padding: 12px 28px 12px 20px
|
||||
border-bottom: 1px solid #e2e2e2
|
||||
.searchbox-close
|
||||
top: 50%
|
||||
right: 6px
|
||||
width: 20px
|
||||
height: 20px
|
||||
line-height: 20px
|
||||
font-size: 16px
|
||||
margin-top: -11px
|
||||
position: absolute
|
||||
text-align: center
|
||||
display: inline-block
|
||||
&:hover
|
||||
color: $primary
|
||||
.searchbox-result-wrapper
|
||||
left: 0
|
||||
right: 0
|
||||
top: 45px
|
||||
bottom: 0
|
||||
overflow-y: auto
|
||||
position: absolute
|
||||
.searchbox-container
|
||||
left: 50%
|
||||
top: 100px
|
||||
width: 540px
|
||||
z-index: 101
|
||||
bottom: 100px
|
||||
margin-left: -270px
|
||||
box-sizing: border-box
|
||||
@media screen and (max-width: 559px), screen and (max-height: 479px)
|
||||
.searchbox .searchbox-container
|
||||
top: 0
|
||||
left: 0
|
||||
margin: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: #f7f7f7
|
||||
|
||||
.timeline
|
||||
margin-left: 1rem
|
||||
padding-left: 1.5rem
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
.ins-section-container {
|
||||
position: relative;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.ins-section {
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.ins-section .ins-section-header, .ins-section .ins-search-item {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.ins-section .ins-section-header {
|
||||
color: #9a9a9a;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
}
|
||||
|
||||
.ins-section .ins-slug {
|
||||
margin-left: 5px;
|
||||
color: #9a9a9a;
|
||||
}
|
||||
|
||||
.ins-section .ins-slug:before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
.ins-section .ins-slug:after {
|
||||
content: ')';
|
||||
}
|
||||
|
||||
.ins-section .ins-search-item header, .ins-section .ins-search-item .ins-search-preview {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ins-section .ins-search-item header .ins-title {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ins-section .ins-search-item .ins-search-preview {
|
||||
height: 15px;
|
||||
font-size: 12px;
|
||||
color: #9a9a9a;
|
||||
margin: 5px 0 0 20px;
|
||||
}
|
||||
|
||||
.ins-section .ins-search-item:hover, .ins-section .ins-search-item.active {
|
||||
color: #fff;
|
||||
background: #3273dc;
|
||||
}
|
||||
|
||||
.ins-section .ins-search-item:hover .ins-slug, .ins-section .ins-search-item.active .ins-slug, .ins-section .ins-search-item:hover .ins-search-preview, .ins-section .ins-search-item.active .ins-search-preview {
|
||||
color: #fff;
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
$box-shadow = 0 4px 10px rgba(0, 0, 0, .05), 0 0 1px rgba(0, 0, 0, .1)
|
||||
$border-radius = 4px
|
||||
$bg-shadow = rgba(0, 0, 0, .7)
|
||||
$bg-container = #f7f7f7
|
||||
$bg-primary = rgb(39, 108, 218)
|
||||
$fg-primary = #fff
|
||||
$fg-input = #333
|
||||
$bg-input = #fff
|
||||
$bg-close-hover = $bg-container
|
||||
$bg-close-active = #eee
|
||||
$fg-result-header = #aaa
|
||||
$fg-result-item-secondary = #aaa
|
||||
$bg-result-item-hover = #fff
|
||||
$fg-result-item-active = $fg-primary
|
||||
$bg-result-item-active = $bg-primary
|
||||
$bg-result-item-highlight = $yellow
|
||||
$fg-pagination-item = #333
|
||||
$bg-pagination-item = #fff
|
||||
$bg-pagination-item-hover = $bg-container
|
||||
$fg-pagination-item-active = $fg-primary
|
||||
$bg-pagination-item-active = $bg-primary
|
||||
$bg-pagination-item-disabled = $bg-container
|
||||
$fg-border = #e2e2e2
|
||||
$container-width = 540px
|
||||
$container-margin = 100px
|
||||
|
||||
.searchbox
|
||||
display: none
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
z-index: 100
|
||||
font-size: 1rem
|
||||
line-height: 0
|
||||
background: $bg-shadow
|
||||
|
||||
&.show
|
||||
display: flex
|
||||
|
||||
a, a:hover
|
||||
color: inherit
|
||||
text-decoration: none
|
||||
|
||||
input
|
||||
font-size: 1rem
|
||||
border: none
|
||||
outline: none
|
||||
box-shadow: none
|
||||
border-radius: 0
|
||||
|
||||
&, .searchbox-container
|
||||
position: fixed
|
||||
align-items: center
|
||||
flex-direction: column
|
||||
line-height: 1.25em
|
||||
|
||||
.searchbox-container
|
||||
z-index: 101
|
||||
display: flex
|
||||
overflow: hidden
|
||||
box-shadow: $box-shadow
|
||||
border-radius: $border-radius
|
||||
background-color: $bg-container
|
||||
width: $container-width
|
||||
top: $container-margin
|
||||
bottom: $container-margin
|
||||
|
||||
.searchbox-header, .searchbox-body, .searchbox-footer
|
||||
width: 100%
|
||||
|
||||
.searchbox-header
|
||||
display: flex
|
||||
flex-direction: row
|
||||
line-height: 1.5em
|
||||
font-weight: normal
|
||||
background-color: $bg-input
|
||||
|
||||
.searchbox-input-container
|
||||
display: flex
|
||||
flex-grow: 1
|
||||
|
||||
.searchbox-input
|
||||
flex-grow: 1
|
||||
color: $fg-input
|
||||
box-sizing: border-box
|
||||
padding: .75em 0 .75em 1.25em
|
||||
|
||||
.searchbox-close
|
||||
display: inline-block
|
||||
color: $fg-input
|
||||
font-size: 1.5em
|
||||
padding: .5em .75em
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
background: $bg-close-hover
|
||||
|
||||
&:active
|
||||
background: $bg-close-active
|
||||
|
||||
.searchbox-body
|
||||
flex-grow: 1
|
||||
overflow-y: auto
|
||||
border-top: 1px solid $fg-border
|
||||
|
||||
.searchbox-result-section header, .searchbox-result-item
|
||||
padding: .75em 1em
|
||||
|
||||
.searchbox-result-section
|
||||
border-bottom: 1px solid $fg-border
|
||||
|
||||
header
|
||||
color: $fg-result-header
|
||||
|
||||
.searchbox-result-item
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
&:not(.disabled):not(.active):not(:active):hover
|
||||
background-color: $bg-result-item-hover
|
||||
|
||||
&:active, &.active
|
||||
color: $fg-result-item-active
|
||||
background-color: $bg-result-item-active
|
||||
|
||||
em
|
||||
font-style: normal
|
||||
background: $bg-result-item-highlight
|
||||
|
||||
.searchbox-result-icon
|
||||
margin-right: 1em
|
||||
|
||||
.searchbox-result-content
|
||||
overflow: hidden
|
||||
|
||||
.searchbox-result-title, .searchbox-result-preview
|
||||
display: block
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
|
||||
.searchbox-result-title-secondary
|
||||
color: $fg-result-item-secondary
|
||||
|
||||
.searchbox-result-preview
|
||||
margin-top: .25em
|
||||
|
||||
.searchbox-result-item:not(:active):not(.active)
|
||||
.searchbox-result-preview
|
||||
color: $fg-result-item-secondary
|
||||
|
||||
.searchbox-footer
|
||||
padding: .5em 1em
|
||||
|
||||
.searchbox-pagination
|
||||
margin: 0
|
||||
padding: 0
|
||||
list-style: none
|
||||
text-align: center
|
||||
|
||||
.searchbox-pagination-item
|
||||
margin: 0 .25rem
|
||||
|
||||
.searchbox-pagination-item, .searchbox-pagination-link
|
||||
display: inline-block
|
||||
|
||||
.searchbox-pagination-link
|
||||
overflow: hidden
|
||||
padding: .5em .8em
|
||||
color: $fg-pagination-item
|
||||
box-shadow: $box-shadow
|
||||
border-radius: $border-radius
|
||||
background-color: $bg-pagination-item
|
||||
|
||||
.searchbox-pagination-item.active
|
||||
.searchbox-pagination-link
|
||||
color: $fg-pagination-item-active
|
||||
background-color: $bg-pagination-item-active
|
||||
|
||||
.searchbox-pagination-item.disabled
|
||||
.searchbox-pagination-link
|
||||
cursor: not-allowed
|
||||
background-color: $bg-pagination-item-disabled
|
||||
|
||||
.searchbox-pagination-item:not(.active):not(.disabled)
|
||||
.searchbox-pagination-link:hover
|
||||
background-color: $bg-pagination-item-hover
|
||||
|
||||
@media screen and (max-width: 559px), screen and (max-height: 479px)
|
||||
.searchbox .searchbox-container
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
border-radius: 0
|
|
@ -1,5 +1,6 @@
|
|||
@import "base"
|
||||
@import "helper"
|
||||
@import "search"
|
||||
|
||||
@import "plugin/back-to-top"
|
||||
@import "plugin/progressbar"
|
|
@ -0,0 +1,86 @@
|
|||
/* global instantsearch, algoliasearch */
|
||||
function loadAlgolia(config, translation) { // eslint-disable-line no-unused-vars
|
||||
const search = instantsearch({
|
||||
indexName: config.indexName,
|
||||
searchClient: algoliasearch(config.applicationId, config.apiKey)
|
||||
});
|
||||
|
||||
search.addWidgets([
|
||||
instantsearch.widgets.configure({
|
||||
attributesToSnippet: ['excerpt']
|
||||
})
|
||||
]);
|
||||
|
||||
search.addWidget(instantsearch.widgets.searchBox({
|
||||
container: '#algolia-input',
|
||||
placeholder: translation.hint,
|
||||
showReset: false,
|
||||
showSubmit: false,
|
||||
showLoadingIndicator: false,
|
||||
cssClasses: {
|
||||
root: 'searchbox-input-container',
|
||||
form: 'searchbox-input-container',
|
||||
input: 'searchbox-input'
|
||||
}
|
||||
}));
|
||||
|
||||
search.addWidget(instantsearch.widgets.poweredBy({
|
||||
container: '#algolia-poweredby'
|
||||
}));
|
||||
|
||||
search.addWidget(instantsearch.widgets.hits({
|
||||
container: '.searchbox-body',
|
||||
escapeHTML: false,
|
||||
cssClasses: {
|
||||
root: 'searchbox-result-container',
|
||||
emptyRoot: ['searchbox-result-item', 'disabled']
|
||||
},
|
||||
templates: {
|
||||
empty: function(results) {
|
||||
return translation.no_result + ': ' + results.query;
|
||||
},
|
||||
item: function(hit) {
|
||||
const title = instantsearch.highlight({ attribute: 'title', hit });
|
||||
let excerpt = instantsearch.highlight({ attribute: 'excerpt', hit });
|
||||
excerpt = excerpt.replace(new RegExp('<em>', 'ig'), '[algolia-highlight]')
|
||||
.replace(new RegExp('</em>', 'ig'), '[/algolia-highlight]')
|
||||
.replace(/(<([^>]+)>)/ig, '')
|
||||
.replace(/(\[algolia-highlight\])/ig, '<em>')
|
||||
.replace(/(\[\/algolia-highlight\])/ig, '</em>');
|
||||
return `<section class="searchbox-result-section">
|
||||
<a class="searchbox-result-item" href="${hit.permalink}">
|
||||
<span class="searchbox-result-content">
|
||||
<span class="searchbox-result-title">${title ? title : translation.untitled}</span>
|
||||
<span class="searchbox-result-preview">${excerpt ? excerpt : translation.empty_preview}</span>
|
||||
</span>
|
||||
</a>
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
search.addWidget(instantsearch.widgets.pagination({
|
||||
container: '.searchbox-footer',
|
||||
cssClasses: {
|
||||
list: 'searchbox-pagination',
|
||||
item: 'searchbox-pagination-item',
|
||||
link: 'searchbox-pagination-link',
|
||||
selectedItem: 'active',
|
||||
disabledItem: 'disabled'
|
||||
}
|
||||
}));
|
||||
|
||||
search.start();
|
||||
|
||||
if (location.hash.trim() === '#algolia-search') {
|
||||
$('.searchbox').addClass('show');
|
||||
}
|
||||
|
||||
$(document).on('click', '.navbar-main .search', () => {
|
||||
$('.searchbox').toggleClass('show');
|
||||
}).on('click', '.searchbox .searchbox-mask', () => {
|
||||
$('.searchbox').removeClass('show');
|
||||
}).on('click', '.searchbox-close', () => {
|
||||
$('.searchbox').removeClass('show');
|
||||
});
|
||||
}
|
|
@ -2,44 +2,114 @@
|
|||
* Insight search plugin
|
||||
* @author PPOffice { @link https://github.com/ppoffice }
|
||||
*/
|
||||
(function($, CONFIG) {
|
||||
const $main = $('.ins-search');
|
||||
const $input = $main.find('.ins-search-input');
|
||||
const $wrapper = $main.find('.ins-section-wrapper');
|
||||
const $container = $main.find('.ins-section-container');
|
||||
$main.parent().remove('.ins-search');
|
||||
$('body').append($main);
|
||||
function loadInsight(config, translation) { // eslint-disable-line no-unused-vars
|
||||
const $main = $('.searchbox');
|
||||
const $input = $main.find('.searchbox-input');
|
||||
const $container = $main.find('.searchbox-body');
|
||||
|
||||
function section(title) {
|
||||
return $('<section>').addClass('ins-section')
|
||||
.append($('<header>').addClass('ins-section-header').text(title));
|
||||
return $('<section>').addClass('searchbox-result-section').append($('<header>').text(title));
|
||||
}
|
||||
|
||||
function merge(ranges) {
|
||||
let last;
|
||||
const result = [];
|
||||
|
||||
ranges.forEach(r => {
|
||||
if (!last || r[0] > last[1]) {
|
||||
result.push(last = r);
|
||||
} else if (r[1] > last[1]) {
|
||||
last[1] = r[1];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function findAndHighlight(text, matches, maxlen) {
|
||||
if (!Array.isArray(matches) || !matches.length || !text) {
|
||||
return maxlen ? text.slice(0, maxlen) : text;
|
||||
}
|
||||
const testText = text.toLowerCase();
|
||||
const indices = matches.map(match => {
|
||||
const index = testText.indexOf(match.toLowerCase());
|
||||
if (!match || index === -1) {
|
||||
return null;
|
||||
}
|
||||
return [index, index + match.length];
|
||||
}).filter(match => {
|
||||
return match !== null;
|
||||
}).sort((a, b) => {
|
||||
return a[0] - b[0] || a[1] - b[1];
|
||||
});
|
||||
|
||||
if (!indices.length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let result = ''; let last = 0;
|
||||
const ranges = merge(indices);
|
||||
const sumRange = [ranges[0][0], ranges[ranges.length - 1][1]];
|
||||
if (maxlen && maxlen < sumRange[1]) {
|
||||
last = sumRange[0];
|
||||
}
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
result += text.slice(last, Math.min(range[0], sumRange[0] + maxlen));
|
||||
if (maxlen && range[0] >= sumRange[0] + maxlen) {
|
||||
break;
|
||||
}
|
||||
result += '<em>' + text.slice(range[0], range[1]) + '</em>';
|
||||
last = range[1];
|
||||
if (i === ranges.length - 1) {
|
||||
if (maxlen) {
|
||||
result += text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1));
|
||||
} else {
|
||||
result += text.slice(range[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function searchItem(icon, title, slug, preview, url) {
|
||||
return $('<div>').addClass('ins-selectable').addClass('ins-search-item')
|
||||
.append($('<header>').append($('<i>').addClass('fa').addClass('fa-' + icon))
|
||||
.append($('<span>').addClass('ins-title').text(title != null && title !== '' ? title : CONFIG.TRANSLATION.UNTITLED))
|
||||
.append(slug ? $('<span>').addClass('ins-slug').text(slug) : null))
|
||||
.append(preview ? $('<p>').addClass('ins-search-preview').text(preview) : null)
|
||||
.attr('data-url', url);
|
||||
title = title != null && title !== '' ? title : translation.untitled;
|
||||
|
||||
return `<a class="searchbox-result-item" href="${url}">
|
||||
<span class="searchbox-result-icon">
|
||||
<i class="fa fa-${icon}" />
|
||||
</span>
|
||||
<span class="searchbox-result-content">
|
||||
<span class="searchbox-result-title">
|
||||
${title}
|
||||
${slug ? '<span class="searchbox-result-title-secondary">(' + slug + ')</span>' : ''}
|
||||
</span>
|
||||
${preview ? '<span class="searchbox-result-preview">' + preview + '</span>' : ''}
|
||||
</span>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function sectionFactory(type, array) {
|
||||
function sectionFactory(keywords, type, array) {
|
||||
let $searchItems;
|
||||
if (array.length === 0) return null;
|
||||
const sectionTitle = CONFIG.TRANSLATION[type];
|
||||
const sectionTitle = translation[type.toLowerCase()];
|
||||
switch (type) {
|
||||
case 'POSTS':
|
||||
case 'PAGES':
|
||||
$searchItems = array.map(item => {
|
||||
// Use config.root instead of permalink to fix url issue
|
||||
return searchItem('file', item.title, null, item.text.slice(0, 150), item.link);
|
||||
const title = findAndHighlight(item.title, keywords);
|
||||
const text = findAndHighlight(item.text, keywords, 100);
|
||||
return searchItem('file', title, null, text, item.link);
|
||||
});
|
||||
break;
|
||||
case 'CATEGORIES':
|
||||
case 'TAGS':
|
||||
$searchItems = array.map(item => {
|
||||
return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', item.name, item.slug, null, item.link);
|
||||
const name = findAndHighlight(item.name, keywords);
|
||||
const slug = findAndHighlight(item.slug, keywords);
|
||||
return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', name, slug, null, item.link);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -52,7 +122,7 @@
|
|||
return keywords.split(' ').filter(keyword => {
|
||||
return !!keyword;
|
||||
}).map(keyword => {
|
||||
return keyword.toUpperCase();
|
||||
return keyword.toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -68,7 +138,7 @@
|
|||
if (!Object.prototype.hasOwnProperty.call(obj, field)) {
|
||||
return false;
|
||||
}
|
||||
if (obj[field].toUpperCase().indexOf(keyword) > -1) {
|
||||
if (obj[field].toLowerCase().indexOf(keyword) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -150,28 +220,29 @@
|
|||
};
|
||||
}
|
||||
|
||||
function searchResultToDOM(searchResult) {
|
||||
function searchResultToDOM(keywords, searchResult) {
|
||||
$container.empty();
|
||||
for (const key in searchResult) {
|
||||
$container.append(sectionFactory(key.toUpperCase(), searchResult[key]));
|
||||
$container.append(sectionFactory(parseKeywords(keywords),
|
||||
key.toUpperCase(), searchResult[key]));
|
||||
}
|
||||
}
|
||||
|
||||
function scrollTo($item) {
|
||||
if ($item.length === 0) return;
|
||||
const wrapperHeight = $wrapper[0].clientHeight;
|
||||
const itemTop = $item.position().top - $wrapper.scrollTop();
|
||||
const wrapperHeight = $container[0].clientHeight;
|
||||
const itemTop = $item.position().top - $container.scrollTop();
|
||||
const itemBottom = $item[0].clientHeight + $item.position().top;
|
||||
if (itemBottom > wrapperHeight + $wrapper.scrollTop()) {
|
||||
$wrapper.scrollTop(itemBottom - $wrapper[0].clientHeight);
|
||||
if (itemBottom > wrapperHeight + $container.scrollTop()) {
|
||||
$container.scrollTop(itemBottom - $container[0].clientHeight);
|
||||
}
|
||||
if (itemTop < 0) {
|
||||
$wrapper.scrollTop($item.position().top);
|
||||
$container.scrollTop($item.position().top);
|
||||
}
|
||||
}
|
||||
|
||||
function selectItemByDiff(value) {
|
||||
const $items = $.makeArray($container.find('.ins-selectable'));
|
||||
const $items = $.makeArray($container.find('.searchbox-result-item'));
|
||||
let prevPosition = -1;
|
||||
$items.forEach((item, index) => {
|
||||
if ($(item).hasClass('active')) {
|
||||
|
@ -187,17 +258,17 @@
|
|||
|
||||
function gotoLink($item) {
|
||||
if ($item && $item.length) {
|
||||
location.href = $item.attr('data-url');
|
||||
location.href = $item.attr('href');
|
||||
}
|
||||
}
|
||||
|
||||
$.getJSON(CONFIG.CONTENT_URL, json => {
|
||||
if (location.hash.trim() === '#ins-search') {
|
||||
$.getJSON(config.contentUrl, json => {
|
||||
if (location.hash.trim() === '#insight-search') {
|
||||
$main.addClass('show');
|
||||
}
|
||||
$input.on('input', function() {
|
||||
const keywords = $(this).val();
|
||||
searchResultToDOM(search(json, keywords));
|
||||
searchResultToDOM(keywords, search(json, keywords));
|
||||
});
|
||||
$input.trigger('input');
|
||||
});
|
||||
|
@ -205,14 +276,14 @@
|
|||
let touch = false;
|
||||
$(document).on('click focus', '.navbar-main .search', () => {
|
||||
$main.addClass('show');
|
||||
$main.find('.ins-search-input').focus();
|
||||
}).on('click touchend', '.ins-search-item', function(e) {
|
||||
$main.find('.searchbox-input').focus();
|
||||
}).on('click touchend', '.searchbox-result-item', function(e) {
|
||||
if (e.type !== 'click' && !touch) {
|
||||
return;
|
||||
}
|
||||
gotoLink($(this));
|
||||
touch = false;
|
||||
}).on('click touchend', '.ins-close', e => {
|
||||
}).on('click touchend', '.searchbox-close', e => {
|
||||
if (e.type !== 'click' && !touch) {
|
||||
return;
|
||||
}
|
||||
|
@ -232,11 +303,11 @@
|
|||
case 40: // DOWN
|
||||
selectItemByDiff(1); break;
|
||||
case 13: // ENTER
|
||||
gotoLink($container.find('.ins-selectable.active').eq(0)); break;
|
||||
gotoLink($container.find('.searchbox-result-item.active').eq(0)); break;
|
||||
}
|
||||
}).on('touchstart', e => {
|
||||
touch = true;
|
||||
}).on('touchmove', e => {
|
||||
touch = false;
|
||||
});
|
||||
}(jQuery, window.INSIGHT_CONFIG));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue