From 2dec296aaec918ac410e856a50a2c1adf8d375f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 9 Feb 2020 19:14:34 +0100 Subject: [PATCH] Use css to show / hide items; track matched items ... to improve search speed. Instead of marking all items as hidden, and then marking the matched ones again, this keeps track of all matched items and only removes those that no longer match and adds those that previously did not match. --- .../jsdoc/api/template/static/scripts/main.js | 230 +++++++++++++----- .../api/template/static/styles/jaguar.css | 44 +++- .../jsdoc/api/template/tmpl/navigation.tmpl | 6 +- 3 files changed, 213 insertions(+), 67 deletions(-) diff --git a/config/jsdoc/api/template/static/scripts/main.js b/config/jsdoc/api/template/static/scripts/main.js index 2d32f6e9de..6d41345717 100644 --- a/config/jsdoc/api/template/static/scripts/main.js +++ b/config/jsdoc/api/template/static/scripts/main.js @@ -3,7 +3,9 @@ $(function () { // Allow user configuration? const allowRegex = true; - const minInputForFullText = 1; + const minInputForSearch = 1; + const minInputForFullText = 2; + const expandAllOnInputWithoutSearch = true; function constructRegex(searchTerm, makeRe, allowRegex) { try { @@ -28,12 +30,12 @@ $(function () { const re = constructRegex(searchTerm, makeRe, allowRegex); return function (matchedItem, beginOnly) { // We could get smarter on the weight here - const name = matchedItem.data('name'); + const name = matchedItem.dataset.name; if (beginOnly) { return re.baseName.test(name) ? 10000 : 0; } // If everything else is equal, prefer shorter names, and prefer classes over modules - let weight = matchedItem.data('longname').length - name.length * 100; + let weight = matchedItem.dataset.longname.length - name.length * 100; if (name.match(re.begin)) { weight += 100000; if (re.baseName.test(name)) { @@ -50,70 +52,162 @@ $(function () { } } - // sort function callback - var weightSorter = function (a, b) { - var aW = $(a).data('weight') || 0; - var bW = $(b).data('weight') || 0; - return bW - aW; - }; + const search = (function () { + const $nav = $('.navigation'); + const $navList = $nav.find('.list'); + let $classItems; + let $members; + let stateClass = (function () { + $nav.removeClass('search-started searching'); + $nav.addClass('search-empty'); + return 'search-empty'; + })(); + let manualToggles = {}; - // Show an item related a current documentation automatically - const longname = $('.page-title').data('filename') - .replace(/\.[a-z]+$/, '') - .replace('module-', 'module:') - .replace(/_/g, '/') - .replace(/-/g, '~'); - var $currentItem = $('.navigation .item[data-longname="' + longname + '"]:eq(0)'); + // Show an item related a current documentation automatically + const longname = $('.page-title').data('filename') + .replace(/\.[a-z]+$/, '') + .replace('module-', 'module:') + .replace(/_/g, '/') + .replace(/-/g, '~'); + const $currentItem = $navList.find('.item[data-longname="' + longname + '"]:eq(0)'); + $currentItem.prependTo($navList); + $currentItem.addClass('item-current'); + return { + $nav: $nav, + $navList: $navList, + $currentItem: $currentItem, + lastSearchTerm: undefined, + lastState: {}, + getClassList: function () { + return $classItems || ($classItems = $navList.find('li.item')); + }, + getMembers: function () { + return $members || ($members = $navList.find('.item li')); + }, + changeStateClass: function (newClass) { + if (newClass !== stateClass) { + const navNode = $nav.get(0); + navNode.classList.remove(stateClass); + navNode.classList.add(newClass); + stateClass = newClass; + } + }, + manualToggle: function ($node, show) { + $node.toggleClass('toggle-manual-hide', !show); + $node.toggleClass('toggle-manual-show', show); + manualToggles[$node.data('longname')] = $node; + }, + clearManualToggles: function() { + for (let clsName in manualToggles) { + manualToggles[clsName].removeClass('toggle-manual-show toggle-manual-hide'); + } + manualToggles = {}; + }, + }; + })(); - if ($currentItem.length) { - $currentItem - .prependTo('.navigation .list') - .show() - .find('.member-list') - .show(); + const dummy = {subItems: {}}; + function clearOldMatches(lastState, searchState) { + for (let itemName in lastState) { + const lastItem = lastState[itemName]; + const item = searchState[itemName]; + if (!item) { + lastItem.item.classList.remove('match'); + } + if (lastItem.subItems) { + clearOldMatches(lastItem.subItems, (item || dummy).subItems); + } + } } function doSearch(searchTerm) { - var $el = $('.navigation'); - - if (searchTerm.length > 1) { - searchTerm = searchTerm.toLowerCase(); - const getSearchWeight = getWeightFunction(searchTerm, allowRegex); - const beginOnly = searchTerm.length < minInputForFullText; - const regexp = constructRegex(searchTerm, function (searchTerm) { - return new RegExp(searchTerm); - }, allowRegex); - $el.find('li, .member-list').hide(); - - $el.find('li').each(function (i, v) { - const $item = $(v); - const name = $item.data('name'); - - if (regexp.test(name)) { - const $classEntry = $item.closest('.item'); - const $members = $item.closest('.member-list'); - - // Do the weight thing - const weight = getSearchWeight($classEntry, beginOnly); - $classEntry.data('weight', weight); - - $item.show(); - $members.show(); - $classEntry.show(); - } - }); - - $(".navigation ul.list li.item:visible") - .sort(weightSorter) // sort elements - .appendTo(".navigation ul.list"); // append again to the list - - } else { - $currentItem.prependTo('.navigation .list'); - $currentItem.find('.member-list, li').show(); - $el.find('.item').show(); + searchTerm = searchTerm.toLowerCase(); + const lastSearchTerm = search.lastSearchTerm; + if (searchTerm === lastSearchTerm) { + return; } - $el.find('.list').scrollTop(0); + // Avoid layout reflow by scrolling to top first. + search.$navList.scrollTop(0); + search.lastSearchTerm = searchTerm; + search.clearManualToggles(); + + if (searchTerm.length < minInputForSearch) { + const state = searchTerm.length && expandAllOnInputWithoutSearch ? 'search-started' : 'search-empty'; + search.changeStateClass(state); + if (lastSearchTerm !== undefined && lastSearchTerm.length >= minInputForSearch) { + // Restore the original, sorted order + search.$navList.append(search.getClassList()); + } + if (state === 'search-empty') { + search.manualToggle(search.$currentItem, true); + } + } else { + search.changeStateClass('searching'); + searchTerm = searchTerm.toLowerCase(); + const beginOnly = searchTerm.length < minInputForFullText; + const getSearchWeight = getWeightFunction(searchTerm, allowRegex); + const re = constructRegex(searchTerm, function (searchTerm) { + return new RegExp((beginOnly ? '\\b' : '') + searchTerm); + }, allowRegex); + const navList = search.$navList.get(0); + const classes = []; + const searchState = {}; + search.getMembers().each(function (i, li) { + const name = li.dataset.name; + if (re.test(name)) { + const itemMember = li.parentElement.parentElement; + const classEntry = itemMember.parentElement; + const className = classEntry.dataset.longname; + let cls = searchState[className]; + if (!cls) { + cls = searchState[className] = { + item: classEntry, + // Do the weight thing + weight: getSearchWeight(classEntry, beginOnly) * 10000, + subItems: {} + }; + classes.push(cls); + classEntry.classList.add('match'); + } + cls.weight += getSearchWeight(li, true) + 1; + const memberType = itemMember.dataset.type; + let members = cls.subItems[memberType]; + if (!members) { + members = cls.subItems[memberType] = { + item: itemMember, + subItems: {} + }; + itemMember.classList.add('match'); + } + members.subItems[name] = { item: li }; + li.classList.add('match'); + } + }); + search.getClassList().each(function (i, classEntry) { + const className = classEntry.dataset.longname; + if (!(className in searchState) && re.test(classEntry.dataset.name)) { + const cls = searchState[className] = { + item: classEntry, + // Do the weight thing + weight: getSearchWeight(classEntry, beginOnly) * 10000, + subItems: {} + }; + classes.push(cls); + classEntry.classList.add('match'); + } + }); + clearOldMatches(search.lastState, searchState); + search.lastState = searchState; + + classes.sort(function (a, b) { + return a.weight - b.weight; + }); + for (let i = classes.length - 1; i >= 0; --i) { + navList.appendChild(classes[i].item); + } + } } const searchInput = $('#search').get(0); @@ -132,7 +226,11 @@ $(function () { const time = Date.now() - start brandNode.innerHTML = time + ' ms'; - console.log(searchTerm + ':', time, 'ms'); + const msg = [searchTerm + ':', time, 'ms']; + if (searchTerm.length >= minInputForSearch) { + msg.push(search.lastState); + } + console.log.apply(console, msg); start = undefined; }, 0); } @@ -145,8 +243,14 @@ $(function () { } // Toggle when click an item element - $('.navigation').on('click', '.toggle', function (e) { - $(this).parent().parent().find('.member-list').toggle(); + search.$nav.on('click', '.toggle', function (e) { + const clsItem = $(this).closest('.item'); + let shown; + clsItem.find('.member-list').each(function (i, v) { + shown = $(v).is(':visible'); + return !shown; + }); + search.manualToggle(clsItem, !shown); }); // Auto resizing on navigation diff --git a/config/jsdoc/api/template/static/styles/jaguar.css b/config/jsdoc/api/template/static/styles/jaguar.css index 0088caa282..bf605518d7 100644 --- a/config/jsdoc/api/template/static/styles/jaguar.css +++ b/config/jsdoc/api/template/static/styles/jaguar.css @@ -190,9 +190,51 @@ li { margin-top: 2px; } .navigation li.item .member-list { - display: none; padding-left: 8px; } + +/* search state */ +/* show all classes when search is empty */ +.navigation.search-empty li.item { + display: block; +} +/* hide all members by default when search is empty */ +.navigation.search-empty li.item .member-list { + display: none; +} +/* but show the members of the current pages module / class */ +.navigation.search-empty li.item.item-current .member-list { + display: block; +} +/* expand all members when one character is entered in the search field */ +.navigation.search-started li.item, +.navigation.search-started .member-list, +.navigation.search-started .member-list li { + display: block; +} +/* when more than one character is entered hide everything that is not a match */ +.navigation.searching li.item, +.navigation.searching .member-list, +.navigation.searching .member-list li { + display: none; +} +.navigation.searching li.item.match, +.navigation.searching .member-list.match, +.navigation.searching .member-list li.match { + display: block; +} +/* allow user to hide / show members */ +.navigation .item.toggle-manual-show .member-list, +.navigation .item.toggle-manual-show li { + display: block!important; +} +.navigation .item.toggle-manual-hide .member-list, +.navigation .item.toggle-manual-hide li, +.navigation.searching .item.toggle-manual-show .member-list:not(.match), +.navigation.searching .item.toggle-manual-show li:not(.match) { + display: none!important; +} + .main { padding: 20px 20px; margin-left: 250px; diff --git a/config/jsdoc/api/template/tmpl/navigation.tmpl b/config/jsdoc/api/template/tmpl/navigation.tmpl index 6445c908a5..7cea44991b 100644 --- a/config/jsdoc/api/template/tmpl/navigation.tmpl +++ b/config/jsdoc/api/template/tmpl/navigation.tmpl @@ -25,7 +25,7 @@ const printListWithStability = v => { function listContent(item, title, listItemPrinter) { const type = title.toLowerCase(); if (item[type] && item[type].length) { ?> -
+
    -