Neodigm 55 Low Code UX micro-library
UX JavaScript

Neodigm 55 Low Code UX micro-library

Scott C. Krause | Friday, Nov 25, 2022



POPUPS

Neodigm 55 is different. It's an eclectic UX micro-library. The lightweight components come together in a unique way that makes your website playful, fun, and bounce-proof.

It's low-code, performant, responsive, and open source. Because it’s so easy to get started it is the perfect solution for quick landing pages, business accelerators, and event sites.

Powerful Popups 💡

BUTTONS

Enhanced CTA Buttons command attention and have a mind of their own. They drive conversions with ambient and interactive animation.

The visual prominence leaves no doubt that this is the button that you must click.

Enchanted Animation 🍭

No JavaScript ⚡

Neodigm 55 Enchanted CTA is “declarative”. That means that you simply add a tag to your existing HTML. No JavaScript required. You can use JavaScript if you want however it's not necessary.

Enchanted CTA Button Usage 💡

🔸   Audio feedback and scrolling animation on hover and click

🔸   Ambient animation makes it clear that this is the button to click

🔸   Random ambient animation cycles, focus user attention on the next action

🔸   Adobe Analytics / Google AnalyticsTrackable

🔸   May contain a Marquee component for verbose text

🔸   May be Disabled

For Example 👁️
<a id="js-open-pup--sm" href="#js-open-pup--sm"
target="_blank" rel="noopener" tabindex="0" aria-haspopup="true"
data-n55-enchanted-cta
data-n55-enchanted-cta-alt="Text|Alternate"
data-n55-enchanted-cta-ambient="emit"
data-n55-enchanted-cta-dont-touch="false"
data-n55-enchanted-cta-shape-off="false"
data-n55-enchanted-cta-hover-an="true"
data-n55-enchanted-cta-size="medium"
data-n55-flash-theme="marcom"
data-n55-sodapop-id="myPopup--sm"
data-n55-theme="night"
data-n55-toast="Toast Message"
data-n55-tulip='{"msg":" Tooltip ","mrq":false,"tmpt":"","theme":"brand","size":"small","position":"top","icon":""}'
data-n55-wired4sound-click="vibrate"
data-n55-typeon-click='{"q1st": ".readampm__caption", "msg":"Hi|How are you?|Well, I hope.","mode":"RANDOM|LOOP|OFF","uniqueDelay":68}'
><span data-n55-wired4sound-hover="3">Open Popup ⭐ Small</span><span>Toast Msg ⭐ Flash</span>
</a>
Copy CodeClipboard content_copy


QUICKSTART

Simply copy the two lines of code and paste into the head of any HTML page that you wish to enchant. It's that easy.


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/arcanus55/neodigm55@gh-pages/dist/neodigm55_v3_0.min.css"></link>
<script src="https://cdn.jsdelivr.net/gh/arcanus55/neodigm55@gh-pages/dist/neodigm55_v3_0.min.js"></script>

TOAST

Neodigm Toast is a super convenient way to show a quick message without disrupting the user’s flow. It just pops from the bottom of the screen, displays a message, and then animates off screen.

Its unobtrusive nature makes it perfect for dynamic messages that need to be seen but do not need to occupy precious screen real-estate.

No JavaScript ⚡

Neodigm 55 Toast is “declarative”. That means that you simply add a tag to your existing HTML. No JavaScript required. You can use JavaScript if you want however it's not necessary.

Toast Usage 💡

Neodigm Toast messages are queued, meaning they will be displayed for 6 seconds in the sequence in which they were called; Allowing adequate time for each message to be read and understood.

On a mobile device a Toast message will cause a slight vibration to call attention to the message.

A Toast message may contain two lines of text. The pipe character is used to break text onto multiple lines.

For Example 👁️

Simply add a tag to your button:

<button data-n55-toast="Hello World Beginner">Beginner</button>

Or call the Toast with JavaScript:

<button onClick="neodigmToast.q('Hello World|Advanced','primary')">Advanced</button>



SOUND FX

Generative Audio - A subtle sound can be the unique sensual differentiator that sets your website apart.



For Example 👁️

Simply add a tag to your button:

Or play the Sound / Vibrate with JavaScript:


    






    
 
        

Neodigm Popup (Large)

Hi, I am a simple popup. I stay fixed when the page scrolls. I stay focused because the background is blurred. I size myself automatically to fit your content. I am easy to open and easy to close. I can be made full-screen and modal. I look fantastic on a phone!

I can be added to your website without knowledge of JavaScript.

No JavaScript 💡

Neodigm 55 Popups are “declarative”. That means that you simply add a tag to your existing HTML. No JavaScript required. You can use JavaScript if you want however it's not necessary.

For Example 👁️

Simply add a tag to your button:

<button data-n55-sodapop-id="myPopup_id"
aria-haspopup="true">Try Me</button>

Or call the Popup with JavaScript:

<button onClick="neodigmSodaPop.autoOpen('myPopup_id')"
aria-haspopup="true">Try Me</button>


low-code open source devtoys wasm pwa infographic shopify htmx

See the Wiki Recipes for easy, low-code examples.


PARALLAX

<neodigm-parallax data-n55-parallax="PATH TO YOUR BACKGROUND IMAGE"
data-n55-parallax-size="small">
    <section>
        <aside></aside>
    </section>
</neodigm-parallax> 

Parallax scrolling is a web design technique in which the website background remains fixed while the foreground moves. This results in a 3D effect as visitors scroll down the site, adding a sense of depth and creating a more immersive browsing experience. A Visual-spatial experience, this scroll effect differentiates from linear top-down sites.



MARQUEE


            

The Marquee display is a powerful and distinct way to draw attention to important text.

<neodigm-marquee data-n55-marquee-text=" YOUR SCROLLING TEXT HERE "
data-n55-marquee-size="small">
<pre data-n55-theme="warning"></pre>
</neodigm-marquee>

KPI

The Key Performance Indicator is a strong and memorable way to communicate important numbers. It visually communicates a clear and actionable summation of key metrics.

16%
YTD Sales Target
55%
YTD Sales Target
76%
YTD Sales Target
100%
YTD Sales Target

                
<neodigm-kpi id="kpi-ytd"
data-n55-kpi
data-n55-tulip='{"msg":"Year to Date","size":"medium","theme":"primary"}'
data-n55-kpi-percent="true"
data-n55-kpi-dont-touch="false"
data-n55-kpi-shape-off="false"
data-n55-kpi-size="large"
data-n55-flash-theme="night"
data-n55-theme="primary"
data-n55-toast="You've reached 100% of your|YTD sales goal"
data-n55-wired4sound-click="5"
><h5>100<span>%</span></h5><h6>YTD Sales Target</h6>
</neodigm-kpi>


            

The Neodigm Carousel is a smooth horizontal content discovery experience. Animated content pages slide into view from the left or right.

A great UX pattern for simplified content navigation, progressive storytelling, or linear questionnaires.

It automatically formats your content to fit inside its scrollable region. It is aware of external events such as browser resize or device orientation change, and adjusts accordingly.

The BEST feature of the carousel is how easy it is to configure. Easily add a simple tag to any button to control the carousel navigation. For example, you could create Next / Previous buttons that would advance the carousel without JavaScript.

Carousel usage events are easily tracked with Adobe Analytics / Google Analytics.

You can put a carousel inside of another carousel or inside of a Popup (soda pop).

You can even synchronize more than one carousel instance. This is a good approach if you want to illustrate the isomorphic properties of your data / content.

Advanced: The carousel life-cycle hooks allow you to fire logic whenever any carousel moves, whenever a particular instance of the carousel moves, or whenever a particular page within a particular carousel instance becomes visible.

This is useful if you need to re-hydrate content from an external API each time the carousel is accessed.

<neodigm-carousel id="js-carousel-inpup">
    <section>
        <section data-n55-carousel-page-name="PageOne">
            <h6>Page Tab Region 1</h6>
        </section>
        <section data-n55-carousel-page-name="PageTwo">
            <h6>Page Tab Region 2</h6>
        </section>
    </section>
 </neodigm-carousel>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "next"}'>Next</a>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "prev"}'>Prev</a>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "random"}'>Random</a>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "1"}'>Page One</a>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "PageTwo"}'>Page Two</a>
 <a data-n55-carousel-nav='{"id": "js-carousel-inpup", "nav": "resize"}'>Resize</a>

ACT ICON

The Neodigm Act Icon component is a succinct and iconic pictograph action button. One simple icon is sufficient to convey its purpose. Less is more in this case. No worries, when combined with a tooltip, the ramifications of clicking this little action icon become crystal clear.

Web applications typically have actions that can be conceptually categorized by their purpose and scope. For example; First, Previous, Next, Last (pagination) or Create, Read, Edit, and Delete (CRED).


skip_previous navigate_before navigate_next skip_next

person_add database edit delete

The ActIcon component (Action Icon) is a simple round icon that may be themed, disabled, and supports light / dark mode.

They are useful for quick actions such as CRED (Create, Read, Edit, Delete) or page navigation (First, Previous, Next, Last).

