DEV: add pick-files-button component (#13764)
* DEV: add pick-files-button component * Scope querySelector to the component, add removeEventListener, fix formatting
This commit is contained in:
parent
366238bb81
commit
27b97e4f64
|
@ -0,0 +1,106 @@
|
||||||
|
import Component from "@ember/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { empty } from "@ember/object/computed";
|
||||||
|
import { bind, default as computed } from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
classNames: ["pick-files-button"],
|
||||||
|
acceptedFileTypes: null,
|
||||||
|
acceptAnyFile: empty("acceptedFileTypes"),
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
const fileInput = this.element.querySelector("input");
|
||||||
|
this.set("fileInput", fileInput);
|
||||||
|
fileInput.addEventListener("change", this.onChange, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.fileInput.removeEventListener("change", this.onChange);
|
||||||
|
},
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onChange() {
|
||||||
|
const files = this.fileInput.files;
|
||||||
|
this._filesPicked(files);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
acceptedFileTypesString() {
|
||||||
|
if (!this.acceptedFileTypes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.acceptedFileTypes.join(",");
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
acceptedExtensions() {
|
||||||
|
if (!this.acceptedFileTypes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.acceptedFileTypes
|
||||||
|
.filter((type) => type.startsWith("."))
|
||||||
|
.map((type) => type.substring(1));
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
acceptedMimeTypes() {
|
||||||
|
if (!this.acceptedFileTypes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.acceptedFileTypes.filter((type) => !type.startsWith("."));
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
openSystemFilePicker() {
|
||||||
|
this.fileInput.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
_filesPicked(files) {
|
||||||
|
if (!files || !files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._haveAcceptedTypes(files)) {
|
||||||
|
const message = I18n.t("pick_files_button.unsupported_file_picked", {
|
||||||
|
types: this.acceptedFileTypesString,
|
||||||
|
});
|
||||||
|
bootbox.alert(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onFilesPicked(files);
|
||||||
|
},
|
||||||
|
|
||||||
|
_haveAcceptedTypes(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (
|
||||||
|
!(this._hasAcceptedExtension(file) && this._hasAcceptedMimeType(file))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasAcceptedExtension(file) {
|
||||||
|
const extension = this._fileExtension(file.name);
|
||||||
|
return (
|
||||||
|
!this.acceptedExtensions || this.acceptedExtensions.includes(extension)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasAcceptedMimeType(file) {
|
||||||
|
return (
|
||||||
|
!this.acceptedMimeTypes || this.acceptedMimeTypes.includes(file.type)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_fileExtension(fileName) {
|
||||||
|
return fileName.split(".").pop();
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
|
||||||
|
{{#if acceptAnyFile}}
|
||||||
|
<input type="file">
|
||||||
|
{{else}}
|
||||||
|
<input type="file" accept={{acceptedFileTypesString}}>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import componentTest, {
|
||||||
|
setupRenderingTest,
|
||||||
|
} from "discourse/tests/helpers/component-test";
|
||||||
|
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import hbs from "htmlbars-inline-precompile";
|
||||||
|
import { triggerEvent } from "@ember/test-helpers";
|
||||||
|
import sinon from "sinon";
|
||||||
|
|
||||||
|
function createBlob(mimeType, extension) {
|
||||||
|
const blob = new Blob(["content"], {
|
||||||
|
type: mimeType,
|
||||||
|
});
|
||||||
|
blob.name = `filename${extension}`;
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
discourseModule(
|
||||||
|
"Integration | Component | pick-files-button",
|
||||||
|
function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it shows alert if a file with an unsupported extension was chosen",
|
||||||
|
{
|
||||||
|
skip: true,
|
||||||
|
template: hbs`
|
||||||
|
{{pick-files-button
|
||||||
|
acceptedFileTypes=this.acceptedFileTypes
|
||||||
|
onFilesChosen=this.onFilesChosen}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const expectedExtension = ".json";
|
||||||
|
this.set("acceptedFileTypes", [expectedExtension]);
|
||||||
|
this.set("onFilesChosen", () => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(assert) {
|
||||||
|
sinon.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
const wrongExtension = ".txt";
|
||||||
|
const file = createBlob("text/json", wrongExtension);
|
||||||
|
|
||||||
|
await triggerEvent("input#file-input", "change", { files: [file] });
|
||||||
|
|
||||||
|
assert.ok(bootbox.alert.calledOnce);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it shows alert if a file with an unsupported MIME type was chosen",
|
||||||
|
{
|
||||||
|
skip: true,
|
||||||
|
template: hbs`
|
||||||
|
{{pick-files-button
|
||||||
|
acceptedFileTypes=this.acceptedFileTypes
|
||||||
|
onFilesChosen=this.onFilesChosen}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const expectedMimeType = "text/json";
|
||||||
|
this.set("acceptedFileTypes", [expectedMimeType]);
|
||||||
|
this.set("onFilesChosen", () => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(assert) {
|
||||||
|
sinon.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
const wrongMimeType = "text/plain";
|
||||||
|
const file = createBlob(wrongMimeType, ".json");
|
||||||
|
|
||||||
|
await triggerEvent("input#file-input", "change", { files: [file] });
|
||||||
|
|
||||||
|
assert.ok(bootbox.alert.calledOnce);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -17,6 +17,7 @@
|
||||||
@import "ignored-user-list";
|
@import "ignored-user-list";
|
||||||
@import "keyboard_shortcuts";
|
@import "keyboard_shortcuts";
|
||||||
@import "navs";
|
@import "navs";
|
||||||
|
@import "pick-files-button";
|
||||||
@import "relative-time-picker";
|
@import "relative-time-picker";
|
||||||
@import "share-and-invite-modal";
|
@import "share-and-invite-modal";
|
||||||
@import "svg";
|
@import "svg";
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.pick-files-button {
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3784,6 +3784,9 @@ en:
|
||||||
leader: "leader"
|
leader: "leader"
|
||||||
detailed_name: "%{level}: %{name}"
|
detailed_name: "%{level}: %{name}"
|
||||||
|
|
||||||
|
pick_files_button:
|
||||||
|
unsupported_file_picked: "You have picked an unsupported file. Supported file types – %{types}."
|
||||||
|
|
||||||
# This section is exported to the javascript for i18n in the admin section
|
# This section is exported to the javascript for i18n in the admin section
|
||||||
admin_js:
|
admin_js:
|
||||||
type_to_filter: "type to filter..."
|
type_to_filter: "type to filter..."
|
||||||
|
|
Loading…
Reference in New Issue