UNPKG

@nysds/nys-fileinput

Version:

The Fileinput component from the NYS Design System.

495 lines (492 loc) 23.4 kB
import { LitElement as h, unsafeCSS as g, html as a } from "lit"; import { property as o } from "lit/decorators.js"; import { ifDefined as m } from "lit/directives/if-defined.js"; /*! * █▄ █ █ █ █▀▀▀█ █▀▀▄ █▀▀▀█ * █ █ █ █▄▄▄█ ▀▀▀▄▄ █ █ ▀▀▀▄▄ * █ ▀█ █ █▄▄▄█ █▄▄▀ █▄▄▄█ * * Fileinput Component v1.18.1 * Part of the New York State Design System * Repository: https://github.com/its-hcd/nysds * License: MIT */ async function _(p, e) { if (!e || e.trim() === "") return !0; const t = e.toLowerCase().split(",").map((r) => r.trim()), i = p.name.toLowerCase(), s = i.includes(".") ? i.split(".").pop() : ""; for (const r of t) if (r.startsWith(".") && r.slice(1) === s || r.endsWith("/*") && p.type.startsWith(r.slice(0, -1)) || p.type === r) return !0; return !1; } const v = ':host{--_nys-fileitem-border-radius: var(--nys-radius-md, 4px);--_nys-fileitem-padding: var(--nys-space-100, 8px) var(--nys-space-200, 16px);--_nys-fileitem-background-color: var(--nys-color-ink-reverse, #ffffff);--_nys-fileitem-border-color: var(--nys-color-neutral-100, #d0d0ce);--_nys-fileitem-font-family: var( --nys-font-family-ui, var( --nys-font-family-sans, "Proxima Nova", "Helvetica Neue", "Helvetica", "Arial", sans-serif ) );--_nys-fileitem-font-size: var(--nys-font-size-ui-md, 16px);--_nys-fileitem-font-weight: var(--nys-font-weight-regular, 400);--_nys-fileitem-line-height: var(--nys-font-lineheight-ui-md, 24px);--_nys-fileitem-letter-spacing: var( --nys-font-letterspacing-ui-md, .044px );--_nys-fileitem-background-color--progress: var( --nys-color-neutral-50, #ededed );--_nys-fileitem-background-color--progress--fill: var( --nys-color-info, #004dd1 )}.file-item{position:relative;border-radius:var(--_nys-fileitem-border-radius);border-width:var(--nys-border-width-sm, 1px);border-style:solid;border-color:var(--_nys-fileitem-border-color);background-color:var(--_nys-fileitem-background-color)}.file-item.error{--_nys-fileitem-border-color: var(--nys-color-danger, #b52c2c)}.file-item__main{display:flex;place-items:center center;gap:var(--_nys-fileinput-gap);padding:var(--_nys-fileitem-padding);height:56px;box-sizing:border-box}.file-item__info{display:flex;flex-direction:column;flex:1;min-width:0;font-family:var(--_nys-fileitem-font-family);font-size:var(--_nys-fileitem-font-size);font-style:normal;font-weight:var(--_nys-fileitem-font-weight);line-height:var(--_nys-fileitem-line-height);letter-spacing:var(--_nys-fileitem-letter-spacing)}.file-item__info-name{display:flex;max-width:100%;overflow:hidden;white-space:nowrap;align-items:center}.file-item__info-name-start{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:0}.file-item p{margin:0}.file-item__error{color:var(--nys-color-danger, #b52c2c);text-overflow:ellipsis;font-weight:700}progress{position:absolute;bottom:0;display:flex;width:100%;height:6px;border-radius:var(--nys-radius-round, 1776px);background:var(--_nys-fileitem-background-color--progress--fill);overflow:hidden;appearance:none}progress::-moz-progress-bar{background-color:var(--_nys-fileitem-background-color--progress)}progress::-webkit-progress-value{background-color:var(--_nys-fileitem-background-color--progress--fill)}progress::-webkit-progress-bar{background-color:var(--_nys-fileitem-background-color--progress)}.file-icon[name=progress_activity]{animation:spin 1s linear infinite}.file-icon[name=error]{color:var(--nys-color-danger, #b52c2c)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}'; var b = Object.defineProperty, y = (p, e, t, i) => { for (var s = void 0, r = p.length - 1, d; r >= 0; r--) (d = p[r]) && (s = d(e, t, s) || s); return s && b(e, t, s), s; }; const u = class u extends h { constructor() { super(...arguments), this.filename = "", this.status = "pending", this.progress = 0, this.errorMessage = ""; } _handleRemove() { this.dispatchEvent( new CustomEvent("nys-fileRemove", { detail: { filename: this.filename }, bubbles: !0, composed: !0 }) ); } splitFilename(e) { const t = e.lastIndexOf("."), i = t !== -1 ? e.slice(t) : "", s = t !== -1 ? e.slice(0, t) : e, r = s.slice(0, s.length - 3), d = s.slice(-3); return { startPart: r, endPart: d, extension: i }; } render() { const { startPart: e, endPart: t, extension: i } = this.splitFilename(this.filename); return a` <div class="file-item ${this.status}" aria-busy=${this.status === "processing" ? "true" : "false"} aria-label="You have selected ${this.filename}" > <div class="file-item__main" role="group"> <nys-icon class="file-icon" name=${this.status === "processing" ? "progress_activity" : this.status === "error" ? "error" : "attach_file"} size="2xl" ></nys-icon> <div class="file-item__info"> <div class="file-item__info-name"> <span class="file-item__info-name-start">${e}</span> <span class="file-item__info-name-end" >${t}${i}</span > </div> ${this.errorMessage ? a`<p class="file-item__error" role="alert" aria-live="assertive" aria-invalid="true" aria-errormessage=${this.errorMessage} id="${this.filename}-error" > ${this.errorMessage} </p>` : null} </div> <nys-button circle icon="close" ariaLabel="close button" size="sm" variant="ghost" @nys-click=${this._handleRemove} ariaLabel="Remove file: ${this.filename}" ></nys-button> </div> ${this.status === "processing" ? a`<div class="file-item__progress-container" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${this.progress}" aria-label="Upload progress for ${this.filename}" > <progress value=${this.progress} max="100"></progress> </div>` : null} </div> `; } }; u.styles = g(v), u.shadowRootOptions = { ...h.shadowRootOptions, delegatesFocus: !0 }; let c = u; y([ o({ type: String }) ], c.prototype, "filename"); y([ o({ type: String }) ], c.prototype, "status"); y([ o({ type: Number }) ], c.prototype, "progress"); y([ o({ type: String }) ], c.prototype, "errorMessage"); customElements.define("nys-fileitem", c); const w = ':host{--_nys-fileinput-gap: var(--nys-space-100, 8px);--_nys-fileinput-font-size: var(--nys-font-size-ui-md, 16px);--_nys-fileinput-font-weight: var(--nys-font-weight-semibold, 600);--_nys-fileinput-line-height: var(--nys-font-lineheight-ui-md, 24px);--_nys-fileinput-font-family: var( --nys-font-family-ui, var( --nys-font-family-sans, "Proxima Nova", "Helvetica Neue", "Helvetica", "Arial", sans-serif ) );--_nys-fileinput-background-color--dropzone: var( --nys-color-ink-reverse, #ffffff );--_nys-fileinput-background-color--dropzone--disabled: var( --nys-color-neutral-10, #f6f6f6 );--_nys-fileinput-background-color--dropzone--active: var( --nys-color-theme-faint, #f7fafd );--_nys-fileinput-border-radius--dropzone: var( --nys-radius-lg, var(--nys-space-100, 8px) );--_nys-fileinput-border-style: dashed;--_nys-fileinput-border-color: var(--nys-color-neutral-200, #bec0c1);--_nys-fileinput-border-width: var(--nys-border-width-sm, 1px)}.nys-fileinput{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:var(--_nys-fileinput-gap);font-family:var(--_nys-fileinput-font-family);font-size:var(--_nys-fileinput-font-size);font-weight:var(--_nys-fileinput-font-weight);line-height:var(--_nys-fileinput-line-height)}:host([width=lg]) .nys-fileinput{max-width:var(--nys-form-width-lg, 384px)}ul{list-style-type:none;padding:0;margin:0;width:100%;display:flex;flex-direction:column;gap:var(--_nys-fileinput-gap)}.nys-fileinput__dropzone{display:flex;padding:var(--nys-space-400, 32px) var(--nys-space-200, 16px);justify-content:center;align-items:center;gap:12px;align-self:stretch;border-radius:var(--_nys-fileinput-border-radius--dropzone);outline:var(--_nys-fileinput-border-width) var(--_nys-fileinput-border-style) var(--_nys-fileinput-border-color);background-color:var(--_nys-fileinput-background-color--dropzone);transition:all 60ms ease-in-out}.nys-fileinput__dropzone:hover{cursor:pointer;--_nys-fileinput-border-width: var(--nys-border-width-md, 2px);--_nys-fileinput-border-color: var(--nys-color-neutral-700, #4a4d4f)}.nys-fileinput__dropzone.drag-active{--_nys-fileinput-border-width: var(--nys-border-width-md, 2px);--_nys-fileinput-border-color: var(--nys-color-theme, #154973);--_nys-fileinput-border-style: solid}.nys-fileinput__dropzone.error{--_nys-fileinput-border-color: var(--nys-color-danger, #b52c2c)}.nys-fileinput__dropzone.error:hover{--_nys-fileinput-border-width: var(--nys-border-width-md, 2px);--_nys-fileinput-border-color: var(--nys-color-emergency, #721c1c)}.nys-fileinput__dropzone.disabled{cursor:not-allowed;--_nys-fileinput-border-color: var(--nys-color-neutral-300, #a7a9ab);--_nys-fileinput-border-width: var(--nys-border-width-sm, 1px);background-color:var(--_nys-fileinput-background-color--dropzone--disabled);color:var(--_nys-fileinput-color--dropzone--disabled)}progress{display:flex;width:100%;height:6px;border-radius:var(--nys-radius-round, 1776px);background-color:var(--_nys-fileinput-progress-background);overflow:hidden;appearance:none;border:none}progress::-moz-progress-bar{background-color:var(--_nys-fileinput-progress-background)}progress::-webkit-progress-value{background-color:var(--_nys-fileinput-progress-background)}progress::-webkit-progress-bar{background-color:var(--_nys-fileinput-progress-background)}'; var $ = Object.defineProperty, l = (p, e, t, i) => { for (var s = void 0, r = p.length - 1, d; r >= 0; r--) (d = p[r]) && (s = d(e, t, s) || s); return s && $(e, t, s), s; }; let F = 0; const f = class f extends h { // allows use of elementInternals' API constructor() { super(), this.id = "", this.name = "", this.label = "", this.description = "", this.multiple = !1, this.form = null, this.tooltip = "", this.accept = "", this.disabled = !1, this.required = !1, this.optional = !1, this.showError = !1, this.errorMessage = "", this.dropzone = !1, this.width = "full", this.inverted = !1, this._selectedFiles = [], this._dragActive = !1, this._internals = this.attachInternals(); } get _isDropDisabled() { return this.disabled || !this.multiple && this._selectedFiles.length > 0; } get _buttonAriaLabel() { return this._selectedFiles.length === 0 ? this.multiple ? "Choose files: " : "Choose file: " : this.multiple ? "Change files: " : "Change file: "; } get _buttonAriaDescription() { if (this._selectedFiles.length === 0) return `${this.label + " " + this.description}`; const e = this._selectedFiles.some( (s) => s.status === "error" ); let t = ""; if (this._selectedFiles.length === 1) t = `You have selected ${this._selectedFiles[0].file.name}.`; else { const s = this._selectedFiles.map((r) => r.file.name).join(", "); t = `You have selected ${this._selectedFiles.length} files: ${s}`; } return `${t}${e ? " Error: One or more files are not valid file types." : ""}`; } get _innerNysButton() { return this.renderRoot.querySelector( '[name="file-btn"]' )?.shadowRoot?.querySelector( "button" ); } // Generate a unique ID if one is not provided connectedCallback() { super.connectedCallback(), this.id || (this.id = `nys-fileinput-${Date.now()}-${F++}`), this.addEventListener("invalid", this._handleInvalid); } disconnectedCallback() { super.disconnectedCallback(), this.removeEventListener("invalid", this._handleInvalid); } firstUpdated() { this._setValue(); } /** * Form Integration * -------------------------------------------------------------------------- */ _setValue() { if (this.multiple) { const e = this._selectedFiles.map((t) => t.file); if (e.length > 0) { const t = new FormData(); e.forEach((i) => { t.append(this.name, i); }), this._internals.setFormValue(t); } else this._internals.setFormValue(null); } else { const e = this._selectedFiles[0]?.file || null; this._internals.setFormValue(e); } this._manageRequire(); } // Called to internally set the initial internalElement required flag. _manageRequire() { const e = this.shadowRoot?.querySelector("input"); if (!e) return; const t = this.errorMessage || "Please upload a file."; this.required && this._selectedFiles.length == 0 ? (this._internals.ariaInvalid = "true", this._internals.setValidity({ valueMissing: !0 }, t, e)) : (this._internals.ariaInvalid = "false", this._internals.setValidity({}, "", e)); } _setValidityMessage(e = "") { const t = this.shadowRoot?.querySelector("input"); t && (this.showError = e === (this.errorMessage || "Please upload a file."), this.errorMessage?.trim() && e !== "" && (e = this.errorMessage), this._internals.setValidity( e ? { customError: !0 } : {}, e, t )); } _validate() { const e = this._selectedFiles.some( (s) => s.status === "error" ), t = this.required && this._selectedFiles.length === 0; let i = ""; t ? i = this.errorMessage || "Please upload a file." : e && (i = "One or more files are invalid."), this._setValidityMessage(i); } // This helper function is called to perform the element's native validation. checkValidity() { const e = this.shadowRoot?.querySelector("input"); return e ? e.checkValidity() : !0; } // Called automatically when the parent form is reset formResetCallback() { this._selectedFiles = []; const e = this.shadowRoot?.querySelector( ".hidden-file-input" ); e && (e.value = ""), this._internals.setFormValue(null), this.showError = !1, this.errorMessage = "", this._internals.setValidity({}), this.requestUpdate(); } _handleInvalid(e) { e.preventDefault(), this._validate(); const t = this._innerNysButton; if (t) { const i = this._internals.form; i ? Array.from(i.elements).find( (d) => typeof d.checkValidity == "function" && !d.checkValidity() ) === this && (t.focus(), t.classList.add("active-focus")) : (t.focus(), t.classList.add("active-focus")); } } /** * Functions * -------------------------------------------------------------------------- */ // Store the files to be displayed async _saveSelectedFiles(e) { if (this._selectedFiles.some( (s) => s.file.name == e.name ) || !this.multiple && this._selectedFiles.length >= 1) return; const i = { file: e, progress: 0, status: "pending" }; this._selectedFiles.push(i), await this._processFile(i), this._setValue(), this._validate(); } // Read the contents of stored files, this will indicate loading progress of the uploaded files async _processFile(e) { e.status = "processing"; try { if (!await _(e.file, this.accept)) { e.status = "error", e.errorMsg = "File type is invalid.", this.requestUpdate(); return; } const i = new FileReader(); i.onprogress = (s) => { if (s.lengthComputable) { const r = Math.round(s.loaded * 100 / s.total); e.progress = r, this.requestUpdate(); } }, i.onload = () => { e.progress = 100, e.status = "done", this.requestUpdate(); }, i.onerror = () => { e.status = "error", e.errorMsg = "Failed to load file.", this.requestUpdate(); }, i.readAsArrayBuffer(e.file); } catch { e.status = "error", e.errorMsg = "Error validating file.", this.requestUpdate(); } } _dispatchChangeEvent() { this.dispatchEvent( new CustomEvent("nys-change", { detail: { id: this.id, files: this._selectedFiles }, bubbles: !0, composed: !0 }) ); } _openFileDialog() { this.renderRoot.querySelector( ".hidden-file-input" )?.click(); } _handlePostFileSelectionFocus() { if (this.multiple) { const e = this._innerNysButton; e && e.focus(); } else this._focusFirstFileItemIfSingleMode(); } async _focusFirstFileItemIfSingleMode() { if (!this.multiple) { await this.updateComplete; const t = this.renderRoot.querySelector( "nys-fileitem" )?.shadowRoot?.querySelector( ".file-item" ); t && (t.setAttribute("tabindex", "-1"), t.focus()); } } /** * Event Handlers * -------------------------------------------------------------------------- */ // Access the selected files & add new files to the internal list via the hidden <input type="file"> _handleFileChange(e) { const i = e.target.files; (i ? Array.from(i) : []).map((r) => { this._saveSelectedFiles(r); }), this.requestUpdate(), this._dispatchChangeEvent(), this._handlePostFileSelectionFocus(); } _handleFileRemove(e) { const t = e.detail.filename; if (this._selectedFiles = this._selectedFiles.filter( (i) => i.file.name !== t ), this._selectedFiles.length === 0) { const i = this.shadowRoot?.querySelector( "input" ); i && (i.value = ""); } this._setValue(), this._validate(), this.requestUpdate(), this._dispatchChangeEvent(); } _onDragOver(e) { this.disabled || (e.stopPropagation(), e.preventDefault(), this._dragActive || (this._dragActive = !0, this.requestUpdate())); } // Mostly used for styling purpose _onDragLeave(e) { this.disabled || (e.stopPropagation(), e.preventDefault(), e.currentTarget === e.target && (this._dragActive = !1, this.requestUpdate())); } _onDrop(e) { if (this.disabled) return; e.preventDefault(), this._dragActive = !1, this.requestUpdate(); const t = e.dataTransfer?.files; if (!t) return; const i = Array.from(t); this.multiple ? i.forEach((s) => { this._saveSelectedFiles(s); }) : this._saveSelectedFiles(i[0]), this.requestUpdate(), this._dispatchChangeEvent(); } render() { return a`<div class="nys-fileinput" @nys-fileRemove=${this._handleFileRemove} > <nys-label label=${this.label} description=${this.description} flag=${this.required ? "required" : this.optional ? "optional" : ""} tooltip=${this.tooltip} ?inverted=${this.inverted} @nys-label-click=${this._openFileDialog} > <slot name="description" slot="description">${this.description}</slot> </nys-label> <input id=${this.id} class="hidden-file-input" tabindex="-1" type="file" name=${this.name} accept=${this.accept} form=${m(this.form || void 0)} ?multiple=${this.multiple} ?required=${this.required} ?disabled=${this.disabled || !this.multiple && this._selectedFiles.length > 0} aria-disabled="${this.disabled}" aria-hidden="true" @change=${this._handleFileChange} hidden /> ${this.dropzone ? a`<div class="nys-fileinput__dropzone ${this._dragActive ? "drag-active" : ""} ${this._isDropDisabled ? "disabled" : ""} ${this.showError && !this._isDropDisabled ? "error" : ""}" @click=${this._isDropDisabled ? null : (e) => { e.target.closest("nys-button") || this._openFileDialog(); }} @dragover=${this._isDropDisabled ? null : this._onDragOver} @dragleave=${this._isDropDisabled ? null : this._onDragLeave} @drop=${this._isDropDisabled ? null : this._onDrop} aria-label="Drag files here or choose from folder" > ${this._dragActive ? a`<p>Drop file to upload</p>` : a` <nys-button id="choose-files-btn-drag" name="file-btn" label=${this.multiple ? "Choose files" : "Choose file"} variant="outline" ariaLabel=${this._buttonAriaLabel} ariaDescription=${this._buttonAriaDescription} ?disabled=${this._isDropDisabled} @nys-click="${(e) => { e.preventDefault(), e.stopPropagation(), this._openFileDialog(); }}" ></nys-button> <p>or drag here</p>`} </div>` : a`<nys-button id="choose-files-btn" name="file-btn" label=${this.multiple ? "Choose files" : "Choose file"} variant="outline" ariaLabel=${this._buttonAriaLabel} ariaDescription=${this._buttonAriaDescription} ?disabled=${this.disabled || !this.multiple && this._selectedFiles.length > 0} @nys-click=${this._openFileDialog} ></nys-button>`} ${this.showError ? a` <nys-errormessage ?showError=${this.showError} errorMessage=${this._internals.validationMessage || this.errorMessage} ></nys-errormessage> ` : null} ${this._selectedFiles.length > 0 ? a` <ul> ${this._selectedFiles.map( (e) => a`<li> <nys-fileitem filename=${e.file.name} status=${e.status} progress=${e.progress} errorMessage=${e.errorMsg || ""} ></nys-fileitem> </li>` )} </ul> ` : null} </div>`; } }; f.styles = g(w), f.shadowRootOptions = { ...h.shadowRootOptions, delegatesFocus: !0 }, f.formAssociated = !0; let n = f; l([ o({ type: String, reflect: !0 }) ], n.prototype, "id"); l([ o({ type: String, reflect: !0 }) ], n.prototype, "name"); l([ o({ type: String }) ], n.prototype, "label"); l([ o({ type: String }) ], n.prototype, "description"); l([ o({ type: Boolean }) ], n.prototype, "multiple"); l([ o({ type: String, reflect: !0 }) ], n.prototype, "form"); l([ o({ type: String }) ], n.prototype, "tooltip"); l([ o({ type: String }) ], n.prototype, "accept"); l([ o({ type: Boolean, reflect: !0 }) ], n.prototype, "disabled"); l([ o({ type: Boolean, reflect: !0 }) ], n.prototype, "required"); l([ o({ type: Boolean, reflect: !0 }) ], n.prototype, "optional"); l([ o({ type: Boolean, reflect: !0 }) ], n.prototype, "showError"); l([ o({ type: String }) ], n.prototype, "errorMessage"); l([ o({ type: Boolean }) ], n.prototype, "dropzone"); l([ o({ type: String, reflect: !0 }) ], n.prototype, "width"); l([ o({ type: Boolean, reflect: !0 }) ], n.prototype, "inverted"); customElements.get("nys-fileinput") || customElements.define("nys-fileinput", n); export { n as NysFileinput }; //# sourceMappingURL=nys-fileinput.js.map