import server from "/@server/server.js" import calendarUtil from "../calendarUtil.js" import "../EventFileList.js" import "../../components/Avatar.js" css(` eventform- { display: flex; flex-direction: column; height: 100%; min-height: 0; scrollbar-width: none; -ms-overflow-style: none; } eventform- > form { display: flex; flex-direction: column; flex: 1 1 auto; height: 100%; min-height: 0; } eventform- ::-webkit-scrollbar { display: none; width: 0; height: 0; } eventform- ::-webkit-scrollbar-thumb { background: transparent; } eventform- ::-webkit-scrollbar-track { background: transparent; } eventform- input::placeholder, eventform- textarea::placeholder { color: var(--headertext); opacity: 0.35; } eventform- input[type="date"], eventform- input[type="time"] { min-width: 0; } eventform- input[type="checkbox"] { flex-shrink: 0; } #eventform-toast-wrap { transition: max-height 0.25s ease, opacity 0.22s ease, padding-top 0.25s ease; } `) class EventForm extends Shadow { cardInputStyles(el) { return el .border("none").outline("none").background("transparent") .color("var(--text)").fontSize(0.9, em).fontFamily("Arial") .padding(0).boxSizing("border-box") } selectedCalendars = []; pendingSaveEventData = null; // stashed before the recurring scope prompt, consumed by performSave uploadedFiles = []; existingAttachments = []; filesOpen = false; attachmentsDeleted = false; pendingDeletions = []; initialAllDay = true; startDate = calendarUtil.toDateInput(new Date()); endDate = calendarUtil.toDateInput(new Date()); startTime = "13:00"; endTime = "14:00"; recurrence = null; constructor(calendars, updateEvents, event = null, onBack = null, onDelete = null, initialDate = null, onSaveError = null) { super() this.calendars = calendars; this.editEvent = event; this.updateEvents = updateEvents; this.onBack = onBack; this.onDelete = onDelete; this.onSaveError = onSaveError; if (event) { const start = new Date(event.time_start); const end = new Date(event.time_end); this.startDate = calendarUtil.toDateInput(start); this.endDate = calendarUtil.toDateInput(end); this.startTime = event.all_day ? "13:00" : calendarUtil.toTimeInput(start); this.endTime = event.all_day ? "14:00" : calendarUtil.toTimeInput(end); this.selectedCalendars = calendars.filter(c => event.calendars?.includes(c.id)); this.existingAttachments = event.attachments ?? []; this.recurrence = event.recurrence ? { frequency: event.recurrence.frequency, interval: event.recurrence.interval ?? 1, days_of_week: event.recurrence.days_of_week ? [...event.recurrence.days_of_week] : null } : null; // Snapshot original state in getFormData() shape for dirty checking this.originalFormData = { title: event.title ?? "", location: event.location ?? "", description: event.description ?? "", all_day: event.all_day, time_start: event.all_day ? this.startDate : `${this.startDate}T${this.startTime}`, time_end: event.all_day ? this.endDate : `${this.endDate}T${this.endTime}`, calendars: [...(event.calendars ?? [])].sort().join(","), recurrence: this.recurrence ? { ...this.recurrence, days_of_week: this.recurrence.days_of_week ? [...this.recurrence.days_of_week] : null } : null }; } else { if (calendars.length > 0) { this.selectedCalendars = [calendars[0]]; } if (initialDate) { this.startDate = calendarUtil.toDateInput(initialDate); const h = initialDate.getHours(), m = initialDate.getMinutes(); if (h !== 0 || m !== 0) { this.initialAllDay = false; this.startTime = calendarUtil.toTimeInput(initialDate); const endDate = new Date(initialDate.getTime() + 30 * 60 * 1000); this.endTime = calendarUtil.toTimeInput(endDate); this.endDate = calendarUtil.toDateInput(endDate); } else { this.endDate = calendarUtil.toDateInput(initialDate); } } this.originalFormData = { title: "", location: "", description: "", all_day: this.initialAllDay, time_start: this.initialAllDay ? this.startDate : `${this.startDate}T${this.startTime}`, time_end: this.initialAllDay ? this.endDate : `${this.endDate}T${this.endTime}`, calendars: (calendars.length > 0 ? [calendars[0].id] : []).sort().join(","), recurrence: null }; } } enforceEndAfterStart() { const isAllDay = this.$('[name="all_day"]')?.checked ?? true; const start = new Date(`${this.startDate}T${isAllDay ? "00:00" : this.startTime}`); const end = new Date(`${this.endDate}T${isAllDay ? "00:00" : this.endTime}`); if (end < start) { const newEnd = new Date(start.getTime() + (isAllDay ? 0 : 60 * 60 * 1000)); this.endDate = calendarUtil.toDateInput(newEnd); this.endTime = calendarUtil.toTimeInput(newEnd); const endDateEl = this.$('[name="time_end_date"]'); const endTimeEl = this.$('[name="time_end_time"]'); if (endDateEl) endDateEl.value = this.endDate; if (endTimeEl) endTimeEl.value = this.endTime; } } recurrenceOptionKey() { const r = this.recurrence; if (!r) return "never"; if (r.frequency === 'daily') return "daily"; if (r.frequency === 'weekly' && r.interval === 2) return "biweekly"; if (r.frequency === 'weekly') return "weekly"; if (r.frequency === 'monthly') return "monthly"; if (r.frequency === 'yearly') return "yearly"; return "never"; } recurrenceLabel() { switch (this.recurrenceOptionKey()) { case "daily": return "Daily"; case "weekly": return "Weekly"; case "biweekly": return "Every 2 weeks"; case "monthly": return "Monthly"; case "yearly": return "Yearly"; default: return "Never"; } } render() { const isEditMode = !!this.editEvent; const showTime = isEditMode ? !this.editEvent.all_day : !this.initialAllDay; const initialAllDay = isEditMode ? this.editEvent.all_day : this.initialAllDay; form(() => { VStack(() => { // ── Header ──────────────────────────────────────────── HStack(() => { button("Cancel") .attr({ type: "button" }) .background("none").border("none").padding(0) .color("var(--quillred)").fontSize(0.95, em) .fontFamily("Arial").cursor("pointer").flexShrink(0) .onTap(() => this.handleBack()) p(isEditMode ? "Edit Event" : "New Event") .margin(0).fontSize(1, em).fontWeight("700") .color("var(--headertext)").fontFamily("Arial") .flex(1).textAlign("center") button("Save") .attr({ type: "submit" }) .background("none").border("none").padding(0) .color("var(--quillred)").fontSize(0.95, em) .fontWeight("600").fontFamily("Arial").cursor("pointer").flexShrink(0) }) .paddingHorizontal(1.25, em).paddingVertical(0.9, em) .alignItems("center").borderBottom("1px solid var(--divider)") .flexShrink(0).background(util.darkMode() ? "transparent" : "var(--sidebottombars)") // ── Error toast ─────────────────────────────────────── VStack(() => { p("") .attr({ id: "eventform-toast" }) .margin(0) .padding("0.55em 1.1em") .background("var(--quillred)") .color("white") .fontSize(0.85, em) .fontWeight("500") .fontFamily("Arial") .borderRadius("0.5em") .boxShadow("0 2px 10px rgba(0,0,0,0.15)") .whiteSpace("nowrap") }) .attr({ id: "eventform-toast-wrap" }) .alignItems("center") .overflow("hidden") .maxHeight(0) .opacity(0) .flexShrink(0) // ── Scrollable body ─────────────────────────────────── VStack(() => { // ── Title card ──────────────────────────────────── VStack(() => { input("Title", "100%") .attr({ name: "title", type: "text", value: this.editEvent?.title ?? "" }) .border("none").outline("none").background("transparent") .color("var(--text)").fontSize(1.1, em).fontFamily("Arial") .fontWeight("500").padding("0.9em 1em").boxSizing("border-box") }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden") .flexShrink(0) // ── Date / time card ────────────────────────────── VStack(() => { // All day row HStack(() => { p("All day") .margin(0).fontSize(0.92, em).color("var(--headertext)") input("", "auto") .attr({ name: "all_day", type: "checkbox", ...(initialAllDay ? { checked: true } : {}) }) .accentColor("var(--quillred)").transform("scale(1.25)") .onChange(() => { const isAllDay = this.$('[name="all_day"]').checked const display = isAllDay ? "none" : "" this.$("#time_start_time").style.display = display this.$("#time_end_time").style.display = display }) }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .justifyContent("space-between").alignItems("center") .borderBottom("1px solid var(--divider)") // Starts row HStack(() => { p("Starts") .margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0) HStack(() => { input("", "auto", "1em") .attr({ name: "time_start_date", type: "date", id: "time_start_date", value: this.startDate }) .styles(this.cardInputStyles).fontSize(0.88, em) .onChange((e) => { const oldDay = new Date(this.startDate + 'T00:00:00').getDay(); this.startDate = e.target.value; this.enforceEndAfterStart(); if (this.recurrence?.frequency === 'weekly' && this.recurrence.days_of_week?.length > 0) { const newDay = new Date(this.startDate + 'T00:00:00').getDay(); if (newDay !== oldDay && this.recurrence.days_of_week.includes(oldDay)) { const updated = [...new Set(this.recurrence.days_of_week.map(d => d === oldDay ? newDay : d))].sort((a, b) => a - b); this.recurrence.days_of_week = updated; const oldEl = this.$(`#recur-day-${oldDay}`); const newEl = this.$(`#recur-day-${newDay}`); if (oldEl) { oldEl.style.background = 'transparent'; oldEl.style.color = 'var(--text)'; oldEl.style.opacity = '0.4'; } if (newEl) { newEl.style.background = 'var(--quillred)'; newEl.style.color = 'white'; newEl.style.opacity = '1'; } } } }) input("", "auto", "1em") .attr({ name: "time_start_time", type: "time", id: "time_start_time", value: this.startTime, step: "300" }) .styles(this.cardInputStyles).fontSize(0.88, em) .display(showTime ? "" : "none") .onChange((e) => { this.startTime = e.target.value; this.enforceEndAfterStart() }) }) .gap(0.35, em).flex(1).justifyContent("flex-end") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").borderBottom("1px solid var(--divider)") // Ends row HStack(() => { p("Ends") .margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0) HStack(() => { input("", "auto", "1em") .attr({ name: "time_end_date", type: "date", id: "time_end_date", value: this.endDate }) .styles(this.cardInputStyles).fontSize(0.88, em) .onChange((e) => { this.endDate = e.target.value; this.enforceEndAfterStart() }) input("", "auto", "1em") .attr({ name: "time_end_time", type: "time", id: "time_end_time", value: this.endTime, step: "300" }) .styles(this.cardInputStyles).fontSize(0.88, em) .display(showTime ? "" : "none") .onChange((e) => { this.endTime = e.target.value; this.enforceEndAfterStart() }) }) .gap(0.35, em).flex(1).justifyContent("flex-end") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").borderBottom("1px solid var(--divider)") // Repeat row HStack(() => { p("Repeat") .margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0) p(this.recurrenceLabel()) .attr({ id: "recur-label" }) .margin(0).fontSize(0.88, em).fontFamily("Arial") .color(this.recurrence ? "var(--text)" : "var(--headertext)") .opacity(this.recurrence ? 1 : 0.55) .flex(1).textAlign("right") p("›") .attr({ id: "recur-chevron" }) .margin(0).opacity(0.3).fontSize(1.1, em).flexShrink(0) .transition("transform 0.25s ease") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.5, em) .cursor("pointer") .onTap(() => { const picker = this.$("#recur-picker") const chevron = this.$("#recur-chevron") if (!picker) return const isOpen = picker.getAttribute("data-open") === "1" if (isOpen) { picker.setAttribute("data-open", "0") picker.style.maxHeight = "0" if (chevron) chevron.style.transform = "" } else { picker.setAttribute("data-open", "1") picker.style.maxHeight = picker.scrollHeight + "px" if (chevron) chevron.style.transform = "rotate(90deg)" } }) // Inline recurrence picker VStack(() => { const currentKey = this.recurrenceOptionKey() const isWeekly = currentKey === 'weekly' || currentKey === 'biweekly' const selectedDays = this.recurrence?.days_of_week ?? [] const recurrenceOptions = [ { key: "never", label: "Never" }, { key: "daily", label: "Daily" }, { key: "weekly", label: "Weekly" }, { key: "biweekly", label: "Every 2 weeks" }, { key: "monthly", label: "Monthly" }, { key: "yearly", label: "Yearly" }, ] recurrenceOptions.forEach(opt => { HStack(() => { p(opt.label) .margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial").flex(1) p("✓") .attr({ id: `recur-check-${opt.key}` }) .margin(0).fontSize(0.88, em) .color("var(--quillred)").fontWeight("700") .display(currentKey === opt.key ? "" : "none") }) .paddingHorizontal(1.25, em).paddingVertical(0.72, em) .alignItems("center") .borderBottom("1px solid var(--divider)") .cursor("pointer") .onTap(() => { let newRecurrence = null if (opt.key !== 'never') { if (opt.key === 'weekly' || opt.key === 'biweekly') { const sd = new Date(this.startDate + 'T00:00:00').getDay() const curDays = this.recurrence?.frequency === 'weekly' ? (this.recurrence.days_of_week ?? [sd]) : [sd] newRecurrence = { frequency: 'weekly', interval: opt.key === 'biweekly' ? 2 : 1, days_of_week: [...curDays] } } else { const freqMap = { daily: 'daily', monthly: 'monthly', yearly: 'yearly' } newRecurrence = { frequency: freqMap[opt.key], interval: 1 } } } this.recurrence = newRecurrence const allKeys = ['never', 'daily', 'weekly', 'biweekly', 'monthly', 'yearly'] allKeys.forEach(k => { const el = this.$(`#recur-check-${k}`) if (el) el.style.display = k === opt.key ? "" : "none" }) const labelEl = this.$("#recur-label") if (labelEl) { labelEl.textContent = this.recurrenceLabel() labelEl.style.color = newRecurrence ? "var(--text)" : "var(--headertext)" labelEl.style.opacity = newRecurrence ? "1" : "0.55" } const isNowWeekly = opt.key === 'weekly' || opt.key === 'biweekly' const daysRow = this.$("#recur-days") if (daysRow) daysRow.style.display = isNowWeekly ? "" : "none" if (isNowWeekly && newRecurrence?.days_of_week) { const days = newRecurrence.days_of_week; [0, 1, 2, 3, 4, 5, 6].forEach(d => { const el = this.$(`#recur-day-${d}`) if (!el) return const sel = days.includes(d) el.style.background = sel ? "var(--quillred)" : "transparent" el.style.color = sel ? "white" : "var(--text)" el.style.opacity = sel ? "1" : "0.4" }) } const picker = this.$("#recur-picker") if (picker && picker.getAttribute("data-open") === "1") { picker.style.maxHeight = picker.scrollHeight + "px" } }) }) // Day-of-week selector (visible only for weekly/biweekly) const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'] HStack(() => { DAY_LABELS.forEach((label, i) => { const sel = selectedDays.includes(i) span(label) .attr({ id: `recur-day-${i}` }) .width(2, em).height(2, em) .lineHeight("2em") .textAlign("center") .borderRadius(50, pct) .fontSize(0.78, em).fontWeight("600") .cursor("pointer").flexShrink(0) .background(sel ? "var(--quillred)" : "transparent") .color(sel ? "white" : "var(--text)") .opacity(sel ? "1" : "0.4") .onTap(() => { if (!this.recurrence?.days_of_week) return const days = this.recurrence.days_of_week const idx = days.indexOf(i) if (idx >= 0) { if (days.length > 1) days.splice(idx, 1) } else { days.push(i) } const nowSel = days.includes(i) const el = this.$(`#recur-day-${i}`) if (el) { el.style.background = nowSel ? "var(--quillred)" : "transparent" el.style.color = nowSel ? "white" : "var(--text)" el.style.opacity = nowSel ? "1" : "0.4" } const picker = this.$("#recur-picker") if (picker && picker.getAttribute("data-open") === "1") { picker.style.maxHeight = picker.scrollHeight + "px" } }) }) }) .attr({ id: "recur-days" }) .justifyContent("space-around") .paddingHorizontal(0.5, em).paddingVertical(0.65, em) .display(isWeekly ? "" : "none") }) .attr({ id: "recur-picker", "data-open": "0" }) .overflow("hidden").maxHeight(0) .transition("max-height 0.3s ease") }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden") .flexShrink(0) // ── Calendar / Location / Notes card ────────────── VStack(() => { // Calendar row — tapping expands inline picker HStack(() => { p("Calendar") .margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0) HStack(() => { this.selectedCalendars.forEach(cal => { HStack(() => { p("").width(0.6, em).height(0.6, em) .background(cal.color).borderRadius(50, pct).flexShrink(0) p(cal.name) .margin(0).fontSize(0.82, em).fontWeight("600") .color("var(--text)").whiteSpace("nowrap") }) .gap(0.3, em).alignItems("center") }) }) .attr({ id: "calendar-display" }) .flex(1).justifyContent("flex-end").flexWrap("wrap").gap(0.5, em) p("›") .attr({ id: "cal-chevron" }) .margin(0).opacity(0.3).fontSize(1.1, em).flexShrink(0) .transition("transform 0.25s ease") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.5, em) .borderBottom("1px solid var(--divider)").cursor("pointer") .onTap(() => { const picker = this.$("#cal-picker") const chevron = this.$("#cal-chevron") if (!picker) return const isOpen = picker.getAttribute("data-open") === "1" if (isOpen) { picker.setAttribute("data-open", "0") picker.style.maxHeight = "0" if (chevron) chevron.style.transform = "" } else { picker.setAttribute("data-open", "1") picker.style.maxHeight = picker.scrollHeight + "px" if (chevron) chevron.style.transform = "rotate(90deg)" } }) // Inline calendar picker (collapsed by default) VStack(() => { this.calendars.forEach(cal => { const isSelected = this.selectedCalendars.some(c => c.id === cal.id) HStack(() => { HStack(() => { p("").width(0.65, em).height(0.65, em) .background(cal.color).borderRadius(50, pct).flexShrink(0) p(cal.name) .margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial") }) .gap(0.45, em).alignItems("center").flex(1) p("✓") .attr({ id: `cal-check-${cal.id}` }) .margin(0).fontSize(0.88, em) .color("var(--quillred)").fontWeight("700") .display(isSelected ? "" : "none") }) .paddingHorizontal(1.25, em).paddingVertical(0.72, em) .alignItems("center") .borderBottom("1px solid var(--divider)") .cursor("pointer") .onTap(() => { const idx = this.selectedCalendars.findIndex(c => c.id === cal.id) if (idx >= 0) { if (this.selectedCalendars.length > 1) { this.selectedCalendars.splice(idx, 1) const check = this.$(`#cal-check-${cal.id}`) if (check) check.style.display = "none" } } else { this.selectedCalendars.push(cal) const check = this.$(`#cal-check-${cal.id}`) if (check) check.style.display = "" } this.updateCalendarDisplay() }) }) }) .attr({ id: "cal-picker", "data-open": "0" }) .overflow("hidden").maxHeight(0) .transition("max-height 0.3s ease") // Location row HStack(() => { p("📍").margin(0).fontSize(0.85, em).flexShrink(0) input("Location", "100%") .attr({ name: "location", type: "text", value: this.editEvent?.location ?? "" }) .styles(this.cardInputStyles).flex(1) }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.65, em) .borderBottom("1px solid var(--divider)") // Noes row HStack(() => { p("📝") .margin(0).fontSize(0.85, em).flexShrink(0).alignSelf("flex-start").paddingTop(0.08, em) textarea(this.editEvent?.description ?? "") .attr({ name: "description" }) .styles(this.cardInputStyles).flex(1) .minHeight(3, em).resize("none") .fieldSizing("content").lineHeight("1.45") .onAppear(function() { this.value = this.placeholder }) }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("flex-start").gap(0.65, em) }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden") .flexShrink(0) // ── Attachments card ────────────────────────────── input("", "100%") .attr({ name: "attachments", type: "file", multiple: true }) .display("none") .onChange(async (e) => { const files = Array.from(e.target.files) e.target.value = "" this.uploadedFiles.push(...files) this.fileListComponent.update(files) this.updateFilesChevron() }) VStack(() => { HStack(() => { p("📎").margin(0).fontSize(0.85, em).flexShrink(0) p("Attachments") .margin(0).fontSize(0.92, em).color("var(--headertext)").flex(1) HStack(() => { span("Upload") .color("var(--quillred)").cursor("pointer") .fontFamily("Arial").fontSize(0.85, em) .padding(0.5, em).margin(-0.5, em) .onTap((e) => { e.stopPropagation(); this.$('[name="attachments"]').click() }) span("▼") .attr({ id: "files-chevron" }) .margin(0).fontSize(0.7, em).color("var(--text)").opacity(0.5) .display(this.hasAnyFiles() ? "inline-block" : "none") .transition("transform 0.3s ease") .transform(this.filesOpen ? "rotate(180deg)" : "rotate(0deg)") .padding(0.5, em).margin(-0.5, em) }) .gap(0.65, em).alignItems("center") }) .paddingHorizontal(1, em).paddingVertical(0.78, em).alignItems("center").gap(0.65, em) .cursor(this.hasAnyFiles() ? "pointer" : "") .onTap(() => { if (this.hasAnyFiles()) this.toggleFiles() }) VStack(() => { this.fileListComponent = EventFileList( this.existingAttachments, isEditMode ? (fileId) => this.handleDeleteAttachment(fileId) : null, (index) => this.handleDeleteNewFile(index), (file, url) => $("filepreview-")?.open(file, url) ) this.fileListComponent .padding("0 1em 0.75em").flexDirection("column") .gap(0.5, em).border("none") }) .attr({ id: "files-content" }) .overflow("hidden") .maxHeight(this.hasAnyFiles() && this.filesOpen ? "600px" : "0") .transition("max-height 0.5s ease") }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden") .flexShrink(0) if (isEditMode) { button("Delete Event") .attr({ type: "button" }) .width("calc(100% - 2em)").marginHorizontal(1, em) .padding(0.85, em).boxSizing("border-box") .background("transparent").color("var(--quillred)") .border("1.5px solid var(--quillred)") .borderRadius(12, px).fontSize(0.95, em).fontFamily("Arial") .fontWeight("600").cursor("pointer") .flexShrink(0) .onTap(() => this.handleDelete()) } }) .overflowY("scroll").flex(1).minHeight(0).paddingTop(0.85, em).paddingBottom(1.5, em).gap(0.85, em) // ── Footer: creator avatar + timestamps ─────────────── if (this.editEvent) { const members = global.currentNetwork.data?.members || [] const creator = members.find(m => m.id === this.editEvent.creator_id) if (creator) { HStack(() => { Avatar(creator, 2) VStack(() => { p(`Created ${calendarUtil.timeAgo(this.editEvent.created)} by ${creator.first_name}`) .margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.5) if (this.editEvent.updated_at && this.editEvent.updated_at !== this.editEvent.created) { p(`Last updated ${calendarUtil.timeAgo(this.editEvent.updated_at)}`) .margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.4) } }) .gap(0.15, em) }) // .position("absolute") // .bottom(0) // .left(0) // .right(0) .paddingHorizontal(1, em) .paddingVertical(0.65, em) .alignItems("center") .gap(0.5, em) .flexShrink(0) } } }) .height(100, pct) .position("relative") .onSubmit((e) => { e.preventDefault() this.handleSend(this.getFormData()) }) .onKeyDown((e) => { if (e.key === "Enter" && e.target.tagName !== "TEXTAREA" && e.target.tagName !== "BUTTON") e.preventDefault() }) }) .height(100, pct) } hasAnyFiles() { return this.existingAttachments.length > 0 || this.uploadedFiles.length > 0; } toggleFiles() { this.filesOpen = !this.filesOpen; const content = this.$("#files-content"); const chevron = this.$("#files-chevron"); if (content) content.style.maxHeight = this.filesOpen ? "600px" : "0"; if (chevron) chevron.style.transform = this.filesOpen ? "rotate(180deg)" : "rotate(0deg)"; } updateFilesChevron() { const chevron = this.$("#files-chevron"); const content = this.$("#files-content"); const hasFiles = this.hasAnyFiles(); if (chevron) chevron.style.display = hasFiles ? "inline-block" : "none"; if (!hasFiles && this.filesOpen) { this.filesOpen = false; if (content) content.style.maxHeight = "0"; } else if (hasFiles && !this.filesOpen) { this.filesOpen = true; if (content) content.style.maxHeight = "600px"; if (chevron) chevron.style.transform = "rotate(180deg)"; } } updateCalendarDisplay() { const el = this.$("#calendar-display"); if (!el) return; el.innerHTML = this.selectedCalendars.map(cal => `
${cal.name}