7

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

 3 years ago
source link: https://segmentfault.com/a/1190000038386398
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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

6nemmuN.png!mobile

yMr6Bb6.gif!mobile

RLayer 在设计及开发上参考了之前的VLayer弹出框组件。在效果上保持一致性。

vlayer一款vue2.x开发的网页弹框组件,感兴趣的可以去看看这篇文章。

https://segmentfault.com/a/11...

VRVrU37.gif!mobile

引入使用

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

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

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

showConfirm = () => {
    let $rlayer = rlayer({
        title: '标题信息',
        content: "<div style='color:#0070f3;padding:50px;'>显示弹窗内容。</div>",
        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({...})

一睹效果

MVnA3uF.png!mobile

uUrEZf3.png!mobile

q2QzIfQ.png!mobile

AzQvumB.png!mobile

jQB3AjI.png!mobile

fEjyueY.png!mobile

2YJRZ3V.png!mobile

MNV7V3Q.png!mobile

Bb6RZfm.png!mobile

FvyqQv2.gif!mobile

7J3QNrv.png!mobile

MFbARnv.png!mobile

vuqeumy.gif!mobile

jAJ7Vfm.png!mobile

ny6ruay.gif!mobile

编码实现

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 (
        <>
        <div className={domUtils.classNames('rui__layer', {'rui__layer-closed': opt.closeCls})} id={opt.id} style={{display: opt.opened?'block':'none'}}>
            {/* 遮罩 */}
            { opt.shade && <div className="rlayer__overlay" onClick={this.shadeClicked} style={{opacity: opt.opacity}}></div> }
            <div className={domUtils.classNames('rlayer__wrap', opt.anim&&'anim-'+opt.anim, opt.type&&'popui__'+opt.type)} style={{...opt.layerStyle}}>
            { opt.title && <div className='rlayer__wrap-tit' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
            <div className='rlayer__wrap-cntbox'>
                { opt.content ? 
                <>
                    {
                    opt.type == 'iframe' ? 
                    (
                        <iframe scrolling='auto' allowtransparency='true' frameBorder='0' src={opt.content}></iframe>
                    )
                    : 
                    (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? 
                    (
                        <div className='rlayer__wrap-cnt'>
                        { opt.icon && <i className={domUtils.classNames('rlayer-msg__icon', opt.icon)} dangerouslySetInnerHTML={{__html: opt.messageIcon[opt.icon]}}></i> }
                        <div className='rlayer-msg__group'>
                            { opt.title && <div className='rlayer-msg__title' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                            { typeof opt.content == 'string' ? 
                            <div className='rlayer-msg__content' dangerouslySetInnerHTML={{__html: opt.content}}></div>
                            :
                            <div className='rlayer-msg__content'>{opt.content}</div>
                            }
                        </div>
                        </div>
                    )
                    : 
                    (
                        typeof opt.content == 'string' ? 
                        (<div className='rlayer__wrap-cnt' dangerouslySetInnerHTML={{__html: opt.content}}></div>)
                        :
                        opt.content
                    )
                    }
                </>
                :
                null
                }
            </div>
            { opt.btns && <div className='rlayer__wrap-btns'>
                {
                    opt.btns.map((btn, index) => {
                        return <span className={domUtils.classNames('btn')} key={index} style={{...btn.style}} dangerouslySetInnerHTML={{__html: btn.text}}></span>
                    })
                }
                </div>
            }
            { opt.xclose && <span className={domUtils.classNames('rlayer__xclose')}></span> }
            { opt.maximize && <span className='rlayer__maximize'></span> }
            { opt.resize && <span className='rlayer__resize'></span> }
            </div>
            <div className='rlayer__dragfix'></div>
        </div>
        </>
    )
}
/**
 * @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 (
            <>
            <div className={domUtils.classNames('rui__layer', {'rui__layer-closed': opt.closeCls})} id={opt.id} style={{display: opt.opened?'block':'none'}}>
                { opt.shade && <div className="rlayer__overlay" onClick={this.shadeClicked} style={{opacity: opt.opacity}}></div> }
                <div className={domUtils.classNames('rlayer__wrap', opt.anim&&'anim-'+opt.anim, opt.type&&'popui__'+opt.type, opt.drawer&&'popui__drawer-'+opt.drawer, opt.xclose&&'rlayer-closable', opt.tipArrow)} style={{...opt.layerStyle}}>
                { opt.title && <div className='rlayer__wrap-tit' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                { opt.type == 'toast' && opt.icon ? <div className={domUtils.classNames('rlayer__toast-icon', 'rlayer__toast-'+opt.icon)} dangerouslySetInnerHTML={{__html: opt.toastIcon[opt.icon]}}></div> : null }
                <div className='rlayer__wrap-cntbox'>
                    { opt.content ? 
                    <>
                        {
                        opt.type == 'iframe' ? 
                        (
                            <iframe scrolling='auto' allowtransparency='true' frameBorder='0' src={opt.content}></iframe>
                        )
                        : 
                        (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? 
                        (
                            <div className='rlayer__wrap-cnt'>
                            { opt.icon && <i className={domUtils.classNames('rlayer-msg__icon', opt.icon)} dangerouslySetInnerHTML={{__html: opt.messageIcon[opt.icon]}}></i> }
                            <div className='rlayer-msg__group'>
                                { opt.title && <div className='rlayer-msg__title' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                                { typeof opt.content == 'string' ? 
                                <div className='rlayer-msg__content' dangerouslySetInnerHTML={{__html: opt.content}}></div>
                                :
                                <div className='rlayer-msg__content'>{opt.content}</div>
                                }
                            </div>
                            </div>
                        )
                        : 
                        (
                            typeof opt.content == 'string' ? 
                            (<div className='rlayer__wrap-cnt' dangerouslySetInnerHTML={{__html: opt.content}}></div>)
                            :
                            opt.content
                        )
                        }
                    </>
                    :
                    null
                    }
                </div>
                {/* btns */}
                { opt.btns && <div className='rlayer__wrap-btns'>
                    {
                        opt.btns.map((btn, index) => {
                            return <span className={domUtils.classNames('btn')} key={index} style={{...btn.style}} dangerouslySetInnerHTML={{__html: btn.text}}></span>
                        })
                    }
                    </div>
                }
                { opt.xclose && <span className={domUtils.classNames('rlayer__xclose')} style={{color: opt.xcolor}}></span> }
                { opt.maximize && <span className='rlayer__maximize'></span> }
                { opt.resize && <span className='rlayer__resize'></span> }
                </div>
                <div className='rlayer__dragfix'></div>
            </div>
            </>
        )
    }
}

动态className

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

// 字符串拼接
<i className={["rlayer"+" "+item.icon]} ></i>

// 判断
<i className={["rlayer ",isOK ? item.icon : '' ].join('')} ></i>

// ES6模板字符串
<i className={`rlayer ${isOK ? item.icon : '' }`} ></i>

这种方法简单的还行,复杂的拼接就麻烦,而且会生成很多莫名的空格。

这里采用了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 即可打开弹框就显示全屏。

EzInEfe.png!mobile

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

ending,附上两个vue.js示例项目

vue|nuxt.js仿微信app聊天实例: https://segmentfault.com/a/11...

vue.js自定义虚拟化滚动条组件: https://segmentfault.com/a/11...

mM3eieN.gif!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK