/*
* Jingle v0.4 Copyright (c) 2013 shixy, http://shixy.github.io/Jingle/
* Released under MIT license
* walker.shixy@gmail.com
*/
var Jingle = J = {
version : '0.41',
$ : window.Zepto,
//参数设置
settings : {
//single 单页面工程 muti 多页面工程
appType : 'single',
//page默认动画效果
transitionType : 'slide',
//自定义动画时的默认动画时间(非page转场动画时间)
transitionTime : 250,
//自定义动画时的默认动画函数(非page转场动画函数)
transitionTimingFunc : 'ease-in',
//toast 持续时间,默认为3s
toastDuration : 3000,
//是否显示欢迎界面
showWelcome : false,
//欢迎界面卡片切换时的执行函数,可以制作酷帅吊炸天的欢迎界面
welcomeSlideChange : null,
//加载page模板时,是否显示遮罩
showPageLoading : false,
//page模板默认的相对位置,主要用于开发hybrid应用,实现page的自动装载
basePagePath : 'html/',
basePageSuffix : '.html',
//page模板的远程路径{#id: href,#id: href}
remotePage:{}
},
//手机或者平板
mode : window.innerWidth < 800 ? "phone" : "tablet",
hasTouch : 'ontouchstart' in window,
//是否启动完成
launchCompleted : false,
//是否有打开的侧边菜单
hasMenuOpen : false,
//是否有打开的弹出框
hasPopupOpen : false,
isWebApp : location.protocol == 'http:',
/**
* 启动Jingle
* @param opts {object}
*/
launch : function(opts){
$.extend(this.settings,opts);
var hasShowWelcome = window.localStorage.getItem('hasShowWelcome');
if(!hasShowWelcome && this.settings.showWelcome){
this.Welcome.show();
}
this.Element.init();
this.Element.initControlGroup();
this.Router.init();
this.Menu.init();
}
};
/**
* 初始化页面组件元素
*/
J.Element = (function($){
var SELECTOR = {
'icon' : '[data-icon]',
'scroll' : '[data-scroll="true"]',
'toggle' : '.toggle',
'range' : '[data-rangeinput]',
'progress' : '[data-progress]',
'count' : '[data-count]',
'checkbox' : '[data-checkbox]'
}
/**
* 初始化容器内组件
* @param {String} 父元素的css选择器
* @param {Object} 父元素或者父元素的zepto实例
*/
var init = function(selector){
if(!selector){
//iscroll 必须在元素可见的情况下才能初始化
$(document).on('articleshow','article',function(){
J.Element.scroll(this);
});
};
var $el = $(selector || 'body');
if($el.length == 0)return;
$.map(_getMatchElements($el,SELECTOR.icon),_init_icon);
$.map(_getMatchElements($el,SELECTOR.toggle),_init_toggle);
$.map(_getMatchElements($el,SELECTOR.range),_init_range);
$.map(_getMatchElements($el,SELECTOR.progress),_init_progress);
$.map(_getMatchElements($el,SELECTOR.count),_init_badge);
$.map(_getMatchElements($el,SELECTOR.checkbox),_init_checkbox);
$el = null;
}
/**
* 初始化按钮组(绑定事件)
*/
var initControlGroup = function(){
$(document).on('tap','ul.control-group li',function(){
var $this = $(this);
if($this.hasClass('active'))return;
$this.addClass('active').siblings('.active').removeClass('active').parent().trigger('change',[$this]);
});
}
/**
* 自身与子集相结合
*/
var _getMatchElements = function($el,selector){
return $el.find(selector).add($el.filter(selector));
}
/**
* 初始化iscroll组件或容器内iscroll组件
*/
var initScroll = function(selector){
$.map(_getMatchElements($(selector),SELECTOR.scroll),function(el){J.Scroll(el);});
}
/**
* 构造icon组件
*/
var _init_icon = function(el){
var $el = $(el),$icon=$el.children('i.icon'),icon = $el.data('icon');
if($icon.length > 0){//已经初始化,就更新icon
$icon.attr('class','icon '+icon);
}else{
$el.prepend('');
}
}
/**
* 构造toggle切换组件
*/
var _init_toggle = function(el){
var $el = $(el);
if($el.find('div.toggle-handle').length>0){//已经初始化
return;
}
var name = $el.attr('name');
//添加隐藏域,方便获取值
if(name){
$el.append('');
}
$el.append('
');
$el.tap(function(){
var $t = $(this),v = !$t.hasClass('active');
$t.toggleClass('active').trigger('toggle',[v]);//定义toggle事件
$t.find('input').val(v);
})
}
/**
* 构造range滑块组件
*/
var _init_range = function(el){
var $el = $(el),$input;
var $range = $('input[type="range"]',el);
var align = $el.data('rangeinput');
var input = $('');
if(align == 'left'){
$input = input.prependTo($el);
}else{
$input = input.appendTo($el);
}
var max = parseInt($range.attr('max'),10);
var min = parseInt($range.attr('min'),10);
$range.change(function(){
$input.val($range.val());
});
$input.on('input',function(){
var value = parseInt($input.val(),10);
value = value>max?max:(value').appendTo($el);
}
$bar.width(progress).text(title+progress);
if(progress == '100%'){
$bar.css('border-radius','10px');
}
}
/**
* 构造badge组件
*/
var _init_badge = function(el){
var $el = $(el),$count = $el.find('span.count'),count = parseInt($el.data('count')),
orient = $el.data('orient'), className = (orient == 'left')?'left':'';
if($count.length>0){
$count.text(count).show();//更新数字
}else{
$count = $(''+count+'').appendTo($el);
}
if(count == 0){
$count.hide();
}
}
var _init_checkbox = function(el){
var $el = $(el);
var value = $el.data('checkbox');
if($el.find('i.icon').length>0){
return;
}
$el.prepend('');
$el.on('tap',function(){
var status = ($el.data('checkbox') == 'checked') ? 'unchecked':'checked';
$el.data('checkbox',status).find('i.icon').attr('class','icon checkbox-'+status);
//自定义change事件
$el.trigger('change');
});
}
return {
init : init,
initControlGroup : initControlGroup,
icon : _init_icon,
toggle : _init_toggle,
progress : _init_progress,
range : _init_range,
badge : _init_badge,
scroll : initScroll
}
})(J.$);
/**
* 侧边菜单
*/
J.Menu = (function($){
var $asideContainer,$sectionContainer,$sectionMask;
var init = function(){
$asideContainer = $('#aside_container');
$sectionContainer = $('#section_container');
$sectionMask = $('').appendTo('#section_container');
//添加各种关闭事件
$sectionMask.on('tap',hideMenu);
$asideContainer.on('swipeRight','aside',function(){
if($(this).data('position') == 'right'){
hideMenu();
}
});
$asideContainer.on('swipeLeft','aside',function(){
if($(this).data('position') != 'right'){
hideMenu();
}
});
$asideContainer.on('tap','.aside-close',hideMenu);
}
/**
* 打开侧边菜单
* @param selector css选择器或element实例
*/
var showMenu = function(selector){
var $aside = $(selector).addClass('active'),
transition = $aside.data('transition'),// push overlay reveal
position = $aside.data('position') || 'left',
showClose = $aside.data('show-close'),
width = $aside.width(),
translateX = position == 'left'?width+'px':'-'+width+'px';
if(showClose && $aside.find('div.aside-close').length == 0){
$aside.append('');
}
//aside中可能需要scroll组件
J.Element.scroll($aside);
if(transition == 'overlay'){
J.anim($aside,{translateX : '0%'});
}else if(transition == 'reveal'){
J.anim($sectionContainer,{translateX : translateX});
}else{//默认为push
J.anim($aside,{translateX : '0%'});
J.anim($sectionContainer,{translateX : translateX});
}
$('#section_container_mask').show();
J.hasMenuOpen = true;
}
/**
* 关闭侧边菜单
* @param duration {int} 动画持续时间
* @param callback 动画完毕回调函数
*/
var hideMenu = function(duration,callback){
var $aside = $('#aside_container aside.active'),
transition = $aside.data('transition'),// push overlay reveal
position = $aside.data('position') || 'left',
translateX = position == 'left'?'-100%':'100%';
var _finishTransition = function(){
$aside.removeClass('active');
J.hasMenuOpen = false;
callback && callback.call(this);
};
if(transition == 'overlay'){
J.anim($aside,{translateX : translateX},duration,_finishTransition);
}else if(transition == 'reveal'){
J.anim($sectionContainer,{translateX : '0'},duration,_finishTransition);
}else{//默认为push
J.anim($aside,{translateX : translateX},duration);
J.anim($sectionContainer,{translateX : '0'},duration,_finishTransition);
}
$('#section_container_mask').hide();
}
return {
init : init,
show : showMenu,
hide : hideMenu
}
})(J.$);
/**
* section 页面远程加载
*/
J.Page = (function($){
var _formatHash = function(hash){
return hash.indexOf('#') == 0 ? hash.substr(1) : hash;
}
/**
* 加载section模板
* @param {string} hash信息
* @param {string} url参数
*/
var loadSectionTpl = function(hash,callback){
var param = {},query,replaceSection = false;
if($.type(hash) == 'object'){
param = hash.param;
query = hash.query;
hash = hash.tag;
}
var q = $(hash).data('query');
//已经存在则直接跳转到对应的页面
if($(hash).length == 1){
if(q == query){
callback();
return;
}else{
replaceSection = true;
}
}
var id = _formatHash(hash);
//当前dom中不存在,需要从服务端加载
var url = J.settings.remotePage[hash];
//检查remotePage中是否有配置,没有则自动从basePagePath中装载模板
url || (url = J.settings.basePagePath+id+J.settings.basePageSuffix);
J.settings.showPageLoading && J.showMask();
loadContent(url,param,function(html){
J.settings.showPageLoading && J.hideMask();
//添加到dom树中
$(hash).remove();
var $h = $(html);
$('#section_container').append($h);
if(replaceSection){
$h.addClass('active');
}
//触发pageload事件
$h.trigger('pageload').data('query',query);
//构造组件
J.Element.init(hash);
callback();
$h = null;
});
}
var loadSectionRemote = function(url,section){
var param = J.Util.parseHash(window.location.hash).param;
loadContent(url,param,function(html){
$(section).html(html);
J.Element.init(section);
});
}
/**
* 加载文档片段
* @param url
*/
var loadContent = function(url,param,callback){
return $.ajax({
url : url,
timeout : 20000,
data : param,
success : function(html){
callback && callback(html);
}
});
}
return {
load : loadSectionTpl,
loadSection : loadSectionRemote,
loadContent : loadContent
}
})(J.$);
/**
* 路由控制器
*/
J.Router = (function($){
var _history = [];
/**
* 初始化events、state
*/
var init = function(){
$(window).on('popstate', _popstateHandler);
//阻止含data-target或者href以'#'开头的的a元素的默认行为
$(document).on('click','a',function(e){
var target = $(this).data('target'),
href = $(this).attr('href');
if(!href || href.match(/^#/) || target){
e.preventDefault();
return false;
}
});
$(document).on('tap','a[data-target]',_targetHandler);
_initIndex();
}
//处理app页面初始化
var _initIndex = function(){
var targetHash = location.hash;
//取页面中第一个section作为app的起始页
var $section = $('#section_container section').first();
var indexHash = '#'+$section.attr('id');
_add2History(indexHash,true);
if(targetHash != '' && targetHash != indexHash){
_showSection(targetHash);//跳转到指定的页面
}else{
$section.trigger('pageinit').trigger('pageshow').data('init',true).find('article.active').trigger('articleshow');
}
}
/**
* 处理浏览器的后退事件
* 前进事件不做处理
* @private
*/
var _popstateHandler = function(e){
if(e.state && e.state.hash){
var hash = e.state.hash;
if(_history[1] && hash === _history[1].hash){//存在历史记录,证明是后退事件
J.hasMenuOpen && J.Menu.hide();//关闭当前页面的菜单
J.hasPopupOpen && J.Popup.close();//关闭当前页面的弹出窗口
back();
}else{//其他认为是非法后退或者前进
return;
}
}else{
return;
}
}
var _targetHandler = function(){
var _this = $(this),
target = _this.attr('data-target'),
href = _this.attr('href');
switch(target){
case 'section' :
if(J.settings.appType == 'single'){
_showSection(href);
}
break;
case 'article' :
_showArticle(href,_this);
break;
case 'menu' :
_toggleMenu(href);
break;
case 'back' :
window.history.go(-1);
break;
}
}
/**
* 跳转到新页面
* @param hash 新page的'#id'
*/
var _showSection = function(hash){
if(J.hasMenuOpen){//关闭菜单后再转场
J.Menu.hide(200,function(){
_showSection(hash);
});
return;
}
//读取hash信息
var hashObj = J.Util.parseHash(hash);
var current = _history[0];
//同一个页面,则不重新加载
if(current.hash === hashObj.hash){
return;
}
//加载模板
J.Page.load(hashObj,function(){
var sameSection = (current.tag == hashObj.tag);
if(sameSection){//相同页面,触发相关事件
$(current.tag).trigger('pageshow').find('article.active').trigger('articlehide');
}else{//不同卡片页跳转动画
_changePage(current.tag,hashObj.tag);
}
_add2History(hash,sameSection);
});
}
/**
* 后退
*/
var back = function(){
if(J.settings.appType == 'single'){
_changePage(_history.shift().tag,_history[0].tag,true)
}
}
var _changePage = function(current,target,isBack){
J.Transition.run(current,target,isBack);
}
/**
* 缓存访问记录
*/
var _add2History = function(hash,noState){
var hashObj = J.Util.parseHash(hash);
if(noState){//不添加浏览器历史记录
_history.shift(hashObj);
window.history.replaceState(hashObj,'',hash);
}else{
window.history.pushState(hashObj,'',hash);
}
_history.unshift(hashObj);
}
/**
* 激活href对应的article
* @param href #id
* @param el 当前锚点
*/
var _showArticle = function(href,el){
var article = $(href);
if(article.hasClass('active'))return;
el.addClass('active').siblings('.active').removeClass('active');
var activeArticle = article.addClass('active').siblings('.active').removeClass('active');
article.trigger('articleshow');
activeArticle.trigger('articlehide');
}
var _toggleMenu = function(hash){
J.hasMenuOpen?J.Menu.hide():J.Menu.show(hash);
}
return {
init : init,
goTo : _showSection,
showArticle : _showArticle,
back : back
}
})(J.$);
/**
* 对zeptojs的ajax进行封装,实现离线访问
* 推荐纯数据的ajax请求调用本方法,其他的依旧使用zeptojs自己的ajax
* @Deprecated 用J.Cache代替
*/
J.Service = (function($){
var UNPOST_KEY = 'JINGLE_POST_DATA',
GET_KEY_PREFIX = 'JINGLE_GET_';
var ajax = function(options){
if(options.type == 'post'){
_doPost(options);
}else{
_doGet(options);
}
}
var _doPost = function(options){
if(J.offline){//离线模式,将数据存到本地,连线时进行提交
_setUnPostData(options.url,options.data);
options.success('数据已存至本地');
}else{//在线模式,直接提交
$.ajax(options);
}
}
var _doGet = function(options){
var key = options.url +JSON.stringify(options.data);
if(J.offline){//离线模式,直接从本地读取
var result = _getCache(key);
if(result){
options.success(result.data,key,result.cacheTime);
}else{//未缓存该数据
options.success(result);
}
}else{//在线模式,将数据保存到本地
var callback = options.success;
options.success = function(result){
_saveData2local(key,result);
callback(result,key);
}
$.ajax(options);
}
}
/**
* 获取本地已缓存的数据
* @private
*/
var _getCache = function(key){
return JSON.parse(window.localStorage.getItem(GET_KEY_PREFIX+key));
}
/**
* 缓存数据到本地
* @private
*/
var _saveData2local = function(key,result){
var data = {
data : result,
cacheTime : new Date()
}
window.localStorage.setItem(GET_KEY_PREFIX+key,JSON.stringify(data));
}
/**
* 将post的数据保存至本地
* @param url
* @param result
* @private
*/
var _setUnPostData = function(url,result){
var data = getUnPostData();
data = data || {};
data[url] = {
data : result,
createdTime : new Date()
}
window.localStorage.setItem(UNPOST_KEY,JSON.stringify(data));
}
/**
* 获取尚未同步的post数据
* @param url 没有就返回所有未同步的数据
*/
var getUnPostData = function(url){
var data = JSON.parse(window.localStorage.getItem(UNPOST_KEY));
return (data && url ) ? data[url] : data;
}
/**
* 移除未同步的数据
* @param url 没有就移除所有未同步的数据
*/
var removeUnPostData = function(url){
if(url){
var data = getUnPostData();
delete data[url];
window.localStorage.setItem(UNPOST_KEY,JSON.stringify(data));
}else{
window.localStorage.removeItem(UNPOST_KEY);
}
}
/**
* 同步本地缓存的post数据
* @param url
*/
var syncPostData = function(url,success,error){
var unPostData = getUnPostData(url).data;
$.ajax({
url : url,
contentType:'application/json',
data : unPostData,
type : 'post',
success : function(){
success(url);
},
error : function(){
error(url);
}
})
}
/**
* 同步所有的数据
* @param callback
*/
var syncAllPostData = function(success,error){
var unPostData = getUnPostData();
for(var url in unPostData){
syncPostData(url,success,error);
}
removeUnPostData();
}
//copy from zepto
function parseArguments(url, data, success, dataType) {
var hasData = !$.isFunction(data)
return {
url: url,
data: hasData ? data : undefined,
success: !hasData ? data : $.isFunction(success) ? success : undefined,
dataType: hasData ? dataType || success : success
}
}
var get = function(url, data, success, dataType){
return ajax(parseArguments.apply(null, arguments))
}
var post = function(url, data, success, dataType){
var options = parseArguments.apply(null, arguments)
options.type = 'POST'
return ajax(options)
}
var getJSON = function(url, data, success){
var options = parseArguments.apply(null, arguments);
options.dataType = 'json'
return ajax(options)
}
var clear = function(){
var storage = window.localStorage;
var keys = [];
for(var i = 0; i< storage.length; i++){
var key = storage.key(i);
key.indexOf(GET_KEY_PREFIX) == 0 && keys.push(key);
}
for(var i = 0; i < keys.length; i++){
storage.removeItem(keys[i]);
}
storage.removeItem(UNPOST_KEY);
}
return {
ajax : ajax,
get : get,
post : post,
getJSON : getJSON,
getUnPostData : getUnPostData,
removeUnPostData : removeUnPostData,
syncPostData : syncPostData,
syncAllPostData : syncAllPostData,
getCacheData : _getCache,
saveCacheData : _saveData2local,
clear : clear
}
})(J.$);
/**
* 提供一些简单的模板,及artTemplate的渲染
*/
J.Template = (function($){
/**
* 背景模板
* @param el selector
* @param title 显示文本
* @param icon 图标
*/
var background = function(el,title,icon){
var markup = '';
$(el).html(markup);
}
/**
* 无记录背景模板
* @param el
*/
var no_result = function(el){
background(el,'没有找到相关数据','drawer');
}
/**
* 加载等待背景模板
* @param el
*/
var loading = function(el){
background(el,'加载中...','cloud-download');
}
/**
* 借助artTemplate模板来渲染页面
* @param containerSelector 目标容器
* @param templateId artTemplate模板ID
* @param data 模板数据
* @param type replace|add 渲染好的文档片段是替换还是添加到目标容器中
*/
var render = function(containerSelector,templateId,data,type){
var el = $(containerSelector),
type = type || 'replace';//replace add
if($.type(data) == 'array' && data.length == 0 ){
no_result(el);
}else{
var html = template(templateId,data);
if(type == 'replace'){
el.html(html);
}else{
el.append(html);
}
J.Element.init(html);
}
}
return {
render : render,
background : background,
loading : loading,
no_result : no_result
}
})(J.$);
/**
* 消息组件
*/
J.Toast = (function($){
var toast_type = 'toast',_toast,timer,
//定义模板
TEMPLATE = {
toast : '{value}',
success : '{value}',
error : '{value}',
info : '{value}'
}
var _init = function(){
//全局只有一个实例
$('body').append('');
_toast = $('#jingle_toast');
_subscribeCloseTag();
}
/**
* 关闭消息提示
*/
var hide = function(){
J.anim(_toast,'scaleOut',function(){
_toast.hide();
_toast.empty();
});
}
/**
* 显示消息提示
* @param type 类型 toast|success|error|info 空格 + class name 可以实现自定义样式
* @param text 文字内容
* @param duration 持续时间 为0则不自动关闭,默认为3000ms
*/
var show = function(type,text,duration){
if(timer) clearTimeout(timer);
var classname = type.split(/\s/);
toast_type = classname[0];
_toast.attr('class',type).html(TEMPLATE[toast_type].replace('{value}',text)).show();
J.anim(_toast,'scaleIn');
if(duration !== 0){//为0 不自动关闭
timer = setTimeout(hide,duration || J.settings.toastDuration);
}
}
var _subscribeCloseTag = function(){
_toast.on('tap','[data-target="close"]',function(){
hide();
})
}
_init();
return {
show : show,
hide : hide
}
})(J.$);
/**
* page转场动画
* 可自定义css动画
*/
J.Transition = (function($){
var isBack,$current,$target,transitionName,
animationClass = {
//[[currentOut,targetIn],[currentOut,targetIn]]
slide : [['slideLeftOut','slideLeftIn'],['slideRightOut','slideRightIn']],
cover : [['','slideLeftIn'],['slideRightOut','']],
slideUp : [['','slideUpIn'],['slideDownOut','']],
slideDown : [['','slideDownIn'],['slideUpOut','']],
popup : [['','scaleIn'],['scaleOut','']]
};
var _doTransition = function(){
//触发 beforepagehide 事件
$current.trigger('beforepagehide',[isBack]);
//触发 beforepageshow 事件
$target.trigger('beforepageshow',[isBack]);
var c_class = transitionName[0]||'empty' ,t_class = transitionName[1]||'empty';
$current.bind('webkitAnimationEnd.jingle', _finishTransition).addClass('anim '+ c_class);
$target.addClass('anim animating '+ t_class);
}
var _finishTransition = function() {
$current.off('webkitAnimationEnd.jingle');
$target.off('webkitAnimationEnd.jingle');
//reset class
$current.attr('class','');
$target.attr('class','active');
//add custom events
!$target.data('init') && $target.trigger('pageinit').data('init',true);
!$current.data('init') && $current.trigger('pageinit').data('init',true);
//触发pagehide事件
$current.trigger('pagehide',[isBack]);
//触发pageshow事件
$target.trigger('pageshow',[isBack]);
$current.find('article.active').trigger('articlehide');
$target.find('article.active').trigger('articleshow');
$current = $target = null;//释放
}
/**
* 执行转场动画,动画类型取决于目标page上动画配置(返回时取决于当前page)
* @param current 当前page
* @param target 目标page
* @param back 是否为后退
*/
var run = function(current,target,back){
//关闭键盘
$(':focus').trigger('blur');
isBack = back;
$current = $(current);
$target = $(target);
var type = isBack?$current.attr('data-transition'):$target.attr('data-transition');
type = type|| J.settings.transitionType;
//后退时取相反的动画效果组
transitionName = isBack ? animationClass[type][1] : animationClass[type][0];
_doTransition();
}
/**
* 添加自定义转场动画效果
* @param name 动画名称
* @param currentOut 正常情况下当前页面退去的动画class
* @param targetIn 正常情况下目标页面进入的动画class
* @param backCurrentOut 后退情况下当前页面退去的动画class
* @param backCurrentIn 后退情况下目标页面进入的动画class
*/
var addAnimation = function(name,currentOut,targetIn,backCurrentOut,backCurrentIn){
if(animationClass[name]){
console.error('该转场动画已经存在,请检查你自定义的动画名称(名称不能重复)');
return;
}
animationClass[name] = [[currentOut,targetIn],[backCurrentOut,backCurrentIn]];
}
return {
run : run,
add : addAnimation
}
})(J.$);
/**
* 常用工具类
*/
J.Util = (function($){
var parseHash = function(hash){
var tag,query,param={};
var arr = hash.split('?');
tag = arr[0];
if(arr.length>1){
var seg,s;
query = arr[1];
seg = query.split('&');
for(var i=0;i