@nysds/nys-fileinput
Version:
The Fileinput component from the NYS Design System.
495 lines (492 loc) • 23.4 kB
JavaScript
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