mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 06:55:51 +00:00
2602 dev init
This commit is contained in:
351
SL/module/Module.js
Normal file
351
SL/module/Module.js
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
export class Module {
|
||||||
|
constructor(builder) {
|
||||||
|
if (!builder || typeof builder.build !== 'function') {
|
||||||
|
throw new Error('[Module] Invalid builder.');
|
||||||
|
}
|
||||||
|
this.builder = builder;
|
||||||
|
this.config = builder.build();
|
||||||
|
this.ctx = null;
|
||||||
|
this.el = null;
|
||||||
|
this.viewEl = null;
|
||||||
|
this._bindings = [];
|
||||||
|
this._disposables = [];
|
||||||
|
this._state = { ...(this.config.state || {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(ctx = {}) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
await this._loadView();
|
||||||
|
this._bindAll();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async mount() {
|
||||||
|
if (this._isStrict('mount')) {
|
||||||
|
this._abstract('mount');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._isStrict('dispose')) {
|
||||||
|
this._abstract('dispose');
|
||||||
|
}
|
||||||
|
this._unbindAll();
|
||||||
|
for (const d of this._disposables) {
|
||||||
|
try { d(); } catch (_) { /* noop */ }
|
||||||
|
}
|
||||||
|
this._disposables = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
expose() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return { ...this._state };
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(next) {
|
||||||
|
if (!next || typeof next !== 'object') return;
|
||||||
|
Object.assign(this._state, next);
|
||||||
|
this._applyStateToBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDisposable(fn) {
|
||||||
|
if (typeof fn === 'function') {
|
||||||
|
this._disposables.push(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_abstract(methodName) {
|
||||||
|
throw new Error(`[Module] Method not implemented: ${methodName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isStrict(methodName) {
|
||||||
|
if (!this.config.strict) return false;
|
||||||
|
const required = this.config.requiredMethods || [];
|
||||||
|
return required.includes(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadView() {
|
||||||
|
const viewPath = this.config.view;
|
||||||
|
if (!viewPath) return;
|
||||||
|
|
||||||
|
const rootTarget = this._resolveRoot();
|
||||||
|
if (!rootTarget) {
|
||||||
|
throw new Error('[Module] Root element not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this._resolveViewUrl(viewPath);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`[Module] Failed to load view: ${viewPath}`);
|
||||||
|
}
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = this.config.wrapperClass || 'amily2-module';
|
||||||
|
wrapper.dataset.module = this.config.name || 'Module';
|
||||||
|
wrapper.innerHTML = html;
|
||||||
|
|
||||||
|
rootTarget.appendChild(wrapper);
|
||||||
|
this.el = wrapper;
|
||||||
|
this.viewEl = wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveRoot() {
|
||||||
|
if (this.config.rootSelector) {
|
||||||
|
return document.querySelector(this.config.rootSelector);
|
||||||
|
}
|
||||||
|
if (this.ctx && this.ctx.root instanceof HTMLElement) {
|
||||||
|
return this.ctx.root;
|
||||||
|
}
|
||||||
|
return document.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveViewUrl(viewPath) {
|
||||||
|
if (/^(https?:)?\/\//.test(viewPath) || viewPath.startsWith('/')) {
|
||||||
|
return viewPath;
|
||||||
|
}
|
||||||
|
if (this.ctx && this.ctx.baseUrl) {
|
||||||
|
const baseUrl = this.ctx.baseUrl;
|
||||||
|
const absoluteBase = /^(https?:)?\/\//.test(baseUrl)
|
||||||
|
? baseUrl
|
||||||
|
: `${window.location.origin}/${String(baseUrl).replace(/^\/+/, '')}`;
|
||||||
|
return new URL(viewPath, absoluteBase).toString();
|
||||||
|
}
|
||||||
|
return new URL(viewPath, import.meta.url).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindAll() {
|
||||||
|
this._bindVars();
|
||||||
|
this._bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindVars() {
|
||||||
|
const bindings = this._normalizeBindings(this.config.bindVars);
|
||||||
|
for (const [selector, spec] of Object.entries(bindings)) {
|
||||||
|
const el = this._query(selector);
|
||||||
|
if (!el) continue;
|
||||||
|
|
||||||
|
const normalized = this._normalizeVarSpec(spec);
|
||||||
|
const { key, attr, event, parser, formatter } = normalized;
|
||||||
|
|
||||||
|
const applyValue = () => {
|
||||||
|
const value = formatter ? formatter(this._state[key]) : this._state[key];
|
||||||
|
if (attr === 'checked') {
|
||||||
|
el.checked = !!value;
|
||||||
|
} else if (attr in el) {
|
||||||
|
el[attr] = value ?? '';
|
||||||
|
} else {
|
||||||
|
el.setAttribute(attr, value ?? '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInput = (e) => {
|
||||||
|
let value;
|
||||||
|
if (attr === 'checked') {
|
||||||
|
value = e.target.checked;
|
||||||
|
} else if (attr in e.target) {
|
||||||
|
value = e.target[attr];
|
||||||
|
} else {
|
||||||
|
value = e.target.getAttribute(attr);
|
||||||
|
}
|
||||||
|
this._state[key] = parser ? parser(value) : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
applyValue();
|
||||||
|
el.addEventListener(event, onInput);
|
||||||
|
this._bindings.push(() => el.removeEventListener(event, onInput));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindEvents() {
|
||||||
|
const bindings = this._normalizeBindings(this.config.bindEvents);
|
||||||
|
for (const [selector, events] of Object.entries(bindings)) {
|
||||||
|
const el = this._query(selector);
|
||||||
|
if (!el) continue;
|
||||||
|
|
||||||
|
for (const [eventName, handler] of Object.entries(events)) {
|
||||||
|
const fn = typeof handler === 'function'
|
||||||
|
? handler.bind(this)
|
||||||
|
: (this[handler] ? this[handler].bind(this) : null);
|
||||||
|
if (!fn) continue;
|
||||||
|
el.addEventListener(eventName, fn);
|
||||||
|
this._bindings.push(() => el.removeEventListener(eventName, fn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyStateToBindings() {
|
||||||
|
const bindings = this._normalizeBindings(this.config.bindVars);
|
||||||
|
for (const [selector, spec] of Object.entries(bindings)) {
|
||||||
|
const el = this._query(selector);
|
||||||
|
if (!el) continue;
|
||||||
|
const normalized = this._normalizeVarSpec(spec);
|
||||||
|
const { key, attr, formatter } = normalized;
|
||||||
|
const value = formatter ? formatter(this._state[key]) : this._state[key];
|
||||||
|
if (attr === 'checked') {
|
||||||
|
el.checked = !!value;
|
||||||
|
} else if (attr in el) {
|
||||||
|
el[attr] = value ?? '';
|
||||||
|
} else {
|
||||||
|
el.setAttribute(attr, value ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_normalizeVarSpec(spec) {
|
||||||
|
if (typeof spec === 'string') {
|
||||||
|
return {
|
||||||
|
key: spec,
|
||||||
|
attr: 'value',
|
||||||
|
event: 'input',
|
||||||
|
parser: null,
|
||||||
|
formatter: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const attr = spec.attr || (spec.type === 'checkbox' ? 'checked' : 'value');
|
||||||
|
const event = spec.event || (attr === 'checked' ? 'change' : 'input');
|
||||||
|
return {
|
||||||
|
key: spec.key,
|
||||||
|
attr,
|
||||||
|
event,
|
||||||
|
parser: spec.parser || null,
|
||||||
|
formatter: spec.formatter || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_normalizeBindings(bindings) {
|
||||||
|
if (!bindings) return {};
|
||||||
|
if (Array.isArray(bindings)) {
|
||||||
|
const out = {};
|
||||||
|
for (const pair of bindings) {
|
||||||
|
if (pair && typeof pair.selector === 'string') {
|
||||||
|
out[pair.selector] = pair.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (bindings && typeof bindings === 'object') {
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_query(selector) {
|
||||||
|
if (!selector) return null;
|
||||||
|
if (this.viewEl) {
|
||||||
|
return this.viewEl.querySelector(selector);
|
||||||
|
}
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
_unbindAll() {
|
||||||
|
for (const unbind of this._bindings) {
|
||||||
|
try { unbind(); } catch (_) { /* noop */ }
|
||||||
|
}
|
||||||
|
this._bindings = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModuleBuilder {
|
||||||
|
constructor() {
|
||||||
|
this._config = {
|
||||||
|
name: '',
|
||||||
|
view: '',
|
||||||
|
rootSelector: '',
|
||||||
|
wrapperClass: '',
|
||||||
|
strict: false,
|
||||||
|
requiredMethods: [],
|
||||||
|
bindVars: {},
|
||||||
|
bindEvents: {},
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
name(value) {
|
||||||
|
this._config.name = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
view(path) {
|
||||||
|
this._config.view = path;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
root(selector) {
|
||||||
|
this._config.rootSelector = selector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapperClass(name) {
|
||||||
|
this._config.wrapperClass = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
strict(flag = true) {
|
||||||
|
this._config.strict = !!flag;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
required(methods = []) {
|
||||||
|
this._config.requiredMethods = Array.isArray(methods) ? methods : [];
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
state(initialState = {}) {
|
||||||
|
this._config.state = { ...initialState };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindVar(map = {}) {
|
||||||
|
this._config.bindVars = this._mergeBindings(this._config.bindVars, map);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvent(map = {}) {
|
||||||
|
this._config.bindEvents = this._mergeBindings(this._config.bindEvents, map);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
if (!this._config.name) {
|
||||||
|
this._config.name = 'Module';
|
||||||
|
}
|
||||||
|
return { ...this._config };
|
||||||
|
}
|
||||||
|
|
||||||
|
_mergeBindings(current, next) {
|
||||||
|
const base = Array.isArray(current) ? this._pairsToObject(current) : { ...(current || {}) };
|
||||||
|
if (Array.isArray(next)) {
|
||||||
|
return { ...base, ...this._pairsToObject(next) };
|
||||||
|
}
|
||||||
|
if (next && typeof next === 'object') {
|
||||||
|
return { ...base, ...next };
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pairsToObject(pairs) {
|
||||||
|
const out = {};
|
||||||
|
for (const pair of pairs) {
|
||||||
|
if (pair && typeof pair.selector === 'string') {
|
||||||
|
out[pair.selector] = pair.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModuleBuilder;
|
||||||
|
|
||||||
|
export class BindingPair {
|
||||||
|
constructor(selector, value) {
|
||||||
|
if (!selector || typeof selector !== 'string') {
|
||||||
|
throw new Error('[BindingPair] selector must be a string.');
|
||||||
|
}
|
||||||
|
this.selector = selector;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
SL/module/TableModule.js
Normal file
22
SL/module/TableModule.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Module, ModuleBuilder } from './Module.js';
|
||||||
|
import { bindTableEvents } from '../../ui/table-bindings.js';
|
||||||
|
|
||||||
|
const builder = new ModuleBuilder()
|
||||||
|
.name('TableModule')
|
||||||
|
.view('assets/amily-data-table/Memorisation-forms.html')
|
||||||
|
.strict(true)
|
||||||
|
.required(['mount']);
|
||||||
|
|
||||||
|
export default class TableModule extends Module {
|
||||||
|
constructor() {
|
||||||
|
super(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mount() {
|
||||||
|
if (this.el) {
|
||||||
|
this.el.id = 'amily2_memorisation_forms_panel';
|
||||||
|
this.el.style.display = 'none';
|
||||||
|
}
|
||||||
|
bindTableEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user