src/textarea.js
// @flow
import update from "undate/lib/update"
import Editor from "./editor"
import { calculateElementOffset, getLineHeightPx } from "./utils"
import SearchResult from "./search_result"
const getCaretCoordinates = require("textarea-caret")
const CALLBACK_METHODS = ["onInput", "onKeydown"]
/**
* Encapsulate the target textarea element.
*/
export default class Textarea extends Editor {
el: HTMLTextAreaElement
/**
* @param {HTMLTextAreaElement} el - Where the textcomplete works on.
*/
constructor(el: HTMLTextAreaElement) {
super()
this.el = el
CALLBACK_METHODS.forEach(method => {
;(this: any)[method] = (this: any)[method].bind(this)
})
this.startListening()
}
/**
* @return {this}
*/
destroy() {
super.destroy()
this.stopListening()
// Release the element reference early to help garbage collection.
;(this: any).el = null
return this
}
/**
* Implementation for {@link Editor#applySearchResult}
*/
applySearchResult(searchResult: SearchResult) {
const before = this.getBeforeCursor()
if (before != null) {
const replace = searchResult.replace(before, this.getAfterCursor())
this.el.focus() // Clicking a dropdown item removes focus from the element.
if (Array.isArray(replace)) {
update(this.el, replace[0], replace[1])
if (this.el)
this.el.dispatchEvent(new Event("input"))
}
}
}
/**
* Implementation for {@link Editor#getCursorOffset}
*/
getCursorOffset() {
const elOffset = calculateElementOffset(this.el)
const elScroll = this.getElScroll()
const cursorPosition = this.getCursorPosition()
const lineHeight = getLineHeightPx(this.el)
const top = elOffset.top - elScroll.top + cursorPosition.top + lineHeight
const left = elOffset.left - elScroll.left + cursorPosition.left
const clientTop = this.el.getBoundingClientRect().top;
if (this.el.dir !== "rtl") {
return { top, left, lineHeight, clientTop }
} else {
const right = document.documentElement
? document.documentElement.clientWidth - left
: 0
return { top, right, lineHeight, clientTop }
}
}
/**
* Implementation for {@link Editor#getBeforeCursor}
*/
getBeforeCursor() {
return this.el.selectionStart !== this.el.selectionEnd
? null
: this.el.value.substring(0, this.el.selectionEnd)
}
/** @private */
getAfterCursor() {
return this.el.value.substring(this.el.selectionEnd)
}
/** @private */
getElScroll(): { top: number, left: number } {
return { top: this.el.scrollTop, left: this.el.scrollLeft }
}
/**
* The input cursor's relative coordinates from the textarea's left
* top corner.
*
* @private
*/
getCursorPosition(): { top: number, left: number } {
return getCaretCoordinates(this.el, this.el.selectionEnd)
}
/** @private */
onInput() {
this.emitChangeEvent()
}
/** @private */
onKeydown(e: KeyboardEvent) {
const code = this.getCode(e)
let event
if (code === "UP" || code === "DOWN") {
event = this.emitMoveEvent(code)
} else if (code === "ENTER") {
event = this.emitEnterEvent()
} else if (code === "ESC") {
event = this.emitEscEvent()
}
if (event && event.defaultPrevented) {
e.preventDefault()
}
}
/** @private */
startListening() {
this.el.addEventListener("input", this.onInput)
this.el.addEventListener("keydown", this.onKeydown)
}
/** @private */
stopListening() {
this.el.removeEventListener("input", this.onInput)
this.el.removeEventListener("keydown", this.onKeydown)
}
}