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