203 lines
4.5 KiB
Plaintext
203 lines
4.5 KiB
Plaintext
// @flow
|
|
|
|
import Completer from "./completer"
|
|
import Editor from "./editor"
|
|
import Dropdown, { type DropdownOptions } from "./dropdown"
|
|
import Strategy, { type StrategyProperties } from "./strategy"
|
|
import SearchResult from "./search_result"
|
|
|
|
import EventEmitter from "eventemitter3"
|
|
|
|
const CALLBACK_METHODS = [
|
|
"handleChange",
|
|
"handleEnter",
|
|
"handleEsc",
|
|
"handleHit",
|
|
"handleMove",
|
|
"handleSelect",
|
|
]
|
|
|
|
/** @typedef */
|
|
type TextcompleteOptions = {
|
|
dropdown?: DropdownOptions,
|
|
}
|
|
|
|
/**
|
|
* The core of textcomplete. It acts as a mediator.
|
|
*/
|
|
export default class Textcomplete extends EventEmitter {
|
|
dropdown: Dropdown
|
|
editor: Editor
|
|
options: TextcompleteOptions
|
|
completer: Completer
|
|
isQueryInFlight: boolean
|
|
nextPendingQuery: string | null
|
|
|
|
/**
|
|
* @param {Editor} editor - Where the textcomplete works on.
|
|
*/
|
|
constructor(editor: Editor, options: TextcompleteOptions = {}) {
|
|
super()
|
|
|
|
this.completer = new Completer()
|
|
this.isQueryInFlight = false
|
|
this.nextPendingQuery = null
|
|
this.dropdown = new Dropdown(options.dropdown || {})
|
|
this.editor = editor
|
|
this.options = options
|
|
|
|
CALLBACK_METHODS.forEach(method => {
|
|
;(this: any)[method] = (this: any)[method].bind(this)
|
|
})
|
|
|
|
this.startListening()
|
|
}
|
|
|
|
/**
|
|
* @return {this}
|
|
*/
|
|
destroy(destroyEditor: boolean = true) {
|
|
this.completer.destroy()
|
|
this.dropdown.destroy()
|
|
if (destroyEditor) {
|
|
this.editor.destroy()
|
|
}
|
|
this.stopListening()
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return {this}
|
|
*/
|
|
hide() {
|
|
this.dropdown.deactivate()
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return {this}
|
|
* @example
|
|
* textcomplete.register([{
|
|
* match: /(^|\s)(\w+)$/,
|
|
* search: function (term, callback) {
|
|
* $.ajax({ ... })
|
|
* .done(callback)
|
|
* .fail([]);
|
|
* },
|
|
* replace: function (value) {
|
|
* return '$1' + value + ' ';
|
|
* }
|
|
* }]);
|
|
*/
|
|
register(strategyPropsArray: StrategyProperties[]) {
|
|
strategyPropsArray.forEach(props => {
|
|
this.completer.registerStrategy(new Strategy(props))
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Start autocompleting.
|
|
*
|
|
* @param {string} text - Head to input cursor.
|
|
* @return {this}
|
|
*/
|
|
trigger(text: string) {
|
|
if (this.isQueryInFlight) {
|
|
this.nextPendingQuery = text
|
|
} else {
|
|
this.isQueryInFlight = true
|
|
this.nextPendingQuery = null
|
|
this.completer.run(text)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/** @private */
|
|
handleHit({ searchResults }: { searchResults: SearchResult[] }) {
|
|
if (searchResults.length) {
|
|
this.dropdown.render(searchResults, this.editor.getCursorOffset())
|
|
} else {
|
|
this.dropdown.deactivate()
|
|
}
|
|
this.isQueryInFlight = false
|
|
if (this.nextPendingQuery !== null) {
|
|
this.trigger(this.nextPendingQuery)
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
handleMove(e: CustomEvent) {
|
|
e.detail.code === "UP" ? this.dropdown.up(e) : this.dropdown.down(e)
|
|
}
|
|
|
|
/** @private */
|
|
handleEnter(e: CustomEvent) {
|
|
const activeItem = this.dropdown.getActiveItem()
|
|
if (activeItem) {
|
|
this.dropdown.select(activeItem)
|
|
e.preventDefault()
|
|
} else {
|
|
this.dropdown.deactivate()
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
handleEsc(e: CustomEvent) {
|
|
if (this.dropdown.shown) {
|
|
this.dropdown.deactivate()
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
handleChange(e: CustomEvent) {
|
|
if (e.detail.beforeCursor != null) {
|
|
this.trigger(e.detail.beforeCursor)
|
|
} else {
|
|
this.dropdown.deactivate()
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
handleSelect(selectEvent: CustomEvent) {
|
|
this.emit("select", selectEvent)
|
|
if (!selectEvent.defaultPrevented) {
|
|
this.editor.applySearchResult(selectEvent.detail.searchResult)
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
startListening() {
|
|
this.editor
|
|
.on("move", this.handleMove)
|
|
.on("enter", this.handleEnter)
|
|
.on("esc", this.handleEsc)
|
|
.on("change", this.handleChange)
|
|
this.dropdown.on("select", this.handleSelect)
|
|
;[
|
|
"show",
|
|
"shown",
|
|
"render",
|
|
"rendered",
|
|
"selected",
|
|
"hidden",
|
|
"hide",
|
|
].forEach(eventName => {
|
|
this.dropdown.on(eventName, () => this.emit(eventName))
|
|
})
|
|
this.completer.on("hit", this.handleHit)
|
|
}
|
|
|
|
/** @private */
|
|
stopListening() {
|
|
this.completer.removeAllListeners()
|
|
this.dropdown.removeAllListeners()
|
|
this.editor
|
|
.removeListener("move", this.handleMove)
|
|
.removeListener("enter", this.handleEnter)
|
|
.removeListener("esc", this.handleEsc)
|
|
.removeListener("change", this.handleChange)
|
|
}
|
|
}
|