Skip to content

Instantly share code, notes, and snippets.

@griponminds
Last active July 31, 2023 05:32
Show Gist options
  • Save griponminds/506f75ae0aa7d12e72a85571727a635f to your computer and use it in GitHub Desktop.
Save griponminds/506f75ae0aa7d12e72a85571727a635f to your computer and use it in GitHub Desktop.
Bookmarklet for CSS Container Queries
javascript:(function(){
/*! cq-resizer.js 1.2.3 | © 2023 grip on minds | MIT License (https://opensource.org/license/mit/) */
const createStyles = () => {
const cssText = `
:host {
--color : hsl(0 0% 6%);
--bg : hsl(170 5% 84%);
--border : hsl(0 0% 40%);
all: initial;
display: block;
contain: size;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
color: var(--color);
font-family: sans-serif;
font-size: 12px;
font-weight: normal;
font-style: normal;
line-height: 1.6;
}
input, button {
font: inherit;
}
dialog {
display: grid;
border: solid 1px var(--border);
width: min(35em, calc(100% - 40px));
height: auto;
max-height: calc(100dvh - 40px);
overflow-y: auto;
position: fixed;
top: 20px;
right: 20px;
left: auto;
z-index: calc(infinity);
background: var(--bg);
}
.header {
display: grid;
grid-template-columns: 1fr auto;
justify-content: space-between;
align-items: center;
border-bottom: solid 1px var(--border);
position: sticky;
top: 0;
z-index: 1;
background: var(--bg);
}
h1 {
padding: 10px 20px;
border-right: solid 1px var(--border);
cursor: move;
}
.message {
padding: 20px;
}
ol {
display: grid;
gap: 20px;
padding: 20px;
counter-set: list-counter;
}
li {
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 20px;
align-items: center;
}
a {
color: var(--color);
text-decoration: none;
overflow-wrap: anywhere;
}
.size {
display: grid;
grid-template-columns: repeat(2, auto);
column-gap: 5px;
align-content: start;
}
input[type="number"] {
padding-left: 0.5em;
width: 5em;
text-align: right;
}
input[type="number"][readonly] {
color: hsl(0 0% 40%);
}
button {
padding: 10px 20px;
border: none;
border-radius: 0;
width: 100%;
height: 100%;
display: block;
background: none;
appearance: none;
cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
a:hover,
button:hover {
opacity: 0.7;
}
}
`;
const styleElem = document.createElement('style');
styleElem.textContent = cssText;
return styleElem;
};
/* アンカーリンクのスタイル */
const createAnchorStyle = () => {
const anchorStyle = document.createElement('style');
anchorStyle.textContent = `
[data-cq-resizer-anchor] {
border: solid 10px green;
display: none;
position: absolute;
inset: 0;
z-index: calc(infinity);
opacity: 0;
pointer-events: none;
}
[data-cq-resizer-anchor="target"] {
display: block;
animation-name: highlight;
animation-duration: 1s;
animation-timing-function: ease;
animation-iteration-count: 2;
}
@keyframes highlight {
0% { opacity: 0; }
50% { opacity: 0.6; }
100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
[data-cq-resizer-anchor="target"] {
animation-timing-function: step-end;
animation-iteration-count: 1;
}
}
`;
document.head.append(anchorStyle);
};
/* ドラッグ時の XY 位置 */
const pos = new Map([
['x', 0],
['y', 0],
['fromX', 0],
['fromY', 0],
['moveX', 0],
['moveY', 0],
]);
const setMoveEvent = (title, dialog) => {
const handleMoveStart = (e) => {
e.preventDefault();
const event = e.touches ? e.touches[0] : e;
dialog.style.willChange = 'transform';
pos.set('fromX', event.clientX);
pos.set('fromY', event.clientY);
document.addEventListener('mousemove', handleMove, { passive: false });
document.addEventListener('touchmove', handleMove, { passive: false });
document.addEventListener('mouseup', handleMoveEnd);
document.addEventListener('touchend', handleMoveEnd);
};
const handleMove = (e) => {
e.preventDefault();
const event = e.touches ? e.touches[0] : e;
pos.set('moveX', event.clientX - pos.get('fromX'));
pos.set('moveY', event.clientY - pos.get('fromY'));
pos.set('x', pos.get('x') + pos.get('moveX'));
pos.set('y', pos.get('y') + pos.get('moveY'));
dialog.style.transform = `translate(${pos.get('x')}px, ${pos.get('y')}px)`;
pos.set('fromX', event.clientX);
pos.set('fromY', event.clientY);
};
const handleMoveEnd = () => {
dialog.style.willChange = 'auto';
document.removeEventListener('mousemove', handleMove, { passive: false });
document.removeEventListener('touchmove', handleMove, { passive: false });
document.removeEventListener('mouseup', handleMoveEnd);
document.removeEventListener('touchend', handleMoveEnd);
};
title.addEventListener('mousedown', handleMoveStart);
title.addEventListener('touchstart', handleMoveStart);
};
const createCloseButton = (dialog) => {
const div = document.createElement('div');
const close = document.createElement('button');
close.setAttribute('type', 'button');
close.textContent = 'Close ✕';
close.addEventListener('click', () => {
const cqResizer = document.querySelector('cq-resizer');
cqResizer && document.body.removeChild(cqResizer);
/* インフォパネルの位置をリセット */
[...pos.keys()].forEach((key) => pos.set(key, 0));
if (dialog) dialog.style.transform = 'none';
}, false);
div.append(close);
return div;
};
const setCheckboxEvent = (checkbox, inputNum, container) => {
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
inputNum.removeAttribute('readonly');
container.setAttribute('style', container.getAttribute('data-cq-resizer-style'));
} else {
inputNum.setAttribute('readonly', 'readonly');
container.setAttribute('data-cq-resizer-style', container.getAttribute('style'));
container.removeAttribute('style');
}
});
};
const setSizeEvent = (input, container) => {
input.addEventListener('change', () => {
if (input.value >= 0) {
container.style.width = `${input.value}px`;
}
});
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
input.value = Math.round(entry.borderBoxSize[0].inlineSize);
}
});
resizeObserver.observe(container);
};
const createLinkText = (container) => {
const array = Array.from(container.classList);
if (array.length < 1) {
return `<${container?.tagName.toLowerCase()}>`;
}
return `.${array.join('.')}`;
};
const setAnchorLinkEvent = (a, anchor) => {
a.addEventListener('click', () => {
anchor.setAttribute('data-cq-resizer-anchor', 'target');
}, false);
anchor.addEventListener('animationend', () => {
anchor.setAttribute('data-cq-resizer-anchor', 'true');
}, false);
};
const createHTML = () => {
let all = document.querySelectorAll('*');
const containers = Array.from(all).map((el) => {
const style = window.getComputedStyle(el);
return style.containerType.match(/size/) ? el : false;
}).filter(Boolean);
/* `dialog` 要素とヘッダの生成 */
const dialog = document.createElement('dialog');
dialog.open = true;
dialog.setAttribute('aria-labelledby', 'title');
const header = document.createElement('div');
header.classList.add('header');
const title = document.createElement('h1');
title.id = 'title';
title.textContent = 'cq-resizer';
setMoveEvent(title, dialog);
const closeBtn = createCloseButton(dialog);
header.append(title);
header.append(closeBtn);
dialog.append(header);
/* `container-type` が指定されている要素見つからない場合にメッセージを返す */
if (containers.length < 1) {
const message = document.createElement('p');
message.textContent = 'container-type が指定されている要素は見つかりませんでした';
message.classList.add('message');
dialog.append(message);
return dialog;
}
/* 親子関係にあり XY 座標が同じ子孫要素のコンテナを除外 */
const filteredContainers = containers.map((el) => {
const isSamePosition = containers.some((parent) => {
if (parent === el) return false;
const parentRect = parent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
if (
parent.contains(el) &&
(Math.round(parentRect.right) === Math.round(elRect.right)) &&
(Math.round(parentRect.bottom) === Math.round(elRect.bottom))
) {
return true;
}
return false;
});
if (isSamePosition) return false;
return el;
}).filter(Boolean);
/* リスト要素の生成 */
const ol = document.createElement('ol');
const fragment = document.createDocumentFragment();
filteredContainers.forEach((container, index) => {
const style = window.getComputedStyle(container);
/* コンテナにリサイズハンドルを追加 */
if (style.containerType.includes('inline-size')) {
container.style.resize = 'horizontal';
container.style.overflowX = 'auto';
} else if (style.containerType.includes('size')) {
container.style.resize = 'both';
container.style.overflow = 'auto';
}
const li = document.createElement('li');
/* No. の生成 */
const num = document.createElement('span');
num.textContent = index + 1;
/* アンカーリンクの生成 */
const a = document.createElement('a');
const id = 'cq-resizer-' + (index + 1);
a.href = `#${id}`;
a.textContent = createLinkText(container);
const anchor = document.createElement('span');
anchor.setAttribute('id', id);
anchor.setAttribute('aria-hidden', 'true');
anchor.setAttribute('data-cq-resizer-anchor', 'true');
setAnchorLinkEvent(a, anchor);
container.append(anchor);
/* 横幅表示の生成 */
const size = document.createElement('div');
size.classList.add('size');
size.textContent = 'px';
const inputNum = document.createElement('input');
inputNum.setAttribute('type', 'number');
inputNum.value = container.style.width;
size.prepend(inputNum);
setSizeEvent(inputNum, container);
/* チェックボックスの生成 */
const checkbox = document.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.checked = true;
setCheckboxEvent(checkbox, inputNum, container);
li.append(num);
li.append(a);
li.append(size);
li.append(checkbox);
fragment.append(li);
});
ol.append(fragment);
dialog.append(ol);
return dialog;
};
if (!customElements.get('cq-resizer')) {
const styles = createStyles();
const html = createHTML();
createAnchorStyle();
class CqResizer extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.append(styles);
shadow.append(html);
}
}
customElements.define('cq-resizer', CqResizer);
}
if (!document.querySelector('cq-resizer')) {
const cqResizer = document.createElement('cq-resizer');
document.body.prepend(cqResizer);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment