UNPKG

@v4fire/client

Version:

V4Fire client core library

492 lines (420 loc) • 11 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-button/README.md]] * @packageDocumentation */ import { derive } from 'core/functools/trait'; //#if runtime has core/data import 'core/data'; //#endif import type bForm from 'form/b-form/b-form'; import iProgress from 'traits/i-progress/i-progress'; import iAccess from 'traits/i-access/i-access'; import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iSize from 'traits/i-size/i-size'; import iOpenToggle, { CloseHelperEvents } from 'traits/i-open-toggle/i-open-toggle'; import type { HintPosition } from 'global/g-hint/interface'; import iData, { component, prop, computed, wait, p, ModsDecl, ModelMethod, ModEvent, RequestFilter } from 'super/i-data/i-data'; import type { ButtonType } from 'form/b-button/interface'; export * from 'super/i-data/i-data'; export * from 'traits/i-open-toggle/i-open-toggle'; export * from 'form/b-button/interface'; interface bButton extends Trait<typeof iAccess>, Trait<typeof iOpenToggle> {} /** * Component to create a button */ @component({ flyweight: true, functional: { dataProvider: undefined, href: undefined } }) @derive(iAccess, iOpenToggle) class bButton extends iData implements iAccess, iOpenToggle, iVisible, iWidth, iSize { override readonly rootTag: string = 'span'; override readonly dataProvider: string = 'Provider'; override readonly defaultRequestFilter: RequestFilter = true; /** @see [[iVisible.prototype.hideIfOffline]] */ @prop(Boolean) readonly hideIfOffline: boolean = false; /** * A button' type to create. There can be values: * * 1. `button` - simple button control; * 2. `submit` - button to send the tied form; * 3. `file` - button to open the file uploading dialog; * 4. `link` - hyperlink to the specified URL (to provide URL, use the `href` prop). * * @example * ``` * < b-button @click = console.log('boom!') * Make boom! * * < b-button :type = 'file' | @onChange = console.log($event) * Upload a file * * < b-button :type = 'link' | :href = 'https://google.com' * Go to Google * * < b-form * < b-input :name = 'name' * < b-button :type = 'submit' * Send * ``` */ @prop(String) readonly type: ButtonType = 'button'; /** * If the `type` prop is passed to `file`, this prop defines which file types are selectable in a file upload control * * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefaccept * @example * ``` * < b-button :type = 'file' | :accept = '.txt' | @onChange = console.log($event) * Upload a file * ``` */ @prop({type: String, required: false}) readonly accept?: string; /** * If the `type` prop is passed to `link`, this prop contains a value for `<a href>`. * Otherwise, the prop includes a base URL for a data provider. * * @example * ``` * < b-button :type = 'link' | :href = 'https://google.com' * Go to Google * * < b-button :href = '/generate/user' * Generate a new user * ``` */ @prop({type: String, required: false}) readonly href?: string; /** * A data provider method to use if `dataProvider` or `href` props are passed * * @example * ``` * < b-button :href = '/generate/user' | :method = 'put' * Generate a new user * * < b-button :dataProvider = 'Cities' | :method = 'peek' * Fetch cities * ``` */ @prop(String) readonly method: ModelMethod = 'get'; /** * A string specifying the `<form>` element with which the component is associated (that is, its form owner). * This string's value, if present, must match the id of a `<form>` element in the same document. * If this attribute isn't specified, the component is associated with the nearest containing form, if any. * * The form prop lets you place a component anywhere in the document but have it included with a form elsewhere * in the document. * * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefform * * @example * ``` * < b-input :name = 'fname' | :form = 'my-form' * * < b-button type = 'submit' | :form = 'my-form' * Submit * * < form id = my-form * ``` */ @prop({type: String, required: false}) readonly form?: string; /** @see [[iAccess.autofocus]] */ @prop({type: Boolean, required: false}) readonly autofocus?: boolean; /** @see [[iAccess.tabIndex]] */ @prop({type: Number, required: false}) readonly tabIndex?: number; /** * Icon to show before the button text * * @example * ``` * < b-button :preIcon = 'dropdown' * Submit * ``` */ @prop({type: String, required: false}) readonly preIcon?: string; /** * Name of the used component to show `preIcon` * * @default `'b-icon'` * @example * ``` * < b-button :preIconComponent = 'b-my-icon' * Submit * ``` */ @prop({type: String, required: false}) readonly preIconComponent?: string; /** * Icon to show after the button text * * @example * ``` * < b-button :icon = 'dropdown' * Submit * ``` */ @prop({type: String, required: false}) readonly icon?: string; /** * Name of the used component to show `icon` * * @default `'b-icon'` * @example * ``` * < b-button :iconComponent = 'b-my-icon' * Submit * ``` */ @prop({type: String, required: false}) readonly iconComponent?: string; /** * A component to show "in-progress" state or * Boolean, if needed to show progress by slot or `b-progress-icon` * * @default `'b-progress-icon'` * @example * ``` * < b-button :progressIcon = 'b-my-progress-icon' * Submit * ``` */ @prop({type: [String, Boolean], required: false}) readonly progressIcon?: string | boolean; /** * Tooltip text to show during hover the cursor * * @example * ``` * < b-button :hint = 'Click on me!!!' * Submit * ``` */ @prop({type: String, required: false}) readonly hint?: string; /** * Tooltip position to show during hover the cursor * * @see [[gHint]] * @example * ``` * < b-button :hint = 'Click on me!!!' | :hintPos = 'bottom-right' * Submit * ``` */ @prop({type: String, required: false}) readonly hintPos?: HintPosition; /** * The way to show dropdown if the `dropdown` slot is provided * @see [[gHint]] * * @example * ``` * < b-button :dropdown = 'bottom-right' * < template #default * Submit * * < template #dropdown * Additional information... * ``` */ @prop(String) readonly dropdown: string = 'bottom'; /** * Initial additional attributes are provided to an "internal" (native) button tag * @see [[bButton.$refs.button]] */ @prop({type: Object, required: false}) readonly attrsProp?: Dictionary; /** * Additional attributes are provided to an "internal" (native) button tag * * @see [[bButton.attrsProp]] * @see [[bButton.$refs.button]] */ get attrs(): Dictionary { const attrs = {...this.attrsProp}; if (this.type === 'link') { attrs.href = this.href; } else { attrs.type = this.type; attrs.form = this.form; } if (this.hasDropdown) { attrs['aria-controls'] = this.dom.getId('dropdown'); attrs['aria-expanded'] = this.mods.opened; } return attrs; } /** @see [[iAccess.isFocused]] */ @computed({dependencies: ['mods.focused']}) get isFocused(): boolean { const {button} = this.$refs; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (button != null) { return document.activeElement === button; } return iAccess.isFocused(this); } /** * List of selected files (works with the `file` type) */ get files(): CanUndef<FileList> { return this.$refs.file?.files ?? undefined; } /** * True if the component has a dropdown area */ get hasDropdown(): boolean { return Boolean( this.vdom.getSlot('dropdown') && ( this.isFunctional || this.opt.ifOnce('opened', this.m.opened !== 'false') > 0 && delete this.watchModsStore.opened ) ); } static override readonly mods: ModsDecl = { ...iAccess.mods, ...iVisible.mods, ...iWidth.mods, ...iSize.mods, opened: [ ...iOpenToggle.mods.opened ?? [], ['false'] ], upper: [ 'true', 'false' ] }; protected override readonly $refs!: { button: HTMLButtonElement; file?: HTMLInputElement; dropdown?: Element; }; /** * If the `type` prop is passed to `file`, resets a file input */ @wait('ready') reset(): CanPromise<void> { const {file} = this.$refs; if (file != null) { file.value = ''; } } /** @see [[iOpenToggle.initCloseHelpers]] */ @p({hook: 'beforeDataCreate', replace: false}) protected initCloseHelpers(events?: CloseHelperEvents): void { iOpenToggle.initCloseHelpers(this, events); } protected override initModEvents(): void { const {localEmitter: $e} = this; super.initModEvents(); iProgress.initModEvents(this); iAccess.initModEvents(this); iOpenToggle.initModEvents(this); iVisible.initModEvents(this); $e.on('block.mod.*.opened.*', (e: ModEvent) => this.waitStatus('ready', () => { const expanded = e.value !== 'false' && e.type !== 'remove'; this.$refs.button.setAttribute('aria-expanded', String(expanded)); })); $e.on('block.mod.*.disabled.*', (e: ModEvent) => this.waitStatus('ready', () => { const { button, file } = this.$refs; const disabled = e.value !== 'false' && e.type !== 'remove'; button.disabled = disabled; if (file != null) { file.disabled = disabled; } })); $e.on('block.mod.*.focused.*', (e: ModEvent) => this.waitStatus('ready', () => { const {button} = this.$refs; if (e.value !== 'false' && e.type !== 'remove') { button.focus(); } else { button.blur(); } })); } /** * Handler: button trigger * * @param e * @emits `click(e: Event)` */ protected async onClick(e: Event): Promise<void> { switch (this.type) { case 'link': break; case 'file': this.$refs.file?.click(); break; default: { const dp = this.dataProvider; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (dp != null && (dp !== 'Provider' || this.href != null)) { let that = this; if (this.href != null) { that = this.base(this.href); } await (<Function>that[this.method])(undefined); // Form attribute fix for MS Edge && IE } else if (this.form != null && this.type === 'submit') { e.preventDefault(); const form = this.dom.getComponent<bForm>(`#${this.form}`); form && await form.submit(); } await this.toggle(); } } this.emit('click', e); } /** * Handler: changing a value of the file input * * @param e * @emits `change(result: InputEvent)` */ protected onFileChange(e: Event): void { this.emit('change', e); } } export default bButton;