diff --git a/README.md b/README.md index 56657706f8..504921ce1a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,24 @@ with. ## My JavaScript Demos - I Love JavaScript! +* [Parsing Plus-Minus Ranges In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/plus-minus-range) +* [Exploring Event.isTrusted In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/event-is-trusted) +* [Table Row Linker Directive In Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/row-linker) +* [Sending Messages Across Documents With The Broadcast Channel API](https://bennadel.github.io/JavaScript-Demos/demos/broadcast-api) +* [Animating DOM Rectangles Over Focused Elements In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/focus-box) +* [Linking To A Disclosure (Details) Element](https://bennadel.github.io/JavaScript-Demos/demos/scroll-to-details) +* [Opening The Dialog Element As A Fly-out Sidebar](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element-sidebar) +* [Exploring The Dialog Element In HTML](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element) +* [Storing Metadata On Select Option Elements](https://bennadel.github.io/JavaScript-Demos/demos/select-option-dataset) +* [Exploring Prev/Next Mechanics In HTMX](https://bennadel.github.io/JavaScript-Demos/demos/htmx-prev-next) +* [Pixel Art With Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/pixel-art-alpine) +* [Movie Ranking With Sortable.js And Kendall Tau Distance](https://bennadel.github.io/JavaScript-Demos/demos/movie-rank) +* [Using :scope To Identify The Host Element In A CSS Selector](https://bennadel.github.io/JavaScript-Demos/demos/scope-pseudo-class) +* [Comparing Undefined Values In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/undefined-comparison) +* [Using The Button Form Attribute To Create Standalone Buttons In HTML](https://bennadel.github.io/JavaScript-Demos/demos/link-buttons) +* [Box Breathing Exercise With SpeechSynthesis And Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/box-breathing-alpine) +* [Using CSS Gap To Control Margins In Website Copy](https://bennadel.github.io/JavaScript-Demos/demos/margins-via-gap-css) +* [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) @@ -696,5 +714,5 @@ with. Want more JavaScript goodness? Check out the [JavaScript blog entries][javascript-blog] on my website. -[bennadel]: http://www.bennadel.com -[javascript-blog]: http://www.bennadel.com/blog/tags/6-javascript-dhtml-blog-entries.htm +[bennadel]: https://www.bennadel.com +[javascript-blog]: https://www.bennadel.com/blog/tags/6-javascript-dhtml-blog-entries.htm diff --git a/demos/box-breathing-alpine/index.htm b/demos/box-breathing-alpine/index.htm new file mode 100644 index 0000000000..fd9931008b --- /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 0000000000..28fbc1bfef --- /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 ;*/ +} diff --git a/demos/broadcast-api/frame.htm b/demos/broadcast-api/frame.htm new file mode 100644 index 0000000000..6e26e34a8d --- /dev/null +++ b/demos/broadcast-api/frame.htm @@ -0,0 +1,75 @@ + + + + + + Broadcast Channel API Frame + + + + + + + + + + + + + + diff --git a/demos/broadcast-api/index.htm b/demos/broadcast-api/index.htm new file mode 100644 index 0000000000..9b0d7694af --- /dev/null +++ b/demos/broadcast-api/index.htm @@ -0,0 +1,30 @@ + + + + + + Sending Messages Across Documents With The Broadcast Channel API + + + + + +

+ Sending Messages Across Documents With The Broadcast Channel API +

+ +

+ These are all <iframe> elements to the same URL: +

