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