Show:
                            var doc = A.config.doc,
                                win = A.config.win;
                            
                            A.SurfaceApp = A.Base.create('surface-app', A.Base, [], {
                                /**
                                 * Holds the active screen.
                                 *
                                 * @property activeScreen
                                 * @type {Screen}
                                 * @protected
                                 */
                                activeScreen: null,
                            
                                /**
                                 * Holds the active path containing the query parameters.
                                 *
                                 * @property activePath
                                 * @type {String}
                                 * @protected
                                 */
                                activePath: null,
                            
                                /**
                                 * Holds the window hotizontal scroll position before navigation using back
                                 * or forward happens to lock the scroll position until the surfaces are
                                 * updated.
                                 *
                                 * @property lockPageXOffset
                                 * @type {Number}
                                 * @protected
                                 */
                                lockPageXOffset: 0,
                            
                                /**
                                 * Holds the window vertical scroll position before navigation using back or
                                 * forward happens to lock the scroll position until the surfaces are
                                 * updated.
                                 *
                                 * @property lockPageYOffset
                                 * @type {Number}
                                 * @protected
                                 */
                                lockPageYOffset: 0,
                            
                                /**
                                 * Holds the window hotizontal scroll position when the navigation using
                                 * back or forward happens to be restored after the surfaces are updated.
                                 *
                                 * @property pageXOffset
                                 * @type {Number}
                                 * @protected
                                 */
                                pageXOffset: 0,
                            
                                /**
                                 * Holds the window vertical scroll position when the navigation using
                                 * back or forward happens to be restored after the surfaces are updated.
                                 *
                                 * @property pageYOffset
                                 * @type {Number}
                                 * @protected
                                 */
                                pageYOffset: 0,
                            
                                /**
                                 * Holds a deferred withe the current navigation.
                                 *
                                 * @property pendingNavigate
                                 * @type {Promise}
                                 * @protected
                                 */
                                pendingNavigate: null,
                            
                                /**
                                 * Holds the screen routes configuration.
                                 *
                                 * @property routes
                                 * @type {Array}
                                 * @protected
                                 */
                                routes: null,
                            
                                /**
                                 * Maps the screen instances by the url containing the parameters.
                                 *
                                 * @property screens
                                 * @type {Object}
                                 * @protected
                                 */
                                screens: null,
                            
                                /**
                                 * Holds the scroll event handle.
                                 *
                                 * @property scrollHandle
                                 * @type {Object}
                                 * @private
                                 */
                                scrollHandle: null,
                            
                                /**
                                 * When set to true the first erroneous popstate fired on page load will be
                                 * ignored, only if `window.history.state` is also `null`.
                                 *
                                 * @property skipLoadPopstate
                                 * @type {Boolean}
                                 * @protected
                                 */
                                skipLoadPopstate: false,
                            
                                /**
                                 * Maps that index the surfaces instances by the surface id.
                                 *
                                 * @property surfaces
                                 * @type {Object}
                                 * @protected
                                 */
                                surfaces: null,
                            
                                /**
                                 * Construction logic executed during SurfaceApp instantiation.
                                 * Lifecycle.
                                 *
                                 * @method initializer
                                 * @protected
                                 */
                                initializer: function() {
                                    this.routes = [];
                                    this.surfaces = {};
                                    this.screens = {};
                                    this.publish({
                                        startNavigate: {
                                            defaultFn: this._defStartNavigateFn,
                                            preventedFn: this._preventNavigateFn
                                        },
                                        failNavigate: {},
                                        successNavigate: {},
                                        endNavigate: {}
                                    });
                                    A.once('load', this._onLoad, win, this);
                                    A.on('scroll', A.debounce(this._onScroll, 50, this));
                                    A.on('popstate', this._onPopState, win, this);
                                    A.delegate('click', this._onDocClick, doc, this.get('linkSelector'), this);
                                },
                            
                                /**
                                 * Adds one or more screens to the application.
                                 *
                                 * @method addScreenRoutes
                                 * @param {Object} or {Array} screens Single object or an array of object. Each object should contain `path`
                                 *     and `screen`.
                                 *     The `path` should be a string or a regex that maps the navigation
                                 *     route to a screen class definition (not an instance), e.g:
                                 *         `{ path: "/home:param1", screen: Y.MyScreen }`
                                 *         `{ path: /foo.+/, screen: Y.MyScreen }`
                                 * @chainable
                                 */
                                addScreenRoutes: function(screens) {
                                    this._registerRoutes(A.Array(screens));
                                    return this;
                                },
                            
                                /**
                                 * Adds one or more surfaces to the application.
                                 *
                                 * @method addSurfaces
                                 * @param {Surface} or {String} or [{Surface | String}] surfaces String (id) or Surface instance. You can
                                 * also pass an Array which contains Surface instances or String (id). In case of ID, these should be the ID of
                                 * surface DOM element.
                                 * @chainable
                                 */
                                addSurfaces: function(surfaces) {
                                    var instance = this;
                            
                                    surfaces = A.Array(surfaces);
                            
                                    A.Array.each(surfaces, function(surface) {
                                        if (A.Lang.isString(surface)) {
                                            surface = new A.Surface({
                                                id: surface
                                            });
                                        }
                                        instance.surfaces[surface] = surface;
                                    });
                            
                                    return this;
                                },
                            
                                /**
                                 * Dispatches to the first route handler that matches the current path, if
                                 * any.
                                 *
                                 * @method dispatch
                                 * @return {Promise} Returns a pending request cancellable promise.
                                 */
                                dispatch: function() {
                                    return this.navigate(
                                        win.location.pathname + win.location.search + win.location.hash,
                                        true
                                    );
                                },
                            
                                /**
                                 * Matches if path is a known route, if not found any returns null. The path
                                 * could contain a fragment-id (#). If the path is the same as the current
                                 * url, and the path contains a fragment, we do not prevent the default
                                 * browser behavior.
                                 *
                                 * @method matchesPath
                                 * @param {String} path Path containing the querystring part.
                                 * @return {Object | null} Route handler if match any or `null` if the path
                                 *     is the same as the current url and the path contains a fragment.
                                 */
                                matchesPath: function(path) {
                                    var basePath = this.get('basePath'),
                                        hashIndex;
                            
                                    // Remove path hash before match
                                    hashIndex = path.lastIndexOf('#');
                                    if (hashIndex > -1) {
                                        path = path.substr(0, hashIndex);
                                        if (path === win.location.pathname + win.location.search) {
                                            return null;
                                        }
                                    }
                            
                                    path = path.substr(basePath.length);
                                    return A.Array.find(this.routes, function(route) {
                                        return route.matchesPath(path);
                                    });
                                },
                            
                                /**
                                 * Navigates to the specified path if there is a route handler that matches.
                                 *
                                 * @method navigate
                                 * @param {String} path Path containing the querystring part.
                                 * @param {Boolean} opt_replaceHistory Replaces browser history.
                                 * @return {Promise} Returns a pending request cancellable promise.
                                 */
                                navigate: function(path, opt_replaceHistory) {
                                    this._stopPending();
                            
                                    this.fire('startNavigate', {
                                        path: path,
                                        replaceHistory: !! opt_replaceHistory
                                    });
                                    return this.pendingNavigate;
                                },
                            
                                /**
                                 * Prefetches the specified path if there is a route handler that matches.
                                 *
                                 * @method navigate
                                 * @param {String} path Path containing the querystring part.
                                 * @return {Promise} Returns a pending request cancellable promise.
                                 */
                                prefetch: function(path) {
                                    var nextScreen,
                                        pendingPrefetch,
                                        route = this.matchesPath(path),
                                        self = this;
                            
                                    if (!route) {
                                        return A.CancellablePromise.reject(new A.CancellablePromise.Error('No screen for ' + path));
                                    }
                            
                                    A.log('Prefetching [' + path + ']', 'info');
                            
                                    nextScreen = this._getScreenInstance(path, route);
                                    pendingPrefetch = A.CancellablePromise.resolve()
                                        .then(function() {
                                            return nextScreen.load(path);
                                        })
                                        .then(function() {
                                                self.screens[path] = nextScreen;
                                            },
                                            function(reason) {
                                                self._removeScreen(path, nextScreen);
                                                throw reason;
                                            });
                            
                                    return pendingPrefetch;
                                },
                            
                                /**
                                 * Starts navigation to a path.
                                 *
                                 * @method  _defStartNavigateFn
                                 * @param {EventFacade} event Event facade containing `path` and
                                 *     `replaceHistory`.
                                 */
                                _defStartNavigateFn: function(event) {
                                    var instance = this,
                                        payload = {
                                            path: event.path
                                        };
                            
                                    this.pendingNavigate = this._doNavigate(
                                        event.path,
                                        event.replaceHistory
                                    ).thenCatch(
                                        function(reason) {
                                            A.log(reason.message, 'info');
                                            payload.error = reason;
                                            instance._stopPending();
                                            throw reason;
                                        }
                                    ).thenAlways(
                                        function() {
                                            instance.fire('endNavigate', payload);
                                        }
                                    );
                                },
                            
                                /**
                                 * Starts navigation to a path.
                                 *
                                 * @method  _doNavigate
                                 * @param {String} path Path containing the querystring part.
                                 * @param {Boolean} opt_replaceHistory Replaces browser history.
                                 * @return {Promise} Returns a pending request cancellable promise.
                                 */
                                _doNavigate: function(path, opt_replaceHistory) {
                                    var instance = this,
                                        route,
                                        nextScreen,
                                        activeScreen = instance.activeScreen;
                            
                                    if (this.activeScreen && this.activeScreen.beforeDeactivate()) {
                                        this.pendingNavigate = A.CancellablePromise.reject(
                                            new A.CancellablePromise.Error('Cancelled by active screen'));
                                        return this.pendingNavigate;
                                    }
                            
                                    route = this.matchesPath(path);
                                    if (!route) {
                                        this.pendingNavigate = A.CancellablePromise.reject(
                                            new A.CancellablePromise.Error('No screen for ' + path));
                                        return this.pendingNavigate;
                                    }
                            
                                    A.log('Navigate to [' + path + ']', 'info');
                            
                                    // When reloading the same path do replaceState instead of pushState to
                                    // avoid polluting history with states with the same path.
                                    if (path === this.activePath) {
                                        opt_replaceHistory = true;
                                    }
                            
                                    nextScreen = this._getScreenInstance(path, route);
                            
                                    this.pendingNavigate = A.CancellablePromise.resolve(
                                        nextScreen.load(path)
                                    ).then(
                                        function(contents) {
                                            var screenId = nextScreen.get('id');
                                            A.Object.each(instance.surfaces, function(surface, surfaceId) {
                                                surface.addContent(screenId, nextScreen.getSurfaceContent(surfaceId, contents));
                                            });
                            
                                            if (activeScreen) {
                                                activeScreen.deactivate();
                                            }
                            
                                            return nextScreen.flip(instance.surfaces);
                                        }
                                    ).then(
                                        function() {
                                            instance._finalizeNavigate(path, nextScreen, opt_replaceHistory);
                                        },
                                        function(reason) {
                                            instance._handleNavigateError(path, nextScreen, reason);
                                        }
                                    );
                            
                                    return this.pendingNavigate;
                                },
                            
                                /**
                                 * Finalizes a screen navigation.
                                 *
                                 * @method  _finalizeNavigate
                                 * @param {String} path Path containing the querystring part.
                                 * @param {Screen} nextScreen
                                 * @param {Boolean} opt_replaceHistory Replaces browser history.
                                 * @private
                                 */
                                _finalizeNavigate: function(path, nextScreen, opt_replaceHistory) {
                                    var activeScreen = this.activeScreen,
                                        title = nextScreen.get('title') || this.get('defaultTitle');
                            
                                    this._updateHistory(title, path, opt_replaceHistory);
                            
                                    this._syncScrollPosition(opt_replaceHistory);
                            
                                    doc.title = title;
                            
                                    nextScreen.activate();
                            
                                    if (activeScreen && !activeScreen.get('cacheable')) {
                                        this._removeScreen(this.activePath, activeScreen);
                                    }
                            
                                    this.activePath = path;
                                    this.activeScreen = nextScreen;
                                    this.screens[path] = nextScreen;
                                    this.pendingNavigate = null;
                                    A.log('Navigation done', 'info');
                                },
                            
                                /**
                                 * Handle navigation error.
                                 *
                                 * @method  _handleNavigateError
                                 * @param {String} path Path containing the querystring part.
                                 * @param {Screen} nextScreen
                                 * @param {Error} error
                                 * @private
                                 */
                                _handleNavigateError: function(path, nextScreen, err) {
                                    A.log('Navigation error for [' + nextScreen + '] (' + err + ')', 'info');
                                    this._removeScreen(path, nextScreen);
                                    this.pendingNavigate = null;
                                },
                            
                                /**
                                 * Tests if hostname is an offsite link.
                                 *
                                 * @method _isLinkSameOrigin
                                 * @param  {String} hostname Link hostname to compare with
                                 *     `window.location.hostname`.
                                 * @return {Boolean}
                                 * @private
                                 */
                                _isLinkSameOrigin: function(hostname) {
                                    return hostname === win.location.hostname;
                                },
                            
                                /**
                                 * Tests if link element has the same app's base path.
                                 *
                                 * @param  {String} path Link path containing the querystring part.
                                 * @return {Boolean}
                                 * @private
                                 */
                                _isSameBasePath: function(path) {
                                    return path.indexOf(this.get('basePath')) === 0;
                                },
                            
                                /**
                                 * Lock the document scroll in order to avoid the browser native back and
                                 * forward navigation to change the scroll position. Surface app takes care
                                 * of updating it when surfaces are ready.
                                 *
                                 * @method _lockScroll
                                 * @private
                                 */
                                _lockScroll: function() {
                                    var instance = this,
                                        lockPageXOffset = instance.lockPageXOffset,
                                        lockPageYOffset = instance.lockPageYOffset;
                            
                                    instance.pageXOffset = win.pageXOffset;
                                    instance.pageYOffset = win.pageYOffset;
                            
                                    // In order to keep the history scrolling fluid, the scroll position
                                    // needs to be locked until content is fully loaded and surfaces
                                    // painted. The problem is that behavior is incosistent through
                                    // browsers. Blink and Webkit popstate event happens before scroll
                                    // position updates, and on Firefox and IE scroll position updates
                                    // before popstate event. If we assume the lock position is the same as
                                    // the current win.page[XY]Offset on popstate, we should capture the
                                    // next scroll tick, otherwise listens for the next scroll once to
                                    // capture the history scroll position. For more information see
                                    // discussion https://bugzilla.mozilla.org/show_bug.cgi?id=679458.
                                    if (lockPageXOffset === instance.pageXOffset &&
                                        lockPageYOffset === instance.pageYOffset) {
                                        A.soon(function() {
                                            instance.pageXOffset = win.pageXOffset;
                                            instance.pageYOffset = win.pageYOffset;
                                            win.scrollTo(lockPageXOffset, lockPageYOffset);
                                        });
                                    }
                                    else {
                                        A.once('scroll', function() {
                                            instance.pageXOffset = win.pageXOffset;
                                            instance.pageYOffset = win.pageYOffset;
                                            win.scrollTo(lockPageXOffset, lockPageYOffset);
                                        });
                                    }
                                },
                            
                                /**
                                 * Retrieves or create a screen instance to a path.
                                 *
                                 * @method  _getScreenInstance
                                 * @param {String} path Path containing the querystring part.
                                 * @private
                                 */
                                _getScreenInstance: function(path, route) {
                                    var screen,
                                        refreshScreen;
                            
                                    // When simulating page refresh the request lifecycle for activeScreen
                                    // and nextScreen should be respected, therefore creating a new screen
                                    // instance for the same path is needed
                                    if (path === this.activePath) {
                                        A.log('Already at destination, refresh navigation', 'info');
                                        refreshScreen = this.screens[path];
                                        delete this.screens[path];
                                    }
                            
                                    screen = this.screens[path];
                                    if (!screen) {
                                        A.log('Create screen for [' + path + ']', 'info');
                                        screen = new(route.get('screen'))();
                                        // When simulating a page refresh the cache should copy the cache
                                        // from refreshScreen to avoid roundtrip to the server
                                        if (refreshScreen) {
                                            screen.addCache(refreshScreen.getCache());
                                        }
                                    }
                            
                                    return screen;
                                },
                            
                                /**
                                 * Intercepts document clicks and test link elements in order to decide
                                 * whether Surface app can navigate.
                                 *
                                 * @method  _onDocClick
                                 * @param {EventFacade} event Event facade
                                 * @private
                                 */
                                _onDocClick: function(event) {
                                    var link = event.currentTarget,
                                        hostname = link.get('hostname'),
                                        path = link.get('pathname') + link.get('search') + link.get('hash'),
                                        navigateFailed = false;
                            
                                    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
                                        A.log('Stop the SPA navigation when a modifier key is pressed');
                                        return;
                                    }
                            
                                    if (!this._isLinkSameOrigin(hostname)) {
                                        A.log('Offsite link clicked', 'info');
                                        return;
                                    }
                                    if (!this._isSameBasePath(path)) {
                                        A.log('Link clicked outside app\'s base path', 'info');
                                        return;
                                    }
                                    if (!this.matchesPath(path)) {
                                        A.log('No screen for ' + path, 'info');
                                        return;
                                    }
                            
                                    this.navigate(path).thenCatch(function() {
                                        // Do not prevent link navigation in case some synchronous error occurs
                                        navigateFailed = true;
                                    });
                            
                                    if (!navigateFailed) {
                                        event.preventDefault();
                                    }
                                },
                            
                                /**
                                 * Listens to the window's load event in order avoid issues with some browsers
                                 * that trigger popstate calls on the first load. For more information see
                                 * http://stackoverflow.com/questions/6421769/popstate-on-pages-load-in-chrome.
                                 *
                                 * @method _onLoad
                                 * @private
                                 */
                                _onLoad: function() {
                                    var instance = this;
                            
                                    this.skipLoadPopstate = true;
                                    setTimeout(function() {
                                        // The timeout ensures that popstate events will be unblocked right
                                        // after the load event occured, but not in the same event-loop cycle.
                                        instance.skipLoadPopstate = false;
                                    }, 0);
                                },
                            
                                /**
                                 * Handles browser history changes and fires app's navigation if the state
                                 * belows to us. If we detect a popstate and the state is `null`, assume it
                                 * is navigating to an external page or to a page we don't have route, then
                                 * `window.location.reload()` is invoked in order to reload the content to
                                 * the current url.
                                 *
                                 * @method _onPopState
                                 * @param {EventFacade} event Event facade
                                 * @private
                                 */
                                _onPopState: function(event) {
                                    var state = event._event.state;
                            
                                    if (state === null) {
                                        if (this.skipLoadPopstate) {
                                            return;
                                        }
                            
                                        if (!win.location.hash) {
                                            win.location.reload();
                                            return;
                                        }
                                    }
                            
                                    if (state && state.surface) {
                                        A.log('History navigation to [' + state.path + ']', 'info');
                                        this._lockScroll();
                                        this.navigate(state.path, true);
                                    }
                                },
                            
                                /**
                                 * Listens document scroll changes in order to capture the possible lock
                                 * scroll position for history scrolling.
                                 *
                                 * @method _onScroll
                                 * @param {EventFacade} event Event facade
                                 * @private
                                 */
                                _onScroll: function() {
                                    this.lockPageXOffset = win.pageXOffset;
                                    this.lockPageYOffset = win.pageYOffset;
                                },
                            
                                /**
                                 * Fires when navigation is prevented from `startNavigate` event.
                                 *
                                 * @method  _preventNavigateFn
                                 * @param {EventFacade} event
                                 */
                                _preventNavigateFn: function() {
                                    this.pendingNavigate = A.CancellablePromise.reject(
                                        new A.CancellablePromise.Error('Navigation has been prevented'));
                                },
                            
                                /**
                                 * Removes a screen.
                                 *
                                 * @method  _removeScreen
                                 * @param {String} path Path containing the querystring part.
                                 * @param {Screen} screen
                                 * @private
                                 */
                                _removeScreen: function(path, screen) {
                                    var screenId = screen.get('id');
                            
                                    A.Object.each(this.surfaces, function(surface) {
                                        surface.remove(screenId);
                                    });
                            
                                    screen.destroy();
                                    delete this.screens[path];
                                },
                            
                                /**
                                 * Registers a route for a screen.
                                 *
                                 * @method  _registerRoutes
                                 * @param {Array} screenRoutes Array of objects with `path` and `screen`
                                 *     keys or `A.ScreenRoute` instances.
                                 * @private
                                 */
                                _registerRoutes: function(screenRoutes) {
                                    var instance = this;
                            
                                    A.Array.each(screenRoutes, function(value) {
                                        if (!A.instanceOf(value, A.ScreenRoute)) {
                                            value = new A.ScreenRoute({
                                                path: value.path,
                                                screen: value.screen
                                            });
                                        }
                            
                                        instance.routes.push(value);
                                    });
                                },
                            
                                /**
                                 * Sync document scroll position to the values captured when the default
                                 * back and forward navigation happened. The scroll position updates after
                                 * `beforeFlip` is called and before the surface transitions.
                                 *
                                 * @method  _syncScrollPosition
                                 * @param {Boolean} opt_replaceHistory Replaces browser history.
                                 * @private
                                 */
                                _syncScrollPosition: function(opt_replaceHistory) {
                                    var hash = win.location.hash,
                                        hashEl,
                                        pos = [0, 0];
                            
                                    if (opt_replaceHistory) {
                                        win.scrollTo(this.pageXOffset, this.pageYOffset);
                                        return;
                                    }
                            
                                    if (hash) {
                                        hashEl = A.one(hash);
                                    }
                            
                                    if (hashEl) {
                                        pos = hashEl.getXY();
                                    }
                            
                                    win.scrollTo(pos[0], pos[1]);
                                },
                            
                                /**
                                 * Cancels pending navigate with `Cancel pending navigation` error.
                                 *
                                 * @method  _stopPending
                                 * @protected
                                 */
                                _stopPending: function() {
                                    if (this.pendingNavigate) {
                                        this.pendingNavigate.cancel('Cancel pending navigation');
                                        this.pendingNavigate = null;
                                    }
                                },
                            
                                /**
                                 * Updates or replace browser history.
                                 *
                                 * @method _updateHistory
                                 * @param {String} path Path containing the querystring part.
                                 * @param {String} title Document title.
                                 * @param {Boolean} opt_replaceHistory Replaces browser history.
                                 * @private
                                 */
                                _updateHistory: function(title, path, opt_replaceHistory) {
                                    var historyParams = {
                                        path: path,
                                        surface: true
                                    };
                            
                                    if (opt_replaceHistory) {
                                        win.history.replaceState(historyParams, title, path);
                                    }
                                    else {
                                        win.history.pushState(historyParams, title, path);
                                    }
                                }
                            }, {
                                ATTRS: {
                                    /**
                                     * Defines the default document title in case the screen doesn't have
                                     * any `title`. If null, the default browser behavior for setting the
                                     * title is used for when there's no title HTML tag on the document.
                                     *
                                     * @attribute defaultTitle
                                     * @default null
                                     * @type {String|null}
                                     */
                                    defaultTitle: {
                                        validator: A.Lang.isString || A.Lang.isNull,
                                        value: null
                                    },
                            
                                    /**
                                     * CSS selector string used to filter link click events so that only the
                                     * links which match it will have the enhanced navigation behavior.
                                     *
                                     * @attribute linkSelector
                                     * @type String
                                     * @default "a"
                                     * @initOnly
                                     */
                                    linkSelector: {
                                        value: 'a',
                                        writeOnce: 'initOnly'
                                    },
                            
                                    /**
                                     * Absolute base path from which all routes should be evaluated.
                                     *
                                     * @attribute basePath
                                     * @type String
                                     * @default ''
                                     */
                                    basePath: {
                                        value: ''
                                    }
                                }
                            });