@v4fire/client
Version:
V4Fire client core library
303 lines (240 loc) • 5.81 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import type bSelect from 'form/b-select/b-select';
import type { ModEvent, SetModEvent } from 'super/i-input-text';
import { openedSelect } from 'form/b-select/const';
/**
* Handler: value changing of a native component `<select>`
*
* @param component
* @emits `actionChange(value: V)`
*/
export function nativeChange<C extends bSelect>(component: C): void {
const {
unsafe,
unsafe: {
block: $b,
$refs: {input}
}
} = component;
if ($b == null || !unsafe.native) {
return;
}
const
// @ts-ignore (TS 4.6.3)
itemName = $b.getElSelector('item'),
checkedItems = input.querySelectorAll(`${itemName}:checked`);
let
value;
for (let i = 0; i < checkedItems.length; i++) {
const
el = checkedItems[i],
id = el.getAttribute('data-id'),
item = unsafe.indexes[String(id)];
if (item == null) {
continue;
}
if (unsafe.multiple) {
value ??= new Set();
value.add(item.value);
} else {
value = item.value;
break;
}
}
if (!Object.fastCompare(value, unsafe.field.get('valueStore'))) {
unsafe.selectValue(value, true);
unsafe.emit('actionChange', unsafe.value);
}
}
/**
* Handler: changing text of a component helper input
*
* @param component
* @emits `actionChange(value: V)`
*/
export function textChange<C extends bSelect>(component: C): void {
let
{text} = component;
if (component.unsafe.compiledMask != null) {
text = text.replace(new RegExp(RegExp.escape(component.maskPlaceholder), 'g'), '');
}
if (text !== '') {
const
rgxp = new RegExp(`^${RegExp.escape(text)}`, 'i');
for (let i = 0; i < component.items.length; i++) {
const
item = component.items[i];
if (item.label != null && rgxp.test(item.label)) {
if (component.selectValue(item.value, true)) {
component.emit('actionChange', component.value);
}
void component.open();
void component.unsafe.setScrollToMarkedOrSelectedItem();
return;
}
}
}
if (component.value !== undefined) {
component.value = undefined;
component.emit('actionChange', undefined);
}
void component.close();
}
/**
* Handler: typing text into a helper text input to search select options
*
* @param component
* @param e
*
* @emits `actionChange(value: V)`
*/
export function searchInput<C extends bSelect>(component: C, e: InputEvent): void {
const
{unsafe} = component;
const
target = <HTMLInputElement>e.target;
if (unsafe.compiledMask != null) {
return;
}
unsafe.text = target.value;
unsafe.onTextChange();
}
/**
* Handler: click to some item element
*
* @param component
* @param itemEl
* @emits `actionChange(value: V)`
*/
export function itemClick<C extends bSelect>(component: C, itemEl: CanUndef<Element>): void {
void component.close();
if (itemEl == null || component.native) {
return;
}
const
id = itemEl.getAttribute('data-id'),
item = component.unsafe.indexes[String(id)];
if (item == null) {
return;
}
if (component.multiple) {
component.toggleValue(item.value);
} else {
component.text = item.label ?? component.text;
component.selectValue(item.value);
}
component.emit('actionChange', component.value);
}
/**
* Handler: "navigation" over select items via "arrow" buttons
*
* @param component
* @param e
*/
export async function itemsNavigate<C extends bSelect>(component: C, e: KeyboardEvent): Promise<void> {
const
{unsafe} = component;
const validKeys = {
ArrowUp: true,
ArrowDown: true,
Enter: true
};
if (unsafe.native || validKeys[e.key] !== true || unsafe.mods.focused !== 'true') {
if (e.key.length === 1) {
await unsafe.focus();
}
return;
}
e.preventDefault();
const
{block: $b} = unsafe;
if ($b == null) {
return;
}
const getMarkedOrSelectedItem = () =>
// @ts-ignore (TS 4.6.3)
$b.element('item', {marked: true}) ??
$b.element('item', {selected: true});
let
currentItemEl = getMarkedOrSelectedItem();
const markItem = (itemEl: Nullable<Element>) => {
if (currentItemEl != null) {
$b.removeElMod(currentItemEl, 'item', 'marked');
}
if (itemEl == null) {
return false;
}
$b.setElMod(itemEl, 'item', 'marked', true);
void unsafe.setScrollToMarkedOrSelectedItem();
return true;
};
switch (e.key) {
case 'Enter':
unsafe.onItemClick(currentItemEl);
break;
case 'ArrowUp':
if (currentItemEl?.previousElementSibling != null) {
markItem(currentItemEl.previousElementSibling);
} else {
await unsafe.close();
}
break;
case 'ArrowDown': {
if (unsafe.mods.opened !== 'true') {
await unsafe.open();
if (unsafe.value != null) {
return;
}
currentItemEl ??= getMarkedOrSelectedItem();
}
markItem(currentItemEl?.nextElementSibling) || markItem($b.element('item'));
break;
}
default:
// Do nothing
}
}
/**
* @see [[iOpenToggle.onOpenedChange]]
* @param component
* @param e
*/
export function openedChange<C extends bSelect>(component: C, e: ModEvent | SetModEvent): void {
const {
unsafe,
unsafe: {async: $a}
} = component;
if (unsafe.native) {
return;
}
// Status: opened == false or opened == null
if (e.type === 'set' && e.value === 'false' || e.type === 'remove') {
if (openedSelect.link === unsafe) {
openedSelect.link = null;
}
if (unsafe.mods.focused !== 'true') {
$a.off({
group: 'navigation'
});
}
return;
}
$a.off({
group: 'navigation'
});
if (!unsafe.multiple) {
if (openedSelect.link != null) {
openedSelect.link.close().catch(() => undefined);
}
openedSelect.link = unsafe;
}
$a.on(document, 'keydown', unsafe.onItemsNavigate.bind(unsafe), {
group: 'navigation'
});
}