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