81 lines
4.0 KiB
JavaScript
81 lines
4.0 KiB
JavaScript
export function fitWindowRect(rect, viewport) {
|
|
const width = Math.min(Math.max(280, rect.width), Math.max(1, viewport.width - 16));
|
|
const height = Math.min(Math.max(200, rect.height), Math.max(1, viewport.height - 16));
|
|
return {
|
|
width, height,
|
|
x: Math.max(8, Math.min(rect.x, viewport.width - width - 8)),
|
|
y: Math.max(8, Math.min(rect.y, viewport.height - height - 8)),
|
|
};
|
|
}
|
|
|
|
export function mountFloatingWindow(element, scope, onClose) {
|
|
const stylesheet = new URL('./window.css', import.meta.url).href;
|
|
if (![...document.querySelectorAll('link[rel="stylesheet"]')].some(link => link.href === stylesheet)) {
|
|
const link = document.createElement('link');
|
|
Object.assign(link, { rel: 'stylesheet', href: stylesheet });
|
|
document.head.append(link);
|
|
}
|
|
let gesture;
|
|
const viewport = () => ({ width: innerWidth, height: innerHeight });
|
|
const move = (x, y) => {
|
|
const rect = element.getBoundingClientRect();
|
|
const next = fitWindowRect({ x, y, width: rect.width, height: rect.height }, viewport());
|
|
element.style.left = `${next.x}px`;
|
|
element.style.top = `${next.y}px`;
|
|
element.style.right = 'auto';
|
|
};
|
|
const resize = (width, height) => {
|
|
const rect = element.getBoundingClientRect();
|
|
const next = fitWindowRect({ x: rect.x, y: rect.y, width, height }, viewport());
|
|
element.style.setProperty('--ca-window-width', `${next.width}px`);
|
|
element.style.setProperty('--ca-window-height', `${next.height}px`);
|
|
move(next.x, next.y);
|
|
};
|
|
const end = () => {
|
|
if (gesture && element.hasPointerCapture(gesture.id)) element.releasePointerCapture(gesture.id);
|
|
gesture = null;
|
|
element.classList.remove('ca-dragging');
|
|
};
|
|
scope.listen(element, 'pointerdown', event => {
|
|
const handle = event.target.closest('[data-window-move], [data-window-resize]');
|
|
if (!handle || event.button !== 0) return;
|
|
const rect = element.getBoundingClientRect();
|
|
gesture = { id: event.pointerId, kind: handle.hasAttribute('data-window-resize') ? 'resize' : 'move', rect, x: event.clientX, y: event.clientY };
|
|
handle.focus();
|
|
element.setPointerCapture(event.pointerId);
|
|
element.classList.add('ca-dragging');
|
|
event.preventDefault();
|
|
});
|
|
scope.listen(element, 'pointermove', event => {
|
|
if (!gesture || event.pointerId !== gesture.id) return;
|
|
const dx = event.clientX - gesture.x;
|
|
const dy = event.clientY - gesture.y;
|
|
if (gesture.kind === 'resize') resize(gesture.rect.width + dx, gesture.rect.height + dy);
|
|
else move(gesture.rect.x + dx, gesture.rect.y + dy);
|
|
});
|
|
for (const event of ['pointerup', 'pointercancel', 'lostpointercapture']) scope.listen(element, event, end);
|
|
scope.listen(element, 'keydown', event => {
|
|
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose(); return; }
|
|
const handle = event.target.closest('[data-window-move], [data-window-resize]');
|
|
if (!handle || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const delta = event.shiftKey ? 40 : 16;
|
|
const dx = event.key === 'ArrowLeft' ? -delta : event.key === 'ArrowRight' ? delta : 0;
|
|
const dy = event.key === 'ArrowUp' ? -delta : event.key === 'ArrowDown' ? delta : 0;
|
|
const rect = element.getBoundingClientRect();
|
|
if (handle.hasAttribute('data-window-resize')) resize(rect.width + dx, rect.height + dy);
|
|
else move(rect.x + dx, rect.y + dy);
|
|
});
|
|
const keepReachable = () => {
|
|
if (element.hidden) return;
|
|
const rect = element.getBoundingClientRect();
|
|
move(rect.x, rect.y);
|
|
};
|
|
scope.listen(window, 'resize', keepReachable);
|
|
const observer = new ResizeObserver(keepReachable);
|
|
observer.observe(element);
|
|
scope.cleanup(() => { observer.disconnect(); end(); });
|
|
return { fit: keepReachable };
|
|
}
|