dialog.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. goog.provide('ng.material.components.dialog');
  8. goog.require('ng.material.components.backdrop');
  9. goog.require('ng.material.core');
  10. /**
  11. * @ngdoc module
  12. * @name material.components.dialog
  13. */
  14. angular
  15. .module('material.components.dialog', [
  16. 'material.core',
  17. 'material.components.backdrop'
  18. ])
  19. .directive('mdDialog', MdDialogDirective)
  20. .provider('$mdDialog', MdDialogProvider);
  21. function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
  22. return {
  23. restrict: 'E',
  24. link: function(scope, element, attr) {
  25. $mdTheming(element);
  26. $$rAF(function() {
  27. var images;
  28. var content = element[0].querySelector('md-dialog-content');
  29. if (content) {
  30. images = content.getElementsByTagName('img');
  31. addOverflowClass();
  32. //-- delayed image loading may impact scroll height, check after images are loaded
  33. angular.element(images).on('load', addOverflowClass);
  34. }
  35. scope.$on('$destroy', function() {
  36. $mdDialog.destroy();
  37. });
  38. /**
  39. *
  40. */
  41. function addOverflowClass() {
  42. element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
  43. }
  44. });
  45. }
  46. };
  47. }
  48. MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
  49. /**
  50. * @ngdoc service
  51. * @name $mdDialog
  52. * @module material.components.dialog
  53. *
  54. * @description
  55. * `$mdDialog` opens a dialog over the app to inform users about critical information or require
  56. * them to make decisions. There are two approaches for setup: a simple promise API
  57. * and regular object syntax.
  58. *
  59. * ## Restrictions
  60. *
  61. * - The dialog is always given an isolate scope.
  62. * - The dialog's template must have an outer `<md-dialog>` element.
  63. * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
  64. * an element with class `md-actions` for the dialog's actions.
  65. * - Dialogs must cover the entire application to keep interactions inside of them.
  66. * Use the `parent` option to change where dialogs are appended.
  67. *
  68. * ## Sizing
  69. * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
  70. * - Default max-width is 80% of the `rootElement` or `parent`.
  71. *
  72. * ## Css
  73. * - `.md-dialog-content` - class that sets the padding on the content as the spec file
  74. *
  75. * @usage
  76. * <hljs lang="html">
  77. * <div ng-app="demoApp" ng-controller="EmployeeController">
  78. * <div>
  79. * <md-button ng-click="showAlert()" class="md-raised md-warn">
  80. * Employee Alert!
  81. * </md-button>
  82. * </div>
  83. * <div>
  84. * <md-button ng-click="showDialog($event)" class="md-raised">
  85. * Custom Dialog
  86. * </md-button>
  87. * </div>
  88. * <div>
  89. * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
  90. * Close Alert
  91. * </md-button>
  92. * </div>
  93. * <div>
  94. * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
  95. * Greet Employee
  96. * </md-button>
  97. * </div>
  98. * </div>
  99. * </hljs>
  100. *
  101. * ### JavaScript: object syntax
  102. * <hljs lang="js">
  103. * (function(angular, undefined){
  104. * "use strict";
  105. *
  106. * angular
  107. * .module('demoApp', ['ngMaterial'])
  108. * .controller('AppCtrl', AppController);
  109. *
  110. * function AppController($scope, $mdDialog) {
  111. * var alert;
  112. * $scope.showAlert = showAlert;
  113. * $scope.showDialog = showDialog;
  114. * $scope.items = [1, 2, 3];
  115. *
  116. * // Internal method
  117. * function showAlert() {
  118. * alert = $mdDialog.alert({
  119. * title: 'Attention',
  120. * content: 'This is an example of how easy dialogs can be!',
  121. * ok: 'Close'
  122. * });
  123. *
  124. * $mdDialog
  125. * .show( alert )
  126. * .finally(function() {
  127. * alert = undefined;
  128. * });
  129. * }
  130. *
  131. * function showDialog($event) {
  132. * var parentEl = angular.element(document.body);
  133. * $mdDialog.show({
  134. * parent: parentEl,
  135. * targetEvent: $event,
  136. * template:
  137. * '<md-dialog aria-label="List dialog">' +
  138. * ' <md-dialog-content>'+
  139. * ' <md-list>'+
  140. * ' <md-list-item ng-repeat="item in items">'+
  141. * ' <p>Number {{item}}</p>' +
  142. * ' </md-item>'+
  143. * ' </md-list>'+
  144. * ' </md-dialog-content>' +
  145. * ' <div class="md-actions">' +
  146. * ' <md-button ng-click="closeDialog()" class="md-primary">' +
  147. * ' Close Dialog' +
  148. * ' </md-button>' +
  149. * ' </div>' +
  150. * '</md-dialog>',
  151. * locals: {
  152. * items: $scope.items
  153. * },
  154. * controller: DialogController
  155. * });
  156. * function DialogController($scope, $mdDialog, items) {
  157. * $scope.items = items;
  158. * $scope.closeDialog = function() {
  159. * $mdDialog.hide();
  160. * }
  161. * }
  162. * }
  163. * }
  164. * })(angular);
  165. * </hljs>
  166. *
  167. * ### JavaScript: promise API syntax, custom dialog template
  168. * <hljs lang="js">
  169. * (function(angular, undefined){
  170. * "use strict";
  171. *
  172. * angular
  173. * .module('demoApp', ['ngMaterial'])
  174. * .controller('EmployeeController', EmployeeEditor)
  175. * .controller('GreetingController', GreetingController);
  176. *
  177. * // Fictitious Employee Editor to show how to use simple and complex dialogs.
  178. *
  179. * function EmployeeEditor($scope, $mdDialog) {
  180. * var alert;
  181. *
  182. * $scope.showAlert = showAlert;
  183. * $scope.closeAlert = closeAlert;
  184. * $scope.showGreeting = showCustomGreeting;
  185. *
  186. * $scope.hasAlert = function() { return !!alert };
  187. * $scope.userName = $scope.userName || 'Bobby';
  188. *
  189. * // Dialog #1 - Show simple alert dialog and cache
  190. * // reference to dialog instance
  191. *
  192. * function showAlert() {
  193. * alert = $mdDialog.alert()
  194. * .title('Attention, ' + $scope.userName)
  195. * .content('This is an example of how easy dialogs can be!')
  196. * .ok('Close');
  197. *
  198. * $mdDialog
  199. * .show( alert )
  200. * .finally(function() {
  201. * alert = undefined;
  202. * });
  203. * }
  204. *
  205. * // Close the specified dialog instance and resolve with 'finished' flag
  206. * // Normally this is not needed, just use '$mdDialog.hide()' to close
  207. * // the most recent dialog popup.
  208. *
  209. * function closeAlert() {
  210. * $mdDialog.hide( alert, "finished" );
  211. * alert = undefined;
  212. * }
  213. *
  214. * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
  215. *
  216. * function showCustomGreeting($event) {
  217. * $mdDialog.show({
  218. * targetEvent: $event,
  219. * template:
  220. * '<md-dialog>' +
  221. *
  222. * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
  223. *
  224. * ' <div class="md-actions">' +
  225. * ' <md-button ng-click="closeDialog()" class="md-primary">' +
  226. * ' Close Greeting' +
  227. * ' </md-button>' +
  228. * ' </div>' +
  229. * '</md-dialog>',
  230. * controller: 'GreetingController',
  231. * onComplete: afterShowAnimation,
  232. * locals: { employee: $scope.userName }
  233. * });
  234. *
  235. * // When the 'enter' animation finishes...
  236. *
  237. * function afterShowAnimation(scope, element, options) {
  238. * // post-show code here: DOM element focus, etc.
  239. * }
  240. * }
  241. *
  242. * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
  243. * // Here we used ng-controller="GreetingController as vm" and
  244. * // $scope.vm === <controller instance>
  245. *
  246. * function showCustomGreeting() {
  247. *
  248. * $mdDialog.show({
  249. * clickOutsideToClose: true,
  250. *
  251. * scope: $scope, // use parent scope in template
  252. * preserveScope: true, // do not forget this if use parent scope
  253. * // Since GreetingController is instantiated with ControllerAs syntax
  254. * // AND we are passing the parent '$scope' to the dialog, we MUST
  255. * // use 'vm.<xxx>' in the template markup
  256. *
  257. * template: '<md-dialog>' +
  258. * ' <md-dialog-content>' +
  259. * ' Hi There {{vm.employee}}' +
  260. * ' </md-dialog-content>' +
  261. * '</md-dialog>',
  262. *
  263. * controller: function DialogController($scope, $mdDialog) {
  264. * $scope.closeDialog = function() {
  265. * $mdDialog.hide();
  266. * }
  267. * }
  268. * });
  269. * }
  270. *
  271. * }
  272. *
  273. * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
  274. *
  275. * function GreetingController($scope, $mdDialog, employee) {
  276. * // Assigned from construction <code>locals</code> options...
  277. * $scope.employee = employee;
  278. *
  279. * $scope.closeDialog = function() {
  280. * // Easily hides most recent dialog shown...
  281. * // no specific instance reference is needed.
  282. * $mdDialog.hide();
  283. * };
  284. * }
  285. *
  286. * })(angular);
  287. * </hljs>
  288. */
  289. /**
  290. * @ngdoc method
  291. * @name $mdDialog#alert
  292. *
  293. * @description
  294. * Builds a preconfigured dialog with the specified message.
  295. *
  296. * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  297. *
  298. * - $mdDialogPreset#title(string) - sets title to string
  299. * - $mdDialogPreset#content(string) - sets content / message to string
  300. * - $mdDialogPreset#ok(string) - sets okay button text to string
  301. * - $mdDialogPreset#theme(string) - sets the theme of the dialog
  302. *
  303. */
  304. /**
  305. * @ngdoc method
  306. * @name $mdDialog#confirm
  307. *
  308. * @description
  309. * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
  310. * will be resolved only if the user clicks the confirm action on the dialog.
  311. *
  312. * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  313. *
  314. * Additionally, it supports the following methods:
  315. *
  316. * - $mdDialogPreset#title(string) - sets title to string
  317. * - $mdDialogPreset#content(string) - sets content / message to string
  318. * - $mdDialogPreset#ok(string) - sets okay button text to string
  319. * - $mdDialogPreset#cancel(string) - sets cancel button text to string
  320. * - $mdDialogPreset#theme(string) - sets the theme of the dialog
  321. *
  322. */
  323. /**
  324. * @ngdoc method
  325. * @name $mdDialog#show
  326. *
  327. * @description
  328. * Show a dialog with the specified options.
  329. *
  330. * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
  331. * `confirm()`, or an options object with the following properties:
  332. * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
  333. * of the dialog.
  334. * - `template` - `{string=}`: Same as templateUrl, except this is an actual template string.
  335. * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
  336. * the location of the click will be used as the starting point for the opening animation
  337. * of the the dialog.
  338. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
  339. * it will create a new isolate scope.
  340. * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
  341. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
  342. * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
  343. * Default true.
  344. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
  345. * Default true.
  346. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
  347. * close it. Default false.
  348. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
  349. * Default true.
  350. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
  351. * focusing some other way, as focus management is required for dialogs to be accessible.
  352. * Defaults to true.
  353. * - `controller` - `{string=}`: The controller to associate with the dialog. The controller
  354. * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
  355. * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
  356. * of values to inject into the controller. For example, `locals: {three: 3}` would inject
  357. * `three` into the controller, with the value 3. If `bindToController` is true, they will be
  358. * copied to the controller instead.
  359. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
  360. * These values will not be available until after initialization.
  361. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
  362. * dialog will not open until all of the promises resolve.
  363. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
  364. * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
  365. * to the root element of the application.
  366. * - `onShowing` `{function=} Callback function used to announce the show() action is
  367. * starting.
  368. * - `onComplete` `{function=}`: Callback function used to announce when the show() action is
  369. * finished.
  370. * - `onRemoving` `{function=} Callback function used to announce the close/hide() action is
  371. * starting. This allows developers to run custom animations in parallel the close animations.
  372. *
  373. * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
  374. * rejected with `$mdDialog.cancel()`.
  375. */
  376. /**
  377. * @ngdoc method
  378. * @name $mdDialog#hide
  379. *
  380. * @description
  381. * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
  382. *
  383. * @param {*=} response An argument for the resolved promise.
  384. *
  385. * @returns {promise} A promise that is resolved when the dialog has been closed.
  386. */
  387. /**
  388. * @ngdoc method
  389. * @name $mdDialog#cancel
  390. *
  391. * @description
  392. * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
  393. *
  394. * @param {*=} response An argument for the rejected promise.
  395. *
  396. * @returns {promise} A promise that is resolved when the dialog has been closed.
  397. */
  398. function MdDialogProvider($$interimElementProvider) {
  399. advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming"];
  400. dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement"];
  401. return $$interimElementProvider('$mdDialog')
  402. .setDefaults({
  403. methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'parent'],
  404. options: dialogDefaultOptions
  405. })
  406. .addPreset('alert', {
  407. methods: ['title', 'content', 'ariaLabel', 'ok', 'theme', 'css'],
  408. options: advancedDialogOptions
  409. })
  410. .addPreset('confirm', {
  411. methods: ['title', 'content', 'ariaLabel', 'ok', 'cancel', 'theme', 'css'],
  412. options: advancedDialogOptions
  413. });
  414. /* ngInject */
  415. function advancedDialogOptions($mdDialog, $mdTheming) {
  416. return {
  417. template: [
  418. '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
  419. ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
  420. ' <h2 class="md-title">{{ dialog.title }}</h2>',
  421. ' <div class="md-dialog-content-body" md-template="::dialog.mdContent"></div>',
  422. ' </md-dialog-content>',
  423. ' <div class="md-actions">',
  424. ' <md-button ng-if="dialog.$type == \'confirm\'"' +
  425. ' ng-click="dialog.abort()" class="md-primary">',
  426. ' {{ dialog.cancel }}',
  427. ' </md-button>',
  428. ' <md-button ng-click="dialog.hide()" class="md-primary" md-autofocus="dialog.$type!=\'confirm\'">',
  429. ' {{ dialog.ok }}',
  430. ' </md-button>',
  431. ' </div>',
  432. '</md-dialog>'
  433. ].join('').replace(/\s\s+/g, ''),
  434. controller: function mdDialogCtrl() {
  435. this.hide = function() {
  436. $mdDialog.hide(true);
  437. };
  438. this.abort = function() {
  439. $mdDialog.cancel();
  440. };
  441. },
  442. controllerAs: 'dialog',
  443. bindToController: true,
  444. theme: $mdTheming.defaultTheme()
  445. };
  446. }
  447. /* ngInject */
  448. function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement) {
  449. return {
  450. hasBackdrop: true,
  451. isolateScope: true,
  452. onShow: onShow,
  453. onRemove: onRemove,
  454. clickOutsideToClose: false,
  455. escapeToClose: true,
  456. targetEvent: null,
  457. focusOnOpen: true,
  458. disableParentScroll: true,
  459. transformTemplate: function(template) {
  460. return '<div class="md-dialog-container">' + validatedTemplate(template) + '</div>';
  461. /**
  462. * The specified template should contain a <md-dialog> wrapper element....
  463. */
  464. function validatedTemplate(template) {
  465. template || ""
  466. return /<\/md-dialog>/g.test(template) ? template : "<md-dialog>" + template + "</md-dialog>";
  467. }
  468. }
  469. };
  470. /**
  471. * Show method for dialogs
  472. */
  473. function onShow(scope, element, options, controller) {
  474. angular.element($document[0].body).addClass('md-dialog-is-showing');
  475. wrapSimpleContent();
  476. captureSourceAndParent(element, options);
  477. configureAria(element.find('md-dialog'), options);
  478. showBackdrop(scope, element, options);
  479. return dialogPopIn(element, options)
  480. .then(function() {
  481. activateListeners(element, options);
  482. lockScreenReader(element, options);
  483. focusOnOpen();
  484. });
  485. /**
  486. * For alerts, focus on content... otherwise focus on
  487. * the close button (or equivalent)
  488. */
  489. function focusOnOpen() {
  490. if (options.focusOnOpen) {
  491. var target = $mdUtil.findFocusTarget(element) || findCloseButton();
  492. target.focus();
  493. }
  494. /**
  495. * If no element with class dialog-close, try to find the last
  496. * button child in md-actions and assume it is a close button
  497. */
  498. function findCloseButton() {
  499. var closeButton = element[0].querySelector('.dialog-close');
  500. if (!closeButton) {
  501. var actionButtons = element[0].querySelectorAll('.md-actions button');
  502. closeButton = actionButtons[actionButtons.length - 1];
  503. }
  504. return angular.element(closeButton);
  505. }
  506. }
  507. /**
  508. * Wrap any simple content [specified via .content("")] in <p></p> tags.
  509. * otherwise accept HTML content within the dialog content area...
  510. * NOTE: Dialog uses the md-template directive to safely inject HTML content.
  511. */
  512. function wrapSimpleContent() {
  513. if ( controller ) {
  514. var HTML_END_TAG = /<\/[\w-]*>/gm;
  515. var content = controller.content || options.content || "";
  516. var hasHTML = HTML_END_TAG.test(content);
  517. if (!hasHTML) {
  518. content = $mdUtil.supplant("<p>{0}</p>", [content]);
  519. }
  520. // Publish updated dialog content body... to be compiled by mdTemplate directive
  521. controller.mdContent = content;
  522. }
  523. }
  524. }
  525. /**
  526. * Remove function for all dialogs
  527. */
  528. function onRemove(scope, element, options) {
  529. options.deactivateListeners();
  530. options.unlockScreenReader();
  531. options.hideBackdrop(options.$destroy);
  532. // For navigation $destroy events, do a quick, non-animated removal,
  533. // but for normal closes (from clicks, etc) animate the removal
  534. return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
  535. /**
  536. * For normal closes, animate the removal.
  537. * For forced closes (like $destroy events), skip the animations
  538. */
  539. function animateRemoval() {
  540. return dialogPopOut(element, options);
  541. }
  542. /**
  543. * Detach the element
  544. */
  545. function detachAndClean() {
  546. angular.element($document[0].body).removeClass('md-dialog-is-showing');
  547. element.remove();
  548. if (!options.$destroy) options.origin.focus();
  549. }
  550. }
  551. /**
  552. * Capture originator/trigger element information (if available)
  553. * and the parent container for the dialog; defaults to the $rootElement
  554. * unless overridden in the options.parent
  555. */
  556. function captureSourceAndParent(element, options) {
  557. options.origin = angular.extend({
  558. element: null,
  559. bounds: null,
  560. focus: angular.noop
  561. }, options.origin || {});
  562. var source = angular.element((options.targetEvent || {}).target);
  563. if (source && source.length) {
  564. // Compute and save the target element's bounding rect, so that if the
  565. // element is hidden when the dialog closes, we can shrink the dialog
  566. // back to the same position it expanded from.
  567. options.origin.element = source;
  568. options.origin.bounds = source[0].getBoundingClientRect();
  569. options.origin.focus = function() {
  570. source.focus();
  571. }
  572. }
  573. // If the parent specifier is a simple string selector, then query for
  574. // the DOM element.
  575. if ( angular.isString(options.parent) ) {
  576. var simpleSelector = options.parent,
  577. container = $document[0].querySelectorAll(simpleSelector);
  578. options.parent = container.length ? container[0] : null;
  579. }
  580. // If we have a reference to a raw dom element, always wrap it in jqLite
  581. options.parent = angular.element(options.parent || $rootElement);
  582. }
  583. /**
  584. * Listen for escape keys and outside clicks to auto close
  585. */
  586. function activateListeners(element, options) {
  587. var window = angular.element($window);
  588. var onWindowResize = $mdUtil.debounce(function(){
  589. stretchDialogContainerToViewport(element, options);
  590. }, 60);
  591. var removeListeners = [];
  592. var smartClose = function() {
  593. // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
  594. // cancel or fallback to hide.
  595. var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
  596. $mdUtil.nextTick(closeFn, true);
  597. };
  598. if (options.escapeToClose) {
  599. var target = options.parent;
  600. var keyHandlerFn = function(ev) {
  601. if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
  602. ev.stopPropagation();
  603. ev.preventDefault();
  604. smartClose();
  605. }
  606. };
  607. // Add keydown listeners
  608. element.on('keydown', keyHandlerFn);
  609. target.on('keydown', keyHandlerFn);
  610. window.on('resize', onWindowResize);
  611. // Queue remove listeners function
  612. removeListeners.push(function() {
  613. element.off('keydown', keyHandlerFn);
  614. target.off('keydown', keyHandlerFn);
  615. window.off('resize', onWindowResize);
  616. });
  617. }
  618. if (options.clickOutsideToClose) {
  619. var target = element;
  620. var sourceElem;
  621. // Keep track of the element on which the mouse originally went down
  622. // so that we can only close the backdrop when the 'click' started on it.
  623. // A simple 'click' handler does not work,
  624. // it sets the target object as the element the mouse went down on.
  625. var mousedownHandler = function(ev) {
  626. sourceElem = ev.target;
  627. };
  628. // We check if our original element and the target is the backdrop
  629. // because if the original was the backdrop and the target was inside the dialog
  630. // we don't want to dialog to close.
  631. var mouseupHandler = function(ev) {
  632. if (sourceElem === target[0] && ev.target === target[0]) {
  633. ev.stopPropagation();
  634. ev.preventDefault();
  635. smartClose();
  636. }
  637. };
  638. // Add listeners
  639. target.on('mousedown', mousedownHandler);
  640. target.on('mouseup', mouseupHandler);
  641. // Queue remove listeners function
  642. removeListeners.push(function() {
  643. target.off('mousedown', mousedownHandler);
  644. target.off('mouseup', mouseupHandler);
  645. });
  646. }
  647. // Attach specific `remove` listener handler
  648. options.deactivateListeners = function() {
  649. removeListeners.forEach(function(removeFn) {
  650. removeFn();
  651. });
  652. options.deactivateListeners = null;
  653. };
  654. }
  655. /**
  656. * Show modal backdrop element...
  657. */
  658. function showBackdrop(scope, element, options) {
  659. if (options.disableParentScroll) {
  660. // !! DO this before creating the backdrop; since disableScrollAround()
  661. // configures the scroll offset; which is used by mdBackDrop postLink()
  662. options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
  663. }
  664. if (options.hasBackdrop) {
  665. options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
  666. $animate.enter(options.backdrop, options.parent);
  667. }
  668. /**
  669. * Hide modal backdrop element...
  670. */
  671. options.hideBackdrop = function hideBackdrop($destroy) {
  672. if (options.backdrop) {
  673. if ( !!$destroy ) options.backdrop.remove();
  674. else $animate.leave(options.backdrop);
  675. }
  676. if (options.disableParentScroll) {
  677. options.restoreScroll();
  678. delete options.restoreScroll;
  679. }
  680. options.hideBackdrop = null;
  681. }
  682. }
  683. /**
  684. * Inject ARIA-specific attributes appropriate for Dialogs
  685. */
  686. function configureAria(element, options) {
  687. var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
  688. var dialogContent = element.find('md-dialog-content');
  689. var dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid());
  690. element.attr({
  691. 'role': role,
  692. 'tabIndex': '-1'
  693. });
  694. if (dialogContent.length === 0) {
  695. dialogContent = element;
  696. }
  697. dialogContent.attr('id', dialogId);
  698. element.attr('aria-describedby', dialogId);
  699. if (options.ariaLabel) {
  700. $mdAria.expect(element, 'aria-label', options.ariaLabel);
  701. }
  702. else {
  703. $mdAria.expectAsync(element, 'aria-label', function() {
  704. var words = dialogContent.text().split(/\s+/);
  705. if (words.length > 3) words = words.slice(0, 3).concat('...');
  706. return words.join(' ');
  707. });
  708. }
  709. }
  710. /**
  711. * Prevents screen reader interaction behind modal window
  712. * on swipe interfaces
  713. */
  714. function lockScreenReader(element, options) {
  715. var isHidden = true;
  716. // get raw DOM node
  717. walkDOM(element[0]);
  718. options.unlockScreenReader = function() {
  719. isHidden = false;
  720. walkDOM(element[0]);
  721. options.unlockScreenReader = null;
  722. };
  723. /**
  724. * Walk DOM to apply or remove aria-hidden on sibling nodes
  725. * and parent sibling nodes
  726. *
  727. */
  728. function walkDOM(element) {
  729. while (element.parentNode) {
  730. if (element === document.body) {
  731. return;
  732. }
  733. var children = element.parentNode.children;
  734. for (var i = 0; i < children.length; i++) {
  735. // skip over child if it is an ascendant of the dialog
  736. // or a script or style tag
  737. if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
  738. children[i].setAttribute('aria-hidden', isHidden);
  739. }
  740. }
  741. walkDOM(element = element.parentNode);
  742. }
  743. }
  744. }
  745. /**
  746. * Ensure the dialog container fill-stretches to the viewport
  747. */
  748. function stretchDialogContainerToViewport(container, options) {
  749. var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
  750. var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
  751. var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
  752. container.css({
  753. top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
  754. height: height ? height + 'px' : '100%'
  755. });
  756. return container;
  757. }
  758. /**
  759. * Dialog open and pop-in animation
  760. */
  761. function dialogPopIn(container, options) {
  762. // Add the `md-dialog-container` to the DOM
  763. options.parent.append(container);
  764. stretchDialogContainerToViewport(container, options);
  765. var dialogEl = container.find('md-dialog');
  766. var animator = $mdUtil.dom.animator;
  767. var buildTranslateToOrigin = animator.calculateZoomToOrigin;
  768. var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
  769. var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.origin));
  770. var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
  771. return animator
  772. .translate3d(dialogEl, from, to, translateOptions)
  773. .then(function(animateReversal) {
  774. // Build a reversal translate function synched to this translation...
  775. options.reverseAnimate = function() {
  776. delete options.reverseAnimate;
  777. return animateReversal(
  778. animator.toTransformCss(
  779. // in case the origin element has moved or is hidden,
  780. // let's recalculate the translateCSS
  781. buildTranslateToOrigin(dialogEl, options.origin)
  782. )
  783. );
  784. };
  785. return true;
  786. });
  787. }
  788. /**
  789. * Dialog close and pop-out animation
  790. */
  791. function dialogPopOut(container, options) {
  792. return options.reverseAnimate();
  793. }
  794. /**
  795. * Utility function to filter out raw DOM nodes
  796. */
  797. function isNodeOneOf(elem, nodeTypeArray) {
  798. if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
  799. return true;
  800. }
  801. }
  802. }
  803. }
  804. MdDialogProvider.$inject = ["$$interimElementProvider"];
  805. ng.material.components.dialog = angular.module("material.components.dialog");