commonroutesApp/www/lib/angular-material/modules/js/panel/panel.js
www/lib/angular-material/modules/js/panel/panel.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10/**
11 * @ngdoc module
12 * @name material.components.panel
13 */
14MdPanelService.$inject = ["$rootElement", "$rootScope", "$injector", "$window"];
15angular
16 .module('material.components.panel', [
17 'material.core',
18 'material.components.backdrop'
19 ])
20 .service('$mdPanel', MdPanelService);
21
22
23/*****************************************************************************
24 * PUBLIC DOCUMENTATION *
25 *****************************************************************************/
26
27/**
28 * @ngdoc service
29 * @name $mdPanel
30 * @module material.components.panel
31 *
32 * @description
33 * `$mdPanel` is a robust, low-level service for creating floating panels on
34 * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc.
35 *
36 * @usage
37 * <hljs lang="js">
38 * (function(angular, undefined) {
39 * ‘use strict’;
40 *
41 * angular
42 * .module('demoApp', ['ngMaterial'])
43 * .controller('DemoDialogController', DialogController);
44 *
45 * var panelRef;
46 *
47 * function showPanel($event) {
48 * var panelPosition = $mdPanel.newPanelPosition()
49 * .absolute()
50 * .top('50%')
51 * .left('50%');
52 *
53 * var panelAnimation = $mdPanel.newPanelAnimation()
54 * .targetEvent($event)
55 * .defaultAnimation('md-panel-animate-fly')
56 * .closeTo('.show-button');
57 *
58 * var config = {
59 * attachTo: angular.element(document.body),
60 * controller: DialogController,
61 * controllerAs: 'ctrl',
62 * position: panelPosition,
63 * animation: panelAnimation,
64 * targetEvent: $event,
65 * templateUrl: 'dialog-template.html',
66 * clickOutsideToClose: true,
67 * escapeToClose: true,
68 * focusOnOpen: true
69 * }
70 *
71 * $mdPanel.open(config)
72 * .then(function(result) {
73 * panelRef = result;
74 * });
75 * }
76 *
77 * function DialogController(MdPanelRef, toppings) {
78 * var toppings;
79 *
80 * function closeDialog() {
81 * MdPanelRef && MdPanelRef.close();
82 * }
83 * }
84 * })(angular);
85 * </hljs>
86 */
87
88/**
89 * @ngdoc method
90 * @name $mdPanel#create
91 * @description
92 * Creates a panel with the specified options.
93 *
94 * @param config {!Object=} Specific configuration object that may contain the
95 * following properties:
96 *
97 * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided,
98 * the created panel is added to a tracked panels object. Any subsequent
99 * requests made to create a panel with that ID are ignored. This is useful
100 * in having the panel service not open multiple panels from the same user
101 * interaction when there is no backdrop and events are propagated. Defaults
102 * to an arbitrary string that is not tracked.
103 * - `template` - `{string=}`: HTML template to show in the panel. This
104 * **must** be trusted HTML with respect to Angular’s
105 * [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
106 * - `templateUrl` - `{string=}`: The URL that will be used as the content of
107 * the panel.
108 * - `controller` - `{(function|string)=}`: The controller to associate with
109 * the panel. The controller can inject a reference to the returned
110 * panelRef, which allows the panel to be closed, hidden, and shown. Any
111 * fields passed in through locals or resolve will be bound to the
112 * controller.
113 * - `controllerAs` - `{string=}`: An alias to assign the controller to on
114 * the scope.
115 * - `bindToController` - `{boolean=}`: Binds locals to the controller
116 * instead of passing them in. Defaults to true, as this is a best
117 * practice.
118 * - `locals` - `{Object=}`: An object containing key/value pairs. The keys
119 * will be used as names of values to inject into the controller. For
120 * example, `locals: {three: 3}` would inject `three` into the controller,
121 * with the value 3.
122 * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as
123 * values. The panel will not open until all of the promises resolve.
124 * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to
125 * attach the panel to. Defaults to appending to the root element of the
126 * application.
127 * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch
128 * events should be allowed to propagate 'go through' the container, aka the
129 * wrapper, of the panel. Defaults to false.
130 * - `panelClass` - `{string=}`: A css class to apply to the panel element.
131 * This class should define any borders, box-shadow, etc. for the panel.
132 * - `zIndex` - `{number=}`: The z-index to place the panel at.
133 * Defaults to 80.
134 * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that
135 * specifies the alignment of the panel. For more information, see
136 * `MdPanelPosition`.
137 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click
138 * outside the panel to close it. Defaults to false.
139 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to
140 * close the panel. Defaults to false.
141 * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the
142 * panel. If `trapFocus` is true, the user will not be able to interact
143 * with the rest of the page until the panel is dismissed. Defaults to
144 * false.
145 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on
146 * open. Only disable if focusing some other way, as focus management is
147 * required for panels to be accessible. Defaults to true.
148 * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen.
149 * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults
150 * to false.
151 * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that
152 * specifies the animation of the panel. For more information, see
153 * `MdPanelAnimation`.
154 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop
155 * behind the panel. Defaults to false.
156 * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the
157 * page behind the panel. Defaults to false.
158 * - `onDomAdded` - `{function=}`: Callback function used to announce when
159 * the panel is added to the DOM.
160 * - `onOpenComplete` - `{function=}`: Callback function used to announce
161 * when the open() action is finished.
162 * - `onRemoving` - `{function=}`: Callback function used to announce the
163 * close/hide() action is starting.
164 * - `onDomRemoved` - `{function=}`: Callback function used to announce when
165 * the panel is removed from the DOM.
166 * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus
167 * on when the panel closes. This is commonly the element which triggered
168 * the opening of the panel. If you do not use `origin`, you need to control
169 * the focus manually.
170 *
171 * @returns {!MdPanelRef} panelRef
172 */
173
174
175/**
176 * @ngdoc method
177 * @name $mdPanel#open
178 * @description
179 * Calls the create method above, then opens the panel. This is a shortcut for
180 * creating and then calling open manually. If custom methods need to be
181 * called when the panel is added to the DOM or opened, do not use this method.
182 * Instead create the panel, chain promises on the domAdded and openComplete
183 * methods, and call open from the returned panelRef.
184 *
185 * @param {!Object=} config Specific configuration object that may contain
186 * the properties defined in `$mdPanel.create`.
187 * @returns {!angular.$q.Promise<!MdPanelRef>} panelRef A promise that resolves
188 * to an instance of the panel.
189 */
190
191
192/**
193 * @ngdoc method
194 * @name $mdPanel#newPanelPosition
195 * @description
196 * Returns a new instance of the MdPanelPosition object. Use this to create
197 * the position config object.
198 *
199 * @returns {!MdPanelPosition} panelPosition
200 */
201
202
203/**
204 * @ngdoc method
205 * @name $mdPanel#newPanelAnimation
206 * @description
207 * Returns a new instance of the MdPanelAnimation object. Use this to create
208 * the animation config object.
209 *
210 * @returns {!MdPanelAnimation} panelAnimation
211 */
212
213
214/*****************************************************************************
215 * MdPanelRef *
216 *****************************************************************************/
217
218
219/**
220 * @ngdoc type
221 * @name MdPanelRef
222 * @module material.components.panel
223 * @description
224 * A reference to a created panel. This reference contains a unique id for the
225 * panel, along with the following properties:
226 *
227 * - `id` - `{string}`: The unique id for the panel. This id is used to track
228 * when a panel was interacted with.
229 * - `config` - `{!Object=}`: The entire config object that was used in
230 * create.
231 * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM.
232 * Visibility to the user does not factor into isAttached.
233 * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the
234 * panel. This property is added in order to have access to the `addClass`,
235 * `removeClass`, `toggleClass`, etc methods.
236 * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added
237 * in order to have access to the `addClass`, `removeClass`, `toggleClass`,
238 * etc methods.
239 */
240
241/**
242 * @ngdoc method
243 * @name MdPanelRef#open
244 * @description
245 * Attaches and shows the panel.
246 *
247 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
248 * opened.
249 */
250
251/**
252 * @ngdoc method
253 * @name MdPanelRef#close
254 * @description
255 * Hides and detaches the panel. Note that this will **not** destroy the panel.
256 * If you don't intend on using the panel again, call the {@link #destroy
257 * destroy} method afterwards.
258 *
259 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
260 * closed.
261 */
262
263/**
264 * @ngdoc method
265 * @name MdPanelRef#attach
266 * @description
267 * Create the panel elements and attach them to the DOM. The panel will be
268 * hidden by default.
269 *
270 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
271 * attached.
272 */
273
274/**
275 * @ngdoc method
276 * @name MdPanelRef#detach
277 * @description
278 * Removes the panel from the DOM. This will NOT hide the panel before removing
279 * it.
280 *
281 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
282 * detached.
283 */
284
285/**
286 * @ngdoc method
287 * @name MdPanelRef#show
288 * @description
289 * Shows the panel.
290 *
291 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
292 * shown and animations are completed.
293 */
294
295/**
296 * @ngdoc method
297 * @name MdPanelRef#hide
298 * @description
299 * Hides the panel.
300 *
301 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
302 * hidden and animations are completed.
303 */
304
305/**
306 * @ngdoc method
307 * @name MdPanelRef#destroy
308 * @description
309 * Destroys the panel. The panel cannot be opened again after this is called.
310 */
311
312/**
313 * @ngdoc method
314 * @name MdPanelRef#addClass
315 * @deprecated
316 * This method is in the process of being deprecated in favor of using the panel
317 * and container JQLite elements that are referenced in the MdPanelRef object.
318 * Full deprecation is scheduled for material 1.2.
319 * @description
320 * Adds a class to the panel. DO NOT use this hide/show the panel.
321 *
322 * @param {string} newClass class to be added.
323 * @param {boolean} toElement Whether or not to add the class to the panel
324 * element instead of the container.
325 */
326
327/**
328 * @ngdoc method
329 * @name MdPanelRef#removeClass
330 * @deprecated
331 * This method is in the process of being deprecated in favor of using the panel
332 * and container JQLite elements that are referenced in the MdPanelRef object.
333 * Full deprecation is scheduled for material 1.2.
334 * @description
335 * Removes a class from the panel. DO NOT use this to hide/show the panel.
336 *
337 * @param {string} oldClass Class to be removed.
338 * @param {boolean} fromElement Whether or not to remove the class from the
339 * panel element instead of the container.
340 */
341
342/**
343 * @ngdoc method
344 * @name MdPanelRef#toggleClass
345 * @deprecated
346 * This method is in the process of being deprecated in favor of using the panel
347 * and container JQLite elements that are referenced in the MdPanelRef object.
348 * Full deprecation is scheduled for material 1.2.
349 * @description
350 * Toggles a class on the panel. DO NOT use this to hide/show the panel.
351 *
352 * @param {string} toggleClass Class to be toggled.
353 * @param {boolean} onElement Whether or not to remove the class from the panel
354 * element instead of the container.
355 */
356
357/**
358 * @ngdoc method
359 * @name MdPanelRef#updatePosition
360 * @description
361 * Updates the position configuration of a panel. Use this to update the
362 * position of a panel that is open, without having to close and re-open the
363 * panel.
364 *
365 * @param {!MdPanelPosition} position
366 */
367
368
369/*****************************************************************************
370 * MdPanelPosition *
371 *****************************************************************************/
372
373
374/**
375 * @ngdoc type
376 * @name MdPanelPosition
377 * @module material.components.panel
378 * @description
379 *
380 * Object for configuring the position of the panel.
381 *
382 * @usage
383 *
384 * #### Centering the panel
385 *
386 * <hljs lang="js">
387 * new MdPanelPosition().absolute().center();
388 * </hljs>
389 *
390 * #### Overlapping the panel with an element
391 *
392 * <hljs lang="js">
393 * new MdPanelPosition()
394 * .relativeTo(someElement)
395 * .addPanelPosition(
396 * $mdPanel.xPosition.ALIGN_START,
397 * $mdPanel.yPosition.ALIGN_TOPS
398 * );
399 * </hljs>
400 *
401 * #### Aligning the panel with the bottom of an element
402 *
403 * <hljs lang="js">
404 * new MdPanelPosition()
405 * .relativeTo(someElement)
406 * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW);
407 * </hljs>
408 */
409
410/**
411 * @ngdoc method
412 * @name MdPanelPosition#absolute
413 * @description
414 * Positions the panel absolutely relative to the parent element. If the parent
415 * is document.body, this is equivalent to positioning the panel absolutely
416 * within the viewport.
417 *
418 * @returns {!MdPanelPosition}
419 */
420
421/**
422 * @ngdoc method
423 * @name MdPanelPosition#relativeTo
424 * @description
425 * Positions the panel relative to a specific element.
426 *
427 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
428 * or angular element to position the panel with respect to.
429 * @returns {!MdPanelPosition}
430 */
431
432/**
433 * @ngdoc method
434 * @name MdPanelPosition#top
435 * @description
436 * Sets the value of `top` for the panel. Clears any previously set vertical
437 * position.
438 *
439 * @param {string=} top Value of `top`. Defaults to '0'.
440 * @returns {!MdPanelPosition}
441 */
442
443/**
444 * @ngdoc method
445 * @name MdPanelPosition#bottom
446 * @description
447 * Sets the value of `bottom` for the panel. Clears any previously set vertical
448 * position.
449 *
450 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
451 * @returns {!MdPanelPosition}
452 */
453
454/**
455 * @ngdoc method
456 * @name MdPanelPosition#start
457 * @description
458 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
459 * `rtl`. Clears any previously set horizontal position.
460 *
461 * @param {string=} start Value of position. Defaults to '0'.
462 * @returns {!MdPanelPosition}
463 */
464
465/**
466 * @ngdoc method
467 * @name MdPanelPosition#end
468 * @description
469 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
470 * Clears any previously set horizontal position.
471 *
472 * @param {string=} end Value of position. Defaults to '0'.
473 * @returns {!MdPanelPosition}
474 */
475
476/**
477 * @ngdoc method
478 * @name MdPanelPosition#left
479 * @description
480 * Sets the value of `left` for the panel. Clears any previously set
481 * horizontal position.
482 *
483 * @param {string=} left Value of `left`. Defaults to '0'.
484 * @returns {!MdPanelPosition}
485 */
486
487/**
488 * @ngdoc method
489 * @name MdPanelPosition#right
490 * @description
491 * Sets the value of `right` for the panel. Clears any previously set
492 * horizontal position.
493 *
494 * @param {string=} right Value of `right`. Defaults to '0'.
495 * @returns {!MdPanelPosition}
496 */
497
498/**
499 * @ngdoc method
500 * @name MdPanelPosition#centerHorizontally
501 * @description
502 * Centers the panel horizontally in the viewport. Clears any previously set
503 * horizontal position.
504 *
505 * @returns {!MdPanelPosition}
506 */
507
508/**
509 * @ngdoc method
510 * @name MdPanelPosition#centerVertically
511 * @description
512 * Centers the panel vertically in the viewport. Clears any previously set
513 * vertical position.
514 *
515 * @returns {!MdPanelPosition}
516 */
517
518/**
519 * @ngdoc method
520 * @name MdPanelPosition#center
521 * @description
522 * Centers the panel horizontally and vertically in the viewport. This is
523 * equivalent to calling both `centerHorizontally` and `centerVertically`.
524 * Clears any previously set horizontal and vertical positions.
525 *
526 * @returns {!MdPanelPosition}
527 */
528
529/**
530 * @ngdoc method
531 * @name MdPanelPosition#addPanelPosition
532 * @description
533 * Sets the x and y position for the panel relative to another element. Can be
534 * called multiple times to specify an ordered list of panel positions. The
535 * first position which allows the panel to be completely on-screen will be
536 * chosen; the last position will be chose whether it is on-screen or not.
537 *
538 * xPosition must be one of the following values available on
539 * $mdPanel.xPosition:
540 *
541 * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END
542 *
543 * *************
544 * * *
545 * * PANEL *
546 * * *
547 * *************
548 * A B C D E
549 *
550 * A: OFFSET_START (for LTR displays)
551 * B: ALIGN_START (for LTR displays)
552 * C: CENTER
553 * D: ALIGN_END (for LTR displays)
554 * E: OFFSET_END (for LTR displays)
555 *
556 * yPosition must be one of the following values available on
557 * $mdPanel.yPosition:
558 *
559 * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW
560 *
561 * F
562 * G *************
563 * * *
564 * H * PANEL *
565 * * *
566 * I *************
567 * J
568 *
569 * F: BELOW
570 * G: ALIGN_TOPS
571 * H: CENTER
572 * I: ALIGN_BOTTOMS
573 * J: ABOVE
574 *
575 * @param {string} xPosition
576 * @param {string} yPosition
577 * @returns {!MdPanelPosition}
578 */
579
580/**
581 * @ngdoc method
582 * @name MdPanelPosition#withOffsetX
583 * @description
584 * Sets the value of the offset in the x-direction.
585 *
586 * @param {string} offsetX
587 * @returns {!MdPanelPosition}
588 */
589
590/**
591 * @ngdoc method
592 * @name MdPanelPosition#withOffsetY
593 * @description
594 * Sets the value of the offset in the y-direction.
595 *
596 * @param {string} offsetY
597 * @returns {!MdPanelPosition}
598 */
599
600
601/*****************************************************************************
602 * MdPanelAnimation *
603 *****************************************************************************/
604
605
606/**
607 * @ngdoc object
608 * @name MdPanelAnimation
609 * @description
610 * Animation configuration object. To use, create an MdPanelAnimation with the
611 * desired properties, then pass the object as part of $mdPanel creation.
612 *
613 * Example:
614 *
615 * var panelAnimation = new MdPanelAnimation()
616 * .openFrom(myButtonEl)
617 * .closeTo('.my-button')
618 * .withAnimation($mdPanel.animation.SCALE);
619 *
620 * $mdPanel.create({
621 * animation: panelAnimation
622 * });
623 */
624
625/**
626 * @ngdoc method
627 * @name MdPanelAnimation#openFrom
628 * @description
629 * Specifies where to start the open animation. `openFrom` accepts a
630 * click event object, query selector, DOM element, or a Rect object that
631 * is used to determine the bounds. When passed a click event, the location
632 * of the click will be used as the position to start the animation.
633 *
634 * @param {string|!Element|!Event|{top: number, left: number}}
635 * @returns {!MdPanelAnimation}
636 */
637
638/**
639 * @ngdoc method
640 * @name MdPanelAnimation#closeTo
641 * @description
642 * Specifies where to animate the panel close. `closeTo` accepts a
643 * query selector, DOM element, or a Rect object that is used to determine
644 * the bounds.
645 *
646 * @param {string|!Element|{top: number, left: number}}
647 * @returns {!MdPanelAnimation}
648 */
649
650/**
651 * @ngdoc method
652 * @name MdPanelAnimation#withAnimation
653 * @description
654 * Specifies the animation class.
655 *
656 * There are several default animations that can be used:
657 * ($mdPanel.animation)
658 * SLIDE: The panel slides in and out from the specified
659 * elements. It will not fade in or out.
660 * SCALE: The panel scales in and out. Slide and fade are
661 * included in this animation.
662 * FADE: The panel fades in and out.
663 *
664 * Custom classes will by default fade in and out unless
665 * "transition: opacity 1ms" is added to the to custom class.
666 *
667 * @param {string|{open: string, close: string}} cssClass
668 * @returns {!MdPanelAnimation}
669 */
670
671
672/*****************************************************************************
673 * IMPLEMENTATION *
674 *****************************************************************************/
675
676
677// Default z-index for the panel.
678var defaultZIndex = 80;
679var MD_PANEL_HIDDEN = '_md-panel-hidden';
680
681var FOCUS_TRAP_TEMPLATE = angular.element(
682 '<div class="_md-panel-focus-trap" tabindex="0"></div>');
683
684
685/**
686 * A service that is used for controlling/displaying panels on the screen.
687 * @param {!angular.JQLite} $rootElement
688 * @param {!angular.Scope} $rootScope
689 * @param {!angular.$injector} $injector
690 * @param {!angular.$window} $window
691 * @final @constructor ngInject
692 */
693function MdPanelService($rootElement, $rootScope, $injector, $window) {
694 /**
695 * Default config options for the panel.
696 * Anything angular related needs to be done later. Therefore
697 * scope: $rootScope.$new(true),
698 * attachTo: $rootElement,
699 * are added later.
700 * @private {!Object}
701 */
702 this._defaultConfigOptions = {
703 bindToController: true,
704 clickOutsideToClose: false,
705 disableParentScroll: false,
706 escapeToClose: false,
707 focusOnOpen: true,
708 fullscreen: false,
709 hasBackdrop: false,
710 propagateContainerEvents: false,
711 transformTemplate: angular.bind(this, this._wrapTemplate),
712 trapFocus: false,
713 zIndex: defaultZIndex
714 };
715
716 /** @private {!Object} */
717 this._config = {};
718
719 /** @private @const */
720 this._$rootElement = $rootElement;
721
722 /** @private @const */
723 this._$rootScope = $rootScope;
724
725 /** @private @const */
726 this._$injector = $injector;
727
728 /** @private @const */
729 this._$window = $window;
730
731 /** @private {!Object<string, !MdPanelRef>} */
732 this._trackedPanels = {};
733
734 /**
735 * Default animations that can be used within the panel.
736 * @type {enum}
737 */
738 this.animation = MdPanelAnimation.animation;
739
740 /**
741 * Possible values of xPosition for positioning the panel relative to
742 * another element.
743 * @type {enum}
744 */
745 this.xPosition = MdPanelPosition.xPosition;
746
747 /**
748 * Possible values of yPosition for positioning the panel relative to
749 * another element.
750 * @type {enum}
751 */
752 this.yPosition = MdPanelPosition.yPosition;
753}
754
755
756/**
757 * Creates a panel with the specified options.
758 * @param {!Object=} config Configuration object for the panel.
759 * @returns {!MdPanelRef}
760 */
761MdPanelService.prototype.create = function(config) {
762 config = config || {};
763
764 // If the passed-in config contains an ID and the ID is within _trackedPanels,
765 // return the tracked panel.
766 if (angular.isDefined(config.id) && this._trackedPanels[config.id]) {
767 return this._trackedPanels[config.id];
768 }
769
770 // If no ID is set within the passed-in config, then create an arbitrary ID.
771 this._config = {
772 id: config.id || 'panel_' + this._$injector.get('$mdUtil').nextUid(),
773 scope: this._$rootScope.$new(true),
774 attachTo: this._$rootElement
775 };
776 angular.extend(this._config, this._defaultConfigOptions, config);
777
778 var panelRef = new MdPanelRef(this._config, this._$injector);
779 this._trackedPanels[config.id] = panelRef;
780
781 return panelRef;
782};
783
784
785/**
786 * Creates and opens a panel with the specified options.
787 * @param {!Object=} config Configuration object for the panel.
788 * @returns {!angular.$q.Promise<!MdPanelRef>} The panel created from create.
789 */
790MdPanelService.prototype.open = function(config) {
791 var panelRef = this.create(config);
792 return panelRef.open().then(function() {
793 return panelRef;
794 });
795};
796
797
798/**
799 * Returns a new instance of the MdPanelPosition. Use this to create the
800 * positioning object.
801 * @returns {!MdPanelPosition}
802 */
803MdPanelService.prototype.newPanelPosition = function() {
804 return new MdPanelPosition(this._$injector);
805};
806
807
808/**
809 * Returns a new instance of the MdPanelAnimation. Use this to create the
810 * animation object.
811 * @returns {!MdPanelAnimation}
812 */
813MdPanelService.prototype.newPanelAnimation = function() {
814 return new MdPanelAnimation(this._$injector);
815};
816
817
818/**
819 * Wraps the users template in two elements, md-panel-outer-wrapper, which
820 * covers the entire attachTo element, and md-panel, which contains only the
821 * template. This allows the panel control over positioning, animations,
822 * and similar properties.
823 * @param {string} origTemplate The original template.
824 * @returns {string} The wrapped template.
825 * @private
826 */
827MdPanelService.prototype._wrapTemplate = function(origTemplate) {
828 var template = origTemplate || '';
829
830 // The panel should be initially rendered offscreen so we can calculate
831 // height and width for positioning.
832 return '' +
833 '<div class="md-panel-outer-wrapper">' +
834 ' <div class="md-panel" style="left: -9999px;">' + template + '</div>' +
835 '</div>';
836};
837
838
839/*****************************************************************************
840 * MdPanelRef *
841 *****************************************************************************/
842
843
844/**
845 * A reference to a created panel. This reference contains a unique id for the
846 * panel, along with properties/functions used to control the panel.
847 * @param {!Object} config
848 * @param {!angular.$injector} $injector
849 * @final @constructor
850 */
851function MdPanelRef(config, $injector) {
852 // Injected variables.
853 /** @private @const {!angular.$q} */
854 this._$q = $injector.get('$q');
855
856 /** @private @const {!angular.$mdCompiler} */
857 this._$mdCompiler = $injector.get('$mdCompiler');
858
859 /** @private @const {!angular.$mdConstant} */
860 this._$mdConstant = $injector.get('$mdConstant');
861
862 /** @private @const {!angular.$mdUtil} */
863 this._$mdUtil = $injector.get('$mdUtil');
864
865 /** @private @const {!angular.Scope} */
866 this._$rootScope = $injector.get('$rootScope');
867
868 /** @private @const {!angular.$animate} */
869 this._$animate = $injector.get('$animate');
870
871 /** @private @const {!MdPanelRef} */
872 this._$mdPanel = $injector.get('$mdPanel');
873
874 /** @private @const {!angular.$log} */
875 this._$log = $injector.get('$log');
876
877 /** @private @const {!angular.$window} */
878 this._$window = $injector.get('$window');
879
880 /** @private @const {!Function} */
881 this._$$rAF = $injector.get('$$rAF');
882
883 // Public variables.
884 /**
885 * Unique id for the panelRef.
886 * @type {string}
887 */
888 this.id = config.id;
889
890 /** @type {!Object} */
891 this.config = config;
892
893 /** @type {!angular.JQLite|undefined} */
894 this.panelContainer;
895
896 /** @type {!angular.JQLite|undefined} */
897 this.panelEl;
898
899 /**
900 * Whether the panel is attached. This is synchronous. When attach is called,
901 * isAttached is set to true. When detach is called, isAttached is set to
902 * false.
903 * @type {boolean}
904 */
905 this.isAttached = false;
906
907 // Private variables.
908 /** @private {Array<function()>} */
909 this._removeListeners = [];
910
911 /** @private {!angular.JQLite|undefined} */
912 this._topFocusTrap;
913
914 /** @private {!angular.JQLite|undefined} */
915 this._bottomFocusTrap;
916
917 /** @private {!$mdPanel|undefined} */
918 this._backdropRef;
919
920 /** @private {Function?} */
921 this._restoreScroll = null;
922}
923
924
925/**
926 * Opens an already created and configured panel. If the panel is already
927 * visible, does nothing.
928 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
929 * the panel is opened and animations finish.
930 */
931MdPanelRef.prototype.open = function() {
932 var self = this;
933 return this._$q(function(resolve, reject) {
934 var done = self._done(resolve, self);
935 var show = self._simpleBind(self.show, self);
936
937 self.attach()
938 .then(show)
939 .then(done)
940 .catch(reject);
941 });
942};
943
944
945/**
946 * Closes the panel.
947 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
948 * the panel is closed and animations finish.
949 */
950MdPanelRef.prototype.close = function() {
951 var self = this;
952
953 return this._$q(function(resolve, reject) {
954 var done = self._done(resolve, self);
955 var detach = self._simpleBind(self.detach, self);
956
957 self.hide()
958 .then(detach)
959 .then(done)
960 .catch(reject);
961 });
962};
963
964
965/**
966 * Attaches the panel. The panel will be hidden afterwards.
967 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
968 * the panel is attached.
969 */
970MdPanelRef.prototype.attach = function() {
971 if (this.isAttached && this.panelEl) {
972 return this._$q.when(this);
973 }
974
975 var self = this;
976 return this._$q(function(resolve, reject) {
977 var done = self._done(resolve, self);
978 var onDomAdded = self.config['onDomAdded'] || angular.noop;
979 var addListeners = function(response) {
980 self.isAttached = true;
981 self._addEventListeners();
982 return response;
983 };
984
985 self._$q.all([
986 self._createBackdrop(),
987 self._createPanel()
988 .then(addListeners)
989 .catch(reject)
990 ]).then(onDomAdded)
991 .then(done)
992 .catch(reject);
993 });
994};
995
996
997/**
998 * Only detaches the panel. Will NOT hide the panel first.
999 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
1000 * the panel is detached.
1001 */
1002MdPanelRef.prototype.detach = function() {
1003 if (!this.isAttached) {
1004 return this._$q.when(this);
1005 }
1006
1007 var self = this;
1008 var onDomRemoved = self.config['onDomRemoved'] || angular.noop;
1009
1010 var detachFn = function() {
1011 self._removeEventListeners();
1012
1013 // Remove the focus traps that we added earlier for keeping focus within
1014 // the panel.
1015 if (self._topFocusTrap && self._topFocusTrap.parentNode) {
1016 self._topFocusTrap.parentNode.removeChild(self._topFocusTrap);
1017 }
1018
1019 if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) {
1020 self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap);
1021 }
1022
1023 self.panelContainer.remove();
1024 self.isAttached = false;
1025 return self._$q.when(self);
1026 };
1027
1028 if (this._restoreScroll) {
1029 this._restoreScroll();
1030 this._restoreScroll = null;
1031 }
1032
1033 return this._$q(function(resolve, reject) {
1034 var done = self._done(resolve, self);
1035
1036 self._$q.all([
1037 detachFn(),
1038 self._backdropRef ? self._backdropRef.detach() : true
1039 ]).then(onDomRemoved)
1040 .then(done)
1041 .catch(reject);
1042 });
1043};
1044
1045
1046/**
1047 * Destroys the panel. The Panel cannot be opened again after this.
1048 */
1049MdPanelRef.prototype.destroy = function() {
1050 this.config.scope.$destroy();
1051 this.config.locals = null;
1052};
1053
1054
1055/**
1056 * Shows the panel.
1057 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
1058 * the panel has shown and animations finish.
1059 */
1060MdPanelRef.prototype.show = function() {
1061 if (!this.panelContainer) {
1062 return this._$q(function(resolve, reject) {
1063 reject('Panel does not exist yet. Call open() or attach().');
1064 });
1065 }
1066
1067 if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
1068 return this._$q.when(this);
1069 }
1070
1071 var self = this;
1072 var animatePromise = function() {
1073 self.panelContainer.removeClass(MD_PANEL_HIDDEN);
1074 return self._animateOpen();
1075 };
1076
1077 return this._$q(function(resolve, reject) {
1078 var done = self._done(resolve, self);
1079 var onOpenComplete = self.config['onOpenComplete'] || angular.noop;
1080
1081 self._$q.all([
1082 self._backdropRef ? self._backdropRef.show() : self,
1083 animatePromise().then(function() { self._focusOnOpen(); }, reject)
1084 ]).then(onOpenComplete)
1085 .then(done)
1086 .catch(reject);
1087 });
1088};
1089
1090
1091/**
1092 * Hides the panel.
1093 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
1094 * the panel has hidden and animations finish.
1095 */
1096MdPanelRef.prototype.hide = function() {
1097 if (!this.panelContainer) {
1098 return this._$q(function(resolve, reject) {
1099 reject('Panel does not exist yet. Call open() or attach().');
1100 });
1101 }
1102
1103 if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
1104 return this._$q.when(this);
1105 }
1106
1107 var self = this;
1108
1109 return this._$q(function(resolve, reject) {
1110 var done = self._done(resolve, self);
1111 var onRemoving = self.config['onRemoving'] || angular.noop;
1112
1113 var focusOnOrigin = function() {
1114 var origin = self.config['origin'];
1115 if (origin) {
1116 getElement(origin).focus();
1117 }
1118 };
1119
1120 var hidePanel = function() {
1121 self.panelContainer.addClass(MD_PANEL_HIDDEN);
1122 };
1123
1124 self._$q.all([
1125 self._backdropRef ? self._backdropRef.hide() : self,
1126 self._animateClose()
1127 .then(onRemoving)
1128 .then(hidePanel)
1129 .then(focusOnOrigin)
1130 .catch(reject)
1131 ]).then(done, reject);
1132 });
1133};
1134
1135
1136/**
1137 * Add a class to the panel. DO NOT use this to hide/show the panel.
1138 * @deprecated
1139 * This method is in the process of being deprecated in favor of using the panel
1140 * and container JQLite elements that are referenced in the MdPanelRef object.
1141 * Full deprecation is scheduled for material 1.2.
1142 *
1143 * @param {string} newClass Class to be added.
1144 * @param {boolean} toElement Whether or not to add the class to the panel
1145 * element instead of the container.
1146 */
1147MdPanelRef.prototype.addClass = function(newClass, toElement) {
1148 this._$log.warn(
1149 'The addClass method is in the process of being deprecated. ' +
1150 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
1151 'To achieve the same results, use the panelContainer or panelEl ' +
1152 'JQLite elements that are referenced in MdPanelRef.');
1153
1154 if (!this.panelContainer) {
1155 throw new Error('Panel does not exist yet. Call open() or attach().');
1156 }
1157
1158 if (!toElement && !this.panelContainer.hasClass(newClass)) {
1159 this.panelContainer.addClass(newClass);
1160 } else if (toElement && !this.panelEl.hasClass(newClass)) {
1161 this.panelEl.addClass(newClass);
1162 }
1163};
1164
1165
1166/**
1167 * Remove a class from the panel. DO NOT use this to hide/show the panel.
1168 * @deprecated
1169 * This method is in the process of being deprecated in favor of using the panel
1170 * and container JQLite elements that are referenced in the MdPanelRef object.
1171 * Full deprecation is scheduled for material 1.2.
1172 *
1173 * @param {string} oldClass Class to be removed.
1174 * @param {boolean} fromElement Whether or not to remove the class from the
1175 * panel element instead of the container.
1176 */
1177MdPanelRef.prototype.removeClass = function(oldClass, fromElement) {
1178 this._$log.warn(
1179 'The removeClass method is in the process of being deprecated. ' +
1180 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
1181 'To achieve the same results, use the panelContainer or panelEl ' +
1182 'JQLite elements that are referenced in MdPanelRef.');
1183
1184 if (!this.panelContainer) {
1185 throw new Error('Panel does not exist yet. Call open() or attach().');
1186 }
1187
1188 if (!fromElement && this.panelContainer.hasClass(oldClass)) {
1189 this.panelContainer.removeClass(oldClass);
1190 } else if (fromElement && this.panelEl.hasClass(oldClass)) {
1191 this.panelEl.removeClass(oldClass);
1192 }
1193};
1194
1195
1196/**
1197 * Toggle a class on the panel. DO NOT use this to hide/show the panel.
1198 * @deprecated
1199 * This method is in the process of being deprecated in favor of using the panel
1200 * and container JQLite elements that are referenced in the MdPanelRef object.
1201 * Full deprecation is scheduled for material 1.2.
1202 *
1203 * @param {string} toggleClass The class to toggle.
1204 * @param {boolean} onElement Whether or not to toggle the class on the panel
1205 * element instead of the container.
1206 */
1207MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) {
1208 this._$log.warn(
1209 'The toggleClass method is in the process of being deprecated. ' +
1210 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
1211 'To achieve the same results, use the panelContainer or panelEl ' +
1212 'JQLite elements that are referenced in MdPanelRef.');
1213
1214 if (!this.panelContainer) {
1215 throw new Error('Panel does not exist yet. Call open() or attach().');
1216 }
1217
1218 if (!onElement) {
1219 this.panelContainer.toggleClass(toggleClass);
1220 } else {
1221 this.panelEl.toggleClass(toggleClass);
1222 }
1223};
1224
1225
1226/**
1227 * Creates a panel and adds it to the dom.
1228 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
1229 * created.
1230 * @private
1231 */
1232MdPanelRef.prototype._createPanel = function() {
1233 var self = this;
1234
1235 return this._$q(function(resolve, reject) {
1236 if (!self.config.locals) {
1237 self.config.locals = {};
1238 }
1239
1240 self.config.locals.mdPanelRef = self;
1241 self._$mdCompiler.compile(self.config)
1242 .then(function(compileData) {
1243 self.panelContainer = compileData.link(self.config['scope']);
1244 getElement(self.config['attachTo']).append(self.panelContainer);
1245
1246 if (self.config['disableParentScroll']) {
1247 self._restoreScroll = self._$mdUtil.disableScrollAround(
1248 null,
1249 self.panelContainer,
1250 { disableScrollMask: true }
1251 );
1252 }
1253
1254 self.panelEl = angular.element(
1255 self.panelContainer[0].querySelector('.md-panel'));
1256
1257 // Add a custom CSS class to the panel element.
1258 if (self.config['panelClass']) {
1259 self.panelEl.addClass(self.config['panelClass']);
1260 }
1261
1262 // Handle click and touch events for the panel container.
1263 if (self.config['propagateContainerEvents']) {
1264 self.panelContainer.css('pointer-events', 'none');
1265 }
1266
1267 // Panel may be outside the $rootElement, tell ngAnimate to animate
1268 // regardless.
1269 if (self._$animate.pin) {
1270 self._$animate.pin(self.panelContainer,
1271 getElement(self.config['attachTo']));
1272 }
1273
1274 self._configureTrapFocus();
1275 self._addStyles().then(function() {
1276 resolve(self);
1277 }, reject);
1278 }, reject);
1279 });
1280};
1281
1282
1283/**
1284 * Adds the styles for the panel, such as positioning and z-index.
1285 * @returns {!angular.$q.Promise<!MdPanelRef>}
1286 * @private
1287 */
1288MdPanelRef.prototype._addStyles = function() {
1289 var self = this;
1290 return this._$q(function(resolve) {
1291 self.panelContainer.css('z-index', self.config['zIndex']);
1292 self.panelEl.css('z-index', self.config['zIndex'] + 1);
1293
1294 var hideAndResolve = function() {
1295 // Remove left: -9999px and add hidden class.
1296 self.panelEl.css('left', '');
1297 self.panelContainer.addClass(MD_PANEL_HIDDEN);
1298 resolve(self);
1299 };
1300
1301 if (self.config['fullscreen']) {
1302 self.panelEl.addClass('_md-panel-fullscreen');
1303 hideAndResolve();
1304 return; // Don't setup positioning.
1305 }
1306
1307 var positionConfig = self.config['position'];
1308 if (!positionConfig) {
1309 hideAndResolve();
1310 return; // Don't setup positioning.
1311 }
1312
1313 // Wait for angular to finish processing the template, then position it
1314 // correctly. This is necessary so that the panel will have a defined height
1315 // and width.
1316 self._$rootScope['$$postDigest'](function() {
1317 self._updatePosition(true);
1318 resolve(self);
1319 });
1320 });
1321};
1322
1323
1324/**
1325 * Updates the position configuration of a panel
1326 * @param {!MdPanelPosition} position
1327 */
1328MdPanelRef.prototype.updatePosition = function(position) {
1329 if (!this.panelContainer) {
1330 throw new Error('Panel does not exist yet. Call open() or attach().');
1331 }
1332
1333 this.config['position'] = position;
1334 this._updatePosition();
1335};
1336
1337
1338/**
1339 * Calculates and updates the position of the panel.
1340 * @param {boolean=} init
1341 * @private
1342 */
1343MdPanelRef.prototype._updatePosition = function(init) {
1344 var positionConfig = this.config['position'];
1345
1346 if (positionConfig) {
1347 positionConfig._setPanelPosition(this.panelEl);
1348
1349 // Hide the panel now that position is known.
1350 if (init) {
1351 this.panelContainer.addClass(MD_PANEL_HIDDEN);
1352 }
1353
1354 this.panelEl.css(
1355 MdPanelPosition.absPosition.TOP,
1356 positionConfig.getTop()
1357 );
1358 this.panelEl.css(
1359 MdPanelPosition.absPosition.BOTTOM,
1360 positionConfig.getBottom()
1361 );
1362 this.panelEl.css(
1363 MdPanelPosition.absPosition.LEFT,
1364 positionConfig.getLeft()
1365 );
1366 this.panelEl.css(
1367 MdPanelPosition.absPosition.RIGHT,
1368 positionConfig.getRight()
1369 );
1370
1371 // Use the vendor prefixed version of transform.
1372 var prefixedTransform = this._$mdConstant.CSS.TRANSFORM;
1373 this.panelEl.css(prefixedTransform, positionConfig.getTransform());
1374 }
1375};
1376
1377
1378/**
1379 * Focuses on the panel or the first focus target.
1380 * @private
1381 */
1382MdPanelRef.prototype._focusOnOpen = function() {
1383 if (this.config['focusOnOpen']) {
1384 // Wait for the template to finish rendering to guarantee md-autofocus has
1385 // finished adding the class md-autofocus, otherwise the focusable element
1386 // isn't available to focus.
1387 var self = this;
1388 this._$rootScope['$$postDigest'](function() {
1389 var target = self._$mdUtil.findFocusTarget(self.panelEl) ||
1390 self.panelEl;
1391 target.focus();
1392 });
1393 }
1394};
1395
1396
1397/**
1398 * Shows the backdrop.
1399 * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop
1400 * is created and attached.
1401 * @private
1402 */
1403MdPanelRef.prototype._createBackdrop = function() {
1404 if (this.config.hasBackdrop) {
1405 if (!this._backdropRef) {
1406 var backdropAnimation = this._$mdPanel.newPanelAnimation()
1407 .openFrom(this.config.attachTo)
1408 .withAnimation({
1409 open: '_md-opaque-enter',
1410 close: '_md-opaque-leave'
1411 });
1412 var backdropConfig = {
1413 animation: backdropAnimation,
1414 attachTo: this.config.attachTo,
1415 focusOnOpen: false,
1416 panelClass: '_md-panel-backdrop',
1417 zIndex: this.config.zIndex - 1
1418 };
1419 this._backdropRef = this._$mdPanel.create(backdropConfig);
1420 }
1421 if (!this._backdropRef.isAttached) {
1422 return this._backdropRef.attach();
1423 }
1424 }
1425};
1426
1427
1428/**
1429 * Listen for escape keys and outside clicks to auto close.
1430 * @private
1431 */
1432MdPanelRef.prototype._addEventListeners = function() {
1433 this._configureEscapeToClose();
1434 this._configureClickOutsideToClose();
1435 this._configureScrollListener();
1436};
1437
1438
1439/**
1440 * Remove event listeners added in _addEventListeners.
1441 * @private
1442 */
1443MdPanelRef.prototype._removeEventListeners = function() {
1444 this._removeListeners && this._removeListeners.forEach(function(removeFn) {
1445 removeFn();
1446 });
1447 this._removeListeners = [];
1448};
1449
1450
1451/**
1452 * Setup the escapeToClose event listeners.
1453 * @private
1454 */
1455MdPanelRef.prototype._configureEscapeToClose = function() {
1456 if (this.config['escapeToClose']) {
1457 var parentTarget = getElement(this.config['attachTo']);
1458 var self = this;
1459
1460 var keyHandlerFn = function(ev) {
1461 if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) {
1462 ev.stopPropagation();
1463 ev.preventDefault();
1464
1465 self.close();
1466 }
1467 };
1468
1469 // Add keydown listeners
1470 this.panelContainer.on('keydown', keyHandlerFn);
1471 parentTarget.on('keydown', keyHandlerFn);
1472
1473 // Queue remove listeners function
1474 this._removeListeners.push(function() {
1475 self.panelContainer.off('keydown', keyHandlerFn);
1476 parentTarget.off('keydown', keyHandlerFn);
1477 });
1478 }
1479};
1480
1481
1482/**
1483 * Setup the clickOutsideToClose event listeners.
1484 * @private
1485 */
1486MdPanelRef.prototype._configureClickOutsideToClose = function() {
1487 if (this.config['clickOutsideToClose']) {
1488 var target = this.panelContainer;
1489 var sourceElem;
1490
1491 // Keep track of the element on which the mouse originally went down
1492 // so that we can only close the backdrop when the 'click' started on it.
1493 // A simple 'click' handler does not work,
1494 // it sets the target object as the element the mouse went down on.
1495 var mousedownHandler = function(ev) {
1496 sourceElem = ev.target;
1497 };
1498
1499 // We check if our original element and the target is the backdrop
1500 // because if the original was the backdrop and the target was inside the
1501 // panel we don't want to panel to close.
1502 var self = this;
1503 var mouseupHandler = function(ev) {
1504 if (sourceElem === target[0] && ev.target === target[0]) {
1505 ev.stopPropagation();
1506 ev.preventDefault();
1507
1508 self.close();
1509 }
1510 };
1511
1512 // Add listeners
1513 target.on('mousedown', mousedownHandler);
1514 target.on('mouseup', mouseupHandler);
1515
1516 // Queue remove listeners function
1517 this._removeListeners.push(function() {
1518 target.off('mousedown', mousedownHandler);
1519 target.off('mouseup', mouseupHandler);
1520 });
1521 }
1522};
1523
1524
1525/**
1526 * Configures the listeners for updating the panel position on scroll.
1527 * @private
1528*/
1529MdPanelRef.prototype._configureScrollListener = function() {
1530 var updatePosition = angular.bind(this, this._updatePosition);
1531 var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition);
1532 var self = this;
1533
1534 var onScroll = function() {
1535 if (!self.config['disableParentScroll']) {
1536 debouncedUpdatePosition();
1537 }
1538 };
1539
1540 // Add listeners.
1541 this._$window.addEventListener('scroll', onScroll, true);
1542
1543 // Queue remove listeners function.
1544 this._removeListeners.push(function() {
1545 self._$window.removeEventListener('scroll', onScroll, true);
1546 });
1547};
1548
1549
1550/**
1551 * Setup the focus traps. These traps will wrap focus when tabbing past the
1552 * panel. When shift-tabbing, the focus will stick in place.
1553 * @private
1554 */
1555MdPanelRef.prototype._configureTrapFocus = function() {
1556 // Focus doesn't remain instead of the panel without this.
1557 this.panelEl.attr('tabIndex', '-1');
1558 if (this.config['trapFocus']) {
1559 var element = this.panelEl;
1560 // Set up elements before and after the panel to capture focus and
1561 // redirect back into the panel.
1562 this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
1563 this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
1564
1565 // When focus is about to move out of the panel, we want to intercept it
1566 // and redirect it back to the panel element.
1567 var focusHandler = function() {
1568 element.focus();
1569 };
1570 this._topFocusTrap.addEventListener('focus', focusHandler);
1571 this._bottomFocusTrap.addEventListener('focus', focusHandler);
1572
1573 // Queue remove listeners function
1574 this._removeListeners.push(this._simpleBind(function() {
1575 this._topFocusTrap.removeEventListener('focus', focusHandler);
1576 this._bottomFocusTrap.removeEventListener('focus', focusHandler);
1577 }, this));
1578
1579 // The top focus trap inserted immediately before the md-panel element (as
1580 // a sibling). The bottom focus trap inserted immediately after the
1581 // md-panel element (as a sibling).
1582 element[0].parentNode.insertBefore(this._topFocusTrap, element[0]);
1583 element.after(this._bottomFocusTrap);
1584 }
1585};
1586
1587
1588/**
1589 * Animate the panel opening.
1590 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
1591 * animated open.
1592 * @private
1593 */
1594MdPanelRef.prototype._animateOpen = function() {
1595 this.panelContainer.addClass('md-panel-is-showing');
1596 var animationConfig = this.config['animation'];
1597 if (!animationConfig) {
1598 // Promise is in progress, return it.
1599 this.panelContainer.addClass('_md-panel-shown');
1600 return this._$q.when(this);
1601 }
1602
1603 var self = this;
1604 return this._$q(function(resolve) {
1605 var done = self._done(resolve, self);
1606 var warnAndOpen = function() {
1607 self._$log.warn(
1608 'MdPanel Animations failed. Showing panel without animating.');
1609 done();
1610 };
1611
1612 animationConfig.animateOpen(self.panelEl)
1613 .then(done, warnAndOpen);
1614 });
1615};
1616
1617
1618/**
1619 * Animate the panel closing.
1620 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
1621 * animated closed.
1622 * @private
1623 */
1624MdPanelRef.prototype._animateClose = function() {
1625 var animationConfig = this.config['animation'];
1626 if (!animationConfig) {
1627 this.panelContainer.removeClass('md-panel-is-showing');
1628 this.panelContainer.removeClass('_md-panel-shown');
1629 return this._$q.when(this);
1630 }
1631
1632 var self = this;
1633 return this._$q(function(resolve) {
1634 var done = function() {
1635 self.panelContainer.removeClass('md-panel-is-showing');
1636 resolve(self);
1637 };
1638 var warnAndClose = function() {
1639 self._$log.warn(
1640 'MdPanel Animations failed. Hiding panel without animating.');
1641 done();
1642 };
1643
1644 animationConfig.animateClose(self.panelEl)
1645 .then(done, warnAndClose);
1646 });
1647};
1648
1649
1650/**
1651 * Faster, more basic than angular.bind
1652 * http://jsperf.com/angular-bind-vs-custom-vs-native
1653 * @param {function} callback
1654 * @param {!Object} self
1655 * @return {function} Callback function with a bound self.
1656 */
1657MdPanelRef.prototype._simpleBind = function(callback, self) {
1658 return function(value) {
1659 return callback.apply(self, value);
1660 };
1661};
1662
1663
1664/**
1665 * @param {function} callback
1666 * @param {!Object} self
1667 * @return {function} Callback function with a self param.
1668 */
1669MdPanelRef.prototype._done = function(callback, self) {
1670 return function() {
1671 callback(self);
1672 };
1673};
1674
1675
1676/*****************************************************************************
1677 * MdPanelPosition *
1678 *****************************************************************************/
1679
1680
1681/**
1682 * Position configuration object. To use, create an MdPanelPosition with the
1683 * desired properties, then pass the object as part of $mdPanel creation.
1684 *
1685 * Example:
1686 *
1687 * var panelPosition = new MdPanelPosition()
1688 * .relativeTo(myButtonEl)
1689 * .addPanelPosition(
1690 * $mdPanel.xPosition.CENTER,
1691 * $mdPanel.yPosition.ALIGN_TOPS
1692 * );
1693 *
1694 * $mdPanel.create({
1695 * position: panelPosition
1696 * });
1697 *
1698 * @param {!angular.$injector} $injector
1699 * @final @constructor
1700 */
1701function MdPanelPosition($injector) {
1702 /** @private @const {!angular.$window} */
1703 this._$window = $injector.get('$window');
1704
1705 /** @private {boolean} */
1706 this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl';
1707
1708 /** @private {boolean} */
1709 this._absolute = false;
1710
1711 /** @private {!angular.JQLite} */
1712 this._relativeToEl;
1713
1714 /** @private {string} */
1715 this._top = '';
1716
1717 /** @private {string} */
1718 this._bottom = '';
1719
1720 /** @private {string} */
1721 this._left = '';
1722
1723 /** @private {string} */
1724 this._right = '';
1725
1726 /** @private {!Array<string>} */
1727 this._translateX = [];
1728
1729 /** @private {!Array<string>} */
1730 this._translateY = [];
1731
1732 /** @private {!Array<{x:string, y:string}>} */
1733 this._positions = [];
1734
1735 /** @private {?{x:string, y:string}} */
1736 this._actualPosition;
1737}
1738
1739
1740/**
1741 * Possible values of xPosition.
1742 * @enum {string}
1743 */
1744MdPanelPosition.xPosition = {
1745 CENTER: 'center',
1746 ALIGN_START: 'align-start',
1747 ALIGN_END: 'align-end',
1748 OFFSET_START: 'offset-start',
1749 OFFSET_END: 'offset-end'
1750};
1751
1752
1753/**
1754 * Possible values of yPosition.
1755 * @enum {string}
1756 */
1757MdPanelPosition.yPosition = {
1758 CENTER: 'center',
1759 ALIGN_TOPS: 'align-tops',
1760 ALIGN_BOTTOMS: 'align-bottoms',
1761 ABOVE: 'above',
1762 BELOW: 'below'
1763};
1764
1765
1766/**
1767 * Possible values of absolute position.
1768 * @enum {string}
1769 */
1770MdPanelPosition.absPosition = {
1771 TOP: 'top',
1772 RIGHT: 'right',
1773 BOTTOM: 'bottom',
1774 LEFT: 'left'
1775};
1776
1777
1778/**
1779 * Sets absolute positioning for the panel.
1780 * @return {!MdPanelPosition}
1781 */
1782MdPanelPosition.prototype.absolute = function() {
1783 this._absolute = true;
1784 return this;
1785};
1786
1787/**
1788 * Sets the value of a position for the panel. Clears any previously set
1789 * position.
1790 * @param {string} position Position to set
1791 * @param {string=} value Value of the position. Defaults to '0'.
1792 * @returns {!MdPanelPosition}
1793 * @private
1794 */
1795MdPanelPosition.prototype._setPosition = function(position, value) {
1796 if (position === MdPanelPosition.absPosition.RIGHT ||
1797 position === MdPanelPosition.absPosition.LEFT) {
1798 this._left = this._right = '';
1799 } else if (
1800 position === MdPanelPosition.absPosition.BOTTOM ||
1801 position === MdPanelPosition.absPosition.TOP) {
1802 this._top = this._bottom = '';
1803 } else {
1804 var positions = Object.keys(MdPanelPosition.absPosition).join()
1805 .toLowerCase();
1806
1807 throw new Error('Position must be one of ' + positions + '.');
1808 }
1809
1810 this['_' + position] = angular.isString(value) ? value : '0';
1811
1812 return this;
1813};
1814
1815
1816/**
1817 * Sets the value of `top` for the panel. Clears any previously set vertical
1818 * position.
1819 * @param {string=} top Value of `top`. Defaults to '0'.
1820 * @returns {!MdPanelPosition}
1821 */
1822MdPanelPosition.prototype.top = function(top) {
1823 return this._setPosition(MdPanelPosition.absPosition.TOP, top);
1824};
1825
1826
1827/**
1828 * Sets the value of `bottom` for the panel. Clears any previously set vertical
1829 * position.
1830 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
1831 * @returns {!MdPanelPosition}
1832 */
1833MdPanelPosition.prototype.bottom = function(bottom) {
1834 return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom);
1835};
1836
1837
1838/**
1839 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
1840 * `rtl`. Clears any previously set horizontal position.
1841 * @param {string=} start Value of position. Defaults to '0'.
1842 * @returns {!MdPanelPosition}
1843 */
1844MdPanelPosition.prototype.start = function(start) {
1845 var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT;
1846 return this._setPosition(position, start);
1847};
1848
1849
1850/**
1851 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
1852 * Clears any previously set horizontal position.
1853 * @param {string=} end Value of position. Defaults to '0'.
1854 * @returns {!MdPanelPosition}
1855 */
1856MdPanelPosition.prototype.end = function(end) {
1857 var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT;
1858 return this._setPosition(position, end);
1859};
1860
1861
1862/**
1863 * Sets the value of `left` for the panel. Clears any previously set
1864 * horizontal position.
1865 * @param {string=} left Value of `left`. Defaults to '0'.
1866 * @returns {!MdPanelPosition}
1867 */
1868MdPanelPosition.prototype.left = function(left) {
1869 return this._setPosition(MdPanelPosition.absPosition.LEFT, left);
1870};
1871
1872
1873/**
1874 * Sets the value of `right` for the panel. Clears any previously set
1875 * horizontal position.
1876 * @param {string=} right Value of `right`. Defaults to '0'.
1877 * @returns {!MdPanelPosition}
1878*/
1879MdPanelPosition.prototype.right = function(right) {
1880 return this._setPosition(MdPanelPosition.absPosition.RIGHT, right);
1881};
1882
1883
1884/**
1885 * Centers the panel horizontally in the viewport. Clears any previously set
1886 * horizontal position.
1887 * @returns {!MdPanelPosition}
1888 */
1889MdPanelPosition.prototype.centerHorizontally = function() {
1890 this._left = '50%';
1891 this._right = '';
1892 this._translateX = ['-50%'];
1893 return this;
1894};
1895
1896
1897/**
1898 * Centers the panel vertically in the viewport. Clears any previously set
1899 * vertical position.
1900 * @returns {!MdPanelPosition}
1901 */
1902MdPanelPosition.prototype.centerVertically = function() {
1903 this._top = '50%';
1904 this._bottom = '';
1905 this._translateY = ['-50%'];
1906 return this;
1907};
1908
1909
1910/**
1911 * Centers the panel horizontally and vertically in the viewport. This is
1912 * equivalent to calling both `centerHorizontally` and `centerVertically`.
1913 * Clears any previously set horizontal and vertical positions.
1914 * @returns {!MdPanelPosition}
1915 */
1916MdPanelPosition.prototype.center = function() {
1917 return this.centerHorizontally().centerVertically();
1918};
1919
1920
1921/**
1922 * Sets element for relative positioning.
1923 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
1924 * or angular element to set the panel relative to.
1925 * @returns {!MdPanelPosition}
1926 */
1927MdPanelPosition.prototype.relativeTo = function(element) {
1928 this._absolute = false;
1929 this._relativeToEl = getElement(element);
1930 return this;
1931};
1932
1933
1934/**
1935 * Sets the x and y positions for the panel relative to another element.
1936 * @param {string} xPosition must be one of the MdPanelPosition.xPosition
1937 * values.
1938 * @param {string} yPosition must be one of the MdPanelPosition.yPosition
1939 * values.
1940 * @returns {!MdPanelPosition}
1941 */
1942MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) {
1943 if (!this._relativeToEl) {
1944 throw new Error('addPanelPosition can only be used with relative ' +
1945 'positioning. Set relativeTo first.');
1946 }
1947
1948 this._validateXPosition(xPosition);
1949 this._validateYPosition(yPosition);
1950
1951 this._positions.push({
1952 x: xPosition,
1953 y: yPosition,
1954 });
1955 return this;
1956};
1957
1958
1959/**
1960 * Ensures that yPosition is a valid position name. Throw an exception if not.
1961 * @param {string} yPosition
1962 */
1963MdPanelPosition.prototype._validateYPosition = function(yPosition) {
1964 // empty is ok
1965 if (yPosition == null) {
1966 return;
1967 }
1968
1969 var positionKeys = Object.keys(MdPanelPosition.yPosition);
1970 var positionValues = [];
1971 for (var key, i = 0; key = positionKeys[i]; i++) {
1972 var position = MdPanelPosition.yPosition[key];
1973 positionValues.push(position);
1974
1975 if (position === yPosition) {
1976 return;
1977 }
1978 }
1979
1980 throw new Error('Panel y position only accepts the following values:\n' +
1981 positionValues.join(' | '));
1982};
1983
1984
1985/**
1986 * Ensures that xPosition is a valid position name. Throw an exception if not.
1987 * @param {string} xPosition
1988 */
1989MdPanelPosition.prototype._validateXPosition = function(xPosition) {
1990 // empty is ok
1991 if (xPosition == null) {
1992 return;
1993 }
1994
1995 var positionKeys = Object.keys(MdPanelPosition.xPosition);
1996 var positionValues = [];
1997 for (var key, i = 0; key = positionKeys[i]; i++) {
1998 var position = MdPanelPosition.xPosition[key];
1999 positionValues.push(position);
2000 if (position === xPosition) {
2001 return;
2002 }
2003 }
2004
2005 throw new Error('Panel x Position only accepts the following values:\n' +
2006 positionValues.join(' | '));
2007};
2008
2009
2010/**
2011 * Sets the value of the offset in the x-direction. This will add to any
2012 * previously set offsets.
2013 * @param {string} offsetX
2014 * @returns {!MdPanelPosition}
2015 */
2016MdPanelPosition.prototype.withOffsetX = function(offsetX) {
2017 this._translateX.push(offsetX);
2018 return this;
2019};
2020
2021
2022/**
2023 * Sets the value of the offset in the y-direction. This will add to any
2024 * previously set offsets.
2025 * @param {string} offsetY
2026 * @returns {!MdPanelPosition}
2027 */
2028MdPanelPosition.prototype.withOffsetY = function(offsetY) {
2029 this._translateY.push(offsetY);
2030 return this;
2031};
2032
2033
2034/**
2035 * Gets the value of `top` for the panel.
2036 * @returns {string}
2037 */
2038MdPanelPosition.prototype.getTop = function() {
2039 return this._top;
2040};
2041
2042
2043/**
2044 * Gets the value of `bottom` for the panel.
2045 * @returns {string}
2046 */
2047MdPanelPosition.prototype.getBottom = function() {
2048 return this._bottom;
2049};
2050
2051
2052/**
2053 * Gets the value of `left` for the panel.
2054 * @returns {string}
2055 */
2056MdPanelPosition.prototype.getLeft = function() {
2057 return this._left;
2058};
2059
2060
2061/**
2062 * Gets the value of `right` for the panel.
2063 * @returns {string}
2064 */
2065MdPanelPosition.prototype.getRight = function() {
2066 return this._right;
2067};
2068
2069
2070/**
2071 * Gets the value of `transform` for the panel.
2072 * @returns {string}
2073 */
2074MdPanelPosition.prototype.getTransform = function() {
2075 var translateX = this._reduceTranslateValues('translateX', this._translateX);
2076 var translateY = this._reduceTranslateValues('translateY', this._translateY);
2077
2078 // It's important to trim the result, because the browser will ignore the set
2079 // operation if the string contains only whitespace.
2080 return (translateX + ' ' + translateY).trim();
2081};
2082
2083/**
2084 * True if the panel is completely on-screen with this positioning; false
2085 * otherwise.
2086 * @param {!angular.JQLite} panelEl
2087 * @return {boolean}
2088 */
2089MdPanelPosition.prototype._isOnscreen = function(panelEl) {
2090 // this works because we always use fixed positioning for the panel,
2091 // which is relative to the viewport.
2092 // TODO(gmoothart): take into account _translateX and _translateY to the
2093 // extent feasible.
2094
2095 var left = parseInt(this.getLeft());
2096 var top = parseInt(this.getTop());
2097 var right = left + panelEl[0].offsetWidth;
2098 var bottom = top + panelEl[0].offsetHeight;
2099
2100 return (left >= 0) &&
2101 (top >= 0) &&
2102 (bottom <= this._$window.innerHeight) &&
2103 (right <= this._$window.innerWidth);
2104};
2105
2106
2107/**
2108 * Gets the first x/y position that can fit on-screen.
2109 * @returns {{x: string, y: string}}
2110 */
2111MdPanelPosition.prototype.getActualPosition = function() {
2112 return this._actualPosition;
2113};
2114
2115
2116/**
2117 * Reduces a list of translate values to a string that can be used within
2118 * transform.
2119 * @param {string} translateFn
2120 * @param {!Array<string>} values
2121 * @returns {string}
2122 * @private
2123 */
2124MdPanelPosition.prototype._reduceTranslateValues =
2125 function(translateFn, values) {
2126 return values.map(function(translation) {
2127 return translateFn + '(' + translation + ')';
2128 }).join(' ');
2129 };
2130
2131
2132/**
2133 * Sets the panel position based on the created panel element and best x/y
2134 * positioning.
2135 * @param {!angular.JQLite} panelEl
2136 * @private
2137 */
2138MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
2139 // Only calculate the position if necessary.
2140 if (this._absolute) {
2141 return;
2142 }
2143
2144 if (this._actualPosition) {
2145 this._calculatePanelPosition(panelEl, this._actualPosition);
2146 return;
2147 }
2148
2149 for (var i = 0; i < this._positions.length; i++) {
2150 this._actualPosition = this._positions[i];
2151 this._calculatePanelPosition(panelEl, this._actualPosition);
2152 if (this._isOnscreen(panelEl)) {
2153 break;
2154 }
2155 }
2156};
2157
2158
2159/**
2160 * Switches between 'start' and 'end'.
2161 * @param {string} position Horizontal position of the panel
2162 * @returns {string} Reversed position
2163 * @private
2164 */
2165MdPanelPosition.prototype._reverseXPosition = function(position) {
2166 if (position === MdPanelPosition.xPosition.CENTER) {
2167 return;
2168 }
2169
2170 var start = 'start';
2171 var end = 'end';
2172
2173 return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start);
2174};
2175
2176
2177/**
2178 * Handles horizontal positioning in rtl or ltr environments.
2179 * @param {string} position Horizontal position of the panel
2180 * @returns {string} The correct position according the page direction
2181 * @private
2182 */
2183MdPanelPosition.prototype._bidi = function(position) {
2184 return this._isRTL ? this._reverseXPosition(position) : position;
2185};
2186
2187
2188/**
2189 * Calculates the panel position based on the created panel element and the
2190 * provided positioning.
2191 * @param {!angular.JQLite} panelEl
2192 * @param {!{x:string, y:string}} position
2193 * @private
2194 */
2195MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) {
2196
2197 var panelBounds = panelEl[0].getBoundingClientRect();
2198 var panelWidth = panelBounds.width;
2199 var panelHeight = panelBounds.height;
2200
2201 var targetBounds = this._relativeToEl[0].getBoundingClientRect();
2202
2203 var targetLeft = targetBounds.left;
2204 var targetRight = targetBounds.right;
2205 var targetWidth = targetBounds.width;
2206
2207 switch (this._bidi(position.x)) {
2208 case MdPanelPosition.xPosition.OFFSET_START:
2209 this._left = targetLeft - panelWidth + 'px';
2210 break;
2211 case MdPanelPosition.xPosition.ALIGN_END:
2212 this._left = targetRight - panelWidth + 'px';
2213 break;
2214 case MdPanelPosition.xPosition.CENTER:
2215 var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth);
2216 this._left = left + 'px';
2217 break;
2218 case MdPanelPosition.xPosition.ALIGN_START:
2219 this._left = targetLeft + 'px';
2220 break;
2221 case MdPanelPosition.xPosition.OFFSET_END:
2222 this._left = targetRight + 'px';
2223 break;
2224 }
2225
2226 var targetTop = targetBounds.top;
2227 var targetBottom = targetBounds.bottom;
2228 var targetHeight = targetBounds.height;
2229
2230 switch (position.y) {
2231 case MdPanelPosition.yPosition.ABOVE:
2232 this._top = targetTop - panelHeight + 'px';
2233 break;
2234 case MdPanelPosition.yPosition.ALIGN_BOTTOMS:
2235 this._top = targetBottom - panelHeight + 'px';
2236 break;
2237 case MdPanelPosition.yPosition.CENTER:
2238 var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight);
2239 this._top = top + 'px';
2240 break;
2241 case MdPanelPosition.yPosition.ALIGN_TOPS:
2242 this._top = targetTop + 'px';
2243 break;
2244 case MdPanelPosition.yPosition.BELOW:
2245 this._top = targetBottom + 'px';
2246 break;
2247 }
2248};
2249
2250
2251/*****************************************************************************
2252 * MdPanelAnimation *
2253 *****************************************************************************/
2254
2255
2256/**
2257 * Animation configuration object. To use, create an MdPanelAnimation with the
2258 * desired properties, then pass the object as part of $mdPanel creation.
2259 *
2260 * Example:
2261 *
2262 * var panelAnimation = new MdPanelAnimation()
2263 * .openFrom(myButtonEl)
2264 * .closeTo('.my-button')
2265 * .withAnimation($mdPanel.animation.SCALE);
2266 *
2267 * $mdPanel.create({
2268 * animation: panelAnimation
2269 * });
2270 *
2271 * @param {!angular.$injector} $injector
2272 * @final @constructor
2273 */
2274function MdPanelAnimation($injector) {
2275 /** @private @const {!angular.$mdUtil} */
2276 this._$mdUtil = $injector.get('$mdUtil');
2277
2278 /**
2279 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
2280 * undefined}
2281 */
2282 this._openFrom;
2283
2284 /**
2285 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
2286 * undefined}
2287 */
2288 this._closeTo;
2289
2290 /** @private {string|{open: string, close: string}} */
2291 this._animationClass = '';
2292}
2293
2294
2295/**
2296 * Possible default animations.
2297 * @enum {string}
2298 */
2299MdPanelAnimation.animation = {
2300 SLIDE: 'md-panel-animate-slide',
2301 SCALE: 'md-panel-animate-scale',
2302 FADE: 'md-panel-animate-fade'
2303};
2304
2305
2306/**
2307 * Specifies where to start the open animation. `openFrom` accepts a
2308 * click event object, query selector, DOM element, or a Rect object that
2309 * is used to determine the bounds. When passed a click event, the location
2310 * of the click will be used as the position to start the animation.
2311 * @param {string|!Element|!Event|{top: number, left: number}} openFrom
2312 * @returns {!MdPanelAnimation}
2313 */
2314MdPanelAnimation.prototype.openFrom = function(openFrom) {
2315 // Check if 'openFrom' is an Event.
2316 openFrom = openFrom.target ? openFrom.target : openFrom;
2317
2318 this._openFrom = this._getPanelAnimationTarget(openFrom);
2319
2320 if (!this._closeTo) {
2321 this._closeTo = this._openFrom;
2322 }
2323 return this;
2324};
2325
2326
2327/**
2328 * Specifies where to animate the panel close. `closeTo` accepts a
2329 * query selector, DOM element, or a Rect object that is used to determine
2330 * the bounds.
2331 * @param {string|!Element|{top: number, left: number}} closeTo
2332 * @returns {!MdPanelAnimation}
2333 */
2334MdPanelAnimation.prototype.closeTo = function(closeTo) {
2335 this._closeTo = this._getPanelAnimationTarget(closeTo);
2336 return this;
2337};
2338
2339
2340/**
2341 * Returns the element and bounds for the animation target.
2342 * @param {string|!Element|{top: number, left: number}} location
2343 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
2344 * @private
2345 */
2346MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) {
2347 if (angular.isDefined(location.top) || angular.isDefined(location.left)) {
2348 return {
2349 element: undefined,
2350 bounds: {
2351 top: location.top || 0,
2352 left: location.left || 0
2353 }
2354 };
2355 } else {
2356 return this._getBoundingClientRect(getElement(location));
2357 }
2358};
2359
2360
2361/**
2362 * Specifies the animation class.
2363 *
2364 * There are several default animations that can be used:
2365 * (MdPanelAnimation.animation)
2366 * SLIDE: The panel slides in and out from the specified
2367 * elements.
2368 * SCALE: The panel scales in and out.
2369 * FADE: The panel fades in and out.
2370 *
2371 * @param {string|{open: string, close: string}} cssClass
2372 * @returns {!MdPanelAnimation}
2373 */
2374MdPanelAnimation.prototype.withAnimation = function(cssClass) {
2375 this._animationClass = cssClass;
2376 return this;
2377};
2378
2379
2380/**
2381 * Animate the panel open.
2382 * @param {!angular.JQLite} panelEl
2383 * @returns {!angular.$q.Promise} A promise that is resolved when the open
2384 * animation is complete.
2385 */
2386MdPanelAnimation.prototype.animateOpen = function(panelEl) {
2387 var animator = this._$mdUtil.dom.animator;
2388
2389 this._fixBounds(panelEl);
2390 var animationOptions = {};
2391
2392 // Include the panel transformations when calculating the animations.
2393 var panelTransform = panelEl[0].style.transform || '';
2394
2395 var openFrom = animator.toTransformCss(panelTransform);
2396 var openTo = animator.toTransformCss(panelTransform);
2397
2398 switch (this._animationClass) {
2399 case MdPanelAnimation.animation.SLIDE:
2400 // Slide should start with opacity: 1.
2401 panelEl.css('opacity', '1');
2402
2403 animationOptions = {
2404 transitionInClass: '_md-panel-animate-enter'
2405 };
2406
2407 var openSlide = animator.calculateSlideToOrigin(
2408 panelEl, this._openFrom) || '';
2409 openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform);
2410 break;
2411
2412 case MdPanelAnimation.animation.SCALE:
2413 animationOptions = {
2414 transitionInClass: '_md-panel-animate-enter'
2415 };
2416
2417 var openScale = animator.calculateZoomToOrigin(
2418 panelEl, this._openFrom) || '';
2419 openFrom = animator.toTransformCss(openScale + ' ' + panelTransform);
2420 break;
2421
2422 case MdPanelAnimation.animation.FADE:
2423 animationOptions = {
2424 transitionInClass: '_md-panel-animate-enter'
2425 };
2426 break;
2427
2428 default:
2429 if (angular.isString(this._animationClass)) {
2430 animationOptions = {
2431 transitionInClass: this._animationClass
2432 };
2433 } else {
2434 animationOptions = {
2435 transitionInClass: this._animationClass['open'],
2436 transitionOutClass: this._animationClass['close'],
2437 };
2438 }
2439 }
2440
2441 return animator
2442 .translate3d(panelEl, openFrom, openTo, animationOptions);
2443};
2444
2445
2446/**
2447 * Animate the panel close.
2448 * @param {!angular.JQLite} panelEl
2449 * @returns {!angular.$q.Promise} A promise that resolves when the close
2450 * animation is complete.
2451 */
2452MdPanelAnimation.prototype.animateClose = function(panelEl) {
2453 var animator = this._$mdUtil.dom.animator;
2454 var reverseAnimationOptions = {};
2455
2456 // Include the panel transformations when calculating the animations.
2457 var panelTransform = panelEl[0].style.transform || '';
2458
2459 var closeFrom = animator.toTransformCss(panelTransform);
2460 var closeTo = animator.toTransformCss(panelTransform);
2461
2462 switch (this._animationClass) {
2463 case MdPanelAnimation.animation.SLIDE:
2464 // Slide should start with opacity: 1.
2465 panelEl.css('opacity', '1');
2466 reverseAnimationOptions = {
2467 transitionInClass: '_md-panel-animate-leave'
2468 };
2469
2470 var closeSlide = animator.calculateSlideToOrigin(
2471 panelEl, this._closeTo) || '';
2472 closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform);
2473 break;
2474
2475 case MdPanelAnimation.animation.SCALE:
2476 reverseAnimationOptions = {
2477 transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave'
2478 };
2479
2480 var closeScale = animator.calculateZoomToOrigin(
2481 panelEl, this._closeTo) || '';
2482 closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform);
2483 break;
2484
2485 case MdPanelAnimation.animation.FADE:
2486 reverseAnimationOptions = {
2487 transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave'
2488 };
2489 break;
2490
2491 default:
2492 if (angular.isString(this._animationClass)) {
2493 reverseAnimationOptions = {
2494 transitionOutClass: this._animationClass
2495 };
2496 } else {
2497 reverseAnimationOptions = {
2498 transitionInClass: this._animationClass['close'],
2499 transitionOutClass: this._animationClass['open']
2500 };
2501 }
2502 }
2503
2504 return animator
2505 .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions);
2506};
2507
2508
2509/**
2510 * Set the height and width to match the panel if not provided.
2511 * @param {!angular.JQLite} panelEl
2512 * @private
2513 */
2514MdPanelAnimation.prototype._fixBounds = function(panelEl) {
2515 var panelWidth = panelEl[0].offsetWidth;
2516 var panelHeight = panelEl[0].offsetHeight;
2517
2518 if (this._openFrom && this._openFrom.bounds.height == null) {
2519 this._openFrom.bounds.height = panelHeight;
2520 }
2521 if (this._openFrom && this._openFrom.bounds.width == null) {
2522 this._openFrom.bounds.width = panelWidth;
2523 }
2524 if (this._closeTo && this._closeTo.bounds.height == null) {
2525 this._closeTo.bounds.height = panelHeight;
2526 }
2527 if (this._closeTo && this._closeTo.bounds.width == null) {
2528 this._closeTo.bounds.width = panelWidth;
2529 }
2530};
2531
2532
2533/**
2534 * Identify the bounding RECT for the target element.
2535 * @param {!angular.JQLite} element
2536 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
2537 * @private
2538 */
2539MdPanelAnimation.prototype._getBoundingClientRect = function(element) {
2540 if (element instanceof angular.element) {
2541 return {
2542 element: element,
2543 bounds: element[0].getBoundingClientRect()
2544 };
2545 }
2546};
2547
2548
2549/*****************************************************************************
2550 * Util Methods *
2551 *****************************************************************************/
2552
2553/**
2554 * Returns the angular element associated with a css selector or element.
2555 * @param el {string|!angular.JQLite|!Element}
2556 * @returns {!angular.JQLite}
2557 */
2558function getElement(el) {
2559 var queryResult = angular.isString(el) ?
2560 document.querySelector(el) : el;
2561 return angular.element(queryResult);
2562}
2563
2564})(window, window.angular);