定义方法调用格式
我发现在写项目的时候,经常会用到 alert
、confirm
、toast
之类的弹出框。但是原生alert
对话框的样式已经定型,和UI设计又不一样,再加上弹出面板真的出现频率太多了,于是自己动手写了一个动态弹窗组件。
首先,我先定义了一下,假设已有此组件,我要怎么调用:
popup({
type:'alert',
content: '这是一个警告框!'
})
于是就有了大概想法,首先这个组件是通过一个方法调用的,其次,这个方法得有一个对象作为自己的参数,对象中的type
用来定义当前面板的类型。
经过一系列测试后,最终定下来以下类型,并列出此popup应该有的功能
/**
* alert confirm prompt toast slot
*/
- 默认居中、有根元素传入时,按照根元素的位置进行定位
- 可以设置弹出面板大小及其他样式
- 可以设置面板css类,修改蒙版的不透明度
- 具有confirm和close函数回调
- 面板内容可以自定义,slot模式时, 可以选择内嵌 组件或者html元素
- 当面板的自定义内容为html元素时,支持绑定事件
调用confirm
popup({
type: 'confirm',
content: '你确定要删除我吗?',
confirm: function() {
// TODO
},
close: function() {
// TODO
}
})
调用prompt
popup({
type: 'prompt',
content: '请输入内容:',
confirm: function(str) {
// TODO
console.log(str);
},
close: function() {
// TODO
}
})
调用toast
popup({
type: 'toast',
content: '我是一个定时提示框!',
timeout: 3000 // 不传 timeout时,默认时长2秒
})
调用slot 自定义面板
popup({
type: 'slot', // 自定义组件模式
slot: {
name: 'myComponent',
url: 'views/components/myComponent.vue' //此项可选,有时将自动加载为全局组件
}
})
popup({
type: 'slot', // html模式
html: true,
innerHTML : `
这是我定义的html内容·
`,
methods: {
logMsg: function(){
console.log('this is a event')
}
}
})
大概定的调用方式就是上面这样了,但是除此外还需要几个属性来修饰一下:
popup({
type: 'alert',
content: '这是一个警告框'
root: document.querySelector('.msg'), // 面板按照此元素在页面中的位置进行定位
opacity: 0.1, // 蒙版的不透明度,当为0 时 完全透明,为1时背景全黑
style: { // 此属性的值将作用于面板上
'width': '200px',
'height': '100px'
},
css: ['change1','change2'] //此属性接收一个数组,数组值作为class类绑定到面板上,这个是为了写复杂的样式时,避免写过长的style属性准备的
})
莽代码
先来定义一个大概代码框架
(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(){
/* 以下我们定义了一个封闭作用域用来编写我们的popup函数需要用到的其他变量 判断和 处理的函数 */
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 typeof fun === 'function';
};
function _isErr(err){
return _toString.call(err).slice(8,-1) === 'Error';
};
let methods = {
location: function(ele) {
// 计算传入element元素在页面上的位置
...
},
initpopupData: function(options){
...
},
template: function(type,options){
// 通过type 创建对应的 template div
if(type){
...
}
template = `...`
},
rootOffset: function(obj,ele){
// 计算 面板位置偏移量
...
},
judgeData: function(options){
// 判断popupData 合法性
},
complieEvents: function(ele,options){
// 编译元素 查询绑定事件的DOM
...
},
addComponent: function(options){
// 添加Vue popup全局组件
...
}
}
const popup = (options) => {
/* 初始化data */
methods.initpopupData(options);
/* 判断是否有data 并查看data中是否有禁止项*/
methods.judgeData(options);
/* 创建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'] :
'请设置一个innerHTML属性,指向自定义容器内容'
};
},
template: function(type,options){
if(type !== 'slot') {
if(!('content' in options)) return new Error('不存在content属性,请添加此字段,字段的值为显示内容!');
}
template = `
${options.content}
`
},
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){
// 面板右侧溢出
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属性值,请更换名字后重试!');
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对象,或methods对象的值有误');
}
}
},
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);
/* 判断类型 并创建template-html模板 */
var ee = methods.template(options.type, options);
if(_isErr(ee)){
console.error(ee.message);
return;
}
/* 判断是否有data 并查看data中是否有禁止项*/
methods.judgeData(options);
/* 创建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;
})
有点智障的是,throw new Error ()
语法 在写的时候写成了 return new Error()
半天没反应。后来跑控制台试了下才发现自己搞错了,原来用的console.error()
,感觉不太聪明的样子,哈哈哈。当做新手练手项了,其实写完了才发现可以改的地方还很多。我唯一担心的是,每次调用slot
模式时, 都会执行 Vue.component
挂载全局popup
方法, 还有 return了一个 new Vue
。但是我暂时又不知道怎么改比较好,如果有人看到这篇文章, 能提出一些改进意见就更好啦!最后附上写的 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;
}