From 68beff92c9d3c9ab355853a1f3c277183b82e051 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 9 Jun 2025 19:39:25 -0700 Subject: [PATCH 1/9] document file list --- .../stories/components/FileList.stories.ts | 26 +++++++++++++++++++ frontend/src/stories/components/FileList.ts | 19 ++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 frontend/src/stories/components/FileList.stories.ts create mode 100644 frontend/src/stories/components/FileList.ts diff --git a/frontend/src/stories/components/FileList.stories.ts b/frontend/src/stories/components/FileList.stories.ts new file mode 100644 index 0000000000..4c09432af3 --- /dev/null +++ b/frontend/src/stories/components/FileList.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { renderComponent, type RenderProps } from "./FileList"; + +const meta = { + title: "Components/File List", + component: "btrix-file-list", + tags: ["autodocs"], + render: renderComponent, + decorators: (story) => html`
${story()}
`, + argTypes: {}, + args: { + files: [ + new File([new Blob()], "file.txt"), + new File([new Blob()], "file-2.txt"), + ], + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + args: {}, +}; diff --git a/frontend/src/stories/components/FileList.ts b/frontend/src/stories/components/FileList.ts new file mode 100644 index 0000000000..77e965a366 --- /dev/null +++ b/frontend/src/stories/components/FileList.ts @@ -0,0 +1,19 @@ +import { html } from "lit"; + +import "@/components/ui/file-list"; + +// import type { FileList } from "@/components/ui/file-list"; + +export type RenderProps = { files: File[] }; + +export const renderComponent = ({ files }: Partial) => { + return html` + + ${files?.map( + (file) => html` + + `, + )} + + `; +}; From 422a47ebcb63880569059f6ef6229f26326cdaf0 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 9 Jun 2025 19:40:04 -0700 Subject: [PATCH 2/9] create file input component --- frontend/src/components/ui/file-input.ts | 94 +++++++++++++++++++ frontend/src/components/ui/file-list.ts | 4 +- frontend/src/components/ui/index.ts | 1 + .../features/archived-items/file-uploader.ts | 6 +- .../stories/components/FileInput.stories.ts | 48 ++++++++++ frontend/src/stories/components/FileInput.ts | 27 ++++++ 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/ui/file-input.ts create mode 100644 frontend/src/stories/components/FileInput.stories.ts create mode 100644 frontend/src/stories/components/FileInput.ts diff --git a/frontend/src/components/ui/file-input.ts b/frontend/src/components/ui/file-input.ts new file mode 100644 index 0000000000..bda6e9ce78 --- /dev/null +++ b/frontend/src/components/ui/file-input.ts @@ -0,0 +1,94 @@ +import { localized } from "@lit/localize"; +import clsx from "clsx"; +import { html } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { TailwindElement } from "@/classes/TailwindElement"; +import type { BtrixChangeEvent } from "@/events/btrix-change"; +import { tw } from "@/utils/tailwind"; + +export type BtrixFileChangeEvent = BtrixChangeEvent; + +/** + * Allow attaching one or more files. + * + * @fires btrix-change + */ +@customElement("btrix-file-input") +@localized() +export class FileInput extends TailwindElement { + /** + * Specify which file types are allowed + */ + @property({ type: String }) + accept?: HTMLInputElement["accept"]; + + /** + * Enable selecting more than one file + */ + @property({ type: Boolean }) + multiple?: HTMLInputElement["multiple"]; + + /** + * Enable dragging files into drop zone + */ + @property({ type: Boolean }) + dropzone = false; + + @query("input[type='file']") + private readonly input?: HTMLInputElement | null; + + render() { + return html``; + } + + private readonly onDrop = (e: DragEvent) => { + e.preventDefault(); + + if (e.dataTransfer?.files) { + void this.handleChange(e.dataTransfer.files); + } else { + console.debug("no files dropped"); + } + }; + + private readonly onDragover = (e: DragEvent) => { + e.preventDefault(); + }; + + private async handleChange(files: FileList) { + await this.updateComplete; + + this.dispatchEvent( + new CustomEvent("btrix-change", { + detail: { value: files }, + composed: true, + bubbles: true, + }), + ); + } +} diff --git a/frontend/src/components/ui/file-list.ts b/frontend/src/components/ui/file-list.ts index 1ff8e37124..51917b6bd9 100644 --- a/frontend/src/components/ui/file-list.ts +++ b/frontend/src/components/ui/file-list.ts @@ -16,7 +16,7 @@ type FileRemoveDetail = { export type FileRemoveEvent = CustomEvent; /** - * @event on-remove FileRemoveEvent + * @event btrix-remove FileRemoveEvent */ @customElement("btrix-file-list-item") @localized() @@ -117,7 +117,7 @@ export class FileListItem extends BtrixElement { if (!this.file) return; await this.updateComplete; this.dispatchEvent( - new CustomEvent("on-remove", { + new CustomEvent("btrix-remove", { detail: { file: this.file, }, diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index d053ee38db..d67d0d68ce 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -18,6 +18,7 @@ import("./copy-button"); import("./copy-field"); import("./data-grid"); import("./details"); +import("./file-input"); import("./file-list"); import("./format-date"); import("./inline-input"); diff --git a/frontend/src/features/archived-items/file-uploader.ts b/frontend/src/features/archived-items/file-uploader.ts index eb7406680f..e7eacc1519 100644 --- a/frontend/src/features/archived-items/file-uploader.ts +++ b/frontend/src/features/archived-items/file-uploader.ts @@ -217,7 +217,7 @@ export class FileUploader extends BtrixElement { (file) => html``, )} @@ -278,7 +278,7 @@ export class FileUploader extends BtrixElement { html``, )} @@ -319,7 +319,7 @@ export class FileUploader extends BtrixElement { html``, )} diff --git a/frontend/src/stories/components/FileInput.stories.ts b/frontend/src/stories/components/FileInput.stories.ts new file mode 100644 index 0000000000..2751162e94 --- /dev/null +++ b/frontend/src/stories/components/FileInput.stories.ts @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { renderComponent, type RenderProps } from "./FileInput"; + +const meta = { + title: "Components/File Input", + component: "btrix-file-input", + tags: ["autodocs"], + render: renderComponent, + decorators: (story) => html`
${story()}
`, + argTypes: { + anchor: { table: { disable: true } }, + }, + args: { + anchor: html` + Select File + `, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + args: {}, +}; + +export const Multiple: Story = { + args: { + multiple: true, + anchor: html` + Select Files + `, + }, +}; + +export const DropZone: Story = { + args: { + dropzone: true, + anchor: html` + Drag file here or + + `, + }, +}; diff --git a/frontend/src/stories/components/FileInput.ts b/frontend/src/stories/components/FileInput.ts new file mode 100644 index 0000000000..578f607675 --- /dev/null +++ b/frontend/src/stories/components/FileInput.ts @@ -0,0 +1,27 @@ +import { html, type TemplateResult } from "lit"; + +import type { FileInput } from "@/components/ui/file-input"; + +import "@/components/ui/file-input"; + +export type RenderProps = FileInput & { + anchor: TemplateResult; +}; + +export const renderComponent = ({ + accept, + multiple, + dropzone, + anchor, +}: Partial) => { + return html` + + ${anchor} + + `; +}; From 0d08d3b2ff2109a11f1a618abb38f559c953ea3a Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 9 Jun 2025 19:53:40 -0700 Subject: [PATCH 3/9] clean up file list component --- .../src/components/ui/file-list/events.ts | 3 + .../file-list-item.ts} | 64 ++++--------------- .../src/components/ui/file-list/file-list.ts | 43 +++++++++++++ frontend/src/components/ui/file-list/index.ts | 4 ++ frontend/src/events/btrix-remove.ts | 7 ++ frontend/src/events/index.ts | 2 - .../features/archived-items/file-uploader.ts | 9 +-- .../stories/components/FileList.stories.ts | 1 + frontend/src/stories/components/FileList.ts | 4 +- 9 files changed, 73 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/ui/file-list/events.ts rename frontend/src/components/ui/{file-list.ts => file-list/file-list-item.ts} (66%) create mode 100644 frontend/src/components/ui/file-list/file-list.ts create mode 100644 frontend/src/components/ui/file-list/index.ts create mode 100644 frontend/src/events/btrix-remove.ts delete mode 100644 frontend/src/events/index.ts diff --git a/frontend/src/components/ui/file-list/events.ts b/frontend/src/components/ui/file-list/events.ts new file mode 100644 index 0000000000..f8e49e2f2f --- /dev/null +++ b/frontend/src/components/ui/file-list/events.ts @@ -0,0 +1,3 @@ +import type { BtrixRemoveEvent } from "@/events/btrix-remove"; + +export type FileRemoveEvent = BtrixRemoveEvent; diff --git a/frontend/src/components/ui/file-list.ts b/frontend/src/components/ui/file-list/file-list-item.ts similarity index 66% rename from frontend/src/components/ui/file-list.ts rename to frontend/src/components/ui/file-list/file-list-item.ts index 51917b6bd9..2a9d906e38 100644 --- a/frontend/src/components/ui/file-list.ts +++ b/frontend/src/components/ui/file-list/file-list-item.ts @@ -1,26 +1,19 @@ import { localized, msg } from "@lit/localize"; import { css, html } from "lit"; -import { - customElement, - property, - queryAssignedElements, -} from "lit/decorators.js"; +import { customElement, property } from "lit/decorators.js"; + +import type { FileRemoveEvent } from "./events"; -import { BtrixElement } from "@/classes/BtrixElement"; import { TailwindElement } from "@/classes/TailwindElement"; +import { LocalizeController } from "@/controllers/localize"; import { truncate } from "@/utils/css"; -type FileRemoveDetail = { - file: File; -}; -export type FileRemoveEvent = CustomEvent; - /** * @event btrix-remove FileRemoveEvent */ @customElement("btrix-file-list-item") @localized() -export class FileListItem extends BtrixElement { +export class FileListItem extends TailwindElement { static styles = [ truncate, css` @@ -75,6 +68,8 @@ export class FileListItem extends BtrixElement { @property({ type: Boolean }) progressIndeterminate?: boolean; + readonly localize = new LocalizeController(this); + render() { if (!this.file) return; return html`
@@ -117,50 +112,13 @@ export class FileListItem extends BtrixElement { if (!this.file) return; await this.updateComplete; this.dispatchEvent( - new CustomEvent("btrix-remove", { + new CustomEvent("btrix-remove", { detail: { - file: this.file, + item: this.file, }, + composed: true, + bubbles: true, }), ); }; } - -@customElement("btrix-file-list") -export class FileList extends TailwindElement { - static styles = [ - css` - ::slotted(btrix-file-list-item) { - --border: 1px solid var(--sl-panel-border-color); - --item-border-top: var(--border); - --item-border-left: var(--border); - --item-border-right: var(--border); - --item-border-bottom: var(--border); - --item-box-shadow: var(--sl-shadow-x-small); - --item-border-radius: var(--sl-border-radius-medium); - display: block; - } - - ::slotted(btrix-file-list-item:not(:last-of-type)) { - margin-bottom: var(--sl-spacing-x-small); - } - `, - ]; - - @queryAssignedElements({ selector: "btrix-file-list-item" }) - listItems!: HTMLElement[]; - - render() { - return html`
- -
`; - } - - private handleSlotchange() { - this.listItems.map((el) => { - if (!el.attributes.getNamedItem("role")) { - el.setAttribute("role", "listitem"); - } - }); - } -} diff --git a/frontend/src/components/ui/file-list/file-list.ts b/frontend/src/components/ui/file-list/file-list.ts new file mode 100644 index 0000000000..9cb6d32673 --- /dev/null +++ b/frontend/src/components/ui/file-list/file-list.ts @@ -0,0 +1,43 @@ +import { css, html } from "lit"; +import { customElement, queryAssignedElements } from "lit/decorators.js"; + +import { TailwindElement } from "@/classes/TailwindElement"; + +@customElement("btrix-file-list") +export class FileList extends TailwindElement { + static styles = [ + css` + ::slotted(btrix-file-list-item) { + --border: 1px solid var(--sl-panel-border-color); + --item-border-top: var(--border); + --item-border-left: var(--border); + --item-border-right: var(--border); + --item-border-bottom: var(--border); + --item-box-shadow: var(--sl-shadow-x-small); + --item-border-radius: var(--sl-border-radius-medium); + display: block; + } + + ::slotted(btrix-file-list-item:not(:last-of-type)) { + margin-bottom: var(--sl-spacing-x-small); + } + `, + ]; + + @queryAssignedElements({ selector: "btrix-file-list-item" }) + listItems!: HTMLElement[]; + + render() { + return html`
+ +
`; + } + + private handleSlotchange() { + this.listItems.map((el) => { + if (!el.attributes.getNamedItem("role")) { + el.setAttribute("role", "listitem"); + } + }); + } +} diff --git a/frontend/src/components/ui/file-list/index.ts b/frontend/src/components/ui/file-list/index.ts new file mode 100644 index 0000000000..e4d99e0183 --- /dev/null +++ b/frontend/src/components/ui/file-list/index.ts @@ -0,0 +1,4 @@ +import "./file-list"; +import "./file-list-item"; + +export type { FileRemoveEvent } from "./events"; diff --git a/frontend/src/events/btrix-remove.ts b/frontend/src/events/btrix-remove.ts new file mode 100644 index 0000000000..b06d8cf25f --- /dev/null +++ b/frontend/src/events/btrix-remove.ts @@ -0,0 +1,7 @@ +export type BtrixRemoveEvent = CustomEvent<{ item: T }>; + +declare global { + interface GlobalEventHandlersEventMap { + "btrix-remove": BtrixRemoveEvent; + } +} diff --git a/frontend/src/events/index.ts b/frontend/src/events/index.ts deleted file mode 100644 index ca58e8d0ae..0000000000 --- a/frontend/src/events/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./btrix-change"; -import "./btrix-input"; diff --git a/frontend/src/features/archived-items/file-uploader.ts b/frontend/src/features/archived-items/file-uploader.ts index e7eacc1519..5076365102 100644 --- a/frontend/src/features/archived-items/file-uploader.ts +++ b/frontend/src/features/archived-items/file-uploader.ts @@ -212,13 +212,10 @@ export class FileUploader extends BtrixElement { } return html` - + ${Array.from(this.fileList).map( (file) => - html``, + html``, )} `; @@ -335,7 +332,7 @@ export class FileUploader extends BtrixElement { private readonly handleRemoveFile = (e: FileRemoveEvent) => { this.cancelUpload(); - const idx = this.fileList.indexOf(e.detail.file); + const idx = this.fileList.indexOf(e.detail.item); if (idx === -1) return; this.fileList = [ ...this.fileList.slice(0, idx), diff --git a/frontend/src/stories/components/FileList.stories.ts b/frontend/src/stories/components/FileList.stories.ts index 4c09432af3..e099cd1cef 100644 --- a/frontend/src/stories/components/FileList.stories.ts +++ b/frontend/src/stories/components/FileList.stories.ts @@ -6,6 +6,7 @@ import { renderComponent, type RenderProps } from "./FileList"; const meta = { title: "Components/File List", component: "btrix-file-list", + subcomponents: { FileListItem: "btrix-file-list-item" }, tags: ["autodocs"], render: renderComponent, decorators: (story) => html`
${story()}
`, diff --git a/frontend/src/stories/components/FileList.ts b/frontend/src/stories/components/FileList.ts index 77e965a366..bb4b346c75 100644 --- a/frontend/src/stories/components/FileList.ts +++ b/frontend/src/stories/components/FileList.ts @@ -2,13 +2,11 @@ import { html } from "lit"; import "@/components/ui/file-list"; -// import type { FileList } from "@/components/ui/file-list"; - export type RenderProps = { files: File[] }; export const renderComponent = ({ files }: Partial) => { return html` - + ${files?.map( (file) => html` From 6d7d4e2a2fb11b14f440745b80523b3154218dc9 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 10 Jun 2025 11:00:21 -0700 Subject: [PATCH 4/9] fix lint issue --- frontend/src/types/events.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/types/events.d.ts b/frontend/src/types/events.d.ts index e3eb5847a3..7b08caaa2d 100644 --- a/frontend/src/types/events.d.ts +++ b/frontend/src/types/events.d.ts @@ -5,8 +5,6 @@ import { type NotifyEventMap } from "@/controllers/notify"; import { type UserGuideEventMap } from "@/index"; import { type AuthEventMap } from "@/utils/AuthService"; -import "@/events"; - /** * Declare custom events here so that typescript can find them. * Custom event names should be prefixed with `btrix-`. From fe7bd09550ad0ef7f15f286e2adbe39dc8fefeea Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 10 Jun 2025 14:57:11 -0700 Subject: [PATCH 5/9] switch to form control --- frontend/src/components/ui/file-input.ts | 183 ++++++++++++++---- .../src/components/ui/file-list/events.ts | 4 +- .../components/ui/file-list/file-list-item.ts | 6 +- frontend/src/components/ui/file-list/index.ts | 2 +- frontend/src/mixins/FormControl.ts | 4 + .../stories/components/FileInput.stories.ts | 48 ++++- frontend/src/stories/components/FileInput.ts | 3 + .../components/decorators/fileInputForm.ts | 54 ++++++ 8 files changed, 262 insertions(+), 42 deletions(-) create mode 100644 frontend/src/stories/components/decorators/fileInputForm.ts diff --git a/frontend/src/components/ui/file-input.ts b/frontend/src/components/ui/file-input.ts index bda6e9ce78..a4277f612c 100644 --- a/frontend/src/components/ui/file-input.ts +++ b/frontend/src/components/ui/file-input.ts @@ -1,14 +1,21 @@ import { localized } from "@lit/localize"; import clsx from "clsx"; -import { html } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; +import { html, type PropertyValues } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; +import { repeat } from "lit/directives/repeat.js"; +import { without } from "lodash/fp"; + +import type { + BtrixFileChangeEvent, + BtrixFileRemoveEvent, +} from "./file-list/events"; import { TailwindElement } from "@/classes/TailwindElement"; -import type { BtrixChangeEvent } from "@/events/btrix-change"; +import { FormControl } from "@/mixins/FormControl"; import { tw } from "@/utils/tailwind"; -export type BtrixFileChangeEvent = BtrixChangeEvent; +import "@/components/ui/file-list"; /** * Allow attaching one or more files. @@ -17,7 +24,13 @@ export type BtrixFileChangeEvent = BtrixChangeEvent; */ @customElement("btrix-file-input") @localized() -export class FileInput extends TailwindElement { +export class FileInput extends FormControl(TailwindElement) { + /** + * Form control name, if used as a form control + */ + @property({ type: String }) + name?: string; + /** * Specify which file types are allowed */ @@ -36,41 +49,124 @@ export class FileInput extends TailwindElement { @property({ type: Boolean }) dropzone = false; + @state() + private files: File[] = []; + @query("input[type='file']") private readonly input?: HTMLInputElement | null; + formResetCallback() { + this.files = []; + + if (this.input) { + this.input.value = ""; + } + } + + protected willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has("files")) { + this.syncFormValue(); + } + } + + private syncFormValue() { + const formControlName = this.name; + + if (!formControlName) return; + + // `ElementInternals["setFormValue"]` doesn't support `FileList` yet, + // construct `FormData` instead + const formData = new FormData(); + + this.files.forEach((file) => { + formData.append(formControlName, file); + }); + + this.setFormValue(formData); + } + render() { - return html``; + return html` + ${this.files.length ? this.renderFiles() : this.renderInput()} + `; } + private readonly renderInput = () => { + return html` + + `; + }; + + private readonly renderFiles = () => { + return html` + { + e.stopPropagation(); + + this.files = without([e.detail.item])(this.files); + }} + > + ${repeat( + this.files, + (file) => file.name, + (file) => html` + + `, + )} + + `; + }; + private readonly onDrop = (e: DragEvent) => { e.preventDefault(); - if (e.dataTransfer?.files) { - void this.handleChange(e.dataTransfer.files); + const files = e.dataTransfer?.files; + + if (files) { + const list = new DataTransfer(); + + if (this.multiple) { + [...files].forEach((file) => { + if (this.valid(file)) { + list.items.add(file); + } + }); + } else { + const file = files[0]; + + if (this.valid(file)) { + list.items.add(file); + } + } + + if (list.items.length) { + void this.handleChange(list.files); + } else { + console.debug("none valid:", files); + } } else { console.debug("no files dropped"); } @@ -78,14 +174,35 @@ export class FileInput extends TailwindElement { private readonly onDragover = (e: DragEvent) => { e.preventDefault(); + + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "link"; + } }; - private async handleChange(files: FileList) { + /** + * @TODO More complex validation based on `accept` + */ + private valid(file: File) { + if (!this.accept) return true; + + return this.accept.split(",").some((accept) => { + if (accept.startsWith(".")) { + return file.name.endsWith(accept.trim()); + } + + return new RegExp(accept.trim().replace("*", ".*")).test(file.type); + }); + } + + private async handleChange(fileList: FileList) { + this.files = [...fileList]; + await this.updateComplete; this.dispatchEvent( new CustomEvent("btrix-change", { - detail: { value: files }, + detail: { value: this.files }, composed: true, bubbles: true, }), diff --git a/frontend/src/components/ui/file-list/events.ts b/frontend/src/components/ui/file-list/events.ts index f8e49e2f2f..233488733f 100644 --- a/frontend/src/components/ui/file-list/events.ts +++ b/frontend/src/components/ui/file-list/events.ts @@ -1,3 +1,5 @@ +import type { BtrixChangeEvent } from "@/events/btrix-change"; import type { BtrixRemoveEvent } from "@/events/btrix-remove"; -export type FileRemoveEvent = BtrixRemoveEvent; +export type BtrixFileRemoveEvent = BtrixRemoveEvent; +export type BtrixFileChangeEvent = BtrixChangeEvent; diff --git a/frontend/src/components/ui/file-list/file-list-item.ts b/frontend/src/components/ui/file-list/file-list-item.ts index 2a9d906e38..034f3940dc 100644 --- a/frontend/src/components/ui/file-list/file-list-item.ts +++ b/frontend/src/components/ui/file-list/file-list-item.ts @@ -2,14 +2,14 @@ import { localized, msg } from "@lit/localize"; import { css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import type { FileRemoveEvent } from "./events"; +import type { BtrixFileRemoveEvent } from "./events"; import { TailwindElement } from "@/classes/TailwindElement"; import { LocalizeController } from "@/controllers/localize"; import { truncate } from "@/utils/css"; /** - * @event btrix-remove FileRemoveEvent + * @event btrix-remove */ @customElement("btrix-file-list-item") @localized() @@ -112,7 +112,7 @@ export class FileListItem extends TailwindElement { if (!this.file) return; await this.updateComplete; this.dispatchEvent( - new CustomEvent("btrix-remove", { + new CustomEvent("btrix-remove", { detail: { item: this.file, }, diff --git a/frontend/src/components/ui/file-list/index.ts b/frontend/src/components/ui/file-list/index.ts index e4d99e0183..73cf8db20a 100644 --- a/frontend/src/components/ui/file-list/index.ts +++ b/frontend/src/components/ui/file-list/index.ts @@ -1,4 +1,4 @@ import "./file-list"; import "./file-list-item"; -export type { FileRemoveEvent } from "./events"; +export type { BtrixFileRemoveEvent as FileRemoveEvent } from "./events"; diff --git a/frontend/src/mixins/FormControl.ts b/frontend/src/mixins/FormControl.ts index 475cded9e0..751fca11f9 100644 --- a/frontend/src/mixins/FormControl.ts +++ b/frontend/src/mixins/FormControl.ts @@ -9,6 +9,10 @@ export const FormControl = >(superClass: T) => static formAssociated = true; readonly #internals: ElementInternals; + get form() { + return this.#internals.form; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...args: any[]) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument diff --git a/frontend/src/stories/components/FileInput.stories.ts b/frontend/src/stories/components/FileInput.stories.ts index 2751162e94..a52e231197 100644 --- a/frontend/src/stories/components/FileInput.stories.ts +++ b/frontend/src/stories/components/FileInput.stories.ts @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/web-components"; import { html } from "lit"; +import { fileInputFormDecorator } from "./decorators/fileInputForm"; import { renderComponent, type RenderProps } from "./FileInput"; const meta = { @@ -39,10 +40,49 @@ export const DropZone: Story = { args: { dropzone: true, anchor: html` - Drag file here or - + + Drag file here or + + + `, + }, +}; + +/** + * Open your browser console log to see what value gets submitted. + */ +export const FormControl: Story = { + decorators: [fileInputFormDecorator], + args: { + ...DropZone.args, + multiple: true, + }, +}; + +/** + * When dragging and dropping, files that are not acceptable are filtered out. + */ +export const FormControlValidation: Story = { + decorators: [fileInputFormDecorator], + args: { + dropzone: true, + multiple: true, + accept: ".txt,.doc,.pdf", + anchor: html` +
+ Drag document here or + + to upload +
+
TXT, DOC, or PDF
`, }, }; diff --git a/frontend/src/stories/components/FileInput.ts b/frontend/src/stories/components/FileInput.ts index 578f607675..3d03ebca88 100644 --- a/frontend/src/stories/components/FileInput.ts +++ b/frontend/src/stories/components/FileInput.ts @@ -1,5 +1,7 @@ import { html, type TemplateResult } from "lit"; +import { formControlName } from "./decorators/fileInputForm"; + import type { FileInput } from "@/components/ui/file-input"; import "@/components/ui/file-input"; @@ -16,6 +18,7 @@ export const renderComponent = ({ }: Partial) => { return html` ReturnType; + + render() { + const onSubmit = (e: SubmitEvent) => { + e.preventDefault(); + + const form = e.target as HTMLFormElement; + const value = serialize(form); + + console.log("form value:", value, form.elements); + }; + + return html` +
+ ${this.renderStory()} +
+ Reset + Submit +
+
+ `; + } +} + +export function fileInputFormDecorator( + story: StoryFn, + context: StoryContext, +) { + return html` + { + return story( + { + ...context.args, + }, + context, + ); + }} + > + `; +} From 930570c28a24fe8acfd79d125e1de3b35b4ab937 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 10 Jun 2025 15:10:27 -0700 Subject: [PATCH 6/9] update uploads --- frontend/src/components/ui/file-input.ts | 28 +++++++--- .../features/archived-items/file-uploader.ts | 55 +++++++------------ .../stories/components/FileInput.stories.ts | 4 +- frontend/src/stories/components/FileInput.ts | 4 +- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/ui/file-input.ts b/frontend/src/components/ui/file-input.ts index a4277f612c..bc07a61c9d 100644 --- a/frontend/src/components/ui/file-input.ts +++ b/frontend/src/components/ui/file-input.ts @@ -17,10 +17,13 @@ import { tw } from "@/utils/tailwind"; import "@/components/ui/file-list"; +const droppingClass = tw`bg-slate-100`; + /** * Allow attaching one or more files. * * @fires btrix-change + * @fires btrix-remove */ @customElement("btrix-file-input") @localized() @@ -47,11 +50,14 @@ export class FileInput extends FormControl(TailwindElement) { * Enable dragging files into drop zone */ @property({ type: Boolean }) - dropzone = false; + drop = false; @state() private files: File[] = []; + @query("#dropzone") + private readonly dropzone?: HTMLElement | null; + @query("input[type='file']") private readonly input?: HTMLInputElement | null; @@ -94,13 +100,20 @@ export class FileInput extends FormControl(TailwindElement) { private readonly renderInput = () => { return html`
`; }; @@ -157,6 +171,7 @@ export class FileInput extends FormControl(TailwindElement) { this.dropzone?.classList.remove(droppingClass); const files = e.dataTransfer?.files; + console.log("files:", files); if (files) { const list = new DataTransfer(); diff --git a/frontend/src/features/archived-items/file-uploader.ts b/frontend/src/features/archived-items/file-uploader.ts index f2b408a28a..6bf053453b 100644 --- a/frontend/src/features/archived-items/file-uploader.ts +++ b/frontend/src/features/archived-items/file-uploader.ts @@ -199,7 +199,7 @@ export class FileUploader extends BtrixElement { >${msg("Browse Files")} -

+

${msg("Select a .wacz file to upload")}

diff --git a/frontend/src/stories/components/FileInput.stories.ts b/frontend/src/stories/components/FileInput.stories.ts index 3e63794513..e6015e09b2 100644 --- a/frontend/src/stories/components/FileInput.stories.ts +++ b/frontend/src/stories/components/FileInput.stories.ts @@ -11,10 +11,10 @@ const meta = { render: renderComponent, decorators: (story) => html`
${story()}
`, argTypes: { - anchor: { table: { disable: true } }, + content: { table: { disable: true } }, }, args: { - anchor: html` + content: html` Select File `, }, @@ -30,7 +30,7 @@ export const Basic: Story = { export const Multiple: Story = { args: { multiple: true, - anchor: html` + content: html` Select Files `, }, @@ -39,11 +39,11 @@ export const Multiple: Story = { export const DropZone: Story = { args: { drop: true, - anchor: html` + content: html` Drag file here or @@ -69,10 +69,11 @@ export const FormControl: Story = { export const FileFormat: Story = { decorators: [fileInputFormDecorator], args: { + label: "Attach a Document", drop: true, multiple: true, accept: ".txt,.doc,.pdf", - anchor: html` + content: html`
Drag document here or