UNPKG

@v4fire/client

Version:

V4Fire client core library

393 lines (317 loc) • 8.32 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ /** * [[include:form/b-checkbox/README.md]] * @packageDocumentation */ //#if demo import 'models/demo/checkbox'; //#endif import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; import iSize from 'traits/i-size/i-size'; import iInput, { component, prop, system, computed, ModsDecl, ModEvent, ValidatorsDecl, ValidatorParams, ValidatorResult, ComponentElement } from 'super/i-input/i-input'; import type { CheckType, Value, FormValue } from 'form/b-checkbox/interface'; export * from 'super/i-input/i-input'; export * from 'form/b-checkbox/interface'; export { Value, FormValue }; export const $$ = symbolGenerator(); /** * Component to create a checkbox */ @component({ flyweight: true, functional: { dataProvider: undefined } }) export default class bCheckbox extends iInput implements iSize { override readonly Value!: Value; override readonly FormValue!: FormValue; override readonly rootTag: string = 'span'; /** * If true, the component is checked by default. * Also, it will be checked after resetting. */ @prop(Boolean) override readonly defaultProp: boolean = false; /** * An identifier of the "parent" checkbox. * Use this prop to organize a hierarchy of checkboxes. Checkboxes of the same level must have the same `name`. * * ``` * - [-] * - [X] * - [ ] * - [X] * - [X] * - [X] * ``` * * When you click a parent checkbox, all children will be checked or unchecked. * When you click a child, the parent checkbox will be * * checked as `'indeterminate'` - if not all checkboxes with the same `name` are checked; * * unchecked - if all checkboxes with the same `name` are checked. * * @example * ``` * < b-checkbox :id = 'parent' * * < b-checkbox & * :id = 'foo' | * :name = 'lvl2' | * :parentId = 'parent' * . * * < b-checkbox & * :id = 'foo2' | * :parentId = 'parent' | * :name = 'lvl2' * . * * < b-checkbox & * :parentId = 'foo' | * :name = 'lvl3-foo' * . * * < b-checkbox & * :parentId = 'foo2' | * :name = 'lvl3-foo2' * . * ``` */ @prop({type: String, required: false}) readonly parentId?: string; /** * A checkbox' label text. Basically, it outputs somewhere in the component layout. */ @prop({type: String, required: false}) readonly label?: string; /** * If true, the checkbox can be unchecked directly after the first check */ @prop(Boolean) readonly changeable: boolean = true; override get value(): this['Value'] { const {checked} = this.mods; if (checked === 'true' || checked === undefined) { const v = super['valueGetter'].call(this); if (checked === undefined) { return v === true || undefined; } return v == null ? true : v; } return undefined; } override set value(value: this['Value']) { super['valueSetter'](value); } override get default(): boolean { return this.defaultProp; } /** * True if the checkbox is checked */ @computed({dependencies: ['mods.checked']}) get isChecked(): boolean { return this.mods.checked === 'true'; } static override readonly mods: ModsDecl = { ...iSize.mods, checked: [ 'true', 'false', 'indeterminate' ] }; static override validators: ValidatorsDecl = { //#if runtime has iInput/validators ...iInput.validators, async required({msg, showMsg = true}: ValidatorParams): Promise<ValidatorResult<boolean>> { const value = await this.groupFormValue; if (value.length === 0) { this.setValidationMsg(this.getValidatorMsg(false, msg, this.t`Required field`), showMsg); return false; } return true; } //#endif }; @system() protected override valueStore!: this['Value']; protected override readonly $refs!: {input: HTMLInputElement}; /** * Checks the checkbox */ check(value?: CheckType): Promise<boolean> { return SyncPromise.resolve(this.setMod('checked', value ?? true)); } /** * Unchecks the checkbox */ uncheck(): Promise<boolean> { return SyncPromise.resolve(this.setMod('checked', false)); } /** * Toggles the checkbox. * The method returns a new value. */ toggle(): Promise<this['Value']> { return (this.mods.checked === 'true' ? this.uncheck() : this.check()).then(() => this.value); } override clear(): Promise<boolean> { const res = super.clear(); void this.uncheck(); return res; } override reset(): Promise<boolean> { const onReset = (res: boolean) => { if (res) { void this.removeMod('valid'); this.emit('reset', this.value); return true; } return false; }; if (this.default) { return this.check().then(onReset); } return this.uncheck().then(onReset); } protected override initBaseAPI(): void { super.initBaseAPI(); const i = this.instance; this.convertValueToChecked = i.convertValueToChecked.bind(this); this.onCheckedChange = i.onCheckedChange.bind(this); } protected override initModEvents(): void { super.initModEvents(); this.sync.mod('checked', 'value', this.convertValueToChecked.bind(this)); this.localEmitter.on('block.mod.*.checked.*', this.onCheckedChange.bind(this)); } protected override initValueListeners(): void { this.on('actionChange', () => this.validate()); let oldVal = this.value; this.localEmitter.on('block.mod.*.checked.*', (e: ModEvent) => { if (e.type === 'remove' && e.reason !== 'removeMod') { return; } this.onValueChange(e.value === 'false' || e.type === 'remove' ? undefined : this.value, oldVal); oldVal = this.value; }); } /** * Returns a modifier value by the component value * @param value */ protected convertValueToChecked(value: Value): boolean | string { const {checked} = this.mods; if (checked === undefined) { return value === true; } return checked; } protected override resolveValue(value?: this['Value']): this['Value'] { const i = this.instance; const canApplyDefault = value === undefined && this.mods.checked === undefined && this.lfc.isBeforeCreate() && Boolean(i['defaultGetter'].call(this)); if (canApplyDefault) { void this.check(); } return value; } /** * Handler: checkbox trigger * * @param e * @emits `actionChange(value: this['Value'])` */ // eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental protected onClick(e: Event): void { void this.focus(); if (this.value === undefined || this.value === false || this.changeable) { void this.toggle(); this.emit('actionChange', this.value); } } /** * Handler: checkbox change * * @param e * @emits `check(type:` [[CheckType]]`)` * @emits `uncheck()` */ protected onCheckedChange(e: ModEvent): void { if (e.type === 'remove' && e.reason !== 'removeMod') { return; } const {input} = this.$refs; const setMod = e.type !== 'remove', checked = setMod && e.value === 'true', unchecked = !setMod || e.value === 'false'; input.checked = checked; input.indeterminate = setMod && e.value === 'indeterminate'; if (unchecked) { this.emit('uncheck'); } else { this.emit('check', Object.parse(e.value)); } if (Object.isTruly(this.id)) { const els = document.querySelectorAll(`.i-block-helper[data-parent-id="${this.id}"]`); for (let i = 0; i < els.length; i++) { const el = (<ComponentElement>els[i]).component; if (this.isComponent(el, bCheckbox)) { if (checked) { void el.check(<CheckType>e.value); } else if (unchecked) { void el.uncheck(); } } } } if (Object.isTruly(this.parentId)) { const parent = (<CanUndef<ComponentElement>>document.getElementById(this.parentId!) ?.closest('.i-block-helper')) ?.component; if (this.isComponent(parent, bCheckbox)) { SyncPromise.resolve(this.groupElements).then((els) => { if (els.every((el) => el.mods.checked == null || el.mods.checked === 'false')) { return parent.uncheck(); } return parent.check(els.every((el) => el.mods.checked === 'true') || 'indeterminate'); }).catch(stderr); } } } }