143 lines
3.6 KiB
Plaintext
143 lines
3.6 KiB
Plaintext
// @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)
|
|
}
|
|
}
|