Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / util / History.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.util.History
17  *
18  * History management component that allows you to register arbitrary tokens that signify application
19  * history state on navigation actions.  You can then handle the history {@link #change} event in order
20  * to reset your application UI to the appropriate state when the user navigates forward or backward through
21  * the browser history stack.
22  *
23  * ## Initializing
24  * The {@link #init} method of the History object must be called before using History. This sets up the internal
25  * state and must be the first thing called before using History.
26  *
27  * ## Setup
28  * The History objects requires elements on the page to keep track of the browser history. For older versions of IE,
29  * an IFrame is required to do the tracking. For other browsers, a hidden field can be used. The history objects expects
30  * these to be on the page before the {@link #init} method is called. The following markup is suggested in order
31  * to support all browsers:
32  *
33  *     <form id="history-form" class="x-hide-display">
34  *         <input type="hidden" id="x-history-field" />
35  *         <iframe id="x-history-frame"></iframe>
36  *     </form>
37  *
38  * @singleton
39  */
40 Ext.define('Ext.util.History', {
41     singleton: true,
42     alternateClassName: 'Ext.History',
43     mixins: {
44         observable: 'Ext.util.Observable'
45     },
46
47     constructor: function() {
48         var me = this;
49         me.oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
50         me.iframe = null;
51         me.hiddenField = null;
52         me.ready = false;
53         me.currentToken = null;
54     },
55
56     getHash: function() {
57         var href = window.location.href,
58             i = href.indexOf("#");
59
60         return i >= 0 ? href.substr(i + 1) : null;
61     },
62
63     doSave: function() {
64         this.hiddenField.value = this.currentToken;
65     },
66
67
68     handleStateChange: function(token) {
69         this.currentToken = token;
70         this.fireEvent('change', token);
71     },
72
73     updateIFrame: function(token) {
74         var html = '<html><body><div id="state">' +
75                     Ext.util.Format.htmlEncode(token) +
76                     '</div></body></html>';
77
78         try {
79             var doc = this.iframe.contentWindow.document;
80             doc.open();
81             doc.write(html);
82             doc.close();
83             return true;
84         } catch (e) {
85             return false;
86         }
87     },
88
89     checkIFrame: function () {
90         var me = this,
91             contentWindow = me.iframe.contentWindow;
92
93         if (!contentWindow || !contentWindow.document) {
94             Ext.Function.defer(this.checkIFrame, 10, this);
95             return;
96         }
97
98         var doc = contentWindow.document,
99             elem = doc.getElementById("state"),
100             oldToken = elem ? elem.innerText : null,
101             oldHash = me.getHash();
102
103         Ext.TaskManager.start({
104             run: function () {
105                 var doc = contentWindow.document,
106                     elem = doc.getElementById("state"),
107                     newToken = elem ? elem.innerText : null,
108                     newHash = me.getHash();
109
110                 if (newToken !== oldToken) {
111                     oldToken = newToken;
112                     me.handleStateChange(newToken);
113                     window.top.location.hash = newToken;
114                     oldHash = newToken;
115                     me.doSave();
116                 } else if (newHash !== oldHash) {
117                     oldHash = newHash;
118                     me.updateIFrame(newHash);
119                 }
120             },
121             interval: 50,
122             scope: me
123         });
124         me.ready = true;
125         me.fireEvent('ready', me);
126     },
127
128     startUp: function () {
129         var me = this;
130
131         me.currentToken = me.hiddenField.value || this.getHash();
132
133         if (me.oldIEMode) {
134             me.checkIFrame();
135         } else {
136             var hash = me.getHash();
137             Ext.TaskManager.start({
138                 run: function () {
139                     var newHash = me.getHash();
140                     if (newHash !== hash) {
141                         hash = newHash;
142                         me.handleStateChange(hash);
143                         me.doSave();
144                     }
145                 },
146                 interval: 50,
147                 scope: me
148             });
149             me.ready = true;
150             me.fireEvent('ready', me);
151         }
152
153     },
154
155     /**
156      * The id of the hidden field required for storing the current history token.
157      * @type String
158      * @property
159      */
160     fieldId: Ext.baseCSSPrefix + 'history-field',
161     /**
162      * The id of the iframe required by IE to manage the history stack.
163      * @type String
164      * @property
165      */
166     iframeId: Ext.baseCSSPrefix + 'history-frame',
167
168     /**
169      * Initialize the global History instance.
170      * @param {Boolean} onReady (optional) A callback function that will be called once the history
171      * component is fully initialized.
172      * @param {Object} scope (optional) The scope (`this` reference) in which the callback is executed. Defaults to the browser window.
173      */
174     init: function (onReady, scope) {
175         var me = this;
176
177         if (me.ready) {
178             Ext.callback(onReady, scope, [me]);
179             return;
180         }
181
182         if (!Ext.isReady) {
183             Ext.onReady(function() {
184                 me.init(onReady, scope);
185             });
186             return;
187         }
188
189         me.hiddenField = Ext.getDom(me.fieldId);
190
191         if (me.oldIEMode) {
192             me.iframe = Ext.getDom(me.iframeId);
193         }
194
195         me.addEvents(
196             /**
197              * @event ready
198              * Fires when the Ext.util.History singleton has been initialized and is ready for use.
199              * @param {Ext.util.History} The Ext.util.History singleton.
200              */
201             'ready',
202             /**
203              * @event change
204              * Fires when navigation back or forwards within the local page's history occurs.
205              * @param {String} token An identifier associated with the page state at that point in its history.
206              */
207             'change'
208         );
209
210         if (onReady) {
211             me.on('ready', onReady, scope, {single: true});
212         }
213         me.startUp();
214     },
215
216     /**
217      * Add a new token to the history stack. This can be any arbitrary value, although it would
218      * commonly be the concatenation of a component id and another id marking the specific history
219      * state of that component. Example usage:
220      *
221      *     // Handle tab changes on a TabPanel
222      *     tabPanel.on('tabchange', function(tabPanel, tab){
223      *          Ext.History.add(tabPanel.id + ':' + tab.id);
224      *     });
225      *
226      * @param {String} token The value that defines a particular application-specific history state
227      * @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current token
228      * it will not save a new history step. Set to false if the same state can be saved more than once
229      * at the same history stack location.
230      */
231     add: function (token, preventDup) {
232         var me = this;
233
234         if (preventDup !== false) {
235             if (me.getToken() === token) {
236                 return true;
237             }
238         }
239
240         if (me.oldIEMode) {
241             return me.updateIFrame(token);
242         } else {
243             window.top.location.hash = token;
244             return true;
245         }
246     },
247
248     /**
249      * Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
250      */
251     back: function() {
252         window.history.go(-1);
253     },
254
255     /**
256      * Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
257      */
258     forward: function(){
259         window.history.go(1);
260     },
261
262     /**
263      * Retrieves the currently-active history token.
264      * @return {String} The token
265      */
266     getToken: function() {
267         return this.ready ? this.currentToken : this.getHash();
268     }
269 });