define("dojox/mobile/View", [ "dojo/_base/array", "dojo/_base/config", "dojo/_base/connect", "dojo/_base/declare", "dojo/_base/lang", "dojo/_base/sniff", "dojo/_base/window", "dojo/_base/Deferred", "dojo/dom", "dojo/dom-class", "dojo/dom-construct", "dojo/dom-geometry", "dojo/dom-style", "dijit/registry", "dijit/_Contained", "dijit/_Container", "dijit/_WidgetBase", "./ViewController", // to load ViewController for you (no direct references) "./common", "./transition", "./viewRegistry" ], function(array, config, connect, declare, lang, has, win, Deferred, dom, domClass, domConstruct, domGeometry, domStyle, registry, Contained, Container, WidgetBase, ViewController, common, transitDeferred, viewRegistry){ // module: // dojox/mobile/View var dm = lang.getObject("dojox.mobile", true); return declare("dojox.mobile.View", [WidgetBase, Container, Contained], { // summary: // A widget that represents a view that occupies the full screen // description: // View acts as a container for any HTML and/or widgets. An entire // HTML page can have multiple View widgets and the user can // navigate through the views back and forth without page // transitions. // selected: Boolean // If true, the view is displayed at startup time. selected: false, // keepScrollPos: Boolean // If true, the scroll position is kept when transition occurs between views. keepScrollPos: true, // tag: String // A name of the HTML tag to create as domNode. tag: "div", /* internal properties */ baseClass: "mblView", constructor: function(/*Object*/params, /*DomNode?*/node){ // summary: // Creates a new instance of the class. // params: // Contains the parameters. // node: // The DOM node. If none is specified, it is automatically created. if(node){ dom.byId(node).style.visibility = "hidden"; } }, destroy: function(){ viewRegistry.remove(this.id); this.inherited(arguments); }, buildRendering: function(){ this.domNode = this.containerNode = this.srcNodeRef || domConstruct.create(this.tag); this._animEndHandle = this.connect(this.domNode, "webkitAnimationEnd", "onAnimationEnd"); this._animStartHandle = this.connect(this.domNode, "webkitAnimationStart", "onAnimationStart"); if(!config['mblCSS3Transition']){ this._transEndHandle = this.connect(this.domNode, "webkitTransitionEnd", "onAnimationEnd"); } if(has('mblAndroid3Workaround')){ // workaround for the screen flicker issue on Android 3.x/4.0 // applying "-webkit-transform-style:preserve-3d" to domNode can avoid // transition animation flicker domStyle.set(this.domNode, "webkitTransformStyle", "preserve-3d"); } viewRegistry.add(this); this.inherited(arguments); }, startup: function(){ if(this._started){ return; } // Determine which view among the siblings should be visible. // Priority: // 1. fragment id in the url (ex. #view1,view2) // 2. this.selected // 3. the first view if(this._visible === undefined){ var views = this.getSiblingViews(); var ids = location.hash && location.hash.substring(1).split(/,/); var fragView, selectedView, firstView; array.forEach(views, function(v, i){ if(array.indexOf(ids, v.id) !== -1){ fragView = v; } if(i == 0){ firstView = v; } if(v.selected){ selectedView = v; } v._visible = false; }, this); (fragView || selectedView || firstView)._visible = true; } if(this._visible){ // The 2nd arg is not to hide its sibling views so that they can be // correctly initialized. this.show(true, true); this.onStartView(); connect.publish("/dojox/mobile/startView", [this]); } if(this.domNode.style.visibility != "visible"){ // this check is to avoid screen flickers this.domNode.style.visibility = "visible"; } // Need to call inherited first - so that child widgets get started // up correctly this.inherited(arguments); var parent = this.getParent(); if(!parent || !parent.resize){ // top level widget this.resize(); } if(!this._visible){ // hide() should be called last so that child widgets can be // initialized while they are visible. this.hide(); } }, resize: function(){ // summary: // Calls resize() of each child widget. array.forEach(this.getChildren(), function(child){ if(child.resize){ child.resize(); } }); }, onStartView: function(){ // summary: // Stub function to connect to from your application. // description: // Called only when this view is shown at startup time. }, onBeforeTransitionIn: function(moveTo, dir, transition, context, method){ // summary: // Stub function to connect to from your application. // description: // Called before the arriving transition occurs. }, onAfterTransitionIn: function(moveTo, dir, transition, context, method){ // summary: // Stub function to connect to from your application. // description: // Called after the arriving transition occurs. }, onBeforeTransitionOut: function(moveTo, dir, transition, context, method){ // summary: // Stub function to connect to from your application. // description: // Called before the leaving transition occurs. }, onAfterTransitionOut: function(moveTo, dir, transition, context, method){ // summary: // Stub function to connect to from your application. // description: // Called after the leaving transition occurs. }, _clearClasses: function(/*DomNode*/node){ // summary: // Clean up the domNode classes that were added while making a transition. // description: // Remove all the "mbl" prefixed classes except mbl*View. if(!node){ return; } var classes = []; array.forEach(lang.trim(node.className||"").split(/\s+/), function(c){ if(c.match(/^mbl\w*View$/) || c.indexOf("mbl") === -1){ classes.push(c); } }, this); node.className = classes.join(' '); }, _fixViewState: function(/*DomNode*/toNode){ // summary: // Sanity check for view transition states. // description: // Sometimes uninitialization of Views fails after making view transition, // and that results in failure of subsequent view transitions. // This function does the uninitialization for all the sibling views. var nodes = this.domNode.parentNode.childNodes; for(var i = 0; i < nodes.length; i++){ var n = nodes[i]; if(n.nodeType === 1 && domClass.contains(n, "mblView")){ this._clearClasses(n); } } this._clearClasses(toNode); // just in case toNode is a sibling of an ancestor. }, convertToId: function(moveTo){ if(typeof(moveTo) == "string"){ // removes a leading hash mark (#) and params if exists // ex. "#bar&myParam=0003" -> "bar" return moveTo.replace(/^#?([^&?]+).*/, "$1"); } return moveTo; }, _isBookmarkable: function(detail){ return detail.moveTo && (config['mblForceBookmarkable'] || detail.moveTo.charAt(0) === '#') && !detail.hashchange; }, performTransition: function(/*String*/moveTo, /*Number*/transitionDir, /*String*/transition, /*Object|null*/context, /*String|Function*/method /*...*/){ // summary: // Function to perform the various types of view transitions, such as fade, slide, and flip. // moveTo: String // The id of the transition destination view which resides in // the current page. // If the value has a hash sign ('#') before the id // (e.g. #view1) and the dojo/hash module is loaded by the user // application, the view transition updates the hash in the // browser URL so that the user can bookmark the destination // view. In this case, the user can also use the browser's // back/forward button to navigate through the views in the // browser history. // If null, transitions to a blank view. // If '#', returns immediately without transition. // transitionDir: Number // The transition direction. If 1, transition forward. If -1, transition backward. // For example, the slide transition slides the view from right to left when transitionDir == 1, // and from left to right when transitionDir == -1. // transition: String // A type of animated transition effect. You can choose from // the standard transition types, "slide", "fade", "flip", or // from the extended transition types, "cover", "coverv", // "dissolve", "reveal", "revealv", "scaleIn", "scaleOut", // "slidev", "swirl", "zoomIn", "zoomOut", "cube", and // "swap". If "none" is specified, transition occurs // immediately without animation. // context: Object // The object that the callback function will receive as "this". // method: String|Function // A callback function that is called when the transition has finished. // A function reference, or name of a function in context. // tags: // public // // example: // Transition backward to a view whose id is "foo" with the slide animation. // | performTransition("foo", -1, "slide"); // // example: // Transition forward to a blank view, and then open another page. // | performTransition(null, 1, "slide", null, function(){location.href = href;}); // normalize the arg var detail, optArgs; if(moveTo && typeof(moveTo) === "object"){ detail = moveTo; optArgs = transitionDir; // array }else{ detail = { moveTo: moveTo, transitionDir: transitionDir, transition: transition, context: context, method: method }; optArgs = []; for(var i = 5; i < arguments.length; i++){ optArgs.push(arguments[i]); } } // save the parameters this._detail = detail; this._optArgs = optArgs; this._arguments = [ detail.moveTo, detail.transitionDir, detail.transition, detail.context, detail.method ]; if(detail.moveTo === "#"){ return; } var toNode; if(detail.moveTo){ toNode = this.convertToId(detail.moveTo); }else{ if(!this._dummyNode){ this._dummyNode = win.doc.createElement("div"); win.body().appendChild(this._dummyNode); } toNode = this._dummyNode; } if(this.addTransitionInfo && typeof(detail.moveTo) == "string" && this._isBookmarkable(detail)){ this.addTransitionInfo(this.id, detail.moveTo, {transitionDir:detail.transitionDir, transition:detail.transition}); } var fromNode = this.domNode; var fromTop = fromNode.offsetTop; toNode = this.toNode = dom.byId(toNode); if(!toNode){ console.log("dojox/mobile/View.performTransition: destination view not found: "+detail.moveTo); return; } toNode.style.visibility = "hidden"; toNode.style.display = ""; this._fixViewState(toNode); var toWidget = registry.byNode(toNode); if(toWidget){ // Now that the target view became visible, it's time to run resize() if(config["mblAlwaysResizeOnTransition"] || !toWidget._resized){ common.resizeAll(null, toWidget); toWidget._resized = true; } if(detail.transition && detail.transition != "none"){ // Temporarily add padding to align with the fromNode while transition toWidget.containerNode.style.paddingTop = fromTop + "px"; } toWidget.load && toWidget.load(); // for ContentView toWidget.movedFrom = fromNode.id; } if(has('mblAndroidWorkaround') && !config['mblCSS3Transition'] && detail.transition && detail.transition != "none"){ // workaround for the screen flicker issue on Android 2.2/2.3 // apply "-webkit-transform-style:preserve-3d" to both toNode and fromNode // to make them 3d-transition-ready state just before transition animation domStyle.set(toNode, "webkitTransformStyle", "preserve-3d"); domStyle.set(fromNode, "webkitTransformStyle", "preserve-3d"); // show toNode offscreen to avoid flicker when switching "display" and "visibility" styles domClass.add(toNode, "mblAndroidWorkaround"); } this.onBeforeTransitionOut.apply(this, this._arguments); connect.publish("/dojox/mobile/beforeTransitionOut", [this].concat(lang._toArray(this._arguments))); if(toWidget){ // perform view transition keeping the scroll position if(this.keepScrollPos && !this.getParent()){ var scrollTop = win.body().scrollTop || win.doc.documentElement.scrollTop || win.global.pageYOffset || 0; fromNode._scrollTop = scrollTop; var toTop = (detail.transitionDir == 1) ? 0 : (toNode._scrollTop || 0); toNode.style.top = "0px"; if(scrollTop > 1 || toTop !== 0){ fromNode.style.top = toTop - scrollTop + "px"; if(config["mblHideAddressBar"] !== false){ setTimeout(function(){ // iPhone needs setTimeout win.global.scrollTo(0, (toTop || 1)); }, 0); } } }else{ toNode.style.top = "0px"; } toWidget.onBeforeTransitionIn.apply(toWidget, this._arguments); connect.publish("/dojox/mobile/beforeTransitionIn", [toWidget].concat(lang._toArray(this._arguments))); } toNode.style.display = "none"; toNode.style.visibility = "visible"; common.fromView = this; common.toView = toWidget; this._doTransition(fromNode, toNode, detail.transition, detail.transitionDir); }, _toCls: function(s){ // convert from transition name to corresponding class name // ex. "slide" -> "mblSlide" return "mbl"+s.charAt(0).toUpperCase() + s.substring(1); }, _doTransition: function(fromNode, toNode, transition, transitionDir){ var rev = (transitionDir == -1) ? " mblReverse" : ""; toNode.style.display = ""; if(!transition || transition == "none"){ this.domNode.style.display = "none"; this.invokeCallback(); }else if(config['mblCSS3Transition']){ //get dojox/css3/transit first Deferred.when(transitDeferred, lang.hitch(this, function(transit){ //follow the style of .mblView.mblIn in View.css //need to set the toNode to absolute position var toPosition = domStyle.get(toNode, "position"); domStyle.set(toNode, "position", "absolute"); Deferred.when(transit(fromNode, toNode, {transition: transition, reverse: (transitionDir===-1)?true:false}),lang.hitch(this,function(){ domStyle.set(toNode, "position", toPosition); this.invokeCallback(); })); })); }else{ if(transition.indexOf("cube") != -1){ if(has('ipad')){ domStyle.set(toNode.parentNode, {webkitPerspective:1600}); }else if(has('iphone')){ domStyle.set(toNode.parentNode, {webkitPerspective:800}); } } var s = this._toCls(transition); if(has('mblAndroidWorkaround')){ // workaround for the screen flicker issue on Android 2.2 // applying transition css classes just after setting toNode.style.display = "" // causes flicker, so wait for a while using setTimeout setTimeout(function(){ domClass.add(fromNode, s + " mblOut" + rev); domClass.add(toNode, s + " mblIn" + rev); domClass.remove(toNode, "mblAndroidWorkaround"); // remove offscreen style setTimeout(function(){ domClass.add(fromNode, "mblTransition"); domClass.add(toNode, "mblTransition"); }, 30); // 30 = 100 - 70, to make total delay equal to 100ms }, 70); // 70ms is experiential value }else{ domClass.add(fromNode, s + " mblOut" + rev); domClass.add(toNode, s + " mblIn" + rev); setTimeout(function(){ domClass.add(fromNode, "mblTransition"); domClass.add(toNode, "mblTransition"); }, 100); } // set transform origin var fromOrigin = "50% 50%"; var toOrigin = "50% 50%"; var scrollTop, posX, posY; if(transition.indexOf("swirl") != -1 || transition.indexOf("zoom") != -1){ if(this.keepScrollPos && !this.getParent()){ scrollTop = win.body().scrollTop || win.doc.documentElement.scrollTop || win.global.pageYOffset || 0; }else{ scrollTop = -domGeometry.position(fromNode, true).y; } posY = win.global.innerHeight / 2 + scrollTop; fromOrigin = "50% " + posY + "px"; toOrigin = "50% " + posY + "px"; }else if(transition.indexOf("scale") != -1){ var viewPos = domGeometry.position(fromNode, true); posX = ((this.clickedPosX !== undefined) ? this.clickedPosX : win.global.innerWidth / 2) - viewPos.x; if(this.keepScrollPos && !this.getParent()){ scrollTop = win.body().scrollTop || win.doc.documentElement.scrollTop || win.global.pageYOffset || 0; }else{ scrollTop = -viewPos.y; } posY = ((this.clickedPosY !== undefined) ? this.clickedPosY : win.global.innerHeight / 2) + scrollTop; fromOrigin = posX + "px " + posY + "px"; toOrigin = posX + "px " + posY + "px"; } domStyle.set(fromNode, {webkitTransformOrigin:fromOrigin}); domStyle.set(toNode, {webkitTransformOrigin:toOrigin}); } }, onAnimationStart: function(e){ // summary: // A handler that is called when transition animation starts. }, onAnimationEnd: function(e){ // summary: // A handler that is called after transition animation ends. var name = e.animationName || e.target.className; if(name.indexOf("Out") === -1 && name.indexOf("In") === -1 && name.indexOf("Shrink") === -1){ return; } var isOut = false; if(domClass.contains(this.domNode, "mblOut")){ isOut = true; this.domNode.style.display = "none"; domClass.remove(this.domNode, [this._toCls(this._detail.transition), "mblIn", "mblOut", "mblReverse"]); }else{ // Reset the temporary padding this.containerNode.style.paddingTop = ""; } domStyle.set(this.domNode, {webkitTransformOrigin:""}); if(name.indexOf("Shrink") !== -1){ var li = e.target; li.style.display = "none"; domClass.remove(li, "mblCloseContent"); // If target is placed inside scrollable, need to call onTouchEnd // to adjust scroll position var p = viewRegistry.getEnclosingScrollable(this.domNode); p && p.onTouchEnd(); } if(isOut){ this.invokeCallback(); } this._clearClasses(this.domNode); // clear the clicked position this.clickedPosX = this.clickedPosY = undefined; if(name.indexOf("Cube") !== -1 && name.indexOf("In") !== -1 && has('iphone')){ this.domNode.parentNode.style.webkitPerspective = ""; } }, invokeCallback: function(){ // summary: // A function to be called after performing a transition to // call a specified callback. this.onAfterTransitionOut.apply(this, this._arguments); connect.publish("/dojox/mobile/afterTransitionOut", [this].concat(this._arguments)); var toWidget = registry.byNode(this.toNode); if(toWidget){ toWidget.onAfterTransitionIn.apply(toWidget, this._arguments); connect.publish("/dojox/mobile/afterTransitionIn", [toWidget].concat(this._arguments)); toWidget.movedFrom = undefined; if(this.setFragIds && this._isBookmarkable(this._detail)){ this.setFragIds(toWidget); // setFragIds is defined in bookmarkable.js } } if(has('mblAndroidWorkaround')){ // workaround for the screen flicker issue on Android 2.2/2.3 // remove "-webkit-transform-style" style after transition finished // to avoid side effects such as input field auto-scrolling issue // use setTimeout to avoid flicker in case of ScrollableView setTimeout(lang.hitch(this, function(){ if(toWidget){ domStyle.set(this.toNode, "webkitTransformStyle", ""); } domStyle.set(this.domNode, "webkitTransformStyle", ""); }), 0); } var c = this._detail.context, m = this._detail.method; if(!c && !m){ return; } if(!m){ m = c; c = null; } c = c || win.global; if(typeof(m) == "string"){ c[m].apply(c, this._optArgs); }else if(typeof(m) == "function"){ m.apply(c, this._optArgs); } }, isVisible: function(/*Boolean?*/checkAncestors){ // summary: // Return true if this view is visible // checkAncestors: // If true, in addition to its own visibility, also checks the // ancestors visibility to see if the view is actually being // shown or not. var visible = function(node){ return domStyle.get(node, "display") !== "none"; }; if(checkAncestors){ for(var n = this.domNode; n.tagName !== "BODY"; n = n.parentNode){ if(!visible(n)){ return false; } } return true; }else{ return visible(this.domNode); } }, getShowingView: function(){ // summary: // Find the currently showing view from my sibling views. // description: // Note that depending on the ancestor views' visibility, // the found view may not be actually shown. var nodes = this.domNode.parentNode.childNodes; for(var i = 0; i < nodes.length; i++){ var n = nodes[i]; if(n.nodeType === 1 && domClass.contains(n, "mblView") && n.style.display !== "none"){ return registry.byNode(n); } } return null; }, getSiblingViews: function(){ // summary: // Returns an array of the sibling views. if(!this.domNode.parentNode){ return [this]; } return array.map(array.filter(this.domNode.parentNode.childNodes, function(n){ return n.nodeType === 1 && domClass.contains(n, "mblView"); }), function(n){ return registry.byNode(n); }); }, show: function(/*Boolean?*/noEvent, /*Boolean?*/doNotHideOthers){ // summary: // Shows this view without a transition animation. var out = this.getShowingView(); if(!noEvent){ if(out){ out.onBeforeTransitionOut(out.id); connect.publish("/dojox/mobile/beforeTransitionOut", [out, out.id]); } this.onBeforeTransitionIn(this.id); connect.publish("/dojox/mobile/beforeTransitionIn", [this, this.id]); } if(doNotHideOthers){ this.domNode.style.display = ""; }else{ array.forEach(this.getSiblingViews(), function(v){ v.domNode.style.display = (v === this) ? "" : "none"; }, this); } this.load && this.load(); // for ContentView if(!noEvent){ if(out){ out.onAfterTransitionOut(out.id); connect.publish("/dojox/mobile/afterTransitionOut", [out, out.id]); } this.onAfterTransitionIn(this.id); connect.publish("/dojox/mobile/afterTransitionIn", [this, this.id]); } }, hide: function(){ // summary: // Hides this view without a transition animation. this.domNode.style.display = "none"; } }); });