定義方法調(diào)用格式
我發(fā)現(xiàn)在寫項(xiàng)目的時(shí)候航攒,經(jīng)常會(huì)用到 alert
、confirm
让簿、toast
之類的彈出框敬察。但是原生alert
對(duì)話框的樣式已經(jīng)定型,和UI設(shè)計(jì)又不一樣拜英,再加上彈出面板真的出現(xiàn)頻率太多了,于是自己動(dòng)手寫了一個(gè)動(dòng)態(tài)彈窗組件琅催。
首先居凶,我先定義了一下,假設(shè)已有此組件藤抡,我要怎么調(diào)用:
popup({
type:'alert',
content: '這是一個(gè)警告框侠碧!'
})
于是就有了大概想法,首先這個(gè)組件是通過一個(gè)方法調(diào)用的缠黍,其次弄兜,這個(gè)方法得有一個(gè)對(duì)象作為自己的參數(shù),對(duì)象中的type
用來定義當(dāng)前面板的類型瓷式。
經(jīng)過一系列測(cè)試后替饿,最終定下來以下類型,并列出此popup應(yīng)該有的功能
/**
* alert confirm prompt toast slot
*/
- 默認(rèn)居中贸典、有根元素傳入時(shí)视卢,按照根元素的位置進(jìn)行定位
- 可以設(shè)置彈出面板大小及其他樣式
- 可以設(shè)置面板css類,修改蒙版的不透明度
- 具有confirm和close函數(shù)回調(diào)
- 面板內(nèi)容可以自定義廊驼,slot模式時(shí)据过, 可以選擇內(nèi)嵌 組件或者h(yuǎn)tml元素
- 當(dāng)面板的自定義內(nèi)容為html元素時(shí),支持綁定事件
調(diào)用confirm
popup({
type: 'confirm',
content: '你確定要?jiǎng)h除我嗎妒挎?',
confirm: function() {
// TODO
},
close: function() {
// TODO
}
})
調(diào)用prompt
popup({
type: 'prompt',
content: '請(qǐng)輸入內(nèi)容:',
confirm: function(str) {
// TODO
console.log(str);
},
close: function() {
// TODO
}
})
調(diào)用toast
popup({
type: 'toast',
content: '我是一個(gè)定時(shí)提示框绳锅!',
timeout: 3000 // 不傳 timeout時(shí),默認(rèn)時(shí)長(zhǎng)2秒
})
調(diào)用slot 自定義面板
popup({
type: 'slot', // 自定義組件模式
slot: {
name: 'myComponent',
url: 'views/components/myComponent.vue' //此項(xiàng)可選酝掩,有時(shí)將自動(dòng)加載為全局組件
}
})
popup({
type: 'slot', // html模式
html: true,
innerHTML : `
<div>
<p>這是我定義的html內(nèi)容·</p>
<button @click="logMsg">綁定一個(gè)方法</button>
</div>
`,
methods: {
logMsg: function(){
console.log('this is a event')
}
}
})
大概定的調(diào)用方式就是上面這樣了鳞芙,但是除此外還需要幾個(gè)屬性來修飾一下:
popup({
type: 'alert',
content: '這是一個(gè)警告框'
root: document.querySelector('.msg'), // 面板按照此元素在頁面中的位置進(jìn)行定位
opacity: 0.1, // 蒙版的不透明度,當(dāng)為0 時(shí) 完全透明期虾,為1時(shí)背景全黑
style: { // 此屬性的值將作用于面板上
'width': '200px',
'height': '100px'
},
css: ['change1','change2'] //此屬性接收一個(gè)數(shù)組积蜻,數(shù)組值作為class類綁定到面板上,這個(gè)是為了寫復(fù)雜的樣式時(shí)彻消,避免寫過長(zhǎng)的style屬性準(zhǔn)備的
})
莽代碼
先來定義一個(gè)大概代碼框架
(function(global, factory){
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.popup = factory());
})(this,function(){
/* 以下我們定義了一個(gè)封閉作用域用來編寫我們的popup函數(shù)需要用到的其他變量 判斷和 處理的函數(shù) */
let [template, popupData] = ['', {}];
const [_toString, doc] = [Object.prototype.toString, document.documentElement];
// 定義工具函數(shù)竿拆,留作判斷備用
function _isObj(obj){
return _toString.call(obj).slice(8,-1) === 'Object';
};
function _isArr(obj){
return _toString.call(obj).slice(8,-1) === 'Array';
};
function _isEle(ele){
return _toString.call(ele).slice(8,12) === 'HTML';
};
function _isFun(fun){
return typeof fun === 'function';
};
function _isErr(err){
return _toString.call(err).slice(8,-1) === 'Error';
};
let methods = {
location: function(ele) {
// 計(jì)算傳入element元素在頁面上的位置
...
},
initpopupData: function(options){
...
},
template: function(type,options){
// 通過type 創(chuàng)建對(duì)應(yīng)的 template div
if(type){
...
}
template = `<div>...</div>`
},
rootOffset: function(obj,ele){
// 計(jì)算 面板位置偏移量
...
},
judgeData: function(options){
// 判斷popupData 合法性
},
complieEvents: function(ele,options){
// 編譯元素 查詢綁定事件的DOM
...
},
addComponent: function(options){
// 添加Vue popup全局組件
...
}
}
const popup = (options) => {
/* 初始化data */
methods.initpopupData(options);
/* 判斷是否有data 并查看data中是否有禁止項(xiàng)*/
methods.judgeData(options);
/* 創(chuàng)建popup組件 */
methods.addComponent(options);
return popup;
})
上源碼宾尚!
(function(global, factory){
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.popup = factory());
})(this,function(){
let [template, popupData] = ['' , {}];
const [_toString, doc] = [ Object.prototype.toString, document.documentElement];
function _isObj(obj){
return _toString.call(obj).slice(8,-1) === 'Object';
};
function _isArr(obj){
return _toString.call(obj).slice(8,-1) === 'Array';
};
function _isEle(ele){
return _toString.call(ele).slice(8,12) === 'HTML';
};
function _isFun(fun){
return _toString.call(fun).slice(8,-1) === 'Function';
};
function _isErr(err){
return _toString.call(err).slice(8,-1) === 'Error';
};
let methods = {
location: function(t){
let [ele,left,top] = [t,0,0];
left = ele.offsetLeft;
top = ele.offsetTop;
while(ele.offsetParent !== null){
ele = ele.offsetParent;
left += ele.offsetLeft;
top += ele.offsetTop;
}
left += t.offsetWidth;
top += t.offsetHeight;
return {
root: t,
left: left,
top: top
};
},
initpopupData: function(options){
popupData = {
type: options.type,
promptValue: '',
res: {},
html: ('html' in options) ? options['html'] : false,
innerHTML: ('html' in options)&&('innerHTML' in options) ?
options['innerHTML'] :
'請(qǐng)?jiān)O(shè)置一個(gè)innerHTML屬性丙笋,指向自定義容器內(nèi)容'
};
},
template: function(type,options){
if(type !== 'slot') {
if(!('content' in options)) return new Error('不存在content屬性谢澈,請(qǐng)?zhí)砑哟俗侄危侄蔚闹禐轱@示內(nèi)容御板!');
}
template = `
<div class="popup-mask" @click.self="close" id="popup-mask" ref="popup" >
<div class="popup-content popup-${type}" ref="popupcon">
<template v-if="type !== 'slot'">
<div class="popup-tips">
<p>${options.content}</p>
<input class="popup-prompt-input" v-model="promptValue" v-if="type === 'prompt'">
</div>
<div class="popup-options">
<button @click="confirm" v-if="type !== 'toast'" class="popup-btn-confirm">確定</button>
<button @click="close" v-if="type === 'confirm' || type === 'prompt'" class="popup-btn-close">取消</button>
</div>
</template>
<template v-else>
<div v-html="innerHTML" v-if="html" ref="chtml"></div>
<slot name="content" v-if="!html"></slot>
</template>
</div>
</div>
`
},
rootOffset: function(obj,ele){
let [bl,bt] = [doc.clientWidth,doc.clientHeight];
ele.style.position = 'absolute';
if(obj.top + ele.clientHeight > bt ){
// 面板底部溢出
ele.style.bottom = bt - obj.top + obj.root.clientHeight + 'px';
//doc.scrollTop = doc.clientHeight;
}else{
ele.style.top = obj.top + 'px';
//doc.scrollTop = 0;
}
if(obj.left + ele.clientWidth > bl){
// 面板右側(cè)溢出
ele.style.right = bl - obj.left + obj.root.clientWidth + 'px';
}else{
ele.style.left = obj.left + 'px';
}
},
judgeData: function(options){
if(('data' in options) && _isObj(options['data'])){
let state = true;
for(let item in options['data']){
for(let i in popupData){
if(item === i){
console.error('data子屬性:'+item+'為初始化data屬性值锥忿,請(qǐng)更換名字后重試!');
state = false;
break;
}
}
}
if(!state) return false;
}
return true;
},
complieEvents: function(ele,options){
let eleArr = [];
push(ele);
function push(fragment){
Array.from(fragment.childNodes).forEach((node)=>{
if(node.nodeType === 1 && node.getAttribute('@click')){
eleArr.push(node);
}
if(node.childNodes){
push(node);
}
})
}
if(eleArr.length === 0) return true;
if(!('methods' in options) || !(_isObj(options['methods']))) return false;
for(let item in options['methods']){
if(!(_isFun(options['methods'][item]))){
return false;
}
}
eleArr.forEach(item=>{
item.addEventListener('click', function(){
options['methods'][item.getAttribute('@click')]();
})
})
return true;
},
addComponent: function(options){
Vue.component('popup',{
template:template,
methods: {
close: function(){
var popupa = this.$refs.popup;
document.body.removeChild(popupa);
// callback close
if(('close' in options) && _isFun(options.close)){
options.close(false);
}
return false;
},
confirm: function(){
var popupa = this.$refs.popup;
document.body.removeChild(popupa);
// callback confirm
if(('confirm' in options) && _isFun(options.confirm)){
if(options.type === 'prompt'){
options.confirm(this.promptValue,this.res);
}else{
options.confirm(true,this.res);
}
}
return true;
},
__initOptions: function(){
// type toast
if(options.type === 'toast'){
let timeout = 1500;
if(('timeout' in options) && !isNaN(Number(options.timeout))){
timeout = options.timeout;
}
let keyframs = [
{
opacity: 1,
},{
opacity: 0,
},
];
setTimeout(()=>{
this.$refs.popup.animate(keyframs,500);
setTimeout(()=>{
this.close();
},500);
},timeout)
}
//mask css --- opacity
if(('opacity' in options) && !isNaN(options.opacity) && options.opacity >=0 && options.opacity <=1){
this.$refs.popup.style.background = 'rgba(0,0,0,'+options.opacity+')';
}
// content style and css
if(('style' in options) && _isObj(options.style)){
for(var i in options.style){
this.$refs.popupcon.style[i] = options.style[i];
}
}
if(('css' in options) && _isArr(options.css)){
options.css.forEach(item=>{
this.$refs.popupcon.classList.add(item);
})
}
// content location
if(('root' in options) !== null && _isEle(options.root)){
let l = methods.location(options.root);
methods.rootOffset(l,this.$refs.popupcon);
}
//content container
if(('html' in options) && options['html']){
}
//bindEvents
this.htmlEvent();
methods.close = this.close;
methods.confirm = this.confirm;
},
htmlEvent: function(){
if(!this.$refs.chtml) return;
let state = methods.complieEvents(this.$refs.chtml,options);
if(!state){
console.error('您未傳入methods對(duì)象,或methods對(duì)象的值有誤');
}
}
},
data: function(){
let obj = popupData;
if(('data' in options) && _isObj(options['data'])){
Object.assign(obj, options['data']);
}
return obj;
},
mounted: function(){
this.__initOptions();
}
})
}
};
const popup = (options) => {
let body = document.body;
let div = document.createElement('div');
div.setAttribute('id','popup');
body.appendChild(div);
/* 初始化data */
methods.initpopupData(options);
/* 判斷類型 并創(chuàng)建template-html模板 */
var ee = methods.template(options.type, options);
if(_isErr(ee)){
console.error(ee.message);
return;
}
/* 判斷是否有data 并查看data中是否有禁止項(xiàng)*/
methods.judgeData(options);
/* 創(chuàng)建popup組件 */
methods.addComponent(options);
if(('slot' in options) && _isObj(options.slot) && options.slot['name'] !== undefined && options.slot['url'] !== undefined){
Vue.component(options.slot['name'], httpVueLoader(options.slot['url']))
}
return new Vue({
render: function(h){
return h('popup',{
scopedSlots: {
content: function(){
return h('div',{
attrs:{
class: 'popup-section'
}
},[h(options.slot.name)])
}
}
})
},
methods: {
close: function(){
methods.close();
},
confirm: function(){
methods.confirm();
}
}
}).$mount('#popup');
};
return popup;
})
有點(diǎn)智障的是怠肋,throw new Error ()
語法 在寫的時(shí)候?qū)懗闪?return new Error()
半天沒反應(yīng)敬鬓。后來跑控制臺(tái)試了下才發(fā)現(xiàn)自己搞錯(cuò)了,原來用的console.error()
,感覺不太聰明的樣子笙各,哈哈哈钉答。當(dāng)做新手練手項(xiàng)了,其實(shí)寫完了才發(fā)現(xiàn)可以改的地方還很多杈抢。我唯一擔(dān)心的是数尿,每次調(diào)用slot
模式時(shí), 都會(huì)執(zhí)行 Vue.component
掛載全局popup
方法惶楼, 還有 return了一個(gè) new Vue
右蹦。但是我暫時(shí)又不知道怎么改比較好,如果有人看到這篇文章歼捐, 能提出一些改進(jìn)意見就更好啦何陆!最后附上寫的 css代碼:
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,.2);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.popup-content {
border-radius: 10px;
border: 1px solid #ccc;
background-color: #eee;
box-shadow: 0 0 4px #555;
color: #333;
padding: 10px;
box-sizing: border-box;
}
.popup-alert,
.popup-confirm,
.popup-prompt,
.popup-toast {
width: 360px;
height: 200px;
}
.popup-prompt-input {
width: 100%;
height: 32px;
line-height: 32px;
padding: 0 4px;
box-sizing: border-box;
border: 1px solid #ccc;
outline: none;
border: 4px;
margin: 6px 0;
}
.popup-slot {
width: 60%;
height: 60%;
}
.popup-tips {
height: calc(100% - 36px);
padding: 10px;
box-sizing: border-box;
}
.popup-options {
display: flex;
justify-content: flex-end;
align-items: center;
height: 36px;
padding: 0 10px;
}
.popup-options .popup-btn-confirm,
.popup-options .popup-btn-close {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #ccc;
outline: none;
background-color: #eee;
color: #fff;
cursor: pointer;
}
.popup-options .popup-btn-confirm {
background-color: #5CB85C;
margin-right: 10px;
color: #fff;
}
.popup-options .popup-btn-close {
color: #333;
}