Skip to content

Commit

Permalink
feat(image-upload): add accepted file types option (#304)
Browse files Browse the repository at this point in the history
Co-authored-by: Dan Cormier <dancormierall@gmail.com>
  • Loading branch information
kristinalustig and dancormier authored Apr 24, 2024
1 parent e85b87b commit 93bf622
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 6 deletions.
7 changes: 5 additions & 2 deletions src/shared/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ export const defaultStrings = {
"Your image is too large to upload (over 2 MiB)" as string,
upload_error_generic:
"Image upload failed. Please try again." as string,
upload_error_unsupported_format:
"Please select an image (jpeg, png, gif) to upload" as string,
upload_error_unsupported_format: ({
supportedFormats,
}: {
supportedFormats: string;
}) => `Please select an image (${supportedFormats}) to upload`,
uploaded_image_preview_alt: "uploaded image preview" as string,
},
} as const;
Expand Down
53 changes: 49 additions & 4 deletions src/shared/prosemirror-plugins/image-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export interface ImageUploadOptions {
* If true, allow users to add images via an external url
*/
allowExternalUrls?: boolean;
/**
* An array of strings containing the accepted file types for the image uploader.
* See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types for appropriate image
* file types.
*/
acceptedFileTypes?: string[];
}

/**
Expand Down Expand Up @@ -84,6 +90,8 @@ export async function defaultImageUploadHandler(file: File): Promise<string> {
return json.UploadedImage;
}

const defaultAcceptedFileTypes = ["image/jpeg", "image/png", "image/gif"];

enum ValidationResult {
Ok,
FileTooLarge,
Expand Down Expand Up @@ -125,6 +133,7 @@ export class ImageUploader extends PluginInterfaceView<
super(INTERFACE_KEY);

const randomId = generateRandomId();
const acceptedFileTypes = uploadOptions.acceptedFileTypes || [];
this.isVisible = false;
this.uploadOptions = uploadOptions;
this.validateLink = validateLink;
Expand All @@ -137,7 +146,7 @@ export class ImageUploader extends PluginInterfaceView<
this.uploadField = document.createElement("input");
this.uploadField.type = "file";
this.uploadField.className = "js-image-uploader-input v-visible-sr";
this.uploadField.accept = "image/*";
this.uploadField.accept = acceptedFileTypes?.join(", ");
this.uploadField.multiple = false;
this.uploadField.id = "fileUpload" + randomId;

Expand All @@ -148,7 +157,7 @@ export class ImageUploader extends PluginInterfaceView<
<div class="fs-body2 p12 pb0 js-cta-container">
<label for="${this.uploadField.id}" class="d-inline-flex f:outline-ring s-link js-browse-button" aria-controls="image-preview-${randomId}">
Browse
</label>, drag & drop<span class="js-external-url-trigger-container d-none">, <button type="button" class="s-btn s-btn__link js-external-url-trigger">enter a link</button></span>, or paste an image <span class="fc-light fs-caption">Max size 2 MiB</span>
</label>, drag & drop<span class="js-external-url-trigger-container d-none">, <button type="button" class="s-btn s-btn__link js-external-url-trigger">enter a link</button></span>, or paste an image.
</div>
<div class="js-external-url-input-container p12 d-none">
Expand All @@ -175,6 +184,21 @@ export class ImageUploader extends PluginInterfaceView<
</div>
`;

// add the caption element to the cta container
const ctaContainer =
this.uploadContainer.querySelector(".js-cta-container");
const acceptedFileTypesString = acceptedFileTypes?.length
? acceptedFileTypes.join(", ").replace(/image\//g, "")
: "";

if (acceptedFileTypesString) {
const breakEl = document.createElement("br");
ctaContainer.appendChild(breakEl);
}
ctaContainer.appendChild(
this.getCaptionElement(acceptedFileTypesString)
);

// add in the uploadField right after the first child element
this.uploadContainer
.querySelector(`.js-browse-button`)
Expand Down Expand Up @@ -286,6 +310,19 @@ export class ImageUploader extends PluginInterfaceView<
event.stopPropagation();
}

getCaptionElement(text: string): HTMLElement {
const uploadCaptionEl = document.createElement("span");
uploadCaptionEl.className = "fc-light fs-caption";

let captionText = "(Max size 2 MiB)";
if (text) {
captionText = `Supported file types: ${text} ${captionText}`;
}
uploadCaptionEl.innerText = captionText;

return uploadCaptionEl;
}

handleFileSelection(view: EditorView): void {
this.resetImagePreview();
const files = this.uploadField.files;
Expand All @@ -311,7 +348,8 @@ export class ImageUploader extends PluginInterfaceView<
}

validateImage(image: File): ValidationResult {
const validTypes = ["image/jpeg", "image/png", "image/gif"];
const validTypes =
this.uploadOptions.acceptedFileTypes ?? defaultAcceptedFileTypes;
const sizeLimit = 0x200000; // 2 MiB

if (validTypes.indexOf(image.type) === -1) {
Expand Down Expand Up @@ -385,7 +423,14 @@ export class ImageUploader extends PluginInterfaceView<
return;
case ValidationResult.InvalidFileType:
this.showValidationError(
_t("image_upload.upload_error_unsupported_format")
_t("image_upload.upload_error_unsupported_format", {
supportedFormats: (
this.uploadOptions.acceptedFileTypes ||
defaultAcceptedFileTypes
)
.join(", ")
.replace(/image\//g, ""),
})
);
reject("invalid filetype");
return;
Expand Down
41 changes: 41 additions & 0 deletions test/shared/prosemirror-plugins/image-upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,47 @@ describe("image upload plugin", () => {
expect(validationMessage.classList).not.toContain("d-none");
});

it("should accept files that match those provided in the acceptedFileTypes option", async () => {
setupTestVariables({
acceptedFileTypes: ["image/bmp"],
});

showImageUploader(view.editorView);

await expect(
uploader.showImagePreview(
mockFile("some bmp file", "image/bmp")
)
).resolves.toBeUndefined();
expect(findPreviewElement(uploader).classList).not.toContain(
"d-none"
);
expect(findAddButton(uploader).disabled).toBe(false);
const validationMessage = findValidationMessage(uploader);
expect(validationMessage.classList).toContain("d-none");
});

it("should reject unaccepted files when acceptedFileTypes option is defined", async () => {
setupTestVariables({
acceptedFileTypes: ["image/bmp"],
});

showImageUploader(view.editorView);

await expect(
uploader.showImagePreview(
mockFile("some gif file", "image/gif")
)
).rejects.toBe("invalid filetype");
expect(findPreviewElement(uploader).classList).toContain("d-none");
expect(findAddButton(uploader).disabled).toBe(true);
const validationMessage = findValidationMessage(uploader);
expect(validationMessage.textContent).toBe(
"Please select an image (bmp) to upload"
);
expect(validationMessage.classList).not.toContain("d-none");
});

it("should hide error when hiding uploader", async () => {
showImageUploader(view.editorView);

Expand Down

0 comments on commit 93bf622

Please sign in to comment.