移动web:Tips消息弹出框
在web开发中经常会用到像alert这样的弹出消息,每个浏览器自带的消息弹出框都不相同。为了统一外观,实现自定义的功能,动手写一个弹出框插件。
对弹出框的实现要求如下:
1. 仿照IOS系统弹出外观
2. 自定义按钮文字
3. 宽高自适应(有最小宽、最大宽限制)
4. 实现弹出框关闭(前、后)的回调 (重点)由于很多操作都是在弹出框显示时操作的
先看下效果:
alert弹出(一个按钮)
confirm弹出(两个按钮)
open弹出(没有按钮)
看完了效果,现在来实现这个消息弹出框。
先贴上代码,仅供参考:
/** * LBS iTips (仿iPhone界面版) * Date: 2015-10-25 * ==================================================================== * 1. 宽高自适应(maxWidth:300 minWidth:250) 引入iTips.css iTips.js (建议js css 放在同一目录) * ==================================================================== * 2. 调用方式1: Tips.alert(option) //显示(确定)按钮 Tips.confirm(option) //显示(确定 取消)按钮 option.after(boolean) boolean布尔值 确定true 取消false Tips.open(option) //无显示按钮 可设置定时关闭 默认不自动关闭需手动关闭 Tips.close() //手动调用关闭 (方式1/方式2都可以调用) * Tips.show(text) // 显示加载提示框 text为弹出文本 默认加载中 * Tips.hide() // 隐藏加载提示框 * ==================================================================== * 3. 调用方式2: Tips.alert(content,fn) //content内容 fn弹出框关闭后执行函数 相当于option.after Tips.confirm(content,fn) //fn(boolean) boolean布尔值 确定true 取消false Tips.open(content, time) //time自动关闭时间(单位秒) 默认不自动关闭需手动关闭 * ==================================================================== * 4. option选项: content:内容(可带html标签自定义样式) before: 点击确定按钮 关闭弹出框前 执行函数 (Tips.alert Tips.confirm中有效) 如果函数返回false 则不会执行(关闭弹出框)和(after) 一般用于做一些检测 after: 点击确定按钮 关闭弹出框后 执行函数 (Tips.alert Tips.confirm中有效) time: 自动关闭时间(单位秒) time 秒后关闭 (Tips.open中有效) define: 定义确定按钮的文本 (Tips.alert Tips.confirm中有效) cancel: 定义取消按钮的文本 (Tips.confirm中有效) * ==================================================================== * Tips.BG //遮罩层 * Tips.Box //弹出框 * Tips.define //确定按钮 * Tips.cancel //取消按钮 * ==================================================================== **/ ;(function() { window.Tips = { _create: function() { if (!this.Box) { var body = document.getElementsByTagName('body')[0], html = '<div id="tips_content"></div><div id="tips_foot"><a href="javascript:;" id="tips_cancel">取消</a><a href="javascript:;" id="tips_define">确定</a></div>'; this.BG = document.createElement('div'); this.BG.id = 'tips_mask'; this.Box = document.createElement('div'); this.Box.id = 'tips_box'; this.Box.innerHTML = html; body.appendChild(this.BG); body.appendChild(this.Box); this.content = this.$('#tips_content'); this.foot = this.$('#tips_foot'); this.define = this.$('#tips_define'); this.cancel = this.$('#tips_cancel'); } }, minWidth: 250, maxWidth: 300, _show: function() { this._fix = true; this.BG.style.display = 'block'; this.Box.style.display = 'block'; this._css(); this._bind(); }, _hide: function() { this._fix = false; this.BG.style.display = 'none'; this.Box.style.display = 'none'; this._unbind(); }, _pos: function() { var d = document, doc = d.documentElement, body = d.body; this.pH = doc.scrollHeight || body.scrollHeight; this.sY = doc.scrollTop || body.scrollTop; this.wW = doc.clientWidth; this.wH = doc.clientHeight; if (document.compatMode != "CSS1Compat") { this.pH = body.scrollHeight; this.sY = body.scrollTop; this.wW = body.clientWidth; this.wH = body.clientHeight; } }, _css: function() { this._pos(); this.BG.style.height = Math.max(this.pH, this.wH) + 'px'; this.Box.style.width = 'auto'; this.content.style.cssText = 'float:left'; var cW = this.content.offsetWidth; this.content.style.cssText = ''; // width max:300 min:200 if (cW < this.minWidth) cW = this.minWidth; if (cW > this.maxWidth) { cW = this.maxWidth; // this.content.style.whiteSpace = ''; this.content.style.whiteSpace = 'normal'; } this.Box.style.width = cW + 'px'; // absolute // this.Box.style.left = (this.wW - cW) / 2 + 'px'; // this.Box.style.top = this.sY + (this.wH - this.Box.offsetHeight) / 2 + 'px'; // fixed 1 // this.Box.style.marginLeft = -(cW / 2) + 'px'; // this.Box.style.marginTop = -(this.Box.offsetHeight / 2) + 'px'; // fixed 2 this.Box.style.marginLeft = -(cW / 2) + 'px'; this.Box.style.top = (this.wH - this.Box.offsetHeight) / 2 + 'px'; }, _fixSize: function() { var _this = this, time = +new Date(); this._timeid && clearInterval(this._timeid); this._timeid = setInterval(function() { if (+new Date() - time > 1000) { clearInterval(_this._timeid); _this._timeid = null; return false; } _this._css(); }, 250); }, _define: function(option) { var _this = this; this.define.onclick = function(e) { e.stopPropagation(); if (typeof option === 'function') { _this._hide(); _this.Bool = true; option && option(_this.Bool); return; } var before = option.before && option.before(); var bool = false; before === false && (bool = true); if (bool) { e.stopPropagation(); return false; } _this._hide(); _this.Bool = true; option.after && option.after(_this.Bool); }; }, _cancel: function(option) { var _this = this; this.cancel.onclick = function(e) { e.stopPropagation(); _this._hide(); _this.Bool = false; if (typeof option === 'function') { option && option(_this.Bool); return; } option.after && option.after(_this.Bool); }; }, _bind: function() { this.Box.focus(); this._setEvent(); }, _unbind: function() { this.Box.blur(); this.define.onclick = null; this.cancel.onclick = null; this.define.innerText = '确定'; this.cancel.innerText = '取消'; this._timer && clearTimeout(this._timer); this._timer = null; this._timeid && clearInterval(this._timeid); this._timeid = null; }, _setEvent: function() { var _this = this; this.on(this.BG, 'touchmove', function(e) { e.preventDefault(); }); this.on(this.Box, 'touchmove', function(e) { e.preventDefault(); }); this.on(this.define, 'touchstart', function(e) { _this.define.className.indexOf('tips_hover') < 0 && (_this.define.className += ' tips_hover'); }); this.on(this.define, 'touchend', function(e) { _this.define.className = _this.define.className.replace('tips_hover', '').trim(); }); this.on(this.cancel, 'touchstart', function(e) { _this.cancel.className.indexOf('tips_hover') < 0 && (_this.cancel.className += ' tips_hover'); }); this.on(this.cancel, 'touchend', function(e) { _this.cancel.className = _this.cancel.className.replace('tips_hover', '').trim(); }); this.on(window, 'resize', function(e) { if (!_this._fix) return; _this._fixSize(); }); }, _setBtn: function(n, option) { this.foot.style.display = 'block'; this.define.style.display = ''; this.cancel.style.display = ''; switch (parseInt(n)) { case 1: this.define.className = 'tips_define'; this.cancel.style.display = 'none'; if (typeof option === 'function') { this.define.innerText = '确定'; this._define(function() { option && option(); }); } else { this.define.innerText = option.define || '确定'; this._define(option); } break; case 2: this.define.className = ''; if (typeof option === 'function') { this.define.innerText = '确定'; this.cancel.innerText = '取消'; this._define(function(b) { option && option(b); }); this._cancel(function(b) { option && option(b); }); } else { this.define.innerText = option.define || '确定'; this.cancel.innerText = option.cancel || '取消'; this._define(option); this._cancel(option); } break; case 0: this.foot.style.display = 'none'; this.define.style.display = 'none'; this.cancel.style.display = 'none'; break; } }, _setContent: function(html) { this.content.innerHTML = html+''; }, _setOption: function(option, n, fn) { var content = ''; this._create(); if (typeof option === 'string' || typeof option === 'number') { content = option || ''; this._setBtn(n, function(b) { fn && fn(b); }); } else { option = option || {}, content = option.content || ''; this._setBtn(n, option); } this._setContent(content); this._show(); }, _setTime: function(option, t) { var time = 0, _this = this; time = (typeof option === 'string' ? t : option.time); if (parseInt(time) > 0) { this._timer = setTimeout(function() { _this._hide(); }, time * 1000); } }, on: function(el, type, handler) { el.addEventListener(type, handler, false); }, off: function(el, type, handler) { el.removeEventListener(type, handler, false); }, $: function(s) { return document.querySelector(s); }, alert: function(option, fn) { this._setOption(option, 1, fn); }, confirm: function(option, fn) { this._setOption(option, 2, function(b) { fn && fn(b); }); }, open: function(option, t) { this._setOption(option, 0); this._setTime(option, t); }, close: function() { this._hide(); } }; }());
第一步创建弹出框基本的html标签,并设定样式。
_create: function() { if (!this.Box) { var body = document.getElementsByTagName('body')[0], html = '<div id="tips_content"></div><div id="tips_foot"><a href="javascript:;" id="tips_cancel">取消</a><a href="javascript:;" id="tips_define">确定</a></div>'; this.BG = document.createElement('div'); this.BG.id = 'tips_mask'; this.Box = document.createElement('div'); this.Box.id = 'tips_box'; this.Box.innerHTML = html; body.appendChild(this.BG); body.appendChild(this.Box); this.content = this.$('#tips_content'); this.foot = this.$('#tips_foot'); this.define = this.$('#tips_define'); this.cancel = this.$('#tips_cancel'); } }
很简单的一个标签布局结构:
<div id="tips_mask"></div> <div id="tips_box"> <div id="tips_content"></div> <div id="tips_foot"> <a href="javascript:;" id="tips_cancel">取消</a> <a href="javascript:;" id="tips_define">确定</a> </div> </div>
相关样式:
#tips_mask, #tips_box *{margin:0;padding:0; -webkit-tap-highlight-color: rgba(0,0,0,0); color: #333; } /* absolute */ /* #tips_mask{display:none;position:absolute;top:0;left:0;z-index:99999;width:100%;height: 100%; background-color:rgba(0,0,0,.3);} #tips_box{display:none;position:absolute; z-index:100000; background:#FFF; font-family: Helvetica,arial,sans-serif; border-radius:10px; animation: bounceIn 0.2s both; -webkit-animation: bounceIn 0.2s both;} */ /* fixed 1*/ /* #tips_mask{display:none;position:fixed;top:0;left:0;z-index:99999;width:100%;height: 100%; background-color:rgba(0,0,0,.3);} #tips_box{display:none;position:fixed; left: 50%; top:50%; z-index:100000; background:#FFF; font-family: Helvetica,arial,sans-serif; border-radius:10px; animation: bounceIn 0.2s both; -webkit-animation: bounceIn 0.2s both;} */ /* fixed 2*/ #tips_mask{display:none;position:fixed;top:0;left:0;z-index:99999;width:100%;height: 100%; background-color:rgba(0,0,0,.5);} #tips_box{display:none;position:fixed; left: 50%; top:0; z-index:100000; background:#FFF; font-family: Helvetica,arial,sans-serif; border-radius:10px; animation: bounceIn 0.2s both; -webkit-animation: bounceIn 0.2s both;} #tips_content{padding:25px 20px; text-align:center; overflow: hidden; font-size:16px; line-height:1.5; white-space: nowrap; /* 不换行 */ border-radius:10px 10px 0 0; } #tips_foot{position:relative; overflow: hidden; height:48px; font-size: 0; line-height:48px; text-align:center; border-top:1px solid #EBEBEB; border-radius:0 0 10px 10px;} #tips_foot a{position:relative; display:inline-block; width:50%; text-align:center; font-size:18px; text-decoration: none; outline: none; color: #0FA5F5; } #tips_foot a:first-child{border-radius:0 0 0 10px; } #tips_foot a:last-child{ border-radius:0 0 10px 0; } #tips_foot a.tips_define{ width: 100%; background: #FFF; border-radius:0 0 10px 10px;} #tips_foot a.tips_hover{ background: #e8e8e8; } #tips_foot:before{content:'';position:absolute; width:1px; height:48px; margin-left: -1px; left:50%; top:0; background:#EBEBEB;} -webkit-keyframes bounceIn{ 0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)} 100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)} } @keyframes bounceIn{ 0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)} 100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)} }
样式中有个属性很重要,这可以让内容容器的文本不换行。
white-space: nowrap;
在PC端版本时不限制宽度很有用,移动端由于设置了最大宽度,当内容超过最大宽度时,设置自动换行。
if (cW > this.maxWidth) { this.content.style.whiteSpace = 'normal'; }
第二步设定内容,并更新位置。
从一个简单的alert弹出消息开始:
// 调用方式1 Tips.alert({ content: 'alert弹出一条消息' }); // 调用方式2 Tips.alert('alert弹出一条消息');
这个弹出经过一个怎样的流程呢?
1. 把内容赋给内容容器
_setContent: function(html) { this.content.innerHTML = html+''; }
2. 修正定位
先获取内容容器的宽度
this.content.style.cssText = 'float:left'; var cW = this.content.offsetWidth; this.content.style.cssText = '';
这里采取把内容容器设为浮动,然后再取消浮动,这种方式获得的宽度是比较实际的,有多少内容就有多少宽,PC端版本采取这种方式也会全兼容。(自适应宽一定要设置内容不换行)
然后是定位,让弹出框在浏览器窗口中垂直居中。
弹出框定位也有几种方式,绝对定位,固定定位。
绝对定位:浏览器滚动条的滚动的距离(scrollTop) 浏览器窗口可视的宽高(clientWidth clientHeight) 弹出框的高(offsetHeight)
固定定位:浏览器窗口可视的宽高(clientWidth clientHeight) 弹出框的高(offsetHeight) (弹出框的宽是内容容器的宽)
// ... this.Box.style.width = 'auto'; // ... this.Box.style.width = cW + 'px'; // absolute // this.Box.style.left = (this.wW - cW) / 2 + 'px'; // this.Box.style.top = this.sY + (this.wH - this.Box.offsetHeight) / 2 + 'px'; // fixed 1 // this.Box.style.marginLeft = -(cW / 2) + 'px'; // this.Box.style.marginTop = -(this.Box.offsetHeight / 2) + 'px'; // fixed 2 this.Box.style.marginLeft = -(cW / 2) + 'px'; this.Box.style.top = (this.wH - this.Box.offsetHeight) / 2 + 'px';
第三步设定按钮,并绑定事件。
这个弹出框插件设定了三种弹出方法,两种调用方式(方法名称可以改成自己想要的名称)。
调用方式1: Tips.alert(option) //显示(确定)按钮 Tips.confirm(option) //显示(确定 取消)按钮 Tips.open(option) //无显示按钮 调用方式2: Tips.alert(content,fn) Tips.confirm(content,fn) Tips.open(content, time)
设置按钮显示和隐藏
_setBtn: function(n, option) { this.foot.style.display = 'block'; this.define.style.display = ''; this.cancel.style.display = ''; switch (parseInt(n)) { case 1: // ... this.cancel.style.display = 'none'; // ... break; case 2: // ... break; case 0: this.foot.style.display = 'none'; this.define.style.display = 'none'; this.cancel.style.display = 'none'; break; } }
设置按钮事件
_define: function(option) { var _this = this; this.define.onclick = function(e) { e.stopPropagation(); if (typeof option === 'function') { _this._hide(); _this.Bool = true; option && option(_this.Bool); return; } var before = option.before && option.before(); var bool = false; before === false && (bool = true); if (bool) { e.stopPropagation(); return false; } _this._hide(); _this.Bool = true; option.after && option.after(_this.Bool); }; }, _cancel: function(option) { var _this = this; this.cancel.onclick = function(e) { e.stopPropagation(); _this._hide(); _this.Bool = false; if (typeof option === 'function') { option && option(_this.Bool); return; } option.after && option.after(_this.Bool); }; }
这里绑定的事件在弹出框显示时有效,弹出框隐藏时会把事件注销掉, 事件绑定和解绑用了偷懒的写法。
xx.onclick = function(){}; xx.onclick = null;
定义好绑定事件的方法,在设置按钮时调用。
_setBtn: function(n, option) { // ... switch (parseInt(n)) { case 1: // ... if (typeof option === 'function') { this.define.innerText = '确定'; this._define(function() { option && option(); }); } else { this.define.innerText = option.define || '确定'; this._define(option); } break; case 2: // ... if (typeof option === 'function') { this.define.innerText = '确定'; this.cancel.innerText = '取消'; this._define(function(b) { option && option(b); }); this._cancel(function(b) { option && option(b); }); } else { this.define.innerText = option.define || '确定'; this.cancel.innerText = option.cancel || '取消'; this._define(option); this._cancel(option); } break; case 0: // ... break; } }
到此弹出框已经基本完成:创建相应的html标签结构,设置样式;根据要弹出的内容更正位置;对按钮绑定事件。
现在看看顶部的 confirm弹出: 购买确认 confirm弹出: 领取红包 这2个弹出是怎样调用的(其实,写这个插件主要目的就是为了实现这2种的调用)。
购买确认:
Tips.$('#alertBtn_06').onclick = function(e){ var price = 100; //单价 var quantity = 500; // 数量 var total = price * quantity; // 总额 Tips.confirm({ content: '<h3>确定要购买吗?</h3><p>您此次购买份数为<strong>'+ quantity +'</strong>份,每份价格<strong>'+ price +'</strong>元,总需<strong>'+ total +'</strong>元</p><div><input type="checkbox" checked="checked" id="isread" name="isread"><label for="isread">我已阅读并同意</label> <a href="http://m.jd.com/" class="a_gmxy">《购买协议》</a><p id="iserror"></p></div>', define: '确定购买', cancel: '不买了', before: function(){ Tips.$('#iserror').innerText = ''; if(!Tips.$('#isread').checked ){ Tips.$('#iserror').innerText = '你还没有同意购买协议'; return false; // 返回false 阻止弹出框关闭 也不会执行after } }, after: function(b){ if(b){ Tips.show('正在处理中'); // 开启加载框 // 在此可以发送ajax请求 // 模拟ajax setTimeout(function(){ Tips.hide(); // ajax请求成功后 关闭加载框 // ajax请求成功后 弹出其他消息 Tips.alert('<h3>恭喜您,购买成功</h3>',function(){ Tips.alert({ define: '知道了,我会跳转的', content: '跳转到交易记录页面' }); // location.href = 'http://m.jd.com/'; }); },1000); } } }); };
领取红包:
Tips.$('#alertBtn_07').onclick = function(e){ // 调用方式1 Tips.confirm({ content: '<h3>输入口令领取红包</h3><div><input type="text" id="str_code" placeholder="请输入口令" style="height:40px;" /><p id="str_error"><p></div>', before: function(){ if( !/^\d{6}$/.test(Tips.$('#str_code').value) ){ Tips.$('#str_error').innerHTML = '口令为6位数字'; Tips.$('#str_code').focus(); return false; } // 发送ajax Tips.show('正在处理中'); //1 显示加载框 // $.ajax({ }); //2 发送ajax请求 // Tips.hide(); // 3 ajax返回结果 隐藏加载框 // Tips.alert('恭喜你,获得<strong>100</strong>元红包!'); //3-1 提示领取成功 // Tips.$('#str_error').innerHTML = '口令有误,请重新输入'; // 3-2 提示领取失败 // 模拟领取成功 setTimeout(function(){ Tips.hide(); Tips.alert('恭喜你,获得<strong>100</strong>元红包!'); },1500); // 模拟领取失败 // setTimeout(function(){ // Tips.hide(); // Tips.$('#str_error').innerHTML = '口令有误,请重新输入'; // },1500); return false; // 始终返回fasle 不让弹出框关闭 } }); // 调用方式2 不支持关闭前 before 执行 };
这两种调用都用到了 before 选项,这个是在点击确定按钮时、关闭隐藏弹出框前执行函数,代码中定义如下:
_define: function(option) { var _this = this; this.define.onclick = function(e) { e.stopPropagation(); if (typeof option === 'function') { _this._hide(); _this.Bool = true; option && option(_this.Bool); return; } var before = option.before && option.before(); var bool = false; before === false && (bool = true); if (bool) { e.stopPropagation(); return false; } _this._hide(); _this.Bool = true; option.after && option.after(_this.Bool); }; }
一个函数执行时,假如没有返回值,一般默认为undefined.
function aa(){} var a_bool = aa(); alert(a_bool) // undefined function bb(){ return false; } var b_bool = bb(); alert(b_bool) // false
利用这一点实现了before选项的功能。
var before = option.before && option.before(); var bool = false; before === false && (bool = true); if (bool) { e.stopPropagation(); return false; }
after选项,就是一般的回调了,在弹出框关闭后执行的函数。
其中有很多可以改进的地方,发挥想象,可以做得更多。