今天要介紹的是一個Vue的增強組件——vue-directive-window,它讓你的模態框輕而易舉地支持類窗口操作,也就是說不僅僅對話框,還將具備一些窗口化的特性,諸如拖拽、最大化、縮放等增強型功能!
https://gitee.com/mirrors/vue-directive-window
旨在以極少的改造成本,讓一個現有的模態框或任何合適的HTMLElement輕松支持拖拽移動、調整大小、最大化等類視窗操作。
同時提供Vue自定義指令以及一般js類庫兩種調用方式。
可以隨時為某個系統里已存在的模塊添上/去除類視窗操作的功能,而不會影響該模塊原有的功能。
也可以使用其它包管理工具,如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, 自己平時根本用不到,如果不刻意學習和了解,自己的知識體系就會產生盲區,可是每個人的知識盲區那么多,掃的過來嘛。對于這一點,我們要抱有積極的心態,少除一個就少一個??墒且獟叱募夹g盲區那么多,為什么要優先選擇掃除它?這一點看個人偏好,沒有標準答案。但有一個大方向是確定的,如果想在技術的道路上走得更遠,就得不斷清除阻礙自己前行的障礙拓寬自己的技術視野。廢話不多說了,現在我們進入今天的主題。
Web Components 是一組標準,用于創建可重用的、封裝良好的自定義元素,能夠與標準的 HTML 元素無縫集成。Web Components 使開發者能夠定義自己的 HTML 標簽,這些標簽具有獨立的樣式和行為,從而增強了組件的可復用性和模塊化程度。Web Components 由以下三項技術組成:
Custom Elements(自定義元素) :
Shadow DOM(影子 DOM) :
HTML Templates(HTML 模板) :
Web Components技術是一組讓開發者能夠創建可重用的、封裝良好的自定義HTML元素的標準。使開發者能夠創建高度復用、獨立、封裝良好的組件,從而提高開發效率和代碼質量。下面是一些典型的場景:
許多公司和團隊使用Web Components來構建設計系統和組件庫。這些系統和庫允許在不同項目中復用一致的UI組件,從而保持設計的一致性和開發的高效性。如Salesforce的Lightning Web Components、Ionic Framework中的Stencil。
Web Components可以在不同的前端框架(如React、Angular、Vue)中無縫使用。這使得開發者能夠創建獨立于框架的組件,從而提高組件的復用性。在一個項目中使用React構建大部分頁面,同時使用Web Components構建特定的獨立組件,比如日期選擇器或地圖。
在微前端架構中,不同團隊可以獨立開發、部署和維護前端應用的不同部分。Web Components使得這些獨立的部分可以以組件的形式集成到一個整體的應用中,而不會互相干擾。如一個電商網站的不同模塊(如購物車、支付、用戶評論)由不同團隊開發,并以Web Components的形式集成。
Web Components非常適合構建可以嵌入到任意網頁中的小部件和插件,比如聊天窗口、表單驗證、廣告模塊等。這些小部件通常需要高度的封裝性和獨立性,以避免與宿主頁面的沖突。如第三方客服聊天窗口、嵌入式視頻播放器。
創建數據可視化組件,如圖表、地圖、數據表等,這些組件可以獨立于具體的應用環境,在不同的項目中重用。如使用D3.js或其他圖表庫創建的自定義元素,用于顯示動態數據圖表。
構建復雜且可復用的表單控件,如日期選擇器、顏色選擇器、富文本編輯器等。這些控件可以被封裝為自定義元素,便于在不同表單中復用。如一個自定義的富文本編輯器,可以用于博客系統、內容管理系統等多個場景。
在開發大型Web應用時,使用Web Components可以實現組件化開發,使得應用結構更清晰,組件更易于測試和維護。如在線文檔編輯器中的各種工具欄和編輯器組件,每個都封裝為獨立的Web Component。
模態對話框是Web應用中常見的UI組件,可以用于顯示重要的消息、表單或確認對話框。我們通過用web components技術創建一個自定義的模態對話框組件,演示一下web components的使用方法。
將模板和自定義元素的定義放在一個JavaScript文件 my-modal.js中。
// 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);
在HTML文件中通過 <script> 標簽引入上述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不是特別熱門的原因??墒怯幸环N場景,特別適合用web components。就是一些很復雜的跨開發框架的組件,比如說日歷組件,富文本編輯器,復雜的圖標和表單等??傮w說來,web components還是有用武之地的。有沒有感覺,多了解一項技術,開發的時候就多了一分靈活性,所以說技多不壓身。
手把手教你用Web Components開發一個跨框架的模態框
原文鏈接:https://juejin.cn/post/7371319684842340363
*請認真填寫需求信息,我們會在24小時內與您取得聯系。