The experience is made discoverable when used in conjunction with tooltips (Neodigm Tulips).


            
<neodigm-acticon data-n55-theme="brand" data-n55-size="xsmall" data-n55-tulip="Search"><neodigm-icon class="material-symbols-outlined">search</neodigm-icon></neodigm-acticon>


POPTART


            
<neodigm-template id="js-myPoptart--id" data-n55-poptart='{"x": 176, "y": 176, "z": 176, "h": 176, "w": 176, "position":"bottom", "arrow": false, "offset":{"h": 0, "l":0, "t":0, "w":0}, "dismiss":"hover"}'>
    YOUR CONTENT HERE
</neodigm-template>

<neodigm-acticon data-n55-theme="ghost" data-n55-size="medium" data-n55-wired4sound-click='5'
data-n55-tulip='My Poptart' data-n55-poptart-click="js-myPoptart--id"
><neodigm-icon class="material-symbols-outlined" data-n55-wired4sound-hover='3'>movie</neodigm-icon></neodigm-acticon>

A Poptart displays content in a scrollable region on top of your page. It can be dismissed by simply tapping or hovering anywhere outside of the Poptart. It can be set to open on hover or click of any button without JavaScript. Poptart will automatically position and size itself, or you can tell it exactly where to display.

Poptart is easy to set-up, intuitive to use, and looks fantastic on a phone.

Poptart supports Dark & Light mode (background & border color).

Poptart supports Adobe analytics & Google analytics dataLayer integration.


A Poptart's width and coordinates may be conveniently inherited from it's calling button. Its height can be automatically derived from its content, making it flexible and easy to maintain.

For example; If your content changes the Poptart definition will not need to be updated because it will automatically adjust to a new theme or additional content.



Star ⭐ on Github⭐ ⭐ ⭐ ⭐ ⭐ Advanced RecipesLow Code Solutions Progressive Web App

HTML Form Validation Patterns

Emerging Tech

HTML Form Validation Patterns
Curated JavaScript Form Validation Content

2023-10-18

HTML Over the Wire

Emerging Tech

HTML Over the Wire
Curated HTMX & Alpine.js Knowledge-base

2023-08-05

Capacitor WASM Custom Plugins

Emerging Tech

Capacitor WASM Custom Plugins
Ionic Curated Capacitor Links

2023-08-05

Neodigm 55 Low Code UX micro-library

Emerging Tech

Neodigm 55 Low Code UX micro-library
Popups, Toast, Parallax, and SFX

2022-11-25

UX Usability Heuristic Evaluation

Emerging Tech

UX Usability Heuristic Evaluation
HE Heuristic Evaluation

2022-10-19

New Macbook Setup for Developers

Emerging Tech

New Macbook Setup for Developers
New Macbook Config for Devs 2023

2022-08-24

WebAssembly WASM

Emerging Tech

WebAssembly WASM
In-depth Curated WebAssembly Links

2022-08-05

Curated PWA Links

Emerging Tech

Curated PWA Links
Indispensable Curated PWA Links

2022-07-09

Curated GA4 Links

Emerging Tech

Curated GA4 Links
Indispensable Curated Google Analytics 4 Links

2022-06-17

Curated SFDC Qlik Tableau Links

Emerging Tech

Curated SFDC Qlik Tableau Links
Curated Salesforce Qlik Tableau Links

2022-03-25

Curated LWC Links

Emerging Tech

Curated LWC Links
Indispensable Curated LWC Links

2021-08-09

Curated TypeScript Vue.js

Emerging Tech

Curated TypeScript Vue.js
Indispensable Curated Vue TypeScript

2021-07-24

Creative 3D animation resources

Emerging Tech

Creative 3D animation resources
Indispensable Curated Creative Links

2021-06-25

Transition to TypeScript

Emerging Tech

Transition to TypeScript
Indispensable Curated TypeScript Links

2021-06-05

The Clandestine Dead Drop

Emerging Tech

The Clandestine Dead Drop
The Ironclad Clandestine Dead Drop

2021-05-31

Curated Blogfolios Links

Emerging Tech

Curated Blogfolios Links
Personal Websites

2021-03-15

Curated JavaScript Links

Emerging Tech

Curated JavaScript Links
Indispensable Curated JavaScript Links

2021-03-12

Curated Emerging Tech Links

Emerging Tech

Curated Emerging Tech Links
Indispensable Curated Tech Links

2021-03-04

Cytoscape Skills Data Visualization

Emerging Tech

Cytoscape Skills Data Visualization
Persuasive Infographics & Data Visualizations

2021-02-20

eCommerce Accessibility A11y

Emerging Tech

eCommerce Accessibility A11y
Accessibility Challenges Unique to eCommerce

2020-12-07

Roll Dice in High-Fidelity 3D

Emerging Tech

Roll Dice in High-Fidelity 3D
Create 3D Dice with Strong Random Entropy

2020-11-02



Flickity Carousel A11y Observer
Flickity Carousel A11y Observer
Observe and listen for changes in the Flickity carousel
// Desc: This patch will observe and listen for changes in the Flickity carousel, and when triggered will remove aria-hidden from the carousel child elements. It will observe every carousel instance that exists on the page. This logic utilizes the mutation observer to watch all carousels for changes. The changes may be user initiated or actuated via autoplay configuration.
// Usage: flickPatch = new FlickPatch( document, ".flickity-slider" ); flickPatch.init();

/* ___ _                                  
  /___\ |__  ___  ___ _ ____   _____ _ __ 
 //  // '_ \/ __|/ _ \ '__\ \ / / _ \ '__|
/ \_//| |_) \__ \  __/ |   \ V /  __/ |   
\___/ |_.__/|___/\___|_|    \_/ \___|_|   👁️👁️ */

 class FlickPatch {  //  Flickity Carousel ARIA-HIDDEN observer
    constructor(_d, _sQ) {
        this._d = _d; this._sQ = _sQ;
        this.aF = []; this.aObs = [];
    }
    init() {  //
        this.aF = Array.from( this._d.querySelectorAll( this._sQ ))
        if( this.aF.length ){
            this.aObs = []
            this.aF.forEach( ( eF )=>{
                const oObs = new MutationObserver( flickPatch.removeAttr );
                oObs.observe( eF, { attributes: true, childList: true, subtree: true } );
                this.aObs.push( oObs )
            })            
        }
        return this;
    }
    removeAttr( aObs ){  //
        if( aObs.length ){
            aObs.forEach( ( elO )=>{
                if( elO?.target ){
                    [ ... elO.target.querySelectorAll( "[aria-hidden='true']" )].forEach( ( eH )=>{
                        eH.removeAttribute("aria-hidden")
                    })
                }
            })
        }
    }
}
//. Usage
let flickPatch = {}
document.addEventListener("DOMContentLoaded", ( ev )=>{
    setTimeout( ()=>{
        flickPatch = new FlickPatch( document, ".flickity-slider" )
        flickPatch.init()   
    }, 8e3 )
})

2023-10-08

Javascript Tiny Type
Javascript Tiny Type
Replace text with a super small character set.
//  Replace text with a super small character set.
/*

_______ _               _______        _   
|__   __(_)             |__   __|      | |  
   | |   _ _ __  _   _     | | _____  _| |_ 
   | |  | | '_ \| | | |    | |/ _ \ \/ / __|
   | |  | | | | | |_| |    | |  __/>  <| |_ 
   |_|  |_|_| |_|\__, |    |_|\___/_/\_\\__|
                  __/ |                     
                 |___/   🗿 🪐 🔨
*/
let aTiny = {"a":"ᵃ","b":"ᵇ","c":"ᶜ","d":"ᵈ","e":"ᵉ","f":"ᶠ","g":"ᵍ","h":"ʰ","i":"ᶦ","j":"ʲ","k":"ᵏ","l":"ᶫ","m":"ᵐ","n":"ᶰ","o":"ᵒ","p":"ᵖ","q":"ᑫ","r":"ʳ","s":"ˢ","t":"ᵗ","u":"ᵘ","v":"ᵛ","w":"ʷ","x":"ˣ","y":"ʸ","z":"ᶻ","A":"ᴬ","B":"ᴮ","C":"ᶜ","D":"ᴰ","E":"ᴱ","F":"ᶠ","G":"ᴳ","H":"ᴴ","I":"ᴵ","J":"ᴶ","K":"ᴷ","L":"ᴸ","M":"ᴹ","N":"ᴺ","O":"ᴼ","P":"ᴾ","Q":"ᑫ","R":"ᴿ","S":"ˢ","T":"ᵀ","U":"ᵁ","V":"ⱽ","W":"ᵂ","X":"ˣ","Y":"ʸ","Z":"ᶻ","`":"`","~":"~","!":"﹗","@":"@","#":"#","$":"﹩","%":"﹪","^":"^","&":"﹠","*":"﹡","(":"⁽",")":"⁾","_":"⁻","-":"⁻","=":"⁼","+":"+","{":"{","[":"[","}":"}","]":"]",":":"﹕",";":"﹔","?":"﹖"};
let doTinyCaption = ( (_d, _q, _t) => {  //  Inject Tiny type
    let aTinyCnt = [..._d.querySelectorAll( _q )];
    if( aTinyCnt ){ setTimeout( ()=>{ doTinyCaption.tick(); }, 32); }
    return {
        "tick": ()=>{
            let sMU = "";
            aTinyCnt.forEach( (eVivCnt) => {
                if(eVivCnt.atTiny !== eVivCnt.dataset.atTiny){ // Data atr changed
                    Array.from( eVivCnt.dataset.atTiny ).filter(( sChr )=>{
                        sMU += ( sChr == " ") ? " "  : aTiny[ sChr ];
                    });
                    eVivCnt.innerHTML = sMU;
                    eVivCnt.atTiny = eVivCnt.dataset.atTiny;
                }
            } );
            setTimeout( ()=>{ doTinyCaption.tick(); }, _t);
        }
    };
})(document, "[data-at-tiny]", 13664 );

