xiaoyan2017

基于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

 

分类:

技术点:

相关文章: