UNPKG

@ryusei/code

Version:

<div align="center"> <a href="https://code.ryuseijs.com"> <img alt="RyuseiCode" src="https://code.ryuseijs.com/images/svg/logo.svg" width="70"> </a>

289 lines (252 loc) 7.23 kB
import { Attributes, Elements, UIButtonSettings, UIFieldSettings, UIGroupData } from '@ryusei/code'; import { CLASS_ACTIVE, CLASS_BUTTON, CLASS_INPUT } from '../../constants/classes'; import { activeElement, addClass, append, assert, attr, create, hasClass, html, isString, isUndefined, normalizeKey, prevent, query, remove, removeClass, toArray, } from '../../utils'; import { icon } from '../../utils/icon'; import { Component } from '../Component/Component'; /** * The stroke linecap value for the path element. */ export const STROKE_LINECAP = 'round'; /** * The base class for creating UI, such as a toolbar or a dialog. * * @since 0.1.0 */ export class UIComponent<T extends UIGroupData = UIGroupData> extends Component { /** * Holds the wrapper element. */ protected wrapper: HTMLDivElement; /** * Holds the active group ID. */ protected group: string; /** * Stores group elements. */ protected groups: Record<string, T> = {}; /** * Initializes the component. * * @param elements - A collection of essential editor elements. */ mount( elements: Elements ): void { super.mount( elements ); this.create(); this.listen(); } /** * Creates elements. * Override this method in a child class and provide a wrapper element. */ protected create(): void { assert( false ); } /** * Listens to some events. */ protected listen(): void { this.bind( window, 'keydown', this.escape, this ); } /** * Hides the toolbar when the escape key is pressed. * * @param e - A KeyboardEvent object. */ protected escape( e: KeyboardEvent ): void { if ( this.isActive() && normalizeKey( e.key ) === 'Escape' ) { this.hide(); prevent( e ); } } /** * Appends the group element to the wrapper element just before displaying the UI. * Override this method to change the default element to append the group to. * * @param group - A group ID. */ protected append( group: string ): void { append( this.wrapper, this.groups[ group ].elm ); } /** * Sets focus to the first element that has the greatest tab index. * If it is not found, sets focus to the first input or button element if available. * * @param group - A group ID. */ protected autoFocus( group: string ): void { const { elm } = this.groups[ group ]; const target = query<HTMLElement>( elm, '[tabindex]' ) || query( elm, 'input, button' ); if ( target ) { target.focus(); if ( target instanceof HTMLInputElement ) { target.select(); } } } /** * Creates a close button. * The wrapper element must exist and have an ID attribute before calling this method. * * @param attrs - Attributes for the button. * * @return A created button element. */ createCloseButton( attrs: Attributes ): HTMLButtonElement { const button = this.createButtons( { id : 'close', icon : 'close', click: 'hide', }, null, this ).close; attr( button, attrs ); return button; } /** * Creates buttons according to the settings. * * @param settings - A settings object. * @param parent - A parent element to append the button to. * @param component - A component instance. * @param classes - Additional classes for buttons. * * @return An object with created buttons. */ createButtons<T extends Component>( settings: UIButtonSettings<T> | UIButtonSettings<T>[], parent: HTMLElement, component: T, classes?: string | string[] ): Record<string, HTMLButtonElement> { const buttons = {}; toArray( settings ).forEach( settings => { const button = this.createButton( settings, parent, classes ); const { click } = settings; if ( click ) { this.bind( button, 'click', e => { if ( isString( click ) ) { component[ click ](); } else { click( e, this.Editor, settings ); } } ); } buttons[ settings.id ] = button; }, [] ); return buttons; } /** * Creates a button with the provided settings. * * @param settings - A settings object. * @param parent - A parent element to append the button to. * @param classes - Additional classes for buttons. * * @return A created button element. */ protected createButton<T extends Component>( settings: UIButtonSettings<T>, parent: HTMLElement, classes: string | string[] ): HTMLButtonElement { const { i18n } = this.options; const { checkbox, tabindex, icon: iconName } = settings; const label = i18n[ settings.i18n || settings.id ]; classes = [ CLASS_BUTTON ].concat( iconName ? `${ CLASS_BUTTON }--icon` : null, classes ); const button = create( 'button', { title : iconName ? label : null, type : 'button', tabindex : ! isUndefined( tabindex ) ? tabindex : null, role : checkbox ? 'checkbox' : null, 'aria-checked': checkbox ? 'false' : null, 'aria-label' : label, }, parent || settings.parent ); addClass( button, classes ); if ( iconName ) { const iconSettings = this.options.icons[ iconName ]; if ( iconSettings ) { append( button, icon( iconSettings[ 0 ], iconSettings[ 1 ], iconSettings[ 2 ] || STROKE_LINECAP ) ); } } else { html( button, settings.html || label ); } return button; } /** * A utility function to create an input field. * * @param settings - A settings object. * @param parent - A parent element where the created input element will be appended. * * @return A created input element. */ createField( settings: UIFieldSettings, parent: HTMLElement ): HTMLInputElement { const label = this.i18n[ settings.i18n || settings.id ]; const { tabindex } = settings; return create( 'input', { class : `${ CLASS_INPUT }`, placeholder : label, spellcheck : false, tabindex : ! isUndefined( tabindex ) ? tabindex : null, 'aria-label': label, }, parent ); } /** * Displays the UI. * * @param group - A group ID. */ show( group: string ): void { if ( this.isActive() ) { remove( this.groups[ this.group ].elm ); } addClass( this.wrapper, CLASS_ACTIVE ); this.append( group ); this.group = group; } /** * Hides the UI. */ hide(): void { if ( this.isActive() ) { removeClass( this.wrapper, CLASS_ACTIVE ); remove( this.groups[ this.group ].elm ); } } /** * Checks if the specified group is active or not. * If omitted, this checks any group is active or not. * * @param group - Optional. A group ID to check. */ isActive( group?: string ): boolean { return hasClass( this.wrapper, CLASS_ACTIVE ) && ( ! group || this.group === group ); } /** * Checks if one of the elements in the UI has focus or not. * * @return `true` if an element in the UI has focus, or otherwise `false`. */ isFocused(): boolean { return this.wrapper.contains( activeElement() ); } }