2023-08-20

Javascript GA4 Intersection Observer
Javascript GA4 Intersection Observer
Heatmap: Track content visibility time in Google Analytics or Adobe Analytics
//  Track content visibility time with Intersection Observer in Adobe Analytics or Google Analytics
/*
  /\  /\___  __ _| |_ _ __ ___   __ _ _ __  
 / /_/ / _ \/ _` | __| '_ ` _ \ / _` | '_ \ 
/ __  /  __/ (_| | |_| | | | | | (_| | |_) |
\/ /_/ \___|\__,_|\__|_| |_| |_|\__,_| .__/ 
                                   |_| 🌶️ 🔥
*/

class SyHeatmap {  //  Neodigm 55 Heatmap Begin
    static oObserved = {}; static aObservedEl = []; static aQryContext = []
    static oIntObserver = null; static NTHRESH_SECS = 3; static bIsInit = false;
    static reInit ( _q, _c = document ){  //  DOM bind to context element
        if( _q && _c ){
            this.aQryContext = [ _q, _c ]
            this.oObserved = {};
            this.aObservedEl = [ ... _c.querySelectorAll( _q[ 0 ] ) ];
            this.aObservedEl.forEach( ( elO )=>{
                let elOsib = elO.nextElementSibling
                const sCap = elOsib.heatmapCaption = elO.innerHTML
                this.oObserved[ sCap ] = elOsib
                this.oObserved[ sCap ].heatmapTime = []
            } )
            this.oIntObserver = new IntersectionObserver( ( entries )=>{
                entries.forEach( ( oEnt )=>{
                    if( oEnt.target?.heatmapCaption ){
                        const sCap = oEnt.target.heatmapCaption
                        if( this.oObserved[ sCap ].heatmapTime.length ){
                            this.oObserved[ sCap ].heatmapTime.push( {"state": oEnt.isIntersecting, "ts": new Date().getTime() })
                        }else{  //  No first time false (vis when page loads)
                            if( oEnt.isIntersecting ){
                                this.oObserved[ sCap ].heatmapTime.push( {"state": oEnt.isIntersecting, "ts": new Date().getTime() })
                            }
                        }
                        if( oEnt.isIntersecting ){
                            oEnt.heatmapTotal = SyHeatmap.totalHeatmapTime( this.oObserved[ sCap ].heatmapTime );  //  Sum and dif array vals
                            console.log( " ~~~ tot | " + oEnt.target.heatmapCaption + " | " + oEnt.heatmapTotal )
                        }
                    }
                } )
            } )
            //SyHeatmap.resetHeatMap()
            this.aObservedEl.forEach( ( elObs )=>{
                let sCap = this.oObserved[ elObs?.innerHTML ]
                if( sCap ) this.oIntObserver.observe( sCap )
            } )
            if( !this.bIsInit ){
                this.bIsInit = true;
                setInterval( ()=>{ SyHeatmap.tick() }, 3e3 )
            }
            return this;
        }
    }
    static totalHeatmapTime ( aHeatmapTime ){  //  Return total time on component in secs
        let nTotStart = 0; let nTotEnd = 0;  //  Note: IntrSec Observ will fire FALSE once upon page load for each entry not visible
        if( aHeatmapTime.length ){  //  Append a FALSE as NOW if the last item is not FALSE (currently in viewport)
            let aDTO = [ ... aHeatmapTime ]
            if( aDTO[ aDTO.length - 1 ].state == true ) aDTO.push( {"state": false, "ts": new Date().getTime() } )
            aDTO.forEach( ( oHMTimes )=>{ 
                if(  oHMTimes.state ) nTotStart = nTotStart + oHMTimes.ts
                if( !oHMTimes.state ) nTotEnd = nTotEnd + oHMTimes.ts
            } )
        }
        return ( nTotEnd - nTotStart ) / 1000;  //  in seconds
    }
    static genHeatmap ( nThresh = this.NTHRESH_SECS ){  //  Return a simple arry of current hm usage filt threshold
        let aCurHM = []
        if( this.aObservedEl.length ){
            for ( const sCap in this.oObserved ) {
                let nTotal = SyHeatmap.totalHeatmapTime( this.oObserved[ sCap ].heatmapTime )
                if( nTotal && ( nTotal >= nThresh ) ) aCurHM.push( {"caption": sCap, "secs": nTotal } )
            }
        }
        return aCurHM;
    }
    static resetHeatMap(){
        this.aObservedEl.forEach( ( elObs )=>{
            let sCap = this.oObserved[ elObs?.innerHTML ]
            if( sCap ) this.oIntObserver.unobserve( sCap )
        } )
    }
    static appendDataLayer (){  //  Iterate filtered heatmap and add to DL - return count
        let iCnt = 0
        if( window.dataLayer ) {
            SyHeatmap.genHeatmap().forEach( ( oHMSum )=>{
                let sMsg = oHMSum.caption + " | " + oHMSum.secs + " | " + Neodigm 55.salesforceGlobal.franchiseconfig.Name + " | " + Neodigm 55.salesforceGlobal.loginuser.UserRole.Name
                window.dataLayer.push( { "event": "Neodigm 55_heatmap",  "msg": sMsg, "hm_secs": oHMSum.secs, "hm_fran": Neodigm 55.salesforceGlobal.franchiseconfig.Name, "hm_role": Neodigm 55.salesforceGlobal.loginuser.UserRole.Name } )
                iCnt++;
            })            
        }
        SyHeatmap.resetHeatMap()  //  Reset and Rebind
        SyHeatmap.reInit( this.aQryContext[ 0 ], this.aQryContext[ 1 ])
        return iCnt;
    }
    static tick (){
        if( this.aQryContext.length ){  //  Must have already been fired
            let elSame = this.aQryContext[ 1 ].querySelector( this.aQryContext[ 0 ] );
            if( elSame ){
                if( elSame.innerHTML != this.aObservedEl[0]?.innerHTML ){ SyHeatmap.appendDataLayer() }
            }else{ SyHeatmap.appendDataLayer() }
        }
    }
}  //  Neodigm 55 Heatmap End

document.addEventListener("DOMContentLoaded", (ev)=>{
    setTimeout( ()=>{
        SyHeatmap.reInit( [ "DIV>H2" ], document.querySelector("#app > div.v-application--wrap > div.container.Neodigm 55-main-container.pa-0.ma-0.pt-0 > div > div") )
    }, 3e3 )
})
   

The Neodigm 55 Heatmap component captures the amount of time that each card is visible to an end user on a laptop or mobile device.

The summation of card activity is then packaged into the analytics data layer to be consumed by Adobe Analytics or Google Analytics. Activity that occurs while offline will be updated upon reconnection to the network if the app has not been closed.

The Neodigm 55 Heatmap component utilized the Intersection Observer pattern to track when and for how long block elements (cards) are visible in the user agent viewport.

The component is configured to only report if a card is visible for greater than 3 seconds. This threshold is configurable. There are some edge cases wherein data may lose fidelity. For example if the user opens another tab or abruptly closes the browser while a card is within the viewport.

The component captures the existence of cards within the browser's viewport, however the application may partially obstruct the viewport with an overlaying menu. Meaning that the Heatmap may report that a particular card is in view a few microseconds before it is actually visible. This discrepancy is so small that it is not statistically significant.

The data layer entries that the heatmaps create are additive, meaning that there may be more than one for a single component. This is because the end user viewed a particular card component, exited the card, then reentered the card.

2023-08-14

Get all products from Any Shopify Site
Get all products from Any Shopify Site
Fetch all Products and Images from any Shopify site.
//  Fetch all Products and Images from any Shopify site.
/* _____ __ __   ___   ____ ____  _____  __ __ 
  / ___/|  |  | /   \ |    \    ||     ||  |  |
 (   \_ |  |  ||     ||  o  )  | |   __||  |  |
  \__  ||  _  ||  O  ||   _/|  | |  |_  |  ~  |
  /  \ ||  |  ||     ||  |  |  | |   _] |___, |
  \    ||  |  ||     ||  |  |  | |  |   |     |
   \___||__|__| \___/ |__| |____||__|   |____/  😎 */

    var aP = [];

    const neodigmMU = `
<textarea id="elT" rows=8></textarea>`;  //  Universal Templs
    let eMU = document.createElement("textarea");
    document.body.appendChild(eMU);
    eMU.outerHTML = neodigmMU;

