UNPKG

@wener/console

Version:

Base console UI toolkit

328 lines (327 loc) 8.86 kB
import { createContext, useContext } from "react"; import { clamp, getGlobalStates, randomUUID } from "@wener/utils"; import { createStore } from "zustand"; import { mutative } from "zustand-mutative"; // import { Window } from './Window'; export const WindowContext = /*#__PURE__*/ createContext(null); export function useWindow() { return useContext(WindowContext) || getRootWindow(); } export function getRootWindow() { return getGlobalStates('ReactRootWindow', ()=>new ReactRootWindow()); } function createRootWindowStore(init = {}) { return createStore(mutative((setState, getState, store)=>{ return { maximized: undefined, windows: [], ...init }; })); } const WindowSizes = { xs: { width: 200, height: 200 }, sm: { width: 400, height: 300 }, md: { width: 600, height: 400 }, lg: { width: 800, height: 600 }, xl: { width: 1000, height: 800 }, xxl: { width: 1200, height: 800 } }; const FrameSize = { title: 28, border: 1, width: 2, height: 28 + 2 }; function normalize(init) { return { zIndex: 0, minimized: false, maximized: false, canMaximize: true, canMinimize: true, canResize: true, canDrag: true, minWidth: 200, minHeight: 200, metadata: {}, attributes: {}, properties: {}, frameless: false, canFullscreen: true, fullscreen: false, windows: [], ...init, ...normalizeCoordinate(init) }; } function normalizeCoordinate({ width = WindowSizes.md.width, height = WindowSizes.md.height, x, y }, { center } = {}) { const { innerWidth: ww, innerHeight: wh } = typeof window === 'undefined' ? { innerWidth: 800, innerHeight: 600 } : window; width = clamp(width, WindowSizes.xs.width, ww); height = clamp(height, WindowSizes.xs.height, wh); let cx = (ww - width) / 2; let cy = (wh - height) / 2; if (center) { x = cx; y = cy; } x = clamp(x || cx, 0, ww - width); y = clamp(y || cy, 0, wh - height); return { x, y, width, height }; } function createWindowStore(init = {}) { return createStore(mutative((setState, getState, store)=>{ return normalize(init); })); } export class ReactWindow extends EventTarget { id; key; store; parent; static MaximizedWindow; constructor({ id, key, store, parent }){ super(); this.id = id; this.key = key || id; this.parent = parent; this.store = store || createWindowStore(); } get state() { return this.store.getState(); } get body() { return this.state.bodyElement; } setBody = (ref)=>{ let current = this.state.bodyElement; if (current === ref) { return; } // do initial focus if (!current) { ref?.focus(); } this.store.setState({ bodyElement: ref }); }; close = (data)=>{ this.dispatchEvent(new CustomEvent('close', { detail: data })); }; focus = ()=>{ let ele = this.state.bodyElement; if (!ele) { return; } if (!document.activeElement || !ele?.contains(document.activeElement)) { ele?.focus(); this.dispatchEvent(new Event('focus')); } }; minimize = (minimize)=>{ let current = this.state.minimized; minimize = minimize ?? !current; if (minimize === current) { return; } this.store.setState((s)=>{ s.minimized = minimize; s.maximized = false; }); this.dispatchEvent(new Event('minimize')); }; maximize = (maximize)=>{ let current = this.state.maximized; maximize = maximize ?? !current; if (maximize === current) { return; } this.store.setState((s)=>{ if (maximize && !s.maximized) { s.maximized = true; s.minimized = false; s.properties['last'] = [ s.x, s.y, s.width, s.height ]; s.width = window.innerWidth; s.height = window.innerHeight; s.x = 0; s.y = 0; ReactWindow.MaximizedWindow = this; } else if (!maximize && s.maximized) { s.maximized = false; s.minimized = false; const [x, y, width, height] = s.properties['last'] ?? []; s.width = width; s.height = height; s.x = x; s.y = y; Object.assign(s, normalizeCoordinate(s)); ReactWindow.MaximizedWindow = undefined; } }); if (maximize) { this.dispatchEvent(new Event('maximize')); } else { this.dispatchEvent(new Event('restore')); } }; center = ()=>{ this.store.setState(normalizeCoordinate(this.store.getState(), { center: true })); }; open = (opts)=>{ getRootWindow().open(opts); }; } let ReactRootWindow = class ReactRootWindow extends ReactWindow { zIndex = 1; current; constructor(){ super({ id: 'root' }); } get top() { if (this.current) { return this.current; } return this.windows.filter((v)=>!v.state.minimized).toSorted((a, b)=>a.state.zIndex - b.state.zIndex)[0]; } get windows() { return this.state.windows; } handleFocusIn = (e, win)=>{ this.setActive(win); }; handleFocusOut = (e, win)=>{ if (win === this.current) { this.current = undefined; } }; setActive(win) { try { const { zIndex } = win.state; if (zIndex === this.zIndex) { this.current = win; win.minimize(false); return; } win.store.setState({ zIndex: ++this.zIndex }); this.current = win; } finally{ win.focus(); } } find(s) { if (s.key) return this.windows.find((v)=>v.key === s.key); } toggle = (opts)=>{ let found = this.find(opts); if (found) { found.close(); return; } return this.open(opts); }; open = (opts)=>{ if (opts.key) { let existing = this.windows.find((v)=>v.key === opts.key); if (existing) { this.setActive(existing); return existing; } } let id = randomUUID(); let key = opts.key || id; if (!opts.frameless) { const { width: fw, height: fh } = FrameSize; const wkeys = [ 'width', 'maxWidth', 'minWidth' ]; const hkeys = [ 'height', 'maxHeight', 'minHeight' ]; for (let key of wkeys){ if (opts[key]) { opts[key] += fw; } } for (let key of hkeys){ if (opts[key]) { opts[key] += fh; } } } let root = (this.parent || getRootWindow()).store; // let root = this.root; let store = createWindowStore({ ...opts, zIndex: this.zIndex++ }); let child = new ReactWindow({ id, key, store }); // const { x, y, width, height } = store.getState(); // console.log(`open window`, id, { x, y, width, height }); child.addEventListener('close', ()=>{ root.setState((s)=>{ s.windows = s.windows.filter((v)=>v !== child); this.current === child && (this.current = undefined); }); }); child.addEventListener('focusin', (e)=>this.handleFocusIn(e, child)); child.addEventListener('focusout', (e)=>this.handleFocusOut(e, child)); root.setState((s)=>{ // fixme typing Element is not draftable s.windows.push(child); }); this.setActive(child); return child; }; close = ()=>{ this.windows.forEach((v)=>v.close()); }; }; //# sourceMappingURL=ReactWindow.js.map