2 * Angular Material Design
3 * https://github.com/angular/material
7 (function( window, angular, undefined ){
12 * @name material.components.dialog
14 angular.module('material.components.dialog', [
16 'material.components.backdrop'
18 .directive('mdDialog', MdDialogDirective)
19 .provider('$mdDialog', MdDialogProvider);
21 function MdDialogDirective($$rAF, $mdTheming) {
24 link: function(scope, element, attr) {
27 var content = element[0].querySelector('md-dialog-content');
28 if (content && content.scrollHeight > content.clientHeight) {
29 element.addClass('md-content-overflow');
35 MdDialogDirective.$inject = ["$$rAF", "$mdTheming"];
40 * @module material.components.dialog
43 * `$mdDialog` opens a dialog over the app to inform users about critical information or require
44 * them to make decisions. There are two approaches for setup: a simple promise API
45 * and regular object syntax.
49 * - The dialog is always given an isolate scope.
50 * - The dialog's template must have an outer `<md-dialog>` element.
51 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
52 * an element with class `md-actions` for the dialog's actions.
53 * - Dialogs must cover the entire application to keep interactions inside of them.
54 * Use the `parent` option to change where dialogs are appended.
57 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
58 * - Default max-width is 80% of the `rootElement` or `parent`.
62 * <div ng-app="demoApp" ng-controller="EmployeeController">
64 * <md-button ng-click="showAlert()" class="md-raised md-warn">
69 * <md-button ng-click="showDialog($event)" class="md-raised">
74 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
79 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
86 * ### JavaScript: object syntax
88 * (function(angular, undefined){
92 * .module('demoApp', ['ngMaterial'])
93 * .controller('AppCtrl', AppController);
95 * function AppController($scope, $mdDialog) {
97 * $scope.showAlert = showAlert;
98 * $scope.showDialog = showDialog;
99 * $scope.items = [1, 2, 3];
102 * function showAlert() {
103 * alert = $mdDialog.alert({
104 * title: 'Attention',
105 * content: 'This is an example of how easy dialogs can be!',
111 * .finally(function() {
116 * function showDialog($event) {
117 * var parentEl = angular.element(document.body);
120 * targetEvent: $event,
122 * '<md-dialog aria-label="List dialog">' +
123 * ' <md-dialog-content>'+
125 * ' <md-list-item ng-repeat="item in items">'+
126 * ' <p>Number {{item}}</p>' +
129 * ' </md-dialog-content>' +
130 * ' <div class="md-actions">' +
131 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
137 * items: $scope.items
139 * controller: DialogController
141 * function DialogController($scope, $mdDialog, items) {
142 * $scope.items = items;
143 * $scope.closeDialog = function() {
152 * ### JavaScript: promise API syntax, custom dialog template
154 * (function(angular, undefined){
158 * .module('demoApp', ['ngMaterial'])
159 * .controller('EmployeeController', EmployeeEditor)
160 * .controller('GreetingController', GreetingController);
162 * // Fictitious Employee Editor to show how to use simple and complex dialogs.
164 * function EmployeeEditor($scope, $mdDialog) {
167 * $scope.showAlert = showAlert;
168 * $scope.closeAlert = closeAlert;
169 * $scope.showGreeting = showCustomGreeting;
171 * $scope.hasAlert = function() { return !!alert };
172 * $scope.userName = $scope.userName || 'Bobby';
174 * // Dialog #1 - Show simple alert dialog and cache
175 * // reference to dialog instance
177 * function showAlert() {
178 * alert = $mdDialog.alert()
179 * .title('Attention, ' + $scope.userName)
180 * .content('This is an example of how easy dialogs can be!')
185 * .finally(function() {
190 * // Close the specified dialog instance and resolve with 'finished' flag
191 * // Normally this is not needed, just use '$mdDialog.hide()' to close
192 * // the most recent dialog popup.
194 * function closeAlert() {
195 * $mdDialog.hide( alert, "finished" );
199 * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
201 * function showCustomGreeting($event) {
203 * targetEvent: $event,
207 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
209 * ' <div class="md-actions">' +
210 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
211 * ' Close Greeting' +
215 * controller: 'GreetingController',
216 * onComplete: afterShowAnimation,
217 * locals: { employee: $scope.userName }
220 * // When the 'enter' animation finishes...
222 * function afterShowAnimation(scope, element, options) {
223 * // post-show code here: DOM element focus, etc.
227 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
228 * // Here we used ng-controller="GreetingController as vm" and
229 * // $scope.vm === <controller instance>
231 * function showCustomGreeting() {
234 * clickOutsideToClose: true,
236 * scope: $scope, // use parent scope in template
237 * preserveScope: true, // do not forget this if use parent scope
239 * // Since GreetingController is instantiated with ControllerAs syntax
240 * // AND we are passing the parent '$scope' to the dialog, we MUST
241 * // use 'vm.<xxx>' in the template markup
243 * template: '<md-dialog>' +
244 * ' <md-dialog-content>' +
245 * ' Hi There {{vm.employee}}' +
246 * ' </md-dialog-content>' +
249 * controller: function DialogController($scope, $mdDialog) {
250 * $scope.closeDialog = function() {
259 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
261 * function GreetingController($scope, $mdDialog, employee) {
262 * // Assigned from construction <code>locals</code> options...
263 * $scope.employee = employee;
265 * $scope.closeDialog = function() {
266 * // Easily hides most recent dialog shown...
267 * // no specific instance reference is needed.
278 * @name $mdDialog#alert
281 * Builds a preconfigured dialog with the specified message.
283 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
285 * - $mdDialogPreset#title(string) - sets title to string
286 * - $mdDialogPreset#content(string) - sets content / message to string
287 * - $mdDialogPreset#ok(string) - sets okay button text to string
288 * - $mdDialogPreset#theme(string) - sets the theme of the dialog
294 * @name $mdDialog#confirm
297 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
298 * will be resolved only if the user clicks the confirm action on the dialog.
300 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
302 * Additionally, it supports the following methods:
304 * - $mdDialogPreset#title(string) - sets title to string
305 * - $mdDialogPreset#content(string) - sets content / message to string
306 * - $mdDialogPreset#ok(string) - sets okay button text to string
307 * - $mdDialogPreset#cancel(string) - sets cancel button text to string
308 * - $mdDialogPreset#theme(string) - sets the theme of the dialog
314 * @name $mdDialog#show
317 * Show a dialog with the specified options.
319 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
320 * `confirm()`, or an options object with the following properties:
321 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
323 * - `template` - `{string=}`: Same as templateUrl, except this is an actual template string.
324 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
325 * the location of the click will be used as the starting point for the opening animation
327 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
328 * it will create a new isolate scope.
329 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
330 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
331 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
333 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
335 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
336 * close it. Default false.
337 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
339 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
340 * focusing some other way, as focus management is required for dialogs to be accessible.
342 * - `controller` - `{string=}`: The controller to associate with the dialog. The controller
343 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
344 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
345 * of values to inject into the controller. For example, `locals: {three: 3}` would inject
346 * `three` into the controller, with the value 3. If `bindToController` is true, they will be
347 * copied to the controller instead.
348 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
349 * These values will not be available until after initialization.
350 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
351 * dialog will not open until all of the promises resolve.
352 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
353 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
354 * to the root element of the application.
355 * - `onComplete` `{function=}`: Callback function used to announce when the show() action is
358 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
359 * rejected with `$mdDialog.cancel()`.
364 * @name $mdDialog#hide
367 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
369 * @param {*=} response An argument for the resolved promise.
371 * @returns {promise} A promise that is resolved when the dialog has been closed.
376 * @name $mdDialog#cancel
379 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
381 * @param {*=} response An argument for the rejected promise.
383 * @returns {promise} A promise that is resolved when the dialog has been closed.
386 function MdDialogProvider($$interimElementProvider) {
388 var alertDialogMethods = ['title', 'content', 'ariaLabel', 'ok'];
390 advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming"];
391 dialogDefaultOptions.$inject = ["$mdAria", "$document", "$mdUtil", "$mdConstant", "$mdTheming", "$mdDialog", "$timeout", "$rootElement", "$animate", "$$rAF", "$q"];
392 return $$interimElementProvider('$mdDialog')
394 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'parent'],
395 options: dialogDefaultOptions
397 .addPreset('alert', {
398 methods: ['title', 'content', 'ariaLabel', 'ok', 'theme'],
399 options: advancedDialogOptions
401 .addPreset('confirm', {
402 methods: ['title', 'content', 'ariaLabel', 'ok', 'cancel', 'theme'],
403 options: advancedDialogOptions
407 function advancedDialogOptions($mdDialog, $mdTheming) {
410 '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}">',
411 '<md-dialog-content role="document" tabIndex="-1">',
412 '<h2 class="md-title">{{ dialog.title }}</h2>',
413 '<p>{{ dialog.content }}</p>',
414 '</md-dialog-content>',
415 '<div class="md-actions">',
416 '<md-button ng-if="dialog.$type == \'confirm\'"' +
417 ' ng-click="dialog.abort()" class="md-primary">',
418 '{{ dialog.cancel }}',
420 '<md-button ng-click="dialog.hide()" class="md-primary">',
426 controller: function mdDialogCtrl() {
427 this.hide = function() {
428 $mdDialog.hide(true);
430 this.abort = function() {
434 controllerAs: 'dialog',
435 bindToController: true,
436 theme: $mdTheming.defaultTheme()
441 function dialogDefaultOptions($mdAria, $document, $mdUtil, $mdConstant, $mdTheming, $mdDialog, $timeout, $rootElement, $animate, $$rAF, $q) {
447 clickOutsideToClose: false,
451 disableParentScroll: true,
452 transformTemplate: function(template) {
453 return '<div class="md-dialog-container">' + template + '</div>';
457 function trapFocus(ev) {
458 var dialog = document.querySelector('md-dialog');
460 if (dialog && !dialog.contains(ev.target)) {
461 ev.stopImmediatePropagation();
466 // On show method for dialogs
467 function onShow(scope, element, options) {
468 angular.element($document[0].body).addClass('md-dialog-is-showing');
469 element = $mdUtil.extractElementByName(element, 'md-dialog');
471 // Incase the user provides a raw dom element, always wrap it in jqLite
472 options.parent = angular.element(options.parent);
474 options.popInTarget = angular.element((options.targetEvent || {}).target);
475 var closeButton = findCloseButton();
477 if (options.hasBackdrop) {
479 var computeFrom = (options.parent[0] == $document[0].body && $document[0].documentElement
480 && $document[0].documentElement.scrollTop) ? angular.element($document[0].documentElement) : options.parent;
481 var parentOffset = computeFrom.prop('scrollTop');
482 options.backdrop = angular.element('<md-backdrop class="md-dialog-backdrop md-opaque">');
483 options.backdrop.css('top', parentOffset +'px');
484 $mdTheming.inherit(options.backdrop, options.parent);
485 $animate.enter(options.backdrop, options.parent);
486 element.css('top', parentOffset +'px');
490 elementToFocus = closeButton;
492 if (options.$type === 'alert') {
493 role = 'alertdialog';
494 elementToFocus = element.find('md-dialog-content');
497 configureAria(element.find('md-dialog'), role, options);
499 document.addEventListener('focus', trapFocus, true);
501 if (options.disableParentScroll) {
502 options.lastOverflow = options.parent.css('overflow');
503 options.parent.css('overflow', 'hidden');
509 options.popInTarget && options.popInTarget.length && options.popInTarget
513 applyAriaToSiblings(element, true);
515 if (options.escapeToClose) {
516 options.rootElementKeyupCallback = function(e) {
517 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
518 $timeout($mdDialog.cancel);
521 $rootElement.on('keyup', options.rootElementKeyupCallback);
524 if (options.clickOutsideToClose) {
525 options.dialogClickOutsideCallback = function(ev) {
526 // Only close if we click the flex container outside the backdrop
527 if (ev.target === element[0]) {
528 $timeout($mdDialog.cancel);
531 element.on('click', options.dialogClickOutsideCallback);
534 if (options.focusOnOpen) {
535 elementToFocus.focus();
540 function findCloseButton() {
541 //If no element with class dialog-close, try to find the last
542 //button child in md-actions and assume it is a close button
543 var closeButton = element[0].querySelector('.dialog-close');
545 var actionButtons = element[0].querySelectorAll('.md-actions button');
546 closeButton = actionButtons[ actionButtons.length - 1 ];
548 return angular.element(closeButton);
553 // On remove function for all dialogs
554 function onRemove(scope, element, options) {
555 angular.element($document[0].body).removeClass('md-dialog-is-showing');
557 if (options.backdrop) {
558 $animate.leave(options.backdrop);
560 if (options.disableParentScroll) {
561 options.parent.css('overflow', options.lastOverflow);
562 delete options.lastOverflow;
564 if (options.escapeToClose) {
565 $rootElement.off('keyup', options.rootElementKeyupCallback);
567 if (options.clickOutsideToClose) {
568 element.off('click', options.dialogClickOutsideCallback);
571 applyAriaToSiblings(element, false);
573 document.removeEventListener('focus', trapFocus, true);
578 options.popInTarget && options.popInTarget.length && options.popInTarget
581 options.popInTarget && options.popInTarget.focus();
587 * Inject ARIA-specific attributes appropriate for Dialogs
589 function configureAria(element, role, options) {
596 var dialogContent = element.find('md-dialog-content');
597 if (dialogContent.length === 0){
598 dialogContent = element;
601 var dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid());
602 dialogContent.attr('id', dialogId);
603 element.attr('aria-describedby', dialogId);
605 if (options.ariaLabel) {
606 $mdAria.expect(element, 'aria-label', options.ariaLabel);
609 $mdAria.expectAsync(element, 'aria-label', function() {
610 var words = dialogContent.text().split(/\s+/);
611 if (words.length > 3) words = words.slice(0,3).concat('...');
612 return words.join(' ');
617 * Utility function to filter out raw DOM nodes
619 function isNodeOneOf(elem, nodeTypeArray) {
620 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
625 * Walk DOM to apply or remove aria-hidden on sibling nodes
626 * and parent sibling nodes
628 * Prevents screen reader interaction behind modal window
629 * on swipe interfaces
631 function applyAriaToSiblings(element, value) {
632 var attribute = 'aria-hidden';
635 element = element[0];
637 function walkDOM(element) {
638 while (element.parentNode) {
639 if (element === document.body) {
642 var children = element.parentNode.children;
643 for (var i = 0; i < children.length; i++) {
644 // skip over child if it is an ascendant of the dialog
645 // or a script or style tag
646 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
647 children[i].setAttribute(attribute, value);
651 walkDOM(element = element.parentNode);
657 function dialogPopIn(container, parentElement, clickElement) {
658 var dialogEl = container.find('md-dialog');
660 parentElement.append(container);
661 transformToClickElement(dialogEl, clickElement);
664 dialogEl.addClass('transition-in')
665 .css($mdConstant.CSS.TRANSFORM, '');
668 return $mdUtil.transitionEndPromise(dialogEl);
671 function dialogPopOut(container, parentElement, clickElement) {
672 var dialogEl = container.find('md-dialog');
674 dialogEl.addClass('transition-out').removeClass('transition-in');
675 transformToClickElement(dialogEl, clickElement);
677 return $mdUtil.transitionEndPromise(dialogEl);
680 function transformToClickElement(dialogEl, clickElement) {
682 var clickRect = clickElement[0].getBoundingClientRect();
683 var dialogRect = dialogEl[0].getBoundingClientRect();
685 var scaleX = Math.min(0.5, clickRect.width / dialogRect.width);
686 var scaleY = Math.min(0.5, clickRect.height / dialogRect.height);
688 dialogEl.css($mdConstant.CSS.TRANSFORM, 'translate3d(' +
689 (-dialogRect.left + clickRect.left + clickRect.width/2 - dialogRect.width/2) + 'px,' +
690 (-dialogRect.top + clickRect.top + clickRect.height/2 - dialogRect.height/2) + 'px,' +
691 '0) scale(' + scaleX + ',' + scaleY + ')'
696 function dialogTransitionEnd(dialogEl) {
697 var deferred = $q.defer();
698 dialogEl.on($mdConstant.CSS.TRANSITIONEND, finished);
699 function finished(ev) {
700 //Make sure this transitionend didn't bubble up from a child
701 if (ev.target === dialogEl[0]) {
702 dialogEl.off($mdConstant.CSS.TRANSITIONEND, finished);
706 return deferred.promise;
711 MdDialogProvider.$inject = ["$$interimElementProvider"];
713 })(window, window.angular);