import { unrefElement, effectDispose, isValueType, setStyle } from '@/utils' import type { BasicTarget } from '@/types' export interface UseElementFullscreenOptions { /** * * @description * 进入全屏前的回调。 * * @default undefined */ beforeEnter?: () => void /** * * @description * 进入全屏后的回调。 * * @default undefined */ enter?: () => void /** * * @description * 退出全屏前的回调。 * * @default undefined */ beforeExit?: () => void /** * * @description * 退出全屏后的回调。 * * @default undefined */ exit?: () => void /** * * @description * 全屏时的 z-index。 * * @default 999 */ zIndex?: number /** * * @description * 全屏时的背景色。 * * @default null */ backgroundColor?: string /** * * @description * 手动设定 transition 过度效果。 * * @default 'transform 0.3s var(--r-bezier)' */ transition?: string } let currentZIndex = 999 let isAppend = false const ID_TAG = 'ELEMENT-FULLSCREEN-RAY' const styleElement = document.createElement('style') /** * * @param target target dom * @param options useElementFullscreen options * * @description * 使元素全屏,但是不调用浏览器的全屏 API,仅使用纯 css 实现。 * 该方法具有入侵性,并且会在元素上覆盖 transition 样式。 * * 该方法虽然能够实现全屏,但是会覆盖元素的一些基本样式,因此需要注意管理元素的一些基本样式,例如:position、z-index、transition、transform、width、height。 * * @example * * */ export const useElementFullscreen = ( target: BasicTarget, options?: UseElementFullscreenOptions, ) => { const { beforeEnter, beforeExit, enter: _enter, exit: _exit, backgroundColor, zIndex, transition = 'transform 0.3s var(--r-bezier)', } = options ?? {} let isSetup = false const catchBoundingClientRect: { x: number | null y: number | null } = { x: null, y: null, } // 使用 ref 来追踪状态 const isFullscreen = ref(false) const updateStyle = () => { const element = unrefElement(target) as HTMLElement | null if (!element) { return } const { left, top } = element.getBoundingClientRect() if ( catchBoundingClientRect.x === null && catchBoundingClientRect.y === null ) { catchBoundingClientRect.x = -left catchBoundingClientRect.y = -top } setStyle(document.body, { '--element-fullscreen-z-index': isValueType(zIndex, 'Null') || isValueType(zIndex, 'Undefined') ? currentZIndex : zIndex, '--element-fullscreen-transition': transition, '--element-fullscreen-background-color': backgroundColor, '--element-fullscreen-width': 'var(--html-width)', '--element-fullscreen-height': 'var(--html-height)', '--element-fullscreen-transform-x': `${catchBoundingClientRect.x}px`, '--element-fullscreen-transform-y': `${catchBoundingClientRect.y}px`, }) const cssContent = ` [${ID_TAG}] { position: fixed; width: var(--element-fullscreen-width) !important; height: var(--element-fullscreen-height) !important; transform: translate(var(--element-fullscreen-transform-x), var(--element-fullscreen-transform-y)) !important; transition: var(--element-fullscreen-transition); z-index: var(--element-fullscreen-z-index) !important; background-color: var(--element-fullscreen-background-color); } `.trim() styleElement.innerHTML = cssContent // 避免重复添加 style 标签 if (!isAppend) { document.head.appendChild(styleElement) } } const enter = () => { const element = unrefElement(target) as HTMLElement | null beforeEnter?.() if (element) { if (!element.getAttribute(ID_TAG)) { element.setAttribute(ID_TAG, ID_TAG) } if (!isSetup) { isSetup = true currentZIndex += 1 } if (!isAppend) { updateStyle() isAppend = true } element.style.transition = transition isFullscreen.value = true _enter?.() } } const exit = () => { beforeExit?.() const element = unrefElement(target) if (element) { element.removeAttribute(ID_TAG) } isFullscreen.value = false _exit?.() } const toggleFullscreen = () => { const element = unrefElement(target) if (element) { if (element.getAttribute(ID_TAG)) { exit() } else { enter() } } } effectDispose(() => { const element = unrefElement(target) as HTMLElement | null if (element) { element.removeAttribute(ID_TAG) } // 回滚 z-index 值,避免无限增加 currentZIndex = Math.max(999, currentZIndex - 1) // 防止 zIndex 小于初始值 isFullscreen.value = false }) return { enter, exit, toggleFullscreen, isFullscreen: readonly(isFullscreen), // 暴露只读状态 } } export type UseElementFullscreenReturnTypes = ReturnType< typeof useElementFullscreen >