import { SmpPopupInit } from './smpPopupInit'; import { typePropsSmpPopup, typeShowOptionsSmpPopup } from './types/smpPopup.types'; import { getHref, getIdFromHash, isIncludeDomElement } from './utils/smpSelection.utils'; import { ID_SUFFIX } from './const/smpSelection.const'; import './style.css'; export default class SmpPopup { private itemsPopup: SmpPopupInit[] = []; private initPopupsMap: Map = new Map(); isAutoFocusInput?: boolean; /** нужно ли ставить фокус на интуп */ classPopup: string; /** Основной класс для popup - по ум. b_popup */ wrapperAddClass?: string; /** Доп. класс для b_popup */ bodyHtmlAddClass?: string; /** Доп. класс для b_popup__body */ closeAddClass?: string; /** Доп. класс для b_popup__close * */ /** вызывается после первого окна * */ callbackOpenAlways: ((wrapperNode: HTMLElement) => void) | null = null; /** вызывается после каждого вызова окна * */ callbackOpenOnce: ((wrapperNode: HTMLElement) => void) | null = null; /** вызывается после каждого закрытия окна * */ callbackCloseAlways: ((wrapperNode: HTMLElement, returnBoolClose: boolean) => void) | null = null; DOM_ELEMENT_POPUP?: HTMLElement; private selectorLinks: HTMLElement | NodeListOf; constructor(props?: typePropsSmpPopup) { if (!props) props = {}; this.classPopup = props.classPopup ? props.classPopup : 'b_popup'; this.wrapperAddClass = props.wrapperAddClass ? props.wrapperAddClass : undefined; this.bodyHtmlAddClass = props.bodyHtmlAddClass ? props.bodyHtmlAddClass : undefined; this.closeAddClass = props.closeAddClass ? props.closeAddClass : undefined; this.callbackOpenOnce = props.callbackOpenOnce ? props.callbackOpenOnce : null; this.callbackOpenAlways = props.callbackOpenAlways ? props.callbackOpenAlways : null; this.callbackCloseAlways = props.callbackCloseAlways ? props.callbackCloseAlways : null; this.isAutoFocusInput = props.isAutoFocusInput ?? false; if (props.selectorLinks) { if (typeof props.selectorLinks === 'string') { this.selectorLinks = document.querySelectorAll(props.selectorLinks); } else { this.selectorLinks = props.selectorLinks; } } else { this.selectorLinks = document.querySelectorAll('[data-open-popup]'); } this.init(); } private init(): void { if (this.selectorLinks) { if (this.selectorLinks instanceof HTMLElement) { this.selectorLinks.addEventListener('click', (e) => this.handlerClick(e, (this.selectorLinks as HTMLElement))); } else if (this.selectorLinks.length) { this.selectorLinks.forEach(($item) => { $item.addEventListener('click', (e) => this.handlerClick(e, $item)); }); } } // Обработка клика "Назад" в браузере window.addEventListener('popstate', (e: PopStateEvent) => { const currentState = e.state || window.history.state || {}; // Проходим по массиву открытых окон с конца (с самого верхнего) for (let i = this.itemsPopup.length - 1; i >= 0; i--) { const popup = this.itemsPopup[i]; // Если окно помечено как открытое, но его ID больше нет в history state — значит нажат "Назад" if (popup && popup.isPopupOpen && !currentState[popup.historyId]) { popup.hide(false, true); // Передаем isFromHistory = true, чтобы НЕ вызывать history.back() } } }); } private handlerClick(e: MouseEvent, elementNode: HTMLElement): void { e.preventDefault(); const href: string | null = getHref(elementNode); if (!href) { return; } const hash: string = getIdFromHash(href); const options: typeShowOptionsSmpPopup = {}; if (elementNode.dataset.addClassBody) { options.bodyAddClass = elementNode.dataset.addClassBody; } this.show(hash, options); } public show(id: string, options: typeShowOptionsSmpPopup = {} /* $link?:HTMLElement */): HTMLElement | null { const $this: HTMLElement | null = document.getElementById(id); if ($this) { let currentInitPopup = this.initPopupsMap.get(id); const isFirstInit = !currentInitPopup; if (!currentInitPopup) { currentInitPopup = new SmpPopupInit({ $this, id, classPopup: this.classPopup, bodyHtmlAddClass: this.bodyHtmlAddClass, wrapperAddClass: this.wrapperAddClass, closeAddClass: this.closeAddClass, options, localCallbackCloseBefore: this.hideBefore, }); this.initPopupsMap.set(id, currentInitPopup); } this.DOM_ELEMENT_POPUP = $this; currentInitPopup.open(options); if (!this.itemsPopup.includes(currentInitPopup)) { this.itemsPopup.push(currentInitPopup); } if (this.itemsPopup.length === 1) { setTimeout(() => { document.addEventListener('keyup', this.keyEscHide); }, 10); } /** вызываем автофокус, если нужно * */ this.inputAutoFocus(); if (isFirstInit && this.callbackOpenOnce) { /** вызывается после первого вызова окна * */ this.callbackOpenOnce($this); } /** вызывается после каждого вызова окна * */ if (this.callbackOpenAlways) { this.callbackOpenAlways($this); } return $this; } return null; } private keyEscHide = (e: KeyboardEvent): void => { if (e.code === 'Escape') { if (isIncludeDomElement(e.target as HTMLElement, 'input,label,textarea,video,audio,select')) { return; } const lastOpenPopup: SmpPopupInit = this.itemsPopup[this.itemsPopup.length - 1]; if (lastOpenPopup) { lastOpenPopup.hide(); } else { document.removeEventListener('keyup', this.keyEscHide); } } }; public hideBefore = (id: string, returnBoolClose = false, isDelete = false): null => { const currentPopup: SmpPopupInit | undefined = this.itemsPopup.find((item) => item.id === id); const newItemsPopup: SmpPopupInit[] = this.itemsPopup.filter((item) => item.id !== id); /** вызывается после каждого закрытия окна * */ if (currentPopup && this.callbackCloseAlways) { this.callbackCloseAlways(currentPopup.$this, returnBoolClose); } if (isDelete) { this.initPopupsMap.delete(id); } /** удаляем не нужный элемент из активных * */ this.itemsPopup = newItemsPopup; if (newItemsPopup.length === 0) { document.removeEventListener('keyup', this.keyEscHide); } return null; }; private inputAutoFocus(): void { if (this.DOM_ELEMENT_POPUP && this.isAutoFocusInput) { let inputAutoFocus: HTMLInputElement | null = this.DOM_ELEMENT_POPUP.querySelector('[data-autofocus]'); if (!inputAutoFocus) { inputAutoFocus = this.DOM_ELEMENT_POPUP.querySelector('input:not([type="hidden"])'); } if (inputAutoFocus) { inputAutoFocus.blur(); // костыль для IOS // eslint-disable-next-line no-void void inputAutoFocus.offsetHeight; inputAutoFocus.focus(); // Fallback for iOS Safari: trigger focus again within the user gesture grace period setTimeout(() => { if (document.activeElement !== inputAutoFocus) { inputAutoFocus.blur(); // eslint-disable-next-line no-void void inputAutoFocus.offsetHeight; inputAutoFocus.focus(); } }, 50); } } } /** закрытия окна по ID - такое же ID как у popup * */ public hideForId(id: string, isDeleteForm = false): null { this.itemsPopup.forEach((item: SmpPopupInit) => { if (item.id === id) { item.hide(isDeleteForm); } }); return null; } public hideAll(): null { this.itemsPopup.forEach((item) => item.hide()); return null; } // удаляет из HTML скрытую форму public removeFormDOM(id: string): void { this.itemsPopup.forEach((item: SmpPopupInit) => { if (item.id === id) { item.hide(true); } }); this.initPopupsMap.delete(id); const element: HTMLElement | null = document.getElementById(id + ID_SUFFIX); if (element) { element.remove(); } } public getOpenPopups(): SmpPopupInit[] { return this.itemsPopup; } }