Replace wynntils search with general expression parser + segregated filtering and sorting

This commit is contained in:
phantamanta44 2021-01-26 22:18:16 -06:00
parent 4accec5be7
commit 2adb962b53
3 changed files with 521 additions and 630 deletions

View file

@ -22,12 +22,19 @@
</div>
<div class="center" id="main" style="padding: 2%">
<div id="search-container" style="margin-bottom: 1.5%">
<div class="left" id="search" style="display: inline-block">
<label for="search-field">Search Items:</label>
<div class="left" id="search-filter" style="display: inline-block; vertical-align: top">
<label for="search-filter-field">Filter By:</label>
<br>
<input id="search-field" type="text" style="width: 50vw; padding: 8px">
<input id="search-filter-field" type="text" placeholder="str >= 15 && dex >= 10" style="width: 25vw; padding: 8px">
<br>
<div id="search-error" style="color: #ff0000"></div>
<div id="search-filter-error" style="color: #ff0000"></div>
</div>
<div class="left" id="search-sort" style="display: inline-block; vertical-align: top">
<label for="search-sort-field">Sort By:</label>
<br>
<input id="search-sort-field" type="text" placeholder="str + dex, mdraw + sdraw" style="width: 25vw; padding: 8px">
<br>
<div id="search-sort-error" style="color: #ff0000"></div>
</div>
</div>
<div id="item-list-container">

132
items.js
View file

@ -1,16 +1,41 @@
// 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;
}
}
}
function init() {
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
const searchField = document.getElementById('search-field');
const searchError = document.getElementById('search-error');
const itemList = document.getElementById('item-list');
const itemListFooter = document.getElementById('item-list-footer');
// compile the search db from the item db
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
// init item list elements
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}`);
itemElem.style.display = 'none';
itemElem.style.width = '20vw';
itemElem.style.margin = '1vw';
itemElem.style.verticalAlign = 'top';
@ -18,50 +43,97 @@ function init() {
itemEntries.push(itemElem);
}
let currentSearchStr = null;
// 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);
return function(a, ae, b, be) {
for (let i = 0; i < subExprs.length; i++) {
let aKey = subExprs[i](a, ae), bKey = subExprs[i](b, be);
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 b.lvl - a.lvl;
}
});
// updates the current search state from the search query input boxes
function updateSearch() {
if (searchField.value === currentSearchStr) return;
currentSearchStr = searchField.value;
// hide old search results
itemListFooter.innerText = '';
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 = [];
try {
for (const itemEntry of itemEntries) itemEntry.style.display = 'none';
const searchState = ItemSearchState.parseSearchString(currentSearchStr);
const searchResults = [];
for (let i = 0; i < searchDb.length; i++) {
if (searchState.test(searchDb[i][0], searchDb[i][1])) searchResults.push(searchDb[i]);
if (checkBool(searchFilterField.output(searchDb[i][0], searchDb[i][1]))) searchResults.push(searchDb[i]);
}
if (searchResults.length === 0) {
itemListFooter.innerText = 'No results!';
} else {
searchResults.sort((a, b) => searchState.compare(a[0], a[1], b[0], b[1]));
const searchMax = Math.min(searchResults.length, ITEM_LIST_SIZE);
for (let i = 0; i < searchMax; i++) {
itemEntries[i].style.display = 'inline-block';
displayExpandedItem(searchResults[i][1], `item-entry-${i}`);
}
if (searchMax < searchResults.length) {
itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
}
}
searchError.innerText = '';
} catch (e) {
searchError.innerText = e.message;
searchFilterField.errorText.innerText = e.message;
return;
}
if (searchResults.length === 0) {
itemListFooter.innerText = 'No results!';
return;
}
try {
searchResults.sort((a, b) => searchSortField.output(a[0], a[1], b[0], b[1]));
} 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++) {
itemEntries[i].style.display = 'inline-block';
displayExpandedItem(searchResults[i][1], `item-entry-${i}`);
}
if (searchMax < searchResults.length) {
itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
}
}
// updates the search state from the input boxes after a brief delay, to prevent excessive DOM updates
let updateSearchTask = null;
searchField.addEventListener('input', e => {
function scheduleSearchUpdate() {
if (updateSearchTask !== null) {
clearTimeout(updateSearchTask);
}
updateSearchTask = setTimeout(() => {
updateSearchTask = null;
updateSearch();
}, 300);
});
updateSearch();
}, 500);
}
searchFilterField.field.addEventListener('input', e => scheduleSearchUpdate());
searchSortField.field.addEventListener('input', e => scheduleSearchUpdate());
searchField.focus();
// display initial items and focus the filter field
updateSearch();
searchFilterField.field.focus();
searchFilterField.field.select();
}
load_init(init);

1004
query.js

File diff suppressed because it is too large Load diff