This commit is contained in:
redphx
2024-12-05 17:10:39 +07:00
parent c836e33f7b
commit 9199351af1
207 changed files with 9833 additions and 6953 deletions

View File

@@ -0,0 +1,270 @@
import type { PrompFont } from "@/enums/prompt-font";
import { KeyHelper, type KeyEventInfo } from "@/modules/mkb/key-helper";
import { BxEvent } from "@/utils/bx-event";
import { CE } from "@/utils/html";
import { t } from "@/utils/translation";
export const enum BxKeyBindingButtonFlag {
KEYBOARD_PRESS = 1,
KEYBOARD_MODIFIER = 2,
MOUSE_CLICK = 4,
MOUSE_WHEEL = 8,
}
export type BxKeyBindingButtonOptions = {
title: string | PrompFont;
isPrompt: boolean;
onChanged: (e: Event) => void;
allowedFlags: BxKeyBindingButtonFlag[];
};
type KeyBindingDialogOptions = {
$elm: BxKeyBindingButton;
};
export class BxKeyBindingButton extends HTMLButtonElement {
title!: string;
isPrompt = false;
allowedFlags!: BxKeyBindingButtonFlag[];
keyInfo: KeyEventInfo | null = null;
// Fake methods
bindKey!: typeof BxKeyBindingButton['bindKey'];
unbindKey!: typeof BxKeyBindingButton['unbindKey'];
static create(options: BxKeyBindingButtonOptions) {
const $btn = CE<BxKeyBindingButton>('button', {
class: 'bx-binding-button bx-focusable',
type: 'button',
});
$btn.title = options.title;
$btn.isPrompt = !!options.isPrompt;
$btn.allowedFlags = options.allowedFlags;
$btn.bindKey = BxKeyBindingButton.bindKey.bind($btn);
$btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn);
$btn.addEventListener('click', BxKeyBindingButton.onClick.bind($btn))
$btn.addEventListener('contextmenu', BxKeyBindingButton.onContextMenu);
$btn.addEventListener('change', options.onChanged);
return $btn;
}
private static onClick(this: BxKeyBindingButton, e: Event) {
KeyBindingDialog.getInstance().show({
$elm: this,
});
}
private static onContextMenu = (e: Event) => {
e.preventDefault();
const $btn = e.target as BxKeyBindingButton;
if (!$btn.disabled) {
$btn.unbindKey.apply($btn);
}
}
private static bindKey(this: BxKeyBindingButton, key: KeyEventInfo | null, force=false) {
if (!key) {
return;
}
if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) {
this.textContent = KeyHelper.codeToKeyName(key);
this.keyInfo = key;
if (!force) {
BxEvent.dispatch(this, 'change');
}
}
}
private static unbindKey(this: BxKeyBindingButton, force=false) {
this.textContent = '';
this.keyInfo = null;
!force && BxEvent.dispatch(this, 'change');
}
private constructor() {
super();
}
}
class KeyBindingDialog {
private static instance: KeyBindingDialog;
public static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog());
$dialog: HTMLElement;
$wait!: HTMLElement;
$title: HTMLElement;
$inputList: HTMLElement;
$overlay: HTMLElement;
$currentElm!: BxKeyBindingButton;
countdownIntervalId!: number | null;
constructor() {
// Create dialog overlay
this.$overlay = CE('div', { class: 'bx-key-binding-dialog-overlay bx-gone' });
// Disable right click
this.$overlay.addEventListener('contextmenu', e => e.preventDefault());
document.documentElement.appendChild(this.$overlay);
this.$dialog = CE('div', { class: `bx-key-binding-dialog bx-gone` },
this.$title = CE('h2', {}),
CE('div', { class: 'bx-key-binding-dialog-content' },
CE('div', {},
this.$wait = CE('p', { class: 'bx-blink-me' }),
this.$inputList = CE('ul', {},
CE('li', { _dataset: { flag: BxKeyBindingButtonFlag.KEYBOARD_PRESS } }, t('keyboard-key')),
CE('li', { _dataset: { flag: BxKeyBindingButtonFlag.KEYBOARD_MODIFIER } }, t('modifiers-note')),
CE('li', { _dataset: { flag: BxKeyBindingButtonFlag.MOUSE_CLICK } }, t('mouse-click')),
CE('li', { _dataset: { flag: BxKeyBindingButtonFlag.MOUSE_WHEEL } }, t('mouse-wheel')),
),
CE('i', {}, t('press-esc-to-cancel')),
),
),
);
// Disable right click
this.$dialog.addEventListener('contextmenu', e => e.preventDefault());
document.documentElement.appendChild(this.$dialog);
}
show(options: KeyBindingDialogOptions) {
this.$currentElm = options.$elm;
this.addEventListeners();
const allowedFlags = this.$currentElm.allowedFlags;
this.$inputList.dataset.flags = '[' + allowedFlags.join('][') + ']';
// Clear focus
document.activeElement && (document.activeElement as HTMLElement).blur();
this.$title.textContent = this.$currentElm.title;
this.$title.classList.toggle('bx-prompt', this.$currentElm.isPrompt);
this.$dialog.classList.remove('bx-gone');
this.$overlay.classList.remove('bx-gone');
// Start counting down
this.startCountdown();
}
private startCountdown() {
this.stopCountdown();
let count = 9;
this.$wait.textContent = `[${count}] ${t('waiting-for-input')}`;
this.countdownIntervalId = window.setInterval(() => {
count -= 1;
if (count === 0) {
this.stopCountdown();
this.hide();
return;
}
this.$wait.textContent = `[${count}] ${t('waiting-for-input')}`;
}, 1000);
}
private stopCountdown() {
this.countdownIntervalId && clearInterval(this.countdownIntervalId);
this.countdownIntervalId = null;
}
private hide = () => {
this.clearEventListeners();
this.$dialog.classList.add('bx-gone');
this.$overlay.classList.add('bx-gone');
}
private addEventListeners() {
const allowedFlags = this.$currentElm.allowedFlags;
if (allowedFlags.includes(BxKeyBindingButtonFlag.KEYBOARD_PRESS)) {
window.addEventListener('keyup', this);
}
if (allowedFlags.includes(BxKeyBindingButtonFlag.MOUSE_CLICK)) {
window.addEventListener('mousedown', this);
}
if (allowedFlags.includes(BxKeyBindingButtonFlag.MOUSE_WHEEL)) {
window.addEventListener('wheel', this);
}
}
private clearEventListeners() {
window.removeEventListener('keyup', this);
window.removeEventListener('mousedown', this);
window.removeEventListener('wheel', this);
}
handleEvent(e: Event) {
const allowedFlags = this.$currentElm.allowedFlags;
let handled = false;
let valid = false;
switch (e.type) {
case 'wheel':
handled = true;
if (allowedFlags.includes(BxKeyBindingButtonFlag.MOUSE_WHEEL)) {
valid = true;
}
break;
case 'mousedown':
handled = true;
if (allowedFlags.includes(BxKeyBindingButtonFlag.MOUSE_CLICK)) {
valid = true;
}
break;
case 'keyup':
handled = true;
if (allowedFlags.includes(BxKeyBindingButtonFlag.KEYBOARD_PRESS)) {
const keyboardEvent = e as KeyboardEvent;
valid = keyboardEvent.code !== 'Escape';
if (valid && allowedFlags.includes(BxKeyBindingButtonFlag.KEYBOARD_MODIFIER)) {
const key = keyboardEvent.key;
valid = key !== 'Control' && key !== 'Shift' && key !== 'Alt';
handled = valid;
}
}
break;
}
if (handled) {
e.preventDefault();
e.stopPropagation();
if (valid) {
this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e));
this.stopCountdown();
} else {
// Restart countDown
this.startCountdown();
}
// Prevent activating the key binding dialog by accident
window.setTimeout(this.hide, 200);
}
}
}

