-
-
Save griponminds/e8def848638a5e9021eaa1a7c251427e to your computer and use it in GitHub Desktop.
Bookmarklet for CSS custom properties
This file contains 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
javascript:(() => { | |
/*! custom-props-viewer.js 1.1.0 | © 2024 grip on minds | MIT License (https://opensource.org/license/mit/) */ | |
class CustomPropsViewer extends HTMLElement { | |
/* preferences */ | |
#title = 'custom-props-viewer'; | |
#inputDebounce = 600; | |
/* CSS */ | |
#styles = ` | |
@layer reset, layouts, components, theme; | |
@layer theme { | |
:host { | |
--color-bg-base : hsl(180 0% 0%); | |
--color-bg-widget : hsl(180 10% 20%); | |
--color-bg-item : hsl(180 78% 10%); | |
--color-primary : hsl(180 17% 84%); | |
--color-secondary : hsl(180 36% 60%); | |
--color-highlight : hsl(180 12% 26%); | |
--checkbox-border : hsl(180 7% 30%); | |
--checkbox-color : hsl(46 100% 50%); | |
--value-bg : hsl(180 0% 0%); | |
--value-fg : hsl(180 52% 72%); | |
--color-scheme: dark; | |
--font-family: Menlo, Consolas, monospace; | |
--font-size-m: calc(1rem * 14 / var(--rem, 16)); | |
--font-size-s: calc(1rem * 12 / var(--rem, 16)); | |
--line-height: 1.6; | |
--letter-spacing: 0.05em; | |
--inline-size: min(600px, 100%); | |
--padding: 16px; | |
} | |
} | |
@layer components.item { | |
.item-container { | |
display: grid; | |
row-gap: 2px; | |
align-content: start; | |
background-color: var(--color-bg-base); | |
} | |
.item { | |
display: grid; | |
counter-increment: num; | |
background-color: var(--color-bg-item); | |
overflow-wrap: anywhere; | |
} | |
.item-selector { | |
display: var(--show-selector, grid); | |
grid-template-columns: auto 1fr; | |
column-gap: 8px; | |
align-items: start; | |
padding-block-start: var(--padding); | |
padding-inline: var(--padding); | |
color: var(--color-secondary); | |
font-size: var(--font-size-s); | |
line-height: 1.2; | |
} | |
.item-selector::before, | |
.item-prop::before { | |
content: counter(num); | |
min-inline-size: 2em; | |
padding: calc(4px + 1px); | |
background-color: var(--color-secondary); | |
color: var(--color-bg-item); | |
font-size: var(--font-size-s); | |
line-height: 1.2; | |
text-align: center; | |
} | |
[style*="--show-selector: none;"] .item-prop::before { | |
display: revert; | |
} | |
.item-selector-rules { | |
display: flex; | |
flex-wrap: wrap; | |
column-gap: 4px; | |
} | |
.item-selector-rules > * { | |
margin-block-end: 4px; | |
} | |
.item-selector-code { | |
display: block; | |
inline-size: fit-content; | |
padding: 4px; | |
border: solid 1px currentColor; | |
} | |
.item-prop { | |
display: grid; | |
grid-template-columns: auto 1fr; | |
column-gap: 8px; | |
align-items: start; | |
margin: var(--padding); | |
} | |
[style*="--show-value: none;"] .item-prop { | |
grid-column: 1 / -1; | |
} | |
.item-prop::before { | |
display: none; | |
} | |
.item-value { | |
display: var(--show-value, grid); | |
margin-block-end: var(--padding); | |
margin-inline: var(--padding); | |
padding-block: 8px; | |
padding-inline: 16px; | |
border-radius: 4px; | |
box-shadow: 0 1px 0 0 var(--color-highlight); | |
background-color: var(--value-bg); | |
color: var(--value-fg); | |
} | |
@property --value-color { | |
syntax: '<color>'; | |
inherits: true; | |
initial-value: transparent; | |
} | |
.item-value[style*="--value-color"] { | |
grid-template-columns: 1fr auto; | |
gap: 8px; | |
align-items: center; | |
} | |
.item-value[style*="--value-color"]::after { | |
content: ''; | |
display: block; | |
inline-size: 1em; | |
aspect-ratio: 1; | |
border: solid 1px color-mix(in srgb, currentColor 40%, transparent); | |
background-color: var(--value-color); | |
} | |
.item-calc { | |
display: grid; | |
grid-template-columns: auto 1fr; | |
column-gap: 8px; | |
align-items: center; | |
margin-block-start: 8px; | |
} | |
.item-calc svg { | |
fill: currentColor; | |
} | |
.item-not-found { | |
display: none; | |
padding: var(--padding); | |
} | |
:is(.item-container:not(:has(.item)), .item-container[data-not-found]) { | |
display: none; | |
} | |
:is(.item-container:not(:has(.item)), .item-container[data-not-found]) + .item-not-found { | |
display: revert; | |
} | |
@container (min-inline-size: 460px) { | |
.item { | |
grid-template-columns: repeat(2, 1fr); | |
grid-template-areas: | |
'selector selector' | |
'prop value'; | |
grid-column: 1 / -1; | |
align-items: start; | |
} | |
.item-selector { | |
grid-area: selector; | |
} | |
.item-prop { | |
margin-block-end: var(--padding); | |
padding-block: 8px; | |
} | |
.item-value { | |
margin-block-start: var(--padding); | |
} | |
} | |
} | |
@layer components.controls { | |
.controls { | |
display: grid; | |
grid-template-columns: 1fr auto; | |
grid-template-areas: | |
'search search' | |
'count reload'; | |
gap: var(--padding); | |
align-items: center; | |
padding: var(--padding); | |
border: solid 4px var(--color-bg-base); | |
background-color: var(--color-bg-widget); | |
} | |
@container (min-inline-size: 360px) { | |
.controls { | |
grid-template-columns: 1fr auto auto; | |
grid-template-areas: 'search count reload'; | |
} | |
} | |
.search { | |
display: grid; | |
grid-template-columns: auto 1fr; | |
grid-area: search; | |
gap: 8px; | |
align-items: center; | |
} | |
.search-label { | |
line-height: 0; | |
} | |
.search-label svg { | |
fill: currentColor; | |
} | |
.search-box { | |
position: relative; | |
border: solid 1px var(--color-secondary); | |
border-radius: 4px; | |
} | |
.search-box:has(input:focus) { | |
border-color: var(--color-primary); | |
} | |
.search-prefix { | |
position: absolute; | |
inset-block-start: 50%; | |
inset-inline-start: 8px; | |
line-height: 0; | |
translate: 0 -50%; | |
} | |
.search input[type="search"] { | |
inline-size: 100%; | |
padding: 8px; | |
padding-inline-start: 32px; | |
border: none; | |
border-radius: 0; | |
background: none; | |
line-height: 1.2; | |
} | |
.count { | |
display: grid; | |
grid-template-columns: 1fr auto; | |
column-gap: 4px; | |
align-items: baseline; | |
text-align: end; | |
} | |
.count-label { | |
font-size: var(--font-size-s); | |
} | |
.count-num { | |
min-inline-size: 1.5em; | |
} | |
.reload-btn { | |
display: block; | |
padding: 4px; | |
border: solid 1px currentColor; | |
border-radius: 8px; | |
color: var(--color-primary); | |
line-height: 0; | |
} | |
.reload-btn svg { | |
fill: currentColor; | |
} | |
@media (any-hover: hover) { | |
.reload-btn:hover { | |
opacity: 0.8; | |
} | |
} | |
.reload-btn[data-rotate] { | |
animation: timer 0.6s ease; | |
} | |
.reload-btn[data-rotate] svg { | |
animation: rotate 0.6s ease; | |
} | |
@keyframes rotate { | |
from { rotate: 0turn; } | |
to { rotate: 1turn; } | |
} | |
.show { | |
display: flex; | |
grid-column: 1 / -1; | |
flex-wrap: wrap; | |
gap: 16px; | |
margin-block-end: calc(-1 * var(--padding)); | |
margin-inline: calc(-1 * var(--padding)); | |
padding: var(--padding); | |
border-block-start: solid 1px var(--color-bg-base); | |
} | |
.show-label { | |
display: grid; | |
grid-template-columns: auto 1fr; | |
gap: 8px; | |
align-items: center; | |
inline-size: fit-content; | |
font-size: var(--font-size-s); | |
text-transform: lowercase; | |
cursor: pointer; | |
} | |
.show-checkbox { | |
position: relative; | |
inline-size: 16px; | |
aspect-ratio: 1; | |
border: solid 2px var(--checkbox-border); | |
border-radius: 0; | |
background-color: var(--color-bg-base); | |
-webkit-appearance: none; | |
appearance: none; | |
} | |
.show-checkbox:checked::after { | |
content: ''; | |
position: absolute; | |
inset: 0; | |
display: block; | |
inline-size: 8px; | |
aspect-ratio: 1; | |
margin: auto; | |
background-color: var(--checkbox-color); | |
} | |
} | |
@layer components.header { | |
.header { | |
position: sticky; | |
inset-block-start: 0; | |
z-index: 10; | |
} | |
.header-top { | |
display: grid; | |
grid-template-columns: 1fr auto; | |
column-gap: 8px; | |
align-items: center; | |
padding-block-end: 8px; | |
padding-inline: 8px; | |
background-color: var(--color-bg-widget); | |
color: var(--color-primary); | |
font-size: var(--font-size-s); | |
} | |
.header svg { | |
fill: currentColor; | |
} | |
.header-movable { | |
display: grid; | |
grid-template-columns: auto 1fr; | |
column-gap: 8px; | |
align-items: center; | |
padding-block: 8px; | |
} | |
.header-movable[data-move-target] { | |
cursor: move; | |
} | |
.header-movable:not([data-move-target]) svg { | |
display: none; | |
} | |
.header-hdg { | |
font-size: inherit; | |
font-weight: normal; | |
line-height: 1.2; | |
} | |
.header-close { | |
position: relative; | |
z-index: 10; | |
display: grid; | |
grid-template-columns: auto auto; | |
column-gap: 8px; | |
align-items: center; | |
justify-self: end; | |
text-transform: uppercase; | |
} | |
.header-close-icon { | |
display: block; | |
inline-size: 12px; | |
block-size: 12px; | |
padding: 2px; | |
border: solid 1px currentColor; | |
} | |
@media (any-hover: hover) { | |
.header-close:hover { | |
opacity: 0.8; | |
} | |
} | |
} | |
@property --x { | |
syntax: '<length>'; | |
inherits: false; | |
initial-value: 0; | |
} | |
@property --y { | |
syntax: '<length>'; | |
inherits: false; | |
initial-value: 0; | |
} | |
@layer layouts { | |
:host { | |
position: fixed; | |
inset-block-start: 0; | |
inset-inline-end: 0; | |
z-index: calc(infinity); | |
display: var(--display, block); | |
inline-size: var(--inline-size); | |
color-scheme: var(--color-scheme); | |
font-family: var(--font-family); | |
font-size: var(--font-size-m); | |
line-height: var(--line-height); | |
letter-spacing: var(--letter-spacing); | |
translate: var(--x, 0) var(--y, 0); | |
} | |
:host[data-is-move] { | |
will-change: translate; | |
} | |
.container { | |
container-type: inline-size; | |
overflow: auto; | |
overscroll-behavior: contain; | |
max-block-size: calc(100svh - 40px - var(--y, 0px)); | |
margin: 20px; | |
border: solid 8px var(--color-bg-widget); | |
border-radius: 8px; | |
color-scheme: var(--color-scheme); | |
background-color: var(--color-bg-item); | |
color: var(--color-primary); | |
resize: both; | |
} | |
.item-box { | |
position: relative; | |
border-block-end: solid 4px var(--color-bg-base); | |
border-inline: solid 4px var(--color-bg-base); | |
} | |
} | |
:host { | |
all: revert-layer !important; | |
} | |
@layer reset { | |
:host { | |
writing-mode: horizontal-tb; | |
font-style: initial; | |
font-weight: initial; | |
font-variant: initial; | |
line-height: initial; | |
letter-spacing: initial; | |
text-align: initial; | |
text-indent: initial; | |
text-transform: initial; | |
word-spacing: initial; | |
text-shadow: initial; | |
cursor: initial; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
} | |
*, | |
::after, | |
::before { | |
box-sizing: border-box; | |
} | |
ul { | |
list-style-type: none; | |
} | |
svg { | |
display: block; | |
block-size: auto; | |
max-inline-size: 100%; | |
} | |
input, button { | |
font: inherit; | |
} | |
button { | |
border: none; | |
background: none; | |
letter-spacing: inherit; | |
cursor: pointer; | |
} | |
:where( | |
button, | |
button:active | |
) { | |
color: inherit; | |
} | |
code { | |
font-family: inherit; | |
} | |
} | |
`; | |
/* data */ | |
#data = []; | |
#dataCopy = []; | |
/* elements */ | |
#container = null; | |
#itemContainer = null; | |
#shadow = null; | |
#countElem = null; | |
#filterStyle = null; | |
/* others */ | |
#timer = undefined; | |
#count = 0; | |
#pos = new Map([ | |
['x', 0], | |
['y', 0], | |
['fromX', 0], | |
['fromY', 0], | |
['moveX', 0], | |
['moveY', 0], | |
]); | |
#mainController = new AbortController(); | |
#moveController = new AbortController(); | |
constructor() { | |
super(); | |
} | |
connectedCallback() { | |
const styles = this.createStyles(); | |
this.#container = this.createContainerHTML(); | |
if (!this.#container) return; | |
this.calcFontSizeRem(); | |
this.#shadow = this.attachShadow({ mode: 'open' }); | |
this.#shadow.append(styles); | |
this.#shadow.append(this.#container); | |
this.#countElem = this.#container.querySelector('[data-count]'); | |
this.#itemContainer = this.#container.querySelector('[data-list-container]'); | |
const checkboxes = this.#container.querySelectorAll('[data-show-checkbox]'); | |
const moveTarget = this.#container.querySelector('[data-move-target]'); | |
const closeBtn = this.#container.querySelector('[data-close]'); | |
const search = this.#container.querySelector('input'); | |
const reloadBtn = this.#container.querySelector('[data-reload]'); | |
const { signal } = this.#mainController; | |
if (moveTarget) { | |
moveTarget.addEventListener('mousedown', this.handleMoveStart, { signal }); | |
moveTarget.addEventListener('touchstart', this.handleMoveStart, { signal }); | |
} | |
checkboxes.forEach((checkbox) => { | |
checkbox.addEventListener('change', () =>{ | |
if (checkbox.checked) { | |
this.#container.style.removeProperty(`--${checkbox.name}`); | |
} else { | |
this.#container.style.setProperty(`--${checkbox.name}`, 'none'); | |
} | |
}, { signal }); | |
}); | |
closeBtn?.addEventListener('click', () => { | |
this.setAttribute('data-toggle', 'true'); | |
}, { signal }); | |
search?.addEventListener('input', () => { | |
this.debounce(this.filterProps, search.value, this.#inputDebounce); | |
}, { signal }); | |
if (this.#countElem) { | |
this.countListItems(); | |
} | |
reloadBtn?.addEventListener('click', () => { | |
reloadBtn && reloadBtn.setAttribute('data-rotate', 'true'); | |
this.reloadListItems(); | |
}, { signal }); | |
reloadBtn?.addEventListener('animationend', () => { | |
reloadBtn && reloadBtn.removeAttribute('data-rotate'); | |
}, { signal }); | |
} | |
disconnectedCallback() { | |
this.#mainController?.abort(); | |
this.#moveController?.abort(); | |
} | |
/* ウィジェットの位置とサイズをリセット */ | |
resetPosAndSizes = () => { | |
this.style.removeProperty('--x'); | |
this.style.removeProperty('--y'); | |
if (this.#container) { | |
this.#container.style.removeProperty('width'); | |
this.#container.style.removeProperty('height'); | |
this.#container.scrollTop = 0; | |
} | |
this.#pos.forEach((_, key) => this.#pos.set(key, 0)); | |
}; | |
/* ウィジェットの表示・非表示の制御 */ | |
static get observedAttributes() { return ['data-toggle']; } | |
attributeChangedCallback(name, _, newValue) { | |
if (name === 'data-toggle') { | |
if (newValue === 'true') { | |
this.removeAttribute('data-toggle'); | |
this.resetPosAndSizes(); | |
const isDisplay = (this.style.getPropertyValue('--display') !== 'none'); | |
if (isDisplay) { | |
this.style.setProperty('--display', 'none'); | |
} else { | |
this.style.removeProperty('--display'); | |
} | |
} | |
} | |
} | |
/* CSS 生成 */ | |
createStyles = () => { | |
const styleElem = document.createElement('style'); | |
styleElem.textContent = this.#styles; | |
return styleElem; | |
}; | |
/* HTML の特殊文字をエンティティ化 */ | |
sanitizeHTML = (str) => { | |
if (typeof str !== 'string') return str; | |
return str | |
.replace(/[<>&'`"]/ug, (match) => { | |
return { | |
'<' : '<', | |
'>' : '>', | |
'&' : '&', | |
'\'': ''', | |
'`' : '`', | |
'"' : '"' | |
}[match]; | |
}) | |
.replace(/&nbsp;/uig, ' '); | |
}; | |
/* At-rules のデータを取得 */ | |
getGroupRules = (name = '', items, separator = ', ') => { | |
if (items.length < 1) return ''; | |
const setItems = this.sanitizeHTML(items.join(separator)); | |
return `<code class="item-selector-code">@${name} ${setItems}</code>`; | |
}; | |
/* var() を展開した値の表示 */ | |
createCalcHTML = (calc) => { | |
if (!calc) return ''; | |
return ` | |
<span class="item-calc"> | |
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"> | |
<rect x="0" y="5" width="8" height="4"/> | |
<polygon points="8,2 14,7, 8,12"/> | |
</svg> | |
<code>${calc}</code> | |
</span> | |
`; | |
}; | |
/* リストアイテムの HTML を生成 */ | |
createListItemsHTML = () => { | |
this.#data = this.createCutomPropsArray(); | |
/* データに変更がない場合には false を返す */ | |
if (JSON.stringify(this.#data) === JSON.stringify(this.#dataCopy)) { | |
return false; | |
} | |
this.#dataCopy = this.#data; | |
const listItemsHTML = this.#data.reduce((html, item) => { | |
const value = this.sanitizeHTML(item.value); | |
const calc = this.sanitizeHTML(item.calc); | |
const isColor = (array) => { | |
return array.find((value) => (!value.match(/^calc|var\(.*\)/u) && CSS.supports('color', value))); | |
}; | |
const color = isColor([value, calc]); | |
return html += ` | |
<li class="item" data-prop=${item.prop.slice(2)}> | |
<div class="item-selector"> | |
<div class="item-selector-inner"> | |
<div class="item-selector-rules"> | |
${this.getGroupRules('layer', item.layers, '.')} | |
${this.getGroupRules('media', item.medias)} | |
${this.getGroupRules('container', item.containers)} | |
</div> | |
<code class="item-selector-code">${item.selector}</code> | |
</div> | |
</div> | |
<div class="item-prop"> | |
<code>${item.prop}</code> | |
</div> | |
<div class="item-value" style="${color ? '--value-color: ' + color : ''}"> | |
<div class="item-value-str"> | |
<code>${value}</code> | |
${this.createCalcHTML(calc)} | |
</div> | |
</div> | |
</li> | |
`; | |
}, ''); | |
return listItemsHTML; | |
}; | |
/* HTML 生成 */ | |
createContainerHTML = () => { | |
const str = ` | |
<div class="container" role="region" aria-labelledby="title"> | |
<header class="header"> | |
<div class="header-top"> | |
<div class="header-movable" data-move-target> | |
<svg width="12" height="12" viewBox="0 0 10 10" aria-hidden="true"> | |
<circle cx="3" cy="1" r="1"/><circle cx="7" cy="1" r="1"/><circle cx="3" cy="5" r="1"/><circle cx="7" cy="5" r="1"/><circle cx="3" cy="9" r="1"/><circle cx="7" cy="9" r="1"/></svg> | |
<h1 id="title" class="header-hdg">${this.sanitizeHTML(this.#title)}</h1> | |
</div> | |
<button type="button" class="header-close" data-close> | |
Close | |
<span class="header-close-icon"> | |
<svg width="8" height="8" viewBox="0 0 8 8" aria-hidden="true"> | |
<line x1="0" y1="0" x2="8" y2="8" stroke="currentColor" stroke-width="2"/> | |
<line x1="8" y1="0" x2="0" y2="8" stroke="currentColor" stroke-width="2"/> | |
</svg> | |
</span> | |
</button> | |
</div> | |
<div class="controls"> | |
<search class="search"> | |
<label for="custom-props-filter" class="search-label"> | |
<svg width="24" height="24" viewBox="0 0 27 27" role="img"> | |
<title>Filter</title> | |
<path d="m20.57,19.16l-3.83-3.83c.79-1.08,1.26-2.4,1.26-3.83,0-3.58-2.92-6.5-6.5-6.5s-6.5,2.92-6.5,6.5,2.92,6.5,6.5,6.5c1.43,0,2.75-.47,3.83-1.26l3.83,3.83c.39.39,1.02.39,1.41,0,.39-.39.39-1.02,0-1.41Zm-13.57-7.66c0-2.48,2.02-4.5,4.5-4.5s4.5,2.02,4.5,4.5-2.02,4.5-4.5,4.5-4.5-2.02-4.5-4.5Z"/> | |
</svg> | |
</label> | |
<div class="search-box"> | |
<span class="search-prefix"> | |
<svg width="16" height="16" viewBox="0 0 16 16" role="img"> | |
<title>--</title> | |
<line x1="0" y1="8" x2="7" y2="8" stroke="currentColor" stroke-width="1"/> | |
<line x1="9" y1="8" x2="16" y2="8" stroke="currentColor" stroke-width="1"/> | |
</svg> | |
</span> | |
<input type="search" id="custom-props-filter" name="property-name" value="" placeholder="property name" spellcheck="false"> | |
</div> | |
</search> | |
<p class="count"> | |
<span class="count-num" data-count>0</span> | |
<span class="count-label">props</span> | |
</p> | |
<div class="reload"> | |
<button type="button" class="reload-btn" data-reload> | |
<svg width="24" height="24" viewBox="0 0 46 46" role="img"> | |
<title>Reload</title> | |
<path d="M23 32a9.003 9.003 0 0 0 8.777-7h3.057C33.882 30.675 28.946 35 23 35c-6.627 0-12-5.373-12-12s5.373-12 12-12c3.869 0 7.31 1.83 9.505 4.674l1.677-.847a.75.75 0 0 1 1.076.801l-1.022 5.743a.75.75 0 0 1-1.071.54l-5.227-2.589a.75.75 0 0 1-.005-1.341l1.827-.923A9 9 0 1 0 23 32Z"/> | |
</svg> | |
</button> | |
</div> | |
<div class="show"> | |
<label class="show-label"> | |
<input type="checkbox" name="show-selector" class="show-checkbox" data-show-checkbox checked>Selector | |
</label> | |
<label class="show-label"> | |
<input type="checkbox" name="show-value" class="show-checkbox" data-show-checkbox checked>Value | |
</label> | |
</div> | |
</div> | |
</header> | |
<div class="item-box"> | |
<ul class="item-container" data-list-container> | |
${this.createListItemsHTML()} | |
</ul> | |
<p class="item-not-found">Not found</p> | |
</div> | |
</div> | |
`; | |
const html = new DOMParser().parseFromString(str, 'text/html'); | |
return html.body.firstElementChild; | |
}; | |
/* ルート要素のフォントサイズを算出 */ | |
calcFontSizeRem = () => { | |
const rem = getComputedStyle(document.documentElement).getPropertyValue('font-size'); | |
this.style.setProperty('--rem', parseInt(rem, 10)); | |
}; | |
/* スクロール位置のリセット */ | |
resetScollTop = () => { | |
if (this.#container) { | |
this.#container.scrollTop = 0; | |
} | |
}; | |
/* カスタムプロパティ名でフィルタ */ | |
filterProps = (value) => { | |
if (!this.#filterStyle) { | |
this.#filterStyle = document.createElement('style'); | |
this.#shadow.append(this.#filterStyle); | |
} else { | |
this.#filterStyle.textContent = ''; | |
} | |
if (value !== '') { | |
/* CSS の属性セレクタでマッチしない要素を非表示 */ | |
const stylesheet = ` | |
[data-prop]:not([data-prop*="${value}"]) { display: none; } | |
`; | |
this.#filterStyle.append(stylesheet); | |
} | |
this.resetScollTop(); | |
this.countListItems(); | |
}; | |
/* リストアイテム数のカウント */ | |
countListItems = () => { | |
if (!this.#countElem || !this.#itemContainer) return; | |
this.#itemContainer.removeAttribute('data-not-found'); | |
const items = this.#itemContainer.querySelectorAll('[data-prop]'); | |
const filteredItems = Array.from(items).filter((item) => item.clientHeight > 0); | |
this.#count = filteredItems.length ?? 0; | |
this.#countElem.textContent = this.#count; | |
if (this.#count < 1) { | |
this.#itemContainer.setAttribute('data-not-found', 'true'); | |
} | |
}; | |
/* リストアイテムをリロード */ | |
reloadListItems = () => { | |
this.resetScollTop(); | |
const html = this.createListItemsHTML(); | |
if (!html) return; | |
if (this.#itemContainer) { | |
this.#itemContainer.innerHTML = html; | |
this.countListItems(); | |
} | |
}; | |
/* スタイルの値を計算 */ | |
calcStyleValue = (selector, prop, value, medias) => { | |
try { | |
if (medias && !window.matchMedia(medias).matches) return ''; | |
/* `var()` もしくは `calc()` に一致する場合のみを対象とする */ | |
const regex = /(^var\(--.+\)|^calc\(.+\))/u; | |
if (!value.match(regex)) return ''; | |
const elems = selector.split(/::?(before|after)/); | |
const targetElem = document.querySelector(elems[0]); | |
if (!targetElem) return ''; | |
const result = getComputedStyle(targetElem, elems[1]).getPropertyValue(prop); | |
return (value === result) ? '' : result; | |
} catch(e) { | |
console.error(`[selector] ${selector}` + '\n' + e.messaage); | |
return ''; | |
} | |
}; | |
/* 同一オリジンの判定 */ | |
isSameOrigin = (styleSheet) => { | |
if (!styleSheet.href) return true; | |
return styleSheet.href.indexOf(window.location.origin) === 0; | |
}; | |
/* カスタムプロパティを抽出 */ | |
filterCssCutomProps = (style) => [...style].filter((propName, _) => { | |
return propName.trim().startsWith('--'); | |
}); | |
/* CSSRules から必要なデータを取得 */ | |
getDataFromCSSRules = (rules) => { | |
/* 再帰的に CSSStyleRule を検索してデータを作成 */ | |
const getStyles = ( | |
rules = [], | |
layers = [], | |
medias = [], | |
containers = [], | |
data = [] | |
) => { | |
for (const rule of rules) { | |
if (rule instanceof CSSStyleRule) { | |
const filteredStyle = this.filterCssCutomProps(rule.style); | |
if (filteredStyle.length > 0) { | |
data.push({ | |
declaration: rule.style, | |
style: filteredStyle, | |
selector: rule?.selectorText, | |
layers, | |
medias, | |
containers, | |
}); | |
} | |
} | |
else if (rule instanceof CSSLayerBlockRule) { | |
getStyles(rule.cssRules, [...layers, rule.name], medias, containers, data); | |
} | |
else if (rule instanceof CSSMediaRule) { | |
getStyles(rule.cssRules, layers, [...medias, rule.media[0]], containers, data); | |
} | |
else if (rule instanceof CSSContainerRule) { | |
const containerName = rule.containerName ? `${rule.containerName} ` : ''; | |
getStyles(rule.cssRules, layers, medias, [...containers, containerName + rule.containerQuery], data); | |
} | |
else if (rule.cssRules) { | |
getStyles(rule.cssRules, layers, medias, containers, data); | |
} | |
} | |
return data; | |
}; | |
return getStyles(rules); | |
}; | |
/* スタイルの値を取得 */ | |
getStyleValue = (item, prop) => { | |
const value = item.declaration.getPropertyValue(prop).trim(); | |
return (value === '') ? ' ' : value; | |
}; | |
/* document.styleSheets から必要なデータの配列を生成 */ | |
createCutomPropsArray = () => { | |
const styleSheets = [...document.styleSheets].filter(this.isSameOrigin); | |
return styleSheets.reduce((acc, sheet) => { | |
const data = this.getDataFromCSSRules([...sheet.cssRules]); | |
/* ルールごとの配列をプロパティごとの配列に変換 */ | |
const propsArray = data.reduce((propsArray, rule) => { | |
const itemArray = rule.style.map((prop) => ( | |
{ | |
prop, | |
selector: rule.selector, | |
value: this.getStyleValue(rule, prop), | |
layers: rule.layers, | |
medias: rule.medias, | |
containers: rule.containers, | |
} | |
)).map((rule) => ( | |
{ | |
...rule, | |
calc: this.calcStyleValue(rule.selector, rule.prop, rule.value, rule.medias), | |
} | |
)); | |
return [...propsArray, ...itemArray]; | |
}, []); | |
return [...acc, ...propsArray]; | |
}, []); | |
}; | |
/* debounce で処理を間引く */ | |
debounce = (fn, arg = null, interval = 600) => { | |
clearTimeout(this.#timer); | |
this.#timer = setTimeout (() => fn(arg), interval); | |
}; | |
/* イベントを取得 */ | |
getEvent = (e) => { | |
return ('touches' in e) ? e.touches[0] : e; | |
}; | |
/* ウィジェットをドラッグして移動 */ | |
handleMoveStart = (e) => { | |
e.preventDefault(); | |
const { signal } = this.#moveController; | |
const event = this.getEvent(e); | |
this.setAttribute('data-is-move', 'true'); | |
this.#pos.set('fromX', event.clientX); | |
this.#pos.set('fromY', event.clientY); | |
document.addEventListener('mousemove', this.handleMove, { passive: false, signal }); | |
document.addEventListener('touchmove', this.handleMove, { passive: false, signal }); | |
document.addEventListener('mouseup', this.handleMoveEnd, { signal }); | |
document.addEventListener('touchend', this.handleMoveEnd, { signal }); | |
}; | |
handleMove = (e) => { | |
e.preventDefault(); | |
const event = this.getEvent(e); | |
this.#pos.set('moveX', event.clientX - this.#pos.get('fromX')); | |
this.#pos.set('moveY', event.clientY - this.#pos.get('fromY')); | |
this.#pos.set('x', this.#pos.get('x') + this.#pos.get('moveX')); | |
this.#pos.set('y', this.#pos.get('y') + this.#pos.get('moveY')); | |
this.style.setProperty('--x', `${this.#pos.get('x')}px`); | |
this.style.setProperty('--y', `${this.#pos.get('y')}px`); | |
this.#pos.set('fromX', event.clientX); | |
this.#pos.set('fromY', event.clientY); | |
}; | |
handleMoveEnd = () => { | |
this.removeAttribute('data-is-move'); | |
this.#moveController.abort(); | |
this.#moveController = new AbortController(); | |
}; | |
} | |
/* カスタム要素の初期化 */ | |
{ | |
if (!customElements.get('custom-props-viewer')) { | |
customElements.define('custom-props-viewer', CustomPropsViewer); | |
} | |
const widget = document.querySelector('custom-props-viewer'); | |
if (!widget) { | |
const newWidget = document.createElement('custom-props-viewer'); | |
document.body.prepend(newWidget); | |
newWidget.setAttribute('data-init', 'true'); | |
} | |
if (widget) { | |
widget.setAttribute('data-toggle', 'true'); | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment