/*!
 * AngularJS Material Design
 * https://github.com/angular/material
 * @license MIT
 * v1.2.3
 */
goog.provide('ngmaterial.components.toast');
goog.require('ngmaterial.components.button');
goog.require('ngmaterial.core');
/**
 * @ngdoc module
 * @name material.components.toast
 * @description
 * Toast and Snackbar component.
 */
MdToastDirective['$inject'] = ["$mdToast"];
MdToastProvider['$inject'] = ["$$interimElementProvider"];
angular.module('material.components.toast', [
  'material.core',
  'material.components.button'
])
  .directive('mdToast', MdToastDirective)
  .provider('$mdToast', MdToastProvider);

/* ngInject */
function MdToastDirective($mdToast) {
  return {
    restrict: 'E',
    link: function postLink(scope, element) {
      element.addClass('_md');     // private md component indicator for styling

      // When navigation force destroys an interimElement, then
      // listen and $destroy() that interim instance...
      scope.$on('$destroy', function() {
        $mdToast.destroy();
      });
    }
  };
}

/**
 * @ngdoc service
 * @name $mdToast
 * @module material.components.toast
 *
 * @description
 * `$mdToast` is a service to build a toast notification on any position
 * on the screen with an optional duration, and provides a simple promise API.
 *
 * The toast will be always positioned at the `bottom`, when the screen size is
 * between `600px` and `959px` (`sm` breakpoint)
 *
 * ## Restrictions on custom toasts
 * - The toast's template must have an outer `<md-toast>` element.
 * - For a toast action, use element with class `md-action`.
 * - Add the class `md-capsule` for curved corners.
 *
 * ### Custom Presets
 * Developers are also able to create their own preset, which can be easily used without repeating
 * their options each time.
 *
 * <hljs lang="js">
 *   $mdToastProvider.addPreset('testPreset', {
 *     options: function() {
 *       return {
 *         template:
 *           '<md-toast>' +
 *             '<div class="md-toast-content">' +
 *               'This is a custom preset' +
 *             '</div>' +
 *           '</md-toast>',
 *         controllerAs: 'toast',
 *         bindToController: true
 *       };
 *     }
 *   });
 * </hljs>
 *
 * After you created your preset at config phase, you can easily access it.
 *
 * <hljs lang="js">
 *   $mdToast.show(
 *     $mdToast.testPreset()
 *   );
 * </hljs>
 *
 * ## Parent container notes
 *
 * The toast is positioned using absolute positioning relative to its first non-static parent
 * container. Thus, if the requested parent container uses static positioning, we will temporarily
 * set its positioning to `relative` while the toast is visible and reset it when the toast is
 * hidden.
 *
 * Because of this, it is usually best to ensure that the parent container has a fixed height and
 * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
 * the parent's height, the toast may be mispositioned if you allow the parent to scroll.
 *
 * You can, however, have a scrollable element inside of the container; just make sure the
 * container itself does not scroll.
 *
 * <hljs lang="html">
 * <div layout-fill id="toast-container">
 *   <md-content>
 *     I can have lots of content and scroll!
 *   </md-content>
 * </div>
 * </hljs>
 *
 * @usage
 * <hljs lang="html">
 * <div ng-controller="MyController">
 *   <md-button ng-click="openToast()">
 *     Open a Toast!
 *   </md-button>
 * </div>
 * </hljs>
 *
 * <hljs lang="js">
 * var app = angular.module('app', ['ngMaterial']);
 * app.controller('MyController', function($scope, $mdToast) {
 *   $scope.openToast = function($event) {
 *     $mdToast.show($mdToast.simple().textContent('Hello!'));
 *     // Could also do $mdToast.showSimple('Hello');
 *   };
 * });
 * </hljs>
 */

/**
 * @ngdoc method
 * @name $mdToast#showSimple
 *
 * @param {string} message The message to display inside the toast
 * @description
 * Convenience method which builds and shows a simple toast.
 *
 * @returns {promise} A promise that can be resolved with `$mdToast.hide()`.
 */

/**
 * @ngdoc method
 * @name $mdToast#simple
 *
 * @description
 * Builds a preconfigured toast.
 *
 * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
 *
 * _**Note:** These configuration methods are provided in addition to the methods provided by
 * the `build()` and `show()` methods below._
 *
 * <table class="md-api-table methods">
 *    <thead>
 *      <tr>
 *        <th>Method</th>
 *        <th>Description</th>
 *      </tr>
 *    </thead>
 *    <tbody>
 *      <tr>
 *        <td>`.textContent(string)`</td>
 *        <td>Sets the toast content to the specified string</td>
 *      </tr>
 *      <tr>
 *        <td>`.action(string)`</td>
 *        <td>
 *          Adds an action button. <br/>
 *          If clicked, the promise (returned from `show()`) will resolve with the value `'ok'`;
 *          otherwise, it is resolved with `true` after a `hideDelay` timeout.
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.actionKey(string)`</td>
 *        <td>
 *          Adds a hotkey for the action button to the page. <br/>
 *          If the `actionKey` and `Control` key are pressed, the toast's action will be triggered.
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.actionHint(string)`</td>
 *        <td>
 *          Text that a screen reader will announce to let the user know how to activate the
 *          action. <br>
 *          If an `actionKey` is defined, this defaults to:
 *          'Press Control-"`actionKey`" to ' followed by the `action`.
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.dismissHint(string)`</td>
 *        <td>
 *          Text that a screen reader will announce to let the user know how to dismiss the toast.
 *          <br>Defaults to: "Press Escape to dismiss."
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.highlightAction(boolean)`</td>
 *        <td>
 *          Whether or not the action button will have an additional highlight class.<br/>
 *          By default the `accent` color will be applied to the action button.
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.highlightClass(string)`</td>
 *        <td>
 *          If set, the given class will be applied to the highlighted action button.<br/>
 *          This allows you to specify the highlight color easily. Highlight classes are
 *          `md-primary`, `md-warn`, and `md-accent`
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.capsule(boolean)`</td>
 *        <td>
 *          Whether or not to add the `md-capsule` class to the toast to provide rounded corners
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.theme(string)`</td>
 *        <td>
 *          Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s
 *          default.
 *        </td>
 *      </tr>
 *      <tr>
 *        <td>`.toastClass(string)`</td>
 *        <td>Sets a class on the toast element</td>
 *      </tr>
 *    </tbody>
 * </table>
 */

/**
 * @ngdoc method
 * @name $mdToast#updateTextContent
 *
 * @description
 * Updates the content of an existing toast. Useful for updating things like counts, etc.
 */

/**
 * @ngdoc method
 * @name $mdToast#build
 *
 * @description
 * Creates a custom `$mdToastPreset` that you can configure.
 *
 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options
 *   (see below).
 */

/**
 * @ngdoc method
 * @name $mdToast#show
 *
 * @description Shows the toast.
 *
 * @param {Object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
 * and `build()`, or an options object with the following properties:
 *
 *   - `templateUrl` - `{string=}`: The url of an html template file that will
 *     be used as the content of the toast. Restrictions: the template must
 *     have an outer `md-toast` element.
 *   - `template` - `{string=}`: Same as templateUrl, except this is an actual
 *     template string.
 *   - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
 *     `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled
 *     if you provide a custom toast directive.
 *   - `scope` - `{Object=}`: the scope to link the template / controller to. If none is specified,
 *     it will create a new child scope. This scope will be destroyed when the toast is removed
 *     unless `preserveScope` is set to true.
 *   - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed.
 *     Default is false
 *   - `hideDelay` - `{number=}`: The number of milliseconds the toast should stay active before
 *     automatically closing. Set to `0` or `false` to have the toast stay open until closed
 *     manually via an action in the toast, a hotkey, or a swipe gesture. For accessibility, toasts
 *     should not automatically close when they contain an action.<br>
 *     Defaults to: `3000`.
 *   - `position` - `{string=}`: Sets the position of the toast. <br/>
 *     Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'`, and
 *     `'start'`. The properties `'end'` and `'start'` are dynamic and can be used for RTL support.
 *     <br/>
 *     Default combination: `'bottom left'`.
 *   - `toastClass` - `{string=}`: A class to set on the toast element.
 *   - `controller` - `{string=}`: The controller to associate with this toast.
 *     The controller will be injected the local `$mdToast.hide()`, which is a function
 *     used to hide the toast.
 *   - `locals` - `{Object=}`: An object containing key/value pairs. The keys will be used as names
 *     of values to inject into the controller. For example, `locals: {three: 3}` would inject
 *     `three` into the controller with the value of 3.
 *   - `bindToController` - `{boolean=}`: bind the locals to the controller, instead of passing
 *     them in.
 *   - `resolve` - `{Object=}`: Similar to locals, except it takes promises as values
 *     and the toast will not open until the promises resolve.
 *   - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
 *   - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
 *     to the root element of the application.
 *
 * @returns {promise} A promise that can be resolved with `$mdToast.hide()`. `$mdToast.hide()` will
 * resolve either with the boolean value `true` or the value passed as an argument to
 * `$mdToast.hide()`.
 */

