mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-02-21 20:20:34 +09:00
195 lines
5.5 KiB
JavaScript
195 lines
5.5 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import FileUploader from "models/file_uploader"
|
|
import { onNextEventLoopTick, nextFrame } from "helpers/timing_helpers"
|
|
import { escapeHTML } from "helpers/dom_helpers"
|
|
|
|
export default class extends Controller {
|
|
static classes = ["toolbar"]
|
|
static targets = [ "clientid", "fields", "fileList", "text" ]
|
|
static values = { roomId: Number }
|
|
static outlets = [ "messages" ]
|
|
|
|
#files = []
|
|
|
|
connect() {
|
|
if (!this.#usingTouchDevice) {
|
|
onNextEventLoopTick(() => this.textTarget.focus())
|
|
}
|
|
}
|
|
|
|
submit(event) {
|
|
event.preventDefault()
|
|
|
|
if (!this.fieldsTarget.disabled) {
|
|
this.#submitFiles()
|
|
this.#submitMessage()
|
|
this.collapseToolbar()
|
|
this.textTarget.focus()
|
|
}
|
|
}
|
|
|
|
submitEnd(event) {
|
|
if (!event.detail.success) {
|
|
this.messagesOutlet.failPendingMessage(this.clientidTarget.value)
|
|
}
|
|
}
|
|
|
|
toggleToolbar() {
|
|
this.element.classList.toggle(this.toolbarClass)
|
|
this.textTarget.focus()
|
|
}
|
|
|
|
collapseToolbar() {
|
|
this.element.classList.remove(this.toolbarClass)
|
|
}
|
|
|
|
replaceMessageContent(content) {
|
|
const editor = this.textTarget.editor
|
|
|
|
editor.recordUndoEntry("Format reply")
|
|
editor.setSelectedRange([0, editor.getDocument().toString().length])
|
|
editor.deleteInDirection("forward")
|
|
editor.insertHTML(content)
|
|
editor.setSelectedRange([editor.getDocument().toString().length - 1])
|
|
}
|
|
|
|
submitByKeyboard(event) {
|
|
const toolbarVisible = this.element.classList.contains(this.toolbarClass)
|
|
const metaEnter = event.key == "Enter" && (event.metaKey || event.ctrlKey)
|
|
const plainEnter = event.keyCode == 13 && !event.shiftKey && !event.isComposing
|
|
|
|
if (!this.#usingTouchDevice && (metaEnter || (plainEnter && !toolbarVisible))) {
|
|
this.submit(event)
|
|
}
|
|
}
|
|
|
|
filePicked(event) {
|
|
for (const file of event.target.files) {
|
|
this.#files.push(file)
|
|
}
|
|
event.target.value = null
|
|
this.#updateFileList()
|
|
}
|
|
|
|
fileUnpicked(event) {
|
|
this.#files.splice(event.params.index, 1)
|
|
this.#updateFileList()
|
|
}
|
|
|
|
pasteFiles(event) {
|
|
if (event.clipboardData.files.length > 0) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
for (const file of event.clipboardData.files) {
|
|
this.#files.push(file)
|
|
}
|
|
|
|
this.#updateFileList()
|
|
}
|
|
|
|
dropFiles({ detail: { files } }) {
|
|
for (const file of files) {
|
|
this.#files.push(file)
|
|
}
|
|
|
|
this.#updateFileList()
|
|
}
|
|
|
|
preventAttachment(event) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
online() {
|
|
this.fieldsTarget.disabled = false
|
|
}
|
|
|
|
offline() {
|
|
this.fieldsTarget.disabled = true
|
|
}
|
|
|
|
get #usingTouchDevice() {
|
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
}
|
|
|
|
async #submitMessage() {
|
|
if (this.#validInput()) {
|
|
const clientMessageId = this.#generateClientId()
|
|
|
|
await this.messagesOutlet.insertPendingMessage(clientMessageId, this.textTarget)
|
|
await nextFrame()
|
|
|
|
this.clientidTarget.value = clientMessageId
|
|
this.element.requestSubmit()
|
|
this.#reset()
|
|
}
|
|
}
|
|
|
|
#validInput() {
|
|
return this.textTarget.textContent.trim().length > 0
|
|
}
|
|
|
|
async #submitFiles() {
|
|
const files = this.#files
|
|
|
|
this.#files = []
|
|
this.#updateFileList()
|
|
|
|
for (const file of files) {
|
|
const clientMessageId = this.#generateClientId()
|
|
const uploader = new FileUploader(file, this.element.action, clientMessageId, this.#uploadProgress.bind(this))
|
|
|
|
const body = this.#pendingUploadProgress(file.name)
|
|
await this.messagesOutlet.insertPendingMessage(clientMessageId, body)
|
|
|
|
const resp = await uploader.upload()
|
|
|
|
Turbo.renderStreamMessage(resp)
|
|
}
|
|
}
|
|
|
|
#uploadProgress(percent, clientMessageId, file) {
|
|
const body = this.#pendingUploadProgress(file.name, percent)
|
|
this.messagesOutlet.updatePendingMessage(clientMessageId, body)
|
|
}
|
|
|
|
#generateClientId() {
|
|
return Math.random().toString(36).slice(2)
|
|
}
|
|
|
|
#reset() {
|
|
this.textTarget.value = ""
|
|
}
|
|
|
|
#updateFileList() {
|
|
this.#files.sort((a, b) => a.name.localeCompare(b.name))
|
|
|
|
const fileNodes = this.#files.map((file, index) => {
|
|
const filename = file.name.split(".").slice(0, -1).join(".")
|
|
const extension = file.name.split(".").pop()
|
|
|
|
const node = document.createElement("button")
|
|
node.setAttribute("type","button")
|
|
node.setAttribute("style","gap: 0")
|
|
node.dataset.action = "composer#fileUnpicked"
|
|
node.dataset.composerIndexParam = index
|
|
node.className = "btn btn--plain composer__file txt-normal position-relative unpad flex-column"
|
|
node.innerHTML = file.type.match(/^image\/.*/) ? `<img role="presentation" class="flex-item-no-shrink composer__file-thumbnail" src="${URL.createObjectURL(file)}">` : `<span class="composer__file-thumbnail composer__file-thumbnail--common colorize--black"></span>`
|
|
node.innerHTML += `<span class="pad-inline txt-small flex align-center max-width composer__file-caption"><span class="overflow-ellipsis">${escapeHTML(filename)}.</span><span class="flex-item-no-shrink">${escapeHTML(extension)}</span></span>`
|
|
|
|
return node
|
|
})
|
|
|
|
this.fileListTarget.replaceChildren(...fileNodes)
|
|
}
|
|
|
|
#pendingUploadProgress(filename, percent=0) {
|
|
return `
|
|
<div class="message__pending-upload flex align-center gap" style="--percentage: ${percent}%">
|
|
<div class="composer__file-thumbnail composer__file-thumbnail--common colorize--black borderless flex-item-no-shrink"></div>
|
|
<div>${escapeHTML(filename)} - <span>${percent}%</span></div>
|
|
</div>
|
|
`
|
|
}
|
|
}
|