/** * 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("Settings Logout") $.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.: