-
-
Save griponminds/506f75ae0aa7d12e72a85571727a635f to your computer and use it in GitHub Desktop.
Bookmarklet for CSS Container Queries
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:(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