@activecollab/components
Version:
ActiveCollab Components
457 lines (439 loc) • 23.6 kB
JavaScript
"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