"""HTML page builder and JS bridge for ISKG widgets."""
from __future__ import annotations
from typing import Any
from .fonts import font_css
BRIDGE_JS = """
// ISKG Client Bridge for pywebview
(function() {
window.iskg_bridge_event = function(widgetId, eventName, eventData) {
try {
if (typeof pywebview === 'undefined' || !pywebview.api || !pywebview.api.on_event) {
console.log('[ISKG:bridge] pywebview not ready', widgetId, eventName, eventData);
return;
}
pywebview.api.on_event(widgetId, eventName, JSON.stringify(eventData || ''));
} catch(e) {
console.warn('[ISKG:bridge]', e.message);
}
};
window.iskg_set_text = function(id, text) {
var el = document.getElementById(id);
if (el) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.value = text;
} else {
el.innerText = text;
}
}
};
window.iskg_set_html = function(id, html) {
var el = document.getElementById(id);
if (el) el.innerHTML = html;
};
window.iskg_set_value = function(id, value) {
var el = document.getElementById(id);
if (el) {
if (el.tagName === 'INPUT' && el.type === 'range') {
el.value = value;
var evt = new Event('input', { bubbles: true });
el.dispatchEvent(evt);
} else {
el.value = value;
}
}
};
window.iskg_set_attr = function(id, attr, value) {
var el = document.getElementById(id);
if (el) el.setAttribute(attr, value);
};
window.iskg_set_enabled = function(id, enabled) {
var el = document.getElementById(id);
if (el) {
el.disabled = !enabled;
el.classList.toggle('disabled', !enabled);
}
};
window.iskg_set_visible = function(id, visible) {
var el = document.getElementById(id);
if (el) el.style.display = visible ? '' : 'none';
};
window.iskg_add_class = function(id, cls) {
var el = document.getElementById(id);
if (el) el.classList.add(cls);
};
window.iskg_remove_class = function(id, cls) {
var el = document.getElementById(id);
if (el) el.classList.remove(cls);
};
window.iskg_toggle_class = function(id, cls) {
var el = document.getElementById(id);
if (el) el.classList.toggle(cls);
};
window.iskg_focus = function(id) {
var el = document.getElementById(id);
if (el) el.focus();
};
window.iskg_bind_key = function(id, eventType, keyFilter, mods) {
var el = document.getElementById(id);
if (!el) { setTimeout(function(){iskg_bind_key(id,eventType,keyFilter,mods);},50); return; }
var fn = function(e) {
if (keyFilter && e.key !== keyFilter && e.code !== keyFilter && e.key.toLowerCase() !== keyFilter.toLowerCase()) return;
if (mods) {
if (mods.ctrl && !e.ctrlKey) return;
if (mods.alt && !e.altKey) return;
if (mods.shift && !e.shiftKey) return;
}
var data = JSON.stringify({key:e.key,code:e.code,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey});
iskg_bridge_event(id, eventType, data);
};
el.addEventListener(eventType === 'keyrelease' ? 'keyup' : 'keydown', fn);
el._iskg_key_fn = fn;
};
window.iskg_unbind_key = function(id, eventType) {
var el = document.getElementById(id);
if (el && el._iskg_key_fn) {
el.removeEventListener(eventType === 'keyrelease' ? 'keyup' : 'keydown', el._iskg_key_fn);
delete el._iskg_key_fn;
}
};
window.iskg_set_style = function(id, cssText) {
var el = document.getElementById(id);
if (!el) return;
var props = cssText.split(';');
for (var i = 0; i < props.length; i++) {
var p = props[i].trim();
if (!p) continue;
var colon = p.indexOf(':');
if (colon > 0) {
var name = p.substring(0, colon).trim();
var value = p.substring(colon + 1).trim();
el.style[name] = value;
}
}
};
// Theme switching at runtime
window._ISKG_THEMES = {};
window.iskg_register_themes = function(themes) {
window._ISKG_THEMES = themes;
};
window.iskg_apply_theme = function(vars) {
var root = document.documentElement;
for (var key in vars) {
if (vars.hasOwnProperty(key)) {
root.style.setProperty(key, vars[key]);
}
}
};
window.iskg_set_theme = function(name) {
var theme = window._ISKG_THEMES[name];
if (theme) {
iskg_apply_theme(theme);
return true;
}
return false;
};
})();
"""
[docs]
def build_html(
root_widgets: list[Any],
ifaz_css: str,
extra_js: str = "",
theme_name: str = "ifaz",
) -> str:
all_widgets: list[Any] = []
for w in root_widgets:
all_widgets.extend(w._collect_widgets())
rendered_html = "".join(w._render() for w in root_widgets if not w._destroyed)
all_js_parts: list[str] = []
for w in root_widgets:
if not w._destroyed:
all_js_parts.append(w._render_js())
all_js_parts.append(w._render_children_js())
# Include base JS (tooltips, key bindings) for every widget in the tree
for _, w in all_widgets:
if not w._destroyed:
all_js_parts.append(w._render_tooltip_js())
all_js_parts.append(w._render_key_bindings_js())
all_js = "\n".join(p for p in all_js_parts if p)
if extra_js:
all_js += "\n" + extra_js
# Embed theme data for runtime switching
import json as _json
from .themes import THEMES as _THEMES
theme_data = _json.dumps(_THEMES, indent=2)
theme_init = f"iskg_register_themes({theme_data});\niskg_set_theme('{theme_name}');\n"
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ISKG App</title>
<style>{font_css()}</style>
<style>{ifaz_css}</style>
</head>
<body>
<div id="iskg-root">{rendered_html}</div>
<script>
{BRIDGE_JS}
</script>
<script>
// Theme registration & init
{theme_init}
// Widget initialization
{all_js}
</script>
</body>
</html>"""