From b7113fbb7be71d765f10cf3fb0c293db15b68ec1 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Thu, 17 Apr 2025 06:02:52 -0400 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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 ; + } +}