Show:
  1. /**
  2. The App Framework provides simple MVC-like building blocks (models, model lists,
  3. views, and URL-based routing) for writing single-page JavaScript applications.
  4. @main app
  5. @module app
  6. @since 3.4.0
  7. **/
  8. /**
  9. Provides a top-level application component which manages navigation and views.
  10. @module app
  11. @submodule app-base
  12. @since 3.5.0
  13. **/
  14. // TODO: Better handling of lifecycle for registered views:
  15. //
  16. // * [!] Just redo basically everything with view management so there are no
  17. // pre-`activeViewChange` side effects and handle the rest of these things:
  18. //
  19. // * Seems like any view created via `createView` should listen for the view's
  20. // `destroy` event and use that to remove it from the `_viewsInfoMap`. I
  21. // should look at what ModelList does for Models as a reference.
  22. //
  23. // * Should we have a companion `destroyView()` method? Maybe this wouldn't be
  24. // needed if we have a `getView(name, create)` method, and already doing the
  25. // above? We could do `app.getView('foo').destroy()` and it would be removed
  26. // from the `_viewsInfoMap` as well.
  27. //
  28. // * Should we wait to call a view's `render()` method inside of the
  29. // `_attachView()` method?
  30. //
  31. // * Should named views support a collection of instances instead of just one?
  32. //
  33. var Lang = Y.Lang,
  34. YObject = Y.Object,
  35. PjaxBase = Y.PjaxBase,
  36. Router = Y.Router,
  37. View = Y.View,
  38. getClassName = Y.ClassNameManager.getClassName,
  39. win = Y.config.win,
  40. AppBase;
  41. /**
  42. Provides a top-level application component which manages navigation and views.
  43. This gives you a foundation and structure on which to build your application; it
  44. combines robust URL navigation with powerful routing and flexible view
  45. management.
  46. @class App.Base
  47. @param {Object} [config] The following are configuration properties that can be
  48. specified _in addition_ to default attribute values and the non-attribute
  49. properties provided by `Y.Base`:
  50. @param {Object} [config.views] Hash of view-name to metadata used to
  51. declaratively describe an application's views and their relationship with
  52. the app and other views. The views specified here will override any defaults
  53. provided by the `views` object on the `prototype`.
  54. @constructor
  55. @extends Base
  56. @uses View
  57. @uses Router
  58. @uses PjaxBase
  59. @since 3.5.0
  60. **/
  61. AppBase = Y.Base.create('app', Y.Base, [View, Router, PjaxBase], {
  62. // -- Public Properties ----------------------------------------------------
  63. /**
  64. Hash of view-name to metadata used to declaratively describe an
  65. application's views and their relationship with the app and its other views.
  66. The view metadata is composed of Objects keyed to a view-name that can have
  67. any or all of the following properties:
  68. * `type`: Function or a string representing the view constructor to use to
  69. create view instances. If a string is used, the constructor function is
  70. assumed to be on the `Y` object; e.g. `"SomeView"` -> `Y.SomeView`.
  71. * `preserve`: Boolean for whether the view instance should be retained. By
  72. default, the view instance will be destroyed when it is no longer the
  73. `activeView`. If `true` the view instance will simply be `removed()`
  74. from the DOM when it is no longer active. This is useful when the view
  75. is frequently used and may be expensive to re-create.
  76. * `parent`: String to another named view in this hash that represents the
  77. parent view within the application's view hierarchy; e.g. a `"photo"`
  78. view could have `"album"` has its `parent` view. This parent/child
  79. relationship is a useful cue for things like transitions.
  80. * `instance`: Used internally to manage the current instance of this named
  81. view. This can be used if your view instance is created up-front, or if
  82. you would rather manage the View lifecycle, but you probably should just
  83. let this be handled for you.
  84. If `views` are specified at instantiation time, the metadata in the `views`
  85. Object here will be used as defaults when creating the instance's `views`.
  86. Every `Y.App` instance gets its own copy of a `views` object so this Object
  87. on the prototype will not be polluted.
  88. @example
  89. // Imagine that `Y.UsersView` and `Y.UserView` have been defined.
  90. var app = new Y.App({
  91. views: {
  92. users: {
  93. type : Y.UsersView,
  94. preserve: true
  95. },
  96. user: {
  97. type : Y.UserView,
  98. parent: 'users'
  99. }
  100. }
  101. });
  102. @property views
  103. @type Object
  104. @default {}
  105. @since 3.5.0
  106. **/
  107. views: {},
  108. // -- Protected Properties -------------------------------------------------
  109. /**
  110. Map of view instance id (via `Y.stamp()`) to view-info object in `views`.
  111. This mapping is used to tie a specific view instance back to its metadata by
  112. adding a reference to the the related view info on the `views` object.
  113. @property _viewInfoMap
  114. @type Object
  115. @default {}
  116. @protected
  117. @since 3.5.0
  118. **/
  119. // -- Lifecycle Methods ----------------------------------------------------
  120. initializer: function (config) {
  121. config || (config = {});
  122. var views = {};
  123. // Merges-in specified view metadata into local `views` object.
  124. function mergeViewConfig(view, name) {
  125. views[name] = Y.merge(views[name], view);
  126. }
  127. // First, each view in the `views` prototype object gets its metadata
  128. // merged-in, providing the defaults.
  129. YObject.each(this.views, mergeViewConfig);
  130. // Then, each view in the specified `config.views` object gets its
  131. // metadata merged-in.
  132. YObject.each(config.views, mergeViewConfig);
  133. // The resulting hodgepodge of metadata is then stored as the instance's
  134. // `views` object, and no one's objects were harmed in the making.
  135. this.views = views;
  136. this._viewInfoMap = {};
  137. // Using `bind()` to aid extensibility.
  138. this.after('activeViewChange', Y.bind('_afterActiveViewChange', this));
  139. // PjaxBase will bind click events when `html5` is `true`, so this just
  140. // forces the binding when `serverRouting` and `html5` are both falsy.
  141. if (!this.get('serverRouting')) {
  142. this._pjaxBindUI();
  143. }
  144. },
  145. // TODO: `destructor` to destroy the `activeView`?
  146. // -- Public Methods -------------------------------------------------------
  147. /**
  148. Creates and returns a new view instance using the provided `name` to look up
  149. the view info metadata defined in the `views` object. The passed-in `config`
  150. object is passed to the view constructor function.
  151. This function also maps a view instance back to its view info metadata.
  152. @method createView
  153. @param {String} name The name of a view defined on the `views` object.
  154. @param {Object} [config] The configuration object passed to the view
  155. constructor function when creating the new view instance.
  156. @return {View} The new view instance.
  157. @since 3.5.0
  158. **/
  159. createView: function (name, config) {
  160. var viewInfo = this.getViewInfo(name),
  161. type = (viewInfo && viewInfo.type) || View,
  162. ViewConstructor, view;
  163. // Looks for a namespaced constructor function on `Y`.
  164. ViewConstructor = Lang.isString(type) ?
  165. YObject.getValue(Y, type.split('.')) : type;
  166. // Create the view instance and map it with its metadata.
  167. view = new ViewConstructor(config);
  168. this._viewInfoMap[Y.stamp(view, true)] = viewInfo;
  169. return view;
  170. },
  171. /**
  172. Returns the metadata associated with a view instance or view name defined on
  173. the `views` object.
  174. @method getViewInfo
  175. @param {View|String} view View instance, or name of a view defined on the
  176. `views` object.
  177. @return {Object} The metadata for the view, or `undefined` if the view is
  178. not registered.
  179. @since 3.5.0
  180. **/
  181. getViewInfo: function (view) {
  182. if (Lang.isString(view)) {
  183. return this.views[view];
  184. }
  185. return view && this._viewInfoMap[Y.stamp(view, true)];
  186. },
  187. /**
  188. Navigates to the specified URL if there is a route handler that matches. In
  189. browsers capable of using HTML5 history or when `serverRouting` is falsy,
  190. the navigation will be enhanced by firing the `navigate` event and having
  191. the app handle the "request". When `serverRouting` is `true`, non-HTML5
  192. browsers will navigate to the new URL via a full page reload.
  193. When there is a route handler for the specified URL and it is being
  194. navigated to, this method will return `true`, otherwise it will return
  195. `false`.
  196. **Note:** The specified URL _must_ be of the same origin as the current URL,
  197. otherwise an error will be logged and navigation will not occur. This is
  198. intended as both a security constraint and a purposely imposed limitation as
  199. it does not make sense to tell the app to navigate to a URL on a
  200. different scheme, host, or port.
  201. @method navigate
  202. @param {String} url The URL to navigate to. This must be of the same origin
  203. as the current URL.
  204. @param {Object} [options] Additional options to configure the navigation.
  205. These are mixed into the `navigate` event facade.
  206. @param {Boolean} [options.replace] Whether or not the current history
  207. entry will be replaced, or a new entry will be created. Will default
  208. to `true` if the specified `url` is the same as the current URL.
  209. @param {Boolean} [options.force] Whether the enhanced navigation
  210. should occur even in browsers without HTML5 history. Will default to
  211. `true` when `serverRouting` is falsy.
  212. @see PjaxBase.navigate()
  213. **/
  214. // Does not override `navigate()` but does use extra `options`.
  215. /**
  216. Renders this application by appending the `viewContainer` node to the
  217. `container` node if it isn't already a child of the container, and the
  218. `activeView` will be appended the view container, if it isn't already.
  219. You should call this method at least once, usually after the initialization
  220. of your app instance so the proper DOM structure is setup and optionally
  221. append the container to the DOM if it's not there already.
  222. You may override this method to customize the app's rendering, but you
  223. should expect that the `viewContainer`'s contents will be modified by the
  224. app for the purpose of rendering the `activeView` when it changes.
  225. @method render
  226. @chainable
  227. @see View.render()
  228. **/
  229. render: function () {
  230. var CLASS_NAMES = Y.App.CLASS_NAMES,
  231. container = this.get('container'),
  232. viewContainer = this.get('viewContainer'),
  233. activeView = this.get('activeView'),
  234. activeViewContainer = activeView && activeView.get('container'),
  235. areSame = container.compareTo(viewContainer);
  236. container.addClass(CLASS_NAMES.app);
  237. viewContainer.addClass(CLASS_NAMES.views);
  238. // Prevents needless shuffling around of nodes and maintains DOM order.
  239. if (activeView && !viewContainer.contains(activeViewContainer)) {
  240. viewContainer.appendChild(activeViewContainer);
  241. }
  242. // Prevents needless shuffling around of nodes and maintains DOM order.
  243. if (!container.contains(viewContainer) && !areSame) {
  244. container.appendChild(viewContainer);
  245. }
  246. return this;
  247. },
  248. /**
  249. Sets which view is active/visible for the application. This will set the
  250. app's `activeView` attribute to the specified `view`.
  251. The `view` will be "attached" to this app, meaning it will be both rendered
  252. into this app's `viewContainer` node and all of its events will bubble to
  253. the app. The previous `activeView` will be "detached" from this app.
  254. When a string-name is provided for a view which has been registered on this
  255. app's `views` object, the referenced metadata will be used and the
  256. `activeView` will be set to either a preserved view instance, or a new
  257. instance of the registered view will be created using the specified `config`
  258. object passed-into this method.
  259. A callback function can be specified as either the third or fourth argument,
  260. and this function will be called after the new `view` becomes the
  261. `activeView`, is rendered to the `viewContainer`, and is ready to use.
  262. @example
  263. var app = new Y.App({
  264. views: {
  265. usersView: {
  266. // Imagine that `Y.UsersView` has been defined.
  267. type: Y.UsersView
  268. }
  269. },
  270. users: new Y.ModelList()
  271. });
  272. app.route('/users/', function () {
  273. this.showView('usersView', {users: this.get('users')});
  274. });
  275. app.render();
  276. app.navigate('/uses/'); // => Creates a new `Y.UsersView` and shows it.
  277. @method showView
  278. @param {String|View} view The name of a view defined in the `views` object,
  279. or a view instance which should become this app's `activeView`.
  280. @param {Object} [config] Optional configuration to use when creating a new
  281. view instance. This config object can also be used to update an existing
  282. or preserved view's attributes when `options.update` is `true`.
  283. @param {Object} [options] Optional object containing any of the following
  284. properties:
  285. @param {Function} [options.callback] Optional callback function to call
  286. after new `activeView` is ready to use, the function will be passed:
  287. @param {View} options.callback.view A reference to the new
  288. `activeView`.
  289. @param {Boolean} [options.prepend=false] Whether the `view` should be
  290. prepended instead of appended to the `viewContainer`.
  291. @param {Boolean} [options.render] Whether the `view` should be rendered.
  292. **Note:** If no value is specified, a view instance will only be
  293. rendered if it's newly created by this method.
  294. @param {Boolean} [options.update=false] Whether an existing view should
  295. have its attributes updated by passing the `config` object to its
  296. `setAttrs()` method. **Note:** This option does not have an effect if
  297. the `view` instance is created as a result of calling this method.
  298. @param {Function} [callback] Optional callback Function to call after the
  299. new `activeView` is ready to use. **Note:** this will override
  300. `options.callback` and it can be specified as either the third or fourth
  301. argument. The function will be passed the following:
  302. @param {View} callback.view A reference to the new `activeView`.
  303. @chainable
  304. @since 3.5.0
  305. **/
  306. showView: function (view, config, options, callback) {
  307. var viewInfo, created;
  308. options || (options = {});
  309. // Support the callback function being either the third or fourth arg.
  310. if (callback) {
  311. options = Y.merge(options, {callback: callback});
  312. } else if (Lang.isFunction(options)) {
  313. options = {callback: options};
  314. }
  315. if (Lang.isString(view)) {
  316. viewInfo = this.getViewInfo(view);
  317. // Use the preserved view instance, or create a new view.
  318. // TODO: Maybe we can remove the strict check for `preserve` and
  319. // assume we'll use a View instance if it is there, and just check
  320. // `preserve` when detaching?
  321. if (viewInfo && viewInfo.preserve && viewInfo.instance) {
  322. view = viewInfo.instance;
  323. // Make sure there's a mapping back to the view metadata.
  324. this._viewInfoMap[Y.stamp(view, true)] = viewInfo;
  325. } else {
  326. // TODO: Add the app as a bubble target during construction, but
  327. // make sure to check that it isn't already in `bubbleTargets`!
  328. // This will allow the app to be notified for about _all_ of the
  329. // view's events. **Note:** This should _only_ happen if the
  330. // view is created _after_ `activeViewChange`.
  331. view = this.createView(view, config);
  332. created = true;
  333. }
  334. }
  335. // Update the specified or preserved `view` when signaled to do so.
  336. // There's no need to updated a view if it was _just_ created.
  337. if (options.update && !created) {
  338. view.setAttrs(config);
  339. }
  340. // TODO: Hold off on rendering the view until after it has been
  341. // "attached", and move the call to render into `_attachView()`.
  342. // When a value is specified for `options.render`, prefer it because it
  343. // represents the developer's intent. When no value is specified, the
  344. // `view` will only be rendered if it was just created.
  345. if ('render' in options) {
  346. if (options.render) {
  347. view.render();
  348. }
  349. } else if (created) {
  350. view.render();
  351. }
  352. return this._set('activeView', view, {options: options});
  353. },
  354. // -- Protected Methods ----------------------------------------------------
  355. /**
  356. Helper method to attach the view instance to the application by making the
  357. app a bubble target of the view, append the view to the `viewContainer`, and
  358. assign it to the `instance` property of the associated view info metadata.
  359. @method _attachView
  360. @param {View} view View to attach.
  361. @param {Boolean} prepend=false Whether the view should be prepended instead
  362. of appended to the `viewContainer`.
  363. @protected
  364. @since 3.5.0
  365. **/
  366. _attachView: function (view, prepend) {
  367. if (!view) {
  368. return;
  369. }
  370. var viewInfo = this.getViewInfo(view),
  371. viewContainer = this.get('viewContainer');
  372. // Bubble the view's events to this app.
  373. view.addTarget(this);
  374. // Save the view instance in the `views` registry.
  375. if (viewInfo) {
  376. viewInfo.instance = view;
  377. }
  378. // TODO: Attach events here for persevered Views?
  379. // See related TODO in `_detachView`.
  380. // TODO: Actually render the view here so that it gets "attached" before
  381. // it gets rendered?
  382. // Insert view into the DOM.
  383. viewContainer[prepend ? 'prepend' : 'append'](view.get('container'));
  384. },
  385. /**
  386. Overrides View's container destruction to deal with the `viewContainer` and
  387. checks to make sure not to remove and purge the `<body>`.
  388. @method _destroyContainer
  389. @protected
  390. @see View._destroyContainer()
  391. **/
  392. _destroyContainer: function () {
  393. var CLASS_NAMES = Y.App.CLASS_NAMES,
  394. container = this.get('container'),
  395. viewContainer = this.get('viewContainer'),
  396. areSame = container.compareTo(viewContainer);
  397. // We do not want to remove or destroy the `<body>`.
  398. if (Y.one('body').compareTo(container)) {
  399. // Just clean-up our events listeners.
  400. this.detachEvents();
  401. // Clean-up `yui3-app` CSS class on the `container`.
  402. container.removeClass(CLASS_NAMES.app);
  403. if (areSame) {
  404. // Clean-up `yui3-app-views` CSS class on the `container`.
  405. container.removeClass(CLASS_NAMES.views);
  406. } else {
  407. // Destroy and purge the `viewContainer`.
  408. viewContainer.remove(true);
  409. }
  410. return;
  411. }
  412. // Remove and purge events from both containers.
  413. viewContainer.remove(true);
  414. if (!areSame) {
  415. container.remove(true);
  416. }
  417. },
  418. /**
  419. Helper method to detach the view instance from the application by removing
  420. the application as a bubble target of the view, and either just removing the
  421. view if it is intended to be preserved, or destroying the instance
  422. completely.
  423. @method _detachView
  424. @param {View} view View to detach.
  425. @protected
  426. @since 3.5.0
  427. **/
  428. _detachView: function (view) {
  429. if (!view) {
  430. return;
  431. }
  432. var viewInfo = this.getViewInfo(view) || {};
  433. if (viewInfo.preserve) {
  434. view.remove();
  435. // TODO: Detach events here for preserved Views? It is possible that
  436. // some event subscriptions are made on elements other than the
  437. // View's `container`.
  438. } else {
  439. view.destroy({remove: true});
  440. // TODO: The following should probably happen automagically from
  441. // `destroy()` being called! Possibly `removeTarget()` as well.
  442. // Remove from view to view-info map.
  443. delete this._viewInfoMap[Y.stamp(view, true)];
  444. // Remove from view-info instance property.
  445. if (view === viewInfo.instance) {
  446. delete viewInfo.instance;
  447. }
  448. }
  449. view.removeTarget(this);
  450. },
  451. /**
  452. Gets a request object that can be passed to a route handler.
  453. This delegates to `Y.Router`'s `_getRequest()` method and adds a reference
  454. to this app instance at `req.app`.
  455. @method _getRequest
  456. @param {String} src What initiated the URL change and need for the request.
  457. @return {Object} Request object.
  458. @protected
  459. @see Router._getRequest
  460. **/
  461. _getRequest: function () {
  462. var req = Router.prototype._getRequest.apply(this, arguments);
  463. req.app = this;
  464. return req;
  465. },
  466. /**
  467. Getter for the `viewContainer` attribute.
  468. @method _getViewContainer
  469. @param {Node|null} value Current attribute value.
  470. @return {Node} View container node.
  471. @protected
  472. @since 3.5.0
  473. **/
  474. _getViewContainer: function (value) {
  475. // This wackiness is necessary to enable fully lazy creation of the
  476. // container node both when no container is specified and when one is
  477. // specified via a valueFn.
  478. if (!value && !this._viewContainer) {
  479. // Create a default container and set that as the new attribute
  480. // value. The `this._viewContainer` property prevents infinite
  481. // recursion.
  482. value = this._viewContainer = this.create();
  483. this._set('viewContainer', value);
  484. }
  485. return value;
  486. },
  487. /**
  488. Provides the default value for the `html5` attribute.
  489. The value returned is dependent on the value of the `serverRouting`
  490. attribute. When `serverRouting` is explicit set to `false` (not just falsy),
  491. the default value for `html5` will be set to `false` for *all* browsers.
  492. When `serverRouting` is `true` or `undefined` the returned value will be
  493. dependent on the browser's capability of using HTML5 history.
  494. @method _initHtml5
  495. @return {Boolean} Whether or not HTML5 history should be used.
  496. @protected
  497. @since 3.5.0
  498. **/
  499. _initHtml5: function () {
  500. // When `serverRouting` is explicitly set to `false` (not just falsy),
  501. // forcing hash-based URLs in all browsers.
  502. if (this.get('serverRouting') === false) {
  503. return false;
  504. }
  505. // Defaults to whether or not the browser supports HTML5 history.
  506. return Router.html5;
  507. },
  508. /**
  509. Determines if the specified `view` is configured as a child of the specified
  510. `parent` view. This requires both views to be either named-views, or view
  511. instances created using configuration data that exists in the `views`
  512. object, e.g. created by the `createView()` or `showView()` method.
  513. @method _isChildView
  514. @param {View|String} view The name of a view defined in the `views` object,
  515. or a view instance.
  516. @param {View|String} parent The name of a view defined in the `views`
  517. object, or a view instance.
  518. @return {Boolean} Whether the view is configured as a child of the parent.
  519. @protected
  520. @since 3.5.0
  521. **/
  522. _isChildView: function (view, parent) {
  523. var viewInfo = this.getViewInfo(view),
  524. parentInfo = this.getViewInfo(parent);
  525. if (viewInfo && parentInfo) {
  526. return this.getViewInfo(viewInfo.parent) === parentInfo;
  527. }
  528. return false;
  529. },
  530. /**
  531. Determines if the specified `view` is configured as the parent of the
  532. specified `child` view. This requires both views to be either named-views,
  533. or view instances created using configuration data that exists in the
  534. `views` object, e.g. created by the `createView()` or `showView()` method.
  535. @method _isParentView
  536. @param {View|String} view The name of a view defined in the `views` object,
  537. or a view instance.
  538. @param {View|String} parent The name of a view defined in the `views`
  539. object, or a view instance.
  540. @return {Boolean} Whether the view is configured as the parent of the child.
  541. @protected
  542. @since 3.5.0
  543. **/
  544. _isParentView: function (view, child) {
  545. var viewInfo = this.getViewInfo(view),
  546. childInfo = this.getViewInfo(child);
  547. if (viewInfo && childInfo) {
  548. return this.getViewInfo(childInfo.parent) === viewInfo;
  549. }
  550. return false;
  551. },
  552. /**
  553. Underlying implementation for `navigate()`.
  554. @method _navigate
  555. @param {String} url The fully-resolved URL that the app should dispatch to
  556. its route handlers to fulfill the enhanced navigation "request", or use to
  557. update `window.location` in non-HTML5 history capable browsers when
  558. `serverRouting` is `true`.
  559. @param {Object} [options] Additional options to configure the navigation.
  560. These are mixed into the `navigate` event facade.
  561. @param {Boolean} [options.replace] Whether or not the current history
  562. entry will be replaced, or a new entry will be created. Will default
  563. to `true` if the specified `url` is the same as the current URL.
  564. @param {Boolean} [options.force] Whether the enhanced navigation
  565. should occur even in browsers without HTML5 history. Will default to
  566. `true` when `serverRouting` is falsy.
  567. @protected
  568. @see PjaxBase._navigate()
  569. **/
  570. _navigate: function (url, options) {
  571. if (!this.get('serverRouting')) {
  572. // Force navigation to be enhanced and handled by the app when
  573. // `serverRouting` is falsy because the server might not be able to
  574. // properly handle the request.
  575. options = Y.merge({force: true}, options);
  576. }
  577. return PjaxBase.prototype._navigate.call(this, url, options);
  578. },
  579. /**
  580. Will either save a history entry using `pushState()` or the location hash,
  581. or gracefully-degrade to sending a request to the server causing a full-page
  582. reload.
  583. Overrides Router's `_save()` method to preform graceful-degradation when the
  584. app's `serverRouting` is `true` and `html5` is `false` by updating the full
  585. URL via standard assignment to `window.location` or by calling
  586. `window.location.replace()`; both of which will cause a request to the
  587. server resulting in a full-page reload.
  588. Otherwise this will just delegate off to Router's `_save()` method allowing
  589. the client-side enhanced routing to occur.
  590. @method _save
  591. @param {String} [url] URL for the history entry.
  592. @param {Boolean} [replace=false] If `true`, the current history entry will
  593. be replaced instead of a new one being added.
  594. @chainable
  595. @protected
  596. @see Router._save()
  597. **/
  598. _save: function (url, replace) {
  599. var path;
  600. // Forces full-path URLs to always be used by modifying
  601. // `window.location` in non-HTML5 history capable browsers.
  602. if (this.get('serverRouting') && !this.get('html5')) {
  603. // Perform same-origin check on the specified URL.
  604. if (!this._hasSameOrigin(url)) {
  605. Y.error('Security error: The new URL must be of the same origin as the current URL.');
  606. return this;
  607. }
  608. // Either replace the current history entry or create a new one
  609. // while navigating to the `url`.
  610. if (win) {
  611. // Results in the URL's full path starting with '/'.
  612. path = this._joinURL(url || '');
  613. if (replace) {
  614. win.location.replace(path);
  615. } else {
  616. win.location = path;
  617. }
  618. }
  619. return this;
  620. }
  621. return Router.prototype._save.apply(this, arguments);
  622. },
  623. /**
  624. Performs the actual change of this app's `activeView` by attaching the
  625. `newView` to this app, and detaching the `oldView` from this app using any
  626. specified `options`.
  627. The `newView` is attached to the app by rendering it to the `viewContainer`,
  628. and making this app a bubble target of its events.
  629. The `oldView` is detached from the app by removing it from the
  630. `viewContainer`, and removing this app as a bubble target for its events.
  631. The `oldView` will either be preserved or properly destroyed.
  632. **Note:** The `activeView` attribute is read-only and can be changed by
  633. calling the `showView()` method.
  634. @method _uiSetActiveView
  635. @param {View} newView The View which is now this app's `activeView`.
  636. @param {View} [oldView] The View which was this app's `activeView`.
  637. @param {Object} [options] Optional object containing any of the following
  638. properties:
  639. @param {Function} [options.callback] Optional callback function to call
  640. after new `activeView` is ready to use, the function will be passed:
  641. @param {View} options.callback.view A reference to the new
  642. `activeView`.
  643. @param {Boolean} [options.prepend=false] Whether the `view` should be
  644. prepended instead of appended to the `viewContainer`.
  645. @param {Boolean} [options.render] Whether the `view` should be rendered.
  646. **Note:** If no value is specified, a view instance will only be
  647. rendered if it's newly created by this method.
  648. @param {Boolean} [options.update=false] Whether an existing view should
  649. have its attributes updated by passing the `config` object to its
  650. `setAttrs()` method. **Note:** This option does not have an effect if
  651. the `view` instance is created as a result of calling this method.
  652. @protected
  653. @since 3.5.0
  654. **/
  655. _uiSetActiveView: function (newView, oldView, options) {
  656. options || (options = {});
  657. var callback = options.callback,
  658. isChild = this._isChildView(newView, oldView),
  659. isParent = !isChild && this._isParentView(newView, oldView),
  660. prepend = !!options.prepend || isParent;
  661. // Prevent detaching (thus removing) the view we want to show. Also hard
  662. // to animate out and in, the same view.
  663. if (newView === oldView) {
  664. return callback && callback.call(this, newView);
  665. }
  666. this._attachView(newView, prepend);
  667. this._detachView(oldView);
  668. if (callback) {
  669. callback.call(this, newView);
  670. }
  671. },
  672. // -- Protected Event Handlers ---------------------------------------------
  673. /**
  674. Handles the application's `activeViewChange` event (which is fired when the
  675. `activeView` attribute changes) by detaching the old view, attaching the new
  676. view.
  677. The `activeView` attribute is read-only, so the public API to change its
  678. value is through the `showView()` method.
  679. @method _afterActiveViewChange
  680. @param {EventFacade} e
  681. @protected
  682. @since 3.5.0
  683. **/
  684. _afterActiveViewChange: function (e) {
  685. this._uiSetActiveView(e.newVal, e.prevVal, e.options);
  686. }
  687. }, {
  688. ATTRS: {
  689. /**
  690. The application's active/visible view.
  691. This attribute is read-only, to set the `activeView` use the
  692. `showView()` method.
  693. @attribute activeView
  694. @type View
  695. @default null
  696. @readOnly
  697. @see App.Base.showView()
  698. @since 3.5.0
  699. **/
  700. activeView: {
  701. value : null,
  702. readOnly: true
  703. },
  704. /**
  705. Container node which represents the application's bounding-box, into
  706. which this app's content will be rendered.
  707. The container node serves as the host for all DOM events attached by the
  708. app. Delegation is used to handle events on children of the container,
  709. allowing the container's contents to be re-rendered at any time without
  710. losing event subscriptions.
  711. The default container is the `<body>` Node, but you can override this in
  712. a subclass, or by passing in a custom `container` config value at
  713. instantiation time.
  714. When `container` is overridden by a subclass or passed as a config
  715. option at instantiation time, it may be provided as a selector string, a
  716. DOM element, or a `Y.Node` instance. During initialization, this app's
  717. `create()` method will be called to convert the container into a
  718. `Y.Node` instance if it isn't one already and stamp it with the CSS
  719. class: `"yui3-app"`.
  720. The container is not added to the page automatically. This allows you to
  721. have full control over how and when your app is actually rendered to
  722. the page.
  723. @attribute container
  724. @type HTMLElement|Node|String
  725. @default Y.one('body')
  726. @initOnly
  727. **/
  728. container: {
  729. valueFn: function () {
  730. return Y.one('body');
  731. }
  732. },
  733. /**
  734. Whether or not this browser is capable of using HTML5 history.
  735. This value is dependent on the value of `serverRouting` and will default
  736. accordingly.
  737. Setting this to `false` will force the use of hash-based history even on
  738. HTML5 browsers, but please don't do this unless you understand the
  739. consequences.
  740. @attribute html5
  741. @type Boolean
  742. @initOnly
  743. @see serverRouting
  744. **/
  745. html5: {
  746. valueFn: '_initHtml5'
  747. },
  748. /**
  749. CSS selector string used to filter link click events so that only the
  750. links which match it will have the enhanced-navigation behavior of pjax
  751. applied.
  752. When a link is clicked and that link matches this selector, navigating
  753. to the link's `href` URL using the enhanced, pjax, behavior will be
  754. attempted; and the browser's default way to navigate to new pages will
  755. be the fallback.
  756. By default this selector will match _all_ links on the page.
  757. @attribute linkSelector
  758. @type String|Function
  759. @default "a"
  760. **/
  761. linkSelector: {
  762. value: 'a'
  763. },
  764. /**
  765. Whether or not this application's server is capable of properly routing
  766. all requests and rendering the initial state in the HTML responses.
  767. This can have three different values, each having particular
  768. implications on how the app will handle routing and navigation:
  769. * `undefined`: The best form of URLs will be chosen based on the
  770. capabilities of the browser. Given no information about the server
  771. environmentm a balanced approach to routing and navigation is
  772. chosen.
  773. The server should be capable of handling full-path requests, since
  774. full-URLs will be generated by browsers using HTML5 history. If this
  775. is a client-side-only app the server could handle full-URL requests
  776. by sending a redirect back to the root with a hash-based URL, e.g:
  777. Request: http://example.com/users/1
  778. Redirect to: http://example.com/#/users/1
  779. * `true`: The server is *fully* capable of properly handling requests
  780. to all full-path URLs the app can produce.
  781. This is the best option for progressive-enhancement because it will
  782. cause **all URLs to always have full-paths**, which means the server
  783. will be able to accurately handle all URLs this app produces. e.g.
  784. http://example.com/users/1
  785. To meet this strict full-URL requirement, browsers which are not
  786. capable of using HTML5 history will make requests to the server
  787. resulting in full-page reloads.
  788. * `false`: The server is *not* capable of properly handling requests
  789. to all full-path URLs the app can produce, therefore all routing
  790. will be handled by this App instance.
  791. Be aware that this will cause **all URLs to always be hash-based**,
  792. even in browsers that are capable of using HTML5 history. e.g.
  793. http://example.com/#/users/1
  794. A single-page or client-side-only app where the server sends a
  795. "shell" page with JavaScript to the client might have this
  796. restriction. If you're setting this to `false`, read the following:
  797. **Note:** When this is set to `false`, the server will *never* receive
  798. the full URL because browsers do not send the fragment-part to the
  799. server, that is everything after and including the "#".
  800. Consider the following example:
  801. URL shown in browser: http://example.com/#/users/1
  802. URL sent to server: http://example.com/
  803. You should feel bad about hurting our precious web if you forcefully set
  804. either `serverRouting` or `html5` to `false`, because you're basically
  805. punching the web in the face here with your lossy URLs! Please make sure
  806. you know what you're doing and that you understand the implications.
  807. Ideally you should always prefer full-path URLs (not /#/foo/), and want
  808. full-page reloads when the client's browser is not capable of enhancing
  809. the experience using the HTML5 history APIs. Setting this to `true` is
  810. the best option for progressive-enhancement (and graceful-degradation).
  811. @attribute serverRouting
  812. @type Boolean
  813. @default undefined
  814. @initOnly
  815. @since 3.5.0
  816. **/
  817. serverRouting: {
  818. valueFn : function () { return Y.App.serverRouting; },
  819. writeOnce: 'initOnly'
  820. },
  821. /**
  822. The node into which this app's `views` will be rendered when they become
  823. the `activeView`.
  824. The view container node serves as the container to hold the app's
  825. `activeView`. Each time the `activeView` is set via `showView()`, the
  826. previous view will be removed from this node, and the new active view's
  827. `container` node will be appended.
  828. The default view container is a `<div>` Node, but you can override this
  829. in a subclass, or by passing in a custom `viewContainer` config value at
  830. instantiation time. The `viewContainer` may be provided as a selector
  831. string, DOM element, or a `Y.Node` instance (having the `viewContainer`
  832. and the `container` be the same node is also supported).
  833. The app's `render()` method will stamp the view container with the CSS
  834. class `"yui3-app-views"` and append it to the app's `container` node if
  835. it isn't already, and any `activeView` will be appended to this node if
  836. it isn't already.
  837. @attribute viewContainer
  838. @type HTMLElement|Node|String
  839. @default Y.Node.create(this.containerTemplate)
  840. @initOnly
  841. @since 3.5.0
  842. **/
  843. viewContainer: {
  844. getter : '_getViewContainer',
  845. setter : Y.one,
  846. writeOnce: true
  847. }
  848. },
  849. /**
  850. Properties that shouldn't be turned into ad-hoc attributes when passed to
  851. App's constructor.
  852. @property _NON_ATTRS_CFG
  853. @type Array
  854. @static
  855. @protected
  856. @since 3.5.0
  857. **/
  858. _NON_ATTRS_CFG: ['views']
  859. });
  860. // -- Namespace ----------------------------------------------------------------
  861. Y.namespace('App').Base = AppBase;
  862. /**
  863. Provides a top-level application component which manages navigation and views.
  864. This gives you a foundation and structure on which to build your application; it
  865. combines robust URL navigation with powerful routing and flexible view
  866. management.
  867. `Y.App` is both a namespace and constructor function. The `Y.App` class is
  868. special in that any `Y.App` class extensions that are included in the YUI
  869. instance will be **auto-mixed** on to the `Y.App` class. Consider this example:
  870. YUI().use('app-base', 'app-transitions', function (Y) {
  871. // This will create two YUI Apps, `basicApp` will not have transitions,
  872. // but `fancyApp` will have transitions support included and turn it on.
  873. var basicApp = new Y.App.Base(),
  874. fancyApp = new Y.App({transitions: true});
  875. });
  876. @class App
  877. @param {Object} [config] The following are configuration properties that can be
  878. specified _in addition_ to default attribute values and the non-attribute
  879. properties provided by `Y.Base`:
  880. @param {Object} [config.views] Hash of view-name to metadata used to
  881. declaratively describe an application's views and their relationship with
  882. the app and other views. The views specified here will override any defaults
  883. provided by the `views` object on the `prototype`.
  884. @constructor
  885. @extends App.Base
  886. @uses App.Content
  887. @uses App.Transitions
  888. @uses PjaxContent
  889. @since 3.5.0
  890. **/
  891. Y.App = Y.mix(Y.Base.create('app', AppBase, []), Y.App, true);
  892. /**
  893. CSS classes used by `Y.App`.
  894. @property CLASS_NAMES
  895. @type Object
  896. @default {}
  897. @static
  898. @since 3.6.0
  899. **/
  900. Y.App.CLASS_NAMES = {
  901. app : getClassName('app'),
  902. views: getClassName('app', 'views')
  903. };
  904. /**
  905. Default `serverRouting` attribute value for all apps.
  906. @property serverRouting
  907. @type Boolean
  908. @default undefined
  909. @static
  910. @since 3.6.0
  911. **/