• 基于React.js网页版弹窗|react pc端自定义对话框组件RLayer


    基于React.js实现PC桌面端自定义弹窗组件RLayer。

    前几天有分享一个Vue网页版弹框组件,今天分享一个最新开发的React PC桌面端自定义对话框组件。

    RLayer 一款基于react.js开发的PC端自定义Layer弹出框组件。支持超过30+参数自由配置,通过轻巧的布局设计、极简的调用方式来解决复杂的弹出层功能,为您呈上不一样的弹窗效果。

    RLayer在设计开发之初有参考之前的VLayer组件,尽量保持功能效果的一致性。

    如上图:展示一些常用的基础普通型弹窗功能。

    • 极简调用方式 rlayer({....})
    • 12+弹框类型(toast、footer、actionsheet|actionsheetPicker、android|ios、contextmenu、drawer、iframe、message|notify|popover)
    • 7+动画效果(scaleIn | fadeIn | footer | fadeInUp | fadeInDown | fadeInLeft | fadeInRight)

    ◆ 快速引入

    在需要使用弹窗功能页面引入rlayer组件。

    // 引入组件RLayer
    import rlayer from './components/rlayer'

    rlayer目前只支持函数式调用方式。 rlayer({...}) 

    const showConfirm = () => {
        let $el = rlayer({
            title: '标题信息',
            content: "<div style='color:#0070f3;padding:30px;'>这里是确认框提示信息!</div>",
            shadeClose: false,
            zIndex: 1001,
            lockScroll: false,
            resize: true,
            dragOut: true,
            btns: [
                {
                    text: '取消',
                    click: () => {
                        $el.close()
                    }
                },
                {
                    text: '确定',
                    style: {color: '#61dafb'},
                    click: () => {
                        // ...
                    }
                }
            ]
        })
    }

    注意:当弹窗类型为 message | notify | popover 需要通过如下方式调用。

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

    ◆ 一睹芳容

    ◆ 编码实现

    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>
                </>
            )
        }
    }

    其中utils/dom.js中是一些常用操作函数。

    为了方便在react.js中动态操作className,于是抽离了classnames函数

    classNames: function() {
        var hasOwn = {}.hasOwnProperty;
        var classes = [];
        for (var i = 0; i < arguments.length; i++) {
            var arg = arguments[i];
            if (!arg) continue;
            var argType = typeof arg;
            if (argType === 'string' || argType === 'number') {
                classes.push(arg);
            } else if (Array.isArray(arg) && arg.length) {
                var inner = classNames.apply(null, arg);
                if (inner) {
                    classes.push(inner);
                }
            } else if (argType === 'object') {
                for (var key in arg) {
                    if (hasOwn.call(arg, key) && arg[key]) {
                        classes.push(key);
                    }
                }
            }
        }
        return classes.join(' ');
    }

    非常轻松方便的在react中实现各种动态操作className。

    <div className="rlayer"></div>
    <div className={domUtils.classNames('rlayer', {'rlayer__closed': opt.close})}></div>
    <div className={domUtils.classNames('rlayer', opt.anim&&'anim-'+opt.anim)}></div>
    <div className={domUtils.classNames('rlayer', opt.icon)}></div>
    ...

    react.js中通过ReactDOM.render方法将弹窗组件挂载到body上。

    function RLayer(options = {}) {
        let $id = options.id
        let $dom = document.querySelector('#' + $id)
        if($id && $dom) return
    
        const div = document.createElement('div')
        const ref = React.createRef()
        document.body.appendChild(div)
    
        /*
        ReactDOM.render(
            <RLayerComponent {...options} remove={()=>{
                ReactDOM.unmountComponentAtNode(div)
                document.body.removeChild(div)
            }} />,
            div
        )
        */
        ReactDOM.render(<RLayerComponent ref={ref} />, div)
    
        ref.current.open({ ...options, remove() {
            if(!ref.current) return
            ReactDOM.unmountComponentAtNode(div)
            document.body.removeChild(div)
        }})
    
        // 返回弹框实例
        return ref.current
    }

    rlayer.js组件支持自定义拖拽区域 (drag:'#xxx'),是否拖动到窗口外 (dragOut:true)。还支持iframe弹窗类型 (type:'iframe')。

    另外rlayer.js还支持弹窗置顶 (topmost:true),永远保持当前窗口在最前。

    好了,以上就是基于react.js开发PC端弹窗的相关介绍。希望大家能喜欢哈~~ ✍✍

    最后分享两个vue自定义组件

    vue自定义对话框组件:https://www.cnblogs.com/xiaoyan2017/p/13913860.html

    vue自定义滚动条组件:https://www.cnblogs.com/xiaoyan2017/p/14062703.html

  • 相关阅读:
    HashMap的理解
    红黑树
    No constructor found matching
    会话 控制终端 setsid
    信息表示和处理 from computer system chapter 2
    tcp keepalive
    TCP 四步挥手
    CS 课程
    close vs shutdown socket
    Linux time总结
  • 原文地址:https://www.cnblogs.com/xiaoyan2017/p/14085142.html
Copyright © 2020-2023  润新知