View File

@@ -0,0 +1,297 @@
import type { NumberStepperParams } from "@/types/setting-definition";
import { BxEvent } from "@/utils/bx-event";
import { CE, escapeCssSelector } from "@/utils/html";
import { setNearby } from "@/utils/navigation-utils";
import type { BxHtmlSettingElement } from "@/utils/setting-element";
type ButtonType = 'inc' | 'dec';
export class BxNumberStepper extends HTMLInputElement implements BxHtmlSettingElement {
private intervalId: number | null = null;
private isHolding!: boolean;
private controlValue!: number;
private controlMin!: number;
private controlMax!: number;
private uiMin!: number;
private uiMax!: number;
private steps!: number;
private options!: NumberStepperParams;
private onChange: any;
private $text!: HTMLSpanElement;
private $btnInc!: HTMLButtonElement;
private $btnDec!: HTMLButtonElement;
private $range!: HTMLInputElement | null;
onInput!: typeof BxNumberStepper['onInput'];
onRangeInput!: typeof BxNumberStepper['onRangeInput'];
onClick!: typeof BxNumberStepper['onClick'];
onPointerUp!: typeof BxNumberStepper['onPointerUp'];
onPointerDown!: typeof BxNumberStepper['onPointerDown'];
setValue!: typeof BxNumberStepper['setValue'];
normalizeValue!: typeof BxNumberStepper['normalizeValue'];
static create(key: string, value: number, min: number, max: number, options: NumberStepperParams={}, onChange: any) {
options = options || {};
options.suffix = options.suffix || '';
options.disabled = !!options.disabled;
options.hideSlider = !!options.hideSlider;
let $text: HTMLSpanElement;
let $btnInc: HTMLButtonElement;
let $btnDec: HTMLButtonElement;
let $range: HTMLInputElement | null;
const $wrapper = CE<BxNumberStepper>('div', {
class: 'bx-number-stepper',
id: `bx_setting_${escapeCssSelector(key)}`,
},
CE('div', {},
$btnDec = CE('button', {
_dataset: {
type: 'dec' as ButtonType,
},
type: 'button',
class: options.hideSlider ? 'bx-focusable' : '',
tabindex: options.hideSlider ? 0 : -1,
}, '-') as HTMLButtonElement,
$text = CE('span') as HTMLSpanElement,
$btnInc = CE('button', {
_dataset: {
type: 'inc' as ButtonType,
},
type: 'button',
class: options.hideSlider ? 'bx-focusable' : '',
tabindex: options.hideSlider ? 0 : -1,
}, '+') as HTMLButtonElement,
),
);
const self = $wrapper;
self.$text = $text;
self.$btnInc = $btnInc;
self.$btnDec = $btnDec;
self.onChange = onChange;
self.onInput = BxNumberStepper.onInput.bind(self);
self.onRangeInput = BxNumberStepper.onRangeInput.bind(self);
self.onClick = BxNumberStepper.onClick.bind(self);
self.onPointerUp = BxNumberStepper.onPointerUp.bind(self);
self.onPointerDown = BxNumberStepper.onPointerDown.bind(self);
self.controlMin = min;
self.controlMax = max;
self.isHolding = false;
self.options = options;
self.uiMin = options.reverse ? -max : min;
self.uiMax = options.reverse ? -min : max;
self.steps = Math.max(options.steps || 1, 1);
BxNumberStepper.setValue.call(self, value);
if (options.disabled) {
$btnInc.disabled = true;
$btnInc.classList.add('bx-inactive');
$btnDec.disabled = true;
$btnDec.classList.add('bx-inactive');
(self as any).disabled = true;
return self;
}
$range = CE<HTMLInputElement>('input', {
id: `bx_inp_setting_${key}`,
type: 'range',
min: self.uiMin,
max: self.uiMax,
value: options.reverse ? -value : value,
step: self.steps,
tabindex: 0,
});
self.$range = $range;
options.hideSlider && $range.classList.add('bx-gone');
$range.addEventListener('input', self.onRangeInput);
self.addEventListener('input', self.onInput);
self.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(min / options.exactTicks), 1) * options.exactTicks;
if (start === min) {
start += options.exactTicks;
}
for (let i = start; i < max; i += options.exactTicks) {
$markers.appendChild(CE<HTMLOptionElement>('option', {
value: options.reverse ? -i : i,
}));
}
} else {
for (let i = self.uiMin + options.ticks!; i < self.uiMax; i += options.ticks!) {
$markers.appendChild(CE<HTMLOptionElement>('option', {value: i}));
}
}
self.appendChild($markers);
}
BxNumberStepper.updateButtonsVisibility.call(self);
self.addEventListener('click', self.onClick);
self.addEventListener('pointerdown', self.onPointerDown);
self.addEventListener('contextmenu', BxNumberStepper.onContextMenu);
setNearby(self, {
focus: options.hideSlider ? $btnInc : $range,
});
Object.defineProperty(self, 'value', {
get() { return self.controlValue; },
set(value) { BxNumberStepper.setValue.call(self, value); },
});
return self;
}
private static setValue(this: BxNumberStepper, value: any) {
this.controlValue = BxNumberStepper.normalizeValue.call(this, value);
this.$text.textContent = BxNumberStepper.updateTextValue.call(this);
if (this.$range) {
this.$range.value = this.options.reverse ? -value : value;
}
BxNumberStepper.updateButtonsVisibility.call(this);
}
private static normalizeValue(this: BxNumberStepper, value: number | string): number {
value = parseInt(value as string);
value = Math.max(this.controlMin, value);
value = Math.min(this.controlMax, value);
return value;
}
private static onInput(this: BxNumberStepper, e: Event) {
BxEvent.dispatch(this.$range, 'input');
}
private static onRangeInput(this: BxNumberStepper, e: Event) {
let value = parseInt((e.target as HTMLInputElement).value);
if (this.options.reverse) {
value *= -1;
}
/*
const valueChanged = this.controlValue !== value;
if (!valueChanged) {
return;
}
*/
BxNumberStepper.setValue.call(this, value);
BxNumberStepper.updateButtonsVisibility.call(this);
if (!(e as any).ignoreOnChange && this.onChange) {
this.onChange(e, value);
}
}
private static onClick(this: BxNumberStepper, e: Event) {
e.preventDefault();
if (this.isHolding) {
return;
}
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
$btn && BxNumberStepper.buttonPressed.call(this, e, $btn);
BxNumberStepper.clearIntervalId.call(this);
this.isHolding = false;
}
private static onPointerDown(this: BxNumberStepper, e: Event) {
BxNumberStepper.clearIntervalId.call(this);
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
if (!$btn) {
return;
}
this.isHolding = true;
e.preventDefault();
this.intervalId = window.setInterval((e: Event) => {
BxNumberStepper.buttonPressed.call(this, e, $btn);
}, 200);
window.addEventListener('pointerup', this.onPointerUp, { once: true });
window.addEventListener('pointercancel', this.onPointerUp, { once: true });
}
private static onPointerUp(this: BxNumberStepper, e: Event) {
BxNumberStepper.clearIntervalId.call(this);
this.isHolding = false;
}
private static onContextMenu(e: Event) {
e.preventDefault();
}
private static updateTextValue(this: BxNumberStepper): string | null {
const value = this.controlValue;
let textContent = null;
if (this.options.customTextValue) {
textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);
}
if (textContent === null) {
textContent = value.toString() + this.options.suffix;
}
return textContent;
}
private static buttonPressed(this: BxNumberStepper, e: Event, $btn: HTMLElement) {
let value = this.controlValue;
value = this.options.reverse ? -value : value;
const btnType = $btn.dataset.type as ButtonType;
if (btnType === 'dec') {
value = Math.max(this.uiMin, value - this.steps);
} else {
value = Math.min(this.uiMax, value + this.steps);
}
value = this.options.reverse ? -value : value;
BxNumberStepper.setValue.call(this, value);
BxNumberStepper.updateButtonsVisibility.call(this);
this.onChange && this.onChange(e, value);
}
private static clearIntervalId(this: BxNumberStepper) {
this.intervalId && clearInterval(this.intervalId);
this.intervalId = null;
}
private static updateButtonsVisibility(this: BxNumberStepper) {
this.$btnDec.classList.toggle('bx-inactive', this.controlValue === this.uiMin);
this.$btnInc.classList.toggle('bx-inactive', this.controlValue === this.uiMax);
if (this.controlValue === this.uiMin || this.controlValue === this.uiMax) {
BxNumberStepper.clearIntervalId.call(this);
}
}
}

