Last active
January 16, 2023 20:35
-
-
Save tomhodgins/c22973a0990248b244cd56c7641b31f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset=utf-8> | |
<meta name=viewport content="width=device-width, initial-scale=1"> | |
<title>#merryCSSmas bundle</title> | |
<style> | |
* { | |
box-sizing: border-box; | |
text-rendering: optimizeLegibility; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
font-kerning: auto; | |
} | |
html { | |
font-size: 12pt; | |
line-height: 1.4; | |
font-weight: 400; | |
font-family: sans-serif; | |
} | |
body { | |
padding: 1em; | |
margin: 0 auto; | |
max-width: 800px; | |
} | |
code, pre, blockquote { | |
padding: .2em; | |
background: rgba(0,0,0,.1); | |
} | |
code, pre { | |
font-family: 'Fira Code', monospace; | |
} | |
h1, h2, h3, h4, h5, h6 { | |
margin: 0 0 .5em 0; | |
line-height: 1.2; | |
letter-spacing: -.02em; | |
} | |
h2 { | |
margin-top: 1.5em; | |
} | |
input, | |
button, | |
textarea { | |
display: block; | |
width: 100%; | |
max-width: 500px; | |
margin: 1em auto; | |
padding: .5em; | |
font-size: 14pt; | |
} | |
@media (min-width: 600px) { | |
h1 { font-size: 300%; } | |
h2 { font-size: 200%; } | |
h3 { font-size: 180%; } | |
h4 { font-size: 160%; } | |
h5 { font-size: 140%; } | |
h6 { font-size: 120%; } | |
} | |
</style> | |
<h1>#merryCSSmas bundle</h1> | |
<p>This is a demo page including all of the jsincss plugins shown in this year's <a href=https://twitter.com/innovati/status/1068998114491678720>#merryCSSmas</a> twitter thread.</p> | |
<p>You can find the plugins in <a href=merrycssmas-bundle.js>merrycssmas-bundle.js</a>, and for this demo the styles are expressed in CSS quoted here in this HTML page alongside the HTML tests they style. This demo page uses <a href=https://www.npmjs.com/package/deqaf>deqaf</a> to parse the JS-powered custom selectors and custom at-rules from CSS, but if you wanted to do this kind of parsing and processing (to only serve regular CSS and the JavaScript that's required to make the JS-powered styles you expressed in your CSS work) you can use <a href=https://www.npmjs.com/package/qaffeine>qaffeine</a> server-side.</p> | |
<h2><a href=https://twitter.com/innovati/status/1069000937321033728>Parent Selector</a></h2> | |
<ul> | |
<li>one | |
<li class=parent-demo>two | |
<li>three | |
</ul> | |
<style> | |
.parent-demo[--parent] { | |
border: 10px dashed purple; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1069281303135170561>:has() Selector</a></h2> | |
<ul> | |
<li>one | |
<li class=has-demo>two | |
<li>three | |
</ul> | |
<style> | |
ul[--has='".has-demo"'] { | |
border: 10px dashed teal; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1069658039416950784>Closest Selector</a></h2> | |
<div class=finish>Finish | |
<div>div | |
<div class=finish>Finish | |
<div>div | |
<div class=start>Start</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<style> | |
.start[--closest='".finish"'] { | |
border: 10px dashed gold; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1070077491342176257>First-in-Document Selector</a></h2> | |
<ul class=first-demo> | |
<li>one | |
<li>two | |
<li>three | |
</ul> | |
<ul class=first-demo> | |
<li>four | |
<li>five | |
<li>six | |
</ul> | |
<style> | |
.first-demo li[--first] { | |
background: red; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1070351130486558721>Last-in-Document Selector</a></h2> | |
<ul class=last-demo> | |
<li>one | |
<li>two | |
<li>three | |
</ul> | |
<ul class=last-demo> | |
<li>four | |
<li>five | |
<li>six | |
</ul> | |
<style> | |
.last-demo li[--last] { | |
background: green; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1070744102168334336>Elder Sibling Selector</a></h2> | |
<ul> | |
<li>one | |
<li>two | |
<li>three | |
<li>four | |
<li>five | |
<li class=elder-demo>six | |
<li>seven | |
<li>eight | |
<li>nine | |
<li>ten | |
</ul> | |
<style> | |
.elder-demo[--elder='"*"'] { | |
background: red; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1071126541411762176>Previous Sibling Selector</a></h2> | |
<ul> | |
<li>one | |
<li>two | |
<li class=previous-demo>three | |
<li>four | |
<li>five | |
</ul> | |
<style> | |
.previous-demo[--previous] { | |
background: green; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1071450464825286658>Select by Text Content</a></h2> | |
<ul class=string-match> | |
<li>example | |
<li>test | |
<li>demo | |
<li>illustration | |
<li>prototype | |
</ul> | |
<style> | |
.string-match li[--contains='"demo"'] { | |
background: red; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1071784033376440321>Regex Selector</a></h2> | |
<ul class=regex-match> | |
<li>test | |
<li>demo | |
<li>example | |
<li>testing | |
<li>demonstration | |
</ul> | |
<style> | |
.regex-match li[--regex='"[aeiou]m"'] { | |
background: green; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1072118941965791232>Computed Style Selector</a></h2> | |
<ul> | |
<li>item | |
<li class=computed-demo>item | |
<li>item | |
</ul> | |
<style> | |
.computed-demo { | |
font-family: courier; | |
} | |
[--computed='"font-family", "courier"'] { | |
background: red; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1072542135365320704>Nth-Letter and Nth-Word</a></h2> | |
<p><strong>:nth-letter()</strong></p> | |
<h1 class=letter>I'm an H1 Headline</h1> | |
<h2 class=letter>I'm an H2 Headline</h2> | |
<h3 class=letter>I'm an H3 Headline</h3> | |
<h4 class=letter>I'm an H4 Headline</h4> | |
<h5 class=letter>I'm an H5 Headline</h5> | |
<h6 class=letter>I'm an H6 Headline</h6> | |
<p><strong>:nth-word()</strong></p> | |
<h1 class=word>I'm an H1 Headline With Seven Words</h1> | |
<h2 class=word>I'm an H2 Headline With Seven Words</h2> | |
<h3 class=word>I'm an H3 Headline With Seven Words</h3> | |
<h4 class=word>I'm an H4 Headline With Seven Words</h4> | |
<h5 class=word>I'm an H5 Headline With Seven Words</h5> | |
<h6 class=word>I'm an H6 Headline With Seven Words</h6> | |
<style> | |
h1.letter[--nth-letter="1"] { background: red; } | |
h2.letter[--nth-letter="2"] { background: orange; } | |
h3.letter[--nth-letter="3"] { background: yellow; } | |
h4.letter[--nth-letter="4"] { background: green; } | |
h5.letter[--nth-letter="5"] { background: blue; } | |
h6.letter[--nth-letter="6"] { background: indigo; } | |
h1.word[--nth-word="1"] { background: cyan; } | |
h2.word[--nth-word="2"] { background: magenta; } | |
h3.word[--nth-word="3"] { background: yellow; } | |
h4.word[--nth-word="4"] { background: red; } | |
h5.word[--nth-word="5"] { background: green; } | |
h6.word[--nth-word="6"] { background: blue; } | |
h1.letter[--nth-letter="10"] { | |
background: orange; | |
} | |
h1.word[--nth-word="4"] { | |
color: orange; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1072878974584455168>Media Pseudo-Classes</a></h2> | |
<video src=https://lookie.ml/uploads/IBIoX.mp4 controls loop></video> | |
<style> | |
video[--playing] { | |
border: 10px solid hotpink; | |
} | |
video[--paused] { | |
transform: scale(.75); | |
} | |
video[--muted] { | |
opacity: .5; | |
} | |
video[--current-time='{"greater": 3}'] { | |
filter: invert(100%); | |
} | |
video { | |
max-width: 100%; | |
transition: | |
opacity .2s ease-in-out, | |
transform .2s ease-in-out, | |
border-width .2s ease-in-out | |
; | |
} | |
</style> | |
<script> | |
document.querySelectorAll('video').forEach(tag => | |
// these events send 'reprocess' event to jsincss | |
[ | |
'play', | |
'playing', | |
'pause', | |
'seeked', | |
'seeking', | |
'ended', | |
'timeupdate', | |
'volumechange' | |
].forEach(event => | |
tag.addEventListener( | |
event, | |
() => window.dispatchEvent(new Event('reprocess')) | |
) | |
) | |
) | |
</script> | |
<h2><a href=https://twitter.com/innovati/status/1073259512436547584>@Document Queries</a></h2> | |
<div class=document-demo>@Document Demo</div> | |
<style> | |
.document-demo { | |
border: 1px solid; | |
padding: 1em; | |
} | |
/* @document regexp("pen") {} */ | |
@supports (--document({"regexp": "demo"})) { | |
.document-demo { | |
background: green; | |
} | |
.document-demo::after { | |
content: ' works because a regex of /demo/ matched this URL' | |
} | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1073623743430356993>:not(:blank):valid & :not(:blank):invalid</a></h2> | |
<form> | |
<input required placeholder="Enter your first name…"> | |
<input required placeholder="Enter your last name…"> | |
<input type=email required placeholder="Enter your email…"> | |
<button>Submit</button> | |
</form> | |
<style> | |
form input[--not-blank-valid] { | |
border-color: green; | |
background-color: lightgreen; | |
} | |
form input[--not-blank-invalid] { | |
border-color: red; | |
background-color: pink; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1073955756368633862>Element Queries</a></h2> | |
<p>min-characters on form inputs (Use keyboard)</p> | |
<input class=mincharacters placeholder="Type 5+ characters…"> | |
<textarea class=mincharacters placeholder="Type 5+ characters…"></textarea> | |
<style> | |
@supports (--element(".mincharacters", {"minCharacters": 5})) { | |
[--self] { | |
background: green; | |
} | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1074298517328486402>Attribute Comparison Selector</a></h2> | |
<label> | |
<p>Drag me!</p> | |
<input type=range value=50 data-price=50 oninput="dataset.price=value"> | |
</label> | |
<style> | |
[type=range][--attr='"data-price", "less", 25'] { | |
box-shadow: red 0 0 0 10px; | |
} | |
[type=range][--attr='"data-price", "greaterEqual", 25'] { | |
box-shadow: orange 0 0 0 10px; | |
} | |
[type=range][--attr='"data-price", "greaterEqual", 50'] { | |
box-shadow: yellow 0 0 0 10px; | |
} | |
[type=range][--attr='"data-price", "greaterEqual", 75'] { | |
box-shadow: yellowgreen 0 0 0 10px; | |
} | |
[type=range][--attr='"data-price", "greater", 90'] { | |
box-shadow: lime 0 0 0 10px; | |
} | |
[type=range] { | |
width: 100%; | |
font-size: 20pt; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1074770741437100032>Custom Specificity</a></h2> | |
<ul class=specificity-demo> | |
<li>item | |
<li class=demo>class | |
<li class=demo id=demo>id and class | |
</ul> | |
<style> | |
.specificity-demo li[--specificity='2'] { | |
background: gold; | |
} | |
.specificity-demo li.demo[--specificity='3'] { | |
background: green; | |
} | |
.specificity-demo li#demo.demo[--specificity='1'] { | |
background: red; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1075105610453147648>Viewport Visibility Queries</a></h2> | |
<section class=viewport-demo> | |
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti facilis quaerat consectetur cupiditate, quasi magnam optio amet, totam doloremque praesentium dicta, et corrupti! Debitis, sapiente labore qui fugiat nemo obcaecati.</p> | |
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti facilis quaerat consectetur cupiditate, quasi magnam optio amet, totam doloremque praesentium dicta, et corrupti! Debitis, sapiente labore qui fugiat nemo obcaecati.</p> | |
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti facilis quaerat consectetur cupiditate, quasi magnam optio amet, totam doloremque praesentium dicta, et corrupti! Debitis, sapiente labore qui fugiat nemo obcaecati.</p> | |
</section> | |
<style> | |
@supports (--viewport(".viewport-demo p", "partly")) { | |
[--self] { | |
background: green; | |
} | |
[--options] { | |
--selector: window; | |
--events: ["load", "resize", "scroll"]; | |
} | |
} | |
@supports (--viewport(".viewport-demo p", "fully")) { | |
[--self] { | |
color: red; | |
} | |
[--options] { | |
--selector: window; | |
--events: ["load", "resize", "scroll"]; | |
} | |
} | |
.viewport-demo p { | |
margin: 20vh 0; | |
padding: 1em; | |
font-size: 14pt; | |
border: 1px solid; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1075441085323845637>Horizontal Overflow Queries</a></h2> | |
<div class=overflow> | |
<pre>Lorem ipsum dolor sit amet.</pre> | |
<span class=left></span> | |
<span class=right></span> | |
</div> | |
<div class=overflow> | |
<pre>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</pre> | |
<span class=left></span> | |
<span class=right></span> | |
</div> | |
<div class=overflow> | |
<pre>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor.</pre> | |
<span class=left></span> | |
<span class=right></span> | |
</div> | |
<div class=overflow> | |
<pre>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore.</pre> | |
<span class=left></span> | |
<span class=right></span> | |
</div> | |
<div class=overflow> | |
<pre>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.</pre> | |
<span class=left></span> | |
<span class=right></span> | |
</div> | |
<style> | |
@supports (--overflow(".overflow pre", "left")) { | |
.overflow [--self] ~ .left { | |
opacity: 1; | |
} | |
} | |
@supports (--overflow(".overflow pre", "right")) { | |
.overflow [--self] ~ .right { | |
opacity: 1; | |
} | |
} | |
.overflow { | |
margin: 1em; | |
position: relative; | |
} | |
.overflow pre { | |
margin: 0; | |
padding: 2em; | |
font-family: sans-serif; | |
white-space: pre; | |
overflow-x: auto; | |
} | |
.overflow .left, | |
.overflow .right { | |
display: block; | |
width: 75px; | |
height: 100%; | |
position: absolute; | |
top: 0; | |
opacity: 0; | |
pointer-events: none; | |
transition: .5s ease-in-out; | |
} | |
.overflow .left { | |
left: 0; | |
background: linear-gradient(90deg, rgba(0,0,0,.2) 0%, rgba(0,0,0,0) 100%); | |
} | |
.overflow .right { | |
right: 0; | |
background: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.2) 100%); | |
} | |
</style> | |
<script> | |
document.querySelectorAll('.overflow pre').forEach(tag => | |
tag.addEventListener( | |
'scroll', | |
e => window.dispatchEvent(new Event('reprocess')) | |
) | |
) | |
</script> | |
<h2><a href=https://twitter.com/innovati/status/1075805194326339586>Navigator Queries</a></h2> | |
<div class=navigator-demo>Navigator query demo</div> | |
<style> | |
/* Avoids Firefox */ | |
@supports (--navigator({"regex": ["userAgent", "^((?!Firefox).)*$"]})) { | |
.navigator-demo { | |
background: red; | |
} | |
} | |
/* Avoids Firefox and targets Chrome */ | |
@supports (--navigator({"excludes": ["userAgent", "Firefox"], "includes": ["userAgent", "Chrome"]})) { | |
.navigator-demo { | |
background: green; | |
} | |
} | |
.navigator-demo { | |
border: 1px solid; | |
padding: 1em; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1076239363766398976>Storage Queries</a></h2> | |
<div class=storage-demo>Storage Demo</div> | |
<style> | |
@supports (--localStorage({"exists": ["demo"]})) { | |
.storage-demo { | |
background: red; | |
} | |
} | |
@supports (--sessionStorage({"includes": ["demo", "test"]})) { | |
.storage-demo { | |
color: green; | |
} | |
} | |
.storage-demo { | |
border: 1px solid; | |
padding: 1em; | |
} | |
</style> | |
<script> | |
localStorage.demo = 'I contain a test string' | |
sessionStorage.demo = 'I contain a test string' | |
</script> | |
<h2><a href=https://twitter.com/innovati/status/1076476126304833536>Date Queries</a></h2> | |
<div class=date-demo></div> | |
<style> | |
@supports (--date({"before": ["December 25 2018"]})) { | |
.date-demo::before { | |
content: "Waiting for Christmas…"; | |
} | |
} | |
@supports (--date({"on": ["December 25 2018"]})) { | |
.date-demo::before { | |
content: "Merry Christmas!"; | |
color: red; | |
background: green; | |
} | |
} | |
@supports (--date({"after": ["December 26 2018"]})) { | |
.date-demo::before { | |
content: "Have a Happy New Year!"; | |
} | |
} | |
@supports (--date({"between": ["December 1 2018", "December 25 2018"]})) { | |
.date-demo::after { | |
content: "I hope you're enjoying #merryCSSmas"; | |
} | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1076849699578105857>Protocol Queries</a></h2> | |
<div class=protocol-demo></div> | |
<style> | |
@supports (--protocol(["file"])) { | |
.protocol-demo::before { | |
content: "You're on Local Development"; | |
background: red; | |
} | |
} | |
@supports (--protocol(["http"])) { | |
.protocol-demo::before { | |
content: "You're on Insecure HTTP"; | |
background: yellow; | |
} | |
} | |
@supports (--protocol(["https"])) { | |
.protocol-demo::before { | |
content: "You're on Secure HTTPS"; | |
background: green; | |
} | |
} | |
.protocol-demo::before { | |
display: block; | |
border: 1px solid; | |
padding: 1em; | |
} | |
</style> | |
<h2><a href=https://twitter.com/innovati/status/1077229664274403328>Deep Hover</a></h2> | |
<section class=deep-hover-demo> | |
<div class=one></div> | |
<div class=two></div> | |
<div class=three></div> | |
<div class=four></div> | |
</section> | |
<style> | |
.deep-hover-demo div:hover { | |
border: 4px dashed purple; | |
} | |
.deep-hover-demo div[--deep-hover] { | |
background: rgba(0, 150, 255, .5); | |
--selector: window; | |
--events: ["mousemove"]; | |
} | |
.deep-hover-demo { | |
position: relative; | |
height: 120px; | |
} | |
.deep-hover-demo div { | |
position: absolute; | |
width: var(--size); | |
height: var(--size); | |
top: 50%; | |
left: 50%; | |
border: 1px dotted skyblue; | |
transform: | |
translateX(-50%) | |
translateY(-50%) | |
; | |
} | |
.one { | |
--size: 30px; | |
} | |
.two { | |
--size: 60px; | |
} | |
.three { | |
--size: 90px; | |
} | |
.four { | |
--size: 120px; | |
} | |
</style> | |
<script src=merrycssmas-bundle.js type=module></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import deqaf from 'https://unpkg.com/deqaf/index.js' | |
// [--parent] | |
// target an element's parent element | |
import parent from 'https://unpkg.com/jsincss-parent-selector/index.vanilla.js' | |
// :has(selector) | |
// [--has='"selector"'] | |
// like CSS's :has() selector | |
import has from 'https://unpkg.com/jsincss-has-selector/index.vanilla.js' | |
// [--closest='"selector"'] | |
// a similar to closest("selector") in JavaScript | |
import closest from 'https://unpkg.com/jsincss-closest-selector/index.vanilla.js' | |
// [--first] | |
// target the first element in the document matching a selector | |
import first from 'https://unpkg.com/jsincss-first-selector/index.vanilla.js' | |
// [--last] | |
// target the last element in the document matching a selector | |
import last from 'https://unpkg.com/jsincss-last-selector/index.vanilla.js' | |
// [--elder='"selector"'] | |
// target elder siblings of an element matching a selector | |
import elder from 'https://unpkg.com/jsincss-elder-selector/index.vanilla.js' | |
// [--previous] | |
// target the element directly preceding another element | |
import previous from 'https://unpkg.com/jsincss-previous-selector/index.vanilla.js' | |
// [--contains='"string"'] | |
// target tags whose text contents contain a given string | |
import contains from 'https://unpkg.com/jsincss-string-match/index.vanilla.js' | |
// [--regex='"re[g]ex"'] | |
// target tags whose text contents match a given regular expression pattern | |
import regex from 'https://unpkg.com/jsincss-regex-match/index.vanilla.js' | |
// @supports (--element()) {} | |
// responsive breakpoints on elements | |
import element from 'https://unpkg.com/jsincss-element-query/index.vanilla.js' | |
// [--specificity='1'] | |
// set the specificity of the rules separately from the weight of the selector | |
import specificity from 'https://unpkg.com/jsincss-custom-specificity/index.vanilla.js' | |
// @supports (--viewport()) {} | |
// target tags when they are partly or fully visible vertically in the viewport | |
import viewport from 'https://unpkg.com/jsincss-viewport/index.vanilla.js' | |
// @supports (--overflow()) {} | |
// target tags based on the state of their horizontal text overflow | |
import overflow from 'https://unpkg.com/jsincss-overflow/index.vanilla.js' | |
// @supports (--protocol()) {} | |
// apply styles based on querying the current protocol the site is accessed over | |
import protocol from 'https://unpkg.com/jsincss-protocol-sniffer/index.vanilla.js' | |
// [--computed='"property", "value"'] | |
// target tags with a computed value of a property matching a certain value | |
function computed(selector, property, value, rule) { | |
const attr = (selector + property + value).replace(/\W/g, '') | |
const result = Array.from(document.querySelectorAll(selector)).reduce( | |
(output, tag, count, tags) => { | |
if (window.getComputedStyle(tag)[property] === value) { | |
output.add.push({tag: tag, count: count}) | |
output.styles.push(`[data-computed-${attr}="${count}"] { ${rule} }`) | |
} else { | |
output.remove.push(tag) | |
} | |
return output | |
}, {styles: [], add: [], remove: []} | |
) | |
result.add.forEach(tag => tag.tag.setAttribute(`data-computed-${attr}`, tag.count)) | |
result.remove.forEach(tag => tag.setAttribute(`data-computed-${attr}`, '')) | |
return result.styles.join('\n') | |
} | |
// [--nth-letter='n'] | |
// target the nth letter of a tag's text content | |
function nthLetter(selector, index, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = selector.replace(/\W/g, '') | |
if ( | |
!tag.dataset.split | |
&& !tag.children.length | |
) { | |
tag.innerHTML = tag.textContent | |
.split(' ') | |
.map(word => `<span data-word>${ | |
word.replace(/\S/g, '<span data-letter>$&</span>') | |
}</span>`) | |
.join(' ') | |
tag.dataset.split = true | |
} | |
tag.querySelectorAll('[data-letter]')[index - 1].setAttribute(`data-nthletter-${attr}`, count) | |
styles += `[data-nthletter-${attr}="${count}"] { ${rule} }\n` | |
return styles | |
}, '') | |
} | |
// [--nth-word='n'] | |
// target the nth whitespace-separated word of a tag's text content | |
function nthWord(selector, index, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = selector.replace(/\W/g, '') | |
if ( | |
!tag.dataset.split | |
&& !tag.children.length | |
) { | |
tag.innerHTML = tag.textContent | |
.split(' ') | |
.map(word => `<span data-word>${ | |
word.replace(/\S/g, '<span data-letter>$&</span>') | |
}</span>`) | |
.join(' ') | |
tag.dataset.split = true | |
} | |
tag.setAttribute(`data-nthword-${attr}`, count) | |
styles += `[data-nthword-${attr}="${count}"] [data-word]:nth-of-type(${index}) { ${rule} }\n` | |
return styles | |
}, '') | |
} | |
// :playing | |
// [--playing] | |
// like CSS's :playing pseudo-class | |
function playing(selector, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = selector.replace(/\W/g, '') | |
if (tag.paused === false) { | |
tag.setAttribute(`data-playing-${attr}`, count) | |
styles += `[data-playing-${attr}="${count}"] { ${rule} }` | |
} else { | |
tag.removeAttribute(`data-playing-${attr}`) | |
} | |
return styles | |
}, '') | |
} | |
// :paused | |
// [--paused] | |
// like CSS's :paused psuedo-class | |
function paused(selector, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = selector.replace(/\W/g, '') | |
if (tag.paused) { | |
tag.setAttribute(`data-paused-${attr}`, count) | |
styles += `[data-paused-${attr}="${count}"] { ${rule} }` | |
} else { | |
tag.removeAttribute(`data-paused-${attr}`) | |
} | |
return styles | |
}, '') | |
} | |
// [--muted] | |
// targets tags with a muted property that is true | |
function muted(selector, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = selector.replace(/\W/g, '') | |
if (tag.muted) { | |
tag.setAttribute(`data-muted-${attr}`, count) | |
styles += `[data-muted-${attr}="${count}"] { ${rule} }` | |
} else { | |
tag.removeAttribute(`data-muted-${attr}`) | |
} | |
return styles | |
}, '') | |
} | |
// [--current-time='{condition: number}'] plugin | |
// conditions: less, lessOrEquals, equals, greaterOrEquals, greater | |
// targets a tag whose current time tests true to all conditions | |
function currentTime(selector, options, rule) { | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = ( | |
selector | |
+ Object.keys(options).join('') | |
+ Object.values(options).join('') | |
).replace(/\W/g, '') | |
if ( | |
Object.keys(options).every(test => | |
({ | |
less: (tag, num) => tag.currentTime < num, | |
lessOrEquals: (tag, num) => tag.currentTime <= num, | |
equals: (tag, num) => tag.currentTime === num, | |
greaterOrEquals: (tag, num) => tag.currentTime >= num, | |
greater: (tag, num) => tag.currentTime > num | |
})[test](tag, options[test]) | |
) | |
) { | |
tag.setAttribute(`data-current-time-${attr}`, count) | |
styles += `[data-current-time-${attr}="${count}"] { ${rule} }` | |
} else { | |
tag.removeAttribute(`data-current-time-${attr}`) | |
} | |
return styles | |
}, '') | |
} | |
// @document () {} | |
// @supports (--document()) {} | |
// like CSS's @document at-rule to target styles by matching the URL | |
function atDocument(conditions, stylesheet) { | |
return Object.entries(conditions).every(test => | |
({ | |
"url": string => location.href === string, | |
"url-prefix": string => location.href.startsWith(string), | |
"domain": string => location.hostname === string, | |
"regexp": string => location.href.match(new RegExp(string)) | |
})[test[0]](test[1]) | |
) | |
? stylesheet | |
: '' | |
} | |
// :not(:blank):valid | |
// [--not-blank-valid] | |
// targets a tag that is not blank and is currently valid | |
function notBlankValid(selector, rule) { | |
const attr = selector.replace(/\W/g, '') | |
const result = Array.from(document.querySelectorAll(selector)).reduce( | |
(output, tag, count) => { | |
if (tag.value && tag.checkValidity()) { | |
output.add.push({tag: tag, count: count}) | |
output.styles.push(`[data-not-blank-valid-${attr}="${count}"] { ${rule} }`) | |
} else { | |
output.remove.push(tag) | |
} | |
return output | |
}, {styles: [], add: [], remove: []} | |
) | |
result.add.forEach(tag => tag.tag.setAttribute(`data-not-blank-valid-${attr}`, tag.count)) | |
result.remove.forEach(tag => tag.setAttribute(`data-not-blank-valid-${attr}`, '')) | |
return result.styles.join('\n') | |
} | |
// :not(:blank):invalid | |
// [--not-blank-invalid] | |
// targets a tag that is not blank and is currently invalid | |
function notBlankInvalid(selector, rule) { | |
const attr = selector.replace(/\W/g, '') | |
const result = Array.from(document.querySelectorAll(selector)).reduce( | |
(output, tag, count) => { | |
if (tag.value && tag.checkValidity() === false) { | |
output.add.push({tag: tag, count: count}) | |
output.styles.push(`[data-not-blank-invalid-${attr}="${count}"] { ${rule} }`) | |
} else { | |
output.remove.push(tag) | |
} | |
return output | |
}, {styles: [], add: [], remove: []} | |
) | |
result.add.forEach(tag => tag.tag.setAttribute(`data-not-blank-invalid-${attr}`, tag.count)) | |
result.remove.forEach(tag => tag.setAttribute(`data-not-blank-invalid-${attr}`, '')) | |
return result.styles.join('\n') | |
} | |
// [--attr=''] | |
// target tags based on comparing attribute values as numbers | |
function attr(selector, attribute, comparison, value, rule) { | |
var features = { | |
less: tag => Number(tag.getAttribute(attribute)) < Number(value), | |
lessEqual: tag => Number(tag.getAttribute(attribute)) <= Number(value), | |
equal: tag => Number(tag.getAttribute(attribute)) === Number(value), | |
greaterEqual: tag => Number(tag.getAttribute(attribute)) >= Number(value), | |
greater: tag => Number(tag.getAttribute(attribute)) > Number(value) | |
} | |
return Array.from(document.querySelectorAll(selector)) | |
.reduce((styles, tag, count) => { | |
const attr = (selector + attribute + comparison + value).replace(/\W/g, '') | |
if (features[comparison](tag)) { | |
tag.setAttribute(`data-attr-${attr}`, count) | |
styles += `[data-attr-${attr}="${count}"] { ${rule} }` | |
} else { | |
tag.removeAttribute(`data-attr-${attr}`) | |
} | |
return styles | |
}, '') | |
} | |
// @supports (--navigator()) {} | |
// target different browsers based on matching information inside window.navigator | |
function navigator(conditions, stylesheet) { | |
const features = { | |
includes: (prop, string) => window.navigator[prop].includes(string), | |
excludes: (prop, string) => window.navigator[prop].includes(string) === false, | |
equals: (prop, string) => window.navigator[prop] === string, | |
regex: (prop, string) => RegExp(string).test(window.navigator[prop]) | |
} | |
return Object.entries(conditions).every(condition => { | |
const [test, [prop, string]] = condition | |
return features[test](prop, string) | |
}) | |
? stylesheet | |
: '' | |
} | |
// @supports (--localStorage()) {} | |
// query window.localStorage for values to determine which styles to apply | |
function atLocalStorage(conditions, stylesheet) { | |
const features = { | |
exists: prop => localStorage.hasOwnProperty(prop), | |
includes: (prop, string) => localStorage[prop].includes(string), | |
excludes: (prop, string) => localStorage[prop].includes(string) === false, | |
equals: (prop, string) => localStorage[prop] === string, | |
regex: (prop, string) => RegExp(string).test(localStorage[prop]) | |
} | |
return Object.entries(conditions).every(condition => { | |
const [test, [prop, string]] = condition | |
return features[test](prop, string) | |
}) | |
? stylesheet | |
: '' | |
} | |
// @supports (--sessionStorage()) {} | |
// query window.sessionStorage for values to determine which styles to apply | |
function atSessionStorage(conditions, stylesheet) { | |
const features = { | |
exists: prop => sessionStorage[prop] !== null, | |
includes: (prop, string) => sessionStorage[prop].includes(string), | |
excludes: (prop, string) => sessionStorage[prop].includes(string) === false, | |
equals: (prop, string) => sessionStorage[prop] === string, | |
regex: (prop, string) => RegExp(string).test(sessionStorage[prop]) | |
} | |
return Object.entries(conditions).every(condition => { | |
const [test, [prop, string]] = condition | |
return features[test](prop, string) | |
}) | |
? stylesheet | |
: '' | |
} | |
// @supports (--date()) {} | |
// apply styles before, after, or between given dates | |
function date(conditions, stylesheet) { | |
const features = { | |
before: string => new Date() < new Date(string), | |
after: string => new Date(string) < new Date(), | |
between: (start, end) => new Date(start) < new Date() && new Date() < new Date(end), | |
on: string => { | |
const target = new Date(string) | |
const today = new Date() | |
return target.getFullYear() === today.getFullYear() | |
&& target.getMonth() === today.getMonth() | |
&& target.getDate() === today.getDate() | |
}, | |
} | |
return Object.entries(conditions).every(condition => { | |
const [test, [prop, string]] = condition | |
return features[test](prop, string) | |
}) | |
? stylesheet | |
: '' | |
} | |
// [--deep-hover] | |
// Target all tags matching a selector being hovered, even if covered by another tag | |
function deepHover(selector, rule) { | |
const attr = selector.replace(/\W/g, '') | |
const result = Array.from(document.querySelectorAll(selector)) | |
.reduce((output, tag, count) => { | |
if ( | |
document.elementsFromPoint(event.clientX, event.clientY).includes(tag) | |
) { | |
output.add.push({tag: tag, count: count}) | |
output.styles.push(`[data-hover-deep-${attr}="${count}"] { ${rule} }`) | |
} else { | |
output.remove.push(tag) | |
} | |
return output | |
}, {add: [], remove: [], styles: []}) | |
result.add.forEach(tag => tag.tag.setAttribute(`data-hover-deep-${attr}`, tag.count)) | |
result.remove.forEach(tag => tag.setAttribute(`data-hover-deep-${attr}`, '')) | |
return result.styles.join('\n') | |
} | |
// Run deqaf with all plugins loaded | |
deqaf({ | |
stylesheet: { | |
document: atDocument, | |
element, | |
viewport, | |
overflow, | |
navigator, | |
localStorage: atLocalStorage, | |
sessionStorage: atSessionStorage, | |
date, | |
protocol | |
}, | |
rule: { | |
parent, | |
has, | |
closest, | |
first, | |
last, | |
elder, | |
previous, | |
contains, | |
regex, | |
computed, | |
nthLetter, | |
nthWord, | |
playing, | |
paused, | |
muted, | |
currentTime, | |
notBlankValid, | |
notBlankInvalid, | |
attr, | |
specificity, | |
deepHover | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment