Sfoglia il codice sorgente

feat: add insight search

ppoffice 9 anni fa
parent
commit
56b701b9bd

+ 1 - 0
_config.yml.example

@@ -43,6 +43,7 @@ widgets:
 
 # Search
 search:
+    insight: true
     swiftype: # enter swiftype install key here
     baidu: false # you need to disable other search engines to use Baidu search, options: true, false
 

+ 5 - 1
layout/search/index-mobile.ejs

@@ -1,4 +1,8 @@
-<% if (theme.search.swiftype) { %>
+<% if (theme.search.insight) { %>
+    <div class="search-form">
+        <input type="text" class="ins-search-input search-form-input" placeholder="<%= __('index.search') %>" />
+    </div>
+<% } else if (theme.search.swiftype) { %>
     <div class="search-form">
         <input type="text" class="st-default-search-input search-form-input" placeholder="<%= __('index.search') %>" />
     </div>

+ 7 - 1
layout/search/index.ejs

@@ -1,5 +1,11 @@
 <div id="search-form-wrap">
-<% if (theme.search.swiftype) { %>
+<% if (theme.search.insight) { %>
+    <form class="search-form">
+        <input type="text" class="ins-search-input search-form-input" placeholder="<%= __('index.search') %>" />
+        <button type="submit" class="search-form-submit"></button>
+    </form>
+    <%- partial('search/insight') %>
+<% } else if (theme.search.swiftype) { %>
     <form class="search-form">
         <input type="text" class="st-default-search-input search-form-input" placeholder="<%= __('index.search') %>" />
         <button type="submit" class="search-form-submit"></button>

+ 201 - 0
layout/search/insight.ejs

@@ -0,0 +1,201 @@
+<div class="ins-search">
+    <div class="ins-search-mask"></div>
+    <div class="ins-search-container">
+        <div class="ins-input-wrapper">
+            <input type="text" class="ins-search-input" placeholder="Type something..." />
+            <span class="ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
+        </div>
+        <div class="ins-section-wrapper">
+            <div class="ins-section-container"></div>
+        </div>
+    </div>
+</div>
+<script>
+    (function ($) {
+        $main = $('.ins-search');
+        $main.parent().remove('.ins-search');
+        $('body').append($main);
+        $container = $('.ins-section-container');
+
+        $(document).on('click focus', '.search-form-input', function () {
+            $main.addClass('show');
+            $main.find('.ins-search-input').focus();
+        }).on('click', '.ins-search-item', function () {
+            location.href=$(this).attr('data-url');
+        }).on('click', '.ins-close', function () {
+            $main.removeClass('show');
+        });
+
+
+        function section (title) {
+            return $('<section>').addClass('ins-section')
+                .append($('<header>').addClass('ins-section-header').text(title));
+        }
+
+        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(title)
+                    .append(slug ? $('<span>').addClass('ins-slug').text(slug) : null))
+                .append(preview ? $('<p>').addClass('ins-search-preview').text(preview) : null)
+                .attr('data-url', url);
+        }
+
+        function sectionFactory (type, array) {
+            var sectionTitle;
+            var $searchItems;
+            if (array.length == 0) return null;
+            switch (type) {
+                case 'POSTS':
+                case 'PAGES':
+                    sectionTitle = type == 'POSTS' ? 'Posts' : 'Pages';
+                    $searchItems = array.map(function (item) {
+                        // Use config.root instead of permalink to fix url issue
+                        return searchItem('file', item.title, null, item.text.slice(0, 150), <%= config.root %> + item.path);
+                    });
+                    break;
+                case 'CATEGORIES':
+                case 'TAGS':
+                    sectionTitle = type == 'CATEGORIES' ? 'Categories' : 'Tags';
+                    $searchItems = array.map(function (item) {
+                        return searchItem(type == 'CATEGORIES' ? 'folder' : 'tag', item.name, item.slug, null, item.permalink);
+                    });
+                    break;
+                default:
+                    return null;
+            }
+            return section(sectionTitle).append($searchItems);
+        }
+
+        function extractToSet (json, key) {
+            var values = {};
+            var entries = json.pages.concat(json.posts);
+            entries.forEach(function (entry) {
+                if (entry[key]) {
+                    entry[key].forEach(function (value) {
+                        values[value.name] = value;
+                    });
+                }
+            });
+            var result = [];
+            for (var key in values) {
+                result.push(values[key]);
+            }
+            return result;
+        }
+
+        function parseKeywords (keywords) {
+            return keywords.split(' ').filter(function (keyword) {
+                return !!keyword;
+            }).map(function (keyword) {
+                return keyword.toUpperCase();
+            });
+        }
+
+        /**
+         * Judge if a given post/page/category/tag contains all of the keywords.
+         * @param Object            obj     Object to be weighted
+         * @param Array<String>     fields  Object's fields to find matches
+         */
+        function filter (keywords, obj, fields) {
+            var result = false;
+            var keywordArray = parseKeywords(keywords);
+            var containKeywords = keywordArray.filter(function (keyword) {
+                var containFields = fields.filter(function (field) {
+                    if (!obj.hasOwnProperty(field))
+                        return false;
+                    if (obj[field].toUpperCase().indexOf(keyword) > -1)
+                        return true;
+                });
+                if (containFields.length > 0)
+                    return true;
+                return false;
+            });
+            return containKeywords.length == keywordArray.length;
+        }
+
+        function filterFactory (keywords) {
+            return {
+                POST: function (obj) {
+                    return filter(keywords, obj, ['title', 'text']);
+                },
+                PAGE: function (obj) {
+                    return filter(keywords, obj, ['title', 'text']);
+                },
+                CATEGORY: function (obj) {
+                    return filter(keywords, obj, ['name', 'slug']);
+                },
+                TAG: function (obj) {
+                    return filter(keywords, obj, ['name', 'slug']);
+                }
+            };
+        }
+
+        /**
+         * Calculate the weight of a matched post/page/category/tag.
+         * @param Object            obj     Object to be weighted
+         * @param Array<String>     fields  Object's fields to find matches
+         * @param Array<Integer>    weights Weight of every field
+         */
+        function weight (keywords, obj, fields, weights) {
+            var value = 0;
+            parseKeywords(keywords).forEach(function (keyword) {
+                var pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive
+                fields.forEach(function (field, index) {
+                    if (obj.hasOwnProperty(field)) {
+                        var matches = obj[field].match(pattern);
+                        value += matches ? matches.length * weights[index] : 0;
+                    }
+                });
+            });
+            return value;
+        }
+
+        function weightFactory (keywords) {
+            return {
+                POST: function (obj) {
+                    return weight(keywords, obj, ['title', 'text'], [3, 1]);
+                },
+                PAGE: function (obj) {
+                    return weight(keywords, obj, ['title', 'text'], [3, 1]);
+                },
+                CATEGORY: function (obj) {
+                    return weight(keywords, obj, ['name', 'slug'], [1, 1]);
+                },
+                TAG: function (obj) {
+                    return weight(keywords, obj, ['name', 'slug'], [1, 1]);
+                }
+            };
+        }
+
+        function search (json, keywords) {
+            var WEIGHTS = weightFactory(keywords);
+            var FILTERS = filterFactory(keywords);
+            var posts = json.posts;
+            var pages = json.pages;
+            var tags = extractToSet(json, 'tags');
+            var categories = extractToSet(json, 'categories');
+            return {
+                posts: posts.filter(FILTERS.POST).sort(function (a, b) { return WEIGHTS.POST(b) - WEIGHTS.POST(a); }).slice(0, 5),
+                pages: pages.filter(FILTERS.PAGE).sort(function (a, b) { return WEIGHTS.PAGE(b) - WEIGHTS.PAGE(a); }).slice(0, 5),
+                categories: categories.filter(FILTERS.CATEGORY).sort(function (a, b) { return WEIGHTS.CATEGORY(b) - WEIGHTS.CATEGORY(a); }).slice(0, 5),
+                tags: tags.filter(FILTERS.TAG).sort(function (a, b) { return WEIGHTS.TAG(b) - WEIGHTS.TAG(a); }).slice(0, 5)
+            };
+        }
+
+        function searchResultToDOM (searchResult) {
+            $container.empty();
+            for (var key in searchResult) {
+                $container.append(sectionFactory(key.toUpperCase(), searchResult[key]));
+            }
+        }
+
+        $.getJSON('<%- url_for("/content.json")%>', function (json) {
+            console.log(json)
+            $('.ins-search-input').on('input', function () {
+                var keywords = $(this).val();
+                searchResultToDOM(search(json, keywords));
+            });
+            $('.ins-search-input').trigger('input');
+        });
+    })(jQuery);
+</script>

+ 1 - 0
source/css/_partial/header.styl

@@ -152,6 +152,7 @@ $nav-link
                 color: #777
 
 .search-form-input,
+.search-form-input.ins-search-input,
 .search-form-input.st-ui-search-input,
 .search-form-input.st-default-search-input
     -webkit-appearance: textarea

+ 126 - 0
source/css/_partial/insight.styl

@@ -0,0 +1,126 @@
+// Insight Search Styles
+ins-container-width = 540px
+ins-text-grey = #9a9a9a
+ins-border-grey = #e2e2e2
+ins-background-grey = #f7f7f7
+ins-background-blue = #006BDE
+
+$ins-full-screen
+    top: 0
+    left: 0
+    margin: 0
+    width: 100%
+    height: 100%
+
+.ins-search
+    display: none
+    &.show
+        display: block
+
+.ins-selectable
+    cursor: pointer
+
+.ins-search-mask,
+.ins-search-container
+    position: fixed
+
+.ins-search-mask
+    top: 0
+    left: 0
+    width: 100%
+    height: 100%
+    z-index: 100
+    background: rgba(0,0,0,0.5)
+
+.ins-input-wrapper
+    position: relative
+
+.ins-search-input
+    width: 100%
+    border: none
+    outline: none
+    font-size: 16px
+    font-weight: 200
+    background: white
+    line-height: 20px
+    box-sizing: border-box
+    padding: 12px 28px 12px 20px
+    border-bottom: 1px solid ins-border-grey
+    font-family: "Microsoft Yahei Light", "Microsoft Yahei", Helvetica, Arial, sans-serif
+
+.ins-close
+    top: 50%
+    right: 6px
+    width: 20px
+    height: 20px
+    font-size: 16px
+    margin-top: -11px
+    position: absolute
+    text-align: center
+    display: inline-block
+    &:hover
+        color: ins-background-blue
+
+.ins-search-container
+    left: 50%
+    top: 100px
+    z-index: 101
+    bottom: 100px
+    box-sizing: border-box
+    width: ins-container-width
+    margin-left: -(ins-container-width/2)
+    @media screen and (max-width: 559px), screen and (max-height: 479px)
+        top: 0
+        left: 0
+        margin: 0
+        width: 100%
+        height: 100%
+        background: ins-background-grey
+
+.ins-section-wrapper
+    left: 0
+    right: 0
+    top: 45px
+    bottom: 0
+    overflow-y: auto
+    position: absolute
+
+.ins-section-container
+    background: ins-background-grey
+
+.ins-section
+    font-size: 14px
+    line-height: 16px
+    .ins-section-header,
+    .ins-search-item
+        padding: 8px 15px
+    .ins-section-header
+        color: ins-text-grey
+        border-bottom: 1px solid ins-border-grey
+    .ins-slug
+        margin-left: 5px
+        color: ins-text-grey
+        &:before
+            content: '('
+        &:after
+            content: ')'
+    .ins-search-item
+        header,
+        .ins-search-preview
+            overflow: hidden
+            white-space: nowrap
+            text-overflow: ellipsis
+        header
+            .fa
+                margin-right: 8px
+        .ins-search-preview
+            height: 15px
+            font-size: 12px
+            color: ins-text-grey
+            margin: 5px 0 0 20px
+        &:hover
+            color: white
+            background: ins-background-blue
+            .ins-slug,
+            .ins-search-preview
+                color: white

+ 1 - 0
source/css/style.styl

@@ -83,6 +83,7 @@ code
 @import "_partial/timeline"
 @import "_partial/footer"
 @import "_partial/sidebar"
+@import "_partial/insight"
 @import "_highlight/index"
 
 if sidebar is left