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