... 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.
306 lines
10 KiB
JavaScript
306 lines
10 KiB
JavaScript
$(function () {
|
|
'use strict';
|
|
|
|
// Allow user configuration?
|
|
const allowRegex = true;
|
|
const minInputForSearch = 1;
|
|
const minInputForFullText = 2;
|
|
const expandAllOnInputWithoutSearch = true;
|
|
|
|
function constructRegex(searchTerm, makeRe, allowRegex) {
|
|
try {
|
|
if (allowRegex) {
|
|
return makeRe(searchTerm);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
// In case of invalid regexp fall back to non-regexp, but still allow . to match /
|
|
return makeRe(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\./g, '[./]'));
|
|
}
|
|
|
|
function getWeightFunction(searchTerm, allowRegex) {
|
|
function makeRe(searchTerm) {
|
|
return {
|
|
begin: new RegExp('\\b' + searchTerm), // Begin matches word boundary
|
|
baseName: new RegExp('\\b' + searchTerm + '[^/]*$'), // Begin matches word boundary of class / module name
|
|
fullName: new RegExp('\\b' + searchTerm + '(?:[~.]|$)'), // Complete word(s) of class / module matches
|
|
completeName: new RegExp('^' + searchTerm + '$') // Match from start to finish
|
|
}
|
|
}
|
|
const re = constructRegex(searchTerm, makeRe, allowRegex);
|
|
return function (matchedItem, beginOnly) {
|
|
// We could get smarter on the weight here
|
|
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.dataset.longname.length - name.length * 100;
|
|
if (name.match(re.begin)) {
|
|
weight += 100000;
|
|
if (re.baseName.test(name)) {
|
|
weight += 10000000;
|
|
if (re.fullName.test(name)) {
|
|
weight += 100000000;
|
|
if (re.completeName.test(name)) {
|
|
weight += 1000000000;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return weight;
|
|
}
|
|
}
|
|
|
|
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, '~');
|
|
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 = {};
|
|
},
|
|
};
|
|
})();
|
|
|
|
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) {
|
|
searchTerm = searchTerm.toLowerCase();
|
|
const lastSearchTerm = search.lastSearchTerm;
|
|
if (searchTerm === lastSearchTerm) {
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
// Skip searches when typing fast.
|
|
let key;
|
|
let start;
|
|
const brandNode = document.querySelector('.brand');
|
|
function queueSearch() {
|
|
if (!key) {
|
|
start = Date.now();
|
|
key = setTimeout(function () {
|
|
key = undefined;
|
|
|
|
const searchTerm = searchInput.value;
|
|
doSearch(searchTerm);
|
|
|
|
const time = Date.now() - start
|
|
brandNode.innerHTML = time + ' ms';
|
|
const msg = [searchTerm + ':', time, 'ms'];
|
|
if (searchTerm.length >= minInputForSearch) {
|
|
msg.push(search.lastState);
|
|
}
|
|
console.log.apply(console, msg);
|
|
start = undefined;
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
// Search Items
|
|
searchInput.addEventListener('input', queueSearch);
|
|
if (searchInput.value) {
|
|
doSearch(searchInput.value);
|
|
}
|
|
|
|
// Toggle when click an item element
|
|
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
|
|
var _onResize = function () {
|
|
var height = $(window).height();
|
|
var $el = $('.navigation');
|
|
|
|
$el.height(height).find('.list').height(height - 133);
|
|
};
|
|
|
|
$(window).on('resize', _onResize);
|
|
_onResize();
|
|
|
|
var currentVersion = document.getElementById('package-version').innerHTML;
|
|
|
|
// warn about outdated version
|
|
var packageUrl = 'https://raw.githubusercontent.com/openlayers/openlayers.github.io/build/package.json';
|
|
fetch(packageUrl).then(function(response) {
|
|
return response.json();
|
|
}).then(function(json) {
|
|
var latestVersion = json.version;
|
|
document.getElementById('latest-version').innerHTML = latestVersion;
|
|
var url = window.location.href;
|
|
var branchSearch = url.match(/\/([^\/]*)\/apidoc\//);
|
|
var cookieText = 'dismissed=-' + latestVersion + '-';
|
|
var dismissed = document.cookie.indexOf(cookieText) != -1;
|
|
if (branchSearch && !dismissed && /^v[0-9\.]*$/.test(branchSearch[1]) && currentVersion != latestVersion) {
|
|
var link = url.replace(branchSearch[0], '/latest/apidoc/');
|
|
fetch(link, {method: 'head'}).then(function(response) {
|
|
var a = document.getElementById('latest-link');
|
|
a.href = response.status == 200 ? link : '../../latest/apidoc/';
|
|
});
|
|
var latestCheck = document.getElementById('latest-check');
|
|
latestCheck.style.display = '';
|
|
document.getElementById('latest-dismiss').onclick = function() {
|
|
latestCheck.style.display = 'none';
|
|
document.cookie = cookieText;
|
|
}
|
|
}
|
|
});
|
|
|
|
// create source code links to github
|
|
var srcLinks = $('div.tag-source');
|
|
srcLinks.each(function(i, el) {
|
|
var textParts = el.innerHTML.trim().split(', ');
|
|
var link = 'https://github.com/openlayers/openlayers/blob/v' + currentVersion + '/src/ol/' +
|
|
textParts[0];
|
|
el.innerHTML = '<a href="' + link + '">' + textParts[0] + '</a>, ' +
|
|
'<a href="' + link + textParts[1].replace('line ', '#L') + '">' +
|
|
textParts[1] + '</a>';
|
|
});
|
|
});
|