@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>
204 lines (181 loc) • 5.77 kB
text/typescript
import { DialogGroupData, Elements, UIButtonSettings } from '@ryusei/code';
import { UIComponent } from '../../classes/UIComponent/UIComponent';
import { CLASS_ACTIVE } from '../../constants/classes';
import { EVENT_INIT_STYLE } from '../../constants/events';
import { PROJECT_CODE } from '../../constants/project';
import { addClass, append, assert, attr, create, div, isString, removeClass, text } from '../../utils';
import { GENERAL_UI_BUTTONS } from './buttons';
import {
CLASS_DIALOG,
CLASS_DIALOG_BODY,
CLASS_DIALOG_FOOTER,
CLASS_DIALOG_GROUP,
CLASS_DIALOG_HEADER,
CLASS_DIALOG_TITLE,
} from './classes';
/**
* The group ID of the common dialog.
*
* @since 0.1.0
*/
const COMMON_DIALOG_GROUP = `${ PROJECT_CODE }-common`;
/**
* The component for displaying a dialog.
*
* @since 0.1.0
*/
export class Dialog extends UIComponent<DialogGroupData> {
/**
* Initializes the component.
*
* @param elements - A collection of editor elements.
*/
mount( elements: Elements ): void {
super.mount( elements );
this.register( COMMON_DIALOG_GROUP, div(), '' );
}
/**
* Listens to some events.
*/
protected listen(): void {
this.bind( window, 'click', e => {
if ( ! this.wrapper.contains( e.target as Node ) ) {
this.hide();
}
} );
this.on( EVENT_INIT_STYLE, ( e, add ) => {
add( `.${ CLASS_DIALOG } code`, 'fontFamily', this.options.monospaceFont );
} );
}
/**
* Creates dialog elements.
* Note that the dialog element must/should have:
* - an accessible name by `aria-label` or `aria-labelledby`.
* - at least one focusable descendant element.
*
* @link https://www.w3.org/TR/wai-aria-1.2/#dialog
*/
protected create(): void {
const { elements } = this;
const id = `${ elements.root.id }-dialog`;
this.wrapper = div( {
id,
class : CLASS_DIALOG,
role : 'dialog',
'aria-labelledby' : `${ id }-title`,
'aria-describedby': `${ id }-body`,
}, elements.overlay );
}
/**
* Called when the general confirm button is clicked.
*
* @internal
*/
confirm(): void {
this.emit( `dialog:${ this.group }:confirmed`, this );
this.hide();
}
/**
* Registers a new dialog.
* Use `message()` instead just for showing a message.
*
* @example
* ```ts
* const ryuseiCode = new RyuseiCode();
* const Dialog = ryuseiCode.Editor.require( 'Dialog' );
*
* // The Dialog extension may not exist.
* if ( Dialog ) {
* const body = document.createElement( 'p' );
* body.textContent = 'Hello!';
* Dialog.register( 'sample', body, 'Sample Dialog', [ 'confirm' ] );
* Dialog.show( 'sample' );
* }
* ```
*
* If you want to add custom buttons, pass an array with button settings to the `buttons`.
*
* ```ts
* const settings = [
* {
* id: 'myButton',
* html: 'Click Me',
* click() {
* console.log( 'Clicked!' );
* },
* }
* ];
*
* Dialog.register( 'sample', body, 'Sample Dialog', settings );
* ```
*
* @param group - A group ID.
* @param elm - An element to display as a dialog body.
* @param title - A title of a dialog.
* @param buttons - Optional. General button names, `'confirm'`, `'cancel'`, or objects with button settings.
*/
register(
group: string,
elm: HTMLElement,
title: string,
buttons?: Array<keyof typeof GENERAL_UI_BUTTONS | UIButtonSettings<Dialog>>
): void {
const settings = ( buttons || [ 'confirm' ] )
.map( settings => isString( settings ) ? GENERAL_UI_BUTTONS[ settings ] : settings )
.filter( Boolean );
assert( settings.length );
const { id } = this.wrapper;
const groupElm = div( CLASS_DIALOG_GROUP );
const headerElm = create( 'header', CLASS_DIALOG_HEADER );
const titleElm = create( 'h3', { id: `${ id }-title`, class: CLASS_DIALOG_TITLE }, headerElm );
const footerElm = create( 'footer', CLASS_DIALOG_FOOTER );
const button = this.createCloseButton( { 'aria-controls': id } );
attr( elm, { id: `${ id }-body`, class: CLASS_DIALOG_BODY } );
text( titleElm, title );
addClass( button, `${ CLASS_DIALOG }__close` );
append( groupElm, [ headerElm, elm, footerElm, button ] );
this.groups[ group ] = {
elm : groupElm,
title : titleElm,
body : elm,
buttons: this.createButtons<Dialog>( settings, footerElm, this ),
};
}
/**
* Opens the specified dialog. The dialog must be registered by `register()` before opening it.
*
* @param group - A dialog ID to open.
*/
show( group: string ): void {
this.hide();
super.show( group );
this.Editor.readOnly = true;
addClass( this.elements.overlay, CLASS_ACTIVE );
this.autoFocus( group );
this.emit( 'dialog:opened', this, group );
}
/**
* Closes the dialog which is visible now. Nothing will happen when there is no shown dialog.
*/
hide(): void {
if ( this.isActive() ) {
this.Editor.readOnly = false;
super.hide();
removeClass( this.elements.overlay, CLASS_ACTIVE );
this.Selection.reselect();
this.emit( 'dialog:closed', this, this.group );
}
}
/**
* Displays a message with a common dialog. No registration required.
*
* @param message - A message to display.
* @param title - Optional. A title of a dialog. If omitted, uses the `notice` in the `i18n` collection.
*/
message( message: string, title?: string ): void {
const data = this.groups[ COMMON_DIALOG_GROUP ];
text( data.title, title || this.i18n.notice );
text( data.body, message );
this.show( COMMON_DIALOG_GROUP );
}
}