https://domlink.deployments.hotsocket.fyi/
at main 139 lines 4.0 kB view raw
1import { Button, Container, Label, Node } from "./domlink.ts"; 2 3class TitleBar extends Container { 4 label: Label; 5 closeButton: Button; 6 constructor(title: string, closeCallback: EventListener) { 7 super(); 8 this.class("LWindowHandle"); 9 this.label = new Label(title); 10 this.closeButton = new Button("x", closeCallback); 11 this.with(this.label,this.closeButton); 12 } 13 get closable(): boolean { 14 return this.closeButton.wraps.style.display != "none"; 15 } 16 set closable(x: boolean) { 17 this.closeButton.wraps.style.display = x ? "inline-block" : "none"; 18 } 19} 20 21export class Window extends Container { 22 // i know that i dont technically need to have these per-intance (there's no more than one cursor), 23 // but i still kind of feel like i should. 24 private static currentlyDragged: Window | null = null; 25 private static mouseRelX = 0; 26 private static mouseRelY = 0; 27 private static zIndexCounter = 100; 28 titleBar: TitleBar; 29 content = new Container().class("LWindowContent"); 30 31 constructor(title: string = "New Window", height: number = 300, width: number = 400) { 32 super(); 33 this.class("LWindow"); 34 this.titleBar = new TitleBar(title, ()=>{this.wraps.remove();}); 35 this.add(this.titleBar); 36 this.titleBar.wraps.addEventListener("mousedown", this.titleGrabHandler.bind(this)); 37 this.wraps.addEventListener("mousedown", this.front); 38 this.front(); 39 this.add(this.content); 40 this.style((s)=>{ 41 s.width = `${width}px`; 42 s.height = `${height}px`; 43 s.top = "0px"; 44 s.left = "0px"; 45 }); 46 47 globalThis.window.addEventListener("resize", ()=>{ 48 // deno-lint-ignore no-self-assign 49 this.position = this.position; 50 }); 51 } 52 53 front() { 54 this.style((s)=>{ 55 s.zIndex = `${Window.zIndexCounter++}`; 56 }); 57 } 58 59 override with(...nodes: (Node | string)[]): this { 60 const w = this.wraps.style.width; 61 const h = this.wraps.style.height; 62 this.content.with(...nodes); 63 this.wraps.style.width = w; 64 this.wraps.style.height = h; 65 return this; 66 } 67 68 get title() { 69 return (this.titleBar.children[0] as Label).text; 70 } 71 set title(newTitle: string) { 72 (this.titleBar.children[0] as Label).text = newTitle; 73 } 74 public closable(can: boolean = true) { 75 this.titleBar.closable = can; 76 return this; 77 } 78 79 private titleGrabHandler(ev: MouseEvent) { 80 if (ev.button !== 0) return; 81 82 Window.currentlyDragged = this; 83 this.titleBar.wraps.style.cursor = 'grabbing'; 84 85 const rect = this.wraps.getBoundingClientRect(); 86 Window.mouseRelX = ev.clientX - rect.left; 87 Window.mouseRelY = ev.clientY - rect.top; 88 89 ev.preventDefault(); 90 } 91 92 private static onMouseMove(ev: MouseEvent) { 93 const draggedWindow = Window.currentlyDragged; 94 if (draggedWindow) { 95 const newLeft = ev.clientX - Window.mouseRelX; 96 const newTop = ev.clientY - Window.mouseRelY; 97 draggedWindow.position = [newLeft, newTop]; 98 } 99 } 100 101 set position([x, y]: [number, number]) { 102 const viewportWidth = document.documentElement.clientWidth; 103 const viewportHeight = document.documentElement.clientHeight; 104 105 const clampedLeft = Math.min(Math.max(0, x), viewportWidth - this.wraps.offsetWidth); 106 const clampedTop = Math.min(Math.max(0, y), viewportHeight - this.wraps.offsetHeight); 107 108 this.wraps.style.left = `${clampedLeft}px`; 109 this.wraps.style.top = `${clampedTop}px`; 110 } 111 112 public get position(): [number, number] { 113 const rect = this.wraps.getBoundingClientRect(); 114 return [rect.left, rect.top]; 115 } 116 117 118 private static onMouseUp(_ev: MouseEvent) { 119 if (Window.currentlyDragged) { 120 Window.currentlyDragged.titleBar.wraps.style.cursor = 'grab'; 121 Window.currentlyDragged = null; 122 } 123 } 124 125 // A single place to initialize global listeners 126 private static _initialized = false; 127 private static initializeGlobalListeners() { 128 if (this._initialized) return; 129 globalThis.addEventListener("mousemove", this.onMouseMove); 130 globalThis.addEventListener("mouseup", this.onMouseUp); 131 this._initialized = true; 132 } 133 134 // Static initializer block to set up listeners once 135 static { 136 this.initializeGlobalListeners(); 137 } 138} 139