]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - jssource/src_files/include/javascript/yui3/build/history/history-hash.js
Release 6.5.0
[Github/sugarcrm.git] / jssource / src_files / include / javascript / yui3 / build / history / history-hash.js
1 /*
2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
5 version: 3.3.0
6 build: 3167
7 */
8 YUI.add('history-hash', function(Y) {
9
10 /**
11  * Provides browser history management backed by
12  * <code>window.location.hash</code>, as well as convenience methods for working
13  * with the location hash and a synthetic <code>hashchange</code> event that
14  * normalizes differences across browsers.
15  *
16  * @module history
17  * @submodule history-hash
18  * @since 3.2.0
19  * @class HistoryHash
20  * @extends HistoryBase
21  * @constructor
22  * @param {Object} config (optional) Configuration object. See the HistoryBase
23  *   documentation for details.
24  */
25
26 var HistoryBase = Y.HistoryBase,
27     Lang        = Y.Lang,
28     YArray      = Y.Array,
29     YObject     = Y.Object,
30     GlobalEnv   = YUI.namespace('Env.HistoryHash'),
31
32     SRC_HASH    = 'hash',
33
34     hashNotifiers,
35     oldHash,
36     oldUrl,
37     win             = Y.config.win,
38     location        = win.location,
39     useHistoryHTML5 = Y.config.useHistoryHTML5;
40
41 function HistoryHash() {
42     HistoryHash.superclass.constructor.apply(this, arguments);
43 }
44
45 Y.extend(HistoryHash, HistoryBase, {
46     // -- Initialization -------------------------------------------------------
47     _init: function (config) {
48         var bookmarkedState = HistoryHash.parseHash();
49
50         // If an initialState was provided, merge the bookmarked state into it
51         // (the bookmarked state wins).
52         config = config || {};
53
54         this._initialState = config.initialState ?
55                 Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
56
57         // Subscribe to the synthetic hashchange event (defined below) to handle
58         // changes.
59         Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
60
61         HistoryHash.superclass._init.apply(this, arguments);
62     },
63
64     // -- Protected Methods ----------------------------------------------------
65     _change: function (src, state, options) {
66         // Stringify all values to ensure that comparisons don't fail after
67         // they're coerced to strings in the location hash.
68         YObject.each(state, function (value, key) {
69             if (Lang.isValue(value)) {
70                 state[key] = value.toString();
71             }
72         });
73
74         return HistoryHash.superclass._change.call(this, src, state, options);
75     },
76
77     _storeState: function (src, newState) {
78         var decode  = HistoryHash.decode,
79             newHash = HistoryHash.createHash(newState);
80
81         HistoryHash.superclass._storeState.apply(this, arguments);
82
83         // Update the location hash with the changes, but only if the new hash
84         // actually differs from the current hash (this avoids creating multiple
85         // history entries for a single state).
86         //
87         // We always compare decoded hashes, since it's possible that the hash
88         // could be set incorrectly to a non-encoded value outside of
89         // HistoryHash.
90         if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(newHash)) {
91             HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
92         }
93     },
94
95     // -- Protected Event Handlers ---------------------------------------------
96
97     /**
98      * Handler for hashchange events.
99      *
100      * @method _afterHashChange
101      * @param {Event} e
102      * @protected
103      */
104     _afterHashChange: function (e) {
105         this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
106     }
107 }, {
108     // -- Public Static Properties ---------------------------------------------
109     NAME: 'historyHash',
110
111     /**
112      * Constant used to identify state changes originating from
113      * <code>hashchange</code> events.
114      *
115      * @property SRC_HASH
116      * @type String
117      * @static
118      * @final
119      */
120     SRC_HASH: SRC_HASH,
121
122     /**
123      * <p>
124      * Prefix to prepend when setting the hash fragment. For example, if the
125      * prefix is <code>!</code> and the hash fragment is set to
126      * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
127      * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
128      * Ajax application crawlable in accordance with Google's guidelines at
129      * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
130      * </p>
131      *
132      * <p>
133      * Note that this prefix applies to all HistoryHash instances. It's not
134      * possible for individual instances to use their own prefixes since they
135      * all operate on the same URL.
136      * </p>
137      *
138      * @property hashPrefix
139      * @type String
140      * @default ''
141      * @static
142      */
143     hashPrefix: '',
144
145     // -- Protected Static Properties ------------------------------------------
146
147     /**
148      * Regular expression used to parse location hash/query strings.
149      *
150      * @property _REGEX_HASH
151      * @type RegExp
152      * @protected
153      * @static
154      * @final
155      */
156     _REGEX_HASH: /([^\?#&]+)=([^&]+)/g,
157
158     // -- Public Static Methods ------------------------------------------------
159
160     /**
161      * Creates a location hash string from the specified object of key/value
162      * pairs.
163      *
164      * @method createHash
165      * @param {Object} params object of key/value parameter pairs
166      * @return {String} location hash string
167      * @static
168      */
169     createHash: function (params) {
170         var encode = HistoryHash.encode,
171             hash   = [];
172
173         YObject.each(params, function (value, key) {
174             if (Lang.isValue(value)) {
175                 hash.push(encode(key) + '=' + encode(value));
176             }
177         });
178
179         return hash.join('&');
180     },
181
182     /**
183      * Wrapper around <code>decodeURIComponent()</code> that also converts +
184      * chars into spaces.
185      *
186      * @method decode
187      * @param {String} string string to decode
188      * @return {String} decoded string
189      * @static
190      */
191     decode: function (string) {
192         return decodeURIComponent(string.replace(/\+/g, ' '));
193     },
194
195     /**
196      * Wrapper around <code>encodeURIComponent()</code> that converts spaces to
197      * + chars.
198      *
199      * @method encode
200      * @param {String} string string to encode
201      * @return {String} encoded string
202      * @static
203      */
204     encode: function (string) {
205         return encodeURIComponent(string).replace(/%20/g, '+');
206     },
207
208     /**
209      * Gets the raw (not decoded) current location hash, minus the preceding '#'
210      * character and the hashPrefix (if one is set).
211      *
212      * @method getHash
213      * @return {String} current location hash
214      * @static
215      */
216     getHash: (Y.UA.gecko ? function () {
217         // Gecko's window.location.hash returns a decoded string and we want all
218         // encoding untouched, so we need to get the hash value from
219         // window.location.href instead. We have to use UA sniffing rather than
220         // feature detection, since the only way to detect this would be to
221         // actually change the hash.
222         var matches = /#(.*)$/.exec(location.href),
223             hash    = matches && matches[1] || '',
224             prefix  = HistoryHash.hashPrefix;
225
226         return prefix && hash.indexOf(prefix) === 0 ?
227                     hash.replace(prefix, '') : hash;
228     } : function () {
229         var hash   = location.hash.substr(1),
230             prefix = HistoryHash.hashPrefix;
231
232         // Slight code duplication here, but execution speed is of the essence
233         // since getHash() is called every 50ms to poll for changes in browsers
234         // that don't support native onhashchange. An additional function call
235         // would add unnecessary overhead.
236         return prefix && hash.indexOf(prefix) === 0 ?
237                     hash.replace(prefix, '') : hash;
238     }),
239
240     /**
241      * Gets the current bookmarkable URL.
242      *
243      * @method getUrl
244      * @return {String} current bookmarkable URL
245      * @static
246      */
247     getUrl: function () {
248         return location.href;
249     },
250
251     /**
252      * Parses a location hash string into an object of key/value parameter
253      * pairs. If <i>hash</i> is not specified, the current location hash will
254      * be used.
255      *
256      * @method parseHash
257      * @param {String} hash (optional) location hash string
258      * @return {Object} object of parsed key/value parameter pairs
259      * @static
260      */
261     parseHash: function (hash) {
262         var decode = HistoryHash.decode,
263             i,
264             len,
265             matches,
266             param,
267             params = {},
268             prefix = HistoryHash.hashPrefix,
269             prefixIndex;
270
271         hash = Lang.isValue(hash) ? hash : HistoryHash.getHash();
272
273         if (prefix) {
274             prefixIndex = hash.indexOf(prefix);
275
276             if (prefixIndex === 0 || (prefixIndex === 1 && hash.charAt(0) === '#')) {
277                 hash = hash.replace(prefix, '');
278             }
279         }
280
281         matches = hash.match(HistoryHash._REGEX_HASH) || [];
282
283         for (i = 0, len = matches.length; i < len; ++i) {
284             param = matches[i].split('=');
285             params[decode(param[0])] = decode(param[1]);
286         }
287
288         return params;
289     },
290
291     /**
292      * Replaces the browser's current location hash with the specified hash
293      * and removes all forward navigation states, without creating a new browser
294      * history entry. Automatically prepends the <code>hashPrefix</code> if one
295      * is set.
296      *
297      * @method replaceHash
298      * @param {String} hash new location hash
299      * @static
300      */
301     replaceHash: function (hash) {
302         if (hash.charAt(0) === '#') {
303             hash = hash.substr(1);
304         }
305
306         location.replace('#' + (HistoryHash.hashPrefix || '') + hash);
307     },
308
309     /**
310      * Sets the browser's location hash to the specified string. Automatically
311      * prepends the <code>hashPrefix</code> if one is set.
312      *
313      * @method setHash
314      * @param {String} hash new location hash
315      * @static
316      */
317     setHash: function (hash) {
318         if (hash.charAt(0) === '#') {
319             hash = hash.substr(1);
320         }
321
322         location.hash = (HistoryHash.hashPrefix || '') + hash;
323     }
324 });
325
326 // -- Synthetic hashchange Event -----------------------------------------------
327
328 // TODO: YUIDoc currently doesn't provide a good way to document synthetic DOM
329 // events. For now, we're just documenting the hashchange event on the YUI
330 // object, which is about the best we can do until enhancements are made to
331 // YUIDoc.
332
333 /**
334  * <p>
335  * Synthetic <code>window.onhashchange</code> event that normalizes differences
336  * across browsers and provides support for browsers that don't natively support
337  * <code>onhashchange</code>.
338  * </p>
339  *
340  * <p>
341  * This event is provided by the <code>history-hash</code> module.
342  * </p>
343  *
344  * <p>
345  * <strong>Usage example:</strong>
346  * </p>
347  *
348  * <code><pre>
349  * YUI().use('history-hash', function (Y) {
350  * &nbsp;&nbsp;Y.on('hashchange', function (e) {
351  * &nbsp;&nbsp;&nbsp;&nbsp;// Handle hashchange events on the current window.
352  * &nbsp;&nbsp;}, Y.config.win);
353  * });
354  * </pre></code>
355  *
356  * @event hashchange
357  * @param {EventFacade} e Event facade with the following additional
358  *   properties:
359  *
360  * <dl>
361  *   <dt>oldHash</dt>
362  *   <dd>
363  *     Previous hash fragment value before the change.
364  *   </dd>
365  *
366  *   <dt>oldUrl</dt>
367  *   <dd>
368  *     Previous URL (including the hash fragment) before the change.
369  *   </dd>
370  *
371  *   <dt>newHash</dt>
372  *   <dd>
373  *     New hash fragment value after the change.
374  *   </dd>
375  *
376  *   <dt>newUrl</dt>
377  *   <dd>
378  *     New URL (including the hash fragment) after the change.
379  *   </dd>
380  * </dl>
381  * @for YUI
382  * @since 3.2.0
383  */
384
385 hashNotifiers = GlobalEnv._notifiers;
386
387 if (!hashNotifiers) {
388     hashNotifiers = GlobalEnv._notifiers = [];
389 }
390
391 Y.Event.define('hashchange', {
392     on: function (node, subscriber, notifier) {
393         // Ignore this subscription if the node is anything other than the
394         // window or document body, since those are the only elements that
395         // should support the hashchange event. Note that the body could also be
396         // a frameset, but that's okay since framesets support hashchange too.
397         if (node.compareTo(win) || node.compareTo(Y.config.doc.body)) {
398             hashNotifiers.push(notifier);
399         }
400     },
401
402     detach: function (node, subscriber, notifier) {
403         var index = YArray.indexOf(hashNotifiers, notifier);
404
405         if (index !== -1) {
406             hashNotifiers.splice(index, 1);
407         }
408     }
409 });
410
411 oldHash = HistoryHash.getHash();
412 oldUrl  = HistoryHash.getUrl();
413
414 if (HistoryBase.nativeHashChange) {
415     // Wrap the browser's native hashchange event.
416     Y.Event.attach('hashchange', function (e) {
417         var newHash = HistoryHash.getHash(),
418             newUrl  = HistoryHash.getUrl();
419
420         // Iterate over a copy of the hashNotifiers array since a subscriber
421         // could detach during iteration and cause the array to be re-indexed.
422         YArray.each(hashNotifiers.concat(), function (notifier) {
423             notifier.fire({
424                 _event : e,
425                 oldHash: oldHash,
426                 oldUrl : oldUrl,
427                 newHash: newHash,
428                 newUrl : newUrl
429             });
430         });
431
432         oldHash = newHash;
433         oldUrl  = newUrl;
434     }, win);
435 } else {
436     // Begin polling for location hash changes if there's not already a global
437     // poll running.
438     if (!GlobalEnv._hashPoll) {
439         if (Y.UA.webkit && !Y.UA.chrome &&
440                 navigator.vendor.indexOf('Apple') !== -1) {
441             // Attach a noop unload handler to disable Safari's back/forward
442             // cache. This works around a nasty Safari bug when the back button
443             // is used to return from a page on another domain, but results in
444             // slightly worse performance. This bug is not present in Chrome.
445             //
446             // Unfortunately a UA sniff is unavoidable here, but the
447             // consequences of a false positive are minor.
448             //
449             // Current as of Safari 5.0 (6533.16).
450             // See: https://bugs.webkit.org/show_bug.cgi?id=34679
451             Y.on('unload', function () {}, win);
452         }
453
454         GlobalEnv._hashPoll = Y.later(50, null, function () {
455             var newHash = HistoryHash.getHash(),
456                 newUrl;
457
458             if (oldHash !== newHash) {
459                 newUrl = HistoryHash.getUrl();
460
461                 YArray.each(hashNotifiers.concat(), function (notifier) {
462                     notifier.fire({
463                         oldHash: oldHash,
464                         oldUrl : oldUrl,
465                         newHash: newHash,
466                         newUrl : newUrl
467                     });
468                 });
469
470                 oldHash = newHash;
471                 oldUrl  = newUrl;
472             }
473         }, null, true);
474     }
475 }
476
477 Y.HistoryHash = HistoryHash;
478
479 // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
480 if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
481         (!HistoryBase.html5 || !Y.HistoryHTML5))) {
482     Y.History = HistoryHash;
483 }
484
485
486 }, '3.3.0' ,{requires:['event-synthetic', 'history-base', 'yui-later']});