1/* ========================================================================== 2 wm.js â window manager. 3 4 Windows are absolutely positioned divs with a title bar you drag and a 5 resize bar you pull. The key (focused) window owns the URL, so every window 6 is a shareable link and the back button does what people expect. 7 ========================================================================== */ 8(function (global) { 9 "use strict"; 10 var el = U.el; 11 12 var apps = {}; // id -> definition 13 var open = []; // live windows, back to front 14 var layer = null; 15 var z = 10; 16 var cascade = 0; 17 var routing = false; // guard against hashchange feedback loops 18 19 function isSmall() { 20 return global.matchMedia("(max-width: 860px), (max-height: 520px)").matches; 21 } 22 23 function define(def) { apps[def.id] = def; } 24 25 function find(id) { 26 for (var i = 0; i < open.length; i++) if (open[i].id === id) return open[i]; 27 return null; 28 } 29 30 function nextPos(w, h) { 31 var pad = 176, maxX = Math.max(pad, global.innerWidth - w - 90); 32 var maxY = Math.max(20, global.innerHeight - h - 70); 33 var x = pad + (cascade % 6) * 26; 34 var y = 26 + (cascade % 6) * 24; 35 cascade++; 36 return { x: Math.min(x, maxX), y: Math.min(y, maxY) }; 37 } 38 39 function focus(win) { 40 if (!win) return; 41 open.forEach(function (w) { 42 w.el.classList.toggle("key", w === win); 43 if (isSmall()) w.el.classList.toggle("stack-hidden", w !== win); 44 else w.el.classList.remove("stack-hidden"); 45 }); 46 win.el.style.zIndex = ++z; 47 open.splice(open.indexOf(win), 1); 48 open.push(win); 49 Dock.sync(); 50 setHash(win); 51 } 52 53 /* Writing location.hash fires hashchange on a LATER task, so the guard has 54 to be cleared by the listener rather than on the next line. */ 55 function setHash(win) { 56 if (!win) return; 57 var h = "#/" + win.id + (win.route ? "/" + encodeURIComponent(win.route) : ""); 58 if (location.hash !== h) { routing = true; location.hash = h; } 59 } 60 61 function openApp(id, route, opts) { 62 var def = apps[id]; 63 if (!def) return null; 64 opts = opts || {}; 65 66 var existing = find(id); 67 if (existing && def.singleton !== false) { 68 existing.el.classList.remove("minimized"); 69 if (route != null && route !== existing.route && existing.navigate) { 70 existing.route = route; 71 existing.navigate(route); 72 } else if (route != null && !existing.navigate) { 73 existing.route = route; /* still loading: it mounts at the new route */ 74 } 75 focus(existing); 76 return existing; 77 } 78 79 var w = opts.w || def.w || 640, h = opts.h || def.h || 420; 80 w = Math.min(w, global.innerWidth - 40); 81 h = Math.min(h, global.innerHeight - 60); 82 var pos = (opts.x != null && opts.y != null) ? { x: opts.x, y: opts.y } : nextPos(w, h); 83 84 var label = el("span", { class: "tb-label", text: def.title }); 85 var minBtn = el("button", { class: "tb-btn tb-min", title: "Miniaturise", "aria-label": "Miniaturise" }, [el("i")]); 86 var closeBtn = el("button", { class: "tb-btn tb-close", title: "Close", "aria-label": "Close" }, [el("i")]); 87 var title = el("div", { class: "win-title" }, [minBtn, label, closeBtn]); 88 var body = el("div", { class: "win-body" }); 89 var resize = el("div", { class: "win-resize" }, [ 90 el("span", { class: "grip" }), el("span"), el("span", { class: "grip" }) 91 ]); 92 var node = el("div", { class: "win", "data-app": id, role: "dialog", "aria-label": def.title }, 93 [title, body, resize]); 94 node.style.left = pos.x + "px"; 95 node.style.top = pos.y + "px"; 96 node.style.width = w + "px"; 97 node.style.height = h + "px"; 98 99 var win = { 100 id: id, el: node, body: body, route: route || "", 101 setTitle: function (t) { label.textContent = t; }, 102 close: function () { closeWin(win); }, 103 focus: function () { focus(win); } 104 }; 105 106 layer.appendChild(node); 107 open.push(win); 108 109 node.addEventListener("mousedown", function () { if (open[open.length - 1] !== win) focus(win); }, true); 110 node.addEventListener("touchstart", function () { if (open[open.length - 1] !== win) focus(win); }, { capture: true, passive: true }); 111 closeBtn.addEventListener("click", function (e) { e.stopPropagation(); closeWin(win); }); 112 minBtn.addEventListener("click", function (e) { 113 e.stopPropagation(); 114 node.classList.add("minimized"); 115 Observer.record("window miniaturised", def.title); 116 var next = open.filter(function (o) { return !o.el.classList.contains("minimized"); }).pop(); 117 if (next) focus(next); else Dock.sync(); 118 }); 119 120 drag(title, win); 121 sizer(resize, win); 122 123 if (global.Need) Need.mount(def, body, win); else def.mount(body, win); 124 focus(win); 125 Observer.record("opened", def.title + (route ? " â " + route : "")); 126 return win; 127 } 128 129 function closeWin(win) { 130 if (win.onClose) try { win.onClose(); } catch (e) {} 131 leaveGhost(win); 132 win.el.remove(); 133 open.splice(open.indexOf(win), 1); 134 Observer.record("closed", (apps[win.id] || {}).title || win.id); 135 var next = open[open.length - 1]; 136 if (next) focus(next); 137 else { routing = true; location.hash = ""; routing = false; Dock.sync(); } 138 } 139 140 /* A closed window leaves an outline where it stood. It fades most of the 141 way out and then stops â the machine does not entirely let go. Ghosts 142 are capped so a long session does not accumulate a fog. */ 143 function leaveGhost(win) { 144 var layer = document.getElementById("ghosts"); 145 if (!layer || document.body.classList.contains("no-fx")) return; 146 if (win.el.classList.contains("minimized")) return; 147 var r = win.el.getBoundingClientRect(); 148 if (r.width < 40) return; 149 var g = el("div", { class: "ghost" }, [ 150 el("div", { class: "g-title", text: (apps[win.id] || {}).title || "" }) 151 ]); 152 g.style.left = r.left + "px"; 153 g.style.top = r.top + "px"; 154 g.style.width = r.width + "px"; 155 g.style.height = r.height + "px"; 156 layer.appendChild(g); 157 var keep = global.FX ? FX.num("ghosts.keep", 6) : 6; 158 while (layer.children.length > keep) layer.firstChild.remove(); 159 } 160 161 /* --- pointer helpers ---------------------------------------------------- */ 162 function pointer(e) { 163 var p = e.touches ? e.touches[0] : e; 164 return { x: p.clientX, y: p.clientY }; 165 } 166 function gesture(handle, onStart, onMove) { 167 function down(e) { 168 if (e.button != null && e.button !== 0) return; 169 if (e.target.closest && e.target.closest("button")) return; 170 var start = pointer(e), ctx = onStart(start); 171 if (!ctx) return; 172 e.preventDefault(); 173 function move(ev) { onMove(ctx, pointer(ev), start); } 174 function up() { 175 document.removeEventListener("mousemove", move); 176 document.removeEventListener("mouseup", up); 177 document.removeEventListener("touchmove", move); 178 document.removeEventListener("touchend", up); 179 } 180 document.addEventListener("mousemove", move); 181 document.addEventListener("mouseup", up); 182 document.addEventListener("touchmove", move, { passive: false }); 183 document.addEventListener("touchend", up); 184 } 185 handle.addEventListener("mousedown", down); 186 handle.addEventListener("touchstart", down, { passive: false }); 187 } 188 189 function drag(handle, win) { 190 gesture(handle, function () { 191 if (isSmall()) return null; 192 focus(win); 193 return { x0: win.el.offsetLeft, y0: win.el.offsetTop }; 194 }, function (ctx, p, start) { 195 var x = ctx.x0 + (p.x - start.x), y = ctx.y0 + (p.y - start.y); 196 x = Math.max(-win.el.offsetWidth + 90, Math.min(x, global.innerWidth - 60));
197 y = Math.max(0, Math.min(y, global.innerHeight - 30)); 198 win.el.style.left = x + "px"; 199 win.el.style.top = y + "px"; 200 }); 201 } 202 203 function sizer(bar, win) { 204 var mode = "s"; 205 bar.addEventListener("mousedown", function (e) { 206 mode = e.target.classList.contains("grip") 207 ? (e.target === bar.firstElementChild ? "sw" : "se") : "s"; 208 }); 209 bar.addEventListener("touchstart", function () { mode = "se"; }, { passive: true }); 210 gesture(bar, function () { 211 if (isSmall()) return null; 212 return { w: win.el.offsetWidth, h: win.el.offsetHeight, x: win.el.offsetLeft, m: mode }; 213 }, function (ctx, p, start) { 214 var dx = p.x - start.x, dy = p.y - start.y; 215 var h = Math.max(140, ctx.h + dy); 216 win.el.style.height = h + "px"; 217 if (ctx.m === "se") win.el.style.width = Math.max(280, ctx.w + dx) + "px"; 218 if (ctx.m === "sw") { 219 var w = Math.max(280, ctx.w - dx); 220 win.el.style.width = w + "px"; 221 win.el.style.left = (ctx.x + (ctx.w - w)) + "px"; 222 } 223 }); 224 } 225 226 /* --- routing ------------------------------------------------------------ */ 227 function fromHash() { 228 if (routing) { routing = false; return; } 229 var h = location.hash.replace(/^#\/?/, ""); 230 if (!h) return; 231 var parts = h.split("/"); 232 var id = parts.shift(); 233 var route = parts.join("/"); 234 try { route = decodeURIComponent(route); } catch (e) {} 235 if (apps[id]) openApp(id, route); 236 } 237 238 function init(container) { 239 layer = container; 240 global.addEventListener("hashchange", fromHash); 241 global.addEventListener("keydown", function (e) { 242 var top = open[open.length - 1]; 243 if (e.key === "Escape" && top && !e.target.closest("input, textarea")) closeWin(top); 244 if (e.key === "w" && (e.metaKey || e.ctrlKey) && top) { e.preventDefault(); closeWin(top); } 245 }); 246 global.addEventListener("resize", U.debounce(function () { 247 var top = open[open.length - 1]; 248 if (top) focus(top); 249 }, 160)); 250 } 251 252 global.WM = { 253 define: define, open: openApp, close: closeWin, focus: focus, 254 init: init, fromHash: fromHash, apps: apps, setRoute: setHash, 255 isOpen: function (id) { return !!find(id); }, 256 list: function () { return open.slice(); }, 257 def: function (id) { return apps[id]; } 258 }; 259})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.