From 1a9f1a1b459562a3f0d01bc010f79d80cea9230c Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 20 Nov 2024 09:55:45 -0500 Subject: [PATCH 01/22] Add local timezone formatting demo in Alpine.js. --- README.md | 1 + demos/local-date-formatter-alpine3/index.htm | 128 +++++++++++++++++++ demos/local-date-formatter-alpine3/main.css | 4 + 3 files changed, 133 insertions(+) create mode 100644 demos/local-date-formatter-alpine3/index.htm create mode 100644 demos/local-date-formatter-alpine3/main.css diff --git a/README.md b/README.md index 56657706f..13e7fca22 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [Formatting Dates In The Local Timezone With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/local-date-formatter-alpine3) * [CSV To CTE Transformer In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/csv-to-cte-angular18/dist) * [Route Changes With OnPush Change Detection In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/on-push-route-change-angular18/dist) * [Signals And Array Mutability In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/signal-array-angular18/dist) diff --git a/demos/local-date-formatter-alpine3/index.htm b/demos/local-date-formatter-alpine3/index.htm new file mode 100644 index 000000000..8c6090ce2 --- /dev/null +++ b/demos/local-date-formatter-alpine3/index.htm @@ -0,0 +1,128 @@ + + + + + Formatting Dates In The Local Timezone With Alpine.js + + + + + +

+ Formatting Dates In The Local Timezone With Alpine.js +

+ + +

+ +

+

+ +

