Skip to content

Commit

Permalink
feat(image-upload): add acceptedFileTypes option (#305)
Browse files Browse the repository at this point in the history
* Refactor `acceptedFileTypes` to affect validation

* Add function to generate the caption element

* Minor tweaks so I can run tests

* Add default accepted file types value
  • Loading branch information
dancormier authored Apr 18, 2024
1 parent bf64d85 commit 5cdca73
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 26 deletions.
1 change: 1 addition & 0 deletions site/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ domReady(() => {
warningNoticeHtml: enableSamplePlugin
? "Images are useful in a post, but <strong>make sure the post is still clear without them</strong>. If you post images of code or error messages, copy and paste or type the actual code or message into the post directly."
: null,
acceptedFileTypes: ["image/jpeg", "image/png", "image/gif"],
};

// TODO should null out entire object, but that currently just defaults back to the original on merge
Expand Down
1 change: 1 addition & 0 deletions src/rich-text/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export class RichTextEditor extends BaseView {
menuParentContainer: null,
imageUpload: {
handler: defaultImageUploadHandler,
acceptedFileTypes: ["image/jpeg", "image/png", "image/gif"],
},
editorPlugins: [],
};
Expand Down
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
80 changes: 56 additions & 24 deletions src/shared/prosemirror-plugins/image-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export interface ImageUploadOptions {
*/
allowExternalUrls?: boolean;
/**
* A string-based list that takes any number of file type names (e.g. bmp, gif, etc.) as input and uses them to populate help text in image uploader
* 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 @@ -129,6 +131,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 @@ -141,12 +144,9 @@ 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;
const acceptedFileTypeString = this.createAcceptedFileTypeString(
uploadOptions.acceptedFileTypes
);

// TODO i18n
this.uploadContainer.innerHTML = escapeHTML`
Expand All @@ -155,11 +155,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.
<div class="fc-light fs-caption d-flex gsx gs4">
<div class="flex--item">${acceptedFileTypeString}</div>
<div class="flex--item">(Max size 2 MiB)</div>
</div>
</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 @@ -186,6 +182,20 @@ export class ImageUploader extends PluginInterfaceView<
</div>
`;

// add the caption element to the cta container
const ctaContainer =
this.uploadContainer.querySelector(".js-cta-container");
const acceptedFileTypesString =
this.getAcceptedFileTypesString(acceptedFileTypes);

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 @@ -297,19 +307,36 @@ export class ImageUploader extends PluginInterfaceView<
event.stopPropagation();
}

createAcceptedFileTypeString(acceptedTypes: string[] = []): string {
if (acceptedTypes.length === 0) {
return "";
}
let acceptedTypesString = "Supported file types: ";
acceptedTypes.forEach((s, i) => {
if (i === acceptedTypes.length - 1) {
acceptedTypesString += `or ${s}.`;
} else {
acceptedTypesString += `${s}, `;
getAcceptedFileTypesString(types: string[]): string {
let uploadCaptionString = "";
const acceptedTypes = types?.sort() || [];
const defaultTypes = ["image/gif", "image/jpeg", "image/png"];

// If the arrays are different, we modify the caption string
if (JSON.stringify(acceptedTypes) !== JSON.stringify(defaultTypes)) {
if (acceptedTypes.length > 1) {
acceptedTypes[acceptedTypes.length - 1] =
"or " + acceptedTypes[acceptedTypes.length - 1];
}
});
return acceptedTypesString;
uploadCaptionString = acceptedTypes
.join(", ")
.replace(/image\//g, "");
}

return uploadCaptionString;
}

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 {
Expand Down Expand Up @@ -337,7 +364,7 @@ export class ImageUploader extends PluginInterfaceView<
}

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

if (validTypes.indexOf(image.type) === -1) {
Expand Down Expand Up @@ -411,7 +438,12 @@ 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.getAcceptedFileTypesString([
...(this.uploadOptions.acceptedFileTypes || []),
]) || "jpeg, png, gif",
})
);
reject("invalid filetype");
return;
Expand Down
2 changes: 2 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,8 @@ describe("image upload plugin", () => {
expect(validationMessage.classList).not.toContain("d-none");
});

it.todo("should respect acceptedFileTypes option");

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

Expand Down

0 comments on commit 5cdca73

Please sign in to comment.