mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-10 07:07:46 +02:00
6.0
This commit is contained in:
277
src/utils/setting-element.ts
Normal file → Executable file
277
src/utils/setting-element.ts
Normal file → Executable file
@@ -1,15 +1,14 @@
|
||||
import type { PreferenceSetting } from "@/types/preferences";
|
||||
import { CE } from "@utils/html";
|
||||
import { setNearby } from "./navigation-utils";
|
||||
import { CE, escapeCssSelector } from "@utils/html";
|
||||
import type { PrefKey } from "@/enums/pref-keys";
|
||||
import type { BaseSettingsStore } from "./settings-storages/base-settings-storage";
|
||||
import { type MultipleOptionsParams, type NumberStepperParams } from "@/types/setting-definition";
|
||||
import { type BaseSettingDefinition, type MultipleOptionsParams, type NumberStepperParams } from "@/types/setting-definition";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
|
||||
export enum SettingElementType {
|
||||
OPTIONS = 'options',
|
||||
MULTIPLE_OPTIONS = 'multiple-options',
|
||||
NUMBER = 'number',
|
||||
NUMBER_STEPPER = 'number-stepper',
|
||||
CHECKBOX = 'checkbox',
|
||||
}
|
||||
@@ -23,7 +22,7 @@ export interface BxHtmlSettingElement extends HTMLElement, BxBaseSettingElement
|
||||
export interface BxSelectSettingElement extends HTMLSelectElement, BxBaseSettingElement {}
|
||||
|
||||
export class SettingElement {
|
||||
static #renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
|
||||
private static renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
tabindex: 0,
|
||||
@@ -62,16 +61,15 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
|
||||
private static renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
multiple: true,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
if (params && params.size) {
|
||||
$control.setAttribute('size', params.size.toString());
|
||||
}
|
||||
const size = params.size ? params.size : Object.keys(setting.multipleOptions!).length;
|
||||
$control.setAttribute('size', size.toString());
|
||||
|
||||
for (let value in setting.multipleOptions) {
|
||||
const label = setting.multipleOptions[value];
|
||||
@@ -111,29 +109,8 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderNumber(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE<HTMLInputElement>('input', {
|
||||
tabindex: 0,
|
||||
type: 'number',
|
||||
min: setting.min,
|
||||
max: setting.max,
|
||||
});
|
||||
|
||||
$control.value = currentValue;
|
||||
onChange && $control.addEventListener('input', (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
const value = Math.max(setting.min!, Math.min(setting.max!, parseInt(target.value)));
|
||||
target.value = value.toString();
|
||||
|
||||
!(e as any).ignoreOnChange && onChange(e, value);
|
||||
});
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderCheckbox(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {'type': 'checkbox', 'tabindex': 0}) as HTMLInputElement;
|
||||
private static renderCheckbox(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {type: 'checkbox', tabindex: 0}) as HTMLInputElement;
|
||||
$control.checked = currentValue;
|
||||
|
||||
onChange && $control.addEventListener('input', e => {
|
||||
@@ -147,233 +124,25 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderNumberStepper(key: string, setting: PreferenceSetting, value: any, onChange: any, options: NumberStepperParams={}) {
|
||||
options = options || {};
|
||||
options.suffix = options.suffix || '';
|
||||
options.disabled = !!options.disabled;
|
||||
options.hideSlider = !!options.hideSlider;
|
||||
|
||||
let $text: HTMLSpanElement;
|
||||
let $btnDec: HTMLButtonElement;
|
||||
let $btnInc: HTMLButtonElement;
|
||||
let $range: HTMLInputElement | null = null;
|
||||
|
||||
let controlValue = value;
|
||||
|
||||
const MIN = options.reverse ? -setting.max! : setting.min!;
|
||||
const MAX = options.reverse ? -setting.min! : setting.max!;
|
||||
const STEPS = Math.max(setting.steps || 1, 1);
|
||||
|
||||
let intervalId: number | null;
|
||||
let isHolding = false;
|
||||
|
||||
const clearIntervalId = () => {
|
||||
intervalId && clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
|
||||
const renderTextValue = (value: any) => {
|
||||
value = parseInt(value as string);
|
||||
|
||||
let textContent = null;
|
||||
if (options.customTextValue) {
|
||||
textContent = options.customTextValue(value);
|
||||
}
|
||||
|
||||
if (textContent === null) {
|
||||
textContent = value.toString() + options.suffix;
|
||||
}
|
||||
|
||||
return textContent;
|
||||
};
|
||||
|
||||
const updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle('bx-inactive', controlValue === MIN);
|
||||
$btnInc.classList.toggle('bx-inactive', controlValue === MAX);
|
||||
|
||||
if (controlValue === MIN || controlValue === MAX) {
|
||||
clearIntervalId();
|
||||
}
|
||||
}
|
||||
|
||||
const $wrapper = CE<BxHtmlSettingElement>('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
|
||||
$btnDec = CE('button', {
|
||||
'data-type': 'dec',
|
||||
type: 'button',
|
||||
class: options.hideSlider ? 'bx-focusable' : '',
|
||||
tabindex: options.hideSlider ? 0 : -1,
|
||||
}, '-') as HTMLButtonElement,
|
||||
$text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement,
|
||||
$btnInc = CE('button', {
|
||||
'data-type': 'inc',
|
||||
type: 'button',
|
||||
class: options.hideSlider ? 'bx-focusable' : '',
|
||||
tabindex: options.hideSlider ? 0 : -1,
|
||||
}, '+') as HTMLButtonElement,
|
||||
);
|
||||
|
||||
if (options.disabled) {
|
||||
$btnInc.disabled = true;
|
||||
$btnInc.classList.add('bx-inactive');
|
||||
|
||||
$btnDec.disabled = true;
|
||||
$btnDec.classList.add('bx-inactive');
|
||||
|
||||
($wrapper as any).disabled = true;
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
$range = CE<HTMLInputElement>('input', {
|
||||
id: `bx_inp_setting_${key}`,
|
||||
type: 'range',
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
value: options.reverse ? -value : value,
|
||||
step: STEPS,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
options.hideSlider && $range.classList.add('bx-gone');
|
||||
|
||||
$range.addEventListener('input', e => {
|
||||
value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (options.reverse) {
|
||||
value *= -1;
|
||||
}
|
||||
|
||||
const valueChanged = controlValue !== value;
|
||||
if (!valueChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
controlValue = options.reverse ? -value : value;
|
||||
updateButtonsVisibility();
|
||||
$text.textContent = renderTextValue(value);
|
||||
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
});
|
||||
|
||||
$wrapper.addEventListener('input', e => {
|
||||
BxEvent.dispatch($range, 'input');
|
||||
});
|
||||
$wrapper.appendChild($range);
|
||||
|
||||
if (options.ticks || options.exactTicks) {
|
||||
const markersId = `markers-${key}`;
|
||||
const $markers = CE('datalist', {id: markersId});
|
||||
$range.setAttribute('list', markersId);
|
||||
|
||||
if (options.exactTicks) {
|
||||
let start = Math.max(Math.floor(setting.min! / options.exactTicks), 1) * options.exactTicks;
|
||||
|
||||
if (start === setting.min!) {
|
||||
start += options.exactTicks;
|
||||
}
|
||||
|
||||
for (let i = start; i < setting.max!; i += options.exactTicks) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {
|
||||
value: options.reverse ? -i : i,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
for (let i = MIN + options.ticks!; i < MAX; i += options.ticks!) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {value: i}));
|
||||
}
|
||||
}
|
||||
$wrapper.appendChild($markers);
|
||||
}
|
||||
|
||||
updateButtonsVisibility();
|
||||
|
||||
const buttonPressed = (e: Event, $btn: HTMLElement) => {
|
||||
let value = parseInt(controlValue);
|
||||
|
||||
const btnType = $btn.dataset.type;
|
||||
if (btnType === 'dec') {
|
||||
value = Math.max(MIN, value - STEPS);
|
||||
} else {
|
||||
value = Math.min(MAX, value + STEPS);
|
||||
}
|
||||
|
||||
controlValue = value;
|
||||
updateButtonsVisibility();
|
||||
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
onChange && onChange(e, value);
|
||||
};
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (isHolding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
$btn && buttonPressed(e, $btn);
|
||||
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
if (!$btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHolding = true;
|
||||
e.preventDefault();
|
||||
|
||||
intervalId = window.setInterval((e: Event) => {
|
||||
buttonPressed(e, $btn);
|
||||
}, 200);
|
||||
|
||||
window.addEventListener('pointerup', onPointerUp, {once: true});
|
||||
window.addEventListener('pointercancel', onPointerUp, {once: true});
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onContextMenu = (e: Event) => e.preventDefault();
|
||||
|
||||
// Custom method
|
||||
$wrapper.setValue = (value: any) => {
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range.value = options.reverse ? -value : value;
|
||||
};
|
||||
|
||||
$wrapper.addEventListener('click', onClick);
|
||||
$wrapper.addEventListener('pointerdown', onPointerDown);
|
||||
$wrapper.addEventListener('contextmenu', onContextMenu);
|
||||
setNearby($wrapper, {
|
||||
focus: options.hideSlider ? $btnInc : $range,
|
||||
})
|
||||
|
||||
return $wrapper;
|
||||
private static renderNumberStepper(key: string, setting: PreferenceSetting, value: any, onChange: any, options: NumberStepperParams={}) {
|
||||
const $control = BxNumberStepper.create(key, value, setting.min!, setting.max!, options, onChange);
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #METHOD_MAP = {
|
||||
[SettingElementType.OPTIONS]: SettingElement.#renderOptions,
|
||||
[SettingElementType.MULTIPLE_OPTIONS]: SettingElement.#renderMultipleOptions,
|
||||
[SettingElementType.NUMBER]: SettingElement.#renderNumber,
|
||||
[SettingElementType.NUMBER_STEPPER]: SettingElement.#renderNumberStepper,
|
||||
[SettingElementType.CHECKBOX]: SettingElement.#renderCheckbox,
|
||||
private static readonly METHOD_MAP = {
|
||||
[SettingElementType.OPTIONS]: SettingElement.renderOptions,
|
||||
[SettingElementType.MULTIPLE_OPTIONS]: SettingElement.renderMultipleOptions,
|
||||
[SettingElementType.NUMBER_STEPPER]: SettingElement.renderNumberStepper,
|
||||
[SettingElementType.CHECKBOX]: SettingElement.renderCheckbox,
|
||||
};
|
||||
|
||||
static render(type: SettingElementType, key: string, setting: PreferenceSetting, currentValue: any, onChange: any, options: any) {
|
||||
const method = SettingElement.#METHOD_MAP[type];
|
||||
static render(type: SettingElementType, key: string, setting: BaseSettingDefinition, currentValue: any, onChange: any, options: any) {
|
||||
const method = SettingElement.METHOD_MAP[type];
|
||||
// @ts-ignore
|
||||
const $control = method(...Array.from(arguments).slice(1)) as HTMLElement;
|
||||
|
||||
if (type !== SettingElementType.NUMBER_STEPPER) {
|
||||
$control.id = `bx_setting_${key}`;
|
||||
$control.id = `bx_setting_${escapeCssSelector(key)}`;
|
||||
}
|
||||
|
||||
// Add "name" property to "select" elements
|
||||
@@ -389,14 +158,12 @@ export class SettingElement {
|
||||
let currentValue = storage.getSetting(key);
|
||||
|
||||
let type;
|
||||
if ('type' in definition) {
|
||||
type = definition.type;
|
||||
} else if ('options' in definition) {
|
||||
if ('options' in definition) {
|
||||
type = SettingElementType.OPTIONS;
|
||||
} else if ('multipleOptions' in definition) {
|
||||
type = SettingElementType.MULTIPLE_OPTIONS;
|
||||
} else if (typeof definition.default === 'number') {
|
||||
type = SettingElementType.NUMBER;
|
||||
type = SettingElementType.NUMBER_STEPPER;
|
||||
} else {
|
||||
type = SettingElementType.CHECKBOX;
|
||||
}
|
||||
|
Reference in New Issue
Block a user