import type { IModalConfig } from './types'; import { IFRAMEURL } from '../../constants'; import { PostMessage } from '../../message'; import { ColorMode } from 'src/types'; const CONTAINER_ID = 'endless_dapp_modal_container'; const CONTAINER_CLASS_DARK = 'endless_dapp_modal_container_dark'; export const ENDLESS_MODEL_POSITION_LEFT = 'endless_dapp_modal_position_left'; export const ENDLESS_MODEL_POSITION_TOP = 'endless_dapp_modal_position_top'; export const IframeID = `${CONTAINER_ID}_iframe`; export const LoadingID = `${CONTAINER_ID}_loading`; export const ENDLESS_WALLET_WEB3_SDK_ENABLETHEME_TOGGLE_KEY = 'endless-wallet-web3-sdk-enableThemeToggle'; export const ENDLESS_WALLET_COLOR_MODE_KEY = 'endless-wallet-web3-sdk-color-mode'; const lightTitleBgSvg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYwIiBoZWlnaHQ9IjQ4IiB2aWV3Qm94PSIwIDAgMzYwIDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfMTM5OV82OTgpIj4KPHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSI0OCIgZmlsbD0id2hpdGUiLz4KPGcgb3BhY2l0eT0iMC4yIiBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzEzOTlfNjk4KSI+CjxlbGxpcHNlIGN4PSItOCIgY3k9IjMyIiByeD0iMTEwIiByeT0iNTEuNSIgZmlsbD0iIzg0NzNGRiIvPgo8L2c+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl8xMzk5XzY5OCIgeD0iLTI2OCIgeT0iLTE2OS41IiB3aWR0aD0iNTIwIiBoZWlnaHQ9IjQwMyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI3NSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzEzOTlfNjk4Ii8+CjwvZmlsdGVyPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzEzOTlfNjk4Ij4KPHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSI0OCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K'; const darkTitleBgSvg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYwIiBoZWlnaHQ9IjQ4IiB2aWV3Qm94PSIwIDAgMzYwIDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNDI1Nl8zMTc2KSI+CjxyZWN0IHdpZHRoPSIzNjAiIGhlaWdodD0iNDgiIGZpbGw9IiMwNTA2MDciLz4KPGcgb3BhY2l0eT0iMC4zIiBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzQyNTZfMzE3NikiPgo8ZWxsaXBzZSBjeD0iLTgiIGN5PSIxMDIiIHJ4PSIxMTAiIHJ5PSI1MS41IiBmaWxsPSIjODQ3M0ZGIi8+CjwvZz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9mXzQyNTZfMzE3NiIgeD0iLTI2OCIgeT0iLTk5LjUiIHdpZHRoPSI1MjAiIGhlaWdodD0iNDAzIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249Ijc1IiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfNDI1Nl8zMTc2Ii8+CjwvZmlsdGVyPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzQyNTZfMzE3NiI+CjxyZWN0IHdpZHRoPSIzNjAiIGhlaWdodD0iNDgiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg=='; function generateThresholdArray(count: number = 100) { const result = []; for (let i = 0; i <= count; i++) { result.push(parseFloat((i / count).toFixed(2))); } return result; } export class Modal { private static _instance: Modal; modalConfig: IModalConfig = { url: IFRAMEURL, colorMode: undefined }; private isContainerDefault: boolean = false; private isLoadingDefault: boolean = false; readyState: boolean = false; private offsetX: number = 0; private offsetY: number = 0; private containerObserver: IntersectionObserver | undefined = undefined; private containerWrap: HTMLElement = document.getElementById(CONTAINER_ID)!; constructor(modalConfig: IModalConfig) { if (Modal._instance) return Modal._instance; this.modalConfig = modalConfig; const containerEle = this.getContainer(modalConfig['endless']); this.createDefaultCSS(); this.createHTML(containerEle); this.containerWrap = document.getElementById(CONTAINER_ID)!; this.containerWrap && this.initializeObserver(); Modal._instance = this; } private getModelPositonStorage(key: string) { return localStorage.getItem(key); } private setModelPositonStorage(key: string, val: number) { localStorage.setItem(key, val.toString()); } private initializeObserver() { this.containerObserver = new IntersectionObserver(this.handleIntersect, { threshold: generateThresholdArray(), }); this.containerObserver.observe(this.containerWrap); } private handleIntersect = (entries: IntersectionObserverEntry[]) => { entries.forEach((entry) => { if (entry.target instanceof HTMLElement) { this.adjustPosition(entry); } }); }; private adjustPosition(entry: IntersectionObserverEntry) { const { top, left, bottom, right, height, width } = entry.boundingClientRect; const { width: viewWidth, height: viewHeight } = entry.rootBounds!; if (left >= 0 && right > viewWidth) { const leftVal = viewWidth - width; this.containerWrap.style.left = leftVal + 'px'; this.setModelPositonStorage(ENDLESS_MODEL_POSITION_LEFT, leftVal); } if (top >= 0 && bottom > viewHeight) { const topVal = viewHeight - height; this.containerWrap.style.top = topVal + 'px'; this.setModelPositonStorage(ENDLESS_MODEL_POSITION_TOP, topVal); } } private checkPosition() { if (!this.isElementInViewport(this.containerWrap)) { const topVal = 20; this.containerWrap.style.top = topVal + 'px'; this.setModelPositonStorage(ENDLESS_MODEL_POSITION_TOP, topVal); const leftVal = window.innerWidth - this.containerWrap.offsetWidth - 20; this.containerWrap.style.left = leftVal + 'px'; this.setModelPositonStorage(ENDLESS_MODEL_POSITION_LEFT, leftVal); } } stopAllObservations() { this.containerObserver?.disconnect(); } private isElementInViewport(element: HTMLElement) { const rect = element.getBoundingClientRect(); return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth; } getContainer(modalConfig: Partial) { if (modalConfig?.container) { const containerEle = document.querySelector(modalConfig.container); if (containerEle) { containerEle.className = modalConfig.className || ''; containerEle.setAttribute('style', modalConfig.style || ''); return containerEle; } } const body = document.body; const containerEle = document.createElement('div'); containerEle.id = CONTAINER_ID; containerEle.className = `${CONTAINER_ID}_hide ${ this.modalConfig.colorMode === 'dark' ? CONTAINER_CLASS_DARK : '' }`; const titleBoxEle = document.createElement('div'); titleBoxEle.className = `${CONTAINER_ID}_modal_title_box`; titleBoxEle.draggable = true; titleBoxEle.addEventListener('drag', this.dragHandler); titleBoxEle.addEventListener('dragstart', this.dragstartHandler); titleBoxEle.addEventListener('dragover', (event) => { event.preventDefault(); if (event.dataTransfer) { event.dataTransfer.dropEffect = 'move'; } }); const titleEle = document.createElement('div'); titleEle.innerHTML = ` `; const closeEle = document.createElement('div'); closeEle.className = `${CONTAINER_ID}_close_btn`; closeEle.innerHTML = ` `; closeEle.addEventListener('click', () => { this.closeModal(); }); titleBoxEle.appendChild(titleEle); titleBoxEle.appendChild(closeEle); containerEle.appendChild(titleBoxEle); // const iframeBoxEle = document.createElement('div'); // iframeBoxEle.className = `${CONTAINER_ID}_modal_iframe_box`; const iframeBodyEle = document.createElement('div'); iframeBodyEle.className = `${CONTAINER_ID}_modal_iframe_body`; // iframeBodyEle.appendChild(iframeBoxEle); containerEle.appendChild(iframeBodyEle); body.appendChild(containerEle); this.isContainerDefault = true; // return iframeBoxEle; return iframeBodyEle; } private dragstartHandler = (e: MouseEvent) => { const { offsetX, offsetY } = e; this.offsetX = offsetX; this.offsetY = offsetY; }; private dragHandler = (e: MouseEvent) => { const dom = document.getElementById(CONTAINER_ID); if (dom && e.clientX && e.clientY) { const _w = window.innerWidth - dom.offsetWidth; const _h = window.innerHeight - dom.offsetHeight; let left = e.clientX - this.offsetX; let top = e.clientY - this.offsetY; left = Math.min(Math.max(left, 0), _w); top = Math.min(Math.max(top, 0), _h); dom.style.left = `${left}px`; dom.style.top = `${top}px`; this.setModelPositonStorage(ENDLESS_MODEL_POSITION_TOP, top); this.setModelPositonStorage(ENDLESS_MODEL_POSITION_LEFT, left); } }; private createDefaultCSS = () => { const head = document.getElementsByTagName('head')[0]; const css = document.createElement('style'); css.className = 'endless_dapp_css'; let left = this.getModelPositonStorage(ENDLESS_MODEL_POSITION_LEFT) ?? window.innerWidth - (this.modalConfig.windowWidth ?? 360) - 20; let top = this.getModelPositonStorage(ENDLESS_MODEL_POSITION_TOP) ?? 20; css.innerHTML = ` #${CONTAINER_ID} { position: fixed; left: ${left}px; top: ${top}px; min-width: 320px; width: ${this.modalConfig.windowWidth}px; height: 100%; max-height: 660px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.07); border: 1px solid #E5E5E5; cursor: move; pointer-events: auto; z-index: 9999 !important; } #${CONTAINER_ID}.${CONTAINER_ID}_hide { display: none; } .${CONTAINER_ID}_modal_title_box{ height: 40px; display: flex; justify-content: space-between; align-items: center; padding: 0 16px; border-bottom: 1px solid #F3F3F3; background-image: url('${lightTitleBgSvg}'); background-repeat: no-repeat; background-size: 100%; color: #000; } .${CONTAINER_ID}_modal_title_box .${CONTAINER_ID}_close_btn { cursor: pointer; font-size: 20px; color: #000; } .${CONTAINER_ID}_modal_iframe_body { flex: 1; height: calc(100% - 40px); overflow-y: auto; } .${CONTAINER_ID}_modal_iframe_body::-webkit-scrollbar { display: none; } .${CONTAINER_ID}_modal_iframe_box{ height: 100%; } #${CONTAINER_ID} > #${IframeID} { width: 100%; height: 100%; background-color: #fff; } #${CONTAINER_ID}_close { position: absolute; right: 0; top: 0; transform: translate(50%, -50%); width: 40px; } #${LoadingID} { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); } @media (max-width: 640px) { #${CONTAINER_ID} { width: 100%; height: 95%; max-height: 100%; left: 0 !important; top: auto !important; bottom: 0 !important; transform: translate(0); border-radius: 10px 10px 0 0; } } #${CONTAINER_ID}.${CONTAINER_CLASS_DARK}{ border-color: #343536; } #${CONTAINER_ID}.${CONTAINER_CLASS_DARK} .${CONTAINER_ID}_modal_title_box{ border-bottom: 1px solid transparent; background-image: url('${darkTitleBgSvg}'); color: #fff; } #${CONTAINER_ID}.${CONTAINER_CLASS_DARK} .${CONTAINER_ID}_modal_title_box .${CONTAINER_ID}_close_btn { color: #fff; } `; head.appendChild(css); }; private createHTML = (container: Element) => { const htmlStr = ` `; container.innerHTML = htmlStr; }; loaded = () => { this.readyState = true; if (this.isLoadingDefault) { const body = document.body; const loadingNode = document.getElementById(LoadingID); body.removeChild(loadingNode!); } this.modalConfig?.loading?.finished && this.modalConfig?.loading?.finished(); }; private getLoadingContainer = (loadingConf: Partial) => { const loadingStr = loadingConf?.element || `
loading...
`; if (loadingConf?.container) { const loadingEle = document.querySelector(loadingConf.container); if (loadingEle) { loadingEle.className = loadingConf.className || ''; loadingEle.setAttribute('style', loadingConf.style || ''); if (typeof loadingStr === 'string') { loadingEle.innerHTML = loadingStr; } else { loadingEle.appendChild(loadingConf.element as Element); } return loadingEle; } } const loadingEle = document.createElement('div'); loadingEle.innerHTML = `
loading...
`; loadingEle.id = LoadingID; const body = document.body; body.appendChild(loadingEle); this.isLoadingDefault = true; return loadingEle; }; waitReady = async () => { return new Promise((resolve) => { const checkReadyState = () => { if (this.readyState) { return resolve(true); } else { window.setTimeout(async () => { checkReadyState(); }, 300); } }; checkReadyState(); }); }; readonly closeModal = (callback?: () => void) => { if (this.isContainerDefault) { const modal = document.getElementById(CONTAINER_ID)!; modal.classList.add(`${CONTAINER_ID}_hide`); } // clear all postMessage promise Object.values(PostMessage.promiseMap).forEach( (promise: { resolve?: (value: any) => void; reject: (value: any) => void }) => { promise.reject('close'); } ); PostMessage.promiseMap = {}; typeof this.modalConfig?.endless?.close === 'function' && this.modalConfig?.endless?.close(); typeof callback === 'function' && callback(); }; readonly setWalletContainerColorMode = async ({ colorMode }: { colorMode: ColorMode }) => { const modal = document.getElementById(CONTAINER_ID)!; switch (colorMode) { case 'light': modal.classList.remove(`${CONTAINER_CLASS_DARK}`); break; case 'dark': modal.classList.add(`${CONTAINER_CLASS_DARK}`); break; default: break; } }; readonly openModal = async (callback?: () => void) => { if (this.isContainerDefault) { await this.waitReady(); const modal = document.getElementById(CONTAINER_ID)!; modal.classList.remove(`${CONTAINER_ID}_hide`); this.checkPosition(); } typeof this.modalConfig?.endless?.open === 'function' && this.modalConfig?.endless?.open(); typeof callback === 'function' && callback(); }; }