dom.mjs raw
1 // TinyJS Runtime — DOM Bridge
2 // Provides Go-callable browser DOM operations.
3 // Elements are tracked by integer handles.
4 // Function names match Go signatures (PascalCase).
5
6 const _elements = new Map();
7 let _nextId = 1;
8 const _callbacks = new Map();
9 let _nextCb = 1;
10
11 // Bootstrap: map handle 0 to document.body (available after DOMContentLoaded).
12 function ensureBody() {
13 if (!_elements.has(0) && typeof document !== 'undefined' && document.body) {
14 _elements.set(0, document.body);
15 }
16 }
17
18 // Element creation and lookup.
19 export function CreateElement(tag) {
20 const el = document.createElement(tag);
21 const id = _nextId++;
22 _elements.set(id, el);
23 return id;
24 }
25
26 export function CreateTextNode(text) {
27 const node = document.createTextNode(text);
28 const id = _nextId++;
29 _elements.set(id, node);
30 return id;
31 }
32
33 export function GetElementById(id) {
34 const el = document.getElementById(id);
35 if (!el) return -1;
36 const handle = _nextId++;
37 _elements.set(handle, el);
38 return handle;
39 }
40
41 export function QuerySelector(sel) {
42 const el = document.querySelector(sel);
43 if (!el) return -1;
44 const handle = _nextId++;
45 _elements.set(handle, el);
46 return handle;
47 }
48
49 export function Body() {
50 ensureBody();
51 return 0;
52 }
53
54 // Tree manipulation.
55 export function AppendChild(parentId, childId) {
56 const parent = _elements.get(parentId);
57 const child = _elements.get(childId);
58 if (parent && child) parent.appendChild(child);
59 }
60
61 export function RemoveChild(parentId, childId) {
62 const parent = _elements.get(parentId);
63 const child = _elements.get(childId);
64 if (parent && child) parent.removeChild(child);
65 }
66
67 export function FirstChild(parentId) {
68 const parent = _elements.get(parentId);
69 if (parent && parent.firstChild) {
70 const id = _nextId++;
71 _elements.set(id, parent.firstChild);
72 return id;
73 }
74 return 0;
75 }
76
77 export function NextSibling(elId) {
78 const el = _elements.get(elId);
79 if (el && el.nextSibling) {
80 const id = _nextId++;
81 _elements.set(id, el.nextSibling);
82 return id;
83 }
84 return 0;
85 }
86
87 export function InsertBefore(parentId, newId, refId) {
88 const parent = _elements.get(parentId);
89 const newEl = _elements.get(newId);
90 const ref = refId >= 0 ? _elements.get(refId) : null;
91 if (parent && newEl) parent.insertBefore(newEl, ref);
92 }
93
94 // Properties and attributes.
95 export function SetAttribute(elId, name, value) {
96 const el = _elements.get(elId);
97 if (el) el.setAttribute(name, value);
98 }
99
100 export function SetTextContent(elId, text) {
101 const el = _elements.get(elId);
102 if (el) el.textContent = text;
103 }
104
105 export function SetInnerHTML(elId, html) {
106 const el = _elements.get(elId);
107 if (el) el.innerHTML = html;
108 }
109
110 export function SetStyle(elId, prop, value) {
111 const el = _elements.get(elId);
112 if (el) el.style[prop] = value;
113 }
114
115 export function SetProperty(elId, prop, value) {
116 const el = _elements.get(elId);
117 if (el) el[prop] = value;
118 }
119
120 export function GetProperty(elId, prop) {
121 const el = _elements.get(elId);
122 if (el) return String(el[prop] ?? '');
123 return '';
124 }
125
126 export function AddClass(elId, cls) {
127 const el = _elements.get(elId);
128 if (el && el.classList) el.classList.add(cls);
129 }
130
131 export function RemoveClass(elId, cls) {
132 const el = _elements.get(elId);
133 if (el && el.classList) el.classList.remove(cls);
134 }
135
136 // Events.
137 export function AddEventListener(elId, event, callbackId) {
138 const el = _elements.get(elId);
139 const cb = _callbacks.get(callbackId);
140 if (el && cb) el.addEventListener(event, cb);
141 }
142
143 export function RemoveEventListener(elId, event, callbackId) {
144 const el = _elements.get(elId);
145 const cb = _callbacks.get(callbackId);
146 if (el && cb) el.removeEventListener(event, cb);
147 }
148
149 // Register a Go function as a JS callback. Returns callback ID.
150 export function RegisterCallback(fn) {
151 const id = _nextCb++;
152 _callbacks.set(id, fn);
153 return id;
154 }
155
156 export function ReleaseCallback(id) {
157 _callbacks.delete(id);
158 }
159
160 // Scheduling.
161 export function RequestAnimationFrame(fn) {
162 if (typeof window !== 'undefined') {
163 window.requestAnimationFrame(fn);
164 } else {
165 setTimeout(fn, 16);
166 }
167 }
168
169 export function SetTimeout(fn, ms) {
170 return setTimeout(fn, ms);
171 }
172
173
174
175 export function ClearTimeout(id) {
176 clearTimeout(id);
177 }
178
179 // Cleanup: release element handle.
180 export function ReleaseElement(id) {
181 _elements.delete(id);
182 }
183
184 // Fetch a URL as text, call fn with result.
185 export function FetchText(url, fn) {
186 fetch(url).then(r => r.text()).then(t => { if (fn) fn(t); });
187 }
188
189 // Fetch NIP-11 relay info document with Accept header.
190 export function FetchRelayInfo(url, fn) {
191 fetch(url, { headers: { 'Accept': 'application/nostr+json' } })
192 .then(r => r.text())
193 .then(t => { if (fn) fn(t); })
194 .catch(() => { if (fn) fn(''); });
195 }
196
197 // --- IndexedDB ---
198
199 let _db = null;
200 let _dbReady = [];
201
202 function ensureDB(cb) {
203 if (_db) { cb(_db); return; }
204 _dbReady.push(cb);
205 if (_dbReady.length > 1) return; // already opening
206 const req = indexedDB.open('sm3sh', 3);
207 req.onupgradeneeded = (e) => {
208 const db = e.target.result;
209 if (!db.objectStoreNames.contains('profiles')) db.createObjectStore('profiles');
210 if (!db.objectStoreNames.contains('relays')) db.createObjectStore('relays');
211 if (!db.objectStoreNames.contains('events')) {
212 const store = db.createObjectStore('events', { keyPath: 'id' });
213 store.createIndex('pubkey', 'pubkey', { unique: false });
214 store.createIndex('kind', 'kind', { unique: false });
215 store.createIndex('pubkey_kind', ['pubkey', 'kind'], { unique: false });
216 store.createIndex('created_at', 'created_at', { unique: false });
217 }
218 if (!db.objectStoreNames.contains('dms')) {
219 const dms = db.createObjectStore('dms', { keyPath: 'id' });
220 dms.createIndex('peer', 'peer', { unique: false });
221 dms.createIndex('peer_ts', ['peer', 'created_at'], { unique: false });
222 }
223 };
224 req.onblocked = () => { console.warn('[sm3sh] IDB upgrade blocked — close other tabs'); };
225 req.onsuccess = (e) => {
226 _db = e.target.result;
227 _db.onversionchange = () => { _db.close(); _db = null; };
228 for (const fn of _dbReady) fn(_db);
229 _dbReady = [];
230 };
231 req.onerror = () => { _dbReady = []; };
232 }
233
234 export function IDBGet(store, key, fn) {
235 ensureDB((db) => {
236 try {
237 const tx = db.transaction(store, 'readonly');
238 const req = tx.objectStore(store).get(key);
239 req.onsuccess = () => { fn(req.result ?? ''); };
240 req.onerror = () => { fn(''); };
241 } catch(e) { fn(''); }
242 });
243 }
244
245 export function IDBPut(store, key, value) {
246 ensureDB((db) => {
247 try {
248 const tx = db.transaction(store, 'readwrite');
249 tx.objectStore(store).put(value, key);
250 } catch(e) {}
251 });
252 }
253
254 export function IDBGetAll(store, fn, done) {
255 ensureDB((db) => {
256 try {
257 const tx = db.transaction(store, 'readonly');
258 const req = tx.objectStore(store).openCursor();
259 req.onsuccess = (e) => {
260 const cursor = e.target.result;
261 if (cursor) {
262 fn(String(cursor.key), String(cursor.value ?? ''));
263 cursor.continue();
264 } else {
265 done();
266 }
267 };
268 req.onerror = () => { done(); };
269 } catch(e) { done(); }
270 });
271 }
272
273 // Check if browser prefers dark color scheme.
274 export function PrefersDark() {
275 if (typeof window !== 'undefined' && window.matchMedia) {
276 return window.matchMedia('(prefers-color-scheme: dark)').matches;
277 }
278 return false;
279 }
280
281 // Log a message to the browser console.
282 export function ConsoleLog(msg) {
283 console.log('[sm3sh]', msg);
284 }
285
286 export function Confirm(msg) {
287 return confirm(msg);
288 }
289
290 // Send a raw JSON string to the service worker controller.
291 // Messages sent before the SW is active are queued and flushed on controllerchange.
292 let _swQueue = null;
293 export function PostToSW(msg) {
294 const sw = navigator.serviceWorker;
295 if (!sw) return;
296 if (sw.controller) {
297 sw.controller.postMessage(msg);
298 } else {
299 if (!_swQueue) {
300 _swQueue = [];
301 sw.addEventListener('controllerchange', () => {
302 if (sw.controller) {
303 for (const m of _swQueue) sw.controller.postMessage(m);
304 }
305 _swQueue = null;
306 }, { once: true });
307 }
308 _swQueue.push(msg);
309 }
310 }
311
312 // Register a handler for non-bus messages from the service worker.
313 // Bus relay (shell SW → satellite SWs) is handled in index.html inline script
314 // so it's active before WASM loads.
315 export function OnSWMessage(fn) {
316 if (!navigator.serviceWorker) return;
317 navigator.serviceWorker.addEventListener('message', (event) => {
318 const d = event.data;
319 // Bus messages handled by index.html — skip here.
320 if (typeof d === 'string' && d.length > 0 && d[0] === '{') return;
321 if (typeof d === 'string') {
322 fn(d);
323 } else if (Array.isArray(d) && d.length > 0) {
324 fn(JSON.stringify(d));
325 }
326 });
327 }
328
329 // --- History API ---
330
331 export function PushState(path) {
332 history.pushState(null, '', path);
333 }
334
335 export function ReplaceState(path) {
336 history.replaceState(null, '', path);
337 }
338
339 export function LocationReload() {
340 location.reload();
341 }
342
343 export function GetPath() {
344 return location.pathname + location.hash;
345 }
346
347 export function Hostname() {
348 return location.hostname;
349 }
350
351 export function Port() {
352 return location.port;
353 }
354
355 export function OnPopState(fn) {
356 window.addEventListener('popstate', () => {
357 fn(location.pathname + location.hash);
358 });
359 }
360
361