scripts/xmlpage/xmlpage.js
//stores xml sources
import {isNumber} from '../path-controller/toolsys/toolprop.js';
let pagecache = new Map()
import {PackFlags, UIBase} from '../core/ui_base.js';
import {sliderDomAttributes} from '../widgets/ui_numsliders.js';
import * as util from '../util/util.js';
import {Menu} from '../widgets/ui_menu.js';
import {Icons} from '../core/ui_base.js';
import {Container} from '../core/ui.js';
export var domTransferAttrs = new Set(["id", "title", "tab-index"]);
export var domEventAttrs = new Set(["click", "mousedown", "mouseup", "mousemove", "keydown", "keypress"]);
export function parseXML(xml) {
let parser = new DOMParser()
xml = `<root>${xml}</root>`;
return parser.parseFromString(xml.trim(), "application/xml");
}
let num_re = /[0-9]+$/;
function getIconFlag(elem) {
if (!elem.hasAttribute("useIcons")) {
return 0;
}
let attr = elem.getAttribute("useIcons");
if (typeof attr === "string") {
attr = attr.toLowerCase().trim();
}
if (attr === "false" || attr === "no") {
return 0;
}
if (attr === "true" || attr === "yes") {
return PackFlags.USE_ICONS;
} else if (attr === "small") {
return PackFlags.SMALL_ICON | PackFlags.USE_ICONS;
} else if (attr === "large") {
return PackFlags.LARGE_ICON | PackFlags.USE_ICONS;
} else {
let isnum = typeof attr === "number";
let sheet = attr;
if (typeof sheet === "string" && sheet.search(num_re) === 0) {
sheet = parseInt(sheet);
isnum = true;
}
if (!isnum) {
return PackFlags.USE_ICONS;
}
let flag = PackFlags.USE_ICONS | PackFlags.CUSTOM_ICON_SHEET;
flag |= ((sheet - 1)<<PackFlags.CUSTOM_ICON_SHEET_START);
return flag;
}
return 0;
}
function getPackFlag(elem) {
let packflag = getIconFlag(elem);
if (elem.hasAttribute("drawChecks")) {
if (!getbool(elem, "drawChecks")) {
packflag |= PackFlags.HIDE_CHECK_MARKS;
} else {
packflag &= ~PackFlags.HIDE_CHECK_MARKS;
}
}
if (getbool(elem, "simpleSlider")) {
packflag |= PackFlags.SIMPLE_NUMSLIDERS;
}
if (getbool(elem, "rollarSlider")) {
packflag |= PackFlags.FORCE_ROLLER_SLIDER;
}
return packflag;
}
function myParseFloat(s) {
s = '' + s;
s = s.trim().toLowerCase();
if (s.endsWith("px")) {
s = s.slice(0, s.length - 2);
}
return parseFloat(s);
}
function getbool(elem, attr) {
let ret = elem.getAttribute(attr);
if (!ret) {
return false;
}
ret = ret.toLowerCase();
return ret === "1" || ret === "true" || ret === "yes";
}
function getfloat(elem, attr, defaultval) {
if (!elem.hasAttribute(attr)) {
return defaultval;
}
return myParseFloat(elem.getAttribute(attr));
}
/*
**
*
* a customHandler can be a callback, or the string "default", e.g.
*
* xmlpage.customHandlers["homegig-element-x"] = "default".
*
* The default case simply suppresses the warning that would otherwise
* be printed to the console.
* */
export const customHandlers = {};
class Handler {
constructor(ctx, container) {
this.container = container;
this.stack = [];
this.ctx = ctx;
this.codefuncs = {};
this.templateVars = {};
let attrs = util.list(sliderDomAttributes);
//note that useIcons, showLabel and sliderMode are PackFlag bits and are inherited through that system
this.inheritDomAttrs = {};
this.inheritDomAttrKeys = new Set(attrs);
}
push() {
this.stack.push(this.container);
this.stack.push(new Set(this.inheritDomAttrKeys));
this.stack.push(Object.assign({}, this.inheritDomAttrs));
}
pop() {
this.inheritDomAttrs = this.stack.pop();
this.inheritDomAttrKeys = this.stack.pop();
this.container = this.stack.pop();
}
handle(elem) {
if (elem.constructor === XMLDocument || elem.nodeName === 'root') {
for (let child of elem.childNodes) {
this.handle(child);
}
window.tree = elem
return;
} else if (elem.constructor === Text || elem.constructor === Comment) {
return;
}
let tagname = "" + elem.tagName;
if (tagname in customHandlers) {
customHandlers[tagname](this, elem);
} else if (this[tagname]) {
this[tagname](elem);
} else {
let elem2 = UIBase.createElement(tagname.toLowerCase());
window.__elem = elem;
//transfer DOM attributes
for (let k of elem.getAttributeNames()) {
elem2.setAttribute(k, elem.getAttribute(k));
}
if (elem2 instanceof UIBase) {
if (!elem2.hasAttribute("datapath") && elem2.hasAttribute("path")) {
elem2.setAttribute("datapath", elem2.getAttribute("path"));
}
if (elem2.hasAttribute("datapath")) {
let path = elem2.getAttribute("datapath");
path = this.container._joinPrefix(path);
elem2.setAttribute("datapath", path);
}
if (elem2.hasAttribute("massSetPath") || this.container.massSetPrefix) {
let massSetPath = "";
if (elem2.hasAttribute("massSetPath")) {
massSetPath = elem2.getAttribute("massSetPath");
}
let path = elem2.getAttribute("datapath");
path = this.container._getMassPath(this.container.ctx, path, massSetPath);
elem2.setAttribute("massSetPath", path);
elem2.setAttribute("mass_set_path", path);
}
this.container.add(elem2);
this._style(elem, elem2);
if (elem2 instanceof Container) {
this.push();
this.container = elem2;
this._container(elem, elem2, true);
this.visit(elem);
this.pop();
return;
}
} else {
console.warn("Unknown element " + elem.tagName + " (" + elem.constructor.name + ")");
let elem2 = document.createElement(elem.tagName.toLowerCase());
for (let attr of elem.getAttributeNames()) {
elem2.setAttribute(attr, elem.getAttribute(attr));
}
this._basic(elem, elem2);
this.container.shadow.appendChild(elem2);
if (!(elem2 instanceof UIBase)) {
elem2.pathux_ctx = this.container.ctx;
} else {
elem2.ctx = this.container.ctx;
}
}
this.visit(elem);
}
}
_style(elem, elem2) {
let style = {};
//try to handle class attribute, at least somewhat
if (elem.hasAttribute("class")) {
elem2.setAttribute("class", elem.getAttribute("class"));
let cls = elem2.getAttribute("class").trim();
let keys = [
cls,
(elem2.tagName.toLowerCase() + "." + cls).trim(),
"#" + elem.getAttribute("id").trim()
];
for (let sheet of document.styleSheets) {
for (let rule of sheet.rules) {
for (let k of keys) {
if (rule.selectorText.trim() === k) {
for (let k2 of rule.styleMap.keys()) {
let val = rule.style[k2]
style[k2] = val;
}
}
}
}
}
}
if (elem.hasAttribute("style")) {
let stylecode = elem.getAttribute("style");
stylecode = stylecode.split(";");
for (let row of stylecode) {
row = row.trim();
let i = row.search(/\:/);
if (i >= 0) {
let key = row.slice(0, i).trim();
let val = row.slice(i + 1, row.length).trim();
style[key] = val;
}
}
}
let keys = Object.keys(style);
if (keys.length === 0) {
return;
}
function setStyle() {
for (let k of keys) {
elem2.style[k] = style[k];
}
}
if (elem2 instanceof UIBase) {
elem2.setCSS.after(() => {
setStyle();
});
}
setStyle();
}
visit(node) {
for (let child of node.childNodes) {
this.handle(child)
}
}
_getattr(elem, k) {
let val = elem.getAttribute(k);
if (!val) {
return val;
}
if (val.startsWith("##")) {
val = val.slice(2, val.length).trim();
if (!(val in this.templateVars)) {
console.error(`unknown template variable '${val}'`);
val = '';
} else {
val = this.templateVars[val];
}
}
return val;
}
_basic(elem, elem2) {
this._style(elem, elem2);
for (let k of elem.getAttributeNames()) {
if (k.startsWith("custom")) {
elem2.setAttribute(k, this._getattr(elem, k));
}
}
let codeattrs = [];
for (let k of elem.getAttributeNames()) {
let val = ""+elem.getAttribute(k);
if (val.startsWith('ng[')) {
val = val.slice(3, val.endsWith("]") ? val.length-1 : val.length);
codeattrs.push([k, "ng", val]);
}
}
for (let k of domEventAttrs) {
let k2 = 'on' + k;
if (elem.hasAttribute(k2)) {
codeattrs.push([k, "dom", elem.getAttribute(k2)]);
}
}
for (let [k, eventType, id] of codeattrs) {
if (!(id in this.codefuncs)) {
console.error("Unknown code fragment " + id);
continue;
}
if (eventType === "dom") {
//click events usually don't go through normal
//dom event system
if (k === 'click') {
let onclick = elem2.onclick;
let func = this.codefuncs[id];
elem2.onclick = function () {
if (onclick) {
onclick.apply(this, arguments);
}
return func.apply(this, arguments);
}
} else {
elem2.addEventListener(k, this.codefuncs[id]);
}
} else if (eventType === "ng") {
elem2.addEventListener(k, this.codefuncs[id]);
}
}
for (let k of domTransferAttrs) {
if (elem.hasAttribute(k)) {
elem2.setAttribute(k, elem.getAttribute(k));
}
}
for (let k in this.inheritDomAttrs) {
if (!elem.hasAttribute(k)) {
elem.setAttribute(k, this.inheritDomAttrs[k]);
}
}
for (let k of sliderDomAttributes) {
if (elem.hasAttribute(k)) {
elem2.setAttribute(k, elem.getAttribute(k));
}
}
if (!(elem2 instanceof UIBase)) {
return;
}
if (elem.hasAttribute("theme-class")) {
elem2.overrideClass(elem.getAttribute("theme-class"));
if (elem2._init_done) {
elem2.setCSS();
elem2.flushUpdate();
}
}
if (elem.hasAttribute("useIcons") && typeof elem2.useIcons === "function") {
let val = elem.getAttribute("useIcons").trim().toLowerCase();
if (val === "small" || val === "true" || val === "yes") {
val = true;
} else if (val === "large") {
val = 1;
} else if (val === "false" || val === "no") {
val = false;
} else {
val = parseInt(val) - 1;
}
elem2.useIcons(val);
}
if (elem.hasAttribute("sliderTextBox")) {
let textbox = getbool(elem, "sliderTextBox");
if (textbox) {
elem2.packflag &= ~PackFlags.NO_NUMSLIDER_TEXTBOX;
elem2.inherit_packflag &= ~PackFlags.NO_NUMSLIDER_TEXTBOX;
} else {
elem2.packflag |= PackFlags.NO_NUMSLIDER_TEXTBOX;
elem2.inherit_packflag |= PackFlags.NO_NUMSLIDER_TEXTBOX;
}
//console.error("textBox", textbox, elem2, elem.getAttribute("sliderTextBox"), elem2.packflag);
}
if (elem.hasAttribute("sliderMode")) {
let sliderMode = elem.getAttribute("sliderMode");
if (sliderMode === "slider") {
elem2.packflag &= ~PackFlags.FORCE_ROLLER_SLIDER;
elem2.inherit_packflag &= ~PackFlags.FORCE_ROLLER_SLIDER;
elem2.packflag |= PackFlags.SIMPLE_NUMSLIDERS;
elem2.inherit_packflag |= PackFlags.SIMPLE_NUMSLIDERS;
} else if (sliderMode === "roller") {
elem2.packflag &= ~PackFlags.SIMPLE_NUMSLIDERS;
elem2.packflag |= PackFlags.FORCE_ROLLER_SLIDER;
elem2.inherit_packflag &= ~PackFlags.SIMPLE_NUMSLIDERS;
elem2.inherit_packflag |= PackFlags.FORCE_ROLLER_SLIDER;
}
//console.error("sliderMode", sliderMode, elem2, elem2.packflag & (PackFlags.SIMPLE_NUMSLIDERS | PackFlags.FORCE_ROLLER_SLIDER));
}
if (elem.hasAttribute("showLabel")) {
let state = getbool(elem, "showLabel");
if (state) {
elem2.packflag |= PackFlags.FORCE_PROP_LABELS;
elem2.inherit_packflag |= PackFlags.FORCE_PROP_LABELS;
} else {
elem2.packflag &= ~PackFlags.FORCE_PROP_LABELS;
elem2.inherit_packflag &= ~PackFlags.FORCE_PROP_LABELS;
}
}
function doBox(key) {
if (elem.hasAttribute(key)) {
let val = elem.getAttribute(key).toLowerCase().trim();
if (val.endsWith("px")) {
val = val.slice(0, val.length - 2).trim();
}
if (val.endsWith("%")) {
//eek! don't support at all?
//or use aspect overlay?
console.warn(`Relative styling of '${key}' may be unstable for this element`, elem);
elem.setCSS.after(function () {
this.style[key] = val;
});
} else {
val = parseFloat(val);
if (isNaN(val) || typeof val !== "number") {
console.error(`Invalid style ${key}:${elem.getAttribute(key)}`);
return;
}
elem2.overrideDefault(key, val);
elem2.setCSS();
elem2.style[key] = "" + val + "px";
}
}
}
doBox("width");
doBox("height");
doBox("margin");
doBox("padding");
for (let i = 0; i < 2; i++) {
let key = i ? "margin" : "padding";
doBox(key + "-bottom");
doBox(key + "-top");
doBox(key + "-left");
doBox(key + "-right");
}
}
_handlePathPrefix(elem, con) {
if (elem.hasAttribute("path")) {
let prefix = con.dataPrefix;
let path = elem.getAttribute("path").trim();
if (prefix.length > 0) {
prefix += ".";
}
prefix += path;
con.dataPrefix = prefix;
}
if (elem.hasAttribute("massSetPath")) {
let prefix = con.massSetPrefix;
let path = elem.getAttribute("massSetPath").trim();
if (prefix.length > 0) {
prefix += ".";
}
prefix += path;
con.massSetPrefix = prefix;
}
}
_container(elem, con, ignorePathPrefix=false) {
for (let k of this.inheritDomAttrKeys) {
if (elem.hasAttribute(k)) {
this.inheritDomAttrs[k] = elem.getAttribute(k);
}
}
let packflag = getPackFlag(elem);
con.packflag |= packflag;
con.inherit_packflag |= packflag;
this._basic(elem, con);
if (!ignorePathPrefix) {
this._handlePathPrefix(elem, con);
}
}
noteframe(elem) {
let ret = this.container.noteframe();
if (ret) {
this._basic(elem, ret);
}
}
cell(elem) {
this.push();
this.container = this.container.cell();
this._container(elem, this.container);
this.visit(elem);
this.pop();
}
table(elem) {
this.push();
this.container = this.container.table();
this._container(elem, this.container);
this.visit(elem);
this.pop();
}
panel(elem) {
let title = "" + elem.getAttribute("label")
let closed = getbool(elem, "closed")
this.push()
this.container = this.container.panel(title);
this.container.closed = closed;
this._container(elem, this.container);
this.visit(elem)
this.pop();
}
pathlabel(elem) {
this._prop(elem, "pathlabel")
}
/**
handle a code element, which are wrapped in functions
*/
code(elem) {
window._codelem = elem;
let buf = '';
for (let elem2 of elem.childNodes) {
if (elem2.nodeName === "#text") {
buf += elem2.textContent + '\n';
}
}
var func, $scope = this.templateScope;
buf = `
func = function() {
${buf};
}
`;
eval(buf);
let id = "" + elem.getAttribute("id");
this.codefuncs[id] = func;
}
textbox(elem) {
if (elem.hasAttribute("path")) {
this._prop(elem, 'textbox');
} else {
//let elem2 = this.container.textbox();
//this._basic(elem, elem2);
}
}
label(elem) {
let elem2 = this.container.label(elem.innerHTML);
this._basic(elem, elem2);
}
colorfield(elem) {
this._prop(elem, "colorfield");
}
/** simpleSliders=true enables simple sliders */
prop(elem) {
this._prop(elem, "prop")
}
_prop(elem, key) {
let packflag = getPackFlag(elem);
let path = elem.getAttribute("path");
let elem2;
if (key === 'pathlabel') {
elem2 = this.container.pathlabel(path, elem.innerHTML, packflag);
} else if (key === 'textbox') {
elem2 = this.container.textbox(path, undefined, undefined, packflag);
elem2.update();
//make textboxes non-modal by default
if (elem.hasAttribute("modal")) {
elem2.setAttribute("modal", elem.getAttribute("modal"));
}
if (elem.hasAttribute("realtime")) {
elem2.setAttribute("realtime", elem.getAttribute("realtime"));
}
} else if (key === "colorfield") {
elem2 = this.container.colorPicker(path, {
packflag,
themeOverride: elem.hasAttribute("theme-class") ? elem.getAttribute("theme-class") : undefined
});
} else {
elem2 = this.container[key](path, packflag);
}
if (!elem2) {
elem2 = document.createElement("span");
elem2.innerHTML = "error";
this.container.shadow.appendChild(elem2);
} else {
this._basic(elem, elem2);
if (elem.hasAttribute("massSetPath") || this.container.massSetPrefix) {
let mpath = elem.getAttribute("massSetPath");
if (!mpath) {
mpath = elem.getAttribute("path");
}
mpath = this.container._getMassPath(this.container.ctx, path, mpath);
elem2.setAttribute("mass_set_path", mpath);
}
}
}
strip(elem) {
this.push();
let dir;
if (elem.hasAttribute("mode")) {
dir = elem.getAttribute("mode").toLowerCase().trim();
dir = dir === "horizontal";
}
let margin1 = getfloat(elem, "margin1", undefined);
let margin2 = getfloat(elem, "margin2", undefined);
this.container = this.container.strip(undefined, margin1, margin2, dir);
this._container(elem, this.container);
this.visit(elem);
this.pop();
}
column(elem) {
this.push();
this.container = this.container.col();
this._container(elem, this.container);
this.visit(elem);
this.pop();
}
row(elem) {
this.push();
this.container = this.container.row();
this._container(elem, this.container);
this.visit(elem);
this.pop();
}
toolPanel(elem) {
this.tool(elem, "toolPanel");
}
tool(elem, key = "tool") {
let path = elem.getAttribute("path");
let packflag = getPackFlag(elem);
let noIcons = false, iconflags;
if (getbool(elem, "useIcons")) {
packflag |= PackFlags.USE_ICONS;
} else if (elem.hasAttribute("useIcons")) {
packflag &= ~PackFlags.USE_ICONS;
noIcons = true;
}
let label = ("" + elem.textContent).trim()
if (label.length > 0) {
path += "|" + label;
}
if (noIcons) {
iconflags = this.container.useIcons(false);
}
let elem2 = this.container[key](path, packflag);
if (elem2) {
this._basic(elem, elem2);
} else {
elem2 = document.createElement("strip")
elem2.innerHTML = "error"
this.container.shadow.appendChild(elem2);
this._basic(elem, elem2);
}
if (noIcons) {
this.container.inherit_packflag |= iconflags;
this.container.packflag |= iconflags;
}
}
dropbox(elem) {
return this.menu(elem, true);
}
menu(elem, isDropBox = false) {
let packflag = getPackFlag(elem);
let title = elem.getAttribute("name")
let list = [];
for (let child of elem.childNodes) {
if (child.tagName === "tool") {
let path = child.getAttribute("path");
let label = child.innerHTML.trim();
if (label.length > 0) {
path += "|" + label;
}
list.push(path);
} else if (child.tagName === "sep") {
list.push(Menu.SEP);
} else if (child.tagName === "item") {
let id, icon, hotkey, description;
if (child.hasAttribute("id")) {
id = child.getAttribute("id");
}
if (child.hasAttribute("icon")) {
icon = child.getAttribute("icon").toUpperCase().trim();
icon = Icons[icon];
}
if (child.hasAttribute("hotkey")) {
hotkey = child.getAttribute("hotkey");
}
if (child.hasAttribute("description")) {
description = child.getAttribute("description");
}
list.push({
name: child.innerHTML.trim(),
id, icon, hotkey, description
});
}
}
let ret = this.container.menu(title, list, packflag);
if (isDropBox) {
ret.removeAttribute("simple");
}
if (elem.hasAttribute("id")) {
ret.setAttribute("id", elem.getAttribute("id"));
}
this._basic(elem, ret);
return ret;
}
button(elem) {
let title = elem.innerHTML.trim();
let ret = this.container.button(title);
if (elem.hasAttribute("id")) {
ret.setAttribute("id", elem.getAttribute("id"));
}
this._basic(elem, ret);
}
iconbutton(elem) {
let title = elem.innerHTML.trim();
let icon = elem.getAttribute("icon");
if (icon) {
icon = UIBase.getIconEnum()[icon];
}
let ret = this.container.iconbutton(icon, title);
if (elem.hasAttribute("id")) {
ret.setAttribute("id", elem.getAttribute("id"));
}
this._basic(elem, ret);
}
tab(elem) {
this.push();
let title = "" + elem.getAttribute("label");
let tabs = this.container;
this.container = this.container.tab(title);
if (elem.hasAttribute("overflow")) {
this.container.setAttribute("overflow", elem.getAttribute("overflow"));
}
if (elem.hasAttribute("overflow-y")) {
this.container.setAttribute("overflow-y", elem.getAttribute("overflow-y"));
}
this._container(elem, this.container)
this.visit(elem);
this.pop();
}
tabs(elem) {
let pos = elem.getAttribute("pos") || "left"
this.push();
let tabs = this.container.tabs(pos)
this.container = tabs;
if (elem.hasAttribute("movable-tabs")) {
tabs.setAttribute("movable-tabs", elem.getAttribute("movable-tabs"));
}
this._container(elem, tabs);
this.visit(elem);
this.pop();
}
}
export function initPage(ctx, xml, parentContainer = undefined, templateVars = {}, templateScope = {}) {
let tree = parseXML(xml);
let container = UIBase.createElement("container-x");
container.ctx = ctx;
if (ctx) {
container._init();
}
if (parentContainer) {
parentContainer.add(container);
}
let handler = new Handler(ctx, container);
handler.templateVars = Object.assign({}, templateVars);
handler.templateScope = templateScope;
handler.handle(tree);
return container;
}
export function loadPage(ctx, url, parentContainer_or_args = undefined, loadSourceOnly = false,
modifySourceCB, templateVars, templateScope) {
let source;
let parentContainer;
if (parentContainer_or_args !== undefined && !(parentContainer_or_args instanceof HTMLElement)) {
let args = parentContainer_or_args;
parentContainer = args.parentContainer;
loadSourceOnly = args.loadSourceOnly;
modifySourceCB = args.modifySourceCB;
templateVars = args.templateVars;
templateScope = args.templateScope;
} else {
parentContainer = parentContainer_or_args;
}
if (pagecache.has(url)) {
source = pagecache.get(url);
if (modifySourceCB) {
source = modifySourceCB(source);
}
return new Promise((accept, reject) => {
if (loadSourceOnly) {
accept(source);
} else {
accept(initPage(ctx, source, parentContainer, templateVars, templateScope));
}
});
} else {
return new Promise((accept, reject) => {
fetch(url).then(res => res.text()).then(data => {
pagecache.set(url, data);
if (modifySourceCB) {
data = modifySourceCB(data);
}
if (loadSourceOnly) {
accept(data);
} else {
accept(initPage(ctx, data, parentContainer, templateVars, templateScope));
}
});
});
}
}