mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-02-22 04:30:33 +09:00
350 lines
8.8 KiB
JavaScript
350 lines
8.8 KiB
JavaScript
import SuggestionResultsController from "lib/autocomplete/suggestion_results_controller"
|
|
import SuggestionContext from "lib/autocomplete/suggestion_context"
|
|
|
|
export default class SuggestionController {
|
|
#active = false
|
|
#canceled = false
|
|
#committing = false
|
|
#context
|
|
#resultsController
|
|
|
|
constructor(delegate) {
|
|
this.delegate = delegate
|
|
this.commitSuggestion = this.commitSuggestion.bind(this)
|
|
this.characterMatchesWordBoundary = this.characterMatchesWordBoundary.bind(this)
|
|
this.matchQueryAndTerminatorForWord = this.matchQueryAndTerminatorForWord.bind(this)
|
|
this.didPressKey = this.didPressKey.bind(this)
|
|
this.didResizeWindow = this.didResizeWindow.bind(this)
|
|
this.didScrollWindow = this.didScrollWindow.bind(this)
|
|
this.#installKeyboardListener()
|
|
this.#installResizeListeners()
|
|
}
|
|
|
|
updateWithContentAndPosition(content, position) {
|
|
if (this.#committing) { return }
|
|
const previousContext = this.#context
|
|
this.#context = new SuggestionContext(this, content, position)
|
|
|
|
if (!this.#context.isEqualTo(previousContext)) {
|
|
if (this.#context.isTerminated()) {
|
|
return this.commitSuggestion()
|
|
} else if (this.#context.isActive()) {
|
|
return this.#activateSuggestion()
|
|
} else {
|
|
return this.#deactivateSuggestion()
|
|
}
|
|
}
|
|
}
|
|
|
|
hideResults() {
|
|
return this.#resultsController.hide()
|
|
}
|
|
|
|
destroy() {
|
|
this.#uninstallResultsController()
|
|
this.#uninstallResizeListeners()
|
|
this.#uninstallKeyboardListener()
|
|
}
|
|
|
|
commitSuggestion({withTerminator} = {}) {
|
|
if (this.#committing || this.#canceled || !this.#active) { return false }
|
|
|
|
const values = this.selectedValues
|
|
if (values.length == 0) { return false }
|
|
|
|
const range = [this.#context.startPosition, this.#context.endPosition]
|
|
const terminator = (withTerminator != null ? withTerminator : this.#context.terminator) || " "
|
|
|
|
this.#committing = true
|
|
this.#resultsController.flashSelection(() => {
|
|
this.#committing = false
|
|
this.#didCommitValuesAtRangeWithTerminator(values, range, terminator)
|
|
this.#deactivateSuggestionWithAnimation()
|
|
})
|
|
|
|
if (values.length > 1) {
|
|
this.#willCommitValuesAtRangeWithTerminator(values, range, terminator, { editor: this.delegate.editor })
|
|
} else {
|
|
this.delegate.willCommitValueAtRangeWithTerminator?.(values[0], range, terminator)
|
|
}
|
|
return true
|
|
}
|
|
|
|
get selectedValues() {
|
|
const value = this.#resultsController.getSelectedValue()
|
|
if (!value) { return [] }
|
|
|
|
const valueOnlyHasCommaSeparatedNumbers = /^\d+(,\d+)*$/.test(value)
|
|
return valueOnlyHasCommaSeparatedNumbers ? value.split(",") : [value]
|
|
}
|
|
|
|
isActive() {
|
|
return this.#active
|
|
}
|
|
|
|
isCanceled() {
|
|
return this.#canceled
|
|
}
|
|
|
|
#activateSuggestion() {
|
|
if (!this.#canceled) {
|
|
this.#active = true
|
|
this.#installResultsController()
|
|
return this.#updateResults(() => {
|
|
if (this.#resultsController.hasResults()) {
|
|
return this.#displayResults()
|
|
} else {
|
|
return this.hideResults()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#deactivateSuggestionWithAnimation() {
|
|
this.#hideResultsWithAnimation(() => {
|
|
this.#deactivateSuggestion()
|
|
})
|
|
}
|
|
|
|
#deactivateSuggestion() {
|
|
this.#uninstallResultsController()
|
|
this.#active = false
|
|
this.#canceled = false
|
|
}
|
|
|
|
#cancelSuggestion() {
|
|
if (this.#active) {
|
|
this.#deactivateSuggestion()
|
|
this.#canceled = true
|
|
}
|
|
}
|
|
|
|
#resumeSuggestion() {
|
|
if (this.#canceled) {
|
|
this.#canceled = false
|
|
return this.#activateSuggestion()
|
|
}
|
|
}
|
|
|
|
#willCommitValuesAtRangeWithTerminator(values, range, terminator, { editor }) {
|
|
editor?.setSelectedRange(range) && editor?.deleteInDirection("forward") // Delete user autocomplete input
|
|
|
|
values.forEach((value) => {
|
|
this.delegate.willCommitValueAtRangeWithTerminator?.(value, null, terminator)
|
|
range = this.#advanceRangeForNextValue(range)
|
|
})
|
|
}
|
|
|
|
#didCommitValuesAtRangeWithTerminator(values, range, terminator) {
|
|
values.forEach((value) => {
|
|
this.delegate.didCommitValueAtRangeWithTerminator?.(value, range, terminator)
|
|
range = this.#advanceRangeForNextValue(range)
|
|
})
|
|
}
|
|
|
|
#advanceRangeForNextValue(range) {
|
|
const startPosition = range[1] + 1
|
|
return Array(startPosition, startPosition + 1)
|
|
}
|
|
|
|
#displayResults() {
|
|
if (this.#active) {
|
|
const offsets = this.delegate.getOffsetsAtPosition(this.#context.startPosition)
|
|
const placement = this.delegate.getResultsPlacement?.()
|
|
return this.#resultsController.displayAtOffsets(offsets, {placement})
|
|
}
|
|
}
|
|
|
|
#updateResults(callback) {
|
|
const query = this.#context?.query
|
|
return this.delegate.fetchResultsForQuery(query, results => {
|
|
if ((this.#resultsController != null) && (query === this.#context?.query)) {
|
|
this.#resultsController.updateResults(results)
|
|
return callback?.()
|
|
}
|
|
})
|
|
}
|
|
|
|
#hideResultsWithAnimation(callback) {
|
|
return this.#resultsController?.hideWithAnimation(callback)
|
|
}
|
|
|
|
// Suggestion context delegate
|
|
|
|
characterMatchesWordBoundary(character) {
|
|
if (this.delegate.characterMatchesWordBoundary != null) {
|
|
return this.delegate.characterMatchesWordBoundary(character)
|
|
} else {
|
|
return /[\s\uFFFC]/.test(character)
|
|
}
|
|
}
|
|
|
|
matchQueryAndTerminatorForWord(word) {
|
|
return this.delegate.matchQueryAndTerminatorForWord(word)
|
|
}
|
|
|
|
// Results controller delegate
|
|
|
|
didClickOption(option) {
|
|
return setTimeout(this.commitSuggestion, 100)
|
|
}
|
|
|
|
didShowResults(element) {
|
|
this.hidden = false
|
|
return this.delegate.didShowResults?.(element)
|
|
}
|
|
|
|
didHideResults(element) {
|
|
this.hidden = true
|
|
return this.delegate.didHideResults?.(element)
|
|
}
|
|
|
|
// Keyboard events
|
|
|
|
didPressKey(event) {
|
|
if (this.#committing) { return }
|
|
|
|
let result
|
|
switch (event.keyCode) {
|
|
case 9:
|
|
result = this.#didPressTabKey()
|
|
break
|
|
case 10: case 13:
|
|
result = this.#didPressReturnKey()
|
|
break
|
|
case 27:
|
|
result = this.#didPressEscapeKey()
|
|
break
|
|
case 32:
|
|
result = this.#didPressSpaceKey()
|
|
break
|
|
case 38:
|
|
result = this.#didPressUpKey()
|
|
break
|
|
case 40:
|
|
result = this.#didPressDownKey()
|
|
break
|
|
default:
|
|
result = this.#didPressKeyWithValue(event.key)
|
|
}
|
|
|
|
if (result === false) {
|
|
event.preventDefault()
|
|
return event.stopPropagation()
|
|
}
|
|
}
|
|
|
|
#didPressTabKey() {
|
|
if (this.#active) {
|
|
if (this.hidden) {
|
|
this.#displayResults()
|
|
return false
|
|
} else if (!this.#committing) {
|
|
if (this.commitSuggestion()) {
|
|
return false
|
|
}
|
|
}
|
|
} else if (this.#canceled) {
|
|
this.#resumeSuggestion()
|
|
return false
|
|
}
|
|
}
|
|
|
|
#didPressReturnKey() {
|
|
if (this.#active) {
|
|
if (this.commitSuggestion()) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
#didPressEscapeKey() {
|
|
if (this.#active) {
|
|
this.#cancelSuggestion()
|
|
return false
|
|
}
|
|
}
|
|
|
|
#didPressSpaceKey() {
|
|
if (this.#active && this.#spaceMatchesWordBoundary()) {
|
|
if (this.commitSuggestion()) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
#didPressUpKey() {
|
|
if (this.#active) {
|
|
this.#resultsController.selectUp()
|
|
return false
|
|
}
|
|
}
|
|
|
|
#didPressDownKey() {
|
|
if (this.#active) {
|
|
this.#resultsController.selectDown()
|
|
return false
|
|
}
|
|
}
|
|
|
|
#didPressKeyWithValue(value) {
|
|
if (this.#active && (value != null) && !this.hidden) {
|
|
const result = this.matchQueryAndTerminatorForWord(value)
|
|
if (result?.query === "") {
|
|
this.#cancelSuggestion()
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scroll and resize events
|
|
|
|
didResizeWindow() {
|
|
if (this.#active) {
|
|
return this.hideResults()
|
|
}
|
|
}
|
|
|
|
didScrollWindow(event) {
|
|
if (this.#active && (event.target === document)) {
|
|
return this.hideResults()
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
#installKeyboardListener() {
|
|
window.addEventListener("keydown", this.didPressKey, true)
|
|
}
|
|
|
|
#uninstallKeyboardListener() {
|
|
window.removeEventListener("keydown", this.didPressKey, true)
|
|
}
|
|
|
|
#installResizeListeners() {
|
|
window.addEventListener("resize", this.didResizeWindow, true)
|
|
window.addEventListener("scroll", this.didScrollWindow, true)
|
|
}
|
|
|
|
#uninstallResizeListeners() {
|
|
window.removeEventListener("resize", this.didResizeWindow, true)
|
|
window.removeEventListener("scroll", this.didScrollWindow, true)
|
|
}
|
|
|
|
#installResultsController() {
|
|
if (!this.#resultsController) {
|
|
this.#resultsController = new SuggestionResultsController({ id: this.delegate.getSuggestionsIdentifier() })
|
|
}
|
|
this.#resultsController.delegate = this
|
|
}
|
|
|
|
#uninstallResultsController() {
|
|
this.#resultsController?.destroy()
|
|
this.#resultsController = null
|
|
}
|
|
|
|
#spaceMatchesWordBoundary() {
|
|
return this.characterMatchesWordBoundary(" ")
|
|
}
|
|
}
|