diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md index ee979f24eb560..a043b5c86cccd 100644 --- a/actiontext/CHANGELOG.md +++ b/actiontext/CHANGELOG.md @@ -1,3 +1,15 @@ +* Dispatch direct-upload events on attachment uploads + + When using Action Text's rich textarea, it's possible to attach files to the + editor. Previously, that action didn't dispatch any events, which made it hard + to react to the file uploads. For instance, if an upload failed, there was no + way to notify the user about it, or remove the attachment from the editor. + + This commits adds new events - `direct-upload:start`, `direct-upload:progress`, + and `direct-upload:end` - similar to how Active Storage's direct uploads work. + + *Matheus Richard*, *Brad Rees* + * Add `store_if_blank` option to `has_rich_text` Pass `store_if_blank: false` to not create `ActionText::RichText` records when saving with a blank attribute, such as from an optional form parameter. diff --git a/actiontext/app/assets/javascripts/actiontext.esm.js b/actiontext/app/assets/javascripts/actiontext.esm.js index 2f433b7f0a83b..79028350380a3 100644 --- a/actiontext/app/assets/javascripts/actiontext.esm.js +++ b/actiontext/app/assets/javascripts/actiontext.esm.js @@ -853,25 +853,47 @@ class AttachmentUpload { } start() { this.directUpload.create(this.directUploadDidComplete.bind(this)); + this.dispatch("start"); } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", (event => { const progress = event.loaded / event.total * 100; this.attachment.setUploadProgress(progress); + if (progress) { + this.dispatch("progress", { + progress: progress + }); + } })); } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`); + this.dispatchError(error); + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }); + this.dispatch("end"); } - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }); } createBlobUrl(signedId, filename) { return this.blobUrlTemplate.replace(":signed_id", signedId).replace(":filename", encodeURIComponent(filename)); } + dispatch(name, detail = {}) { + detail.attachment = this.attachment; + return dispatchEvent(this.element, `direct-upload:${name}`, { + detail: detail + }); + } + dispatchError(error) { + const event = this.dispatch("error", { + error: error + }); + if (!event.defaultPrevented) { + alert(error); + } + } get directUploadUrl() { return this.element.dataset.directUploadUrl; } diff --git a/actiontext/app/assets/javascripts/actiontext.js b/actiontext/app/assets/javascripts/actiontext.js index ef283f27577e7..e61f03ce103fa 100644 --- a/actiontext/app/assets/javascripts/actiontext.js +++ b/actiontext/app/assets/javascripts/actiontext.js @@ -826,25 +826,47 @@ } start() { this.directUpload.create(this.directUploadDidComplete.bind(this)); + this.dispatch("start"); } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", (event => { const progress = event.loaded / event.total * 100; this.attachment.setUploadProgress(progress); + if (progress) { + this.dispatch("progress", { + progress: progress + }); + } })); } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`); + this.dispatchError(error); + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }); + this.dispatch("end"); } - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }); } createBlobUrl(signedId, filename) { return this.blobUrlTemplate.replace(":signed_id", signedId).replace(":filename", encodeURIComponent(filename)); } + dispatch(name, detail = {}) { + detail.attachment = this.attachment; + return dispatchEvent(this.element, `direct-upload:${name}`, { + detail: detail + }); + } + dispatchError(error) { + const event = this.dispatch("error", { + error: error + }); + if (!event.defaultPrevented) { + alert(error); + } + } get directUploadUrl() { return this.element.dataset.directUploadUrl; } diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js index 77fbc97df64db..d2c0a3af2e15c 100644 --- a/actiontext/app/javascript/actiontext/attachment_upload.js +++ b/actiontext/app/javascript/actiontext/attachment_upload.js @@ -1,4 +1,4 @@ -import { DirectUpload } from "@rails/activestorage" +import { DirectUpload, dispatchEvent } from "@rails/activestorage" export class AttachmentUpload { constructor(attachment, element) { @@ -9,24 +9,29 @@ export class AttachmentUpload { start() { this.directUpload.create(this.directUploadDidComplete.bind(this)) + this.dispatch("start") } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", event => { const progress = event.loaded / event.total * 100 this.attachment.setUploadProgress(progress) + if (progress) { + this.dispatch("progress", { progress: progress }) + } }) } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`) + this.dispatchError(error) + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }) + this.dispatch("end") } - - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }) } createBlobUrl(signedId, filename) { @@ -35,6 +40,18 @@ export class AttachmentUpload { .replace(":filename", encodeURIComponent(filename)) } + dispatch(name, detail = {}) { + detail.attachment = this.attachment + return dispatchEvent(this.element, `direct-upload:${name}`, { detail }) + } + + dispatchError(error) { + const event = this.dispatch("error", { error }) + if (!event.defaultPrevented) { + alert(error); + } + } + get directUploadUrl() { return this.element.dataset.directUploadUrl } diff --git a/activestorage/app/assets/javascripts/activestorage.esm.js b/activestorage/app/assets/javascripts/activestorage.esm.js index e6d60e64a6f2e..1ac98355eccb5 100644 --- a/activestorage/app/assets/javascripts/activestorage.esm.js +++ b/activestorage/app/assets/javascripts/activestorage.esm.js @@ -845,4 +845,4 @@ function autostart() { setTimeout(autostart, 1); -export { DirectUpload, DirectUploadController, DirectUploadsController, start }; +export { DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent, start }; diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index a31bf08fbf433..754c727bc8970 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -822,6 +822,7 @@ exports.DirectUpload = DirectUpload; exports.DirectUploadController = DirectUploadController; exports.DirectUploadsController = DirectUploadsController; + exports.dispatchEvent = dispatchEvent; exports.start = start; Object.defineProperty(exports, "__esModule", { value: true diff --git a/activestorage/app/javascript/activestorage/index.js b/activestorage/app/javascript/activestorage/index.js index d8e3f150c4f25..4f15c19ba6118 100644 --- a/activestorage/app/javascript/activestorage/index.js +++ b/activestorage/app/javascript/activestorage/index.js @@ -2,7 +2,8 @@ import { start } from "./ujs" import { DirectUpload } from "./direct_upload" import { DirectUploadController } from "./direct_upload_controller" import { DirectUploadsController } from "./direct_uploads_controller" -export { start, DirectUpload, DirectUploadController, DirectUploadsController } +import { dispatchEvent } from "./helpers" +export { start, DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent } function autostart() { if (window.ActiveStorage) { diff --git a/guides/source/action_text_overview.md b/guides/source/action_text_overview.md index 16a5bc1df2319..2ed2afd3bcbfa 100644 --- a/guides/source/action_text_overview.md +++ b/guides/source/action_text_overview.md @@ -233,7 +233,6 @@ To customize the HTML rendered for embedded images and other attachments (known as blobs), edit the `app/views/active_storage/blobs/_blob.html.erb` template created by the installer: - ```html+erb <%# app/views/active_storage/blobs/_blob.html.erb %>
attachment--<%= blob.filename.extension %>"> @@ -270,6 +269,14 @@ encounter when working with Action Text and Active Storage is that images do not render correctly in the editor. This is usually due to the `libvips` dependency not being installed. +#### Attachment Direct Upload JavaScript Events + +| Event name | Event target | Event data (`event.detail`) | Description | +| --- | --- | --- | --- | +| `direct-upload:start` | `` | `{id, file}` | A direct upload is starting. | +| `direct-upload:progress` | `` | `{id, file, progress}` | As requests to store files progress. | +| `direct-upload:error` | `` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. | +| `direct-upload:end` | `` | `{id, file}` | A direct upload has ended. | ### Signed GlobalID