2021-01-27 04:18:16 +00:00
|
|
|
// represents a field containing a query expression string
|
|
|
|
class ExprField {
|
|
|
|
constructor(fieldId, errorTextId, compiler) {
|
|
|
|
this.field = document.getElementById(fieldId);
|
|
|
|
this.errorText = document.getElementById(errorTextId);
|
|
|
|
this.compiler = compiler;
|
|
|
|
this.output = null;
|
|
|
|
this.text = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
compile() {
|
|
|
|
if (this.field.value === this.text) return;
|
|
|
|
this.text = this.field.value;
|
|
|
|
this.errorText.innerText = '';
|
|
|
|
try {
|
|
|
|
this.output = this.compiler(this.text);
|
|
|
|
} catch (e) {
|
|
|
|
this.errorText.innerText = e.message;
|
|
|
|
this.output = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-29 19:11:56 +00:00
|
|
|
function compareLexico(ia, keysA, ib, keysB) {
|
|
|
|
for (let i = 0; i < keysA.length; i++) { // assuming keysA and keysB are the same length
|
|
|
|
let aKey = keysA[i], bKey = keysB[i];
|
|
|
|
if (typeof aKey !== typeof bKey) throw new Error(`Incomparable types ${typeof aKey} and ${typeof bKey}`); // can this even happen?
|
|
|
|
switch (typeof aKey) {
|
|
|
|
case 'string':
|
|
|
|
aKey = aKey.toLowerCase();
|
|
|
|
bKey = bKey.toLowerCase();
|
|
|
|
if (aKey < bKey) return -1;
|
|
|
|
if (aKey > bKey) return 1;
|
|
|
|
break;
|
|
|
|
case 'number': // sort numeric stuff in reverse order
|
|
|
|
if (aKey < bKey) return 1;
|
|
|
|
if (aKey > bKey) return -1;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error(`Incomparable type ${typeof aKey}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ib.lvl - ia.lvl;
|
|
|
|
}
|
|
|
|
|
|
|
|
function stringify(v) {
|
|
|
|
return typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : v;
|
|
|
|
}
|
|
|
|
|
2021-01-23 16:38:13 +00:00
|
|
|
function init() {
|
2021-01-25 22:06:41 +00:00
|
|
|
const itemList = document.getElementById('item-list');
|
|
|
|
const itemListFooter = document.getElementById('item-list-footer');
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// compile the search db from the item db
|
|
|
|
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
|
|
|
|
|
|
|
|
// init item list elements
|
2021-01-25 22:06:41 +00:00
|
|
|
const ITEM_LIST_SIZE = 64;
|
|
|
|
const itemEntries = [];
|
|
|
|
for (let i = 0; i < ITEM_LIST_SIZE; i++) {
|
|
|
|
const itemElem = document.createElement('div');
|
|
|
|
itemElem.classList.add('box');
|
|
|
|
itemElem.setAttribute('id', `item-entry-${i}`);
|
2021-01-27 04:18:16 +00:00
|
|
|
itemElem.style.display = 'none';
|
2021-01-25 22:06:41 +00:00
|
|
|
itemElem.style.width = '20vw';
|
|
|
|
itemElem.style.margin = '1vw';
|
|
|
|
itemElem.style.verticalAlign = 'top';
|
|
|
|
itemList.append(itemElem);
|
|
|
|
itemEntries.push(itemElem);
|
|
|
|
}
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// the two search query input boxes
|
|
|
|
const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) {
|
|
|
|
const expr = compileQueryExpr(exprStr);
|
|
|
|
return expr !== null ? expr : (i, ie) => true;
|
|
|
|
});
|
|
|
|
const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) {
|
|
|
|
const subExprs = exprStr.split(';').map(compileQueryExpr).filter(f => f != null);
|
2021-01-29 19:11:56 +00:00
|
|
|
return function(i, ie) {
|
|
|
|
const sortKeys = [];
|
|
|
|
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k](i, ie));
|
|
|
|
return sortKeys;
|
|
|
|
};
|
2021-01-27 04:18:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// updates the current search state from the search query input boxes
|
2021-01-25 22:06:41 +00:00
|
|
|
function updateSearch() {
|
2021-01-27 04:18:16 +00:00
|
|
|
// hide old search results
|
2021-01-25 22:06:41 +00:00
|
|
|
itemListFooter.innerText = '';
|
2021-01-27 04:18:16 +00:00
|
|
|
for (const itemEntry of itemEntries) itemEntry.style.display = 'none';
|
|
|
|
|
|
|
|
// compile query expressions, aborting if either fails to compile
|
|
|
|
searchFilterField.compile();
|
|
|
|
searchSortField.compile();
|
|
|
|
if (searchFilterField.output === null || searchSortField.output === null) return;
|
|
|
|
|
|
|
|
// index and sort search results
|
|
|
|
const searchResults = [];
|
2021-01-25 22:06:41 +00:00
|
|
|
try {
|
|
|
|
for (let i = 0; i < searchDb.length; i++) {
|
2021-01-29 19:11:56 +00:00
|
|
|
const item = searchDb[i][0], itemExp = searchDb[i][1];
|
|
|
|
if (checkBool(searchFilterField.output(item, itemExp))) {
|
|
|
|
searchResults.push({ item, itemExp, sortKeys: searchSortField.output(item, itemExp) });
|
|
|
|
}
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2021-01-27 04:18:16 +00:00
|
|
|
searchFilterField.errorText.innerText = e.message;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (searchResults.length === 0) {
|
|
|
|
itemListFooter.innerText = 'No results!';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2021-01-29 19:11:56 +00:00
|
|
|
searchResults.sort((a, b) => compareLexico(a.item, a.sortKeys, b.item, b.sortKeys));
|
2021-01-27 04:18:16 +00:00
|
|
|
} catch (e) {
|
|
|
|
searchSortField.errorText.innerText = e.message;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// display search results
|
|
|
|
const searchMax = Math.min(searchResults.length, ITEM_LIST_SIZE);
|
|
|
|
for (let i = 0; i < searchMax; i++) {
|
2021-01-29 19:11:56 +00:00
|
|
|
const result = searchResults[i];
|
2021-01-27 04:18:16 +00:00
|
|
|
itemEntries[i].style.display = 'inline-block';
|
2021-01-29 19:11:56 +00:00
|
|
|
displayExpandedItem(result.itemExp, `item-entry-${i}`);
|
|
|
|
if (result.sortKeys.length > 0) {
|
|
|
|
const sortKeyListContainer = document.createElement('div');
|
|
|
|
sortKeyListContainer.classList.add('itemleft');
|
|
|
|
const sortKeyList = document.createElement('ul');
|
|
|
|
sortKeyList.classList.add('itemp', 'T0');
|
|
|
|
sortKeyList.style.marginLeft = '1.75em';
|
|
|
|
sortKeyListContainer.append(sortKeyList);
|
|
|
|
for (let j = 0; j < result.sortKeys.length; j++) {
|
|
|
|
const sortKeyElem = document.createElement('li');
|
|
|
|
sortKeyElem.innerText = stringify(result.sortKeys[j]);
|
|
|
|
sortKeyList.append(sortKeyElem);
|
|
|
|
}
|
|
|
|
itemEntries[i].append(sortKeyListContainer);
|
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
if (searchMax < searchResults.length) {
|
|
|
|
itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// updates the search state from the input boxes after a brief delay, to prevent excessive DOM updates
|
2021-01-25 22:06:41 +00:00
|
|
|
let updateSearchTask = null;
|
2021-01-27 04:18:16 +00:00
|
|
|
function scheduleSearchUpdate() {
|
2021-01-25 22:06:41 +00:00
|
|
|
if (updateSearchTask !== null) {
|
|
|
|
clearTimeout(updateSearchTask);
|
|
|
|
}
|
|
|
|
updateSearchTask = setTimeout(() => {
|
|
|
|
updateSearchTask = null;
|
|
|
|
updateSearch();
|
2021-01-27 04:18:16 +00:00
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
searchFilterField.field.addEventListener('input', e => scheduleSearchUpdate());
|
|
|
|
searchSortField.field.addEventListener('input', e => scheduleSearchUpdate());
|
2021-01-25 22:36:09 +00:00
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// display initial items and focus the filter field
|
|
|
|
updateSearch();
|
|
|
|
searchFilterField.field.focus();
|
|
|
|
searchFilterField.field.select();
|
2021-01-23 16:38:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
load_init(init);
|