React.js 全局公共弹框(RLayer)|react弹窗组件

前言

RLayer.js 一个react构建的桌面PC端自定义Dialog组件内置30+参数配置、10+弹框类型、7+动画效果,提供极简的接口及清爽的皮肤。拥有顺滑般最大化/缩放/拖拽体验!

image

RLayer在设计及开发上参考了之前的VLayer弹出框组件。在效果上保持一致性。
vlayer一款vue2.x开发的网页弹框组件,感兴趣的可以去看看这篇文章。
https://segmentfault.com/a/11...

引入使用

在需要用到弹出框的页面引入rlayer组件即可。

// 引入RLayer
import rlayer from './components/rlayer';

提供了非常简易的调用写法 rlayer({...})

showConfirm = () => {
    let $rlayer = rlayer({
        title: '标题信息',
        content: "
显示弹窗内容。
", shadeClose: true, zIndex: 2021, lockScroll: true, resize: true, dragOut: false, btns: [ { text: '取消', click: () => { $rlayer.close() } }, { text: '确定', style: {color: '#09f'}, click: () => { // ... } } ] }) }

注意:如果弹框类型为 message|notify|popover,则需要使用如下调用方式。

rlayer.message({...})
rlayer.notify({...})
rlayer.popover({...})

一睹效果

image

编码实现

rlayer支持如下丰富的参数配置。

/**
 * 弹出框参数配置
 */
static defaultProps = {
    // 参数
    id: '',                       // {string} 控制弹层唯一标识,相同id共享一个实例
    title: '',                    // {string} 标题
    content: '',                  // {string|element} 内容(支持字符串或组件)
    type: '',                     // {string} 弹框类型(toast|footer|actionsheet|actionsheetPicker|android|ios|contextmenu|drawer|iframe)
    layerStyle: '',               // {object} 自定义弹框样式
    icon: '',                     // {string} Toast图标(loading|success|fail)
    shade: true,                  // {bool} 是否显示遮罩层
    shadeClose: true,             // {bool} 是否点击遮罩层关闭弹框
    lockScroll: true,             // {bool} 是否弹框显示时将body滚动锁定
    opacity: '',                  // {number|string} 遮罩层透明度
    xclose: true,                 // {bool} 是否显示关闭图标
    xposition: 'right',           // {string} 关闭图标位置(top|right|bottom|left)
    xcolor: '#333',               // {string} 关闭图标颜色
    anim: 'scaleIn',              // {string} 弹框动画(scaleIn|fadeIn|footer|fadeInUp|fadeInDown|fadeInLeft|fadeInRight)
    position: 'auto',             // {string|array} 弹框位置(auto|['150px','100px']|t|r|b|l|lt|rt|lb|rb)
    drawer: '',                   // {string} 抽屉弹框(top|right|bottom|left)
    follow: null,                 // {string|array} 跟随定位弹框(支持.xxx #xxx 或 [e.clientX,e.clientY])
    time: 0,                      // {number} 弹框自动关闭秒数(1|2|3...)
    zIndex: 8090,                 // {number} 弹框层叠
    topmost: false,               // {bool} 是否置顶当前弹框
    area: 'auto',                 // {string|array} 弹框宽高(auto|'250px'|['','200px']|['650px','300px'])
    maxWidth: 375,                // {number} 弹框最大宽度(只有当area:'auto'时设定才有效)
    maximize: false,              // {bool} 是否显示最大化按钮
    fullscreen: false,            // {bool} 是否全屏弹框
    fixed: true,                  // {bool} 是否固定弹框
    drag: '.rlayer__wrap-tit',    // {string|bool} 拖拽元素(可自定义拖动元素drag:'#xxx' 禁止拖拽drag:false)
    dragOut: false,               // {bool} 是否允许拖拽到浏览器外
    lockAxis: null,               // {string} 限制拖拽方向可选: v 垂直、h 水平,默认不限制
    resize: false,                // {bool} 是否允许拉伸弹框
    btns: null,                   // {array} 弹框按钮(参数:text|style|disabled|click)
 
    // 事件
    success: null,                // {func} 层弹出后回调
    end: null,                    // {func} 层销毁后回调
}

rlayer弹框模板

render() {
    let opt = this.state
 
    return (
        <>
        
{/* 遮罩 */} { opt.shade &&
}
{ opt.title &&
}
{ opt.content ? <> { opt.type == 'iframe' ? ( ) : (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? (
{ opt.icon && }
{ opt.title &&
} { typeof opt.content == 'string' ?
:
{opt.content}
}
) : ( typeof opt.content == 'string' ? (
) : opt.content ) } : null }
{ opt.btns &&
{ opt.btns.map((btn, index) => { return }) }
} { opt.xclose && } { opt.maximize && } { opt.resize && }
) }
/**
 * @Desc     ReactJs|Next.js自定义弹窗组件RLayer
 * @Time     andy by 2020-12-04
 * @About    Q:282310962  wx:xy190310
 */
import React from 'react'
import ReactDOM from 'react-dom'
 
// 引入操作类
import domUtils from './utils/dom'
 
let $index = 0, $lockCount = 0, $timer = {}
 
class RLayerComponent extends React.Component {
    static defaultProps = {
        // ...
    }
 
    constructor(props) {
        super(props)
        this.state = {
            opened: false,
            closeCls: '',
            toastIcon: {
                // ...
            },
            messageIcon: {
                // ...
            },
            rlayerOpts: {},
            tipArrow: null,
        }
 
        this.closeTimer = null
    }
 
    componentDidMount() {
        window.addEventListener('resize', this.autopos, false)
    }
    componentWillUnmount() {
        window.removeEventListener('resize', this.autopos, false)
        clearTimeout(this.closeTimer)
    }
 
    /**
     * 打开弹框
     */
    open = (options) => {
        options.id = options.id || `rlayer-${domUtils.generateId()}`
 
        this.setState({
            ...this.props, ...options, opened: true,
        }, () => {
            const { success } = this.state
            typeof success === 'function' && success.call(this)
 
            this.auto()
            this.callback()
        })
    }
 
    /**
     * 关闭弹框
     */
    close = () => {
        const { opened, time, end, remove, rlayerOpts, action } = this.state
        if(!opened) return
 
        this.setState({ closeCls: true })
        clearTimeout(this.closeTimer)
        this.closeTimer = setTimeout(() => {
            this.setState({
                closeCls: false,
                opened: false,
            })
            if(rlayerOpts.lockScroll) {
                $lockCount--
                if(!$lockCount) {
                    document.body.style.paddingRight = ''
                    document.body.classList.remove('rc-overflow-hidden')
                }
            }
            if(time) {
                $index--
            }
            if(action == 'update') {
                document.body.style.paddingRight = ''
                document.body.classList.remove('rc-overflow-hidden')
            }
            rlayerOpts.isBodyOverflow && (document.body.style.overflow = '')
            remove()
            typeof end === 'function' && end.call(this)
        }, 200);
    }
 
    // 弹框位置
    auto = () => {
        // ...
 
        this.autopos()
 
        // 全屏弹框
        if(fullscreen) {
            this.full()
        }
 
        // 弹框拖拽|缩放
        this.move()
    }
 
    autopos = () => {
        const { opened, id, fixed, follow, position } = this.state
        if(!opened) return
        let oL, oT
        let dom = document.querySelector('#' + id)
        let rlayero = dom.querySelector('.rlayer__wrap')
 
        if(!fixed || follow) {
            rlayero.style.position = 'absolute'
        }
 
        let area = [domUtils.client('width'), domUtils.client('height'), rlayero.offsetWidth, rlayero.offsetHeight]
 
        oL = (area[0] - area[2]) / 2
        oT = (area[1] - area[3]) / 2
 
        if(follow) {
            this.offset()
        } else {
            typeof position === 'object' ? (
                oL = parseFloat(position[0]) || 0, oT = parseFloat(position[1]) || 0
            ) : (
                position == 't' ? oT = 0 : 
                position == 'r' ? oL = area[0] - area[2] : 
                position == 'b' ? oT = area[1] - area[3] : 
                position == 'l' ? oL = 0 : 
                position == 'lt' ? (oL = 0, oT = 0) : 
                position == 'rt' ? (oL = area[0] - area[2], oT = 0) : 
                position == 'lb' ? (oL = 0, oT = area[1] - area[3]) :
                position == 'rb' ? (oL = area[0] - area[2], oT = area[1] - area[3]) : 
                null
            )
 
            rlayero.style.left = parseFloat(fixed ? oL : domUtils.scroll('left') + oL) + 'px'
            rlayero.style.top = parseFloat(fixed ? oT : domUtils.scroll('top') + oT) + 'px'
        }
    }
 
    // 跟随元素定位
    offset = () => {
        const { id, follow } = this.state
        let oW, oH, pS
        let dom = document.querySelector('#' + id)
        let rlayero = dom.querySelector('.rlayer__wrap')
 
        oW = rlayero.offsetWidth
        oH = rlayero.offsetHeight
        pS = domUtils.getFollowRect(follow, oW, oH)
 
        rlayero.style.left = pS[0] + 'px'
        rlayero.style.top = pS[1] + 'px'
    }
 
    // 最大化弹框
    full = () => {
        // ...
    }
 
    // 恢复弹框
    restore = () => {
        // ...
    }
 
    // 拖拽|缩放弹框
    move = () => {
        // ...
    }
 
    // 事件处理
    callback = () => {
        const { time } = this.state
        // 倒计时关闭弹框
        if(time) {
            $index++
            // 防止重复计数
            if($timer[$index] != null) clearTimeout($timer[$index])
            $timer[$index] = setTimeout(() => {
                this.close()
            }, parseInt(time) * 1000);
        }
    }
 
    // 点击最大化按钮
    maximizeClicked = (e) => {
        let o = e.target
        if(o.classList.contains('maximized')) {
            // 恢复
            this.restore()
        } else {
            // 最大化
            this.full()
        }
    }
 
    // 点击遮罩层
    shadeClicked = () => {
        if(this.state.shadeClose) {
            this.close()
        }
    }
 
    // 按钮事件
    btnClicked = (index, e) => {
        let btn = this.state.btns[index]
        if(!btn.disabled) {
            typeof btn.click === 'function' && btn.click(e)
        }
    }
 
    render() {
        let opt = this.state
        return (
            <>
            
{ opt.shade &&
}
{ opt.title &&
} { opt.type == 'toast' && opt.icon ?
: null }
{ opt.content ? <> { opt.type == 'iframe' ? ( ) : (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? (
{ opt.icon && }
{ opt.title &&
} { typeof opt.content == 'string' ?
:
{opt.content}
}
) : ( typeof opt.content == 'string' ? (
) : opt.content ) } : null }
{/* btns */} { opt.btns &&
{ opt.btns.map((btn, index) => { return }) }
} { opt.xclose && } { opt.maximize && } { opt.resize && }
) } }

动态className

在react.js中动态绑定class类名。有如下几种常用方法。

// 字符串拼接


// 判断


// ES6模板字符串

这种方法简单的还行,复杂的拼接就麻烦,而且会生成很多莫名的空格。
这里采用了React classnames库。
看看如下的使用方法,就知道有多方便。

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

rlayer组件支持自定义拖拽把手 drag:'#aaa',是否可以拖动到窗口外部 dragOut:true
另外还支持iframe弹框,只需设置 type:'iframe',content传入网址就行。
配置 fullscreen:true 即可打开弹框就显示全屏。

image

好了,基于React.js开发pc端弹窗组件就分享到这里。希望对大家有些帮助哈!✍✍

ending,附上两个vue.js示例项目
vue|nuxt.js仿微信app聊天实例:https://segmentfault.com/a/11...
vue.js自定义虚拟化滚动条组件:https://segmentfault.com/a/11...

image

你可能感兴趣的:(react.js,react-hooks,typescript,es6)