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