import { Controller } from "@hotwired/stimulus" let activeEditor = null function insertAtCursor(field, text) { const start = field.selectionStart ?? field.value.length const end = field.selectionEnd ?? field.value.length field.value = `${field.value.slice(0, start)}${text}${field.value.slice(end)}` const pos = start + text.length field.setSelectionRange(pos, pos) field.dispatchEvent(new Event("input", { bubbles: true })) field.focus() } export function insertMergeToken(token) { const field = document.activeElement if (field && field.matches("input:not([type=hidden]):not([type=checkbox]):not([type=radio]), textarea")) { insertAtCursor(field, token) return } if (activeEditor) { activeEditor.insertString(token) return } document.querySelector("trix-editor")?.editor?.insertString(token) } export default class extends Controller { static targets = ["input", "editor"] static values = { uploadUrl: String } connect() { this.onFocus = () => { activeEditor = this.editorTarget.editor } this.onEditorClick = this.onEditorClick.bind(this) this.onHandlePointerDown = this.onHandlePointerDown.bind(this) this.onPointerMove = this.onPointerMove.bind(this) this.onPointerUp = this.onPointerUp.bind(this) this.editorTarget.addEventListener("trix-focus", this.onFocus) this.editorTarget.addEventListener("click", this.onEditorClick) } disconnect() { this.editorTarget.removeEventListener("trix-focus", this.onFocus) this.editorTarget.removeEventListener("click", this.onEditorClick) this.teardownResize() if (activeEditor === this.editorTarget.editor) activeEditor = null } acceptFile(event) { const file = event.file if (file && file.type.startsWith("image/")) return event.preventDefault() window.alert("Puoi inserire solo immagini (JPG, PNG, GIF o WebP).") } async upload(event) { const attachment = event.attachment if (!attachment.file) return const csrf = document.querySelector('meta[name="csrf-token"]')?.content const form = new FormData() form.append("file", attachment.file) try { const response = await fetch(this.uploadUrlValue, { method: "POST", headers: { Accept: "application/json", "X-CSRF-Token": csrf }, body: form }) const data = await response.json().catch(() => ({})) if (!response.ok) { attachment.remove() window.alert(data.error || "Upload immagine non riuscito.") return } attachment.setAttributes({ url: data.url, href: data.url }) } catch (_error) { attachment.remove() window.alert("Upload immagine non riuscito.") } } onEditorClick(event) { if (event.target.closest(".trix-resize-handle")) return const figure = event.target.closest("figure.attachment") if (figure && this.editorTarget.contains(figure) && figure.querySelector("img")) { this.showHandles(figure) return } this.clearHandles() } showHandles(figure) { this.clearHandles() figure.classList.add("is-resizing") ;["nw", "ne", "sw", "se"].forEach((corner) => { const handle = document.createElement("span") handle.className = `trix-resize-handle trix-resize-handle--${corner}` handle.dataset.corner = corner handle.addEventListener("pointerdown", this.onHandlePointerDown) figure.appendChild(handle) }) this.resizeFigure = figure } clearHandles() { this.editorTarget.querySelectorAll(".trix-resize-handle").forEach((handle) => handle.remove()) this.editorTarget.querySelectorAll("figure.attachment.is-resizing").forEach((figure) => { figure.classList.remove("is-resizing") }) this.resizeFigure = null } onHandlePointerDown(event) { event.preventDefault() event.stopPropagation() const figure = event.currentTarget.closest("figure.attachment") const img = figure?.querySelector("img") if (!img) return const rect = img.getBoundingClientRect() this.drag = { figure, img, corner: event.currentTarget.dataset.corner, startX: event.clientX, startW: rect.width, ratio: rect.height > 0 ? rect.width / rect.height : 1 } event.currentTarget.setPointerCapture?.(event.pointerId) window.addEventListener("pointermove", this.onPointerMove) window.addEventListener("pointerup", this.onPointerUp) } onPointerMove(event) { const drag = this.drag if (!drag) return const dx = event.clientX - drag.startX const grow = drag.corner.includes("e") ? dx : -dx const max = Math.max(80, this.editorTarget.clientWidth - 32) const width = Math.round(Math.min(max, Math.max(64, drag.startW + grow))) const height = Math.round(width / drag.ratio) drag.img.style.width = `${width}px` drag.img.style.height = "auto" drag.img.setAttribute("width", String(width)) drag.img.setAttribute("height", String(height)) drag.width = width drag.height = height } onPointerUp() { window.removeEventListener("pointermove", this.onPointerMove) window.removeEventListener("pointerup", this.onPointerUp) this.commitResize() } commitResize() { const drag = this.drag this.drag = null if (!drag?.figure || !drag.width) return const editor = this.editorTarget.editor editor?.recordUndoEntry?.("Ridimensiona immagine") const attachment = this.findAttachment(drag.figure) if (attachment) { attachment.setAttributes({ width: drag.width, height: drag.height }) requestAnimationFrame(() => { const next = this.findFigureByUrl(attachment.getURL?.() || attachment.getAttributes?.().url) if (next) this.showHandles(next) }) return } drag.img.style.width = `${drag.width}px` drag.img.style.height = "auto" this.editorTarget.dispatchEvent(new Event("input", { bubbles: true })) } findAttachment(figure) { const raw = figure.getAttribute("data-trix-attachment") if (!raw) return null let data try { data = JSON.parse(raw) } catch (_error) { return null } const attachments = this.editorTarget.editor?.getDocument?.().getAttachments?.() || [] return attachments.find((attachment) => { const url = attachment.getURL?.() || attachment.getAttributes?.().url return url && data.url && url === data.url }) } findFigureByUrl(url) { if (!url) return null return Array.from(this.editorTarget.querySelectorAll("figure.attachment")).find((figure) => { const raw = figure.getAttribute("data-trix-attachment") || "" return raw.includes(url) }) } teardownResize() { this.clearHandles() window.removeEventListener("pointermove", this.onPointerMove) window.removeEventListener("pointerup", this.onPointerUp) this.drag = null } }