/** * @author Steffen Meschkat (mesch@google.com) * * @fileoverview Core functions that are used to manage Ajax application * state with the URL fragment identifier method. * * These functions use functions defined in example.js. */ /** * This variable contains the current state of the application in its * external representation, i.e. as URL parameters. It is used by the * history detection function to detect history navigation: If the * fragment identifier of the URL of the page is different that the * value stored in this object, then a history navigation event * occurred and the application is notified. It is global in order to * share it with the function that updates the page URL to reflect * changed application state: this must not be mistaken for history * navigation. */ var currentHash = null; /** * Map that says which state elements dominate which other. The keys * are the dominant elements, the values are arrays of properties that * are recessive against that element. If an element changes, all * state elements that are dominated by it are deleted. */ var dominatedElements = null; /** * Obtains the initial state of the application based on the URL it * was loaded from. * * @param {Function} loader A function that takes a state Object as * argument adnd resets the applications into the state represented by * that object. * * @param {Object} dominate Initializer for dominatedElements. */ function applyInitialState(loader, dominate) { dominatedElements = dominate; currentHash = document.location.hash; var state = decode(currentHash.substring(1)); loader(state); detectHistoryNavigation(loader); } /** * Records the value of a state element in the hard or soft state part * of the application state. * * This uses the global variable currentHash. This function updates * the page URL, but we don't want the function that detects history * navigation to false consider this a history navigation * event. Therefore, we update the URL that the * detectHistoryNavigation() function expects to see as the current * URL of the page. This is accomplished by sharing the value through * this object. * * @param {String} name The name of the state element. * * @param {String} value The value of the state element. * * @param {Boolean} hard If true, the state change is hard, i.e. it * creates a new browser history step. */ function recordState(name, value, hard) { setTimeout(function() { recordStateImpl_(name, value, hard); }, 10); } function recordStateImpl_(name, value, hard) { if (typeof console != 'undefined') { console.log(name + ' ' + value); } var state = decode(location.hash.substring(1)); state[name] = value; var dominated = dominatedElements[name] || []; for (var i = 0; i < dominated.length; ++i) { delete state[dominated[i]]; } var stateUrl = encode(state); currentHash = "#" + stateUrl; // Internet Explorer will not create a history entry for a changed // fragment identifier if there is no anchor in the page that // corresponds to it. So we have to create one. Because the browser // will scroll to it, we also have to position it at the top of // screen. var a = document.createElement('A'); a.style.position = 'absolute'; a.style.top = getScrollTop(window) + 'px'; a.name = stateUrl; document.body.appendChild(a); if (hard) { location.href = currentHash; } else { location.replace(currentHash); } document.body.removeChild(a); } /** * Computes the vertical position on the page of the top edge of the * browser window. * * @param {Window} win The window of which to compute the scroll * position. * * @return {Number} The vertical scroll position of the window in * pixels. */ function getScrollTop(win) { if (typeof win.scrollY != 'undefined') { return win.scrollY; } else if (typeof win.documentElement != 'undefined' && typeof win.documentElement.scrollTop != 'undefined') { return win.documentElement.scrollTop + win.document.body.scrollTop; } else { return win.document.body.scrollTop; } } /** * Initializes history navigation detection. Sets up a function that * polls the current URL and detects if it changes. If it does, * invokes a callback with the new application state decoded from the * new URL. * * This function uses the global variable currentHash. The last seen * state is stored in the property hash of this object. This allows to * share the current value with recordState(), so that the load() * callback is not invoked when the application updates the state. * * @param {Function} load A function that is invoked when the page URL * chnages. Receives the new application state decoded from the new * page URL as argument. */ function detectHistoryNavigation(load) { window.setInterval(function() { if (location.hash != currentHash) { if (typeof console != 'undefined') { console.log(location.hash + ' <- ' + currentHash); } currentHash = location.hash; var state = decode(location.hash.substr(1)); load(state); } }, 50); }