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