162 lines
3.7 KiB
Plaintext
162 lines
3.7 KiB
Plaintext
// @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()
|
|
}
|
|
}
|