共计 10522 个字符,预计需要花费 27 分钟才能阅读完成。
前言
RLayer.js 一个 react 构建的桌面 PC 端 自定义 Dialog 组件 。 内置 30+ 参数配置、10+ 弹框类型、7+ 动画成果
,提供极简的接口及清新的皮肤。领有顺滑般 最大化 / 缩放 / 拖拽
体验!
RLayer在设计及开发上参考了之前的 VLayer 弹出框组件。在成果上放弃一致性。
vlayer一款 vue2.x 开发的网页弹框组件,感兴趣的能够去看看这篇文章。
https://segmentfault.com/a/11…
引入应用
在须要用到弹出框的页面引入 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({...})
一睹成果
编码实现
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
即可关上弹框就显示全屏。
好了,基于 React.js 开发 pc 端弹窗组件就分享到这里。心愿对大家有些帮忙哈!✍✍
ending,附上两个 vue.js 示例我的项目
vue|nuxt.js 仿微信 app 聊天实例:https://segmentfault.com/a/11…
vue.js 自定义虚拟化滚动条组件:https://segmentfault.com/a/11…