Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('history-base', function (Y, NAME) {
2
 
3
/**
4
 * Provides browser history management functionality using a simple
5
 * add/replace/get paradigm. This can be used to ensure that the browser's back
6
 * and forward buttons work as the user expects and to provide bookmarkable URLs
7
 * that return the user to the current application state, even in an Ajax
8
 * application that doesn't perform full-page refreshes.
9
 *
10
 * @module history
11
 * @main history
12
 * @since 3.2.0
13
 */
14
 
15
/**
16
 * Provides global state management backed by an object, but with no browser
17
 * history integration. For actual browser history integration and back/forward
18
 * support, use the history-html5 or history-hash modules.
19
 *
20
 * @module history
21
 * @submodule history-base
22
 * @class HistoryBase
23
 * @uses EventTarget
24
 * @constructor
25
 * @param {Object} config (optional) configuration object, which may contain
26
 *   zero or more of the following properties:
27
 *
28
 * <dl>
29
 *   <dt>force (Boolean)</dt>
30
 *   <dd>
31
 *     If `true`, a `history:change` event will be fired whenever the URL
32
 *     changes, even if there is no associated state change. Default is `false`.
33
 *   </dd>
34
 *
35
 *   <dt>initialState (Object)</dt>
36
 *   <dd>
37
 *     Initial state to set, as an object hash of key/value pairs. This will be
38
 *     merged into the current global state.
39
 *   </dd>
40
 * </dl>
41
 */
42
 
43
var Lang      = Y.Lang,
44
    Obj       = Y.Object,
45
    GlobalEnv = YUI.namespace('Env.History'),
46
    YArray    = Y.Array,
47
 
48
    doc       = Y.config.doc,
49
    docMode   = doc.documentMode,
50
    win       = Y.config.win,
51
 
52
    DEFAULT_OPTIONS = {merge: true},
53
    EVT_CHANGE      = 'change',
54
    SRC_ADD         = 'add',
55
    SRC_REPLACE     = 'replace';
56
 
57
function HistoryBase() {
58
    this._init.apply(this, arguments);
59
}
60
 
61
Y.augment(HistoryBase, Y.EventTarget, null, null, {
62
    emitFacade : true,
63
    prefix     : 'history',
64
    preventable: false,
65
    queueable  : true
66
});
67
 
68
if (!GlobalEnv._state) {
69
    GlobalEnv._state = {};
70
}
71
 
72
// -- Private Methods ----------------------------------------------------------
73
 
74
/**
75
 * Returns <code>true</code> if <i>value</i> is a simple object and not a
76
 * function or an array.
77
 *
78
 * @method _isSimpleObject
79
 * @param {mixed} value
80
 * @return {Boolean}
81
 * @private
82
 */
83
function _isSimpleObject(value) {
84
    return Lang.type(value) === 'object';
85
}
86
 
87
// -- Public Static Properties -------------------------------------------------
88
 
89
/**
90
 * Name of this component.
91
 *
92
 * @property NAME
93
 * @type String
94
 * @static
95
 */
96
HistoryBase.NAME = 'historyBase';
97
 
98
/**
99
 * Constant used to identify state changes originating from the
100
 * <code>add()</code> method.
101
 *
102
 * @property SRC_ADD
103
 * @type String
104
 * @static
105
 * @final
106
 */
107
HistoryBase.SRC_ADD = SRC_ADD;
108
 
109
/**
110
 * Constant used to identify state changes originating from the
111
 * <code>replace()</code> method.
112
 *
113
 * @property SRC_REPLACE
114
 * @type String
115
 * @static
116
 * @final
117
 */
118
HistoryBase.SRC_REPLACE = SRC_REPLACE;
119
 
120
/**
121
 * Whether or not this browser supports the HTML5 History API.
122
 *
123
 * @property html5
124
 * @type Boolean
125
 * @static
126
 */
127
 
128
// All HTML5-capable browsers except Gecko 2+ (Firefox 4+) correctly return
129
// true for 'onpopstate' in win. In order to support Gecko 2, we fall back to a
130
// UA sniff for now. (current as of Firefox 4.0b2)
131
HistoryBase.html5 = !!(win.history && win.history.pushState &&
132
        win.history.replaceState && ('onpopstate' in win || Y.UA.gecko >= 2) &&
133
        (!Y.UA.android || Y.UA.android >= 2.4));
134
 
135
/**
136
 * Whether or not this browser supports the <code>window.onhashchange</code>
137
 * event natively. Note that even if this is <code>true</code>, you may
138
 * still want to use HistoryHash's synthetic <code>hashchange</code> event
139
 * since it normalizes implementation differences and fixes spec violations
140
 * across various browsers.
141
 *
142
 * @property nativeHashChange
143
 * @type Boolean
144
 * @static
145
 */
146
 
147
// Most browsers that support hashchange expose it on the window. Opera 10.6+
148
// exposes it on the document (but you can still attach to it on the window).
149
//
150
// IE8 supports the hashchange event, but only in IE8 Standards
151
// Mode. However, IE8 in IE7 compatibility mode still defines the
152
// event but never fires it, so we can't just detect the event. We also can't
153
// just UA sniff for IE8, since other browsers support this event as well.
154
HistoryBase.nativeHashChange = ('onhashchange' in win || 'onhashchange' in doc) &&
155
        (!docMode || docMode > 7);
156
 
157
Y.mix(HistoryBase.prototype, {
158
    // -- Initialization -------------------------------------------------------
159
 
160
    /**
161
     * Initializes this HistoryBase instance. This method is called by the
162
     * constructor.
163
     *
164
     * @method _init
165
     * @param {Object} config configuration object
166
     * @protected
167
     */
168
    _init: function (config) {
169
        var initialState;
170
 
171
        /**
172
         * Configuration object provided by the user on instantiation, or an
173
         * empty object if one wasn't provided.
174
         *
175
         * @property _config
176
         * @type Object
177
         * @default {}
178
         * @protected
179
         */
180
        config = this._config = config || {};
181
 
182
        /**
183
         * If `true`, a `history:change` event will be fired whenever the URL
184
         * changes, even if there is no associated state change.
185
         *
186
         * @property force
187
         * @type Boolean
188
         * @default false
189
         */
190
         this.force = !!config.force;
191
 
192
        /**
193
         * Resolved initial state: a merge of the user-supplied initial state
194
         * (if any) and any initial state provided by a subclass. This may
195
         * differ from <code>_config.initialState</code>. If neither the config
196
         * nor a subclass supplies an initial state, this property will be
197
         * <code>null</code>.
198
         *
199
         * @property _initialState
200
         * @type Object|null
201
         * @default {}
202
         * @protected
203
         */
204
        initialState = this._initialState = this._initialState ||
205
                config.initialState || null;
206
 
207
        /**
208
         * Fired when the state changes. To be notified of all state changes
209
         * regardless of the History or YUI instance that generated them,
210
         * subscribe to this event on <code>Y.Global</code>. If you would rather
211
         * be notified only about changes generated by this specific History
212
         * instance, subscribe to this event on the instance.
213
         *
214
         * @event history:change
215
         * @param {EventFacade} e Event facade with the following additional
216
         *   properties:
217
         *
218
         * <dl>
219
         *   <dt>changed (Object)</dt>
220
         *   <dd>
221
         *     Object hash of state items that have been added or changed. The
222
         *     key is the item key, and the value is an object containing
223
         *     <code>newVal</code> and <code>prevVal</code> properties
224
         *     representing the values of the item both before and after the
225
         *     change. If the item was newly added, <code>prevVal</code> will be
226
         *     <code>undefined</code>.
227
         *   </dd>
228
         *
229
         *   <dt>newVal (Object)</dt>
230
         *   <dd>
231
         *     Object hash of key/value pairs of all state items after the
232
         *     change.
233
         *   </dd>
234
         *
235
         *   <dt>prevVal (Object)</dt>
236
         *   <dd>
237
         *     Object hash of key/value pairs of all state items before the
238
         *     change.
239
         *   </dd>
240
         *
241
         *   <dt>removed (Object)</dt>
242
         *   <dd>
243
         *     Object hash of key/value pairs of state items that have been
244
         *     removed. Values are the old values prior to removal.
245
         *   </dd>
246
         *
247
         *   <dt>src (String)</dt>
248
         *   <dd>
249
         *     The source of the event. This can be used to selectively ignore
250
         *     events generated by certain sources.
251
         *   </dd>
252
         * </dl>
253
         */
254
        this.publish(EVT_CHANGE, {
255
            broadcast: 2,
256
            defaultFn: this._defChangeFn
257
        });
258
 
259
        // If initialState was provided, merge it into the current state.
260
        if (initialState) {
261
            this.replace(initialState);
262
        }
263
    },
264
 
265
    // -- Public Methods -------------------------------------------------------
266
 
267
    /**
268
     * Adds a state entry with new values for the specified keys. By default,
269
     * the new state will be merged into the existing state, and new values will
270
     * override existing values. Specifying a <code>null</code> or
271
     * <code>undefined</code> value will cause that key to be removed from the
272
     * new state entry.
273
     *
274
     * @method add
275
     * @param {Object} state Object hash of key/value pairs.
276
     * @param {Object} options (optional) Zero or more of the following options:
277
     *   <dl>
278
     *     <dt>merge (Boolean)</dt>
279
     *     <dd>
280
     *       <p>
281
     *       If <code>true</code> (the default), the new state will be merged
282
     *       into the existing state. New values will override existing values,
283
     *       and <code>null</code> or <code>undefined</code> values will be
284
     *       removed from the state.
285
     *       </p>
286
     *       <p>
287
     *       If <code>false</code>, the existing state will be discarded as a
288
     *       whole and the new state will take its place.
289
     *       </p>
290
     *     </dd>
291
     *   </dl>
292
     * @chainable
293
     */
294
    add: function () {
295
        var args = YArray(arguments, 0, true);
296
        args.unshift(SRC_ADD);
297
        return this._change.apply(this, args);
298
    },
299
 
300
    /**
301
     * Adds a state entry with a new value for a single key. By default, the new
302
     * value will be merged into the existing state values, and will override an
303
     * existing value with the same key if there is one. Specifying a
304
     * <code>null</code> or <code>undefined</code> value will cause the key to
305
     * be removed from the new state entry.
306
     *
307
     * @method addValue
308
     * @param {String} key State parameter key.
309
     * @param {String} value New value.
310
     * @param {Object} options (optional) Zero or more options. See
311
     *   <code>add()</code> for a list of supported options.
312
     * @chainable
313
     */
314
    addValue: function (key, value, options) {
315
        var state = {};
316
        state[key] = value;
317
        return this._change(SRC_ADD, state, options);
318
    },
319
 
320
    /**
321
     * Returns the current value of the state parameter specified by <i>key</i>,
322
     * or an object hash of key/value pairs for all current state parameters if
323
     * no key is specified.
324
     *
325
     * @method get
326
     * @param {String} key (optional) State parameter key.
327
     * @return {Object|String} Value of the specified state parameter, or an
328
     *   object hash of key/value pairs for all current state parameters.
329
     */
330
    get: function (key) {
331
        var state    = GlobalEnv._state,
332
            isObject = _isSimpleObject(state);
333
 
334
        if (key) {
335
            return isObject && Obj.owns(state, key) ? state[key] : undefined;
336
        } else {
337
            return isObject ? Y.mix({}, state, true) : state; // mix provides a fast shallow clone.
338
        }
339
    },
340
 
341
    /**
342
     * Same as <code>add()</code> except that a new browser history entry will
343
     * not be created. Instead, the current history entry will be replaced with
344
     * the new state.
345
     *
346
     * @method replace
347
     * @param {Object} state Object hash of key/value pairs.
348
     * @param {Object} options (optional) Zero or more options. See
349
     *   <code>add()</code> for a list of supported options.
350
     * @chainable
351
     */
352
    replace: function () {
353
        var args = YArray(arguments, 0, true);
354
        args.unshift(SRC_REPLACE);
355
        return this._change.apply(this, args);
356
    },
357
 
358
    /**
359
     * Same as <code>addValue()</code> except that a new browser history entry
360
     * will not be created. Instead, the current history entry will be replaced
361
     * with the new state.
362
     *
363
     * @method replaceValue
364
     * @param {String} key State parameter key.
365
     * @param {String} value New value.
366
     * @param {Object} options (optional) Zero or more options. See
367
     *   <code>add()</code> for a list of supported options.
368
     * @chainable
369
     */
370
    replaceValue: function (key, value, options) {
371
        var state = {};
372
        state[key] = value;
373
        return this._change(SRC_REPLACE, state, options);
374
    },
375
 
376
    // -- Protected Methods ----------------------------------------------------
377
 
378
    /**
379
     * Changes the state. This method provides a common implementation shared by
380
     * the public methods for changing state.
381
     *
382
     * @method _change
383
     * @param {String} src Source of the change, for inclusion in event facades
384
     *   to facilitate filtering.
385
     * @param {Object} state Object hash of key/value pairs.
386
     * @param {Object} options (optional) Zero or more options. See
387
     *   <code>add()</code> for a list of supported options.
388
     * @protected
389
     * @chainable
390
     */
391
    _change: function (src, state, options) {
392
        options = options ? Y.merge(DEFAULT_OPTIONS, options) : DEFAULT_OPTIONS;
393
 
394
        if (options.merge && _isSimpleObject(state) &&
395
                _isSimpleObject(GlobalEnv._state)) {
396
            state = Y.merge(GlobalEnv._state, state);
397
        }
398
 
399
        this._resolveChanges(src, state, options);
400
        return this;
401
    },
402
 
403
    /**
404
     * Called by _resolveChanges() when the state has changed. This method takes
405
     * care of actually firing the necessary events.
406
     *
407
     * @method _fireEvents
408
     * @param {String} src Source of the changes, for inclusion in event facades
409
     *   to facilitate filtering.
410
     * @param {Object} changes Resolved changes.
411
     * @param {Object} options Zero or more options. See <code>add()</code> for
412
     *   a list of supported options.
413
     * @protected
414
     */
415
    _fireEvents: function (src, changes, options) {
416
        // Fire the global change event.
417
        this.fire(EVT_CHANGE, {
418
            _options: options,
419
            changed : changes.changed,
420
            newVal  : changes.newState,
421
            prevVal : changes.prevState,
422
            removed : changes.removed,
423
            src     : src
424
        });
425
 
426
        // Fire change/remove events for individual items.
427
        Obj.each(changes.changed, function (value, key) {
428
            this._fireChangeEvent(src, key, value);
429
        }, this);
430
 
431
        Obj.each(changes.removed, function (value, key) {
432
            this._fireRemoveEvent(src, key, value);
433
        }, this);
434
    },
435
 
436
    /**
437
     * Fires a dynamic "[key]Change" event.
438
     *
439
     * @method _fireChangeEvent
440
     * @param {String} src source of the change, for inclusion in event facades
441
     *   to facilitate filtering
442
     * @param {String} key key of the item that was changed
443
     * @param {Object} value object hash containing <i>newVal</i> and
444
     *   <i>prevVal</i> properties for the changed item
445
     * @protected
446
     */
447
    _fireChangeEvent: function (src, key, value) {
448
        /**
449
         * <p>
450
         * Dynamic event fired when an individual history item is added or
451
         * changed. The name of this event depends on the name of the key that
452
         * changed. To listen to change events for a key named "foo", subscribe
453
         * to the <code>fooChange</code> event; for a key named "bar", subscribe
454
         * to <code>barChange</code>, etc.
455
         * </p>
456
         *
457
         * <p>
458
         * Key-specific events are only fired for instance-level changes; that
459
         * is, changes that were made via the same History instance on which the
460
         * event is subscribed. To be notified of changes made by other History
461
         * instances, subscribe to the global <code>history:change</code> event.
462
         * </p>
463
         *
464
         * @event [key]Change
465
         * @param {EventFacade} e Event facade with the following additional
466
         *   properties:
467
         *
468
         * <dl>
469
         *   <dt>newVal (mixed)</dt>
470
         *   <dd>
471
         *     The new value of the item after the change.
472
         *   </dd>
473
         *
474
         *   <dt>prevVal (mixed)</dt>
475
         *   <dd>
476
         *     The previous value of the item before the change, or
477
         *     <code>undefined</code> if the item was just added and has no
478
         *     previous value.
479
         *   </dd>
480
         *
481
         *   <dt>src (String)</dt>
482
         *   <dd>
483
         *     The source of the event. This can be used to selectively ignore
484
         *     events generated by certain sources.
485
         *   </dd>
486
         * </dl>
487
         */
488
        this.fire(key + 'Change', {
489
            newVal : value.newVal,
490
            prevVal: value.prevVal,
491
            src    : src
492
        });
493
    },
494
 
495
    /**
496
     * Fires a dynamic "[key]Remove" event.
497
     *
498
     * @method _fireRemoveEvent
499
     * @param {String} src source of the change, for inclusion in event facades
500
     *   to facilitate filtering
501
     * @param {String} key key of the item that was removed
502
     * @param {mixed} value value of the item prior to its removal
503
     * @protected
504
     */
505
    _fireRemoveEvent: function (src, key, value) {
506
        /**
507
         * <p>
508
         * Dynamic event fired when an individual history item is removed. The
509
         * name of this event depends on the name of the key that was removed.
510
         * To listen to remove events for a key named "foo", subscribe to the
511
         * <code>fooRemove</code> event; for a key named "bar", subscribe to
512
         * <code>barRemove</code>, etc.
513
         * </p>
514
         *
515
         * <p>
516
         * Key-specific events are only fired for instance-level changes; that
517
         * is, changes that were made via the same History instance on which the
518
         * event is subscribed. To be notified of changes made by other History
519
         * instances, subscribe to the global <code>history:change</code> event.
520
         * </p>
521
         *
522
         * @event [key]Remove
523
         * @param {EventFacade} e Event facade with the following additional
524
         *   properties:
525
         *
526
         * <dl>
527
         *   <dt>prevVal (mixed)</dt>
528
         *   <dd>
529
         *     The value of the item before it was removed.
530
         *   </dd>
531
         *
532
         *   <dt>src (String)</dt>
533
         *   <dd>
534
         *     The source of the event. This can be used to selectively ignore
535
         *     events generated by certain sources.
536
         *   </dd>
537
         * </dl>
538
         */
539
        this.fire(key + 'Remove', {
540
            prevVal: value,
541
            src    : src
542
        });
543
    },
544
 
545
    /**
546
     * Resolves the changes (if any) between <i>newState</i> and the current
547
     * state and fires appropriate events if things have changed.
548
     *
549
     * @method _resolveChanges
550
     * @param {String} src source of the changes, for inclusion in event facades
551
     *   to facilitate filtering
552
     * @param {Object} newState object hash of key/value pairs representing the
553
     *   new state
554
     * @param {Object} options Zero or more options. See <code>add()</code> for
555
     *   a list of supported options.
556
     * @protected
557
     */
558
    _resolveChanges: function (src, newState, options) {
559
        var changed   = {},
560
            isChanged,
561
            prevState = GlobalEnv._state,
562
            removed   = {};
563
 
564
        newState || (newState = {});
565
        options  || (options  = {});
566
 
567
        if (_isSimpleObject(newState) && _isSimpleObject(prevState)) {
568
            // Figure out what was added or changed.
569
            Obj.each(newState, function (newVal, key) {
570
                var prevVal = prevState[key];
571
 
572
                if (newVal !== prevVal) {
573
                    changed[key] = {
574
                        newVal : newVal,
575
                        prevVal: prevVal
576
                    };
577
 
578
                    isChanged = true;
579
                }
580
            }, this);
581
 
582
            // Figure out what was removed.
583
            Obj.each(prevState, function (prevVal, key) {
584
                if (!Obj.owns(newState, key) || newState[key] === null) {
585
                    delete newState[key];
586
                    removed[key] = prevVal;
587
                    isChanged = true;
588
                }
589
            }, this);
590
        } else {
591
            isChanged = newState !== prevState;
592
        }
593
 
594
        if (isChanged || this.force) {
595
            this._fireEvents(src, {
596
                changed  : changed,
597
                newState : newState,
598
                prevState: prevState,
599
                removed  : removed
600
            }, options);
601
        }
602
    },
603
 
604
    /**
605
     * Stores the specified state. Don't call this method directly; go through
606
     * _resolveChanges() to ensure that changes are resolved and all events are
607
     * fired properly.
608
     *
609
     * @method _storeState
610
     * @param {String} src source of the changes
611
     * @param {Object} newState new state to store
612
     * @param {Object} options Zero or more options. See <code>add()</code> for
613
     *   a list of supported options.
614
     * @protected
615
     */
616
    _storeState: function (src, newState) {
617
        // Note: the src and options params aren't used here, but they are used
618
        // by subclasses.
619
        GlobalEnv._state = newState || {};
620
    },
621
 
622
    // -- Protected Event Handlers ---------------------------------------------
623
 
624
    /**
625
     * Default <code>history:change</code> event handler.
626
     *
627
     * @method _defChangeFn
628
     * @param {EventFacade} e state change event facade
629
     * @protected
630
     */
631
    _defChangeFn: function (e) {
632
        this._storeState(e.src, e.newVal, e._options);
633
    }
634
}, true);
635
 
636
Y.HistoryBase = HistoryBase;
637
 
638
 
639
}, '3.18.1', {"requires": ["event-custom-complex"]});