mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 06:22:44 -08:00
initial commit
This commit is contained in:
319
vendor/yomitan/js/dom/dom-data-binder.js
vendored
Normal file
319
vendor/yomitan/js/dom/dom-data-binder.js
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2025 Yomitan Authors
|
||||
* Copyright (C) 2020-2022 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {TaskAccumulator} from '../general/task-accumulator.js';
|
||||
import {convertElementValueToNumber} from './document-util.js';
|
||||
import {SelectorObserver} from './selector-observer.js';
|
||||
|
||||
/**
|
||||
* @template [T=unknown]
|
||||
*/
|
||||
export class DOMDataBinder {
|
||||
/**
|
||||
* @param {string[]} selectors
|
||||
* @param {import('dom-data-binder').CreateElementMetadataCallback<T>} createElementMetadata
|
||||
* @param {import('dom-data-binder').CompareElementMetadataCallback<T>} compareElementMetadata
|
||||
* @param {import('dom-data-binder').GetValuesCallback<T>} getValues
|
||||
* @param {import('dom-data-binder').SetValuesCallback<T>} setValues
|
||||
* @param {import('dom-data-binder').OnErrorCallback<T>|null} [onError]
|
||||
*/
|
||||
constructor(selectors, createElementMetadata, compareElementMetadata, getValues, setValues, onError = null) {
|
||||
/** @type {string[]} */
|
||||
this._selectors = selectors;
|
||||
/** @type {import('dom-data-binder').CreateElementMetadataCallback<T>} */
|
||||
this._createElementMetadata = createElementMetadata;
|
||||
/** @type {import('dom-data-binder').CompareElementMetadataCallback<T>} */
|
||||
this._compareElementMetadata = compareElementMetadata;
|
||||
/** @type {import('dom-data-binder').GetValuesCallback<T>} */
|
||||
this._getValues = getValues;
|
||||
/** @type {import('dom-data-binder').SetValuesCallback<T>} */
|
||||
this._setValues = setValues;
|
||||
/** @type {?import('dom-data-binder').OnErrorCallback<T>} */
|
||||
this._onError = onError;
|
||||
/** @type {TaskAccumulator<import('dom-data-binder').ElementObserver<T>, import('dom-data-binder').UpdateTaskValue>} */
|
||||
this._updateTasks = new TaskAccumulator(this._onBulkUpdate.bind(this));
|
||||
/** @type {TaskAccumulator<import('dom-data-binder').ElementObserver<T>, import('dom-data-binder').AssignTaskValue>} */
|
||||
this._assignTasks = new TaskAccumulator(this._onBulkAssign.bind(this));
|
||||
/** @type {SelectorObserver<import('dom-data-binder').ElementObserver<T>>[]} */
|
||||
this._selectorObservers = selectors.map((selector) => new SelectorObserver({
|
||||
selector,
|
||||
ignoreSelector: null,
|
||||
onAdded: this._createObserver.bind(this),
|
||||
onRemoved: this._removeObserver.bind(this),
|
||||
onChildrenUpdated: this._onObserverChildrenUpdated.bind(this),
|
||||
isStale: this._isObserverStale.bind(this),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
*/
|
||||
observe(element) {
|
||||
for (const selectorObserver of this._selectorObservers) {
|
||||
selectorObserver.observe(element, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** */
|
||||
disconnect() {
|
||||
for (const selectorObserver of this._selectorObservers) {
|
||||
selectorObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** */
|
||||
async refresh() {
|
||||
await this._updateTasks.enqueue(null, {all: true});
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
/**
|
||||
* @param {import('dom-data-binder').UpdateTask<T>[]} tasks
|
||||
*/
|
||||
async _onBulkUpdate(tasks) {
|
||||
let all = false;
|
||||
/** @type {import('dom-data-binder').ApplyTarget<T>[]} */
|
||||
const targets = [];
|
||||
for (const [observer, task] of tasks) {
|
||||
if (observer === null) {
|
||||
if (task.data.all) {
|
||||
all = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
targets.push([observer, task]);
|
||||
}
|
||||
}
|
||||
if (all) {
|
||||
targets.length = 0;
|
||||
for (const selectorObserver of this._selectorObservers) {
|
||||
for (const observer of selectorObserver.datas()) {
|
||||
targets.push([observer, null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const args = targets.map(([observer]) => ({
|
||||
element: observer.element,
|
||||
metadata: observer.metadata,
|
||||
}));
|
||||
const responses = await this._getValues(args);
|
||||
this._applyValues(targets, responses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('dom-data-binder').AssignTask<T>[]} tasks
|
||||
*/
|
||||
async _onBulkAssign(tasks) {
|
||||
/** @type {import('dom-data-binder').ApplyTarget<T>[]} */
|
||||
const targets = [];
|
||||
const args = [];
|
||||
for (const [observer, task] of tasks) {
|
||||
if (observer === null) { continue; }
|
||||
args.push({
|
||||
element: observer.element,
|
||||
metadata: observer.metadata,
|
||||
value: task.data.value,
|
||||
});
|
||||
targets.push([observer, task]);
|
||||
}
|
||||
const responses = await this._setValues(args);
|
||||
this._applyValues(targets, responses, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('dom-data-binder').ElementObserver<T>} observer
|
||||
*/
|
||||
_onElementChange(observer) {
|
||||
const value = this._getElementValue(observer.element);
|
||||
observer.value = value;
|
||||
observer.hasValue = true;
|
||||
void this._assignTasks.enqueue(observer, {value});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('dom-data-binder').ApplyTarget<T>[]} targets
|
||||
* @param {import('dom-data-binder').TaskResult[]} response
|
||||
* @param {boolean} ignoreStale
|
||||
*/
|
||||
_applyValues(targets, response, ignoreStale) {
|
||||
for (let i = 0, ii = targets.length; i < ii; ++i) {
|
||||
const [observer, task] = targets[i];
|
||||
const {error, result} = response[i];
|
||||
const stale = (task !== null && task.stale);
|
||||
|
||||
if (error) {
|
||||
if (typeof this._onError === 'function') {
|
||||
this._onError(error, stale, observer.element, observer.metadata);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stale && !ignoreStale) { continue; }
|
||||
|
||||
observer.value = result;
|
||||
observer.hasValue = true;
|
||||
this._setElementValue(observer.element, result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @returns {import('dom-data-binder').ElementObserver<T>|undefined}
|
||||
*/
|
||||
_createObserver(element) {
|
||||
const metadata = this._createElementMetadata(element);
|
||||
if (typeof metadata === 'undefined') { return void 0; }
|
||||
const type = this._getNormalizedElementType(element);
|
||||
const eventType = 'change';
|
||||
/** @type {import('dom-data-binder').ElementObserver<T>} */
|
||||
const observer = {
|
||||
element,
|
||||
type,
|
||||
value: null,
|
||||
hasValue: false,
|
||||
eventType,
|
||||
onChange: null,
|
||||
metadata,
|
||||
};
|
||||
observer.onChange = this._onElementChange.bind(this, observer);
|
||||
element.addEventListener(eventType, observer.onChange, false);
|
||||
|
||||
void this._updateTasks.enqueue(observer, {all: false});
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {import('dom-data-binder').ElementObserver<T>} observer
|
||||
*/
|
||||
_removeObserver(element, observer) {
|
||||
if (observer.onChange === null) { return; }
|
||||
element.removeEventListener(observer.eventType, observer.onChange, false);
|
||||
observer.onChange = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {import('dom-data-binder').ElementObserver<T>} observer
|
||||
*/
|
||||
_onObserverChildrenUpdated(element, observer) {
|
||||
if (observer.hasValue && this._getNormalizedElementType(element) !== 'element') {
|
||||
this._setElementValue(element, observer.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {import('dom-data-binder').ElementObserver<T>} observer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isObserverStale(element, observer) {
|
||||
const {type, metadata} = observer;
|
||||
if (type !== this._getNormalizedElementType(element)) { return false; }
|
||||
const newMetadata = this._createElementMetadata(element);
|
||||
return typeof newMetadata === 'undefined' || !this._compareElementMetadata(metadata, newMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {unknown} value
|
||||
*/
|
||||
_setElementValue(element, value) {
|
||||
switch (this._getNormalizedElementType(element)) {
|
||||
case 'checkbox':
|
||||
/** @type {HTMLInputElement} */ (element).checked = typeof value === 'boolean' && value;
|
||||
break;
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'textarea':
|
||||
case 'select':
|
||||
/** @type {HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement} */ (element).value = typeof value === 'string' ? value : `${value}`;
|
||||
break;
|
||||
case 'element':
|
||||
element.textContent = typeof value === 'string' ? value : `${value}`;
|
||||
break;
|
||||
}
|
||||
|
||||
/** @type {number|string|boolean} */
|
||||
let safeValue;
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
safeValue = value;
|
||||
break;
|
||||
default:
|
||||
safeValue = `${value}`;
|
||||
break;
|
||||
}
|
||||
/** @type {import('dom-data-binder').SettingChangedEvent} */
|
||||
const event = new CustomEvent('settingChanged', {detail: {value: safeValue}});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @returns {boolean|string|number|null}
|
||||
*/
|
||||
_getElementValue(element) {
|
||||
switch (this._getNormalizedElementType(element)) {
|
||||
case 'checkbox':
|
||||
return !!(/** @type {HTMLInputElement} */ (element).checked);
|
||||
case 'text':
|
||||
return `${/** @type {HTMLInputElement} */ (element).value}`;
|
||||
case 'number':
|
||||
return convertElementValueToNumber(/** @type {HTMLInputElement} */ (element).value, /** @type {HTMLInputElement} */ (element));
|
||||
case 'textarea':
|
||||
return /** @type {HTMLTextAreaElement} */ (element).value;
|
||||
case 'select':
|
||||
return /** @type {HTMLSelectElement} */ (element).value;
|
||||
case 'element':
|
||||
return element.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @returns {import('dom-data-binder').NormalizedElementType}
|
||||
*/
|
||||
_getNormalizedElementType(element) {
|
||||
switch (element.nodeName.toUpperCase()) {
|
||||
case 'INPUT':
|
||||
{
|
||||
const {type} = /** @type {HTMLInputElement} */ (element);
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'password':
|
||||
return 'text';
|
||||
case 'number':
|
||||
case 'checkbox':
|
||||
return type;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TEXTAREA':
|
||||
return 'textarea';
|
||||
case 'SELECT':
|
||||
return 'select';
|
||||
}
|
||||
return 'element';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user