/**
* appframework.ui - A User Interface library for App Framework applications
*
* @copyright 2011-2014 Intel
* @author Intel
* @version 3.0
*/
/* global FastClick*/
/* jshint camelcase:false*/
(function($) {
"use strict";
var startPath = window.location.pathname + window.location.search;
var defaultHash = window.location.hash;
var previousTarget = defaultHash;
var AFUi = function() {
// Init the page
var that = this;
if (typeof define === "function" && define.amd) {
that.autoLaunch=false;
define("appframeworkui", [], function() {
return $.afui;
});
} else if (typeof module !== "undefined" && module.exports) {
that.autoLaunch=false;
$.afui = that;
}
var setupAFDom = function() {
setupCustomTheme();
if(window.FastClick)
FastClick.attach(document.documentElement);
};
if (document.readyState === "complete" || document.readyState === "loaded") {
setupAFDom();
if(that.init)
that.autoBoot();
else{
$(window).one("afui:init", function() {
that.autoBoot();
});
}
} else $(document).ready(
function() {
setupAFDom();
if(that.init)
that.autoBoot();
else{
$(window).one("afui:init", function() {
that.autoBoot();
});
}
},
false);
//click back event
window.addEventListener("popstate", function() {
if(!that.useInternalRouting) return;
var id = that.getPanelId(document.location.hash);
var hashChecker = document.location.href.replace(document.location.origin + "/", "");
//make sure we allow hash changes outside afui
if (hashChecker === "#") return;
if (id === "" && that.history.length === 1) //Fix going back to first panel and an empty hash
id = "#" + that.firstPanel.id;
if (id === "") return;
if (af(id).filter(".panel").length === 0) return;
if (id !== "#" + that.activeDiv.id) that.goBack();
}, false);
window.addEventListener("orientationchange",function(){
window.scrollTo(0, 0);
});
function setupCustomTheme() {
if (that.useOSThemes) {
var $el=$(document.body);
$el.removeClass("ios ios7 win8 tizen bb android light dark firefox");
if ($.os.android)
$el.addClass("android");
else if ($.os.ie) {
$el.addClass("win8");
} else if ($.os.blackberry||$.os.blackberry10||$.os.playbook) {
$el.addClass("bb");
that.backButtonText = "Back";
} else if ($.os.ios7){
$el.addClass("ios7");
} else if ($.os.ios)
$el.addClass("ios7");
else if($.os.tizen)
$el.addClass("tizen");
else if($.os.fennec)
$el.addClass("firefox");
}
if($.os.ios7&&that.overlayStatusbar){
that.ready(function(){
$(document.body).addClass("overlayStatusbar");
});
}
}
};
var clickHandlers=[];
AFUi.prototype = {
init:false,
showLoading: true,
loadingText: "Loading Content",
remotePages: {},
history: [],
views:{},
_readyFunc: null,
doingTransition: false,
ajaxUrl: "",
transitionType: "slide",
firstPanel: "",
hasLaunched: false,
isLaunching:false,
launchCompleted: false,
activeDiv: "",
customClickHandler: "",
useOSThemes: true,
overlayStatusbar: false,
useAutoPressed: true,
useInternalRouting:true,
autoBoot: function() {
this.hasLaunched = true;
if (this.autoLaunch) {
this.launch();
}
},
/**
* This blocks the page from bouncing on iOS
* @api private
*/
blockPageBounce:function(enable){
if(enable===false){
window.removeEventListener("touchmove",this.handlePageBounce,false);
window.removeEventListener("touchstart",this.handlePageBounce,false);
return;
}
window.addEventListener("touchmove",this.handlePageBounce,false);
window.addEventListener("touchstart",this.handlePageBounce,false);
},
/**
* Handle touch events for scrolling divs
* @api private
*/
handlePageBounce:function(evt){
if(evt.type==="touchstart"){
this._startTouchY=evt.touches[0].screenY;
return;
}
var panel=$(evt.target).closest(".panel");
if(panel.length===0) return evt.preventDefault();
var el=panel.get(0);
var canScroll=el.scrollHeight>el.clientHeight;
var hasTouchOverflow=$(el).computedStyle("-webkit-overflow-scrolling")==="touch";
var hasOverflow=$(el).computedStyle("overflowY")!=="hidden";
var height=parseInt($(el).computedStyle("height"),10);
if(canScroll&&hasTouchOverflow&&hasOverflow){
var currY=evt.touches[0].screenY;
var scrollAtTop=((this._startTouchY<=currY)&&(el.scrollTop===0));
var scrollAtBottom=((this._startTouchY>=currY)&&((el.scrollHeight-el.scrollTop)===height));
if(scrollAtTop||scrollAtBottom)
evt.preventDefault();
return;
}
},
/**
* Register a data directive for a click event. Plugins use this to allow
* html based execution (see af.popup.js)
```
$.afui.registerDataDirective("[data-foo]",function(){
console.log("foo was clicked");
})
```
* @param {string} selector
* @param {function} callback to execute
* @title $.afui.registerDataDirective
*/
registerDataDirective:function(selector,cb){
clickHandlers.push({sel:selector,cb:cb});
},
/**
* This enables the tab bar ability to keep pressed states on elements
```
$.afui.enableTabBar();
```
@title $.afui.enableTabBar
*/
enableTabBar:function(){
$(document).on("click",".button-grouped.tabbed",function(e){
var $el=$(e.target);
$el.closest(".tabbed").find(".button").data("ignore-pressed","true").removeClass("pressed");
//this is a hack, but the touchEvents plugin will remove pressed
$el.closest(".button").addClass("pressed");
setTimeout(function(){
$el.closest(".button").addClass("pressed");
});
});
},
/**
* This disables the tab bar ability to keep pressed states on elements
```
$.afui.disableTabBar();
```
* @title $.afui.disableTabBar
*/
disableTabBar:function(){
$(document).off("click",".button-grouped.tabbed");
$(".button-grouped.tabbed .button").removeAttr("data-ignore-pressed");
},
/**
* This is a boolean property. When set to true, we manage history and update the hash
```
$.afui.manageHistory=false;//Don't manage for apps using Backbone
```
*@title $.afui.manageHistory
*/
manageHistory: true,
/**
* This is a boolean property. When set to true (default) it will load that panel when the app is started
```
$.afui.loadDefaultHash=false; //Never load the page from the hash when the app is started
$.afui.loadDefaultHash=true; //Default
```
*@title $.afui.loadDefaultHash
*/
loadDefaultHash: true,
/**
* This is a shorthand call to the $.actionsheet plugin. We wire it to the afui div automatically
```
$.afui.actionsheet("SettingsLogout")
$.afui.actionsheet("[{
text: 'back',
cssClasses: 'red',
handler: function () { $.afui.goBack(); ; }
}, {
text: 'show alert 5',
cssClasses: 'blue',
handler: function () { alert("hi"); }
}, {
text: 'show alert 6',
cssClasses: '',
handler: function () { alert("goodbye"); }
}]");
```
* @param {(string|Array.)} links
* @title $.afui.actionsheet()
*/
actionsheet: function(opts) {
return $.query(document.body).actionsheet(opts);
},
/**
* This is a wrapper to $.popup.js plugin. If you pass in a text string, it acts like an alert box and just gives a message
```
$.afui.popup(opts);
$.afui.popup( {
title:"Alert! Alert!",
message:"This is a test of the emergency alert system!! Don't PANIC!",
cancelText:"Cancel me",
cancelCallback: function(){console.log("cancelled");},
doneText:"I'm done!",
doneCallback: function(){console.log("Done for!");},
cancelOnly:false
});
$.afui.popup('Hi there');
```
* @param {(object|string)} options
* @title $.afui.popup(opts)
*/
popup: function(opts) {
return $.query(document.body).popup(opts);
},
/**
* This is a reference to the drawer plugin.
```
$.afui.drawer.show('#left','left','reveal')
```
* @param {string} id of drawer
* @param {string} position (left|right)
* @param {string} transition (push, cover, reveal)
* @title $.afui.drawer.show
*/
/**
* This is a reference to the drawer plugin.
```
$.afui.drawer.hide('#left','left')
```
* @param {string} id of drawer
* @param {string} position (left|right)
* @title $.afui.drawer.hide
*/
/**
*This will throw up a mask and block the UI
```
$.afui.blockUI(.9)
````
* @param {number} opacity
* @title $.afui.blockUI(opacity)
*/
blockUI: function(opacity) {
$.blockUI(opacity);
},
/**
*This will remove the UI mask
```
$.afui.unblockUI()
````
* @title $.afui.unblockUI()
*/
unblockUI: function() {
$.unblockUI();
},
/**
* Boolean if you want to auto launch afui
```
$.afui.autoLaunch = false; //
```
* @title $.afui.autoLaunch
*/
autoLaunch: true,
/**
* function to fire when afui is ready and completed launch
```
$.afui.ready(function(){console.log('afui is ready');});
```
* @param {function} param function to execute
* @title $.afui.ready
*/
ready: function(param) {
if (this.launchCompleted)
param();
else {
$(document).one("afui:ready", function() {
param();
});
}
},
/**
* Initiate a back transition
```
$.afui.goBack()
```
* @title $.afui.goBack()
* @param {number=} delta relative position from the last element (> 0)
*/
goBack: function(e) {
//find the view
var view=$(this.activeDiv).closest(".view");
if(e&&e.target)
view=$(e.target).closest(".view");
if(view.length===0) return;
//history entry
if(!this.views[view.prop("id")]) return;
var hist=this.views[view.prop("id")];
if(hist.length===0) return;
var item=hist.pop();
if(item.length===0) return;
if(hist.length>0){
var toTarget=hist[hist.length-1].target;
if(!toTarget||item.target===toTarget) return;
this.runTransition(item.transition,item.target,toTarget,true);
this.loadContentData(toTarget,view,true);
this.updateHash(toTarget.id);
}
else {
//try to dismiss the view
try{
this.dismissView(item.target,item.transition);
}
catch(ex){
}
}
},
/**
* Clear the history queue for the current view based off the active div
```
$.afui.clearHistory()
```
* @title $.afui.clearHistory()
*/
clearHistory: function() {
//find the view
var view=this.findViewTarget(this.activeDiv);
this.views[view.prop("id")]=[];
this.setBackButtonVisibility(false);
},
/**
* PushHistory
```
$.afui.pushHistory(previousPage, newPage, transition, hashExtras)
```
* @api private
* @title $.afui.pushHistory()
*/
pushHistory: function(previousPage, newPage, transition, hashExtras) {
//push into local history
//push into the browser history
try {
if (!this.manageHistory) return;
window.history.pushState(newPage, newPage, startPath + "#" + newPage + hashExtras);
$(window).trigger("hashchange", null, {
newUrl: startPath + "#" + newPage + hashExtras,
oldUrl: startPath + previousPage
});
} catch (e) {}
},
/**
* Updates the current window hash
*
* @param {string} newHash New Hash value
* @title $.afui.updateHash(newHash)
* @api private
*/
updateHash: function(newHash) {
if (!this.manageHistory) return;
newHash = newHash.indexOf("#") === -1 ? "#" + newHash : newHash; //force having the # in the begginning as a standard
previousTarget = newHash;
var previousHash = window.location.hash;
var panelName = this.getPanelId(newHash).substring(1); //remove the #
try {
window.history.replaceState(panelName, panelName, startPath + newHash);
$(window).trigger("hashchange", null, {
newUrl: startPath + newHash,
oldUrl: startPath + previousHash
});
} catch (e) {}
},
/**
* gets the panel name from an hash
* @api private
*/
getPanelId: function(hash) {
var firstSlash = hash.indexOf("/");
return firstSlash === -1 ? hash : hash.substring(0, firstSlash);
},
/**
* set the back button text
```
$.afui.setBackButtonText("about");
```
* @param {string} text
* @title $.afui.setBackButtonText(title)
*/
setBackButtonText:function(text){
$(this.activeDiv).closest(".view").find("header .backButton").html(text);
},
/**
* Set the title of the active header from
```
$.afui.setTitle("New Title")
```
* @param {string|object} String or DOM node to get the title from
* @title $.afui.setTitle
*/
setTitle:function(item){
//find the header
var title="";
if(typeof(item)==="string"){
title=item;
item=$(this.activeDiv).closest(".view");
}
else if($(item).attr("data-title"))
title=$(item).attr("data-title");
else if($(item).attr("title"))
title=$(item).attr("title");
if(title)
$(item).closest(".view").children("header").find("h1").html(title);
},
/**
* Get the title of the active header
```
$.afui.getTitle()
```
* @title $.afui.getTitle
*/
getTitle:function(){
return $(this.activeDiv).closest(".view").children("header").find("h1").html();
},
/**
* Set the visibility of the back button for the current header
```
$.afui.setBackButtonVisbility(true)
```
* @param {boolean} Boolean to set the visibility. true show, false hide
* @title $.afui.setBackButtonVisbility
*/
setBackButtonVisibility:function(what){
var visibility=what?"visible":"hidden";
$(this.activeDiv).closest(".view").children("header").find(".backButton").css("visibility",visibility);
},
/**
* Update a badge on the selected target. Position can be
bl = bottom left
tl = top left
br = bottom right
tr = top right (default)
```
$.afui.updateBadge("#mydiv","3","bl","green");
```
* @param {string} target
* @param {string} value
* @param {string=} position
* @param {(string=|object)} color Color or CSS hash
* @title $.afui.updateBadge(target,value,[position],[color])
*/
updateBadge: function(target, value, position, color) {
if (position === undefined) position = "";
var $target = $(target);
var badge = $target.find("span.af-badge");
if (badge.length === 0) {
if ($target.css("position") !== "absolute") $target.css("position", "relative");
badge = $.create("span", {
className: "af-badge " + position,
html: value
});
$target.append(badge);
} else badge.html(value);
badge.removeClass("tl bl br tr");
badge.addClass(position);
if (color === undefined)
color = "red";
if ($.isObject(color)) {
badge.css(color);
} else if (color) {
badge.css("background", color);
}
badge.data("ignore-pressed", "true");
},
/**
* Removes a badge from the selected target.
```
$.afui.removeBadge("#mydiv");
```
* @param {string} target
* @title $.afui.removeBadge(target)
*/
removeBadge: function(target) {
$(target).find("span.af-badge").remove();
},
/**
* Show the loading mask with specificed text
```
$.afui.showMask()
$.afui.showMask('Fetching data...')
```
* @param {string=} text
* @title $.afui.showMask(text);
*/
showMask: function(text) {
if (!text) text = this.loadingText || "";
$.query("#afui_mask>h1").html(text);
$.query("#afui_mask").show();
},
/**
* Hide the loading mask
```
$.afui.hideMask();
```
* @title $.afui.hideMask();
*/
hideMask: function() {
$.query("#afui_mask").hide();
},
/**
* @api private
*/
dismissView:function(target,transition){
transition=transition.replace(":dismiss","");
var theView=$(target).closest(".view");
this.runTransition(transition,theView,null,true,$(target.hash).addClass("active").closest(".view"));
//this.activeDiv=target;
this.activeDiv=$(".view").not(theView).find(".panel.active").get(0);
this.updateHash(this.activeDiv.id);
},
/**
* This is called to initiate a transition or load content via ajax.
* We can pass in a hash+id or URL.
```
$.afui.loadContent("#main",false,false,"up");
```
* @param {string} target
* @param {boolean=} newtab (resets history)
* @param {boolean=} go back (initiate the back click)
* @param {string=} transition
* @param {object=} anchor
* @title $.afui.loadContent(target, newTab, goBack, transition, anchor);
* @api public
*/
loadContent: function(target, newView, back, transition, anchor) {
if (this.doingTransition) {
return;
}
if (target.length === 0) return;
if(target.indexOf("#")!==-1){
this.loadDiv(target, newView, back, transition,anchor);
}
else{
this.loadAjax(target, newView, back, transition,anchor);
}
},
/**
* This is called internally by loadContent. Here we are loading a div instead of an Ajax link
```
$.afui.loadDiv("#main",false,false,"up");
```
* @param {string} target
* @param {boolean=} newview (resets history)
* @param {boolean=} back Go back (initiate the back click)
* @param {string=} transition
* @title $.afui.loadDiv(target,newTab,goBack,transition);
* @api private
*/
loadDiv: function(target, newView, back, transition,anchor) {
// load a div
var newDiv = target;
var hashIndex = newDiv.indexOf("#");
var slashIndex = newDiv.indexOf("/");
if ((slashIndex !== -1)&&(hashIndex !== -1)) {
//Ignore everything after the slash in the hash part of a URL
//For example: app.com/#panelid/option1/option2 will become -> app.com/#panelid
//For example: app.com/path/path2/path3 will still be -> app.com/path/path2/path3
if (slashIndex > hashIndex) {
newDiv = newDiv.substr(0, slashIndex);
}
}
newDiv = newDiv.replace("#", "");
newDiv = $.query("#" + newDiv).get(0);
if (!newDiv) {
$(document).trigger("missingpanel", null, {missingTarget: target});
this.doingTransition=false;
return;
}
if (newDiv === this.activeDiv && !back) {
//toggle the menu if applicable
this.doingTransition=false;
return;
}
this.transitionType = transition;
var view=this.findViewTarget(newDiv);
var previous=this.findPreviousPanel(newDiv,view);
//check current view
var currentView;
if(anchor){
currentView=this.findViewTarget(anchor);
}
else
currentView=this.findViewTarget(this.activeDiv);
//Check if we are dealing with the same view
var isSplitViewParent=(currentView&¤tView.get(0)!==view.get(0)&¤tView.closest(".splitview").get(0)===view.closest(".splitview").get(0)&¤tView.closest(".splitview").length!==0);
if(isSplitViewParent){
newView=false;
}
$(newDiv).trigger("panelbeforeload");
$(previous).trigger("panelbeforeunload");
var isNewView=false;
//check nested views
if(!isSplitViewParent)
isSplitViewParent=currentView.parent().closest(".view").length===1;
if(isSplitViewParent&¤tView&¤tView.get(0)!==view.get(0))
$(currentView).trigger("nestedviewunload");
if(!isSplitViewParent&&(newView||currentView&¤tView.get(0)!==view.get(0))){
//Need to transition the view
newView=currentView||newView;
this.runViewTransition(transition,view,newView,newDiv,back);
this.updateViewHistory(view,newDiv,transition,target);
isNewView=true;
}
else{
this.runTransition(transition,previous, newDiv, back);
this.updateViewHistory(view,newDiv,transition,target);
}
//Let's check if it has a function to run to update the data
//Need to call after parsePanelFunctions, since new headers can override
this.loadContentData(newDiv,view,false,isNewView);
},
/**
* @api private
*/
findViewTarget:function(panel){
var view=$(panel).closest(".view");
if(!view) return false;
if(!this.views[view.prop("id")])
this.views[view.prop("id")]=[];
return view;
},
/**
* @api private
*/
findPreviousPanel:function(div,view){
var item=$(view).find(">.pages .panel.active").not(div);
if(item.length===0)
item=$(view).find(">.pages .panel:first-of-type");
return item.get(0);
},
/**
* This is called internally by loadDiv. This sets up the back button in the header and scroller for the panel
```
$.afui.loadContentData("#main",false,false,"up");
```
* @param {string} target
* @param {boolean=} newtab (resets history)
* @param {boolean=} go back (initiate the back click)
* @title $.afui.loadContentData(target,newTab,goBack);
* @api private
*/
loadContentData: function(what,view,back,isNewView) {
this.activeDiv = what;
this.setTitle(what,view,back,isNewView);
this.showBackButton(view,isNewView);
this.setActiveTab(what,view);
},
/**
* Set the active tab in the footer
```
$.afui.setActiveTab("main",currView)
```
@param {string|Node} id or DOM node for footer tab
@param {Node} DOM node of the current view to set the footer
@title $.afui.setActiveTab
*/
setActiveTab:function(ele,view){
var elementId;
if(typeof(ele)!=="string")
elementId=$(ele).prop("id");
/*
Check if an item exists:
Note that footer hrefs' may point to elements preceded by a # when trying to load a div (f.ex.: