Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
Run results for:
Array vs NamedNodeMap
Comparing perf of iterating through a NamedNodeMap in syncNodeFrom vs an Array created from said map.
Go to the benchmark
Embed
Embed Benchmark Result
Run details:
User agent:
Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15
Browser:
Safari 18
Operating system:
Ubuntu
Device Platform:
Desktop
Date tested:
one year ago
Test name
Executions per second
NamedNodeMap
457.6 Ops/sec
Array
426.0 Ops/sec
HTML Preparation code:
<div id="start"> <input checked="checked" class="very-hidden" id="cb-expect-ikeys" name="expect.ikeys" type="checkbox" value="True"> <div class="form-check mx-auto filter-check" hx-post="ml" hx-target="#ml-lander" hx-trigger="click consume" hx-vals='{"base.sort.col": "rank", "base.sort.dir": "a", "filter.sort.str": "", "filter.sort.col": "", "modal.name": "symbol", "duplicated.col": ""}' id="cb-outer-combo-0"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="(Select All)" id="cb-0" name="i" style="pointer-events: none;" type="checkbox" value="0"> <label class="form-check-label w-10" for="cb-0" id="cb-label-0"> (Select All) </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-1"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AAVE" id="cb-1" name="i" type="checkbox" value="1"> <label class="form-check-label w-10" for="cb-1" id="cb-label-1"> AAVE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-2"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ADA" id="cb-2" name="i" type="checkbox" value="2"> <label class="form-check-label w-10" for="cb-2" id="cb-label-2"> ADA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-3"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ALGO" id="cb-3" name="i" type="checkbox" value="3"> <label class="form-check-label w-10" for="cb-3" id="cb-label-3"> ALGO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-4"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="APE" id="cb-4" name="i" type="checkbox" value="4"> <label class="form-check-label w-10" for="cb-4" id="cb-label-4"> APE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-5"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AR" id="cb-5" name="i" type="checkbox" value="5"> <label class="form-check-label w-10" for="cb-5" id="cb-label-5"> AR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-6"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ATOM" id="cb-6" name="i" type="checkbox" value="6"> <label class="form-check-label w-10" for="cb-6" id="cb-label-6"> ATOM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-7"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AVAX" id="cb-7" name="i" type="checkbox" value="7"> <label class="form-check-label w-10" for="cb-7" id="cb-label-7"> AVAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-8"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AXS" id="cb-8" name="i" type="checkbox" value="8"> <label class="form-check-label w-10" for="cb-8" id="cb-label-8"> AXS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-9"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BAT" id="cb-9" name="i" type="checkbox" value="9"> <label class="form-check-label w-10" for="cb-9" id="cb-label-9"> BAT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-10"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BCH" id="cb-10" name="i" type="checkbox" value="10"> <label class="form-check-label w-10" for="cb-10" id="cb-label-10"> BCH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-11"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BIT" id="cb-11" name="i" type="checkbox" value="11"> <label class="form-check-label w-10" for="cb-11" id="cb-label-11"> BIT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-12"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BMAX" id="cb-12" name="i" type="checkbox" value="12"> <label class="form-check-label w-10" for="cb-12" id="cb-label-12"> BMAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-13"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BNB" id="cb-13" name="i" type="checkbox" value="13"> <label class="form-check-label w-10" for="cb-13" id="cb-label-13"> BNB </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-14"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BOT" id="cb-14" name="i" type="checkbox" value="14"> <label class="form-check-label w-10" for="cb-14" id="cb-label-14"> BOT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-15"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BSV" id="cb-15" name="i" type="checkbox" value="15"> <label class="form-check-label w-10" for="cb-15" id="cb-label-15"> BSV </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-16"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BTC" id="cb-16" name="i" type="checkbox" value="16"> <label class="form-check-label w-10" for="cb-16" id="cb-label-16"> BTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-17"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BTT" id="cb-17" name="i" type="checkbox" value="17"> <label class="form-check-label w-10" for="cb-17" id="cb-label-17"> BTT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-18"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BUSD" id="cb-18" name="i" type="checkbox" value="18"> <label class="form-check-label w-10" for="cb-18" id="cb-label-18"> BUSD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-19"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CAKE" id="cb-19" name="i" type="checkbox" value="19"> <label class="form-check-label w-10" for="cb-19" id="cb-label-19"> CAKE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-20"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CDAI" id="cb-20" name="i" type="checkbox" value="20"> <label class="form-check-label w-10" for="cb-20" id="cb-label-20"> CDAI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-21"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CETH" id="cb-21" name="i" type="checkbox" value="21"> <label class="form-check-label w-10" for="cb-21" id="cb-label-21"> CETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-22"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CHZ" id="cb-22" name="i" type="checkbox" value="22"> <label class="form-check-label w-10" for="cb-22" id="cb-label-22"> CHZ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-23"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CRO" id="cb-23" name="i" type="checkbox" value="23"> <label class="form-check-label w-10" for="cb-23" id="cb-label-23"> CRO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-24"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CUSDC" id="cb-24" name="i" type="checkbox" value="24"> <label class="form-check-label w-10" for="cb-24" id="cb-label-24"> CUSDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-25"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CWBTC" id="cb-25" name="i" type="checkbox" value="25"> <label class="form-check-label w-10" for="cb-25" id="cb-label-25"> CWBTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-26"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DAI" id="cb-26" name="i" type="checkbox" value="26"> <label class="form-check-label w-10" for="cb-26" id="cb-label-26"> DAI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-27"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DASH" id="cb-27" name="i" type="checkbox" value="27"> <label class="form-check-label w-10" for="cb-27" id="cb-label-27"> DASH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-28"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DOGE" id="cb-28" name="i" type="checkbox" value="28"> <label class="form-check-label w-10" for="cb-28" id="cb-label-28"> DOGE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-29"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DOT" id="cb-29" name="i" type="checkbox" value="29"> <label class="form-check-label w-10" for="cb-29" id="cb-label-29"> DOT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-30"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ECOIN" id="cb-30" name="i" type="checkbox" value="30"> <label class="form-check-label w-10" for="cb-30" id="cb-label-30"> ECOIN </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-31"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EGLD" id="cb-31" name="i" type="checkbox" value="31"> <label class="form-check-label w-10" for="cb-31" id="cb-label-31"> EGLD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-32"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ENJ" id="cb-32" name="i" type="checkbox" value="32"> <label class="form-check-label w-10" for="cb-32" id="cb-label-32"> ENJ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-33"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ENS" id="cb-33" name="i" type="checkbox" value="33"> <label class="form-check-label w-10" for="cb-33" id="cb-label-33"> ENS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-34"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EOS" id="cb-34" name="i" type="checkbox" value="34"> <label class="form-check-label w-10" for="cb-34" id="cb-label-34"> EOS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-35"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETC" id="cb-35" name="i" type="checkbox" value="35"> <label class="form-check-label w-10" for="cb-35" id="cb-label-35"> ETC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-36"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETH" id="cb-36" name="i" type="checkbox" value="36"> <label class="form-check-label w-10" for="cb-36" id="cb-label-36"> ETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-37"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETHW" id="cb-37" name="i" type="checkbox" value="37"> <label class="form-check-label w-10" for="cb-37" id="cb-label-37"> ETHW </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-38"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EVMOS" id="cb-38" name="i" type="checkbox" value="38"> <label class="form-check-label w-10" for="cb-38" id="cb-label-38"> EVMOS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-39"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FEI" id="cb-39" name="i" type="checkbox" value="39"> <label class="form-check-label w-10" for="cb-39" id="cb-label-39"> FEI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-40"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FIL" id="cb-40" name="i" type="checkbox" value="40"> <label class="form-check-label w-10" for="cb-40" id="cb-label-40"> FIL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-41"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FLOW" id="cb-41" name="i" type="checkbox" value="41"> <label class="form-check-label w-10" for="cb-41" id="cb-label-41"> FLOW </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-42"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FRAX" id="cb-42" name="i" type="checkbox" value="42"> <label class="form-check-label w-10" for="cb-42" id="cb-label-42"> FRAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-43"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FRAX3CRV-f" id="cb-43" name="i" type="checkbox" value="43"> <label class="form-check-label w-10" for="cb-43" id="cb-label-43"> FRAX3CRV-f </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-44"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FTM" id="cb-44" name="i" type="checkbox" value="44"> <label class="form-check-label w-10" for="cb-44" id="cb-label-44"> FTM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-45"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FTT" id="cb-45" name="i" type="checkbox" value="45"> <label class="form-check-label w-10" for="cb-45" id="cb-label-45"> FTT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-46"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="G-UNI" id="cb-46" name="i" type="checkbox" value="46"> <label class="form-check-label w-10" for="cb-46" id="cb-label-46"> G-UNI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-47"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="GRT" id="cb-47" name="i" type="checkbox" value="47"> <label class="form-check-label w-10" for="cb-47" id="cb-label-47"> GRT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-48"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="GT" id="cb-48" name="i" type="checkbox" value="48"> <label class="form-check-label w-10" for="cb-48" id="cb-label-48"> GT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-49"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HBAR" id="cb-49" name="i" type="checkbox" value="49"> <label class="form-check-label w-10" for="cb-49" id="cb-label-49"> HBAR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-50"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HEART" id="cb-50" name="i" type="checkbox" value="50"> <label class="form-check-label w-10" for="cb-50" id="cb-label-50"> HEART </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-51"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HNT" id="cb-51" name="i" type="checkbox" value="51"> <label class="form-check-label w-10" for="cb-51" id="cb-label-51"> HNT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-52"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HT" id="cb-52" name="i" type="checkbox" value="52"> <label class="form-check-label w-10" for="cb-52" id="cb-label-52"> HT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-53"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ICP" id="cb-53" name="i" type="checkbox" value="53"> <label class="form-check-label w-10" for="cb-53" id="cb-label-53"> ICP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-54"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KAVA" id="cb-54" name="i" type="checkbox" value="54"> <label class="form-check-label w-10" for="cb-54" id="cb-label-54"> KAVA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-55"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KCS" id="cb-55" name="i" type="checkbox" value="55"> <label class="form-check-label w-10" for="cb-55" id="cb-label-55"> KCS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-56"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KLAY" id="cb-56" name="i" type="checkbox" value="56"> <label class="form-check-label w-10" for="cb-56" id="cb-label-56"> KLAY </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-57"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LDO" id="cb-57" name="i" type="checkbox" value="57"> <label class="form-check-label w-10" for="cb-57" id="cb-label-57"> LDO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-58"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LINK" id="cb-58" name="i" type="checkbox" value="58"> <label class="form-check-label w-10" for="cb-58" id="cb-label-58"> LINK </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-59"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LTC" id="cb-59" name="i" type="checkbox" value="59"> <label class="form-check-label w-10" for="cb-59" id="cb-label-59"> LTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-60"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LUNC" id="cb-60" name="i" type="checkbox" value="60"> <label class="form-check-label w-10" for="cb-60" id="cb-label-60"> LUNC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-61"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MANA" id="cb-61" name="i" type="checkbox" value="61"> <label class="form-check-label w-10" for="cb-61" id="cb-label-61"> MANA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-62"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MATIC" id="cb-62" name="i" type="checkbox" value="62"> <label class="form-check-label w-10" for="cb-62" id="cb-label-62"> MATIC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-63"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MIOTA" id="cb-63" name="i" type="checkbox" value="63"> <label class="form-check-label w-10" for="cb-63" id="cb-label-63"> MIOTA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-64"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MKR" id="cb-64" name="i" type="checkbox" value="64"> <label class="form-check-label w-10" for="cb-64" id="cb-label-64"> MKR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-65"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEAR" id="cb-65" name="i" type="checkbox" value="65"> <label class="form-check-label w-10" for="cb-65" id="cb-label-65"> NEAR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-66"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEO" id="cb-66" name="i" type="checkbox" value="66"> <label class="form-check-label w-10" for="cb-66" id="cb-label-66"> NEO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-67"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEXO" id="cb-67" name="i" type="checkbox" value="67"> <label class="form-check-label w-10" for="cb-67" id="cb-label-67"> NEXO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-68"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="OSMO" id="cb-68" name="i" type="checkbox" value="68"> <label class="form-check-label w-10" for="cb-68" id="cb-label-68"> OSMO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-69"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="PAXG" id="cb-69" name="i" type="checkbox" value="69"> <label class="form-check-label w-10" for="cb-69" id="cb-label-69"> PAXG </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-70"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="QNT" id="cb-70" name="i" type="checkbox" value="70"> <label class="form-check-label w-10" for="cb-70" id="cb-label-70"> QNT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-71"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="RUNE" id="cb-71" name="i" type="checkbox" value="71"> <label class="form-check-label w-10" for="cb-71" id="cb-label-71"> RUNE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-72"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SAND" id="cb-72" name="i" type="checkbox" value="72"> <label class="form-check-label w-10" for="cb-72" id="cb-label-72"> SAND </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-73"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SHIB" id="cb-73" name="i" type="checkbox" value="73"> <label class="form-check-label w-10" for="cb-73" id="cb-label-73"> SHIB </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-74"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SNX" id="cb-74" name="i" type="checkbox" value="74"> <label class="form-check-label w-10" for="cb-74" id="cb-label-74"> SNX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-75"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SOL" id="cb-75" name="i" type="checkbox" value="75"> <label class="form-check-label w-10" for="cb-75" id="cb-label-75"> SOL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-76"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="STETH" id="cb-76" name="i" type="checkbox" value="76"> <label class="form-check-label w-10" for="cb-76" id="cb-label-76"> STETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-77"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="THETA" id="cb-77" name="i" type="checkbox" value="77"> <label class="form-check-label w-10" for="cb-77" id="cb-label-77"> THETA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-78"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TKX" id="cb-78" name="i" type="checkbox" value="78"> <label class="form-check-label w-10" for="cb-78" id="cb-label-78"> TKX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-79"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TRX" id="cb-79" name="i" type="checkbox" value="79"> <label class="form-check-label w-10" for="cb-79" id="cb-label-79"> TRX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-80"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TUSD" id="cb-80" name="i" type="checkbox" value="80"> <label class="form-check-label w-10" for="cb-80" id="cb-label-80"> TUSD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-81"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="UNI" id="cb-81" name="i" type="checkbox" value="81"> <label class="form-check-label w-10" for="cb-81" id="cb-label-81"> UNI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-82"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDC" id="cb-82" name="i" type="checkbox" value="82"> <label class="form-check-label w-10" for="cb-82" id="cb-label-82"> USDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-83"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDD" id="cb-83" name="i" type="checkbox" value="83"> <label class="form-check-label w-10" for="cb-83" id="cb-label-83"> USDD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-84"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDP" id="cb-84" name="i" type="checkbox" value="84"> <label class="form-check-label w-10" for="cb-84" id="cb-label-84"> USDP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-85"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDT" id="cb-85" name="i" type="checkbox" value="85"> <label class="form-check-label w-10" for="cb-85" id="cb-label-85"> USDT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-86"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USTC" id="cb-86" name="i" type="checkbox" value="86"> <label class="form-check-label w-10" for="cb-86" id="cb-label-86"> USTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-87"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="VET" id="cb-87" name="i" type="checkbox" value="87"> <label class="form-check-label w-10" for="cb-87" id="cb-label-87"> VET </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-88"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="VGX" id="cb-88" name="i" type="checkbox" value="88"> <label class="form-check-label w-10" for="cb-88" id="cb-label-88"> VGX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-89"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="WBTC" id="cb-89" name="i" type="checkbox" value="89"> <label class="form-check-label w-10" for="cb-89" id="cb-label-89"> WBTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-90"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XCN" id="cb-90" name="i" type="checkbox" value="90"> <label class="form-check-label w-10" for="cb-90" id="cb-label-90"> XCN </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-91"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XDC" id="cb-91" name="i" type="checkbox" value="91"> <label class="form-check-label w-10" for="cb-91" id="cb-label-91"> XDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-92"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XEC" id="cb-92" name="i" type="checkbox" value="92"> <label class="form-check-label w-10" for="cb-92" id="cb-label-92"> XEC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-93"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XLM" id="cb-93" name="i" type="checkbox" value="93"> <label class="form-check-label w-10" for="cb-93" id="cb-label-93"> XLM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-94"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XMR" id="cb-94" name="i" type="checkbox" value="94"> <label class="form-check-label w-10" for="cb-94" id="cb-label-94"> XMR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-95"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XRD" id="cb-95" name="i" type="checkbox" value="95"> <label class="form-check-label w-10" for="cb-95" id="cb-label-95"> XRD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-96"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XRP" id="cb-96" name="i" type="checkbox" value="96"> <label class="form-check-label w-10" for="cb-96" id="cb-label-96"> XRP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-97"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XTZ" id="cb-97" name="i" type="checkbox" value="97"> <label class="form-check-label w-10" for="cb-97" id="cb-label-97"> XTZ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-98"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ZEC" id="cb-98" name="i" type="checkbox" value="98"> <label class="form-check-label w-10" for="cb-98" id="cb-label-98"> ZEC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-99"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ZIL" id="cb-99" name="i" type="checkbox" value="99"> <label class="form-check-label w-10" for="cb-99" id="cb-label-99"> ZIL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-100"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="f6-sOHM" id="cb-100" name="i" type="checkbox" value="100"> <label class="form-check-label w-10" for="cb-100" id="cb-label-100"> f6-sOHM </label> </div> </div> <div id="end"> <input checked="checked" class="very-hidden" id="cb-expect-ikeys" name="expect.ikeys" type="checkbox" value="True"> <div class="form-check mx-auto filter-check" hx-post="ml" hx-target="#ml-lander" hx-trigger="click consume" hx-vals='{"base.sort.col": "rank", "base.sort.dir": "a", "filter.sort.str": "", "filter.sort.col": "", "modal.name": "symbol", "duplicated.col": ""}' id="cb-outer-combo-0"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="(Select All)" id="cb-0" name="i" style="pointer-events: none;" type="checkbox" value="0"> <label class="form-check-label w-10" for="cb-0" id="cb-label-0"> (Select All) </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-1"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AAVE" id="cb-1" name="i" type="checkbox" value="1"> <label class="form-check-label w-10" for="cb-1" id="cb-label-1"> AAVE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-2"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ADA" id="cb-2" name="i" type="checkbox" value="2"> <label class="form-check-label w-10" for="cb-2" id="cb-label-2"> ADA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-3"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ALGO" id="cb-3" name="i" type="checkbox" value="3"> <label class="form-check-label w-10" for="cb-3" id="cb-label-3"> ALGO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-4"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="APE" id="cb-4" name="i" type="checkbox" value="4"> <label class="form-check-label w-10" for="cb-4" id="cb-label-4"> APE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-5"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AR" id="cb-5" name="i" type="checkbox" value="5"> <label class="form-check-label w-10" for="cb-5" id="cb-label-5"> AR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-6"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ATOM" id="cb-6" name="i" type="checkbox" value="6"> <label class="form-check-label w-10" for="cb-6" id="cb-label-6"> ATOM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-7"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AVAX" id="cb-7" name="i" type="checkbox" value="7"> <label class="form-check-label w-10" for="cb-7" id="cb-label-7"> AVAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-8"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="AXS" id="cb-8" name="i" type="checkbox" value="8"> <label class="form-check-label w-10" for="cb-8" id="cb-label-8"> AXS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-9"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BAT" id="cb-9" name="i" type="checkbox" value="9"> <label class="form-check-label w-10" for="cb-9" id="cb-label-9"> BAT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-10"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BCH" id="cb-10" name="i" type="checkbox" value="10"> <label class="form-check-label w-10" for="cb-10" id="cb-label-10"> BCH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-11"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BIT" id="cb-11" name="i" type="checkbox" value="11"> <label class="form-check-label w-10" for="cb-11" id="cb-label-11"> BIT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-12"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BMAX" id="cb-12" name="i" type="checkbox" value="12"> <label class="form-check-label w-10" for="cb-12" id="cb-label-12"> BMAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-13"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BNB" id="cb-13" name="i" type="checkbox" value="13"> <label class="form-check-label w-10" for="cb-13" id="cb-label-13"> BNB </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-14"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BOT" id="cb-14" name="i" type="checkbox" value="14"> <label class="form-check-label w-10" for="cb-14" id="cb-label-14"> BOT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-15"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BSV" id="cb-15" name="i" type="checkbox" value="15"> <label class="form-check-label w-10" for="cb-15" id="cb-label-15"> BSV </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-16"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BTC" id="cb-16" name="i" type="checkbox" value="16"> <label class="form-check-label w-10" for="cb-16" id="cb-label-16"> BTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-17"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BTT" id="cb-17" name="i" type="checkbox" value="17"> <label class="form-check-label w-10" for="cb-17" id="cb-label-17"> BTT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-18"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="BUSD" id="cb-18" name="i" type="checkbox" value="18"> <label class="form-check-label w-10" for="cb-18" id="cb-label-18"> BUSD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-19"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CAKE" id="cb-19" name="i" type="checkbox" value="19"> <label class="form-check-label w-10" for="cb-19" id="cb-label-19"> CAKE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-20"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CDAI" id="cb-20" name="i" type="checkbox" value="20"> <label class="form-check-label w-10" for="cb-20" id="cb-label-20"> CDAI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-21"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CETH" id="cb-21" name="i" type="checkbox" value="21"> <label class="form-check-label w-10" for="cb-21" id="cb-label-21"> CETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-22"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CHZ" id="cb-22" name="i" type="checkbox" value="22"> <label class="form-check-label w-10" for="cb-22" id="cb-label-22"> CHZ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-23"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CRO" id="cb-23" name="i" type="checkbox" value="23"> <label class="form-check-label w-10" for="cb-23" id="cb-label-23"> CRO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-24"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CUSDC" id="cb-24" name="i" type="checkbox" value="24"> <label class="form-check-label w-10" for="cb-24" id="cb-label-24"> CUSDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-25"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="CWBTC" id="cb-25" name="i" type="checkbox" value="25"> <label class="form-check-label w-10" for="cb-25" id="cb-label-25"> CWBTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-26"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DAI" id="cb-26" name="i" type="checkbox" value="26"> <label class="form-check-label w-10" for="cb-26" id="cb-label-26"> DAI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-27"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DASH" id="cb-27" name="i" type="checkbox" value="27"> <label class="form-check-label w-10" for="cb-27" id="cb-label-27"> DASH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-28"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DOGE" id="cb-28" name="i" type="checkbox" value="28"> <label class="form-check-label w-10" for="cb-28" id="cb-label-28"> DOGE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-29"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="DOT" id="cb-29" name="i" type="checkbox" value="29"> <label class="form-check-label w-10" for="cb-29" id="cb-label-29"> DOT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-30"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ECOIN" id="cb-30" name="i" type="checkbox" value="30"> <label class="form-check-label w-10" for="cb-30" id="cb-label-30"> ECOIN </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-31"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EGLD" id="cb-31" name="i" type="checkbox" value="31"> <label class="form-check-label w-10" for="cb-31" id="cb-label-31"> EGLD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-32"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ENJ" id="cb-32" name="i" type="checkbox" value="32"> <label class="form-check-label w-10" for="cb-32" id="cb-label-32"> ENJ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-33"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ENS" id="cb-33" name="i" type="checkbox" value="33"> <label class="form-check-label w-10" for="cb-33" id="cb-label-33"> ENS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-34"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EOS" id="cb-34" name="i" type="checkbox" value="34"> <label class="form-check-label w-10" for="cb-34" id="cb-label-34"> EOS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-35"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETC" id="cb-35" name="i" type="checkbox" value="35"> <label class="form-check-label w-10" for="cb-35" id="cb-label-35"> ETC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-36"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETH" id="cb-36" name="i" type="checkbox" value="36"> <label class="form-check-label w-10" for="cb-36" id="cb-label-36"> ETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-37"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ETHW" id="cb-37" name="i" type="checkbox" value="37"> <label class="form-check-label w-10" for="cb-37" id="cb-label-37"> ETHW </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-38"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="EVMOS" id="cb-38" name="i" type="checkbox" value="38"> <label class="form-check-label w-10" for="cb-38" id="cb-label-38"> EVMOS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-39"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FEI" id="cb-39" name="i" type="checkbox" value="39"> <label class="form-check-label w-10" for="cb-39" id="cb-label-39"> FEI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-40"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FIL" id="cb-40" name="i" type="checkbox" value="40"> <label class="form-check-label w-10" for="cb-40" id="cb-label-40"> FIL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-41"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FLOW" id="cb-41" name="i" type="checkbox" value="41"> <label class="form-check-label w-10" for="cb-41" id="cb-label-41"> FLOW </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-42"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FRAX" id="cb-42" name="i" type="checkbox" value="42"> <label class="form-check-label w-10" for="cb-42" id="cb-label-42"> FRAX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-43"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FRAX3CRV-f" id="cb-43" name="i" type="checkbox" value="43"> <label class="form-check-label w-10" for="cb-43" id="cb-label-43"> FRAX3CRV-f </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-44"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FTM" id="cb-44" name="i" type="checkbox" value="44"> <label class="form-check-label w-10" for="cb-44" id="cb-label-44"> FTM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-45"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="FTT" id="cb-45" name="i" type="checkbox" value="45"> <label class="form-check-label w-10" for="cb-45" id="cb-label-45"> FTT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-46"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="G-UNI" id="cb-46" name="i" type="checkbox" value="46"> <label class="form-check-label w-10" for="cb-46" id="cb-label-46"> G-UNI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-47"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="GRT" id="cb-47" name="i" type="checkbox" value="47"> <label class="form-check-label w-10" for="cb-47" id="cb-label-47"> GRT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-48"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="GT" id="cb-48" name="i" type="checkbox" value="48"> <label class="form-check-label w-10" for="cb-48" id="cb-label-48"> GT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-49"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HBAR" id="cb-49" name="i" type="checkbox" value="49"> <label class="form-check-label w-10" for="cb-49" id="cb-label-49"> HBAR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-50"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HEART" id="cb-50" name="i" type="checkbox" value="50"> <label class="form-check-label w-10" for="cb-50" id="cb-label-50"> HEART </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-51"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HNT" id="cb-51" name="i" type="checkbox" value="51"> <label class="form-check-label w-10" for="cb-51" id="cb-label-51"> HNT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-52"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="HT" id="cb-52" name="i" type="checkbox" value="52"> <label class="form-check-label w-10" for="cb-52" id="cb-label-52"> HT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-53"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ICP" id="cb-53" name="i" type="checkbox" value="53"> <label class="form-check-label w-10" for="cb-53" id="cb-label-53"> ICP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-54"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KAVA" id="cb-54" name="i" type="checkbox" value="54"> <label class="form-check-label w-10" for="cb-54" id="cb-label-54"> KAVA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-55"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KCS" id="cb-55" name="i" type="checkbox" value="55"> <label class="form-check-label w-10" for="cb-55" id="cb-label-55"> KCS </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-56"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="KLAY" id="cb-56" name="i" type="checkbox" value="56"> <label class="form-check-label w-10" for="cb-56" id="cb-label-56"> KLAY </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-57"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LDO" id="cb-57" name="i" type="checkbox" value="57"> <label class="form-check-label w-10" for="cb-57" id="cb-label-57"> LDO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-58"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LINK" id="cb-58" name="i" type="checkbox" value="58"> <label class="form-check-label w-10" for="cb-58" id="cb-label-58"> LINK </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-59"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LTC" id="cb-59" name="i" type="checkbox" value="59"> <label class="form-check-label w-10" for="cb-59" id="cb-label-59"> LTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-60"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="LUNC" id="cb-60" name="i" type="checkbox" value="60"> <label class="form-check-label w-10" for="cb-60" id="cb-label-60"> LUNC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-61"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MANA" id="cb-61" name="i" type="checkbox" value="61"> <label class="form-check-label w-10" for="cb-61" id="cb-label-61"> MANA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-62"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MATIC" id="cb-62" name="i" type="checkbox" value="62"> <label class="form-check-label w-10" for="cb-62" id="cb-label-62"> MATIC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-63"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MIOTA" id="cb-63" name="i" type="checkbox" value="63"> <label class="form-check-label w-10" for="cb-63" id="cb-label-63"> MIOTA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-64"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="MKR" id="cb-64" name="i" type="checkbox" value="64"> <label class="form-check-label w-10" for="cb-64" id="cb-label-64"> MKR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-65"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEAR" id="cb-65" name="i" type="checkbox" value="65"> <label class="form-check-label w-10" for="cb-65" id="cb-label-65"> NEAR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-66"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEO" id="cb-66" name="i" type="checkbox" value="66"> <label class="form-check-label w-10" for="cb-66" id="cb-label-66"> NEO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-67"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="NEXO" id="cb-67" name="i" type="checkbox" value="67"> <label class="form-check-label w-10" for="cb-67" id="cb-label-67"> NEXO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-68"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="OSMO" id="cb-68" name="i" type="checkbox" value="68"> <label class="form-check-label w-10" for="cb-68" id="cb-label-68"> OSMO </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-69"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="PAXG" id="cb-69" name="i" type="checkbox" value="69"> <label class="form-check-label w-10" for="cb-69" id="cb-label-69"> PAXG </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-70"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="QNT" id="cb-70" name="i" type="checkbox" value="70"> <label class="form-check-label w-10" for="cb-70" id="cb-label-70"> QNT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-71"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="RUNE" id="cb-71" name="i" type="checkbox" value="71"> <label class="form-check-label w-10" for="cb-71" id="cb-label-71"> RUNE </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-72"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SAND" id="cb-72" name="i" type="checkbox" value="72"> <label class="form-check-label w-10" for="cb-72" id="cb-label-72"> SAND </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-73"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SHIB" id="cb-73" name="i" type="checkbox" value="73"> <label class="form-check-label w-10" for="cb-73" id="cb-label-73"> SHIB </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-74"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SNX" id="cb-74" name="i" type="checkbox" value="74"> <label class="form-check-label w-10" for="cb-74" id="cb-label-74"> SNX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-75"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="SOL" id="cb-75" name="i" type="checkbox" value="75"> <label class="form-check-label w-10" for="cb-75" id="cb-label-75"> SOL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-76"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="STETH" id="cb-76" name="i" type="checkbox" value="76"> <label class="form-check-label w-10" for="cb-76" id="cb-label-76"> STETH </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-77"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="THETA" id="cb-77" name="i" type="checkbox" value="77"> <label class="form-check-label w-10" for="cb-77" id="cb-label-77"> THETA </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-78"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TKX" id="cb-78" name="i" type="checkbox" value="78"> <label class="form-check-label w-10" for="cb-78" id="cb-label-78"> TKX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-79"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TRX" id="cb-79" name="i" type="checkbox" value="79"> <label class="form-check-label w-10" for="cb-79" id="cb-label-79"> TRX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-80"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="TUSD" id="cb-80" name="i" type="checkbox" value="80"> <label class="form-check-label w-10" for="cb-80" id="cb-label-80"> TUSD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-81"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="UNI" id="cb-81" name="i" type="checkbox" value="81"> <label class="form-check-label w-10" for="cb-81" id="cb-label-81"> UNI </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-82"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDC" id="cb-82" name="i" type="checkbox" value="82"> <label class="form-check-label w-10" for="cb-82" id="cb-label-82"> USDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-83"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDD" id="cb-83" name="i" type="checkbox" value="83"> <label class="form-check-label w-10" for="cb-83" id="cb-label-83"> USDD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-84"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDP" id="cb-84" name="i" type="checkbox" value="84"> <label class="form-check-label w-10" for="cb-84" id="cb-label-84"> USDP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-85"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USDT" id="cb-85" name="i" type="checkbox" value="85"> <label class="form-check-label w-10" for="cb-85" id="cb-label-85"> USDT </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-86"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="USTC" id="cb-86" name="i" type="checkbox" value="86"> <label class="form-check-label w-10" for="cb-86" id="cb-label-86"> USTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-87"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="VET" id="cb-87" name="i" type="checkbox" value="87"> <label class="form-check-label w-10" for="cb-87" id="cb-label-87"> VET </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-88"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="VGX" id="cb-88" name="i" type="checkbox" value="88"> <label class="form-check-label w-10" for="cb-88" id="cb-label-88"> VGX </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-89"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="WBTC" id="cb-89" name="i" type="checkbox" value="89"> <label class="form-check-label w-10" for="cb-89" id="cb-label-89"> WBTC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-90"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XCN" id="cb-90" name="i" type="checkbox" value="90"> <label class="form-check-label w-10" for="cb-90" id="cb-label-90"> XCN </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-91"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XDC" id="cb-91" name="i" type="checkbox" value="91"> <label class="form-check-label w-10" for="cb-91" id="cb-label-91"> XDC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-92"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XEC" id="cb-92" name="i" type="checkbox" value="92"> <label class="form-check-label w-10" for="cb-92" id="cb-label-92"> XEC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-93"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XLM" id="cb-93" name="i" type="checkbox" value="93"> <label class="form-check-label w-10" for="cb-93" id="cb-label-93"> XLM </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-94"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XMR" id="cb-94" name="i" type="checkbox" value="94"> <label class="form-check-label w-10" for="cb-94" id="cb-label-94"> XMR </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-95"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XRD" id="cb-95" name="i" type="checkbox" value="95"> <label class="form-check-label w-10" for="cb-95" id="cb-label-95"> XRD </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-96"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XRP" id="cb-96" name="i" type="checkbox" value="96"> <label class="form-check-label w-10" for="cb-96" id="cb-label-96"> XRP </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-97"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="XTZ" id="cb-97" name="i" type="checkbox" value="97"> <label class="form-check-label w-10" for="cb-97" id="cb-label-97"> XTZ </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-98"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ZEC" id="cb-98" name="i" type="checkbox" value="98"> <label class="form-check-label w-10" for="cb-98" id="cb-label-98"> ZEC </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-99"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="ZIL" id="cb-99" name="i" type="checkbox" value="99"> <label class="form-check-label w-10" for="cb-99" id="cb-label-99"> ZIL </label> </div> <div class="form-check mx-auto filter-check" id="cb-outer-combo-100"> <input checked="checked" class="form-check-input float-none tcheckbox" data-value="f6-sOHM" id="cb-100" name="i" type="checkbox" value="100"> <label class="form-check-label w-10" for="cb-100" id="cb-label-100"> f6-sOHM </label> </div> </div> <script> /** * @typedef {object} ConfigHead * * @property {'merge' | 'append' | 'morph' | 'none'} [style] * @property {boolean} [block] * @property {boolean} [ignore] * @property {function(Element): boolean} [shouldPreserve] * @property {function(Element): boolean} [shouldReAppend] * @property {function(Element): boolean} [shouldRemove] * @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed] */ /** * @typedef {object} ConfigCallbacks * * @property {function(Node): boolean} [beforeNodeAdded] * @property {function(Node): void} [afterNodeAdded] * @property {function(Element, Node): boolean} [beforeNodeMorphed] * @property {function(Element, Node): void} [afterNodeMorphed] * @property {function(Element): boolean} [beforeNodeRemoved] * @property {function(Element): void} [afterNodeRemoved] * @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated] */ /** * @typedef {object} Config * * @property {'outerHTML' | 'innerHTML'} [morphStyle] * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {ConfigCallbacks} [callbacks] * @property {ConfigHead} [head] */ /** * @typedef {function} NoOp * * @returns {void} */ /** * @typedef {object} ConfigHeadInternal * * @property {'merge' | 'append' | 'morph' | 'none'} style * @property {boolean} [block] * @property {boolean} [ignore] * @property {(function(Element): boolean) | NoOp} shouldPreserve * @property {(function(Element): boolean) | NoOp} shouldReAppend * @property {(function(Element): boolean) | NoOp} shouldRemove * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed */ /** * @typedef {object} ConfigCallbacksInternal * * @property {(function(Node): boolean) | NoOp} beforeNodeAdded * @property {(function(Node): void) | NoOp} afterNodeAdded * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved * @property {(function(Node): void) | NoOp} afterNodeRemoved * @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated */ /** * @typedef {object} ConfigInternal * * @property {'outerHTML' | 'innerHTML'} morphStyle * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {ConfigCallbacksInternal} callbacks * @property {ConfigHeadInternal} head */ /** * @typedef {Function} Morph * * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {undefined | HTMLCollection | Node[]} */ // base IIFE to define idiomorph /** * * @type {{defaults: ConfigInternal, morph: Morph}} */ window.Idiomorph = (function () { 'use strict'; /** * @typedef {object} MorphContext * * @property {Node} target * @property {Node} newContent * @property {ConfigInternal} config * @property {ConfigInternal['morphStyle']} morphStyle * @property {ConfigInternal['ignoreActive']} ignoreActive * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue * @property {Map<Node, Set<string>>} idMap * @property {Set<string>} deadIds * @property {ConfigInternal['callbacks']} callbacks * @property {ConfigInternal['head']} head */ //============================================================================= // AND NOW IT BEGINS... //============================================================================= /** * * @type {Set<string>} */ let EMPTY_SET = new Set(); /** * Default configuration values, updatable by users now * @type {ConfigInternal} */ let defaults = { morphStyle: "outerHTML", callbacks : { beforeNodeAdded: noOp, afterNodeAdded: noOp, beforeNodeMorphed: noOp, afterNodeMorphed: noOp, beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp, }, head: { style: 'merge', shouldPreserve: function (elt) { return elt.getAttribute("im-preserve") === "true"; }, shouldReAppend: function (elt) { return elt.getAttribute("im-re-append") === "true"; }, shouldRemove: noOp, afterHeadMorphed: noOp, } }; /** * ============================================================================= * Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren * ============================================================================= * * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {undefined | HTMLCollection | Node[]} */ function morph(oldNode, newContent, config = {}) { if (oldNode instanceof Document) { oldNode = oldNode.documentElement; } if (typeof newContent === 'string') { newContent = parseContent(newContent); } let normalizedContent = normalizeContent(newContent); let ctx = createMorphContext(oldNode, normalizedContent, config); return morphNormalizedContent(oldNode, normalizedContent, ctx); } /** * * @param {Element} oldNode * @param {Element} normalizedNewContent * @param {MorphContext} ctx * @returns {undefined | HTMLCollection| Node[]} */ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { if (ctx.head.block) { let oldHead = oldNode.querySelector('head'); let newHead = normalizedNewContent.querySelector('head'); if (oldHead && newHead) { let promises = handleHeadElement(newHead, oldHead, ctx); // when head promises resolve, call morph again, ignoring the head tag Promise.all(promises).then(function () { morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { head: { block: false, ignore: true } })); }); return; } } if (ctx.morphStyle === "innerHTML") { // innerHTML, so we are only updating the children morphChildren(normalizedNewContent, oldNode, ctx); return Array.from(oldNode.children); } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { // otherwise find the best element match in the new content, morph that, and merge its siblings // into either side of the best match let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); // stash the siblings that will need to be inserted on either side of the best match let previousSibling = bestMatch?.previousSibling ?? null; let nextSibling = bestMatch?.nextSibling ?? null; // morph it let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); if (bestMatch) { // if there was a best match, merge the siblings in too and return the // whole bunch if (morphedNode) { return insertSiblings(previousSibling, morphedNode, nextSibling); } } else { // otherwise nothing was added to the DOM return [] } } else { throw "Do not understand how to morph style " + ctx.morphStyle; } } /** * @param {Node} possibleActiveElement * @param {MorphContext} ctx * @returns {boolean} */ // TODO: ignoreActive and ignoreActiveValue are marked as optional since they are not // initialised in the default config object. As a result the && in the function body may // return undefined instead of boolean. Either expand the type of the return value to // include undefined or wrap the ctx.ignoreActiveValue into a Boolean() function ignoreValueOfActiveElement(possibleActiveElement, ctx) { return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body; } /** * @param {Node} oldNode root node to merge content into * @param {Node | null} newContent new content to merge * @param {MorphContext} ctx the merge context * @returns {Node | null} the element that ended up in the DOM */ function morphOldNodeTo(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) { // don't morph focused element } else if (newContent == null) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; oldNode.parentNode?.removeChild(oldNode); ctx.callbacks.afterNodeRemoved(oldNode); return null; } else if (!isSoftMatch(oldNode, newContent)) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; oldNode.parentNode?.replaceChild(newContent, oldNode); ctx.callbacks.afterNodeAdded(newContent); ctx.callbacks.afterNodeRemoved(oldNode); return newContent; } else { if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { // ignore the head element } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { // ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above handleHeadElement(/** @type {HTMLHeadElement} */ (newContent), oldNode, ctx); } else { syncNodeFrom(newContent, oldNode, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { morphChildren(newContent, oldNode, ctx); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); return oldNode; } return null; } /** * This is the core algorithm for matching up children. The idea is to use id sets to try to match up * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but * by using id sets, we are able to better match up with content deeper in the DOM. * * Basic algorithm is, for each node in the new content: * * - if we have reached the end of the old parent, append the new content * - if the new content has an id set match with the current insertion point, morph * - search for an id set match * - if id set match found, morph * - otherwise search for a "soft" match * - if a soft match is found, morph * - otherwise, prepend the new node before the current insertion point * * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch() and findSoftMatch() for details. * * @param {Node} newParent the parent element of the new content * @param {Node} oldParent the old content that we are merging the new content into * @param {MorphContext} ctx the merge context * @returns {void} */ function morphChildren(newParent, oldParent, ctx) { if (newParent instanceof HTMLTemplateElement && oldParent instanceof HTMLTemplateElement) { newParent = newParent.content; oldParent = oldParent.content; } /** * * @type {Node | null} */ let nextNewChild = newParent.firstChild; /** * * @type {Node | null} */ let insertionPoint = oldParent.firstChild; let newChild; // run through all the new content while (nextNewChild) { newChild = nextNewChild; nextNewChild = newChild.nextSibling; // if we are at the end of the exiting parent's children, just append if (insertionPoint == null) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; oldParent.appendChild(newChild); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); continue; } // if the current node has an id set match then morph if (isIdSetMatch(newChild, insertionPoint, ctx)) { morphOldNodeTo(insertionPoint, newChild, ctx); insertionPoint = insertionPoint.nextSibling; removeIdsFromConsideration(ctx, newChild); continue; } // otherwise search forward in the existing old children for an id set match let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); // if we found a potential match, remove the nodes until that point and morph if (idSetMatch) { insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); morphOldNodeTo(idSetMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } // no id set match found, so scan forward for a soft match for the current node let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); // if we found a soft match for the current node, morph if (softMatch) { insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); morphOldNodeTo(softMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } // abandon all hope of morphing, just insert the new child before the insertion point // and move on if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; oldParent.insertBefore(newChild, insertionPoint); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); } // remove any remaining old nodes that didn't match up with new content while (insertionPoint !== null) { let tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } } //============================================================================= // Attribute Syncing Code //============================================================================= /** * @param {string} attr the attribute to be mutated * @param {Element} to the element that is going to be updated * @param {"update" | "remove"} updateType * @param {MorphContext} ctx the merge context * @returns {boolean} true if the attribute should be ignored, false otherwise */ function ignoreAttribute(attr, to, updateType, ctx) { if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){ return true; } return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; } /** * syncs a given node with another node, copying over all attributes and * inner element state from the 'from' node to the 'to' node * * @param {Node} from the element to copy attributes & state from * @param {Node} to the element to copy attributes & state to * @param {MorphContext} ctx the merge context */ function syncNodeFrom(from, to, ctx) { let type = from.nodeType // if is an element type, sync the attributes from the // new node into the new node if (type === 1 /* element type */) { const fromEl = /** @type {Element} */ (from); const toEl = /** @type {Element} */ (to); const fromAttributes = Array.from(fromEl.attributes); const toAttributes = Array.from(toEl.attributes); for (const fromAttribute of fromAttributes) { if (ignoreAttribute(fromAttribute.name, toEl, 'update', ctx)) { continue; } if (toEl.getAttribute(fromAttribute.name) !== fromAttribute.value) { toEl.setAttribute(fromAttribute.name, fromAttribute.value); } } // iterate backwards to avoid skipping over items when a delete occurs for (let i = toAttributes.length - 1; 0 <= i; i--) { const toAttribute = toAttributes[i]; if (!fromEl.hasAttribute(toAttribute.name)) { if (ignoreAttribute(toAttribute.name, toEl, 'remove', ctx)) { continue; } toEl.removeAttribute(toAttribute.name); } } } // sync text nodes if (type === 8 /* comment */ || type === 3 /* text */) { if (to.nodeValue !== from.nodeValue) { to.nodeValue = from.nodeValue; } } if (!ignoreValueOfActiveElement(to, ctx)) { // sync input values syncInputValue(from, to, ctx); } } /** * @param {Element} from element to sync the value from * @param {Element} to element to sync the value to * @param {string} attributeName the attribute name * @param {MorphContext} ctx the merge context */ function syncBooleanAttribute(from, to, attributeName, ctx) { // TODO: prefer set/getAttribute here if (!(from instanceof Element && to instanceof Element)) return // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties const fromLiveValue = from[attributeName], toLiveValue = to[attributeName]; if (fromLiveValue !== toLiveValue) { let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx); if (!ignoreUpdate) { // update attribute's associated DOM property // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties to[attributeName] = from[attributeName]; } if (fromLiveValue) { if (!ignoreUpdate) { // TODO: do we really want this? tests say so but it feels wrong to.setAttribute(attributeName, fromLiveValue); } } else { if (!ignoreAttribute(attributeName, to, 'remove', ctx)) { to.removeAttribute(attributeName); } } } } /** * NB: many bothans died to bring us information: * * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 * * @param {Node} from the element to sync the input value from * @param {Node} to the element to sync the input value to * @param {MorphContext} ctx the merge context */ function syncInputValue(from, to, ctx) { if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== 'file') { let fromValue = from.value; let toValue = to.value; // sync boolean attributes syncBooleanAttribute(from, to, 'checked', ctx); syncBooleanAttribute(from, to, 'disabled', ctx); if (!from.hasAttribute('value')) { if (!ignoreAttribute('value', to, 'remove', ctx)) { to.value = ''; to.removeAttribute('value'); } } else if (fromValue !== toValue) { if (!ignoreAttribute('value', to, 'update', ctx)) { to.setAttribute('value', fromValue); to.value = fromValue; } } // TODO: QUESTION(1cg): this used to only check `from` unlike the other branches -- why? // did I break something? } else if (from instanceof HTMLOptionElement && to instanceof HTMLOptionElement) { syncBooleanAttribute(from, to, 'selected', ctx) } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { let fromValue = from.value; let toValue = to.value; if (ignoreAttribute('value', to, 'update', ctx)) { return; } if (fromValue !== toValue) { to.value = fromValue; } if (to.firstChild && to.firstChild.nodeValue !== fromValue) { to.firstChild.nodeValue = fromValue } } } /** * ============================================================================= * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style * ============================================================================= * @param {Element} newHeadTag * @param {Element} currentHead * @param {MorphContext} ctx * @returns {Promise<void>[]} */ function handleHeadElement(newHeadTag, currentHead, ctx) { /** * @type {Node[]} */ let added = [] /** * @type {Element[]} */ let removed = [] /** * @type {Element[]} */ let preserved = [] /** * @type {Element[]} */ let nodesToAppend = [] let headMergeStyle = ctx.head.style; // put all new head elements into a Map, by their outerHTML let srcToNewHeadNodes = new Map(); for (const newHeadChild of newHeadTag.children) { srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); } // for each elt in the current head for (const currentHeadElt of currentHead.children) { // If the current head element is in the map let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); let isReAppended = ctx.head.shouldReAppend(currentHeadElt); let isPreserved = ctx.head.shouldPreserve(currentHeadElt); if (inNewContent || isPreserved) { if (isReAppended) { // remove the current version and let the new version replace it and re-execute removed.push(currentHeadElt); } else { // this element already exists and should not be re-appended, so remove it from // the new content map, preserving it in the DOM srcToNewHeadNodes.delete(currentHeadElt.outerHTML); preserved.push(currentHeadElt); } } else { if (headMergeStyle === "append") { // we are appending and this existing element is not new content // so if and only if it is marked for re-append do we do anything if (isReAppended) { removed.push(currentHeadElt); nodesToAppend.push(currentHeadElt); } } else { // if this is a merge, we remove this content since it is not in the new head if (ctx.head.shouldRemove(currentHeadElt) !== false) { removed.push(currentHeadElt); } } } } // Push the remaining new head elements in the Map into the // nodes to append to the head tag nodesToAppend.push(...srcToNewHeadNodes.values()); log("to append: ", nodesToAppend); let promises = []; for (const newNode of nodesToAppend) { log("adding: ", newNode); // TODO: This could theoretically be null, based on type let newElt = /** @type {ChildNode} */ ( document.createRange().createContextualFragment(newNode.outerHTML).firstChild); log(newElt); if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { if (('href' in newElt && newElt.href) || ('src' in newElt && newElt.src)) { /** @type {(result?: any) => void} */ let resolve; let promise = new Promise(function (_resolve) { resolve = _resolve; }); newElt.addEventListener('load', function () { resolve(); }); promises.push(promise); } currentHead.appendChild(newElt); ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); } } // remove all removed elements, after we have appended the new elements to avoid // additional network requests for things like style sheets for (const removedElement of removed) { if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { currentHead.removeChild(removedElement); ctx.callbacks.afterNodeRemoved(removedElement); } } ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); return promises; } //============================================================================= // Misc //============================================================================= /** * @param {any[]} _args */ function log(..._args) { //console.log(args); } function noOp() { } /** * Deep merges the config object and the Idiomoroph.defaults object to * produce a final configuration object * @param {Config} config * @returns {ConfigInternal} */ function mergeDefaults(config) { /** * @type {ConfigInternal} */ let finalConfig= Object.assign({}, defaults); // copy top level stuff into final config Object.assign(finalConfig, config); // copy callbacks into final config (do this to deep merge the callbacks) finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks); // copy head config into final config (do this to deep merge the head) finalConfig.head = Object.assign({}, defaults.head, config.head); return finalConfig; } /** * * @param {Element} oldNode * @param {Element} newContent * @param {Config} config * @returns {MorphContext} */ function createMorphContext(oldNode, newContent, config) { const mergedConfig = mergeDefaults(config); return { target: oldNode, newContent: newContent, config: mergedConfig, morphStyle: mergedConfig.morphStyle, ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, idMap: createIdMap(oldNode, newContent), deadIds: new Set(), callbacks: mergedConfig.callbacks, head: mergedConfig.head } } /** * * @param {Node | null} node1 * @param {Node | null} node2 * @param {MorphContext} ctx * @returns {boolean} */ // TODO: The function handles this as if it's Element or null, but the function is called in // places where the arguments may be just a Node, not an Element function isIdSetMatch(node1, node2, ctx) { if (node1 == null || node2 == null) { return false; } if ((node1 instanceof Element) && (node2 instanceof Element) && node1.tagName === node2.tagName) { if (node1.id !== "" && node1.id === node2.id) { return true; } else { return getIdIntersectionCount(ctx, node1, node2) > 0; } } return false; } /** * * @param {Node | null} node1 * @param {Node | null} node2 * @returns {boolean} */ function isSoftMatch(node1, node2) { if (node1 == null || node2 == null) { return false; } return node1.nodeType === node2.nodeType && // ok to cast: if one is not element, `tagName` will be undefined and we'll compare that /** @type {Element} */ (node1).tagName === /** @type {Element} */ (node2).tagName } /** * * @param {Node} startInclusive * @param {Node} endExclusive * @param {MorphContext} ctx * @returns {Node | null} */ function removeNodesBetween(startInclusive, endExclusive, ctx) { /** @type {Node | null} */ let cursor = startInclusive; while (cursor !== endExclusive) { let tempNode = /** @type {Node} */ (cursor); // TODO: Prefer assigning to a new variable here or expand the type of startInclusive // to be Node | null cursor = tempNode.nextSibling; removeNode(tempNode, ctx); } removeIdsFromConsideration(ctx, endExclusive); return endExclusive.nextSibling; } /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential id match * for the newChild. We stop if we find a potential id match for the new child OR * if the number of potential id matches we are discarding is greater than the * potential id matches for the new child * ============================================================================= * @param {Node} newContent * @param {Node} oldParent * @param {Node} newChild * @param {Node} insertionPoint * @param {MorphContext} ctx * @returns {null | Node} */ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { // max id matches we are willing to discard in our search let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); /** * @type {Node | null} */ let potentialMatch = null; // only search forward if there is a possibility of an id match if (newChildPotentialIdCount > 0) { // TODO: This is ghosting the potentialMatch variable outside of this block. // Probably an error potentialMatch = insertionPoint; // if there is a possibility of an id match, scan forward // keep track of the potential id match count we are discarding (the // newChildPotentialIdCount must be greater than this to make it likely // worth it) let otherMatchCount = 0; while (potentialMatch != null) { // If we have an id match, return the current potential match if (isIdSetMatch(newChild, potentialMatch, ctx)) { return potentialMatch; } // computer the other potential matches of this new content otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); if (otherMatchCount > newChildPotentialIdCount) { // if we have more potential id matches in _other_ content, we // do not have a good candidate for an id match, so return null return null; } // advanced to the next old content child potentialMatch = potentialMatch.nextSibling; } } return potentialMatch; } /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential soft match * for the newChild. We stop if we find a potential soft match for the new child OR * if we find a potential id match in the old parents children OR if we find two * potential soft matches for the next two pieces of new content * ============================================================================= * @param {Node} newContent * @param {Node} oldParent * @param {Node} newChild * @param {Node} insertionPoint * @param {MorphContext} ctx * @returns {null | Node} */ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { /** * @type {Node | null} */ let potentialSoftMatch = insertionPoint; /** * @type {Node | null} */ let nextSibling = newChild.nextSibling; let siblingSoftMatchCount = 0; while (potentialSoftMatch != null) { if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { // the current potential soft match has a potential id set match with the remaining new // content so bail out of looking return null; } // if we have a soft match with the current node, return it if (isSoftMatch(newChild, potentialSoftMatch)) { return potentialSoftMatch; } if (isSoftMatch(nextSibling, potentialSoftMatch)) { // the next new node has a soft match with this node, so // increment the count of future soft matches siblingSoftMatchCount++; // ok to cast: if it was null it couldn't be a soft match nextSibling = /** @type {Node} */ (nextSibling).nextSibling; // If there are two future soft matches, bail to allow the siblings to soft match // so that we don't consume future soft matches for the sake of the current node if (siblingSoftMatchCount >= 2) { return null; } } // advanced to the next old content child potentialSoftMatch = potentialSoftMatch.nextSibling; } return potentialSoftMatch; } /** @type {WeakSet<Node>} */ const generatedByIdiomorph = new WeakSet(); /** * * @param {string} newContent * @returns {Node | null | DocumentFragment} */ function parseContent(newContent) { let parser = new DOMParser(); // remove svgs to avoid false-positive matches on head, etc. let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { let content = parser.parseFromString(newContent, "text/html"); // if it is a full HTML document, return the document itself as the parent container if (contentWithSvgsRemoved.match(/<\/html>/)) { generatedByIdiomorph.add(content); return content; } else { // otherwise return the html element as the parent container let htmlElement = content.firstChild; if (htmlElement) { generatedByIdiomorph.add(htmlElement); return htmlElement; } else { return null; } } } else { // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help // deal with touchy tags like tr, tbody, etc. let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); let content = /** @type {HTMLTemplateElement} */ (responseDoc.body.querySelector('template')).content; generatedByIdiomorph.add(content); return content } } /** * * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent * @returns {Element} */ function normalizeContent(newContent) { if (newContent == null) { // noinspection UnnecessaryLocalVariableJS const dummyParent = document.createElement('div'); return dummyParent; } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) { // the template tag created by idiomorph parsing can serve as a dummy parent return /** @type {Element} */ (newContent); } else if (newContent instanceof Node) { // a single node is added as a child to a dummy parent const dummyParent = document.createElement('div'); dummyParent.append(newContent); return dummyParent; } else { // all nodes in the array or HTMLElement collection are consolidated under // a single dummy parent element const dummyParent = document.createElement('div'); for (const elt of [...newContent]) { dummyParent.append(elt); } return dummyParent; } } /** * * @param {Node | null} previousSibling * @param {Node} morphedNode * @param {Node | null} nextSibling * @returns {Node[]} */ function insertSiblings(previousSibling, morphedNode, nextSibling) { /** * @type {Node[]} */ let stack = [] /** * @type {Node[]} */ let added = [] while (previousSibling != null) { stack.push(previousSibling); previousSibling = previousSibling.previousSibling; } // Base the loop on the node variable, so that you do not need runtime checks for // undefined value inside the loop let node = stack.pop(); while (node !== undefined) { added.push(node); // push added preceding siblings on in order and insert morphedNode.parentElement?.insertBefore(node, morphedNode); node = stack.pop(); } added.push(morphedNode); while (nextSibling != null) { stack.push(nextSibling); added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add nextSibling = nextSibling.nextSibling; } while (stack.length > 0) { const node = /** @type {Node} */ (stack.pop()); morphedNode.parentElement?.insertBefore(node, morphedNode.nextSibling); } return added; } /** * * @param {Element} newContent * @param {Element} oldNode * @param {MorphContext} ctx * @returns {Node | null} */ function findBestNodeMatch(newContent, oldNode, ctx) { /** * @type {Node | null} */ let currentElement; currentElement = newContent.firstChild; /** * @type {Node | null} */ let bestElement = currentElement; let score = 0; while (currentElement) { let newScore = scoreElement(currentElement, oldNode, ctx); if (newScore > score) { bestElement = currentElement; score = newScore; } currentElement = currentElement.nextSibling; } return bestElement; } /** * * @param {Node | null} node1 * @param {Element} node2 * @param {MorphContext} ctx * @returns {number} */ // TODO: The function handles node1 and node2 as if they are Elements but the function is // called in places where node1 and node2 may be just Nodes, not Elements function scoreElement(node1, node2, ctx) { if (isSoftMatch(node1, node2)) { // ok to cast: isSoftMatch performs a null check return .5 + getIdIntersectionCount(ctx, /** @type {Node} */ (node1), node2); } return 0; } /** * * @param {Node} tempNode * @param {MorphContext} ctx */ // TODO: The function handles tempNode as if it's Element but the function is called in // places where tempNode may be just a Node, not an Element function removeNode(tempNode, ctx) { removeIdsFromConsideration(ctx, tempNode) if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; tempNode.parentNode?.removeChild(tempNode); ctx.callbacks.afterNodeRemoved(tempNode); } //============================================================================= // ID Set Functions //============================================================================= /** * * @param {MorphContext} ctx * @param {string} id * @returns {boolean} */ function isIdInConsideration(ctx, id) { return !ctx.deadIds.has(id); } /** * * @param {MorphContext} ctx * @param {string} id * @param {Node} targetNode * @returns {boolean} */ function idIsWithinNode(ctx, id, targetNode) { let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; return idSet.has(id); } /** * * @param {MorphContext} ctx * @param {Node} node * @returns {void} */ function removeIdsFromConsideration(ctx, node) { let idSet = ctx.idMap.get(node) || EMPTY_SET; for (const id of idSet) { ctx.deadIds.add(id); } } /** * * @param {MorphContext} ctx * @param {Node} node1 * @param {Node} node2 * @returns {number} */ function getIdIntersectionCount(ctx, node1, node2) { let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; let matchCount = 0; for (const id of sourceSet) { // a potential match is an id in the source and potentialIdsSet, but // that has not already been merged into the DOM if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { ++matchCount; } } return matchCount; } /** * A bottom up algorithm that finds all elements with ids inside of the node * argument and populates id sets for those nodes and all their parents, generating * a set of ids contained within all nodes for the entire hierarchy in the DOM * * @param {Element} node * @param {Map<Node, Set<string>>} idMap */ function populateIdMapForNode(node, idMap) { let nodeParent = node.parentElement; // find all elements with an id property let idElements = node.querySelectorAll('[id]'); for (const elt of idElements) { /** * @type {Element|null} */ let current = elt; // walk up the parent hierarchy of that element, adding the id // of element to the parent's id set while (current !== nodeParent && current != null) { let idSet = idMap.get(current); // if the id set doesn't exist, create it and insert it in the map if (idSet == null) { idSet = new Set(); idMap.set(current, idSet); } idSet.add(elt.id); current = current.parentElement; } } } /** * This function computes a map of nodes to all ids contained within that node (inclusive of the * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows * for a looser definition of "matching" than tradition id matching, and allows child nodes * to contribute to a parent nodes matching. * * @param {Element} oldContent the old content that will be morphed * @param {Element} newContent the new content to morph to * @returns {Map<Node, Set<string>>} a map of nodes to id sets for the */ function createIdMap(oldContent, newContent) { /** * * @type {Map<Node, Set<string>>} */ let idMap = new Map(); populateIdMapForNode(oldContent, idMap); populateIdMapForNode(newContent, idMap); return idMap; } //============================================================================= // This is what ends up becoming the Idiomorph global object //============================================================================= return { morph, defaults } })(); /** * @typedef {object} ConfigHead * * @property {'merge' | 'append' | 'morph' | 'none'} [style] * @property {boolean} [block] * @property {boolean} [ignore] * @property {function(Element): boolean} [shouldPreserve] * @property {function(Element): boolean} [shouldReAppend] * @property {function(Element): boolean} [shouldRemove] * @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed] */ /** * @typedef {object} ConfigCallbacks * * @property {function(Node): boolean} [beforeNodeAdded] * @property {function(Node): void} [afterNodeAdded] * @property {function(Element, Node): boolean} [beforeNodeMorphed] * @property {function(Element, Node): void} [afterNodeMorphed] * @property {function(Element): boolean} [beforeNodeRemoved] * @property {function(Element): void} [afterNodeRemoved] * @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated] */ /** * @typedef {object} Config * * @property {'outerHTML' | 'innerHTML'} [morphStyle] * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {ConfigCallbacks} [callbacks] * @property {ConfigHead} [head] */ /** * @typedef {function} NoOp * * @returns {void} */ /** * @typedef {object} ConfigHeadInternal * * @property {'merge' | 'append' | 'morph' | 'none'} style * @property {boolean} [block] * @property {boolean} [ignore] * @property {(function(Element): boolean) | NoOp} shouldPreserve * @property {(function(Element): boolean) | NoOp} shouldReAppend * @property {(function(Element): boolean) | NoOp} shouldRemove * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed */ /** * @typedef {object} ConfigCallbacksInternal * * @property {(function(Node): boolean) | NoOp} beforeNodeAdded * @property {(function(Node): void) | NoOp} afterNodeAdded * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved * @property {(function(Node): void) | NoOp} afterNodeRemoved * @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated */ /** * @typedef {object} ConfigInternal * * @property {'outerHTML' | 'innerHTML'} morphStyle * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {ConfigCallbacksInternal} callbacks * @property {ConfigHeadInternal} head */ /** * @typedef {Function} Morph * * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {undefined | HTMLCollection | Node[]} */ // base IIFE to define idiomorph /** * * @type {{defaults: ConfigInternal, morph: Morph}} */ window.Idiomorph.original = (function () { 'use strict'; /** * @typedef {object} MorphContext * * @property {Node} target * @property {Node} newContent * @property {ConfigInternal} config * @property {ConfigInternal['morphStyle']} morphStyle * @property {ConfigInternal['ignoreActive']} ignoreActive * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue * @property {Map<Node, Set<string>>} idMap * @property {Set<string>} deadIds * @property {ConfigInternal['callbacks']} callbacks * @property {ConfigInternal['head']} head */ //============================================================================= // AND NOW IT BEGINS... //============================================================================= /** * * @type {Set<string>} */ let EMPTY_SET = new Set(); /** * Default configuration values, updatable by users now * @type {ConfigInternal} */ let defaults = { morphStyle: "outerHTML", callbacks : { beforeNodeAdded: noOp, afterNodeAdded: noOp, beforeNodeMorphed: noOp, afterNodeMorphed: noOp, beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp, }, head: { style: 'merge', shouldPreserve: function (elt) { return elt.getAttribute("im-preserve") === "true"; }, shouldReAppend: function (elt) { return elt.getAttribute("im-re-append") === "true"; }, shouldRemove: noOp, afterHeadMorphed: noOp, } }; /** * ============================================================================= * Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren * ============================================================================= * * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {undefined | HTMLCollection | Node[]} */ function morph(oldNode, newContent, config = {}) { if (oldNode instanceof Document) { oldNode = oldNode.documentElement; } if (typeof newContent === 'string') { newContent = parseContent(newContent); } let normalizedContent = normalizeContent(newContent); let ctx = createMorphContext(oldNode, normalizedContent, config); return morphNormalizedContent(oldNode, normalizedContent, ctx); } /** * * @param {Element} oldNode * @param {Element} normalizedNewContent * @param {MorphContext} ctx * @returns {undefined | HTMLCollection| Node[]} */ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { if (ctx.head.block) { let oldHead = oldNode.querySelector('head'); let newHead = normalizedNewContent.querySelector('head'); if (oldHead && newHead) { let promises = handleHeadElement(newHead, oldHead, ctx); // when head promises resolve, call morph again, ignoring the head tag Promise.all(promises).then(function () { morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { head: { block: false, ignore: true } })); }); return; } } if (ctx.morphStyle === "innerHTML") { // innerHTML, so we are only updating the children morphChildren(normalizedNewContent, oldNode, ctx); return Array.from(oldNode.children); } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { // otherwise find the best element match in the new content, morph that, and merge its siblings // into either side of the best match let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); // stash the siblings that will need to be inserted on either side of the best match let previousSibling = bestMatch?.previousSibling ?? null; let nextSibling = bestMatch?.nextSibling ?? null; // morph it let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); if (bestMatch) { // if there was a best match, merge the siblings in too and return the // whole bunch if (morphedNode) { return insertSiblings(previousSibling, morphedNode, nextSibling); } } else { // otherwise nothing was added to the DOM return [] } } else { throw "Do not understand how to morph style " + ctx.morphStyle; } } /** * @param {Node} possibleActiveElement * @param {MorphContext} ctx * @returns {boolean} */ // TODO: ignoreActive and ignoreActiveValue are marked as optional since they are not // initialised in the default config object. As a result the && in the function body may // return undefined instead of boolean. Either expand the type of the return value to // include undefined or wrap the ctx.ignoreActiveValue into a Boolean() function ignoreValueOfActiveElement(possibleActiveElement, ctx) { return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body; } /** * @param {Node} oldNode root node to merge content into * @param {Node | null} newContent new content to merge * @param {MorphContext} ctx the merge context * @returns {Node | null} the element that ended up in the DOM */ function morphOldNodeTo(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) { // don't morph focused element } else if (newContent == null) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; oldNode.parentNode?.removeChild(oldNode); ctx.callbacks.afterNodeRemoved(oldNode); return null; } else if (!isSoftMatch(oldNode, newContent)) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; oldNode.parentNode?.replaceChild(newContent, oldNode); ctx.callbacks.afterNodeAdded(newContent); ctx.callbacks.afterNodeRemoved(oldNode); return newContent; } else { if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { // ignore the head element } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { // ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above handleHeadElement(/** @type {HTMLHeadElement} */ (newContent), oldNode, ctx); } else { syncNodeFrom(newContent, oldNode, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { morphChildren(newContent, oldNode, ctx); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); return oldNode; } return null; } /** * This is the core algorithm for matching up children. The idea is to use id sets to try to match up * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but * by using id sets, we are able to better match up with content deeper in the DOM. * * Basic algorithm is, for each node in the new content: * * - if we have reached the end of the old parent, append the new content * - if the new content has an id set match with the current insertion point, morph * - search for an id set match * - if id set match found, morph * - otherwise search for a "soft" match * - if a soft match is found, morph * - otherwise, prepend the new node before the current insertion point * * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch() and findSoftMatch() for details. * * @param {Node} newParent the parent element of the new content * @param {Node} oldParent the old content that we are merging the new content into * @param {MorphContext} ctx the merge context * @returns {void} */ function morphChildren(newParent, oldParent, ctx) { if (newParent instanceof HTMLTemplateElement && oldParent instanceof HTMLTemplateElement) { newParent = newParent.content; oldParent = oldParent.content; } /** * * @type {Node | null} */ let nextNewChild = newParent.firstChild; /** * * @type {Node | null} */ let insertionPoint = oldParent.firstChild; let newChild; // run through all the new content while (nextNewChild) { newChild = nextNewChild; nextNewChild = newChild.nextSibling; // if we are at the end of the exiting parent's children, just append if (insertionPoint == null) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; oldParent.appendChild(newChild); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); continue; } // if the current node has an id set match then morph if (isIdSetMatch(newChild, insertionPoint, ctx)) { morphOldNodeTo(insertionPoint, newChild, ctx); insertionPoint = insertionPoint.nextSibling; removeIdsFromConsideration(ctx, newChild); continue; } // otherwise search forward in the existing old children for an id set match let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); // if we found a potential match, remove the nodes until that point and morph if (idSetMatch) { insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); morphOldNodeTo(idSetMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } // no id set match found, so scan forward for a soft match for the current node let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); // if we found a soft match for the current node, morph if (softMatch) { insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); morphOldNodeTo(softMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } // abandon all hope of morphing, just insert the new child before the insertion point // and move on if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; oldParent.insertBefore(newChild, insertionPoint); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); } // remove any remaining old nodes that didn't match up with new content while (insertionPoint !== null) { let tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } } //============================================================================= // Attribute Syncing Code //============================================================================= /** * @param {string} attr the attribute to be mutated * @param {Element} to the element that is going to be updated * @param {"update" | "remove"} updateType * @param {MorphContext} ctx the merge context * @returns {boolean} true if the attribute should be ignored, false otherwise */ function ignoreAttribute(attr, to, updateType, ctx) { if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){ return true; } return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; } /** * syncs a given node with another node, copying over all attributes and * inner element state from the 'from' node to the 'to' node * * @param {Node} from the element to copy attributes & state from * @param {Node} to the element to copy attributes & state to * @param {MorphContext} ctx the merge context */ function syncNodeFrom(from, to, ctx) { let type = from.nodeType // if is an element type, sync the attributes from the // new node into the new node if (type === 1 /* element type */) { const fromEl = /** @type {Element} */ (from); const toEl = /** @type {Element} */ (to); const fromAttributes = fromEl.attributes; const toAttributes = toEl.attributes; for (const fromAttribute of fromAttributes) { if (ignoreAttribute(fromAttribute.name, toEl, 'update', ctx)) { continue; } if (toEl.getAttribute(fromAttribute.name) !== fromAttribute.value) { toEl.setAttribute(fromAttribute.name, fromAttribute.value); } } // iterate backwards to avoid skipping over items when a delete occurs for (let i = toAttributes.length - 1; 0 <= i; i--) { const toAttribute = toAttributes[i]; if (!fromEl.hasAttribute(toAttribute.name)) { if (ignoreAttribute(toAttribute.name, toEl, 'remove', ctx)) { continue; } toEl.removeAttribute(toAttribute.name); } } } // sync text nodes if (type === 8 /* comment */ || type === 3 /* text */) { if (to.nodeValue !== from.nodeValue) { to.nodeValue = from.nodeValue; } } if (!ignoreValueOfActiveElement(to, ctx)) { // sync input values syncInputValue(from, to, ctx); } } /** * @param {Element} from element to sync the value from * @param {Element} to element to sync the value to * @param {string} attributeName the attribute name * @param {MorphContext} ctx the merge context */ function syncBooleanAttribute(from, to, attributeName, ctx) { // TODO: prefer set/getAttribute here if (!(from instanceof Element && to instanceof Element)) return // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties const fromLiveValue = from[attributeName], toLiveValue = to[attributeName]; if (fromLiveValue !== toLiveValue) { let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx); if (!ignoreUpdate) { // update attribute's associated DOM property // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties to[attributeName] = from[attributeName]; } if (fromLiveValue) { if (!ignoreUpdate) { // TODO: do we really want this? tests say so but it feels wrong to.setAttribute(attributeName, fromLiveValue); } } else { if (!ignoreAttribute(attributeName, to, 'remove', ctx)) { to.removeAttribute(attributeName); } } } } /** * NB: many bothans died to bring us information: * * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 * * @param {Node} from the element to sync the input value from * @param {Node} to the element to sync the input value to * @param {MorphContext} ctx the merge context */ function syncInputValue(from, to, ctx) { if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== 'file') { let fromValue = from.value; let toValue = to.value; // sync boolean attributes syncBooleanAttribute(from, to, 'checked', ctx); syncBooleanAttribute(from, to, 'disabled', ctx); if (!from.hasAttribute('value')) { if (!ignoreAttribute('value', to, 'remove', ctx)) { to.value = ''; to.removeAttribute('value'); } } else if (fromValue !== toValue) { if (!ignoreAttribute('value', to, 'update', ctx)) { to.setAttribute('value', fromValue); to.value = fromValue; } } // TODO: QUESTION(1cg): this used to only check `from` unlike the other branches -- why? // did I break something? } else if (from instanceof HTMLOptionElement && to instanceof HTMLOptionElement) { syncBooleanAttribute(from, to, 'selected', ctx) } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { let fromValue = from.value; let toValue = to.value; if (ignoreAttribute('value', to, 'update', ctx)) { return; } if (fromValue !== toValue) { to.value = fromValue; } if (to.firstChild && to.firstChild.nodeValue !== fromValue) { to.firstChild.nodeValue = fromValue } } } /** * ============================================================================= * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style * ============================================================================= * @param {Element} newHeadTag * @param {Element} currentHead * @param {MorphContext} ctx * @returns {Promise<void>[]} */ function handleHeadElement(newHeadTag, currentHead, ctx) { /** * @type {Node[]} */ let added = [] /** * @type {Element[]} */ let removed = [] /** * @type {Element[]} */ let preserved = [] /** * @type {Element[]} */ let nodesToAppend = [] let headMergeStyle = ctx.head.style; // put all new head elements into a Map, by their outerHTML let srcToNewHeadNodes = new Map(); for (const newHeadChild of newHeadTag.children) { srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); } // for each elt in the current head for (const currentHeadElt of currentHead.children) { // If the current head element is in the map let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); let isReAppended = ctx.head.shouldReAppend(currentHeadElt); let isPreserved = ctx.head.shouldPreserve(currentHeadElt); if (inNewContent || isPreserved) { if (isReAppended) { // remove the current version and let the new version replace it and re-execute removed.push(currentHeadElt); } else { // this element already exists and should not be re-appended, so remove it from // the new content map, preserving it in the DOM srcToNewHeadNodes.delete(currentHeadElt.outerHTML); preserved.push(currentHeadElt); } } else { if (headMergeStyle === "append") { // we are appending and this existing element is not new content // so if and only if it is marked for re-append do we do anything if (isReAppended) { removed.push(currentHeadElt); nodesToAppend.push(currentHeadElt); } } else { // if this is a merge, we remove this content since it is not in the new head if (ctx.head.shouldRemove(currentHeadElt) !== false) { removed.push(currentHeadElt); } } } } // Push the remaining new head elements in the Map into the // nodes to append to the head tag nodesToAppend.push(...srcToNewHeadNodes.values()); log("to append: ", nodesToAppend); let promises = []; for (const newNode of nodesToAppend) { log("adding: ", newNode); // TODO: This could theoretically be null, based on type let newElt = /** @type {ChildNode} */ ( document.createRange().createContextualFragment(newNode.outerHTML).firstChild); log(newElt); if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { if (('href' in newElt && newElt.href) || ('src' in newElt && newElt.src)) { /** @type {(result?: any) => void} */ let resolve; let promise = new Promise(function (_resolve) { resolve = _resolve; }); newElt.addEventListener('load', function () { resolve(); }); promises.push(promise); } currentHead.appendChild(newElt); ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); } } // remove all removed elements, after we have appended the new elements to avoid // additional network requests for things like style sheets for (const removedElement of removed) { if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { currentHead.removeChild(removedElement); ctx.callbacks.afterNodeRemoved(removedElement); } } ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); return promises; } //============================================================================= // Misc //============================================================================= /** * @param {any[]} _args */ function log(..._args) { //console.log(args); } function noOp() { } /** * Deep merges the config object and the Idiomoroph.defaults object to * produce a final configuration object * @param {Config} config * @returns {ConfigInternal} */ function mergeDefaults(config) { /** * @type {ConfigInternal} */ let finalConfig= Object.assign({}, defaults); // copy top level stuff into final config Object.assign(finalConfig, config); // copy callbacks into final config (do this to deep merge the callbacks) finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks); // copy head config into final config (do this to deep merge the head) finalConfig.head = Object.assign({}, defaults.head, config.head); return finalConfig; } /** * * @param {Element} oldNode * @param {Element} newContent * @param {Config} config * @returns {MorphContext} */ function createMorphContext(oldNode, newContent, config) { const mergedConfig = mergeDefaults(config); return { target: oldNode, newContent: newContent, config: mergedConfig, morphStyle: mergedConfig.morphStyle, ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, idMap: createIdMap(oldNode, newContent), deadIds: new Set(), callbacks: mergedConfig.callbacks, head: mergedConfig.head } } /** * * @param {Node | null} node1 * @param {Node | null} node2 * @param {MorphContext} ctx * @returns {boolean} */ // TODO: The function handles this as if it's Element or null, but the function is called in // places where the arguments may be just a Node, not an Element function isIdSetMatch(node1, node2, ctx) { if (node1 == null || node2 == null) { return false; } if ((node1 instanceof Element) && (node2 instanceof Element) && node1.tagName === node2.tagName) { if (node1.id !== "" && node1.id === node2.id) { return true; } else { return getIdIntersectionCount(ctx, node1, node2) > 0; } } return false; } /** * * @param {Node | null} node1 * @param {Node | null} node2 * @returns {boolean} */ function isSoftMatch(node1, node2) { if (node1 == null || node2 == null) { return false; } return node1.nodeType === node2.nodeType && // ok to cast: if one is not element, `tagName` will be undefined and we'll compare that /** @type {Element} */ (node1).tagName === /** @type {Element} */ (node2).tagName } /** * * @param {Node} startInclusive * @param {Node} endExclusive * @param {MorphContext} ctx * @returns {Node | null} */ function removeNodesBetween(startInclusive, endExclusive, ctx) { /** @type {Node | null} */ let cursor = startInclusive; while (cursor !== endExclusive) { let tempNode = /** @type {Node} */ (cursor); // TODO: Prefer assigning to a new variable here or expand the type of startInclusive // to be Node | null cursor = tempNode.nextSibling; removeNode(tempNode, ctx); } removeIdsFromConsideration(ctx, endExclusive); return endExclusive.nextSibling; } /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential id match * for the newChild. We stop if we find a potential id match for the new child OR * if the number of potential id matches we are discarding is greater than the * potential id matches for the new child * ============================================================================= * @param {Node} newContent * @param {Node} oldParent * @param {Node} newChild * @param {Node} insertionPoint * @param {MorphContext} ctx * @returns {null | Node} */ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { // max id matches we are willing to discard in our search let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); /** * @type {Node | null} */ let potentialMatch = null; // only search forward if there is a possibility of an id match if (newChildPotentialIdCount > 0) { // TODO: This is ghosting the potentialMatch variable outside of this block. // Probably an error potentialMatch = insertionPoint; // if there is a possibility of an id match, scan forward // keep track of the potential id match count we are discarding (the // newChildPotentialIdCount must be greater than this to make it likely // worth it) let otherMatchCount = 0; while (potentialMatch != null) { // If we have an id match, return the current potential match if (isIdSetMatch(newChild, potentialMatch, ctx)) { return potentialMatch; } // computer the other potential matches of this new content otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); if (otherMatchCount > newChildPotentialIdCount) { // if we have more potential id matches in _other_ content, we // do not have a good candidate for an id match, so return null return null; } // advanced to the next old content child potentialMatch = potentialMatch.nextSibling; } } return potentialMatch; } /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential soft match * for the newChild. We stop if we find a potential soft match for the new child OR * if we find a potential id match in the old parents children OR if we find two * potential soft matches for the next two pieces of new content * ============================================================================= * @param {Node} newContent * @param {Node} oldParent * @param {Node} newChild * @param {Node} insertionPoint * @param {MorphContext} ctx * @returns {null | Node} */ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { /** * @type {Node | null} */ let potentialSoftMatch = insertionPoint; /** * @type {Node | null} */ let nextSibling = newChild.nextSibling; let siblingSoftMatchCount = 0; while (potentialSoftMatch != null) { if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { // the current potential soft match has a potential id set match with the remaining new // content so bail out of looking return null; } // if we have a soft match with the current node, return it if (isSoftMatch(newChild, potentialSoftMatch)) { return potentialSoftMatch; } if (isSoftMatch(nextSibling, potentialSoftMatch)) { // the next new node has a soft match with this node, so // increment the count of future soft matches siblingSoftMatchCount++; // ok to cast: if it was null it couldn't be a soft match nextSibling = /** @type {Node} */ (nextSibling).nextSibling; // If there are two future soft matches, bail to allow the siblings to soft match // so that we don't consume future soft matches for the sake of the current node if (siblingSoftMatchCount >= 2) { return null; } } // advanced to the next old content child potentialSoftMatch = potentialSoftMatch.nextSibling; } return potentialSoftMatch; } /** @type {WeakSet<Node>} */ const generatedByIdiomorph = new WeakSet(); /** * * @param {string} newContent * @returns {Node | null | DocumentFragment} */ function parseContent(newContent) { let parser = new DOMParser(); // remove svgs to avoid false-positive matches on head, etc. let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { let content = parser.parseFromString(newContent, "text/html"); // if it is a full HTML document, return the document itself as the parent container if (contentWithSvgsRemoved.match(/<\/html>/)) { generatedByIdiomorph.add(content); return content; } else { // otherwise return the html element as the parent container let htmlElement = content.firstChild; if (htmlElement) { generatedByIdiomorph.add(htmlElement); return htmlElement; } else { return null; } } } else { // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help // deal with touchy tags like tr, tbody, etc. let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); let content = /** @type {HTMLTemplateElement} */ (responseDoc.body.querySelector('template')).content; generatedByIdiomorph.add(content); return content } } /** * * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent * @returns {Element} */ function normalizeContent(newContent) { if (newContent == null) { // noinspection UnnecessaryLocalVariableJS const dummyParent = document.createElement('div'); return dummyParent; } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) { // the template tag created by idiomorph parsing can serve as a dummy parent return /** @type {Element} */ (newContent); } else if (newContent instanceof Node) { // a single node is added as a child to a dummy parent const dummyParent = document.createElement('div'); dummyParent.append(newContent); return dummyParent; } else { // all nodes in the array or HTMLElement collection are consolidated under // a single dummy parent element const dummyParent = document.createElement('div'); for (const elt of [...newContent]) { dummyParent.append(elt); } return dummyParent; } } /** * * @param {Node | null} previousSibling * @param {Node} morphedNode * @param {Node | null} nextSibling * @returns {Node[]} */ function insertSiblings(previousSibling, morphedNode, nextSibling) { /** * @type {Node[]} */ let stack = [] /** * @type {Node[]} */ let added = [] while (previousSibling != null) { stack.push(previousSibling); previousSibling = previousSibling.previousSibling; } // Base the loop on the node variable, so that you do not need runtime checks for // undefined value inside the loop let node = stack.pop(); while (node !== undefined) { added.push(node); // push added preceding siblings on in order and insert morphedNode.parentElement?.insertBefore(node, morphedNode); node = stack.pop(); } added.push(morphedNode); while (nextSibling != null) { stack.push(nextSibling); added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add nextSibling = nextSibling.nextSibling; } while (stack.length > 0) { const node = /** @type {Node} */ (stack.pop()); morphedNode.parentElement?.insertBefore(node, morphedNode.nextSibling); } return added; } /** * * @param {Element} newContent * @param {Element} oldNode * @param {MorphContext} ctx * @returns {Node | null} */ function findBestNodeMatch(newContent, oldNode, ctx) { /** * @type {Node | null} */ let currentElement; currentElement = newContent.firstChild; /** * @type {Node | null} */ let bestElement = currentElement; let score = 0; while (currentElement) { let newScore = scoreElement(currentElement, oldNode, ctx); if (newScore > score) { bestElement = currentElement; score = newScore; } currentElement = currentElement.nextSibling; } return bestElement; } /** * * @param {Node | null} node1 * @param {Element} node2 * @param {MorphContext} ctx * @returns {number} */ // TODO: The function handles node1 and node2 as if they are Elements but the function is // called in places where node1 and node2 may be just Nodes, not Elements function scoreElement(node1, node2, ctx) { if (isSoftMatch(node1, node2)) { // ok to cast: isSoftMatch performs a null check return .5 + getIdIntersectionCount(ctx, /** @type {Node} */ (node1), node2); } return 0; } /** * * @param {Node} tempNode * @param {MorphContext} ctx */ // TODO: The function handles tempNode as if it's Element but the function is called in // places where tempNode may be just a Node, not an Element function removeNode(tempNode, ctx) { removeIdsFromConsideration(ctx, tempNode) if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; tempNode.parentNode?.removeChild(tempNode); ctx.callbacks.afterNodeRemoved(tempNode); } //============================================================================= // ID Set Functions //============================================================================= /** * * @param {MorphContext} ctx * @param {string} id * @returns {boolean} */ function isIdInConsideration(ctx, id) { return !ctx.deadIds.has(id); } /** * * @param {MorphContext} ctx * @param {string} id * @param {Node} targetNode * @returns {boolean} */ function idIsWithinNode(ctx, id, targetNode) { let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; return idSet.has(id); } /** * * @param {MorphContext} ctx * @param {Node} node * @returns {void} */ function removeIdsFromConsideration(ctx, node) { let idSet = ctx.idMap.get(node) || EMPTY_SET; for (const id of idSet) { ctx.deadIds.add(id); } } /** * * @param {MorphContext} ctx * @param {Node} node1 * @param {Node} node2 * @returns {number} */ function getIdIntersectionCount(ctx, node1, node2) { let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; let matchCount = 0; for (const id of sourceSet) { // a potential match is an id in the source and potentialIdsSet, but // that has not already been merged into the DOM if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { ++matchCount; } } return matchCount; } /** * A bottom up algorithm that finds all elements with ids inside of the node * argument and populates id sets for those nodes and all their parents, generating * a set of ids contained within all nodes for the entire hierarchy in the DOM * * @param {Element} node * @param {Map<Node, Set<string>>} idMap */ function populateIdMapForNode(node, idMap) { let nodeParent = node.parentElement; // find all elements with an id property let idElements = node.querySelectorAll('[id]'); for (const elt of idElements) { /** * @type {Element|null} */ let current = elt; // walk up the parent hierarchy of that element, adding the id // of element to the parent's id set while (current !== nodeParent && current != null) { let idSet = idMap.get(current); // if the id set doesn't exist, create it and insert it in the map if (idSet == null) { idSet = new Set(); idMap.set(current, idSet); } idSet.add(elt.id); current = current.parentElement; } } } /** * This function computes a map of nodes to all ids contained within that node (inclusive of the * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows * for a looser definition of "matching" than tradition id matching, and allows child nodes * to contribute to a parent nodes matching. * * @param {Element} oldContent the old content that will be morphed * @param {Element} newContent the new content to morph to * @returns {Map<Node, Set<string>>} a map of nodes to id sets for the */ function createIdMap(oldContent, newContent) { /** * * @type {Map<Node, Set<string>>} */ let idMap = new Map(); populateIdMapForNode(oldContent, idMap); populateIdMapForNode(newContent, idMap); return idMap; } //============================================================================= // This is what ends up becoming the Idiomorph global object //============================================================================= return { morph, defaults } })(); </script>
Script Preparation code:
const startEl = document.getElementById("start") const startElHTML = startEl.innerHTML const endEl = document.getElementById("end")
Tests:
NamedNodeMap
startEl.innerHTML = startElHTML window.Idiomorph.original.morph(startEl, endEl)
Array
startEl.innerHTML = startElHTML window.Idiomorph.morph(startEl, endEl)