You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1294 lines
46 KiB

  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.1
  6. */
  7. (function( window, angular, undefined ){
  8. "use strict";
  9. /**
  10. * @ngdoc module
  11. * @name material.components.dialog
  12. */
  13. MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
  14. MdDialogProvider.$inject = ["$$interimElementProvider"];
  15. angular
  16. .module('material.components.dialog', [
  17. 'material.core',
  18. 'material.components.backdrop'
  19. ])
  20. .directive('mdDialog', MdDialogDirective)
  21. .provider('$mdDialog', MdDialogProvider);
  22. /**
  23. * @ngdoc directive
  24. * @name mdDialog
  25. * @module material.components.dialog
  26. *
  27. * @restrict E
  28. *
  29. * @description
  30. * `<md-dialog>` - The dialog's template must be inside this element.
  31. *
  32. * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
  33. * an `<md-dialog-actions>` element for the dialog's actions.
  34. *
  35. * ## CSS
  36. * - `.md-dialog-content` - class that sets the padding on the content as the spec file
  37. *
  38. * ## Notes
  39. * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
  40. * prefixed with `dialogContent_`.
  41. *
  42. * @usage
  43. * ### Dialog template
  44. * <hljs lang="html">
  45. * <md-dialog aria-label="List dialog">
  46. * <md-dialog-content>
  47. * <md-list>
  48. * <md-list-item ng-repeat="item in items">
  49. * <p>Number {{item}}</p>
  50. * </md-list-item>
  51. * </md-list>
  52. * </md-dialog-content>
  53. * <md-dialog-actions>
  54. * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
  55. * </md-dialog-actions>
  56. * </md-dialog>
  57. * </hljs>
  58. */
  59. function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
  60. return {
  61. restrict: 'E',
  62. link: function(scope, element) {
  63. element.addClass('_md'); // private md component indicator for styling
  64. $mdTheming(element);
  65. $$rAF(function() {
  66. var images;
  67. var content = element[0].querySelector('md-dialog-content');
  68. if (content) {
  69. images = content.getElementsByTagName('img');
  70. addOverflowClass();
  71. //-- delayed image loading may impact scroll height, check after images are loaded
  72. angular.element(images).on('load', addOverflowClass);
  73. }
  74. scope.$on('$destroy', function() {
  75. $mdDialog.destroy(element);
  76. });
  77. /**
  78. *
  79. */
  80. function addOverflowClass() {
  81. element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
  82. }
  83. });
  84. }
  85. };
  86. }
  87. /**
  88. * @ngdoc service
  89. * @name $mdDialog
  90. * @module material.components.dialog
  91. *
  92. * @description
  93. * `$mdDialog` opens a dialog over the app to inform users about critical information or require
  94. * them to make decisions. There are two approaches for setup: a simple promise API
  95. * and regular object syntax.
  96. *
  97. * ## Restrictions
  98. *
  99. * - The dialog is always given an isolate scope.
  100. * - The dialog's template must have an outer `<md-dialog>` element.
  101. * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
  102. * an `<md-dialog-actions>` element for the dialog's actions.
  103. * - Dialogs must cover the entire application to keep interactions inside of them.
  104. * Use the `parent` option to change where dialogs are appended.
  105. *
  106. * ## Sizing
  107. * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
  108. * - Default max-width is 80% of the `rootElement` or `parent`.
  109. *
  110. * ## CSS
  111. * - `.md-dialog-content` - class that sets the padding on the content as the spec file
  112. *
  113. * @usage
  114. * <hljs lang="html">
  115. * <div ng-app="demoApp" ng-controller="EmployeeController">
  116. * <div>
  117. * <md-button ng-click="showAlert()" class="md-raised md-warn">
  118. * Employee Alert!
  119. * </md-button>
  120. * </div>
  121. * <div>
  122. * <md-button ng-click="showDialog($event)" class="md-raised">
  123. * Custom Dialog
  124. * </md-button>
  125. * </div>
  126. * <div>
  127. * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
  128. * Close Alert
  129. * </md-button>
  130. * </div>
  131. * <div>
  132. * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
  133. * Greet Employee
  134. * </md-button>
  135. * </div>
  136. * </div>
  137. * </hljs>
  138. *
  139. * ### JavaScript: object syntax
  140. * <hljs lang="js">
  141. * (function(angular, undefined){
  142. * "use strict";
  143. *
  144. * angular
  145. * .module('demoApp', ['ngMaterial'])
  146. * .controller('AppCtrl', AppController);
  147. *
  148. * function AppController($scope, $mdDialog) {
  149. * var alert;
  150. * $scope.showAlert = showAlert;
  151. * $scope.showDialog = showDialog;
  152. * $scope.items = [1, 2, 3];
  153. *
  154. * // Internal method
  155. * function showAlert() {
  156. * alert = $mdDialog.alert({
  157. * title: 'Attention',
  158. * textContent: 'This is an example of how easy dialogs can be!',
  159. * ok: 'Close'
  160. * });
  161. *
  162. * $mdDialog
  163. * .show( alert )
  164. * .finally(function() {
  165. * alert = undefined;
  166. * });
  167. * }
  168. *
  169. * function showDialog($event) {
  170. * var parentEl = angular.element(document.body);
  171. * $mdDialog.show({
  172. * parent: parentEl,
  173. * targetEvent: $event,
  174. * template:
  175. * '<md-dialog aria-label="List dialog">' +
  176. * ' <md-dialog-content>'+
  177. * ' <md-list>'+
  178. * ' <md-list-item ng-repeat="item in items">'+
  179. * ' <p>Number {{item}}</p>' +
  180. * ' </md-item>'+
  181. * ' </md-list>'+
  182. * ' </md-dialog-content>' +
  183. * ' <md-dialog-actions>' +
  184. * ' <md-button ng-click="closeDialog()" class="md-primary">' +
  185. * ' Close Dialog' +
  186. * ' </md-button>' +
  187. * ' </md-dialog-actions>' +
  188. * '</md-dialog>',
  189. * locals: {
  190. * items: $scope.items
  191. * },
  192. * controller: DialogController
  193. * });
  194. * function DialogController($scope, $mdDialog, items) {
  195. * $scope.items = items;
  196. * $scope.closeDialog = function() {
  197. * $mdDialog.hide();
  198. * }
  199. * }
  200. * }
  201. * }
  202. * })(angular);
  203. * </hljs>
  204. *
  205. * ### Pre-Rendered Dialogs
  206. * By using the `contentElement` option, it is possible to use an already existing element in the DOM.
  207. *
  208. * <hljs lang="js">
  209. * $scope.showPrerenderedDialog = function() {
  210. * $mdDialog.show({
  211. * contentElement: '#myStaticDialog',
  212. * parent: angular.element(document.body)
  213. * });
  214. * };
  215. * </hljs>
  216. *
  217. * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
  218. *
  219. * <hljs lang="html">
  220. * <div style="visibility: hidden">
  221. * <div class="md-dialog-container" id="myStaticDialog">
  222. * <md-dialog>
  223. * This is a pre-rendered dialog.
  224. * </md-dialog>
  225. * </div>
  226. * </div>
  227. * </hljs>
  228. *
  229. * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
  230. * will not show up.
  231. *
  232. * It also possible to use a DOM element for the `contentElement` option.
  233. * - `contentElement: document.querySelector('#myStaticDialog')`
  234. * - `contentElement: angular.element(TEMPLATE)`
  235. *
  236. * When using a `template` as content element, it will be not compiled upon open.
  237. * This allows you to compile the element yourself and use it each time the dialog opens.
  238. *
  239. * ### Custom Presets
  240. * Developers are also able to create their own preset, which can be easily used without repeating
  241. * their options each time.
  242. *
  243. * <hljs lang="js">
  244. * $mdDialogProvider.addPreset('testPreset', {
  245. * options: function() {
  246. * return {
  247. * template:
  248. * '<md-dialog>' +
  249. * 'This is a custom preset' +
  250. * '</md-dialog>',
  251. * controllerAs: 'dialog',
  252. * bindToController: true,
  253. * clickOutsideToClose: true,
  254. * escapeToClose: true
  255. * };
  256. * }
  257. * });
  258. * </hljs>
  259. *
  260. * After you created your preset at config phase, you can easily access it.
  261. *
  262. * <hljs lang="js">
  263. * $mdDialog.show(
  264. * $mdDialog.testPreset()
  265. * );
  266. * </hljs>
  267. *
  268. * ### JavaScript: promise API syntax, custom dialog template
  269. * <hljs lang="js">
  270. * (function(angular, undefined){
  271. * "use strict";
  272. *
  273. * angular
  274. * .module('demoApp', ['ngMaterial'])
  275. * .controller('EmployeeController', EmployeeEditor)
  276. * .controller('GreetingController', GreetingController);
  277. *
  278. * // Fictitious Employee Editor to show how to use simple and complex dialogs.
  279. *
  280. * function EmployeeEditor($scope, $mdDialog) {
  281. * var alert;
  282. *
  283. * $scope.showAlert = showAlert;
  284. * $scope.closeAlert = closeAlert;
  285. * $scope.showGreeting = showCustomGreeting;
  286. *
  287. * $scope.hasAlert = function() { return !!alert };
  288. * $scope.userName = $scope.userName || 'Bobby';
  289. *
  290. * // Dialog #1 - Show simple alert dialog and cache
  291. * // reference to dialog instance
  292. *
  293. * function showAlert() {
  294. * alert = $mdDialog.alert()
  295. * .title('Attention, ' + $scope.userName)
  296. * .textContent('This is an example of how easy dialogs can be!')
  297. * .ok('Close');
  298. *
  299. * $mdDialog
  300. * .show( alert )
  301. * .finally(function() {
  302. * alert = undefined;
  303. * });
  304. * }
  305. *
  306. * // Close the specified dialog instance and resolve with 'finished' flag
  307. * // Normally this is not needed, just use '$mdDialog.hide()' to close
  308. * // the most recent dialog popup.
  309. *
  310. * function closeAlert() {
  311. * $mdDialog.hide( alert, "finished" );
  312. * alert = undefined;
  313. * }
  314. *
  315. * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
  316. *
  317. * function showCustomGreeting($event) {
  318. * $mdDialog.show({
  319. * targetEvent: $event,
  320. * template:
  321. * '<md-dialog>' +
  322. *
  323. * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
  324. *
  325. * ' <md-dialog-actions>' +
  326. * ' <md-button ng-click="closeDialog()" class="md-primary">' +
  327. * ' Close Greeting' +
  328. * ' </md-button>' +
  329. * ' </md-dialog-actions>' +
  330. * '</md-dialog>',
  331. * controller: 'GreetingController',
  332. * onComplete: afterShowAnimation,
  333. * locals: { employee: $scope.userName }
  334. * });
  335. *
  336. * // When the 'enter' animation finishes...
  337. *
  338. * function afterShowAnimation(scope, element, options) {
  339. * // post-show code here: DOM element focus, etc.
  340. * }
  341. * }
  342. *
  343. * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
  344. * // Here we used ng-controller="GreetingController as vm" and
  345. * // $scope.vm === <controller instance>
  346. *
  347. * function showCustomGreeting() {
  348. *
  349. * $mdDialog.show({
  350. * clickOutsideToClose: true,
  351. *
  352. * scope: $scope, // use parent scope in template
  353. * preserveScope: true, // do not forget this if use parent scope
  354. * // Since GreetingController is instantiated with ControllerAs syntax
  355. * // AND we are passing the parent '$scope' to the dialog, we MUST
  356. * // use 'vm.<xxx>' in the template markup
  357. *
  358. * template: '<md-dialog>' +
  359. * ' <md-dialog-content>' +
  360. * ' Hi There {{vm.employee}}' +
  361. * ' </md-dialog-content>' +
  362. * '</md-dialog>',
  363. *
  364. * controller: function DialogController($scope, $mdDialog) {
  365. * $scope.closeDialog = function() {
  366. * $mdDialog.hide();
  367. * }
  368. * }
  369. * });
  370. * }
  371. *
  372. * }
  373. *
  374. * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
  375. *
  376. * function GreetingController($scope, $mdDialog, employee) {
  377. * // Assigned from construction <code>locals</code> options...
  378. * $scope.employee = employee;
  379. *
  380. * $scope.closeDialog = function() {
  381. * // Easily hides most recent dialog shown...
  382. * // no specific instance reference is needed.
  383. * $mdDialog.hide();
  384. * };
  385. * }
  386. *
  387. * })(angular);
  388. * </hljs>
  389. */
  390. /**
  391. * @ngdoc method
  392. * @name $mdDialog#alert
  393. *
  394. * @description
  395. * Builds a preconfigured dialog with the specified message.
  396. *
  397. * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  398. *
  399. * - $mdDialogPreset#title(string) - Sets the alert title.
  400. * - $mdDialogPreset#textContent(string) - Sets the alert message.
  401. * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize
  402. * module to be loaded. HTML is not run through Angular's compiler.
  403. * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text.
  404. * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog.
  405. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  406. * the location of the click will be used as the starting point for the opening animation
  407. * of the the dialog.
  408. *
  409. */
  410. /**
  411. * @ngdoc method
  412. * @name $mdDialog#confirm
  413. *
  414. * @description
  415. * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
  416. * will be resolved only if the user clicks the confirm action on the dialog.
  417. *
  418. * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  419. *
  420. * Additionally, it supports the following methods:
  421. *
  422. * - $mdDialogPreset#title(string) - Sets the confirm title.
  423. * - $mdDialogPreset#textContent(string) - Sets the confirm message.
  424. * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize
  425. * module to be loaded. HTML is not run through Angular's compiler.
  426. * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text.
  427. * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text.
  428. * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog.
  429. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  430. * the location of the click will be used as the starting point for the opening animation
  431. * of the the dialog.
  432. *
  433. */
  434. /**
  435. * @ngdoc method
  436. * @name $mdDialog#prompt
  437. *
  438. * @description
  439. * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned
  440. * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument.
  441. *
  442. * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  443. *
  444. * Additionally, it supports the following methods:
  445. *
  446. * - $mdDialogPreset#title(string) - Sets the prompt title.
  447. * - $mdDialogPreset#textContent(string) - Sets the prompt message.
  448. * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize
  449. * module to be loaded. HTML is not run through Angular's compiler.
  450. * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input.
  451. * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input.
  452. * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text.
  453. * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text.
  454. * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog.
  455. * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  456. * the location of the click will be used as the starting point for the opening animation
  457. * of the the dialog.
  458. *
  459. */
  460. /**
  461. * @ngdoc method
  462. * @name $mdDialog#show
  463. *
  464. * @description
  465. * Show a dialog with the specified options.
  466. *
  467. * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
  468. * `confirm()`, or an options object with the following properties:
  469. * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
  470. * of the dialog.
  471. * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
  472. * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
  473. * This template should **never** be constructed with any kind of user input or user data.
  474. * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
  475. * dialog opens, you can also use a DOM element.<br/>
  476. * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
  477. * the dialog and restores it at the old DOM position upon close.
  478. * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
  479. * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
  480. * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
  481. * custom dialog directive.
  482. * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
  483. * the location of the click will be used as the starting point for the opening animation
  484. * of the the dialog.
  485. * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object
  486. * that is used to determine the bounds (top, left, height, width) from which the Dialog will
  487. * originate.
  488. * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object
  489. * that is used to determine the bounds (top, left, height, width) to which the Dialog will
  490. * target.
  491. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
  492. * it will create a new isolate scope.
  493. * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
  494. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
  495. * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
  496. * Default true.
  497. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
  498. * Default true.
  499. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
  500. * close it. Default false.
  501. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
  502. * Default true.
  503. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
  504. * focusing some other way, as focus management is required for dialogs to be accessible.
  505. * Defaults to true.
  506. * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller
  507. * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
  508. * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
  509. * of values to inject into the controller. For example, `locals: {three: 3}` would inject
  510. * `three` into the controller, with the value 3. If `bindToController` is true, they will be
  511. * copied to the controller instead.
  512. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
  513. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
  514. * dialog will not open until all of the promises resolve.
  515. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
  516. * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
  517. * to the root element of the application.
  518. * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is
  519. * starting.
  520. * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is
  521. * finished.
  522. * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the
  523. * close/hide() action is starting. This allows developers to run custom animations
  524. * in parallel the close animations.
  525. * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen
  526. * or not. Defaults to `false`.
  527. * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
  528. * rejected with `$mdDialog.cancel()`.
  529. */
  530. /**
  531. * @ngdoc method
  532. * @name $mdDialog#hide
  533. *
  534. * @description
  535. * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
  536. *
  537. * @param {*=} response An argument for the resolved promise.
  538. *
  539. * @returns {promise} A promise that is resolved when the dialog has been closed.
  540. */
  541. /**
  542. * @ngdoc method
  543. * @name $mdDialog#cancel
  544. *
  545. * @description
  546. * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
  547. *
  548. * @param {*=} response An argument for the rejected promise.
  549. *
  550. * @returns {promise} A promise that is resolved when the dialog has been closed.
  551. */
  552. function MdDialogProvider($$interimElementProvider) {
  553. // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
  554. advancedDialogOptions.$inject = ["$mdDialog", "$mdConstant"];
  555. dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming"];
  556. var topFocusTrap, bottomFocusTrap;
  557. return $$interimElementProvider('$mdDialog')
  558. .setDefaults({
  559. methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
  560. 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'contentElement'],
  561. options: dialogDefaultOptions
  562. })
  563. .addPreset('alert', {
  564. methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme',
  565. 'css'],
  566. options: advancedDialogOptions
  567. })
  568. .addPreset('confirm', {
  569. methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel',
  570. 'theme', 'css'],
  571. options: advancedDialogOptions
  572. })
  573. .addPreset('prompt', {
  574. methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel',
  575. 'ok', 'cancel', 'theme', 'css'],
  576. options: advancedDialogOptions
  577. });
  578. /* ngInject */
  579. function advancedDialogOptions($mdDialog, $mdConstant) {
  580. return {
  581. template: [
  582. '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
  583. ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
  584. ' <h2 class="md-title">{{ dialog.title }}</h2>',
  585. ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
  586. ' ng-bind-html="::dialog.mdHtmlContent"></div>',
  587. ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
  588. ' <p>{{::dialog.mdTextContent}}</p>',
  589. ' </div>',
  590. ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
  591. ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' +
  592. ' placeholder="{{::dialog.placeholder}}">',
  593. ' </md-input-container>',
  594. ' </md-dialog-content>',
  595. ' <md-dialog-actions>',
  596. ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
  597. ' ng-click="dialog.abort()" class="md-primary md-cancel-button">',
  598. ' {{ dialog.cancel }}',
  599. ' </md-button>',
  600. ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'">',
  601. ' {{ dialog.ok }}',
  602. ' </md-button>',
  603. ' </md-dialog-actions>',
  604. '</md-dialog>'
  605. ].join('').replace(/\s\s+/g, ''),
  606. controller: function mdDialogCtrl() {
  607. var isPrompt = this.$type == 'prompt';
  608. if (isPrompt && this.initialValue) {
  609. this.result = this.initialValue;
  610. }
  611. this.hide = function() {
  612. $mdDialog.hide(isPrompt ? this.result : true);
  613. };
  614. this.abort = function() {
  615. $mdDialog.cancel();
  616. };
  617. this.keypress = function($event) {
  618. if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
  619. $mdDialog.hide(this.result);
  620. }
  621. };
  622. },
  623. controllerAs: 'dialog',
  624. bindToController: true,
  625. };
  626. }
  627. /* ngInject */
  628. function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement,
  629. $log, $injector, $mdTheming) {
  630. return {
  631. hasBackdrop: true,
  632. isolateScope: true,
  633. onCompiling: beforeCompile,
  634. onShow: onShow,
  635. onShowing: beforeShow,
  636. onRemove: onRemove,
  637. clickOutsideToClose: false,
  638. escapeToClose: true,
  639. targetEvent: null,
  640. contentElement: null,
  641. closeTo: null,
  642. openFrom: null,
  643. focusOnOpen: true,
  644. disableParentScroll: true,
  645. autoWrap: true,
  646. fullscreen: false,
  647. transformTemplate: function(template, options) {
  648. // Make the dialog container focusable, because otherwise the focus will be always redirected to
  649. // an element outside of the container, and the focus trap won't work probably..
  650. // Also the tabindex is needed for the `escapeToClose` functionality, because
  651. // the keyDown event can't be triggered when the focus is outside of the container.
  652. return '<div class="md-dialog-container" tabindex="-1">' + validatedTemplate(template) + '</div>';
  653. /**
  654. * The specified template should contain a <md-dialog> wrapper element....
  655. */
  656. function validatedTemplate(template) {
  657. if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
  658. return '<md-dialog>' + (template || '') + '</md-dialog>';
  659. } else {
  660. return template || '';
  661. }
  662. }
  663. }
  664. };
  665. function beforeCompile(options) {
  666. // Automatically apply the theme, if the user didn't specify a theme explicitly.
  667. // Those option changes need to be done, before the compilation has started, because otherwise
  668. // the option changes will be not available in the $mdCompilers locales.
  669. detectTheming(options);
  670. if (options.contentElement) {
  671. options.restoreContentElement = installContentElement(options);
  672. }
  673. }
  674. function beforeShow(scope, element, options, controller) {
  675. if (controller) {
  676. controller.mdHtmlContent = controller.htmlContent || options.htmlContent || '';
  677. controller.mdTextContent = controller.textContent || options.textContent ||
  678. controller.content || options.content || '';
  679. if (controller.mdHtmlContent && !$injector.has('$sanitize')) {
  680. throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
  681. }
  682. if (controller.mdHtmlContent && controller.mdTextContent) {
  683. throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
  684. }
  685. }
  686. }
  687. /** Show method for dialogs */
  688. function onShow(scope, element, options, controller) {
  689. angular.element($document[0].body).addClass('md-dialog-is-showing');
  690. var dialogElement = element.find('md-dialog');
  691. // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly.
  692. // This is a very common problem, so we have to notify the developer about this.
  693. if (dialogElement.hasClass('ng-cloak')) {
  694. var message = '$mdDialog: using `<md-dialog ng-cloak >` will affect the dialog opening animations.';
  695. $log.warn( message, element[0] );
  696. }
  697. captureParentAndFromToElements(options);
  698. configureAria(dialogElement, options);
  699. showBackdrop(scope, element, options);
  700. activateListeners(element, options);
  701. return dialogPopIn(element, options)
  702. .then(function() {
  703. lockScreenReader(element, options);
  704. warnDeprecatedActions();
  705. focusOnOpen();
  706. });
  707. /**
  708. * Check to see if they used the deprecated .md-actions class and log a warning
  709. */
  710. function warnDeprecatedActions() {
  711. if (element[0].querySelector('.md-actions')) {
  712. $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.');
  713. }
  714. }
  715. /**
  716. * For alerts, focus on content... otherwise focus on
  717. * the close button (or equivalent)
  718. */
  719. function focusOnOpen() {
  720. if (options.focusOnOpen) {
  721. var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement;
  722. target.focus();
  723. }
  724. /**
  725. * If no element with class dialog-close, try to find the last
  726. * button child in md-actions and assume it is a close button.
  727. *
  728. * If we find no actions at all, log a warning to the console.
  729. */
  730. function findCloseButton() {
  731. var closeButton = element[0].querySelector('.dialog-close');
  732. if (!closeButton) {
  733. var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button');
  734. closeButton = actionButtons[actionButtons.length - 1];
  735. }
  736. return closeButton;
  737. }
  738. }
  739. }
  740. /**
  741. * Remove function for all dialogs
  742. */
  743. function onRemove(scope, element, options) {
  744. options.deactivateListeners();
  745. options.unlockScreenReader();
  746. options.hideBackdrop(options.$destroy);
  747. // Remove the focus traps that we added earlier for keeping focus within the dialog.
  748. if (topFocusTrap && topFocusTrap.parentNode) {
  749. topFocusTrap.parentNode.removeChild(topFocusTrap);
  750. }
  751. if (bottomFocusTrap && bottomFocusTrap.parentNode) {
  752. bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
  753. }
  754. // For navigation $destroy events, do a quick, non-animated removal,
  755. // but for normal closes (from clicks, etc) animate the removal
  756. return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
  757. /**
  758. * For normal closes, animate the removal.
  759. * For forced closes (like $destroy events), skip the animations
  760. */
  761. function animateRemoval() {
  762. return dialogPopOut(element, options);
  763. }
  764. /**
  765. * Detach the element
  766. */
  767. function detachAndClean() {
  768. angular.element($document[0].body).removeClass('md-dialog-is-showing');
  769. // Only remove the element, if it's not provided through the contentElement option.
  770. if (!options.contentElement) {
  771. element.remove();
  772. } else {
  773. options.reverseContainerStretch();
  774. options.restoreContentElement();
  775. }
  776. if (!options.$destroy) options.origin.focus();
  777. }
  778. }
  779. function detectTheming(options) {
  780. // Only detect the theming, if the developer didn't specify the theme specifically.
  781. if (options.theme) return;
  782. options.theme = $mdTheming.defaultTheme();
  783. if (options.targetEvent && options.targetEvent.target) {
  784. var targetEl = angular.element(options.targetEvent.target);
  785. // Once the user specifies a targetEvent, we will automatically try to find the correct
  786. // nested theme.
  787. options.theme = (targetEl.controller('mdTheme') || {}).$mdTheme || options.theme;
  788. }
  789. }
  790. /**
  791. * Installs a content element to the current $$interimElement provider options.
  792. * @returns {Function} Function to restore the content element at its old DOM location.
  793. */
  794. function installContentElement(options) {
  795. var contentEl = options.contentElement;
  796. var restoreFn = null;
  797. if (angular.isString(contentEl)) {
  798. contentEl = document.querySelector(contentEl);
  799. restoreFn = createRestoreFn(contentEl);
  800. } else {
  801. contentEl = contentEl[0] || contentEl;
  802. // When the element is visible in the DOM, then we restore it at close of the dialog.
  803. // Otherwise it will be removed from the DOM after close.
  804. if (document.contains(contentEl)) {
  805. restoreFn = createRestoreFn(contentEl);
  806. } else {
  807. restoreFn = function() {
  808. contentEl.parentNode.removeChild(contentEl);
  809. }
  810. }
  811. }
  812. // Overwrite the options to use the content element.
  813. options.element = angular.element(contentEl);
  814. options.skipCompile = true;
  815. return restoreFn;
  816. function createRestoreFn(element) {
  817. var parent = element.parentNode;
  818. var nextSibling = element.nextElementSibling;
  819. return function() {
  820. if (!nextSibling) {
  821. // When the element didn't had any sibling, then it can be simply appended to the
  822. // parent, because it plays no role, which index it had before.
  823. parent.appendChild(element);
  824. } else {
  825. // When the element had a sibling, which marks the previous position of the element
  826. // in the DOM, we insert it correctly before the sibling, to have the same index as
  827. // before.
  828. parent.insertBefore(element, nextSibling);
  829. }
  830. }
  831. }
  832. }
  833. /**
  834. * Capture originator/trigger/from/to element information (if available)
  835. * and the parent container for the dialog; defaults to the $rootElement
  836. * unless overridden in the options.parent
  837. */
  838. function captureParentAndFromToElements(options) {
  839. options.origin = angular.extend({
  840. element: null,
  841. bounds: null,
  842. focus: angular.noop
  843. }, options.origin || {});
  844. options.parent = getDomElement(options.parent, $rootElement);
  845. options.closeTo = getBoundingClientRect(getDomElement(options.closeTo));
  846. options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
  847. if ( options.targetEvent ) {
  848. options.origin = getBoundingClientRect(options.targetEvent.target, options.origin);
  849. }
  850. /**
  851. * Identify the bounding RECT for the target element
  852. *
  853. */
  854. function getBoundingClientRect (element, orig) {
  855. var source = angular.element((element || {}));
  856. if (source && source.length) {
  857. // Compute and save the target element's bounding rect, so that if the
  858. // element is hidden when the dialog closes, we can shrink the dialog
  859. // back to the same position it expanded from.
  860. //
  861. // Checking if the source is a rect object or a DOM element
  862. var bounds = {top:0,left:0,height:0,width:0};
  863. var hasFn = angular.isFunction(source[0].getBoundingClientRect);
  864. return angular.extend(orig || {}, {
  865. element : hasFn ? source : undefined,
  866. bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]),
  867. focus : angular.bind(source, source.focus),
  868. });
  869. }
  870. }
  871. /**
  872. * If the specifier is a simple string selector, then query for
  873. * the DOM element.
  874. */
  875. function getDomElement(element, defaultElement) {
  876. if (angular.isString(element)) {
  877. element = $document[0].querySelector(element);
  878. }
  879. // If we have a reference to a raw dom element, always wrap it in jqLite
  880. return angular.element(element || defaultElement);
  881. }
  882. }
  883. /**
  884. * Listen for escape keys and outside clicks to auto close
  885. */
  886. function activateListeners(element, options) {
  887. var window = angular.element($window);
  888. var onWindowResize = $mdUtil.debounce(function() {
  889. stretchDialogContainerToViewport(element, options);
  890. }, 60);
  891. var removeListeners = [];
  892. var smartClose = function() {
  893. // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
  894. // cancel or fallback to hide.
  895. var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
  896. $mdUtil.nextTick(closeFn, true);
  897. };
  898. if (options.escapeToClose) {
  899. var parentTarget = options.parent;
  900. var keyHandlerFn = function(ev) {
  901. if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
  902. ev.stopPropagation();
  903. ev.preventDefault();
  904. smartClose();
  905. }
  906. };
  907. // Add keydown listeners
  908. element.on('keydown', keyHandlerFn);
  909. parentTarget.on('keydown', keyHandlerFn);
  910. // Queue remove listeners function
  911. removeListeners.push(function() {
  912. element.off('keydown', keyHandlerFn);
  913. parentTarget.off('keydown', keyHandlerFn);
  914. });
  915. }
  916. // Register listener to update dialog on window resize
  917. window.on('resize', onWindowResize);
  918. removeListeners.push(function() {
  919. window.off('resize', onWindowResize);
  920. });
  921. if (options.clickOutsideToClose) {
  922. var target = element;
  923. var sourceElem;
  924. // Keep track of the element on which the mouse originally went down
  925. // so that we can only close the backdrop when the 'click' started on it.
  926. // A simple 'click' handler does not work,
  927. // it sets the target object as the element the mouse went down on.
  928. var mousedownHandler = function(ev) {
  929. sourceElem = ev.target;
  930. };
  931. // We check if our original element and the target is the backdrop
  932. // because if the original was the backdrop and the target was inside the dialog
  933. // we don't want to dialog to close.
  934. var mouseupHandler = function(ev) {
  935. if (sourceElem === target[0] && ev.target === target[0]) {
  936. ev.stopPropagation();
  937. ev.preventDefault();
  938. smartClose();
  939. }
  940. };
  941. // Add listeners
  942. target.on('mousedown', mousedownHandler);
  943. target.on('mouseup', mouseupHandler);
  944. // Queue remove listeners function
  945. removeListeners.push(function() {
  946. target.off('mousedown', mousedownHandler);
  947. target.off('mouseup', mouseupHandler);
  948. });
  949. }
  950. // Attach specific `remove` listener handler
  951. options.deactivateListeners = function() {
  952. removeListeners.forEach(function(removeFn) {
  953. removeFn();
  954. });
  955. options.deactivateListeners = null;
  956. };
  957. }
  958. /**
  959. * Show modal backdrop element...
  960. */
  961. function showBackdrop(scope, element, options) {
  962. if (options.disableParentScroll) {
  963. // !! DO this before creating the backdrop; since disableScrollAround()
  964. // configures the scroll offset; which is used by mdBackDrop postLink()
  965. options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
  966. }
  967. if (options.hasBackdrop) {
  968. options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
  969. $animate.enter(options.backdrop, options.parent);
  970. }
  971. /**
  972. * Hide modal backdrop element...
  973. */
  974. options.hideBackdrop = function hideBackdrop($destroy) {
  975. if (options.backdrop) {
  976. if ( !!$destroy ) options.backdrop.remove();
  977. else $animate.leave(options.backdrop);
  978. }
  979. if (options.disableParentScroll) {
  980. options.restoreScroll();
  981. delete options.restoreScroll;
  982. }
  983. options.hideBackdrop = null;
  984. };
  985. }
  986. /**
  987. * Inject ARIA-specific attributes appropriate for Dialogs
  988. */
  989. function configureAria(element, options) {
  990. var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
  991. var dialogContent = element.find('md-dialog-content');
  992. var existingDialogId = element.attr('id');
  993. var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid());
  994. element.attr({
  995. 'role': role,
  996. 'tabIndex': '-1'
  997. });
  998. if (dialogContent.length === 0) {
  999. dialogContent = element;
  1000. // If the dialog element already had an ID, don't clobber it.
  1001. if (existingDialogId) {
  1002. dialogContentId = existingDialogId;
  1003. }
  1004. }
  1005. dialogContent.attr('id', dialogContentId);
  1006. element.attr('aria-describedby', dialogContentId);
  1007. if (options.ariaLabel) {
  1008. $mdAria.expect(element, 'aria-label', options.ariaLabel);
  1009. }
  1010. else {
  1011. $mdAria.expectAsync(element, 'aria-label', function() {
  1012. var words = dialogContent.text().split(/\s+/);
  1013. if (words.length > 3) words = words.slice(0, 3).concat('...');
  1014. return words.join(' ');
  1015. });
  1016. }
  1017. // Set up elements before and after the dialog content to capture focus and
  1018. // redirect back into the dialog.
  1019. topFocusTrap = document.createElement('div');
  1020. topFocusTrap.classList.add('md-dialog-focus-trap');
  1021. topFocusTrap.tabIndex = 0;
  1022. bottomFocusTrap = topFocusTrap.cloneNode(false);
  1023. // When focus is about to move out of the dialog, we want to intercept it and redirect it
  1024. // back to the dialog element.
  1025. var focusHandler = function() {
  1026. element.focus();
  1027. };
  1028. topFocusTrap.addEventListener('focus', focusHandler);
  1029. bottomFocusTrap.addEventListener('focus', focusHandler);
  1030. // The top focus trap inserted immeidately before the md-dialog element (as a sibling).
  1031. // The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
  1032. element[0].parentNode.insertBefore(topFocusTrap, element[0]);
  1033. element.after(bottomFocusTrap);
  1034. }
  1035. /**
  1036. * Prevents screen reader interaction behind modal window
  1037. * on swipe interfaces
  1038. */
  1039. function lockScreenReader(element, options) {
  1040. var isHidden = true;
  1041. // get raw DOM node
  1042. walkDOM(element[0]);
  1043. options.unlockScreenReader = function() {
  1044. isHidden = false;
  1045. walkDOM(element[0]);
  1046. options.unlockScreenReader = null;
  1047. };
  1048. /**
  1049. * Walk DOM to apply or remove aria-hidden on sibling nodes
  1050. * and parent sibling nodes
  1051. *
  1052. */
  1053. function walkDOM(element) {
  1054. while (element.parentNode) {
  1055. if (element === document.body) {
  1056. return;
  1057. }
  1058. var children = element.parentNode.children;
  1059. for (var i = 0; i < children.length; i++) {
  1060. // skip over child if it is an ascendant of the dialog
  1061. // or a script or style tag
  1062. if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
  1063. children[i].setAttribute('aria-hidden', isHidden);
  1064. }
  1065. }
  1066. walkDOM(element = element.parentNode);
  1067. }
  1068. }
  1069. }
  1070. /**
  1071. * Ensure the dialog container fill-stretches to the viewport
  1072. */
  1073. function stretchDialogContainerToViewport(container, options) {
  1074. var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
  1075. var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
  1076. var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
  1077. var previousStyles = {
  1078. top: container.css('top'),
  1079. height: container.css('height')
  1080. };
  1081. container.css({
  1082. top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
  1083. height: height ? height + 'px' : '100%'
  1084. });
  1085. return function() {
  1086. // Reverts the modified styles back to the previous values.
  1087. // This is needed for contentElements, which should have the same styles after close
  1088. // as before.
  1089. container.css(previousStyles);
  1090. };
  1091. }
  1092. /**
  1093. * Dialog open and pop-in animation
  1094. */
  1095. function dialogPopIn(container, options) {
  1096. // Add the `md-dialog-container` to the DOM
  1097. options.parent.append(container);
  1098. options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
  1099. var dialogEl = container.find('md-dialog');
  1100. var animator = $mdUtil.dom.animator;
  1101. var buildTranslateToOrigin = animator.calculateZoomToOrigin;
  1102. var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
  1103. var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin));
  1104. var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
  1105. dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen);
  1106. return animator
  1107. .translate3d(dialogEl, from, to, translateOptions)
  1108. .then(function(animateReversal) {
  1109. // Build a reversal translate function synced to this translation...
  1110. options.reverseAnimate = function() {
  1111. delete options.reverseAnimate;
  1112. if (options.closeTo) {
  1113. // Using the opposite classes to create a close animation to the closeTo element
  1114. translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'};
  1115. from = to;
  1116. to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
  1117. return animator
  1118. .translate3d(dialogEl, from, to,translateOptions);
  1119. }
  1120. return animateReversal(
  1121. to = animator.toTransformCss(
  1122. // in case the origin element has moved or is hidden,
  1123. // let's recalculate the translateCSS
  1124. buildTranslateToOrigin(dialogEl, options.origin)
  1125. )
  1126. );
  1127. };
  1128. // Function to revert the generated animation styles on the dialog element.
  1129. // Useful when using a contentElement instead of a template.
  1130. options.clearAnimate = function() {
  1131. delete options.clearAnimate;
  1132. // Remove the transition classes, added from $animateCSS, since those can't be removed
  1133. // by reversely running the animator.
  1134. dialogEl.removeClass([
  1135. translateOptions.transitionOutClass,
  1136. translateOptions.transitionInClass
  1137. ].join(' '));
  1138. // Run the animation reversely to remove the previous added animation styles.
  1139. return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {});
  1140. };
  1141. return true;
  1142. });
  1143. }
  1144. /**
  1145. * Dialog close and pop-out animation
  1146. */
  1147. function dialogPopOut(container, options) {
  1148. return options.reverseAnimate().then(function() {
  1149. if (options.contentElement) {
  1150. // When we use a contentElement, we want the element to be the same as before.
  1151. // That means, that we have to clear all the animation properties, like transform.
  1152. options.clearAnimate();
  1153. }
  1154. });
  1155. }
  1156. /**
  1157. * Utility function to filter out raw DOM nodes
  1158. */
  1159. function isNodeOneOf(elem, nodeTypeArray) {
  1160. if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
  1161. return true;
  1162. }
  1163. }
  1164. }
  1165. }
  1166. })(window, window.angular);