Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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