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\/.*/) ? `` : `` node.innerHTML += `${escapeHTML(filename)}.${escapeHTML(extension)}` return node }) this.fileListTarget.replaceChildren(...fileNodes) } #pendingUploadProgress(filename, percent=0) { return `
${escapeHTML(filename)} - ${percent}%
` } }