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.

489 lines
18 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.toast
  12. * @description
  13. * Toast
  14. */
  15. MdToastDirective.$inject = ["$mdToast"];
  16. MdToastProvider.$inject = ["$$interimElementProvider"];
  17. angular.module('material.components.toast', [
  18. 'material.core',
  19. 'material.components.button'
  20. ])
  21. .directive('mdToast', MdToastDirective)
  22. .provider('$mdToast', MdToastProvider);
  23. /* ngInject */
  24. function MdToastDirective($mdToast) {
  25. return {
  26. restrict: 'E',
  27. link: function postLink(scope, element) {
  28. element.addClass('_md'); // private md component indicator for styling
  29. // When navigation force destroys an interimElement, then
  30. // listen and $destroy() that interim instance...
  31. scope.$on('$destroy', function() {
  32. $mdToast.destroy();
  33. });
  34. }
  35. };
  36. }
  37. /**
  38. * @ngdoc service
  39. * @name $mdToast
  40. * @module material.components.toast
  41. *
  42. * @description
  43. * `$mdToast` is a service to build a toast notification on any position
  44. * on the screen with an optional duration, and provides a simple promise API.
  45. *
  46. * The toast will be always positioned at the `bottom`, when the screen size is
  47. * between `600px` and `959px` (`sm` breakpoint)
  48. *
  49. * ## Restrictions on custom toasts
  50. * - The toast's template must have an outer `<md-toast>` element.
  51. * - For a toast action, use element with class `md-action`.
  52. * - Add the class `md-capsule` for curved corners.
  53. *
  54. * ### Custom Presets
  55. * Developers are also able to create their own preset, which can be easily used without repeating
  56. * their options each time.
  57. *
  58. * <hljs lang="js">
  59. * $mdToastProvider.addPreset('testPreset', {
  60. * options: function() {
  61. * return {
  62. * template:
  63. * '<md-toast>' +
  64. * '<div class="md-toast-content">' +
  65. * 'This is a custom preset' +
  66. * '</div>' +
  67. * '</md-toast>',
  68. * controllerAs: 'toast',
  69. * bindToController: true
  70. * };
  71. * }
  72. * });
  73. * </hljs>
  74. *
  75. * After you created your preset at config phase, you can easily access it.
  76. *
  77. * <hljs lang="js">
  78. * $mdToast.show(
  79. * $mdToast.testPreset()
  80. * );
  81. * </hljs>
  82. *
  83. * ## Parent container notes
  84. *
  85. * The toast is positioned using absolute positioning relative to its first non-static parent
  86. * container. Thus, if the requested parent container uses static positioning, we will temporarily
  87. * set its positioning to `relative` while the toast is visible and reset it when the toast is
  88. * hidden.
  89. *
  90. * Because of this, it is usually best to ensure that the parent container has a fixed height and
  91. * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
  92. * the parent's height, the toast may be mispositioned if you allow the parent to scroll.
  93. *
  94. * You can, however, have a scrollable element inside of the container; just make sure the
  95. * container itself does not scroll.
  96. *
  97. * <hljs lang="html">
  98. * <div layout-fill id="toast-container">
  99. * <md-content>
  100. * I can have lots of content and scroll!
  101. * </md-content>
  102. * </div>
  103. * </hljs>
  104. *
  105. * @usage
  106. * <hljs lang="html">
  107. * <div ng-controller="MyController">
  108. * <md-button ng-click="openToast()">
  109. * Open a Toast!
  110. * </md-button>
  111. * </div>
  112. * </hljs>
  113. *
  114. * <hljs lang="js">
  115. * var app = angular.module('app', ['ngMaterial']);
  116. * app.controller('MyController', function($scope, $mdToast) {
  117. * $scope.openToast = function($event) {
  118. * $mdToast.show($mdToast.simple().textContent('Hello!'));
  119. * // Could also do $mdToast.showSimple('Hello');
  120. * };
  121. * });
  122. * </hljs>
  123. */
  124. /**
  125. * @ngdoc method
  126. * @name $mdToast#showSimple
  127. *
  128. * @param {string} message The message to display inside the toast
  129. * @description
  130. * Convenience method which builds and shows a simple toast.
  131. *
  132. * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
  133. * rejected with `$mdToast.cancel()`.
  134. *
  135. */
  136. /**
  137. * @ngdoc method
  138. * @name $mdToast#simple
  139. *
  140. * @description
  141. * Builds a preconfigured toast.
  142. *
  143. * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
  144. *
  145. * _**Note:** These configuration methods are provided in addition to the methods provided by
  146. * the `build()` and `show()` methods below._
  147. *
  148. * <table class="md-api-table methods">
  149. * <thead>
  150. * <tr>
  151. * <th>Method</th>
  152. * <th>Description</th>
  153. * </tr>
  154. * </thead>
  155. * <tbody>
  156. * <tr>
  157. * <td>`.textContent(string)`</td>
  158. * <td>Sets the toast content to the specified string</td>
  159. * </tr>
  160. * <tr>
  161. * <td>`.action(string)`</td>
  162. * <td>
  163. * Adds an action button. <br/>
  164. * If clicked, the promise (returned from `show()`)
  165. * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay`
  166. * timeout
  167. * </td>
  168. * </tr>
  169. * <tr>
  170. * <td>`.highlightAction(boolean)`</td>
  171. * <td>
  172. * Whether or not the action button will have an additional highlight class.<br/>
  173. * By default the `accent` color will be applied to the action button.
  174. * </td>
  175. * </tr>
  176. * <tr>
  177. * <td>`.highlightClass(string)`</td>
  178. * <td>
  179. * If set, the given class will be applied to the highlighted action button.<br/>
  180. * This allows you to specify the highlight color easily. Highlight classes are `md-primary`, `md-warn`
  181. * and `md-accent`
  182. * </td>
  183. * </tr>
  184. * <tr>
  185. * <td>`.capsule(boolean)`</td>
  186. * <td>Whether or not to add the `md-capsule` class to the toast to provide rounded corners</td>
  187. * </tr>
  188. * <tr>
  189. * <td>`.theme(string)`</td>
  190. * <td>Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s default.</td>
  191. * </tr>
  192. * <tr>
  193. * <td>`.toastClass(string)`</td>
  194. * <td>Sets a class on the toast element</td>
  195. * </tr>
  196. * </tbody>
  197. * </table>
  198. *
  199. */
  200. /**
  201. * @ngdoc method
  202. * @name $mdToast#updateTextContent
  203. *
  204. * @description
  205. * Updates the content of an existing toast. Useful for updating things like counts, etc.
  206. *
  207. */
  208. /**
  209. * @ngdoc method
  210. * @name $mdToast#build
  211. *
  212. * @description
  213. * Creates a custom `$mdToastPreset` that you can configure.
  214. *
  215. * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
  216. */
  217. /**
  218. * @ngdoc method
  219. * @name $mdToast#show
  220. *
  221. * @description Shows the toast.
  222. *
  223. * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
  224. * and `build()`, or an options object with the following properties:
  225. *
  226. * - `templateUrl` - `{string=}`: The url of an html template file that will
  227. * be used as the content of the toast. Restrictions: the template must
  228. * have an outer `md-toast` element.
  229. * - `template` - `{string=}`: Same as templateUrl, except this is an actual
  230. * template string.
  231. * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
  232. * `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a
  233. * custom toast directive.
  234. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
  235. * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true.
  236. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
  237. * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
  238. * active before automatically closing. Set to 0 or false to have the toast stay open until
  239. * closed manually. Default: 3000.
  240. * - `position` - `{string=}`: Sets the position of the toast. <br/>
  241. * Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`.
  242. * The properties `'end'` and `'start'` are dynamic and can be used for RTL support.<br/>
  243. * Default combination: `'bottom left'`.
  244. * - `toastClass` - `{string=}`: A class to set on the toast element.
  245. * - `controller` - `{string=}`: The controller to associate with this toast.
  246. * The controller will be injected the local `$mdToast.hide( )`, which is a function
  247. * used to hide the toast.
  248. * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
  249. * be used as names of values to inject into the controller. For example,
  250. * `locals: {three: 3}` would inject `three` into the controller with the value
  251. * of 3.
  252. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
  253. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
  254. * and the toast will not open until the promises resolve.
  255. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
  256. * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
  257. * to the root element of the application.
  258. *
  259. * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
  260. * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean
  261. * value == 'true' or the value passed as an argument to `$mdToast.hide()`.
  262. * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false'
  263. */
  264. /**
  265. * @ngdoc method
  266. * @name $mdToast#hide
  267. *
  268. * @description
  269. * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
  270. *
  271. * @param {*=} response An argument for the resolved promise.
  272. *
  273. * @returns {promise} a promise that is called when the existing element is removed from the DOM.
  274. * The promise is resolved with either a Boolean value == 'true' or the value passed as the
  275. * argument to `.hide()`.
  276. *
  277. */
  278. /**
  279. * @ngdoc method
  280. * @name $mdToast#cancel
  281. *
  282. * @description
  283. * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast.
  284. * As such, there isn't any reason to also allow that promise to be rejected,
  285. * since it's not clear what the difference between resolve and reject would be.
  286. *
  287. * Hide the existing toast and reject the promise returned from
  288. * `$mdToast.show()`.
  289. *
  290. * @param {*=} response An argument for the rejected promise.
  291. *
  292. * @returns {promise} a promise that is called when the existing element is removed from the DOM
  293. * The promise is resolved with a Boolean value == 'false'.
  294. *
  295. */
  296. function MdToastProvider($$interimElementProvider) {
  297. // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok).
  298. toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"];
  299. var ACTION_RESOLVE = 'ok';
  300. var activeToastContent;
  301. var $mdToast = $$interimElementProvider('$mdToast')
  302. .setDefaults({
  303. methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'],
  304. options: toastDefaultOptions
  305. })
  306. .addPreset('simple', {
  307. argOption: 'textContent',
  308. methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ],
  309. options: /* ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
  310. return {
  311. template:
  312. '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
  313. ' <div class="md-toast-content">' +
  314. ' <span class="md-toast-text" role="alert" aria-relevant="all" aria-atomic="true">' +
  315. ' {{ toast.content }}' +
  316. ' </span>' +
  317. ' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' +
  318. ' ng-class="highlightClasses">' +
  319. ' {{ toast.action }}' +
  320. ' </md-button>' +
  321. ' </div>' +
  322. '</md-toast>',
  323. controller: /* ngInject */ ["$scope", function mdToastCtrl($scope) {
  324. var self = this;
  325. if (self.highlightAction) {
  326. $scope.highlightClasses = [
  327. 'md-highlight',
  328. self.highlightClass
  329. ]
  330. }
  331. $scope.$watch(function() { return activeToastContent; }, function() {
  332. self.content = activeToastContent;
  333. });
  334. this.resolve = function() {
  335. $mdToast.hide( ACTION_RESOLVE );
  336. };
  337. }],
  338. theme: $mdTheming.defaultTheme(),
  339. controllerAs: 'toast',
  340. bindToController: true
  341. };
  342. }]
  343. })
  344. .addMethod('updateTextContent', updateTextContent)
  345. .addMethod('updateContent', updateTextContent);
  346. function updateTextContent(newContent) {
  347. activeToastContent = newContent;
  348. }
  349. return $mdToast;
  350. /* ngInject */
  351. function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) {
  352. var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
  353. return {
  354. onShow: onShow,
  355. onRemove: onRemove,
  356. toastClass: '',
  357. position: 'bottom left',
  358. themable: true,
  359. hideDelay: 3000,
  360. autoWrap: true,
  361. transformTemplate: function(template, options) {
  362. var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);
  363. if (shouldAddWrapper) {
  364. // Root element of template will be <md-toast>. We need to wrap all of its content inside of
  365. // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled
  366. // content (meaning we're not attempting to guard against XSS).
  367. var templateRoot = document.createElement('md-template');
  368. templateRoot.innerHTML = template;
  369. // Iterate through all root children, to detect possible md-toast directives.
  370. for (var i = 0; i < templateRoot.children.length; i++) {
  371. if (templateRoot.children[i].nodeName === 'MD-TOAST') {
  372. var wrapper = angular.element('<div class="md-toast-content">');
  373. // Wrap the children of the `md-toast` directive in jqLite, to be able to append multiple
  374. // nodes with the same execution.
  375. wrapper.append(angular.element(templateRoot.children[i].childNodes));
  376. // Append the new wrapped element to the `md-toast` directive.
  377. templateRoot.children[i].appendChild(wrapper[0]);
  378. }
  379. }
  380. // We have to return the innerHTMl, because we do not want to have the `md-template` element to be
  381. // the root element of our interimElement.
  382. return templateRoot.innerHTML;
  383. }
  384. return template || '';
  385. }
  386. };
  387. function onShow(scope, element, options) {
  388. activeToastContent = options.textContent || options.content; // support deprecated #content method
  389. var isSmScreen = !$mdMedia('gt-sm');
  390. element = $mdUtil.extractElementByName(element, 'md-toast', true);
  391. options.element = element;
  392. options.onSwipe = function(ev, gesture) {
  393. //Add the relevant swipe class to the element so it can animate correctly
  394. var swipe = ev.type.replace('$md.','');
  395. var direction = swipe.replace('swipe', '');
  396. // If the swipe direction is down/up but the toast came from top/bottom don't fade away
  397. // Unless the screen is small, then the toast always on bottom
  398. if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) ||
  399. (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) {
  400. return;
  401. }
  402. if ((direction === 'left' || direction === 'right') && isSmScreen) {
  403. return;
  404. }
  405. element.addClass('md-' + swipe);
  406. $mdUtil.nextTick($mdToast.cancel);
  407. };
  408. options.openClass = toastOpenClass(options.position);
  409. element.addClass(options.toastClass);
  410. // 'top left' -> 'md-top md-left'
  411. options.parent.addClass(options.openClass);
  412. // static is the default position
  413. if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
  414. options.parent.css('position', 'relative');
  415. }
  416. element.on(SWIPE_EVENTS, options.onSwipe);
  417. element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) {
  418. return 'md-' + pos;
  419. }).join(' '));
  420. if (options.parent) options.parent.addClass('md-toast-animating');
  421. return $animate.enter(element, options.parent).then(function() {
  422. if (options.parent) options.parent.removeClass('md-toast-animating');
  423. });
  424. }
  425. function onRemove(scope, element, options) {
  426. element.off(SWIPE_EVENTS, options.onSwipe);
  427. if (options.parent) options.parent.addClass('md-toast-animating');
  428. if (options.openClass) options.parent.removeClass(options.openClass);
  429. return ((options.$destroy == true) ? element.remove() : $animate.leave(element))
  430. .then(function () {
  431. if (options.parent) options.parent.removeClass('md-toast-animating');
  432. if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
  433. options.parent.css('position', '');
  434. }
  435. });
  436. }
  437. function toastOpenClass(position) {
  438. // For mobile, always open full-width on bottom
  439. if (!$mdMedia('gt-xs')) {
  440. return 'md-toast-open-bottom';
  441. }
  442. return 'md-toast-open-' +
  443. (position.indexOf('top') > -1 ? 'top' : 'bottom');
  444. }
  445. }
  446. }
  447. })(window, window.angular);