text: import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
  import '@polymer/iron-flex-layout/iron-flex-layout.js';
  import '@polymer/polymer/lib/elements/array-selector.js';
  import {
    addListener,
    removeListener
  } from '@polymer/polymer/lib/utils/gestures.js';
  import { dom } from '@polymer/polymer/lib/legacy/polymer.dom.js';
  import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js';
  import { animationFrame } from '@polymer/polymer/lib/utils/async.js';
  import { enqueueDebouncer } from '@polymer/polymer/lib/utils/flush.js';
  import { matches, translate } from '@polymer/polymer/lib/utils/path.js';
  import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class.js';
  import { Templatizer } from '@polymer/polymer/lib/legacy/templatizer-behavior.js';
  
  /**
   * @namespace Exmg
   */
  window.Exmg = window.Exmg || {};
  
  const SECRET_TABINDEX = -100;
  
  export class ExmgPaperTbodyElement extends mixinBehaviors(
    [Templatizer],
    PolymerElement
  ) {
    static get template() {
      return html`
        <style>
          :host {
            width: 100%;
          }
  
          .tr {
            @apply --layout;
            box-sizing: border-box;
            border-bottom: 1px solid var(--divider-color);
            @apply --exmg-paper-tbody-tr;
          }
  
          .tr.selected {
            background: #fff;
            position: relative;
            z-index: 20;
            @apply --exmg-paper-tbody-tr-selected;
          }
  
          .tr.selected:hover .td {
            background: none;
          }
  
          .tr[hidden] {
            display: none;
          }
  
          .tr .td.hover > span > * {
            display: none;
          }
  
          .tr.selected .td.hover > * {
            display: none;
          }
  
          .tr:hover .td {
            background-color: #f5f5f5;
            @apply --exmg-paper-tbody-td-hover;
          }
  
          .tr:hover .td.hover > span > * {
            display: inline-block;
            @apply --exmg-paper-tbody-td-hover-content;
          }
  
          .tr.expandable {
            @apply --layout-vertical;
            display: none;
            padding: 72px 36px 24px;
            position: relative;
            margin-left: -12px;
            margin-top: -50px;
            z-index: 10;
            width: calc(100% + 24px);
            height: calc(100% + 48px);
            background: #fff;
            border: 1px solid var(--divider-color);
            box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
              0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
            @apply --exmg-paper-tbody-tr-expandable;
          }
  
          .tr.expandable[aria-expanded] {
            @apply --layout;
          }
  
          .tr.expandable > span {
            @apply --layout;
            @apply --layout-center;
            @apply --exmg-paper-tbody-tr-expandable-expanded;
          }
  
          .tr.expandable > span > * {
            padding: 0 24px 0 0;
          }
  
          .td {
            @apply --layout;
            @apply --layout-center;
            height: 48px;
            box-sizing: border-box;
            overflow: hidden;
            padding: 0 6px;
            @apply --exmg-paper-tbody-td;
          }
  
          .td > span {
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
          }
  
          .td:first-child {
            padding-left: 24px;
          }
  
          .td:last-child {
            padding-right: 24px;
          }
  
          .flex {
            @apply --layout-flex;
          }
  
          .flex-auto {
            /* stretch and squish */
            @apply --layout-flex-auto;
          }
  
          .flex-none {
            /* fixed width */
            @apply --layout-flex-none;
          }
  
          .flex-right {
            @apply --layout;
            @apply --layout-end-justified;
            padding-right: 24px;
            padding-left: 6px !important;
          }
  
          .collapsable {
            display: none;
          }
  
          @media (min-width: 600px) {
            .collapsable {
              @apply --layout;
            }
          }
  
          /* Should this be here or elsewhere? */
          paper-button {
            @apply --layout-self-end;
            transition: all 0.2s linear;
            font-size: 14px;
            font-weight: 500;
            opacity: 0.6;
          }
  
          paper-button:hover {
            background: #ddd;
            opacity: 1;
          }
        </style>
        <slot></slot>
        <!-- Array helper -->
        <array-selector
          id="selector"
          items="{{items}}"
          selected="{{selectedItems}}"
          selected-item="{{selectedItem}}"
        ></array-selector>
      `;
    }
  
    static get is() {
      return 'exmg-paper-tbody';
    }
    static get properties() {
      return {
        /**
         * An array containing items determining how many instances of the template
         * to stamp and that that each template instance should bind to.
         */
        items: {
          type: Array
        },
        /**
         * The name of the variable to add to the binding scope for the array
         * element associated with a given template instance.
         */
        as: {
          type: String,
          value: 'item'
        },
        /**
         * The name of the variable to add to the binding scope to indicate
         * if the row is selected.
         */
        selectedAs: {
          type: String,
          value: 'selected'
        },
        /**
         * The name of the variable to add to the binding scope with the index
         * for the row.
         */
        indexAs: {
          type: String,
          value: 'index'
        },
        /**
         * When true, tapping a row will select the item, placing its data model
         * in the set of selected items retrievable via the selection property.
         *
         * Note that tapping focusable elements within the list item will not
         * result in selection, since they are presumed to have their * own action.
         */
        selectionEnabled: {
          type: Boolean,
          value: false
        },
        /**
         * When `multiSelection` is false, this is the currently selected item, or `null`
         * if no item is selected.
         */
        selectedItem: {
          type: Object,
          notify: true
        },
        /**
         * When `multiSelection` is true, this is an array that contains the selected items.
         */
        selectedItems: {
          type: Object,
          notify: true
        },
        /**
         * When `true`, multiple items may be selected at once (in this case,
         * `selected` is an array of currently selected items).  When `false`,
         * only one item may be selected at a time.
         */
        multiSelection: {
          type: Boolean,
          value: false
        },
  
        /**
         * When `true`, extra style information is set on the first column for correct display.
         */
        checkboxUsed: {
          type: Boolean,
          value: false
        }
      };
    }
  
    static get observers() {
      return [
        '_itemsChanged(items.*)',
        '_multiSelectionChanged(multiSelection)',
        '_selectionEnabledChanged(selectionEnabled)'
      ];
    }
  
    _multiSelectionChanged(multiSelection) {
      this.clearSelection();
      this.$.selector.multi = multiSelection;
    }
  
    /**
     * Add an event listener to `tap` if `selectionEnabled` is true,
     * it will remove the listener otherwise.
     */
    _selectionEnabledChanged(selectionEnabled) {
      if (selectionEnabled) {
        this._addTapListener();
      } else {
        this._removeTapListener();
      }
    }
  
    _addTapListener() {
      addListener(this, 'tap', this._selectionHandler);
    }
  
    _removeTapListener() {
      removeListener(this, 'tap', this._selectionHandler);
    }
  
    /**
     * Select an item from an event object.
     */
    _selectionHandler(e) {
      const m = e.path.find(entry => {
        return entry.classList && entry.classList.contains('tr');
      });
      const model = this.modelForElement(m);
      if (!model) {
        return;
      }
      const target = dom(e).path[0];
      const activeEl = dom(this.domHost ? this.domHost.root : document)
        .activeElement;
      const physicalItem = this._physicalItems[model[this.indexAs]];
      // Safari does not focus certain form controls via mouse
      // https://bugs.webkit.org/show_bug.cgi?id=118043
      if (
        target.localName === 'input' ||
        target.localName === 'button' ||
        target.localName === 'select'
      ) {
        return;
      }
      // Set a temporary tabindex
      const modelTabIndex = model.tabIndex;
      model.tabIndex = -100;
      const activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
      model.tabIndex = modelTabIndex;
      // Only select the item if the tap wasn't on a focusable child
      // or the element bound to `tabIndex`
      if (
        activeEl &&
        physicalItem !== activeEl &&
        physicalItem.contains(activeEl) &&
        activeElTabIndex !== SECRET_TABINDEX
      ) {
        return;
      }
      this.toggleSelectionForItem(model[this.as]);
    }
  
    disconnectedCallback() {
      super.disconnectedCallback();
      this._removeTapListener();
    }
  
    connectedCallback() {
      super.connectedCallback();
      if (this._physicalCount === 0) {
        this._debounceRender(this._render);
      }
    }
  
    _debounceRender(cb) {
      this._renderDebouncer = Debouncer.debounce(
        this._renderDebouncer,
        animationFrame,
        cb.bind(this)
      );
      enqueueDebouncer(this._renderDebouncer);
    }
  
    /**
     * Renders the a new list.
     */
    _render() {
      if (!this.isAttached) {
        return;
      }
      if (this._physicalCount === 0) {
        this._increasePool(this.items.length);
      } else {
        if (this.items.length > this._physicalCount) {
          this._increasePool(this.items.length - this._physicalCount);
        } else {
          this._update();
        }
      }
    }
  
    /**
     * Called when the items have changed. That is, ressignments
     * to `items`, splices or updates to a single item.
     */
    _itemsChanged(change) {
      if (change.path === 'items') {
        this.items = this.items || [];
        this._physicalCount = this._physicalCount || 0;
        this._physicalItems = this._physicalItems || [];
        this._debounceRender(this._render);
      } else if (change.path === 'items.splices') {
        change.value.indexSplices.forEach(function(splice) {
          // deselect removed items
          splice.removed.forEach(this._removeItem, this);
        }, this);
        this._debounceRender(this._render);
      } else {
        if (change.path !== 'items.length') {
          this._forwardItemPath(change.path, change.value);
        }
      }
    }
  
    _removeItem(item) {
      this.$.selector.deselect(item);
    }
  
    _forwardItemPath(path, value) {
      path = path.slice(6); // 'items.'.length == 6
      let dot = path.indexOf('.') + 1;
      if (dot === 0) {
        dot = path.length;
      }
      const idx = parseInt(path.substring(0, dot), 10);
      const inst = this.modelForElement(this._physicalItems[idx]);
      if (!inst || inst[this.indexAs] !== idx) {
        return;
      }
      path = path.substring(dot);
      path = this.as + (path ? '.' + path : '');
      inst._setPendingPropertyOrPath(path, value, false, true);
      inst._flushProperties && inst._flushProperties(true);
    }
  
    /**
     * Creates a pool of DOM elements and attaches them to the local dom.
     *
     * @param {number} size Size of the pool
     */
    _createPool(size) {
      const physicalItems = new Array(size);
      this._ensureTemplatized();
      for (let i = 0; i < size; i++) {
        const inst = this.stamp(null);
        // First element child is item; Safari doesn't support children[0]
        // on a doc fragment.
        physicalItems[i] = inst.root.querySelector('*');
        this.shadowRoot.appendChild(inst.root);
      }
      return physicalItems;
    }
  
    /**
     * Increases the pool size.
     */
    _increasePool(poolSize) {
      // Concat arrays in place.
      [].push.apply(this._physicalItems, this._createPool(poolSize));
      this._physicalCount = this._physicalItems.length;
      this._update();
    }
  
    /**
     * Templetizes the user template.
     */
    _ensureTemplatized() {
      // Check if already templatized
      if (!this.ctor) {
        // Template instance props that should be excluded from forwarding
        const props = {};
        props.__key__ = true;
        props[this.as] = true;
        props[this.indexAs] = true;
        props[this.selectedAs] = true;
        props.tabIndex = true;
        this._instanceProps = props;
        this._userTemplate = this.querySelector('template');
        if (this._userTemplate) {
          this.templatize(this._userTemplate);
        } else {
          console.warn(
            'exm-tbody requires a template to be provided in light-dom'
          );
        }
      }
    }
  
    /**
     * Update the list of items, starting from the `_virtualStart` item.
     * @param {!Array<number>=} itemSet
     */
    _update(itemSet) {
      if ((itemSet && itemSet.length === 0) || this._physicalCount === 0) {
        return;
      }
      this._assignModels();
    }
  
    /**
     * Assigns the data models to a given set of items.
     * @param {!Array<number>=} itemSet
     */
    _assignModels() {
      let count = 0;
      this._physicalItems.forEach(el => {
        const inst = this.modelForElement(el);
        const item = this.items[count];
        if (item != null && item !== undefined) {
          this._forwardProperty(inst, this.as, item);
          this._forwardProperty(
            inst,
            this.selectedAs,
            this.$.selector.isSelected(item)
          );
          this._forwardProperty(inst, this.indexAs, count);
          this._forwardProperty(inst, 'tabIndex', -1);
          inst._flushProperties && inst._flushProperties(true);
          el.removeAttribute('hidden');
        } else {
          el.setAttribute('hidden', '');
        }
        count++;
      });
    }
  
    _forwardProperty(inst, name, value) {
      inst._setPendingProperty(name, value);
    }
  
    /* Templatizer bindings for v2 */
    _forwardHostPropV2(prop, value) {
      (this._physicalItems || [])
        .concat([this._offscreenFocusedItem, this._focusBackfillItem])
        .forEach(function(item) {
          if (item) {
            this.modelForElement(item).forwardHostProp(prop, value);
          }
        }, this);
    }
  
    _notifyInstancePropV2(inst, prop, value) {
      if (matches(this.as, prop)) {
        const idx = inst[this.indexAs];
        if (prop == this.as) {
          this.items[idx] = value;
        }
        this.notifyPath(translate(this.as, 'items.' + idx, prop), value);
      }
    }
  
    /**
     * Selects the given item.
     *
     * @method selectItem
     * @param {Object} item The item instance.
     */
    selectItem(item) {
      return this.selectIndex(this.items.indexOf(item));
    }
  
    /**
     * Selects the item at the given index in the items array.
     *
     * @method selectIndex
     * @param {Object} index The item instance.
     */
    selectIndex(index) {
      if (index < 0 || index >= this.items) {
        return;
      }
      if (!this.multiSelection && this.selectedItem) {
        this.clearSelection();
      }
  
      const model = this.modelForElement(this._physicalItems[index]);
      if (model) {
        model[this.selectedAs] = true;
      }
  
      this.$.selector.selectIndex(index);
    }
  
    /**
     * Clears the current selection in the list.
     *
     * @method clearSelection
     */
    clearSelection() {
      if (this._physicalItems) {
        this._physicalItems.map(el => {
          this.modelForElement(el)[this.selectedAs] = false;
        });
      }
      this.$.selector.clearSelection();
    }
  
    /**
     * Deselects the given item.
     *
     * @method deselect
     * @param {Object} item The item instance.
     */
    deselectItem(item) {
      return this.deselectIndex(this.items.indexOf(item));
    }
  
    /**
     * Deselects the item at the given index in the items array.
     *
     * @method deselectIndex
     * @param {number} index The index of the item in the items array.
     */
    deselectIndex(index) {
      if (index < 0 || index >= this.items) {
        return;
      }
  
      const model = this.modelForElement(this._physicalItems[index]);
      model[this.selectedAs] = false;
  
      this.$.selector.deselectIndex(index);
    }
  
    /**
     * Selects or deselects a given item depending on whether the item
     * has already been selected.
     *
     * @method toggleSelectionForItem
     * @param {Object} item The item object.
     */
    toggleSelectionForItem(item) {
      return this.toggleSelectionForIndex(this.items.indexOf(item));
    }
  
    /**
     * Selects or deselects the item at the given index in the items array
     * depending on whether the item has already been selected.
     *
     * @method toggleSelectionForIndex
     * @param {Object} index The index of the item in the items array.
     */
    toggleSelectionForIndex(index) {
      const isSelected = this.$.selector.isIndexSelected
        ? this.$.selector.isIndexSelected(index)
        : this.$.selector.isSelected(this.items[index]);
      isSelected ? this.deselectIndex(index) : this.selectIndex(index);
    }
  }
  
  window.customElements.define(ExmgPaperTbodyElement.is, ExmgPaperTbodyElement);
  
  Exmg.ExmgPaperTbodyElement = ExmgPaperTbodyElement;
  