355
src/web-components/bx-select.ts Normal file → Executable file
View File

@@ -1,13 +1,35 @@
import { PrefKey } from "@/enums/pref-keys";
import type { NavigationElement } from "@/modules/ui/dialog/navigation-dialog";
import { BxEvent } from "@/utils/bx-event";
import type { BxSelectSettingElement } from "@/utils/setting-element";
import { ButtonStyle, CE, createButton } from "@utils/html";
import { setNearby } from "@/utils/navigation-utils";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { ButtonStyle, CE, clearDataSet, createButton } from "@utils/html";
export class BxSelectElement extends HTMLSelectElement {
private optionsList!: HTMLOptionElement[];
private indicatorsList!: HTMLElement[];
private $indicators!: HTMLElement;
private visibleIndex!: number;
private isMultiple!: boolean;
private $select!: HTMLSelectElement;
private $btnNext!: HTMLButtonElement;
private $btnPrev!: HTMLButtonElement;
private $label!: HTMLLabelElement;
private $checkBox!: HTMLInputElement;
static create($select: HTMLSelectElement, forceFriendly=false): BxSelectElement {
// Return normal <select> if not using controller-friendly UI
if (!forceFriendly && !getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
$select.classList.add('bx-select');
// @ts-ignore
return $select;
}
export class BxSelectElement {
static wrap($select: HTMLSelectElement) {
// Remove "tabindex" attribute from <select>
$select.removeAttribute('tabindex');
const $wrapper = CE<BxSelectElement & NavigationElement>('div', { class: 'bx-select' });
const $btnPrev = createButton({
label: '<',
style: ButtonStyle.FOCUSABLE,
@@ -18,124 +40,68 @@ export class BxSelectElement {
style: ButtonStyle.FOCUSABLE,
});
const isMultiple = $select.multiple;
let $checkBox: HTMLInputElement;
let $label: HTMLElement;
let visibleIndex = $select.selectedIndex;
setNearby($wrapper, {
orientation: 'horizontal',
focus: $btnNext,
});
let $content;
if (isMultiple) {
const self = $wrapper;
self.isMultiple = $select.multiple;
self.visibleIndex = $select.selectedIndex;
self.$select = $select;
self.optionsList = Array.from($select.querySelectorAll<HTMLOptionElement>('option'));
self.$indicators = CE('div', { class: 'bx-select-indicators' });
self.indicatorsList = [];
self.$btnNext = $btnNext;
self.$btnPrev = $btnPrev;
if (self.isMultiple) {
$content = CE('button', {
class: 'bx-select-value bx-focusable',
tabindex: 0,
},
$checkBox = CE('input', {type: 'checkbox'}),
$label = CE('span', {}, ''),
CE('div', {},
self.$checkBox = CE('input', { type: 'checkbox' }),
self.$label = CE('span', {}, ''),
),
self.$indicators,
);
$content.addEventListener('click', e => {
$checkBox.click();
self.$checkBox.click();
});
$checkBox.addEventListener('input', e => {
const $option = getOptionAtIndex(visibleIndex);
self.$checkBox.addEventListener('input', e => {
const $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);
$option && ($option.selected = (e.target as HTMLInputElement).checked);
BxEvent.dispatch($select, 'input');
});
} else {
$content = CE('div', {},
$label = CE('label', {for: $select.id + '_checkbox'}, ''),
self.$label = CE('label', { for: $select.id + '_checkbox' }, ''),
self.$indicators,
);
}
const getOptionAtIndex = (index: number): HTMLOptionElement | undefined => {
const options = Array.from($select.querySelectorAll('option'));
return options[index];
}
const boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);
const render = (e?: Event) => {
// console.log('options', this.options, 'selectedIndices', this.selectedIndices, 'selectedOptions', this.selectedOptions);
if (e && (e as any).manualTrigger) {
visibleIndex = $select.selectedIndex;
}
visibleIndex = normalizeIndex(visibleIndex);
const $option = getOptionAtIndex(visibleIndex);
let content = '';
if ($option) {
content = $option.textContent || '';
if (content && $option.parentElement!.tagName === 'OPTGROUP') {
$label.innerHTML = '';
const fragment = document.createDocumentFragment();
fragment.appendChild(CE('span', {}, ($option.parentElement as HTMLOptGroupElement).label));
fragment.appendChild(document.createTextNode(content));
$label.appendChild(fragment);
} else {
$label.textContent = content;
}
} else {
$label.textContent = content;
}
// Add line-through on disabled option
$label.classList.toggle('bx-line-through', $option && $option.disabled);
// Hide checkbox when the selection is empty
if (isMultiple) {
$checkBox.checked = $option?.selected || false;
$checkBox.classList.toggle('bx-gone', !content);
}
const disablePrev = visibleIndex <= 0;
const disableNext = visibleIndex === $select.querySelectorAll('option').length - 1;
$btnPrev.classList.toggle('bx-inactive', disablePrev);
$btnNext.classList.toggle('bx-inactive', disableNext);
// Focus the other button when reaching the beginning/end
disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus();
disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus();
}
const normalizeIndex = (index: number): number => {
return Math.min(Math.max(index, 0), $select.querySelectorAll('option').length - 1);
}
const onPrevNext = (e: Event) => {
if (!e.target) {
return;
}
const goNext = (e.target as any).closest('button') === $btnNext;
const currentIndex = visibleIndex;
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
newIndex = normalizeIndex(newIndex);
visibleIndex = newIndex;
if (!isMultiple && newIndex !== currentIndex) {
$select.selectedIndex = newIndex;
}
if (isMultiple) {
render();
} else {
BxEvent.dispatch($select, 'input');
}
};
$select.addEventListener('input', render);
$btnPrev.addEventListener('click', onPrevNext);
$btnNext.addEventListener('click', onPrevNext);
$select.addEventListener('input', BxSelectElement.render.bind(self));
$btnPrev.addEventListener('click', boundOnPrevNext);
$btnNext.addEventListener('click', boundOnPrevNext);
const observer = new MutationObserver((mutationList, observer) => {
mutationList.forEach(mutation => {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
render();
self.visibleIndex = $select.selectedIndex;
self.optionsList = Array.from($select.querySelectorAll<HTMLOptionElement>('option'));
BxSelectElement.resetIndicators.call(self);
BxSelectElement.render.call(self);
}
});
});
@@ -146,54 +112,201 @@ export class BxSelectElement {
attributes: true,
});
render();
const $div = CE<NavigationElement>('div', {
class: 'bx-select',
_nearby: {
orientation: 'horizontal',
focus: $btnNext,
}
},
self.append(
$select,
$btnPrev,
$content,
$btnNext,
);
Object.defineProperty($div, 'value', {
get() {
return $select.value;
},
BxSelectElement.resetIndicators.call(self);
BxSelectElement.render.call(self);
Object.defineProperty(self, 'value', {
get() { return $select.value; },
set(value) {
($div as any).setValue(value);
}
self.optionsList = Array.from($select.querySelectorAll<HTMLOptionElement>('option'));
$select.value = value;
// Update visible index
self.visibleIndex = $select.selectedIndex;
// Re-render
BxSelectElement.resetIndicators.call(self);
BxSelectElement.render.call(self);
},
});
$div.addEventListener = function() {
Object.defineProperty(self, 'disabled', {
get() { return $select.disabled; },
set(value) { $select.disabled = value; },
});
self.addEventListener = function() {
// @ts-ignore
$select.addEventListener.apply($select, arguments);
};
$div.removeEventListener = function() {
self.removeEventListener = function() {
// @ts-ignore
$select.removeEventListener.apply($select, arguments);
};
$div.dispatchEvent = function() {
self.dispatchEvent = function() {
// @ts-ignore
return $select.dispatchEvent.apply($select, arguments);
};
($div as any).setValue = (value: any) => {
if ('setValue' in $select) {
($select as BxSelectSettingElement).setValue(value);
} else {
$select.value = value;
}
self.appendChild = function(node) {
$select.appendChild(node);
return node;
};
return $div;
return self as BxSelectElement;
}
private static resetIndicators(this: BxSelectElement) {
const {
optionsList,
indicatorsList,
$indicators,
} = this;
const targetSize = optionsList.length;
if (indicatorsList.length > targetSize) {
// Detach indicator from parent
while (indicatorsList.length > targetSize) {
indicatorsList.pop()?.remove();
}
} else if (indicatorsList.length < targetSize) {
// Add empty indicators
while (indicatorsList.length < targetSize) {
const $indicator = CE('span', {});
indicatorsList.push($indicator);
$indicators.appendChild($indicator);
}
}
// Reset dataset
for (const $indicator of indicatorsList) {
clearDataSet($indicator);
}
// Toggle visibility
$indicators.classList.toggle('bx-invisible', targetSize <= 1);
}
private static getOptionAtIndex(this: BxSelectElement, index: number): HTMLOptionElement | undefined {
return this.optionsList[index];
}
private static render(this: BxSelectElement, e?: Event) {
const {
$label,
$btnNext,
$btnPrev,
$checkBox,
visibleIndex,
optionsList,
indicatorsList,
} = this;
// console.log('options', this.options, 'selectedIndices', this.selectedIndices, 'selectedOptions', this.selectedOptions);
if (e && (e as any).manualTrigger) {
this.visibleIndex = this.$select.selectedIndex;
}
this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);
const $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex);
let content = '';
if ($option) {
const $parent = $option.parentElement!;
const hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector('optgroup');
content = $option.textContent || '';
if (content && hasLabel) {
const groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : ' ';
$label.innerHTML = '';
const fragment = document.createDocumentFragment();
fragment.appendChild(CE('span', {}, groupLabel));
fragment.appendChild(document.createTextNode(content));
$label.appendChild(fragment);
} else {
$label.textContent = content;
}
} else {
$label.textContent = content;
}
// Add line-through on disabled option
$label.classList.toggle('bx-line-through', $option && $option.disabled);
// Hide checkbox when the selection is empty
if (this.isMultiple) {
$checkBox.checked = $option?.selected || false;
$checkBox.classList.toggle('bx-gone', !content);
}
// Disable buttons when there is only one option or fewer
const disableButtons = optionsList.length <= 1;
$btnPrev.classList.toggle('bx-inactive', disableButtons);
$btnNext.classList.toggle('bx-inactive', disableButtons);
// Update indicators
for (let i = 0; i < optionsList.length; i++) {
const $option = optionsList[i];
const $indicator = indicatorsList[i];
clearDataSet($indicator);
if ($option.selected) {
$indicator.dataset.selected = 'true';
}
if ($option.index === visibleIndex) {
$indicator.dataset.highlighted = 'true';
}
}
}
private static normalizeIndex(this: BxSelectElement, index: number): number {
return Math.min(Math.max(index, 0), this.optionsList.length - 1);
}
private static onPrevNext(this: BxSelectElement, e: Event) {
if (!e.target) {
return;
}
const {
$btnNext,
$select,
isMultiple,
visibleIndex: currentIndex,
} = this;
const goNext = (e.target as any).closest('button') === $btnNext;
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
if (newIndex > this.optionsList.length - 1) {
newIndex = 0;
} else if (newIndex < 0) {
newIndex = this.optionsList.length - 1;
}
newIndex = BxSelectElement.normalizeIndex.call(this, newIndex);
this.visibleIndex = newIndex;
if (!isMultiple && newIndex !== currentIndex) {
$select.selectedIndex = newIndex;
}
if (isMultiple) {
BxSelectElement.render.call(this);
} else {
BxEvent.dispatch($select, 'input');
}
};
}