+ + + + + + diff --git a/demos/local-date-formatter-alpine3/main.css b/demos/local-date-formatter-alpine3/main.css new file mode 100644 index 000000000..6aa79d9ec --- /dev/null +++ b/demos/local-date-formatter-alpine3/main.css @@ -0,0 +1,4 @@ +html { + font-family: monospace ; + font-size: 140% ; +} From f03d4f23ac96618a35a66f1b2a88831767173150 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 27 Nov 2024 07:12:00 -0500 Subject: [PATCH 02/22] Add flex-gap demo for content spacing. --- README.md | 1 + demos/margins-via-gap-css/index.htm | 189 ++++++++++++++++++++++++++++ demos/margins-via-gap-css/main.css | 45 +++++++ 3 files changed, 235 insertions(+) create mode 100644 demos/margins-via-gap-css/index.htm create mode 100644 demos/margins-via-gap-css/main.css diff --git a/README.md b/README.md index 13e7fca22..8aa9a4531 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [Using CSS Gap To Control Margins In Website Copy](https://bennadel.github.io/JavaScript-Demos/demos/margins-via-gap-css) * [Formatting Dates In The Local Timezone With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/local-date-formatter-alpine3) * [CSV To CTE Transformer In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/csv-to-cte-angular18/dist) * [Route Changes With OnPush Change Detection In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/on-push-route-change-angular18/dist) diff --git a/demos/margins-via-gap-css/index.htm b/demos/margins-via-gap-css/index.htm new file mode 100644 index 000000000..a15532f14 --- /dev/null +++ b/demos/margins-via-gap-css/index.htm @@ -0,0 +1,189 @@ + + + + + Using CSS Gap To Control Margins In Website Copy + + + + + + + +
+ Gap: + + rem +
+ + +
+ +

+ Using CSS Gap To Control Margins In Website Copy +

+ +

+ Oratio vel iudicium indico quamvis +

+ +

+ Lorem ipsum dolor sit amet aufero tu, quis inferus os. Meus credo post niger hiems. Proprius labor, quotiens inter divitiae, iuro sanctus dexter spatium. Aliquis quaero apud caecus aevum. Aureus frons, tandem post fama, traho candidus tuus auxilium. Bos curo foedus senex. Aliquis impleo quicumque negotium ve ille laus. +

+ +

+ Forum vel saeculum sto sicut +

+ +

+ Tu occurro tu lumen et qua murus. Ille scribo quisque, quidam honestus exercitus. Ille corrumpo quisque pondus licet quis frumentum. Meus eripio quidam causa. Ille procedo aliquis custos. Aliquis trado ecce vereor sine alter caput. Aliquis soleo quisquam, quis levis iudex. +

+ +

+ Ipse orior quantus ferrum uterque quisquis cohors. Idem desidero postea habeo ob vetus disciplina. Hic iungo iste, ego niger cor. Nos vaco per aptus cohors. Candidus reus, quo de tribunus, morior ingens posterus necessitas. Medius sacerdos, hodie de virtus, tego brevis rectus custos. Sui ascendo hic, quicumque sacer damnum. +

+ +

+ Nomen ipse doleo desero exercitus +

+ +

+ Mollis familia, mox prope arbor, consto quattuor pulcher equus. Hic cingo quisquis timor. Meus desino haud irascor a longus domus. Iste fleo quisquam, quisquis castus fortuna. Pulcher via, aliquando propter vir, agito humanus miser cinis. Custos trado superus aes. +

+ +

+ Is supero iste facies enim qui avis. Meus curro ob longus mos. Tu aperio idem res autem qui amnis. Tu debeo qua, qualis gravis genus. Ille creo idem, quidam ingens equus. Nos curo aliquis clamor ergo quantus testis. Noster sustineo quicumque fames vel ille nefas. Nos vivo quidam, idem singuli cor. +

+ +

+ Meus confero ipse deus sed quisquis gladius. Ille intellego qualis femina sed sui pater. Sui respondeo hic gradus. Idem sentio saepe appello sub vanus onus. Noster patior aliquis, aliquis integer barbarus. Nos soleo quantus dux seu idem laus. +

+ +

+ Hic constituo qua, sui aequus exemplum. Ego licet idem os. Hic intersum qui, sui potens dolus. Aliquis pertineo quantus rex. Copia descendo longus lingua. +

+ +

+ Potestas decerno tristis honor. Idem video ille gens. Carmen tollo ferus vultus. Nemo spero quisque, meus audax plebs. Noster memini quisque res vel ego umbra. Is ingredior idem, ipse fidelis honor. Ille revoco pro aureus moenia. Nos nosco per candidus magnitudo. +

+ +

+ Furor nam consul incido quin +

+ +

+ Ego eripio nemo via licet tu miles. Nemo oportet noster telum. Ille nascor nos, quantus inimicus periculum. Nemo contemno aliquis praeceptum. Noster cogo meus culpa tamen quisquis urbs. Nos offero aegre nolo post superbus pars. +

+ +

+ Meus lateo quantus, meus nobilis moenia. Prex sono durus negotium. Varius fabula, quare cum libertas, verto caecus facilis pax. Amplus hostis, vix sub error, nolo fortis albus coma. Is pario cur defendo ex integer civis. Is sequor aliquis fortuna. Noster moveo supra potens via. Ego venio sine adversus casus. +

+ +

+ Nemo pugno sui miles. Noster exspecto noster, quantus posterus coma. Hic possum qualis sors utrum qui cor. Nos intersum sui, quidam caelestis via. Meus gaudeo sine vacuus domus. +

+ +

+ Fessus cor, interim de libertas, eo fortis ceterus filia. Ille laedo quisque convivium. Hic tollo idem labor. Hic pario noster, sui aeternus sedes. Ille fateor ad praesens nox. Nemo cado non desero sine amicus fortuna. Sui advenio quisquis dignitas utrum nos officium. Hic traho statim procedo ob celer voluntas. +

+ +

+ Ipse confero ex superus vita. Meus fundo ego urbs at meus magnitudo. Nos consto quisque votum. Nos audeo qui imperator quamquam hic spiritus. Decus appareo tres multitudo. Nemo supersum quidam, sui divus saeculum. Aeger stella, solum circa labor, surgo aptus communis casus. Nemo pertineo idem iudex. +

+ +

+ Sors quisquis corrumpo condo ara +

+ +

+ Negotium peto iustus vita. Is cognosco sine liber facinus. Ordo retineo mollis flumen. Aliquis caedo nos, quidam turpis acies. Aliquis eligo circa rarus cohors. Idem supero tu sidus. Nemo promitto ille pontus. +

+ +

+ Nemo recedo is, tu altus gaudium. Extremus ius, quamvis contra arbor, cano medius duo virtus. Nemo reverto tamquam praesto a audax dolor. Ipse porto ob pauper insula. Ille cogo non sto circa socius frater. Ille perdo qui, is gravis copia. +

+ +

+ Opera deduco cura +

+ +

+ Frons divido gratus copia. Difficilis custos, quippe inter civitas, incido rectus privatus turba. Sui colligo hic, tu honestus pontus. Nemo differo quisquis ingenium at noster spes. Hic irascor quis dignitas neque qua carmen. Aura careo secundus acies. Carmen adverto tener locus. +

+ +

+ Meus iudico quis gens. Noster relinquo qualis fax quoque noster natura. Ille puto apud alius victoria. Iste effundo quidam, is vanus mens. Amicus pes, velut sine civis, edico proprius amicus animal. Noster respicio quidam laus. Potens iugum, sic intra imago, advenio candidus fessus nox. +

+ +

+ Hic verto quisque parens. Supplicium perdo ingens donum. Iste recipio quisque cinis. Ipse iuvo quidam facies tamen nos coma. Nemo convenio semper traho supra unus condicio. Tu rumpo ego, qualis novus terra. Idem offero quisque reus. Nobilis vultus, paulo circa domus, iaceo perpetuus malus facies. +

+ +

+ Saevus familia, num post modus, pareo utilis posterus auctor. Clamor cognosco regius crimen. Is impleo protinus ardeo prope niger filia. Aura indico tardus tribunus. Saeculum abeo vetus ventus. Idem accido iste vulgus nisi ipse saxum. Spiritus fruor tardus litus. +

+ +

+ Sui do quisquis, quisquis inferus culpa. Principium incipio proprius dux. Ipse careo quisquam, quidam proprius latus. Meus tego iste, idem aureus cornu. Nos rapio aliquis tectum. Meus studeo hic, qua secundus fides. Ipse vito supra falsus nomen. Maiores praecipio sapiens imperator. +

+ +

+ Tenebrae atque potestas adeo breviter +

+ +

+ Ille tego non conor circa singuli tempus. Nemo propono contra medius usus. Hic noceo ex ingens signum. Noster licet is ars utrum iste flumen. Ego posco nos voluptas. Tu moneo is, tu brevis annus. +

+ +

+ Tristis luna, ceterum contra ingenium, pergo albus extremus arvum. Adversus vultus, quantum inter hostis, statuo solus suus comes. Nemo pergo noster, iste totus virtus. Nefas iudico miser nihil. Noster transeo ille fortuna. Hic colligo quis, iste proprius votum. +

+ +

+ Noster vito quisque iudex aut quisquam error. Idem creo meus fortuna. Noster soleo quemadmodum cogito intra reliquus forum. Noster sino aliquis morbus postquam tu dies. Dulcis damnum, num de decus, colo vacuus potis vir. Noster fio ego memoria utrum ille dies. +

+ +

+ Parens aufero impetus +

+ +

+ Aliquis moneo is moenia. Iste paro aegre cano contra celer opera. Ipse dimitto quantus, is potens fortuna. Noster veho iam accedo post egregius invidia. Decus habeo similis vis. Sui effundo vulgo propono ante dives stella. +

+ +

+ Noster arbitror tu gaudium si quicumque exsilium. Hic lateo quisquam pretium nisi ipse fabula. Castrum disco altus natus. Pretium certo cunctus patria. Nemo posco quo posco a aeger eques. Sui taceo ipse dolus sed quantus pars. Vanus dolor, quando sine pax, doceo volucer sapiens magister. Ipse premo saepe noceo in clarus barbarus. +

+ +
+ + + diff --git a/demos/margins-via-gap-css/main.css b/demos/margins-via-gap-css/main.css new file mode 100644 index 000000000..1a687e3df --- /dev/null +++ b/demos/margins-via-gap-css/main.css @@ -0,0 +1,45 @@ + +html { + font-family: Seravek, Gill Sans Nova, Ubuntu, Calibri, DejaVu Sans, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +body { + margin: 10px ; +} + +h1 { + font-size: 2rem ; +} + +h2 { + font-size: 1.7rem ; +} + +h3 { + font-size: 1.3rem ; +} + +p { + font-size: 1.2rem ; +} + +form { + align-items: center ; + display: flex ; + justify-content: center ; + margin: 0 auto 20px ; + max-inline-size: 700px ; +} + +input[ type="range" ] { + flex: 1 1 auto ; + margin: 0 10px ; + width: 300px ; +} + +article { + margin: 0 auto ; + max-inline-size: 700px ; +} From 026b9411e1aaccae5443421284dc382b9b23d9de Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 27 Nov 2024 08:30:00 -0500 Subject: [PATCH 03/22] Add fieldset test. --- demos/margins-via-gap-css/test.htm | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 demos/margins-via-gap-css/test.htm diff --git a/demos/margins-via-gap-css/test.htm b/demos/margins-via-gap-css/test.htm new file mode 100644 index 000000000..5666a9e25 --- /dev/null +++ b/demos/margins-via-gap-css/test.htm @@ -0,0 +1,47 @@ + + + + + + + +
+ + This is not an inline element + + + + + + + + + + + + +
+ + + From 75e17e008337730ded744ff8db205dd8bb847e07 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 27 Nov 2024 08:40:38 -0500 Subject: [PATCH 04/22] Tweak. --- demos/margins-via-gap-css/test.htm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/demos/margins-via-gap-css/test.htm b/demos/margins-via-gap-css/test.htm index 5666a9e25..dac19072e 100644 --- a/demos/margins-via-gap-css/test.htm +++ b/demos/margins-via-gap-css/test.htm @@ -35,12 +35,13 @@ - + + From d5d84ba0e32507d6e4c77b2b29801bc40f718346 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 14 Dec 2024 07:25:31 -0500 Subject: [PATCH 05/22] Add box breathing demo. --- README.md | 1 + demos/box-breathing-alpine/index.htm | 275 +++++++++++++++++++++++++++ demos/box-breathing-alpine/main.css | 31 +++ 3 files changed, 307 insertions(+) create mode 100644 demos/box-breathing-alpine/index.htm create mode 100644 demos/box-breathing-alpine/main.css diff --git a/README.md b/README.md index 8aa9a4531..159188a3d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) * [Formatting Dates In The Local Timezone With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/local-date-formatter-alpine3) * [CSV To CTE Transformer In Angular 18](https://bennadel.github.io/JavaScript-Demos/demos/csv-to-cte-angular18/dist) diff --git a/demos/box-breathing-alpine/index.htm b/demos/box-breathing-alpine/index.htm new file mode 100644 index 000000000..fd9931008 --- /dev/null +++ b/demos/box-breathing-alpine/index.htm @@ -0,0 +1,275 @@ + + + + + Box Breathing Exercise With SpeechSynthesis And Alpine.js + + + + + + +

+ Box Breathing Exercise With SpeechSynthesis And Alpine.js +

+ +
+ +
+ + + + +
+ +

+ []: + +

+ +
+ + + + + diff --git a/demos/box-breathing-alpine/main.css b/demos/box-breathing-alpine/main.css new file mode 100644 index 000000000..28fbc1bfe --- /dev/null +++ b/demos/box-breathing-alpine/main.css @@ -0,0 +1,31 @@ + +body { + font-family: monospace ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +select { + font-family: inherit ; + font-size: inherit ; + line-height: inherit ; +} + +button { + padding: 0.5rem 2ch ; +} +select { + padding: 0.5rem 1.5ch ; +} + +.form { + display: flex ; + gap: 10px ; +} + +.text { + color: #666666 ; + font-size: 20px ; +/* text-transform: capitalize ;*/ +} From 6df991554370cc74e02dfd816884184f9582291a Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 1 Mar 2025 06:03:53 -0500 Subject: [PATCH 06/22] Add link button demos. --- README.md | 1 + demos/link-buttons/index.htm | 57 ++++++++++++++++++++++++ demos/link-buttons/index2.htm | 82 +++++++++++++++++++++++++++++++++++ demos/link-buttons/main.css | 59 +++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 demos/link-buttons/index.htm create mode 100644 demos/link-buttons/index2.htm create mode 100644 demos/link-buttons/main.css diff --git a/README.md b/README.md index 159188a3d..fc6878920 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) * [Formatting Dates In The Local Timezone With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/local-date-formatter-alpine3) diff --git a/demos/link-buttons/index.htm b/demos/link-buttons/index.htm new file mode 100644 index 000000000..965b3e19f --- /dev/null +++ b/demos/link-buttons/index.htm @@ -0,0 +1,57 @@ + + + + + + + Using The Button Form Attribute To Create Standalone Buttons In HTML + + + + + +

+ Using The Button Form Attribute To Create Standalone Buttons In HTML +

+ + + + +
+ + + + +
+ +

+ See Demo 2 +

+ + + \ No newline at end of file diff --git a/demos/link-buttons/index2.htm b/demos/link-buttons/index2.htm new file mode 100644 index 000000000..c6cc1bb2f --- /dev/null +++ b/demos/link-buttons/index2.htm @@ -0,0 +1,82 @@ + + + + + + + Using The Button Form Attribute To Create Standalone Buttons In HTML + + + + + +

+ Using The Button Form Attribute To Create Standalone Buttons In HTML +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNameAction
1Kim + +
2Jim + +
3Lynn + +
+ + +
+ + + + +
+ +

+ See Demo 1 +

+ + + \ No newline at end of file diff --git a/demos/link-buttons/main.css b/demos/link-buttons/main.css new file mode 100644 index 000000000..06033afc1 --- /dev/null +++ b/demos/link-buttons/main.css @@ -0,0 +1,59 @@ + +body { + font-family: monospace ; + font-size: 18px ; + line-height: 1.5 ; +} + +.link-button { + background-color: transparent ; + border-width: 0 ; + cursor: pointer ; + font-family: inherit ; + font-size: inherit ; + font-weight: inherit ; + line-height: inherit ; + margin: 0 ; + padding: 0 ; + text-decoration: underline ; +} + +.nav { + display: flex ; + gap: 8px ; +} +.nav__item { + border: 1px solid red ; + border-radius: 3px ; + color: red ; + padding: 5px 10px ; + text-decoration: none ; +} +.nav__item:hover { + text-decoration: underline ; +} + +.visually-hidden { + opacity: 0 ; + position: fixed ; + top: -100px ; +} + +*:focus, +*:focus-visible { + outline: 2px solid blue ; + outline-offset: 2px ; +} + +table { + border-collapse: collapse ; +} + +table th, +table td { + padding: 5px 10px ; +} + +table button { + +} \ No newline at end of file From b7113fbb7be71d765f10cf3fb0c293db15b68ec1 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Thu, 17 Apr 2025 06:02:52 -0400 Subject: [PATCH 07/22] Add comparison of undefined values test. --- README.md | 1 + demos/undefined-comparison/index.htm | 61 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 demos/undefined-comparison/index.htm diff --git a/README.md b/README.md index fc6878920..18365a91a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/undefined-comparison/index.htm b/demos/undefined-comparison/index.htm new file mode 100644 index 000000000..009474b4f --- /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. +

+ + + + + From 6cce03d4e27faa458429702e52258afbd7f1a9cc Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Tue, 29 Apr 2025 06:25:51 -0400 Subject: [PATCH 08/22] Add css scope demo. --- README.md | 1 + demos/scope-pseudo-class/index.htm | 47 ++++++++++++++++++++++++++++++ demos/scope-pseudo-class/main.css | 29 ++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 demos/scope-pseudo-class/index.htm create mode 100644 demos/scope-pseudo-class/main.css diff --git a/README.md b/README.md index 18365a91a..8356c5125 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/scope-pseudo-class/index.htm b/demos/scope-pseudo-class/index.htm new file mode 100644 index 000000000..335f17631 --- /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 000000000..ec6752de3 --- /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 From c8c3282cf5f2902a07391fd8fd60f0323c0aafbe Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sun, 22 Jun 2025 05:57:05 -0400 Subject: [PATCH 09/22] Add Sortable.js exploration. --- README.md | 1 + demos/movie-rank/app.css | 69 ++++ demos/movie-rank/app.js | 324 ++++++++++++++++++ demos/movie-rank/index.htm | 61 ++++ vendor/sortable/1.15.6/sortable-1.15.6.min.js | 2 + 5 files changed, 457 insertions(+) create mode 100644 demos/movie-rank/app.css create mode 100644 demos/movie-rank/app.js create mode 100644 demos/movie-rank/index.htm create mode 100644 vendor/sortable/1.15.6/sortable-1.15.6.min.js diff --git a/README.md b/README.md index 8356c5125..b3f6c743f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/movie-rank/app.css b/demos/movie-rank/app.css new file mode 100644 index 000000000..39e7df67d --- /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 000000000..ef791c549 --- /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 000000000..768ace488 --- /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: +

+ +
+ +
    + +
+ + + 0% + + +
    + +
+ +
+ +

+ + + +

+ + + 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 000000000..95423a649 --- /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 Date: Thu, 19 Jun 2025 06:08:01 -0400 Subject: [PATCH 10/22] WIP pixel art. --- demos/uint8-colors/index.htm | 88 +++++++++++ demos/uint8-colors/main.css | 96 ++++++++++++ demos/uint8-colors/main.js | 265 ++++++++++++++++++++++++++++++++++ demos/uint8-colors/palette.js | 178 +++++++++++++++++++++++ 4 files changed, 627 insertions(+) create mode 100644 demos/uint8-colors/index.htm create mode 100644 demos/uint8-colors/main.css create mode 100644 demos/uint8-colors/main.js create mode 100644 demos/uint8-colors/palette.js diff --git a/demos/uint8-colors/index.htm b/demos/uint8-colors/index.htm new file mode 100644 index 000000000..1411b7326 --- /dev/null +++ b/demos/uint8-colors/index.htm @@ -0,0 +1,88 @@ + + + + + + + Colors.... + + + + + + + + +
+ +

+ Colors.... +

+ +
+ +
+ +
+ +
+ +
+
+
Name:
+
+
+
+
Hex:
+
+
+
+ +
+ + +
+ +
+ + + + +
+ +
+ + + diff --git a/demos/uint8-colors/main.css b/demos/uint8-colors/main.css new file mode 100644 index 000000000..292115d6c --- /dev/null +++ b/demos/uint8-colors/main.css @@ -0,0 +1,96 @@ +*, +*:before, +*:after { + box-sizing: border-box ; +} + +body { + font-family: monospace ; + font-size: 16px ; + line-height: 1.4 ; +} + +main { + display: flex ; + flex-direction: column ; + align-items: center ; + gap: 30px ; +} + +.grid { + background-color: #eaeaea ; + display: inline-flex ; + flex-direction: column ; + user-select: none ; +} +.grid div { + display: flex ; +} +.grid button { + background-color: #fafafa ; + border: 1px solid #eaeaea ; + border-width: 0 1px 1px 0 ; + box-sizing: content-box ; + height: 26px ; + margin: 0 ; + padding: 0 ; + width: 26px ; +} +.grid button:first-of-type { + border-left-width: 1px ; +} +.grid div:first-of-type button { + border-top-width: 1px ; +} + +.palette { + display: flex ; + flex-wrap: wrap ; + width: 650px ; +} +.palette button { + box-shadow: 0 0 1px #000000 ; + border: none ; + flex: 1 1 auto ; + height: 30px ; + width: 30px ; +} +.palette button.isSelected { + box-shadow: 0 0 3px 5px #000000 ; + outline: 2px solid #ffffff ; + z-index: 2 ; +} +.palette button span { + display: none ; +} + +.selected { + display: flex ; + font-size: 20px ; + margin: 0 ; + gap: 30px ; +} +.selected div { + display: flex ; + gap: 10px ; +} +.selected dt { + font-weight: 700 ; + margin: 0 ; +} +.selected dd { + margin: 0 ; +} + +.tools { + display: flex ; + font-size: 20px ; + gap: 10px ; +} +.tools button { + background-color: #333 ; + border: none ; + color: #ffffff ; + font: inherit ; + padding: 10px 20px ; +} \ No newline at end of file diff --git a/demos/uint8-colors/main.js b/demos/uint8-colors/main.js new file mode 100644 index 000000000..e1e696c36 --- /dev/null +++ b/demos/uint8-colors/main.js @@ -0,0 +1,265 @@ +function Demo() { + + var grid = this.$el; + + return { + init: $init, + + palette: palette, + swatch: palette.swatches[ 5 ], + canvasWidth: 24, + canvasHeight: 24, + isDrawing: false, + + fill, + clear, + setSwatch, + startDrawing, + stopDrawing, + enterPixel, + pullLeft, + pullRight, + pullUp, + pullDown, + + fillPixel, + clearPixel, + getPixels, + getColoredPixels, + decodeCanvasFromHash, + encodeCanvasInHash + }; + + function $init() { + + if ( location.hash.slice( 1 ).length ) { + + setTimeout( + () => { + this.decodeCanvasFromHash(); + }, + 100 + ); + + } + + window.addEventListener( + "hashchange", + () => { + this.decodeCanvasFromHash(); + + } + ); + + } + + function getPixels() { + + return this.$refs.grid.querySelectorAll( ".grid-pixel" ); + + } + + function getColoredPixels() { + + return this.$refs.grid.querySelectorAll( ".grid-pixel[data-key]" ); + + } + + function fill( swatch = this.swatch ) { + + for ( var node of this.getPixels() ) { + + this.fillPixel( node ); + + } + + this.encodeCanvasInHash(); + + } + + function fillPixel( node, swatch = this.swatch ) { + + node.style.backgroundColor = swatch.hex; + node.dataset.key = swatch.key; + + } + + function clear() { + + for ( var node of this.getColoredPixels() ) { + + clearPixel( node ); + + } + + this.encodeCanvasInHash(); + + } + + function clearPixel( node ) { + + node.style = ""; + delete node.dataset.key; + + } + + + function setSwatch( swatch ) { + + this.swatch = swatch; + + } + + function startDrawing( event ) { + + this.isDrawing = true; + this.enterPixel( event ); + + } + + function stopDrawing() { + + this.isDrawing = false; + this.encodeCanvasInHash(); + + } + + function enterPixel( event ) { + + if ( ! this.isDrawing ) { + + return; + + } + + if ( event.altKey ) { + + this.clearPixel( this.$el ); + + } else { + + this.fillPixel( this.$el ); + + } + + } + + function pullLeft() { + + } + + function pullRight() { + + } + + function pullUp() { + + } + + function pullDown() { + + } + + + function decodeCanvasFromHash() { + + for ( var node of this.getPixels() ) { + + this.clearPixel( node ); + + } + + var hash = location.hash.slice( 1 ); + var pixels = pixelsFromBase64( hash ); + var grid = this.$refs.grid; + + for ( var pixel of pixels ) { + + var node = grid.querySelector( `[data-x="${ pixel.x }"][data-y="${ pixel.y }"]` ); + + this.fillPixel( node, palette.byKey[ pixel.key ] ); + + } + + } + + function encodeCanvasInHash() { + + var pixels = Array + .from( this.$refs.grid.querySelectorAll( "[data-key]" ) ) + .map( + ( node ) => { + + return { + key: +node.dataset.key, + x: +node.dataset.x, + y: +node.dataset.y + }; + + } + ) + ; + + var nextHash = `#${ pixelsToBase64( pixels ) }`; + + if ( nextHash === location.hash ) { + + return; + + } + + history.pushState( null, null, nextHash ); + + } + +} + +// ----------------------------------------------------------------------------------- // +// ----------------------------------------------------------------------------------- // + +/** +* +*/ +function pixelsToBase64( pixelsArray ) { + + var bytes = new Uint8Array( pixelsArray.length * 3 ); + var i = 0; + + for ( var pixel of pixelsArray ) { + + bytes[ i++ ] = pixel.key; + bytes[ i++ ] = pixel.x; + bytes[ i++ ] = pixel.y; + + } + + return btoa( String.fromCharCode( ...bytes ) ); + +} + +function pixelsFromBase64( pixelsString ) { + + var bytes = Uint8Array.from( + atob( pixelsString ), + ( byte ) => { + + return byte.charCodeAt( 0 ); + + } + ); + var pixels = []; + var i = 0; + + while ( i < bytes.length ) { + + pixels.push({ + key: bytes[ i++ ], + x: bytes[ i++ ], + y: bytes[ i++ ] + }); + + } + + return pixels; + +} diff --git a/demos/uint8-colors/palette.js b/demos/uint8-colors/palette.js new file mode 100644 index 000000000..475641b02 --- /dev/null +++ b/demos/uint8-colors/palette.js @@ -0,0 +1,178 @@ +var palette = (() => { + + // Colors courtesy of : https://htmlcolorcodes.com/color-names/ + 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; + + } + +})(); From ab29779424d93afe2378173ae42951b716d3c497 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Mon, 23 Jun 2025 06:47:02 -0400 Subject: [PATCH 11/22] Pixel art fun with Alpine.js. --- README.md | 1 + demos/pixel-art-alpine/index.htm | 113 ++++ demos/pixel-art-alpine/main.css | 171 +++++ demos/pixel-art-alpine/main.js | 610 ++++++++++++++++++ .../palette.js | 2 + demos/uint8-colors/index.htm | 88 --- demos/uint8-colors/main.css | 96 --- demos/uint8-colors/main.js | 265 -------- 8 files changed, 897 insertions(+), 449 deletions(-) create mode 100644 demos/pixel-art-alpine/index.htm create mode 100644 demos/pixel-art-alpine/main.css create mode 100644 demos/pixel-art-alpine/main.js rename demos/{uint8-colors => pixel-art-alpine}/palette.js (99%) delete mode 100644 demos/uint8-colors/index.htm delete mode 100644 demos/uint8-colors/main.css delete mode 100644 demos/uint8-colors/main.js diff --git a/README.md b/README.md index b3f6c743f..561f3fb97 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/pixel-art-alpine/index.htm b/demos/pixel-art-alpine/index.htm new file mode 100644 index 000000000..39f78a2e4 --- /dev/null +++ b/demos/pixel-art-alpine/index.htm @@ -0,0 +1,113 @@ + + + + + + + Pixel Art With Alpine.js + + + + + + + + +
+ +

+ Pixel Art With Alpine.js +

+ + +
+ +
+ + +
+ +
+ +
+
+
Name:
+
+
+
+
Hex:
+
+
+
+ +
+ + +
+ +
+ + + + + +
+ +
+

+ 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. +

+
+ +
+ Heart + Boobs + Smile +
+ +
+ + + diff --git a/demos/pixel-art-alpine/main.css b/demos/pixel-art-alpine/main.css new file mode 100644 index 000000000..2f11b67bd --- /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 000000000..d67383648 --- /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/uint8-colors/palette.js b/demos/pixel-art-alpine/palette.js similarity index 99% rename from demos/uint8-colors/palette.js rename to demos/pixel-art-alpine/palette.js index 475641b02..89db38130 100644 --- a/demos/uint8-colors/palette.js +++ b/demos/pixel-art-alpine/palette.js @@ -1,6 +1,8 @@ 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 }, diff --git a/demos/uint8-colors/index.htm b/demos/uint8-colors/index.htm deleted file mode 100644 index 1411b7326..000000000 --- a/demos/uint8-colors/index.htm +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - Colors.... - - - - - - - - -
- -

- Colors.... -

- -
- -
- -
- -
- -
-
-
Name:
-
-
-
-
Hex:
-
-
-
- -
- - -
- -
- - - - -
- -
- - - diff --git a/demos/uint8-colors/main.css b/demos/uint8-colors/main.css deleted file mode 100644 index 292115d6c..000000000 --- a/demos/uint8-colors/main.css +++ /dev/null @@ -1,96 +0,0 @@ -*, -*:before, -*:after { - box-sizing: border-box ; -} - -body { - font-family: monospace ; - font-size: 16px ; - line-height: 1.4 ; -} - -main { - display: flex ; - flex-direction: column ; - align-items: center ; - gap: 30px ; -} - -.grid { - background-color: #eaeaea ; - display: inline-flex ; - flex-direction: column ; - user-select: none ; -} -.grid div { - display: flex ; -} -.grid button { - background-color: #fafafa ; - border: 1px solid #eaeaea ; - border-width: 0 1px 1px 0 ; - box-sizing: content-box ; - height: 26px ; - margin: 0 ; - padding: 0 ; - width: 26px ; -} -.grid button:first-of-type { - border-left-width: 1px ; -} -.grid div:first-of-type button { - border-top-width: 1px ; -} - -.palette { - display: flex ; - flex-wrap: wrap ; - width: 650px ; -} -.palette button { - box-shadow: 0 0 1px #000000 ; - border: none ; - flex: 1 1 auto ; - height: 30px ; - width: 30px ; -} -.palette button.isSelected { - box-shadow: 0 0 3px 5px #000000 ; - outline: 2px solid #ffffff ; - z-index: 2 ; -} -.palette button span { - display: none ; -} - -.selected { - display: flex ; - font-size: 20px ; - margin: 0 ; - gap: 30px ; -} -.selected div { - display: flex ; - gap: 10px ; -} -.selected dt { - font-weight: 700 ; - margin: 0 ; -} -.selected dd { - margin: 0 ; -} - -.tools { - display: flex ; - font-size: 20px ; - gap: 10px ; -} -.tools button { - background-color: #333 ; - border: none ; - color: #ffffff ; - font: inherit ; - padding: 10px 20px ; -} \ No newline at end of file diff --git a/demos/uint8-colors/main.js b/demos/uint8-colors/main.js deleted file mode 100644 index e1e696c36..000000000 --- a/demos/uint8-colors/main.js +++ /dev/null @@ -1,265 +0,0 @@ -function Demo() { - - var grid = this.$el; - - return { - init: $init, - - palette: palette, - swatch: palette.swatches[ 5 ], - canvasWidth: 24, - canvasHeight: 24, - isDrawing: false, - - fill, - clear, - setSwatch, - startDrawing, - stopDrawing, - enterPixel, - pullLeft, - pullRight, - pullUp, - pullDown, - - fillPixel, - clearPixel, - getPixels, - getColoredPixels, - decodeCanvasFromHash, - encodeCanvasInHash - }; - - function $init() { - - if ( location.hash.slice( 1 ).length ) { - - setTimeout( - () => { - this.decodeCanvasFromHash(); - }, - 100 - ); - - } - - window.addEventListener( - "hashchange", - () => { - this.decodeCanvasFromHash(); - - } - ); - - } - - function getPixels() { - - return this.$refs.grid.querySelectorAll( ".grid-pixel" ); - - } - - function getColoredPixels() { - - return this.$refs.grid.querySelectorAll( ".grid-pixel[data-key]" ); - - } - - function fill( swatch = this.swatch ) { - - for ( var node of this.getPixels() ) { - - this.fillPixel( node ); - - } - - this.encodeCanvasInHash(); - - } - - function fillPixel( node, swatch = this.swatch ) { - - node.style.backgroundColor = swatch.hex; - node.dataset.key = swatch.key; - - } - - function clear() { - - for ( var node of this.getColoredPixels() ) { - - clearPixel( node ); - - } - - this.encodeCanvasInHash(); - - } - - function clearPixel( node ) { - - node.style = ""; - delete node.dataset.key; - - } - - - function setSwatch( swatch ) { - - this.swatch = swatch; - - } - - function startDrawing( event ) { - - this.isDrawing = true; - this.enterPixel( event ); - - } - - function stopDrawing() { - - this.isDrawing = false; - this.encodeCanvasInHash(); - - } - - function enterPixel( event ) { - - if ( ! this.isDrawing ) { - - return; - - } - - if ( event.altKey ) { - - this.clearPixel( this.$el ); - - } else { - - this.fillPixel( this.$el ); - - } - - } - - function pullLeft() { - - } - - function pullRight() { - - } - - function pullUp() { - - } - - function pullDown() { - - } - - - function decodeCanvasFromHash() { - - for ( var node of this.getPixels() ) { - - this.clearPixel( node ); - - } - - var hash = location.hash.slice( 1 ); - var pixels = pixelsFromBase64( hash ); - var grid = this.$refs.grid; - - for ( var pixel of pixels ) { - - var node = grid.querySelector( `[data-x="${ pixel.x }"][data-y="${ pixel.y }"]` ); - - this.fillPixel( node, palette.byKey[ pixel.key ] ); - - } - - } - - function encodeCanvasInHash() { - - var pixels = Array - .from( this.$refs.grid.querySelectorAll( "[data-key]" ) ) - .map( - ( node ) => { - - return { - key: +node.dataset.key, - x: +node.dataset.x, - y: +node.dataset.y - }; - - } - ) - ; - - var nextHash = `#${ pixelsToBase64( pixels ) }`; - - if ( nextHash === location.hash ) { - - return; - - } - - history.pushState( null, null, nextHash ); - - } - -} - -// ----------------------------------------------------------------------------------- // -// ----------------------------------------------------------------------------------- // - -/** -* -*/ -function pixelsToBase64( pixelsArray ) { - - var bytes = new Uint8Array( pixelsArray.length * 3 ); - var i = 0; - - for ( var pixel of pixelsArray ) { - - bytes[ i++ ] = pixel.key; - bytes[ i++ ] = pixel.x; - bytes[ i++ ] = pixel.y; - - } - - return btoa( String.fromCharCode( ...bytes ) ); - -} - -function pixelsFromBase64( pixelsString ) { - - var bytes = Uint8Array.from( - atob( pixelsString ), - ( byte ) => { - - return byte.charCodeAt( 0 ); - - } - ); - var pixels = []; - var i = 0; - - while ( i < bytes.length ) { - - pixels.push({ - key: bytes[ i++ ], - x: bytes[ i++ ], - y: bytes[ i++ ] - }); - - } - - return pixels; - -} From 48766e4a6f24fddf8a165840195c6d5975674e46 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Mon, 15 Sep 2025 05:57:42 -0400 Subject: [PATCH 12/22] Adding prev/next exploration of htmx mechanics. --- README.md | 1 + demos/htmx-prev-next/index.htm | 140 +++++++++++++++++++++++++++++++++ demos/htmx-prev-next/main.css | 20 +++++ 3 files changed, 161 insertions(+) create mode 100644 demos/htmx-prev-next/index.htm create mode 100644 demos/htmx-prev-next/main.css diff --git a/README.md b/README.md index 561f3fb97..6577f0a6a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/htmx-prev-next/index.htm b/demos/htmx-prev-next/index.htm new file mode 100644 index 000000000..16b6f4693 --- /dev/null +++ b/demos/htmx-prev-next/index.htm @@ -0,0 +1,140 @@ + + + + + + + + +

+ Exploring Prev/Next Mechanics In HTMX +

+ + + +
+ + + + + diff --git a/demos/htmx-prev-next/main.css b/demos/htmx-prev-next/main.css new file mode 100644 index 000000000..832631675 --- /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 ; +} + From ad762ccadd7681d1c35658b74e91f6c2876bd37e Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 10 Jan 2026 06:43:03 -0500 Subject: [PATCH 13/22] Adding data-* demo for select options. --- README.md | 1 + demos/select-option-dataset/index.htm | 65 +++++++++++++++++++++++++++ demos/select-option-dataset/main.css | 36 +++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 demos/select-option-dataset/index.htm create mode 100644 demos/select-option-dataset/main.css diff --git a/README.md b/README.md index 6577f0a6a..5561b17a2 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/select-option-dataset/index.htm b/demos/select-option-dataset/index.htm new file mode 100644 index 000000000..d69d7c7b5 --- /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 +

+ + + + + + + diff --git a/demos/select-option-dataset/main.css b/demos/select-option-dataset/main.css new file mode 100644 index 000000000..6dbca334c --- /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 ; +} From f7452ffb4e320d489824cec6987998d18aef1242 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Mon, 12 Jan 2026 05:51:57 -0500 Subject: [PATCH 14/22] Exploring the dialog element. --- README.md | 1 + demos/dialog-element/index.htm | 144 +++++++++++++++++++++++++++++++++ demos/dialog-element/main.css | 50 ++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 demos/dialog-element/index.htm create mode 100644 demos/dialog-element/main.css diff --git a/README.md b/README.md index 5561b17a2..2a2346286 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/dialog-element/index.htm b/demos/dialog-element/index.htm new file mode 100644 index 000000000..a0647c5eb --- /dev/null +++ b/demos/dialog-element/index.htm @@ -0,0 +1,144 @@ + + + + + + Exploring The Dialog Element In HTML + + + + + +

+ Exploring The Dialog Element In HTML +

+ +

+ + +

+ + +
+ + + + +
+

+ Are you sure? +

+

+ + +

+
+
+ + + + + + diff --git a/demos/dialog-element/main.css b/demos/dialog-element/main.css new file mode 100644 index 000000000..2fa849f34 --- /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 ; +} From 33f76adf5ef814081b920f91ea5fece7fa009f56 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Tue, 13 Jan 2026 06:38:09 -0500 Subject: [PATCH 15/22] Add dialog as sidebar demo. --- README.md | 5 +- demos/dialog-element-sidebar/index.htm | 126 +++++++++++++++++++++++++ demos/dialog-element-sidebar/main.css | 45 +++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 demos/dialog-element-sidebar/index.htm create mode 100644 demos/dialog-element-sidebar/main.css diff --git a/README.md b/README.md index 2a2346286..83729e22d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) @@ -707,5 +708,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/dialog-element-sidebar/index.htm b/demos/dialog-element-sidebar/index.htm new file mode 100644 index 000000000..19835437c --- /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 +

+ + + + + +

+ Dialog As 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. +

+ + + + + +
+ + +

+ Back to Top +

+ + + + + + diff --git a/demos/dialog-element-sidebar/main.css b/demos/dialog-element-sidebar/main.css new file mode 100644 index 000000000..e9a48ed8a --- /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. */ +} From 5d98a69ec4069a45c9cba2a24a1519a22b1ee694 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Thu, 15 Jan 2026 06:32:51 -0500 Subject: [PATCH 16/22] Add scroll-to details demo. --- README.md | 1 + demos/scroll-to-details/index.htm | 87 +++++++++++++++++++++++++++++++ demos/scroll-to-details/main.css | 87 +++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 demos/scroll-to-details/index.htm create mode 100644 demos/scroll-to-details/main.css diff --git a/README.md b/README.md index 83729e22d..ca0a66344 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/scroll-to-details/index.htm b/demos/scroll-to-details/index.htm new file mode 100644 index 000000000..6a4925888 --- /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 000000000..6fb08f51a --- /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 ; + } +} From 1e169fd4e36886745b173742a28935ddb4e76d4f Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 17 Jan 2026 07:03:52 -0500 Subject: [PATCH 17/22] Code kata for focus ring zoomies. --- README.md | 1 + demos/focus-box/index.htm | 165 ++++++++++++++++++++++++++++++++++++++ demos/focus-box/main.css | 67 ++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 demos/focus-box/index.htm create mode 100644 demos/focus-box/main.css diff --git a/README.md b/README.md index ca0a66344..57012215a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/focus-box/index.htm b/demos/focus-box/index.htm new file mode 100644 index 000000000..e61ee14cb --- /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 000000000..6016e0bb2 --- /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 !important ; + outline: none !important ; + } +} + + +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 ; +} From 3964f30dcafa7ccdd2009cc44bfb6b00b104a7ee Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 17 Jan 2026 08:06:30 -0500 Subject: [PATCH 18/22] Remove superfluous important. --- demos/focus-box/main.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/focus-box/main.css b/demos/focus-box/main.css index 6016e0bb2..2d6ee4756 100644 --- a/demos/focus-box/main.css +++ b/demos/focus-box/main.css @@ -35,8 +35,8 @@ .textBlock * { &:focus, &:focus-visible { - animation-name: none !important ; - outline: none !important ; + animation-name: none ; + outline: none ; } } From 8b1c791eeaf5104473edf17af3086d3efb941a67 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 28 Jan 2026 06:58:49 -0500 Subject: [PATCH 19/22] Add broadcast channel api demo. --- README.md | 1 + demos/broadcast-api/frame.htm | 75 ++++++++++++++++++++++++ demos/broadcast-api/index.htm | 30 ++++++++++ demos/broadcast-api/main.css | 107 ++++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 demos/broadcast-api/frame.htm create mode 100644 demos/broadcast-api/index.htm create mode 100644 demos/broadcast-api/main.css diff --git a/README.md b/README.md index 57012215a..b2423fe38 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/broadcast-api/frame.htm b/demos/broadcast-api/frame.htm new file mode 100644 index 000000000..6e26e34a8 --- /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 000000000..9b0d7694a --- /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 000000000..74c4b740f --- /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 ) ; + } +} From 12304645f71ce2ce1d61014702e1286f59832f82 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Fri, 20 Feb 2026 06:05:30 -0500 Subject: [PATCH 20/22] Add row-linker directive in Alpine.js --- README.md | 1 + demos/row-linker/index.htm | 233 +++++++++++++++++++++++++++++++++++++ demos/row-linker/main.css | 82 +++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 demos/row-linker/index.htm create mode 100644 demos/row-linker/main.css diff --git a/README.md b/README.md index b2423fe38..c4d3c1dc4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/row-linker/index.htm b/demos/row-linker/index.htm new file mode 100644 index 000000000..988443058 --- /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 +

