UNPKG

@activecollab/components

Version:

ActiveCollab Components

457 lines (439 loc) 23.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useMockUpload = exports.revokeThumbnail = exports.renderFileThumbnail = exports.formatFileSize = exports.describeFile = exports.binaryFrom = exports.UPLOAD_TICK_MS = exports.UPLOAD_STEP = exports.StyledUploadCover = exports.StyledPlaceholderBox = exports.StyledHiddenFileInput = exports.StyledFileName = exports.StyledFileFallback = exports.StyledFileCard = exports.StyledDropOverlay = exports.PREPARING_MS = exports.MAX_UPLOAD_BYTES = exports.FileUploadCard = exports.FilePlaceholder = exports.FileFormattingToolbar = exports.FileElement = exports.FILE_SIZE_UNITS = exports.FILE_DEFAULT_WIDTH = void 0; var _react = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireDefault(require("styled-components")); var _elements = require("./elements"); var _Icons = require("../../components/Icons"); var _Loaders = require("../../components/Loaders"); var _ProgressRing = require("../../components/ProgressRing"); var _StackedCard = require("../../components/StackedCard"); var _Tooltip = require("../../components/Tooltip"); var _Typography = require("../../components/Typography"); var _content = require("../stackedCard/content"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /* ------------------------------------------------------------------ */ /* File element — the upload lifecycle + the file card */ /* ------------------------------------------------------------------ */ /** * The file card is the second element built on `StackedCard`, and it mirrors the * card in a project's Files tab (`Folders/GridView/Items/File.js`): a thumbnail * over a strip with the name, then size · date. * * Its lifecycle is the app's real upload lifecycle, which is two phases and not * one — the distinction the ring alone doesn't tell you: * * uploading XHR progress 0–100. `ProgressRing radius={22} stroke={4}` with * the percentage, exactly as `Items/FileUpload.js` does it. * preparing bytes are in, the server is generating the thumbnail. Progress * is no longer knowable, so the ring gives way to the * indeterminate `SpinnerLoader` at the same size — again as the * real card does at `progress === 100`. * ready the thumbnail exists; the card renders it. * error the upload failed: retry, same as the app's `ArrowRefreshIcon`. * * Both phases reserve the same box as the finished cover, so a card never * reflows as it climbs — the discipline the unfurl ladder established. */ /* The Files-tab card is a fixed 208×208. On a canvas the width is the host's, so 208 is only the default a click-placed card takes. */ var FILE_DEFAULT_WIDTH = exports.FILE_DEFAULT_WIDTH = 208; /* Big enough to be worth refusing, so the error state is reachable: the server enforces the real limits (and answers StorageOverusedError when full). */ var MAX_UPLOAD_BYTES = exports.MAX_UPLOAD_BYTES = 25 * 1024 * 1024; var UPLOAD_TICK_MS = exports.UPLOAD_TICK_MS = 110; var UPLOAD_STEP = exports.UPLOAD_STEP = 9; /* How long the server spends making a thumbnail once the bytes have landed. */ var PREPARING_MS = exports.PREPARING_MS = 1100; /** What the mock keeps about a picked file. */ /** A file element on the board. Position lives in the shared positions map. */ /* Angie.functions.format_file_size — two decimals, no space, lower-case k. "1.20MB", not "1.2 MB". Reproduced exactly so the mock reads like the app. */ var FILE_SIZE_UNITS = exports.FILE_SIZE_UNITS = [["TB", 1099511627776], ["GB", 1073741824], ["MB", 1048576], ["kB", 1024]]; var formatFileSize = exports.formatFileSize = function formatFileSize(value) { for (var _ref3 of FILE_SIZE_UNITS) { var _ref2 = _slicedToArray(_ref3, 2); var unit = _ref2[0]; var bytes = _ref2[1]; var inUnit = value / bytes; if (inUnit > 0.9) { return "".concat(inUnit.toFixed(2)).concat(unit); } } return "".concat(Math.trunc(value), "B"); }; var describeFile = exports.describeFile = function describeFile(file) { var dot = file.name.lastIndexOf("."); var hasExtension = dot > 0; return { base: hasExtension ? file.name.slice(0, dot) : file.name, extension: hasExtension ? file.name.slice(dot + 1) : "", size: file.size, mimeType: file.type, // Images get a real thumbnail; the server would generate one for every type, // but the mock can only show what the browser can already decode. thumbnailUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : null }; }; /* Object URLs are a manual allocation — release one whenever the file it stands for stops being on the board. */ var revokeThumbnail = exports.revokeThumbnail = function revokeThumbnail(file) { if (file !== null && file !== void 0 && file.thumbnailUrl) { URL.revokeObjectURL(file.thumbnailUrl); } }; /** The first actual file on a clipboard or a drop, or null if there is none. */ var binaryFrom = exports.binaryFrom = function binaryFrom(data) { if (!data) { return null; } if (data.files && data.files.length > 0) { return data.files[0]; } // A screenshot paste arrives as an item, not in `files`. for (var item of Array.from((_data$items = data.items) !== null && _data$items !== void 0 ? _data$items : [])) { var _data$items; if (item.kind === "file") { var picked = item.getAsFile(); if (picked) { return picked; } } } return null; }; /** * Drives one file through the upload lifecycle. Stands in for * `useFileHandler` + the XHR in `utils/upload.jsx`: ticks progress to 100, * hands over to the thumbnail phase, then resolves. */ var useMockUpload = exports.useMockUpload = function useMockUpload(file) { var _useState = (0, _react.useState)("uploading"), _useState2 = _slicedToArray(_useState, 2), status = _useState2[0], setStatus = _useState2[1]; var _useState3 = (0, _react.useState)(0), _useState4 = _slicedToArray(_useState3, 2), progress = _useState4[0], setProgress = _useState4[1]; var timers = (0, _react.useRef)([]); var ticker = (0, _react.useRef)(null); var progressRef = (0, _react.useRef)(0); var clearAll = (0, _react.useCallback)(function () { timers.current.forEach(clearTimeout); timers.current = []; if (ticker.current) { clearInterval(ticker.current); ticker.current = null; } }, []); var retry = (0, _react.useCallback)(function () { clearAll(); progressRef.current = 0; setProgress(0); setStatus("uploading"); if (!file) { return; } if (file.size > MAX_UPLOAD_BYTES) { timers.current.push(setTimeout(function () { return setStatus("error"); }, 700)); return; } ticker.current = setInterval(function () { progressRef.current = Math.min(100, progressRef.current + UPLOAD_STEP); setProgress(progressRef.current); if (progressRef.current >= 100) { clearAll(); // The bytes are up; the thumbnail is the server's job now. setStatus("preparing"); timers.current.push(setTimeout(function () { return setStatus("ready"); }, PREPARING_MS)); } }, UPLOAD_TICK_MS); }, [clearAll, file]); (0, _react.useEffect)(function () { retry(); return clearAll; }, [retry, clearAll]); return { status, progress, retry }; }; /* The card fills the box the board drew; height stays auto. */ var StyledFileCard = exports.StyledFileCard = (0, _styledComponents.default)(_StackedCard.StackedCard).withConfig({ displayName: "fileElement__StyledFileCard", componentId: "sc-1bsy8py-0" })(["&&{width:100%;}user-select:none;"]); /* The cover while uploading or preparing — the ring/spinner centred in the box the finished thumbnail will occupy, so resolving never reflows the card. */ var StyledUploadCover = exports.StyledUploadCover = _styledComponents.default.div.withConfig({ displayName: "fileElement__StyledUploadCover", componentId: "sc-1bsy8py-1" })(["display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;width:100%;height:100%;background-color:var(--color-theme-200);"]); /* No thumbnail (the browser can't decode a .zip): the clip plus the extension. The real card has NO type-icon fallback — it renders a bare <img> and trusts the server — so this is net-new and worth a decision. */ var StyledFileFallback = exports.StyledFileFallback = _styledComponents.default.div.withConfig({ displayName: "fileElement__StyledFileFallback", componentId: "sc-1bsy8py-2" })(["display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;width:100%;height:100%;background-color:var(--color-theme-200);color:var(--color-theme-600);> svg{width:36px;height:36px;fill:var(--color-theme-500);}"]); /* Name row: the base name truncates, the extension never does — the same split the real card makes with removeExtensionFromFileName. */ var StyledFileName = exports.StyledFileName = _styledComponents.default.div.withConfig({ displayName: "fileElement__StyledFileName", componentId: "sc-1bsy8py-3" })(["display:flex;align-items:baseline;min-width:0;width:100%;.file-base{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.file-ext{flex-shrink:0;}"]); /* The drop overlay, from directive.ang_drop_files.less: a near-opaque cover with a 2px dashed primary border and "Drop here". The app sets 24px type for a full-page target; on a card it reads at Body2. */ var StyledDropOverlay = exports.StyledDropOverlay = _styledComponents.default.div.withConfig({ displayName: "fileElement__StyledDropOverlay", componentId: "sc-1bsy8py-4" })(["position:absolute;inset:0;z-index:3;display:flex;align-items:center;justify-content:center;padding:8px;box-sizing:border-box;background-color:var(--page-paper-main);.drop-box{display:flex;align-items:center;justify-content:center;width:100%;height:100%;box-sizing:border-box;border:2px dashed var(--color-primary);border-radius:6px;}"]); var StyledHiddenFileInput = exports.StyledHiddenFileInput = _styledComponents.default.input.withConfig({ displayName: "fileElement__StyledHiddenFileInput", componentId: "sc-1bsy8py-5" })(["display:none;"]); /* The placeholder reserves the same box the finished cover will occupy — the cover's ratio, not a pixel height — so attaching a file does not resize the card under the pointer. Placeholder's own fixed height is overridden here; the descendant selector outranks its class. */ var StyledPlaceholderBox = exports.StyledPlaceholderBox = _styledComponents.default.div.withConfig({ displayName: "fileElement__StyledPlaceholderBox", componentId: "sc-1bsy8py-6" })(["width:100%;aspect-ratio:", ";> button{height:100%;}"], _content.COVER_RATIO_IMAGE); /* The interactive empty state, in the shared placeholder frame the other card types use. Doubles as the drop target, like its copy promises. */ var FilePlaceholder = exports.FilePlaceholder = function FilePlaceholder(props) { return /*#__PURE__*/_react.default.createElement(StyledPlaceholderBox, null, /*#__PURE__*/_react.default.createElement(_content.Placeholder, _extends({ icon: /*#__PURE__*/_react.default.createElement(_Icons.AttachmentIcon, null), label: "Drop a file or click to upload", dashed: true, "data-drag-through": "" }, props))); }; /* The file element's floating toolbar. The leading action is whatever the current phase makes meaningful; Move to Trash is always available. */ var FileFormattingToolbar = exports.FileFormattingToolbar = function FileFormattingToolbar(_ref4) { var status = _ref4.status, onPick = _ref4.onPick, onCancel = _ref4.onCancel, onRetry = _ref4.onRetry, onDelete = _ref4.onDelete; return /*#__PURE__*/_react.default.createElement(_elements.StyledFormattingToolbar, { onMouseDown: _elements.stopClick, onClick: _elements.stopClick }, status === "placeholder" ? /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: onPick }, /*#__PURE__*/_react.default.createElement(_Icons.UploadIcon, { className: "i16" }), "Upload") : null, status === "uploading" || status === "preparing" ? /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: onCancel }, /*#__PURE__*/_react.default.createElement(_Icons.CancelCrossIcon, { className: "i16" }), "Cancel") : null, status === "error" ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: onRetry }, /*#__PURE__*/_react.default.createElement(_Icons.ArrowRefreshIcon, { className: "i16" }), "Retry"), /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: onCancel }, /*#__PURE__*/_react.default.createElement(_Icons.CancelCrossIcon, { className: "i16" }), "Cancel")) : null, status === "ready" ? /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button" }, /*#__PURE__*/_react.default.createElement(_Icons.DownloadIcon, { className: "i16" }), "Download") : null, /*#__PURE__*/_react.default.createElement("span", { className: "divider" }), /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, { title: "Move to Trash" }, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", className: "icon-only", onClick: onDelete }, /*#__PURE__*/_react.default.createElement(_Icons.TrashIcon, { className: "i16" })))); }; /** * A file element with a file attached: the card runs the upload lifecycle and * then renders the finished preview. */ var FileUploadCard = exports.FileUploadCard = function FileUploadCard(_ref5) { var element = _ref5.element, selected = _ref5.selected, resize = _ref5.resize, onCancel = _ref5.onCancel, onDelete = _ref5.onDelete; var file = element.file; var upload = useMockUpload(file); var ready = upload.status === "ready"; return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(StyledFileCard, _extends({ selected: selected, resizable: selected, resizeAxis: "width", resizeLabel: "Resize file card" }, resize), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardCover, { aspectRatio: _content.COVER_RATIO_IMAGE }, ready ? renderFileThumbnail(file) : /*#__PURE__*/_react.default.createElement(StyledUploadCover, null, upload.status === "uploading" ? /*#__PURE__*/_react.default.createElement(_ProgressRing.ProgressRing, { progress: upload.progress, radius: 22, stroke: 4 }, /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { weight: "bold", color: "secondary" }, upload.progress, "%")) : null, upload.status === "preparing" ? /*#__PURE__*/_react.default.createElement(_Loaders.SpinnerLoader, { radius: 22, stroke: 4 }) : null, upload.status === "error" ? /*#__PURE__*/_react.default.createElement(_Icons.ArrowRefreshIcon, { style: { cursor: "pointer" }, onClick: upload.retry }) : null, /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { color: "tertiary" }, upload.status === "uploading" ? "Uploading…" : null, upload.status === "preparing" ? "Preparing preview…" : null, upload.status === "error" ? "Upload failed" : null))), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, { role: "title" }, /*#__PURE__*/_react.default.createElement(StyledFileName, null, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold", className: "file-base" }, file === null || file === void 0 ? void 0 : file.base), file !== null && file !== void 0 && file.extension ? /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold", className: "file-ext" }, ".", file.extension) : null)), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, { role: "meta" }, /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { color: "tertiary" }, ready && file ? "".concat(formatFileSize(file.size), " \xB7 Just now") : " "))), selected ? /*#__PURE__*/_react.default.createElement(FileFormattingToolbar, { status: upload.status, onCancel: onCancel, onRetry: upload.retry, onDelete: onDelete }) : null); }; /* An image renders for real; anything else falls back to the clip + extension. */ var renderFileThumbnail = exports.renderFileThumbnail = function renderFileThumbnail(file) { return file !== null && file !== void 0 && file.thumbnailUrl ? /*#__PURE__*/_react.default.createElement("img", { src: file.thumbnailUrl, alt: "" }) : /*#__PURE__*/_react.default.createElement(StyledFileFallback, null, /*#__PURE__*/_react.default.createElement(_Icons.AttachmentIcon, null), /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { color: "tertiary" }, file !== null && file !== void 0 && file.extension ? file.extension.toUpperCase() : "FILE")); }; /** * The two shapes of a file element: an empty placeholder that can be clicked or * dropped onto, and the uploading/ready card. * * A box drawn with the File tool opens the picker immediately. Dismissing the * picker fires no change, so the card simply stays a placeholder — which is what * makes Esc a no-op by construction rather than by handling. */ var FileElement = exports.FileElement = function FileElement(_ref6) { var element = _ref6.element, selected = _ref6.selected, autoOpen = _ref6.autoOpen, onPickFile = _ref6.onPickFile, resize = _ref6.resize, onCancel = _ref6.onCancel, onDelete = _ref6.onDelete; var inputRef = (0, _react.useRef)(null); var _useState5 = (0, _react.useState)(false), _useState6 = _slicedToArray(_useState5, 2), dropping = _useState6[0], setDropping = _useState6[1]; var openPicker = (0, _react.useCallback)(function () { var _inputRef$current; (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.click(); }, []); (0, _react.useEffect)(function () { if (autoOpen) { openPicker(); } }, [autoOpen, openPicker]); var handleInputChange = (0, _react.useCallback)(function (event) { var _event$target$files; var picked = (_event$target$files = event.target.files) === null || _event$target$files === void 0 ? void 0 : _event$target$files[0]; if (picked) { onPickFile(picked); } // Let the same file be chosen again after a cancel or a delete. event.target.value = ""; }, [onPickFile]); var handleDrop = (0, _react.useCallback)(function (event) { event.preventDefault(); event.stopPropagation(); setDropping(false); var dropped = binaryFrom(event.dataTransfer); if (dropped) { onPickFile(dropped); } }, [onPickFile]); var input = /*#__PURE__*/_react.default.createElement(StyledHiddenFileInput, { ref: inputRef, type: "file", onChange: handleInputChange }); if (element.file != null) { return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, input, /*#__PURE__*/_react.default.createElement(FileUploadCard, { element: element, selected: selected, resize: resize, onCancel: onCancel, onDelete: onDelete })); } return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, input, /*#__PURE__*/_react.default.createElement(StyledFileCard, { selected: selected, "aria-label": "Empty file card" }, /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, { role: "custom", bleed: true /* Marks this row as owning its own drops, so the board-level handler stands aside and lets the card take the file. */, "data-file-dropzone": "", onDragEnter: function onDragEnter(event) { event.preventDefault(); setDropping(true); }, onDragOver: function onDragOver(event) { return event.preventDefault(); }, onDragLeave: function onDragLeave(event) { // Ignore the leaves fired when crossing onto a child. if (!event.currentTarget.contains(event.relatedTarget)) { setDropping(false); } }, onDrop: handleDrop }, /*#__PURE__*/_react.default.createElement(FilePlaceholder, { onClick: openPicker }), dropping ? /*#__PURE__*/_react.default.createElement(StyledDropOverlay, null, /*#__PURE__*/_react.default.createElement("span", { className: "drop-box" }, /*#__PURE__*/_react.default.createElement(_Typography.Body2, { color: "secondary" }, "Drop here"))) : null)), selected ? /*#__PURE__*/_react.default.createElement(FileFormattingToolbar, { status: "placeholder", onPick: openPicker, onDelete: onDelete }) : null); }; //# sourceMappingURL=fileElement.js.map