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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,7 @@ const printListWithStability = v => {
|
||||
function listContent(item, title, listItemPrinter) {
|
||||
const type = title.toLowerCase();
|
||||
if (item[type] && item[type].length) { ?>
|
||||
<div class="member-list">
|
||||
<div class="member-list" data-type="<?js= type ?>">
|
||||
<span class="subtitle"><?js= title ?></span>
|
||||
<ul><?js
|
||||
item[type].forEach(function (v) {
|
||||
@@ -36,13 +36,13 @@ function listContent(item, title, listItemPrinter) {
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="navigation">
|
||||
<div class="navigation search-empty">
|
||||
<div class="search">
|
||||
<input id="search" type="text" class="form-control input-sm" placeholder="Search Documentation">
|
||||
</div>
|
||||
<ul class="list"><?js
|
||||
this.nav.forEach(function (item) { ?>
|
||||
<li class="item" data-longname="<?js= item.longname ?>" data-name="<?js= item.name.toLowerCase() ?>">
|
||||
<li class="item" data-longname="<?js= item.longname ?>" data-name="<?js= item.prettyname.toLowerCase() ?>">
|
||||
<span class="title">
|
||||
<span class="glyphicon <?js= getItemCssClass(item.type) ?> toggle"></span>
|
||||
<span><?js= self.linkto(item.longname, item.prettyname.replace(/[.~]/g, '\u200b$&')) ?></span>
|
||||
|
||||
Reference in New Issue
Block a user