+ + + + + + + + + + + + + +
NameInfoCategoryActions
+ + + + + + diff --git a/demos/row-linker/main.css b/demos/row-linker/main.css new file mode 100644 index 000000000..208dea217 --- /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 ; +} From bd7fece85871a38f053a41d7f2cdb74a25404135 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Sat, 21 Feb 2026 06:14:33 -0500 Subject: [PATCH 21/22] Add isTrusted exploration. --- README.md | 1 + demos/event-is-trusted/index.htm | 77 ++++++++++++++++++++++++++++++ demos/event-is-trusted/main.css | 82 ++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 demos/event-is-trusted/index.htm create mode 100644 demos/event-is-trusted/main.css diff --git a/README.md b/README.md index c4d3c1dc4..27cb3c8f1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [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) diff --git a/demos/event-is-trusted/index.htm b/demos/event-is-trusted/index.htm new file mode 100644 index 000000000..497c68960 --- /dev/null +++ b/demos/event-is-trusted/index.htm @@ -0,0 +1,77 @@ + + + + + + Exploring Event.isTrusted In JavaScript + + + + + +

+ Exploring Event.isTrusted In JavaScript +

+ +

+ Form +

+ +
+ + + Cancel + +
+ +

+ Triggers on Form +

+ +

+ + + +

+ + + + + diff --git a/demos/event-is-trusted/main.css b/demos/event-is-trusted/main.css new file mode 100644 index 000000000..208dea217 --- /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 ; +} From 5bc908d332d4fe9216e06ec2f9ab3d7866012e38 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Wed, 25 Feb 2026 06:43:43 -0500 Subject: [PATCH 22/22] Add plus-minus range parsing demo. --- README.md | 1 + demos/plus-minus-range/index.htm | 80 +++++++++++++++++++++++++++ demos/plus-minus-range/main.css | 95 ++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 demos/plus-minus-range/index.htm create mode 100644 demos/plus-minus-range/main.css diff --git a/README.md b/README.md index 27cb3c8f1..504921ce1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ 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) diff --git a/demos/plus-minus-range/index.htm b/demos/plus-minus-range/index.htm new file mode 100644 index 000000000..5ea431c51 --- /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 000000000..b3a260c6f --- /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 ; + } +}