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) => { return { use: [withUndo()], ...data, saved: { ...data }, saving: false, ready: false, dragging: false, errors: [], thumbnailFile: null, get bgImagePicker() { return getData(this.$root.querySelector("[data-role='bg-image-picker']")); }, init() { // Add property changes to the undo/redo history this._fields.forEach((name) => { this.$watch(name, (value, oldValue) => this.history.add(name, value, oldValue) ); }); // 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(() => (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; } } this.$refs.form.requestSubmit(); }, 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 }); }, get slideStyles() { return { backgroundColor: this.bgColor, color: this.textColor, backgroundImage: this.bgImage && this.bgImage.data && `url('${this.bgImage.data}')`, }; }, get slideClasses() { return { "slide-bg-dark": this.isDarkBg, "slide-bg-light": !this.isDarkBg, }; }, get isDarkBg() { return (this.bgImage && this.bgImage.data) || this.bgColor ? isDark(this.bgColor) : false; }, get shouldGenerateThumbnail() { return !!this.$refs.thumbnail; }, get _fields() { return Object.keys(data); }, 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.saving = false; this.$dispatch("slide:save-end", { success: false }); }, }; });