insight.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /**
  2. * Insight search plugin
  3. * @author PPOffice { @link https://github.com/ppoffice }
  4. */
  5. (function ($, CONFIG) {
  6. var $main = $('.ins-search');
  7. var $input = $main.find('.ins-search-input');
  8. var $wrapper = $main.find('.ins-section-wrapper');
  9. var $container = $main.find('.ins-section-container');
  10. $main.parent().remove('.ins-search');
  11. $('body').append($main);
  12. function section (title) {
  13. return $('<section>').addClass('ins-section')
  14. .append($('<header>').addClass('ins-section-header').text(title));
  15. }
  16. function searchItem (icon, title, slug, preview, url) {
  17. return $('<div>').addClass('ins-selectable').addClass('ins-search-item')
  18. .append($('<header>').append($('<i>').addClass('fa').addClass('fa-' + icon))
  19. .append($('<span>').addClass('ins-title').text(title != null && title !== '' ? title : CONFIG.TRANSLATION['UNTITLED']))
  20. .append(slug ? $('<span>').addClass('ins-slug').text(slug) : null))
  21. .append(preview ? $('<p>').addClass('ins-search-preview').text(preview) : null)
  22. .attr('data-url', url);
  23. }
  24. function sectionFactory (type, array) {
  25. var sectionTitle;
  26. var $searchItems;
  27. if (array.length === 0) return null;
  28. sectionTitle = CONFIG.TRANSLATION[type];
  29. switch (type) {
  30. case 'POSTS':
  31. case 'PAGES':
  32. $searchItems = array.map(function (item) {
  33. // Use config.root instead of permalink to fix url issue
  34. return searchItem('file', item.title, null, item.text.slice(0, 150), item.link);
  35. });
  36. break;
  37. case 'CATEGORIES':
  38. case 'TAGS':
  39. $searchItems = array.map(function (item) {
  40. return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', item.name, item.slug, null, item.link);
  41. });
  42. break;
  43. default:
  44. return null;
  45. }
  46. return section(sectionTitle).append($searchItems);
  47. }
  48. function parseKeywords (keywords) {
  49. return keywords.split(' ').filter(function (keyword) {
  50. return !!keyword;
  51. }).map(function (keyword) {
  52. return keyword.toUpperCase();
  53. });
  54. }
  55. /**
  56. * Judge if a given post/page/category/tag contains all of the keywords.
  57. * @param Object obj Object to be weighted
  58. * @param Array<String> fields Object's fields to find matches
  59. */
  60. function filter (keywords, obj, fields) {
  61. var keywordArray = parseKeywords(keywords);
  62. var containKeywords = keywordArray.filter(function (keyword) {
  63. var containFields = fields.filter(function (field) {
  64. if (!obj.hasOwnProperty(field))
  65. return false;
  66. if (obj[field].toUpperCase().indexOf(keyword) > -1)
  67. return true;
  68. });
  69. if (containFields.length > 0)
  70. return true;
  71. return false;
  72. });
  73. return containKeywords.length === keywordArray.length;
  74. }
  75. function filterFactory (keywords) {
  76. return {
  77. POST: function (obj) {
  78. return filter(keywords, obj, ['title', 'text']);
  79. },
  80. PAGE: function (obj) {
  81. return filter(keywords, obj, ['title', 'text']);
  82. },
  83. CATEGORY: function (obj) {
  84. return filter(keywords, obj, ['name', 'slug']);
  85. },
  86. TAG: function (obj) {
  87. return filter(keywords, obj, ['name', 'slug']);
  88. }
  89. };
  90. }
  91. /**
  92. * Calculate the weight of a matched post/page/category/tag.
  93. * @param Object obj Object to be weighted
  94. * @param Array<String> fields Object's fields to find matches
  95. * @param Array<Integer> weights Weight of every field
  96. */
  97. function weight (keywords, obj, fields, weights) {
  98. var value = 0;
  99. parseKeywords(keywords).forEach(function (keyword) {
  100. var pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive
  101. fields.forEach(function (field, index) {
  102. if (obj.hasOwnProperty(field)) {
  103. var matches = obj[field].match(pattern);
  104. value += matches ? matches.length * weights[index] : 0;
  105. }
  106. });
  107. });
  108. return value;
  109. }
  110. function weightFactory (keywords) {
  111. return {
  112. POST: function (obj) {
  113. return weight(keywords, obj, ['title', 'text'], [3, 1]);
  114. },
  115. PAGE: function (obj) {
  116. return weight(keywords, obj, ['title', 'text'], [3, 1]);
  117. },
  118. CATEGORY: function (obj) {
  119. return weight(keywords, obj, ['name', 'slug'], [1, 1]);
  120. },
  121. TAG: function (obj) {
  122. return weight(keywords, obj, ['name', 'slug'], [1, 1]);
  123. }
  124. };
  125. }
  126. function search (json, keywords) {
  127. var WEIGHTS = weightFactory(keywords);
  128. var FILTERS = filterFactory(keywords);
  129. var posts = json.posts;
  130. var pages = json.pages;
  131. var tags = json.tags;
  132. var categories = json.categories;
  133. return {
  134. posts: posts.filter(FILTERS.POST).sort(function (a, b) { return WEIGHTS.POST(b) - WEIGHTS.POST(a); }).slice(0, 5),
  135. pages: pages.filter(FILTERS.PAGE).sort(function (a, b) { return WEIGHTS.PAGE(b) - WEIGHTS.PAGE(a); }).slice(0, 5),
  136. categories: categories.filter(FILTERS.CATEGORY).sort(function (a, b) { return WEIGHTS.CATEGORY(b) - WEIGHTS.CATEGORY(a); }).slice(0, 5),
  137. tags: tags.filter(FILTERS.TAG).sort(function (a, b) { return WEIGHTS.TAG(b) - WEIGHTS.TAG(a); }).slice(0, 5)
  138. };
  139. }
  140. function searchResultToDOM (searchResult) {
  141. $container.empty();
  142. for (var key in searchResult) {
  143. $container.append(sectionFactory(key.toUpperCase(), searchResult[key]));
  144. }
  145. }
  146. function scrollTo ($item) {
  147. if ($item.length === 0) return;
  148. var wrapperHeight = $wrapper[0].clientHeight;
  149. var itemTop = $item.position().top - $wrapper.scrollTop();
  150. var itemBottom = $item[0].clientHeight + $item.position().top;
  151. if (itemBottom > wrapperHeight + $wrapper.scrollTop()) {
  152. $wrapper.scrollTop(itemBottom - $wrapper[0].clientHeight);
  153. }
  154. if (itemTop < 0) {
  155. $wrapper.scrollTop($item.position().top);
  156. }
  157. }
  158. function selectItemByDiff (value) {
  159. var $items = $.makeArray($container.find('.ins-selectable'));
  160. var prevPosition = -1;
  161. $items.forEach(function (item, index) {
  162. if ($(item).hasClass('active')) {
  163. prevPosition = index;
  164. return;
  165. }
  166. });
  167. var nextPosition = ($items.length + prevPosition + value) % $items.length;
  168. $($items[prevPosition]).removeClass('active');
  169. $($items[nextPosition]).addClass('active');
  170. scrollTo($($items[nextPosition]));
  171. }
  172. function gotoLink ($item) {
  173. if ($item && $item.length) {
  174. location.href = $item.attr('data-url');
  175. }
  176. }
  177. $.getJSON(CONFIG.CONTENT_URL, function (json) {
  178. if (location.hash.trim() === '#ins-search') {
  179. $main.addClass('show');
  180. }
  181. $input.on('input', function () {
  182. var keywords = $(this).val();
  183. searchResultToDOM(search(json, keywords));
  184. });
  185. $input.trigger('input');
  186. });
  187. var touch = false;
  188. $(document).on('click focus', '.navbar-main .search', function () {
  189. $main.addClass('show');
  190. $main.find('.ins-search-input').focus();
  191. }).on('click touchend', '.ins-search-item', function (e) {
  192. if (e.type !== 'click' && !touch) {
  193. return;
  194. }
  195. gotoLink($(this));
  196. touch = false;
  197. }).on('click touchend', '.ins-close', function (e) {
  198. if (e.type !== 'click' && !touch) {
  199. return;
  200. }
  201. $('.navbar-main').css('pointer-events', 'none');
  202. setTimeout(function(){
  203. $('.navbar-main').css('pointer-events', 'auto');
  204. }, 400);
  205. $main.removeClass('show');
  206. touch = false;
  207. }).on('keydown', function (e) {
  208. if (!$main.hasClass('show')) return;
  209. switch (e.keyCode) {
  210. case 27: // ESC
  211. $main.removeClass('show'); break;
  212. case 38: // UP
  213. selectItemByDiff(-1); break;
  214. case 40: // DOWN
  215. selectItemByDiff(1); break;
  216. case 13: //ENTER
  217. gotoLink($container.find('.ins-selectable.active').eq(0)); break;
  218. }
  219. }).on('touchstart', function (e) {
  220. touch = true;
  221. }).on('touchmove', function (e) {
  222. touch = false;
  223. });
  224. })(jQuery, window.INSIGHT_CONFIG);