HTML Form Validation Patterns
Scott C. Krause | Wednesday, Oct 18, 2023
JavaScript UX and Data Integrity
Modern Web Form Validation Patterns
Ensuring that valid data is entered into a form on a web page has been a fundamental requirement since the inception of the web. While web forms have evolved over the years the goal remains the same:
As web pages have grown into web apps it is no longer acceptable to refresh the entire page every time a form is submitted. It’s no longer acceptable to make a round trip to the server only to notify the end user that some required data is missing.
In this article, I will introduce a few simple patterns to passively enforce data integrity with a delightful user experience using only vanilla JavaScript.
You will learn why the passive validation model works well in modern, semantic, mobile first single-page web apps (SPA).
Client Data Validation vs. Server Data Integrity
Because our focus is client-side validation, we do this in the browser / native app, primarily to make the user experience smooth. However there is still a need for server-side integrity checks. We should strive to conceptually separate concerns between the client’s data validation logic and the server’s data integrity logic. One reason to maintain server-side data integrity checks is that the browser can be tampered with. Meaning that a bad actor can easily modify the page to circumvent our validation logic and send bad data to the server. Server application data verification is a good thing, and while it may seem redundant, it is necessary.
A Brief History of Web Form Validation
Millions of years ago, back in the 1990s. Web forms would either POST or GET to a particular URI, after which they would land / redirect to a separate “thank you” page. The HTML Form element would have to fire a submit event usually actuated via a SUBMIT input or button click. This is significant because early web form validation patterns involved canceling that submit event, validating the data then firing that event once all the data was verified. It is also worth noting that tracking conversion analytics often involves tracking those that landed on the aforementioned “thank you” page.
The Form Validation UX / Micro-copy Objective
Iterating through a form and determining which fields are invalid is the easy part. Communicating that information to the end user in a way that is actionable, predictable, and not overwhelming is the real success metric.
Understand that filling out a form is often the end of a long user journey. It may be the onboarding call-to-action that converts a visitor to a customer. It may be the result of tens of thousands of dollars invested in targeted omni-channel marketing. It would be a catastrophic waste to lose a conversion simply because the visitor became frustrated while filling out the form. For example if you are onboarding a new user to a SaaS product; If this form is submitted successfully then you have a new customer. If it is not successful then you do not have a new customer. It’s a high stakes endeavor.
Micro-copy is the text content that exists within the UI elements, such as labels, button captions, tooltips, placeholders, and toast pop ups. It’s important to utilize micro-copy to set the tone of the engagement as one that is calm, forgiving, helpful, and conversational. Speaking in the voice that one would talk to a friend can make a stressful form easier to use.
Cold & Robotic |
Friendly & Conversational |
Invalid Email Address |
Hmm… That Email Doesn't Look Valid |
SUBMIT |
Okay, Lets do this! |
Fundamental Form UX ( Heuristic Evaluation ) Wisdom
Input fields that are required should be adorned in a way that it is conspicuous. Utilize the placeholder attribute, and / or an red asterisk in the label (if ever-present), and / or a red left border. A tooltip can reinforce this message, however be aware that hover will not work on mobile devices. Also be aware that not all users can distinguish color, so the red adornments should also be heavy (bolded).
A longer form is more difficult to manage than a short form. Longer forms can be incrementally validated per slide or region. Consider a linear carousel with pagination and a Percent Complete Infographic.
Ideally a form can be navigated with the keyboard. It should act upon tab, shift-tab, enter, and escape keystrokes. Perhaps consider a keyboard trap for extra accessibility coverage.
Feedback messaging should be visible in the current viewport. Avoid binding an error message to an INPUT element's X/Y coordinates because that INPUT element might not be in view. Also avoid showing multiple error description messages at the same time for the same reason. The problem with displaying error description text inline with the associated INPUT element is that it could cause the browser to repaint the form, causing some disorientation. Consider an unobtrusive medium such as a Toast message.
Undoubtedly you will want to change the display of an INPUT element to denote that it is in an error state. Whatever visual cue that you decide upon, keep in mind that this change needs to be temporal. Imagine a big red square around an INPUT box that contains an invalid email address. This is going to be both confusing and distracting once the end user corrects the data. The user will be staring at a big red square suggesting that the data is incorrect, while knowing that it is in fact correct. That's a credibility issue. Ideally the error state will display for about two seconds then return to its original state.
Use the various HTML INPUT types, such as email, number, tel, and password. These will allow the user agent to assist the end user in entering data. For example, if the field is an email then a smart phone can display a virtual keyboard that is optimized for entering email addresses.
Let’s start with a simple HTML form. It’s vintage old school, but a good starting point.
You’ll notice that the form has the NOVALIDATE attribute, because we will be providing the validation. Also none of the INPUT elements have the REQUIRED attribute, again because we will be doing the validation.
<form id="contact-us__input" class="l-form" action="#" method="post" novalidate aria-label="Contact information">
<fieldset>
<legend>Your Info:</legend>
<label for="fName">First Name</label>
<input id="fName" name="fName" placeholder="First Name"
autocapitalize="off" autocorrect="off" spellcheck="false" type="text">
<label for="lName">Last Name</label>
<input id="lName" name="lName" placeholder="Last Name"
autocapitalize="off" autocorrect="off" spellcheck="false" type="text">
<label for="eMail">Email Address</label>
<input id="eMail" name="eMail" placeholder="address@domain.com"
autocapitalize="off" autocorrect="off" spellcheck="false" type="email">
<nav>
<button type="submit">Contact Us</button>
</nav>
</fieldset>
</form>
Here we have some simple Mobile First CSS to help layout the form.
<style>
.l-form {font-family: Arial, Helvetica, sans-serif;}
.l-form fieldset * { font-size: 22px; }
.l-form input, .l-form button { margin: 4px; padding: 4px; border: solid 3px #888; border-radius: 6px;}
.l-form nav { text-align: center; }
.l-form .l-form__inp--invalid { border-left: solid 3px #FF0000;}
@media only screen and (min-width:48.1em){/* med *//* lg */
.l-form fieldset * { font-size: 18px; }
}
</style>
The JavaScript logic is implemented as a singleton object that expects the id of a form and a configuration object that defines the validation.
There are polymorphic validation functions, all of which will return true if valid or an error description if invalid.
<script>
class InputValidation {
constructor() {
this.elForm = this.aConfig = this.invalidCount = this.sErrorMsg = null
}
init( sId, aConfig ){
this.elForm = document.getElementById( sId ); this.aConfig = aConfig;
this.elForm.addEventListener( "submit", ( ev )=>{
ev.preventDefault()
this.invalidCount = 0; this.sErrorMsg = "";
Array.from( ev.target?.elements ).forEach( ( el )=>{ // Iterate Form INP
if( el?.tagName == "INPUT" ){
const aVal = this.aConfig.filter( ( aCnf )=>{ return ( aCnf[0] == el.id ) } )[0] // Get first Config by ID
if( aVal ){
aVal.forEach( ( sfValConvert, nDx )=>{
if( nDx ){
let sMsg = InputValidation[ sfValConvert ]( el )
if( sMsg !== true ){
InputValidation.showInvalid( el )
this.invalidCount++
this.sErrorMsg = this.sErrorMsg + " - " + sMsg
if( neodigmToast ) neodigmToast.q( sMsg, "danger")
}
}
} )
}
}
} )
if( this.invalidCount == 0 ) this.elForm.submit()
} )
}
static isEmail( elInp ){ return ( (elInp.value.indexOf( "@" ) != -1 ) && (elInp.value.indexOf( "." ) != -1 ) ) ? true : "Please Enter Valid Email Address" }
static isRequired( elInp ){ return (elInp.value.length) ? true : "Please Enter Required Information" }
static convertCap( elInp ){
if( elInp.value ){
elInp.value = elInp.value.charAt( 0 ).toUpperCase() + elInp.value.slice( 1 ).toLowerCase()
}
return true
}
static showInvalid( el ){
el.classList.add("l-form__inp--invalid")
setTimeout( ()=>{ el.classList.remove("l-form__inp--invalid")}, 2e3 )
}
}
</script>
Usage:
<script>
let inputVal = {}
document.addEventListener("DOMContentLoaded", ()=>{
inputVal = new InputValidation()
inputVal.init( "contact-us__input", [["fName", "isRequired", "convertCap"], ["lName", "isRequired", "convertCap"], ["eMail", "isRequired", "isEmail"]] )
});
</script>
Lets understand the configuration object. It's just an array of arrays with the first value being the ID of INPUT element, the subsequent values are the method names that represent the types of validation functions that we need to check for this element.
<script>
[
[
"fName",
"isRequired",
"convertCap"
],
[
"lName",
"isRequired",
"convertCap"
],
[
"eMail",
"isRequired",
"isEmail"
]
]
</script>
Salesforce Web to Lead in an IFRAME to Prevent Page Refresh
<script>
function fw2l(_oFlds) { // Salesforce Web 2 Lead
let _d = document;
var eIF_id="if-w2l-id";
if(!_d.getElementById(eIF_id)){ // IFRAME
var _eIF=_d.createElement("iframe");
_eIF.id=eIF_id;
_eIF.name=eIF_id;
_eIF.src="about:blank";
_eIF.style.display="none";
_d.body.appendChild(_eIF);
}
_oFlds["retURL"]="http://127.0.0.1"; // Dummy URL
var form = _d.createElement("form");
form.method = "POST"; form.action = "https://webto.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8";
form.setAttribute("target", eIF_id);
for (var fieldName in _oFlds) {
var theInput = oD.createElement("input");
theInput.name=fieldName;
theInput.value=_oFlds[fieldName];
theInput.setAttribute("type", "hidden");
form.appendChild(theInput);
}
oD.body.appendChild(form);
form.submit();
if( rdt ) rdt('track', 'Lead', { "transactionId": Date.now() });
}
function DoForm( sElm, sOther, sToken ){
var oW2L = {oid:"xxxxxxxxxxxxxxx", retURL: "https://www.MachFiveMarketing.com/?thanks"};
var _sN = new Date();
_sN = String( _sN.getTime() ) + "_";
oW2L.email = _sN + sElm; // Make elm unique
oW2L.description = sElm + " | " + sToken + " | " + sOther;
fw2l( oW2L );
return true;
}
</script>
Best up-to-date resource for HTML and JavaScript Form Validation patterns?
Bookmark JavaScript Web Form Validation for code examples and tutorials of client-side Form validation.
What is an accessibility keyboard trap?
A keyboard trap is a restriction on navigation that prevents an end user from exiting a form via the Tab or Shift-Tab keystrokes. It is implemented to assist users who rely on the keyboard to stay focused on the form that they are filling out.
What is Micro-copy and how do I use it to help fill out a form?
Micro-copy is the text content that exists within the UI elements, such as labels, button captions, tooltips, placeholders, and toast pop ups. It’s important to utilize micro-copy to set the tone of the engagement as one that is calm, forgiving, helpful, and conversational. Speaking in the voice that one would talk to a friend can make a stressful form easier to use.
Is JavaScript form validation better than HTML form validation?
Yes. JavaScript can provide a better UX and test for more complex types of errors than HTML alone. For example testing whether two password fields are the same. Or that a new password is sufficiently complex (password strength test).
Is client-side JavaScript sufficient to protect FORM data integrity?
No. Server-side data integrity checks are still important because the client can be compromised and changed in a way to submit bad data. Modern APIs may accept posts from web clients or other APIs (webhooks). This is a good argument for always performing data integrity checks because the source may change over time. Server application data verification is a good thing, and while it may seem redundant, it is necessary.
Can a Salesforce Web to Lead Form be submitted without refreshing the page?
Yes, a common workaround is to manage the SFDC form within a hidden iframe. This has the added advantage of allowing you to filter out scripts that look for Salesforce Web to Lead forms and submit spam.
Can an HTML form be filled out while offline then submitted when online?
Yes. Modern browsers can detect when you are offline. These events can direct your form to be stored locally in the IndexDB. Once the device has network connectivity another event will fire. That event can check for the existence of an offline form and submit it in the background. This ability is common in all major browsers and a service worker is not required.
Emerging Tech
HTML Form Validation Patterns
Curated JavaScript Form Validation Content
2023-10-18
Emerging Tech
HTML Over the Wire
Curated HTMX & Alpine.js Knowledge-base
2023-08-05
Emerging Tech
Capacitor WASM Custom Plugins
Ionic Curated Capacitor Links
2023-08-05
Emerging Tech
Neodigm 55 Low Code UX micro-library
Popups, Toast, Parallax, and SFX
2022-11-25
Emerging Tech
UX Usability Heuristic Evaluation
HE Heuristic Evaluation
2022-10-19
Emerging Tech
New Macbook Setup for Developers
New Macbook Config for Devs 2023
2022-08-24
Emerging Tech
WebAssembly WASM
In-depth Curated WebAssembly Links
2022-08-05
Emerging Tech
Curated PWA Links
Indispensable Curated PWA Links
2022-07-09
Emerging Tech
Curated GA4 Links
Indispensable Curated Google Analytics 4 Links
2022-06-17
Emerging Tech
Curated Lit Web Component Links
Curated Lit Web Component Links
2022-03-25
Emerging Tech
Curated LWC Links
Indispensable Curated LWC Links
2021-08-09
Emerging Tech
Curated TypeScript Vue.js
Indispensable Curated Vue TypeScript
2021-07-24
Emerging Tech
Creative 3D animation resources
Indispensable Curated Creative Links
2021-06-25
Emerging Tech
Transition to TypeScript
Indispensable Curated TypeScript Links
2021-06-05
Emerging Tech
The Clandestine Dead Drop
The Ironclad Clandestine Dead Drop
2021-05-31
Emerging Tech
Curated Blogfolios Links
Personal Websites
2021-03-15
Emerging Tech
Curated JavaScript Links
Indispensable Curated JavaScript Links
2021-03-12
Emerging Tech
Curated Emerging Tech Links
Indispensable Curated Tech Links
2021-03-04
Emerging Tech
Cytoscape Skills Data Visualization
Persuasive Infographics & Data Visualizations
2021-02-20
Emerging Tech
eCommerce Accessibility A11y
Accessibility Challenges Unique to eCommerce
2020-12-07
Emerging Tech
Roll Dice in High-Fidelity 3D
Create 3D Dice with Strong Random Entropy
2020-11-02
Simple valid JSON test
A simple JavaScript function that can determine if a given JSON is valid.
2023-12-07
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
Generate Lorem Ipsum Text
JavaScript Generate Lorem Ipsum from original Latin De finibus.
/*
,--. ,--.
| | ,---. ,--.--. ,---. ,--,--,--. `--' ,---. ,---. ,--.,--.,--,--,--.
| || .-. || .--'| .-. :| | ,--.| .-. |( .-' | || || |
| |' '-' '| | \ --.| | | | | || '-' '.-' `)' '' '| | | |
`--' `---' `--' `----'`--`--`--' `--'| |-' `----' `----' `--`--`--'
`--' 🌶️ 🌴 🍰 🔥 🗝️ 🎲 */
const genLoremIpsum = ( Sentences=1 )=>{ // Generate Lorem Ipsum | Orig Latin De finibus
if( Sentences == -1 ) Sentences = Math.floor(Math.random() * 5) + 1 // If -1 gen rnd num sentences 1-5
const aLI = "lorem ipsum a ab accusamus accusantium ad adipiscing alias aliquam aliquid amet animi aperiam architecto asperiores aspernatur assumenda at atque aut autem beatae blanditiis commodi consectetur consequatur consequuntur corporis corrupti culpa cum cumque cupiditate debitis delectus deleniti deserunt dicta dignissimos distinctio do dolor dolore dolorem doloremque dolores doloribus dolorem dquis ducimus ea eaque earum eius eligendi enim eos error ert esse est et eum eveniet ex excepturi exercitationem expedita explicabo facere facilis fuga fugiat fugit harum hic id illo illum impedit in incididunt inventore ipsa ipsam irure iste itaque iusto labore laboriosam laborum laudantium libero magnam magni maiores maxime minima minus modi molestiae molestias mollitia nam natus necessitatibus nemo neque nesciunt nihil nisi nobis non nostrumd nulla numquam obcaecati odio odit officia officiis omnis optio pariatur perferendis perspiciatis placeat porro possimus praesentium provident quae quaerat quam quas quasi qui quia quibusdam quidem quis quisquam quo quod quos ratione recusandae reiciendis rem repellat repellendaus reprehenderit repudiandae rerudum rerum saepe sapiente sed sequi similique sint sit soluta sunt suscipit tempora tempore temporibus tenetur totam ullam unde ut vel velit veniam veritatis vero vitae voluptas voluptate voluptatem voluptates voluptatibus voluptatum".split(" ")
let sOut = ""
for( let nS = 0; nS <= Sentences; nS++){
let nWc = Math.floor(Math.random() * 6) + 3 // Word count per sentence rnd 3-8
for( let nW = 0; nW <= nWc; nW++){
let sWrd = aLI[ Math.floor(Math.random() * aLI.length) ]
if( !nW ) sWrd = sWrd[0].toUpperCase() + sWrd.slice(1) // Cap first
if( sOut.indexOf( sWrd ) == -1 ) sOut += " " + sWrd // Dedupe
}
sOut += "."
}
return sOut.trim();
}
// USAGE: console.log( genLoremIpsum( -1 ) )
2023-10-07
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
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
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
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
// 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
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
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
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
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
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
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.
/* __ __ __ __ __ __ __
/\ \/\ \ /\ \ /\ \/\ \ /\_\_\_\
\ \ \_\ \ \ \ \ \ \ \_\ \ \/_/\_\/_
\ \_____\ \ \_\ \ \_____\ /\_\/\_\
\/_____/ \/_/ \/_____/ \/_/\/_/ */
class NeodigmPopTart {
constructor(_d, _aQ) { // Orthogonal Diagonalizer 🌶️ Protomolecule
this._d = _d; this._aQ = _aQ; this.oPopTmpls = {}
this.elBound = null; this.sBoundTheme = neodigmOpt.N55_THEME_DEFAULT
this.fOnBeforeOpen = {}; this.fOnAfterOpen = {}; this.fOnClose = {}
this.bIsOpen = this.bIsInit = false
}
init() {
if( !this.bIsInit ){ // once
this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].addEventListener("mouseover", ( ev ) => { // data-n55-poptart-hover
if( ev.target?.dataset?.n55PoptartHover ){
const sAttrEv = ev.target?.dataset?.n55PoptartHover // || ev?.srcElement?.parentNode?.dataset?.n55PoptartHover
this.sBoundTheme = ev.target.n55Theme || ev.target?.dataset?.n55Theme || ev.target?.parentNode?.dataset?.n55Theme || neodigmOpt.N55_THEME_DEFAULT
if( this.sBoundTheme != "disabled" ) {
let elPopTmpl = this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].querySelector( "#" + sAttrEv )
if( elPopTmpl?.dataset?.n55Poptart ){
this.elBound = ev.target
this.sBoundTheme = this.elBound ||neodigmOpt.N55_THEME_DEFAULT
ev.preventDefault()
neodigmPopTart.open( this.oPopTmpls[ sAttrEv ] = elPopTmpl, JSON.parse( elPopTmpl.dataset.n55Poptart ) )
}
}
}
}, false)
this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].addEventListener("contextmenu", ( ev ) => { // data-n55-poptart-rightclick
if( ev.target?.dataset?.n55PoptartRightclick || ev.target?.parentNode?.dataset?.n55PoptartRightclick ){
const sAttrEv = ev.target?.dataset?.n55PoptartRightclick || ev.target?.parentNode?.dataset?.n55PoptartRightclick
neodigmPopTart.click_and_right_click( ev, sAttrEv )
}
}, false)
this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].addEventListener("click", ( ev ) => { // 👁️ Outside Click
if( this.bIsOpen ){
let eTarget = ev.target, bInside = false;
while( eTarget.tagName !== "HTML" ){
if( eTarget.dataset.n55PoptartOpen ){ bInside = true; break; }
eTarget = eTarget.parentNode;
}
if( !bInside ){ neodigmPopTart.close() }
}else{ // data-n55-poptart-click
if( ev.target?.dataset?.n55PoptartClick || ev.target?.parentNode?.dataset?.n55PoptartClick ){
const sAttrEv = ev.target?.dataset?.n55PoptartClick || ev.target?.parentNode?.dataset?.n55PoptartClick
neodigmPopTart.click_and_right_click( ev, sAttrEv )
}
}
}, true)
this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].addEventListener("keydown", ( ev ) => { // Close on Esc Key
if ( ev.key == "Escape" ){ if( this.bIsOpen ) this.close() }
}, true)
this.bIsInit = true
}
return this;
}
click_and_right_click( ev, sAttrEv, bPrevDef=true ){
this.sBoundTheme = ev.target.n55Theme || ev.target?.dataset.n55Theme || ev.target?.parentNode?.dataset.n55Theme || neodigmOpt.N55_THEME_DEFAULT
if( this.sBoundTheme != "disabled" ) {
let elPopTmpl = this._d[ neodigmOpt.N55_APP_STATE.CONTEXT ].querySelector( "#" + sAttrEv )
if( elPopTmpl?.dataset?.n55Poptart ){
this.elBound = ev.target
if( bPrevDef ) ev.preventDefault()
neodigmPopTart.open( this.oPopTmpls[ sAttrEv ] = elPopTmpl, JSON.parse( elPopTmpl.dataset.n55Poptart ) )
}
}
}
open( elPop, oPos ) {
if( this.bIsInit && !this.bIsPause && elPop.id && !elPop.dataset?.n55PoptartOpen ) {
let nOffSetT, nOffSetL, nOffSetH, nOffSetW; // oPos offset conf
nOffSetT = nOffSetL = nOffSetH = nOffSetW = 0;
if( oPos?.offset ){
nOffSetH = oPos?.offset?.h || 0
nOffSetL = oPos?.offset?.l || 0
nOffSetT = oPos?.offset?.t || 0
nOffSetW = oPos?.offset?.w || 0
}
let oRctBound = this.elBound.getBoundingClientRect()
let pxLft = window.pageXOffset || this._d.documentElement.scrollLeft
let pxTop = window.pageYOffset || this._d.documentElement.scrollTop
const NOFFSET = 10
// Allow pre CB to cancel open
if( this.fOnBeforeOpen[ elPop.id ] ){ if( !this.fOnBeforeOpen[ elPop.id ]() ) return false; }
if( this.fOnBeforeOpen[ "def" ] ){ if( !this.fOnBeforeOpen[ "def" ]() ) return false; }
elPop.dataset.n55PoptartOpen = Date.now()
let oRctPopCt = elPop.getBoundingClientRect()
oPos.w = ( ( oPos.w ) ? oPos.w : ( oRctBound.width + nOffSetW ) ) // W
oPos.x = ( ( oPos.x ) ? oPos.x : ( ( oRctBound.left + (oRctBound.width / 2) ) - ( oPos.w / 2) + pxLft + nOffSetL ) ) // X // TODO calc and align x center of bound elm
oPos.y = ( ( oPos.y ) ? oPos.y : ( oRctBound.top + pxTop - nOffSetT ) ) // Y
oPos.z = ( ( oPos.z ) ? oPos.z : neodigmOpt.N55_ZIND.PopTart ) // Z
oPos.h = ( ( oPos.h ) ? (oPos.h + nOffSetH) : "auto" ) // H
oPos.position = ( ( oPos.position ) ? oPos.position : "bottom" ) // P
switch( oPos.position ){
case "top":
oPos.y = ( oPos.y - ( oRctBound.height + oRctPopCt.height ) + NOFFSET )
break
case "right":
oPos.x = ( oPos.x + oRctBound.width )
break
case "bottom":
oPos.y = ( oPos.y + oRctBound.height ) + NOFFSET
break
case "left":
oPos.x = ( oPos.x - oRctBound.width )
break
}
elPop.style.left = oPos.x + "px"; elPop.style.top = oPos.y + "px"; elPop.style.width = oPos.w + "px"
elPop.style.height = ( oPos.h == "auto" ) ? "auto" : oPos.h + "px";
//elPop.style.position = "absolute";
elPop.style.zIndex = oPos.z;
if( !elPop.dataset?.n55Theme ) elPop.dataset.n55Theme = this.sBoundTheme // Inherit Theme from Bound El, may be flash theme
if( neodigmOpt.N55_GTM_DL_POPTRT ) neodigmUtils.doDataLayer( neodigmOpt.N55_GTM_DL_POPTRT, elPop.id )
this.bIsOpen = true
if( this.fOnAfterOpen[ elPop.id ] ) this.fOnAfterOpen[ elPop.id ]()
if( this.fOnAfterOpen["def"] ) this.fOnAfterOpen["def"]()
}
return this;
}
close() {
for( let e in this.oPopTmpls ){
if( this.oPopTmpls[ e ]?.dataset?.n55PoptartOpen ){
let sId = this.oPopTmpls[ e ]?.id
let bOkClose = true // CBs must explicitly return false to prevent closing
if( neodigmOpt.N55_DEBUG_lOG ) console.log( "~Poptart Close | " + sId, this.fOnClose[ sId ] )
if( this.fOnClose[ sId ] ) bOkClose = !(this.fOnClose[ sId ]( sId ) === false) // The specific can cancel the generic
if( bOkClose && this.fOnClose["def"] ) bOkClose = !(this.fOnClose["def"]( sId ) === false)
if( bOkClose ){
delete this.oPopTmpls[ e ].dataset.n55PoptartOpen;
this.bIsOpen = false
}
}
}
return this;
}
pause ( nT ){
if( this.bIsInit ){
if( nT ) setTimeout( () =>{neodigmPopTart.play()}, nT )
this.bIsPause = true; return this;
}
}
play (){
this.bIsPause = false;
return this;
}
shake( bSound = true) { // Shake All Open
if(this.bIsInit && this.bIsOpen) {
if( neodigmOpt.neodigmWired4Sound ) neodigmWired4Sound.doHaptic([8, 32, 48])
for( let e in this.oPopTmpls ){
if( this.oPopTmpls[ e ]?.dataset?.n55PoptartOpen ){
this.oPopTmpls[ e ].classList.add("ndsp__opened--shake1");
setTimeout(function(){
neodigmPopTart.oPopTmpls[ e ].classList.remove("ndsp__opened--shake1");
}, 460)
}
}
if( bSound && neodigmOpt.neodigmWired4Sound && neodigmOpt.EVENT_SOUNDS ) neodigmWired4Sound.sound( 13, "QUITE" )
if( neodigmOpt.neodigmWired4Sound ) neodigmWired4Sound.doHaptic([48, 32, 8])
}
return this
}
isOpen(){ return this.bIsOpen }
setOnBeforeOpen( _f, id="def"){ this.fOnBeforeOpen[ id ] = _f }
setOnAfterOpen( _f, id="def"){ this.fOnAfterOpen[ id ] = _f }
setOnClose( _f, id="def"){ this.fOnClose[ id ] = _f }
}
2020-12-16
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
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
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 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 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
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
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
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
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