Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
6.8 KiB
JavaScript
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
|
|
}
|
|
}
|