UNPKG

zextra

Version:

zikojs extra components

266 lines (224 loc) 8.7 kB
import { UIElement, call_with_optional_props, tags} from 'ziko/dom'; const { div, button, svg, line, rect } = tags; import { X } from 'ziko-lucide/X'; import { Minus } from 'ziko-lucide/Minus' import { Maximize } from 'ziko-lucide/Maximize' import { Minimize } from 'ziko-lucide/Minimize' const ICONS = { maximize: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>', restore: '<rect x="8" y="4" width="12" height="12" rx="2"></rect><path d="M4 8v12a2 2 0 0 0 2 2h12"></path>' }; export class UIFloatingPanel extends UIElement{ constructor({ title = 'Floating Panel', i18n = { reduceBtn : 'Reduce', extendBtn : 'Extend to full size', restoreSizeBtn : 'Restore size', closeBtn : 'Close' } } = {}, ...items){ super({element : 'div'}); this.setAttr({ class : 'floating-panel'}) this.extendSizeBtn = button( {class: 'ctrl-btn', title: i18n.extendBtn}, Maximize() ) this.restoreSizeBtn = button( {class: 'ctrl-btn', title: i18n.restoreSizeBtn}, Minimize() ) this.panel_header = div( {class: 'panel-header'}, div({class: 'panel-title'},'Floating Panel'), div({class: 'panel-controls'}, button({class: 'ctrl-btn', title: i18n.reduceBtn}, Minus() ).onClick(()=> this.toggleReduce()), this.extendSizeBtn.onClick(()=> this.extendSize()), this.restoreSizeBtn.onClick(()=> this.restoreSize()), button( {class: 'ctrl-btn', title: i18n.closeBtn }, X(), ).onClick(()=> this.close()), ), ).onPtrDown(e => this.startDrag(e.event)) this.panel_body = div({class: 'panel-body'}, ...items); this.append( this.panel_header, this.panel_body, div({class: 'resize-handle n', 'data-axis': 'n'}), div({class: 'resize-handle s', 'data-axis': 's'}), div({class: 'resize-handle e', 'data-axis': 'e'}), div({class: 'resize-handle w', 'data-axis': 'w'}), div({class: 'resize-handle ne', 'data-axis': 'ne'}), div({class: 'resize-handle nw', 'data-axis': 'nw'}), div({class: 'resize-handle se', 'data-axis': 'se'}), div({class: 'resize-handle sw', 'data-axis': 'sw'}), ) this.isDragging = false; this.isResizing = false; this.isMinimized = false; this.isMaximized = false; this.dragOffset = { x: 0, y: 0 }; this.resizeState = null; this.normalRect = { left: (window.innerWidth - 380) / 2, top: (window.innerHeight - 260) / 2, width: 380, height: 260 }; // TEMP this.extendIcon = document.getElementById('extendIcon'); const handles = this.element.querySelectorAll('.resize-handle'); handles.forEach(handle => { handle.addEventListener('pointerdown', (e) => this.startResize(e, handle.dataset.axis)); }); // Global pointer events document.addEventListener('pointermove', (e) => { if (this.isDragging) this.drag(e); if (this.isResizing) this.resize(e); }); document.addEventListener('pointerup', () => { this.isDragging = false; this.isResizing = false; }); } open(){ if(!this.element.classList.contains('open')) this.style({ left : `${this.normalRect.left}px`, top : `${this.normalRect.top}px`, width : `${this.normalRect.width}px`, bottom : `${this.normalRect.height}px` }) this.element.classList.add('open'); return this; } close(){ this.element.classList.remove('open'); return this; } toggleReduce() { this.isMinimized = !this.isMinimized; if (this.isMinimized) { if (this.isMaximized) this.toggleExtend(); this.element.classList.add('minimized'); } else this.element.classList.remove('minimized'); return this; } extendSize() { if (this.isMaximized) return; this.triggerAnimation(); this.isMaximized = true; if (this.isMinimized) { this.isMinimized = false; this.element.classList.remove("minimized"); } const rect = this.element.getBoundingClientRect(); this.normalRect = { left: rect.left, top: rect.top, width: rect.width, height: rect.height, }; this.element.classList.add("maximized"); this.extendSizeBtn.style({display : 'none'}); this.restoreSizeBtn.style({display : ''}); } restoreSize() { if (!this.isMaximized) return; this.triggerAnimation(); this.isMaximized = false; this.element.classList.remove("maximized"); this.element.style.left = `${this.normalRect.left}px`; this.element.style.top = `${this.normalRect.top}px`; this.element.style.width = `${this.normalRect.width}px`; this.element.style.height = `${this.normalRect.height}px`; this.extendSizeBtn.style({display : ''}); this.restoreSizeBtn.style({display : 'none'}); } toggleExtend() { if (this.isMaximized) { this.restoreSize(); } else { this.extendSize(); } } triggerAnimation() { this.element.classList.add('animating'); setTimeout(() => this.element.classList.remove('animating'), 200); } startDrag(e) { if (e.target.closest('.ctrl-btn') || this.isMaximized) return; this.isDragging = true; // Capture pointer so we keep receiving events even outside the element this.panel_header.element.setPointerCapture(e.pointerId); const rect = this.element.getBoundingClientRect(); this.dragOffset.x = e.clientX - rect.left; this.dragOffset.y = e.clientY - rect.top; } drag(e) { let x = e.clientX - this.dragOffset.x; let y = e.clientY - this.dragOffset.y; x = Math.max(0, Math.min(x, window.innerWidth - this.element.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - this.element.offsetHeight)); this.element.style.left = `${x}px`; this.element.style.top = `${y}px`; } startResize(e, axis) { e.stopPropagation(); if (this.isMinimized || this.isMaximized) return; this.isResizing = true; // Capture pointer so resize works even if finger/mouse leaves the handle e.target.setPointerCapture(e.pointerId); const rect = this.element.getBoundingClientRect(); this.resizeState = { axis, startX: e.clientX, startY: e.clientY, startWidth: rect.width, startHeight: rect.height, startLeft: rect.left, startTop: rect.top }; } resize(e) { const { axis, startX, startY, startWidth, startHeight, startLeft, startTop } = this.resizeState; const dx = e.clientX - startX; const dy = e.clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; const minW = 260; const minH = 140; if (axis.includes('e')) newWidth = Math.max(minW, startWidth + dx); if (axis.includes('s')) newHeight = Math.max(minH, startHeight + dy); if (axis.includes('w')) { const possibleW = startWidth - dx; if (possibleW > minW) { newWidth = possibleW; newLeft = startLeft + dx; } } if (axis.includes('n')) { const possibleH = startHeight - dy; if (possibleH > minH) { newHeight = possibleH; newTop = startTop + dy; } } this.element.style.width = `${newWidth}px`; this.element.style.height = `${newHeight}px`; this.element.style.left = `${newLeft}px`; this.element.style.top = `${newTop}px`; } } export const FloatingPanel = call_with_optional_props(UIFloatingPanel);