<!--
@license
Copyright (c) 2017 Vaadin Ltd.
This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
-->

<link rel="import" href="../polymer/polymer-element.html">
<link rel="import" href="../polymer/lib/mixins/gesture-event-listeners.html">
<link rel="import" href="../vaadin-overlay/vaadin-overlay.html">
<link rel="import" href="vaadin-contextmenu-event.html">
<link rel="import" href="vaadin-device-detector.html">

<dom-module id="vaadin-context-menu-overlay-styles" theme-for="vaadin-context-menu-overlay">
  <template>
    <style>
      :host {
        align-items: flex-start;
        justify-content: flex-start;
      }

      :host([phone]) {
        top: 0 !important;
        right: 0 !important;
        bottom: 0 !important;
        left: 0 !important;
        padding: 24px;
        align-items: stretch;
        justify-content: flex-end;
      }

      :host([phone]) [part="content"] {
        /* Ideally should be 100vh but iOS phone addr-bar covers view port */
        max-height: 80vh;
      }
    </style>
  </template>
</dom-module>

<dom-module id="vaadin-context-menu-overlay">
  <script>
    {
      /**
       * The overlay element.
       *
       * ### Styling
       *
       * See [`<vaadin-overlay>` documentation](https://github.com/vaadin/vaadin-overlay/blob/master/vaadin-overlay.html)
       * for `<vaadin-context-menu-overlay>` parts.
       *
       * @memberof Vaadin
       */
      class VaadinContextMenuOverlayElement extends Vaadin.OverlayElement {
        static get is() {
          return 'vaadin-context-menu-overlay';
        }
      }

      customElements.define(VaadinContextMenuOverlayElement.is, VaadinContextMenuOverlayElement);
    }
  </script>
</dom-module>

<dom-module id="vaadin-context-menu">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>

    <slot id="slot"></slot>

    <vaadin-device-detector phone="{{_phone}}"></vaadin-device-detector>

    <vaadin-context-menu-overlay id="overlay"
        on-opened-changed="_onOverlayOpened"
        with-backdrop="[[_phone]]"
        phone$="[[_phone]]">
    </vaadin-context-menu-overlay>
  </template>
  <script>
    {
      /**
       * ```html
       * <vaadin-context-menu>
       *   <template>
       *     <paper-listbox>
       *       <paper-item>First menu item</paper-item>
       *       <paper-item>Second menu item</paper-item>
       *     </paper-listbox>
       *   </template>
       * </vaadin-context-menu>
       * ```
       *
       * ### “vaadin-contextmenu” Gesture Event
       *
       * `vaadin-contextmenu` is a gesture event (a custom event fired by Polymer),
       * which is dispatched after either `contextmenu` and long touch events.
       * This enables support for both mouse and touch environments in a uniform way.
       *
       * `<vaadin-context-menu>` opens the menu overlay on the `vaadin-contextmenu`
       * event by default.
       *
       * ### Menu Listener
       *
       * By default, the `<vaadin-context-menu>` element listens for the menu opening
       * event on itself. In order to have a context menu on your content, wrap
       * your content with the `<vaadin-context-menu>` element, and add a template
       * element with a menu. Example:
       *
       * ```html
       * <vaadin-context-menu>
       *   <template>
       *     <paper-listbox>
       *       <paper-item>First menu item</paper-item>
       *       <paper-item>Second menu item</paper-item>
       *     </paper-listbox>
       *   </template>
       *
       *   <p>This paragraph has the context menu provided in the above template.</p>
       *   <p>Another paragraph with the context menu.</p>
       * </vaadin-context-menu>
       * ```
       *
       * In case if you do not want to wrap the page content, you can listen for
       * events on an element outside the `<vaadin-context-menu>` by setting the
       * `listenOn` property:
       *
       * ```html
       * <vaadin-context-menu id="customListener">
       *   <template>
       *     <paper-listbox>
       *       ...
       *     </paper-listbox>
       *   </template>
       * </vaadin-context-menu>
       *
       * <div id="menuListener">The element that listens for the context menu.</div>
       *
       * &lt;script&gt;
       *   const contextMenu = document.querySelector('vaadin-context-menu#customListener');
       *   contextMenu.listenOn = document.querySelector('#menuListener');
       * &lt;/script&gt;
       * ```
       *
       * ### Filtering Menu Targets
       *
       * By default, the listener element and all its descendants open the context
       * menu. You can filter the menu targets to a smaller set of elements inside
       * the listener element by setting the `selector` property.
       *
       * In the following example, only the elements matching `.has-menu` will open the context menu:
       *
       * ```html
       * <vaadin-context-menu selector=".has-menu">
       *   <template>
       *     <paper-listbox>
       *       ...
       *     </paper-listbox>
       *   </template>
       *
       *   <p class="has-menu">This paragraph opens the context menu</p>
       *   <p>This paragraph does not open the context menu</p>
       * </vaadin-context-menu>
       * ```
       *
       * ### Menu Context
       *
       * You can bind to the following properties in the menu template:
       *
       * - `target` is the menu opening event target, which is the element that
       * the user has called the context menu for
       * - `detail` is the menu opening event detail
       *
       * In the following example, the menu item text is composed with the contents
       * of the element that opened the menu:
       *
       * ```html
       * <vaadin-context-menu selector="li">
       *   <template>
       *     <paper-listbox>
       *       <paper-item>The menu target: [[target.textContent]]</paper-item>
       *     </paper-listbox>
       *   </template>
       *
       *   <ul>
       *     <li>Foo</li>
       *     <li>Bar</li>
       *     <li>Baz</li>
       *   </ul>
       * </vaadin-context-menu>
       * ```
       *
       * ### Styling
       *
       * [Generic styling/theming documentation](https://cdn.vaadin.com/vaadin-valo-theme/0.3.1/demo/customization.html)
       *
       * See [`<vaadin-overlay>` documentation](https://github.com/vaadin/vaadin-overlay/blob/master/vaadin-overlay.html)
       * for `<vaadin-context-menu-overlay>` parts.
       *
       * @memberof Vaadin
       * @mixes Polymer.GestureEventListeners
       * @demo demo/index.html
       */
      class ContextMenuElement extends Polymer.GestureEventListeners(Polymer.Element) {

        static get is() {
          return 'vaadin-context-menu';
        }

        static get properties() {
          return {

            /**
             * CSS selector that can be used to target any child element
             * of the context menu to listen for `openOn` events.
             */
            selector: {
              type: String
            },

            /**
             * True if the overlay is currently displayed.
             */
            opened: {
              type: Boolean,
              value: false,
              notify: true,
              readOnly: true
            },

            /**
             * Event name to listen for opening the context menu.
             */
            openOn: {
              type: String,
              value: 'vaadin-contextmenu'
            },

            /**
             * The target element that's listened to for context menu opening events.
             * By default the vaadin-context-menu listens to the target's `vaadin-contextmenu`
             * events.
             * @type {HTMLElement}
             * @default self
             */
            listenOn: {
              type: Object,
              value: function() {
                return this;
              }
            },

            /**
             * Event name to listen for closing the context menu.
             */
            closeOn: {
              type: String,
              value: 'click',
              observer: '_closeOnChanged'
            },

            _context: Object,

            _contentTemplate: Object,

            _boundClose: Object,

            _boundOpen: Object,

            _templateClass: Object
          };
        }

        static get observers() {
          return [
            '_openedChanged(opened)',
            '_contextChanged(_context, _instance)',
            '_targetOrOpenOnChanged(listenOn, openOn)'
          ];
        }

        constructor() {
          super();
          this._boundOpen = this.open.bind(this);
          this._boundClose = this.close.bind(this);
        }

        ready() {
          super.ready();

          this.$.overlay.addEventListener('contextmenu', e => {
            this.close();
            this._preventDefault(e);
          });
        }

        _onOverlayOpened(e) {
          if (e.detail.value === true) {
            // wait for a microtask before focusing the child element
            // to allow overlay to position itself correctly first.
            // Otherwise, the browser window will jump scroll.
            Polymer.Async.microTask.run(() => {
              const child = this.$.overlay.root.querySelector('#content :not(style):not(slot)');
              if (child) {
                child.focus();
              }
            });

          } else if (e.detail.value === false) {
            this._setOpened(false);
          }
        }

        _targetOrOpenOnChanged(listenOn, openOn) {
          if (this._oldListenOn && this._oldOpenOn) {
            this._unlisten(this._oldListenOn, this._oldOpenOn, this._boundOpen);

            this._oldListenOn.style.webkitTouchCallout = '';
            this._oldListenOn.style.webkitUserSelect = '';

            this._oldListenOn = null;
            this._oldOpenOn = null;
          }

          if (listenOn && openOn) {
            this._listen(listenOn, openOn, this._boundOpen);

            // note: these styles don't seem to work in Firefox on iOS.
            listenOn.style.webkitTouchCallout = 'none';
            listenOn.style.webkitUserSelect = 'none';

            this._oldListenOn = listenOn;
            this._oldOpenOn = openOn;
          }
        }

        _closeOnChanged(closeOn, oldCloseOn) {
          // Listen on this.$.overlay.root to workaround issue on
          //  ShadyDOM polyfill: https://github.com/webcomponents/shadydom/issues/159

          // Outside click event from overlay
          const evtOverlay = 'vaadin-overlay-outside-click';

          if (oldCloseOn) {
            this._unlisten(this.$.overlay, oldCloseOn, this._boundClose);
            this._unlisten(this.$.overlay.root, oldCloseOn, this._boundClose);
          }
          if (closeOn) {
            this._listen(this.$.overlay, closeOn, this._boundClose);
            this._listen(this.$.overlay.root, closeOn, this._boundClose);
            this._unlisten(this.$.overlay, evtOverlay, this._preventDefault);
          } else {
            this._listen(this.$.overlay, evtOverlay, this._preventDefault);
          }
        }

        _preventDefault(e) {
          e.preventDefault();
        }

        _openedChanged(opened) {
          if (opened && !this._instance) {
            this._contentTemplate = this.querySelector('template');

            const Templatizer = Polymer.Templatize.templatize(this._contentTemplate, this, {
              instanceProps: {
                detail: true,
                target: true
              },
              forwardHostProp: function(prop, value) {
                if (this._instance) {
                  this._instance.forwardHostProp(prop, value);
                }
              }
            });

            this._instance = new Templatizer({});
            this.$.overlay.content = this._instance.root;
          }

          this.$.overlay.opened = opened;
        }

        _contextChanged(context, instance) {
          if (context === undefined || instance === undefined) {
            return;
          }
          instance.detail = context.detail;
          instance.target = context.target;
        }

        /**
         * Closes the overlay.
         */
        close() {
          this._setOpened(false);
        }

        _contextTarget(e) {
          if (this.selector) {
            const targets = this.listenOn.querySelectorAll(this.selector);

            return Array.prototype.filter.call(targets, el => {
              return e.composedPath().indexOf(el) > -1;
            })[0];
          } else {
            return e.target;
          }
        }

        /**
         * Opens the overlay.
         * @param {Event} e used as the context for the menu. Overlay coordinates are taken from this event.
         */
        open(e) {
          if (e && !this.opened) {
            this._context = {
              detail: e.detail,
              target: this._contextTarget(e)
            };

            if (this._context.target) {
              this._preventDefault(e);
              e.stopPropagation();

              this.$.overlay.opened = true;

              this.$.overlay.style.left = this._getEventCoordinate(e, 'x') + 'px';
              const top = this._getEventCoordinate(e, 'y');
              this.$.overlay.style.top = top + 'px';

              this._setOpened(true);
            }
          }
        }

        _getEventCoordinate(event, coord) {
          if (event.detail instanceof Object) {
            if (event.detail[coord]) {
              // Polymer gesture events, get coordinate from detail
              return event.detail[coord];
            } else if (event.detail.sourceEvent) {
              // Unwrap detailed event
              return this._getEventCoordinate(event.detail.sourceEvent, coord);
            }
          } else {
            // Native mouse or touch event
            const prop = 'client' + coord.toUpperCase();
            return event.changedTouches ? event.changedTouches[0][prop] : event[prop];
          }
        }

        _listen(node, evType, handler) {
          if (Polymer.Gestures.gestures[evType]) {
            Polymer.Gestures.addListener(node, evType, handler);
          } else {
            node.addEventListener(evType, handler);
          }
        }

        _unlisten(node, evType, handler) {
          if (Polymer.Gestures.gestures[evType]) {
            Polymer.Gestures.removeListener(node, evType, handler);
          } else {
            node.removeEventListener(evType, handler);
          }
        }
      }

      customElements.define(ContextMenuElement.is, ContextMenuElement);
      /**
       * @namespace Vaadin
       */
      window.Vaadin = window.Vaadin || {};
      Vaadin.ContextMenuElement = ContextMenuElement;
    }
  </script>
</dom-module>