UNPKG

@zag-js/file-upload

Version:

Core logic for the file-upload widget implemented as a state machine

359 lines (357 loc) • 14 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/file-upload.connect.ts var file_upload_connect_exports = {}; __export(file_upload_connect_exports, { connect: () => connect }); module.exports = __toCommonJS(file_upload_connect_exports); var import_dom_query = require("@zag-js/dom-query"); var import_file_utils = require("@zag-js/file-utils"); var import_i18n_utils = require("@zag-js/i18n-utils"); var import_types = require("@zag-js/types"); var import_utils = require("@zag-js/utils"); var import_file_upload = require("./file-upload.anatomy.js"); var dom = __toESM(require("./file-upload.dom.js")); var import_file_upload2 = require("./file-upload.utils.js"); var DEFAULT_ITEM_TYPE = "accepted"; var INTERACTIVE_SELECTOR = "button, a[href], input:not([type='file']), select, textarea, [tabindex], [contenteditable]"; function isInteractiveTarget(element, container) { if (!element || element.getAttribute("type") === "file") return false; const interactive = element.closest(INTERACTIVE_SELECTOR); return interactive != container && (0, import_dom_query.contains)(container, interactive); } function connect(service, normalize) { const { state, send, prop, computed, scope, context } = service; const disabled = !!prop("disabled"); const readOnly = !!prop("readOnly"); const required = !!prop("required"); const allowDrop = prop("allowDrop"); const translations = prop("translations"); const dragging = state.matches("dragging"); const focused = state.matches("focused") && !disabled; const acceptedFiles = context.get("acceptedFiles"); const maxFiles = prop("maxFiles"); return { dragging, focused, disabled, readOnly, transforming: context.get("transforming"), maxFilesReached: acceptedFiles.length >= maxFiles, remainingFiles: Math.max(0, maxFiles - acceptedFiles.length), openFilePicker() { if (disabled || readOnly) return; send({ type: "OPEN" }); }, deleteFile(file, type = DEFAULT_ITEM_TYPE) { if (disabled || readOnly) return; send({ type: "FILE.DELETE", file, itemType: type }); }, acceptedFiles, rejectedFiles: context.get("rejectedFiles"), setFiles(files) { if (disabled || readOnly) return; send({ type: "FILES.SET", files, count: files.length }); }, clearRejectedFiles() { if (disabled || readOnly) return; send({ type: "REJECTED_FILES.CLEAR" }); }, clearFiles() { if (disabled || readOnly) return; send({ type: "FILES.CLEAR" }); }, getFileSize(file) { return (0, import_i18n_utils.formatBytes)(file.size, prop("locale")); }, createFileUrl(file, cb) { const win = scope.getWin(); const url = win.URL.createObjectURL(file); cb(url); return () => win.URL.revokeObjectURL(url); }, setClipboardFiles(dt) { if (disabled || readOnly) return false; const items = Array.from(dt?.items ?? []); const files = items.reduce((acc, item) => { if (item.kind !== "file") return acc; const file = item.getAsFile(); if (!file) return acc; return [...acc, file]; }, []); if (!files.length) return false; send({ type: "FILE.SELECT", files }); return true; }, getRootProps() { return normalize.element({ ...import_file_upload.parts.root.attrs, dir: prop("dir"), id: dom.getRootId(scope), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-readonly": (0, import_dom_query.dataAttr)(readOnly), "data-dragging": (0, import_dom_query.dataAttr)(dragging) }); }, getDropzoneProps(props = {}) { return normalize.element({ ...import_file_upload.parts.dropzone.attrs, dir: prop("dir"), id: dom.getDropzoneId(scope), tabIndex: disabled || readOnly || props.disableClick ? void 0 : 0, role: props.disableClick ? "application" : "button", "aria-label": translations.dropzone, "aria-disabled": disabled || readOnly || void 0, "data-invalid": (0, import_dom_query.dataAttr)(prop("invalid")), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-readonly": (0, import_dom_query.dataAttr)(readOnly), "data-dragging": (0, import_dom_query.dataAttr)(dragging), onKeyDown(event) { if (disabled || readOnly) return; if (event.defaultPrevented) return; const target = (0, import_dom_query.getEventTarget)(event); if (!(0, import_dom_query.contains)(event.currentTarget, target)) return; if (isInteractiveTarget(target, event.currentTarget)) return; if (props.disableClick) return; if (event.key !== "Enter" && event.key !== " ") return; send({ type: "DROPZONE.CLICK", src: "keydown" }); }, onClick(event) { if (disabled || readOnly) return; if (event.defaultPrevented) return; if (props.disableClick) return; const target = (0, import_dom_query.getEventTarget)(event); if (!(0, import_dom_query.contains)(event.currentTarget, target)) return; if (isInteractiveTarget(target, event.currentTarget)) return; if (event.currentTarget.localName === "label") { event.preventDefault(); } send({ type: "DROPZONE.CLICK" }); }, onDragOver(event) { if (disabled || readOnly) return; if (!allowDrop) return; event.preventDefault(); event.stopPropagation(); try { event.dataTransfer.dropEffect = "copy"; } catch { } const hasFiles = (0, import_file_upload2.isEventWithFiles)(event); if (!hasFiles) return; const count = event.dataTransfer.items.length; send({ type: "DROPZONE.DRAG_OVER", count }); }, onDragLeave(event) { if (disabled || readOnly) return; if (!allowDrop) return; if ((0, import_dom_query.contains)(event.currentTarget, event.relatedTarget)) return; send({ type: "DROPZONE.DRAG_LEAVE" }); }, onDrop(event) { if (disabled || readOnly) return; if (allowDrop) { event.preventDefault(); event.stopPropagation(); } const hasFiles = (0, import_file_upload2.isEventWithFiles)(event); if (!hasFiles) return; (0, import_file_utils.getFileEntries)(event.dataTransfer.items, prop("directory")).then((files) => { send({ type: "DROPZONE.DROP", files: (0, import_utils.flatArray)(files) }); }); }, onFocus() { if (disabled || readOnly) return; send({ type: "DROPZONE.FOCUS" }); }, onBlur() { if (disabled || readOnly) return; send({ type: "DROPZONE.BLUR" }); } }); }, getTriggerProps() { return normalize.button({ ...import_file_upload.parts.trigger.attrs, dir: prop("dir"), id: dom.getTriggerId(scope), disabled: disabled || readOnly, "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-readonly": (0, import_dom_query.dataAttr)(readOnly), "data-invalid": (0, import_dom_query.dataAttr)(prop("invalid")), type: "button", onClick(event) { if (disabled || readOnly) return; if ((0, import_dom_query.contains)(dom.getDropzoneEl(scope), event.currentTarget)) { event.stopPropagation(); } send({ type: "OPEN" }); } }); }, getHiddenInputProps() { return normalize.input({ id: dom.getHiddenInputId(scope), tabIndex: -1, disabled: disabled || readOnly, type: "file", required: prop("required"), capture: prop("capture"), name: prop("name"), accept: computed("acceptAttr"), webkitdirectory: prop("directory") ? "" : void 0, multiple: computed("multiple") || prop("maxFiles") > 1, // exclude from accessibility tree since the dropzone/trigger provides the accessible interface "aria-hidden": true, onClick(event) { event.stopPropagation(); event.currentTarget.value = ""; }, onInput(event) { if (disabled || readOnly) return; const { files } = event.currentTarget; send({ type: "FILE.SELECT", files: files ? Array.from(files) : [] }); }, style: import_dom_query.visuallyHiddenStyle }); }, getItemGroupProps(props = {}) { const { type = DEFAULT_ITEM_TYPE } = props; return normalize.element({ ...import_file_upload.parts.itemGroup.attrs, dir: prop("dir"), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemProps(props) { const { file, type = DEFAULT_ITEM_TYPE } = props; return normalize.element({ ...import_file_upload.parts.item.attrs, dir: prop("dir"), id: dom.getItemId(scope, dom.getFileId(file)), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemNameProps(props) { const { file, type = DEFAULT_ITEM_TYPE } = props; return normalize.element({ ...import_file_upload.parts.itemName.attrs, dir: prop("dir"), id: dom.getItemNameId(scope, dom.getFileId(file)), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemSizeTextProps(props) { const { file, type = DEFAULT_ITEM_TYPE } = props; return normalize.element({ ...import_file_upload.parts.itemSizeText.attrs, dir: prop("dir"), id: dom.getItemSizeTextId(scope, dom.getFileId(file)), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemPreviewProps(props) { const { file, type = DEFAULT_ITEM_TYPE } = props; return normalize.element({ ...import_file_upload.parts.itemPreview.attrs, dir: prop("dir"), id: dom.getItemPreviewId(scope, dom.getFileId(file)), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemPreviewImageProps(props) { const { file, url, type = DEFAULT_ITEM_TYPE } = props; const isImage = file.type.startsWith("image/"); if (!isImage) { throw new Error("Preview Image is only supported for image files"); } return normalize.img({ ...import_file_upload.parts.itemPreviewImage.attrs, alt: translations.itemPreview?.(file), src: url, "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-type": type }); }, getItemDeleteTriggerProps(props) { const { file, type = DEFAULT_ITEM_TYPE } = props; return normalize.button({ ...import_file_upload.parts.itemDeleteTrigger.attrs, dir: prop("dir"), id: dom.getItemDeleteTriggerId(scope, dom.getFileId(file)), type: "button", disabled: disabled || readOnly, "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-readonly": (0, import_dom_query.dataAttr)(readOnly), "data-type": type, "aria-label": translations.deleteFile?.(file), onClick() { if (disabled || readOnly) return; send({ type: "FILE.DELETE", file, itemType: type }); } }); }, getLabelProps() { return normalize.label({ ...import_file_upload.parts.label.attrs, dir: prop("dir"), id: dom.getLabelId(scope), htmlFor: dom.getHiddenInputId(scope), "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-required": (0, import_dom_query.dataAttr)(required) }); }, getClearTriggerProps() { return normalize.button({ ...import_file_upload.parts.clearTrigger.attrs, dir: prop("dir"), type: "button", disabled: disabled || readOnly, hidden: acceptedFiles.length === 0, "data-disabled": (0, import_dom_query.dataAttr)(disabled), "data-readonly": (0, import_dom_query.dataAttr)(readOnly), onClick(event) { if (event.defaultPrevented) return; if (disabled || readOnly) return; send({ type: "FILES.CLEAR" }); } }); } }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { connect });