@siberiaweb/components
Version:
366 lines (298 loc) • 10.9 kB
text/typescript
import { ModalResult } from "./Types";
import CSS from "./CSS";
import Properties from "./Properties";
import "./ModalForm.css";
import HTMLElementUtils from "../utils/HTMLElementUtils";
/**
* Модальная форма.
*/
export default class ModalForm {
/**
* Хост.
*/
private readonly host: HTMLDivElement;
/**
* Подложка.
*/
private readonly overlay: HTMLDivElement;
/**
* Функция модального разрешения.
*/
private modalResolve: Function | null = null;
/**
* Модальный результат.
*/
private modalResult: ModalResult = "undefined";
/**
* Закрытие модальной формы при клике вне границ.
*/
private readonly closeOnClickOutOfBounds: boolean;
/**
* Закрытие модальной формы по клавише Escape.
*/
private readonly closeOnEscape: boolean;
/**
* Элемент для установки фокуса.
*/
private _focusElement: HTMLElement | null = null;
/**
* Предотвращение получения событий от клавиатуры элементами модальной формы.
*/
private preventKeyboardEvents: boolean = false;
/**
* Создание хоста.
*/
private createHost(
id?: string
): HTMLDivElement {
let container: HTMLDivElement = document.createElement( "div" );
container.classList.add( CSS.MODAL_FORM );
if ( id ) {
container.id = id;
}
container.addEventListener( "keydown", (
event: KeyboardEvent
): void => {
if ( this.preventKeyboardEvents ) {
event.preventDefault();
event.stopImmediatePropagation();
}
}, {
capture: true
} );
return container;
}
/**
* Создание подложки.
*/
private createOverlay(): HTMLDivElement {
let container: HTMLDivElement = document.createElement( "div" );
container.classList.add( CSS.OVERLAY );
container.addEventListener( "click", (
event: MouseEvent
): void => {
if ( ( event.target === container ) && this.closeOnClickOutOfBounds ) {
this.close( "cancel" );
}
} );
return container;
}
/**
* Инициализация элементов управления.
*/
private initControls(): void {
let modalOkElements: HTMLElement[] = Array.from( this.host.querySelectorAll( "[data-mr-ok]" ) );
for ( const element of modalOkElements ) {
element.addEventListener( "click", (): void => {
this.close( "ok" );
} );
}
let modalCancelElements: HTMLElement[] = Array.from( this.host.querySelectorAll( "[data-mr-cancel]" ) );
for ( const element of modalCancelElements ) {
element.addEventListener( "click", (): void => {
this.close( "cancel" );
} );
}
}
/**
* Вывод.
*/
public show(): Promise< void > {
return new Promise< void >( ( resolve => {
this.modalResolve = resolve;
if ( ModalForm.shawnForms.length === 0 ) {
document.body.classList.add( CSS.OVERFLOW_HIDDEN );
}
else {
ModalForm.shawnForms[ ModalForm.shawnForms.length - 1 ].getOverlay().classList.add(
CSS.OVERFLOW_HIDDEN );
}
ModalForm.shawnForms.push( this );
document.body.appendChild( this.overlay );
window.requestAnimationFrame( (): void => {
let enabledControls: HTMLElement[] = this.getEnabledControls();
if ( enabledControls.length > 0 ) {
enabledControls[ 0 ].focus();
}
} );
} ) );
}
/**
* Закрытие.
*/
public close(
result: ModalResult = "undefined"
): void {
( async () => {
this.preventKeyboardEvents = true;
this.overlay.classList.add( CSS.CLOSING );
await HTMLElementUtils.waitForAnimation( this.overlay );
this.overlay.classList.remove( CSS.CLOSING );
this.preventKeyboardEvents = false;
this.modalResult = result;
this.overlay.remove();
this.overlay.classList.remove( CSS.OVERFLOW_HIDDEN );
ModalForm.shawnForms.splice( ModalForm.shawnForms.indexOf( this ), 1 );
if ( ModalForm.shawnForms.length === 0 ) {
document.body.classList.remove( CSS.OVERFLOW_HIDDEN );
}
else {
let topModalForm: ModalForm = ModalForm.shawnForms[ ModalForm.shawnForms.length - 1 ];
if ( topModalForm.focusElement !== null ) {
topModalForm.focusElement.focus();
}
}
if ( this.modalResolve !== null ) {
this.modalResolve();
}
} )();
}
/**
* Получение модального результата.
*/
public getModalResult(): ModalResult {
return this.modalResult;
}
/**
* Получение элемента формы по идентификатору.
*
* @param id Идентификатор.
*/
public getElementById< T extends HTMLElement >(
id: string
): T {
return this.getHost().querySelector( `[data-mf-id="${ id }"]` ) as T;
}
/**
* Получение хоста.
*/
public getHost(): HTMLDivElement {
return this.host
}
/**
* Получение подложки.
*/
public getOverlay(): HTMLDivElement {
return this.overlay;
}
/**
* Получение признака закрытия модальной формы при клике вне границ.
*/
public isCloseOnClickOutOfBounds(): boolean {
return this.closeOnClickOutOfBounds;
}
/**
* Получение признака закрытия модальной формы по клавише Escape.
*/
public isCloseOnEscape(): boolean {
return this.closeOnEscape;
}
/**
* Получение активных элементов управления.
*/
public getEnabledControls(): HTMLElement[] {
let controls: HTMLElement[] = Array.from( this.host.querySelectorAll(
'a, button, input, select, textarea, [ tabindex ]:not( [ tabindex="-1" ] )' ) );
return controls.filter( ( control) => {
return !control.hasAttribute( "disabled" );
} );
}
/**
* Получение элемента для фокусировки.
*/
get focusElement(): HTMLElement | null {
return this._focusElement;
}
/**
* Установка элемента для фокусировки.
*
* @param value Значение.
*/
set focusElement(
value: HTMLElement | null
) {
this._focusElement = value;
}
/**
* Конструктор.
*
* @param template Шаблон содержания.
* @param properties Свойства.
*/
constructor(
template: HTMLTemplateElement,
properties: Properties = {}
) {
this.host = this.createHost( template.dataset[ "modalFormId" ] );
this.overlay = this.createOverlay();
this.closeOnClickOutOfBounds = ( properties.closeOnClickOutOfBounds === undefined ) ||
properties.closeOnClickOutOfBounds;
this.closeOnEscape = ( properties.closeOnEscape === undefined ) || properties.closeOnEscape;
this.host.appendChild( template.content.cloneNode( true ) );
this.overlay.appendChild( this.host );
this.initControls();
if ( !ModalForm.globalEventListenersAssigned ) {
document.addEventListener( "keydown", ModalForm.documentKeyDownEventListener );
document.addEventListener( "focusin", ModalForm.documentFocusInEventListener );
}
}
/**
* Выведенные на экран формы.
*/
private static shawnForms: ModalForm[] = [];
/**
* Признак установки глобальных обработчиков.
*/
private static globalEventListenersAssigned: boolean = false;
/**
* Глобальный обработчик нажатия клавиш.
*
* @param event Событие.
*/
private static documentKeyDownEventListener(
event: KeyboardEvent
): void {
if ( ( event.code === "Escape" ) && ( ModalForm.shawnForms.length > 0 ) ) {
let modalForm: ModalForm = ModalForm.shawnForms[ ModalForm.shawnForms.length - 1 ];
if ( modalForm.isCloseOnEscape() ) {
modalForm.close( "cancel" );
}
}
}
/**
* Глобальный обработчик установки фокуса.
*
* @param event Событие.
*/
private static documentFocusInEventListener(
event: FocusEvent
): void {
if ( !( event.target instanceof HTMLElement ) || ( ModalForm.shawnForms.length === 0 ) ) {
return;
}
let modalForm: ModalForm = ModalForm.shawnForms[ ModalForm.shawnForms.length - 1 ];
if ( modalForm.getHost().contains( event.target ) ) {
modalForm.focusElement = event.target;
return;
}
let enabledControls: HTMLElement[] = modalForm.getEnabledControls();
if ( enabledControls.length === 0 ) {
return;
}
if ( modalForm.focusElement === null ) {
event.preventDefault();
enabledControls[ 0 ].focus();
return;
}
if ( enabledControls[ 0 ] === modalForm.focusElement ) {
event.preventDefault();
enabledControls[ enabledControls.length - 1 ].focus();
return;
}
if ( enabledControls[ enabledControls.length - 1 ] === modalForm.focusElement ) {
event.preventDefault();
enabledControls[ 0 ].focus();
return;
}
}
}