mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
285 lines
10 KiB
JavaScript
285 lines
10 KiB
JavaScript
// assets/controllers/compose_controller.js
|
|
import { Controller } from '@hotwired/stimulus'
|
|
|
|
export default class extends Controller {
|
|
static targets = ['ccField', 'bccField', 'body', 'saveStatus', 'toCollection', 'collapsible', 'minimizeIcon', 'expandIcon', 'ccBtn', 'bccBtn', 'title', 'accountSelect', 'fromBtn', 'fromLabel', 'fromChevron', 'fromDropdown', 'fromRow'];
|
|
static values = {
|
|
draftUrl: String,
|
|
autosaveDelay: { type: Number, default: 2000 },
|
|
minimized: { type: Boolean, default: false },
|
|
expanded: { type: Boolean, default: false },
|
|
}
|
|
|
|
#autosaveTimer = null
|
|
|
|
connect() {
|
|
const toCollection = this.element.querySelector('.compose-to[data-prototype]')
|
|
this._ensureEntry(toCollection)
|
|
|
|
this._boundAutosave = this._scheduleAutosave.bind(this)
|
|
const form = this.element.querySelector('form');
|
|
const formAction = this.element.dataset.composeSendUrlValue;
|
|
form?.addEventListener('input', this._boundAutosave);
|
|
form.action = formAction;
|
|
// Mirror subject into header title
|
|
const subjectInput = this.element.querySelector('[name$="[subject]"]')
|
|
if (subjectInput) {
|
|
this._updateTitle(subjectInput.value)
|
|
subjectInput.addEventListener('input', () => this._updateTitle(subjectInput.value))
|
|
}
|
|
|
|
// Close from-dropdown when clicking outside
|
|
this._boundCloseDropdown = this._closeFromDropdown.bind(this)
|
|
|
|
// Auto-expand on mobile
|
|
if (window.innerWidth < 768) {
|
|
this.expandedValue = true
|
|
}
|
|
console.log(this.accountSelectTarget.value);
|
|
}
|
|
|
|
disconnect() {
|
|
clearTimeout(this.#autosaveTimer)
|
|
this.element.querySelector('form')
|
|
?.removeEventListener('input', this._boundAutosave)
|
|
document.removeEventListener('click', this._boundCloseDropdown, { capture: true })
|
|
document.body.style.overflow = ''
|
|
}
|
|
|
|
// ── Minimize ──────────────────────────────────────────────────────
|
|
|
|
toggleMinimize() {
|
|
// Can't minimize while expanded — collapse first
|
|
if (this.expandedValue) {
|
|
this.expandedValue = false
|
|
return
|
|
}
|
|
this.minimizedValue = !this.minimizedValue
|
|
}
|
|
|
|
minimizedValueChanged() {
|
|
const minimized = this.minimizedValue
|
|
|
|
if (this.hasCollapsibleTarget) {
|
|
this.collapsibleTarget.style.display = minimized ? 'none' : ''
|
|
}
|
|
|
|
if (this.hasMinimizeIconTarget) {
|
|
this.minimizeIconTarget.className = minimized
|
|
? 'fa-solid fa-chevron-up text-xs'
|
|
: 'fa-solid fa-minus text-xs'
|
|
}
|
|
|
|
// Minimized: fully rounded pill. Normal: rounded top only via rounded-2xl + no bottom radius override.
|
|
this.element.classList.toggle('rounded-b-2xl', minimized)
|
|
}
|
|
|
|
// ── Expand / fullscreen ───────────────────────────────────────────
|
|
|
|
toggleExpand() {
|
|
this.expandedValue = !this.expandedValue
|
|
}
|
|
|
|
expandedValueChanged() {
|
|
const expanded = this.expandedValue
|
|
const el = this.element
|
|
|
|
if (expanded) {
|
|
this.minimizedValue = false
|
|
|
|
el.style.cssText = `
|
|
position: fixed;
|
|
inset: 1rem;
|
|
width: auto;
|
|
max-width: none;
|
|
height: auto;
|
|
margin: 0;
|
|
z-index: 50;
|
|
display: flex;
|
|
flex-direction: column;
|
|
`
|
|
|
|
// The body div wrapper needs to flex-grow too
|
|
if (this.hasBodyTarget) {
|
|
this.bodyTarget.closest('div').style.flex = '1'
|
|
this.bodyTarget.style.flex = '1'
|
|
this.bodyTarget.style.height = '0' // 0 basis so flex-1 can take over
|
|
}
|
|
|
|
document.body.style.overflow = 'hidden'
|
|
|
|
} else {
|
|
el.style.cssText = ''
|
|
|
|
if (this.hasBodyTarget) {
|
|
this.bodyTarget.closest('div').style.flex = ''
|
|
this.bodyTarget.style.flex = ''
|
|
this.bodyTarget.style.height = ''
|
|
}
|
|
|
|
document.body.style.overflow = ''
|
|
}
|
|
|
|
if (this.hasExpandIconTarget) {
|
|
this.expandIconTarget.className = expanded
|
|
? 'fa-solid fa-down-left-and-up-right-to-center text-[10px]'
|
|
: 'fa-solid fa-up-right-and-down-left-from-center text-[10px]'
|
|
}
|
|
}
|
|
|
|
// ── From dropdown ─────────────────────────────────────────────────
|
|
|
|
toggleFromDropdown() {
|
|
const open = !this.fromDropdownTarget.classList.contains('hidden')
|
|
if (open) {
|
|
this._closeFromDropdown()
|
|
} else {
|
|
this.fromDropdownTarget.classList.remove('hidden')
|
|
this.fromChevronTarget.style.transform = 'rotate(180deg)'
|
|
document.addEventListener('click', this._boundCloseDropdown, { capture: true, once: true })
|
|
}
|
|
}
|
|
|
|
selectAccount(event) {
|
|
console.log(this.accountSelectTarget.value);
|
|
const btn = event.currentTarget
|
|
const value = btn.dataset.value
|
|
const label = btn.dataset.label
|
|
|
|
// Update hidden select
|
|
this.accountSelectTarget.value = value
|
|
|
|
// Update button label
|
|
this.fromLabelTarget.textContent = label
|
|
|
|
// Highlight selected option
|
|
this.fromDropdownTarget.querySelectorAll('button').forEach(b => {
|
|
const selected = b.dataset.value === value
|
|
b.classList.toggle('bg-blue-50', selected)
|
|
b.classList.toggle('dark:bg-blue-500/10', selected)
|
|
b.classList.toggle('text-blue-600', selected)
|
|
b.classList.toggle('dark:text-blue-300', selected)
|
|
})
|
|
|
|
this._closeFromDropdown()
|
|
}
|
|
|
|
_closeFromDropdown() {
|
|
if (this.hasFromDropdownTarget) {
|
|
this.fromDropdownTarget.classList.add('hidden')
|
|
}
|
|
if (this.hasFromChevronTarget) {
|
|
this.fromChevronTarget.style.transform = ''
|
|
}
|
|
}
|
|
|
|
// ── Close ─────────────────────────────────────────────────────────
|
|
|
|
close() {
|
|
document.body.style.overflow = ''
|
|
this.element.closest('turbo-frame').innerHTML = ''
|
|
}
|
|
|
|
// ── Save draft ────────────────────────────────────────────────────
|
|
|
|
_scheduleAutosave() {
|
|
clearTimeout(this.#autosaveTimer)
|
|
this.#autosaveTimer = setTimeout(
|
|
() => this.saveDraft(),
|
|
this.autosaveDelayValue,
|
|
)
|
|
}
|
|
|
|
async saveDraft(event = null) {
|
|
event?.preventDefault()
|
|
|
|
const form = this.element.querySelector('form')
|
|
if (!form) return
|
|
|
|
const url = this.hasDraftUrlValue ? this.draftUrlValue : form.action
|
|
const status = this.hasSaveStatusTarget ? this.saveStatusTarget : null
|
|
|
|
if (status) {
|
|
status.textContent = 'Saving…'
|
|
status.classList.remove('text-red-500', 'text-green-600')
|
|
status.classList.add('text-gray-400')
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: new FormData(form),
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json',
|
|
},
|
|
})
|
|
|
|
if (response.ok) {
|
|
if (status) {
|
|
const html = await response.text()
|
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
const newController = doc.querySelector('[data-compose-draft-url-value]')
|
|
const oldForm = this.element.querySelector('form')
|
|
|
|
if (newController) {
|
|
this.draftUrlValue = newController.dataset.composeDraftUrlValue
|
|
}
|
|
|
|
if (oldForm) {
|
|
oldForm.action = newController.dataset.composeSendUrlValue
|
|
}
|
|
|
|
status.textContent = 'Draft saved'
|
|
status.classList.remove('text-gray-400', 'text-red-500')
|
|
status.classList.add('text-green-600')
|
|
}
|
|
} else {
|
|
throw new Error('Server error')
|
|
}
|
|
} catch {
|
|
if (status) {
|
|
status.textContent = 'Failed to save'
|
|
status.classList.remove('text-gray-400', 'text-green-600')
|
|
status.classList.add('text-red-500')
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Cc / Bcc ──────────────────────────────────────────────────────
|
|
|
|
showCc() {
|
|
this.ccFieldTarget.classList.remove('hidden')
|
|
this.ccFieldTarget.classList.add('flex')
|
|
if (this.hasCcBtnTarget) this.ccBtnTarget.classList.add('hidden')
|
|
this._ensureEntry(this.ccFieldTarget.querySelector('[data-prototype]'))
|
|
}
|
|
|
|
showBcc() {
|
|
this.bccFieldTarget.classList.remove('hidden')
|
|
this.bccFieldTarget.classList.add('flex')
|
|
if (this.hasBccBtnTarget) this.bccBtnTarget.classList.add('hidden')
|
|
this._ensureEntry(this.bccFieldTarget.querySelector('[data-prototype]'))
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
|
|
_updateTitle(value) {
|
|
if (this.hasTitleTarget) {
|
|
this.titleTarget.textContent = value.trim() || 'New Message'
|
|
}
|
|
}
|
|
|
|
_ensureEntry(collection) {
|
|
if (!collection || collection.children.length > 0) return
|
|
|
|
const index = collection.dataset.index ?? 0
|
|
const prototype = collection.dataset.prototype
|
|
.replace(/__cc__|__bcc__|__to__/g, index)
|
|
|
|
collection.dataset.index = parseInt(index) + 1
|
|
collection.insertAdjacentHTML('beforeend', prototype)
|
|
collection.querySelector('input')?.focus()
|
|
}
|
|
}
|