/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.11.4 */ goog.provide('ng.material.components.dialog'); goog.require('ng.material.components.backdrop'); goog.require('ng.material.core'); /** * @ngdoc module * @name material.components.dialog */ angular .module('material.components.dialog', [ 'material.core', 'material.components.backdrop' ]) .directive('mdDialog', MdDialogDirective) .provider('$mdDialog', MdDialogProvider); function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { return { restrict: 'E', link: function(scope, element, attr) { $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(); }); /** * */ function addOverflowClass() { element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); } }); } }; } MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; /** * @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 `` element. * Inside, use an `` element for the dialog's content, and use * an element with class `md-actions` 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 * *
*
* * Employee Alert! * *
*
* * Custom Dialog * *
*
* * Close Alert * *
*
* * Greet Employee * *
*
*
* * ### JavaScript: object syntax * * (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', * content: '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: * '' + * ' '+ * ' '+ * ' '+ * '

Number {{item}}

' + * ' '+ * '
'+ * '
' + * '
' + * ' ' + * ' Close Dialog' + * ' ' + * '
' + * '
', * locals: { * items: $scope.items * }, * controller: DialogController * }); * function DialogController($scope, $mdDialog, items) { * $scope.items = items; * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * } * } * })(angular); *
* * ### JavaScript: promise API syntax, custom dialog template * * (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) * .content('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: * '' + * * ' Hello {{ employee }}!' + * * '
' + * ' ' + * ' Close Greeting' + * ' ' + * '
' + * '
', * 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 === * * 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.' in the template markup * * template: '' + * ' ' + * ' Hi There {{vm.employee}}' + * ' ' + * '', * * 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 locals options... * $scope.employee = employee; * * $scope.closeDialog = function() { * // Easily hides most recent dialog shown... * // no specific instance reference is needed. * $mdDialog.hide(); * }; * } * * })(angular); *
*/ /** * @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 title to string * - $mdDialogPreset#content(string) - sets content / message to string * - $mdDialogPreset#ok(string) - sets okay button text to string * - $mdDialogPreset#theme(string) - sets the theme of 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 title to string * - $mdDialogPreset#content(string) - sets content / message to string * - $mdDialogPreset#ok(string) - sets okay button text to string * - $mdDialogPreset#cancel(string) - sets cancel button text to string * - $mdDialogPreset#theme(string) - sets the theme of 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=}`: Same as templateUrl, except this is an actual template string. * - `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. * - `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` - `{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. * These values will not be available until after initialization. * - `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=} Callback function used to announce the show() action is * starting. * - `onComplete` `{function=}`: Callback function used to announce when the show() action is * finished. * - `onRemoving` `{function=} Callback function used to announce the close/hide() action is * starting. This allows developers to run custom animations in parallel the close animations. * * @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) { advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming"]; dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement"]; return $$interimElementProvider('$mdDialog') .setDefaults({ methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'parent'], options: dialogDefaultOptions }) .addPreset('alert', { methods: ['title', 'content', 'ariaLabel', 'ok', 'theme', 'css'], options: advancedDialogOptions }) .addPreset('confirm', { methods: ['title', 'content', 'ariaLabel', 'ok', 'cancel', 'theme', 'css'], options: advancedDialogOptions }); /* ngInject */ function advancedDialogOptions($mdDialog, $mdTheming) { return { template: [ '', ' ', '

{{ dialog.title }}

', '
', '
', '
', ' ', ' {{ dialog.cancel }}', ' ', ' ', ' {{ dialog.ok }}', ' ', '
', '
' ].join('').replace(/\s\s+/g, ''), controller: function mdDialogCtrl() { this.hide = function() { $mdDialog.hide(true); }; this.abort = function() { $mdDialog.cancel(); }; }, controllerAs: 'dialog', bindToController: true, theme: $mdTheming.defaultTheme() }; } /* ngInject */ function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement) { return { hasBackdrop: true, isolateScope: true, onShow: onShow, onRemove: onRemove, clickOutsideToClose: false, escapeToClose: true, targetEvent: null, focusOnOpen: true, disableParentScroll: true, transformTemplate: function(template) { return '
' + validatedTemplate(template) + '
'; /** * The specified template should contain a wrapper element.... */ function validatedTemplate(template) { template || "" return /<\/md-dialog>/g.test(template) ? template : "" + template + ""; } } }; /** * Show method for dialogs */ function onShow(scope, element, options, controller) { angular.element($document[0].body).addClass('md-dialog-is-showing'); wrapSimpleContent(); captureSourceAndParent(element, options); configureAria(element.find('md-dialog'), options); showBackdrop(scope, element, options); return dialogPopIn(element, options) .then(function() { activateListeners(element, options); lockScreenReader(element, options); focusOnOpen(); }); /** * For alerts, focus on content... otherwise focus on * the close button (or equivalent) */ function focusOnOpen() { if (options.focusOnOpen) { var target = $mdUtil.findFocusTarget(element) || findCloseButton(); 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 */ function findCloseButton() { var closeButton = element[0].querySelector('.dialog-close'); if (!closeButton) { var actionButtons = element[0].querySelectorAll('.md-actions button'); closeButton = actionButtons[actionButtons.length - 1]; } return angular.element(closeButton); } } /** * Wrap any simple content [specified via .content("")] in

tags. * otherwise accept HTML content within the dialog content area... * NOTE: Dialog uses the md-template directive to safely inject HTML content. */ function wrapSimpleContent() { if ( controller ) { var HTML_END_TAG = /<\/[\w-]*>/gm; var content = controller.content || options.content || ""; var hasHTML = HTML_END_TAG.test(content); if (!hasHTML) { content = $mdUtil.supplant("

{0}

", [content]); } // Publish updated dialog content body... to be compiled by mdTemplate directive controller.mdContent = content; } } } /** * Remove function for all dialogs */ function onRemove(scope, element, options) { options.deactivateListeners(); options.unlockScreenReader(); options.hideBackdrop(options.$destroy); // 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'); element.remove(); if (!options.$destroy) options.origin.focus(); } } /** * Capture originator/trigger element information (if available) * and the parent container for the dialog; defaults to the $rootElement * unless overridden in the options.parent */ function captureSourceAndParent(element, options) { options.origin = angular.extend({ element: null, bounds: null, focus: angular.noop }, options.origin || {}); var source = angular.element((options.targetEvent || {}).target); 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. options.origin.element = source; options.origin.bounds = source[0].getBoundingClientRect(); options.origin.focus = function() { source.focus(); } } // If the parent specifier is a simple string selector, then query for // the DOM element. if ( angular.isString(options.parent) ) { var simpleSelector = options.parent, container = $document[0].querySelectorAll(simpleSelector); options.parent = container.length ? container[0] : null; } // If we have a reference to a raw dom element, always wrap it in jqLite options.parent = angular.element(options.parent || $rootElement); } /** * 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 target = 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); target.on('keydown', keyHandlerFn); window.on('resize', onWindowResize); // Queue remove listeners function removeListeners.push(function() { element.off('keydown', keyHandlerFn); target.off('keydown', keyHandlerFn); 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(); 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 dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid()); element.attr({ 'role': role, 'tabIndex': '-1' }); if (dialogContent.length === 0) { dialogContent = element; } dialogContent.attr('id', dialogId); element.attr('aria-describedby', dialogId); 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(' '); }); } } /** * 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; container.css({ top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', height: height ? height + 'px' : '100%' }); return container; } /** * Dialog open and pop-in animation */ function dialogPopIn(container, options) { // Add the `md-dialog-container` to the DOM options.parent.append(container); 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.origin)); var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) return animator .translate3d(dialogEl, from, to, translateOptions) .then(function(animateReversal) { // Build a reversal translate function synched to this translation... options.reverseAnimate = function() { delete options.reverseAnimate; return animateReversal( animator.toTransformCss( // in case the origin element has moved or is hidden, // let's recalculate the translateCSS buildTranslateToOrigin(dialogEl, options.origin) ) ); }; return true; }); } /** * Dialog close and pop-out animation */ function dialogPopOut(container, options) { return options.reverseAnimate(); } /** * Utility function to filter out raw DOM nodes */ function isNodeOneOf(elem, nodeTypeArray) { if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { return true; } } } } MdDialogProvider.$inject = ["$$interimElementProvider"]; ng.material.components.dialog = angular.module("material.components.dialog");