手动封装一个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 元素结构。否则在调用的时候会出现因为样式覆盖导致样式错乱的问题

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

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

相关推荐
罗_三金8 分钟前
前端框架对比和选择?
javascript·前端框架·vue·react·angular
Redstone Monstrosity15 分钟前
字节二面
前端·面试
东方翱翔22 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
冯宝宝^1 小时前
基于mongodb+flask(Python)+vue的实验室器材管理系统
vue.js·python·flask
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby