整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Vue中轻松使模态框支持类窗口操作的增强组件

Vue中轻松使模态框支持类窗口操作的增强组件

今天要介绍的是一个Vue的增强组件——vue-directive-window,它让你的模态框轻而易举地支持类窗口操作,也就是说不仅仅对话框,还将具备一些窗口化的特性,诸如拖拽、最大化、缩放等增强型功能!




Github

https://gitee.com/mirrors/vue-directive-window

特性

  • 节约成本

旨在以极少的改造成本,让一个现有的模态框或任何合适的HTMLElement轻松支持拖拽移动、调整大小、最大化等类视窗操作。

  • 使用便捷

同时提供Vue自定义指令以及一般js类库两种调用方式。

  • 可插拔

可以随时为某个系统里已存在的模块添上/去除类视窗操作的功能,而不会影响该模块原有的功能。

快速开始

  • 安装使用npm

也可以使用其它包管理工具,如yarn

npm install vue-directive-window

vue-directive-window支持Vue自定义指令及一般js类两种方式来使用。

  • 自定义指令方式
<template>
  <div v-window="windowParams">
    <!-- 容器内容 -->
  </div>
</template>
<script>
import VueDirectiveWindow from 'vue-directive-window';
Vue.use(VueDirectiveWindow); // 如果是以静态文件方式引入的话,则不需要 import,直接使用Vue.use(window['vue-directive-window'])
export default {
  data() {
    return {
      windowParams: {
        movable: false,
        resizable: ['left', 'left-top'],
      },
    };
  },
}
</script
  • 一般使用
<div class="demo-window" v-window="windowParams">
  <!-- 容器内容 -->
</div>
import { enhanceWindow } from 'vue-directive-window'; // 如果是以静态文件方式引入的话,则是const enhanceWindow=window['vue-directive-window'].enhanceWindow;

const windowParams={
  movable: false
  resizable: ['left', 'left-top']
};

enhanceWindow(document.querySelector('.demo-window'), windowParams);

vue-directive-window支持IE10以及以后的浏览器

案例

本文只展示官方的一个案例,其它案例参考官方文档

<template>
  <div class="container">
    <div class="window window1" v-show="ifShowWindow" v-window="windowParams">
      <div class="window__header">
        一般窗口
        <button class="maximize-btn" type="button">
          <template v-if="!isMaximize"
            >点这放大</template
          >
          <template v-else
            >点这缩小</template
          >
        </button>
      </div>
      <div class="window__body">
        <iframe height="100%" width="100%" frameborder="0" src="https://array-huang.github.io/vue-directive-window/">
      </div>
    </div>

    <button type="button" @click="ifShowWindow=true" v-if="!ifShowWindow">
      显示窗口
    </button>
    <button type="button" @click="ifShowWindow=false" v-else>隐藏窗口</button>
  </div>
</template>
<script>
  Vue.use(window['vue-directive-window']);

  function maximizeCb(isMaximize) {
    this.isMaximize=isMaximize;
  }

  export default {
    data() {
      return {
        windowParams: {
          minWidth: 10,
          maxWidth: 800,
          minHeight: 100,
          maxHeight: 800,
          customMaximizeHandler: '.maximize-btn',
          maximizeCallback: maximizeCb.bind(this),
        },
        ifShowWindow: false,
        isMaximize: false,
      };
    },
  };
</script>
<style>
  .container {
    padding: 30px;
  }
  .window1 {
    width: 400px;
    position: fixed;
    top: 60px;
    left: 0;
  }
</style>

效果如下

前的文章讲了可视窗口可改变位置大小(查看),本文介绍配合react-draggable来实现既可改变大小又可移动位置的模态窗口实现。

实现后效果:

<script src="https://lf3-cdn-tos.bytescm.com/obj/cdn-static-resource/tt_player/tt.player.js?v=20160723"></script>

该实现过程旨在它拖拽改变大小和改变位置抽象出来,所以具体的布局由调用方来处理,后面会有使用示例。

可拖拽窗口(DragableWindow)

import React, {createRef, useEffect,useState } from 'react'
import Drag,{ControlPosition } from 'react-draggable';
import {Resizable} from 're-resizable';
import {resizeOnVertical} from './utils'
import useStyle from './style'
import {calculatePositionAlwaysShowInView} from './utils'
interface Point extends ControlPosition{
}
interface DragabeWindowProps{
    width:number;
    height:number;
    children?:any;
    dragContorlClassName?:string;
}
const DragableWindow : React.FC<DragabeWindowProps>=(props)=>{
    const CONTAINER_MIN_HEIGHT=504;
    const {children}=props;
    const [resizePosition,setResizePosition]=useState<Point>({x:0,y:0});
    const [position,setPosition]=useState<Point|null>(); 
    const [containerHeight,setContainerHeight]=useState<number>(CONTAINER_MIN_HEIGHT);
     /**
	 * @param e {object} 事件源
	 * @param direction {string} 拖动方向
	 * @param ref {dom} 拖动的元素
	 * @param d {object} 移动偏移量
	 */
	const onResize=(e:any, direction:any, ref:any, d:any)=> {
		/* resize 之前的值 */
		let originX=resizePosition?resizePosition.x:0;
		let originY=resizePosition?resizePosition.y:0;

		/* 移动的位移 */
		let moveW=d.width;
		let moveH=d.height;

		/* 移动的位移 */
		let x=null;
		let y=null;

		/* 处理上边缘 */
		if (/left/i.test(direction)) {
			x=originX - moveW;
			y=originY;
			setPosition({ x, y });

			/* 处理左边缘 */
		} else if (/top/i.test(direction)) {
			x=originX;
			y=originY - moveH;
			setPosition({ x, y });
		} else {
			setPosition(null);
		}

		if (x || y) {
			ref.style.transform=`translate(${x}px, ${y}px)`;
		}
	}

    const onResizeStop=(e:any, direction:any, ref:any, d:any)=> {
		if (position) {
			setResizePosition(position);
		}
        if (resizeOnVertical(direction)) {
            //setContainerHeight(containerHeight + d.height);
		}
	}
    /**弹出窗口的蒙层样式 */
    const popContainer: React.CSSProperties={
        top: 0,
        left: 0,
        overflow: 'hidden',
        position: 'fixed',
        height: '100%',
        width: '100%',
        alignItems:'center',
        flexDirection: 'column',
        display:'flex',
        zIndex: 301,
        backgroundColor:'rgba(0, 0, 0, 0.5)',
    }
    const {width,height}=props;
    const {dragContorlClassName}=props;
    const [dragStartPosition,setDragStartPosition]=useState<ControlPosition | null>();
    useEffect(()=>{
        /*初始容器(可拖拽组件对于的dom元素)的高度 */ 
        if(height > CONTAINER_MIN_HEIGHT){
            setContainerHeight(height);
        }
    },[height]);
    const onDragStart=()=> {
		dragStartPosition !==null && setDragStartPosition(null);
	}

	const onDragStop=(e:any, draggableData:any)=> {
		calculatePositionAlwaysShowInView(e, draggableData,(x,y)=>{
            setDragStartPosition({x,y});
        });
	};
    let popoverRef=createRef<HTMLDivElement>();
    const {styles,cx}=useStyle();
    return (
        <>
            {/**弹出窗口的蒙层 */}
            <div style={popContainer}>
                {/** handler属性指定拖拽移动生效的样式类,因弹出窗口还可弹出窗口,其应该具有唯一性,
                 * 这个设计旨把拖拽移动和拖拽改变大小抽象出来,通常拖拽部分由调用方指定,所以这个样式
                 * 类名是一个组件属性由外部传入 */}  
                <Drag 
                    handle={dragContorlClassName}
                    defaultClassName="js-drag-wrapped"
                    position={dragStartPosition as Point}
                    onStart={onDragStart}
                    onStop={onDragStop}
                    >
                    {/** 下面的div作为拖拽区域的外部容器 */}             
                    <div style={{width:width}}>
                        <Resizable 
                            style={{}} 
                            onResize={onResize}
                            onResizeStop={onResizeStop}
                            minWidth={width}
                            defaultSize={{width: width,height:containerHeight}}
                            className={cx("js-resize-container popover-shadow",styles.resizeContainerShadow)}
                            >
                            {/** 下面的div用作Window的可见样式,例如圆角 
                             * 拖拽区域使用tansform圆角不起作用,背景色要透明
                             * 大小可变区域tansform圆角不起作用,背景色要透明
                             * 在可变区域增加div,设置圆角样式,child有颜色,需要设置overflow:hidden
                             */} 
                            <div className={cx(styles.winPopover,styles.clearFix)}
                                ref={popoverRef}
                                onClick={e=> {
                                    e.stopPropagation();
                                }}
                                onDoubleClick={e=> {
                                    e.stopPropagation();
                                }}
                            >
                                {/** 承载外部设计组件
                                 * 该实例只是展示了拖拽改变位置和大小,实际对外接口并不完善,需要根据自己的需求进行扩展
                                 * 原则:外部组件不能直接改变一个组件的状态,所以组件内公开能做什么事,由外部组件来通知,
                                 * 内部实际做具体的动作。
                                 */}
                                {children}
                            </div>
                        </Resizable>
                    </div>
                </Drag>
            </div>
        </>
    );
}

export default DragableWindow;

代码重点,请参照代码中的注释,这样描述更适合程序员来阅读。

这里强调的一下,标签布局的层次,否则读起来感觉一头雾水。如下图


布局容器层次

组件引用样例

模拟了一个参照弹出窗口样例(ReferWindow),见前面的视频。样例使用的是antd组件进行布局演示

import React,{Component} from 'react'
import DragableWindow from './DragableWindow'
import {Button} from 'antd'
import {Flex,Layout} from 'antd'
import {CloseOutlined,WindowsOutlined} from '@ant-design/icons'
import {getUniqueString} from './utils'
const { Header, Sider,Footer }=Layout;

interface ReferWindowProps {
    refType:string;
    container:Element|DocumentFragment;
    title?:string;
    headIcon?:React.ReactNode;
    headContents?:[React.ReactNode];
    headComps?:[];
    visible?:boolean;
    children?:any;
    closeListener?:(v:boolean)=> void;
}
interface ReferWindowInnerProps {
    visible?:boolean;
    resizeHeight?:number;
    max:boolean;
    activeKey:number;
    isExpandLeftArea:boolean;
    isDragging:boolean;
}

class ReferWindow extends Component<ReferWindowProps,ReferWindowInnerProps>{
    constructor(props:ReferWindowProps){
        super(props);
        this.state={
            visible:props.visible===undefined?true:props.visible,
            max: false,
            activeKey:0,
            isExpandLeftArea:false,
            isDragging:false
        };
        this.closeWindow.bind(this);
    }

    closeWindow=()=>{
        const {closeListener}=this.props;
        closeListener&&closeListener(false);
    }
  
    render(): React.ReactNode {
        const {title,headIcon}=this.props;
        let winIcon=headIcon?headIcon:<WindowsOutlined/>;
        let {visible}=this.props;
        let uniqueString=getUniqueString();
        return (
           visible&&(
                <DragableWindow height={360} width={600} dragContorlClassName={`.drag-header-${uniqueString}`}>
                    <Layout style={{height:'100%'}}>
                        <Header className={`drag-header-${uniqueString}`} style={{ cursor:'move', display: 'flex', alignItems: 'center',backgroundColor:'#f1f1f1', height:'50px', lineHeight:'50px',padding:'0px 15px' }}>
                            <Flex justify="space-between" align={'center'} style={{width:'100%',height:'100%'}}  vertical={false}>
                                <Flex>{winIcon}<span style={{padding:'5px'}}>{title??title}</span></Flex>
                                <Flex align={'flex-end'}><CloseOutlined onClick={this.closeWindow} style={{cursor:'pointer'}}/></Flex>
                            </Flex>
                        </Header>
                        <Layout style={{}}>
                            <Sider width={200}>
                            
                            </Sider>
                        </Layout>
                        <Footer style={{padding:'10px 10px'}}>
                            <Flex justify={'flex-end'}>
                                <span><Button type='primary'>确定</Button></span>
                                <span style={{padding:'0px 5px'}}><Button  onClick={this.closeWindow}>取消</Button></span>
                            </Flex>
                        </Footer>
                    </Layout>
                </DragableWindow>
            ))
    };
}

export default ReferWindow;

参照

import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import {Input} from 'antd';
import { BarsOutlined } from '@ant-design/icons';
import ReferWindow from './referWindow'

interface BaseReferProps {
    refType:string;
    container:Element|DocumentFragment;
    title?:string;
}
interface BaseReferInnerProps {
    popoverPanelVisible: boolean;
}
class BaseRefer extends Component<BaseReferProps,BaseReferInnerProps> {
    constructor(props:BaseReferProps){
        super(props);
        this.state={
            popoverPanelVisible:false,
        };
        this.showPopoverPanel.bind(this)
        this.onReferWindowCloseClick.bind(this);
    }
    showPopoverPanel=()=> {
        this.setState({popoverPanelVisible:true});
    }

    onReferWindowCloseClick=()=>{
        this.setState({popoverPanelVisible:false});
    }

    render(): React.ReactNode {
        const {container,refType,title}=this.props;
        let { popoverPanelVisible }=this.state;
        return <>
            <Input addonAfter={<BarsOutlined style={{cursor:'pointer'}} onClick={this.showPopoverPanel.bind(this)}/>}></Input>
            {ReactDOM.createPortal(
                <ReferWindow 
                visible={popoverPanelVisible} 
                closeListener={this.onReferWindowCloseClick}
                title={title} 
                refType={refType} 
                container={container}>
                </ReferWindow>,
            container
            )}  
        </>
    }
}

export default BaseRefer;

参照使用

import React from 'react';
import BaseRefer from './components/container/Refer/baseRefer'
import {Row,Col} from 'antd'
import './App.css';
const App : React.FC=()=>{
  return (
    
      <Row>
        <Col span={6}><BaseRefer refType='treeGrid' title='门店商品' container={document.body}></BaseRefer></Col>
        <Col span={18}></Col>
      </Row>
    
  )
}

export default App;

效果图

有些知识点,比如说Web Components, 自己平时根本用不到,如果不刻意学习和了解,自己的知识体系就会产生盲区,可是每个人的知识盲区那么多,扫的过来嘛。对于这一点,我们要抱有积极的心态,少除一个就少一个。可是要扫除的技术盲区那么多,为什么要优先选择扫除它?这一点看个人偏好,没有标准答案。但有一个大方向是确定的,如果想在技术的道路上走得更远,就得不断清除阻碍自己前行的障碍拓宽自己的技术视野。废话不多说了,现在我们进入今天的主题。

Web Components简介

Web Components 是一组标准,用于创建可重用的、封装良好的自定义元素,能够与标准的 HTML 元素无缝集成。Web Components 使开发者能够定义自己的 HTML 标签,这些标签具有独立的样式和行为,从而增强了组件的可复用性和模块化程度。Web Components 由以下三项技术组成:

Custom Elements(自定义元素)

  • 允许开发者定义自己的 HTML 元素,并赋予这些元素自定义的行为。
  • 通过 customElements.define 方法注册自定义元素。
  • 自定义元素具有生命周期回调方法,例如 connectedCallback(挂载)、disconnectedCallback(卸载)、attributeChangedCallback(属性改变) 等。

Shadow DOM(影子 DOM)

  • 提供了一种封装组件内部 DOM 和样式的方法,使其与外部 DOM 和样式隔离。
  • 使用 attachShadow 方法创建一个影子 DOM 根节点。
  • 影子 DOM 内部的样式和结构不会影响外部的 DOM,反之亦然。

HTML Templates(HTML 模板)

  • 提供了一种定义可重用 HTML 结构的方法,这些结构在页面加载时不会立即呈现。
  • 使用 <template> 标签定义模板内容。
  • 模板内容在通过 JavaScript 克隆并插入到 DOM 中。

Web Components使用场景

Web Components技术是一组让开发者能够创建可重用的、封装良好的自定义HTML元素的标准。使开发者能够创建高度复用、独立、封装良好的组件,从而提高开发效率和代码质量。下面是一些典型的场景:

  1. 设计系统和组件库

许多公司和团队使用Web Components来构建设计系统和组件库。这些系统和库允许在不同项目中复用一致的UI组件,从而保持设计的一致性和开发的高效性。如Salesforce的Lightning Web Components、Ionic Framework中的Stencil。

  1. 跨框架组件共享

Web Components可以在不同的前端框架(如React、Angular、Vue)中无缝使用。这使得开发者能够创建独立于框架的组件,从而提高组件的复用性。在一个项目中使用React构建大部分页面,同时使用Web Components构建特定的独立组件,比如日期选择器或地图。

  1. 微前端架构

在微前端架构中,不同团队可以独立开发、部署和维护前端应用的不同部分。Web Components使得这些独立的部分可以以组件的形式集成到一个整体的应用中,而不会互相干扰。如一个电商网站的不同模块(如购物车、支付、用户评论)由不同团队开发,并以Web Components的形式集成。

  1. Web Widgets和插件

Web Components非常适合构建可以嵌入到任意网页中的小部件和插件,比如聊天窗口、表单验证、广告模块等。这些小部件通常需要高度的封装性和独立性,以避免与宿主页面的冲突。如第三方客服聊天窗口、嵌入式视频播放器。

  1. 数据可视化

创建数据可视化组件,如图表、地图、数据表等,这些组件可以独立于具体的应用环境,在不同的项目中重用。如使用D3.js或其他图表库创建的自定义元素,用于显示动态数据图表。

  1. 定制表单控件

构建复杂且可复用的表单控件,如日期选择器、颜色选择器、富文本编辑器等。这些控件可以被封装为自定义元素,便于在不同表单中复用。如一个自定义的富文本编辑器,可以用于博客系统、内容管理系统等多个场景。

  1. Web应用的组件化开发

在开发大型Web应用时,使用Web Components可以实现组件化开发,使得应用结构更清晰,组件更易于测试和维护。如在线文档编辑器中的各种工具栏和编辑器组件,每个都封装为独立的Web Component。

Web Components开发组件示例

模态对话框是Web应用中常见的UI组件,可以用于显示重要的消息、表单或确认对话框。我们通过用web components技术创建一个自定义的模态对话框组件,演示一下web components的使用方法。

1. 定义模板和自定义元素

将模板和自定义元素的定义放在一个JavaScript文件 my-modal.js中。

  • 创建了一个模板 template,其中包含了模态对话框的结构和样式。 注意模板有三个插槽,可以让我们自定义模态框的标题,内容和底部区域。
  • 定义了一个 MyModal 类,继承自 HTMLElement。
  • 在 constructor 中使用 Shadow DOM 绑定模板内容。
  • 实现了打开和关闭模态对话框的方法,以及处理相关的事件。
// my-modal.js
const template=document.createElement('template');
template.innerHTML=`
  <style>
    :host {
      display: block;
    }
    .modal {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      justify-content: center;
      align-items: center;
    }
    .modal.open {
      display: flex;
    }
    .modal-content {
      background: white;
      padding: 20px;
      border-radius: 5px;
      max-width: 500px;
      width: 100%;
    }
    .modal-header,
    .modal-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .modal-footer {
      margin-top: 20px;
    }
    .close-button {
      cursor: pointer;
    }
  </style>
  <div class="modal">
    <div class="modal-content">
      <div class="modal-header">
        <slot name="header">头部</slot>
        <span class="close-button">X</span>
      </div>
      <div class="modal-body">
        <slot name="body">内容区域</slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button id="close-button">关闭</button>
        </slot>
      </div>
    </div>
  </div>
`;

class MyModal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    this.modal=this.shadowRoot.querySelector('.modal');
    this.closeButton=this.shadowRoot.querySelector('.close-button');
    this.footerCloseButton=this.shadowRoot.querySelector('#close-button');

    this.close=this.close.bind(this);
  }

  connectedCallback() {
    if (this.closeButton) {
      this.closeButton.addEventListener('click', this.close);
    }
    if (this.footerCloseButton) {
      this.footerCloseButton.addEventListener('click', this.close);
    }
  }

  disconnectedCallback() {
    if (this.closeButton) {
      this.closeButton.removeEventListener('click', this.close);
    }
    if (this.footerCloseButton) {
      this.footerCloseButton.removeEventListener('click', this.close);
    }
  }

  open() {
    this.modal.classList.add('open');
  }

  close() {
    this.modal.classList.remove('open');
  }
}

customElements.define('my-modal', MyModal);

2. 在 index.html 中引入模板和自定义元素

在HTML文件中通过 <script> 标签引入上述JavaScript文件,并使用自定义的模态对话框组件的插槽功能定制内容。

  • 使用 <script src="my-modal.js" defer></script> 引入自定义元素的定义文件。
  • 使用 <my-modal> 自定义元素,并通过 slot 插槽填充自定义的内容。
  • 使用JavaScript控制按钮的点击事件,调用自定义元素的方法来打开和关闭模态对话框。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Modal Component Example</title>
    <script src="my-modal.js" defer></script>
</head>
<body>
    <button id="open-modal-button">Open Modal</button>

    <my-modal id="my-modal">
        <span slot="header">自定义头部</span>
        <p slot="body">自定义内容区域</p>
        <div slot="footer">
            <button id="footer-close-button">关闭按钮</button>
        </div>
    </my-modal>

    <script>
        document.getElementById('open-modal-button').addEventListener('click', ()=> {
            document.getElementById('my-modal').open();
        });

        document.getElementById('footer-close-button').addEventListener('click', ()=> {
            document.getElementById('my-modal').close();
        });
    </script>
</body>
</html>

至此,我们实现了一个功能完整的模态对话框组件,并且能够在不同的页面中复用。

最后

使用web components开发了一个模态框之后,我们发现Web Components的一些不方便之处,比如说template的定义无法单独写在一个html文件中,必须用模版字符串包裹起来,不优雅。另外,我们习惯使用框架组件之后,发现Web Component和原生dom开发一样,不支持响应式数据,api相对繁琐等。这可能是web components不是特别热门的原因。可是有一种场景,特别适合用web components。就是一些很复杂的跨开发框架的组件,比如说日历组件,富文本编辑器,复杂的图标和表单等。总体说来,web components还是有用武之地的。有没有感觉,多了解一项技术,开发的时候就多了一分灵活性,所以说技多不压身。


手把手教你用Web Components开发一个跨框架的模态框
原文链接:https://juejin.cn/post/7371319684842340363