Files
eminuxandCursor c4e5f289cf
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled
Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 23:01:48 +02:00

231 lines
6.8 KiB
JavaScript

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
}
}