Skip to content

Instantly share code, notes, and snippets.

@tomhodgins
Last active January 16, 2023 20:35
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tomhodgins/c22973a0990248b244cd56c7641b31f0 to your computer and use it in GitHub Desktop.
Save tomhodgins/c22973a0990248b244cd56c7641b31f0 to your computer and use it in GitHub Desktop.
<!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>
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