|
|
/*! * Angular Material Design * https://github.com/angular/material
* @license MIT * v1.1.3 */ goog.provide('ngmaterial.components.dialog'); goog.require('ngmaterial.components.backdrop'); goog.require('ngmaterial.core'); /** * @ngdoc module * @name material.components.dialog */ MdDialogDirective['$inject'] = ["$$rAF", "$mdTheming", "$mdDialog"]; MdDialogProvider['$inject'] = ["$$interimElementProvider"]; angular .module('material.components.dialog', [ 'material.core', 'material.components.backdrop' ]) .directive('mdDialog', MdDialogDirective) .provider('$mdDialog', MdDialogProvider);
/** * @ngdoc directive * @name mdDialog * @module material.components.dialog * * @restrict E * * @description * `<md-dialog>` - The dialog's template must be inside this element. * * Inside, use an `<md-dialog-content>` element for the dialog's content, and use * an `<md-dialog-actions>` element for the dialog's actions. * * ## CSS * - `.md-dialog-content` - class that sets the padding on the content as the spec file * * ## Notes * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id` * prefixed with `dialogContent_`. * * @usage * ### Dialog template * <hljs lang="html"> * <md-dialog aria-label="List dialog"> * <md-dialog-content> * <md-list> * <md-list-item ng-repeat="item in items"> * <p>Number {{item}}</p> * </md-list-item> * </md-list> * </md-dialog-content> * <md-dialog-actions> * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button> * </md-dialog-actions> * </md-dialog> * </hljs> */ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { return { restrict: 'E', link: function(scope, element) { element.addClass('_md'); // private md component indicator for styling
$mdTheming(element); $$rAF(function() { var images; var content = element[0].querySelector('md-dialog-content');
if (content) { images = content.getElementsByTagName('img'); addOverflowClass(); //-- delayed image loading may impact scroll height, check after images are loaded
angular.element(images).on('load', addOverflowClass); }
scope.$on('$destroy', function() { $mdDialog.destroy(element); });
/** * */ function addOverflowClass() { element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); }
}); } }; }
/** * @ngdoc service * @name $mdDialog * @module material.components.dialog * * @description * `$mdDialog` opens a dialog over the app to inform users about critical information or require * them to make decisions. There are two approaches for setup: a simple promise API * and regular object syntax. * * ## Restrictions * * - The dialog is always given an isolate scope. * - The dialog's template must have an outer `<md-dialog>` element. * Inside, use an `<md-dialog-content>` element for the dialog's content, and use * an `<md-dialog-actions>` element for the dialog's actions. * - Dialogs must cover the entire application to keep interactions inside of them. * Use the `parent` option to change where dialogs are appended. * * ## Sizing * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. * - Default max-width is 80% of the `rootElement` or `parent`. * * ## CSS * - `.md-dialog-content` - class that sets the padding on the content as the spec file * * @usage * <hljs lang="html"> * <div ng-app="demoApp" ng-controller="EmployeeController"> * <div> * <md-button ng-click="showAlert()" class="md-raised md-warn"> * Employee Alert! * </md-button> * </div> * <div> * <md-button ng-click="showDialog($event)" class="md-raised"> * Custom Dialog * </md-button> * </div> * <div> * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised"> * Close Alert * </md-button> * </div> * <div> * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" > * Greet Employee * </md-button> * </div> * </div> * </hljs> * * ### JavaScript: object syntax * <hljs lang="js"> * (function(angular, undefined){ * "use strict"; * * angular * .module('demoApp', ['ngMaterial']) * .controller('AppCtrl', AppController); * * function AppController($scope, $mdDialog) { * var alert; * $scope.showAlert = showAlert; * $scope.showDialog = showDialog; * $scope.items = [1, 2, 3]; * * // Internal method
* function showAlert() { * alert = $mdDialog.alert({ * title: 'Attention', * textContent: 'This is an example of how easy dialogs can be!', * ok: 'Close' * }); * * $mdDialog * .show( alert ) * .finally(function() { * alert = undefined; * }); * } * * function showDialog($event) { * var parentEl = angular.element(document.body); * $mdDialog.show({ * parent: parentEl, * targetEvent: $event, * template: * '<md-dialog aria-label="List dialog">' + * ' <md-dialog-content>'+ * ' <md-list>'+ * ' <md-list-item ng-repeat="item in items">'+ * ' <p>Number {{item}}</p>' + * ' </md-item>'+ * ' </md-list>'+ * ' </md-dialog-content>' + * ' <md-dialog-actions>' + * ' <md-button ng-click="closeDialog()" class="md-primary">' + * ' Close Dialog' + * ' </md-button>' + * ' </md-dialog-actions>' + * '</md-dialog>', * locals: { * items: $scope.items * }, * controller: DialogController * }); * function DialogController($scope, $mdDialog, items) { * $scope.items = items; * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * } * } * })(angular); * </hljs> * * ### Multiple Dialogs * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs * at the same time. * * <hljs lang="js"> * // From plain options
* $mdDialog.show({ * multiple: true * }); * * // From a dialog preset
* $mdDialog.show( * $mdDialog * .alert() * .multiple(true) * ); * * </hljs> * * ### Pre-Rendered Dialogs * By using the `contentElement` option, it is possible to use an already existing element in the DOM. * * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.<br/> * > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`) * * <hljs lang="js"> * $scope.showPrerenderedDialog = function() { * $mdDialog.show({ * contentElement: '#myStaticDialog', * parent: angular.element(document.body) * }); * }; * </hljs> * * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector. * * <hljs lang="html"> * <div style="visibility: hidden"> * <div class="md-dialog-container" id="myStaticDialog"> * <md-dialog> * This is a pre-rendered dialog. * </md-dialog> * </div> * </div> * </hljs> * * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog * will not show up. * * It also possible to use a DOM element for the `contentElement` option. * - `contentElement: document.querySelector('#myStaticDialog')` * - `contentElement: angular.element(TEMPLATE)` * * When using a `template` as content element, it will be not compiled upon open. * This allows you to compile the element yourself and use it each time the dialog opens. * * ### Custom Presets * Developers are also able to create their own preset, which can be easily used without repeating * their options each time. * * <hljs lang="js"> * $mdDialogProvider.addPreset('testPreset', { * options: function() { * return { * template: * '<md-dialog>' + * 'This is a custom preset' + * '</md-dialog>', * controllerAs: 'dialog', * bindToController: true, * clickOutsideToClose: true, * escapeToClose: true * }; * } * }); * </hljs> * * After you created your preset at config phase, you can easily access it. * * <hljs lang="js"> * $mdDialog.show( * $mdDialog.testPreset() * ); * </hljs> * * ### JavaScript: promise API syntax, custom dialog template * <hljs lang="js"> * (function(angular, undefined){ * "use strict"; * * angular * .module('demoApp', ['ngMaterial']) * .controller('EmployeeController', EmployeeEditor) * .controller('GreetingController', GreetingController); * * // Fictitious Employee Editor to show how to use simple and complex dialogs.
* * function EmployeeEditor($scope, $mdDialog) { * var alert; * * $scope.showAlert = showAlert; * $scope.closeAlert = closeAlert; * $scope.showGreeting = showCustomGreeting; * * $scope.hasAlert = function() { return !!alert }; * $scope.userName = $scope.userName || 'Bobby'; * * // Dialog #1 - Show simple alert dialog and cache
* // reference to dialog instance
* * function showAlert() { * alert = $mdDialog.alert() * .title('Attention, ' + $scope.userName) * .textContent('This is an example of how easy dialogs can be!') * .ok('Close'); * * $mdDialog * .show( alert ) * .finally(function() { * alert = undefined; * }); * } * * // Close the specified dialog instance and resolve with 'finished' flag
* // Normally this is not needed, just use '$mdDialog.hide()' to close
* // the most recent dialog popup.
* * function closeAlert() { * $mdDialog.hide( alert, "finished" ); * alert = undefined; * } * * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
* * function showCustomGreeting($event) { * $mdDialog.show({ * targetEvent: $event, * template: * '<md-dialog>' + * * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' + * * ' <md-dialog-actions>' + * ' <md-button ng-click="closeDialog()" class="md-primary">' + * ' Close Greeting' + * ' </md-button>' + * ' </md-dialog-actions>' + * '</md-dialog>', * controller: 'GreetingController', * onComplete: afterShowAnimation, * locals: { employee: $scope.userName } * }); * * // When the 'enter' animation finishes...
* * function afterShowAnimation(scope, element, options) { * // post-show code here: DOM element focus, etc.
* } * } * * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
* // Here we used ng-controller="GreetingController as vm" and
* // $scope.vm === <controller instance>
* * function showCustomGreeting() { * * $mdDialog.show({ * clickOutsideToClose: true, * * scope: $scope, // use parent scope in template
* preserveScope: true, // do not forget this if use parent scope
* // Since GreetingController is instantiated with ControllerAs syntax
* // AND we are passing the parent '$scope' to the dialog, we MUST
* // use 'vm.<xxx>' in the template markup
* * template: '<md-dialog>' + * ' <md-dialog-content>' + * ' Hi There {{vm.employee}}' + * ' </md-dialog-content>' + * '</md-dialog>', * * controller: function DialogController($scope, $mdDialog) { * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * }); * } * * } * * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
* * function GreetingController($scope, $mdDialog, employee) { * // Assigned from construction <code>locals</code> options...
* $scope.employee = employee; * * $scope.closeDialog = function() { * // Easily hides most recent dialog shown...
* // no specific instance reference is needed.
* $mdDialog.hide(); * }; * } * * })(angular); * </hljs> */
/** * @ngdoc method * @name $mdDialog#alert * * @description * Builds a preconfigured dialog with the specified message. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * - $mdDialogPreset#title(string) - Sets the alert title. * - $mdDialogPreset#textContent(string) - Sets the alert message. * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */
/** * @ngdoc method * @name $mdDialog#confirm * * @description * Builds a preconfigured dialog with the specified message. You can call show and the promise returned * will be resolved only if the user clicks the confirm action on the dialog. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * Additionally, it supports the following methods: * * - $mdDialogPreset#title(string) - Sets the confirm title. * - $mdDialogPreset#textContent(string) - Sets the confirm message. * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */
/** * @ngdoc method * @name $mdDialog#prompt * * @description * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. * * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: * * Additionally, it supports the following methods: * * - $mdDialogPreset#title(string) - Sets the prompt title. * - $mdDialogPreset#textContent(string) - Sets the prompt message. * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize * module to be loaded. HTML is not run through Angular's compiler. * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input. * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * */
/** * @ngdoc method * @name $mdDialog#show * * @description * Show a dialog with the specified options. * * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and * `confirm()`, or an options object with the following properties: * - `templateUrl` - `{string=}`: The url of a template that will be used as the content * of the dialog. * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
* This template should **never** be constructed with any kind of user input or user data. * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a * dialog opens, you can also use a DOM element.<br/> * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into * the dialog and restores it at the old DOM position upon close. * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM. * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a * custom dialog directive. * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object * that is used to determine the bounds (top, left, height, width) from which the Dialog will * originate. * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object * that is used to determine the bounds (top, left, height, width) to which the Dialog will * target. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, * it will create a new isolate scope. * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. * Default true. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. * Default true. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to * close it. Default false. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. * Default true. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if * focusing some other way, as focus management is required for dialogs to be accessible. * Defaults to true. * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names * of values to inject into the controller. For example, `locals: {three: 3}` would inject * `three` into the controller, with the value 3. If `bindToController` is true, they will be * copied to the controller instead. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the * dialog will not open until all of the promises resolve. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending * to the root element of the application. * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is * starting. * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is * finished. * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the * close/hide() action is starting. This allows developers to run custom animations * in parallel the close animations. * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen * or not. Defaults to `false`. * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or * rejected with `$mdDialog.cancel()`. */
/** * @ngdoc method * @name $mdDialog#hide * * @description * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. * * @param {*=} response An argument for the resolved promise. * * @returns {promise} A promise that is resolved when the dialog has been closed. */
/** * @ngdoc method * @name $mdDialog#cancel * * @description * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. * * @param {*=} response An argument for the rejected promise. * * @returns {promise} A promise that is resolved when the dialog has been closed. */
function MdDialogProvider($$interimElementProvider) { // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
advancedDialogOptions['$inject'] = ["$mdDialog", "$mdConstant"]; dialogDefaultOptions['$inject'] = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"]; var topFocusTrap, bottomFocusTrap;
return $$interimElementProvider('$mdDialog') .setDefaults({ methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'], options: dialogDefaultOptions }) .addPreset('alert', { methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', 'css'], options: advancedDialogOptions }) .addPreset('confirm', { methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', 'theme', 'css'], options: advancedDialogOptions }) .addPreset('prompt', { methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel', 'ok', 'cancel', 'theme', 'css'], options: advancedDialogOptions });
/* ngInject */ function advancedDialogOptions($mdDialog, $mdConstant) { return { template: [ '<md-dialog md-theme="{{ dialog.theme || dialog.defaultTheme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">', ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">', ' <h2 class="md-title">{{ dialog.title }}</h2>', ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ', ' ng-bind-html="::dialog.mdHtmlContent"></div>', ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">', ' <p>{{::dialog.mdTextContent}}</p>', ' </div>', ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">', ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' + ' placeholder="{{::dialog.placeholder}}">', ' </md-input-container>', ' </md-dialog-content>', ' <md-dialog-actions>', ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' + ' ng-click="dialog.abort()" class="md-primary md-cancel-button">', ' {{ dialog.cancel }}', ' </md-button>', ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'">', ' {{ dialog.ok }}', ' </md-button>', ' </md-dialog-actions>', '</md-dialog>' ].join('').replace(/\s\s+/g, ''), controller: function mdDialogCtrl() { var isPrompt = this.$type == 'prompt';
if (isPrompt && this.initialValue) { this.result = this.initialValue; }
this.hide = function() { $mdDialog.hide(isPrompt ? this.result : true); }; this.abort = function() { $mdDialog.cancel(); }; this.keypress = function($event) { if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { $mdDialog.hide(this.result); } }; }, controllerAs: 'dialog', bindToController: true, }; }
/* ngInject */ function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector, $mdTheming, $interpolate, $mdInteraction) {
return { hasBackdrop: true, isolateScope: true, onCompiling: beforeCompile, onShow: onShow, onShowing: beforeShow, onRemove: onRemove, clickOutsideToClose: false, escapeToClose: true, targetEvent: null, closeTo: null, openFrom: null, focusOnOpen: true, disableParentScroll: true, autoWrap: true, fullscreen: false, transformTemplate: function(template, options) { // Make the dialog container focusable, because otherwise the focus will be always redirected to
// an element outside of the container, and the focus trap won't work probably..
// Also the tabindex is needed for the `escapeToClose` functionality, because
// the keyDown event can't be triggered when the focus is outside of the container.
var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol; return '<div class="md-dialog-container" tabindex="-1" md-theme="' + theme + '">' + validatedTemplate(template) + '</div>';
/** * The specified template should contain a <md-dialog> wrapper element.... */ function validatedTemplate(template) { if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { return '<md-dialog>' + (template || '') + '</md-dialog>'; } else { return template || ''; } } } };
function beforeCompile(options) { // Automatically apply the theme, if the user didn't specify a theme explicitly.
// Those option changes need to be done, before the compilation has started, because otherwise
// the option changes will be not available in the $mdCompilers locales.
options.defaultTheme = $mdTheming.defaultTheme();
detectTheming(options); }
function beforeShow(scope, element, options, controller) {
if (controller) { var mdHtmlContent = controller.htmlContent || options.htmlContent || ''; var mdTextContent = controller.textContent || options.textContent || controller.content || options.content || '';
if (mdHtmlContent && !$injector.has('$sanitize')) { throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); }
if (mdHtmlContent && mdTextContent) { throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); }
// Only assign the content if nothing throws, otherwise it'll still be compiled.
controller.mdHtmlContent = mdHtmlContent; controller.mdTextContent = mdTextContent; } }
/** Show method for dialogs */ function onShow(scope, element, options, controller) { angular.element($document[0].body).addClass('md-dialog-is-showing');
var dialogElement = element.find('md-dialog');
// Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly.
// This is a very common problem, so we have to notify the developer about this.
if (dialogElement.hasClass('ng-cloak')) { var message = '$mdDialog: using `<md-dialog ng-cloak>` will affect the dialog opening animations.'; $log.warn( message, element[0] ); }
captureParentAndFromToElements(options); configureAria(dialogElement, options); showBackdrop(scope, element, options); activateListeners(element, options);
return dialogPopIn(element, options) .then(function() { lockScreenReader(element, options); warnDeprecatedActions(); focusOnOpen(); });
/** * Check to see if they used the deprecated .md-actions class and log a warning */ function warnDeprecatedActions() { if (element[0].querySelector('.md-actions')) { $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.'); } }
/** * For alerts, focus on content... otherwise focus on * the close button (or equivalent) */ function focusOnOpen() { if (options.focusOnOpen) { var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement; target.focus(); }
/** * If no element with class dialog-close, try to find the last * button child in md-actions and assume it is a close button. * * If we find no actions at all, log a warning to the console. */ function findCloseButton() { return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child'); } } }
/** * Remove function for all dialogs */ function onRemove(scope, element, options) { options.deactivateListeners(); options.unlockScreenReader(); options.hideBackdrop(options.$destroy);
// Remove the focus traps that we added earlier for keeping focus within the dialog.
if (topFocusTrap && topFocusTrap.parentNode) { topFocusTrap.parentNode.removeChild(topFocusTrap); }
if (bottomFocusTrap && bottomFocusTrap.parentNode) { bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); }
// For navigation $destroy events, do a quick, non-animated removal,
// but for normal closes (from clicks, etc) animate the removal
return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
/** * For normal closes, animate the removal. * For forced closes (like $destroy events), skip the animations */ function animateRemoval() { return dialogPopOut(element, options); }
/** * Detach the element */ function detachAndClean() { angular.element($document[0].body).removeClass('md-dialog-is-showing');
// Reverse the container stretch if using a content element.
if (options.contentElement) { options.reverseContainerStretch(); }
// Exposed cleanup function from the $mdCompiler.
options.cleanupElement();
// Restores the focus to the origin element if the last interaction upon opening was a keyboard.
if (!options.$destroy && options.originInteraction === 'keyboard') { options.origin.focus(); } } }
function detectTheming(options) { // Once the user specifies a targetEvent, we will automatically try to find the correct
// nested theme.
var targetEl; if (options.targetEvent && options.targetEvent.target) { targetEl = angular.element(options.targetEvent.target); }
var themeCtrl = targetEl && targetEl.controller('mdTheme');
if (!themeCtrl) { return; }
options.themeWatch = themeCtrl.$shouldWatch;
var theme = options.theme || themeCtrl.$mdTheme;
if (theme) { options.scope.theme = theme; }
var unwatch = themeCtrl.registerChanges(function (newTheme) { options.scope.theme = newTheme;
if (!options.themeWatch) { unwatch(); } }); }
/** * Capture originator/trigger/from/to element information (if available) * and the parent container for the dialog; defaults to the $rootElement * unless overridden in the options.parent */ function captureParentAndFromToElements(options) { options.origin = angular.extend({ element: null, bounds: null, focus: angular.noop }, options.origin || {});
options.parent = getDomElement(options.parent, $rootElement); options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
if ( options.targetEvent ) { options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); options.originInteraction = $mdInteraction.getLastInteractionType(); }
/** * Identify the bounding RECT for the target element * */ function getBoundingClientRect (element, orig) { var source = angular.element((element || {})); if (source && source.length) { // Compute and save the target element's bounding rect, so that if the
// element is hidden when the dialog closes, we can shrink the dialog
// back to the same position it expanded from.
//
// Checking if the source is a rect object or a DOM element
var bounds = {top:0,left:0,height:0,width:0}; var hasFn = angular.isFunction(source[0].getBoundingClientRect);
return angular.extend(orig || {}, { element : hasFn ? source : undefined, bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), focus : angular.bind(source, source.focus), }); } }
/** * If the specifier is a simple string selector, then query for * the DOM element. */ function getDomElement(element, defaultElement) { if (angular.isString(element)) { element = $document[0].querySelector(element); }
// If we have a reference to a raw dom element, always wrap it in jqLite
return angular.element(element || defaultElement); }
}
/** * Listen for escape keys and outside clicks to auto close */ function activateListeners(element, options) { var window = angular.element($window); var onWindowResize = $mdUtil.debounce(function() { stretchDialogContainerToViewport(element, options); }, 60);
var removeListeners = []; var smartClose = function() { // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
// cancel or fallback to hide.
var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; $mdUtil.nextTick(closeFn, true); };
if (options.escapeToClose) { var parentTarget = options.parent; var keyHandlerFn = function(ev) { if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { ev.stopPropagation(); ev.preventDefault();
smartClose(); } };
// Add keydown listeners
element.on('keydown', keyHandlerFn); parentTarget.on('keydown', keyHandlerFn);
// Queue remove listeners function
removeListeners.push(function() {
element.off('keydown', keyHandlerFn); parentTarget.off('keydown', keyHandlerFn);
}); }
// Register listener to update dialog on window resize
window.on('resize', onWindowResize);
removeListeners.push(function() { window.off('resize', onWindowResize); });
if (options.clickOutsideToClose) { var target = element; var sourceElem;
// Keep track of the element on which the mouse originally went down
// so that we can only close the backdrop when the 'click' started on it.
// A simple 'click' handler does not work,
// it sets the target object as the element the mouse went down on.
var mousedownHandler = function(ev) { sourceElem = ev.target; };
// We check if our original element and the target is the backdrop
// because if the original was the backdrop and the target was inside the dialog
// we don't want to dialog to close.
var mouseupHandler = function(ev) { if (sourceElem === target[0] && ev.target === target[0]) { ev.stopPropagation(); ev.preventDefault();
smartClose(); } };
// Add listeners
target.on('mousedown', mousedownHandler); target.on('mouseup', mouseupHandler);
// Queue remove listeners function
removeListeners.push(function() { target.off('mousedown', mousedownHandler); target.off('mouseup', mouseupHandler); }); }
// Attach specific `remove` listener handler
options.deactivateListeners = function() { removeListeners.forEach(function(removeFn) { removeFn(); }); options.deactivateListeners = null; }; }
/** * Show modal backdrop element... */ function showBackdrop(scope, element, options) {
if (options.disableParentScroll) { // !! DO this before creating the backdrop; since disableScrollAround()
// configures the scroll offset; which is used by mdBackDrop postLink()
options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); }
if (options.hasBackdrop) { options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); $animate.enter(options.backdrop, options.parent); }
/** * Hide modal backdrop element... */ options.hideBackdrop = function hideBackdrop($destroy) { if (options.backdrop) { if ( !!$destroy ) options.backdrop.remove(); else $animate.leave(options.backdrop); }
if (options.disableParentScroll) { options.restoreScroll && options.restoreScroll(); delete options.restoreScroll; }
options.hideBackdrop = null; }; }
/** * Inject ARIA-specific attributes appropriate for Dialogs */ function configureAria(element, options) {
var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; var dialogContent = element.find('md-dialog-content'); var existingDialogId = element.attr('id'); var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid());
element.attr({ 'role': role, 'tabIndex': '-1' });
if (dialogContent.length === 0) { dialogContent = element; // If the dialog element already had an ID, don't clobber it.
if (existingDialogId) { dialogContentId = existingDialogId; } }
dialogContent.attr('id', dialogContentId); element.attr('aria-describedby', dialogContentId);
if (options.ariaLabel) { $mdAria.expect(element, 'aria-label', options.ariaLabel); } else { $mdAria.expectAsync(element, 'aria-label', function() { var words = dialogContent.text().split(/\s+/); if (words.length > 3) words = words.slice(0, 3).concat('...'); return words.join(' '); }); }
// Set up elements before and after the dialog content to capture focus and
// redirect back into the dialog.
topFocusTrap = document.createElement('div'); topFocusTrap.classList.add('md-dialog-focus-trap'); topFocusTrap.tabIndex = 0;
bottomFocusTrap = topFocusTrap.cloneNode(false);
// When focus is about to move out of the dialog, we want to intercept it and redirect it
// back to the dialog element.
var focusHandler = function() { element.focus(); }; topFocusTrap.addEventListener('focus', focusHandler); bottomFocusTrap.addEventListener('focus', focusHandler);
// The top focus trap inserted immeidately before the md-dialog element (as a sibling).
// The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
element[0].parentNode.insertBefore(topFocusTrap, element[0]); element.after(bottomFocusTrap); }
/** * Prevents screen reader interaction behind modal window * on swipe interfaces */ function lockScreenReader(element, options) { var isHidden = true;
// get raw DOM node
walkDOM(element[0]);
options.unlockScreenReader = function() { isHidden = false; walkDOM(element[0]);
options.unlockScreenReader = null; };
/** * Walk DOM to apply or remove aria-hidden on sibling nodes * and parent sibling nodes * */ function walkDOM(element) { while (element.parentNode) { if (element === document.body) { return; } var children = element.parentNode.children; for (var i = 0; i < children.length; i++) { // skip over child if it is an ascendant of the dialog
// or a script or style tag
if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { children[i].setAttribute('aria-hidden', isHidden); } }
walkDOM(element = element.parentNode); } } }
/** * Ensure the dialog container fill-stretches to the viewport */ function stretchDialogContainerToViewport(container, options) { var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
var previousStyles = { top: container.css('top'), height: container.css('height') };
// If the body is fixed, determine the distance to the viewport in relative from the parent.
var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top);
container.css({ top: (isFixed ? parentTop : 0) + 'px', height: height ? height + 'px' : '100%' });
return function() { // Reverts the modified styles back to the previous values.
// This is needed for contentElements, which should have the same styles after close
// as before.
container.css(previousStyles); }; }
/** * Dialog open and pop-in animation */ function dialogPopIn(container, options) { // Add the `md-dialog-container` to the DOM
options.parent.append(container); options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
var dialogEl = container.find('md-dialog'); var animator = $mdUtil.dom.animator; var buildTranslateToOrigin = animator.calculateZoomToOrigin; var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen);
return animator .translate3d(dialogEl, from, to, translateOptions) .then(function(animateReversal) {
// Build a reversal translate function synced to this translation...
options.reverseAnimate = function() { delete options.reverseAnimate;
if (options.closeTo) { // Using the opposite classes to create a close animation to the closeTo element
translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; from = to; to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
return animator .translate3d(dialogEl, from, to,translateOptions); }
return animateReversal( to = animator.toTransformCss( // in case the origin element has moved or is hidden,
// let's recalculate the translateCSS
buildTranslateToOrigin(dialogEl, options.origin) ) );
};
// Function to revert the generated animation styles on the dialog element.
// Useful when using a contentElement instead of a template.
options.clearAnimate = function() { delete options.clearAnimate;
// Remove the transition classes, added from $animateCSS, since those can't be removed
// by reversely running the animator.
dialogEl.removeClass([ translateOptions.transitionOutClass, translateOptions.transitionInClass ].join(' '));
// Run the animation reversely to remove the previous added animation styles.
return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {}); };
return true; }); }
/** * Dialog close and pop-out animation */ function dialogPopOut(container, options) { return options.reverseAnimate().then(function() { if (options.contentElement) { // When we use a contentElement, we want the element to be the same as before.
// That means, that we have to clear all the animation properties, like transform.
options.clearAnimate(); } }); }
/** * Utility function to filter out raw DOM nodes */ function isNodeOneOf(elem, nodeTypeArray) { if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { return true; } }
} }
ngmaterial.components.dialog = angular.module("material.components.dialog");
|