(function getProducts(url = 'https://SHOPIFY-SITE.com/admin/api/2019-07/products.json?limit=250&fields=id,images') {
        fetch(url).then(res => {
                const headerLink = res.headers.get('link');
                const match = headerLink?.match(/<[^;]+\/(\w+\.json[^;]+)>;\srel="next"/);
                const url = match ? match[1] : false;
                if(url){
                    res.json().then((data) => {
                    data.products.map((el) => {
                        //console.log( JSON.stringify( el ) )
                        aP.push( JSON.stringify( el ) )
                    })
                })
                getProducts(url)
                } else {
                    res.json().then((data) => {
                    data.products.map((el) => {
                        //console.log( JSON.stringify( el ) )
                        aP.push( JSON.stringify( el ) )
                    })
                        doTextArea()
                })
        }
    })
})()

function doTextArea(){
        let elTA = document.getElementById("elT")
        
        aP.forEach( (p) =>{
            // console.log( " ~~~ | " + p );
            elTA.textContent = elTA.textContent + p + "\n"  }
        )
}   

2023-08-10

JS Airport Geo-Proximity Radius
JS Airport Geo-Proximity Radius
Airport geo-proximity logic that answers questions, like What are the three closest airports to me right now?
// Desc: Get the closest airports by geolocation radius
// Usage: closestAirports.find(-99, -99, oAirports, 4); // 4 miles

/*       ___           _                 _   _             
 *      / _ \___  ___ | | ___   ___ __ _| |_(_) ___  _ __  
 *     / /_\/ _ \/ _ \| |/ _ \ / __/ _` | __| |/ _ \| '_ \ 
 *    / /_\\  __/ (_) | | (_) | (_| (_| | |_| | (_) | | | |
 *    \____/\___|\___/|_|\___/ \___\__,_|\__|_|\___/|_| |_|  ✈️ */

function getDistance(lat1, lon1, lat2, lon2) {
    let radlat1 = Math.PI * lat1/180;
    let radlat2 = Math.PI * lat2/180;
    let theta = lon1-lon2;
    let radtheta = Math.PI * theta/180;
    let dst = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
    if (dst > 1) dst = 1;
    dst = Math.acos(dst);
    dst = dst * (180/Math.PI) * (60 * 1.1515); // miles
    return dst;
  }

let closestAirports = ((_d) => {
    return {
      "find": function(nLat, nLon, objPorts, nRadius){
        if(nLat && nLon && objPorts){
          let arrPorts = [];
          for (let prop in objPorts) { // Sort Object
            if (objPorts.hasOwnProperty(prop)) {
              let lat = objPorts[prop].geoCode.split(",")[0];
              let lon = objPorts[prop].geoCode.split(",")[1];
              arrPorts.push({
                'key': prop, 'lat': lat, 'lon': lon,
                "dist": getDistance(lat, lon, nLat, nLon),
                "formattedAirport": objPorts[prop].formattedAirport
              });
            }
          }
          arrPorts.sort(function(a, b){
            // Sort by Distance
            return a.dist - b.dist;
          });
          return arrPorts.filter(function(aP){
            return (aP.dist <= nRadius);
          });
        }
      }
    };
})(document);

2022-12-13

Calculate Aspect Ratio of Viewport
Calculate Aspect Ratio of Viewport
Calculate Aspect Ratio of Viewport
// Desc: Calculate Aspect Ratio of Viewport
// Usage: Console log getDims() onresize event of body
/*                            _     _____       _   _       
    /\                       | |   |  __ \     | | (_)      
   /  \   ___ _ __   ___  ___| |_  | |__) |__ _| |_ _  ___  
  / /\ \ / __| '_ \ / _ \/ __| __| |  _  // _` | __| |/ _ \ 
 / ____ \\__ \ |_) |  __/ (__| |_  | | \ \ (_| | |_| | (_) |
/_/    \_\___/ .__/ \___|\___|\__| |_|  \_\__,_|\__|_|\___/ 
             | |                                            
             |_|  🎯 */

const gcd = (a, b) => {
    return b
      ? gcd(b, a % b)
      : a;
};
  
const aspectRatio = (width, height)  => {
    const divisor = gcd(width, height);
    return `${width / divisor}:${height / divisor}`;
};

const getDims = function(){
    if(window.innerWidth !== undefined && window.innerHeight !== undefined) { 
      var w = Number( window.innerWidth )
      var h = Number( window.innerHeight )
      var a = aspectRatio( w, h )
    } else {
      var w = Number( document.documentElement.clientWidth )
      var h = Number( document.documentElement.clientHeight )
      var a = aspectRatio( w, h )
    }
    return {"ratio": a, "h": h, "w": w};
}

2021-04-16

Javascript Generate and Download CSV
Javascript Generate and Download CSV
Produce CSV with client-side JS. Construct Blog and Download as CSV file.
// Desc: Produce CSV with client-side JS. Contruct Blob and Download as CSV file
/*    _________   _____________   ____ __________.__        ___.    
 *    \_   ___ \ /   _____/\   \ /   / \______   \  |   ____\_ |__  
 *    /    \  \/ \_____  \  \   Y   /   |    |  _/  |  /  _ \| __ \ 
 *    \     \____/        \  \     /    |    |   \  |_(  <_> ) \_\ \
 *     \______  /_______  /   \___/     |______  /____/\____/|___  /
 *            \/        \/                     \/                \/    CSV Report */

  The resulting CSV files will contain a header row deterministic column names
  The resulting CSV files will be quoted
  The file name is auto-generated timestamp
  Cell string data may contain a comma , however quotes will be removed
  Cell string data may contain only utf-8 characters

let nativeCSV = ( ( _d )=>{
let oCnt, jnCSV, sCSV, blCSV, elCSV;  //  config, json, array, blob, and element
let retObj = {
    "init": ( _oCnt )=>{
        oCnt = _oCnt;
        if( oCnt.fileName.indexOf("####") !== -1) {
            oCnt.fileName = oCnt.fileName.replace("####", Date.now() );}
        jnCSV = sCSV = blCSV = elCSV = "";
        return retObj;
    },
    "setArray": ( _jnCSV )=>{  //  An array (rows) of arrays (cols) !jagged
        jnCSV = _jnCSV;
        if( oCnt.header ) jnCSV.unshift( oCnt.header );
        jnCSV.forEach(( aRow )=>{
            aRow.forEach(( sCol )=>{
                if( typeof sCol === "string"){
                    sCSV += oCnt.delimQuote + sCol
                        .split( oCnt.delimQuote ).join("");
                    sCSV += oCnt.delimQuote + oCnt.delimCol;                       
                }
            });
            sCSV = sCSV.slice(0, -1) + oCnt.delimLine;
        });
        return retObj;
    },
    "getBlob": ()=>{
        blCSV = new Blob([ sCSV ], { type: "text/csv;charset=utf-8;" });
        return retObj;
    },
    "createLink": ()=>{
        elCSV = _d.createElement("a");
        elCSV.setAttribute("href", URL.createObjectURL( blCSV ));
        elCSV.setAttribute("download", oCnt.fileName );
        elCSV.style.visibility = 'hidden';
        _d.body.appendChild( elCSV );
        return retObj;
    },
    "clickLink": ()=>{
        elCSV.click();
        return retObj;
    },
    "removeLink": ()=>{
        _d.body.removeChild( elCSV );
        return retObj;
    }
};
return retObj;
})( document );

console.log( nativeCSV.init({  //  Usage: 
"delimCol": ",",
"delimQuote": '"',
"delimLine": "\n",
"fileName": "graph_nodes_####.csv",
"header": ["id","name", "FQDN"]})
.setArray( currentGraph2Array(jCurrentGraph) )
.getBlob()
.createLink()
.clickLink()
.removeLink()    
);

2021-02-27

PWA Add to Home Screen
PWA Add to Home Screen
Progressive Web App ⚡ Advanced Cache && Notification Patterns
/*     ______   __     __     ______    
      /\  == \ /\ \  _ \ \   /\  __ \   
      \ \  _-/ \ \ \/ ".\ \  \ \  __ \  
       \ \_\    \ \__/".~\_\  \ \_\ \_\ 
        \/_/     \/_/   \/_/   \/_/\/_/  ✨ Add to Home Screen
        chrome://serviceworker-internals/
 */
 if ("serviceWorker" in navigator) {
    window.addEventListener("load", () => {
      navigator.serviceWorker.register("sw.js");
    });
}

let eA2hs = oD.getElementsByClassName("js-a2hs")[0];
let eA2hsP = oD.getElementsByClassName("js-a2hs--post")[0];

eA2hs.addEventListener("click", (e) => {
    eA2hs.style.display = "none";
    eA2hsP.style.display = "block";
    evDefPrompt.prompt();
    evDefPrompt.userChoice
      .then((choiceResult) => {
        if (choiceResult.outcome === "accepted") {
          if( snck ) neodigmToast.q("Wow, Now I'm an App on your Desktop|How Convenient!");
          playAudioFile( 7 );  //  ggl tag event | User accepted the A2HS prompt
        } else {
          playAudioFile( 3 );  //  ggl tag event | User dismissed the A2HS prompt
        }
        evDefPrompt = null;
      });
  });