/**
 * @ngdoc method
 * @name $mdToast#hide
 *
 * @description
 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
 *
 * @param {*=} response An argument for the resolved promise.
 *
 * @returns {promise} A promise that is called when the existing element is removed from the DOM.
 * The promise is resolved with either the Boolean value `true` or the value passed as the
 * argument to `$mdToast.hide()`.
 */

function MdToastProvider($$interimElementProvider) {
  // Differentiate promise resolves: hide timeout (value == true) and hide action clicks
  // (value == ok).
  MdToastController['$inject'] = ["$mdToast", "$scope", "$log"];
  toastDefaultOptions['$inject'] = ["$animate", "$mdToast", "$mdUtil", "$mdMedia", "$document", "$q"];
  var ACTION_RESOLVE = 'ok';

  var activeToastContent;
  var $mdToast = $$interimElementProvider('$mdToast')
    .setDefaults({
      methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'],
      options: toastDefaultOptions
    })
    .addPreset('simple', {
      argOption: 'textContent',
      methods: ['textContent', 'action', 'actionKey', 'actionHint', 'highlightAction',
                'highlightClass', 'theme', 'parent', 'dismissHint'],
      options: /* ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
        return {
          template:
            '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
            '  <div class="md-toast-content" aria-live="polite" aria-relevant="all">' +
            '    <span class="md-toast-text">' +
            '      {{ toast.content }}' +
            '    </span>' +
            '    <span class="md-visually-hidden">{{ toast.dismissHint }}</span>' +
            '    <span class="md-visually-hidden" ng-if="toast.action && toast.actionKey">' +
            '      {{ toast.actionHint }}' +
            '    </span>' +
            '    <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' +
            '               ng-class="highlightClasses">' +
            '      {{ toast.action }}' +
            '    </md-button>' +
            '  </div>' +
            '</md-toast>',
          controller: MdToastController,
          theme: $mdTheming.defaultTheme(),
          controllerAs: 'toast',
          bindToController: true
        };
      }]
    })
    .addMethod('updateTextContent', updateTextContent);

    function updateTextContent(newContent) {
      activeToastContent = newContent;
    }

    return $mdToast;

  /**
   * Controller for the Toast interim elements.
   * ngInject
   */
  function MdToastController($mdToast, $scope, $log) {
    // For compatibility with AngularJS 1.6+, we should always use the $onInit hook in
    // interimElements. The $mdCompiler simulates the $onInit hook for all versions.
    this.$onInit = function() {
      var self = this;

      if (self.highlightAction) {
        $scope.highlightClasses = [
          'md-highlight',
          self.highlightClass
        ];
      }

      // If an action is defined and no actionKey is specified, then log a warning.
      if (self.action && !self.actionKey) {
        $log.warn('Toasts with actions should define an actionKey for accessibility.',
          'Details: https://material.angularjs.org/latest/api/service/$mdToast#mdtoast-simple');
      }

      if (self.actionKey && !self.actionHint) {
        self.actionHint = 'Press Control-"' + self.actionKey + '" to ';
      }

      if (!self.dismissHint) {
        self.dismissHint = 'Press Escape to dismiss.';
      }

      $scope.$watch(function() { return activeToastContent; }, function() {
        self.content = activeToastContent;
      });

      this.resolve = function() {
        $mdToast.hide(ACTION_RESOLVE);
      };
    };
  }

  /* ngInject */
  function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia, $document, $q) {
    var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
    return {
      onShow: onShow,
      onRemove: onRemove,
      toastClass: '',
      position: 'bottom left',
      themable: true,
      hideDelay: 3000,
      autoWrap: true,
      transformTemplate: function(template, options) {
        var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);

        if (shouldAddWrapper) {
          // Root element of template will be <md-toast>. We need to wrap all of its content inside
          // of <div class="md-toast-content">. All templates provided here should be static,
          // developer-controlled content (meaning we're not attempting to guard against XSS).
          var templateRoot = document.createElement('md-template');
          templateRoot.innerHTML = template;

          // Iterate through all root children, to detect possible md-toast directives.
          for (var i = 0; i < templateRoot.children.length; i++) {
            if (templateRoot.children[i].nodeName === 'MD-TOAST') {
              var wrapper = angular.element('<div class="md-toast-content">');

              // Wrap the children of the `md-toast` directive in jqLite, to be able to append
              // multiple nodes with the same execution.
              wrapper.append(angular.element(templateRoot.children[i].childNodes));

              // Append the new wrapped element to the `md-toast` directive.
              templateRoot.children[i].appendChild(wrapper[0]);
            }
          }

          // We have to return the innerHTML, because we do not want to have the `md-template`
          // element to be the root element of our interimElement.
          return templateRoot.innerHTML;
        }

        return template || '';
      }
    };

    /**
     * @param {{toast: {actionKey: string=}}=} scope
     * @param {JQLite} element
     * @param {Object.<string, string>} options
     * @return {*}
     */
    function onShow(scope, element, options) {
      activeToastContent = options.textContent;

      var isSmScreen = !$mdMedia('gt-sm');

      element = $mdUtil.extractElementByName(element, 'md-toast', true);
      options.element = element;

      options.onSwipe = function(ev) {
        // Add the relevant swipe class to the element so it can animate correctly
        var swipe = ev.type.replace('$md.','');
        var direction = swipe.replace('swipe', '');

        // If the swipe direction is down/up but the toast came from top/bottom don't fade away
        // Unless the screen is small, then the toast always on bottom
        if ((direction === 'down' && options.position.indexOf('top') !== -1 && !isSmScreen) ||
            (direction === 'up' && (options.position.indexOf('bottom') !== -1 || isSmScreen))) {
          return;
        }

        if ((direction === 'left' || direction === 'right') && isSmScreen) {
          return;
        }

        element.addClass('md-' + swipe);
        $mdUtil.nextTick($mdToast.cancel);
      };
      options.openClass = toastOpenClass(options.position);

      element.addClass(options.toastClass);

      // 'top left' -> 'md-top md-left'
      options.parent.addClass(options.openClass);

      // static is the default position
      if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
        options.parent.css('position', 'relative');
      }

      setupActionKeyListener(scope.toast && scope.toast.actionKey ?
        scope.toast.actionKey : undefined);

      element.on(SWIPE_EVENTS, options.onSwipe);

      var verticalPositionDefined = false;
      var positionClasses = options.position.split(' ').map(function (position) {
        if (position) {
          var className = 'md-' + position;
          if (className === 'md-top' || className === 'md-bottom') {
            verticalPositionDefined = true;
          }
          return className;
        }
        return 'md-bottom';
      });
      // If only "right" or "left" are defined, default to a vertical position of "bottom"
      // as documented.
      if (!verticalPositionDefined) {
        positionClasses.push('md-bottom');
      }
      element.addClass(isSmScreen ? 'md-bottom' : positionClasses.join(' '));

      if (options.parent) {
        options.parent.addClass('md-toast-animating');
      }
      return $animate.enter(element, options.parent).then(function() {
        if (options.parent) {
          options.parent.removeClass('md-toast-animating');
        }
      });
    }

    /**
     * @param {Object} scope the toast's scope
     * @param {JQLite} element the toast to be removed
     * @param {Object} options
     * @return {Promise<*>} a Promise to remove the element immediately or to animate it out.
     */
    function onRemove(scope, element, options) {
      if (scope.toast && scope.toast.actionKey) {
        removeActionKeyListener();
      }
      element.off(SWIPE_EVENTS, options.onSwipe);
      if (options.parent) options.parent.addClass('md-toast-animating');
      if (options.openClass) options.parent.removeClass(options.openClass);

      // Don't run the leave animation if the element has already been destroyed.
      return ((options.$destroy === true) ? $q.when(element.remove()) : $animate.leave(element))
        .then(function () {
          if (options.parent) options.parent.removeClass('md-toast-animating');
          if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
            options.parent.css('position', '');
          }
        });
    }

    function toastOpenClass(position) {
      // For mobile, always open full-width on bottom
      if (!$mdMedia('gt-xs')) {
        return 'md-toast-open-bottom';
      }

      return 'md-toast-open-' + (position.indexOf('top') > -1 ? 'top' : 'bottom');
    }

    /**
     * @param {string} actionKey
     */
    function setupActionKeyListener(actionKey) {
      /**
       * @param {KeyboardEvent} event
       */
      var handleKeyDown = function(event) {
        if (event.key === 'Escape') {
          $mdToast.hide(false);
        }
        if (actionKey && event.key === actionKey && event.ctrlKey) {
          $mdToast.hide(ACTION_RESOLVE);
        }
      };
      $document.on('keydown', handleKeyDown);
    }

    function removeActionKeyListener() {
      $document.off('keydown');
    }
  }
}

ngmaterial.components.toast = angular.module("material.components.toast");