2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ngmaterial.components.dialog');
8 goog.require('ngmaterial.components.backdrop');
9 goog.require('ngmaterial.core');
12 * @name material.components.dialog
14 MdDialogDirective['$inject'] = ["$$rAF", "$mdTheming", "$mdDialog"];
15 MdDialogProvider['$inject'] = ["$$interimElementProvider"];
17 .module('material.components.dialog', [
19 'material.components.backdrop'
21 .directive('mdDialog', MdDialogDirective)
22 .provider('$mdDialog', MdDialogProvider);
27 * @module material.components.dialog
32 * `<md-dialog>` - The dialog's template must be inside this element.
34 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
35 * an `<md-dialog-actions>` element for the dialog's actions.
38 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
41 * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
42 * prefixed with `dialogContent_`.
47 * <md-dialog aria-label="List dialog">
50 * <md-list-item ng-repeat="item in items">
51 * <p>Number {{item}}</p>
54 * </md-dialog-content>
56 * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
57 * </md-dialog-actions>
61 function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
64 link: function(scope, element) {
65 element.addClass('_md'); // private md component indicator for styling
70 var content = element[0].querySelector('md-dialog-content');
73 images = content.getElementsByTagName('img');
75 //-- delayed image loading may impact scroll height, check after images are loaded
76 angular.element(images).on('load', addOverflowClass);
79 scope.$on('$destroy', function() {
80 $mdDialog.destroy(element);
86 function addOverflowClass() {
87 element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
99 * @module material.components.dialog
102 * `$mdDialog` opens a dialog over the app to inform users about critical information or require
103 * them to make decisions. There are two approaches for setup: a simple promise API
104 * and regular object syntax.
108 * - The dialog is always given an isolate scope.
109 * - The dialog's template must have an outer `<md-dialog>` element.
110 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
111 * an `<md-dialog-actions>` element for the dialog's actions.
112 * - Dialogs must cover the entire application to keep interactions inside of them.
113 * Use the `parent` option to change where dialogs are appended.
116 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
117 * - Default max-width is 80% of the `rootElement` or `parent`.
120 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
124 * <div ng-app="demoApp" ng-controller="EmployeeController">
126 * <md-button ng-click="showAlert()" class="md-raised md-warn">
131 * <md-button ng-click="showDialog($event)" class="md-raised">
136 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
141 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
148 * ### JavaScript: object syntax
150 * (function(angular, undefined){
154 * .module('demoApp', ['ngMaterial'])
155 * .controller('AppCtrl', AppController);
157 * function AppController($scope, $mdDialog) {
159 * $scope.showAlert = showAlert;
160 * $scope.showDialog = showDialog;
161 * $scope.items = [1, 2, 3];
164 * function showAlert() {
165 * alert = $mdDialog.alert({
166 * title: 'Attention',
167 * textContent: 'This is an example of how easy dialogs can be!',
173 * .finally(function() {
178 * function showDialog($event) {
179 * var parentEl = angular.element(document.body);
182 * targetEvent: $event,
184 * '<md-dialog aria-label="List dialog">' +
185 * ' <md-dialog-content>'+
187 * ' <md-list-item ng-repeat="item in items">'+
188 * ' <p>Number {{item}}</p>' +
191 * ' </md-dialog-content>' +
192 * ' <md-dialog-actions>' +
193 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
196 * ' </md-dialog-actions>' +
199 * items: $scope.items
201 * controller: DialogController
203 * function DialogController($scope, $mdDialog, items) {
204 * $scope.items = items;
205 * $scope.closeDialog = function() {
214 * ### Multiple Dialogs
215 * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs
219 * // From plain options
224 * // From a dialog preset
233 * ### Pre-Rendered Dialogs
234 * By using the `contentElement` option, it is possible to use an already existing element in the DOM.
236 * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.<br/>
237 * > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`)
240 * $scope.showPrerenderedDialog = function() {
242 * contentElement: '#myStaticDialog',
243 * parent: angular.element(document.body)
248 * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
251 * <div style="visibility: hidden">
252 * <div class="md-dialog-container" id="myStaticDialog">
254 * This is a pre-rendered dialog.
260 * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
263 * It also possible to use a DOM element for the `contentElement` option.
264 * - `contentElement: document.querySelector('#myStaticDialog')`
265 * - `contentElement: angular.element(TEMPLATE)`
267 * When using a `template` as content element, it will be not compiled upon open.
268 * This allows you to compile the element yourself and use it each time the dialog opens.
271 * Developers are also able to create their own preset, which can be easily used without repeating
272 * their options each time.
275 * $mdDialogProvider.addPreset('testPreset', {
276 * options: function() {
280 * 'This is a custom preset' +
282 * controllerAs: 'dialog',
283 * bindToController: true,
284 * clickOutsideToClose: true,
285 * escapeToClose: true
291 * After you created your preset at config phase, you can easily access it.
295 * $mdDialog.testPreset()
299 * ### JavaScript: promise API syntax, custom dialog template
301 * (function(angular, undefined){
305 * .module('demoApp', ['ngMaterial'])
306 * .controller('EmployeeController', EmployeeEditor)
307 * .controller('GreetingController', GreetingController);
309 * // Fictitious Employee Editor to show how to use simple and complex dialogs.
311 * function EmployeeEditor($scope, $mdDialog) {
314 * $scope.showAlert = showAlert;
315 * $scope.closeAlert = closeAlert;
316 * $scope.showGreeting = showCustomGreeting;
318 * $scope.hasAlert = function() { return !!alert };
319 * $scope.userName = $scope.userName || 'Bobby';
321 * // Dialog #1 - Show simple alert dialog and cache
322 * // reference to dialog instance
324 * function showAlert() {
325 * alert = $mdDialog.alert()
326 * .title('Attention, ' + $scope.userName)
327 * .textContent('This is an example of how easy dialogs can be!')
332 * .finally(function() {
337 * // Close the specified dialog instance and resolve with 'finished' flag
338 * // Normally this is not needed, just use '$mdDialog.hide()' to close
339 * // the most recent dialog popup.
341 * function closeAlert() {
342 * $mdDialog.hide( alert, "finished" );
346 * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
348 * function showCustomGreeting($event) {
350 * targetEvent: $event,
354 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
356 * ' <md-dialog-actions>' +
357 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
358 * ' Close Greeting' +
360 * ' </md-dialog-actions>' +
362 * controller: 'GreetingController',
363 * onComplete: afterShowAnimation,
364 * locals: { employee: $scope.userName }
367 * // When the 'enter' animation finishes...
369 * function afterShowAnimation(scope, element, options) {
370 * // post-show code here: DOM element focus, etc.
374 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
375 * // Here we used ng-controller="GreetingController as vm" and
376 * // $scope.vm === <controller instance>
378 * function showCustomGreeting() {
381 * clickOutsideToClose: true,
383 * scope: $scope, // use parent scope in template
384 * preserveScope: true, // do not forget this if use parent scope
386 * // Since GreetingController is instantiated with ControllerAs syntax
387 * // AND we are passing the parent '$scope' to the dialog, we MUST
388 * // use 'vm.<xxx>' in the template markup
390 * template: '<md-dialog>' +
391 * ' <md-dialog-content>' +
392 * ' Hi There {{vm.employee}}' +
393 * ' </md-dialog-content>' +
396 * controller: function DialogController($scope, $mdDialog) {
397 * $scope.closeDialog = function() {
406 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
408 * function GreetingController($scope, $mdDialog, employee) {
409 * // Assigned from construction <code>locals</code> options...
410 * $scope.employee = employee;
412 * $scope.closeDialog = function() {
413 * // Easily hides most recent dialog shown...
414 * // no specific instance reference is needed.
425 * @name $mdDialog#alert
428 * Builds a preconfigured dialog with the specified message.
430 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
432 * - $mdDialogPreset#title(string) - Sets the alert title.
433 * - $mdDialogPreset#textContent(string) - Sets the alert message.
434 * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize
435 * module to be loaded. HTML is not run through Angular's compiler.
436 * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text.
437 * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog.
438 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
439 * the location of the click will be used as the starting point for the opening animation
446 * @name $mdDialog#confirm
449 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
450 * will be resolved only if the user clicks the confirm action on the dialog.
452 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
454 * Additionally, it supports the following methods:
456 * - $mdDialogPreset#title(string) - Sets the confirm title.
457 * - $mdDialogPreset#textContent(string) - Sets the confirm message.
458 * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize
459 * module to be loaded. HTML is not run through Angular's compiler.
460 * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text.
461 * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text.
462 * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog.
463 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
464 * the location of the click will be used as the starting point for the opening animation
471 * @name $mdDialog#prompt
474 * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned
475 * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument.
477 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
479 * Additionally, it supports the following methods:
481 * - $mdDialogPreset#title(string) - Sets the prompt title.
482 * - $mdDialogPreset#textContent(string) - Sets the prompt message.
483 * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize
484 * module to be loaded. HTML is not run through Angular's compiler.
485 * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input.
486 * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input.
487 * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text.
488 * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text.
489 * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog.
490 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
491 * the location of the click will be used as the starting point for the opening animation
498 * @name $mdDialog#show
501 * Show a dialog with the specified options.
503 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
504 * `confirm()`, or an options object with the following properties:
505 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
507 * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
508 * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
509 * This template should **never** be constructed with any kind of user input or user data.
510 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
511 * dialog opens, you can also use a DOM element.<br/>
512 * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
513 * the dialog and restores it at the old DOM position upon close.
514 * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
515 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
516 * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
517 * custom dialog directive.
518 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
519 * the location of the click will be used as the starting point for the opening animation
521 * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object
522 * that is used to determine the bounds (top, left, height, width) from which the Dialog will
524 * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object
525 * that is used to determine the bounds (top, left, height, width) to which the Dialog will
527 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
528 * it will create a new isolate scope.
529 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
530 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
531 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
533 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
535 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
536 * close it. Default false.
537 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
539 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
540 * focusing some other way, as focus management is required for dialogs to be accessible.
542 * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller
543 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
544 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
545 * of values to inject into the controller. For example, `locals: {three: 3}` would inject
546 * `three` into the controller, with the value 3. If `bindToController` is true, they will be
547 * copied to the controller instead.
548 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
549 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
550 * dialog will not open until all of the promises resolve.
551 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
552 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
553 * to the root element of the application.
554 * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is
556 * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is
558 * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the
559 * close/hide() action is starting. This allows developers to run custom animations
560 * in parallel the close animations.
561 * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen
562 * or not. Defaults to `false`.
563 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
564 * rejected with `$mdDialog.cancel()`.
569 * @name $mdDialog#hide
572 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
574 * @param {*=} response An argument for the resolved promise.
576 * @returns {promise} A promise that is resolved when the dialog has been closed.
581 * @name $mdDialog#cancel
584 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
586 * @param {*=} response An argument for the rejected promise.
588 * @returns {promise} A promise that is resolved when the dialog has been closed.
591 function MdDialogProvider($$interimElementProvider) {
592 // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
593 advancedDialogOptions['$inject'] = ["$mdDialog", "$mdConstant"];
594 dialogDefaultOptions['$inject'] = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"];
595 var topFocusTrap, bottomFocusTrap;
597 return $$interimElementProvider('$mdDialog')
599 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
600 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'],
601 options: dialogDefaultOptions
603 .addPreset('alert', {
604 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme',
606 options: advancedDialogOptions
608 .addPreset('confirm', {
609 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel',
611 options: advancedDialogOptions
613 .addPreset('prompt', {
614 methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel',
615 'ok', 'cancel', 'theme', 'css'],
616 options: advancedDialogOptions
620 function advancedDialogOptions($mdDialog, $mdConstant) {
623 '<md-dialog md-theme="{{ dialog.theme || dialog.defaultTheme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
624 ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
625 ' <h2 class="md-title">{{ dialog.title }}</h2>',
626 ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
627 ' ng-bind-html="::dialog.mdHtmlContent"></div>',
628 ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
629 ' <p>{{::dialog.mdTextContent}}</p>',
631 ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
632 ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' +
633 ' placeholder="{{::dialog.placeholder}}">',
634 ' </md-input-container>',
635 ' </md-dialog-content>',
636 ' <md-dialog-actions>',
637 ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
638 ' ng-click="dialog.abort()" class="md-primary md-cancel-button">',
639 ' {{ dialog.cancel }}',
641 ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'">',
644 ' </md-dialog-actions>',
646 ].join('').replace(/\s\s+/g, ''),
647 controller: function mdDialogCtrl() {
648 var isPrompt = this.$type == 'prompt';
650 if (isPrompt && this.initialValue) {
651 this.result = this.initialValue;
654 this.hide = function() {
655 $mdDialog.hide(isPrompt ? this.result : true);
657 this.abort = function() {
660 this.keypress = function($event) {
661 if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
662 $mdDialog.hide(this.result);
666 controllerAs: 'dialog',
667 bindToController: true,
672 function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement,
673 $log, $injector, $mdTheming, $interpolate, $mdInteraction) {
678 onCompiling: beforeCompile,
680 onShowing: beforeShow,
682 clickOutsideToClose: false,
688 disableParentScroll: true,
691 transformTemplate: function(template, options) {
692 // Make the dialog container focusable, because otherwise the focus will be always redirected to
693 // an element outside of the container, and the focus trap won't work probably..
694 // Also the tabindex is needed for the `escapeToClose` functionality, because
695 // the keyDown event can't be triggered when the focus is outside of the container.
696 var startSymbol = $interpolate.startSymbol();
697 var endSymbol = $interpolate.endSymbol();
698 var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol;
699 return '<div class="md-dialog-container" tabindex="-1" md-theme="' + theme + '">' + validatedTemplate(template) + '</div>';
702 * The specified template should contain a <md-dialog> wrapper element....
704 function validatedTemplate(template) {
705 if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
706 return '<md-dialog>' + (template || '') + '</md-dialog>';
708 return template || '';
714 function beforeCompile(options) {
715 // Automatically apply the theme, if the user didn't specify a theme explicitly.
716 // Those option changes need to be done, before the compilation has started, because otherwise
717 // the option changes will be not available in the $mdCompilers locales.
718 options.defaultTheme = $mdTheming.defaultTheme();
720 detectTheming(options);
723 function beforeShow(scope, element, options, controller) {
726 var mdHtmlContent = controller.htmlContent || options.htmlContent || '';
727 var mdTextContent = controller.textContent || options.textContent ||
728 controller.content || options.content || '';
730 if (mdHtmlContent && !$injector.has('$sanitize')) {
731 throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
734 if (mdHtmlContent && mdTextContent) {
735 throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
738 // Only assign the content if nothing throws, otherwise it'll still be compiled.
739 controller.mdHtmlContent = mdHtmlContent;
740 controller.mdTextContent = mdTextContent;
744 /** Show method for dialogs */
745 function onShow(scope, element, options, controller) {
746 angular.element($document[0].body).addClass('md-dialog-is-showing');
748 var dialogElement = element.find('md-dialog');
750 // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly.
751 // This is a very common problem, so we have to notify the developer about this.
752 if (dialogElement.hasClass('ng-cloak')) {
753 var message = '$mdDialog: using `<md-dialog ng-cloak>` will affect the dialog opening animations.';
754 $log.warn( message, element[0] );
757 captureParentAndFromToElements(options);
758 configureAria(dialogElement, options);
759 showBackdrop(scope, element, options);
760 activateListeners(element, options);
762 return dialogPopIn(element, options)
764 lockScreenReader(element, options);
765 warnDeprecatedActions();
770 * Check to see if they used the deprecated .md-actions class and log a warning
772 function warnDeprecatedActions() {
773 if (element[0].querySelector('.md-actions')) {
774 $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.');
779 * For alerts, focus on content... otherwise focus on
780 * the close button (or equivalent)
782 function focusOnOpen() {
783 if (options.focusOnOpen) {
784 var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement;
789 * If no element with class dialog-close, try to find the last
790 * button child in md-actions and assume it is a close button.
792 * If we find no actions at all, log a warning to the console.
794 function findCloseButton() {
795 return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child');
801 * Remove function for all dialogs
803 function onRemove(scope, element, options) {
804 options.deactivateListeners();
805 options.unlockScreenReader();
806 options.hideBackdrop(options.$destroy);
808 // Remove the focus traps that we added earlier for keeping focus within the dialog.
809 if (topFocusTrap && topFocusTrap.parentNode) {
810 topFocusTrap.parentNode.removeChild(topFocusTrap);
813 if (bottomFocusTrap && bottomFocusTrap.parentNode) {
814 bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
817 // For navigation $destroy events, do a quick, non-animated removal,
818 // but for normal closes (from clicks, etc) animate the removal
819 return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
822 * For normal closes, animate the removal.
823 * For forced closes (like $destroy events), skip the animations
825 function animateRemoval() {
826 return dialogPopOut(element, options);
832 function detachAndClean() {
833 angular.element($document[0].body).removeClass('md-dialog-is-showing');
835 // Reverse the container stretch if using a content element.
836 if (options.contentElement) {
837 options.reverseContainerStretch();
840 // Exposed cleanup function from the $mdCompiler.
841 options.cleanupElement();
843 // Restores the focus to the origin element if the last interaction upon opening was a keyboard.
844 if (!options.$destroy && options.originInteraction === 'keyboard') {
845 options.origin.focus();
850 function detectTheming(options) {
851 // Once the user specifies a targetEvent, we will automatically try to find the correct
854 if (options.targetEvent && options.targetEvent.target) {
855 targetEl = angular.element(options.targetEvent.target);
858 var themeCtrl = targetEl && targetEl.controller('mdTheme');
864 options.themeWatch = themeCtrl.$shouldWatch;
866 var theme = options.theme || themeCtrl.$mdTheme;
869 options.scope.theme = theme;
872 var unwatch = themeCtrl.registerChanges(function (newTheme) {
873 options.scope.theme = newTheme;
875 if (!options.themeWatch) {
882 * Capture originator/trigger/from/to element information (if available)
883 * and the parent container for the dialog; defaults to the $rootElement
884 * unless overridden in the options.parent
886 function captureParentAndFromToElements(options) {
887 options.origin = angular.extend({
891 }, options.origin || {});
893 options.parent = getDomElement(options.parent, $rootElement);
894 options.closeTo = getBoundingClientRect(getDomElement(options.closeTo));
895 options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
897 if ( options.targetEvent ) {
898 options.origin = getBoundingClientRect(options.targetEvent.target, options.origin);
899 options.originInteraction = $mdInteraction.getLastInteractionType();
904 * Identify the bounding RECT for the target element
907 function getBoundingClientRect (element, orig) {
908 var source = angular.element((element || {}));
909 if (source && source.length) {
910 // Compute and save the target element's bounding rect, so that if the
911 // element is hidden when the dialog closes, we can shrink the dialog
912 // back to the same position it expanded from.
914 // Checking if the source is a rect object or a DOM element
915 var bounds = {top:0,left:0,height:0,width:0};
916 var hasFn = angular.isFunction(source[0].getBoundingClientRect);
918 return angular.extend(orig || {}, {
919 element : hasFn ? source : undefined,
920 bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]),
921 focus : angular.bind(source, source.focus),
927 * If the specifier is a simple string selector, then query for
930 function getDomElement(element, defaultElement) {
931 if (angular.isString(element)) {
932 element = $document[0].querySelector(element);
935 // If we have a reference to a raw dom element, always wrap it in jqLite
936 return angular.element(element || defaultElement);
942 * Listen for escape keys and outside clicks to auto close
944 function activateListeners(element, options) {
945 var window = angular.element($window);
946 var onWindowResize = $mdUtil.debounce(function() {
947 stretchDialogContainerToViewport(element, options);
950 var removeListeners = [];
951 var smartClose = function() {
952 // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
953 // cancel or fallback to hide.
954 var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
955 $mdUtil.nextTick(closeFn, true);
958 if (options.escapeToClose) {
959 var parentTarget = options.parent;
960 var keyHandlerFn = function(ev) {
961 if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
962 ev.stopPropagation();
969 // Add keydown listeners
970 element.on('keydown', keyHandlerFn);
971 parentTarget.on('keydown', keyHandlerFn);
973 // Queue remove listeners function
974 removeListeners.push(function() {
976 element.off('keydown', keyHandlerFn);
977 parentTarget.off('keydown', keyHandlerFn);
982 // Register listener to update dialog on window resize
983 window.on('resize', onWindowResize);
985 removeListeners.push(function() {
986 window.off('resize', onWindowResize);
989 if (options.clickOutsideToClose) {
990 var target = element;
993 // Keep track of the element on which the mouse originally went down
994 // so that we can only close the backdrop when the 'click' started on it.
995 // A simple 'click' handler does not work,
996 // it sets the target object as the element the mouse went down on.
997 var mousedownHandler = function(ev) {
998 sourceElem = ev.target;
1001 // We check if our original element and the target is the backdrop
1002 // because if the original was the backdrop and the target was inside the dialog
1003 // we don't want to dialog to close.
1004 var mouseupHandler = function(ev) {
1005 if (sourceElem === target[0] && ev.target === target[0]) {
1006 ev.stopPropagation();
1007 ev.preventDefault();
1014 target.on('mousedown', mousedownHandler);
1015 target.on('mouseup', mouseupHandler);
1017 // Queue remove listeners function
1018 removeListeners.push(function() {
1019 target.off('mousedown', mousedownHandler);
1020 target.off('mouseup', mouseupHandler);
1024 // Attach specific `remove` listener handler
1025 options.deactivateListeners = function() {
1026 removeListeners.forEach(function(removeFn) {
1029 options.deactivateListeners = null;
1034 * Show modal backdrop element...
1036 function showBackdrop(scope, element, options) {
1038 if (options.disableParentScroll) {
1039 // !! DO this before creating the backdrop; since disableScrollAround()
1040 // configures the scroll offset; which is used by mdBackDrop postLink()
1041 options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
1044 if (options.hasBackdrop) {
1045 options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
1046 $animate.enter(options.backdrop, options.parent);
1050 * Hide modal backdrop element...
1052 options.hideBackdrop = function hideBackdrop($destroy) {
1053 if (options.backdrop) {
1054 if ( !!$destroy ) options.backdrop.remove();
1055 else $animate.leave(options.backdrop);
1059 if (options.disableParentScroll) {
1060 options.restoreScroll && options.restoreScroll();
1061 delete options.restoreScroll;
1064 options.hideBackdrop = null;
1069 * Inject ARIA-specific attributes appropriate for Dialogs
1071 function configureAria(element, options) {
1073 var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
1074 var dialogContent = element.find('md-dialog-content');
1075 var existingDialogId = element.attr('id');
1076 var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid());
1083 if (dialogContent.length === 0) {
1084 dialogContent = element;
1085 // If the dialog element already had an ID, don't clobber it.
1086 if (existingDialogId) {
1087 dialogContentId = existingDialogId;
1091 dialogContent.attr('id', dialogContentId);
1092 element.attr('aria-describedby', dialogContentId);
1094 if (options.ariaLabel) {
1095 $mdAria.expect(element, 'aria-label', options.ariaLabel);
1098 $mdAria.expectAsync(element, 'aria-label', function() {
1099 var words = dialogContent.text().split(/\s+/);
1100 if (words.length > 3) words = words.slice(0, 3).concat('...');
1101 return words.join(' ');
1105 // Set up elements before and after the dialog content to capture focus and
1106 // redirect back into the dialog.
1107 topFocusTrap = document.createElement('div');
1108 topFocusTrap.classList.add('md-dialog-focus-trap');
1109 topFocusTrap.tabIndex = 0;
1111 bottomFocusTrap = topFocusTrap.cloneNode(false);
1113 // When focus is about to move out of the dialog, we want to intercept it and redirect it
1114 // back to the dialog element.
1115 var focusHandler = function() {
1118 topFocusTrap.addEventListener('focus', focusHandler);
1119 bottomFocusTrap.addEventListener('focus', focusHandler);
1121 // The top focus trap inserted immeidately before the md-dialog element (as a sibling).
1122 // The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
1123 element[0].parentNode.insertBefore(topFocusTrap, element[0]);
1124 element.after(bottomFocusTrap);
1128 * Prevents screen reader interaction behind modal window
1129 * on swipe interfaces
1131 function lockScreenReader(element, options) {
1132 var isHidden = true;
1135 walkDOM(element[0]);
1137 options.unlockScreenReader = function() {
1139 walkDOM(element[0]);
1141 options.unlockScreenReader = null;
1145 * Walk DOM to apply or remove aria-hidden on sibling nodes
1146 * and parent sibling nodes
1149 function walkDOM(element) {
1150 while (element.parentNode) {
1151 if (element === document.body) {
1154 var children = element.parentNode.children;
1155 for (var i = 0; i < children.length; i++) {
1156 // skip over child if it is an ascendant of the dialog
1157 // or a script or style tag
1158 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
1159 children[i].setAttribute('aria-hidden', isHidden);
1163 walkDOM(element = element.parentNode);
1169 * Ensure the dialog container fill-stretches to the viewport
1171 function stretchDialogContainerToViewport(container, options) {
1172 var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
1173 var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
1174 var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
1176 var previousStyles = {
1177 top: container.css('top'),
1178 height: container.css('height')
1181 // If the body is fixed, determine the distance to the viewport in relative from the parent.
1182 var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top);
1185 top: (isFixed ? parentTop : 0) + 'px',
1186 height: height ? height + 'px' : '100%'
1190 // Reverts the modified styles back to the previous values.
1191 // This is needed for contentElements, which should have the same styles after close
1193 container.css(previousStyles);
1198 * Dialog open and pop-in animation
1200 function dialogPopIn(container, options) {
1201 // Add the `md-dialog-container` to the DOM
1202 options.parent.append(container);
1203 options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
1205 var dialogEl = container.find('md-dialog');
1206 var animator = $mdUtil.dom.animator;
1207 var buildTranslateToOrigin = animator.calculateZoomToOrigin;
1208 var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
1209 var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin));
1210 var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
1212 dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen);
1215 .translate3d(dialogEl, from, to, translateOptions)
1216 .then(function(animateReversal) {
1218 // Build a reversal translate function synced to this translation...
1219 options.reverseAnimate = function() {
1220 delete options.reverseAnimate;
1222 if (options.closeTo) {
1223 // Using the opposite classes to create a close animation to the closeTo element
1224 translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'};
1226 to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
1229 .translate3d(dialogEl, from, to,translateOptions);
1232 return animateReversal(
1233 to = animator.toTransformCss(
1234 // in case the origin element has moved or is hidden,
1235 // let's recalculate the translateCSS
1236 buildTranslateToOrigin(dialogEl, options.origin)
1242 // Function to revert the generated animation styles on the dialog element.
1243 // Useful when using a contentElement instead of a template.
1244 options.clearAnimate = function() {
1245 delete options.clearAnimate;
1247 // Remove the transition classes, added from $animateCSS, since those can't be removed
1248 // by reversely running the animator.
1249 dialogEl.removeClass([
1250 translateOptions.transitionOutClass,
1251 translateOptions.transitionInClass
1254 // Run the animation reversely to remove the previous added animation styles.
1255 return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {});
1263 * Dialog close and pop-out animation
1265 function dialogPopOut(container, options) {
1266 return options.reverseAnimate().then(function() {
1267 if (options.contentElement) {
1268 // When we use a contentElement, we want the element to be the same as before.
1269 // That means, that we have to clear all the animation properties, like transform.
1270 options.clearAnimate();
1276 * Utility function to filter out raw DOM nodes
1278 function isNodeOneOf(elem, nodeTypeArray) {
1279 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
1287 ngmaterial.components.dialog = angular.module("material.components.dialog");