2026-06-10 12:49:43 +08:00
|
|
|
(function() {
|
2026-06-05 20:05:26 +08:00
|
|
|
"use strict";
|
2026-06-10 12:49:43 +08:00
|
|
|
(function(global) {
|
|
|
|
|
const { Component, Util, $ } = global;
|
|
|
|
|
const UI = {};
|
|
|
|
|
Component.register("Modal", (container) => {
|
|
|
|
|
container.modal = new bootstrap.Modal(container);
|
|
|
|
|
container.addEventListener("bind", (e) => {
|
|
|
|
|
e.detail ? container.modal.show() : container.modal.hide();
|
2026-06-05 20:05:26 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
container.addEventListener("hide.bs.modal", () => {
|
|
|
|
|
var _a;
|
|
|
|
|
(_a = document.activeElement) == null ? void 0 : _a.blur();
|
|
|
|
|
container.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: false }));
|
2026-05-14 20:04:31 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
Util.copyFunction(container, container.modal, "show", "hide");
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
2026-05-14 20:04:31 +08:00
|
|
|
<div class="modal fade" data-bs-backdrop="static">
|
|
|
|
|
<div class="modal-dialog modal-dialog-centered">
|
2026-05-27 23:02:25 +08:00
|
|
|
<div $class="modal-content border-\${this.state?.type || 'primary'} border-2 shadow-lg">
|
|
|
|
|
<div slot-id="header" class="modal-header py-2 px-3 bg-light">
|
|
|
|
|
<h6 $class="modal-title fw-bold text-\${this.state?.type || 'primary'}" $text="this.state?.title"></h6>
|
|
|
|
|
<button type="button" class="btn btn-link ms-2 bi bi-x-lg link-reset p-0" style="color:inherit; text-decoration:none" data-bs-dismiss="modal"></button>
|
2026-05-14 20:04:31 +08:00
|
|
|
</div>
|
2026-05-27 23:02:25 +08:00
|
|
|
<div slot-id="body" class="modal-body p-3"></div>
|
|
|
|
|
<div slot-id="footer" class="modal-footer py-2 px-3 bg-light"></div>
|
2026-05-14 20:04:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
Component.register("Dialog", Component.getSetupFunction("Modal"), Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
2026-05-14 20:04:31 +08:00
|
|
|
<div class="modal fade" data-bs-backdrop="static">
|
|
|
|
|
<div class="modal-dialog modal-dialog-centered">
|
2026-05-27 23:02:25 +08:00
|
|
|
<div $class="modal-content border-\${this.state?.type || 'primary'} border-2 shadow-lg">
|
2026-06-06 11:45:53 +08:00
|
|
|
<template $if="this.state?.title">
|
|
|
|
|
<div $class="modal-header py-2 px-3 bg-light fw-bold text-\${this.state?.type || 'primary'}" $text="this.state?.title"></div>
|
|
|
|
|
</template>
|
2026-05-27 23:02:25 +08:00
|
|
|
<div slot-id="body" class="modal-body p-4"><div $html="this.state?.message"></div></div>
|
|
|
|
|
<div class="modal-footer py-2 px-3 bg-light">
|
2026-06-06 11:45:53 +08:00
|
|
|
<template $each="this.state?.buttons || ['{#Close#}']">
|
|
|
|
|
<button type="button" $class="btn btn-sm px-3 btn-\${index === (this.state?.buttons || []).length - 1 ? (this.state?.type && this.state?.type !== 'body' ? this.state?.type : 'primary') : 'outline-secondary border'}" $onclick="this.result=index+1;this.hide()" $text="\${item}"></button>
|
|
|
|
|
</template>
|
2026-05-14 20:04:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
let _dialogCount = 0;
|
|
|
|
|
UI.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();
|
2026-05-14 20:04:31 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
d.addEventListener("change", (e) => {
|
|
|
|
|
_dialogCount--;
|
|
|
|
|
resolve(d.result || 0);
|
|
|
|
|
d.remove();
|
2026-06-05 20:05:26 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
UI.alert = function(message, options = {}) {
|
|
|
|
|
return UI.showDialog({ message, ...options });
|
|
|
|
|
};
|
|
|
|
|
UI.confirm = function(message, options = {}) {
|
|
|
|
|
return new Promise((resolve) => UI.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*/
|
|
|
|
|
`
|
2026-05-14 20:04:31 +08:00
|
|
|
<div class="toast align-items-center border-0 m-1">
|
2026-05-17 16:59:44 +08:00
|
|
|
<div $class="toast-body rounded p-3 text-bg-\${this.state?.type}">
|
2026-05-14 20:04:31 +08:00
|
|
|
<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>
|
2026-06-06 11:45:53 +08:00
|
|
|
<template $if="this.state?.left !== undefined">
|
|
|
|
|
<span class="small text-dim ms-2" $text="\${this.state?.left}s"></span>
|
|
|
|
|
</template>
|
2026-05-14 20:04:31 +08:00
|
|
|
</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">
|
2026-06-06 11:45:53 +08:00
|
|
|
<template $each="this.state?.buttons || ['{#Close#}']">
|
|
|
|
|
<button type="button" $class="btn btn-sm btn-\${this.state?.type} mt-2" data-bs-dismiss="toast" $onclick="this.result=index+1" $text="item"></button>
|
|
|
|
|
</template>
|
2026-05-14 20:04:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
), 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.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());
|
2026-06-05 20:05:26 +08:00
|
|
|
};
|
2026-06-10 12:49:43 +08:00
|
|
|
UI.toastConfirm = function(message, options = {}) {
|
|
|
|
|
return new Promise((resolve) => UI.toast(message, { buttons: ["{#Confirm#}"], ...options }).then((index2) => resolve(index2 === 1)).catch(() => resolve(false)));
|
2026-06-08 21:57:18 +08:00
|
|
|
};
|
2026-06-10 12:49:43 +08:00
|
|
|
global.UI = UI;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component } = global;
|
|
|
|
|
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 }) => {
|
|
|
|
|
method = method.toUpperCase();
|
|
|
|
|
const opt = { method, headers: { "Content-Type": "application/json", ...headers } };
|
|
|
|
|
if (data !== void 0) opt.body = JSON.stringify(data);
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
|
|
|
opt.signal = controller.signal;
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(url, opt);
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
const result = responseType === "blob" ? await response.blob() : responseType === "text" ? await response.text() : await response.json();
|
|
|
|
|
return { ok: response.ok, status: response.status, result };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
return { ok: false, status: 0, error: err.message };
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
2026-05-14 20:04:31 +08:00
|
|
|
}
|
2026-06-10 12:49:43 +08:00
|
|
|
};
|
|
|
|
|
Component.register("API", (container) => {
|
|
|
|
|
if (!container.state.request) container.state.request = { url: "", method: "GET", data: {}, noui: false };
|
|
|
|
|
if (!container.state.response) container.state.response = { ok: false, status: 0, result: null, loading: false };
|
|
|
|
|
container.do = async (req) => {
|
|
|
|
|
var _a;
|
|
|
|
|
const opt = { ...container.state.request, ...req };
|
|
|
|
|
container.state.response.loading = true;
|
|
|
|
|
const resp = await HTTP.request(opt);
|
|
|
|
|
Object.assign(container.state.response, resp, { loading: false });
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
if (!opt.noui && ((_a = global.UI) == null ? void 0 : _a.toast)) global.UI.toast(resp.error || "Request failed", { type: "danger" });
|
|
|
|
|
}
|
|
|
|
|
container.dispatchEvent(new CustomEvent("success", { bubbles: false, detail: resp.result }));
|
|
|
|
|
return resp;
|
|
|
|
|
};
|
|
|
|
|
let _autoTimer = null;
|
|
|
|
|
container.state.request.__watch(null, () => {
|
|
|
|
|
if (!container.hasAttribute("auto") || !container.state.request.url) return;
|
|
|
|
|
if (_autoTimer) return;
|
|
|
|
|
_autoTimer = Promise.resolve().then(() => {
|
|
|
|
|
container.do();
|
|
|
|
|
_autoTimer = null;
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-05 20:05:26 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
global.HTTP = HTTP;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component, NewState, Util, $ } = global;
|
|
|
|
|
const AutoForm = {
|
|
|
|
|
customTypes: [],
|
|
|
|
|
register: (name, typeName) => {
|
|
|
|
|
const type = typeName || name;
|
|
|
|
|
if (!AutoForm.customTypes.includes(type)) AutoForm.customTypes.push(type);
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
|
|
|
|
};
|
2026-06-10 12:49:43 +08:00
|
|
|
Component.register("AutoForm", (container) => {
|
|
|
|
|
if (!container.state.schema) container.state.schema = [];
|
|
|
|
|
const ensureProxy = (v) => v && typeof v === "object" && !v.__isProxy ? NewState(v) : v;
|
|
|
|
|
container.state.__watch("data", (v) => container.data = ensureProxy(v));
|
|
|
|
|
container.data = ensureProxy(container.state.data || {});
|
|
|
|
|
container.addEventListener("submit", async (event) => {
|
|
|
|
|
var _a, _b, _c, _d;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!container.form.reportValidity()) return (_b = (_a = global.UI) == null ? void 0 : _a.toast) == null ? void 0 : _b.call(_a, "{#verify failed#}", { type: "danger" });
|
|
|
|
|
container.state.formState = "submitting";
|
|
|
|
|
const detail = JSON.parse(JSON.stringify(container.data));
|
|
|
|
|
const customEvent = new CustomEvent("submit", { bubbles: false, cancelable: true, detail });
|
|
|
|
|
container.dispatchEvent(customEvent);
|
|
|
|
|
if (customEvent.defaultPrevented) return;
|
|
|
|
|
try {
|
|
|
|
|
if (container.state.action) {
|
|
|
|
|
const resp = await global.HTTP.request({ url: container.state.action, method: "POST", data: detail });
|
|
|
|
|
if (!resp.ok) throw new Error(resp.error);
|
|
|
|
|
}
|
|
|
|
|
container.state.formState = "success";
|
|
|
|
|
if ((_c = global.UI) == null ? void 0 : _c.toast) global.UI.toast("{#submit success#}", { type: "success" });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
container.state.formState = "error";
|
|
|
|
|
if ((_d = global.UI) == null ? void 0 : _d.toast) global.UI.toast(err.message, { type: "danger" });
|
2026-06-08 22:16:57 +08:00
|
|
|
}
|
|
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<form class="auto-form-root" $onsubmit="this.dispatchEvent(new Event('submit'))">
|
|
|
|
|
<div $class="auto-grid-form \${this.state?.grid ? 'row g-3' : ''}">
|
|
|
|
|
<template $each="this.state?.schema" as="field">
|
|
|
|
|
<div $class="\${this.state?.grid ? 'col-md-' + (field.col || 12) : 'mb-3'}">
|
|
|
|
|
<label $if="field.label" class="form-label small fw-bold text-muted mb-1" $text="field.label"></label>
|
|
|
|
|
<div class="field-container">
|
|
|
|
|
<template $if="!field.type || field.type === 'text'">
|
|
|
|
|
<input type="text" class="form-control" $bind="this.data[field.id]" $required="field.required" $placeholder="field.placeholder || ''">
|
|
|
|
|
</template>
|
|
|
|
|
<template $if="field.type === 'textarea'">
|
|
|
|
|
<textarea class="form-control" $bind="this.data[field.id]" $required="field.required" $placeholder="field.placeholder || ''" $rows="field.rows || 3"></textarea>
|
|
|
|
|
</template>
|
|
|
|
|
<template $if="field.type === 'select'">
|
|
|
|
|
<select class="form-select" $bind="this.data[field.id]" $required="field.required">
|
|
|
|
|
<template $each="field.options" as="opt">
|
|
|
|
|
<option $value="typeof opt === 'object' ? opt.value : opt" $text="typeof opt === 'object' ? opt.label : opt"></option>
|
|
|
|
|
</template>
|
|
|
|
|
</select>
|
|
|
|
|
</template>
|
|
|
|
|
<template $if="field.type === 'switch'">
|
|
|
|
|
<div class="form-check form-switch mt-1">
|
|
|
|
|
<input class="form-check-input" type="checkbox" $bind="this.data[field.id]">
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template $each="AutoForm.customTypes" as="type">
|
|
|
|
|
<template $if="field.type === type">
|
|
|
|
|
<div $is="type" $.data="this.data[field.id]" $onchange="this.data[field.id] = event.detail"></div>
|
|
|
|
|
</template>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
<div $if="!this.state?.hideSubmit" class="mt-4 pt-2 border-top d-flex justify-content-end gap-2">
|
|
|
|
|
<button type="button" class="btn btn-light px-4" $if="this.state?.showCancel" $onclick="this.dispatchEvent(new CustomEvent('cancel'))">{#Cancel#}</button>
|
|
|
|
|
<button type="submit" class="btn btn-primary px-4" $disabled="this.state?.formState === 'submitting'">
|
|
|
|
|
<span $if="this.state?.formState === 'submitting'" class="spinner-border spinner-border-sm me-2"></span>
|
|
|
|
|
<span $text="this.state?.submitLabel || '{#Submit#}'"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
2026-05-29 22:45:56 +08:00
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
Component.register("TagsInput", (container) => {
|
|
|
|
|
container._thisObj = container;
|
|
|
|
|
if (!container.state) container.state = NewState({ tags: [] });
|
|
|
|
|
container.addEventListener("bind", (e) => {
|
|
|
|
|
container.state.tags = Array.isArray(e.detail) ? e.detail : [];
|
|
|
|
|
});
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<div class="form-control d-flex flex-wrap gap-1 align-items-center" style="min-height:38px;cursor:text">
|
|
|
|
|
<template $each="this.state.tags">
|
|
|
|
|
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(function(event) {
|
|
|
|
|
if (["Backspace", "Delete"].includes(event.key)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.state.tags.splice(index, 1);
|
|
|
|
|
this.state.tags = this.state.tags;
|
|
|
|
|
this.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: this.state.tags }));
|
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
|
const buttons = $$(this, "button");
|
|
|
|
|
(buttons.length > 0 ? buttons[index > 0 ? index - 1 : 0] : $(this, "input")).focus();
|
|
|
|
|
});
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
2026-06-10 12:49:43 +08:00
|
|
|
})}" $text="item"></button>
|
|
|
|
|
</template>
|
|
|
|
|
<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(function(event) {
|
|
|
|
|
if (event.isComposing) return;
|
|
|
|
|
if (["Enter", ",", " "].includes(event.key)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const v = thisNode.value.trim();
|
|
|
|
|
if (v && !this.state.tags.includes(v)) {
|
|
|
|
|
this.state.tags.push(v);
|
|
|
|
|
this.state.tags = this.state.tags;
|
|
|
|
|
this.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: this.state.tags }));
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
2026-06-10 12:49:43 +08:00
|
|
|
thisNode.value = "";
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
2026-06-10 12:49:43 +08:00
|
|
|
})}">
|
|
|
|
|
</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");
|
|
|
|
|
global.AutoForm = AutoForm;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component, Util } = global;
|
|
|
|
|
const VirtualScroll = (options = {}) => {
|
|
|
|
|
const config = {
|
|
|
|
|
itemHeight: 50,
|
|
|
|
|
buffer: 5,
|
|
|
|
|
...options
|
|
|
|
|
};
|
|
|
|
|
return (container) => {
|
|
|
|
|
if (!container.state.list) container.state.list = [];
|
|
|
|
|
if (!container.state._renderedList) container.state._renderedList = [];
|
|
|
|
|
let _ticking = false;
|
|
|
|
|
const update = () => {
|
|
|
|
|
const list = container.state.list || [];
|
|
|
|
|
const scrollTop = container.scrollTop;
|
|
|
|
|
const containerHeight = container.clientHeight;
|
|
|
|
|
const itemHeight = config.itemHeight;
|
|
|
|
|
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - config.buffer);
|
|
|
|
|
const endIndex = Math.min(list.length, Math.ceil((scrollTop + containerHeight) / itemHeight) + config.buffer);
|
|
|
|
|
const rendered = [];
|
|
|
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
|
|
|
rendered.push({
|
|
|
|
|
...list[i],
|
|
|
|
|
_top: i * itemHeight,
|
|
|
|
|
_index: i
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
container.state._renderedList = rendered;
|
|
|
|
|
const spacerPost = container.querySelector(".dt-spacer-post");
|
|
|
|
|
if (spacerPost) {
|
|
|
|
|
spacerPost.style.height = list.length * itemHeight - endIndex * itemHeight + "px";
|
|
|
|
|
spacerPost.style.display = "block";
|
|
|
|
|
}
|
|
|
|
|
const spacerPrev = container.querySelector(".dt-spacer-prev");
|
|
|
|
|
if (spacerPrev) {
|
|
|
|
|
spacerPrev.style.height = startIndex * itemHeight + "px";
|
|
|
|
|
spacerPrev.style.display = "block";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
container.addEventListener("scroll", () => {
|
|
|
|
|
if (!_ticking) {
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
update();
|
|
|
|
|
_ticking = false;
|
|
|
|
|
});
|
|
|
|
|
_ticking = true;
|
2026-05-20 22:52:44 +08:00
|
|
|
}
|
2026-06-05 20:05:26 +08:00
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
container.state.__watch("list", update);
|
|
|
|
|
window.addEventListener("resize", update);
|
|
|
|
|
Promise.resolve().then(update);
|
|
|
|
|
};
|
2026-06-05 20:05:26 +08:00
|
|
|
};
|
2026-06-10 12:49:43 +08:00
|
|
|
Component.register("FastList", (container) => {
|
|
|
|
|
const itemHeights = /* @__PURE__ */ new Map();
|
|
|
|
|
container.state.renderedList = [];
|
|
|
|
|
Util.newAvg();
|
|
|
|
|
const update = () => {
|
|
|
|
|
const list = container.state.list || [];
|
|
|
|
|
container.state.groups || [];
|
|
|
|
|
const scrollTop = container.scrollTop;
|
|
|
|
|
const viewHeight = container.clientHeight;
|
|
|
|
|
const itemHeight = container.state.itemHeight || 40;
|
|
|
|
|
let currentTop = 0;
|
|
|
|
|
let startIndex = -1;
|
|
|
|
|
let endIndex = list.length;
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const h = itemHeights.get(list[i].id) || itemHeight;
|
|
|
|
|
if (startIndex === -1 && currentTop + h > scrollTop - 200) startIndex = i;
|
|
|
|
|
if (startIndex !== -1 && currentTop > scrollTop + viewHeight + 200) {
|
|
|
|
|
endIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
currentTop += h;
|
2026-06-05 20:05:26 +08:00
|
|
|
}
|
2026-06-10 12:49:43 +08:00
|
|
|
if (startIndex === -1) startIndex = 0;
|
|
|
|
|
container.state.renderedList = list.slice(startIndex, endIndex).map((item, i) => ({ ...item, _index: startIndex + i }));
|
|
|
|
|
const prevH = list.slice(0, startIndex).reduce((s, item) => s + (itemHeights.get(item.id) || itemHeight), 0);
|
|
|
|
|
const postH = list.slice(endIndex).reduce((s, item) => s + (itemHeights.get(item.id) || itemHeight), 0);
|
|
|
|
|
const prev = container.querySelector(".list-spacer-prev");
|
|
|
|
|
const post = container.querySelector(".list-spacer-post");
|
|
|
|
|
if (prev) prev.style.height = prevH + "px";
|
|
|
|
|
if (post) post.style.height = postH + "px";
|
|
|
|
|
};
|
|
|
|
|
container.addEventListener("scroll", update);
|
|
|
|
|
container.state.__watch("list", update);
|
|
|
|
|
Promise.resolve().then(update);
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<div class="fast-list-root overflow-auto h-100">
|
|
|
|
|
<div class="list-spacer-prev"></div>
|
|
|
|
|
<div class="list-container">
|
|
|
|
|
<template $each="this.state.renderedList" key="id">
|
|
|
|
|
<div slot-id="item"></div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="list-spacer-post"></div>
|
2026-05-19 07:18:01 +08:00
|
|
|
</div>
|
2026-05-14 20:04:31 +08:00
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
global.VirtualScroll = VirtualScroll;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component, Util } = global;
|
|
|
|
|
const MouseMover = {
|
|
|
|
|
bind: (handle, target, options = {}) => {
|
|
|
|
|
let isMoving = false;
|
|
|
|
|
let startX, startY, startLeft, startTop;
|
|
|
|
|
const onMouseDown = (e) => {
|
|
|
|
|
if (options.shouldStart && !options.shouldStart(e)) return;
|
|
|
|
|
isMoving = true;
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
startY = e.clientY;
|
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
|
startLeft = rect.left;
|
|
|
|
|
startTop = rect.top;
|
|
|
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
|
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
|
|
|
if (options.onStart) options.onStart(e);
|
|
|
|
|
};
|
|
|
|
|
const onMouseMove = (e) => {
|
|
|
|
|
if (!isMoving) return;
|
|
|
|
|
const dx = e.clientX - startX;
|
|
|
|
|
const dy = e.clientY - startY;
|
|
|
|
|
if (options.axis !== "y") target.style.left = startLeft + dx + "px";
|
|
|
|
|
if (options.axis !== "x") target.style.top = startTop + dy + "px";
|
|
|
|
|
if (options.onMove) options.onStart(e);
|
|
|
|
|
};
|
|
|
|
|
const onMouseUp = (e) => {
|
|
|
|
|
isMoving = false;
|
|
|
|
|
document.removeEventListener("mousemove", onMouseMove);
|
|
|
|
|
document.removeEventListener("mouseup", onMouseUp);
|
|
|
|
|
if (options.onEnd) options.onEnd(e);
|
|
|
|
|
};
|
|
|
|
|
handle.addEventListener("mousedown", onMouseDown);
|
|
|
|
|
return () => handle.removeEventListener("mousedown", onMouseDown);
|
|
|
|
|
}
|
2026-06-05 20:05:26 +08:00
|
|
|
};
|
2026-06-10 12:49:43 +08:00
|
|
|
Component.register("Resizer", (container) => {
|
|
|
|
|
container.style.cursor = container.hasAttribute("vertical") ? "col-resize" : "row-resize";
|
|
|
|
|
container.addEventListener("mousedown", (e) => {
|
|
|
|
|
const target = container.parentElement;
|
|
|
|
|
if (!target) return;
|
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
|
const startX = e.clientX;
|
|
|
|
|
const startY = e.clientY;
|
|
|
|
|
const startW = rect.width;
|
|
|
|
|
const startH = rect.height;
|
|
|
|
|
const onMouseMove = (e2) => {
|
|
|
|
|
const dw = e2.clientX - startX;
|
|
|
|
|
const dh = e2.clientY - startY;
|
|
|
|
|
if (container.hasAttribute("vertical")) target.style.width = startW + dw + "px";
|
|
|
|
|
else target.style.height = startH + dh + "px";
|
|
|
|
|
container.dispatchEvent(new CustomEvent("resize", { bubbles: false, detail: { width: target.offsetWidth, height: target.offsetHeight } }));
|
|
|
|
|
};
|
|
|
|
|
const onMouseUp = () => {
|
|
|
|
|
document.removeEventListener("mousemove", onMouseMove);
|
|
|
|
|
document.removeEventListener("mouseup", onMouseUp);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("mousemove", onMouseMove);
|
|
|
|
|
document.addEventListener("mouseup", onMouseUp);
|
|
|
|
|
});
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`<div style="position:absolute;right:0;bottom:0;width:10px;height:10px;z-index:100"></div>`
|
|
|
|
|
));
|
|
|
|
|
global.MouseMover = MouseMover;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component, Util } = global;
|
|
|
|
|
const BOOTSTRAP_ICONS = ["alarm", "archive", "arrow-left", "arrow-right", "bag", "bank", "basket", "bell", "bookmark", "box", "briefcase", "calendar", "camera", "cart", "chat", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "clock", "cloud", "code", "collection", "command", "cpu", "credit-card", "cup", "dash", "database", "display", "door-closed", "download", "droplet", "earbuds", "edit", "egg", "eject", "envelope", "eraser", "eye", "file", "filter", "flag", "folder", "gear", "gem", "gift", "graph-up", "grid", "hammer", "hand-thumbs-up", "heart", "house", "image", "inbox", "info-circle", "journal", "key", "laptop", "layers", "layout-text-sidebar-reverse", "lightbulb", "link", "list", "lock", "map", "mic", "moon", "mouse", "music-note", "newspaper", "palette", "paperclip", "pause", "pencil", "person", "phone", "pie-chart", "play", "plus", "printer", "puzzle", "question-circle", "reception-4", "record", "reply", "rss", "save", "search", "send", "server", "share", "shield", "shop", "shuffle", "skip-end", "skip-start", "slash", "sliders", "smartphone", "speaker", "speedometer", "spellcheck", "square", "star", "stickies", "stop", "stopwatch", "suit-heart", "sun", "table", "tag", "tags", "telephone", "terminal", "text-paragraph", "thermometer", "three-dots", "ticket", "tools", "trash", "trophy", "truck", "tv", "umbrella", "unlock", "upload", "vector-pen", "wallet", "watch", "wifi", "window", "wrench", "x", "zoom-in", "zoom-out", "activity", "at", "award", "backspace", "badge-3d", "badge-4k", "badge-8k", "badge-ad", "badge-ar", "badge-cc", "badge-hd", "badge-tm", "badge-vo", "badge-vr", "badge-wc", "bar-chart", "battery", "bicycle", "binoculars", "blockquote-left", "blockquote-right", "book", "bookshelf", "bootstrap", "border-all", "border-bottom", "border-center", "border-inner", "border-left", "border-middle", "border-outer", "border-right", "border-style", "border-top", "border-width", "bounding-box", "box-arrow-down", "box-arrow-in-down", "box-arrow-in-left", "box-arrow-in-right", "box-arrow-in-up", "box-arrow-left", "box-arrow-right", "box-arrow-up", "box-seam", "brightness-alt-high", "brightness-alt-low", "brightness-high", "brightness-low", "broadcast", "brush", "bucket", "bug", "building", "bullseye", "calculator", "calendar-check", "calendar-date", "calendar-day", "calendar-event", "calendar-minus", "calendar-month", "calendar-plus", "calendar-range", "calendar-week", "calendar-x", "calendar2", "calendar3", "calendar4", "camera-reels", "camera-video", "capslock", "card-checklist", "card-heading", "card-image", "card-list", "card-text", "caret-down", "caret-left", "caret-right", "caret-up", "cart-check", "cart-dash", "cart-plus", "cart-x", "cash", "cash-stack", "cast", "chat-dots", "chat-left", "chat-quote", "chat-right", "chat-square", "chat-text", "check-all", "check-circle", "check-square", "circle", "clipboard", "cloud-arrow-down", "cloud-arrow-up", "cloud-check", "cloud-download", "cloud-fog", "cloud-hail", "cloud-lightning", "cloud-minus", "cloud-moon", "cloud-plus", "cloud-rain", "cloud-slash", "cloud-snow", "cloud-sun", "cloud-upload", "clouds", "cloudy", "code-slash", "code-square", "collection-play", "columns", "columns-gap", "compass", "cone", "cone-striped", "controller", "credit-card-2-back", "credit-card-2-front", "crop", "cup-straw", "cursor", "dash-circle", "dash-square", "diagram-2", "diagram-3", "diamond", "dice-1", "dice-2", "dice-3", "dice-4", "dice-5", "dice-6", "disc", "discord", "distribute-horizontal", "distribute-vertical", "door-open", "dot", "droplet-half", "easel", "egg-fried", "emoji-angry", "emoji-dizzy", "emoji-expressionless", "emoji-frown", "emoji-heart-eyes", "emoji-laughing", "emoji-neutral", "emoji-smile", "emoji-sunglasses", "emoji-wink", "envelope-open", "exclamation", "exclamation-circle", "exclamation-diamond", "exclamation-octagon", "exclamation-square", "exclamation-triangle", "eye-slash", "eyedropper", "facebook", "file-arrow-down", "file-arrow-up", "file-binary", "file-break", "file-check", "file-code", "file-diff", "file-earmark", "file-excel", "
|
|
|
|
|
Component.register("Icon", (container) => {
|
|
|
|
|
container.state.name = container.getAttribute("name");
|
|
|
|
|
}, Util.makeDom(`<i $class="bi bi-\${this.state.name}"></i>`));
|
|
|
|
|
Component.register("IconSelector", (container) => {
|
|
|
|
|
container.state.icons = BOOTSTRAP_ICONS;
|
|
|
|
|
container.state.search = "";
|
|
|
|
|
container.state.selected = "";
|
|
|
|
|
container.state.__watch("search", (v) => {
|
|
|
|
|
const s = v.toLowerCase();
|
|
|
|
|
container.state.icons = BOOTSTRAP_ICONS.filter((i) => i.includes(s));
|
|
|
|
|
});
|
|
|
|
|
container.select = (icon) => {
|
|
|
|
|
container.state.selected = icon;
|
|
|
|
|
container.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: icon }));
|
|
|
|
|
};
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<div class="icon-selector-root">
|
|
|
|
|
<input type="text" class="form-control mb-2" $bind="this.state.search" placeholder="Search icons...">
|
|
|
|
|
<div class="icon-grid d-flex flex-wrap gap-2 overflow-auto" style="max-height: 200px;">
|
|
|
|
|
<template $each="this.state.icons" as="icon">
|
|
|
|
|
<div $class="icon-item p-2 border rounded \${this.state.selected === icon ? 'bg-primary text-white' : ''}"
|
|
|
|
|
style="cursor:pointer; width:40px; height:40px"
|
|
|
|
|
$onclick="this.select(icon)">
|
|
|
|
|
<i $class="bi bi-\${icon}"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
2026-05-14 20:04:31 +08:00
|
|
|
</div>
|
|
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
global.BOOTSTRAP_ICONS = BOOTSTRAP_ICONS;
|
|
|
|
|
})(globalThis);
|
|
|
|
|
(function(global) {
|
|
|
|
|
const { Component, Hash, Util } = global;
|
|
|
|
|
Component.register("Nav", (container) => {
|
|
|
|
|
container.state.items = [];
|
|
|
|
|
container.state.activeId = Hash.nav;
|
|
|
|
|
container.state.__watch("activeId", (v) => Hash.nav = v);
|
|
|
|
|
window.addEventListener("hashchange", () => container.state.activeId = Hash.nav);
|
|
|
|
|
container.select = (id) => {
|
|
|
|
|
container.state.activeId = id;
|
|
|
|
|
container.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: id }));
|
|
|
|
|
};
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<ul class="nav nav-pills flex-column">
|
|
|
|
|
<template $each="this.state.items" as="item">
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a $class="nav-link \${this.state.activeId === item.id ? 'active' : ''}"
|
|
|
|
|
href="#" $onclick="this.select(item.id)">
|
|
|
|
|
<i $if="item.icon" $class="bi bi-\${item.icon} me-2"></i>
|
|
|
|
|
<span $text="item.label"></span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-06-08 22:16:57 +08:00
|
|
|
</template>
|
2026-06-10 12:49:43 +08:00
|
|
|
</ul>
|
2026-06-08 22:16:57 +08:00
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
Component.register("Breadcrumb", (container) => {
|
|
|
|
|
container.state.items = [];
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<nav aria-label="breadcrumb">
|
|
|
|
|
<ol class="breadcrumb mb-0">
|
|
|
|
|
<template $each="this.state.items" as="item">
|
|
|
|
|
<li $class="breadcrumb-item \${index === (this.state.items || []).length - 1 ? 'active' : ''}">
|
|
|
|
|
<template $if="index < (this.state.items || []).length - 1">
|
|
|
|
|
<a href="#" $onclick="this.dispatchEvent(new CustomEvent('select', {detail: item}))" $text="item.label"></a>
|
|
|
|
|
</template>
|
|
|
|
|
<template $if="index === (this.state.items || []).length - 1">
|
|
|
|
|
<span $text="item.label"></span>
|
|
|
|
|
</template>
|
|
|
|
|
</li>
|
|
|
|
|
</template>
|
|
|
|
|
</ol>
|
|
|
|
|
</nav>
|
2026-05-14 20:04:31 +08:00
|
|
|
`
|
2026-06-10 12:49:43 +08:00
|
|
|
));
|
|
|
|
|
})(globalThis);
|
|
|
|
|
if (typeof document !== "undefined") {
|
|
|
|
|
window.addEventListener("beforeunload", (event) => {
|
|
|
|
|
var _a;
|
|
|
|
|
if (((_a = globalThis.State) == null ? void 0 : _a.exitBlocks) > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.returnValue = "";
|
2026-06-08 22:16:57 +08:00
|
|
|
}
|
|
|
|
|
});
|
2026-06-10 12:49:43 +08:00
|
|
|
}
|
|
|
|
|
})();
|