前言

在日常开发过程中,有时分咱们会希望全屏一个元素。可是,仅仅是希望它是处于网页全屏状况,而不是浏览器全屏状况。这时分,咱们能够统一封装一个 useElementFullscreen 办法,来完成这个功用。

办法剖析

在完成这个功用之前,咱们先剖析一下咱们需求这个办法做什么?

功用需求剖析

希望传递一个元素,然后这个元素就能够全屏显现。而且仅仅是运用 js + css 完成。

办法设计

  • 参数一:元素
  • 参数二:办法配置项

完成前需求考虑的问题

在完成这个功用之前,咱们需求考虑一下,咱们需求处理哪些问题?

怎么全屏一个元素

咱们剖析一下,怎么运用 css + js 全屏一个元素?

肯定是先获取到元素,然后将其宽高铺满整个屏幕,然后将其 position 设置为 fixed,然后将其 topleft 设置为 0,这样就能够完成全屏了。

怎么退出全屏

将所有的款式还原即可。

款式注入与特点注入或许产生的问题

由所以手动经过 css + js 的办法,使一个元素完成铺满效果,所以,咱们需求考虑一下,如果该元素本身包括一些基础款式,那么咱们的款式注入或许会掩盖掉这些款式,导致元素款式错乱。

怎么处理这个问题是一个需求考虑的问题。咱们后续会在完成的时分,考虑这个问题。

完成

辅佐函数

import type { ComponentPublicInstance } from 'vue'
export type TargetValue<T> = T | undefined | null
export type TargetType =
  | HTMLElement
  | Element
  | SVGElement
  | Window
  | Document
  | ComponentPublicInstance
export type BasicTarget<T extends TargetType = Element> =
  | (() => TargetValue<T>)
  | TargetValue<T>
  | Ref<TargetValue<T>>
/**
 *
 * @param target 获取 ref dom, vue instance 的 dom
 * @param defaultTarget 默认值
 *
 * @example
 * <template>
 *  <div ref="refDom"></div>
 * </template>
 *
 * const refDom = ref<HTMLElement | null>(null)
 * const computedDom = computed(() => refDom.value)
 *
 * unrefElement(refDom) => div
 * unrefElement(computedDom) => div
 */
function unrefElement<T extends TargetType>(
  target: BasicTarget<T>,
  defaultElement?: T,
) {
  if (!target) {
    return defaultElement
  }
  let targetElement: TargetValue<T>
  if (typeof target === 'function') {
    targetElement = target()
  } else if (isRef(target)) {
    targetElement =
      (target.value as ComponentPublicInstance)?.$el ?? target.value
  } else {
    targetElement = target
  }
  return targetElement
}
/**
 *
 * @param fc effect 效果域卸载时需执行函数
 *
 * @remark 返回 true 表明获取到 effect 效果域而且卸载;false 表明未存在 effect 效果域
 */
export function effectDispose<T extends (...args: any[]) => any>(fc: T) {
  if (getCurrentScope()) {
    onScopeDispose(fc)
    return true
  }
  return false
}

正式完成

import { isUndefined, isNull } from 'lodash-es'
import { useWindowSize } from '@vueuse/core'
export interface UseElementFullscreenOptions {
  beforeEnter?: () => void
  beforeExit?: () => void
  zIndex?: number
  backgroundColor?: string
}
let currentZIndex = 999
let isAppend = false
const ID_TAG = 'ELEMENT-FULLSCREEN-RAY'
const { height } = useWindowSize() // 获取实践高度防止 100vh 会导致手机端浏览器获取不精确问题
const styleElement = document.createElement('style')
export const useElementFullscreen = (
  target: BasicTarget,
  options?: UseElementFullscreenOptions,
) => {
  const { beforeEnter, beforeExit, backgroundColor, zIndex } = options ?? {}
  const cacheStyle: Partial<CSSStyleDeclaration> = {} // 缓存一些需求被掩盖的款式,例如: transition
  let isSetup = false
  const updateStyle = () => {
    const element = unrefElement(target) as HTMLElement | null
    if (!element) {
      return
    }
    const { left, top } = element.getBoundingClientRect()
    const cssContent = `
          #${ID_TAG} {
            position: fixed;
            width: 100% !important;
            height: ${height.value}px !important;
            transform: translate(-${left}px, -${top}px) !important;
            transition: all 0.3s var(--r-bezier);
            z-index: ${
              isValueType<null>(zIndex, 'Null') ||
              isValueType<undefined>(zIndex, 'Undefined')
                ? currentZIndex
                : zIndex
            } !important;
            background-color: ${backgroundColor ?? ''};
          }
        `
    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
      }
      cacheStyle.transition = element.style.transition
      element.style.transition = 'all 0.3s var(--r-bezier)'
      element.setAttribute('id', ID_TAG)
    }
  }
  const exit = () => {
    beforeExit?.()
    const element = unrefElement(target)
    if (element) {
      element.removeAttribute('id')
      element.removeAttribute(ID_TAG)
    }
  }
  const toggleFullscreen = () => {
    const element = unrefElement(target)
    if (element) {
      if (element.getAttribute(ID_TAG)) {
        exit()
      } else {
        enter()
      }
    }
  }
  const stopWatch = watch(() => height.value, updateStyle)
  effectDispose(() => {
    const element = unrefElement(target) as HTMLElement | null
    if (element) {
      element.style.transition = cacheStyle.transition ?? ''
      element.removeAttribute(ID_TAG)
      element.removeAttribute('id')
    }
    stopWatch()
  })
  return {
    enter,
    exit,
    toggleFullscreen,
  }
}

代码解说

怎么处理款式注入与特点注入或许产生的问题

在完成的时分注入了一个 style 标签,这个标签的效果是用来掩盖元素的款式,使其全屏。

而且关于传递元素注入了 id, ID_TAG 特点。

currentZIndex

这个变量的效果是用来记录当前全屏元素的 z-index 值,每次全屏一个元素,都会自增 1

isAppend

这个变量的效果是用来记录 style 标签是否已经被添加到 head 中。

isSetup

标记是否是初始化状况,如果是初始化状况,那么 currentZIndex 就会自增 1

运用演示

<template>
  <div ref="demoRef">
    <div @click="enter">全屏</div>
    <div @click="exit">退出全屏</div>
    <div @click="toggleFullscreen">切换全屏</div>
  </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const { enter, exit, toggleFullscreen } = useElementFullscreen(demoRef)
</script>

最终

其实该办法还是有一些问题没有处理:

  • 会注入 id,导致掩盖原有的 id,或许会导致一些问题
  • 全局注入了一个 style 标签,或许会导致一些问题
  • 办法侵略性很强,而且需求开发者自己考虑 dom 元素结构。否则在调用的时分会出现因为款式掩盖导致款式错乱的问题

而且,这个办法通用性没有那么强。适合在项目中提前约定好某些功用运用,而且在运用的时分,需求开发者自己已经考虑了一些问题。

如果大家有更好的思路,请多多指教~