https://domlink.deployments.hotsocket.fyi/
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