mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 05:25: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