feat(search): improve insight and add algolia

pull/645/head
ppoffice 2020-03-09 02:45:43 -04:00
parent c84710bc3d
commit e49b77d7f2
13 changed files with 514 additions and 210 deletions

View File

@ -41,7 +41,8 @@
"class",
"onclick",
"onload",
"onsubmit"
"onsubmit",
"crossorigin"
]
}
],

View File

@ -12,6 +12,9 @@
},
{
"$ref": "/search/google_cse.json"
},
{
"$ref": "/search/algolia.json"
}
]
}

View File

@ -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"
]
}

View File

@ -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)'

View File

@ -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: '(无内容预览)'

69
layout/search/algolia.jsx Normal file
View File

@ -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:;">&times;</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;

View File

@ -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:;">&times;</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')
};
});

View File

@ -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

View File

@ -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;
}

196
source/css/search.styl Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
@import "base"
@import "helper"
@import "search"
@import "plugin/back-to-top"
@import "plugin/progressbar"

86
source/js/algolia.js Normal file
View File

@ -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');
});
}

View File

@ -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));
}