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