2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('history-base', function (Y, NAME) {
11 * Provides browser history management functionality using a simple
12 * add/replace/get paradigm. This can be used to ensure that the browser's back
13 * and forward buttons work as the user expects and to provide bookmarkable URLs
14 * that return the user to the current application state, even in an Ajax
15 * application that doesn't perform full-page refreshes.
23 * Provides global state management backed by an object, but with no browser
24 * history integration. For actual browser history integration and back/forward
25 * support, use the history-html5 or history-hash modules.
28 * @submodule history-base
32 * @param {Object} config (optional) configuration object, which may contain
33 * zero or more of the following properties:
36 * <dt>force (Boolean)</dt>
38 * If `true`, a `history:change` event will be fired whenever the URL
39 * changes, even if there is no associated state change. Default is `false`.
42 * <dt>initialState (Object)</dt>
44 * Initial state to set, as an object hash of key/value pairs. This will be
45 * merged into the current global state.
52 GlobalEnv = YUI.namespace('Env.History'),
56 docMode = doc.documentMode,
59 DEFAULT_OPTIONS = {merge: true},
60 EVT_CHANGE = 'change',
62 SRC_REPLACE = 'replace';
64 function HistoryBase() {
65 this._init.apply(this, arguments);
68 Y.augment(HistoryBase, Y.EventTarget, null, null, {
75 if (!GlobalEnv._state) {
76 GlobalEnv._state = {};
79 // -- Private Methods ----------------------------------------------------------
82 * Returns <code>true</code> if <i>value</i> is a simple object and not a
83 * function or an array.
85 * @method _isSimpleObject
86 * @param {mixed} value
90 function _isSimpleObject(value) {
91 return Lang.type(value) === 'object';
94 // -- Public Static Properties -------------------------------------------------
97 * Name of this component.
103 HistoryBase.NAME = 'historyBase';
106 * Constant used to identify state changes originating from the
107 * <code>add()</code> method.
114 HistoryBase.SRC_ADD = SRC_ADD;
117 * Constant used to identify state changes originating from the
118 * <code>replace()</code> method.
120 * @property SRC_REPLACE
125 HistoryBase.SRC_REPLACE = SRC_REPLACE;
128 * Whether or not this browser supports the HTML5 History API.
135 // All HTML5-capable browsers except Gecko 2+ (Firefox 4+) correctly return
136 // true for 'onpopstate' in win. In order to support Gecko 2, we fall back to a
137 // UA sniff for now. (current as of Firefox 4.0b2)
138 HistoryBase.html5 = !!(win.history && win.history.pushState &&
139 win.history.replaceState && ('onpopstate' in win || Y.UA.gecko >= 2) &&
140 (!Y.UA.android || Y.UA.android >= 2.4));
143 * Whether or not this browser supports the <code>window.onhashchange</code>
144 * event natively. Note that even if this is <code>true</code>, you may
145 * still want to use HistoryHash's synthetic <code>hashchange</code> event
146 * since it normalizes implementation differences and fixes spec violations
147 * across various browsers.
149 * @property nativeHashChange
154 // Most browsers that support hashchange expose it on the window. Opera 10.6+
155 // exposes it on the document (but you can still attach to it on the window).
157 // IE8 supports the hashchange event, but only in IE8 Standards
158 // Mode. However, IE8 in IE7 compatibility mode still defines the
159 // event but never fires it, so we can't just detect the event. We also can't
160 // just UA sniff for IE8, since other browsers support this event as well.
161 HistoryBase.nativeHashChange = ('onhashchange' in win || 'onhashchange' in doc) &&
162 (!docMode || docMode > 7);
164 Y.mix(HistoryBase.prototype, {
165 // -- Initialization -------------------------------------------------------
168 * Initializes this HistoryBase instance. This method is called by the
172 * @param {Object} config configuration object
175 _init: function (config) {
179 * Configuration object provided by the user on instantiation, or an
180 * empty object if one wasn't provided.
187 config = this._config = config || {};
190 * If `true`, a `history:change` event will be fired whenever the URL
191 * changes, even if there is no associated state change.
197 this.force = !!config.force;
200 * Resolved initial state: a merge of the user-supplied initial state
201 * (if any) and any initial state provided by a subclass. This may
202 * differ from <code>_config.initialState</code>. If neither the config
203 * nor a subclass supplies an initial state, this property will be
206 * @property _initialState
211 initialState = this._initialState = this._initialState ||
212 config.initialState || null;
215 * Fired when the state changes. To be notified of all state changes
216 * regardless of the History or YUI instance that generated them,
217 * subscribe to this event on <code>Y.Global</code>. If you would rather
218 * be notified only about changes generated by this specific History
219 * instance, subscribe to this event on the instance.
221 * @event history:change
222 * @param {EventFacade} e Event facade with the following additional
226 * <dt>changed (Object)</dt>
228 * Object hash of state items that have been added or changed. The
229 * key is the item key, and the value is an object containing
230 * <code>newVal</code> and <code>prevVal</code> properties
231 * representing the values of the item both before and after the
232 * change. If the item was newly added, <code>prevVal</code> will be
233 * <code>undefined</code>.
236 * <dt>newVal (Object)</dt>
238 * Object hash of key/value pairs of all state items after the
242 * <dt>prevVal (Object)</dt>
244 * Object hash of key/value pairs of all state items before the
248 * <dt>removed (Object)</dt>
250 * Object hash of key/value pairs of state items that have been
251 * removed. Values are the old values prior to removal.
254 * <dt>src (String)</dt>
256 * The source of the event. This can be used to selectively ignore
257 * events generated by certain sources.
261 this.publish(EVT_CHANGE, {
263 defaultFn: this._defChangeFn
266 // If initialState was provided, merge it into the current state.
268 this.replace(initialState);
272 // -- Public Methods -------------------------------------------------------
275 * Adds a state entry with new values for the specified keys. By default,
276 * the new state will be merged into the existing state, and new values will
277 * override existing values. Specifying a <code>null</code> or
278 * <code>undefined</code> value will cause that key to be removed from the
282 * @param {Object} state Object hash of key/value pairs.
283 * @param {Object} options (optional) Zero or more of the following options:
285 * <dt>merge (Boolean)</dt>
288 * If <code>true</code> (the default), the new state will be merged
289 * into the existing state. New values will override existing values,
290 * and <code>null</code> or <code>undefined</code> values will be
291 * removed from the state.
295 * If <code>false</code>, the existing state will be discarded as a
296 * whole and the new state will take its place.
303 var args = YArray(arguments, 0, true);
304 args.unshift(SRC_ADD);
305 return this._change.apply(this, args);
309 * Adds a state entry with a new value for a single key. By default, the new
310 * value will be merged into the existing state values, and will override an
311 * existing value with the same key if there is one. Specifying a
312 * <code>null</code> or <code>undefined</code> value will cause the key to
313 * be removed from the new state entry.
316 * @param {String} key State parameter key.
317 * @param {String} value New value.
318 * @param {Object} options (optional) Zero or more options. See
319 * <code>add()</code> for a list of supported options.
322 addValue: function (key, value, options) {
325 return this._change(SRC_ADD, state, options);
329 * Returns the current value of the state parameter specified by <i>key</i>,
330 * or an object hash of key/value pairs for all current state parameters if
331 * no key is specified.
334 * @param {String} key (optional) State parameter key.
335 * @return {Object|String} Value of the specified state parameter, or an
336 * object hash of key/value pairs for all current state parameters.
338 get: function (key) {
339 var state = GlobalEnv._state,
340 isObject = _isSimpleObject(state);
343 return isObject && Obj.owns(state, key) ? state[key] : undefined;
345 return isObject ? Y.mix({}, state, true) : state; // mix provides a fast shallow clone.
350 * Same as <code>add()</code> except that a new browser history entry will
351 * not be created. Instead, the current history entry will be replaced with
355 * @param {Object} state Object hash of key/value pairs.
356 * @param {Object} options (optional) Zero or more options. See
357 * <code>add()</code> for a list of supported options.
360 replace: function () {
361 var args = YArray(arguments, 0, true);
362 args.unshift(SRC_REPLACE);
363 return this._change.apply(this, args);
367 * Same as <code>addValue()</code> except that a new browser history entry
368 * will not be created. Instead, the current history entry will be replaced
369 * with the new state.
371 * @method replaceValue
372 * @param {String} key State parameter key.
373 * @param {String} value New value.
374 * @param {Object} options (optional) Zero or more options. See
375 * <code>add()</code> for a list of supported options.
378 replaceValue: function (key, value, options) {
381 return this._change(SRC_REPLACE, state, options);
384 // -- Protected Methods ----------------------------------------------------
387 * Changes the state. This method provides a common implementation shared by
388 * the public methods for changing state.
391 * @param {String} src Source of the change, for inclusion in event facades
392 * to facilitate filtering.
393 * @param {Object} state Object hash of key/value pairs.
394 * @param {Object} options (optional) Zero or more options. See
395 * <code>add()</code> for a list of supported options.
399 _change: function (src, state, options) {
400 options = options ? Y.merge(DEFAULT_OPTIONS, options) : DEFAULT_OPTIONS;
402 if (options.merge && _isSimpleObject(state) &&
403 _isSimpleObject(GlobalEnv._state)) {
404 state = Y.merge(GlobalEnv._state, state);
407 this._resolveChanges(src, state, options);
412 * Called by _resolveChanges() when the state has changed. This method takes
413 * care of actually firing the necessary events.
415 * @method _fireEvents
416 * @param {String} src Source of the changes, for inclusion in event facades
417 * to facilitate filtering.
418 * @param {Object} changes Resolved changes.
419 * @param {Object} options Zero or more options. See <code>add()</code> for
420 * a list of supported options.
423 _fireEvents: function (src, changes, options) {
424 // Fire the global change event.
425 this.fire(EVT_CHANGE, {
427 changed : changes.changed,
428 newVal : changes.newState,
429 prevVal : changes.prevState,
430 removed : changes.removed,
434 // Fire change/remove events for individual items.
435 Obj.each(changes.changed, function (value, key) {
436 this._fireChangeEvent(src, key, value);
439 Obj.each(changes.removed, function (value, key) {
440 this._fireRemoveEvent(src, key, value);
445 * Fires a dynamic "[key]Change" event.
447 * @method _fireChangeEvent
448 * @param {String} src source of the change, for inclusion in event facades
449 * to facilitate filtering
450 * @param {String} key key of the item that was changed
451 * @param {Object} value object hash containing <i>newVal</i> and
452 * <i>prevVal</i> properties for the changed item
455 _fireChangeEvent: function (src, key, value) {
458 * Dynamic event fired when an individual history item is added or
459 * changed. The name of this event depends on the name of the key that
460 * changed. To listen to change events for a key named "foo", subscribe
461 * to the <code>fooChange</code> event; for a key named "bar", subscribe
462 * to <code>barChange</code>, etc.
466 * Key-specific events are only fired for instance-level changes; that
467 * is, changes that were made via the same History instance on which the
468 * event is subscribed. To be notified of changes made by other History
469 * instances, subscribe to the global <code>history:change</code> event.
473 * @param {EventFacade} e Event facade with the following additional
477 * <dt>newVal (mixed)</dt>
479 * The new value of the item after the change.
482 * <dt>prevVal (mixed)</dt>
484 * The previous value of the item before the change, or
485 * <code>undefined</code> if the item was just added and has no
489 * <dt>src (String)</dt>
491 * The source of the event. This can be used to selectively ignore
492 * events generated by certain sources.
496 this.fire(key + 'Change', {
497 newVal : value.newVal,
498 prevVal: value.prevVal,
504 * Fires a dynamic "[key]Remove" event.
506 * @method _fireRemoveEvent
507 * @param {String} src source of the change, for inclusion in event facades
508 * to facilitate filtering
509 * @param {String} key key of the item that was removed
510 * @param {mixed} value value of the item prior to its removal
513 _fireRemoveEvent: function (src, key, value) {
516 * Dynamic event fired when an individual history item is removed. The
517 * name of this event depends on the name of the key that was removed.
518 * To listen to remove events for a key named "foo", subscribe to the
519 * <code>fooRemove</code> event; for a key named "bar", subscribe to
520 * <code>barRemove</code>, etc.
524 * Key-specific events are only fired for instance-level changes; that
525 * is, changes that were made via the same History instance on which the
526 * event is subscribed. To be notified of changes made by other History
527 * instances, subscribe to the global <code>history:change</code> event.
531 * @param {EventFacade} e Event facade with the following additional
535 * <dt>prevVal (mixed)</dt>
537 * The value of the item before it was removed.
540 * <dt>src (String)</dt>
542 * The source of the event. This can be used to selectively ignore
543 * events generated by certain sources.
547 this.fire(key + 'Remove', {
554 * Resolves the changes (if any) between <i>newState</i> and the current
555 * state and fires appropriate events if things have changed.
557 * @method _resolveChanges
558 * @param {String} src source of the changes, for inclusion in event facades
559 * to facilitate filtering
560 * @param {Object} newState object hash of key/value pairs representing the
562 * @param {Object} options Zero or more options. See <code>add()</code> for
563 * a list of supported options.
566 _resolveChanges: function (src, newState, options) {
569 prevState = GlobalEnv._state,
572 newState || (newState = {});
573 options || (options = {});
575 if (_isSimpleObject(newState) && _isSimpleObject(prevState)) {
576 // Figure out what was added or changed.
577 Obj.each(newState, function (newVal, key) {
578 var prevVal = prevState[key];
580 if (newVal !== prevVal) {
590 // Figure out what was removed.
591 Obj.each(prevState, function (prevVal, key) {
592 if (!Obj.owns(newState, key) || newState[key] === null) {
593 delete newState[key];
594 removed[key] = prevVal;
599 isChanged = newState !== prevState;
602 if (isChanged || this.force) {
603 this._fireEvents(src, {
606 prevState: prevState,
613 * Stores the specified state. Don't call this method directly; go through
614 * _resolveChanges() to ensure that changes are resolved and all events are
617 * @method _storeState
618 * @param {String} src source of the changes
619 * @param {Object} newState new state to store
620 * @param {Object} options Zero or more options. See <code>add()</code> for
621 * a list of supported options.
624 _storeState: function (src, newState) {
625 // Note: the src and options params aren't used here, but they are used
627 GlobalEnv._state = newState || {};
630 // -- Protected Event Handlers ---------------------------------------------
633 * Default <code>history:change</code> event handler.
635 * @method _defChangeFn
636 * @param {EventFacade} e state change event facade
639 _defChangeFn: function (e) {
640 this._storeState(e.src, e.newVal, e._options);
644 Y.HistoryBase = HistoryBase;
647 }, '3.13.0', {"requires": ["event-custom-complex"]});