1/** 2 * Insight search plugin 3 * @author PPOffice { @link https://github.com/ppoffice } 4 */ 5// eslint-disable-next-line no-unused-vars 6function loadInsight(config, translation) { 7 const $main = $('.searchbox'); 8 const $input = $main.find('.searchbox-input'); 9 const $container = $main.find('.searchbox-body'); 10 11 function section(title) { 12 return $('<section>').addClass('searchbox-result-section').append($('<header>').text(title)); 13 } 14 15 function merge(ranges) { 16 let last; 17 const result = []; 18 19 ranges.forEach((r) => { 20 if (!last || r[0] > last[1]) { 21 result.push((last = r)); 22 } else if (r[1] > last[1]) { 23 last[1] = r[1]; 24 } 25 }); 26 27 return result; 28 } 29 30 function findAndHighlight(text, matches, maxlen) { 31 if (!Array.isArray(matches) || !matches.length || !text) { 32 return maxlen ? text.slice(0, maxlen) : text; 33 } 34 const testText = text.toLowerCase(); 35 const indices = matches 36 .map((match) => { 37 const index = testText.indexOf(match.toLowerCase()); 38 if (!match || index === -1) { 39 return null; 40 } 41 return [index, index + match.length]; 42 }) 43 .filter((match) => { 44 return match !== null; 45 }) 46 .sort((a, b) => { 47 return a[0] - b[0] || a[1] - b[1]; 48 }); 49 50 if (!indices.length) { 51 return text; 52 } 53 54 let result = ''; 55 let last = 0; 56 const ranges = merge(indices); 57 const sumRange = [ranges[0][0], ranges[ranges.length - 1][1]]; 58 if (maxlen && maxlen < sumRange[1]) { 59 last = sumRange[0]; 60 } 61 62 for (let i = 0; i < ranges.length; i++) { 63 const range = ranges[i]; 64 result += text.slice(last, Math.min(range[0], sumRange[0] + maxlen)); 65 if (maxlen && range[0] >= sumRange[0] + maxlen) { 66 break; 67 } 68 result += '<em>' + text.slice(range[0], range[1]) + '</em>'; 69 last = range[1]; 70 if (i === ranges.length - 1) { 71 if (maxlen) { 72 result += text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1)); 73 } else { 74 result += text.slice(range[1]); 75 } 76 } 77 } 78 79 return result; 80 } 81 82 function searchItem(icon, title, slug, preview, url) { 83 title = title != null && title !== '' ? title : translation.untitled; 84 const subtitle = slug 85 ? '<span class="searchbox-result-title-secondary">(' + slug + ')</span>' 86 : ''; 87 88 return `<a class="searchbox-result-item" href="${url}"> 89 <span class="searchbox-result-icon"> 90 <i class="fa fa-${icon}" /> 91 </span> 92 <span class="searchbox-result-content"> 93 <span class="searchbox-result-title"> 94 ${title} 95 ${subtitle} 96 </span> 97 ${preview ? '<span class="searchbox-result-preview">' + preview + '</span>' : ''} 98 </span> 99 </a>`; 100 } 101 102 function sectionFactory(keywords, type, array) { 103 let $searchItems; 104 if (array.length === 0) return null; 105 const sectionTitle = translation[type.toLowerCase()]; 106 switch (type) { 107 case 'POSTS': 108 case 'PAGES': 109 $searchItems = array.map((item) => { 110 const title = findAndHighlight(item.title, keywords); 111 const text = findAndHighlight(item.text, keywords, 100); 112 return searchItem('file', title, null, text, item.link); 113 }); 114 break; 115 case 'CATEGORIES': 116 case 'TAGS': 117 $searchItems = array.map((item) => { 118 const name = findAndHighlight(item.name, keywords); 119 const slug = findAndHighlight(item.slug, keywords); 120 return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', name, slug, null, item.link); 121 }); 122 break; 123 default: 124 return null; 125 } 126 return section(sectionTitle).append($searchItems); 127 } 128 129 function parseKeywords(keywords) { 130 return keywords 131 .split(' ') 132 .filter((keyword) => { 133 return !!keyword; 134 }) 135 .map((keyword) => { 136 return keyword.toLowerCase(); 137 }); 138 } 139 140 /** 141 * Judge if a given post/page/category/tag contains all of the keywords. 142 * @param Object obj Object to be weighted 143 * @param Array<String> fields Object's fields to find matches 144 */ 145 function filter(keywords, obj, fields) {
146 const keywordArray = parseKeywords(keywords); 147 const containKeywords = keywordArray.filter((keyword) => { 148 const containFields = fields.filter((field) => { 149 if (!Object.prototype.hasOwnProperty.call(obj, field)) { 150 return false; 151 } 152 if (obj[field].toLowerCase().indexOf(keyword) > -1) { 153 return true; 154 } 155 return false; 156 }); 157 if (containFields.length > 0) { 158 return true; 159 } 160 return false; 161 }); 162 return containKeywords.length === keywordArray.length; 163 } 164 165 function filterFactory(keywords) { 166 return { 167 post: function (obj) { 168 return filter(keywords, obj, ['title', 'text']); 169 }, 170 page: function (obj) { 171 return filter(keywords, obj, ['title', 'text']); 172 }, 173 category: function (obj) { 174 return filter(keywords, obj, ['name', 'slug']); 175 }, 176 tag: function (obj) { 177 return filter(keywords, obj, ['name', 'slug']); 178 }, 179 }; 180 } 181 182 /** 183 * Calculate the weight of a matched post/page/category/tag. 184 * @param Object obj Object to be weighted 185 * @param Array<String> fields Object's fields to find matches 186 * @param Array<Integer> weights Weight of every field 187 */ 188 function weight(keywords, obj, fields, weights) { 189 let value = 0;
190 parseKeywords(keywords).forEach((keyword) => { 191 const pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive 192 fields.forEach((field, index) => { 193 if (Object.prototype.hasOwnProperty.call(obj, field)) { 194 const matches = obj[field].match(pattern); 195 value += matches ? matches.length * weights[index] : 0; 196 } 197 }); 198 }); 199 return value; 200 } 201 202 function weightFactory(keywords) { 203 return { 204 post: function (obj) { 205 return weight(keywords, obj, ['title', 'text'], [3, 1]); 206 }, 207 page: function (obj) { 208 return weight(keywords, obj, ['title', 'text'], [3, 1]); 209 }, 210 category: function (obj) { 211 return weight(keywords, obj, ['name', 'slug'], [1, 1]); 212 }, 213 tag: function (obj) { 214 return weight(keywords, obj, ['name', 'slug'], [1, 1]); 215 }, 216 }; 217 } 218 219 function search(json, keywords) { 220 const weights = weightFactory(keywords); 221 const filters = filterFactory(keywords); 222 const posts = json.posts; 223 const pages = json.pages; 224 const tags = json.tags; 225 const categories = json.categories; 226 return { 227 posts: posts 228 .filter(filters.post) 229 .sort((a, b) => { 230 return weights.post(b) - weights.post(a); 231 }) 232 .slice(0, 5), 233 pages: pages 234 .filter(filters.page) 235 .sort((a, b) => { 236 return weights.page(b) - weights.page(a); 237 }) 238 .slice(0, 5), 239 categories: categories 240 .filter(filters.category) 241 .sort((a, b) => { 242 return weights.category(b) - weights.category(a); 243 }) 244 .slice(0, 5), 245 tags: tags 246 .filter(filters.tag) 247 .sort((a, b) => { 248 return weights.tag(b) - weights.tag(a); 249 }) 250 .slice(0, 5), 251 }; 252 } 253 254 function searchResultToDOM(keywords, searchResult) { 255 $container.empty(); 256 for (const key in searchResult) { 257 $container.append( 258 sectionFactory(parseKeywords(keywords), key.toUpperCase(), searchResult[key]), 259 ); 260 } 261 } 262 263 function scrollTo($item) { 264 if ($item.length === 0) return; 265 const wrapperHeight = $container[0].clientHeight; 266 const itemTop = $item.position().top - $container.scrollTop(); 267 const itemBottom = $item[0].clientHeight + $item.position().top; 268 if (itemBottom > wrapperHeight + $container.scrollTop()) { 269 $container.scrollTop(itemBottom - $container[0].clientHeight); 270 } 271 if (itemTop < 0) { 272 $container.scrollTop($item.position().top); 273 } 274 } 275 276 function selectItemByDiff(value) { 277 const $items = $.makeArray($container.find('.searchbox-result-item')); 278 let prevPosition = -1; 279 $items.forEach((item, index) => { 280 if ($(item).hasClass('active')) { 281 prevPosition = index; 282 } 283 }); 284 const nextPosition = ($items.length + prevPosition + value) % $items.length; 285 $($items[prevPosition]).removeClass('active'); 286 $($items[nextPosition]).addClass('active'); 287 scrollTo($($items[nextPosition])); 288 } 289 290 function gotoLink($item) { 291 if ($item && $item.length) { 292 location.href = $item.attr('href'); 293 } 294 } 295 296 $.getJSON(config.contentUrl, (json) => { 297 if (location.hash.trim() === '#insight-search') { 298 $main.addClass('show'); 299 } 300 $input.on('input', function () { 301 const keywords = $(this).val(); 302 searchResultToDOM(keywords, search(json, keywords)); 303 }); 304 $input.trigger('input'); 305 }); 306 307 let touch = false; 308 $(document) 309 .on('click focus', '.navbar-main .search', () => { 310 $main.addClass('show'); 311 $main.find('.searchbox-input').focus(); 312 }) 313 .on('click touchend', '.searchbox-result-item', function (e) { 314 if (e.type !== 'click' && !touch) { 315 return; 316 } 317 gotoLink($(this)); 318 touch = false; 319 }) 320 .on('click touchend', '.searchbox-close', (e) => { 321 if (e.type !== 'click' && !touch) { 322 return; 323 } 324 $('.navbar-main').css('pointer-events', 'none'); 325 setTimeout(() => { 326 $('.navbar-main').css('pointer-events', 'auto'); 327 }, 400); 328 $main.removeClass('show'); 329 touch = false;
330 }) 331 .on('keydown', (e) => { 332 if (!$main.hasClass('show')) return; 333 switch (e.keyCode) { 334 case 27: // ESC 335 $main.removeClass('show'); 336 break; 337 case 38: // UP 338 selectItemByDiff(-1); 339 break; 340 case 40: // DOWN 341 selectItemByDiff(1); 342 break; 343 case 13: // ENTER 344 gotoLink($container.find('.searchbox-result-item.active').eq(0)); 345 break; 346 } 347 }) 348 .on('touchstart', (e) => { 349 touch = true; 350 }) 351 .on('touchmove', (e) => { 352 touch = false; 353 }); 354}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.