UNPKG

@siberiaweb/components

Version:
366 lines (298 loc) 10.9 kB
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; } } }