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>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user