React 模态框的设计(六)Draggable的整合

2024-03-01 15:52

本文主要是介绍React 模态框的设计(六)Draggable的整合,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前一节课中漏了一个知识点,当内容很长时需要滚动,这个滚动条是很影响美观的。在MacOS下的还能忍,win系统下简直不能看。如何让长内容能滚动又不显示滚动条呢,我尝试过很多办法,最终下面这个方法目前来说是最完美的。我们创建一个css文件。

_ModelContent.css

/** 本样式表用于隐藏滚动条但保留滚动功能*//* 隐藏 Chrome、Safari 和 Opera 的滚动条 */
.noscrollbar::-webkit-scrollbar {display: none;
}/* 为 IE、Edge 和 Firefox 隐藏滚动条 */
.noscrollbar {-ms-overflow-style: none;/* IE 和 Edge */scrollbar-width: none;/* Firefox */
}

把它引入 到 ModelContent组件中就好了。目前我测试了Edge、Safari、Chrome三款浏览器,效果不错。其它的没有测试,不知道什么效果,欢迎大家告诉我。

再次升级Draggable组件

关于前面我已经讲过Draggable组件,想让一个组件移动起来不难,想要在弹窗中多状态下的移动有点难度。

动态获取视口的大小参数

_useWindowSize.jsx

import { useState, useEffect } from 'react';/*** 动态获取窗口的宽高* @returns */
export const useWindowSize = () => {const [windowSize, setWindowSize] = useState({width: window.innerWidth,height: window.innerHeight,});useEffect(() => {const updateSize = () => setWindowSize({width: window.innerWidth,height: window.innerHeight,});window.addEventListener('resize', updateSize);return () => window.removeEventListener('resize', updateSize);}, []);return windowSize;
}

当调整浏览器的大小时,我们要实时动态的获取视口的大小,以使我们的弹窗及时做出响应。

弹窗弹出时的主体动画

//弹窗的动画
const attentionKeyframes = keyframes`from,to {transform: scale(1);}50% {transform: scale(1.03);}
`;//弹窗的开始时动画
const anim = css`animation: ${attentionKeyframes} 400ms ease;
`;//弹窗的结束时动画
const stopAnim = css`animation: null;
`;

设置加载后运行动画,

// 弹窗注目动画的监听useEffect(function () {// 弹窗动画监听事件const listener = (e) => {if (e.type === "animationend") {setAttentionStyle(stopAnim);}};if (wrapperRef.current !== null) {wrapperRef.current.addEventListener("animationend", listener, true);}return () => {if (wrapperRef.current !== null) {wrapperRef.current.removeEventListener("animationend", listener);}};}, []);

只运行一次,所以useEffect中没有依赖。

如果transform动画有多个属性动画,而主体的位置又是发生变化的,那么这个属性一定要分割开分别进行动画,原为transform动画是针对原始位置的动画,当主体位移后,动画还在原来的位置动画,这就很尴尬了。所以我们要调整

