821 lines
39 KiB
JavaScript
821 lines
39 KiB
JavaScript
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);
|
||
}, Util.makeDom(
|
||
/*html*/
|
||
`
|
||
<FastList class="list-group">
|
||
<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);
|
||
}, Util.makeDom(
|
||
/*html*/
|
||
`
|
||
<FastList class="list-group list-group-action">
|
||
<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;
|
||
}
|
||
};
|
||
globalThis.MouseMover = MouseMover;
|
||
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'}" $style="\${this.isVertical?'height':'width'}:3px;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();
|
||
});
|
||
}
|
||
globalThis.HTTP = HTTP;
|
||
globalThis.UI = UI$1;
|
||
globalThis.AutoForm = AutoForm;
|
||
globalThis.MouseMover = MouseMover;
|
||
if (typeof document !== "undefined") {
|
||
const doRefresh = () => {
|
||
console.log("Base project triggering RefreshState");
|
||
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
|
||
};
|