import PlainDraggable from 'plain-draggable'; import { onRemove, animateRectTransition } from './utils'; export class Layout { constructor(options = {}) { if (!options.name) throw Error('Layout name is not given'); this.name = options.name; this.enterHandler = options.enterHandler; this.leaveHandler = options.leaveHandler; } valueOf = () => this.name; } export class SideBySide extends Layout { name = 'side-by-side'; enterHandler = ({ container, htmlHolder, showcase }) => { const bar = document.createElement('div'); bar.className = 'bar'; bar.innerHTML = '
'; const handle = bar.querySelector('.bar-handle'); container.appendChild(bar); // Resize views by value const resizeByLeft = left => { htmlHolder.style.width = left + 'px'; showcase.style.width = parseFloat(getComputedStyle(container).width) - left + 'px'; }; const draggable = new PlainDraggable(bar, { handle: handle, containment: { left: '25%', top: 0, right: '75%', height: 0 }, }); draggable.draggableCursor = 'grab'; draggable.onDrag = pos => { handle.style.transform = 'unset'; resizeByLeft(pos.left); }; draggable.onDragEnd = _ => { handle.removeAttribute('style'); }; onRemove(bar, () => draggable.remove()); }; leaveHandler = ({ container }) => { container.querySelector('.bar')?.remove(); }; } export class Overlay extends Layout { name = 'overlay'; saveLeftTopAsData = element => { const { left, top } = element.getBoundingClientRect(); element.setAttribute('data-left', left); element.setAttribute('data-top', top); }; addDraggable = element => { // Make sure current element always on top const siblings = Array.from( element.parentElement?.querySelectorAll(':scope > *') ?? [], ); let popTimer = null; element.onmouseover = () => { popTimer = setTimeout(() => { siblings.forEach(e => e.style.removeProperty('z-index')); element.style.zIndex = '9001'; }, 200); }; element.onmouseout = () => { clearTimeout(popTimer); }; // Add draggable part const draggablePart = document.createElement('div'); element.appendChild(draggablePart); draggablePart.className = 'draggable-part'; draggablePart.innerHTML = '
\u2630
'; // Add draggable instance const { left, top } = element.getBoundingClientRect(); const draggable = new PlainDraggable(element, { top: top, left: left, handle: draggablePart, snap: { x: { step: 20 }, y: { step: 20 } }, }); // FIXME use pure CSS to hide utils const utils = element.querySelector('.utils'); draggable.onDragStart = () => { utils.style.display = 'none'; element.classList.add('drag'); }; draggable.onDragEnd = () => { utils.style = ''; element.classList.remove('drag'); element.style.zIndex = '9000'; }; // Reposition draggable instance when resized new ResizeObserver(() => { try { draggable.position(); } catch (_) { null; } }).observe(element); // Callback for remove onRemove(element, () => { draggable.remove(); }); }; enterHandler = ({ htmlHolder, blocks }) => { // FIXME It is weird rect from this method and this scope are different... blocks.forEach(this.saveLeftTopAsData); // Create draggable blocks and set each position by previous one let [left, top] = [20, 20]; blocks.forEach(block => { const originLeft = Number(block.getAttribute('data-left')); const originTop = Number(block.getAttribute('data-top')); // Create draggable block const wrapper = document.createElement('div'); wrapper.classList.add('draggable-block'); wrapper.innerHTML = `
\u274C
\u2795
\u2796
`; wrapper.title = 'Middle-click to hide block'; wrapper.onmouseup = e => { // Hide block with middle click if (e.button === 1) { wrapper.classList.add('hide'); } }; // Set DOMRect for wrapper wrapper.appendChild(block); wrapper.style.left = left + 'px'; wrapper.style.top = top + 'px'; htmlHolder.appendChild(wrapper); const { width } = wrapper.getBoundingClientRect(); left += width + 30; if (left > window.innerWidth) { top += 200; left = left % window.innerWidth; } // Animation for DOMRect animateRectTransition( wrapper, { left: originLeft, top: originTop }, { resume: true, duration: 300 }, ).finished.finally(() => this.addDraggable(wrapper)); // Trivial case: // This hack make sure utils remains at the same place even when wrapper resized // Prevent DOMRect changes when user clicking plus/minus button many times const utils = wrapper.querySelector('.utils'); utils.onmouseover = () => { const { left, top } = utils.getBoundingClientRect(); utils.style.cssText = `visibility: visible; z-index: 9000; position: fixed; transition: unset; left: ${left}px; top: ${top}px;`; document.body.appendChild(utils); }; utils.onmouseout = () => { wrapper.appendChild(utils); utils.removeAttribute('style'); }; // Close button wrapper.querySelector('#close').onclick = () => { wrapper.classList.add('hide'); utils.removeAttribute('style'); }; // Plus/Minus font-size of content wrapper.querySelector('#plus-font-size').onclick = () => { const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; block.style.fontSize = `${fontSize + 0.2}rem`; }; wrapper.querySelector('#minus-font-size').onclick = () => { const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16; block.style.fontSize = `${fontSize - 0.2}rem`; }; }); }; leaveHandler = ({ htmlHolder, blocks }) => { const resumeFromDraggable = block => { const draggableContainer = block.closest('.draggable-block'); if (!draggableContainer) return; htmlHolder.appendChild(block); draggableContainer.remove(); }; blocks.forEach(resumeFromDraggable); }; }