import { typePropsInit, typeShowOptionsSmpPopup } from './types/smpPopup.types'; import { ID_SUFFIX } from './const/smpSelection.const'; export class SmpPopupInit { id: string; $this: HTMLElement; options: typeShowOptionsSmpPopup = {}; private $wrapper?: HTMLElement; private $bodyHTML?: HTMLElement; private readonly classPopup: string; closePopupButton: HTMLButtonElement | undefined; private readonly wrapperAddClass: string | undefined; private readonly bodyHtmlAddClass: string | undefined; private readonly closeAddClass: string | undefined; /** вызывается после каждого закрытия окна * */ callbackCloseAlways: (($wrapper: HTMLElement) => void) | null = null; localCallbackCloseBefore: (id: string, returnBoolClose?: boolean, isDelete?: boolean) => null; returnBoolClose = false; isPopupOpen = false; historyId: string; private readonly htmlElement = document.documentElement; private readonly bodyElement = document.body; constructor(props: typePropsInit) { this.id = props.id; this.$this = props.$this; this.wrapperAddClass = props.wrapperAddClass; this.bodyHtmlAddClass = props.bodyHtmlAddClass; this.closeAddClass = props.closeAddClass; this.options = props.options || {}; this.classPopup = props.classPopup; this.localCallbackCloseBefore = props.localCallbackCloseBefore; this.initHTML(); this.historyId = `smpPopup_${this.id}`; } public open(customOptions?: typeShowOptionsSmpPopup): void { if (customOptions) { this.options = { ...this.options, ...customOptions }; } if (this.$wrapper) { if (this.$this && this.$bodyHTML && !this.$bodyHTML.contains(this.$this)) { this.$bodyHTML.append(this.$this); } this.$wrapper.tabIndex = 0; // document.body.classList.add('open_popup'); this.toggleOpenClass(true); this.isPopupOpen = true; // Переносим в конец body для актуального z-index слоя, только если он еще не там if (this.$wrapper.parentElement !== document.body || this.$wrapper.nextSibling !== null) { document.body.appendChild(this.$wrapper); } this.$wrapper.classList.add('open'); // Сохраняем текущее состояние истории и добавляем ID попапа const currentState = window.history.state || {}; window.history.pushState({ ...currentState, [this.historyId]: true }, ''); } } hide(isDelete = false, isFromHistory = false): void { if (!this.$wrapper) { return; } // Explicitly blur the active element if it is inside this popup wrapper const activeEl = document.activeElement; if (activeEl && this.$wrapper.contains(activeEl)) { (activeEl as HTMLElement).blur(); } this.$wrapper.classList.remove('open'); // 1. Проверяем, есть ли в DOM ДРУГИЕ открытые модальные окна const remainingOpenPopups = document.querySelectorAll(`.${this.classPopup}.open`); // Убираем класс с body только если открытых окон больше нет if (remainingOpenPopups.length === 0) { this.toggleOpenClass(false); // document.body.classList.remove('open_popup'); } const willDelete = isDelete || !!(this.$this?.dataset?.removeBeforeClose && this.$this.dataset.removeBeforeClose !== 'false'); this.localCallbackCloseBefore(this.id, this.returnBoolClose, !!willDelete); // Если закрыто НЕ по кнопке "Назад" браузера, откатываем историю if (window.history.state?.[this.historyId] && !isFromHistory) { window.history.back(); } this.isPopupOpen = false; if (willDelete) { setTimeout(() => { if (this.$this && this.$wrapper && this.$wrapper.contains(this.$this)) { document.body.appendChild(this.$this); } this.$wrapper?.parentNode?.removeChild(this.$wrapper); }, 300); } } private toggleOpenClass(isOpen: boolean): void { if (isOpen) { this.htmlElement.classList.add('open_popup_html'); this.bodyElement.classList.add('open_popup'); } else { this.htmlElement.classList.remove('open_popup_html'); this.bodyElement.classList.remove('open_popup'); } } private initHTML(): void { const oldInitPopup: HTMLElement | null = document.getElementById(this.id + ID_SUFFIX); if (oldInitPopup) { this.$wrapper = oldInitPopup; this.$wrapper.dataset.firstInit = 'false'; /** если окно уже было добавлено, переносим в конец body (если это второе окно, для z-index) * */ document.body.appendChild(this.$wrapper); this.toggleOpenClass(true); // document.body.classList.add('open_popup'); // setTimeout(() => { // this.$wrapper?.classList.add('open'); // document.body.classList.add('open_popup'); // }, 10); } else { this.$wrapper = document.createElement('div'); this.$wrapper.id = this.id + ID_SUFFIX; this.$wrapper.className = this.classPopup; /* b_popup */ if (this.wrapperAddClass) { this.$wrapper.classList.add(this.wrapperAddClass); } this.$wrapper.dataset.firstInit = 'true'; this.$wrapper.setAttribute('aria-modal', 'true'); this.$wrapper.setAttribute('role', 'true'); const overlay: HTMLElement = document.createElement('div'); overlay.className = `${this.classPopup}__overlay`; /* b_popup__overlay */ overlay.dataset.close = 'true'; const currentWrapper: HTMLElement = document.createElement('div'); currentWrapper.className = `${this.classPopup}__wrapper`; /* b_popup__wrapper */ if (this.bodyHtmlAddClass) { currentWrapper.classList.add(this.bodyHtmlAddClass); } currentWrapper.dataset.close = 'true'; this.closePopupButton = document.createElement('button'); this.closePopupButton.type = 'button'; let closePopupButton = `${this.classPopup}__close`; if (this.closeAddClass) { closePopupButton += ` ${this.closeAddClass}`; } this.closePopupButton.className = closePopupButton; /* b_popup__close */ this.closePopupButton.setAttribute('aria-label', 'Close window'); this.closePopupButton.innerText = 'Close window'; this.closePopupButton.dataset.close = 'true'; this.closePopupButton.dataset.tabIndex = '0'; this.closePopupButton.dataset.focusClose = 'true'; this.$bodyHTML = document.createElement('div'); this.$bodyHTML.className = `${this.classPopup}__body`; /* b_popup__body */ currentWrapper.append(this.$bodyHTML); const lastActive = document.createElement('div'); lastActive.tabIndex = 0; lastActive.dataset.lastIndexActive = '1'; currentWrapper.append(lastActive); this.$bodyHTML.append(this.closePopupButton); let isClose = false; this.$wrapper.addEventListener('mousedown', (e) => { isClose = false; const $target: HTMLElement = e.target as HTMLElement; if ($target.dataset.close && e.button === 0) { /** отлов клик по скроллу * */ if ($target === currentWrapper) { const clickOutScroll: boolean = $target.clientWidth > e.clientX; if (clickOutScroll) { isClose = true; } } else { isClose = true; } } }); this.$wrapper.addEventListener('mouseup', (e) => { const $target: HTMLElement = e.target as HTMLElement; if ($target.dataset.close && e.button === 0 && isClose) { /** отлов клик по скроллу * */ if ($target === currentWrapper) { const clickOutScroll: boolean = $target.clientWidth > e.clientX; if (clickOutScroll) { e.preventDefault(); this.returnBoolClose = !!$target.dataset.returnTrue; this.hide(false); } } else { this.returnBoolClose = !!$target.dataset.returnTrue; e.preventDefault(); this.hide(false); } } isClose = false; }); this.$wrapper.addEventListener('keyup', (e) => { const $target: HTMLElement = e.target as HTMLElement; if (e.key === 'Enter' && $target.dataset.focusClose) { this.returnBoolClose = !!$target.dataset.returnTrue; e.preventDefault(); this.hide(false); } if (e.key === 'Tab' && $target.dataset.lastIndexActive) { this.closePopupButton?.focus(); } }); this.$wrapper.addEventListener('keydown', (e) => { const $target: HTMLElement = e.target as HTMLElement; if (e.key === 'Tab' && $target.dataset.lastIndexActive) { this.closePopupButton?.focus(); } }); this.$wrapper.append(overlay); this.$wrapper.append(currentWrapper); document.body.append(this.$wrapper); this.$bodyHTML.append(this.$this); if (this.options.wrapperAddClass) { this.$wrapper.classList.add(this.options.wrapperAddClass); } if (this.options.bodyAddClass) { this.$bodyHTML.classList.add(this.options.bodyAddClass); } } if (this.options.restart === true) { const classContainer: HTMLElement = this.$wrapper.querySelector(`.${this.classPopup}__content`) as HTMLDivElement; if (classContainer) { classContainer.dataset.removeBeforeClose = 'true'; } } } }