UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

183 lines (182 loc) 8.03 kB
/** * @typedef {Object} UploaderConfig * * @property {string | HTMLElement | Array<string | HTMLElement>} triggers - * Single or multiple elements (or selectors) that will act as upload triggers. * * @property {boolean} [multiple=false] - * Whether to allow selection of multiple files. * * @property {string|string[]} [accept=""] - * A comma-separated list of accepted file types (e.g., ".png,.jpg" or "image/*"). * * @property {Record<string, string>} [inputAttributes={}] - * Additional attributes to apply to the created `<input type="file">`. * * @property {Partial<CSSStyleDeclaration>} [inputStyles={ display: 'none' }] - * Inline CSS styles to apply to the hidden input. Default hides the element. * * @property {((triggerElement: HTMLElement) => void)|null} [onClick=null] - * Callback executed when a trigger is clicked (before the file dialog opens). * * @property {((files: FileList, triggerElement: HTMLElement) => void)|null} [onFileLoad=null] - * Callback executed when files are selected through the file input. */ import { isJsonObject } from '../index.mjs'; /** * TinyUploadClicker is a lightweight utility class for attaching custom clickable elements * (like buttons, divs, icons, etc.) that trigger hidden file input elements. * * It provides full control over the input file element without needing to display it, * and allows per-trigger customization, style injection, and event handling. * * Each trigger element creates its own associated hidden file input, enabling multiple * upload zones with different behaviors. You can handle `onClick` and `onFileLoad` callbacks, * define accepted file types, allow multiple selections, and cleanly destroy everything via `destroy()`. * * Designed for maximum flexibility in pure JavaScript environments, including dynamic UI rendering. * * @example * const uploader = new TinyUploadClicker({ * triggers: '#uploadBtn', * accept: ['.png', '.jpg'], * multiple: true, * inputAttributes: { 'data-tracker': 'upload' }, * inputStyles: { display: 'none' }, * onClick: (el) => console.log('Trigger clicked:', el), * onFileLoad: (files, el) => console.log('Files selected:', files) * }); * * // Later, if needed: * uploader.destroy(); */ class TinyUploadClicker { /** @type {UploaderConfig} */ #config; /** @type {Array<HTMLElement|Element|null>} */ #triggerElements = []; /** @type {WeakMap<HTMLElement, HTMLInputElement>} */ #inputs = new WeakMap(); /** @type {(ev: MouseEvent) => void} */ #boundClick; /** * Creates a new instance of TinyUploadClicker. * * @param {UploaderConfig} options - Configuration object for customizing the uploader behavior and appearance. * @throws {TypeError} If the config is invalid or required options are missing. */ constructor(options) { if (!isJsonObject(options)) throw new TypeError('TinyUploadClicker: "options" must be a valid object.'); this.#config = { multiple: false, accept: '', inputAttributes: {}, inputStyles: { display: 'none' }, onClick: null, onFileLoad: null, ...options, }; const { triggers } = options; if (!Array.isArray(triggers) && typeof triggers !== 'string' && !(triggers instanceof HTMLElement)) throw new TypeError('TinyUploadClicker: "triggers" is required and must be a string, HTMLElement, or an array of them.'); const triggerList = Array.isArray(this.#config.triggers) ? this.#config.triggers : [this.#config.triggers]; const resolvedTriggers = triggerList.map((ref) => { if (typeof ref === 'string') { const el = document.querySelector(ref); if (!el) { throw new Error(`TinyUploadClicker: No element found for selector "${ref}".`); } return el; } if (!(ref instanceof HTMLElement)) throw new TypeError('TinyUploadClicker: All triggers must be either strings (selectors) or HTMLElements.'); return ref; }); // Validate optional values if (options.accept !== undefined && typeof options.accept !== 'string' && !Array.isArray(options.accept)) throw new TypeError('TinyUploadClicker: "accept" must be a string or an array of strings.'); if (options.onClick !== undefined && typeof options.onClick !== 'function' && options.onClick !== null) throw new TypeError('TinyUploadClicker: "onClick" must be a function or null.'); if (options.onFileLoad !== undefined && typeof options.onFileLoad !== 'function' && options.onFileLoad !== null) throw new TypeError('TinyUploadClicker: "onFileLoad" must be a function or null.'); if (options.inputAttributes !== undefined && !isJsonObject(options.inputAttributes)) throw new TypeError('TinyUploadClicker: "inputAttributes" must be an object.'); if (options.inputStyles !== undefined && !isJsonObject(options.inputStyles)) throw new TypeError('TinyUploadClicker: "inputStyles" must be an object.'); this.#boundClick = this.#handleClick.bind(this); this.#triggerElements = resolvedTriggers; this.#triggerElements.forEach((trigger) => { if (!(trigger instanceof HTMLElement)) return; const input = document.createElement('input'); input.type = 'file'; if (this.#config.multiple) input.multiple = true; if (typeof this.#config.accept === 'string') input.accept = this.#config.accept; else if (Array.isArray(this.#config.accept)) input.accept = this.#config.accept.join(','); // Apply attributes const attr = this.#config.inputAttributes; if (attr && typeof attr === 'object') { for (const [key, value] of Object.entries(attr)) { input.setAttribute(key, value); } } // Apply styles Object.assign(input.style, this.#config.inputStyles || { display: 'none' }); // Listen for file changes input.addEventListener('change', () => { if (input.files && typeof this.#config.onFileLoad === 'function') this.#config.onFileLoad(input.files, trigger); input.value = ''; // reset so same file can be reselected }); document.body.appendChild(input); this.#inputs.set(trigger, input); trigger.addEventListener('click', this.#boundClick); }); } /** * Handles the click on a trigger element and forwards it to the associated file input. * * @param {MouseEvent} event - The click event object. */ #handleClick(event) { const trigger = event.currentTarget; if (!(trigger instanceof HTMLElement)) return; const input = this.#inputs.get(trigger); if (!input) return; if (typeof this.#config.onClick === 'function') this.#config.onClick(trigger); input.click(); } /** * Cleans up all internal elements and event listeners, removing created inputs and breaking references. */ destroy() { for (const trigger of this.#triggerElements) { if (!(trigger instanceof HTMLElement)) return; trigger.removeEventListener('click', this.#boundClick); const input = this.#inputs.get(trigger); if (input && input.parentNode) input.remove(); this.#inputs.delete(trigger); } this.#triggerElements = []; } } export default TinyUploadClicker;