Searching Changes (#248)

* fix sort keys not clearing

* ingredient searching

* add dura and charges

* modify comment

* fix effectiveness formula)

* remove comment
This commit is contained in:
Incompleteusern 2023-01-30 19:55:25 -07:00 committed by GitHub
parent 88fe5217d2
commit bf29018a60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1476 additions and 750 deletions

View file

@ -16,41 +16,49 @@
/* rarity selectors */
#rarity-box {
#tier-box {
cursor: pointer;
width: 58.33%;
text-align: center;
}
#rarity-box > div {
#tier-box > div {
width: calc(100% / 7);
aspect-ratio: 1/1;
position: relative;
display: inline-block;
}
#rarity-box > div > b {
#tier-box > div > b {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#rarity-box > div.rarity-selected > b {
#tier-box > div.tier-selected > b {
color: black;
}
#rarity-normal { color: #FFFFFF; }
#rarity-normal.rarity-selected { background-color: #FFFFFF; }
#rarity-unique { color: #FFFF55; }
#rarity-unique.rarity-selected { background-color: #FFFF55; }
#rarity-set { color: #55FF55; }
#rarity-set.rarity-selected { background-color: #55FF55; }
#rarity-rare { color: #FF55FF; }
#rarity-rare.rarity-selected { background-color: #FF55FF; }
#rarity-legendary { color: #55FFFF; }
#rarity-legendary.rarity-selected { background-color: #55FFFF; }
#rarity-fabled { color: #FF5555; }
#rarity-fabled.rarity-selected { background-color: #FF5555; }
#rarity-mythic { color: #AA00AA; }
#rarity-mythic.rarity-selected { background-color: #AA00AA; }
#tier-normal { color: #FFFFFF; }
#tier-normal.tier-selected { background-color: #FFFFFF; }
#tier-unique { color: #FFFF55; }
#tier-unique.tier-selected { background-color: #FFFF55; }
#tier-set { color: #55FF55; }
#tier-set.tier-selected { background-color: #55FF55; }
#tier-rare { color: #FF55FF; }
#tier-rare.tier-selected { background-color: #FF55FF; }
#tier-legendary { color: #55FFFF; }
#tier-legendary.tier-selected { background-color: #55FFFF; }
#tier-fabled { color: #FF5555; }
#tier-fabled.tier-selected { background-color: #FF5555; }
#tier-mythic { color: #AA00AA; }
#tier-mythic.tier-selected { background-color: #AA00AA; }
#tier-zero { color: #FFFFFF; }
#tier-zero.tier-selected { background-color: #FFFFFF; }
#tier-one { color: #FFFFBB; }
#tier-one.tier-selected { background-color: #FFFFBB; }
#tier-two { color: #FFFF88; }
#tier-two.tier-selected { background-color: #FFFF88; }
#tier-three { color: #FFFF55; }
#tier-three.tier-selected { background-color: #FFFF55; }
/* filters */
.filter-row {

174
ingredients/index.html Normal file
View file

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html scroll-behavior="smooth">
<head>
<title>WynnAtlas</title>
<link rel="icon" href="../media/icons/new/searcher.png" type="image/icon type">
<meta name="viewport" content="width=device-width, initial-scale=.45, user-scalable=no">
<!-- nunito font, copying wynnbuilder, which is copying wynndata -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/css/autoComplete.min.css">
<link rel="stylesheet" href="../css/sq2bs.css">
<link rel="stylesheet" href="../css/sidebar.css">
<link rel="stylesheet" href="../css/wynnstyles.css">
<link rel="stylesheet" href="../css/search.css">
</head>
<body class = "text-light d-flex justify-content-center" id = "body">
<div id="main-sidebar" class="sidebar dark-7 dark-shadow">
<a href = "../builder/"><img src="../media/icons/new/builder.png" alt = "WynnBuilder" title = "WynnBuilder"><b>WynnBuilder</b></a>
<a href = "../crafter/"><img src = "../media/icons/new/crafter.png" alt = "WynnCrafter" title = "WynnCrafter"><b>WynnCrafter</b></a>
<a href = "../items/"><img src = "../media/icons/new/searcher.png" alt = "WynnAtlas" title = "WynnAtlas"><b>WynnAtlas</b></a>
<a href = "../custom/"><img src = "../media/icons/new/custom.png" alt = "WynnCustom" title = "WynnCustom"><b>WynnCustom</b></a>
<a href = "../map/"><img src = "../media/icons/new/compass.png" alt = "WynnGPS" title = "WynnGPS"><b>WynnGPS</b></a>
<a href = "../wynnfo/"><img src = "../media/icons/new/book.png" alt = "Wynnfo" title = "Wynnfo"><b>Wynnfo</b></a>
<a onclick = "toggleIcons()"><img src = "../media/icons/new/reload.png" alt = "" title = "Swap items on page"><b>Swap Icon Style</b></a>
<hr/>
<a href = "https://discord.gg/CGavnAnerv" target = "_blank"><img src = "../media/icons/discord.png" alt = "WB Discord" title = "WB Discord"><b>WB Discord</b></a>
</div>
<div id="mobile-navbar" class="navbar dark-5 dark-shadow fixed-top d-lg-none pb-0">
<div class="container-fluid scaled-font justify-content-center" style="height: 5vh;">
<button class="btn dropdown-toggle dark-2 px-4 text-white scaled-font border-dark border-3" onclick="toggle_tab('mobile-navbar-dropdown');"></button>
</div>
<div class="container-fluid scaled-font dark-3 px-3 py-3" id="mobile-navbar-dropdown" style="display: none;">
<a href="../builder/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/builder.png" alt="" style="height: 100%;">
<span>WynnBuilder</span>
</a>
<a href="../crafter/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/crafter.png" alt="" style="height: 100%;">
<span>WynnCrafter</span>
</a>
<a href="../items/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/searcher.png" alt="" style="height: 100%;">
<span>WynnAtlas</span>
</a>
<a href="../custom/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/custom.png" alt="" style="height: 100%;">
<span>WynnCustom</span>
</a>
<a href="../map/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/compass.png" alt="" style="height: 100%;">
<span>WynnGPS</span>
</a>
<a href="../wynnfo/" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/book.png" alt="" style="height: 100%;">
<span>WynnFo</span>
</a>g
<a onclick = "toggleIcons()" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/new/reload.png" alt="" style="height: 100%;">
<span>Swap Icon Style</span>
</a>
<a href="https://discord.gg/CGavnAnerv" class="w-100 mb-3 text-white" style="height: 5vh; text-decoration: none;">
<img src="../media/icons/discord.png" alt="" style="height: 100%;">
<span>Discord</span>
</a>
</div>
</div>
<div class = "container py-5 vh-100 mx-0 mx-lg-auto scaled-font">
<div class = "row">
<div class = "row" style = "margin-top: 3ch;">
<div class = "col text-start" id = "credits">
<a href="../items/" class="link">Item Searching</a>
</div>
<div class = "col text-end">
<a href = "../ingredients_adv/">Advanced Ingredient Search</a>
</div>
</div>
<div class = "col-auto" style = "width: 12.5%;"></div>
<div class = "col-lg-4">
<div class = "row">
<div class = "col-lg col-sm-12">
<div class = "col fw-bold">Name:</div>
<input class = "col border-dark text-light dark-5 rounded scaled-font form-control form-control-sm" style="width: 100%!important;" type="text" id="item-name-choice" name="item-name-choice" placeholder="Ingredient name (case insensitive)"/>
<p class="error col-auto"></p>
</div>
</div>
<div class = "row">
<div class = "col-lg col-sm-12">
<div class = "col"><span class = "fw-bold">Types:</span> <span style = "cursor: pointer; float: right;"><span id = "all-types">All</span> &nbsp; <span id = "none-types">None</span></span></div>
<div id = "type-box">
<p id = "type-armouring" style = "margin: 0; ">Armouring</p>
<p id = "type-tailoring" style = "margin: 0;">Tailoring</p>
<p id = "type-weaponsmithing" style = "margin: 0;">Weaponsmithing</p>
<p id = "type-woodworking" style = "margin: 0;">Woodworking</p>
<p id = "type-jeweling" style = "margin: 0;">Jeweling</p>
<p id = "type-cooking" style = "margin: 0;">Cooking</p>
<p id = "type-alchemism" style = "margin: 0;">Alchemism</p>
<p id = "type-scribing" style = "margin: 0;">Scribing</p>
</div>
<p class="error col-auto"></p>
</div>
</div>
<div class = "row">
<div class = "col-lg col-sm-12">
<div class = "col"><span class = "fw-bold">Stars:</span> <span style = "cursor: pointer; float: right;"><span id = "all-tiers">All</span> &nbsp; <span id = "none-tiers">None</span></span></div>
<div id = "tier-box">
<!-- unfortunately they must be stacked up like this because newlines are considered spaces and muck up the organization -->
<div id = "tier-zero" class = "tier-selected"><b>0</b></div><div id = "tier-one" class = "tier-selected"><b>1</b></div><div id = "tier-two" class = "tier-selected"><b>2</b></div><div id = "tier-three" class = "tier-selected"><b>3</b></div>
</div>
<p class="error col-auto"></p>
</div>
</div>
</div>
<div class = "col-lg-5">
<div id = "filter-container" class = "col">
<div class = "col fw-bold">Filters:</div>
<div class = "row">
<div id = "add-filter" class = "col fw-bold" style = "cursor: pointer; padding-top: 5px;">
+ Add Filter
</div>
</div>
</div>
<br/>
<div id = "exclude-container" class = "col">
<div class = "col fw-bold">Excluded Filters:</div>
<div class = "row">
<div id = "add-exclude" class = "col fw-bold" style = "cursor: pointer; padding-top: 5px;">
+ Add Excluded Filter
</div>
</div>
</div>
</div>
<div class = "row">
<div class = "col-auto" style = "width: 12.5%;"></div>
<div class = "col-auto">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "search-button" onclick = "do_item_search()">
Search!
</button>
</div>
<div class = "col-auto">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "reset-button" onclick = "reset_item_search()">
Reset
</button>
</div>
</div>
<div class = "row box-title justify-content-center" id = "summary">
</div>
<div class = "row" id = "search-results">
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/autoComplete.min.js"></script>
<script type="text/javascript" src="../js/drag_drop_touch.js"></script>
<script type="text/javascript" src="../js/utils.js"></script>
<script type="text/javascript" src="../js/build_utils.js"></script>
<script type="text/javascript" src="../js/icons.js"></script>
<script type="text/javascript" src="../js/damage_calc.js"></script>
<script type="text/javascript" src="../js/display_constants.js"></script>
<script type="text/javascript" src="../js/display.js"></script>
<script type="text/javascript" src="../js/query.js"></script>
<script type="text/javascript" src="../js/expr_parser.js"></script>
<script type="text/javascript" src="../js/load_ing.js"></script>
<script type="text/javascript" src="../js/search.js"></script>
<script type="text/javascript" src="../js/ingredients.js"></script>
<script type="text/javascript" src="../js/powders.js"></script>
</body>
</html>

View file

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html scroll-behavior="smooth">
<head>
<title>WynnAtlas</title>
<link rel="icon" href="../media/icons/new/searcher.png" type="image/icon type">
<meta name="viewport" content="width=device-width, initial-scale=.45, user-scalable=no">
<!-- nunito font, copying wynnbuilder, which is copying wynndata -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/css/autoComplete.min.css">
<link rel="stylesheet" href="../css/sq2bs.css">
<link rel="stylesheet" href="../css/items_adv.css">
<link rel="stylesheet" href="../css/sidebar.css">
<link rel="stylesheet" href="../css/wynnstyles.css">
</head>
<body class = "text-light d-flex justify-content-center" id = "body">
<div id="main-sidebar" class="sidebar dark-7 dark-shadow">
<a href = "../builder/"><img src="../media/icons/new/builder.png" alt = "WynnBuilder" title = "WynnBuilder"><b>WynnBuilder</b></a>
<a href = "../crafter/"><img src = "../media/icons/new/crafter.png" alt = "WynnCrafter" title = "WynnCrafter"><b>WynnCrafter</b></a>
<a href = "../items/"><img src = "../media/icons/new/searcher.png" alt = "WynnAtlas" title = "WynnAtlas"><b>WynnAtlas</b></a>
<a href = "/customizer.html"><img src = "../media/icons/new/custom.png" alt = "WynnCustom" title = "WynnCustom"><b>WynnCustom</b></a>
<a href = "/map.html"><img src = "../media/icons/new/compass.png" alt = "WynnGPS" title = "WynnGPS"><b>WynnGPS</b></a>
<a href = "/wynnfo/index.html"><img src = "../media/icons/new/book.png" alt = "Wynnfo" title = "WynnCrafter"><b>WynnCrafter</b></a>
<a onclick = "toggleIcons()"><img src = "../media/icons/new/reload.png" alt = "" title = "Swap items on page"><b>Swap Icon Style</b></a>
<hr/>
<a href = "https://discord.gg/CGavnAnerv" target = "_blank"><img src = "../media/icons/discord.png" alt = "WB Discord" title = "WB Discord"><b>WB Discord</b></a>
</div>
<div class = "container py-5 vh-100 mx-0 mx-lg-auto scaled-font">
<div class = "col">
<div class = "row">
<div class = "col text-start" id = "credits">
<a href="../credits.txt" class="link">Additional credits</a>
</div>
<div class = "col text-end">
<a href = "../ingredients/">Basic Ingredient Search</a>
</div>
</div>
<div class = "row">
<div class="col" id="main">
<div class = "row" id="search-container">
<div class="col search-field-container" id="search-filter">
<div class="col fw-bold">Filter By:</div>
<input class="col border-dark text-light dark-5 rounded scaled-font form-control form-control-sm" id="search-filter-field" type="text" autofocus="true"
placeholder="name ?= &quot;blue&quot; & str >= 15 & dex >= 10">
<div class="search-field-error" id="search-filter-error"></div>
<div class="search-field-compl" id="search-filter-compl"></div>
</div>
<div class="col search-field-container" id="search-sort">
<div class="col fw-bold">Sort By:</div>
<input class="col border-dark text-light dark-5 rounded scaled-font form-control form-control-sm" id="search-sort-field" type="text"
placeholder="str + dex; meleerawdmg + spellrawdmg">
<div class="search-field-error" id="search-sort-error"></div>
<div class="search-field-compl" id="search-sort-compl"></div>
</div>
</div>
<div class = "row" id="item-list-container">
<div class="row" id="item-list"></div>
<div class="row" id="item-list-footer"></div>
</div>
</div>
</div>
<div class = "row" id="scroll-up">&uparrow;</div>
</div>
</div>
<script type="text/javascript" src="/js/utils.js"></script>
<script type="text/javascript" src="/js/build_utils.js"></script>
<script type="text/javascript" src="/js/icons.js"></script>
<script type="text/javascript" src="/js/powders.js"></script>
<script type="text/javascript" src="/js/damage_calc.js"></script>
<script type="text/javascript" src="/js/display_constants.js"></script>
<script type="text/javascript" src="/js/display.js"></script>
<script type="text/javascript" src="/js/query.js"></script>
<script type="text/javascript" src="/js/expr_parser.js"></script>
<script type="text/javascript" src="/js/load_ing.js"></script>
<script type="text/javascript" src="/js/search_adv.js"></script>
<script type="text/javascript" src="/js/ingredients_adv.js"></script>
</body>
</html>

View file

@ -18,7 +18,7 @@
<link rel="stylesheet" href="../css/sq2bs.css">
<link rel="stylesheet" href="../css/sidebar.css">
<link rel="stylesheet" href="../css/wynnstyles.css">
<link rel="stylesheet" href="../css/items.css">
<link rel="stylesheet" href="../css/search.css">
</head>
<body class = "text-light d-flex justify-content-center" id = "body">
<div id="main-sidebar" class="sidebar dark-7 dark-shadow">
@ -78,6 +78,9 @@
<div class = "container py-5 vh-100 mx-0 mx-lg-auto scaled-font">
<div class = "row">
<div class = "row" style = "margin-top: 3ch;">
<div class = "col text-start" id = "credits">
<a href="../ingredients/" class="link">Ingredient Searching</a>
</div>
<div class = "col text-end">
<a href = "../items_adv/">Advanced Item Search</a>
</div>
@ -113,10 +116,10 @@
</div>
<div class = "row">
<div class = "col-lg col-sm-12">
<div class = "col"><span class = "fw-bold">Rarity:</span> <span style = "cursor: pointer; float: right;"><span id = "all-rarities">All</span> &nbsp; <span id = "none-rarities">None</span></span></div>
<div id = "rarity-box">
<div class = "col"><span class = "fw-bold">Rarity:</span> <span style = "cursor: pointer; float: right;"><span id = "all-tiers">All</span> &nbsp; <span id = "none-tiers">None</span></span></div>
<div id = "tier-box">
<!-- unfortunately they must be stacked up like this because newlines are considered spaces and muck up the organization -->
<div id = "rarity-normal" class = "rarity-selected"><b>N</b></div><div id = "rarity-unique" class = "rarity-selected"><b>U</b></div><div id = "rarity-set" class = "rarity-selected"><b>S</b></div><div id = "rarity-rare" class = "rarity-selected"><b>R</b></div><div id = "rarity-legendary" class = "rarity-selected"><b>L</b></div><div id = "rarity-fabled" class = "rarity-selected"><b>F</b></div><div id = "rarity-mythic" class = "rarity-selected"><b>M</b></div>
<div id = "tier-normal" class = "tier-selected"><b>N</b></div><div id = "tier-unique" class = "tier-selected"><b>U</b></div><div id = "tier-set" class = "tier-selected"><b>S</b></div><div id = "tier-rare" class = "tier-selected"><b>R</b></div><div id = "tier-legendary" class = "tier-selected"><b>L</b></div><div id = "tier-fabled" class = "tier-selected"><b>F</b></div><div id = "tier-mythic" class = "tier-selected"><b>M</b></div>
</div>
<p class="error col-auto"></p>
</div>
@ -172,6 +175,7 @@
<script type="text/javascript" src="../js/query.js"></script>
<script type="text/javascript" src="../js/expr_parser.js"></script>
<script type="text/javascript" src="../js/load.js"></script>
<script type="text/javascript" src="../js/search.js"></script>
<script type="text/javascript" src="../js/items.js"></script>
<script type="text/javascript" src="../js/powders.js"></script>
</body>

View file

@ -84,6 +84,7 @@
<script type="text/javascript" src="/js/query.js"></script>
<script type="text/javascript" src="/js/expr_parser.js"></script>
<script type="text/javascript" src="/js/load.js"></script>
<script type="text/javascript" src="/js/search_adv.js"></script>
<script type="text/javascript" src="/js/items_adv.js"></script>
</body>
</html>

View file

@ -178,6 +178,8 @@ let rolledIDs = [
];
let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ];
let ingFields = ["fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "lq", "ref", "str", "dex", "int", "agi", "def", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "jh", "sprint", "sprintReg", "gXp", "gSpd"];
/**
* Take an item with id list and turn it into a set of minrolls and maxrolls.
*/

View file

@ -1,6 +1,5 @@
let recipeTypes = ["HELMET","CHESTPLATE","LEGGINGS","BOOTS","RELIK","WAND","SPEAR","DAGGER","BOW","RING","NECKLACE","BRACELET","SCROLL","FOOD","POTION"];
let levelTypes = ["1-3","3-5","5-7","7-9","10-13","13-15","15-17","17-19","20-23","23-25","25-27","27-29","30-33","33-35","35-37","37-39","40-43","43-45","45-47","47-49","50-53","53-55","55-57","57-59","60-63","63-65","65-67","67-69","70-73","73-75","75-77","77-79","80-83","83-85","85-87","87-89","90-93","93-95","95-97","97-99","100-103","103-105",]
let ingFields = ["fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "lq", "ref", "str", "dex", "int", "agi", "def", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "jh", "sprint", "sprintReg", "gXp", "gSpd"];
function encodeCraft(craft) {
if (craft) {

163
js/ingredients.js Normal file
View file

@ -0,0 +1,163 @@
// commented out filters
// "Name": "name",
// "Display Name": "displayName",
// "Tier": "stars",
// "Powder Slots": "slots",
// "Health": "hp",
// "Raw Fire Defense": "fDef",
// "Raw Water Defense": "wDef",
//"Raw Air Defense": "aDef",
// "Raw Thunder Defense": "tDef",
// "Raw Earth Defense": "eDef",
const translate_mappings = {
"Durability": "durability",
"Duration": "duration",
"Charges": "charges",
"Effectiveness Left": "left",
"Effectiveness Right": "right",
"Effectiveness Above": "above",
"Effectiveness Under": "under",
"Effectiveness Touching": "touching",
"Effectiveness Not Touching": "nottouching",
"Combat Level": "lvl",
"Req Strength": "strReq",
"Req Dexterity": "dexReq",
"Req Intelligence": "intReq",
"Req Agility": "agiReq",
"Req Defense": "defReq",
"% Health Regen": "hprPct",
"Mana Regen": "mr",
"% Spell Damage": "sdPct",
"% Melee Damage": "mdPct",
"Life Steal": "ls",
"Mana Steal": "ms",
"XP Bonus": "xpb",
"Loot Bonus": "lb",
"Reflection": "ref",
"Strength": "str",
"Dexterity": "dex",
"Intelligence": "int",
"Agility": "agi",
"Defense": "def",
"Thorns": "thorns",
"Exploding": "expd",
"Walk Speed": "spd",
"Attack Speed Bonus": "atkTier",
"Poison": "poison",
"Health Bonus": "hpBonus",
"Soul Point Regen": "spRegen",
"Stealing": "eSteal",
"Raw Health Regen": "hprRaw",
"Raw Spell": "sdRaw",
"Raw Melee": "mdRaw",
"% Fire Damage": "fDamPct",
"% Water Damage": "wDamPct",
"% Air Damage": "aDamPct",
"% Thunder Damage": "tDamPct",
"% Earth Damage": "eDamPct",
"% Fire Defense": "fDefPct",
"% Water Defense": "wDefPct",
"% Air Defense": "aDefPct",
"% Thunder Defense": "tDefPct",
"% Earth Defense": "eDefPct",
"1st Spell Cost %": "-spPct1",
"1st Spell Cost Raw": "-spRaw1",
"2nd Spell Cost %": "-spPct2",
"2nd Spell Cost Raw": "-spRaw2",
"3rd Spell Cost %": "-spPct3",
"3rd Spell Cost Raw": "-spRaw3",
"4th Spell Cost %": "-spPct4",
"4th Spell Cost Raw": "-spRaw4",
"Rainbow Spell Damage Raw": "rainbowRaw",
"Sprint": "sprint",
"Sprint Regen": "sprintReg",
"Jump Height": "jh",
"Loot Quality": "lq",
"Gather XP Bonus": "gXp",
"Gather Speed Bonus": "gSpd"
};
const special_mappings = {
"Sum (skill points)": "str+dex+int+def+agi",
"Sum (Mana Sustain)": "mr+ms",
"Sum (Life Sustain)": "hpr+ls",
"Sum (Effectiveness)": "7/3 * touching + 8/3 * nottouching + 2/3 * (top + bottom) + 1/2 * (left + right)"
};
for (let x in translate_mappings) {
item_filters.push(x);
}
for (let x in special_mappings) {
item_filters.push(x);
}
types = {armouring: false, tailoring: false, weaponsmithing: false, woodworking: false, jeweling: false, cooking: false, alchemism: false, scribing: false};
search_tiers = {zero: true, one: true, two: true, three: true};
function display(ing_copy) {
let ing_parent = document.getElementById("search-results");
for (let i in ing_copy) {
if (i > 200) {break;}
let ing = ing_copy[i].itemExp;
let box = make_elem('div', ['ing-stats', 'col-lg-3', 'p-2', 'col-sm-6'], {id: 'ing'+i});
let bckgrdbox = make_elem('div', ["rounded", "g-0", "dark-7", "border", "border-dark", "dark-shadow", "p-3", "col-auto"], {id: 'ing'+i+'b'});
box.append(bckgrdbox);
ing_parent.appendChild(box);
displayExpandedIngredient(ing, bckgrdbox.id, true);
}
}
function filter_types_tiers(queries) {
// type
let allTypes = true, noTypes = true;
let typeQuery = "f:("
for (const type of Object.keys(types)) {
if (types[type]) {
typeQuery += type + "|";
noTypes = false;
} else {
allTypes = false;
}
}
if (noTypes) {
document.getElementById("summary").innerHTML = "Error: Cannot search without at least 1 type selected";
return false;
} else if (!allTypes) {
queries.push(typeQuery.substring(0, typeQuery.length - 1) + ")");
}
// stars
let allStars = true, noStars = true;
let starQuery = "f:("
for (const star of Object.keys(search_tiers)) {
if (search_tiers[star]) {
starQuery += "starsname=\"" + star + "\"|";
noStars = false;
} else {
allStars = false;
}
}
if (noStars) {
document.getElementById("summary").innerHTML = "Error: Cannot search without at least 1 star selected";
return false;
} else if (!allStars) {
queries.push(starQuery.substring(0, starQuery.length - 1) + ")");
}
return true;
}
function init_values() {
search_db = ings.filter( i => ! i.remapID ).map( i => [i, expandIngredient(i, [])] );
expr_parser = new ExprParser(ingredientQueryProps, queryFuncs);
}
(async function() {
await Promise.resolve(load_ing_init());
init_search();
})();

54
js/ingredients_adv.js Normal file
View file

@ -0,0 +1,54 @@
const getQueryIdentifiers = (function() {
let identCache = null;
return function() {
if (identCache === null) {
const idents = new Set();
for (const ident of Object.keys(ingredientQueryProps)) {
idents.add(ident);
}
for (const ident of Object.keys(queryFuncs)) {
idents.add(ident);
}
identCache = [...idents].sort(); // might use a trie optimally, but the set is probably small enough...
}
return identCache;
};
})();
function generateEntries(size, itemList, itemEntries) {
for (let i = 0; i < size; i++) {
const itemElem = document.createElement('div');
itemElem.classList.add('col-lg-3', 'col-sm-6', "p-2", "ing-stats");
// itemElem.setAttribute('id', `item-entry-${i}`);
itemList.append(itemElem);
itemEntries.push(itemElem);
const itemElemContained = document.createElement("div");
itemElemContained.classList.add("dark-7", "rounded", "p-3", "col-auto", "g-0", "border", "border-dark", "dark-shadow");
itemElemContained.setAttribute('id', `item-entry-${i}`);
itemElem.appendChild(itemElemContained);
const sortKeyListContainer = document.createElement('div');
sortKeyListContainer.classList.add('row');
sortKeyListContainer.setAttribute('id', `item-sort-entry-${i}`);
itemEntries[i].append(sortKeyListContainer);
}
}
function init_values() {
// compile the search db from the item db
searchDb = ings.filter(i => !i.remapID).map(i => [i, expandIngredient(i)]);
// create the expression parser
exprParser = new ExprParser(ingredientQueryProps, queryFuncs);
}
function display(itemExp, id) {
displayExpandedIngredient(itemExp, id);
}
(async function() {
await Promise.resolve(load_ing_init());
init_items_adv();
})();

View file

@ -93,7 +93,6 @@ const special_mappings = {
"Base DPS": "(nDam+fDam+wDam+aDam+tDam+eDam) * atkspdmod(atkspd)"
};
let item_filters = [];
for (let x in translate_mappings) {
item_filters.push(x);
}
@ -101,18 +100,17 @@ for (let x in special_mappings) {
item_filters.push(x);
}
let item_categories = ["armor", "accessory", "weapon"];
types = {bow: false, spear: false, wand: false, dagger: false, relik: false, helmet: false, chestplate: false, leggings: false, boots: false, ring: false, bracelet: false, necklace: false};
search_tiers = {normal: true, unique: true, set: true, rare: true, legendary: true, fabled: true, mythic: true};
const types = {bow: false, spear: false, wand: false, dagger: false, relik: false, helmet: false, chestplate: false, leggings: false, boots: false, ring: false, bracelet: false, necklace: false};
const rarities = {normal: true, unique: true, set: true, rare: true, legendary: true, fabled: true, mythic: true};
const filters = [], excludes = [];
let filter_id_counter = 0;
function displayItems(items_copy) {
function display(items_copy) {
let items_parent = document.getElementById("search-results");
for (let i in items_copy) {
if (i > 200) {break;}
let item = items_copy[i].itemExp;
let box = make_elem('div', ['col-lg-3', 'col-sm-6', 'p-2'], {id: 'item'+i});
let bckgrdbox = make_elem("div", ["dark-7", "rounded", "px-2", "col-auto"], {id: 'item'+i+'b'});
@ -126,19 +124,7 @@ function displayItems(items_copy) {
}
}
let search_db;
let expr_parser;
function do_item_search() {
document.getElementById("summary").style.color = "red"; // to display errors, changed to white if search successful
window.scrollTo(0, 0);
let queries = [];
// name
if (document.getElementById("item-name-choice").value != "") {
queries.push("f:name?=\"" + document.getElementById("item-name-choice").value.trim() + "\"");
}
function filter_types_tiers(queries) {
// types
let allTypes = true, noTypes = true;
let typeQuery = "f:("
@ -152,7 +138,7 @@ function do_item_search() {
}
if (noTypes) {
document.getElementById("summary").innerHTML = "Error: Cannot search without at least 1 type selected";
return;
return false;
} else if (!allTypes) {
queries.push(typeQuery.substring(0, typeQuery.length - 1) + ")");
}
@ -160,8 +146,8 @@ function do_item_search() {
// rarities
let allRarities = true, noRarities = true;
let rarityQuery = "f:("
for (const rarity of Object.keys(rarities)) {
if (rarities[rarity]) {
for (const rarity of Object.keys(search_tiers)) {
if (search_tiers[rarity]) {
rarityQuery += "tiername=\"" + rarity + "\"|";
noRarities = false;
} else {
@ -170,353 +156,20 @@ function do_item_search() {
}
if (noRarities) {
document.getElementById("summary").innerHTML = "Error: Cannot search without at least 1 rarity selected";
return;
return false;
} else if (!allRarities) {
queries.push(rarityQuery.substring(0, rarityQuery.length - 1) + ")");
}
// filters
for (const filter of filters) {
let min = parseInt(filter.min_elem.value);
let max = parseInt(filter.max_elem.value);
if (min > max) {
document.getElementById("summary").innerHTML = "Error: The minimum of filter " + filter.input_elem.value + " (" + min + ") is greater than its maximum (" + max + ")";
return;
}
let zero_in_min_max = (isNaN(min) || min < 0) && (isNaN(max) || max > 0);
let raw_name = filter.input_elem.value;
if (raw_name == "") {
continue; // empty
}
let filter_name = translate_mappings[raw_name];
if (filter_name === undefined) {
filter_name = special_mappings[raw_name];
if (filter_name === undefined) {
document.getElementById("summary").innerHTML = "Error: The filter \"" + filter.input_elem.value + "\" is not recognized";
return;
}
filter_name = "(" + filter_name + ")";
}
if (!isNaN(min)) {
queries.push("f:" + filter_name + ">=" + min);
}
if (!isNaN(max)) {
queries.push("f:" + filter_name + "<=" + max);
}
if (zero_in_min_max) {
queries.push("f:" + filter_name + "!=0");
}
queries.push("s:" + (filter.ascending ? "0-" : "") + filter_name);
}
// excludes
for (const exclude of excludes) {
let raw_name = exclude.input_elem.value;
if (raw_name == "") {
continue; // empty
}
let filter_name = translate_mappings[raw_name];
if (filter_name === undefined) {
filter_name = special_mappings[raw_name];
if (filter_name === undefined) {
document.getElementById("summary").innerHTML = "Error: The excluded filter \"" + exclude.input_elem.value + "\" is not recognized";
return;
}
filter_name = "(" + filter_name + ")";
}
queries.push("f:" + filter_name + "=0");
}
let filter_query = "true";
let sort_queries = [];
console.log(queries);
for (const query of queries) {
if (query.startsWith("s:")) {
sort_queries.push(query.slice(2));
}
else if (query.startsWith("f:")) {
filter_query = filter_query + "&" + query.slice(2);
}
}
document.getElementById("search-results").textContent = "";
let results = [];
try {
const filter_expr = expr_parser.parse(filter_query);
const sort_exprs = sort_queries.map(q => expr_parser.parse(q));
for (let i = 0; i < search_db.length; ++i) {
const item = search_db[i][0];
const itemExp = search_db[i][1];
if (checkBool(filter_expr.resolve(item, itemExp))) {
results.push({ item, itemExp, sortKeys: sort_exprs.map(e => e.resolve(item, itemExp)) });
}
}
results.sort((a, b) => {
return compareLexico(a.item, a.sortKeys, b.item, b.sortKeys);
});
} catch (e) {
document.getElementById("summary").textContent = e.message;
return;
}
document.getElementById("summary").textContent = results.length + " results:";
document.getElementById("summary").style.color = "white";
displayItems(results);
return true;
}
function init_items() {
function init_values() {
search_db = items.filter( i => ! i.remapID ).map( i => [i, expandItem(i, [])] );
expr_parser = new ExprParser(itemQueryProps, itemQueryFuncs);
// init type buttons
for (const type of Object.keys(types)) {
document.getElementById("type-" + type).addEventListener("click", function() {
types[type] = !types[type];
this.classList.toggle("type-selected");
});
}
document.getElementById("all-types").addEventListener("click", function() {
for (const type of Object.keys(types)) {
types[type] = true;
document.getElementById("type-" + type).classList.add("type-selected");
}
});
document.getElementById("none-types").addEventListener("click", function() {
for (const type of Object.keys(types)) {
types[type] = false;
document.getElementById("type-" + type).classList.remove("type-selected");
}
});
// init rarity buttons
for (const rarity of Object.keys(rarities)) {
document.getElementById("rarity-" + rarity).addEventListener("click", function() {
rarities[rarity] = !rarities[rarity];
this.classList.toggle("rarity-selected");
});
}
document.getElementById("all-rarities").addEventListener("click", function() {
for (const rarity of Object.keys(rarities)) {
rarities[rarity] = true;
document.getElementById("rarity-" + rarity).classList.add("rarity-selected");
}
});
document.getElementById("none-rarities").addEventListener("click", function() {
for (const rarity of Object.keys(rarities)) {
rarities[rarity] = false;
document.getElementById("rarity-" + rarity).classList.remove("rarity-selected");
}
});
// filters
document.getElementById("add-filter").addEventListener("click", create_filter);
document.getElementById("add-exclude").addEventListener("click", create_exclude);
create_filter();
filters[0].input_elem.value = "Combat Level";
init_filter_drag();
}
function reset_item_search() {
document.getElementById("item-name-choice").value = "";
document.getElementById("all-types").click();
document.getElementById("all-rarities").click();
}
function create_filter() {
let data = {ascending: false};
let row = make_elem("div", ["row", "filter-row"], {});
let col = make_elem("div", ["col"], {});
row.appendChild(col);
data.div = row;
let reorder_img = make_elem("img", ["reorder-filter"], {src: "../media/icons/3-lines.svg", draggable: "true"});
col.appendChild(reorder_img);
let filter_input = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "filter-input"],
{id: "filter-input-" + filter_id_counter, type: "text", placeholder: "Filter"}
);
filter_id_counter++;
col.appendChild(filter_input);
data.input_elem = filter_input;
let asc_desc = make_elem("div", [], {style: "cursor: pointer; display: inline-block;"});
asc_desc.appendChild(make_elem("img", ["desc-icon", "asc-sel"], {src: "../media/icons/triangle.svg"}));
asc_desc.appendChild(make_elem("img", ["asc-icon"], {src: "../media/icons/triangle.svg"}));
asc_desc.addEventListener("click", function() {
data.ascending = !data.ascending;
asc_desc.children[0].classList.toggle("asc-sel");
asc_desc.children[1].classList.toggle("asc-sel");
});
col.appendChild(asc_desc);
let min = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "min-max-input"],
{type: "number", placeholder: "-\u221E"}
);
col.appendChild(min);
data.min_elem = min;
let to = make_elem("span", [], {innerHTML: "&nbsp;to&nbsp;"});
col.appendChild(to);
let max = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "min-max-input"],
{type: "number", placeholder: "\u221E"}
);
col.appendChild(max);
data.max_elem = max;
let trash = make_elem("img", ["delete-filter"], {src: "../media/icons/trash.svg"});
trash.addEventListener("click", function() {
filters.splice(Array.from(row.parentElement.children).indexOf(row) - 1, 1);
row.remove();
});
col.appendChild(trash);
document.getElementById("filter-container").insertBefore(row, document.getElementById("add-filter").parentElement);
filters.push(data);
init_filter_dropdown(data);
}
let currently_dragging = null;
function init_filter_drag() {
let container = document.getElementById("filter-container");
container.addEventListener("dragstart", function(e) {
if (e.path[0].classList.contains("reorder-filter")) {
currently_dragging = filters[Array.from(e.path[3].children).indexOf(e.path[2]) - 1];
} else {
e.preventDefault();
}
});
container.addEventListener("dragenter", function(e) {
e.preventDefault();
});
container.addEventListener("dragleave", function(e) {
e.preventDefault();
});
container.addEventListener("dragend", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
currently_dragging = null;
});
container.addEventListener("dragover", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
if (!e.path.includes(currently_dragging.div)) {
for (let i = 0; i < e.path.length; i++) {
if (e.path[i].classList.contains("filter-row")) {
e.path[i].classList.add("filter-dragged-over");
break;
}
}
}
});
container.addEventListener("drop", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
if (!e.path.includes(currently_dragging.div)) {
for (let i = 0; i < e.path.length; i++) {
if (e.path[i].classList.contains("filter-row")) {
let old_index = filters.indexOf(currently_dragging);
let new_index = Array.from(e.path[i + 1].children).indexOf(e.path[i]) - 1;
filters.splice(old_index, 1);
filters.splice(new_index, 0, currently_dragging);
currently_dragging.div.remove();
container.insertBefore(currently_dragging.div, container.children[new_index + 1]);
break;
}
}
}
currently_dragging = null;
});
}
function create_exclude() {
let data = {};
let row = make_elem("div", ["row", "filter-row"], {});
let col = make_elem("div", ["col"], {});
row.appendChild(col);
data.div = row;
let filter_input = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "filter-input"],
{id: "filter-input-" + filter_id_counter, type: "text", placeholder: "Excluded Filter"}
);
filter_id_counter++;
col.appendChild(filter_input);
data.input_elem = filter_input;
let trash = make_elem("img", ["delete-filter"], {src: "../media/icons/trash.svg"});
trash.addEventListener("click", function() {
excludes.splice(Array.from(row.parentElement.children).indexOf(row) - 1, 1);
row.remove();
});
col.appendChild(trash);
document.getElementById("exclude-container").insertBefore(row, document.getElementById("add-exclude").parentElement);
excludes.push(data);
init_filter_dropdown(data);
}
function init_filter_dropdown(filter) {
let field_choice = filter.input_elem;
field_choice.onclick = function() {field_choice.dispatchEvent(new Event('input', {bubbles:true}));};
filter.autoComplete = new autoComplete({
data: {
src: item_filters,
},
threshold: 0,
selector: "#" + field_choice.id,
wrapper: false,
resultsList: {
maxResults: 100,
tabSelect: true,
noResults: true,
class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm",
element: (list, data) => {
let position = field_choice.getBoundingClientRect();
list.style.top = position.bottom + window.scrollY +"px";
list.style.left = position.x+"px";
list.style.width = position.width+"px";
list.style.maxHeight = position.height * 4 +"px";
if (!data.results.length) {
const message = make_elem('li', ['scaled-font'], {textContent: "No results found!"});
list.prepend(message);
};
},
},
resultItem: {
class: "scaled-font search-item",
selected: "dark-5",
},
events: {
input: {
selection: (event) => {
if (event.detail.selection.value) {
event.target.value = event.detail.selection.value;
};
},
},
}
});
expr_parser = new ExprParser(itemQueryProps, queryFuncs);
}
(async function() {
await Promise.resolve(load_init());
init_items();
init_search();
})();

View file

@ -1,103 +1,3 @@
function isIdentifierChar(character) {
return /[\w\d%]/i.test(character);
}
function isIdentifierFirstChar(character) {
return /\w/i.test(character);
}
class AutocompleteContext {
constructor(field) {
this.field = field;
this.text = field.value;
this.cursorPos = this.startIndex = this.endIndex = field.selectionEnd;
while (this.startIndex > 0 && isIdentifierChar(this.text.charAt(this.startIndex - 1))) {
--this.startIndex;
}
if (!isIdentifierFirstChar(this.text.charAt(this.startIndex))) {
this.startIndex = this.cursorPos;
return;
}
while (this.endIndex < this.text.length && isIdentifierChar(this.text.charAt(this.endIndex))) {
++this.endIndex;
}
}
get valid() {
return this.endIndex > this.startIndex;
}
get complText() {
return this.text.substring(this.startIndex, this.cursorPos);
}
insert(completion, supplant) {
this.field.setRangeText(completion, this.startIndex, supplant ? this.endIndex : this.cursorPos, 'end');
this.field.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
this.startIndex = this.endIndex = -1;
setTimeout(() => this.field.focus(), 5); // no idea why i need a delay here
}
}
class AutocompleteController {
constructor(ctx, completions, exprField) {
this.ctx = ctx;
this.completions = completions;
this.exprField = exprField;
this.currentFocus = null;
for (const completion of completions) {
const complElem = document.createElement('div');
complElem.classList.add('search-field-compl-entry')
complElem.setAttribute('data-compl', completion);
complElem.innerText = completion;
complElem.addEventListener('mousemove', e => this.focus(complElem));
complElem.addEventListener('mousedown', e => this.complete(completion, true));
exprField.completions.append(complElem);
}
}
get valid() {
return this.ctx.valid && this.completions.length > 0;
}
focus(complElem) {
if (this.currentFocus !== null) {
this.currentFocus.classList.remove('focused');
}
this.currentFocus = complElem;
complElem.classList.add('focused');
complElem.scrollIntoView({ block: 'nearest' });
}
focusNext() {
if (this.currentFocus === null || !this.currentFocus.nextSibling) {
this.focus(this.exprField.completions.firstChild);
} else {
this.focus(this.currentFocus.nextSibling);
}
}
focusPrev() {
if (this.currentFocus === null || !this.currentFocus.previousSibling) {
this.focus(this.exprField.completions.lastChild);
} else {
this.focus(this.currentFocus.previousSibling);
}
}
complete(completion, supplant) {
if (completion === null) {
completion = this.currentFocus.getAttribute('data-compl');
if (completion === null) {
return;
}
}
this.ctx.insert(completion, supplant);
this.exprField.clearAutocomplete();
}
}
const getQueryIdentifiers = (function() {
let identCache = null;
return function() {
@ -106,7 +6,7 @@ const getQueryIdentifiers = (function() {
for (const ident of Object.keys(itemQueryProps)) {
idents.add(ident);
}
for (const ident of Object.keys(itemQueryFuncs)) {
for (const ident of Object.keys(queryFuncs)) {
idents.add(ident);
}
identCache = [...idents].sort(); // might use a trie optimally, but the set is probably small enough...
@ -115,136 +15,8 @@ const getQueryIdentifiers = (function() {
};
})();
// represents a field containing a query expression string
class ExprField {
constructor(key, compiler) {
this.field = document.getElementById(`search-${key}-field`);
this.completions = document.getElementById(`search-${key}-compl`);
this.errorText = document.getElementById(`search-${key}-error`);
this.prevComplText = null;
this.prevComplPos = null;
this.complCtrl = null;
this.compiler = compiler;
this.output = null;
this.text = null;
this.field.addEventListener('focus', e => this.scheduleAutocomplete());
this.field.addEventListener('change', e => this.scheduleAutocomplete());
this.field.addEventListener('keydown', e => {
if (this.complCtrl !== null && this.complCtrl.valid) {
switch (e.key) {
case 'Up':
case 'ArrowUp':
this.complCtrl.focusPrev();
break;
case 'Down':
case 'ArrowDown':
this.complCtrl.focusNext();
break;
case 'Tab':
this.complCtrl.complete(null, true);
break;
case 'Enter':
this.complCtrl.complete(null, false);
break;
case 'Escape':
this.clearAutocomplete();
break;
default:
this.scheduleAutocomplete();
return;
}
e.preventDefault();
} else {
switch (e.key) {
case 'Spacebar':
case ' ':
if (e.ctrlKey) {
this.autocomplete();
return;
}
break;
}
}
this.scheduleAutocomplete()
});
this.field.addEventListener('mousedown', e => this.scheduleAutocomplete());
this.field.addEventListener('blur', e => this.clearAutocomplete());
}
get value() {
return this.field.value;
}
scheduleAutocomplete() {
setTimeout(() => {
if (this.field.value !== this.prevComplText || this.field.selectionEnd !== this.prevComplPos) {
this.prevComplText = this.field.value;
this.prevComplPos = this.field.selectionEnd;
this.autocomplete();
}
}, 1);
}
autocomplete() {
while (this.completions.lastChild) {
this.completions.removeChild(this.completions.lastChild);
}
const complCtx = new AutocompleteContext(this.field);
if (!complCtx.valid) {
this.clearAutocomplete();
return;
}
const complText = complCtx.complText;
const completions = getQueryIdentifiers().filter(ident => ident.startsWith(complText));
if (completions.length === 0) {
this.clearAutocomplete();
return;
}
this.complCtrl = new AutocompleteController(complCtx, completions, this);
this.complCtrl.focusNext();
this.completions.classList.add('visible');
}
clearAutocomplete() {
this.completions.classList.remove('visible');
this.prevComplText = this.field.value;
this.prevComplPos = this.field.selectionEnd;
this.complCtrl = null;
}
compile() {
if (this.value === this.text) return false;
this.text = this.value;
this.errorText.innerText = '';
try {
this.output = this.compiler(this.text);
} catch (e) {
this.errorText.innerText = e.message;
this.output = null;
}
return true;
}
}
function stringify(v) {
return typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : v;
}
function init_items_adv() {
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++) {
function generateEntries(size, itemList, itemEntries) {
for (let i = 0; i < size; i++) {
const itemElem = document.createElement('div');
itemElem.classList.add('col-lg-3', 'col-sm-auto', "p-2");
// itemElem.setAttribute('id', `item-entry-${i}`);
@ -255,144 +27,29 @@ function init_items_adv() {
itemElemContained.classList.add("dark-7", "rounded", "px-2", "col-auto");
itemElemContained.setAttribute('id', `item-entry-${i}`);
itemElem.appendChild(itemElemContained);
const sortKeyListContainer = document.createElement('div');
sortKeyListContainer.classList.add('row');
sortKeyListContainer.setAttribute('id', `item-sort-entry-${i}`);
itemEntries[i].append(sortKeyListContainer);
}
}
function init_values() {
// compile the search db from the item db
searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i)]);
// create the expression parser
const exprParser = new ExprParser(itemQueryProps, itemQueryFuncs);
exprParser = new ExprParser(itemQueryProps, queryFuncs);
}
// the two search query input boxes
const searchFilterField = new ExprField('filter', function(exprStr) {
const expr = exprParser.parse(exprStr);
return expr !== null ? expr : new BoolLitTerm(true);
});
const searchSortField = new ExprField('sort', function(exprStr) {
const subExprs = exprStr.split(';').map(e => exprParser.parse(e)).filter(f => f != null);
return {
type: 'array',
resolve(i, ie) {
const sortKeys = [];
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k].resolve(i, ie));
return sortKeys;
}
};
});
// updates the current search state from the search query input boxes
function updateSearch() {
// compile query expressions, aborting if nothing has changed or either fails to compile
const changed = searchFilterField.compile() | searchSortField.compile();
if (!changed || searchFilterField.output === null || searchSortField.output === null) return;
// update url query string
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`
+ `?f=${encodeURIComponent(searchFilterField.value)}&s=${encodeURIComponent(searchSortField.value)}`;
window.history.pushState({ path: newUrl }, '', newUrl);
// hide old search results
itemListFooter.innerText = '';
for (const itemEntry of itemEntries) itemEntry.classList.remove('visible');
// index and sort search results
const searchResults = [];
try {
for (let i = 0; i < searchDb.length; i++) {
const item = searchDb[i][0], itemExp = searchDb[i][1];
if (checkBool(searchFilterField.output.resolve(item, itemExp))) {
searchResults.push({ item, itemExp, sortKeys: searchSortField.output.resolve(item, itemExp) });
}
}
} catch (e) {
searchFilterField.errorText.innerText = e.message;
return;
}
if (searchResults.length === 0) {
itemListFooter.innerText = 'No results!';
return;
}
try {
searchResults.sort((a, b) => {
try {
return compareLexico(a.item, a.sortKeys, b.item, b.sortKeys);
} catch (e) {
console.log(a.item, b.item);
throw e;
}
});
} 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++) {
const result = searchResults[i];
itemEntries[i].classList.add('visible');
result.itemExp.set("powders", []);
if (result.itemExp.get("category") == "weapon") {
apply_weapon_powders(result.itemExp);
}
displayExpandedItem(result.itemExp, `item-entry-${i}`);
if (result.sortKeys.length > 0) {
const sortKeyListContainer = document.createElement('div');
sortKeyListContainer.classList.add('row');
const sortKeyList = document.createElement('ul');
sortKeyList.classList.add('item-entry-sort-key', 'itemp', 'T0');
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);
}
}
if (searchMax < searchResults.length) {
itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
}
function display(itemExp, id) {
itemExp.set("powders", []);
if (itemExp.get("category") == "weapon") {
apply_weapon_powders(itemExp);
}
// updates the search state from the input boxes after a brief delay, to prevent excessive DOM updates
let updateSearchTask = null;
function scheduleSearchUpdate() {
if (updateSearchTask !== null) {
clearTimeout(updateSearchTask);
}
updateSearchTask = setTimeout(() => {
updateSearchTask = null;
updateSearch();
}, 500);
}
searchFilterField.field.addEventListener('input', e => scheduleSearchUpdate());
searchSortField.field.addEventListener('input', e => scheduleSearchUpdate());
// parse query string, display initial search results
if (window.location.search.startsWith('?')) {
for (const entryStr of window.location.search.substring(1).split('&')) {
const ndx = entryStr.indexOf('=');
if (ndx !== -1) {
switch (entryStr.substring(0, ndx)) {
case 'f':
searchFilterField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
break;
case 's':
searchSortField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
break;
}
}
}
}
updateSearch();
// focus the query filter text box
searchFilterField.field.focus();
searchFilterField.field.select();
// scroll-to-top button
document.getElementById('scroll-up')
.addEventListener('mousedown', e => scrollTo({ top: 0, behavior: 'smooth' }));
displayExpandedItem(itemExp, id);
}
(async function() {

View file

@ -172,8 +172,174 @@ const itemQueryProps = (function() {
return props;
})();
// properties of ingredients that can be looked up
// each entry is a function `(item, extended item) -> value`
const ingredientQueryProps = (function() {
const props = {};
function prop(names, type, resolve) {
if (Array.isArray(names)) {
for (name of names) {
props[name] = { type, resolve };
}
} else {
props[names] = { type, resolve };
}
}
function typeProp(names, idKey) {
prop(names, 'boolean', (i, ie) => ie.get('skills').filter(i => i === idKey).length > 0);
}
function modProp(names, idKey) {
prop(names, 'boolean', (i, ie) => ie.get('posMods').get(idKey) || 0);
}
function maxId(names, idKey) {
prop(names, 'number', (i, ie) => ie.get("ids").get('maxRolls').get(idKey) || 0);
}
function minId(names, idKey) {
prop(names, 'number', (i, ie) => ie.get("ids").get('minRolls').get(idKey) || 0);
}
function rangeAvg(names, getProp) {
prop(names, 'number', (i, ie) => {
const range = getProp(i, ie);
if (!range) return 0;
const ndx = range.indexOf('-');
return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2;
});
}
function map(names, comps, outType, f) {
return prop(names, outType, (i, ie) => {
const args = [];
for (let k = 0; k < comps.length; k++) args.push(comps[k].resolve(i, ie));
return f.apply(null, args);
});
}
function sum(names, ...comps) {
return map(names, comps, 'number', (...summands) => {
let total = 0;
for (let i = 0; i < summands.length; i++) total += summands[i];
return total;
});
}
prop('name', 'string', (i, ie) => i.displayName || i.name);
const starIndices = { 0: "zero", 1: "one", 2: "two", 3: "three" };
prop(['starsname', 'starsstr', 'tiername', 'tierstr'], 'string', (i, ie) => starIndices[i.tier]);
prop(['stars', 'tier'], 'number', (i, ie) => i.tier);
prop(['level', 'lvl', 'combatlevel', 'combatlvl'], 'number', (i, ie) => i.lvl);
// TODO allow specification of item types
for (const entry of [
["armouring", "helmet", "chestplate"],
["tailoring", "leggings", "boots"],
["weaponsmithing", "spear", "dagger"],
["woodworking", "bow", "relik", "wand"],
["jeweling", "ring", "bracelet", "necklace"],
["cooking", "food"],
["alchemism", "potion"],
["scribing", "scroll"]
]) {
typeProp(entry, entry[0].toUpperCase());
}
for (const entry of [
["left"],
["right"],
["above", "top"],
["under", "bottom"],
["touching", "touch"],
["notTouching", "notTouch"]
]) {
modProp(entry.map(i => i.toLowerCase()), entry[0]);
}
prop(['strmin', 'strreq'], 'number', (i, ie) => i["itemIDs"].strReq);
prop(['dexmin', 'dexreq'], 'number', (i, ie) => i["itemIDs"].dexReq);
prop(['intmin', 'intreq'], 'number', (i, ie) => i["itemIDs"].intReq);
prop(['defmin', 'defreq'], 'number', (i, ie) => i["itemIDs"].defReq);
prop(['agimin', 'agireq'], 'number', (i, ie) => i["itemIDs"].agiReq);
prop(['durability'], 'number', (i, ie) => i["itemIDs"].dura || 0);
prop(['charges'], 'number', (i, ie) => i["consumableIDs"].charges || 0);
prop(['duration'], 'number', (i, ie) => i["consumableIDs"].dura || 0);
sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin);
maxId('str', 'str');
maxId('dex', 'dex');
maxId('int', 'int');
maxId('def', 'def');
maxId('agi', 'agi');
sum(['skillpoints', 'skillpts', 'attributes', 'attrs'], props.str, props.dex, props.int, props.def, props.agi);
maxId(['earthdmg%', 'earthdam%', 'edmg%', 'edam%', 'edampct'], 'eDamPct');
maxId(['thunderdmg%', 'thunderdam%', 'tdmg%', 'tdam%', 'tdampct'], 'tDamPct');
maxId(['waterdmg%', 'waterdam%', 'wdmg%', 'wdam%', 'wdampct'], 'wDamPct');
maxId(['firedmg%', 'firedam%', 'fdmg%', 'fdam%', 'fdampct'], 'fDamPct');
maxId(['airdmg%', 'airdam%', 'admg%', 'adam%', 'adampct'], 'aDamPct');
sum(['sumdmg%', 'sumdam%', 'totaldmg%', 'totaldam%', 'sumdampct', 'totaldampct'], props.edampct, props.tdampct, props.wdampct, props.fdampct, props.adampct);
maxId(['mainatkdmg', 'mainatkdam', 'mainatkdmg%', 'mainatkdam%', 'meleedmg', 'meleedam', 'meleedmg%', 'meleedam%', 'mdpct'], 'mdPct');
maxId(['mainatkrawdmg', 'mainatkrawdam', 'mainatkneutraldmg', 'mainatkneutraldam', 'meleerawdmg', 'meleerawdam', 'meleeneutraldmg', 'meleeneutraldam', 'mdraw'], 'mdRaw');
maxId(['spelldmg', 'spelldam', 'spelldmg%', 'spelldam%', 'sdpct'], 'sdPct');
maxId(['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw'], 'sdRaw');
maxId(['rainbowraw'], 'rSdRaw');
maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'atktier'], 'atkTier');
maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct');
maxId(['thunderdef%', 'tdef%', 'tdefpct'], 'tDefPct');
maxId(['waterdef%', 'wdef%', 'wdefpct'], 'wDefPct');
maxId(['firedef%', 'fdef%', 'fdefpct'], 'fDefPct');
maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct');
sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct);
maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'hpbonus'], 'hpBonus');
maxId(['hpregen', 'hpr', 'hr', 'hprraw'], 'hprRaw');
maxId(['hpregen%', 'hpr%', 'hr%', 'hprpct'], 'hprPct');
maxId(['lifesteal', 'ls'], 'ls');
maxId(['manaregen', 'mr'], 'mr');
maxId(['manasteal', 'ms'], 'ms');
maxId(['walkspeed', 'movespeed', 'ws', 'spd'], 'spd');
maxId('sprint', 'sprint');
maxId(['sprintregen', 'sprintreg'], 'sprintReg');
maxId(['jumpheight', 'jh'], 'jh');
maxId(['spellcost1', 'rawspellcost1', 'spcost1', 'spraw1'], 'spRaw1');
maxId(['spellcost1%', 'spcost1%', 'sppct1'], 'spPct1');
maxId(['spellcost2', 'rawspellcost2', 'spcost2', 'spraw2'], 'spRaw2');
maxId(['spellcost2%', 'spcost2%', 'sppct2'], 'spPct2');
maxId(['spellcost3', 'rawspellcost3', 'spcost3', 'spraw3'], 'spRaw3');
maxId(['spellcost3%', 'spcost3%', 'sppct3'], 'spPct3');
maxId(['spellcost4', 'rawspellcost4', 'spcost4', 'spraw4'], 'spRaw4');
maxId(['spellcost4%', 'spcost4%', 'sppct4'], 'spPct4');
sum(['sumspellcost', 'totalspellcost', 'sumrawspellcost', 'totalrawspellcost', 'sumspcost', 'totalspcost', 'sumspraw', 'totalspraw'], props.spraw1, props.spraw2, props.spraw3, props.spraw4);
sum(['sumspellcost%', 'totalspellcost%', 'sumspcost%', 'totalspcost%', 'sumsppct', 'totalsppct'], props.sppct1, props.sppct2, props.sppct3, props.sppct4);
maxId(['exploding', 'expl', 'expd'], 'expd');
maxId('poison', 'poison');
maxId('thorns', 'thorns');
maxId(['reflection', 'refl', 'ref'], 'ref');
maxId(['soulpointregen', 'spr', 'spregen'], 'spRegen');
maxId(['lootbonus', 'lb'], 'lb');
maxId(['xpbonus', 'xpb', 'xb'], 'xpb');
maxId(['stealing', 'esteal'], 'eSteal');
return props;
})();
// functions that can be called in query expressions
const itemQueryFuncs = {
const queryFuncs = {
max: {
type: 'number',
fn: function(item, itemExp, args) {

365
js/search.js Normal file
View file

@ -0,0 +1,365 @@
let types;
let search_tiers;
const filters = [], excludes = [];
let item_filters = [];
let filter_id_counter = 0;
let search_db;
let expr_parser;
function do_item_search() {
document.getElementById("summary").style.color = "red"; // to display errors, changed to white if search successful
window.scrollTo(0, 0);
let queries = [];
// name
if (document.getElementById("item-name-choice").value != "") {
queries.push("f:name?=\"" + document.getElementById("item-name-choice").value.trim() + "\"");
}
if (!filter_types_tiers(queries)) return;
// filters
for (const filter of filters) {
let min = parseInt(filter.min_elem.value);
let max = parseInt(filter.max_elem.value);
if (min > max) {
document.getElementById("summary").innerHTML = "Error: The minimum of filter " + filter.input_elem.value + " (" + min + ") is greater than its maximum (" + max + ")";
return;
}
let zero_in_min_max = (isNaN(min) || min < 0) && (isNaN(max) || max > 0);
let raw_name = filter.input_elem.value;
if (raw_name == "") {
continue; // empty
}
let filter_name = translate_mappings[raw_name];
if (filter_name === undefined) {
filter_name = special_mappings[raw_name];
if (filter_name === undefined) {
document.getElementById("summary").innerHTML = "Error: The filter \"" + filter.input_elem.value + "\" is not recognized";
return;
}
filter_name = "(" + filter_name + ")";
}
if (!isNaN(min)) {
queries.push("f:" + filter_name + ">=" + min);
}
if (!isNaN(max)) {
queries.push("f:" + filter_name + "<=" + max);
}
if (zero_in_min_max) {
queries.push("f:" + filter_name + "!=0");
}
queries.push("s:" + (filter.ascending ? "0-" : "") + filter_name);
}
// excludes
for (const exclude of excludes) {
let raw_name = exclude.input_elem.value;
if (raw_name == "") {
continue; // empty
}
let filter_name = translate_mappings[raw_name];
if (filter_name === undefined) {
filter_name = special_mappings[raw_name];
if (filter_name === undefined) {
document.getElementById("summary").innerHTML = "Error: The excluded filter \"" + exclude.input_elem.value + "\" is not recognized";
return;
}
filter_name = "(" + filter_name + ")";
}
queries.push("f:" + filter_name + "=0");
}
let filter_query = "true";
let sort_queries = [];
console.log(queries);
for (const query of queries) {
if (query.startsWith("s:")) {
sort_queries.push(query.slice(2));
}
else if (query.startsWith("f:")) {
filter_query = filter_query + "&" + query.slice(2);
}
}
document.getElementById("search-results").textContent = "";
let results = [];
try {
const filter_expr = expr_parser.parse(filter_query);
const sort_exprs = sort_queries.map(q => expr_parser.parse(q));
for (let i = 0; i < search_db.length; ++i) {
const item = search_db[i][0];
const itemExp = search_db[i][1];
if (checkBool(filter_expr.resolve(item, itemExp))) {
results.push({ item, itemExp, sortKeys: sort_exprs.map(e => e.resolve(item, itemExp)) });
}
}
results.sort((a, b) => {
return compareLexico(a.item, a.sortKeys, b.item, b.sortKeys);
});
} catch (e) {
document.getElementById("summary").textContent = e.message;
return;
}
document.getElementById("summary").textContent = results.length + " results:";
document.getElementById("summary").style.color = "white";
display(results);
}
function init_search() {
init_values();
// init type buttons
for (const type of Object.keys(types)) {
document.getElementById("type-" + type).addEventListener("click", function() {
types[type] = !types[type];
this.classList.toggle("type-selected");
});
}
document.getElementById("all-types").addEventListener("click", function() {
for (const type of Object.keys(types)) {
types[type] = true;
document.getElementById("type-" + type).classList.add("type-selected");
}
});
document.getElementById("none-types").addEventListener("click", function() {
for (const type of Object.keys(types)) {
types[type] = false;
document.getElementById("type-" + type).classList.remove("type-selected");
}
});
// init rarity buttons
for (const tier of Object.keys(search_tiers)) {
document.getElementById("tier-" + tier).addEventListener("click", function() {
search_tiers[tier] = !search_tiers[tier];
this.classList.toggle("tier-selected");
});
}
document.getElementById("all-tiers").addEventListener("click", function() {
for (const tier of Object.keys(search_tiers)) {
search_tiers[tier] = true;
document.getElementById("tier-" + tier).classList.add("tier-selected");
}
});
document.getElementById("none-tiers").addEventListener("click", function() {
for (const tier of Object.keys(search_tiers)) {
search_tiers[tier] = false;
document.getElementById("tier-" + tier).classList.remove("tier-selected");
}
});
// filters
document.getElementById("add-filter").addEventListener("click", create_filter);
document.getElementById("add-exclude").addEventListener("click", create_exclude);
create_filter();
filters[0].input_elem.value = "Combat Level";
init_filter_drag();
}
function reset_item_search() {
document.getElementById("item-name-choice").value = "";
document.getElementById("all-types").click();
document.getElementById("all-tiers").click();
}
function create_filter() {
let data = {ascending: false};
let row = make_elem("div", ["row", "filter-row"], {});
let col = make_elem("div", ["col"], {});
row.appendChild(col);
data.div = row;
let reorder_img = make_elem("img", ["reorder-filter"], {src: "../media/icons/3-lines.svg", draggable: "true"});
col.appendChild(reorder_img);
let filter_input = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "filter-input"],
{id: "filter-input-" + filter_id_counter, type: "text", placeholder: "Filter"}
);
filter_id_counter++;
col.appendChild(filter_input);
data.input_elem = filter_input;
let asc_desc = make_elem("div", [], {style: "cursor: pointer; display: inline-block;"});
asc_desc.appendChild(make_elem("img", ["desc-icon", "asc-sel"], {src: "../media/icons/triangle.svg"}));
asc_desc.appendChild(make_elem("img", ["asc-icon"], {src: "../media/icons/triangle.svg"}));
asc_desc.addEventListener("click", function() {
data.ascending = !data.ascending;
asc_desc.children[0].classList.toggle("asc-sel");
asc_desc.children[1].classList.toggle("asc-sel");
});
col.appendChild(asc_desc);
let min = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "min-max-input"],
{type: "number", placeholder: "-\u221E"}
);
col.appendChild(min);
data.min_elem = min;
let to = make_elem("span", [], {innerHTML: "&nbsp;to&nbsp;"});
col.appendChild(to);
let max = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "min-max-input"],
{type: "number", placeholder: "\u221E"}
);
col.appendChild(max);
data.max_elem = max;
let trash = make_elem("img", ["delete-filter"], {src: "../media/icons/trash.svg"});
trash.addEventListener("click", function() {
filters.splice(Array.from(row.parentElement.children).indexOf(row) - 1, 1);
row.remove();
});
col.appendChild(trash);
document.getElementById("filter-container").insertBefore(row, document.getElementById("add-filter").parentElement);
filters.push(data);
init_filter_dropdown(data);
}
let currently_dragging = null;
function init_filter_drag() {
let container = document.getElementById("filter-container");
container.addEventListener("dragstart", function(e) {
if (e.path[0].classList.contains("reorder-filter")) {
currently_dragging = filters[Array.from(e.path[3].children).indexOf(e.path[2]) - 1];
} else {
e.preventDefault();
}
});
container.addEventListener("dragenter", function(e) {
e.preventDefault();
});
container.addEventListener("dragleave", function(e) {
e.preventDefault();
});
container.addEventListener("dragend", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
currently_dragging = null;
});
container.addEventListener("dragover", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
if (!e.path.includes(currently_dragging.div)) {
for (let i = 0; i < e.path.length; i++) {
if (e.path[i].classList.contains("filter-row")) {
e.path[i].classList.add("filter-dragged-over");
break;
}
}
}
});
container.addEventListener("drop", function(e) {
e.preventDefault();
for (const el of document.getElementsByClassName("filter-dragged-over")) {
el.classList.remove("filter-dragged-over");
}
if (!e.path.includes(currently_dragging.div)) {
for (let i = 0; i < e.path.length; i++) {
if (e.path[i].classList.contains("filter-row")) {
let old_index = filters.indexOf(currently_dragging);
let new_index = Array.from(e.path[i + 1].children).indexOf(e.path[i]) - 1;
filters.splice(old_index, 1);
filters.splice(new_index, 0, currently_dragging);
currently_dragging.div.remove();
container.insertBefore(currently_dragging.div, container.children[new_index + 1]);
break;
}
}
}
currently_dragging = null;
});
}
function create_exclude() {
let data = {};
let row = make_elem("div", ["row", "filter-row"], {});
let col = make_elem("div", ["col"], {});
row.appendChild(col);
data.div = row;
let filter_input = make_elem("input",
["col", "border-dark", "text-light", "dark-5", "rounded", "scaled-font", "form-control", "form-control-sm", "filter-input"],
{id: "filter-input-" + filter_id_counter, type: "text", placeholder: "Excluded Filter"}
);
filter_id_counter++;
col.appendChild(filter_input);
data.input_elem = filter_input;
let trash = make_elem("img", ["delete-filter"], {src: "../media/icons/trash.svg"});
trash.addEventListener("click", function() {
excludes.splice(Array.from(row.parentElement.children).indexOf(row) - 1, 1);
row.remove();
});
col.appendChild(trash);
document.getElementById("exclude-container").insertBefore(row, document.getElementById("add-exclude").parentElement);
excludes.push(data);
init_filter_dropdown(data);
}
function init_filter_dropdown(filter) {
let field_choice = filter.input_elem;
field_choice.onclick = function() {field_choice.dispatchEvent(new Event('input', {bubbles:true}));};
filter.autoComplete = new autoComplete({
data: {
src: item_filters,
},
threshold: 0,
selector: "#" + field_choice.id,
wrapper: false,
resultsList: {
maxResults: 100,
tabSelect: true,
noResults: true,
class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm",
element: (list, data) => {
let position = field_choice.getBoundingClientRect();
list.style.top = position.bottom + window.scrollY +"px";
list.style.left = position.x+"px";
list.style.width = position.width+"px";
list.style.maxHeight = position.height * 4 +"px";
if (!data.results.length) {
const message = make_elem('li', ['scaled-font'], {textContent: "No results found!"});
list.prepend(message);
};
},
},
resultItem: {
class: "scaled-font search-item",
selected: "dark-5",
},
events: {
input: {
selection: (event) => {
if (event.detail.selection.value) {
event.target.value = event.detail.selection.value;
};
},
},
}
});
}

393
js/search_adv.js Normal file
View file

@ -0,0 +1,393 @@
function isIdentifierChar(character) {
return /[\w\d%]/i.test(character);
}
function isIdentifierFirstChar(character) {
return /\w/i.test(character);
}
class AutocompleteContext {
constructor(field) {
this.field = field;
this.text = field.value;
this.cursorPos = this.startIndex = this.endIndex = field.selectionEnd;
while (this.startIndex > 0 && isIdentifierChar(this.text.charAt(this.startIndex - 1))) {
--this.startIndex;
}
if (!isIdentifierFirstChar(this.text.charAt(this.startIndex))) {
this.startIndex = this.cursorPos;
return;
}
while (this.endIndex < this.text.length && isIdentifierChar(this.text.charAt(this.endIndex))) {
++this.endIndex;
}
}
get valid() {
return this.endIndex > this.startIndex;
}
get complText() {
return this.text.substring(this.startIndex, this.cursorPos);
}
insert(completion, supplant) {
this.field.setRangeText(completion, this.startIndex, supplant ? this.endIndex : this.cursorPos, 'end');
this.field.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
this.startIndex = this.endIndex = -1;
setTimeout(() => this.field.focus(), 5); // no idea why i need a delay here
}
}
class AutocompleteController {
constructor(ctx, completions, exprField) {
this.ctx = ctx;
this.completions = completions;
this.exprField = exprField;
this.currentFocus = null;
for (const completion of completions) {
const complElem = document.createElement('div');
complElem.classList.add('search-field-compl-entry')
complElem.setAttribute('data-compl', completion);
complElem.innerText = completion;
complElem.addEventListener('mousemove', e => this.focus(complElem));
complElem.addEventListener('mousedown', e => this.complete(completion, true));
exprField.completions.append(complElem);
}
}
get valid() {
return this.ctx.valid && this.completions.length > 0;
}
focus(complElem) {
if (this.currentFocus !== null) {
this.currentFocus.classList.remove('focused');
}
this.currentFocus = complElem;
complElem.classList.add('focused');
complElem.scrollIntoView({ block: 'nearest' });
}
focusNext() {
if (this.currentFocus === null || !this.currentFocus.nextSibling) {
this.focus(this.exprField.completions.firstChild);
} else {
this.focus(this.currentFocus.nextSibling);
}
}
focusPrev() {
if (this.currentFocus === null || !this.currentFocus.previousSibling) {
this.focus(this.exprField.completions.lastChild);
} else {
this.focus(this.currentFocus.previousSibling);
}
}
complete(completion, supplant) {
if (completion === null) {
completion = this.currentFocus.getAttribute('data-compl');
if (completion === null) {
return;
}
}
this.ctx.insert(completion, supplant);
this.exprField.clearAutocomplete();
}
}
// represents a field containing a query expression string
class ExprField {
constructor(key, compiler) {
this.field = document.getElementById(`search-${key}-field`);
this.completions = document.getElementById(`search-${key}-compl`);
this.errorText = document.getElementById(`search-${key}-error`);
this.prevComplText = null;
this.prevComplPos = null;
this.complCtrl = null;
this.compiler = compiler;
this.output = null;
this.text = null;
this.field.addEventListener('focus', e => this.scheduleAutocomplete());
this.field.addEventListener('change', e => this.scheduleAutocomplete());
this.field.addEventListener('keydown', e => {
if (this.complCtrl !== null && this.complCtrl.valid) {
switch (e.key) {
case 'Up':
case 'ArrowUp':
this.complCtrl.focusPrev();
break;
case 'Down':
case 'ArrowDown':
this.complCtrl.focusNext();
break;
case 'Tab':
this.complCtrl.complete(null, true);
break;
case 'Enter':
this.complCtrl.complete(null, false);
break;
case 'Escape':
this.clearAutocomplete();
break;
default:
this.scheduleAutocomplete();
return;
}
e.preventDefault();
} else {
switch (e.key) {
case 'Spacebar':
case ' ':
if (e.ctrlKey) {
this.autocomplete();
return;
}
break;
}
}
this.scheduleAutocomplete()
});
this.field.addEventListener('mousedown', e => this.scheduleAutocomplete());
this.field.addEventListener('blur', e => this.clearAutocomplete());
}
get value() {
return this.field.value;
}
scheduleAutocomplete() {
setTimeout(() => {
if (this.field.value !== this.prevComplText || this.field.selectionEnd !== this.prevComplPos) {
this.prevComplText = this.field.value;
this.prevComplPos = this.field.selectionEnd;
this.autocomplete();
}
}, 1);
}
autocomplete() {
while (this.completions.lastChild) {
this.completions.removeChild(this.completions.lastChild);
}
const complCtx = new AutocompleteContext(this.field);
if (!complCtx.valid) {
this.clearAutocomplete();
return;
}
const complText = complCtx.complText;
const completions = getQueryIdentifiers().filter(ident => ident.startsWith(complText));
if (completions.length === 0) {
this.clearAutocomplete();
return;
}
this.complCtrl = new AutocompleteController(complCtx, completions, this);
this.complCtrl.focusNext();
this.completions.classList.add('visible');
}
clearAutocomplete() {
this.completions.classList.remove('visible');
this.prevComplText = this.field.value;
this.prevComplPos = this.field.selectionEnd;
this.complCtrl = null;
}
compile() {
if (this.value === this.text) return false;
this.text = this.value;
this.errorText.innerText = '';
try {
this.output = this.compiler(this.text);
} catch (e) {
this.errorText.innerText = e.message;
this.output = null;
}
return true;
}
}
function stringify(v) {
return typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : v;
}
function generateEntries(size, itemList, itemEntries) {
for (let i = 0; i < size; i++) {
const itemElem = document.createElement('div');
itemElem.classList.add('col-lg-3', 'col-sm-6', "p-2", "ing-stats");
// itemElem.setAttribute('id', `item-entry-${i}`);
itemList.append(itemElem);
itemEntries.push(itemElem);
const itemElemContained = document.createElement("div");
itemElemContained.classList.add("dark-7", "rounded", "p-3", "col-auto", "g-0", "border", "border-dark", "dark-shadow");
itemElemContained.setAttribute('id', `item-entry-${i}`);
itemElem.appendChild(itemElemContained);
const sortKeyListContainer = document.createElement('div');
sortKeyListContainer.classList.add('row');
sortKeyListContainer.setAttribute('id', `item-sort-entry-${i}`);
itemEntries[i].append(sortKeyListContainer);
}
}
let searchDb;
let exprParser;
function init_items_adv() {
const itemList = document.getElementById('item-list');
const itemListFooter = document.getElementById('item-list-footer');
// init item list elements
const ITEM_LIST_SIZE = 64;
const itemEntries = [];
init_values();
generateEntries(ITEM_LIST_SIZE, itemList, itemEntries);
// the two search query input boxes
const searchFilterField = new ExprField('filter', function(exprStr) {
const expr = exprParser.parse(exprStr);
return expr !== null ? expr : new BoolLitTerm(true);
});
const searchSortField = new ExprField('sort', function(exprStr) {
const subExprs = exprStr.split(';').map(e => exprParser.parse(e)).filter(f => f != null);
return {
type: 'array',
resolve(i, ie) {
const sortKeys = [];
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k].resolve(i, ie));
return sortKeys;
}
};
});
// updates the current search state from the search query input boxes
function updateSearch() {
// compile query expressions, aborting if nothing has changed or either fails to compile
const changed = searchFilterField.compile() | searchSortField.compile();
if (!changed || searchFilterField.output === null || searchSortField.output === null) return;
// update url query string
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`
+ `?f=${encodeURIComponent(searchFilterField.value)}&s=${encodeURIComponent(searchSortField.value)}`;
window.history.pushState({ path: newUrl }, '', newUrl);
// hide old search results
itemListFooter.innerText = '';
for (let i = 0; i < ITEM_LIST_SIZE; i++) {
// Clear old entries
setHTML(`item-entry-${i}`, "");
itemEntries[i].classList.remove('visible');
// Clear old sort keys
setHTML(`item-sort-entry-${i}`, "");
}
// index and sort search results
const searchResults = [];
try {
for (let i = 0; i < searchDb.length; i++) {
const item = searchDb[i][0], itemExp = searchDb[i][1];
if (checkBool(searchFilterField.output.resolve(item, itemExp))) {
searchResults.push({ item, itemExp, sortKeys: searchSortField.output.resolve(item, itemExp) });
}
}
} catch (e) {
searchFilterField.errorText.innerText = e.message;
return;
}
if (searchResults.length === 0) {
itemListFooter.innerText = 'No results!';
return;
}
try {
searchResults.sort((a, b) => {
try {
return compareLexico(a.item, a.sortKeys, b.item, b.sortKeys);
} catch (e) {
console.log(a.item, b.item);
throw e;
}
});
} 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++) {
const result = searchResults[i];
itemEntries[i].classList.add('visible');
display(result.itemExp, `item-entry-${i}`);
// Add new sort keys if present
if (result.sortKeys.length > 0) {
const sortKeyList = document.createElement('ul');
sortKeyList.classList.add('item-entry-sort-key', 'itemp', 'T0');
const sortKeyListContainer = document.getElementById(`item-sort-entry-${i}`);
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);
}
}
}
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;
function scheduleSearchUpdate() {
if (updateSearchTask !== null) {
clearTimeout(updateSearchTask);
}
updateSearchTask = setTimeout(() => {
updateSearchTask = null;
updateSearch();
}, 500);
}
searchFilterField.field.addEventListener('input', e => scheduleSearchUpdate());
searchSortField.field.addEventListener('input', e => scheduleSearchUpdate());
// parse query string, display initial search results
if (window.location.search.startsWith('?')) {
for (const entryStr of window.location.search.substring(1).split('&')) {
const ndx = entryStr.indexOf('=');
if (ndx !== -1) {
switch (entryStr.substring(0, ndx)) {
case 'f':
searchFilterField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
break;
case 's':
searchSortField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
break;
}
}
}
}
updateSearch();
// focus the query filter text box
searchFilterField.field.focus();
searchFilterField.field.select();
// scroll-to-top button
document.getElementById('scroll-up')
.addEventListener('mousedown', e => scrollTo({ top: 0, behavior: 'smooth' }));
}