/** @jsx createElement */ import { createElement, Component, createRef, RenderableProps } from 'preact'; import b from 'bem-react-helper'; import { Theme } from '@app/common/types'; import { sleep } from '@app/utils/sleep'; import { Button } from '@app/components/button'; type Props = RenderableProps<{ title: string; titleClass?: string; heading?: string; isActive?: boolean; disabled?: boolean; buttonTitle?: string; onTitleClick?: () => void; mix?: string; theme: Theme; onOpen?: (root: HTMLDivElement) => unknown; onClose?: (root: HTMLDivElement) => unknown; }>; interface State { isActive: boolean; contentTranslateX: number; } export class Dropdown extends Component { rootNode = createRef(); storedDocumentHeight: string | null = null; storedDocumentHeightSet: boolean = false; checkInterval: number | undefined = undefined; constructor(props: Props) { super(props); this.state = { isActive: props.isActive || false, contentTranslateX: 0, }; this.onOutsideClick = this.onOutsideClick.bind(this); this.receiveMessage = this.receiveMessage.bind(this); this.__onOpen = this.__onOpen.bind(this); this.__onClose = this.__onClose.bind(this); } onTitleClick = () => { const isActive = !this.state.isActive; const contentTranslateX = isActive ? this.state.contentTranslateX : 0; this.setState( { contentTranslateX, isActive, }, async () => { await this.__adjustDropDownContent(); if (isActive) { this.__onOpen(); this.props.onOpen && this.props.onOpen(this.rootNode.current!); } else { this.__onClose(); this.props.onClose && this.props.onClose(this.rootNode.current!); } if (this.props.onTitleClick) { this.props.onTitleClick(); } } ); }; __onOpen() { const isChildOfDropDown = (() => { if (!this.rootNode.current) return false; let parent = this.rootNode.current.parentElement!; while (parent !== document.body) { if (parent.classList.contains('dropdown')) return true; parent = parent.parentElement!; } return false; })(); if (isChildOfDropDown) return; this.storedDocumentHeight = document.body.style.minHeight; this.storedDocumentHeightSet = true; let prevDcBottom: number | null = null; this.checkInterval = window.setInterval(() => { if (!this.rootNode.current || !this.state.isActive) return; const windowHeight = window.innerHeight; const dcBottom = (() => { // TODO: use ref const dc = Array.from(this.rootNode.current.children).find(c => c.classList.contains('dropdown__content')); if (!dc) return 0; const rect = dc.getBoundingClientRect(); return window.scrollY + Math.abs(rect.top) + dc.scrollHeight + 10; })(); if (prevDcBottom === null && dcBottom <= windowHeight) return; if (dcBottom !== prevDcBottom) { prevDcBottom = dcBottom; document.body.style.minHeight = dcBottom + 'px'; } }, 100); } __onClose() { window.clearInterval(this.checkInterval); if (this.storedDocumentHeightSet) { document.body.style.minHeight = typeof this.storedDocumentHeight === 'string' ? this.storedDocumentHeight : ''; } } async __adjustDropDownContent() { if (!this.rootNode.current) return; // TODO: use ref const dc = this.rootNode.current.querySelector('.dropdown__content'); if (!dc) return; await sleep(0); const rect = dc.getBoundingClientRect(); if (rect.left > 0) { const wWindow = window.innerWidth; if (rect.right <= wWindow) return; const delta = rect.right - wWindow; const max = Math.min(rect.left, delta); this.setState({ contentTranslateX: -max, }); return; } this.setState({ contentTranslateX: -rect.left, }); } receiveMessage(e: { data: string | object }) { try { const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; if (!data.clickOutside) return; if (!this.state.isActive) return; this.setState( { contentTranslateX: 0, isActive: false, }, () => { this.__onClose(); this.props.onClose && this.props.onClose(this.rootNode.current!); } ); } catch (e) {} } onOutsideClick(e: MouseEvent) { if (!this.rootNode.current || this.rootNode.current.contains(e.target as Node) || !this.state.isActive) return; this.setState( { contentTranslateX: 0, isActive: false, }, () => { this.__onClose(); this.props.onClose && this.props.onClose(this.rootNode.current!); } ); } componentDidMount() { document.addEventListener('click', this.onOutsideClick); window.addEventListener('message', this.receiveMessage); } componentWillUnmount() { document.removeEventListener('click', this.onOutsideClick); window.removeEventListener('message', this.receiveMessage); } render({ title, titleClass, heading, children, mix, theme, disabled, buttonTitle }: Props, { isActive }: State) { return (
{isActive && (
{heading &&
{heading}
}
{children}
)}
); } }