function displayMsg( sMsg ){
    //    System Tray Notification
    if (!("Notification" in window)) {
        console.log('Notification API not supported.');
        return;
    } else if (Notification.permission === "granted") {
        // If it's okay let's create a notification
        var notification = new Notification( Nowish(), {icon: "https://repository-images.githubusercontent.com/178555357/2b6ad880-7aa0-11ea-8dde-63e70187e3e9", body: sMsg} );
    } else if (Notification.permission !== "denied") {
        // Otherwise, we need to ask the user for permission
        Notification.requestPermission(function (permission) {
            // If the user accepts, let's create a notification
            if (permission === "granted") {
                var notification = new Notification( Nowish(), {icon: "https://repository-images.githubusercontent.com/178555357/2b6ad880-7aa0-11ea-8dde-63e70187e3e9", body: sMsg} );
            }
        });
    }
}

/*    ╔═╗┌─┐┬─┐┬  ┬┬┌─┐┌─┐
 *    ╚═╗├┤ ├┬┘└┐┌┘││  ├┤ 
 *    ╚═╝└─┘┴└─ └┘ ┴└─┘└─┘
 *    ╦ ╦┌─┐┬─┐┬┌─┌─┐┬─┐  
 *    ║║║│ │├┬┘├┴┐├┤ ├┬┘  
 *    ╚╩╝└─┘┴└─┴ ┴└─┘┴└─  Advanced Cache ⚡ Notifications
 */
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.0.0/workbox-sw.js');
workbox.LOG_LEVEL = "debug";
self.addEventListener("fetch", event => {
  event.respondWith(caches.match(event.request)
    .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request);
      })
    );
});
workbox.routing.registerRoute(
  // Cache CSS files
  /.*\.css/,
  // Use cache but update in the background ASAP
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'css-cache',
  })
);
workbox.routing.registerRoute(
  // Cache image files
  /\.(?:png|gif|jpg|jpeg|webp|avif|svg|mp3|mp4|json|html|js)$/,
  // Use the cache if it's available
  workbox.strategies.cacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 256, maxAgeSeconds: 172800,
      })
    ],
  })
);

class NeodigmPWA {
  constructor(){
  }
  init () {
      window.addEventListener('appinstalled', () => {
          setTimeout(function(){
              neodigmToast.q("##Application Installed|Neodigm UX ✨ Scott C. Krause")
              neodigmWired4Sound.play( 8 )
              if( dataLayer ) dataLayer.push({'event': 'appinstalled'})
          }, 1200)
        });
  }
}
let neodigmPWA = new NeodigmPWA()
neodigmPWA.init()

2020-12-21

HTML data attrib to JavaScript camel-case dataset
HTML data attrib to JavaScript camel-case dataset
Convert an HTML formatted data attrib name to a JS formatted name.
// Desc: data-is-whatever will be converted to isWhatever
// Usage: element.dataset[ data2prop("data-is-whatever") ]
/*______ _____ __  __           _____           _       _   
 |  ____/ ____|  \/  |   /\    / ____|         (_)     | |  
 | |__ | |    | \  / |  /  \  | (___   ___ _ __ _ _ __ | |_ 
 |  __|| |    | |\/| | / /\ \  \___ \ / __| '__| | '_ \| __|
 | |___| |____| |  | |/ ____ \ ____) | (__| |  | | |_) | |_ 
 |______\_____|_|  |_/_/    \_\_____/ \___|_|  |_| .__/ \__|
                                                 | |        
                                                 |_|        ES2021*/

function data2prop( sDset ){  //  Convert HTML data attrib name to JS dataset name
    sDset = sDset.replace("data-", "").toLowerCase();
    let aDset = sDset.split(""), aDret = [], bUpper = false;
    aDset.forEach( ( sChar ) => {
        if( sChar == "-" ){
            bUpper = true;
        }else{
            aDret.push( ( bUpper ) ? sChar.toUpperCase() : sChar );
            bUpper = false;
        }
    });
    return aDret.join("");
}

2020-12-19

Oracle PL/SQL Stored Procedure
Oracle PL/SQL Stored Procedure
Vintage Stored Procedure to denormalize department codes
--  ███████  ██████  ██      
--  ██      ██    ██ ██      
--  ███████ ██    ██ ██      
--       ██ ██ ▄▄ ██ ██      
--  ███████  ██████  ███████  Relational ⚡ Transactional
--              ▀▀           
PROCEDURE post_stage
(
    in_rowid_job            cmxlb.cmx_rowid,
    in_ldg_table_name       cmxlb.cmx_table_name,
    in_stg_table_name       cmxlb.cmx_table_name,
    out_error_msg      OUT  cmxlb.cmx_message,
    out_return_code    OUT  int
)
AS
sql_stmt varchar2(2000);
t_party_acct_id varchar2(14);
t_txn_div_cd varchar2(20);
t_txn_div_display varchar2(50);
commit_count NUMBER := 0;
commit_inc NUMBER := 1000;
--
CURSOR C_PTAC_TXN IS
SELECT PARTY_ACCT_ID, TXN_DIV_CD, TXN_DIV_DISPLAY
FROM   C_STG_PTAC_TXN_DIV;
--
BEGIN
--
    commit_inc := to_number(GET_PARAMETER('post_stage_commit', commit_inc));
    IF in_ldg_table_name = 'C_LDG_PTAC_TXN_DIV' AND in_stg_table_name = 'C_STG_PTAC_TXN_DIV' THEN
    --    20130225 SCK Update the stage txn_div_display col with a denormalized string derived
    --    from an aggregate of both staging and base object. 
    --    🏄 SQL ⚡ ETL MDM ⚡ PL/SQL ORM
        cmxlog.debug ('ADDUE: Landing table name is ' || in_ldg_table_name || ' Staging table name is ' || in_stg_table_name);
        BEGIN
              FOR R_PTAC_TXN in C_PTAC_TXN LOOP
                    post_stage_concat(R_PTAC_TXN.PARTY_ACCT_ID, t_txn_div_display);
                    UPDATE C_STG_PTAC_TXN_DIV
                    SET txn_div_display = t_txn_div_display, create_date = sysdate WHERE TXN_DIV_CD = R_PTAC_TXN.TXN_DIV_CD AND
                    PARTY_ACCT_ID = R_PTAC_TXN.PARTY_ACCT_ID;  -- CURRENT OF C_PTAC_TXN;
                    commit_count := commit_count + commit_inc;
                    IF MOD(commit_count, 1000) = 0 THEN
                        cmxlog.debug ('ADDUE: post_stage_concat is: ' || commit_count || ':' || R_PTAC_TXN.PARTY_ACCT_ID || ' : ' || t_txn_div_display);
                        COMMIT;
                    END IF;
              END LOOP;
              COMMIT;
        END;
    ELSE
      CMXlog.debug ('ADDUE Post Stage - no action taken');
    END IF;
END post_stage;
END ADD_UE;

2020-12-19

Dark Mode and Reduced Motion
Dark Mode and Reduced Motion
Making Dark Mode work with both a UI switch && the OS preference.
//  Desc: Listen to the OS for user preference
//  but override with a UI toggle.
/*  ______                  __        ____    ____               __        
   |_   _ `.               [  |  _   |_   \  /   _|             |  ]       
     | | `. \ ,--.   _ .--. | | / ]    |   \/   |   .--.    .--.| | .---.  
     | |  | |`'_\ : [ `/'`\]| '' <     | |\  /| | / .'`\ \/ /'`\' |/ /__\\ 
    _| |_.' /// | |, | |    | |`\ \   _| |_\/_| |_| \__. || \__/  || \__., 
   |______.' \'-;__/[___]  [__|  \_] |_____||_____|'.__.'  '.__.;__]'.__.'  User Prefs */


let doPrefersReducedMotion = function( bMotion ){// Stop 3D rotation
    o3Config.controls.autoRotate = !bMotion; 
}
let doPrefersColorScheme = function( bScheme ){ // UI | OS Semaphore
    document.body.dataset.n55AmpmTheme = ((bScheme) ? "dark" : "light"); //  🌙  /  ☀️
}

// Capture the prefers media queries
const mqPrefReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const mqPrefColorScheme = window.matchMedia("(prefers-color-scheme: dark)");

doPrefersReducedMotion( (mqPrefReducedMotion && mqPrefReducedMotion.matches) );
doPrefersColorScheme( (mqPrefColorScheme && mqPrefColorScheme.matches) );

// listen to changes in the media query's value
mqPrefReducedMotion.addEventListener("change", () => {
    doPrefersReducedMotion( mqPrefReducedMotion.matches );
});
mqPrefColorScheme.addEventListener("change", () => {
    doPrefersColorScheme( mqPrefColorScheme.matches );
});

