import { CocoComponent } from "@js/coco"; import { getData } from "@helpers/alpine"; import { captureElementScreenshot } from "@helpers/screenshot"; import { isDark } from "@helpers/color"; import { wasSuccessful } from "@helpers/turbo_events"; import { withUndo } from "@js/base/mixins"; const thumbnailOpts = { canvasWidth: 800, canvasHeight: 500, quality: 0.7, pixelRatio: 1, }; export default CocoComponent("appSlideEditor", (data) => { const initialData = { layout: data.layout, title: data.title, text1: data.text1, bgColor: data.bgColor, textColor: data.textColor, bgImage: { name: data.bgImage, data: data.bgImage, }, }; return { use: [withUndo()], ...initialData, saved: { ...initialData }, saving: false, ready: false, dragging: false, errors: [], thumbnailFile: null, get bgImagePicker() { return getData(this.$root.querySelector("[data-role='bg-image-picker']")); }, init() { // Update thumbnail field when new thumbnail is generated this.$watch("thumbnailFile", () => this._syncThumbnailField()); 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) ); }); this.ready = true; }); }, undo(name, value) { this[name] = value; }, redo(name, value) { this[name] = value; }, handleImageDrop(event) { this.dragging = false; if (this.bgImagePicker) { this.bgImagePicker.handleExternalDrop(event); } else { event.preventDefault(); } }, async save() { this.errors.length = 0; this.saving = true; if (this.shouldGenerateThumbnail) { try { await this._generateThumbnail(); } catch (error) { this.thumbnailFile = null; message = error.message || "Error generating slide thumbnail"; this._handleSaveError(message, { error }); return; } } if (this.$refs.form) { this.$refs.form.querySelector("form").requestSubmit(); } else { this.submitSuccess(); } }, submitEnd(event) { handler = wasSuccessful(event) ? "submitSuccess" : "submitError"; this[handler](event); }, submitSuccess() { this.history.clear(); this._fields.forEach((name) => (this.saved[name] = this[name])); this.saving = false; console.info("Custom slide saved"); this.$dispatch("slide:save-end", { success: true }); }, submitError($event) { message = "Error saving slide"; this._handleSaveError(message, { event: $event }); }, directUploadError($event) { $event.preventDefault(); this._handleSaveError($event.detail.error, { event: $event }); }, syncImageField(el, image) { const dataTransfer = new DataTransfer(); if (image.file && image.file instanceof File) { dataTransfer.items.add(image.file); } el.files = dataTransfer.files; }, input: { layout: { "x-model.fill": "layout" }, title: { "x-model.fill": "title" }, text1: { "x-model.fill": "text1" }, bgColor: { "x-model.fill": "bgColorHex" }, textColor: { "x-model.fill": "textColorHex" }, bgImage: { "x-effect": "syncImageField($el, bgImage)" }, bgImagePurge: { ":checked": "!hasBgImage" }, }, get hasBgImage() { return !!(this.bgImage && this.bgImage.data); }, get bgColorHex() { return this.bgColor.replace("#", ""); }, get textColorHex() { return this.textColor.replace("#", ""); }, get slideStyles() { return { backgroundColor: this.bgColor, color: this.textColor, backgroundImage: this.hasBgImage ? `url('${this.bgImage.data}')` : "none", }; }, get slideClasses() { return { "slide-bg-dark": this.isDarkBg, "slide-bg-light": !this.isDarkBg, }; }, get isDarkBg() { return this.hasBgImage || this.bgColor ? isDark(this.bgColor) : false; }, get shouldGenerateThumbnail() { return !!this.$refs.thumbnail; }, get _fields() { return Object.keys(initialData); }, async _generateThumbnail() { const screenshotSlide = this.$refs.screenshot.firstElementChild; // The HTML-to-image library didn't like these styles being dynamically // bound in the usual Alpine way so we have to manually apply them instead. for (const [key, value] of Object.entries(this.slideStyles)) { screenshotSlide.style[key] = value; } this.thumbnailFile = await captureElementScreenshot( screenshotSlide, "slide-thumbnail", thumbnailOpts ); console.info( "Slide thumbnail generated", `${this.thumbnailFile.size / 1000}KiB` ); }, // Called whenever a new thumbnail is generated, // adds the thumbnail file to the hidden thumbnail file // input upload field. _syncThumbnailField() { const dataTransfer = new DataTransfer(); if (this.thumbnailFile instanceof File) { dataTransfer.items.add(this.thumbnailFile); } this.$refs.thumbnail.files = dataTransfer.files; }, _handleSaveError(message = "Error saving slide", context = {}) { this.errors.push({ message, context }); this.errors.forEach((err) => { console.log(err.message); }); this.saving = false; this.$dispatch("slide:save-end", { success: false }); }, }; });