@wener/console
Version:
Base console UI toolkit
328 lines (327 loc) • 8.86 kB
JavaScript
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