From 852f24946dae1c328208d992ecfa6ecb421be1a1 Mon Sep 17 00:00:00 2001 From: liuyib <1656081615@qq.com> Date: Tue, 6 Aug 2019 21:41:07 +0800 Subject: [PATCH] feat: Add local search whitout dependencies --- layout/_common/header.pug | 6 +- layout/_common/layout.pug | 4 +- layout/_components/search/index.pug | 4 + layout/_components/search/localsearch.pug | 9 + layout/_third-party/search/index.pug | 2 + layout/_third-party/search/localsearch.pug | 288 ++++++++++++++++++ source/css/_common/responsive.styl | 11 +- source/css/_components/search/algolia.styl | 55 ---- source/css/_components/search/common.styl | 61 ++++ source/css/_components/search/index.styl | 2 + .../css/_components/search/localsearch.styl | 37 +++ 11 files changed, 417 insertions(+), 62 deletions(-) create mode 100644 layout/_components/search/index.pug create mode 100644 layout/_components/search/localsearch.pug create mode 100644 layout/_third-party/search/localsearch.pug create mode 100644 source/css/_components/search/common.styl create mode 100644 source/css/_components/search/localsearch.styl diff --git a/layout/_common/header.pug b/layout/_common/header.pug index 76384a9..c9f51e9 100644 --- a/layout/_common/header.pug +++ b/layout/_common/header.pug @@ -9,13 +9,13 @@ header#header( div.header-nav-inner div.fa.fa-bars.header-nav-menu-icon - if theme.algolia_search && theme.algolia_search.enable + if theme.algolia_search.enable || theme.local_search.enable div.header-nav-search if !theme.menu_settings.text_only i.fa.fa-search if !theme.menu_settings.icon_only span!= _p("nav.search") - + div.header-nav-menu each value, name in (theme.menu || []) if name && value @@ -29,7 +29,7 @@ header#header( menuItemIcon : "question-circle-o"}`) if !theme.menu_settings.icon_only != _p(`nav.${name}`) - + div.header-info div.header-info-inner div.header-info-title= config.title diff --git a/layout/_common/layout.pug b/layout/_common/layout.pug index 72afc79..ad937da 100644 --- a/layout/_common/layout.pug +++ b/layout/_common/layout.pug @@ -11,8 +11,6 @@ html(lang=config.language) if theme.back2top.enable include ../_components/back2top.pug - if theme.algolia_search.enable - include ../_components/search/algolia.pug - + include ../_components/search/index.pug include ../_scripts/cdn.pug include ../_scripts/common.pug diff --git a/layout/_components/search/index.pug b/layout/_components/search/index.pug new file mode 100644 index 0000000..9bb74e5 --- /dev/null +++ b/layout/_components/search/index.pug @@ -0,0 +1,4 @@ +if theme.algolia_search.enable + include ./algolia.pug +else if theme.local_search.enable + include ./localsearch.pug diff --git a/layout/_components/search/localsearch.pug b/layout/_components/search/localsearch.pug new file mode 100644 index 0000000..b5d8970 --- /dev/null +++ b/layout/_components/search/localsearch.pug @@ -0,0 +1,9 @@ +div.localsearch-mask + +div.localsearch-popup + div.localsearch-input-wrapper + input(placeholder= _p("local_search.input_placeholder")) + span.localsearch-close + i.fa.fa-times + + div.localsearch-result diff --git a/layout/_third-party/search/index.pug b/layout/_third-party/search/index.pug index 7c768dd..9bb74e5 100644 --- a/layout/_third-party/search/index.pug +++ b/layout/_third-party/search/index.pug @@ -1,2 +1,4 @@ if theme.algolia_search.enable include ./algolia.pug +else if theme.local_search.enable + include ./localsearch.pug diff --git a/layout/_third-party/search/localsearch.pug b/layout/_third-party/search/localsearch.pug new file mode 100644 index 0000000..0adb303 --- /dev/null +++ b/layout/_third-party/search/localsearch.pug @@ -0,0 +1,288 @@ +if theme.local_search.enable + script. + window.onload = function () { + $('.header-nav-search').on('click', function (e) { + e.stopPropagation(); + + $('body').css('overflow', 'hidden'); + $('.localsearch-popup') + .velocity('stop') + .velocity('transition.expandIn', { + duration: 300, + complete: function () { + $('.localsearch-popup input').focus(); + } + }); + $('.localsearch-mask') + .velocity('stop') + .velocity('transition.fadeIn', { + duration: 300 + }); + + initSearch(); + }); + + $('.localsearch-mask, .localsearch-close').on('click', function () { + closeSearch(); + }); + + $(document).on('keydown', function (e) { + // Escape <=> 27 + if (e.keyCode === Stun.utils.codeToKeyCode('Escape')) { + closeSearch(); + } + }); + + var isXML = true; + var search_path = '!{ config.search.path }'; + + if (!search_path) { + search_path = 'search.xml'; + } else if (/json$/i.test(search_path)) { + isXML = false; + } + + var path = '!{ config.root }' + search_path; + + function initSearch() { + $.ajax({ + url: path, + dataType: isXML ? 'xml' : 'json', + async: true, + success: function (res) { + var datas = isXML ? $('entry', res).map(function () { + // 将 XML 转为 JSON + return { + title: $('title', this).text(), + content: $('content', this).text(), + url: $('url', this).text(), + categories: $('category', this).text().trim().split(/[\s]+/), + tags: $('tag', this).text().trim().split(/[\s]+/) + }; + }).get() : res; + + var $input = $('.localsearch-input-wrapper input'); + var $result = $('.localsearch-result'); + + // 搜索对象(标题,标签,分类,内容)的权值,影响显示顺序 + var WEIGHT = { title: 100, tag: 10, category: 10, content: 1 }; + + var searchPost = function () { + var searchText = $input.val().toLowerCase().trim(); + // 根据空白字符分隔关键字 + var keywords = searchText.split(/[\s]+/); + // 将关键字进行排列组合,使得搜索到结果的可能性更大 + var groupKeywords = sortGroup(keywords); + // 搜索结果 + var matchPosts = []; + + // 防止未输入字符时搜索 + if (searchText.length > 0) { + datas.forEach(function (data) { + var isMatch = false; + + // 没有标题的文章使用预设的 i18n 变量代替 + var title = (data.title && data.title.trim()) || '!{_p("post.untitled")}';; + var titleLower = title && title.toLowerCase(); + // 删除 HTML 标签 和 所有空白字符 + var content = data.content && data.content.replace(/<[^>]+>|\s+/, ''); + var contentLower = content && content.toLowerCase(); + // 删除重复的 / + var postURL = data.url && decodeURI(data.url).replace(/\/{2,}/, '/'); + var tags = data.tags; + var categories = data.categories; + + // 标题中匹配到的关键词 + var titleHitSlice = []; + // 内容中匹配到的关键词 + var contentHitSlice = []; + + groupKeywords.forEach(function (keyword) { + /** + * 获取匹配文字的索引 + * @param {String} keyword 要匹配的关键字 + * @param {String} text 原文字 + * @param {Boolean} caseSensitive 是否区分大小写 + * @param {Number} weight 权重 + * @return {Array} + */ + function getIndexByword (word, text, caseSensitive, weight) { + if (!word || !text) return []; + + var startIndex = 0; // 开始匹配的索引 + var index = -1; // 匹配到的索引值 + var result = []; // 匹配结果 + + if (!caseSensitive) { + word = word.toLowerCase(); + text = text.toLowerCase(); + } + + while((index = text.indexOf(word, startIndex)) !== -1) { + result.push({ index: index, word: word, weight: weight }); + startIndex = index + word.length; + } + + return result; + } + + titleHitSlice = titleHitSlice.concat(getIndexByword(keyword, titleLower, false, WEIGHT.title)); + contentHitSlice = contentHitSlice.concat(getIndexByword(keyword, contentLower, false, WEIGHT.content)); + }); + + var hitTitle = titleHitSlice.length; + var hitContent = contentHitSlice.length; + + if (hitTitle > 0 || hitContent > 0) { + isMatch = true; + } + + if (isMatch) { + /** + * 给匹配的文本添加标记标签 + * @param {String} text 原文本 + * @param {Array} hitSlice 匹配项的索引信息 + * @return {String} + */ + function highlightKeyword (text, hitSlice) { + if (!text || !hitSlice || !hitSlice.length) return; + + var startIndex = 0; + var result = ''; + + hitSlice.forEach(function (hit) { + // 匹配到较长的字符串后,防止再匹配较短的字符串 + if (hit.index < startIndex) return; + + var hitWordEnd = hit.index + hit.word.length; + + result += text.slice(startIndex, hit.index); + result += '' + text.slice(hit.index, hitWordEnd) + ''; + startIndex = hitWordEnd; + }); + result += text.slice(startIndex); + + return result; + } + + var postData = {}; + // 计算文章总的搜索权重 + var postWeight = titleHitSlice.length * 100 + contentHitSlice.length; + var postTitle = highlightKeyword(title, titleHitSlice) || title; + var postContent = highlightKeyword(content, contentHitSlice) || content; + + // 截取匹配的第一个字符,前后共 200 个字符来显示 + if (contentHitSlice.length > 0) { + var firstIndex = contentHitSlice[0].index; + var start = firstIndex > 20 ? firstIndex - 20 : 0; + var end = firstIndex + 180; + + postContent = postContent.slice(start, end); + } else { // 未匹配到内容,直接截取前 200 个字符来显示 + postContent = postContent.slice(0, 200); + } + + postData.title = postTitle; + postData.content = postContent; + postData.url = postURL; + postData.weight = postWeight; + matchPosts.push(postData); + } + }); + } + + var resultInnerHtml = ''; + $result.html(resultInnerHtml); + }; + + $input.on('input', searchPost); + $input.on('keypress', function (e) { + if (e.keyCode === Stun.utils.codeToKeyCode('Enter')) { + searchPost(); + } + }); + + /** + * 将关键字进行排列组合 + * @param {Array} keywords 关键词数组 + * @return {Array} + */ + function sortGroup (keywords) { + if (!Array.isArray(keywords)) return; + if (keywords.length <= 1) return keywords; + + // 对数组中的元素进行组合 + function groupSplit (arr, size) { + var r = []; // result + + function group(t, a, n) { // tempArr, arr, num + if (n === 0) { + r[r.length] = t; + return; + } + for (var i = 0, l = a.length - n; i <= l; i++) { + var b = t.slice(); + b.push(a[i]); + group(b, a.slice(i + 1), n - 1); + } + } + group([], arr, size); + + return r; + } + + // 组合结果 + var result = []; + + for (var i = keywords.length; i > 0; i--) { + var groupKeyword = groupSplit(keywords, i); + + if (groupKeyword.length !== 0) { + groupKeyword.forEach(function (item) { + // 将排列组合后的项,连接成新的关键词 + if (item.length > 1) { + result.push(item.join('')); + result.push(item.join(' ')); + result.push(item.slice(0).reverse().join('')); + result.push(item.slice(0).reverse().join(' ')); + } else { + result.push(item.join('')); + } + }); + } + } + + return result; + } + } + }); + } + + function closeSearch () { + $('body').css('overflow', 'auto'); + $('.localsearch-popup') + .velocity('stop') + .velocity('transition.expandOut', { + duration: 300 + }); + $('.localsearch-mask') + .velocity('stop') + .velocity('transition.fadeOut', { + duration: 300 + }); + } + }; diff --git a/source/css/_common/responsive.styl b/source/css/_common/responsive.styl index afcfcd5..1d83759 100644 --- a/source/css/_common/responsive.styl +++ b/source/css/_common/responsive.styl @@ -78,7 +78,8 @@ .header-info-subtitle font-size: 1rem - div.algolia-popup + div.algolia-popup, + div.localsearch-popup left: 0 margin: 0 1rem width: calc(100% - 2rem) @@ -153,3 +154,11 @@ .gallery-image width: 100% + + div.algolia-popup, + div.localsearch-popup + padding: .8rem .6rem 1rem + + div.localsearch-result + & > ul + padding-left: 1rem diff --git a/source/css/_components/search/algolia.styl b/source/css/_components/search/algolia.styl index 0aaaa8d..83e3f74 100644 --- a/source/css/_components/search/algolia.styl +++ b/source/css/_components/search/algolia.styl @@ -1,62 +1,7 @@ -.algolia-mask - display: none - position: fixed - top: 0 - left: 0 - z-index: $z-index2 - width: 100% - height: 100% - background-color: alpha($black-dark, .7) - -.algolia-popup - display: none - position: fixed - top: 10% - left: 50% - z-index: $z-index2 - margin-left: -350px - border-radius: 5px - padding: 1rem - width: 700px - max-height: 80% - font-size: $font-size-large - color: $font-color - background-color: $white-light - .algolia-input-wrapper - margin: 1rem 0 - .ais-search-box max-width: 100% - input - border: 2px solid alpha($blue-light, .5) - border-radius: 2rem - padding: .4rem .8rem - outline: 0 - transition: border-color .3s - - &:hover, - &:focus - border-color: $blue-light - -.algolia-close - position: absolute - top: 0 - right: .6rem - font-size: 1.25em - color: $black-light - cursor: pointer - -.algolia-results - overflow-x: hidden - overflow-y: scroll - max-height: 16rem - - em - font-style: normal - background-color: $yellow-light - .algolia-hit-item &::before content: '' diff --git a/source/css/_components/search/common.styl b/source/css/_components/search/common.styl new file mode 100644 index 0000000..41689e3 --- /dev/null +++ b/source/css/_components/search/common.styl @@ -0,0 +1,61 @@ +.algolia-mask, +.localsearch-mask + display: none + position: fixed + top: 0 + left: 0 + z-index: $z-index2 + width: 100% + height: 100% + background-color: alpha($black-dark, .7) + +.algolia-popup, +.localsearch-popup + display: none + position: fixed + top: 10% + left: 50% + z-index: $z-index2 + margin-left: -350px + border-radius: 5px + padding: 1rem + width: 700px + max-height: 80% + font-size: $font-size-large + color: $font-color + background-color: $white-light + +.algolia-input-wrapper, +.localsearch-input-wrapper + margin: 1.2rem 0 1rem + + input + border: 2px solid alpha($blue-light, .5) + border-radius: 2rem + padding: .2rem .8rem + line-height: 1.3rem + outline: 0 + transition: border-color .3s + + &:hover, + &:focus + border-color: $blue-light + +.algolia-close, +.localsearch-close + position: absolute + top: 0 + right: .6rem + font-size: 1.25em + color: $black-light + cursor: pointer + +.algolia-results, +.localsearch-results + overflow-x: hidden + overflow-y: scroll + max-height: 16rem + + em + font-style: normal + background-color: $yellow-light \ No newline at end of file diff --git a/source/css/_components/search/index.styl b/source/css/_components/search/index.styl index 295ebe7..772c214 100644 --- a/source/css/_components/search/index.styl +++ b/source/css/_components/search/index.styl @@ -1 +1,3 @@ +@import './common.styl' @import './algolia.styl' if (hexo-config('algolia_search.enable')) +@import './localsearch.styl' if (hexo-config('local_search.enable')) diff --git a/source/css/_components/search/localsearch.styl b/source/css/_components/search/localsearch.styl new file mode 100644 index 0000000..3d70e8d --- /dev/null +++ b/source/css/_components/search/localsearch.styl @@ -0,0 +1,37 @@ +.localsearch-input-wrapper + input + width: 100% + +.localsearch-result + overflow: auto + max-height: calc(80vh - 6rem) + + b + border-bottom: 1px dashed red + color: red + + & > ul + margin: 0 + padding-left: 1.4rem + + li + &:not(:last-child) + margin-bottom: 1rem + + &::after + content: '' + display: block + border-bottom: 1px dashed #ccc + padding-bottom: .5rem + width: 100% + + &-title + clearAStyle() + font-weight: $font-weight-bold + color: #222 + + &-content + overflow: hidden + width: 100% + max-height: 5rem + color: #666 -- GitLab