手动封装一个useElementFullscreen方法实现网页全屏效果

前言

在日常开发过程中,有时候我们会希望全屏一个元素。但是,仅仅是期望它是处于网页全屏状态,而不是浏览器全屏状态。这时候,我们可以统一封装一个 useElementFullscreen 方法,来实现这个功能。

方法分析

在实现这个功能之前,我们先分析一下我们需要这个方法做什么?

功能需求分析

期望传递一个元素,然后这个元素就可以全屏显示。并且仅仅是使用 js + css 实现。

方法设计

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

实现前需要考虑的问题

在实现这个功能之前,我们需要考虑一下,我们需要解决哪些问题?

如何全屏一个元素

我们分析一下,如何使用 css + js 全屏一个元素?

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

如何退出全屏

将所有的样式还原即可。

样式注入与属性注入可能产生的问题

由于是手动通过 css + js 的方式,使一个元素实现铺满效果,所以,我们需要考虑一下,如果该元素本身包含一些基础样式,那么我们的样式注入可能会覆盖掉这些样式,导致元素样式错乱。

如何解决这个问题是一个需要考虑的问题。我们后续会在实现的时候,考虑这个问题。

实现

辅助函数

ts 复制代码
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
}

正式实现

ts 复制代码
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

使用演示

vue 复制代码
<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 元素结构。否则在调用的时候会出现因为样式覆盖导致样式错乱的问题

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

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

相关推荐
ekskef_sef14 分钟前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine64138 分钟前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云1 小时前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr1 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
程序员_三木1 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网2 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工2 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染