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; // Safe up to 2^53 in JS. No wrap needed.
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 0;
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 0;
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 InsertBefore(parentId, newId, refId) {
78 const parent = _elements.get(parentId);
79 const newEl = _elements.get(newId);
80 const ref = refId >= 0 ? _elements.get(refId) : null;
81 if (parent && newEl) parent.insertBefore(newEl, ref);
82 }
83
84 export function ReplaceChild(parentId, newId, oldId) {
85 const parent = _elements.get(parentId);
86 const newEl = _elements.get(newId);
87 const oldEl = _elements.get(oldId);
88 if (parent && newEl && oldEl) parent.replaceChild(newEl, oldEl);
89 }
90
91 // Properties and attributes.
92 export function SetAttribute(elId, name, value) {
93 const el = _elements.get(elId);
94 if (el) el.setAttribute(name, value);
95 }
96
97 export function RemoveAttribute(elId, name) {
98 const el = _elements.get(elId);
99 if (el) el.removeAttribute(name);
100 }
101
102 export function SetTextContent(elId, text) {
103 const el = _elements.get(elId);
104 if (el) el.textContent = text;
105 }
106
107 export function SetInnerHTML(elId, html) {
108 const el = _elements.get(elId);
109 if (el) el.innerHTML = html;
110 }
111
112 export function SetStyle(elId, prop, value) {
113 const el = _elements.get(elId);
114 if (el) el.style[prop] = value;
115 }
116
117 export function Focus(elId) {
118 const el = _elements.get(elId);
119 if (el && el.focus) el.focus();
120 }
121
122 export function SetProperty(elId, prop, value) {
123 const el = _elements.get(elId);
124 if (el) el[prop] = value;
125 }
126
127 export function GetProperty(elId, prop) {
128 const el = _elements.get(elId);
129 if (el) return String(el[prop] ?? '');
130 return '';
131 }
132
133 export function AddClass(elId, cls) {
134 const el = _elements.get(elId);
135 if (el && el.classList) el.classList.add(cls);
136 }
137
138 export function RemoveClass(elId, cls) {
139 const el = _elements.get(elId);
140 if (el && el.classList) el.classList.remove(cls);
141 }
142
143 // Events.
144 export function AddEventListener(elId, event, callbackId) {
145 const el = _elements.get(elId);
146 const cb = _callbacks.get(callbackId);
147 if (el && cb) el.addEventListener(event, cb);
148 }
149
150 // AddEnterKeyListener fires callback when Enter is pressed on the element.
151 export function AddEnterKeyListener(elId, callbackId) {
152 const el = _elements.get(elId);
153 const cb = _callbacks.get(callbackId);
154 if (el && cb) el.addEventListener('keydown', function(e) {
155 if (e.key === 'Enter') { e.preventDefault(); cb(); }
156 });
157 }
158
159 // Like AddEventListener but only fires when e.target === e.currentTarget.
160 // Useful for backdrop click-to-close without catching child clicks.
161 export function AddSelfEventListener(elId, event, callbackId) {
162 const el = _elements.get(elId);
163 const cb = _callbacks.get(callbackId);
164 if (el && cb) el.addEventListener(event, function(e) {
165 if (e.target === e.currentTarget) cb();
166 });
167 }
168
169 export function RemoveEventListener(elId, event, callbackId) {
170 const el = _elements.get(elId);
171 const cb = _callbacks.get(callbackId);
172 if (el && cb) el.removeEventListener(event, cb);
173 }
174
175 // Register a Go function as a JS callback. Returns callback ID.
176 export function RegisterCallback(fn) {
177 const id = _nextCb++;
178 _callbacks.set(id, fn);
179 return id;
180 }
181
182 export function ReleaseCallback(id) {
183 _callbacks.delete(id);
184 }
185
186 // Scheduling.
187 export function RequestAnimationFrame(fn) {
188 if (typeof window !== 'undefined') {
189 window.requestAnimationFrame(fn);
190 } else {
191 setTimeout(fn, 16);
192 }
193 }
194
195 export function SetTimeout(fn, ms) {
196 return setTimeout(fn, ms);
197 }
198
199 export function SetInterval(fn, ms) {
200 return setInterval(fn, ms);
201 }
202
203 export function ClearInterval(id) {
204 clearInterval(id);
205 }
206
207 export function ClearTimeout(id) {
208 clearTimeout(id);
209 }
210
211 // Cleanup: release element handle.
212 export function ReleaseElement(id) {
213 _elements.delete(id);
214 }
215
216 // Fetch a URL as text, call fn with result.
217 export function FetchText(url, fn) {
218 fetch(url).then(r => r.text()).then(t => { if (fn) fn(t); });
219 }
220
221 // Navigation.
222 export function NextSibling(elId) {
223 const el = _elements.get(elId);
224 if (el && el.nextSibling) {
225 const id = _nextId++;
226 _elements.set(id, el.nextSibling);
227 return id;
228 }
229 return 0;
230 }
231
232 // Console / browser.
233 export function ConsoleLog(msg) {
234 console.log(msg);
235 }
236
237 export function PrefersDark() {
238 if (typeof window !== 'undefined' && window.matchMedia) {
239 return window.matchMedia('(prefers-color-scheme: dark)').matches;
240 }
241 return false;
242 }
243
244 export function Confirm(msg) {
245 if (typeof window !== 'undefined') return window.confirm(msg);
246 return false;
247 }
248
249 // Service Worker communication.
250 let _swMessageHandler = null;
251
252 export function PostToSW(msg) {
253 if (navigator.serviceWorker && navigator.serviceWorker.controller) {
254 navigator.serviceWorker.controller.postMessage(msg);
255 }
256 }
257
258 export function OnSWMessage(fn) {
259 _swMessageHandler = fn;
260 if (navigator.serviceWorker) {
261 navigator.serviceWorker.addEventListener('message', function(e) {
262 if (_swMessageHandler && typeof e.data === 'string') {
263 _swMessageHandler(e.data);
264 }
265 });
266 }
267 }
268
269 // IndexedDB (simple key-value).
270 let _idb = null;
271 const _idbName = 'smesh-kv';
272 const _idbStores = ['profiles', 'events', 'cache'];
273
274 function _openIDB() {
275 return new Promise(function(resolve, reject) {
276 if (_idb) { resolve(_idb); return; }
277 const req = indexedDB.open(_idbName, 1);
278 req.onupgradeneeded = function(e) {
279 const db = e.target.result;
280 for (const s of _idbStores) {
281 if (!db.objectStoreNames.contains(s)) db.createObjectStore(s);
282 }
283 };
284 req.onsuccess = function(e) { _idb = e.target.result; resolve(_idb); };
285 req.onerror = function() { reject(req.error); };
286 });
287 }
288
289 export function IDBGet(store, key, fn) {
290 _openIDB().then(function(db) {
291 const tx = db.transaction(store, 'readonly');
292 const req = tx.objectStore(store).get(key);
293 req.onsuccess = function() { fn(req.result ?? ''); };
294 req.onerror = function() { fn(''); };
295 }).catch(function() { fn(''); });
296 }
297
298 export function IDBPut(store, key, value) {
299 _openIDB().then(function(db) {
300 const tx = db.transaction(store, 'readwrite');
301 tx.objectStore(store).put(value, key);
302 });
303 }
304
305 export function IDBGetAll(store, fn, done) {
306 _openIDB().then(function(db) {
307 const tx = db.transaction(store, 'readonly');
308 const req = tx.objectStore(store).openCursor();
309 req.onsuccess = function(e) {
310 const cursor = e.target.result;
311 if (cursor) {
312 fn(String(cursor.key), String(cursor.value));
313 cursor.continue();
314 } else {
315 if (done) done();
316 }
317 };
318 req.onerror = function() { if (done) done(); };
319 }).catch(function() { if (done) done(); });
320 }
321
322 // Fetch relay NIP-11 info.
323 export function FetchRelayInfo(url, fn) {
324 fetch(url, { headers: { 'Accept': 'application/nostr+json' } })
325 .then(function(r) { return r.text(); })
326 .then(function(t) { if (fn) fn(t); })
327 .catch(function() { if (fn) fn(''); });
328 }
329
330 // History / location.
331 export function PushState(path) {
332 if (typeof history !== 'undefined') history.pushState(null, '', path);
333 }
334
335 export function ReplaceState(path) {
336 if (typeof history !== 'undefined') history.replaceState(null, '', path);
337 }
338
339 export function LocationReload() {
340 if (typeof location !== 'undefined') location.reload();
341 }
342
343 export function GetPath() {
344 if (typeof location !== 'undefined') return location.pathname;
345 return '/';
346 }
347
348 export function Hostname() {
349 if (typeof location !== 'undefined') return location.hostname;
350 return '';
351 }
352
353 export function Port() {
354 if (typeof location !== 'undefined') return location.port;
355 return '';
356 }
357
358 export function UserAgent() {
359 if (typeof navigator !== 'undefined') return navigator.userAgent;
360 return '';
361 }
362
363 export function OnPopState(fn) {
364 if (typeof window !== 'undefined') {
365 window.addEventListener('popstate', function() {
366 if (fn) fn(location.pathname);
367 });
368 }
369 }
370
371 // DownloadText triggers a file download with the given text content.
372 export function DownloadText(filename, content, mimeType) {
373 const blob = new Blob([content], { type: mimeType || 'text/plain' });
374 const url = URL.createObjectURL(blob);
375 const a = document.createElement('a');
376 a.href = url;
377 a.download = filename;
378 document.body.appendChild(a);
379 a.click();
380 setTimeout(function() { a.remove(); URL.revokeObjectURL(url); }, 1000);
381 }
382
383 // PickFileText opens a file picker and reads the selected file as text.
384 export function PickFileText(accept, fn) {
385 const input = document.createElement('input');
386 input.type = 'file';
387 if (accept) input.accept = accept;
388 input.style.display = 'none';
389 input.addEventListener('change', function() {
390 if (!input.files || !input.files[0]) { fn(''); return; }
391 const reader = new FileReader();
392 reader.onload = function() { fn(reader.result || ''); };
393 reader.onerror = function() { fn(''); };
394 reader.readAsText(input.files[0]);
395 });
396 document.body.appendChild(input);
397 input.click();
398 // Clean up after a delay (even if user cancels).
399 setTimeout(function() { input.remove(); }, 60000);
400 }
401
402 // NowSeconds returns current Unix time in seconds.
403 export function NowSeconds() {
404 return BigInt(Math.floor(Date.now() / 1000));
405 }
406
407 // Get raw element (for advanced use within JS runtime only).
408 export function getRawElement(id) {
409 return _elements.get(id);
410 }
411