diff --git a/README.md b/README.md
index fc6878920f..504921ce1a 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,20 @@ with.
## My JavaScript Demos - I Love JavaScript!
+* [Parsing Plus-Minus Ranges In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/plus-minus-range)
+* [Exploring Event.isTrusted In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/event-is-trusted)
+* [Table Row Linker Directive In Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/row-linker)
+* [Sending Messages Across Documents With The Broadcast Channel API](https://bennadel.github.io/JavaScript-Demos/demos/broadcast-api)
+* [Animating DOM Rectangles Over Focused Elements In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/focus-box)
+* [Linking To A Disclosure (Details) Element](https://bennadel.github.io/JavaScript-Demos/demos/scroll-to-details)
+* [Opening The Dialog Element As A Fly-out Sidebar](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element-sidebar)
+* [Exploring The Dialog Element In HTML](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element)
+* [Storing Metadata On Select Option Elements](https://bennadel.github.io/JavaScript-Demos/demos/select-option-dataset)
+* [Exploring Prev/Next Mechanics In HTMX](https://bennadel.github.io/JavaScript-Demos/demos/htmx-prev-next)
+* [Pixel Art With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/pixel-art-alpine)
+* [Movie Ranking With Sortable.js And Kendall Tau Distance](https://bennadel.github.io/JavaScript-Demos/demos/movie-rank)
+* [Using :scope To Identify The Host Element In A CSS Selector](https://bennadel.github.io/JavaScript-Demos/demos/scope-pseudo-class)
+* [Comparing Undefined Values In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/undefined-comparison)
* [Using The Button Form Attribute To Create Standalone Buttons In HTML](https://bennadel.github.io/JavaScript-Demos/demos/link-buttons)
* [Box Breathing Exercise With SpeechSynthesis And Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/box-breathing-alpine)
* [Using CSS Gap To Control Margins In Website Copy](https://bennadel.github.io/JavaScript-Demos/demos/margins-via-gap-css)
@@ -700,5 +714,5 @@ with.
Want more JavaScript goodness? Check out the [JavaScript blog entries][javascript-blog] on my website.
-[bennadel]: http://www.bennadel.com
-[javascript-blog]: http://www.bennadel.com/blog/tags/6-javascript-dhtml-blog-entries.htm
+[bennadel]: https://www.bennadel.com
+[javascript-blog]: https://www.bennadel.com/blog/tags/6-javascript-dhtml-blog-entries.htm
diff --git a/demos/broadcast-api/frame.htm b/demos/broadcast-api/frame.htm
new file mode 100644
index 0000000000..6e26e34a8d
--- /dev/null
+++ b/demos/broadcast-api/frame.htm
@@ -0,0 +1,75 @@
+
+
+
+
+
+ Broadcast Channel API Frame
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/broadcast-api/index.htm b/demos/broadcast-api/index.htm
new file mode 100644
index 0000000000..9b0d7694af
--- /dev/null
+++ b/demos/broadcast-api/index.htm
@@ -0,0 +1,30 @@
+
+
+
+
+
+ Sending Messages Across Documents With The Broadcast Channel API
+
+
+
+
+
+
+ Sending Messages Across Documents With The Broadcast Channel API
+
+
+
+ These are all <iframe> elements to the same URL:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/broadcast-api/main.css b/demos/broadcast-api/main.css
new file mode 100644
index 0000000000..74c4b740f5
--- /dev/null
+++ b/demos/broadcast-api/main.css
@@ -0,0 +1,107 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+.textBlock * {
+ &:focus,
+ &:focus-visible {
+ animation-name: none ;
+ outline: none ;
+ }
+}
+
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
+
+.frame-list {
+ display: flex ;
+ gap: 10px ;
+ flex-wrap: wrap ;
+
+ & iframe {
+ border: 1px solid #333333 ;
+ flex: 0 0 auto ;
+ height: 200px ;
+ width: 220px ;
+ }
+}
+
+.frame-body {
+ margin: 0 ;
+ min-height: 100vh ;
+
+ &:hover .laser {
+ --color: 0, 155, 0 ;
+ --size: 20px ;
+ }
+}
+
+.laser {
+ --color: 255, 0, 0 ;
+ --size: 8px ;
+ position: fixed ;
+
+ &:before {
+ background: rgb( var( --color ) ) ;
+ border-radius: 100% ;
+ box-shadow: 0 0 8px 0 rgba( var( --color ), 0.8 ) ;
+ content: "" ;
+ height: var( --size ) ;
+ position: absolute ;
+ translate: -50% -50% ;
+ width: var( --size ) ;
+ }
+}
diff --git a/demos/dialog-element-sidebar/index.htm b/demos/dialog-element-sidebar/index.htm
new file mode 100644
index 0000000000..19835437c3
--- /dev/null
+++ b/demos/dialog-element-sidebar/index.htm
@@ -0,0 +1,126 @@
+
+
+
+
+
+ Opening The Dialog Element As A Fly-out Sidebar
+
+
+
+
+
+
+ Opening The Dialog Element As A Fly-out Sidebar
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet nuntius, inde cum cor, supero ferus difficilis
+ poena. Ipse intro longe incido ex pauci culpa. Tu moneo quidam, ego magnus
+ consul. Ego timeo quis virtus que quis numen. Idem patior forte augeo ultra
+ caecus poena.
+
+
+
+ Close
+
+
+
+
+
+
+
+
+ Back to Top
+
+
+
+
+
+
+
diff --git a/demos/dialog-element-sidebar/main.css b/demos/dialog-element-sidebar/main.css
new file mode 100644
index 0000000000..e9a48ed8a3
--- /dev/null
+++ b/demos/dialog-element-sidebar/main.css
@@ -0,0 +1,45 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-within {
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+dialog h2 {
+ margin-block-start: 0 ;
+ scroll-margin-block-start: 2rem ; /* To counteract dialog padding. */
+}
diff --git a/demos/dialog-element/index.htm b/demos/dialog-element/index.htm
new file mode 100644
index 0000000000..a0647c5eb1
--- /dev/null
+++ b/demos/dialog-element/index.htm
@@ -0,0 +1,144 @@
+
+
+
+
+
+ Exploring The Dialog Element In HTML
+
+
+
+
+
+
+ Exploring The Dialog Element In HTML
+
+
+
+
+ Toggle Modal
+
+
+ Toggle Non-Modal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/dialog-element/main.css b/demos/dialog-element/main.css
new file mode 100644
index 0000000000..2fa849f347
--- /dev/null
+++ b/demos/dialog-element/main.css
@@ -0,0 +1,50 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-within {
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+
+form p:first-child {
+ margin-top: 0 ;
+}
+form p:last-child {
+ display: flex ;
+ gap: 12px ;
+ margin-bottom: 0 ;
+}
diff --git a/demos/event-is-trusted/index.htm b/demos/event-is-trusted/index.htm
new file mode 100644
index 0000000000..497c689600
--- /dev/null
+++ b/demos/event-is-trusted/index.htm
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Exploring Event.isTrusted In JavaScript
+
+
+
+
+
+
+ Exploring Event.isTrusted In JavaScript
+
+
+
+ Form
+
+
+
+
+
+ Triggers on Form
+
+
+
+
+ Trigger Submit
+
+
+ Trigger Click
+
+
+ Trigger Focus(in)
+
+
+
+
+
+
+
diff --git a/demos/event-is-trusted/main.css b/demos/event-is-trusted/main.css
new file mode 100644
index 0000000000..208dea2172
--- /dev/null
+++ b/demos/event-is-trusted/main.css
@@ -0,0 +1,82 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
+
+table {
+ border: 1px solid #333333 ;
+ border-collapse: collapse ;
+ width: 100% ;
+
+ & :where(th, td) {
+ border: 1px solid #333333 ;
+ padding: 5px 10px ;
+
+ &:not([align]) {
+ text-align: left ;
+ }
+ }
+}
+
+tbody tr:hover {
+ background-color: #e7f9ff ;
+}
+
+a.active {
+ background-color: #333333 ;
+ color: #fafafa ;
+}
diff --git a/demos/focus-box/index.htm b/demos/focus-box/index.htm
new file mode 100644
index 0000000000..e61ee14cb5
--- /dev/null
+++ b/demos/focus-box/index.htm
@@ -0,0 +1,165 @@
+
+
+
+
+
+ Animating DOM Rectangles Over Focused Elements In JavaScript
+
+
+
+
+
+
+ Animating DOM Rectangles Over Focused Elements In JavaScript
+
+
+
+ Before demo text block (to take focus from demo block and to contrast
+ the normal focus-outline behavior).
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet interrogo communis flumen.
+ Mille iuvenis , umquam ante cohors, adhibeo citus fortis provincia.
+ Hic posco ego, quis frequens tenebrae. Aliquis turbo is epistula. Hic
+ experior quidam voluntas nam aliquis vinculum. Noster puto paulo sum post
+ saevus natura. Diversus ventus, quasi cum for meus, scribo dexter prior
+ astrum. Idem condo qua ictus quoque nemo iter. Mortalis frater, denique in
+ puer, tollo nullus quattuor vestigium. Iste dico ipse voluntas an sui
+ vitium meus desino quisquis, qua gratus ira tu opto nemo praemium gens rideo
+ diversus error .
+
+
+
+
+
+
+
+
diff --git a/demos/focus-box/main.css b/demos/focus-box/main.css
new file mode 100644
index 0000000000..2d6ee4756b
--- /dev/null
+++ b/demos/focus-box/main.css
@@ -0,0 +1,67 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+.textBlock * {
+ &:focus,
+ &:focus-visible {
+ animation-name: none ;
+ outline: none ;
+ }
+}
+
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
diff --git a/demos/htmx-prev-next/index.htm b/demos/htmx-prev-next/index.htm
new file mode 100644
index 0000000000..16b6f4693b
--- /dev/null
+++ b/demos/htmx-prev-next/index.htm
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+ Exploring Prev/Next Mechanics In HTMX
+
+
+
+
+
+
+ Prev
+
+
+
+ Select Next
+
+
+
+
+ Select Prev
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
diff --git a/demos/htmx-prev-next/main.css b/demos/htmx-prev-next/main.css
new file mode 100644
index 0000000000..832631675f
--- /dev/null
+++ b/demos/htmx-prev-next/main.css
@@ -0,0 +1,20 @@
+
+body {
+ font-family: verdana, arial, sans-serif ;
+ font-size: 18px ;
+}
+
+button {
+ border: 1px solid #999999 ;
+ color: inherit ;
+ cursor: pointer ;
+ font-family: inherit ;
+ font-size: 20px ;
+ padding: 5px 10px ;
+}
+
+.selected {
+ font-weight: bold ;
+ background: gold ;
+}
+
diff --git a/demos/movie-rank/app.css b/demos/movie-rank/app.css
new file mode 100644
index 0000000000..39e7df67d5
--- /dev/null
+++ b/demos/movie-rank/app.css
@@ -0,0 +1,69 @@
+
+html {
+ box-sizing: border-box ;
+ font-family: monospace ;
+ font-size: 18px ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+body {
+ text-align: center ;
+}
+
+.panels {
+ align-items: center ;
+ display: flex ;
+ gap: 30px ;
+ justify-content: center ;
+ margin: 40px 0 ;
+
+ & ul {
+ list-style-type: none ;
+ margin: 0 ;
+ padding: 0 ;
+ }
+
+ & li {
+ background-color: #fafafa ;
+ border: 1px solid #cccccc ;
+ padding: 10px 30px ;
+ margin: 3px ;
+ user-select: none ;
+ }
+
+ & mark {
+ border-radius: 20px ;
+ border: 1px solid #cccccc ;
+ font-size: 30px ;
+ line-height: 1 ;
+ padding: 30px 0 ;
+ width: 150px ;
+
+ &:after {
+ content: "%" ;
+ }
+ }
+
+ & .sortable-ghost {
+ background-color: hotpink ;
+ color: white ;
+ }
+}
+
+.resets {
+ display: flex ;
+ gap: 145px ;
+ justify-content: center ;
+
+ & button {
+ font-family: inherit ;
+ font-size: inherit ;
+ line-height: inherit ;
+ padding: 5px 12px ;
+ }
+}
diff --git a/demos/movie-rank/app.js b/demos/movie-rank/app.js
new file mode 100644
index 0000000000..ef791c5498
--- /dev/null
+++ b/demos/movie-rank/app.js
@@ -0,0 +1,324 @@
+function App() {
+
+ // Note: the "id" value matches the "index" value. This fact will be leveraged when
+ // reading the DOM for the sorted list of IDs.
+ var id = -1;
+ var titles = [
+ { id: ++id, name: "10 Things I Hate About You" },
+ { id: ++id, name: "50 First Dates" },
+ { id: ++id, name: "Annie Hall" },
+ { id: ++id, name: "Bridge Jones's Diary" },
+ { id: ++id, name: "Crazy Rich Asians" },
+ { id: ++id, name: "Dave" },
+ { id: ++id, name: "Defending Your Life" },
+ { id: ++id, name: "Dirty Dancing" },
+ { id: ++id, name: "Love Actually" },
+ { id: ++id, name: "Midnight In Paris" },
+ { id: ++id, name: "Moonstruck" },
+ { id: ++id, name: "Notting Hill" },
+ { id: ++id, name: "Pretty Woman" },
+ { id: ++id, name: "Princess Bride" },
+ { id: ++id, name: "Say Anything" },
+ { id: ++id, name: "Sleepless In Seattle" },
+ { id: ++id, name: "Tommy Boy" },
+ { id: ++id, name: "Wedding Singer" },
+ { id: ++id, name: "What About Bob" },
+ { id: ++id, name: "When Harry Met Sally" },
+ { id: ++id, name: "You've Got Mail" },
+ ];
+
+ // ------------------------------------------------------------------------------- //
+ // ------------------------------------------------------------------------------- //
+
+ return {
+ // Public properties.
+ listOne: null,
+ listTwo: null,
+ similarity: "100",
+ titles,
+ // Private properties.
+ distance: 0,
+ sortableOne: null,
+ sortableTwo: null,
+ coreIds: null,
+ // Life-cycle methods.
+ init,
+ // Public methods.
+ handlePopstate,
+ randomKeyBecauseXForBug,
+ resetList,
+ syncRight,
+ // Private methods.
+ compareRankings,
+ getSortedListsFromDom,
+ loadFromhash,
+ persistToHash,
+ };
+
+ // ---
+ // LIFE-CYCLE METHODS.
+ // ---
+
+ /**
+ * I initialize the component.
+ */
+ function init() {
+
+ // Keep track of the core list of title IDs so that we can use this in the default
+ // list assignment as well as in the hash parsing.
+ this.coreIds = this.titles.map( title => title.id );
+ // By default, set each list of title IDs to start with the core list. This will
+ // then be overridden, as needed, by the URL fragment.
+ this.listOne = this.coreIds.slice();
+ this.listTwo = this.coreIds.slice();
+
+ // Override the lists (if possible) using the URL fragment.
+ this.loadFromhash();
+ this.compareRankings();
+
+ var sortableOptions = {
+ direction: "vertical",
+ swapThreshold: 0.8, // Overlap required to trigger move (0...1).
+ animation: 0,
+ onUpdate: ( event ) => {
+ this.getSortedListsFromDom( event );
+ this.compareRankings();
+ this.persistToHash();
+ }
+ };
+
+ // Enable sorting on the DOM lists.
+ this.sortableOne = new Sortable( this.$refs.listOneNode, sortableOptions );
+ this.sortableTwo = new Sortable( this.$refs.listTwoNode, sortableOptions );
+
+ }
+
+ // ---
+ // PUBLIC METHODS.
+ // ---
+
+ /**
+ * I handle the history popState event, and sync the URL state down into the app state.
+ */
+ function handlePopstate() {
+
+ this.loadFromhash();
+ this.compareRankings();
+
+ }
+
+
+ /**
+ * There's a BUG(ish) in the way that X-For handles DOM-initiated re-sorting. As such,
+ * we are working around it by providing a random key to the DOM id. This will create
+ * DOM churn; but, it is what it is.
+ *
+ * Read more: https://github.com/alpinejs/alpine/discussions/4157
+ */
+ function randomKeyBecauseXForBug() {
+
+ return Math.floor( Math.random() * 999999 );
+
+ }
+
+
+ /**
+ * I reset the selected list to the original titles order.
+ */
+ function resetList( whichList ) {
+
+ this[ whichList ] = this.coreIds.slice();
+ this.compareRankings();
+ this.persistToHash();
+
+ }
+
+
+ /**
+ * I sync the first list into the second list to give the user a matching base from
+ * which to start customizing the sort.
+ */
+ function syncRight() {
+
+ this.listTwo = this.listOne.slice();
+ this.compareRankings();
+ this.persistToHash();
+
+ }
+
+ // ---
+ // PRIVATE METHODS.
+ // ---
+
+ /**
+ * I compute the distance and similarity of the two lists.
+ */
+ function compareRankings() {
+
+ // Calculate the Kendall Tau Distance between the two list of titles.
+ this.distance = kendallTauDistance( this.listOne, this.listTwo );
+ // Convert the Kendall Tau Distance to something more human friendly.
+ this.similarity = ( ( 1 - this.distance ) * 100 ).toFixed( 1 );
+
+ }
+
+
+ /**
+ * I read the list of IDs back out of the DOM (via the Sortable proxy).
+ */
+ function getSortedListsFromDom( event ) {
+
+ // Our list DOM is rendered by Alpine.js (via the x-for directive); but, we're
+ // allowing the list to be updated arbitrarily by Sortable.js. Once the sorting
+ // operation is done, we need to read the state of the DOM back into the state of
+ // Alpine.js so that the x-for attribute remains in a predictable state.
+ // --
+ // Note: there's a bug(ish) in the [x-for] attribute in the way it keeps track of
+ // keys internally. We get around this by assigning random keys in the DOM.
+ this.listOne = this.sortableOne.toArray();
+ this.listTwo = this.sortableTwo.toArray();
+
+ }
+
+
+ /**
+ * I override the rankings if the values are available in the URL fragment.
+ */
+ function loadFromhash() {
+
+ var rankings = location.hash
+ .slice( 1 )
+ // Split fragment into two comma-delimited lists.
+ .split( ":" )
+ // Map each comma-delimited list onto a set of IDs.
+ .map(
+ ( rankList ) => {
+
+ return rankList
+ // Split each comma-delimited list into a set of IDs.
+ .split( "," )
+ // Make sure we didn't have any incorrect mappings.
+ .filter( id => titles[ id ] )
+ ;
+
+ }
+ )
+ ;
+
+ // We've already set up default values for the lists to use the core set of IDs.
+ // We only want to now override these lists if they have the same length as the
+ // defaults. This way, we don't have to do any more validation on the URL.
+
+ if ( rankings[ 0 ]?.length === this.listOne.length ) {
+
+ this.listOne = rankings[ 0 ];
+
+ }
+
+ if ( rankings[ 1 ]?.length === this.listTwo.length ) {
+
+ this.listTwo = rankings[ 1 ];
+
+ }
+
+ }
+
+
+ /**
+ * I persist the current ranks to the URL fragment.
+ */
+ function persistToHash() {
+
+ var flattenedOne = this.listOne.join( "," );
+ var flattenedTwo = this.listTwo.join( "," );
+ var flattenedCore = this.coreIds.join( "," );
+
+ // Vanity: if either of the lists matches the original list of titles, just omit
+ // it from the URL. This has no functional bearing - it just makes the URL look a
+ // little bit nicer.
+ if ( flattenedOne === flattenedCore ) flattenedOne = "";
+ if ( flattenedTwo === flattenedCore ) flattenedTwo = "";
+
+ document.title = `Lists are a ${ this.similarity }% match!`;
+ history.pushState( {}, null, `#${ flattenedOne }:${ flattenedTwo }` );
+
+ }
+
+ // ---
+ // HELPER METHODS (ie, pure functions, not on THIS scope).
+ // ---
+
+ /**
+ * I return an index of the given array in which the value maps to the index of the
+ * value within the collection.
+ */
+ function arrayReflectIndex( collection ) {
+
+ var index = Object.create( null );
+
+ collection.forEach(
+ ( element, i ) => {
+
+ index[ element ] = i;
+
+ }
+ );
+
+ return index;
+
+ }
+
+
+ /**
+ * I calculate the Kendall Tau Distance for the two lists. Returns a decimal value
+ * between 0 (fully identical) and 1 (fully reversed).
+ */
+ function kendallTauDistance( listOne, listTwo ) {
+
+ // We're going to be counting the number of (A,B) pairs that are in a different
+ // relative order in the two lists.
+ var size = listOne.length;
+ var totalPairs = ( size * ( size - 1 ) / 2 );
+ var discordantPairs = 0;
+
+ // As we iterate over the FIRST list, we'll need to check the corresponding rank
+ // of the same items in the SECOND list. To make this efficient, let's calculate
+ // the index-by-value for all elements in the second list. We don't need to do
+ // this for the first list since we'll be iterating over the first list in order.
+ var listTwoIndex = arrayReflectIndex( listTwo );
+
+ // Iterate over the FIRST list using a nested, forward looking loop. The outer
+ // loop will iterate over the entirety of the first list.
+ for ( var a = 0 ; a < ( size - 1 ) ; a++ ) {
+
+ // ... the inner loop only needs to iterate from [a...] since we're looking
+ // for unique pairs of elements. If the inner loop started from 0, we'd be
+ // counting the same pairs more than once (since (A,B) and (B,A) are
+ // considered the same pair for this algorithm).
+ for ( var b = ( a + 1 ) ; b < size ; b++ ) {
+
+ // Get the elements at the current iteration indices.
+ var elementA = listOne[ a ];
+ var elementB = listOne[ b ];
+ // Since our nested loop is always exploring elements in a forward-looking
+ // order, we know that the ranking of the elements in the first list is
+ // always -1. We only need to calculate the corresponding rank in the
+ // second list.
+ var rankOne = -1;
+ var rankTwo = Math.sign( listTwoIndex[ elementA ] - listTwoIndex[ elementB ] );
+
+ if ( rankTwo != rankOne ) {
+
+ discordantPairs++;
+
+ }
+
+ }
+
+ }
+
+ return ( discordantPairs / totalPairs );
+
+ }
+
+}
diff --git a/demos/movie-rank/index.htm b/demos/movie-rank/index.htm
new file mode 100644
index 0000000000..768ace4884
--- /dev/null
+++ b/demos/movie-rank/index.htm
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ Movie Ranking With Sortable.js And Kendall Tau Distance
+
+
+
+
+
+
+
+
+
+ Movie Ranking With Sortable.js And Kendall Tau Distance
+
+
+
+ Check your movie preference compatibility with others:
+
+
+
+
+
+
+ Reset List One
+
+
+ Sync →
+
+
+ Reset List Two
+
+
+
+
+
diff --git a/demos/pixel-art-alpine/index.htm b/demos/pixel-art-alpine/index.htm
new file mode 100644
index 0000000000..39f78a2e49
--- /dev/null
+++ b/demos/pixel-art-alpine/index.htm
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+ Pixel Art With Alpine.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Name:
+
+
+
+
Hex:
+
+
+
+
+
+
+ Clear Canvas
+
+
+
+ Set Background
+
+
+
+
+
+
+ Center
+
+
+ Up
+
+
+ Down
+
+
+ Left
+
+
+ Right
+
+
+
+
+
+ Use CMD +Click to sample a pixel.
+
+
+ Use ALT +Click to erase a pixel.
+
+
+ Use CMD+Z to undo pixel and Shift+CMD+Z to redo pixel.
+
+
+
+
+
+
+
+
+
diff --git a/demos/pixel-art-alpine/main.css b/demos/pixel-art-alpine/main.css
new file mode 100644
index 0000000000..2f11b67bd3
--- /dev/null
+++ b/demos/pixel-art-alpine/main.css
@@ -0,0 +1,171 @@
+*,
+*:before,
+*:after {
+ box-sizing: border-box ;
+ margin: 0 ;
+ padding: 0 ;
+}
+
+:root {
+ --grid-size: 25 ;
+ --pixel-size: 26px ;
+}
+
+body {
+ font-family: monospace ;
+ font-size: 16px ;
+ line-height: 1.4 ;
+}
+
+main {
+ align-items: center ;
+ display: flex ;
+ flex-direction: column ;
+ gap: 30px ;
+ padding: 30px ;
+
+ & h1 {
+ margin: 0 0 0px 0 ;
+
+ & a {
+ color: inherit ;
+ text-decoration: none ;
+
+ &:hover {
+ color: #ff1493 ;
+ text-decoration: underline ;
+ }
+ }
+ }
+}
+
+.grid {
+ background-color: #ffffff ;
+ box-shadow: 0 0 1px 0 #999999 ;
+ display: grid ;
+ grid-template-columns: repeat( var( --grid-size ), 1fr ) ;
+ height: calc( var( --grid-size ) * var( --pixel-size ) ) ;
+ user-select: none ;
+ width: calc( var( --grid-size ) * var( --pixel-size ) ) ;
+
+ & button {
+ border: none ;
+ box-shadow: inset 0 0 1px 0 #ffffff ;
+ height: var( --pixel-size ) ;
+ width: var( --pixel-size ) ;
+ }
+}
+
+.palette {
+ box-shadow: 0 0 1px 0 #999999 ;
+ display: flex ;
+ flex-wrap: wrap ;
+ min-height: 156px ;
+ user-select: none ;
+ width: calc( var( --grid-size ) * var( --pixel-size ) ) ;
+
+ & button {
+ border: none ;
+ box-shadow: inset 0 0 1px 0 #ffffff ;
+ flex: 0 0 auto ;
+ height: var( --pixel-size ) ;
+ width: var( --pixel-size ) ;
+
+ &:last-child {
+ flex-grow: 1 ;
+ }
+
+ &.isSelected {
+ box-shadow: 0 0 3px 5px #000000 ;
+ outline: 2px solid #ffffff ;
+ z-index: 2 ;
+ }
+ }
+}
+
+.selected {
+ display: flex ;
+ font-size: 20px ;
+ margin: 0 ;
+ gap: 30px ;
+
+ & div {
+ display: flex ;
+ gap: 10px ;
+ }
+
+ & dt {
+ font-weight: 700 ;
+ margin: 0 ;
+ }
+
+ & dd {
+ margin: 0 ;
+ }
+}
+
+.fillers {
+ display: flex ;
+ font-size: 20px ;
+ gap: 13px ;
+
+ & button {
+ align-items: center ;
+ background-color: #333333 ;
+ border: none ;
+ border-radius: 4px ;
+ color: #ffffff ;
+ display: flex ;
+ font: inherit ;
+ gap: 13px ;
+ padding: 10px 20px ;
+ }
+
+ & span {
+ box-shadow: 0 0 1px 1px #ffffff ;
+ border-radius: 16px ;
+ height: 16px ;
+ width: 16px ;
+ }
+}
+
+.tuggers {
+ display: flex ;
+ font-size: 20px ;
+ gap: 13px ;
+
+ & button {
+ background-color: #ffffff ;
+ border: 1px solid #333333 ;
+ border-radius: 4px ;
+ color: #333333 ;
+ font: inherit ;
+ padding: 10px 20px ;
+ min-width: 100px ;
+ }
+}
+
+.tips {
+ align-items: center ;
+ display: flex ;
+ flex-direction: column ;
+ gap: 15px ;
+
+ & kbd {
+ background-color: #232323 ;
+ border-radius: 2px ;
+ color: #ffffff ;
+ display: inline-block ;
+ padding: 0 3px ;
+ }
+}
+
+.examples {
+ display: flex ;
+ gap: 20px ;
+
+ & a {
+ color: #ff1493 ;
+ }
+}
+
diff --git a/demos/pixel-art-alpine/main.js b/demos/pixel-art-alpine/main.js
new file mode 100644
index 0000000000..d673836486
--- /dev/null
+++ b/demos/pixel-art-alpine/main.js
@@ -0,0 +1,610 @@
+// Note: this depends on "palette" existing as an external module.
+function Demo() {
+
+ return {
+ // Public properties.
+ pixels: null,
+ foregroundSwatch: palette.byName.LightSkyBlue,
+ backgroundSwatch: palette.byName.Snow,
+
+ // Private properties.
+ canvasWidth: 0,
+ canvasHeight: 0,
+ isDrawing: false,
+ palette: palette,
+
+ // Life-Cycle methods.
+ init,
+
+ // Public methods.
+ changeCanvasBackground,
+ clearCanvas,
+ enterPixel,
+ handleDo,
+ handleHashchange,
+ pullCanvasCenter,
+ pullCanvasDown,
+ pullCanvasLeft,
+ pullCanvasRight,
+ pullCanvasUp,
+ selectSwatch,
+ startDrawing,
+ stopDrawing,
+
+ // Private methods.
+ hashDecodeState,
+ hashEncodeState,
+ matrixNudge,
+ matrixRead,
+ matrixWrite,
+ stateDecodeString,
+ stateEncodeString,
+ };
+
+ // ---
+ // LIFE-CYCLE METHODS.
+ // ---
+
+ /**
+ * I initialize the alpine component.
+ */
+ function init() {
+
+ // Pull grid dimensions from the DOM.
+ this.canvasWidth = ( +this.$el.dataset.width || 25 );
+ this.canvasHeight = ( +this.$el.dataset.height || 25 );
+
+ // Setup the pixel matrix: a linear set of pixels being rendered in two dimensions
+ // within the user interface using CSS Grid.
+ this.pixels = new Array( this.canvasWidth * this.canvasHeight )
+ .fill( this.backgroundSwatch )
+ ;
+
+ // If the current request is a link to an existing pixel configuration, pull it in
+ // from the URL fragment.
+ this.hashDecodeState();
+
+ }
+
+ // ---
+ // PUBLIC METHODS.
+ // ---
+
+ /**
+ * I change any pixel with the current background color to be the new foreground color,
+ * then use the new foreground color as the future background color. Basically, this
+ * rotates the background color pixels only, leaving foreground color pixels in place.
+ */
+ function changeCanvasBackground() {
+
+ this.pixels = this.pixels.map(
+ ( swatch ) => {
+
+ return ( swatch === this.backgroundSwatch )
+ ? this.foregroundSwatch
+ : swatch
+ ;
+
+ }
+ );
+
+ this.backgroundSwatch = this.foregroundSwatch;
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I completely reset the pixel matrix to use one solid color (the selected color).
+ */
+ function clearCanvas() {
+
+ this.backgroundSwatch = this.foregroundSwatch;
+ this.pixels.fill( this.backgroundSwatch );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I apply a swatch to the contextual pixel if this is a draw operation.
+ */
+ function enterPixel( event, i ) {
+
+ if ( ! this.isDrawing ) {
+
+ return;
+
+ }
+
+ this.pixels[ i ] = event.altKey
+ ? this.backgroundSwatch
+ : this.foregroundSwatch
+ ;
+
+ }
+
+
+ /**
+ * I attempt to redo / undo a recent change using this history.
+ *
+ * Note: these are being handled with a single event handler since Alpine.js doesn't
+ * limit key-bindings based on modifiers. As such, it's easier to just handle both
+ * events in a single handler.
+ */
+ function handleDo( event ) {
+
+ // Since all pixel changes are persisted in the hash, we should be able to
+ // navigate back / forward through all of the changes in the canvas. However, I'm
+ // not keep track of whether or not the commands are available - I'm just blindly
+ // invoke the history API and letting the hash play-out.
+ event.preventDefault();
+
+ // Redo.
+ if ( event.shiftKey ) {
+
+ history.go( 1 );
+
+ // Undo.
+ } else {
+
+ history.go( -1 );
+
+ }
+
+ }
+
+
+ /**
+ * I handle the hash change, and push the URL data into the pixel state.
+ */
+ function handleHashchange( event ) {
+
+ this.hashDecodeState();
+
+ }
+
+
+ /**
+ * I shift the foreground pixels to the center of the canvas.
+ */
+ function pullCanvasCenter() {
+
+ var MAX = 999999;
+ var colMin = MAX;
+ var rowMin = MAX;
+ var colMax = -1;
+ var rowMax = -1;
+ var matrix = this.matrixRead();
+
+ // Iterate over the pixels and try to identify the smallest bounding box around
+ // the non-background swatch.
+ matrix.forEach(
+ ( row, rowIndex ) => {
+
+ row.forEach(
+ ( pixel, colIndex ) => {
+
+ if ( pixel != this.backgroundSwatch ) {
+
+ colMin = Math.min( colMin, colIndex );
+ colMax = Math.max( colMax, colIndex );
+ rowMin = Math.min( rowMin, rowIndex );
+ rowMax = Math.max( rowMax, rowIndex );
+
+ }
+
+ }
+ );
+
+ }
+ );
+
+ // If we found no foreground pixel data, there's nothing else to do.
+ if ( rowMin === MAX ) {
+
+ return;
+
+ }
+
+ var boxWidth = ( colMax - colMin + 1 );
+ var boxHeight = ( rowMax - rowMin + 1 );
+ var deltaWidth = ( this.canvasWidth - boxWidth );
+ var deltaHeight = ( this.canvasHeight - boxHeight );
+ var targetX = Math.floor( deltaWidth / 2 );
+ var targetY = Math.floor( deltaHeight / 2 );
+
+ this.matrixWrite(
+ this.matrixNudge(
+ matrix,
+ ( targetX - colMin ), // Delta columns.
+ ( targetY - rowMin ) // Delta rows.
+ )
+ );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I shift the foreground pixels down 1 row on the canvas.
+ */
+ function pullCanvasDown() {
+
+ this.matrixWrite(
+ this.matrixNudge(
+ this.matrixRead(),
+ 0, // Delta columns.
+ 1 // Delta rows.
+ )
+ );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I shift the foreground pixels left 1 column on the canvas.
+ */
+ function pullCanvasLeft() {
+
+ this.matrixWrite(
+ this.matrixNudge(
+ this.matrixRead(),
+ -1, // Delta columns.
+ 0 // Delta rows.
+ )
+ );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I shift the foreground pixels up 1 row on the canvas.
+ */
+ function pullCanvasUp() {
+
+ this.matrixWrite(
+ this.matrixNudge(
+ this.matrixRead(),
+ 0, // Delta columns.
+ -1 // Delta rows.
+ )
+ );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I shift the foreground pixels right 1 column on the canvas.
+ */
+ function pullCanvasRight() {
+
+ this.matrixWrite(
+ this.matrixNudge(
+ this.matrixRead(),
+ 1, // Delta columns.
+ 0 // Delta rows.
+ )
+ );
+ this.hashEncodeState();
+
+ }
+
+
+ /**
+ * I set the given swatch as the foreground drawing color.
+ */
+ function selectSwatch( swatch ) {
+
+ this.foregroundSwatch = swatch;
+
+ }
+
+
+ /**
+ * I start a drawing operation, filling in the contextual pixel.
+ */
+ function startDrawing( event, i ) {
+
+ // If the mouse event is modified, first sample the pixel for its swatch.
+ if ( event.metaKey || event.ctrlKey ) {
+
+ this.foregroundSwatch = this.pixels[ i ];
+
+ }
+
+ this.isDrawing = true;
+ this.enterPixel( event, i );
+
+ }
+
+
+ /**
+ * I stop a drawing operation and persist the current pixel state to the URL.
+ */
+ function stopDrawing() {
+
+ if ( ! this.isDrawing ) {
+
+ return;
+
+ }
+
+ this.isDrawing = false;
+ this.hashEncodeState();
+
+ }
+
+ // ---
+ // PRIVATE METHODS.
+ // ---
+
+ /**
+ * I decode the canvas state from the URL fragment and use it to set the current pixel
+ * and color state.
+ */
+ function hashDecodeState() {
+
+ var state = this.stateDecodeString( location.hash.slice( 1 ) );
+
+ if ( ! state ) {
+
+ return;
+
+ }
+
+ this.foregroundSwatch = state.foregroundSwatch;
+ this.backgroundSwatch = state.backgroundSwatch;
+ this.pixels = state.pixels;
+
+ }
+
+
+ /**
+ * I encode the current canvas state into the URL fragment.
+ */
+ function hashEncodeState() {
+
+ history.pushState( null, null, `#${ this.stateEncodeString() }` );
+
+ }
+
+
+ /**
+ * I nudge the given pixel 2D matrix by the given column and row deltas. New pixels use
+ * the currently selected background swatch.
+ */
+ function matrixNudge( matrix, colDelta, rowDelta ) {
+
+ // Nudge left.
+ for ( ; colDelta < 0 ; colDelta++ ) {
+
+ for ( var row of matrix ) {
+
+ row.shift();
+ row.push( this.backgroundSwatch );
+
+ }
+
+ }
+
+ // Nudge right.
+ for ( ; colDelta > 0 ; colDelta-- ) {
+
+ for ( var row of matrix ) {
+
+ row.pop();
+ row.unshift( this.backgroundSwatch );
+
+ }
+
+ }
+
+ // Nudge up.
+ for ( ; rowDelta < 0 ; rowDelta++ ) {
+
+ matrix.shift();
+ matrix.push( new Array( this.canvasWidth ).fill( this.backgroundSwatch ) );
+
+ }
+
+ // Nudge down.
+ for ( ; rowDelta > 0 ; rowDelta-- ) {
+
+ matrix.pop();
+ matrix.unshift( new Array( this.canvasWidth ).fill( this.backgroundSwatch ) );
+
+ }
+
+ return matrix;
+
+ }
+
+
+ /**
+ * I read the current linear pixel state into a 2D matrix.
+ */
+ function matrixRead() {
+
+ var matrix = [];
+
+ for ( var i = 0 ; i < this.canvasHeight ; i++ ) {
+
+ var rowOffset = ( i * this.canvasWidth );
+ var rowEnd = ( rowOffset + this.canvasWidth );
+
+ matrix.push( this.pixels.slice( rowOffset, rowEnd ) );
+
+ }
+
+ return matrix;
+
+ }
+
+
+ /**
+ * I write the 2D matrix back into the current linear pixel state.
+ */
+ function matrixWrite( matrix ) {
+
+ this.pixels = matrix.flat();
+
+ }
+
+
+ /**
+ * I parse the given string value back into a state object that contains the foreground
+ * swatch, the background swatch, and the pixels.
+ */
+ function stateDecodeString( value = "" ) {
+
+ // Every part of the state is represented by either a single Base36 value; or, a
+ // pair of Base36 values in a ":" delimited list.
+ var matches = value
+ .toLowerCase()
+ .matchAll( /([a-z0-9]+)(:([a-z0-9]+))?/g )
+ .toArray()
+ .map(
+ ([ $0, $key, $2, $count ]) => {
+
+ return {
+ key: urlDecodeInt( $key ),
+ count: urlDecodeInt( $count )
+ };
+
+ }
+ )
+ ;
+
+ // We know that the encoded state will be, at the very smallest, the foreground
+ // swatch, the background swatch, and then a single run of a solid color.
+ // Therefore, if we have less than 3 matches, the input is invalid.
+ if ( matches.length < 3 ) {
+
+ return null;
+
+ }
+
+ // Set up the core state object into which we will parse the input.
+ var state = {
+ // First two matches are always the selected swatches.
+ foregroundSwatch: this.palette.byKey[ matches.shift().key ],
+ backgroundSwatch: this.palette.byKey[ matches.shift().key ],
+ pixels: new Array( this.pixels.length )
+ };
+
+ // Blank-out the canvas - we'll fill in pixels next.
+ state.pixels.fill( state.backgroundSwatch );
+
+ // As we iterate over the matches, we need to translate the runs into pixel
+ // offsets. Will use "i" to keep track of the start offset of the next fill.
+ var i = 0;
+
+ for ( var match of matches ) {
+
+ state.pixels.fill(
+ this.palette.byKey[ match.key ],
+ i,
+ ( i += match.count ) // Warning: incrementing AND consuming.
+ );
+
+ }
+
+ return state;
+
+ }
+
+
+ /**
+ * I encode the current pixel art state into a string representation.
+ */
+ function stateEncodeString() {
+
+ // The state will be encoded as a series of "runs". Meaning, each sequence of
+ // pixels that used the same swatch will be condensed down into the swatch "key"
+ // followed by the number of repeated pixels (`key`:`count`). If a swatch run is
+ // only a single pixel, the count can be omitted and will be assumed to be one.
+ // The first two runs implicitly represent the foreground and background swatches.
+ var runs = [
+ {
+ key: this.foregroundSwatch.key,
+ count: 1
+ },
+ {
+ key: this.backgroundSwatch.key,
+ count: 1
+ }
+ ];
+ var run = {};
+
+ for ( var pixel of this.pixels ) {
+
+ // Did we enter a new swatch run?
+ if ( run.key !== pixel.key ) {
+
+ run = {
+ key: pixel.key,
+ count: 0
+ };
+ runs.push( run );
+
+ }
+
+ run.count++;
+
+ }
+
+ // Map runs to a list of `key`:`count` pairs.
+ return runs
+ .map(
+ ( run ) => {
+
+ // Single pixel runs will be assumed to be "1" during parsing. As
+ // such, we can omit the count - keep the URL shorter.
+ if ( run.count === 1 ) {
+
+ return urlEncodeInt( run.key );
+
+ }
+
+ return `${ urlEncodeInt( run.key ) }:${ urlEncodeInt( run.count ) }`;
+
+ }
+ )
+ .join( "," )
+ ;
+
+ }
+
+
+ /**
+ * In order to create shorter URLs, we're encoding numbers using Base36. This decodes
+ * the value back into an int.
+ */
+ function urlDecodeInt( value = undefined ) {
+
+ if ( value === undefined ) {
+
+ return 1;
+
+ }
+
+ return parseInt( value, 36 );
+
+ }
+
+
+ /**
+ * In order to create shorter URLs, we're encoding numbers using Base36. This encodes
+ * the int value.
+ */
+ function urlEncodeInt( value ) {
+
+ return value.toString( 36 );
+
+ }
+
+}
diff --git a/demos/pixel-art-alpine/palette.js b/demos/pixel-art-alpine/palette.js
new file mode 100644
index 0000000000..89db381304
--- /dev/null
+++ b/demos/pixel-art-alpine/palette.js
@@ -0,0 +1,180 @@
+var palette = (() => {
+
+ // Colors courtesy of : https://htmlcolorcodes.com/color-names/
+ // --
+ // IMPORTANT: the first swatch key will be "1" so that "0" can represent transparent.
+ var key = 0;
+ var swatches = [
+ { name: "IndianRed", hex: "#cd5c5c", key: ++key },
+ { name: "LightCoral", hex: "#f08080", key: ++key },
+ { name: "Salmon", hex: "#fa8072", key: ++key },
+ { name: "DarkSalmon", hex: "#e9967a", key: ++key },
+ { name: "LightSalmon", hex: "#ffa07a", key: ++key },
+ { name: "Crimson", hex: "#dc143c", key: ++key },
+ { name: "Red", hex: "#ff0000", key: ++key },
+ { name: "FireBrick", hex: "#b22222", key: ++key },
+ { name: "DarkRed", hex: "#8b0000", key: ++key },
+ { name: "Pink", hex: "#ffc0cb", key: ++key },
+ { name: "LightPink", hex: "#ffb6c1", key: ++key },
+ { name: "HotPink", hex: "#ff69b4", key: ++key },
+ { name: "DeepPink", hex: "#ff1493", key: ++key },
+ { name: "MediumVioletRed", hex: "#c71585", key: ++key },
+ { name: "PaleVioletRed", hex: "#db7093", key: ++key },
+ { name: "LightSalmon", hex: "#ffa07a", key: ++key },
+ { name: "Coral", hex: "#ff7f50", key: ++key },
+ { name: "Tomato", hex: "#ff6347", key: ++key },
+ { name: "OrangeRed", hex: "#ff4500", key: ++key },
+ { name: "DarkOrange", hex: "#ff8c00", key: ++key },
+ { name: "Orange", hex: "#ffa500", key: ++key },
+ { name: "Gold", hex: "#ffd700", key: ++key },
+ { name: "Yellow", hex: "#ffff00", key: ++key },
+ { name: "LightYellow", hex: "#ffffe0", key: ++key },
+ { name: "LemonChiffon", hex: "#fffacd", key: ++key },
+ { name: "LightGoldenrodYellow", hex: "#fafad2", key: ++key },
+ { name: "PapayaWhip", hex: "#ffefd5", key: ++key },
+ { name: "Moccasin", hex: "#ffe4b5", key: ++key },
+ { name: "PeachPuff", hex: "#ffdab9", key: ++key },
+ { name: "PaleGoldenrod", hex: "#eee8aa", key: ++key },
+ { name: "Khaki", hex: "#f0e68c", key: ++key },
+ { name: "DarkKhaki", hex: "#bdb76b", key: ++key },
+ { name: "Lavender", hex: "#e6e6fa", key: ++key },
+ { name: "Thistle", hex: "#d8bfd8", key: ++key },
+ { name: "Plum", hex: "#dda0dd", key: ++key },
+ { name: "Violet", hex: "#ee82ee", key: ++key },
+ { name: "Orchid", hex: "#da70d6", key: ++key },
+ { name: "Fuchsia", hex: "#ff00ff", key: ++key },
+ { name: "Magenta", hex: "#ff00ff", key: ++key },
+ { name: "MediumOrchid", hex: "#ba55d3", key: ++key },
+ { name: "MediumPurple", hex: "#9370db", key: ++key },
+ { name: "RebeccaPurple", hex: "#663399", key: ++key },
+ { name: "BlueViolet", hex: "#8a2be2", key: ++key },
+ { name: "DarkViolet", hex: "#9400d3", key: ++key },
+ { name: "DarkOrchid", hex: "#9932cc", key: ++key },
+ { name: "DarkMagenta", hex: "#8b008b", key: ++key },
+ { name: "Purple", hex: "#800080", key: ++key },
+ { name: "Indigo", hex: "#4b0082", key: ++key },
+ { name: "SlateBlue", hex: "#6a5acd", key: ++key },
+ { name: "DarkSlateBlue", hex: "#483d8b", key: ++key },
+ { name: "MediumSlateBlue", hex: "#7b68ee", key: ++key },
+ { name: "GreenYellow", hex: "#adff2f", key: ++key },
+ { name: "Chartreuse", hex: "#7fff00", key: ++key },
+ { name: "LawnGreen", hex: "#7cfc00", key: ++key },
+ { name: "Lime", hex: "#00ff00", key: ++key },
+ { name: "LimeGreen", hex: "#32cd32", key: ++key },
+ { name: "PaleGreen", hex: "#98fb98", key: ++key },
+ { name: "LightGreen", hex: "#90ee90", key: ++key },
+ { name: "MediumSpringGreen", hex: "#00fa9a", key: ++key },
+ { name: "SpringGreen", hex: "#00ff7f", key: ++key },
+ { name: "MediumSeaGreen", hex: "#3cb371", key: ++key },
+ { name: "SeaGreen", hex: "#2e8b57", key: ++key },
+ { name: "ForestGreen", hex: "#228b22", key: ++key },
+ { name: "Green", hex: "#008000", key: ++key },
+ { name: "DarkGreen", hex: "#006400", key: ++key },
+ { name: "YellowGreen", hex: "#9acd32", key: ++key },
+ { name: "OliveDrab", hex: "#6b8e23", key: ++key },
+ { name: "Olive", hex: "#808000", key: ++key },
+ { name: "DarkOliveGreen", hex: "#556b2f", key: ++key },
+ { name: "MediumAquamarine", hex: "#66cdaa", key: ++key },
+ { name: "DarkSeaGreen", hex: "#8fbc8b", key: ++key },
+ { name: "LightSeaGreen", hex: "#20b2aa", key: ++key },
+ { name: "DarkCyan", hex: "#008b8b", key: ++key },
+ { name: "Teal", hex: "#008080", key: ++key },
+ { name: "Aqua", hex: "#00ffff", key: ++key },
+ { name: "Cyan", hex: "#00ffff", key: ++key },
+ { name: "LightCyan", hex: "#e0ffff", key: ++key },
+ { name: "PaleTurquoise", hex: "#afeeee", key: ++key },
+ { name: "Aquamarine", hex: "#7fffd4", key: ++key },
+ { name: "Turquoise", hex: "#40e0d0", key: ++key },
+ { name: "MediumTurquoise", hex: "#48d1cc", key: ++key },
+ { name: "DarkTurquoise", hex: "#00ced1", key: ++key },
+ { name: "CadetBlue", hex: "#5f9ea0", key: ++key },
+ { name: "SteelBlue", hex: "#4682b4", key: ++key },
+ { name: "LightSteelBlue", hex: "#b0c4de", key: ++key },
+ { name: "PowderBlue", hex: "#b0e0e6", key: ++key },
+ { name: "LightBlue", hex: "#add8e6", key: ++key },
+ { name: "SkyBlue", hex: "#87ceeb", key: ++key },
+ { name: "LightSkyBlue", hex: "#87cefa", key: ++key },
+ { name: "DeepSkyBlue", hex: "#00bfff", key: ++key },
+ { name: "DodgerBlue", hex: "#1e90ff", key: ++key },
+ { name: "CornflowerBlue", hex: "#6495ed", key: ++key },
+ { name: "MediumSlateBlue", hex: "#7b68ee", key: ++key },
+ { name: "RoyalBlue", hex: "#4169e1", key: ++key },
+ { name: "Blue", hex: "#0000ff", key: ++key },
+ { name: "MediumBlue", hex: "#0000cd", key: ++key },
+ { name: "DarkBlue", hex: "#00008b", key: ++key },
+ { name: "Navy", hex: "#000080", key: ++key },
+ { name: "MidnightBlue", hex: "#191970", key: ++key },
+ { name: "Cornsilk", hex: "#fff8dc", key: ++key },
+ { name: "BlanchedAlmond", hex: "#ffebcd", key: ++key },
+ { name: "Bisque", hex: "#ffe4c4", key: ++key },
+ { name: "NavajoWhite", hex: "#ffdead", key: ++key },
+ { name: "Wheat", hex: "#f5deb3", key: ++key },
+ { name: "BurlyWood", hex: "#deb887", key: ++key },
+ { name: "Tan", hex: "#d2b48c", key: ++key },
+ { name: "RosyBrown", hex: "#bc8f8f", key: ++key },
+ { name: "SandyBrown", hex: "#f4a460", key: ++key },
+ { name: "Goldenrod", hex: "#daa520", key: ++key },
+ { name: "DarkGoldenrod", hex: "#b8860b", key: ++key },
+ { name: "Peru", hex: "#cd853f", key: ++key },
+ { name: "Chocolate", hex: "#d2691e", key: ++key },
+ { name: "SaddleBrown", hex: "#8b4513", key: ++key },
+ { name: "Sienna", hex: "#a0522d", key: ++key },
+ { name: "Brown", hex: "#a52a2a", key: ++key },
+ { name: "Maroon", hex: "#800000", key: ++key },
+ { name: "Snow", hex: "#fffafa", key: ++key },
+ { name: "HoneyDew", hex: "#f0fff0", key: ++key },
+ { name: "MintCream", hex: "#f5fffa", key: ++key },
+ { name: "Azure", hex: "#f0ffff", key: ++key },
+ { name: "AliceBlue", hex: "#f0f8ff", key: ++key },
+ { name: "GhostWhite", hex: "#f8f8ff", key: ++key },
+ { name: "WhiteSmoke", hex: "#f5f5f5", key: ++key },
+ { name: "SeaShell", hex: "#fff5ee", key: ++key },
+ { name: "Beige", hex: "#f5f5dc", key: ++key },
+ { name: "OldLace", hex: "#fdf5e6", key: ++key },
+ { name: "FloralWhite", hex: "#fffaf0", key: ++key },
+ { name: "Ivory", hex: "#fffff0", key: ++key },
+ { name: "AntiqueWhite", hex: "#faebd7", key: ++key },
+ { name: "Linen", hex: "#faf0e6", key: ++key },
+ { name: "LavenderBlush", hex: "#fff0f5", key: ++key },
+ { name: "MistyRose", hex: "#ffe4e1", key: ++key },
+ { name: "Gainsboro", hex: "#dcdcdc", key: ++key },
+ { name: "LightGray", hex: "#d3d3d3", key: ++key },
+ { name: "Silver", hex: "#c0c0c0", key: ++key },
+ { name: "DarkGray", hex: "#a9a9a9", key: ++key },
+ { name: "Gray", hex: "#808080", key: ++key },
+ { name: "DimGray", hex: "#696969", key: ++key },
+ { name: "LightSlateGray", hex: "#778899", key: ++key },
+ { name: "SlateGray", hex: "#708090", key: ++key },
+ { name: "DarkSlateGray", hex: "#2f4f4f", key: ++key },
+ { name: "Black", hex: "#000000", key: ++key },
+ { name: "White", hex: "#ffffff", key: ++key }
+ ];
+
+ return {
+ swatches,
+ byName: indexBy( "name" ),
+ byHex: indexBy( "hex" ),
+ byKey: indexBy( "key" )
+ };
+
+ // ------------------------------------------------------------------------------- //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * I index the swatches collection by the given property.
+ */
+ function indexBy( property ) {
+
+ var index = {};
+
+ for ( var swatch of swatches ) {
+
+ index[ swatch[ property ] ] = swatch;
+
+ }
+
+ return index;
+
+ }
+
+})();
diff --git a/demos/plus-minus-range/index.htm b/demos/plus-minus-range/index.htm
new file mode 100644
index 0000000000..5ea431c51d
--- /dev/null
+++ b/demos/plus-minus-range/index.htm
@@ -0,0 +1,80 @@
+
+
+
+
+
+ Parsing Plus-Minus Ranges In JavaScript
+
+
+
+
+
+
+ Parsing Plus-Minus (±) Ranges In JavaScript
+
+
+
+
+
+
+
+
diff --git a/demos/plus-minus-range/main.css b/demos/plus-minus-range/main.css
new file mode 100644
index 0000000000..b3a260c6fc
--- /dev/null
+++ b/demos/plus-minus-range/main.css
@@ -0,0 +1,95 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
+
+table {
+ border: 1px solid #333333 ;
+ border-collapse: collapse ;
+ width: 100% ;
+
+ & :where(th, td) {
+ border: 1px solid #333333 ;
+ padding: 5px 10px ;
+
+ &:not([align]) {
+ text-align: left ;
+ }
+ }
+}
+
+tbody tr:hover {
+ background-color: #e7f9ff ;
+}
+
+
+form {
+ display: flex ;
+ gap: 13px ;
+ max-width: 670px ;
+}
+
+form {
+ & input:nth-child(1) {
+ flex: 1 0 auto ;
+ width: 250px ;
+ }
+ & input {
+ flex: 1 0 auto ;
+ width: 100px ;
+ }
+}
diff --git a/demos/row-linker/index.htm b/demos/row-linker/index.htm
new file mode 100644
index 0000000000..988443058b
--- /dev/null
+++ b/demos/row-linker/index.htm
@@ -0,0 +1,233 @@
+
+
+
+
+
+ Table Row Linker Directive In Alpine.js
+
+
+
+
+
+
+ Table Row Linker Directive In Alpine.js
+
+
+
+
+
+
+
+
+
diff --git a/demos/row-linker/main.css b/demos/row-linker/main.css
new file mode 100644
index 0000000000..208dea2172
--- /dev/null
+++ b/demos/row-linker/main.css
@@ -0,0 +1,82 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
+
+table {
+ border: 1px solid #333333 ;
+ border-collapse: collapse ;
+ width: 100% ;
+
+ & :where(th, td) {
+ border: 1px solid #333333 ;
+ padding: 5px 10px ;
+
+ &:not([align]) {
+ text-align: left ;
+ }
+ }
+}
+
+tbody tr:hover {
+ background-color: #e7f9ff ;
+}
+
+a.active {
+ background-color: #333333 ;
+ color: #fafafa ;
+}
diff --git a/demos/scope-pseudo-class/index.htm b/demos/scope-pseudo-class/index.htm
new file mode 100644
index 0000000000..335f176315
--- /dev/null
+++ b/demos/scope-pseudo-class/index.htm
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Using :scope To Identify The Host Element In A CSS Selector
+
+
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/scope-pseudo-class/main.css b/demos/scope-pseudo-class/main.css
new file mode 100644
index 0000000000..ec6752de30
--- /dev/null
+++ b/demos/scope-pseudo-class/main.css
@@ -0,0 +1,29 @@
+
+html {
+ box-sizing: border-box ;
+}
+html *,
+html *:before,
+html *:after {
+ box-sizing: inherit ;
+}
+
+body {
+ font-family: monospace ;
+ font-size: 20px ;
+}
+
+p {
+ display: flex ;
+ font-size: 30px ;
+ gap: 10px ;
+}
+p span {
+ background-color: #f0f0f0 ;
+ border-radius: 5px ;
+ padding: 3px 15px ;
+}
+p:hover {
+ outline: 2px solid cyan ;
+ outline-offset: 3px ;
+}
\ No newline at end of file
diff --git a/demos/scroll-to-details/index.htm b/demos/scroll-to-details/index.htm
new file mode 100644
index 0000000000..6a4925888c
--- /dev/null
+++ b/demos/scroll-to-details/index.htm
@@ -0,0 +1,87 @@
+
+
+
+
+
+ Linking To A Disclosure (Details) Element
+
+
+
+
+
+
+ Linking To A Disclosure (Details) Element
+
+
+
+
+
+
+
+ Markdown Syntax
+
+
+ You can use limited Markdown syntax in your content.
+
+
+
+
+
+ Keyboard Shortcuts
+
+
+ You can use the following keyboard shortcuts in the application.
+
+
+
+
+
+ Privacy Policy
+
+
+ All your data is belong to us!
+
+
+
+
+
+
+
diff --git a/demos/scroll-to-details/main.css b/demos/scroll-to-details/main.css
new file mode 100644
index 0000000000..6fb08f51a0
--- /dev/null
+++ b/demos/scroll-to-details/main.css
@@ -0,0 +1,87 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-visible {
+ animation-duration: 200ms ;
+ animation-fill-mode: forwards ;
+ animation-iteration-count: 1 ;
+ animation-name: outlineEnter ;
+ animation-timing-function: ease-out ;
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ /* outline-style: solid ; */
+ outline-width: 2px ;
+ }
+}
+
+@keyframes outlineEnter {
+ from {
+ outline-offset: 8px ;
+ }
+ to {
+ outline-offset: 4px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
+
+button {
+ padding: 5px 15px ;
+}
+
+a {
+ color: red ;
+}
+
+details {
+ border: 1px solid #cccccc ;
+ margin-block-start: 10rem ;
+ padding: 10px ;
+
+ &[ open ] {
+ background-color: #f0f0f0 ;
+ }
+
+ & > summary {
+ cursor: pointer ;
+ scroll-margin-block-start: 35px ;
+
+ /*
+ By default, the browser doesn't appear to give an outline to the summary
+ element. As such, I'm explicitly giving it an outline when focused.
+ */
+ &:focus,
+ &:focus-visible {
+ outline-style: solid ;
+ }
+ }
+
+ & > section {
+ margin: 10px 0 0 0 ;
+ }
+}
diff --git a/demos/select-option-dataset/index.htm b/demos/select-option-dataset/index.htm
new file mode 100644
index 0000000000..d69d7c7b54
--- /dev/null
+++ b/demos/select-option-dataset/index.htm
@@ -0,0 +1,65 @@
+
+
+
+
+
+ Storing Metadata On Select Option Elements
+
+
+
+
+
+
+ Storing Metadata On Select Option Elements
+
+
+
+
+ - Select -
+
+
+ Naomi
+
+
+ Jon
+
+
+ Kim
+
+
+ Lara
+
+
+
+
+
+
+
diff --git a/demos/select-option-dataset/main.css b/demos/select-option-dataset/main.css
new file mode 100644
index 0000000000..6dbca334cf
--- /dev/null
+++ b/demos/select-option-dataset/main.css
@@ -0,0 +1,36 @@
+
+:where( html ) {
+ box-sizing: border-box ;
+
+ & *,
+ & *:before,
+ & *:after {
+ box-sizing: inherit ;
+ }
+}
+
+:where( * ) {
+ &:focus,
+ &:focus-within {
+ outline-color: hotpink ;
+ outline-offset: 4px ;
+ outline-width: 2px ;
+ }
+}
+
+body {
+ font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
+ font-size: 18px ;
+ line-height: 1.4 ;
+}
+
+button,
+input:where([type="text"]),
+select,
+textarea {
+ color: inherit ;
+ font-family: inherit ;
+ font-size: 20px ;
+ line-height: inherit ;
+ padding: 5px 10px ;
+}
diff --git a/demos/undefined-comparison/index.htm b/demos/undefined-comparison/index.htm
new file mode 100644
index 0000000000..009474b4f1
--- /dev/null
+++ b/demos/undefined-comparison/index.htm
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ Comparing Undefined Values In JavaScript
+
+
+
+
+
+ Comparing Undefined Values In JavaScript
+
+
+
+ See console for results.
+
+
+
+
+
+
diff --git a/vendor/sortable/1.15.6/sortable-1.15.6.min.js b/vendor/sortable/1.15.6/sortable-1.15.6.min.js
new file mode 100644
index 0000000000..95423a6491
--- /dev/null
+++ b/vendor/sortable/1.15.6/sortable-1.15.6.min.js
@@ -0,0 +1,2 @@
+/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY