@activecollab/components 2.0.398 → 2.0.399

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/cjs/presentation/whiteboard/bottomDock.js +194 -0
  2. package/dist/cjs/presentation/whiteboard/bottomDock.js.map +1 -0
  3. package/dist/cjs/presentation/whiteboard/canvasElements.js +877 -0
  4. package/dist/cjs/presentation/whiteboard/canvasElements.js.map +1 -0
  5. package/dist/cjs/presentation/whiteboard/elements.js +816 -0
  6. package/dist/cjs/presentation/whiteboard/elements.js.map +1 -0
  7. package/dist/cjs/presentation/whiteboard/fileElement.js +457 -0
  8. package/dist/cjs/presentation/whiteboard/fileElement.js.map +1 -0
  9. package/dist/cjs/presentation/whiteboard/glyphs.js +67 -0
  10. package/dist/cjs/presentation/whiteboard/glyphs.js.map +1 -0
  11. package/dist/cjs/presentation/whiteboard/inspector.js +884 -0
  12. package/dist/cjs/presentation/whiteboard/inspector.js.map +1 -0
  13. package/dist/cjs/presentation/whiteboard/linkElement.js +357 -0
  14. package/dist/cjs/presentation/whiteboard/linkElement.js.map +1 -0
  15. package/dist/cjs/presentation/whiteboard/outline.js +966 -0
  16. package/dist/cjs/presentation/whiteboard/outline.js.map +1 -0
  17. package/dist/cjs/presentation/whiteboard/peopleMenu.js +516 -0
  18. package/dist/cjs/presentation/whiteboard/peopleMenu.js.map +1 -0
  19. package/dist/cjs/presentation/whiteboard/settingsMenu.js +177 -0
  20. package/dist/cjs/presentation/whiteboard/settingsMenu.js.map +1 -0
  21. package/dist/esm/presentation/whiteboard/bottomDock.d.ts +51 -0
  22. package/dist/esm/presentation/whiteboard/bottomDock.d.ts.map +1 -0
  23. package/dist/esm/presentation/whiteboard/bottomDock.js +162 -0
  24. package/dist/esm/presentation/whiteboard/bottomDock.js.map +1 -0
  25. package/dist/esm/presentation/whiteboard/canvasElements.d.ts +61 -0
  26. package/dist/esm/presentation/whiteboard/canvasElements.d.ts.map +1 -0
  27. package/dist/esm/presentation/whiteboard/canvasElements.js +746 -0
  28. package/dist/esm/presentation/whiteboard/canvasElements.js.map +1 -0
  29. package/dist/esm/presentation/whiteboard/elements.d.ts +180 -0
  30. package/dist/esm/presentation/whiteboard/elements.d.ts.map +1 -0
  31. package/dist/esm/presentation/whiteboard/elements.js +736 -0
  32. package/dist/esm/presentation/whiteboard/elements.js.map +1 -0
  33. package/dist/esm/presentation/whiteboard/fileElement.d.ts +113 -0
  34. package/dist/esm/presentation/whiteboard/fileElement.d.ts.map +1 -0
  35. package/dist/esm/presentation/whiteboard/fileElement.js +430 -0
  36. package/dist/esm/presentation/whiteboard/fileElement.js.map +1 -0
  37. package/dist/esm/presentation/whiteboard/glyphs.d.ts +32 -0
  38. package/dist/esm/presentation/whiteboard/glyphs.d.ts.map +1 -0
  39. package/dist/esm/presentation/whiteboard/glyphs.js +50 -0
  40. package/dist/esm/presentation/whiteboard/glyphs.js.map +1 -0
  41. package/dist/esm/presentation/whiteboard/inspector.d.ts +100 -0
  42. package/dist/esm/presentation/whiteboard/inspector.d.ts.map +1 -0
  43. package/dist/esm/presentation/whiteboard/inspector.js +753 -0
  44. package/dist/esm/presentation/whiteboard/inspector.js.map +1 -0
  45. package/dist/esm/presentation/whiteboard/linkElement.d.ts +97 -0
  46. package/dist/esm/presentation/whiteboard/linkElement.d.ts.map +1 -0
  47. package/dist/esm/presentation/whiteboard/linkElement.js +327 -0
  48. package/dist/esm/presentation/whiteboard/linkElement.js.map +1 -0
  49. package/dist/esm/presentation/whiteboard/outline.d.ts +89 -0
  50. package/dist/esm/presentation/whiteboard/outline.d.ts.map +1 -0
  51. package/dist/esm/presentation/whiteboard/outline.js +870 -0
  52. package/dist/esm/presentation/whiteboard/outline.js.map +1 -0
  53. package/dist/esm/presentation/whiteboard/peopleMenu.d.ts +44 -0
  54. package/dist/esm/presentation/whiteboard/peopleMenu.d.ts.map +1 -0
  55. package/dist/esm/presentation/whiteboard/peopleMenu.js +440 -0
  56. package/dist/esm/presentation/whiteboard/peopleMenu.js.map +1 -0
  57. package/dist/esm/presentation/whiteboard/settingsMenu.d.ts +13 -0
  58. package/dist/esm/presentation/whiteboard/settingsMenu.d.ts.map +1 -0
  59. package/dist/esm/presentation/whiteboard/settingsMenu.js +155 -0
  60. package/dist/esm/presentation/whiteboard/settingsMenu.js.map +1 -0
  61. package/package.json +1 -1
@@ -0,0 +1,746 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import React, { useCallback, useEffect, useRef, useState } from "react";
3
+ import { CARD_PREVIEW_HEIGHT, SHAPE_DEFAULT_SIZES, isShapeTool } from "./bottomDock";
4
+ import { CANVAS_ELEMENTS, PostitFormattingToolbar, ResizeHandles, ShapeFormattingToolbar, StyledCanvasElement, StyledCanvasLayer, StyledSticky, defaultShapeState, gridLevel, renderShapeBody, snapValue, stickyGradient } from "./elements";
5
+ import { FILE_DEFAULT_WIDTH, FileElement, binaryFrom, describeFile, revokeThumbnail } from "./fileElement";
6
+ import { CARD_DRAW_THRESHOLD, LINK_DEFAULT_WIDTH, LinkElement, StyledLinkDraft, StyledShapePreview, clampCardWidth, isValidLinkUrl, shapePreviewGlyph } from "./linkElement";
7
+
8
+ /**
9
+ * A card is full of its own controls — the URL field, the display toggle, the
10
+ * retry button, the toolbar. A mousedown on any of them selects the element but
11
+ * must not start a board drag.
12
+ *
13
+ * The resize grip needs no entry here: the shipped handle calls
14
+ * `preventDefault()` on pointerdown, which suppresses the compatibility mouse
15
+ * events entirely, so a grip press never reaches this at all.
16
+ */
17
+ export const NO_DRAG_SELECTOR = "a, button, input, textarea, [data-no-drag]";
18
+
19
+ /**
20
+ * …except a control that IS the whole card.
21
+ *
22
+ * A placeholder is one big button filling its card, so treating it as a control
23
+ * left no surface to grab and the card could not be moved at all. Marked
24
+ * drag-through, it does both: moving the pointer drags the card, and releasing
25
+ * without moving is still a click that opens the editor or the file picker.
26
+ * (Atlassian's rule — make a handle draggable, not the whole element — assumes
27
+ * the element has some non-interactive surface. This one has none.)
28
+ */
29
+ export const DRAG_THROUGH_SELECTOR = "[data-drag-through]";
30
+ /* The canvas elements + selection state. The rounded rectangle starts
31
+ selected; clicking an element selects it, clicking the canvas deselects,
32
+ Delete on the shape toolbar removes the element. Toolbar submenus really
33
+ edit the per-element state. Elements are draggable, and snap to the grid
34
+ when the board's Snap to Grid setting is on.
35
+
36
+ Link elements are created here too — by drawing a box with the Link tool
37
+ armed, or by pasting a URL onto the canvas. */
38
+ export const CanvasElements = _ref => {
39
+ let gridSize = _ref.gridSize,
40
+ snapToGrid = _ref.snapToGrid,
41
+ activeTool = _ref.activeTool,
42
+ onToolUsed = _ref.onToolUsed,
43
+ selectedId = _ref.selectedId,
44
+ setSelectedId = _ref.onSelectedIdChange,
45
+ positions = _ref.positions,
46
+ setPositions = _ref.setPositions,
47
+ shapes = _ref.shapes,
48
+ setShapes = _ref.setShapes,
49
+ stickyStyle = _ref.stickyStyle,
50
+ setStickyStyle = _ref.setStickyStyle,
51
+ links = _ref.links,
52
+ setLinks = _ref.setLinks,
53
+ files = _ref.files,
54
+ setFiles = _ref.setFiles,
55
+ createdShapes = _ref.createdShapes,
56
+ setCreatedShapes = _ref.setCreatedShapes,
57
+ sizes = _ref.sizes,
58
+ rotations = _ref.rotations,
59
+ locks = _ref.locks,
60
+ setLocks = _ref.setLocks;
61
+ const toggleLock = id => setLocks(current => {
62
+ var _current$id;
63
+ return _extends({}, current, {
64
+ [id]: !((_current$id = current[id]) != null ? _current$id : false)
65
+ });
66
+ });
67
+ const _useState = useState([]),
68
+ removedIds = _useState[0],
69
+ setRemovedIds = _useState[1];
70
+ const _useState2 = useState(null),
71
+ autoEditId = _useState2[0],
72
+ setAutoEditId = _useState2[1];
73
+ const _useState3 = useState(null),
74
+ autoOpenId = _useState3[0],
75
+ setAutoOpenId = _useState3[1];
76
+ const _useState4 = useState(null),
77
+ draft = _useState4[0],
78
+ setDraft = _useState4[1];
79
+ // The active drag and the live snap step are held in refs so the document
80
+ // listeners stay stable and always read the current Board Settings.
81
+ const dragRef = useRef(null);
82
+ const snapStepRef = useRef(null);
83
+ snapStepRef.current = snapToGrid ? gridLevel(gridSize).snapStep : null;
84
+ /* Set once a press turns into a move, so the click that ends the gesture can
85
+ be swallowed before it reaches a drag-through control. */
86
+ const suppressClickRef = useRef(false);
87
+
88
+ /* Runs in the capture phase, so it can stop the click before the placeholder
89
+ button underneath ever sees it. It must stay a no-op otherwise — stopping
90
+ propagation on the way down would keep every ordinary click from reaching
91
+ the control it was aimed at. */
92
+ const handleElementClickCapture = useCallback(event => {
93
+ if (!suppressClickRef.current) {
94
+ return;
95
+ }
96
+ suppressClickRef.current = false;
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ }, []);
100
+ const handleDragMove = useCallback(event => {
101
+ const drag = dragRef.current;
102
+ if (!drag) {
103
+ return;
104
+ }
105
+ const dx = event.clientX - drag.startX;
106
+ const dy = event.clientY - drag.startY;
107
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 3) {
108
+ return;
109
+ }
110
+ // Past the threshold this gesture is a move, so the click it ends with must
111
+ // not also activate whatever was under the pointer.
112
+ drag.moved = true;
113
+ suppressClickRef.current = true;
114
+ const step = snapStepRef.current;
115
+ const x = snapValue(drag.originX + dx, step);
116
+ const y = snapValue(drag.originY + dy, step);
117
+ setPositions(current => _extends({}, current, {
118
+ [drag.id]: {
119
+ x,
120
+ y
121
+ }
122
+ }));
123
+ }, [setPositions]);
124
+ const handleDragEnd = useCallback(() => {
125
+ dragRef.current = null;
126
+ window.removeEventListener("mousemove", handleDragMove);
127
+ window.removeEventListener("mouseup", handleDragEnd);
128
+ }, [handleDragMove]);
129
+
130
+ /* A native HTML5 drag (an <img>, a text selection) swallows the mouseup this
131
+ drag ends on, which would leave the element following the pointer with no
132
+ button held. Ending the board drag on `dragstart` closes that hole; the
133
+ cover's media is also marked undraggable in the design system, so the case
134
+ should not arise from a card in the first place. */
135
+ useEffect(() => {
136
+ const handleNativeDragStart = event => {
137
+ event.preventDefault();
138
+ handleDragEnd();
139
+ };
140
+ window.addEventListener("dragstart", handleNativeDragStart);
141
+ return () => window.removeEventListener("dragstart", handleNativeDragStart);
142
+ }, [handleDragEnd]);
143
+ const handleElementMouseDown = useCallback((event, id) => {
144
+ if (event.button !== 0) {
145
+ return;
146
+ }
147
+
148
+ // An armed tool owns the canvas: let the press through to the layer so a
149
+ // box can be drawn over an existing element instead of moving it.
150
+ if (activeToolRef.current !== "select") {
151
+ return;
152
+ }
153
+ event.stopPropagation();
154
+ setSelectedId(id);
155
+
156
+ // A fresh press starts a fresh gesture: whatever the last one decided
157
+ // about swallowing its click no longer applies.
158
+ suppressClickRef.current = false;
159
+ const target = event.target;
160
+ if (target.closest(NO_DRAG_SELECTOR) && !target.closest(DRAG_THROUGH_SELECTOR)) {
161
+ return;
162
+ }
163
+ const origin = positions[id];
164
+ dragRef.current = {
165
+ id,
166
+ startX: event.clientX,
167
+ startY: event.clientY,
168
+ originX: origin.x,
169
+ originY: origin.y,
170
+ moved: false
171
+ };
172
+ window.addEventListener("mousemove", handleDragMove);
173
+ window.addEventListener("mouseup", handleDragEnd);
174
+ }, [positions, handleDragMove, handleDragEnd, setSelectedId]);
175
+ const updateShape = useCallback((id, partial) => setShapes(current => _extends({}, current, {
176
+ [id]: _extends({}, current[id], partial)
177
+ })), [setShapes]);
178
+ const handleDelete = useCallback(() => {
179
+ setSelectedId(current => {
180
+ if (current) {
181
+ setRemovedIds(removed => [...removed, current]);
182
+ }
183
+ return null;
184
+ });
185
+ }, [setSelectedId]);
186
+
187
+ /* ---- link elements ------------------------------------------------ */
188
+
189
+ const layerRef = useRef(null);
190
+ const linkCounterRef = useRef(0);
191
+ // Where a pasted card lands: the pointer's last canvas position.
192
+ const pointerRef = useRef({
193
+ x: 160,
194
+ y: 160
195
+ });
196
+ /* The width each live resize gesture started from. The grip reports deltas
197
+ measured from the gesture START and brackets the stream with
198
+ `onResizeStart` / `onResizeCommit`, so this is a snapshot taken when a
199
+ gesture opens and dropped when it closes — not a running accumulator. That
200
+ is what lets a slow, few-pixels-at-a-time drag keep growing: the delta
201
+ itself grows, so it crosses grid lines instead of rounding each hop away.
202
+ Having a defined gesture end also means nothing is ever left over to make
203
+ the next gesture jump. */
204
+ const resizeStartRef = useRef({});
205
+
206
+ // Mirrors so the stable document/window handlers always read current state.
207
+ const activeToolRef = useRef(activeTool);
208
+ activeToolRef.current = activeTool;
209
+ const linksRef = useRef(links);
210
+ linksRef.current = links;
211
+ const filesRef = useRef(files);
212
+ filesRef.current = files;
213
+ const canvasPoint = useCallback((clientX, clientY) => {
214
+ var _layerRef$current, _rect$left, _rect$top;
215
+ const rect = (_layerRef$current = layerRef.current) == null ? void 0 : _layerRef$current.getBoundingClientRect();
216
+ return {
217
+ x: clientX - ((_rect$left = rect == null ? void 0 : rect.left) != null ? _rect$left : 0),
218
+ y: clientY - ((_rect$top = rect == null ? void 0 : rect.top) != null ? _rect$top : 0)
219
+ };
220
+ }, []);
221
+
222
+ /* ---- shape elements ------------------------------------------------ */
223
+
224
+ const shapeCounterRef = useRef(0);
225
+
226
+ /* Create a shape element. A plain click drops the tool's default size at the
227
+ (snapped) point; a real drag creates the drawn box. */
228
+ const addShape = useCallback((point, tool, size) => {
229
+ shapeCounterRef.current += 1;
230
+ const id = "shape-created-" + shapeCounterRef.current;
231
+ const step = snapStepRef.current;
232
+ setPositions(current => _extends({}, current, {
233
+ [id]: {
234
+ x: snapValue(point.x, step),
235
+ y: snapValue(point.y, step)
236
+ }
237
+ }));
238
+ setShapes(current => _extends({}, current, {
239
+ [id]: defaultShapeState(tool)
240
+ }));
241
+ setCreatedShapes(current => [...current, {
242
+ id,
243
+ kind: "shape",
244
+ initialType: tool,
245
+ x: point.x,
246
+ y: point.y,
247
+ width: size.width,
248
+ height: size.height
249
+ }]);
250
+ setSelectedId(id);
251
+ }, [setPositions, setShapes, setCreatedShapes, setSelectedId]);
252
+
253
+ /* Create a link element. `url` null lands a placeholder (the draw gesture);
254
+ a URL goes straight to the unfurl (the paste gesture, where the address is
255
+ already known). */
256
+ const addLink = useCallback((point, width, url) => {
257
+ linkCounterRef.current += 1;
258
+ const id = "link-" + linkCounterRef.current;
259
+ const step = snapStepRef.current;
260
+ setPositions(current => _extends({}, current, {
261
+ [id]: {
262
+ x: snapValue(point.x, step),
263
+ y: snapValue(point.y, step)
264
+ }
265
+ }));
266
+ setLinks(current => [...current, {
267
+ id,
268
+ width: clampCardWidth(width),
269
+ url
270
+ }]);
271
+ setSelectedId(id);
272
+ setAutoEditId(url == null ? id : null);
273
+ }, [setSelectedId, setPositions, setLinks]);
274
+ const commitLinkUrl = useCallback((id, url) => {
275
+ setLinks(current => current.map(link => link.id === id ? _extends({}, link, {
276
+ url
277
+ }) : link));
278
+ }, [setLinks]);
279
+ const deleteLink = useCallback(id => {
280
+ setLinks(current => current.filter(link => link.id !== id));
281
+ delete resizeStartRef.current[id];
282
+ setSelectedId(null);
283
+ }, [setSelectedId, setLinks]);
284
+
285
+ /* ---- file elements ------------------------------------------------ */
286
+
287
+ const fileCounterRef = useRef(0);
288
+
289
+ /* Create a file element. `file` null lands a placeholder and opens the picker
290
+ (the draw gesture); a file goes straight to the upload (the paste and drop
291
+ gestures, where the bytes are already in hand). */
292
+ const addFile = useCallback((point, width, file) => {
293
+ fileCounterRef.current += 1;
294
+ const id = "file-" + fileCounterRef.current;
295
+ const step = snapStepRef.current;
296
+
297
+ /* `describeFile` allocates an object URL, so it must run HERE and not
298
+ inside the updater: a state updater has to be pure, and a double-invoked
299
+ one would mint a second URL that nothing holds a reference to and that
300
+ therefore could never be revoked. */
301
+ const described = file ? describeFile(file) : null;
302
+ setPositions(current => _extends({}, current, {
303
+ [id]: {
304
+ x: snapValue(point.x, step),
305
+ y: snapValue(point.y, step)
306
+ }
307
+ }));
308
+ setFiles(current => [...current, {
309
+ id,
310
+ width: clampCardWidth(width),
311
+ file: described
312
+ }]);
313
+ setSelectedId(id);
314
+ setAutoOpenId(file == null ? id : null);
315
+ }, [setSelectedId, setPositions, setFiles]);
316
+ const attachFile = useCallback((id, picked) => {
317
+ var _filesRef$current$fin, _filesRef$current$fin2;
318
+ const described = describeFile(picked);
319
+ // Replacing a file releases the one it replaces.
320
+ revokeThumbnail((_filesRef$current$fin = (_filesRef$current$fin2 = filesRef.current.find(entry => entry.id === id)) == null ? void 0 : _filesRef$current$fin2.file) != null ? _filesRef$current$fin : null);
321
+ setFiles(current => current.map(entry => entry.id === id ? _extends({}, entry, {
322
+ file: described
323
+ }) : entry));
324
+ }, [setFiles]);
325
+
326
+ /* Cancelling an upload keeps the card and drops the file, so the placeholder
327
+ is what you land back on — the same place Esc leaves you. */
328
+ const detachFile = useCallback(id => {
329
+ var _filesRef$current$fin3, _filesRef$current$fin4;
330
+ revokeThumbnail((_filesRef$current$fin3 = (_filesRef$current$fin4 = filesRef.current.find(entry => entry.id === id)) == null ? void 0 : _filesRef$current$fin4.file) != null ? _filesRef$current$fin3 : null);
331
+ setFiles(current => current.map(entry => entry.id === id ? _extends({}, entry, {
332
+ file: null
333
+ }) : entry));
334
+ setAutoOpenId(null);
335
+ }, [setFiles]);
336
+ const deleteFile = useCallback(id => {
337
+ var _filesRef$current$fin5, _filesRef$current$fin6;
338
+ revokeThumbnail((_filesRef$current$fin5 = (_filesRef$current$fin6 = filesRef.current.find(entry => entry.id === id)) == null ? void 0 : _filesRef$current$fin6.file) != null ? _filesRef$current$fin5 : null);
339
+ setFiles(current => current.filter(entry => entry.id !== id));
340
+ delete resizeStartRef.current[id];
341
+ setSelectedId(null);
342
+ }, [setSelectedId, setFiles]);
343
+ const resizeFile = useCallback((id, dx) => {
344
+ const from = resizeStartRef.current[id];
345
+ if (from == null) {
346
+ return;
347
+ }
348
+ const width = clampCardWidth(snapValue(from + dx, snapStepRef.current));
349
+ setFiles(all => all.map(entry => entry.id === id ? _extends({}, entry, {
350
+ width
351
+ }) : entry));
352
+ }, [setFiles]);
353
+
354
+ // Object URLs outlive React, so release every one still held on unmount.
355
+ useEffect(() => () => filesRef.current.forEach(entry => revokeThumbnail(entry.file)), []);
356
+
357
+ /* The gesture brackets. `beginResize` snapshots the width the gesture opened
358
+ on; `endResize` is where a real board would push its single undo entry, the
359
+ previews before it being live feedback only (spec §7e). */
360
+ const beginResize = useCallback((id, width) => {
361
+ resizeStartRef.current[id] = width;
362
+ }, []);
363
+ const endResize = useCallback(id => {
364
+ delete resizeStartRef.current[id];
365
+ }, []);
366
+
367
+ /* Width-only resize: the card reports the delta from the gesture start, the
368
+ board decides what it means — clamp to the link bounds, then land on the
369
+ board grid. Resolving against the snapshot rather than the last value makes
370
+ this idempotent, so a double-invoked updater cannot count a delta twice. */
371
+ const resizeLink = useCallback((id, dx) => {
372
+ const from = resizeStartRef.current[id];
373
+ if (from == null) {
374
+ return;
375
+ }
376
+ const width = clampCardWidth(snapValue(from + dx, snapStepRef.current));
377
+ setLinks(all => all.map(link => link.id === id ? _extends({}, link, {
378
+ width
379
+ }) : link));
380
+ }, [setLinks]);
381
+
382
+ /* ---- draw-to-place ------------------------------------------------ */
383
+
384
+ const drawRef = useRef(null);
385
+ /* A completed draw is followed by a click on the canvas layer, whose job is
386
+ to deselect — which would drop the selection off the card just created.
387
+ This swallows exactly that one click. */
388
+ const drawEndedRef = useRef(false);
389
+ const handleDrawMove = useCallback(event => {
390
+ const origin = drawRef.current;
391
+ if (!origin) {
392
+ return;
393
+ }
394
+ const point = canvasPoint(event.clientX, event.clientY);
395
+ setDraft({
396
+ x: Math.min(origin.x, point.x),
397
+ y: Math.min(origin.y, point.y),
398
+ width: Math.abs(point.x - origin.x),
399
+ height: Math.abs(point.y - origin.y),
400
+ tool: origin.tool
401
+ });
402
+ }, [canvasPoint]);
403
+ const handleDrawEnd = useCallback(event => {
404
+ const origin = drawRef.current;
405
+ drawRef.current = null;
406
+ window.removeEventListener("mousemove", handleDrawMove);
407
+ window.removeEventListener("mouseup", handleDrawEnd);
408
+ setDraft(null);
409
+ if (!origin) {
410
+ return;
411
+ }
412
+
413
+ // Swallow the click that follows this mouseup, so the layer's deselect
414
+ // does not drop the selection off the card we are about to create. The
415
+ // click may never arrive (a release over the dock retargets it), so it
416
+ // expires on a timer rather than waiting to be consumed.
417
+ drawEndedRef.current = true;
418
+ window.setTimeout(() => {
419
+ drawEndedRef.current = false;
420
+ }, 0);
421
+ const point = canvasPoint(event.clientX, event.clientY);
422
+ const width = Math.abs(point.x - origin.x);
423
+ const height = Math.abs(point.y - origin.y);
424
+ // A click (or a nudge) means "default element here"; a real drag sets
425
+ // the size.
426
+ const drawn = Math.max(width, height) >= CARD_DRAW_THRESHOLD;
427
+ const at = drawn ? {
428
+ x: Math.min(origin.x, point.x),
429
+ y: Math.min(origin.y, point.y)
430
+ } : origin;
431
+ if (isShapeTool(origin.tool)) {
432
+ addShape(at, origin.tool, drawn ? {
433
+ width,
434
+ height
435
+ } : SHAPE_DEFAULT_SIZES[origin.tool]);
436
+ } else if (origin.tool === "file") {
437
+ addFile(at, drawn ? width : FILE_DEFAULT_WIDTH, null);
438
+ } else {
439
+ addLink(at, drawn ? width : LINK_DEFAULT_WIDTH, null);
440
+ }
441
+ onToolUsed();
442
+ }, [addFile, addLink, addShape, canvasPoint, handleDrawMove, onToolUsed]);
443
+ const handleLayerMouseDown = useCallback(event => {
444
+ const tool = activeToolRef.current;
445
+ if (event.button !== 0 || tool === "select") {
446
+ return;
447
+ }
448
+
449
+ // Without this the draw drag also runs the browser's text selection and
450
+ // highlights everything it sweeps across.
451
+ event.preventDefault();
452
+ const point = canvasPoint(event.clientX, event.clientY);
453
+ // The tool is captured with the gesture: disarming mid-drag must not
454
+ // change what the release creates.
455
+ drawRef.current = _extends({}, point, {
456
+ tool
457
+ });
458
+ setHoverPoint(null);
459
+ setDraft(_extends({}, point, {
460
+ width: 0,
461
+ height: 0,
462
+ tool
463
+ }));
464
+ window.addEventListener("mousemove", handleDrawMove);
465
+ window.addEventListener("mouseup", handleDrawEnd);
466
+ }, [canvasPoint, handleDrawMove, handleDrawEnd]);
467
+
468
+ /* Where the armed tool's hover preview sits — cleared while a draw gesture
469
+ runs (the rubber band takes over) and whenever the tool disarms. */
470
+ const _useState5 = useState(null),
471
+ hoverPoint = _useState5[0],
472
+ setHoverPoint = _useState5[1];
473
+ useEffect(() => {
474
+ if (activeTool === "select") {
475
+ setHoverPoint(null);
476
+ }
477
+ }, [activeTool]);
478
+ const handleLayerMouseMove = useCallback(event => {
479
+ pointerRef.current = canvasPoint(event.clientX, event.clientY);
480
+ if (activeTool !== "select" && !drawRef.current) {
481
+ setHoverPoint(pointerRef.current);
482
+ }
483
+ }, [activeTool, canvasPoint]);
484
+ const handleLayerMouseLeave = useCallback(() => {
485
+ setHoverPoint(null);
486
+ }, []);
487
+ const handleLayerClick = useCallback(() => {
488
+ if (drawEndedRef.current) {
489
+ drawEndedRef.current = false;
490
+ return;
491
+ }
492
+ setSelectedId(null);
493
+ }, [setSelectedId]);
494
+
495
+ /* ---- drop-to-place ------------------------------------------------ */
496
+
497
+ /**
498
+ * The whole board is a drop target, which the dock hint already promises.
499
+ *
500
+ * This also has to exist for safety, not just for the feature: a file dropped
501
+ * anywhere the app does not handle makes the browser NAVIGATE to it, throwing
502
+ * the board away. So the document swallows every file drag, and a drop on the
503
+ * canvas lands a card at the pointer.
504
+ */
505
+ useEffect(() => {
506
+ const isFileDrag = event => {
507
+ var _event$dataTransfer$t, _event$dataTransfer;
508
+ return Array.from((_event$dataTransfer$t = (_event$dataTransfer = event.dataTransfer) == null ? void 0 : _event$dataTransfer.types) != null ? _event$dataTransfer$t : []).includes("Files");
509
+ };
510
+ const handleDragOver = event => {
511
+ if (!isFileDrag(event)) {
512
+ return;
513
+ }
514
+ // Both of these are required: without the dragover default prevented the
515
+ // drop event never fires at all.
516
+ event.preventDefault();
517
+ if (event.dataTransfer) {
518
+ event.dataTransfer.dropEffect = "copy";
519
+ }
520
+ };
521
+ const handleDrop = event => {
522
+ if (!isFileDrag(event)) {
523
+ return;
524
+ }
525
+ event.preventDefault();
526
+ const target = event.target;
527
+ // A drop onto an empty placeholder is that card's own business.
528
+ if (target != null && target.closest("[data-file-dropzone]")) {
529
+ return;
530
+ }
531
+ const dropped = binaryFrom(event.dataTransfer);
532
+ if (!dropped) {
533
+ return;
534
+ }
535
+ addFile(canvasPoint(event.clientX, event.clientY), FILE_DEFAULT_WIDTH, dropped);
536
+ onToolUsed();
537
+ };
538
+ document.addEventListener("dragover", handleDragOver);
539
+ document.addEventListener("drop", handleDrop);
540
+ return () => {
541
+ document.removeEventListener("dragover", handleDragOver);
542
+ document.removeEventListener("drop", handleDrop);
543
+ };
544
+ }, [addFile, canvasPoint, onToolUsed]);
545
+
546
+ /* ---- paste-to-place ----------------------------------------------- */
547
+
548
+ useEffect(() => {
549
+ const handlePaste = event => {
550
+ var _layerRef$current2, _event$clipboardData$, _event$clipboardData;
551
+ /* Only the board may claim a paste, and two different things can take it
552
+ away: a focused field keeps its own paste (the card's URL editor is
553
+ INSIDE the canvas, so "on the board" is not sufficient), and a dialog
554
+ keeps its own too — otherwise pasting with Add Users open would quietly
555
+ drop a card behind the modal. With nothing focused the target is
556
+ <body>, which is the board's own case. */
557
+ const target = event.target;
558
+ if (target != null && target.closest("input, textarea, [contenteditable='true']")) {
559
+ return;
560
+ }
561
+ const onTheBoard = target === document.body || target != null && ((_layerRef$current2 = layerRef.current) == null ? void 0 : _layerRef$current2.contains(target)) === true;
562
+ if (!onTheBoard) {
563
+ return;
564
+ }
565
+
566
+ /* Binary wins over text. A screenshot or a copied file carries both — the
567
+ bytes as an item and often a filename as text — and the bytes are the
568
+ thing the user meant. Only once there is nothing binary do we read the
569
+ clipboard as a URL. */
570
+ const binary = binaryFrom(event.clipboardData);
571
+ if (binary) {
572
+ event.preventDefault();
573
+ // The bytes are in hand, so the card skips the placeholder and the
574
+ // picker: it goes straight to uploading them.
575
+ addFile(pointerRef.current, FILE_DEFAULT_WIDTH, binary);
576
+ onToolUsed();
577
+ return;
578
+ }
579
+ const text = (_event$clipboardData$ = (_event$clipboardData = event.clipboardData) == null ? void 0 : _event$clipboardData.getData("text/plain")) != null ? _event$clipboardData$ : "";
580
+ if (!isValidLinkUrl(text)) {
581
+ return;
582
+ }
583
+ event.preventDefault();
584
+ // The address is already known, so this card skips the placeholder and
585
+ // the editor: it drops straight into the unfurl.
586
+ addLink(pointerRef.current, LINK_DEFAULT_WIDTH, text.trim());
587
+ onToolUsed();
588
+ };
589
+ document.addEventListener("paste", handlePaste);
590
+ return () => document.removeEventListener("paste", handlePaste);
591
+ }, [addFile, addLink, onToolUsed]);
592
+ return /*#__PURE__*/React.createElement(StyledCanvasLayer, {
593
+ ref: layerRef,
594
+ className: activeTool === "select" ? undefined : "drawing",
595
+ onClick: handleLayerClick,
596
+ onMouseDown: handleLayerMouseDown,
597
+ onMouseMove: handleLayerMouseMove,
598
+ onMouseLeave: handleLayerMouseLeave
599
+ }, [...CANVAS_ELEMENTS, ...createdShapes].filter(element => !removedIds.includes(element.id)).map(element => {
600
+ var _sizes$element$id$wid, _sizes$element$id, _sizes$element$id$hei, _sizes$element$id2, _rotations$element$id, _locks$element$id, _locks$element$id2;
601
+ const selected = element.id === selectedId;
602
+ const shape = element.kind === "shape" ? shapes[element.id] : null;
603
+ const width = (_sizes$element$id$wid = (_sizes$element$id = sizes[element.id]) == null ? void 0 : _sizes$element$id.width) != null ? _sizes$element$id$wid : element.width;
604
+ const height = (_sizes$element$id$hei = (_sizes$element$id2 = sizes[element.id]) == null ? void 0 : _sizes$element$id2.height) != null ? _sizes$element$id$hei : element.height;
605
+ const rotation = (_rotations$element$id = rotations[element.id]) != null ? _rotations$element$id : 0;
606
+ return /*#__PURE__*/React.createElement(StyledCanvasElement, {
607
+ key: element.id,
608
+ className: selected ? "selected" : undefined,
609
+ style: {
610
+ left: positions[element.id].x,
611
+ top: positions[element.id].y,
612
+ width,
613
+ height,
614
+ transform: rotation ? "rotate(" + rotation + "deg)" : undefined
615
+ },
616
+ onMouseDown: event => handleElementMouseDown(event, element.id),
617
+ onClick: event => event.stopPropagation()
618
+ }, shape ? renderShapeBody(element, shape, width, height) : null, element.kind === "sticky" ? /*#__PURE__*/React.createElement(StyledSticky, {
619
+ style: {
620
+ background: stickyGradient(stickyStyle.color, stickyStyle.fillOpacity),
621
+ color: stickyStyle.textColor,
622
+ fontSize: stickyStyle.fontSize,
623
+ fontWeight: stickyStyle.bold ? "bold" : "normal",
624
+ fontStyle: stickyStyle.italic ? "italic" : "normal",
625
+ textDecoration: [stickyStyle.underline ? "underline" : "", stickyStyle.strikethrough ? "line-through" : ""].filter(Boolean).join(" ") || undefined,
626
+ textAlign: stickyStyle.align
627
+ }
628
+ }, "Retro: what went well?") : null, selected && shape ? /*#__PURE__*/React.createElement(ShapeFormattingToolbar, {
629
+ element: shape,
630
+ onUpdate: partial => updateShape(element.id, partial),
631
+ onDelete: handleDelete,
632
+ locked: (_locks$element$id = locks[element.id]) != null ? _locks$element$id : false,
633
+ onToggleLock: () => toggleLock(element.id)
634
+ }) : null, selected && element.kind === "sticky" ? /*#__PURE__*/React.createElement(PostitFormattingToolbar, {
635
+ value: stickyStyle,
636
+ onChange: setStickyStyle,
637
+ onDelete: handleDelete,
638
+ locked: (_locks$element$id2 = locks[element.id]) != null ? _locks$element$id2 : false,
639
+ onToggleLock: () => toggleLock(element.id)
640
+ }) : null, selected ? /*#__PURE__*/React.createElement(ResizeHandles, null) : null);
641
+ }), links.map(link => {
642
+ var _positions$link$id$x, _positions$link$id, _positions$link$id$y, _positions$link$id2;
643
+ const selected = link.id === selectedId;
644
+ return /*#__PURE__*/React.createElement(StyledCanvasElement, {
645
+ key: link.id,
646
+ className: selected ? "is-card selected" : "is-card",
647
+ style: {
648
+ left: (_positions$link$id$x = (_positions$link$id = positions[link.id]) == null ? void 0 : _positions$link$id.x) != null ? _positions$link$id$x : 0,
649
+ top: (_positions$link$id$y = (_positions$link$id2 = positions[link.id]) == null ? void 0 : _positions$link$id2.y) != null ? _positions$link$id$y : 0,
650
+ width: link.width
651
+ },
652
+ onMouseDown: event => handleElementMouseDown(event, link.id),
653
+ onClickCapture: handleElementClickCapture,
654
+ onClick: event => event.stopPropagation()
655
+ }, /*#__PURE__*/React.createElement(LinkElement, {
656
+ element: link,
657
+ selected: selected,
658
+ autoEdit: link.id === autoEditId,
659
+ onCommitUrl: url => commitLinkUrl(link.id, url),
660
+ resize: {
661
+ onResizeStart: () => beginResize(link.id, link.width),
662
+ onResizeDelta: dx => resizeLink(link.id, dx),
663
+ onResizeCommit: () => endResize(link.id)
664
+ },
665
+ onDelete: () => deleteLink(link.id)
666
+ }));
667
+ }), files.map(entry => {
668
+ var _positions$entry$id$x, _positions$entry$id, _positions$entry$id$y, _positions$entry$id2;
669
+ const selected = entry.id === selectedId;
670
+ return /*#__PURE__*/React.createElement(StyledCanvasElement, {
671
+ key: entry.id,
672
+ className: selected ? "is-card selected" : "is-card",
673
+ style: {
674
+ left: (_positions$entry$id$x = (_positions$entry$id = positions[entry.id]) == null ? void 0 : _positions$entry$id.x) != null ? _positions$entry$id$x : 0,
675
+ top: (_positions$entry$id$y = (_positions$entry$id2 = positions[entry.id]) == null ? void 0 : _positions$entry$id2.y) != null ? _positions$entry$id$y : 0,
676
+ width: entry.width
677
+ },
678
+ onMouseDown: event => handleElementMouseDown(event, entry.id),
679
+ onClickCapture: handleElementClickCapture,
680
+ onClick: event => event.stopPropagation()
681
+ }, /*#__PURE__*/React.createElement(FileElement, {
682
+ element: entry,
683
+ selected: selected,
684
+ autoOpen: entry.id === autoOpenId,
685
+ onPickFile: picked => attachFile(entry.id, picked),
686
+ resize: {
687
+ onResizeStart: () => beginResize(entry.id, entry.width),
688
+ onResizeDelta: dx => resizeFile(entry.id, dx),
689
+ onResizeCommit: () => endResize(entry.id)
690
+ },
691
+ onCancel: () => detachFile(entry.id),
692
+ onDelete: () => deleteFile(entry.id)
693
+ }));
694
+ }), draft && !isShapeTool(draft.tool) ? /*#__PURE__*/React.createElement(StyledLinkDraft, {
695
+ style: {
696
+ left: draft.x,
697
+ top: draft.y,
698
+ width: Math.max(draft.width, 1),
699
+ height: Math.max(draft.height, 1)
700
+ }
701
+ }, /*#__PURE__*/React.createElement("span", {
702
+ className: "draft-width"
703
+ }, "W", " ", draft.width >= CARD_DRAW_THRESHOLD ? clampCardWidth(draft.width) : draft.tool === "file" ? FILE_DEFAULT_WIDTH : LINK_DEFAULT_WIDTH, " ", "\xB7 height follows content")) : null, draft && isShapeTool(draft.tool) ? /*#__PURE__*/React.createElement(StyledShapePreview, {
704
+ style: {
705
+ left: draft.x,
706
+ top: draft.y,
707
+ width: Math.max(draft.width, 1),
708
+ height: Math.max(draft.height, 1)
709
+ }
710
+ }, shapePreviewGlyph(draft.tool, Math.max(draft.width, 1), Math.max(draft.height, 1))) : null, hoverPoint && !draft && activeTool !== "select" ? (() => {
711
+ const step = snapStepRef.current;
712
+ const at = {
713
+ x: snapValue(hoverPoint.x, step),
714
+ y: snapValue(hoverPoint.y, step)
715
+ };
716
+ if (isShapeTool(activeTool)) {
717
+ const size = SHAPE_DEFAULT_SIZES[activeTool];
718
+ return /*#__PURE__*/React.createElement(StyledShapePreview, {
719
+ style: _extends({
720
+ left: at.x,
721
+ top: at.y
722
+ }, size)
723
+ }, shapePreviewGlyph(activeTool, size.width, size.height));
724
+ }
725
+ const width = activeTool === "file" ? FILE_DEFAULT_WIDTH : LINK_DEFAULT_WIDTH;
726
+ return /*#__PURE__*/React.createElement(StyledShapePreview, {
727
+ style: {
728
+ left: at.x,
729
+ top: at.y,
730
+ width,
731
+ height: CARD_PREVIEW_HEIGHT
732
+ }
733
+ }, /*#__PURE__*/React.createElement("svg", {
734
+ width: "100%",
735
+ height: "100%"
736
+ }, /*#__PURE__*/React.createElement("rect", {
737
+ x: "0",
738
+ y: "0",
739
+ width: "100%",
740
+ height: "100%",
741
+ rx: 8,
742
+ ry: 8
743
+ })));
744
+ })() : null);
745
+ };
746
+ //# sourceMappingURL=canvasElements.js.map