import "@material/mwc-linear-progress/mwc-linear-progress"; import { mdiDelete, mdiFileUpload } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../common/dom/fire_event"; import type { HomeAssistant } from "../types"; import "./ha-button"; import "./ha-icon-button"; import { blankBeforePercent } from "../common/translations/blank_before_percent"; import { ensureArray } from "../common/array/ensure-array"; import { bytesToString } from "../util/bytes-to-string"; import type { LocalizeFunc } from "../common/translations/localize"; declare global { interface HASSDomEvents { "file-picked": { files: File[] }; "files-cleared": undefined; } } @customElement("ha-file-upload") export class HaFileUpload extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public localize?: LocalizeFunc; @property() public accept!: string; @property() public icon?: string; @property() public label?: string; @property() public secondary?: string; @property({ attribute: "uploading-label" }) public uploadingLabel?: string; @property({ attribute: "delete-label" }) public deleteLabel?: string; @property() public supports?: string; @property({ type: Object }) public value?: File | File[] | FileList | string; @property({ type: Boolean }) public multiple = false; @property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean }) public uploading = false; @property({ type: Number }) public progress?: number; @property({ type: Boolean, attribute: "auto-open-file-dialog" }) public autoOpenFileDialog = false; @state() private _drag = false; @query("#input") private _input?: HTMLInputElement; protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); if (this.autoOpenFileDialog) { this._openFilePicker(); } } private get _name() { if (this.value === undefined) { return ""; } if (typeof this.value === "string") { return this.value; } const files = this.value instanceof FileList ? Array.from(this.value) : ensureArray(this.value); return files.map((file) => file.name).join(", "); } public render(): TemplateResult { const localize = this.localize || this.hass!.localize; return html` ${this.uploading ? html`
${this.uploadingLabel || (this.value ? localize("ui.components.file-upload.uploading_name", { name: this._name, }) : localize("ui.components.file-upload.uploading"))} ${this.progress ? html`
${this.progress}${this.hass && blankBeforePercent(this.hass!.locale)}%
` : nothing}
` : html``} `; } private _openFilePicker() { this._input?.click(); } private _handleDrop(ev: DragEvent) { ev.preventDefault(); ev.stopPropagation(); if (ev.dataTransfer?.files) { fireEvent(this, "file-picked", { files: this.multiple || ev.dataTransfer.files.length === 1 ? Array.from(ev.dataTransfer.files) : [ev.dataTransfer.files[0]], }); } this._drag = false; } private _handleDragStart(ev: DragEvent) { ev.preventDefault(); ev.stopPropagation(); this._drag = true; } private _handleDragEnd(ev: DragEvent) { ev.preventDefault(); ev.stopPropagation(); this._drag = false; } private _handleFilePicked(ev) { if (ev.target.files.length === 0) { return; } this.value = ev.target.files; fireEvent(this, "file-picked", { files: ev.target.files }); } private _clearValue(ev: Event) { ev.preventDefault(); this._input!.value = ""; this.value = undefined; fireEvent(this, "change"); fireEvent(this, "files-cleared"); } static styles = css` :host { display: block; height: 240px; } :host([disabled]) { pointer-events: none; color: var(--disabled-text-color); } .container { position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; border: solid 1px var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42)); border-radius: var(--mdc-shape-small, var(--ha-border-radius-sm)); height: 100%; } .row { display: flex; align-items: center; } label.container { border: dashed 1px var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42)); cursor: pointer; } .container .uploading { display: flex; flex-direction: column; width: 100%; align-items: flex-start; padding: 0 32px; box-sizing: border-box; } :host([disabled]) .container { border-color: var(--disabled-color); } label:hover, label.dragged { border-style: solid; } label.dragged { border-color: var(--primary-color); } .dragged:before { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: var(--primary-color); content: ""; opacity: var(--dark-divider-opacity); pointer-events: none; border-radius: var(--mdc-shape-small, 4px); } label.value { cursor: default; } label.value.multiple { justify-content: unset; overflow: auto; } .highlight { color: var(--primary-color); } ha-button { margin-bottom: 8px; } .supports { color: var(--secondary-text-color); font-size: var(--ha-font-size-s); } :host([disabled]) .secondary { color: var(--disabled-text-color); } input.file { display: none; } .value { cursor: pointer; } .value ha-svg-icon { margin-right: 8px; margin-inline-end: 8px; margin-inline-start: initial; } ha-button { --mdc-button-outline-color: var(--primary-color); --mdc-icon-button-size: 24px; } mwc-linear-progress { width: 100%; padding: 8px 32px; box-sizing: border-box; } .header { font-weight: var(--ha-font-weight-medium); } .progress { color: var(--secondary-text-color); } button.link { background: none; border: none; padding: 0; font-size: var(--ha-font-size-m); color: var(--primary-color); text-decoration: underline; cursor: pointer; } `; } declare global { interface HTMLElementTagNameMap { "ha-file-upload": HaFileUpload; } }