Skip to content

Instantly share code, notes, and snippets.

@griponminds
Last active June 19, 2024 10:38
Show Gist options
  • Save griponminds/e8def848638a5e9021eaa1a7c251427e to your computer and use it in GitHub Desktop.
Save griponminds/e8def848638a5e9021eaa1a7c251427e to your computer and use it in GitHub Desktop.
Bookmarklet for CSS custom properties
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 {
'<' : '&lt;',
'>' : '&gt;',
'&' : '&amp;',
'\'': '&#x27;',
'`' : '&#x60;',
'"' : '&quot;'
}[match];
})
.replace(/&amp;nbsp;/uig, '&nbsp;');
};
/* 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 === '') ? '&nbsp;' : 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