/* Dark Mode begin */
/*@media (prefers-color-scheme: dark) {*/

    body[data-n55-ampm-theme='dark'] [role='main'] {
        background: linear-gradient(to right, #5A5852, #c2c2c2, #5A5852)
    }
    body[data-n55-ampm-theme='dark'] .h-bg__stripe, body[data-n55-ampm-theme='dark'] .l-caro-design > article, body[data-n55-ampm-theme='dark'] article.l-caro-design {
        background: repeating-linear-gradient(45deg,#242424,#242424 24px,#444 24px,#444 48px);
    }
    body[data-n55-ampm-theme='dark'] section.pfmf-grid > div > article {
        border: solid 1px #888;
        border-top: solid 2px #888;
        box-shadow: 0px 2px 6px -2px rgba(164,164,164,0.6);
        background-color: #242424;
    }
    body[data-n55-ampm-theme='dark'] .readable__doc { color: #fff; }
    body[data-n55-ampm-theme='dark'] .readable__caption { color: #fff; }
    body[data-n55-ampm-theme='dark'] .h-vect-line-art { stroke: #fff;}
/*}*/
/* Dark Mode end */

2020-12-19

Vanilla JS Popover Microinteraction
Vanilla JS Popover Microinteraction
A popover is a transient view that shows on a content screen when a user clicks on a control button or within a defined area.
//  A popover is a transient view that shows on a content screen when
//  a user clicks on a control button or within a defined area.
/*   __  __     __           __  __     __  __    
    /\ \/\ \   /\ \         /\ \/\ \   /\_\_\_\   
    \ \ \_\ \  \ \ \        \ \ \_\ \  \/_/\_\/_  
     \ \_____\  \ \_\        \ \_____\   /\_\/\_\ 
      \/_____/   \/_/         \/_____/   \/_/\/_/   */

let oPopOver = ( ( _win, _doc, _qry ) => {
        let arPops = [], ePos, iOffTop=0, iOffLft=0;
        return {  // Popover UX pattern
            "init": function(){ // wire DOM events
                arPops= [].slice.call(_doc.querySelectorAll( _qry ));
                _win.addEventListener("resize", oPopOver.closeAll);
                _win.addEventListener("scroll", oPopOver.closeAll); 
                _doc.body.addEventListener("click", function( e ){ // 👁️ Outside Click
                    let eTarget = e.target, bInside = false;
                    while( eTarget.tagName !== "HTML" ){
                        if( eTarget.dataset.popover ){ bInside = true; break; }
                        eTarget = eTarget.parentNode;
                    }
                    if( !bInside ){ // Tapped Outside of Popover
                        oPopOver.closeAll();
                    }
                }, true);
            },
            "open": function(id, evPos){ // Open a single Popover
                if( arPops.length == 0) return false;
                oPopOver.closeAll();
                ePos = evPos.currentTarget;
                let elPop = arPops.filter(function(el){
                    return ( el.id == id );
                })[0];
                iOffTop = Number(elPop.dataset.popoverPos.split("|")[0]);
                iOffLft = Number(elPop.dataset.popoverPos.split("|")[1]);
                elPop.dataset.popover = "true"; // Open and Active
                elPop.style.left = oPopOver.position().left+"px";
                elPop.style.top = oPopOver.position().top+"px";
            },
            "closeAll": function(){ // Close all Popovers
                if( arPops.length == 0) return false;
                arPops.map(function(el){
                    el.dataset.popover = "false";
                });
            },
            "position": function(){ // Determine Popover position
                let rec = ePos.getBoundingClientRect(),
                pxLft = _win.pageXOffset || _doc.documentElement.scrollLeft,
                pxTop = _win.pageYOffset || _doc.documentElement.scrollTop;
                return { top: (rec.top + pxTop + iOffTop), left: (rec.left + pxLft + iOffLft) }
            }
        }
    })(window, document, "[data-popover]"); // Declarative implementation

2020-12-16

Vue.js double tap Microinteraction
Vue.js double tap Microinteraction
Firing both a tap and a double-tap on the same element
//  A Vue.js snippet that shows how to capture both a tap and
//  a double-tap on the same element within the template.
//
//  Canonical Use Case: Double-Tap to zoom into a hero image
//  and single-tap to zoom out.

/*    ____   ____                      __        
 *    \   \ /   /_ __   ____          |__| ______
 *     \   Y   /  |  \_/ __ \         |  |/  ___/
 *      \     /|  |  /\  ___/         |  |\___ \ 
 *       \___/ |____/  \___  > /\ /\__|  /____  >
 *                         \/  \/ \______|    \/ */

 methods: {
    "doHeroMobMouseUp": function( ev ){ // Double Tap
      var oHro = this.oHeroZmMob;
      if( oHro.isInit ){
        if( oHro.doubleTap ){ // Zoom In
          oHro.doubleTap = false;
          this.doHeroMobScale( .5 ); // Double Tap
        }else{
          oHro.doubleTap = true;
          setTimeout(function(){ this.doHeroMobMouseUp_expire() }, 380);
        }
      }
      this.oHeroZmMob.isDown = false;
    },
    "doHeroMobMouseUp_expire": function(){ // Single Tap
      var oHro = this.oHeroZmMob;
      if( oHro.isInit ){ // Zoom Out
        if( oHro.doubleTap ) this.doHeroMobScale( -.5 ); // Single Tap
        oHro.doubleTap = false;
      } 
    }
 }
 /*
 This is only part of a larger Vue gesture implementation supporting
 Pinch 🤏, Zoom, Pan, and Swipe. Reach out to me if you want to learn more.
 */

2020-12-15

CSS Advanced Accessibility
CSS Advanced Accessibility
Motion, theme, and skip A11Y CSS solutions
/* Skip to Main Content - CSS Focus rules that make the
link visible when focused from the omnibox.
========================================
====  ========  ========  =====  ====  =
===    =====    ======    =====   ==   =
==  ==  ======  ========  ======  ==  ==
=  ====  =====  ========  ======  ==  ==
=  ====  =====  ========  =======    ===
=        =====  ========  ========  ====
=  ====  =====  ========  ========  ====
=  ====  =====  ========  ========  ====
=  ====  ===      ====      ======  ====
======================================== */

a.skip__main:active, a.skip__main:focus {
    background-color: #fff;
    border-radius: 4px;
    border: 2px solid #000;
    color: #000;
    font-size: 1em;
    height: auto; width: 16%;
    left: auto;
    margin: 8px 42%;
    overflow: auto;
    padding: 4px;
    text-align: center;
    top: auto;
    z-index: 1024;
}
a.skip__main {
    left: -1024px;
    overflow: hidden;
    position: absolute;
    top: auto;
    width: 1px; height: 1px;
    z-index: -1024;
}
/* Dark Mode begin */
@media (prefers-color-scheme: dark) {
    body, [role='main'] {
        background: linear-gradient(to right, #5A5852, #c2c2c2, #5A5852)
    }
    .h-bg__stripe, .l-caro-design > article, article.l-caro-design {
        background: repeating-linear-gradient(45deg,#bbb,#bbb 24px,#ddd 24px,#ddd 48px);
    }
}
/* Dark Mode end */

/* Reduced Motion begin*/
@media (prefers-reduced-motion: reduce) {
    .hero__vect { animation: none; }
}
/* Reduced Motion end*/

<a class="js-skip__main--id skip__main"
href="#a11y-skipmain">Skip to Main Content</a>

2020-12-13

Cypress E2E Quality Assurance
Cypress E2E Quality Assurance
End to End testing 🚀 Headless browser automation
/*    _____                               
     /  __ \                              
     | /  \/_   _ _ __  _ __ ___  ___ ___ 
     | |   | | | | '_ \| '__/ _ \/ __/ __|
     | \__/\ |_| | |_) | | |  __/\__ \__ \
      \____/\__, | .__/|_|  \___||___/___/.io
             __/ | |                      
            |___/|_|                      E2E

✅ Automatically capture a video when a test fails
✅ Test values persisted in the Vuex (Vue.js Vuex specific) store
✅ Apply optional configuration files via the command line
✅ Test the uploading of images
✅ Create custom reusable, and chainable commands, such as cy.signIn() or cy.turnOnFeature()
✅ Test responsive layout & Local Storage
✅ Test A11y WCAG success criteria  */

describe('E2E test | Hotel navigation, selection, and discovery', () => {
  context('Admin Add Hotel to Event', function () { // ignore CORS
      Cypress.on('uncaught:exception', (err, runnable) => { return false });
      it('Success Login then Save Event', () => {
          cy.viewport(1066, 600)  //  large laptop 66.563em
          cy.log( JSON.stringify(Cypress.env()) )
          let event_url;  //  The URL of the first event (default)
          let dMessage = new Date();  //  Now
          dMessage = dMessage.toDateString() + " " + dMessage.toLocaleTimeString();

          cy.tt_SignIn(Cypress.env( "mock_email" ), Cypress.env( "mock_password" ))
          .then(() => {
              cy.window().then( $win => {
                  cy.wrap( $win.system_output ).should("exist")
              })
          })
          cy.url().should('not.include', 'login.')
          cy.visit( Cypress.env( "e2e_url_events" ) )
          cy.url().should('include', 'events.')
          Cypress.Cookies.debug(true, { verbose: false })
          cy.getCookie('logintoken').should('exist')
          cy.getCookie('role_id').should('exist')
          cy.getCookie('username').should('exist')
          cy.getCookie('otapi_token').should('exist')
          cy.get("a[href*='event-edit']" ).first().click()  //  Find the first matching link in the table.
          cy.get("#messages" ).type("{selectall}{backspace}E2E Test: " + dMessage )
          cy.get("#eventForm > div.border-top.d-flex.pt-3.row > div > input" ).first().click()  //  Save change

          cy.get("#airTab" ).click()  //  select tab
          cy.get("#activate_flights" ).check();
          cy.get("#flightForm > div.border-top.d-flex.pt-3.row > div > input" ).click();           

          cy.get("#vehicleTab" ).click()  //  select tab
          cy.get("#activate_vehicle" ).uncheck();
          cy.get("#vehicleForm > div.border-top.d-flex.pt-3.row > div > input" ).click();   

          cy.get("#hotelTab" ).click()  //  select tab
          cy.get("#activate_hotels" ).check();
          cy.get("#hotelForm > div.border-top.d-flex.pt-3.row > div > input" ).click();   

          //  Extract URL from INPUT
          cy.get('#siteURL').invoke('val')
              .then( value => { event_url = value; });
          cy.then(() => { return cy.visit(event_url); });
      })
  })
  context('Choose Flight', function () {
      Cypress.on('uncaught:exception', (err, runnable) => { return false }); // ignore CORS
      it('Success Flight added to cart', () => {
          cy.viewport(1066, 600)  //  large laptop 66.563em 
          cy.get("#from_airport" ).type( "ORD" )
          cy.get("#to_airport" ).type( "LGA" )
          cy.get("input[name='from_date']" ).click({ force: true })

          cy.server()
          cy.route("*").as( "checkout" )

          cy.get("div.vdp-datepicker.flex-fill > div:nth-child(2) > div > span:nth-child(39)" ).first().click()
          cy.get("#search-widget-btn" ).click()
          cy.wait("@checkout" ).its('status').should('eq', 200)

          cy.get("h5.modal-title").should("not.be.visible")
              .then( ($ModalMsg) => {
                  cy.get("div.align-self-center.col-6.col-md.col-sm.col-xl.order-12.p-xs-1.text-right > button" ).first().click() 
              } )
      })
  })
  context('Hotel LightBox', function () {
      Cypress.on('uncaught:exception', (err, runnable) => { return false }); // ignore CORS
      it('Success Hotel added to cart', () => {
          cy.viewport(1066, 600)  //  large laptop 66.563em
          cy.wait(2000)
          cy.get("picture > img" ).first()
              .then( ( $picture )=>{
                  cy.wrap( $picture  ).click()
                  cy.wait( 6000 )
              })
          cy.get(".l-ltbx__image" ).first().click()  //  Cycle photos forward
          cy.get(".l-ltbx__vect--right" )
              .then( ( $arrow_right ) => {
                  cy.wait( 1000 )
                  cy.wrap( $arrow_right ).click()
                  cy.wait( 1000 )
                  cy.wrap( $arrow_right ).click()
                  cy.wait( 1000 )
                  cy.wrap( $arrow_right ).click()
          })
          cy.wait( 1000 )
          cy.get(".l-ltbx__btn" ).first()  //  Cycle photos backward
              .then( ( $arrow_left ) => {
                  cy.wrap( $arrow_left ).click()
                  cy.wait( 1000 )
              })
          cy.get(".l-ltbx__figcap").invoke("text").should("include", "4 of")
              .then( () => {
                  cy.get(".l-ltbx__vect" ).first().click()  //  Close Modal
                  cy.get("OUTPUT BUTTON.l-button" ).first().click()  //  Book Room
                      .then( () => {
                          cy.get( "A.ttfont-semibold.tttext-gray-700").first().click()  //  Change Tab
                          cy.wait( 1000 )
                          cy.get( "A.ttfont-semibold.tttext-gray-700").first().click()  //  Change Tab
                          cy.wait( 1000 )
                          cy.get( "ARTICLE SECTION BUTTON.l-button").first().click()  //  Book Room
                              .then( ()=>{
                                  cy.wait( 4000 )
                                  cy.url().should('include', '/checkout')
                              })
                      })
              })
      })
  })
})

2020-12-07

Asynchronous eCom Nav Category Count
Asynchronous eCom Nav Category Count
Asynchronous recursive crawl reports the total number of products by category.
// Desc:  Asynchronous recursive crawl report the total number of products by category
// Usage: Console SNIPPET catCount.init();

/*   @@@@@@ @@@  @@@ @@@ @@@@@@@  @@@@@@@  @@@@@@@@ @@@@@@@ 
!@@     @@!@!@@@ @@! @@!  @@@ @@!  @@@ @@!        @!!   
 !@@!!  @!@@!!@! !!@ @!@@!@!  @!@@!@!  @!!!:!     @!!   
    !:! !!:  !!! !!: !!:      !!:      !!:        !!:   
::.: :  ::    :  :    :        :       : :: ::     :     run in console */

var catCount = (function(_d,_q){
"use strict";
let aSub = [];
console.clear();

return {
    init: function(){
        // Get ref to all product categories in the left nav 🛒
        aSub = [ ... _d.querySelectorAll( _q ) ].filter( ( el ) => {
            return (( el.firstChild.nodeValue ) && ( el.href ));
        } );
        aSub.forEach( ( elLink ) => {
            if( elLink ) catCount.asyncTotal( elLink );
        } );
    },
    parse: function( _Name, _Contents ){
        let aTotl = _Contents.split("sizeTotalNumRecs");
        if( aTotl[1].split('"')[2] ){
             console.log( _Name, aTotl[1].split('"')[2]);
        }
        return true;
    },
    asyncTotal: function( _elLink ){
        let oXhr = new XMLHttpRequest();
        oXhr.open("GET", _elLink.href, true);
        oXhr.onreadystatechange = () => {
            if( this.readyState!==4 || this.status!==200 ) return;
            catCount.parse( _elLink.firstChild.nodeValue, this.responseText );
        };
        oXhr.send();
    }
}

})(document, "LI.item nav > a" );

2020-12-07

Color of the Year CSS Styles
Color of the Year CSS Styles
Color of the Year 2000 thru 2021 CSS Utility classes
/* Tailwind like CSS Utility classes for the
Pantone Color of the Years from 2000 thru 2021
/*      ____             _                   
 *     |  _ \ __ _ _ __ | |_ ___  _ __   ___ 
 *     | |_) / _` | '_ \| __/ _ \| '_ \ / _ \  2000- 2021
 *     |  __/ (_| | | | | || (_) | | | |  __/
 *     |_|   \__,_|_| |_|\__\___/|_| |_|\___|  🟥 🟩 🟦 🟪 🟨 */

 /* Color of the Year begin */
 .bg-coy_2000  {background-color: #9BB7D4;}  /* Cerulean */
 .bg-coy_2001  {background-color: #C74375;}  /* Fuchsia Rose */
 .bg-coy_2002  {background-color: #BF1932;}  /* True Red */
 .bg-coy_2003  {background-color: #7BC4C4;}  /* Aqua Sky */
 .bg-coy_2004  {background-color: #E2583E;}  /* Tigerlily */
 .bg-coy_2005  {background-color: #53B0AE;}  /* Blue Turquoise */
 .bg-coy_2006  {background-color: #DECDBE;}  /* Sand Dollar */
 .bg-coy_2007  {background-color: #9B1B30;}  /* Chili Pepper */
 .bg-coy_2008  {background-color: #5A5B9F;}  /* Blue Iris */
 .bg-coy_2009  {background-color: #F0C05A;}  /* Mimosa */
 .bg-coy_2010  {background-color: #45B5AA;}  /* Turquoise */
 .bg-coy_2011  {background-color: #D94F70;}  /* Honeysuckle */
 .bg-coy_2012  {background-color: #DD4124;}  /* Tangerine Tango */
 .bg-coy_2013  {background-color: #009473;}  /* Emerald */
 .bg-coy_2014  {background-color: #B163A3;}  /* Radiant Orchid */
 .bg-coy_2015  {background-color: #955251;}  /* Marsala */
 .bg-coy_2016  {background-color: #F7CAC9;}  /* Rose Quartz */
 .bg-coy_2016b {background-color: #92A8D1;}  /* Serenity */
 .bg-coy_2017  {background-color: #88B04B;}  /* Greenery */
 .bg-coy_2018  {background-color: #5F4B8B;}  /* Ultra Violet */
 .bg-coy_2019  {background-color: #FF6F61;}  /* Living Coral */
 .bg-coy_2020  {background-color: #0F4C81;}  /* Classic Blue */
 .bg-coy_2021  {background-color: #939597;}  /* Ultimate Gray */
 .bg-coy_2021b {background-color: #F5DF4D;}  /* Illuminating */
 .bg-coy_2024  {background-color: #FFBE98;}  /* Peach Fuzz */

 .text-coy_2000  {color: #9BB7D4;}  /* Cerulean */
 .text-coy_2001  {color: #C74375;}  /* Fuchsia Rose */
 .text-coy_2002  {color: #BF1932;}  /* True Red */
 .text-coy_2003  {color: #7BC4C4;}  /* Aqua Sky */
 .text-coy_2004  {color: #E2583E;}  /* Tigerlily */
 .text-coy_2005  {color: #53B0AE;}  /* Blue Turquoise */
 .text-coy_2006  {color: #DECDBE;}  /* Sand Dollar */
 .text-coy_2007  {color: #9B1B30;}  /* Chili Pepper */
 .text-coy_2008  {color: #5A5B9F;}  /* Blue Iris */
 .text-coy_2009  {color: #F0C05A;}  /* Mimosa */
 .text-coy_2010  {color: #45B5AA;}  /* Turquoise */
 .text-coy_2011  {color: #D94F70;}  /* Honeysuckle */
 .text-coy_2012  {color: #DD4124;}  /* Tangerine Tango */
 .text-coy_2013  {color: #009473;}  /* Emerald */
 .text-coy_2014  {color: #B163A3;}  /* Radiant Orchid */
 .text-coy_2015  {color: #955251;}  /* Marsala */
 .text-coy_2016  {color: #F7CAC9;}  /* Rose Quartz */
 .text-coy_2016b {color: #92A8D1;}  /* Serenity */
 .text-coy_2017  {color: #88B04B;}  /* Greenery */
 .text-coy_2018  {color: #5F4B8B;}  /* Ultra Violet */
 .text-coy_2019  {color: #FF6F61;}  /* Living Coral */
 .text-coy_2020  {color: #0F4C81;}  /* Classic Blue */
 .text-coy_2021  {color: #939597;}  /* Ultimate Gray */
 .text-coy_2021b {color: #F5DF4D;}  /* Illuminating */
 .text-coy_2022  {color: #6667AB;}  /* Very Peri */
 .text-coy_2023  {color: #BE3455;}  /* Viva Magenta */
 .text-coy_2024  {color: #FFBE98;}  /* Peach Fuzz */

 /* Color of the Year end */

2020-12-07

Solve Anagram Puzzle
Solve Anagram Puzzle
Do two strings contain the exact amount of letters to form two words?
/* An anagram is a word or phrase formed by rearranging the letters
of a different word or phrase, typically using all the original
letters exactly once. For example, the word anagram itself can be
rearranged into nag a ram, also the word binary into brainy. 🎯 🍰 🔥
   _                                         
  /_\  _ __   __ _  __ _ _ __ __ _ _ __ ___  
 //_\\| '_ \ / _` |/ _` | '__/ _` | '_ ` _ \ 
/  _  \ | | | (_| | (_| | | | (_| | | | | | |
\_/ \_/_| |_|\__,_|\__, |_|  \__,_|_| |_| |_|
                 |___/                     */


//  Determine if two strings are Anagrams
function isAnagram( word1 = "DOCTORWHO", word2 = "TORCHWOOD"){
  let uc1 = word1.toUpperCase(), uc2 = word2.toUpperCase()
  return ([ ... uc1 ].filter(( c )=>{
    if( uc2.indexOf( c ) != -1 ){
      uc2 = uc2.replace( c, "" )  //  Replace First Occurrence
      return true;
    }
  }).length === uc1.length && (!uc2))
}
console.warn( isAnagram("neodigm", "dogimen") );

// Palindromes | They can be read the same backwards and forwards!
// Is TACOCAT spelled backward still TACOCAT?
// People have been asking this question for thousands of years until...
// I wrote a function in JavaScript to prove it and end the debate. Palindrome in JavaScript

let isPalindrome = ( sIn = "tacocat" ) => ( sIn.split("").reverse().join("") === sIn );
/*🐈‍🐱
 _._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'- My name is Taco! ^_^
*/

2020-12-07

Virtual Keyboard Extention Configuration
Virtual Keyboard Extention Configuration
TS Virtual Keyboard Chrome Extention
// TS Virtual Keyboard ⌨️ Chrome Extention | Configuration Class
/***
 *     _______                     _                _            
 *    (_______)                   | |              (_)      _    
 *     _      _   _ ____   ____    \ \   ____  ____ _ ____ | |_  
 *    | |    | | | |  _ \ / _  )    \ \ / ___)/ ___) |  _ \|  _) 
 *    | |____| |_| | | | ( (/ / _____) | (___| |   | | | | | |__ 
 *     \______)__  | ||_/ \____|______/ \____)_|   |_| ||_/ \___)
 *           (____/|_|                               |_|         
 npm install --save @types/chrome
 */

class AVKOptions {
    aOpts : Array<any>;
    constructor ( pAr : Array<any> = [] ) {
        this.aOpts = pAr;
    }
    setState (  sOpt : string, bState : boolean ) : boolean{
        this.aOpts = this.aOpts.filter( (e) => {
            if (e[0] === sOpt) e[1] = bState;
             return true;
        } );
        return bState;
    }
    getState ( sOpt : string ) : boolean {
        return this.aOpts.filter( (e) => {
            if (e[0] === sOpt) {
                return true;
            }            
        })[0][1];
    }
    getFeedback ( sOpt : string ) : string {
        return this.aOpts.filter( (e) => {
            if (e[0] === sOpt) {
                return true;
            }            
        })[0][2];
    }
}
export let options = new AVKOptions([["audio", false, "Click Sounds"],
["autohide", false, "Hide if not in use"], ["blur", false, "Blur Text"],
["hover", false, "Hover No Click"], ["opaque", false, "Cannot See Through"],
["scramble", false, "Rearrange Keys"], ["theme", false, "Daytime theme"]]);

2020-12-07

Web Music Ad Blocker Snippet
Web Music Ad Blocker Snippet
Automatically mute the Music player when Ads are playing and unmute when they are done (in Chrome).
/* Install: Open Chrome Dev Tools (Command+option+I on Mac). Menu > Sources > Snippets
   Install: Create a new Snippet named musicADify.js, Paste this script, Save (Command+S).
   Usage: Run the Snippet once each time you start the Music Web Player.
   Usage: Right-Click the snippet named musicADify.js and choose Run from the drop-down.
   Usage: Close Chrome Dev Tools. 🏖️ Play your Jams! 🎶

╔═╗┌─┐┌─┐┌┬┐┬┌─┐┬ ┬  ╔═╗┌┬┐┌─┐
╚═╗├─┘│ │ │ │├┤ └┬┘  ╠═╣ ││└─┐
╚═╝┴  └─┘ ┴ ┴└   ┴   ╩ ╩─┴┘└─┘ */

 let spotADify = ( (_d, _q, _t) => {
    let eS = _d.querySelector( _q ), bS = true;
    if( eS ){ // 🏖️ Play your Jams! 🎶
        bS = ( eS.getAttribute("aria-label") == "Mute" );
        setInterval( () => {spotADify.tick();}, _t);
        return {
        "tick":  () => {
          if((_d.title.indexOf("Adve") != -1) || (_d.title.indexOf("Spoti") != -1)){
              if(  bS ){ eS.click(); bS=!true; }
          }else{
              if( !bS ){ eS.click(); bS=true; }
          }
        }
      }
    }
  })( document, "[aria-label='Mute'],[aria-label='Unmute']", 256);

2020-12-07

Capture Entire DOM State into Inline CSS Snapshot
Capture Entire DOM State into Inline CSS Snapshot
Save As HTML a snapshot capture of entire DOM State with inline CSS
// Desc: Save As HTML a snapshot capture of entire DOM State with inline CSS
// Usage: Just paste this code into the console 🌴
/*  _________             __    __     ____  __.                                  
  /   _____/ ____  _____/  |__/  |_  |    |/ _|___________   __ __  ______ ____  
  \_____  \_/ ___\/  _ \   __\   __\ |      < \_  __ \__  \ |  |  \/  ___// __ \ 
  /        \  \__(  <_> )  |  |  |   |    |  \ |  | \// __ \|  |  /\___ \\  ___/ 
 /_______  /\___  >____/|__|  |__|   |____|__ \|__|  (____  /____//____  >\___  >   ES2022*/


function computedCSS2inline(element, options = {}) {
    if (!element) {
        throw new Error("No element specified.");
    }

    if (options.recursive) {
        Array.from( element.children ).forEach(child => {
        computedCSS2inline(child, options);
        });
    }

    const computedStyle = getComputedStyle(element);
    //(options.properties || computedStyle)::each(property => {
        Array.from( computedStyle ).forEach(property => {
        element.style[property] = computedStyle.getPropertyValue(property);
        //element.setAttribute("class", "")
    });
}

computedCSS2inline(document.body, {recursive: true});

[ ... document.querySelectorAll("script, link, style")].forEach(function(s){ s.outerHTML = ""})

async function saveToFile() {
    const handle = await showSaveFilePicker({
        suggestedName: 'grabbed.html',
        types: [{
            description: 'HTML',
            accept: {'text/html': ['.html']},
        }]
    });
    const writable = await handle.createWritable();
    await writable.write(document.body.parentNode.innerHTML);
    writable.close();
};
console.log("NOTE: Run saveToFile() in console!")

2020-09-16

Real-world Vetted Snippets

JavaScript TypeScript Emerging Tech