Upgrade to ExtJS 4.0.2 - Released 06/09/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  * @markdown
39  * @singleton
40  */
41 Ext.define('Ext.util.History', {
42     singleton: true,
43     alternateClassName: 'Ext.History',
44     mixins: {
45         observable: 'Ext.util.Observable'
46     },
47     
48     constructor: function() {
49         var me = this;
50         me.oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
51         me.iframe = null;
52         me.hiddenField = null;
53         me.ready = false;
54         me.currentToken = null;
55     },
56     
57     getHash: function() {
58         var href = window.location.href,
59             i = href.indexOf("#");
60             
61         return i >= 0 ? href.substr(i + 1) : null;
62     },
63
64     doSave: function() {
65         this.hiddenField.value = this.currentToken;
66     },
67     
68
69     handleStateChange: function(token) {
70         this.currentToken = token;
71         this.fireEvent('change', token);
72     },
73
74     updateIFrame: function(token) {
75         var html = '<html><body><div id="state">' + 
76                     Ext.util.Format.htmlEncode(token) + 
77                     '</div></body></html>';
78
79         try {
80             var doc = this.iframe.contentWindow.document;
81             doc.open();
82             doc.write(html);
83             doc.close();
84             return true;
85         } catch (e) {
86             return false;
87         }
88     },
89
90     checkIFrame: function () {
91         var me = this,
92             contentWindow = me.iframe.contentWindow;
93             
94         if (!contentWindow || !contentWindow.document) {
95             Ext.Function.defer(this.checkIFrame, 10, this);
96             return;
97         }
98        
99         var doc = contentWindow.document,
100             elem = doc.getElementById("state"),
101             oldToken = elem ? elem.innerText : null,
102             oldHash = me.getHash();
103            
104         Ext.TaskManager.start({
105             run: function () {
106                 var doc = contentWindow.document,
107                     elem = doc.getElementById("state"),
108                     newToken = elem ? elem.innerText : null,
109                     newHash = me.getHash();
110
111                 if (newToken !== oldToken) {
112                     oldToken = newToken;
113                     me.handleStateChange(newToken);
114                     window.top.location.hash = newToken;
115                     oldHash = newToken;
116                     me.doSave();
117                 } else if (newHash !== oldHash) {
118                     oldHash = newHash;
119                     me.updateIFrame(newHash);
120                 }
121             }, 
122             interval: 50,
123             scope: me
124         });
125         me.ready = true;
126         me.fireEvent('ready', me);            
127     },
128
129     startUp: function () {
130         var me = this;
131         
132         me.currentToken = me.hiddenField.value || this.getHash();
133
134         if (me.oldIEMode) {
135             me.checkIFrame();
136         } else {
137             var hash = me.getHash();
138             Ext.TaskManager.start({
139                 run: function () {
140                     var newHash = me.getHash();
141                     if (newHash !== hash) {
142                         hash = newHash;
143                         me.handleStateChange(hash);
144                         me.doSave();
145                     }
146                 },
147                 interval: 50,
148                 scope: me
149             });
150             me.ready = true;
151             me.fireEvent('ready', me);
152         }
153         
154     },
155
156     /**
157      * The id of the hidden field required for storing the current history token.
158      * @type String
159      * @property
160      */
161     fieldId: Ext.baseCSSPrefix + 'history-field',
162     /**
163      * The id of the iframe required by IE to manage the history stack.
164      * @type String
165      * @property
166      */
167     iframeId: Ext.baseCSSPrefix + 'history-frame',
168
169     /**
170      * Initialize the global History instance.
171      * @param {Boolean} onReady (optional) A callback function that will be called once the history
172      * component is fully initialized.
173      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to the browser window.
174      */
175     init: function (onReady, scope) {
176         var me = this;
177         
178         if (me.ready) {
179             Ext.callback(onReady, scope, [me]);
180             return;
181         }
182         
183         if (!Ext.isReady) {
184             Ext.onReady(function() {
185                 me.init(onReady, scope);
186             });
187             return;
188         }
189         
190         me.hiddenField = Ext.getDom(me.fieldId);
191         
192         if (me.oldIEMode) {
193             me.iframe = Ext.getDom(me.iframeId);
194         }
195         
196         me.addEvents(
197             /**
198              * @event ready
199              * Fires when the Ext.util.History singleton has been initialized and is ready for use.
200              * @param {Ext.util.History} The Ext.util.History singleton.
201              */
202             'ready',
203             /**
204              * @event change
205              * Fires when navigation back or forwards within the local page's history occurs.
206              * @param {String} token An identifier associated with the page state at that point in its history.
207              */
208             'change'
209         );
210         
211         if (onReady) {
212             me.on('ready', onReady, scope, {single: true});
213         }
214         me.startUp();
215     },
216
217     /**
218      * Add a new token to the history stack. This can be any arbitrary value, although it would
219      * commonly be the concatenation of a component id and another id marking the specifc history
220      * state of that component.  Example usage:
221      * <pre><code>
222 // Handle tab changes on a TabPanel
223 tabPanel.on('tabchange', function(tabPanel, tab){
224 Ext.History.add(tabPanel.id + ':' + tab.id);
225 });
226 </code></pre>
227      * @param {String} token The value that defines a particular application-specific history state
228      * @param {Boolean} preventDuplicates When true, if the passed token matches the current token
229      * it will not save a new history step. Set to false if the same state can be saved more than once
230      * at the same history stack location (defaults to true).
231      */
232     add: function (token, preventDup) {
233         var me = this;
234         
235         if (preventDup !== false) {
236             if (me.getToken() === token) {
237                 return true;
238             }
239         }
240         
241         if (me.oldIEMode) {
242             return me.updateIFrame(token);
243         } else {
244             window.top.location.hash = token;
245             return true;
246         }
247     },
248
249     /**
250      * Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
251      */
252     back: function() {
253         window.history.go(-1);
254     },
255
256     /**
257      * Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
258      */
259     forward: function(){
260         window.history.go(1);
261     },
262
263     /**
264      * Retrieves the currently-active history token.
265      * @return {String} The token
266      */
267     getToken: function() {
268         return this.ready ? this.currentToken : this.getHash();
269     }
270 });