// wx-dom.js — shim additions for the WASM DOM port (non-universal build).
//
// Loaded as a second --pre-js AFTER wx.js, only in DOM-port bundles.
// wx.js keeps owning window management, the element registry, clipboard,
// file dialogs and the Canvas2D drawing used by owner-drawn widgets
// (canvas islands). This file owns native-control DOM elements: creation
// (type-based factory, composites where a wx control maps to more than one
// element), property sync, clone-based intrinsic measurement, and event
// wiring back into WASM via wx_dom_event (src/wasm/domevents.cpp).
//
// All functions are no-ops/guarded in Web Workers (-pthread evaluates
// pre-js there too) — see the wx.js worker guard precedent.
(function () {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
var nextControlId = 1;
var controls = new Map(); // domId -> root HTMLElement
var inputs = new Map(); // domId -> value-bearing element (if != root)
var labels = new Map(); // domId -> label text target (if != root)
// Flag read by the C++ keyboard callback (src/wasm/app.cpp): while a DOM
// editable owns browser focus, wx must not swallow/preventDefault keys.
window.wxDomEditableFocused = 0;
// Mirror of wxDomEventKind in include/wx/wasm/window.h.
var EVT = { CLICK: 1, INPUT: 2, CHANGE: 3, FOCUSIN: 4, FOCUSOUT: 5,
ENTER: 6, SPIN_UP: 7, SPIN_DOWN: 8, MENU: 9, TOOL: 10,
TAB: 11, SCROLL: 12 };
// Last pointer position in viewport (clientX/Y) coordinates — read by
// wxShowContextMenu when DoPopupMenu is invoked "at the mouse" (the KiCad
// canvas right-click case passes wxDefaultCoord). Tracked on every pointer
// event below, including over the GAL canvas (where forwardTarget is null).
var lastPointerClientX = 0;
var lastPointerClientY = 0;
// Marks the bundle as DOM-port for tests/boot code.
window.wxDomPort = true;
window.wxDomControls = controls;
function dispatch(domId, kind) {
try {
Module['ccall']('wx_dom_event', null, ['number', 'number'], [domId, kind]);
} catch (e) {
// Surfaces in test logs; must never throw back into DOM event handlers.
console.error('wx_dom_event(' + domId + ',' + kind + ') failed:', e);
}
}
function isEditable(el) {
return el && (el.tagName === 'TEXTAREA' ||
(el.tagName === 'INPUT' &&
['text', 'password', 'number', 'search'].indexOf(el.type) >= 0));
}
function flexCenter(el) {
el.dataset.wxDisplay = 'flex';
el.style.display = 'flex';
el.style.alignItems = 'center';
}
// Builds the element structure for a logical wx control type. Plain HTML
// tags ("span", "button", "textarea", "input"+typeAttr) pass through for
// the simple controls; composite types build wrappers.
// Returns { root, input, label } (input/label optional).
function buildControl(type, typeAttr) {
var root, input, label;
switch (type) {
case 'checkbox':
case 'radio': {
root = document.createElement('label');
input = document.createElement('input');
input.type = type;
input.style.margin = '0 3px 0 0';
label = document.createElement('span');
label.className = 'wx-label';
root.appendChild(input);
root.appendChild(label);
flexCenter(root);
break;
}
case 'toggle': {
root = document.createElement('button');
root.setAttribute('aria-pressed', 'false');
flexCenter(root);
root.style.justifyContent = 'center';
root.style.padding = '1px 9px';
break;
}
case 'statbox': {
// Visual chrome only: the wx children of a wxStaticBox are SIBLING
// DOM controls, so the fieldset must never intercept their input.
root = document.createElement('fieldset');
root.style.border = '1px solid #b5b2aa';
root.style.borderRadius = '2px';
label = document.createElement('legend');
label.className = 'wx-label';
label.style.padding = '0 3px';
root.appendChild(label);
root.dataset.wxChrome = '1';
break;
}
case 'statline': {
root = document.createElement('div');
root.style.background = '#909090';
root.dataset.wxChrome = '1';
break;
}
case 'gauge': {
root = document.createElement('progress');
root.max = 100;
root.value = 0;
break;
}
case 'slider': {
root = document.createElement('input');
root.type = 'range';
break;
}
case 'scrollbar': {
// Custom track+thumb (a native can't express a
// proportional thumb). Drag handlers are wired in wxDomCreateControl
// once the domId is known; metrics arrive via wxDomSetScrollbar.
// typeAttr "v" = vertical, "h" = horizontal.
root = document.createElement('div');
root.dataset.wxScrollbar = '1';
var sbVertical = (typeAttr === 'v');
root.style.background = '#e8e8e8';
root.style.userSelect = 'none';
var sbTrack = document.createElement('div');
sbTrack.className = 'wx-sb-track';
sbTrack.style.cssText = 'position:absolute;left:0;top:0;right:0;bottom:0;';
var sbThumb = document.createElement('div');
sbThumb.className = 'wx-sb-thumb';
sbThumb.style.cssText =
'position:absolute;background:#a0a0a0;border:1px solid #808080;' +
'border-radius:2px;box-sizing:border-box;touch-action:none;' +
'display:none;' +
(sbVertical ? 'left:0;right:0;top:0;height:0;'
: 'top:0;bottom:0;left:0;width:0;');
sbTrack.appendChild(sbThumb);
root.appendChild(sbTrack);
root._wxSb = { pos: 0, thumb: 0, range: 0, page: 0,
vertical: sbVertical, dragging: false, phase: 1,
dragStart: 0, dragStartOffset: 0 };
break;
}
case 'choice': {
root = document.createElement('select');
break;
}
case 'listbox': {
root = document.createElement('select');
root.multiple = true;
break;
}
case 'spinbutton': {
// vertical up/down button pair; clicks dispatch SPIN_UP/SPIN_DOWN
root = document.createElement('div');
root.dataset.wxSpin = '1';
var mk = function (txt, cls) {
var b = document.createElement('button');
b.textContent = txt;
b.className = cls;
b.style.cssText = 'flex:1;padding:0;margin:0;font-size:7px;' +
'line-height:1;min-height:0;overflow:hidden;';
root.appendChild(b);
return b;
};
root.style.display = 'flex';
root.dataset.wxDisplay = 'flex';
root.style.flexDirection = 'column';
mk('▲', 'wx-spin-up');
mk('▼', 'wx-spin-down');
break;
}
case 'radiobox': {
// owns its item rows (unlike statbox chrome): fieldset + legend +
// one per item,
// filled by wxDomSetItems.
root = document.createElement('fieldset');
root.style.border = '1px solid #b5b2aa';
root.style.borderRadius = '2px';
label = document.createElement('legend');
label.className = 'wx-label';
label.style.padding = '0 3px';
root.appendChild(label);
root.dataset.wxRadioBox = '1';
break;
}
case 'image': {
root = document.createElement('img');
root.dataset.wxChrome = '1'; // non-interactive like statbmp
break;
}
case 'combobox': {
// Editable combo: text input + datalist autocomplete (HTML has no
// native editable select). wxDomSetItems fills the datalist.
root = document.createElement('input');
root.type = 'text';
var dl = document.createElement('datalist');
dl.id = 'wx-datalist-' + nextControlId; // == the domId assigned below
root.setAttribute('list', dl.id);
document.body.appendChild(dl);
root.dataset.wxDatalist = dl.id;
break;
}
case 'checklistbox': {
// Scrollable list of checkbox rows; row checkbox toggles dispatch
// CHANGE with the row index retrievable via wxDomGetIntValue.
root = document.createElement('div');
root.dataset.wxCheckList = '1';
root.style.overflowY = 'auto';
root.style.background = '#ffffff';
root.style.border = '1px solid #b5b2aa';
break;
}
case 'menubar': {
// Horizontal strip of menu-title buttons; menus open as popup divs
// (built by wxDomMenuSetStructure).
root = document.createElement('div');
root.dataset.wxMenuBar = '1';
root.style.background = '#d4d0c8';
flexCenter(root);
break;
}
case 'toolbar': {
// Horizontal strip of tool buttons (built by wxDomToolbarSetTools).
root = document.createElement('div');
root.dataset.wxToolBar = '1';
root.style.background = '#d4d0c8';
flexCenter(root);
break;
}
case 'notebook': {
// The root box covers the whole page area, so it is CHROME — it
// must never swallow mouse events meant for page content (canvas
// islands and sibling DOM controls). Only the tab strip inside is
// interactive (built by wxDomNotebookSetTabs).
root = document.createElement('div');
root.dataset.wxNotebook = '1';
root.dataset.wxChrome = '1';
var strip = document.createElement('div');
strip.className = 'wx-tab-strip';
strip.setAttribute('role', 'tablist');
strip.style.cssText =
'position:absolute;left:0;top:0;right:0;display:flex;' +
'align-items:flex-end;background:#d4d0c8;' +
'border-bottom:1px solid #808080;pointer-events:auto;' +
'overflow:hidden;';
root.appendChild(strip);
break;
}
default: {
root = document.createElement(type);
if (typeAttr) root.setAttribute('type', typeAttr);
if (type === 'span' || type === 'button' || type === 'label') {
flexCenter(root);
}
if (type === 'button') {
root.style.padding = '1px 9px';
root.style.justifyContent = 'center';
}
break;
}
}
// Passive controls have no interactive DOM behavior of their own —
// left clicks on them belong to wx (KiCad binds LEFT_DOWN on labels).
if (root && (type === 'span' || type === 'gauge')) {
root.dataset.wxPassive = '1';
}
return { root: root, input: input, label: label };
}
window.wxDomCreateControl = function (tlwCssId, type, typeAttr) {
var container = window.__wxGetWindowElement(tlwCssId);
if (!container) {
console.error('wxDomCreateControl: no window element for css id ' + tlwCssId);
return 0;
}
var built = buildControl(type, typeAttr);
var el = built.root;
var domId = nextControlId++;
el.dataset.wxDomId = String(domId);
el.classList.add('wx-dom-control');
el.style.position = 'absolute';
el.style.left = '0px';
el.style.top = '0px';
el.style.boxSizing = 'border-box';
el.style.margin = '0';
if (!el.style.padding) el.style.padding = '0';
el.style.overflow = type === 'statbox' ? 'visible' : 'hidden';
if (el.dataset.wxDisplay === undefined) el.dataset.wxDisplay = '';
// wx labels never soft-wrap (the wx sizer sizes them exactly), but
// multiline wxStaticText labels DO contain hard \n breaks — 'pre'
// preserves those while preventing wrapping.