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:
parent
88fe5217d2
commit
bf29018a60
14 changed files with 1476 additions and 750 deletions
|
@ -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
174
ingredients/index.html
Normal 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> <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> <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>
|
87
ingredients_adv/index.html
Normal file
87
ingredients_adv/index.html
Normal 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 ?= "blue" & 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">↑</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>
|
|
@ -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> <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> <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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
163
js/ingredients.js
Normal 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
54
js/ingredients_adv.js
Normal 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();
|
||||
})();
|
377
js/items.js
377
js/items.js
|
@ -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: " to "});
|
||||
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();
|
||||
})();
|
||||
|
|
383
js/items_adv.js
383
js/items_adv.js
|
@ -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() {
|
||||
|
|
168
js/query.js
168
js/query.js
|
@ -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
365
js/search.js
Normal 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: " to "});
|
||||
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
393
js/search_adv.js
Normal 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' }));
|
||||
}
|
Loading…
Reference in a new issue