base/dist/base.js

829 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, NewState, Util, $, Hash, RefreshState } from "@web/state";
const HTTP = {
get: ({ url, ...opt }) => HTTP.request({ url, method: "GET", ...opt }),
post: ({ url, data, ...opt }) => HTTP.request({ url, method: "POST", data, ...opt }),
put: ({ url, data, ...opt }) => HTTP.request({ url, method: "PUT", data, ...opt }),
delete: ({ url, ...opt }) => HTTP.request({ url, method: "DELETE", ...opt }),
head: ({ url, ...opt }) => HTTP.request({ url, method: "HEAD", ...opt }),
request: async ({ url, method = "POST", data = void 0, headers = {}, responseType, timeout = 1e4 }) => {
var _a;
method = method.toUpperCase();
const options = { method, signal: (_a = AbortSignal.timeout) == null ? void 0 : _a.call(AbortSignal, timeout) };
if (data !== void 0 && method !== "GET" && method !== "HEAD") {
if (data instanceof HTMLFormElement) data = new FormData(data);
if (data && typeof data === "object" && !(data instanceof FormData) && !(data instanceof ArrayBuffer || ArrayBuffer.isView(data)) && Object.values(data).some((v) => v instanceof File || v instanceof Blob || v instanceof FileList || Array.isArray(v) && v.some((i) => i instanceof File || i instanceof Blob))) {
const fd = new FormData();
for (const [k, v] of Object.entries(data)) {
if (v instanceof FileList || Array.isArray(v)) Array.from(v).forEach((item) => fd.append(k, item));
else if (v !== void 0 && v !== null) fd.append(k, v);
}
data = fd;
}
if (data instanceof FormData) {
delete headers["Content-Type"];
} else if (typeof data !== "string" && !(data instanceof ArrayBuffer || ArrayBuffer.isView(data))) {
data = JSON.stringify(data);
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
}
options.body = data;
}
if (Object.keys(headers).length) options.headers = headers;
const response = { error: null, ok: null, status: 0, headers: {}, responseType: "", result: null };
try {
const resp = await fetch(url, options);
Object.assign(response, { ok: resp.ok, status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
if (!responseType) {
const contentType = resp.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) responseType = "json";
else if (/image|video|audio|pdf|zip|octet-stream/.test(contentType)) responseType = "binary";
else responseType = "text";
response.responseType = responseType;
}
if (response.ok === false) response.error = (response.statusText || "HTTP " + response.status + " error") + " for " + url;
if (responseType === "json") response.result = await resp.json();
else response.result = responseType === "binary" ? await resp.arrayBuffer() : await resp.text();
} catch (err) {
Object.assign(response, { error: err.message || String(err), ok: false });
}
return response;
}
};
const APIComponent = Component.register("API", (container) => {
container.request = NewState({ url: "", method: "GET", headers: {}, data: null, timeout: 1e4, responseType: "" });
container.response = NewState({ loading: false, ok: null, status: null, error: null, headers: {}, responseType: "", result: null });
container.result = NewState();
container.do = (opt = {}) => {
return new Promise((resolve, reject) => {
const req = { ...container.request, ...opt };
if (!req.url) throw new Error(".url is required");
req.headers = { ...container.request.headers, ...opt.headers };
container.response.loading = true;
HTTP.request(req).then((resp) => {
Object.keys(resp).forEach((k) => {
if (k !== "result") container.response[k] = resp[k];
});
if (resp.result && typeof resp.result === "object" && container.result && typeof container.result === "object") {
Object.assign(container.result, resp.result);
} else {
container.result = resp.result;
}
container.response.loading = false;
if (resp.ok === false) throw new Error(resp.error);
if (typeof resp.result === "object" && resp.result.error) throw new Error(resp.result.error);
container.dispatchEvent(new CustomEvent("response", { detail: resp, bubbles: false }));
resolve(resp);
}).catch((err) => {
var _a;
if (!opt.noui && ((_a = globalThis.UI) == null ? void 0 : _a.toast)) UI.toast(err.message, { type: "danger" });
container.dispatchEvent(new CustomEvent("error", { detail: err, bubbles: true }));
reject(err);
});
});
};
let _autoTimer = null;
container.request.__watch(null, () => {
if (!container.hasAttribute("auto") || !container.request.url) return;
if (_autoTimer) return;
_autoTimer = Promise.resolve().then(() => {
container.do();
_autoTimer = null;
});
});
});
const UI$1 = {};
Component.register("Modal", (container) => {
container.modal = new bootstrap.Modal(container);
container.addEventListener("bind", (e) => {
e.detail ? container.modal.show() : container.modal.hide();
});
container.addEventListener("hide.bs.modal", () => {
var _a;
(_a = document.activeElement) == null ? void 0 : _a.blur();
container.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: false }));
});
Util.copyFunction(container, container.modal, "show", "hide");
}, Util.makeDom(
/*html*/
`
<div class="modal fade" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div $class="modal-content text-bg-\${this.state?.type || 'primary'}">
<div slot-id="header" class="modal-header">
<h6 class="modal-title" $text="this.state?.title"></h6>
<button type="button" class="btn btn-link ms-2 bi bi-x-lg link-reset" style="color:inherit" data-bs-dismiss="modal"></button>
</div>
<div slot-id="body" class="modal-body"></div>
<div slot-id="footer" class="modal-footer"></div>
</div>
</div>
</div>
`
));
Component.register("Dialog", Component.getSetupFunction("Modal"), Util.makeDom(
/*html*/
`
<div class="modal fade" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div $class="modal-content text-bg-\${this.state?.type || 'primary'}">
<div $if="this.state?.title" class="modal-header" $text="this.state?.title"></div>
<div slot-id="body" class="modal-body"><div $html="this.state?.message"></div></div>
<div class="modal-footer">
<button $each="this.state?.buttons || ['{#Close#}']" type="button" class="btn btn-sm btn-\${index === buttons.length - 1 ? (type && type !== 'body' ? type : 'primary') : 'secondary'}" $onclick="this.result=index+1;this.hide()" $text="\${item}"></button>
</div>
</div>
</div>
</div>
`
));
let _dialogCount = 0;
UI$1.showDialog = function({ title = "", message = "", buttons = ["{#Close#}"], type = "body" }) {
const d = document.body.appendChild(document.createElement("Dialog"));
d.style.zIndex = 2e3 + ++_dialogCount;
Promise.resolve().then(() => {
Object.assign(d.state, { message, title, type, buttons });
d.show();
});
return new Promise((resolve) => {
d.addEventListener("change", (e) => {
_dialogCount--;
resolve(d.result || 0);
d.remove();
});
});
};
UI$1.alert = function(message, options = {}) {
return UI$1.showDialog({ message, ...options });
};
UI$1.confirm = function(message, options = {}) {
return new Promise((resolve) => UI$1.showDialog({ message, buttons: ["{#Cancel#}", "{#Confirm#}"], ...options }).then((index2) => resolve(index2 >= 2)).catch(() => resolve(false)));
};
Component.register("Toast", (container) => {
container.toast = new bootstrap.Toast(container, { autohide: container.state.delay > 0 });
Util.copyFunction(container, container.toast, "show", "hide");
container.addEventListener("show.bs.toast", () => {
if (container.state.delay > 0) {
let timer;
const startTimer = () => {
container.state.left = container.state.delay / 1e3;
timer = setInterval(() => {
if (!container.isConnected || --container.state.left <= 0) clearInterval(timer);
}, 1e3);
};
startTimer();
container.addEventListener("mouseenter", () => {
clearInterval(timer);
container.state.left = void 0;
});
container.addEventListener("mouseleave", startTimer);
}
});
}, Util.makeDom(
/*html*/
`
<div class="toast align-items-center border-0 m-1">
<div $class="toast-body rounded p-3 text-bg-\${this.state?.type}">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<span style="white-space:pre-wrap" class="fs-6" $text="this.state?.message"></span>
<span $if="this.state?.left !== undefined" class="small text-dim ms-2" $text="\${this.state?.left}s"></span>
</div>
<button type="button" class="btn btn-link ms-3 bi bi-x-lg link-reset" style="color:inherit" data-bs-dismiss="toast"></button>
</div>
<div class="d-flex justify-content-end gap-3">
<button $each="this.state?.buttons || ['{#Close#}']" type="button" $class="btn btn-sm btn-\${this.state?.type} mt-2" data-bs-dismiss="toast" $onclick="this.result=index+1" $text="item"></button>
</div>
</div>
</div>
`
), Util.makeDom(
/*html*/
`<div toast-container="default" class="position-fixed bottom-0 end-0 overflow-auto" style="z-index:3000;max-height:80%"></div>`
));
UI$1.toast = function(message, options = {}) {
const delay = options.delay ?? 5e3;
const t = document.createElement("Toast");
t.state = { delay, left: delay ? delay / 1e3 : void 0, type: options.type || "primary", message, buttons: options.buttons || [] };
$(`[toast-container="${options.container || "default"}"]`).appendChild(t);
Promise.resolve().then(() => t.show());
};
UI$1.toastConfirm = function(message, options = {}) {
return new Promise((resolve) => UI$1.toast(message, { buttons: ["{#Confirm#}"], ...options }).then((index2) => resolve(index2 === 1)).catch(() => resolve(false)));
};
Component.register("AutoForm", (container) => {
if (!container.state.schema) container.state.schema = [];
container.vertical = container.hasAttribute("vertical");
container.inline = container.hasAttribute("inline");
container.request = { method: "POST" };
container.response = {};
container.result = null;
const setupData = () => {
if (!container.data || !container.data.__watch) {
container.data = NewState(container.data || {});
}
container.data.__watch("*", () => {
if (container.inline) {
const dt = container.closest("DataTable");
if (dt && dt.refresh) dt.refresh();
}
});
};
if (container.data) setupData();
else requestAnimationFrame(setupData);
container.form = $(container, "form");
container.submit = (opt = {}) => {
var _a, _b;
if (!container.form.reportValidity()) return (_b = (_a = globalThis.UI) == null ? void 0 : _a.toast) == null ? void 0 : _b.call(_a, "{#verify failed#}", { type: "danger" });
if (!container.dispatchEvent(new CustomEvent("submit", { detail: container.data, cancelable: true, bubbles: false }))) return;
const req = { ...container.request, data: container.data, noui: true, ...opt };
let task = null;
if (container.api) task = container.api.do(req);
else if (container.request.url) task = HTTP.request(req);
else return console.warn("{#please config .api or .request.url to auto submit#}");
task.then((resp) => {
container.response = resp;
container.result = resp.result;
if (typeof resp.result === "object" && resp.result.error) throw new Error(resp.result.error);
container.dispatchEvent(new CustomEvent("response", { detail: resp, bubbles: false }));
}).catch((err) => {
var _a2;
if ((_a2 = globalThis.UI) == null ? void 0 : _a2.toast) UI.toast(err.message, { type: "danger" });
container.dispatchEvent(new CustomEvent("error", { detail: err, bubbles: true }));
});
};
}, Util.makeDom(
/*html*/
`
<div $class="'auto-form-root' + (this.inline ? ' auto-form-inline min-h-100 w-100' : '')">
<form $class="'h-100 w-100 d-flex ' + (this.inline ? 'align-items-center' : (this.vertical ? 'flex-column' : 'auto-grid-form'))" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
<div $each="this.state.schema || []" style="display:contents">
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
<div control-wrapper $class="this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'">
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" class="form-control" $type="item.type" $.="item.setting || {}" $bind="this.data[item.name]" $class="item.type === 'number' ? 'text-end' : ''">
<select $if="item.type === 'select'" $name="item.name" class="form-select" $.="item.setting || {}" $bind="this.data[item.name]">
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
</select>
<div $if="['checkbox', 'radio'].includes(item.type)" $class="this.inline ? 'h-100 d-flex align-items-center px-2 gap-3 justify-content-center w-100' : ''" style="white-space:nowrap">
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="'form-check' + (item.vertical ? '' : ' form-check-inline') + ' mb-0 d-flex align-items-center p-0'">
<input $name="item.name" class="form-check-input me-1" $type="item.type" $.="item.setting || {}" $value="item.options?option:'on'" $bind="this.data[item.name]">
<span $if="!this.inline || (item.options && item.options.length > 0)" $text="option" class="form-check-label"></span>
</label>
</div>
<div $if="item.type==='switch'" class="form-check form-switch fs-4 h-100 d-flex align-items-center px-2 m-0 justify-content-center w-100" style="padding-left:0">
<input $name="item.name" class="form-check-input m-0" type="checkbox" style="cursor:pointer" $bind="this.data[item.name]">
</div>
<textarea $if="item.type==='textarea'" $name="item.name" class="form-control" $.="item.setting || {}" $bind="this.data[item.name]"></textarea>
</div>
</div>
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
<div slot-id="actions"></div>
<button type="submit" class="btn btn-primary" $text="this.submitlabel || '{#Submit#}'"></button>
</div>
</form>
</div>
`
), Util.makeDom(
/*html*/
`<style>
@media (min-width: 576px) { .auto-grid-form {display: grid;grid-template-columns: max-content 1fr} .auto-grid-form .col-form-label {text-align: right; margin-bottom: 1rem;padding-right: 1rem;max-width: 200px} }
.auto-form-inline { background-color: var(--bs-body-bg); }
.auto-form-inline.min-h-100 { min-height: 100%; height: auto; }
.auto-form-inline .form-control, .auto-form-inline .form-select { border-radius: 0; border: none; background-color: transparent !important; min-height: 100%; height: auto; width: 100%; padding: 0 8px; box-shadow: none; color: inherit; }
.auto-form-inline .form-select { background-position: right 4px center; background-size: 12px 10px; padding-right: 20px; }
.auto-form-inline textarea.form-control { padding: 8px; min-height: 100px; height: auto; }
</style>`
));
const _pendingAutoFormComponents = [];
const AutoForm = {
register: (name) => {
if (typeof document !== "undefined") {
if (document.readyState !== "loading" && Component.getTemplate("AutoForm")) AutoForm._addAutoFormComponent(name);
else _pendingAutoFormComponents.push(name);
}
},
_addAutoFormComponent: (name) => {
var _a;
const template = Component.getTemplate("AutoForm");
if (template) {
(_a = $(template.content, "[control-wrapper]")) == null ? void 0 : _a.appendChild(Util.makeDom(`<${name} $if="item.type?.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]"></${name}>`));
}
}
};
if (typeof document !== "undefined") {
const initAutoForm = () => {
_pendingAutoFormComponents.forEach((name) => AutoForm._addAutoFormComponent(name));
_pendingAutoFormComponents.length = 0;
};
if (document.readyState !== "loading") setTimeout(initAutoForm, 100);
else document.addEventListener("DOMContentLoaded", () => setTimeout(initAutoForm, 100), true);
}
Component.register("TagsInput", (container) => {
container.addEventListener("bind", (e) => {
container.state.tags = e.detail || [];
});
}, Util.makeDom(
/*html*/
`
<div class="form-control d-flex flex-wrap gap-1 align-items-center" style="min-height:38px;cursor:text">
<button $each="this.state.tags" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody((event) => {
if (["Backspace", "Delete"].includes(event.key)) {
event.preventDefault();
(void 0).state.tags.splice(index, 1);
(void 0).state.tags = (void 0).state.tags;
(void 0).dispatchEvent(new CustomEvent("change", { bubbles: false, detail: (void 0).state.tags }));
Promise.resolve().then(() => {
const buttons = $$(void 0, "button");
(buttons.length > 0 ? buttons[index > 0 ? index - 1 : 0] : $(void 0, "input")).focus();
});
}
})}" $text="item"></button>
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none" $onkeydown="${Util.getFunctionBody((event) => {
if (event.isComposing) return;
if (["Enter", ",", " "].includes(event.key)) {
event.preventDefault();
const v = thisNode.value.trim();
if (v && !(void 0).state.tags.includes(v)) {
(void 0).state.tags.push(v);
(void 0).state.tags = (void 0).state.tags;
(void 0).dispatchEvent(new CustomEvent("change", { bubbles: false, detail: (void 0).state.tags }));
}
thisNode.value = "";
}
})}">
</div>
`
), Util.makeDom(
/*html*/
`<style>TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}</style>`
));
AutoForm.register("TagsInput");
const VirtualScroll = () => {
const itemHeights = /* @__PURE__ */ new Map();
const groupHeights = /* @__PURE__ */ new Map();
let groupItemCount = 1;
const avg = Util.newAvg();
let padTop = 0;
let rowGap = 0;
let topMargin = 0;
let itemMarginTop = null;
let itemMarginBottom = null;
let listInited = false;
return {
// 核心1重置与首屏预渲染
reset: (list, container) => {
listInited = false;
itemHeights.clear();
groupHeights.clear();
avg.clear();
topMargin = 0;
itemMarginTop = null;
itemMarginBottom = null;
if (!(list == null ? void 0 : list.length)) return [];
const size = list.length;
groupItemCount = Math.ceil(Math.sqrt(size)) || 10;
const style = window.getComputedStyle(container);
padTop = parseFloat(style.paddingTop) || 0;
rowGap = parseFloat(style.rowGap) || 0;
return list.slice(0, Math.min(30, size));
},
// 核心2无痛初始化缓存 (通过 rAF 调用)
init: (list, refreshCallback) => {
if (listInited) return;
const size = list.length;
const defaultHeight = avg.get() || 32;
for (let i = 0; i < size; i++) {
if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight);
}
for (let i = 0; i < size; i += groupItemCount) {
let sum = 0;
for (let j = i; j < Math.min(i + groupItemCount, size); j++) {
sum += itemHeights.get(j);
}
groupHeights.set(i, sum);
}
listInited = true;
refreshCallback();
},
// 核心3原汁原味的极速高度更新
update: (absoluteIndex, node) => {
if (itemMarginTop === null) {
const style = window.getComputedStyle(node);
itemMarginTop = parseFloat(style.marginTop) || 0;
itemMarginBottom = parseFloat(style.marginBottom) || 0;
}
if (absoluteIndex === 0) topMargin = itemMarginTop;
const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap;
const oldHeight = itemHeights.get(absoluteIndex);
if (newHeight !== oldHeight) {
itemHeights.set(absoluteIndex, newHeight);
avg.add(newHeight);
const offset = newHeight - (oldHeight || 0);
const groupIndex = absoluteIndex - absoluteIndex % groupItemCount;
if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset);
}
},
// 核心4原汁原味的状态机滚动计算
calc: (container, list) => {
if (!listInited || !list) return null;
const size = list.length;
const visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32));
let prev = padTop + topMargin + rowGap;
let post = 0;
let status = 0;
let listStartIndex = 0;
let listEndIndex = 0;
for (let i = 0; i < size; i++) {
if (status === 0) {
const gh = groupHeights.get(i);
if (gh && prev + gh < container.scrollTop) {
prev += gh;
i += Math.min(groupItemCount, size - i) - 1;
} else {
const ih = itemHeights.get(i);
if (prev + ih < container.scrollTop) {
prev += ih;
} else {
status = 1;
let visibleStartIndex = Math.max(0, i);
listStartIndex = Math.max(0, visibleStartIndex - visibleCount);
listEndIndex = Math.min(listStartIndex + visibleCount * 3, size);
i = listEndIndex - 1;
for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j);
}
}
} else if (status === 1) {
const gh = groupHeights.get(i);
if (gh) {
post += gh;
i += groupItemCount - 1;
} else {
post += itemHeights.get(i);
}
}
}
return {
prevHeight: prev - padTop - topMargin - rowGap,
postHeight: post,
renderedList: list.slice(listStartIndex, listEndIndex),
listStartIndex
};
}
};
};
const FastListComponent = Component.register("FastList", (container) => {
const vs = VirtualScroll();
container.state.renderedList = [];
container.onItemUpdate = (index2, node) => {
vs.update(index2 + (container.state._listStartIndex || 0), node);
};
container.refresh = () => {
const res = vs.calc(container, container.state.list);
if (res) {
container.state.prevHeight = res.prevHeight;
container.state.postHeight = res.postHeight;
container.state._listStartIndex = res.listStartIndex;
container.state.renderedList = res.renderedList;
}
};
container.state.__watch("list", (list) => {
container.state._listStartIndex = 0;
container.state.renderedList = vs.reset(list, container) || [];
requestAnimationFrame(() => {
if (container.state.list !== list) return;
vs.init(list, container.refresh);
});
});
}, Util.makeDom(
/*html*/
`
<div class="overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none">
<div $if="this.state.prevHeight>0" $style="height:\${this.state.prevHeight}px;" class="flex-shrink-0"></div>
<div slot-id="item" class="flex-shrink-0" $each="this.state.renderedList" $onupdate="this.onItemUpdate(index,thisNode)"></div>
<div $if="this.state.postHeight>0" $style="height:\${this.state.postHeight}px;" class="flex-shrink-0"></div>
</div>
`
));
Component.attachSelectFeature = (container, eventName = "itemclick", selectActionName = "selectItem", activeActionName = "itemActiveTag", selectedItemKey = "selectedItem", idfieldKey = "idfield", selectableAttr = "auto-select") => {
const selectable = selectableAttr && container.hasAttribute(selectableAttr);
container[selectActionName] = (item, index2) => {
if (selectable) container.state[selectedItemKey] = container.state[selectedItemKey] === item[container[idfieldKey]] ? null : item[container[idfieldKey]];
container.dispatchEvent(new CustomEvent(eventName, { bubbles: false, detail: !selectable || container.state[selectedItemKey] ? { item, index: index2 } : { item: {}, index: null } }));
};
container[activeActionName] = (item) => container.state[selectedItemKey] !== null && container.state[selectedItemKey] === item[container[idfieldKey]] ? "active" : "";
};
Component.register("List", (container) => {
Component.attachSelectFeature(container);
const defaultSets = { idfield: "id", labelfield: "label", summaryfield: "summary" };
container.state.__watch("list", (oldList) => {
Util.updateDefaults(container, defaultSets);
});
}, Util.makeDom(
/*html*/
`
<div class="list-group overflow-auto">
<div slot="item" slot-id="item" $each="this.state.list" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action list-group-item-heading d-inline-flex pe-auto align-items-baseline \${this.itemActiveTag?.(item)}">
<span $if="this.itemicon" class="bi bi-\${this.itemicon} text-body"></span>
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</div>
`
));
Component.register("GroupedList", (container) => {
Component.getSetupFunction("List")(container);
container._selectItem = container.selectItem;
container.selectItem = (item, index2) => {
container.state.selectedGroup = null;
container._selectItem(item, index2);
};
Component.attachSelectFeature(container, "groupclick", "selectGroup", "groupActiveTag", "selectedGroup", "groupidfield", container.hasAttribute("auto-select-group") || container.hasAttribute("auto-select") && "group-selectable");
container._selectGroup = container.selectGroup;
container.selectGroup = (item, index2) => {
container.state.selectedItem = null;
container._selectGroup(item, index2);
};
const defaultSets = { typefield: "type", groupidfield: "id", grouplabelfield: "label", groupsummaryfield: "summary", groupfield: "group" };
container.state.__watch("list", (oldList) => {
Util.updateDefaults(container, defaultSets);
const newList = [];
(container.state.groups || []).forEach((group) => {
newList.push({ [container.typefield]: "group", ...group });
if (oldList) oldList.forEach((item) => {
if (item[container.groupfield] === group[container.groupidfield]) {
newList.push({ [container.typefield]: "item", ...item });
}
});
});
return newList;
});
}, Util.makeDom(
/*html*/
`
<div class="list-group overflow-auto">
<template $each="this.state.list">
<div slot-id="group" $if="item.type === 'group'" $onclick="this.selectGroup(item,index)" style="height: 36px" $class="list-group-item list-group-item-action small d-inline-flex align-items-center ps-2 pe-2 \${this.groupActiveTag(item)}">
<span $if="this.groupicon" $class="bi bi-\${this.groupicon} text-body"></span>
<div class="flex-shrink-0 px-1" $text>\${item[this.grouplabelfield]}</div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.groupsummaryfield]"></div>
<div slot-id="group-actions"></div>
</div>
<div slot-id="item" $if="item.type === 'item'" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action d-inline-flex align-items-baseline ps-3 pe-2 \${this.itemActiveTag?.(item)}">
<span $if="this.itemicon" $class="bi bi-\${this.itemicon} text-body"></span>
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</template>
</div>
`
));
Component.register("FastGroupedList", (container) => {
Component.getSetupFunction("List")(container);
Component.getSetupFunction("GroupedList")(container);
container.refresh = () => {
var _a;
return (_a = container.querySelector("FastList")) == null ? void 0 : _a.refresh();
};
}, Util.makeDom(
/*html*/
`
<FastList class="list-group" $.state.list="this.state.list">
<div slot="item">
<div slot-id="group" $if="item.type === 'group'" $onclick="this.selectGroup(item,index)" style="height: 36px" $class="list-group-item list-group-item-action small d-inline-flex align-items-center ps-2 pe-2 \${this.groupActiveTag(item)}">
<span $if="this.groupicon" $class="bi bi-\${this.groupicon} text-body"></span>
<div class="flex-shrink-0 px-1" $text>\${item[this.grouplabelfield]}</div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.groupsummaryfield]"></div>
<div slot-id="group-actions"></div>
</div>
<div slot-id="item" $if="item.type === 'item'" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action d-inline-flex align-items-baseline ps-3 pe-2 \${this.itemActiveTag?.(item)}">
<span $if="this.itemicon" $class="bi bi-\${this.itemicon} text-body"></span>
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</div>
</FastList>
`
));
Component.register("Tree", (container) => {
const defaultSets = { parentfield: "parent", groupicon: "folder", itemicon: "file" };
container.state.__watch("list", () => {
Util.updateDefaults(container, defaultSets);
const childrenMap = {};
(container.state.list || []).forEach((item) => {
var _a;
return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item);
});
const newList = [];
const traverse = (list, level) => list.forEach((item) => {
var _a;
const id = item[container.idfield];
const hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length);
newList.push({ ...item, _level: level, _hasChildren: hasChildren });
if (hasChildren) traverse(childrenMap[id], level + 1);
});
traverse(childrenMap[""] || [], 0);
return newList;
});
}, Util.makeDom(
/*html*/
`
<List>
<div slot="item" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
<div $style="width:\${item._level * 16}px" class="flex-shrink-0"></div>
<div $class="text-muted bi bi-\${item._hasChildren?this.groupicon:this.itemicon}"></div>
<div class="flex-grow-1 text-truncate px-2" $text="item[this.labelfield]"></div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</List>
`
));
const FastTreeComponent = Component.register("FastTree", (container) => {
Component.getSetupFunction("List")(container);
Component.getSetupFunction("Tree")(container);
container.refresh = () => {
var _a;
return (_a = container.querySelector("FastList")) == null ? void 0 : _a.refresh();
};
}, Util.makeDom(
/*html*/
`
<FastList class="list-group list-group-action" $.state.list="this.state.list">
<div slot="item" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
<div $style="width:\${item._level * 16}px" class="flex-shrink-0"></div>
<div $class="text-muted bi bi-\${item._hasChildren?this.groupicon:this.itemicon}"></div>
<div class="flex-grow-1 text-truncate px-2" $text="item[this.labelfield]"></div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</FastList>
`
));
Component.register("CollapseTree", (container) => {
Component.attachSelectFeature(container);
const defaultSets = { idfield: "id", parentfield: "parent", labelfield: "label", summaryfield: "summary" };
container.collapsed = NewState({});
container.state.__watch("list", () => {
Util.updateDefaults(container, defaultSets);
const childrenMap = {};
(container.state.list || []).forEach((item) => {
var _a;
return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item);
});
const newList = [];
const traverse = (list, level, parents) => list.forEach((item) => {
var _a;
const id = item[container.idfield];
const hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length);
newList.push({ ...item, _level: level, _hasChildren: hasChildren, _parents: parents });
if (hasChildren) traverse(childrenMap[id], level + 1, [...parents, id]);
});
traverse(childrenMap[""] || [], 0, []);
return newList;
});
}, Util.makeDom(
/*html*/
`
<List>
<div slot="item" slot-id="item" $if="!item._parents.some(cur=>this.collapsed[cur])" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
<div $style="width:\${item._level * 16 + 16}px; cursor: pointer;" class="text-end text-muted flex-shrink-0" $onclick="event.stopPropagation();this.collapsed[item[this.idfield]] = !this.collapsed[item[this.idfield]]">
<i $if="item._hasChildren" $class="bi \${this.collapsed[item[this.idfield]] ? 'bi-caret-right-fill' : 'bi-caret-down-fill'}"></i>
</div>
<div class="flex-grow-1 text-truncate px-1" $text="item[this.labelfield]"></div>
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
<div slot-id="item-actions"></div>
</div>
</List>
`
));
Component.register("Nav", (container) => {
container.click = (item, noselect) => {
if (!item.noselect && !noselect) Hash.nav = item.name;
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
}, Util.makeDom(
/*html*/
`
<div class="navbar navbar-expand bg-body-secondary px-3 pb-0 border-bottom align-items-center">
<img $if="this.state.brand.image" $src="this.state.brand.image" class="me-2" style="height:30px;width:auto;max-width:300px">
<i $if="this.state.brand.icon" $class="bi bi-\${this.state.brand.icon} me-2"></i>
<span $if="this.state.brand.label" class="me-2" $text="this.state.brand.label"></span>
<div class="ms-2"></div>
<div $each="this.state.list" $class="navbar-nav text-truncate \${item.type==='fill'?'flex-fill':''}">
<button $if="item.type==='button'" $class="nav-link \${Hash.nav===item.name?'active':''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="d-none d-\${this.state.list.length>5?'lg':'md'}-inline" $text="item.label"></span>
</button>
<div $if="item.type==='dropdown'" class="dropdown">
<button $class="nav-link \${Hash.nav===item.name?'active':''}" data-bs-toggle="dropdown">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="d-none d-\${this.state.list.length>5?'lg':'md'}-inline" $text="item.label"></span>
</button>
<div class="dropdown-menu dropdown-menu-end p-3 bg-body-secondary shadow" $style="width: \${item.width || 250}px;">
<template $each="item.list" as="subitem">
<button $if="subitem.type==='button'" class="nav-link px-0 w-100 text-start" $onclick="this.click(subitem, true)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
<div $if="subitem.type==='switch'" class="d-flex align-items-center">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch"><input class="form-check-input mx-0" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</div>
</div>
</div>
</div>
`
));
let _mouseMoverMoving = false;
let _mouseMoverPos = {};
let _mouseMoverEvents = {};
const MouseMover = {
start: (event, { onmousemove, onmouseup }) => {
_mouseMoverPos = { x: event.clientX, y: event.clientY, w: 0, h: 0 };
_mouseMoverEvents = { onmousemove, onmouseup };
_mouseMoverMoving = true;
}
};
if (typeof document !== "undefined") {
document.addEventListener("mouseup", (event) => {
var _a;
if (!_mouseMoverMoving) return;
_mouseMoverMoving = false;
(_a = _mouseMoverEvents.onmouseup) == null ? void 0 : _a.call(_mouseMoverEvents, { event, ..._mouseMoverPos });
});
document.addEventListener("mousemove", (event) => {
var _a;
if (!_mouseMoverMoving) return;
_mouseMoverPos.w = event.clientX - _mouseMoverPos.x;
_mouseMoverPos.h = event.clientY - _mouseMoverPos.y;
(_a = _mouseMoverEvents.onmousemove) == null ? void 0 : _a.call(_mouseMoverEvents, { event, ..._mouseMoverPos });
});
}
Component.register("Resizer", (container) => {
container.isVertical = container.hasAttribute("vertical");
const min = parseInt(container.getAttribute("min")) || 10;
const max = parseInt(container.getAttribute("max")) || 1e3;
const target = container.target || container.previousElementSibling;
container.addEventListener("bind", (e) => {
if (e.detail !== void 0 && e.detail !== null) {
target.style[container.isVertical ? "height" : "width"] = e.detail + "px";
}
});
const getSize = (startSize, w, h) => {
const newSize = startSize + (container.isVertical ? h : w);
return newSize < min ? min : newSize > max ? max : newSize;
};
container.addEventListener("mousedown", (event) => {
const startSize = container.isVertical ? target.offsetHeight : target.offsetWidth;
MouseMover.start(event, {
onmousemove: ({ w, h }) => {
const newSize = getSize(startSize, w, h);
target.style[container.isVertical ? "height" : "width"] = newSize + "px";
container.dispatchEvent(new CustomEvent("resizing", { detail: { oldSize: startSize, newSize }, bubbles: false }));
},
onmouseup: ({ w, h }) => {
const newSize = getSize(startSize, w, h);
container.dispatchEvent(new CustomEvent("resize", { detail: { oldSize: startSize, newSize }, bubbles: false }));
container.dispatchEvent(new CustomEvent("change", { detail: newSize, bubbles: false }));
}
});
});
}, Util.makeDom(
/*html*/
`
<div $class="border-\${this.isVertical?'top':'start'} flex-shrink-0" $style="\${this.isVertical?'height':'width'}:3px;\${!this.isVertical?'height':'width'}:100%;cursor:\${this.isVertical?'row-resize':'col-resize'}"></div>
`
));
const State = NewState({ exitBlocks: 0 });
globalThis.State = State;
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", (event) => {
if (State.exitBlocks > 0) event.preventDefault();
});
}
const htmlNode = document.documentElement;
if (!htmlNode.hasAttribute("$data-bs-theme") && !htmlNode.hasAttribute("data-bs-theme")) {
htmlNode.setAttribute("$data-bs-theme", "LocalStorage.darkMode?'dark':'light'");
}
globalThis.HTTP = HTTP;
globalThis.UI = UI$1;
globalThis.AutoForm = AutoForm;
globalThis.MouseMover = MouseMover;
if (typeof document !== "undefined") {
const doRefresh = () => RefreshState(document.documentElement);
if (document.readyState !== "loading") doRefresh();
else document.addEventListener("DOMContentLoaded", doRefresh, true);
}
export {
APIComponent,
AutoForm,
FastListComponent,
FastTreeComponent,
HTTP,
MouseMover,
State,
UI$1 as UI,
VirtualScroll
};