UNPKG

@magnit-ce/fileimage-input

Version:

A custom html input element that accepts a file or an image and provides a simple preview for common image value use-cases.

230 lines (227 loc) 11.7 kB
// fileimage-input.css?raw var fileimage_input_default = '\n:host \n{ \n display: inline-grid;\n grid-template-columns: 1fr auto;\n gap: .25em;\n min-height: 34px;\n\n /* user-agent input defaults */\n --border-color: rgb(118, 118, 118);\n\n /* slotted elements can inherit this for easy color matching */\n --placeholder-color: #757575;\n}\n@media (prefers-color-scheme: dark) \n{\n :host\n {\n --border-color: rgb(133, 133, 133);\n }\n}\n\n/* block styles */\n:host(.block)\n{\n grid-template-columns: 1fr 1fr;\n}\n:host(.block) .label\n{\n grid-column: span 2;\n grid-row: 1;\n}\n:host(.block) .field\n{\n border: dashed 1px #666;\n display: grid;\n gap: .5em;\n justify-items: center;\n}\n:host(.block) .preview\n{\n height: 70px;\n}\n:host(.block) .placeholder-icon\n{\n font-size: 3em;\n}\n:host(.block) .clear\n{\n grid-column: 1;\n grid-row: 2;\n}\n:host(.block) .view-link\n{\n grid-column: 2;\n grid-row: 2;\n justify-self: flex-end;\n}\n/* end block styles */\n\ninput\n{\n display: none;\n}\n\n.label\n{\n flex: 1;\n display: flex;\n grid-row: span 2;\n grid-column: 1;\n overflow: hidden;\n}\n\n.field\n{\n flex: 1;\n white-space: nowrap;\n\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n gap: .25em;\n padding: .25em .5em;\n\n background-color: field;\n color: fieldtext;\n\n font-size: 13.33px;\n border-width: 1px;\n border-style: solid;\n border-color: var(--border-color);\n border-radius: 2px;\n overflow: hidden;\n min-width: 0;\n\n}\n.field:focus-visible\n,.field:focus\n{\n outline: solid 2px;\n border-radius: 3px;\n}\n\n.status\n{\n overflow: hidden;\n}\n\n.preview[src=""]\n,.preview:not([src])\n{\n display: none;\n}\n.preview\n{\n height: 1em;\n}\n\n.view-link[href="#"]\n{\n display: none;\n}\n.view-link\n{\n white-space: nowrap;\n font-size: .75em;\n grid-column: 2;\n grid-row: 2;\n align-self: center;\n}\n\n.thumbnail\n{\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.placeholder-label\n,.placeholder-icon\n,::slotted([slot="placeholder"])\n,::slotted([slot="placeholder-icon"])\n{\n color: var(--placeholder-color);\n}\n:host([specified]) .placeholder-label\n,:host([specified].image) .placeholder-icon\n,:host([specified]) ::slotted([slot="placeholder"])\n,:host([specified].image) ::slotted([slot="placeholder-icon"])\n{\n display: none;\n}\n\n.clear\n{\n display: none;\n white-space: nowrap;\n font-size: .75em;\n grid-column: 2;\n grid-row: 1;\n align-self: center;\n}\n:host([specified]) .clear\n{\n display: block;\n}'; // fileimage-input.html?raw var fileimage_input_default2 = '<label class="label" part="label" tabindex="0">\n <input type="file" class="input" part="input text" />\n <span class="field text" part="field text">\n <span class="thumbnail" part="thumbnail">\n <slot name="placeholder-icon"><span class="placeholder-icon" part="placeholder-icon">\u{1F5CE}</span></slot>\n <img alt="image preview" title="Image Preview" class="preview" part="preview" />\n </span>\n <span class="status" part="status">\n <span class="filename" part="filename"></span>\n <slot name="placeholder"><span class="placeholder-label" part="placeholder-label"></span></slot>\n </span>\n </span>\n</label>\n<a href="" class="clear" part="clear" tabindex="0"><slot name="clear">Clear Selection</slot></a>\n<a href="#" target="_blank" class="view-link" part="view-link" tabindex="0"><slot name="view-link">View Selection</slot></a>'; // fileimage-input.ts var COMPONENT_STYLESHEET = new CSSStyleSheet(); COMPONENT_STYLESHEET.replaceSync(fileimage_input_default); var COMPONENT_TAG_NAME = "fileimage-input"; var FileImageInputElement = class extends HTMLElement { componentParts = /* @__PURE__ */ new Map(); // getPart<T extends HTMLElement = HTMLElement>(key: string) // { // if(this.componentParts.get(key) == null) // { // const part = this.shadowRoot!.querySelector(`[part="${key}"]`) as HTMLElement; // if(part != null) { this.componentParts.set(key, part); } // } // return this.componentParts.get(key) as T; // } // findPart<T extends HTMLElement = HTMLElement>(key: string) { return this.shadowRoot!.querySelector(`[part="${key}"]`) as T; } get files() { return this.shadowRoot.querySelector("input")?.files ?? null; } #previewURL; constructor() { super(); this.#internals = this.attachInternals(); this.#internals.role = "file"; this.attachShadow({ mode: "open" }); this.shadowRoot.innerHTML = fileimage_input_default2; this.shadowRoot.adoptedStyleSheets.push(COMPONENT_STYLESHEET); this.shadowRoot.querySelector(".label").tabIndex = 0; const placeholderLabel = this.shadowRoot.querySelector(".placeholder-label"); if (placeholderLabel != null) { placeholderLabel.textContent = this.getAttribute("placeholder") ?? "Select a file..."; } this.updateFormStatus(); const input = this.shadowRoot.querySelector("input"); input.addEventListener("input", () => { const value = input.files == null ? null : input.files[0]; this.updateFormStatus(); this.updatePreview(value); this.dispatchEvent(new Event("change")); }); this.addEventListener("keydown", (event) => { if (event.code == "Space" || event.code == "Enter" || event.code == "NumpadEnter") { this.shadowRoot.querySelector("input").click(); event.preventDefault(); event.stopPropagation(); } }); this.shadowRoot.querySelector(".clear").addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); this.value = null; this.dispatchEvent(new Event("change")); return false; }); this.shadowRoot.querySelector(".clear").addEventListener("keydown", (event) => { if (event.code == "Space" || event.code == "Enter" || event.code == "NumpadEnter") { event.preventDefault(); event.stopPropagation(); this.value = null; this.dispatchEvent(new Event("change")); return false; } }); this.addEventListener("dragover", (event) => { event.preventDefault(); }); this.addEventListener("drop", (event) => { event.preventDefault(); if (event.dataTransfer == null) { return; } const accepted = this.shadowRoot.querySelector("input").getAttribute("accept").split(",").map((item) => item.trim()); if (event.dataTransfer.items) { const dataItems = [...event.dataTransfer.items]; for (let i = 0; i < dataItems.length; i++) { const item = dataItems[i]; if (item.kind == "file") { const file = item.getAsFile(); if (file == null) { this.value = file; return; } if (accepted.indexOf(file.type) > -1) { this.value = file; } else { this.dispatchEvent(new CustomEvent("deny", { detail: { file, message: "File type disallowed by accepted list", accepted } })); } } } } else { const dataFiles = [...event.dataTransfer.files]; for (let i = 0; i < dataFiles.length; i++) { const file = dataFiles[i]; if (file == null) { this.value = file; return; } if (accepted.indexOf(file.type) > -1) { this.value = file; } else { this.dispatchEvent(new CustomEvent("deny", { detail: { file, message: "File type disallowed by accepted list", accepted } })); } } } }); } connectedCallback() { this.updateFormStatus(); this.updatePreview(this.value); } // custom elements reference static observedAttributes = ["accept"]; attributeChangedCallback(attributeName, _oldValue, newValue) { if (attributeName == "accept") { this.shadowRoot.querySelector("input").setAttribute("accept", newValue); } } updatePreview(file) { if (file == null) { this.shadowRoot.querySelector(".preview").removeAttribute("src"); this.shadowRoot.querySelector(".view-link").href = "#"; this.shadowRoot.querySelector(".filename").textContent = ""; this.removeAttribute("specified"); this.classList.remove("file", "image"); const placeholderLabel = this.shadowRoot.querySelector(".placeholder-label"); if (placeholderLabel != null) { placeholderLabel.textContent = this.getAttribute("placeholder") ?? "Select a file..."; } if (this.#previewURL != null) { window.URL.revokeObjectURL(this.#previewURL); this.#previewURL = void 0; } return; } this.shadowRoot.querySelector(".filename").textContent = file.name; this.toggleAttribute("specified", true); if (file.type.startsWith("image")) { this.classList.add("image"); const reader = new FileReader(); reader.addEventListener("load", (event) => { const result = event.target?.result; this.shadowRoot.querySelector(".preview").src = result; }); reader.readAsDataURL(file); } else { this.classList.add("file"); } this.#previewURL = window.URL.createObjectURL(file); this.shadowRoot.querySelector(".view-link").href = this.#previewURL; } ///// Form Functionality ///// static formAssociated = true; // this allows form event functionality; #internals; // #defaultValue: null = null; get value() { const input = this.shadowRoot.querySelector("input"); return input.files == null ? null : input.files[0]; } set value(val) { const transfer = new DataTransfer(); if (val != null) { transfer.items.add(val); } const input = this.shadowRoot.querySelector("input"); input.files = transfer.files; this.updateFormStatus(); this.updatePreview(input.files == null ? null : input.files[0]); } get validity() { return this.#internals.validity; } #validationMessage = "Please fill out this field."; get validationMessage() { return this.#validationMessage; } setCustomValidity(value) { this.#validationMessage = value; const input = this.shadowRoot.querySelector("input"); const formValue = input.files == null ? null : input.files[0]; this.#internals.setValidity( { valueMissing: this.getAttribute("required") != null && formValue == null }, this.#validationMessage, this.shadowRoot.querySelector(".label") ); } formDisabledCallback(disabled) { this.shadowRoot.querySelector("input").disabled = disabled; } formResetCallback() { this.value = null; } checkValidity() { return this.#internals.checkValidity(); } reportValidity() { return this.#internals.reportValidity(); } updateFormStatus() { const input = this.shadowRoot.querySelector("input"); const formValue = input.files == null ? null : input.files[0]; this.#internals.setValidity( { valueMissing: this.getAttribute("required") != null && formValue == null }, this.#validationMessage, this.shadowRoot.querySelector("label") ); this.#internals.setFormValue(formValue); } }; if (customElements.get(COMPONENT_TAG_NAME) == null) { customElements.define(COMPONENT_TAG_NAME, FileImageInputElement); } export { FileImageInputElement };