+ +
+ + + + + + +
+ + + diff --git a/demos/broadcast-api/main.css b/demos/broadcast-api/main.css new file mode 100644 index 0000000000..74c4b740f5 --- /dev/null +++ b/demos/broadcast-api/main.css @@ -0,0 +1,107 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +.textBlock * { + &:focus, + &:focus-visible { + animation-name: none ; + outline: none ; + } +} + + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} + +.frame-list { + display: flex ; + gap: 10px ; + flex-wrap: wrap ; + + & iframe { + border: 1px solid #333333 ; + flex: 0 0 auto ; + height: 200px ; + width: 220px ; + } +} + +.frame-body { + margin: 0 ; + min-height: 100vh ; + + &:hover .laser { + --color: 0, 155, 0 ; + --size: 20px ; + } +} + +.laser { + --color: 255, 0, 0 ; + --size: 8px ; + position: fixed ; + + &:before { + background: rgb( var( --color ) ) ; + border-radius: 100% ; + box-shadow: 0 0 8px 0 rgba( var( --color ), 0.8 ) ; + content: "" ; + height: var( --size ) ; + position: absolute ; + translate: -50% -50% ; + width: var( --size ) ; + } +} diff --git a/demos/dialog-element-sidebar/index.htm b/demos/dialog-element-sidebar/index.htm new file mode 100644 index 0000000000..19835437c3 --- /dev/null +++ b/demos/dialog-element-sidebar/index.htm @@ -0,0 +1,126 @@ + + + + + + Opening The Dialog Element As A Fly-out Sidebar + + + + + +

+ Opening The Dialog Element As A Fly-out Sidebar +

+ + + + + +

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

+ Back to Top +

+ + + + + + diff --git a/demos/dialog-element-sidebar/main.css b/demos/dialog-element-sidebar/main.css new file mode 100644 index 0000000000..e9a48ed8a3 --- /dev/null +++ b/demos/dialog-element-sidebar/main.css @@ -0,0 +1,45 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-within { + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +dialog h2 { + margin-block-start: 0 ; + scroll-margin-block-start: 2rem ; /* To counteract dialog padding. */ +} diff --git a/demos/dialog-element/index.htm b/demos/dialog-element/index.htm new file mode 100644 index 0000000000..a0647c5eb1 --- /dev/null +++ b/demos/dialog-element/index.htm @@ -0,0 +1,144 @@ + + + + + + Exploring The Dialog Element In HTML + + + + + +

+ Exploring The Dialog Element In HTML +

+ +

+ + +

+ + +
+ + + + +
+

+ Are you sure? +

+

+ + +

+
+
+ + + + + + diff --git a/demos/dialog-element/main.css b/demos/dialog-element/main.css new file mode 100644 index 0000000000..2fa849f347 --- /dev/null +++ b/demos/dialog-element/main.css @@ -0,0 +1,50 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-within { + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + + +form p:first-child { + margin-top: 0 ; +} +form p:last-child { + display: flex ; + gap: 12px ; + margin-bottom: 0 ; +} diff --git a/demos/event-is-trusted/index.htm b/demos/event-is-trusted/index.htm new file mode 100644 index 0000000000..497c689600 --- /dev/null +++ b/demos/event-is-trusted/index.htm @@ -0,0 +1,77 @@ + + + + + + Exploring Event.isTrusted In JavaScript + + + + + +

+ Exploring Event.isTrusted In JavaScript +

+ +

+ Form +

+ +
+ + + 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 0000000000..208dea2172 --- /dev/null +++ b/demos/event-is-trusted/main.css @@ -0,0 +1,82 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} + +table { + border: 1px solid #333333 ; + border-collapse: collapse ; + width: 100% ; + + & :where(th, td) { + border: 1px solid #333333 ; + padding: 5px 10px ; + + &:not([align]) { + text-align: left ; + } + } +} + +tbody tr:hover { + background-color: #e7f9ff ; +} + +a.active { + background-color: #333333 ; + color: #fafafa ; +} diff --git a/demos/focus-box/index.htm b/demos/focus-box/index.htm new file mode 100644 index 0000000000..e61ee14cb5 --- /dev/null +++ b/demos/focus-box/index.htm @@ -0,0 +1,165 @@ + + + + + + Animating DOM Rectangles Over Focused Elements In JavaScript + + + + + +

+ Animating DOM Rectangles Over Focused Elements In JavaScript +

+ +

+ Before demo text block (to take focus from demo block and to contrast + the normal focus-outline behavior). +

+ +

+ + + + Lorem ipsum dolor sit amet interrogo communis flumen. + Mille iuvenis, umquam ante cohors, adhibeo citus fortis provincia. + Hic posco ego, quis frequens tenebrae. Aliquis turbo is epistula. Hic + experior quidam voluntas nam aliquis vinculum. Noster puto paulo sum post + saevus natura. Diversus ventus, quasi cum for meus, scribo dexter prior + astrum. Idem condo qua ictus quoque nemo iter. Mortalis frater, denique in + puer, tollo nullus quattuor vestigium. Iste dico ipse voluntas an sui + vitium meus desino quisquis, qua gratus ira tu opto nemo praemium gens rideo + diversus error. + +

+ + + + + + diff --git a/demos/focus-box/main.css b/demos/focus-box/main.css new file mode 100644 index 0000000000..2d6ee4756b --- /dev/null +++ b/demos/focus-box/main.css @@ -0,0 +1,67 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +.textBlock * { + &:focus, + &:focus-visible { + animation-name: none ; + outline: none ; + } +} + + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} diff --git a/demos/htmx-prev-next/index.htm b/demos/htmx-prev-next/index.htm new file mode 100644 index 0000000000..16b6f4693b --- /dev/null +++ b/demos/htmx-prev-next/index.htm @@ -0,0 +1,140 @@ + + + + + + + + +

+ Exploring Prev/Next Mechanics In HTMX +

+ + + +
+ + + + + diff --git a/demos/htmx-prev-next/main.css b/demos/htmx-prev-next/main.css new file mode 100644 index 0000000000..832631675f --- /dev/null +++ b/demos/htmx-prev-next/main.css @@ -0,0 +1,20 @@ + +body { + font-family: verdana, arial, sans-serif ; + font-size: 18px ; +} + +button { + border: 1px solid #999999 ; + color: inherit ; + cursor: pointer ; + font-family: inherit ; + font-size: 20px ; + padding: 5px 10px ; +} + +.selected { + font-weight: bold ; + background: gold ; +} + diff --git a/demos/link-buttons/index.htm b/demos/link-buttons/index.htm new file mode 100644 index 0000000000..965b3e19ff --- /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 0000000000..c6cc1bb2f3 --- /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 0000000000..06033afc1d --- /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 diff --git a/demos/local-date-formatter-alpine3/index.htm b/demos/local-date-formatter-alpine3/index.htm new file mode 100644 index 0000000000..8c6090ce2c --- /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 0000000000..6aa79d9ecb --- /dev/null +++ b/demos/local-date-formatter-alpine3/main.css @@ -0,0 +1,4 @@ +html { + font-family: monospace ; + font-size: 140% ; +} diff --git a/demos/margins-via-gap-css/index.htm b/demos/margins-via-gap-css/index.htm new file mode 100644 index 0000000000..a15532f146 --- /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 0000000000..1a687e3dff --- /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 ; +} diff --git a/demos/margins-via-gap-css/test.htm b/demos/margins-via-gap-css/test.htm new file mode 100644 index 0000000000..dac19072e5 --- /dev/null +++ b/demos/margins-via-gap-css/test.htm @@ -0,0 +1,48 @@ + + + + + + + +
+ + This is not an inline element + + + + + + + + + + + + + +
+ + + diff --git a/demos/movie-rank/app.css b/demos/movie-rank/app.css new file mode 100644 index 0000000000..39e7df67d5 --- /dev/null +++ b/demos/movie-rank/app.css @@ -0,0 +1,69 @@ + +html { + box-sizing: border-box ; + font-family: monospace ; + font-size: 18px ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +body { + text-align: center ; +} + +.panels { + align-items: center ; + display: flex ; + gap: 30px ; + justify-content: center ; + margin: 40px 0 ; + + & ul { + list-style-type: none ; + margin: 0 ; + padding: 0 ; + } + + & li { + background-color: #fafafa ; + border: 1px solid #cccccc ; + padding: 10px 30px ; + margin: 3px ; + user-select: none ; + } + + & mark { + border-radius: 20px ; + border: 1px solid #cccccc ; + font-size: 30px ; + line-height: 1 ; + padding: 30px 0 ; + width: 150px ; + + &:after { + content: "%" ; + } + } + + & .sortable-ghost { + background-color: hotpink ; + color: white ; + } +} + +.resets { + display: flex ; + gap: 145px ; + justify-content: center ; + + & button { + font-family: inherit ; + font-size: inherit ; + line-height: inherit ; + padding: 5px 12px ; + } +} diff --git a/demos/movie-rank/app.js b/demos/movie-rank/app.js new file mode 100644 index 0000000000..ef791c5498 --- /dev/null +++ b/demos/movie-rank/app.js @@ -0,0 +1,324 @@ +function App() { + + // Note: the "id" value matches the "index" value. This fact will be leveraged when + // reading the DOM for the sorted list of IDs. + var id = -1; + var titles = [ + { id: ++id, name: "10 Things I Hate About You" }, + { id: ++id, name: "50 First Dates" }, + { id: ++id, name: "Annie Hall" }, + { id: ++id, name: "Bridge Jones's Diary" }, + { id: ++id, name: "Crazy Rich Asians" }, + { id: ++id, name: "Dave" }, + { id: ++id, name: "Defending Your Life" }, + { id: ++id, name: "Dirty Dancing" }, + { id: ++id, name: "Love Actually" }, + { id: ++id, name: "Midnight In Paris" }, + { id: ++id, name: "Moonstruck" }, + { id: ++id, name: "Notting Hill" }, + { id: ++id, name: "Pretty Woman" }, + { id: ++id, name: "Princess Bride" }, + { id: ++id, name: "Say Anything" }, + { id: ++id, name: "Sleepless In Seattle" }, + { id: ++id, name: "Tommy Boy" }, + { id: ++id, name: "Wedding Singer" }, + { id: ++id, name: "What About Bob" }, + { id: ++id, name: "When Harry Met Sally" }, + { id: ++id, name: "You've Got Mail" }, + ]; + + // ------------------------------------------------------------------------------- // + // ------------------------------------------------------------------------------- // + + return { + // Public properties. + listOne: null, + listTwo: null, + similarity: "100", + titles, + // Private properties. + distance: 0, + sortableOne: null, + sortableTwo: null, + coreIds: null, + // Life-cycle methods. + init, + // Public methods. + handlePopstate, + randomKeyBecauseXForBug, + resetList, + syncRight, + // Private methods. + compareRankings, + getSortedListsFromDom, + loadFromhash, + persistToHash, + }; + + // --- + // LIFE-CYCLE METHODS. + // --- + + /** + * I initialize the component. + */ + function init() { + + // Keep track of the core list of title IDs so that we can use this in the default + // list assignment as well as in the hash parsing. + this.coreIds = this.titles.map( title => title.id ); + // By default, set each list of title IDs to start with the core list. This will + // then be overridden, as needed, by the URL fragment. + this.listOne = this.coreIds.slice(); + this.listTwo = this.coreIds.slice(); + + // Override the lists (if possible) using the URL fragment. + this.loadFromhash(); + this.compareRankings(); + + var sortableOptions = { + direction: "vertical", + swapThreshold: 0.8, // Overlap required to trigger move (0...1). + animation: 0, + onUpdate: ( event ) => { + this.getSortedListsFromDom( event ); + this.compareRankings(); + this.persistToHash(); + } + }; + + // Enable sorting on the DOM lists. + this.sortableOne = new Sortable( this.$refs.listOneNode, sortableOptions ); + this.sortableTwo = new Sortable( this.$refs.listTwoNode, sortableOptions ); + + } + + // --- + // PUBLIC METHODS. + // --- + + /** + * I handle the history popState event, and sync the URL state down into the app state. + */ + function handlePopstate() { + + this.loadFromhash(); + this.compareRankings(); + + } + + + /** + * There's a BUG(ish) in the way that X-For handles DOM-initiated re-sorting. As such, + * we are working around it by providing a random key to the DOM id. This will create + * DOM churn; but, it is what it is. + * + * Read more: https://github.com/alpinejs/alpine/discussions/4157 + */ + function randomKeyBecauseXForBug() { + + return Math.floor( Math.random() * 999999 ); + + } + + + /** + * I reset the selected list to the original titles order. + */ + function resetList( whichList ) { + + this[ whichList ] = this.coreIds.slice(); + this.compareRankings(); + this.persistToHash(); + + } + + + /** + * I sync the first list into the second list to give the user a matching base from + * which to start customizing the sort. + */ + function syncRight() { + + this.listTwo = this.listOne.slice(); + this.compareRankings(); + this.persistToHash(); + + } + + // --- + // PRIVATE METHODS. + // --- + + /** + * I compute the distance and similarity of the two lists. + */ + function compareRankings() { + + // Calculate the Kendall Tau Distance between the two list of titles. + this.distance = kendallTauDistance( this.listOne, this.listTwo ); + // Convert the Kendall Tau Distance to something more human friendly. + this.similarity = ( ( 1 - this.distance ) * 100 ).toFixed( 1 ); + + } + + + /** + * I read the list of IDs back out of the DOM (via the Sortable proxy). + */ + function getSortedListsFromDom( event ) { + + // Our list DOM is rendered by Alpine.js (via the x-for directive); but, we're + // allowing the list to be updated arbitrarily by Sortable.js. Once the sorting + // operation is done, we need to read the state of the DOM back into the state of + // Alpine.js so that the x-for attribute remains in a predictable state. + // -- + // Note: there's a bug(ish) in the [x-for] attribute in the way it keeps track of + // keys internally. We get around this by assigning random keys in the DOM. + this.listOne = this.sortableOne.toArray(); + this.listTwo = this.sortableTwo.toArray(); + + } + + + /** + * I override the rankings if the values are available in the URL fragment. + */ + function loadFromhash() { + + var rankings = location.hash + .slice( 1 ) + // Split fragment into two comma-delimited lists. + .split( ":" ) + // Map each comma-delimited list onto a set of IDs. + .map( + ( rankList ) => { + + return rankList + // Split each comma-delimited list into a set of IDs. + .split( "," ) + // Make sure we didn't have any incorrect mappings. + .filter( id => titles[ id ] ) + ; + + } + ) + ; + + // We've already set up default values for the lists to use the core set of IDs. + // We only want to now override these lists if they have the same length as the + // defaults. This way, we don't have to do any more validation on the URL. + + if ( rankings[ 0 ]?.length === this.listOne.length ) { + + this.listOne = rankings[ 0 ]; + + } + + if ( rankings[ 1 ]?.length === this.listTwo.length ) { + + this.listTwo = rankings[ 1 ]; + + } + + } + + + /** + * I persist the current ranks to the URL fragment. + */ + function persistToHash() { + + var flattenedOne = this.listOne.join( "," ); + var flattenedTwo = this.listTwo.join( "," ); + var flattenedCore = this.coreIds.join( "," ); + + // Vanity: if either of the lists matches the original list of titles, just omit + // it from the URL. This has no functional bearing - it just makes the URL look a + // little bit nicer. + if ( flattenedOne === flattenedCore ) flattenedOne = ""; + if ( flattenedTwo === flattenedCore ) flattenedTwo = ""; + + document.title = `Lists are a ${ this.similarity }% match!`; + history.pushState( {}, null, `#${ flattenedOne }:${ flattenedTwo }` ); + + } + + // --- + // HELPER METHODS (ie, pure functions, not on THIS scope). + // --- + + /** + * I return an index of the given array in which the value maps to the index of the + * value within the collection. + */ + function arrayReflectIndex( collection ) { + + var index = Object.create( null ); + + collection.forEach( + ( element, i ) => { + + index[ element ] = i; + + } + ); + + return index; + + } + + + /** + * I calculate the Kendall Tau Distance for the two lists. Returns a decimal value + * between 0 (fully identical) and 1 (fully reversed). + */ + function kendallTauDistance( listOne, listTwo ) { + + // We're going to be counting the number of (A,B) pairs that are in a different + // relative order in the two lists. + var size = listOne.length; + var totalPairs = ( size * ( size - 1 ) / 2 ); + var discordantPairs = 0; + + // As we iterate over the FIRST list, we'll need to check the corresponding rank + // of the same items in the SECOND list. To make this efficient, let's calculate + // the index-by-value for all elements in the second list. We don't need to do + // this for the first list since we'll be iterating over the first list in order. + var listTwoIndex = arrayReflectIndex( listTwo ); + + // Iterate over the FIRST list using a nested, forward looking loop. The outer + // loop will iterate over the entirety of the first list. + for ( var a = 0 ; a < ( size - 1 ) ; a++ ) { + + // ... the inner loop only needs to iterate from [a...] since we're looking + // for unique pairs of elements. If the inner loop started from 0, we'd be + // counting the same pairs more than once (since (A,B) and (B,A) are + // considered the same pair for this algorithm). + for ( var b = ( a + 1 ) ; b < size ; b++ ) { + + // Get the elements at the current iteration indices. + var elementA = listOne[ a ]; + var elementB = listOne[ b ]; + // Since our nested loop is always exploring elements in a forward-looking + // order, we know that the ranking of the elements in the first list is + // always -1. We only need to calculate the corresponding rank in the + // second list. + var rankOne = -1; + var rankTwo = Math.sign( listTwoIndex[ elementA ] - listTwoIndex[ elementB ] ); + + if ( rankTwo != rankOne ) { + + discordantPairs++; + + } + + } + + } + + return ( discordantPairs / totalPairs ); + + } + +} diff --git a/demos/movie-rank/index.htm b/demos/movie-rank/index.htm new file mode 100644 index 0000000000..768ace4884 --- /dev/null +++ b/demos/movie-rank/index.htm @@ -0,0 +1,61 @@ + + + + + + + Movie Ranking With Sortable.js And Kendall Tau Distance + + + + + + + + +

+ Movie Ranking With Sortable.js And Kendall Tau Distance +

+ +

+ Check your movie preference compatibility with others: +

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

+ + + +

+ + + diff --git a/demos/pixel-art-alpine/index.htm b/demos/pixel-art-alpine/index.htm new file mode 100644 index 0000000000..39f78a2e49 --- /dev/null +++ b/demos/pixel-art-alpine/index.htm @@ -0,0 +1,113 @@ + + + + + + + Pixel Art With Alpine.js + + + + + + + + +
+ +

+ 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 0000000000..2f11b67bd3 --- /dev/null +++ b/demos/pixel-art-alpine/main.css @@ -0,0 +1,171 @@ +*, +*:before, +*:after { + box-sizing: border-box ; + margin: 0 ; + padding: 0 ; +} + +:root { + --grid-size: 25 ; + --pixel-size: 26px ; +} + +body { + font-family: monospace ; + font-size: 16px ; + line-height: 1.4 ; +} + +main { + align-items: center ; + display: flex ; + flex-direction: column ; + gap: 30px ; + padding: 30px ; + + & h1 { + margin: 0 0 0px 0 ; + + & a { + color: inherit ; + text-decoration: none ; + + &:hover { + color: #ff1493 ; + text-decoration: underline ; + } + } + } +} + +.grid { + background-color: #ffffff ; + box-shadow: 0 0 1px 0 #999999 ; + display: grid ; + grid-template-columns: repeat( var( --grid-size ), 1fr ) ; + height: calc( var( --grid-size ) * var( --pixel-size ) ) ; + user-select: none ; + width: calc( var( --grid-size ) * var( --pixel-size ) ) ; + + & button { + border: none ; + box-shadow: inset 0 0 1px 0 #ffffff ; + height: var( --pixel-size ) ; + width: var( --pixel-size ) ; + } +} + +.palette { + box-shadow: 0 0 1px 0 #999999 ; + display: flex ; + flex-wrap: wrap ; + min-height: 156px ; + user-select: none ; + width: calc( var( --grid-size ) * var( --pixel-size ) ) ; + + & button { + border: none ; + box-shadow: inset 0 0 1px 0 #ffffff ; + flex: 0 0 auto ; + height: var( --pixel-size ) ; + width: var( --pixel-size ) ; + + &:last-child { + flex-grow: 1 ; + } + + &.isSelected { + box-shadow: 0 0 3px 5px #000000 ; + outline: 2px solid #ffffff ; + z-index: 2 ; + } + } +} + +.selected { + display: flex ; + font-size: 20px ; + margin: 0 ; + gap: 30px ; + + & div { + display: flex ; + gap: 10px ; + } + + & dt { + font-weight: 700 ; + margin: 0 ; + } + + & dd { + margin: 0 ; + } +} + +.fillers { + display: flex ; + font-size: 20px ; + gap: 13px ; + + & button { + align-items: center ; + background-color: #333333 ; + border: none ; + border-radius: 4px ; + color: #ffffff ; + display: flex ; + font: inherit ; + gap: 13px ; + padding: 10px 20px ; + } + + & span { + box-shadow: 0 0 1px 1px #ffffff ; + border-radius: 16px ; + height: 16px ; + width: 16px ; + } +} + +.tuggers { + display: flex ; + font-size: 20px ; + gap: 13px ; + + & button { + background-color: #ffffff ; + border: 1px solid #333333 ; + border-radius: 4px ; + color: #333333 ; + font: inherit ; + padding: 10px 20px ; + min-width: 100px ; + } +} + +.tips { + align-items: center ; + display: flex ; + flex-direction: column ; + gap: 15px ; + + & kbd { + background-color: #232323 ; + border-radius: 2px ; + color: #ffffff ; + display: inline-block ; + padding: 0 3px ; + } +} + +.examples { + display: flex ; + gap: 20px ; + + & a { + color: #ff1493 ; + } +} + diff --git a/demos/pixel-art-alpine/main.js b/demos/pixel-art-alpine/main.js new file mode 100644 index 0000000000..d673836486 --- /dev/null +++ b/demos/pixel-art-alpine/main.js @@ -0,0 +1,610 @@ +// Note: this depends on "palette" existing as an external module. +function Demo() { + + return { + // Public properties. + pixels: null, + foregroundSwatch: palette.byName.LightSkyBlue, + backgroundSwatch: palette.byName.Snow, + + // Private properties. + canvasWidth: 0, + canvasHeight: 0, + isDrawing: false, + palette: palette, + + // Life-Cycle methods. + init, + + // Public methods. + changeCanvasBackground, + clearCanvas, + enterPixel, + handleDo, + handleHashchange, + pullCanvasCenter, + pullCanvasDown, + pullCanvasLeft, + pullCanvasRight, + pullCanvasUp, + selectSwatch, + startDrawing, + stopDrawing, + + // Private methods. + hashDecodeState, + hashEncodeState, + matrixNudge, + matrixRead, + matrixWrite, + stateDecodeString, + stateEncodeString, + }; + + // --- + // LIFE-CYCLE METHODS. + // --- + + /** + * I initialize the alpine component. + */ + function init() { + + // Pull grid dimensions from the DOM. + this.canvasWidth = ( +this.$el.dataset.width || 25 ); + this.canvasHeight = ( +this.$el.dataset.height || 25 ); + + // Setup the pixel matrix: a linear set of pixels being rendered in two dimensions + // within the user interface using CSS Grid. + this.pixels = new Array( this.canvasWidth * this.canvasHeight ) + .fill( this.backgroundSwatch ) + ; + + // If the current request is a link to an existing pixel configuration, pull it in + // from the URL fragment. + this.hashDecodeState(); + + } + + // --- + // PUBLIC METHODS. + // --- + + /** + * I change any pixel with the current background color to be the new foreground color, + * then use the new foreground color as the future background color. Basically, this + * rotates the background color pixels only, leaving foreground color pixels in place. + */ + function changeCanvasBackground() { + + this.pixels = this.pixels.map( + ( swatch ) => { + + return ( swatch === this.backgroundSwatch ) + ? this.foregroundSwatch + : swatch + ; + + } + ); + + this.backgroundSwatch = this.foregroundSwatch; + this.hashEncodeState(); + + } + + + /** + * I completely reset the pixel matrix to use one solid color (the selected color). + */ + function clearCanvas() { + + this.backgroundSwatch = this.foregroundSwatch; + this.pixels.fill( this.backgroundSwatch ); + this.hashEncodeState(); + + } + + + /** + * I apply a swatch to the contextual pixel if this is a draw operation. + */ + function enterPixel( event, i ) { + + if ( ! this.isDrawing ) { + + return; + + } + + this.pixels[ i ] = event.altKey + ? this.backgroundSwatch + : this.foregroundSwatch + ; + + } + + + /** + * I attempt to redo / undo a recent change using this history. + * + * Note: these are being handled with a single event handler since Alpine.js doesn't + * limit key-bindings based on modifiers. As such, it's easier to just handle both + * events in a single handler. + */ + function handleDo( event ) { + + // Since all pixel changes are persisted in the hash, we should be able to + // navigate back / forward through all of the changes in the canvas. However, I'm + // not keep track of whether or not the commands are available - I'm just blindly + // invoke the history API and letting the hash play-out. + event.preventDefault(); + + // Redo. + if ( event.shiftKey ) { + + history.go( 1 ); + + // Undo. + } else { + + history.go( -1 ); + + } + + } + + + /** + * I handle the hash change, and push the URL data into the pixel state. + */ + function handleHashchange( event ) { + + this.hashDecodeState(); + + } + + + /** + * I shift the foreground pixels to the center of the canvas. + */ + function pullCanvasCenter() { + + var MAX = 999999; + var colMin = MAX; + var rowMin = MAX; + var colMax = -1; + var rowMax = -1; + var matrix = this.matrixRead(); + + // Iterate over the pixels and try to identify the smallest bounding box around + // the non-background swatch. + matrix.forEach( + ( row, rowIndex ) => { + + row.forEach( + ( pixel, colIndex ) => { + + if ( pixel != this.backgroundSwatch ) { + + colMin = Math.min( colMin, colIndex ); + colMax = Math.max( colMax, colIndex ); + rowMin = Math.min( rowMin, rowIndex ); + rowMax = Math.max( rowMax, rowIndex ); + + } + + } + ); + + } + ); + + // If we found no foreground pixel data, there's nothing else to do. + if ( rowMin === MAX ) { + + return; + + } + + var boxWidth = ( colMax - colMin + 1 ); + var boxHeight = ( rowMax - rowMin + 1 ); + var deltaWidth = ( this.canvasWidth - boxWidth ); + var deltaHeight = ( this.canvasHeight - boxHeight ); + var targetX = Math.floor( deltaWidth / 2 ); + var targetY = Math.floor( deltaHeight / 2 ); + + this.matrixWrite( + this.matrixNudge( + matrix, + ( targetX - colMin ), // Delta columns. + ( targetY - rowMin ) // Delta rows. + ) + ); + this.hashEncodeState(); + + } + + + /** + * I shift the foreground pixels down 1 row on the canvas. + */ + function pullCanvasDown() { + + this.matrixWrite( + this.matrixNudge( + this.matrixRead(), + 0, // Delta columns. + 1 // Delta rows. + ) + ); + this.hashEncodeState(); + + } + + + /** + * I shift the foreground pixels left 1 column on the canvas. + */ + function pullCanvasLeft() { + + this.matrixWrite( + this.matrixNudge( + this.matrixRead(), + -1, // Delta columns. + 0 // Delta rows. + ) + ); + this.hashEncodeState(); + + } + + + /** + * I shift the foreground pixels up 1 row on the canvas. + */ + function pullCanvasUp() { + + this.matrixWrite( + this.matrixNudge( + this.matrixRead(), + 0, // Delta columns. + -1 // Delta rows. + ) + ); + this.hashEncodeState(); + + } + + + /** + * I shift the foreground pixels right 1 column on the canvas. + */ + function pullCanvasRight() { + + this.matrixWrite( + this.matrixNudge( + this.matrixRead(), + 1, // Delta columns. + 0 // Delta rows. + ) + ); + this.hashEncodeState(); + + } + + + /** + * I set the given swatch as the foreground drawing color. + */ + function selectSwatch( swatch ) { + + this.foregroundSwatch = swatch; + + } + + + /** + * I start a drawing operation, filling in the contextual pixel. + */ + function startDrawing( event, i ) { + + // If the mouse event is modified, first sample the pixel for its swatch. + if ( event.metaKey || event.ctrlKey ) { + + this.foregroundSwatch = this.pixels[ i ]; + + } + + this.isDrawing = true; + this.enterPixel( event, i ); + + } + + + /** + * I stop a drawing operation and persist the current pixel state to the URL. + */ + function stopDrawing() { + + if ( ! this.isDrawing ) { + + return; + + } + + this.isDrawing = false; + this.hashEncodeState(); + + } + + // --- + // PRIVATE METHODS. + // --- + + /** + * I decode the canvas state from the URL fragment and use it to set the current pixel + * and color state. + */ + function hashDecodeState() { + + var state = this.stateDecodeString( location.hash.slice( 1 ) ); + + if ( ! state ) { + + return; + + } + + this.foregroundSwatch = state.foregroundSwatch; + this.backgroundSwatch = state.backgroundSwatch; + this.pixels = state.pixels; + + } + + + /** + * I encode the current canvas state into the URL fragment. + */ + function hashEncodeState() { + + history.pushState( null, null, `#${ this.stateEncodeString() }` ); + + } + + + /** + * I nudge the given pixel 2D matrix by the given column and row deltas. New pixels use + * the currently selected background swatch. + */ + function matrixNudge( matrix, colDelta, rowDelta ) { + + // Nudge left. + for ( ; colDelta < 0 ; colDelta++ ) { + + for ( var row of matrix ) { + + row.shift(); + row.push( this.backgroundSwatch ); + + } + + } + + // Nudge right. + for ( ; colDelta > 0 ; colDelta-- ) { + + for ( var row of matrix ) { + + row.pop(); + row.unshift( this.backgroundSwatch ); + + } + + } + + // Nudge up. + for ( ; rowDelta < 0 ; rowDelta++ ) { + + matrix.shift(); + matrix.push( new Array( this.canvasWidth ).fill( this.backgroundSwatch ) ); + + } + + // Nudge down. + for ( ; rowDelta > 0 ; rowDelta-- ) { + + matrix.pop(); + matrix.unshift( new Array( this.canvasWidth ).fill( this.backgroundSwatch ) ); + + } + + return matrix; + + } + + + /** + * I read the current linear pixel state into a 2D matrix. + */ + function matrixRead() { + + var matrix = []; + + for ( var i = 0 ; i < this.canvasHeight ; i++ ) { + + var rowOffset = ( i * this.canvasWidth ); + var rowEnd = ( rowOffset + this.canvasWidth ); + + matrix.push( this.pixels.slice( rowOffset, rowEnd ) ); + + } + + return matrix; + + } + + + /** + * I write the 2D matrix back into the current linear pixel state. + */ + function matrixWrite( matrix ) { + + this.pixels = matrix.flat(); + + } + + + /** + * I parse the given string value back into a state object that contains the foreground + * swatch, the background swatch, and the pixels. + */ + function stateDecodeString( value = "" ) { + + // Every part of the state is represented by either a single Base36 value; or, a + // pair of Base36 values in a ":" delimited list. + var matches = value + .toLowerCase() + .matchAll( /([a-z0-9]+)(:([a-z0-9]+))?/g ) + .toArray() + .map( + ([ $0, $key, $2, $count ]) => { + + return { + key: urlDecodeInt( $key ), + count: urlDecodeInt( $count ) + }; + + } + ) + ; + + // We know that the encoded state will be, at the very smallest, the foreground + // swatch, the background swatch, and then a single run of a solid color. + // Therefore, if we have less than 3 matches, the input is invalid. + if ( matches.length < 3 ) { + + return null; + + } + + // Set up the core state object into which we will parse the input. + var state = { + // First two matches are always the selected swatches. + foregroundSwatch: this.palette.byKey[ matches.shift().key ], + backgroundSwatch: this.palette.byKey[ matches.shift().key ], + pixels: new Array( this.pixels.length ) + }; + + // Blank-out the canvas - we'll fill in pixels next. + state.pixels.fill( state.backgroundSwatch ); + + // As we iterate over the matches, we need to translate the runs into pixel + // offsets. Will use "i" to keep track of the start offset of the next fill. + var i = 0; + + for ( var match of matches ) { + + state.pixels.fill( + this.palette.byKey[ match.key ], + i, + ( i += match.count ) // Warning: incrementing AND consuming. + ); + + } + + return state; + + } + + + /** + * I encode the current pixel art state into a string representation. + */ + function stateEncodeString() { + + // The state will be encoded as a series of "runs". Meaning, each sequence of + // pixels that used the same swatch will be condensed down into the swatch "key" + // followed by the number of repeated pixels (`key`:`count`). If a swatch run is + // only a single pixel, the count can be omitted and will be assumed to be one. + // The first two runs implicitly represent the foreground and background swatches. + var runs = [ + { + key: this.foregroundSwatch.key, + count: 1 + }, + { + key: this.backgroundSwatch.key, + count: 1 + } + ]; + var run = {}; + + for ( var pixel of this.pixels ) { + + // Did we enter a new swatch run? + if ( run.key !== pixel.key ) { + + run = { + key: pixel.key, + count: 0 + }; + runs.push( run ); + + } + + run.count++; + + } + + // Map runs to a list of `key`:`count` pairs. + return runs + .map( + ( run ) => { + + // Single pixel runs will be assumed to be "1" during parsing. As + // such, we can omit the count - keep the URL shorter. + if ( run.count === 1 ) { + + return urlEncodeInt( run.key ); + + } + + return `${ urlEncodeInt( run.key ) }:${ urlEncodeInt( run.count ) }`; + + } + ) + .join( "," ) + ; + + } + + + /** + * In order to create shorter URLs, we're encoding numbers using Base36. This decodes + * the value back into an int. + */ + function urlDecodeInt( value = undefined ) { + + if ( value === undefined ) { + + return 1; + + } + + return parseInt( value, 36 ); + + } + + + /** + * In order to create shorter URLs, we're encoding numbers using Base36. This encodes + * the int value. + */ + function urlEncodeInt( value ) { + + return value.toString( 36 ); + + } + +} diff --git a/demos/pixel-art-alpine/palette.js b/demos/pixel-art-alpine/palette.js new file mode 100644 index 0000000000..89db381304 --- /dev/null +++ b/demos/pixel-art-alpine/palette.js @@ -0,0 +1,180 @@ +var palette = (() => { + + // Colors courtesy of : https://htmlcolorcodes.com/color-names/ + // -- + // IMPORTANT: the first swatch key will be "1" so that "0" can represent transparent. + var key = 0; + var swatches = [ + { name: "IndianRed", hex: "#cd5c5c", key: ++key }, + { name: "LightCoral", hex: "#f08080", key: ++key }, + { name: "Salmon", hex: "#fa8072", key: ++key }, + { name: "DarkSalmon", hex: "#e9967a", key: ++key }, + { name: "LightSalmon", hex: "#ffa07a", key: ++key }, + { name: "Crimson", hex: "#dc143c", key: ++key }, + { name: "Red", hex: "#ff0000", key: ++key }, + { name: "FireBrick", hex: "#b22222", key: ++key }, + { name: "DarkRed", hex: "#8b0000", key: ++key }, + { name: "Pink", hex: "#ffc0cb", key: ++key }, + { name: "LightPink", hex: "#ffb6c1", key: ++key }, + { name: "HotPink", hex: "#ff69b4", key: ++key }, + { name: "DeepPink", hex: "#ff1493", key: ++key }, + { name: "MediumVioletRed", hex: "#c71585", key: ++key }, + { name: "PaleVioletRed", hex: "#db7093", key: ++key }, + { name: "LightSalmon", hex: "#ffa07a", key: ++key }, + { name: "Coral", hex: "#ff7f50", key: ++key }, + { name: "Tomato", hex: "#ff6347", key: ++key }, + { name: "OrangeRed", hex: "#ff4500", key: ++key }, + { name: "DarkOrange", hex: "#ff8c00", key: ++key }, + { name: "Orange", hex: "#ffa500", key: ++key }, + { name: "Gold", hex: "#ffd700", key: ++key }, + { name: "Yellow", hex: "#ffff00", key: ++key }, + { name: "LightYellow", hex: "#ffffe0", key: ++key }, + { name: "LemonChiffon", hex: "#fffacd", key: ++key }, + { name: "LightGoldenrodYellow", hex: "#fafad2", key: ++key }, + { name: "PapayaWhip", hex: "#ffefd5", key: ++key }, + { name: "Moccasin", hex: "#ffe4b5", key: ++key }, + { name: "PeachPuff", hex: "#ffdab9", key: ++key }, + { name: "PaleGoldenrod", hex: "#eee8aa", key: ++key }, + { name: "Khaki", hex: "#f0e68c", key: ++key }, + { name: "DarkKhaki", hex: "#bdb76b", key: ++key }, + { name: "Lavender", hex: "#e6e6fa", key: ++key }, + { name: "Thistle", hex: "#d8bfd8", key: ++key }, + { name: "Plum", hex: "#dda0dd", key: ++key }, + { name: "Violet", hex: "#ee82ee", key: ++key }, + { name: "Orchid", hex: "#da70d6", key: ++key }, + { name: "Fuchsia", hex: "#ff00ff", key: ++key }, + { name: "Magenta", hex: "#ff00ff", key: ++key }, + { name: "MediumOrchid", hex: "#ba55d3", key: ++key }, + { name: "MediumPurple", hex: "#9370db", key: ++key }, + { name: "RebeccaPurple", hex: "#663399", key: ++key }, + { name: "BlueViolet", hex: "#8a2be2", key: ++key }, + { name: "DarkViolet", hex: "#9400d3", key: ++key }, + { name: "DarkOrchid", hex: "#9932cc", key: ++key }, + { name: "DarkMagenta", hex: "#8b008b", key: ++key }, + { name: "Purple", hex: "#800080", key: ++key }, + { name: "Indigo", hex: "#4b0082", key: ++key }, + { name: "SlateBlue", hex: "#6a5acd", key: ++key }, + { name: "DarkSlateBlue", hex: "#483d8b", key: ++key }, + { name: "MediumSlateBlue", hex: "#7b68ee", key: ++key }, + { name: "GreenYellow", hex: "#adff2f", key: ++key }, + { name: "Chartreuse", hex: "#7fff00", key: ++key }, + { name: "LawnGreen", hex: "#7cfc00", key: ++key }, + { name: "Lime", hex: "#00ff00", key: ++key }, + { name: "LimeGreen", hex: "#32cd32", key: ++key }, + { name: "PaleGreen", hex: "#98fb98", key: ++key }, + { name: "LightGreen", hex: "#90ee90", key: ++key }, + { name: "MediumSpringGreen", hex: "#00fa9a", key: ++key }, + { name: "SpringGreen", hex: "#00ff7f", key: ++key }, + { name: "MediumSeaGreen", hex: "#3cb371", key: ++key }, + { name: "SeaGreen", hex: "#2e8b57", key: ++key }, + { name: "ForestGreen", hex: "#228b22", key: ++key }, + { name: "Green", hex: "#008000", key: ++key }, + { name: "DarkGreen", hex: "#006400", key: ++key }, + { name: "YellowGreen", hex: "#9acd32", key: ++key }, + { name: "OliveDrab", hex: "#6b8e23", key: ++key }, + { name: "Olive", hex: "#808000", key: ++key }, + { name: "DarkOliveGreen", hex: "#556b2f", key: ++key }, + { name: "MediumAquamarine", hex: "#66cdaa", key: ++key }, + { name: "DarkSeaGreen", hex: "#8fbc8b", key: ++key }, + { name: "LightSeaGreen", hex: "#20b2aa", key: ++key }, + { name: "DarkCyan", hex: "#008b8b", key: ++key }, + { name: "Teal", hex: "#008080", key: ++key }, + { name: "Aqua", hex: "#00ffff", key: ++key }, + { name: "Cyan", hex: "#00ffff", key: ++key }, + { name: "LightCyan", hex: "#e0ffff", key: ++key }, + { name: "PaleTurquoise", hex: "#afeeee", key: ++key }, + { name: "Aquamarine", hex: "#7fffd4", key: ++key }, + { name: "Turquoise", hex: "#40e0d0", key: ++key }, + { name: "MediumTurquoise", hex: "#48d1cc", key: ++key }, + { name: "DarkTurquoise", hex: "#00ced1", key: ++key }, + { name: "CadetBlue", hex: "#5f9ea0", key: ++key }, + { name: "SteelBlue", hex: "#4682b4", key: ++key }, + { name: "LightSteelBlue", hex: "#b0c4de", key: ++key }, + { name: "PowderBlue", hex: "#b0e0e6", key: ++key }, + { name: "LightBlue", hex: "#add8e6", key: ++key }, + { name: "SkyBlue", hex: "#87ceeb", key: ++key }, + { name: "LightSkyBlue", hex: "#87cefa", key: ++key }, + { name: "DeepSkyBlue", hex: "#00bfff", key: ++key }, + { name: "DodgerBlue", hex: "#1e90ff", key: ++key }, + { name: "CornflowerBlue", hex: "#6495ed", key: ++key }, + { name: "MediumSlateBlue", hex: "#7b68ee", key: ++key }, + { name: "RoyalBlue", hex: "#4169e1", key: ++key }, + { name: "Blue", hex: "#0000ff", key: ++key }, + { name: "MediumBlue", hex: "#0000cd", key: ++key }, + { name: "DarkBlue", hex: "#00008b", key: ++key }, + { name: "Navy", hex: "#000080", key: ++key }, + { name: "MidnightBlue", hex: "#191970", key: ++key }, + { name: "Cornsilk", hex: "#fff8dc", key: ++key }, + { name: "BlanchedAlmond", hex: "#ffebcd", key: ++key }, + { name: "Bisque", hex: "#ffe4c4", key: ++key }, + { name: "NavajoWhite", hex: "#ffdead", key: ++key }, + { name: "Wheat", hex: "#f5deb3", key: ++key }, + { name: "BurlyWood", hex: "#deb887", key: ++key }, + { name: "Tan", hex: "#d2b48c", key: ++key }, + { name: "RosyBrown", hex: "#bc8f8f", key: ++key }, + { name: "SandyBrown", hex: "#f4a460", key: ++key }, + { name: "Goldenrod", hex: "#daa520", key: ++key }, + { name: "DarkGoldenrod", hex: "#b8860b", key: ++key }, + { name: "Peru", hex: "#cd853f", key: ++key }, + { name: "Chocolate", hex: "#d2691e", key: ++key }, + { name: "SaddleBrown", hex: "#8b4513", key: ++key }, + { name: "Sienna", hex: "#a0522d", key: ++key }, + { name: "Brown", hex: "#a52a2a", key: ++key }, + { name: "Maroon", hex: "#800000", key: ++key }, + { name: "Snow", hex: "#fffafa", key: ++key }, + { name: "HoneyDew", hex: "#f0fff0", key: ++key }, + { name: "MintCream", hex: "#f5fffa", key: ++key }, + { name: "Azure", hex: "#f0ffff", key: ++key }, + { name: "AliceBlue", hex: "#f0f8ff", key: ++key }, + { name: "GhostWhite", hex: "#f8f8ff", key: ++key }, + { name: "WhiteSmoke", hex: "#f5f5f5", key: ++key }, + { name: "SeaShell", hex: "#fff5ee", key: ++key }, + { name: "Beige", hex: "#f5f5dc", key: ++key }, + { name: "OldLace", hex: "#fdf5e6", key: ++key }, + { name: "FloralWhite", hex: "#fffaf0", key: ++key }, + { name: "Ivory", hex: "#fffff0", key: ++key }, + { name: "AntiqueWhite", hex: "#faebd7", key: ++key }, + { name: "Linen", hex: "#faf0e6", key: ++key }, + { name: "LavenderBlush", hex: "#fff0f5", key: ++key }, + { name: "MistyRose", hex: "#ffe4e1", key: ++key }, + { name: "Gainsboro", hex: "#dcdcdc", key: ++key }, + { name: "LightGray", hex: "#d3d3d3", key: ++key }, + { name: "Silver", hex: "#c0c0c0", key: ++key }, + { name: "DarkGray", hex: "#a9a9a9", key: ++key }, + { name: "Gray", hex: "#808080", key: ++key }, + { name: "DimGray", hex: "#696969", key: ++key }, + { name: "LightSlateGray", hex: "#778899", key: ++key }, + { name: "SlateGray", hex: "#708090", key: ++key }, + { name: "DarkSlateGray", hex: "#2f4f4f", key: ++key }, + { name: "Black", hex: "#000000", key: ++key }, + { name: "White", hex: "#ffffff", key: ++key } + ]; + + return { + swatches, + byName: indexBy( "name" ), + byHex: indexBy( "hex" ), + byKey: indexBy( "key" ) + }; + + // ------------------------------------------------------------------------------- // + // ------------------------------------------------------------------------------- // + + /** + * I index the swatches collection by the given property. + */ + function indexBy( property ) { + + var index = {}; + + for ( var swatch of swatches ) { + + index[ swatch[ property ] ] = swatch; + + } + + return index; + + } + +})(); diff --git a/demos/plus-minus-range/index.htm b/demos/plus-minus-range/index.htm new file mode 100644 index 0000000000..5ea431c51d --- /dev/null +++ b/demos/plus-minus-range/index.htm @@ -0,0 +1,80 @@ + + + + + + Parsing Plus-Minus Ranges In JavaScript + + + + + +

+ Parsing Plus-Minus (±) Ranges In JavaScript +

+ +
+ + + +
+ + + + + diff --git a/demos/plus-minus-range/main.css b/demos/plus-minus-range/main.css new file mode 100644 index 0000000000..b3a260c6fc --- /dev/null +++ b/demos/plus-minus-range/main.css @@ -0,0 +1,95 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} + +table { + border: 1px solid #333333 ; + border-collapse: collapse ; + width: 100% ; + + & :where(th, td) { + border: 1px solid #333333 ; + padding: 5px 10px ; + + &:not([align]) { + text-align: left ; + } + } +} + +tbody tr:hover { + background-color: #e7f9ff ; +} + + +form { + display: flex ; + gap: 13px ; + max-width: 670px ; +} + +form { + & input:nth-child(1) { + flex: 1 0 auto ; + width: 250px ; + } + & input { + flex: 1 0 auto ; + width: 100px ; + } +} diff --git a/demos/row-linker/index.htm b/demos/row-linker/index.htm new file mode 100644 index 0000000000..988443058b --- /dev/null +++ b/demos/row-linker/index.htm @@ -0,0 +1,233 @@ + + + + + + Table Row Linker Directive In Alpine.js + + + + + +

+ Table Row Linker Directive In Alpine.js +

+ + + + + + + + + + + + + +
NameInfoCategoryActions
+ + + + + + diff --git a/demos/row-linker/main.css b/demos/row-linker/main.css new file mode 100644 index 0000000000..208dea2172 --- /dev/null +++ b/demos/row-linker/main.css @@ -0,0 +1,82 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} + +table { + border: 1px solid #333333 ; + border-collapse: collapse ; + width: 100% ; + + & :where(th, td) { + border: 1px solid #333333 ; + padding: 5px 10px ; + + &:not([align]) { + text-align: left ; + } + } +} + +tbody tr:hover { + background-color: #e7f9ff ; +} + +a.active { + background-color: #333333 ; + color: #fafafa ; +} diff --git a/demos/scope-pseudo-class/index.htm b/demos/scope-pseudo-class/index.htm new file mode 100644 index 0000000000..335f176315 --- /dev/null +++ b/demos/scope-pseudo-class/index.htm @@ -0,0 +1,47 @@ + + + + + +

+ Using :scope To Identify The Host Element In A CSS Selector +

+ +

+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +

+ + + + + \ No newline at end of file diff --git a/demos/scope-pseudo-class/main.css b/demos/scope-pseudo-class/main.css new file mode 100644 index 0000000000..ec6752de30 --- /dev/null +++ b/demos/scope-pseudo-class/main.css @@ -0,0 +1,29 @@ + +html { + box-sizing: border-box ; +} +html *, +html *:before, +html *:after { + box-sizing: inherit ; +} + +body { + font-family: monospace ; + font-size: 20px ; +} + +p { + display: flex ; + font-size: 30px ; + gap: 10px ; +} +p span { + background-color: #f0f0f0 ; + border-radius: 5px ; + padding: 3px 15px ; +} +p:hover { + outline: 2px solid cyan ; + outline-offset: 3px ; +} \ No newline at end of file diff --git a/demos/scroll-to-details/index.htm b/demos/scroll-to-details/index.htm new file mode 100644 index 0000000000..6a4925888c --- /dev/null +++ b/demos/scroll-to-details/index.htm @@ -0,0 +1,87 @@ + + + + + + Linking To A Disclosure (Details) Element + + + + + +

+ Linking To A Disclosure (Details) Element +

+ + + + +
+ + Markdown Syntax + +
+ You can use limited Markdown syntax in your content. +
+
+ +
+ + Keyboard Shortcuts + +
+ You can use the following keyboard shortcuts in the application. +
+
+ +
+ + Privacy Policy + +
+ All your data is belong to us! +
+
+ + + + + diff --git a/demos/scroll-to-details/main.css b/demos/scroll-to-details/main.css new file mode 100644 index 0000000000..6fb08f51a0 --- /dev/null +++ b/demos/scroll-to-details/main.css @@ -0,0 +1,87 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-visible { + animation-duration: 200ms ; + animation-fill-mode: forwards ; + animation-iteration-count: 1 ; + animation-name: outlineEnter ; + animation-timing-function: ease-out ; + outline-color: hotpink ; + outline-offset: 4px ; + /* outline-style: solid ; */ + outline-width: 2px ; + } +} + +@keyframes outlineEnter { + from { + outline-offset: 8px ; + } + to { + outline-offset: 4px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} + +button { + padding: 5px 15px ; +} + +a { + color: red ; +} + +details { + border: 1px solid #cccccc ; + margin-block-start: 10rem ; + padding: 10px ; + + &[ open ] { + background-color: #f0f0f0 ; + } + + & > summary { + cursor: pointer ; + scroll-margin-block-start: 35px ; + + /* + By default, the browser doesn't appear to give an outline to the summary + element. As such, I'm explicitly giving it an outline when focused. + */ + &:focus, + &:focus-visible { + outline-style: solid ; + } + } + + & > section { + margin: 10px 0 0 0 ; + } +} diff --git a/demos/select-option-dataset/index.htm b/demos/select-option-dataset/index.htm new file mode 100644 index 0000000000..d69d7c7b54 --- /dev/null +++ b/demos/select-option-dataset/index.htm @@ -0,0 +1,65 @@ + + + + + + Storing Metadata On Select Option Elements + + + + + +

+ Storing Metadata On Select Option Elements +

+ + + + + + + diff --git a/demos/select-option-dataset/main.css b/demos/select-option-dataset/main.css new file mode 100644 index 0000000000..6dbca334cf --- /dev/null +++ b/demos/select-option-dataset/main.css @@ -0,0 +1,36 @@ + +:where( html ) { + box-sizing: border-box ; + + & *, + & *:before, + & *:after { + box-sizing: inherit ; + } +} + +:where( * ) { + &:focus, + &:focus-within { + outline-color: hotpink ; + outline-offset: 4px ; + outline-width: 2px ; + } +} + +body { + font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ; + font-size: 18px ; + line-height: 1.4 ; +} + +button, +input:where([type="text"]), +select, +textarea { + color: inherit ; + font-family: inherit ; + font-size: 20px ; + line-height: inherit ; + padding: 5px 10px ; +} diff --git a/demos/undefined-comparison/index.htm b/demos/undefined-comparison/index.htm new file mode 100644 index 0000000000..009474b4f1 --- /dev/null +++ b/demos/undefined-comparison/index.htm @@ -0,0 +1,61 @@ + + + + + + + Comparing Undefined Values In JavaScript + + + + +

+ Comparing Undefined Values In JavaScript +

+ +

+ See console for results. +

+ + + + + diff --git a/vendor/sortable/1.15.6/sortable-1.15.6.min.js b/vendor/sortable/1.15.6/sortable-1.15.6.min.js new file mode 100644 index 0000000000..95423a6491 --- /dev/null +++ b/vendor/sortable/1.15.6/sortable-1.15.6.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY