yuku-t/textcomplete

View on GitHub
src/dropdown_item.js

Summary

Maintainability
A
0 mins
Test Coverage
// @flow

import SearchResult from "./search_result"

export const DEFAULT_CLASS_NAME = "textcomplete-item"
const CALLBACK_METHODS = ["onClick", "onMouseover"]

/** @typedef */
export type DropdownItemOptions = {
  className?: string,
}

// Declare interface instead of importing Dropdown itself to prevent circular dependency.
interface Dropdown {
  activeItem: DropdownItem | null;
  items: DropdownItem[];
  rotate: ?Boolean;
  getActiveItem(): DropdownItem | null;
  select(DropdownItem): DropdownItem;
  el: HTMLUListElement;
}

/**
 * Encapsulate an item of dropdown.
 */
export default class DropdownItem {
  searchResult: SearchResult
  active: boolean
  className: string
  activeClassName: string
  siblings: DropdownItem[]
  dropdown: Dropdown
  index: number
  _el: ?HTMLLIElement

  constructor(searchResult: SearchResult, options: DropdownItemOptions) {
    this.searchResult = searchResult
    this.active = false
    this.className = options.className || DEFAULT_CLASS_NAME
    this.activeClassName = `${this.className} active`

    CALLBACK_METHODS.forEach(method => {
      ;(this: any)[method] = (this: any)[method].bind(this)
    })
  }

  get el(): HTMLLIElement {
    if (this._el) {
      return this._el
    }
    const li = document.createElement("li")
    li.className = this.active ? this.activeClassName : this.className
    const a = document.createElement("a")
    a.innerHTML = this.searchResult.render()
    li.appendChild(a)
    this._el = li
    li.addEventListener("mousedown", this.onClick)
    li.addEventListener("mouseover", this.onMouseover)
    li.addEventListener("touchstart", this.onClick)
    return li
  }

  /**
   * Try to free resources and perform other cleanup operations.
   */
  destroy() {
    this.el.removeEventListener("mousedown", this.onClick, false)
    this.el.removeEventListener("mouseover", this.onMouseover, false)
    this.el.removeEventListener("touchstart", this.onClick, false)
    if (this.active) {
      this.dropdown.activeItem = null
    }
    // This element has already been removed by {@link Dropdown#clear}.
    this._el = null
  }

  /**
   * Callbacked when it is appended to a dropdown.
   *
   * @see Dropdown#append
   */
  appended(dropdown: Dropdown) {
    this.dropdown = dropdown
    this.siblings = dropdown.items
    this.index = this.siblings.length - 1
  }

  /**
   * Deactivate active item then activate itself.
   *
   * @return {this}
   */
  activate() {
    if (!this.active) {
      const activeItem = this.dropdown.getActiveItem()
      if (activeItem) {
        activeItem.deactivate()
      }
      this.dropdown.activeItem = this
      this.active = true
      this.el.className = this.activeClassName

      const offsetToScroll = this.el.offsetTop
      this.dropdown.el.scrollTop = offsetToScroll
    }
    return this
  }

  /**
   * Get the next sibling.
   */
  get next(): ?DropdownItem {
    let nextIndex
    if (this.index === this.siblings.length - 1) {
      if (!this.dropdown.rotate) {
        return null
      }
      nextIndex = 0
    } else {
      nextIndex = this.index + 1
    }
    return this.siblings[nextIndex]
  }

  /**
   * Get the previous sibling.
   */
  get prev(): ?DropdownItem {
    let nextIndex
    if (this.index === 0) {
      if (!this.dropdown.rotate) {
        return null
      }
      nextIndex = this.siblings.length - 1
    } else {
      nextIndex = this.index - 1
    }
    return this.siblings[nextIndex]
  }

  /** @private */
  deactivate() {
    if (this.active) {
      this.active = false
      this.el.className = this.className
      this.dropdown.activeItem = null
    }
    return this
  }

  /** @private */
  onClick(e: Event) {
    e.preventDefault() // Prevent blur event
    this.dropdown.select(this)
  }

  /** @private */
  onMouseover() {
    this.activate()
  }
}