...
return (<Boxref={wrapperRef}sx={{transform: `translate(${position.x}px, ${position.y}px)`,cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",transition: isDragging ? null : `transform 200ms ease-in-out`,}}onMouseDown={handleMouseDown}onMouseMove={onMouseMove}onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}><Boxsx={{transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,transition: `transform 200ms ease-in-out`,}}css={attentionStyle}>{children}</Box></Box>);

上面我们做了两层嵌套,外面一层执行位置动画,里面一层执行缩放动画。因为这一层相对于外层的位置始终不变。外面带着内层移动了,但它相对于外层而言位置没有发生变化。

移动

移动的原理很简单,移动的偏移量 = 鼠标当前的位置 - 上次的偏移量后的位置(初始为0);最小化、最大化、正常模式三个状态下的移动量都是分别保存的,当弹窗处于某一种状态下时就把它的位置信息更新到 position中以实现更新UI。

const normalPos = useRef({ x: 0, y: 0 }); // 正常模式下弹窗的位置(translate的值)
const minPos = useRef({ x: 0, y: 0 }); // 最小化时的位置
const maxPos = { x: 0, y: 0 }; // 最大化时的位置,因为最大化时弹窗的位置是固定的,所以不需要ref// 当所有模式下的位置变化都是通过position来反映到UI上的,所以position是唯一的位置状态
const [position, setPosition] = useState({x: 0, y: 0}); // 弹窗的位置(translate的值)// 当鼠标按下时,记录鼠标的位置并以当前位置为基准进行拖动(相对位置),与position的差值为偏移量,position为上一次的偏移量。
// 因为采用的是translate的方式进行拖动,这种方式下,是以组件第一次渲染的位置为基准参考点(也就是相对0,0的位置)进行拖动的.
// 正常模式下的偏移量
const normalOffsetX = useRef(0); // x轴偏移量
const normalOffsetY = useRef(0); // y轴偏移量// 最小化时的偏移量
const minOffsetX = useRef(0); // x轴偏移量
const minOffsetY = useRef(0); // y轴偏移量const initedRect = useRef(0); // 初始化后的弹窗大小

偏移量的计算如下:

// 鼠标移动事件
const handleMouseMove = (e) => {if (isDragging) {switch (stateMode) {case 0:const xt = e.clientX - minOffsetX.current;const yt = e.clientY - minOffsetY.current;const xtMinTop = -((windowSize.height - minHeight) / 2 - 10);const xtMaxTop = (windowSize.height - minHeight) / 2 - 10;const xtMinLeft = -((windowSize.width - minWidth) / 2 - 10);const xtMaxLeft = (windowSize.width - minWidth) / 2 - 10;const xm = xt < xtMinLeft ? xtMinLeft : xt > xtMaxLeft ? xtMaxLeft : xt;const ym = yt < xtMinTop ? xtMinTop : yt > xtMaxTop ? xtMaxTop : yt;minPos.current = { x: xm, y: ym};setPosition({ ...minPos.current });break;case 2:break;default:const xTmp = e.clientX - normalOffsetX.current;const yTmp = e.clientY - normalOffsetY.current;const minLetf = -(windowSize.width - initedRect.current.width) / 2; const minTop = -(windowSize.height - initedRect.current.height) / 2;const maxLeft = (windowSize.width - initedRect.current.width) / 2;const maxTop = (windowSize.height - initedRect.current.height) / 2;const x = xTmp < minLetf ? minLetf : xTmp > maxLeft ? maxLeft : xTmp;const y = yTmp < minTop ? minTop : yTmp > maxTop ? maxTop : yTmp;normalPos.current = { x, y };setPosition({ ...normalPos.current });break;}}
};

状态0 为最小化,1 为正常模式、2为最大化模式,由于最大化下是固定的,所以不用复杂计算。

完整的代码如下:

_Draggable.jsx

/** @jsxImportSource @emotion/react */
import { css, keyframes } from '@emotion/react'
import React, { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import { useOutsideClick } from './_useOutsideClick';
import { useWindowSize } from './_useWindowSize';
import { minHeight, minWidth } from './_ModelConfigure';//弹窗的动画
const attentionKeyframes = keyframes`from,to {transform: scale(1);}50% {transform: scale(1.03);}
`;//弹窗的开始时动画
const anim = css`animation: ${attentionKeyframes} 400ms ease;
`;//弹窗的结束时动画
const stopAnim = css`animation: null;
`;const draggableHandler = ".model-handler"; // 拖动句柄的类名/*** 拖动组件,使被包裹的组件可以拖动,支持拖动句柄* @param {是否启用拖动句柄 } enableHandler * @param {拖动句柄的类名} draggableHandler* @param {外部点击事件} onOutsideClick*/
export default function Draggable({children, // 子组件enableDragging = true,enableHandler = false, // 是否启用拖动句柄stateMode
}) {const [attentionStyle, setAttentionStyle] = useState(anim); // 弹窗动画,当点击外部时,弹窗会有一个动画效果const [isDragging, setIsDragging] = useState(false); // 是否正在拖动const [canDrag, setCanDrag] = useState(true); // 是否可以触发拖动操作,改变鼠标样式const normalPos = useRef({ x: 0, y: 0 }); // 正常模式下弹窗的位置(translate的值)const minPos = useRef({ x: 0, y: 0 }); // 最小化时的位置const maxPos = { x: 0, y: 0 }; // 最大化时的位置,因为最大化时弹窗的位置是固定的,所以不需要ref// 当所有模式下的位置变化都是通过position来反映到UI上的,所以position是唯一的位置状态const [position, setPosition] = useState({x: 0, y: 0}); // 弹窗的位置(translate的值)// 当鼠标按下时,记录鼠标的位置并以当前位置为基准进行拖动(相对位置),与position的差值为偏移量,position为上一次的偏移量。// 因为采用的是translate的方式进行拖动,这种方式下,是以组件第一次渲染的位置为基准参考点(也就是相对0,0的位置)进行拖动的.// 正常模式下的偏移量const normalOffsetX = useRef(0); // x轴偏移量const normalOffsetY = useRef(0); // y轴偏移量// 最小化时的偏移量const minOffsetX = useRef(0); // x轴偏移量const minOffsetY = useRef(0); // y轴偏移量const initedRect = useRef(0); // 初始化后的弹窗大小const wrapperRef = useRef(null);const windowSize = useWindowSize();// 当点击外部时,弹窗会有一个注目动画效果useOutsideClick(wrapperRef, () => {setAttentionStyle(anim);});// 弹窗注目动画的监听useEffect(function () {// 弹窗动画监听事件const listener = (e) => {if (e.type === "animationend") {setAttentionStyle(stopAnim);}};if (wrapperRef.current !== null) {wrapperRef.current.addEventListener("animationend", listener, true);}return () => {if (wrapperRef.current !== null) {wrapperRef.current.removeEventListener("animationend", listener);}};}, []);// document的鼠标移动事件和鼠标抬起事件监听useEffect(() => {// 鼠标移动事件const handleMouseMove = (e) => {if (isDragging) {switch (stateMode) {case 0:const xt = e.clientX - minOffsetX.current;const yt = e.clientY - minOffsetY.current;const xtMinTop = -((windowSize.height - minHeight) / 2 - 10);const xtMaxTop = (windowSize.height - minHeight) / 2 - 10;const xtMinLeft = -((windowSize.width - minWidth) / 2 - 10);const xtMaxLeft = (windowSize.width - minWidth) / 2 - 10;const xm = xt < xtMinLeft ? xtMinLeft : xt > xtMaxLeft ? xtMaxLeft : xt;const ym = yt < xtMinTop ? xtMinTop : yt > xtMaxTop ? xtMaxTop : yt;minPos.current = { x: xm, y: ym};setPosition({ ...minPos.current });break;case 2:break;default:const xTmp = e.clientX - normalOffsetX.current;const yTmp = e.clientY - normalOffsetY.current;const minLetf = -(windowSize.width - initedRect.current.width) / 2; const minTop = -(windowSize.height - initedRect.current.height) / 2;const maxLeft = (windowSize.width - initedRect.current.width) / 2;const maxTop = (windowSize.height - initedRect.current.height) / 2;const x = xTmp < minLetf ? minLetf : xTmp > maxLeft ? maxLeft : xTmp;const y = yTmp < minTop ? minTop : yTmp > maxTop ? maxTop : yTmp;normalPos.current = { x, y };setPosition({ ...normalPos.current });break;}}};// 鼠标抬起事件const handleMouseUp = (e) => {if (e.button !== 0) return;setIsDragging(false);};// 在相关的事件委托到document上if (isDragging) {document.addEventListener('mousemove', handleMouseMove);document.addEventListener('mouseup', handleMouseUp);} else {document.removeEventListener('mousemove', handleMouseMove);document.removeEventListener('mouseup', handleMouseUp);}// 组件卸载时移除事件return () => {document.removeEventListener('mousemove', handleMouseMove);document.removeEventListener('mouseup', handleMouseUp);};}, [isDragging]);// 弹窗位置的监听, 每当弹窗状态改变时,都会重新设置弹窗的位置, 将相应状态下的最后位置设置为当前位置// 但最小化状态下的位置有所不同,因为最小化状态下的初始位置为左下角,每次从其它状态切换到最小化状态时都要进行相同的设置。useEffect(() => {switch (stateMode) {case 0:const initX = -((windowSize.width - minWidth - 20) / 2);const initY = windowSize.height / 2 - minHeight + 10;setPosition({ x: initX, y: initY });minPos.current = { x: initX, y: initY };break;case 2:setPosition({...maxPos.current});break;default:setPosition({ ...normalPos.current });break;}}, [stateMode]);// ref对象的鼠标移动事件,用于判断是否在拖动句柄上const onMouseMove = (e) => {if (!enableDragging) {setCanDrag(false);return;}if (enableHandler) {const clickedElement = e.target;// 检查鼠标点击的 DOM 元素是否包含特定类名if (clickedElement.classList.contains(draggableHandler)) {setCanDrag(true);} else {setCanDrag(false);}}}// ref对象的鼠标按下事件,用于触发拖动操作,// 如果启用了拖动句柄,那么只有在拖动句柄上按下鼠标才会触发拖动操作,// 否则直接按下鼠标就会触发拖动操作const handleMouseDown = (e) => {if (!enableDragging) return;switch (stateMode) {case 0:if (enableHandler) {// 判断是否在拖动句柄上const curElement = e.target;// 检查鼠标点击的 DOM 元素是否包含特定类名if (curElement.classList.contains(draggableHandler)) {if (e.button !== 0) return;setIsDragging(true);minOffsetX.current = e.clientX - minPos.current.x;minOffsetY.current = e.clientY - minPos.current.y;} else {setCanDrag(false);}} else {if (e.button !== 0) return;setIsDragging(true);minOffsetX.current = e.clientX - minPos.current.x;minOffsetY.current = e.clientY - minPos.current.y;}return;case 2:return; default:if (enableHandler) {// 判断是否在拖动句柄上const curElement = e.target;// 检查鼠标点击的 DOM 元素是否包含特定类名if (curElement.classList.contains(draggableHandler)) {if (e.button !== 0) return;setIsDragging(true);normalOffsetX.current = e.clientX - normalPos.current.x;normalOffsetY.current = e.clientY - normalPos.current.y;} else {setCanDrag(false);}} else {if (e.button !== 0) return;setIsDragging(true);normalOffsetX.current = e.clientX - normalPos.current.x;normalOffsetY.current = e.clientY - normalPos.current.y;}return;}};return (<Boxref={wrapperRef}sx={{transform: `translate(${position.x}px, ${position.y}px)`,cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",transition: isDragging ? null : `transform 200ms ease-in-out`,}}onMouseDown={handleMouseDown}onMouseMove={onMouseMove}onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}onPointerEnter={() => {if (initedRect.current === 0 && wrapperRef.current !== null) {const rect = wrapperRef.current.getBoundingClientRect();initedRect.current = {width: rect.width,height: rect.height,};}}}><Boxsx={{transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,transition: `transform 200ms ease-in-out`,}}css={attentionStyle}>{children}</Box></Box>);
}

上面我都做了说明,应该不难理解。这样我们组合后我们弹窗就可以移动了。最后的测试请关注下一篇文章。

这篇关于React 模态框的设计(六)Draggable的整合的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/762998

相关文章

SpringBoot整合OpenFeign的完整指南

《SpringBoot整合OpenFeign的完整指南》OpenFeign是由Netflix开发的一个声明式Web服务客户端,它使得编写HTTP客户端变得更加简单,本文为大家介绍了SpringBoot... 目录什么是OpenFeign环境准备创建 Spring Boot 项目添加依赖启用 OpenFeig

SpringBoot整合mybatisPlus实现批量插入并获取ID详解

《SpringBoot整合mybatisPlus实现批量插入并获取ID详解》这篇文章主要为大家详细介绍了SpringBoot如何整合mybatisPlus实现批量插入并获取ID,文中的示例代码讲解详细... 目录【1】saveBATch(一万条数据总耗时:2478ms)【2】集合方式foreach(一万条数

Spring Boot 整合 SSE的高级实践(Server-Sent Events)

《SpringBoot整合SSE的高级实践(Server-SentEvents)》SSE(Server-SentEvents)是一种基于HTTP协议的单向通信机制,允许服务器向浏览器持续发送实... 目录1、简述2、Spring Boot 中的SSE实现2.1 添加依赖2.2 实现后端接口2.3 配置超时时

HTML5中的Microdata与历史记录管理详解

《HTML5中的Microdata与历史记录管理详解》Microdata作为HTML5新增的一个特性,它允许开发者在HTML文档中添加更多的语义信息,以便于搜索引擎和浏览器更好地理解页面内容,本文将探... 目录html5中的Mijscrodata与历史记录管理背景简介html5中的Microdata使用M

html5的响应式布局的方法示例详解

《html5的响应式布局的方法示例详解》:本文主要介绍了HTML5中使用媒体查询和Flexbox进行响应式布局的方法,简要介绍了CSSGrid布局的基础知识和如何实现自动换行的网格布局,详细内容请阅读本文,希望能对你有所帮助... 一 使用媒体查询响应式布局        使用的参数@media这是常用的

HTML5表格语法格式详解

《HTML5表格语法格式详解》在HTML语法中,表格主要通过table、tr和td3个标签构成,本文通过实例代码讲解HTML5表格语法格式,感兴趣的朋友一起看看吧... 目录一、表格1.表格语法格式2.表格属性 3.例子二、不规则表格1.跨行2.跨列3.例子一、表格在html语法中,表格主要通过< tab

Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案

《Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案》:本文主要介绍Vue3组件中getCurrentInstance()获取App实例,但是返回nu... 目录vue3组件中getCurrentInstajavascriptnce()获取App实例,但是返回n

springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法

《springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法》:本文主要介绍springboot整合阿里云百炼DeepSeek实现sse流式打印,本文给大家介绍的非常详细,对大... 目录1.开通阿里云百炼,获取到key2.新建SpringBoot项目3.工具类4.启动类5.测试类6.测

JS+HTML实现在线图片水印添加工具

《JS+HTML实现在线图片水印添加工具》在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题,本文将实现一个完全基于HTML+CSS构建的现代化图片水印在线工具... 目录概述功能亮点使用方法技术解析延伸思考运行效果项目源码下载总结概述在社交媒体和内容创作日益频繁的

前端CSS Grid 布局示例详解

《前端CSSGrid布局示例详解》CSSGrid是一种二维布局系统,可以同时控制行和列,相比Flex(一维布局),更适合用在整体页面布局或复杂模块结构中,:本文主要介绍前端CSSGri... 目录css Grid 布局详解(通俗易懂版)一、概述二、基础概念三、创建 Grid 容器四、定义网格行和列五、设置行