import { CocoComponent } from "@assets/js/coco/component"; import { getData } from "@helpers/alpine"; import { captureElementScreenshot } from "@helpers/screenshot"; import { isDark } from "@helpers/color"; import { wasSuccessful } from "@helpers/turbo_events"; export default CocoComponent("appSlideEditor", (data) => { const initialData = { layout: data.layout, title: data.title, text1: data.text1, text2: data.text2, bgColor: data.bgColor, textColor: data.textColor, bgImage: { name: data.bgImage, data: data.bgImage, }, image1: { name: data.image1, data: data.image1, }, image2: { name: data.image2, data: data.image2, }, }; return { ...initialData, saved: { ...initialData }, saving: false, ready: false, dragging: false, errors: [], thumbnailFile: null, get bgImagePicker() { return getData(this.$root.querySelector("[data-role='bg-image-picker']")); }, get layoutPicker() { return getData(this.$root.querySelector("[data-role='layout-picker']")); }, init() { this.$watch("errors", (errors) => { errors.forEach((error) => console.error(error.message)); // TODO display errors properly! }); this.$nextTick(() => { // Add property changes to the undo/redo history this._fields.forEach((name) => { this.$watch(name, (value, oldValue) => this.history.add(name, value, oldValue) ); }); // Stop navigation when changes have not been saved this.$watch("history.undoable", (undoable) => { if (undoable) { window.onbeforeunload = () => true; } else { window.onbeforeunload = null; } }); this.ready = true; }); }, undo(name, value) { this[name] = value; this.$nextTick(() => this.updateTextareaSizes()); }, redo(name, value) { this[name] = value; this.$nextTick(() => this.updateTextareaSizes()); }, updateTextareaSizes() { const inputs = this.$root.querySelectorAll( "[data-component='seamless-textarea']" ); Array.from(inputs).forEach((el) => getData(el).onResize()); }, setLayout(layout) { this.layout = layout; }, handleImageDrop(event) { this.dragging = false; if (this.bgImagePicker) { this.bgImagePicker.handleExternalDrop(event); } else { event.preventDefault(); } }, async save() { this.clearErrors(); this.saving = true; if (this.$refs.thumbnail) { try { await this._generateThumbnail(); } catch (error) { this.thumbnailFile = null; message = error.message || "Error generating slide thumbnail"; this.handleSaveError(message, { error }); return; } } const form = this.$refs.form && this.$refs.form.querySelector("form"); if (form && form.dataset.submit !== "false") { const submitButton = form.querySelector("[type='submit']"); if (submitButton) { submitButton.click(); } else { this.handleSaveError("Slide form is missing a submit button"); } } else { this.submitSuccess(); } }, submitEnd($event) { this[wasSuccessful($event) ? "submitSuccess" : "submitError"]($event); }, submitSuccess($event) { this.history.clear(); this._fields.forEach((name) => (this.saved[name] = this[name])); this.saving = false; console.info("Slide changes saved"); this.$dispatch("slide:save-end", { success: true }); }, submitError($event) { const message = "Error saving slide - form could not be submitted"; this.handleSaveError(message, { event: $event }); }, directUploadError($event) { $event.preventDefault(); // prevent generic alert dialog error const message = $event.detail.error || "Error uploading file to storage"; this.handleSaveError(message, { event: $event }); }, handleSaveError(message = "Error saving slide", context = {}) { this.errors.push({ message, context }); this.saving = false; this.$dispatch("slide:save-end", { success: false }); }, clearErrors() { this.errors.length = 0; }, syncFileInput(input, file) { const dataTransfer = new DataTransfer(); if (file && file instanceof File) { dataTransfer.items.add(file); } input.files = dataTransfer.files; }, handleSlideClick(event) { if (Array.from(event.target.classList).includes("slide-text")) { event.target.querySelector("textarea").focus(); } }, get blankTextAreasList() { return ["title", "text1", "text2"] .map((textareaName) => (this[textareaName] === "" ? textareaName : "")) .join(" ") .trim(); }, get hasBgImage() { return !!(this.bgImage && this.bgImage.data); }, get hasImage1() { return !!(this.image1 && this.image1.data); }, get hasImage2() { return !!(this.image2 && this.image2.data); }, get bgColorHex() { return this.bgColor.replace("#", ""); }, get textColorHex() { return this.textColor.replace("#", ""); }, get isDarkBg() { return this.hasBgImage || this.bgColor ? isDark(this.bgColor) : false; }, get _fields() { return Object.keys(initialData); }, async _generateThumbnail() { this.thumbnailFile = await captureElementScreenshot( this.$refs.thumbnail.firstElementChild, "slide-thumbnail" ); console.info( "Slide thumbnail generated", `${this.thumbnailFile.size / 1000}KiB` ); }, /* bindings */ slide: { [":style"]() { return { backgroundColor: this.bgColor, color: this.textColor, backgroundImage: this.hasBgImage ? `url('${this.bgImage.data}')` : "none", }; }, [":class"]() { return { "slide-bg-dark": this.isDarkBg, "slide-bg-light": !this.isDarkBg, }; }, [":data-layout"]: "layout", }, input: { layout: { "x-model.fill": "layout.replace(/-/g,'_')" }, title: { "x-model.fill": "title" }, text1: { "x-model.fill": "text1" }, text2: { "x-model.fill": "text2" }, bgColor: { "x-model.fill": "bgColorHex" }, textColor: { "x-model.fill": "textColorHex" }, bgImage: { "x-effect": "syncFileInput($el, bgImage.file)" }, bgImagePurge: { ":checked": "!hasBgImage" }, image1: { "x-effect": "syncFileInput($el, image1.file)" }, image1Purge: { ":checked": "!hasImage1" }, image2: { "x-effect": "syncFileInput($el, image2.file)" }, image2Purge: { ":checked": "!hasImage2" }, thumbnailImage: { "x-effect": "syncFileInput($el, thumbnailFile)" }, }, }; });