text: /**
   * Copyright 2019 Adobe. All rights reserved.
   * This file is licensed to you under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License. You may obtain a copy
   * of the License at http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software distributed under
   * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
   * OF ANY KIND, either express or implied. See the License for the specific language
   * governing permissions and limitations under the License.
   */
  
  import {BaseComponent} from '../../../coral-base-component';
  import ColumnViewCollection from './ColumnViewCollection';
  import isInteractiveTarget from './isInteractiveTarget';
  import selectionMode from './selectionMode';
  import {commons, transform, validate} from '../../../coral-utils';
  import {Decorator} from '../../../coral-decorator';
  
  const CLASSNAME = '_coral-MillerColumns-item';
  
  // The number of milliseconds for which scroll events should be debounced.
  const SCROLL_DEBOUNCE = 100;
  
  // Height if every item to avoid using offsetHeight during calculations.
  const ITEM_HEIGHT = 40;
  
  // Flag to check if window load was called
  let WINDOW_LOAD = false;
  window.addEventListener('load', () => {
    WINDOW_LOAD = true;
  });
  
  /**
   @class Coral.ColumnView.Column
   @classdesc A ColumnView Column component
   @htmltag coral-columnview-column
   @extends {HTMLElement}
   @extends {BaseComponent}
   */
  const ColumnViewColumn = Decorator(class extends BaseComponent(HTMLElement) {
    /** @ignore */
    constructor() {
      super();
  
      // Events
      this._delegateEvents({
        // we need to use capture as scroll events do not bubble
        'capture:scroll coral-columnview-column-content': '_onContentScroll',
        'click coral-columnview-column-content': '_onColumnContentClick',
  
        // item interaction
        'click coral-columnview-item': '_onItemClick',
        'click [coral-columnview-itemselect]': '_onItemSelectClick',
  
        // item events
        'coral-columnview-item:_activechanged coral-columnview-item': '_onItemActiveChange',
        'coral-columnview-item:_selectedchanged coral-columnview-item': '_onItemSelectedChange'
      });
  
      // Content zone
      this._elements = {
        content: this.querySelector('coral-columnview-column-content') || document.createElement('coral-columnview-column-content')
      };
  
      // default values
      this._bulkSelectionChange = false;
      this._oldSelection = [];
      this._oldActiveItem = null;
  
      // cache bound event handler functions
      this._onDebouncedScroll = this._onDebouncedScroll.bind(this);
      this._toggleItemSelection = this._toggleItemSelection.bind(this);
  
      // Init the collection mutation observer
      this.items._startHandlingItems(true);
    }
  
    /**
     The current active item.
  
     @type {HTMLElement}
     @readonly
     @default null
     */
    get activeItem() {
      return this.items._getAllActive()[0] || null;
    }
  
    /**
     The content of the column. This container is where the items should be added and is responsible for handling the
     scrolling.
  
     @type {ColumnViewColumnContent}
     @contentzone
     */
    get content() {
      return this._getContentZone(this._elements.content);
    }
  
    set content(value) {
      this._setContentZone('content', value, {
        handle: 'content',
        tagName: 'coral-columnview-column-content',
        insert: function (content) {
          content.classList.add('_coral-AssetList');
          this.appendChild(content);
        }
      });
    }
  
    /**
     The Collection Interface that allows interacting with the items that the component contains.
  
     @type {ColumnViewCollection}
     @readonly
     */
    get items() {
      // we do lazy initialization of the collection
      if (!this._items) {
        this._items = new ColumnViewCollection({
          host: this,
          container: this._elements.content,
          itemTagName: 'coral-columnview-item',
          onItemAdded: this._toggleItemSelection,
          onCollectionChange: this._handleMutation
        });
      }
  
      return this._items;
    }
  
    /**
     Returns the first selected item in the ColumnView. The value <code>null</code> is returned if no element is
     selected.
  
     @type {?HTMLElement}
     @readonly
     */
    get selectedItem() {
      return this._selectionMode !== selectionMode.NONE ? this.items._getFirstSelected() : null;
    }
  
    /**
     Returns an Array containing the set selected items inside this Column.
  
     @type {Array.<HTMLElement>}
     @readonly
     */
    get selectedItems() {
      return this._selectionMode !== selectionMode.NONE ? this.items._getAllSelected() : [];
    }
  
    /**
     Private property that indicates the selection mode. If the <code>Coral.ColumnView.Column</code> is not inside
     a <code>Coral.ColumnView</code> this value will be <code>undefined</code>.
     See {@link ColumnViewSelectionModeEnum}.
  
     @type {String}
     @htmlattribute _selectionmode
     @htmlattributereflected
     @private
     */
    get _selectionMode() {
      return this.__selectionMode;
    }
  
    set _selectionMode(value) {
      value = transform.string(value).toLowerCase();
      value = validate.enumeration(selectionMode)(value) && value || null;
  
      this._reflectAttribute('_selectionmode', value);
      
      if(validate.valueMustChange(this.__selectionMode, value)) {
        this.__selectionMode = value;
        let items = this.items.getAll();
        items.forEach(item => this._toggleItemSelection(item));
        this._setStateFromDOM();
      }
    }
  
    /**
     Returns an Array containing the last selected items inside this Column in selected order.
  
     @type {Array.<HTMLElement>}
     @private
     */
    get _lastSelectedItems() {
      return this.__lastSelectedItems || this.selectedItems;
    }
  
    set _lastSelectedItems(value) {
      this.__lastSelectedItems = value;
    }
  
    /** @private */
    _onItemClick(event) {
      if (isInteractiveTarget(event.target)) {
        return;
      }
  
      // since transform will kill the modification, we trigger the event manually
      if (event.matchedTarget.hasAttribute('active')) {
        // directly calls the event since setting the attribute will not trigger an event
        this._onItemActiveChange(event);
      } else {
        // sets the item as active. while handling mouse interaction, items are not toggled
        event.matchedTarget.active = true;
      }
    }
  
    _toggleItemSelection(item) {
      item[this._selectionMode !== selectionMode.NONE ? 'setAttribute' : 'removeAttribute']('_selectable', '');
    }
  
    /** @private */
    _onItemSelectClick(event) {
      if (this._selectionMode && this._selectionMode !== selectionMode.NONE) {
        // stops propagation so that active is not called as well
        event.stopPropagation();
  
        const item = event.matchedTarget.parentElement;
  
        // toggles the selection of the item
        const isSelected = item.hasAttribute('selected');
  
        // Handle multi-selection with shiftKey
        if (!isSelected && event.shiftKey && this._selectionMode === selectionMode.MULTIPLE) {
          const lastSelectedItem = this._lastSelectedItems[this._lastSelectedItems.length - 1];
  
          if (lastSelectedItem) {
            const items = this.items.getAll();
            const lastSelectedItemIndex = items.indexOf(lastSelectedItem);
            const selectedItemIndex = items.indexOf(item);
  
            // true : selection goes up, false : selection goes down
            const direction = selectedItemIndex < lastSelectedItemIndex;
            const selectionRange = [];
            let selectionIndex = lastSelectedItemIndex;
  
            // Retrieve all items in the range
            while (selectedItemIndex !== selectionIndex) {
              selectionIndex = direction ? selectionIndex - 1 : selectionIndex + 1;
              selectionRange.push(items[selectionIndex]);
            }
  
            // Select all items in the range silently
            selectionRange.forEach((rangeItem) => {
              // Except for item which is needed to trigger the selection change event
              if (rangeItem !== item) {
                rangeItem.set('selected', true, true);
              }
            });
          }
        }
  
        item[isSelected ? 'removeAttribute' : 'setAttribute']('selected', '');
  
        // if item was selected, make it active
        if (isSelected && !this._lastSelectedItems.length) {
          item.setAttribute('active', '');
        }
      }
    }
  
    /**
     Handles the item activation, this causes the current item to get active and sets the next column to the item's
     src.
  
     @private
     */
    _onItemActiveChange(event) {
      // we stop propagation since it is a private event
      event.stopImmediatePropagation();
  
      // ignores event handling due to bulk select operation
      if (this._bulkSelectionChange) {
        return;
      }
  
      const item = event.matchedTarget;
  
      this._bulkSelectionChange = true;
      // clears the selection and keeps the item active. this force only 1 item to be active per column
      this.items._deselectAndDeactivateAllExcept(item);
      this._bulkSelectionChange = false;
  
      // we check if the selection requires an event to be triggered
      this._validateColumnChange(item);
  
      // loads data using the item as the activator and 0 as the start since it is a new column
      this._loadItems(0, item);
    }
  
    /**
     Handles selecting multiple items in the same column. Selection could result in none, a single or multiple selected
     items.
  
     @private
     */
    _onItemSelectedChange(event) {
      // we stop propagation since it is a private event
      event.stopImmediatePropagation();
  
      // item that was selected
      const item = event.target;
      const isSelected = item.selected;
  
      if (isSelected) {
        // Remember the last selected item
        this._lastSelectedItems.push(item);
      } else {
        const removedItemIndex = this._lastSelectedItems.indexOf(item);
        if (removedItemIndex !== -1) {
          this._lastSelectedItems = this._lastSelectedItems.splice(removedItemIndex, 1);
        }
      }
  
      // ignores event handling due to bulk select operation
      if (this._bulkSelectionChange) {
        return;
      }
  
      // when the item is selected, we need to enforce the selection mode
      if (isSelected) {
        this._bulkSelectionChange = true;
        // when there is selection, no item can be active
        this.items._deactivateAll();
  
        // enforces the selection mode
        if (this._selectionMode === selectionMode.SINGLE) {
          this.items._deselectAllExcept(item);
        }
        this._bulkSelectionChange = false;
      }
  
      // we make sure the change event is triggered before the load event.
      this._validateColumnChange();
    }
  
    /** @ignore */
    _tryToLoadAdditionalItems() {
      // makes sure that not too many events are triggered (only one per frame)
      if (this._bulkCollectionChange) {
        return;
      }
  
      this._bulkCollectionChange = true;
  
      // we use setTimeout instead of nextFrame because macrotasks allow for more flexibility since they are less
      // aggressive in executing the code
      window.setTimeout(() => {
        // trigger 'coral-columnview:loaditems' asynchronously in order to be sure the application is done
        // adding/removing elements. Also make sure that only one event is triggered at once
  
        // bulkCollectionChange has to be reset before loading new items
        this._bulkCollectionChange = false;
        this._loadFittingAdditionalItems();
      }, 0);
    }
  
    /** @private */
    _onContentScroll() {
      window.clearTimeout(this._scrollTimeout);
      this._scrollTimeout = window.setTimeout(this._onDebouncedScroll, SCROLL_DEBOUNCE);
    }
  
    /**
     Handles the column click. When the column body is clicked, we need to deselect everything up to that column.
  
     @private
     */
    _onColumnContentClick(event) {
      // we make sure the content was clicked directly and not an item
      if (event.target !== event.matchedTarget || isInteractiveTarget(event.target)) {
        return;
      }
  
      event.preventDefault();
  
      // ignores event handling due to bulk select operation
      if (this._bulkSelectionChange) {
        return false;
      }
  
      this._bulkSelectionChange = true;
      // clears the current column
      this.items._deselectAndDeactivateAllExcept();
      this._bulkSelectionChange = false;
  
      // we check if the selection requires an event to be triggered
      this._validateColumnChange();
    }
  
    /** @private */
    _onDebouncedScroll() {
      const threshold = 20;
      if (this.content.scrollTop + this.offsetHeight >= this.content.scrollHeight - threshold) {
        this._loadItems(this.items.length, undefined);
      }
    }
  
    /** @private */
    _arraysAreDifferent(selection, oldSelection) {
      let diff = [];
  
      if (oldSelection.length === selection.length) {
        diff = oldSelection.filter((item) => selection.indexOf(item) === -1);
      }
  
      // since we guarantee that they are arrays, we can start by comparing their size
      return oldSelection.length !== selection.length || diff.length !== 0;
    }
  
    /** @private */
    _validateColumnChange(item) {
      const newActiveItem = this.activeItem;
      const oldActiveItem = this._oldActiveItem || null;
  
      // we have to force the event in case the same active item was clicked again, but still try to avoid triggering as
      // less events as possible
      if (newActiveItem !== oldActiveItem || item === newActiveItem) {
        this.trigger('coral-columnview-column:_activeitemchanged', {
          activeItem: newActiveItem,
          oldActiveItem: oldActiveItem
        });
  
        // we cache the active item for the next time
        this._oldActiveItem = newActiveItem;
      }
  
      const newSelection = this.selectedItems;
      const oldSelection = this._oldSelection;
  
      if (this._arraysAreDifferent(newSelection, oldSelection)) {
        this.trigger('coral-columnview-column:_selecteditemchanged', {
          selection: newSelection,
          oldSelection: oldSelection
        });
  
        // changes the old selection array since we selected something new
        this._oldSelection = newSelection;
      }
    }
  
    /**
     Loads additional Items if the current items of the column to not exceed its height and a path this.next is given.
  
     @private
     */
    _loadFittingAdditionalItems() {
      const itemsCount = this.items.length;
      // this value must match $columnview-item-height
      const itemsHeight = itemsCount * ITEM_HEIGHT;
  
      // we request more items if there is still space for them. in case the values are the same, we request more data
      // just to be sure, specially when the value is 0
      if (itemsHeight <= this.offsetHeight) {
        this._loadItems(itemsCount, undefined);
      }
    }
  
    /**
     Loads additional items. If the given item is not <code>active</code>, no data will be requested.
  
     @param {Number} count
     Amount of items in the column.
     @param {?HTMLElement} item
     Item that triggered the load.
  
     @private
     */
    _loadItems(count, item) {
      // if the given item is not active, it should not request data
      if (!item || item.hasAttribute('active')) {
        this.trigger('coral-columnview-column:_loaditems', {
          start: count,
          item: item
        });
      }
    }
  
    /**
     Updates the active and selected options from the DOM.
  
     @ignore
     */
    _setStateFromDOM() {
      // if the selection mode has not been set, we do no try to force selection
      if (this._selectionMode) {
        // single: only the last item is selected
        if (this._selectionMode === selectionMode.SINGLE) {
          this.items._deselectAllExceptLast();
        }
        // none: deselects everything
        else if (this._selectionMode === selectionMode.NONE) {
          this.items._deselectAllExcept();
        }
  
        // makes sure only one item is active
        this.items._deactivateAllExceptFirst();
      }
    }
  
    /** @private */
    _handleMutation() {
      this._setStateFromDOM();
  
      // in case items were added removed and selection changed
      this._validateColumnChange();
  
      // checks if more items can be added after the childlist change
      this._tryToLoadAdditionalItems();
    }
  
    get _contentZones() {
      return {'coral-columnview-column-content': 'content'};
    }
  
    static get _attributePropertyMap() {
      return commons.extend(super._attributePropertyMap, {
        _selectionmode: '_selectionMode'
      });
    }
  
    /** @ignore */
    static get observedAttributes() {
      return super.observedAttributes.concat([
        '_selectionmode'
      ]);
    }
  
    /** @ignore */
    render() {
      super.render();
  
      this.classList.add(CLASSNAME);
      // @a11y
      if (!this.hasAttribute('role')) {
        this.setAttribute('role', 'group');
      }
  
      this.id = this.id || commons.getUID();
  
      // @todo: initial collection items needs to be triggered
  
      const content = this._elements.content;
  
      // when the content zone was not created, we need to make sure that everything is added inside it as a content.
      // this stops the content zone from being voracious
      if (!content.parentNode) {
        // move the contents of the item into the content zone
        while (this.firstChild) {
          content.appendChild(this.firstChild);
        }
      }
  
      // @a11y
      content.setAttribute('role', 'presentation');
  
      // Call content zone insert
      this.content = content;
  
      // handles the initial selection
      this._setStateFromDOM();
  
      // we keep a list of the last selection to determine if something changed. we need to do this after
      // validateSelection since it modifies the initial state based on the option
      this._oldSelection = this.selectedItems;
      this._oldActiveItem = this.activeItem;
  
      if (!WINDOW_LOAD) {
        // instead of being super aggressive on requesting data, we use window onload so it is scheduled after
        // all the code has been executed, this way events can be added before
        window.addEventListener('load', () => {
          this._loadFittingAdditionalItems();
        });
      } else {
        // macro-task is necessary for the same reasons as listed above
        window.setTimeout(() => {
          this._loadFittingAdditionalItems();
        });
      }
    }
  });
  
  export default ColumnViewColumn;
  