@fieldnotes/react 0.14.0 → 0.14.1

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ React bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes
8
8
  npm install @fieldnotes/core @fieldnotes/react
9
9
  ```
10
10
 
11
- Requires React 18+.
11
+ Requires React 18+. Using Vue or Svelte? See [`@fieldnotes/vue`](../vue) and [`@fieldnotes/svelte`](../svelte).
12
12
 
13
13
  ## Quick Start
14
14
 
package/dist/index.cjs CHANGED
@@ -193,12 +193,11 @@ function CanvasElement({ position, size, children }) {
193
193
  };
194
194
  elementIdRef.current = register();
195
195
  setPortalTarget(container);
196
- let disposed = false;
197
196
  const handleStoreReplacement = () => {
198
197
  const clearedId = elementIdRef.current;
199
198
  if (clearedId === null) return;
200
199
  queueMicrotask(() => {
201
- if (disposed || elementIdRef.current !== clearedId) return;
200
+ if (elementIdRef.current !== clearedId) return;
202
201
  if (viewport.store.getById(clearedId)?.type === "html") {
203
202
  viewport.updateHtmlElement(clearedId, container);
204
203
  } else {
@@ -210,7 +209,6 @@ function CanvasElement({ position, size, children }) {
210
209
  const unsubscribeClear = viewport.store.on("clear", handleStoreReplacement);
211
210
  const unsubscribeBatch = viewport.store.on("batch", handleStoreReplacement);
212
211
  return () => {
213
- disposed = true;
214
212
  unsubscribeClear();
215
213
  unsubscribeBatch();
216
214
  if (elementIdRef.current) {
@@ -235,65 +233,40 @@ function CanvasElement({ position, size, children }) {
235
233
 
236
234
  // src/hooks/use-active-tool.ts
237
235
  var import_react5 = require("react");
236
+ var import_core2 = require("@fieldnotes/core");
238
237
  function useActiveTool() {
239
238
  const viewport = useViewport();
240
- const subscribe = (0, import_react5.useCallback)(
241
- (onStoreChange) => viewport.toolManager.onChange(onStoreChange),
242
- [viewport]
243
- );
244
- const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
245
- const toolName = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot);
239
+ const source = (0, import_react5.useMemo)(() => (0, import_core2.createActiveToolSource)(viewport), [viewport]);
240
+ const toolName = (0, import_react5.useSyncExternalStore)(source.subscribe, source.getSnapshot);
246
241
  const setTool = (0, import_react5.useCallback)((name) => viewport.setTool(name), [viewport]);
247
242
  return [toolName, setTool];
248
243
  }
249
244
 
250
245
  // src/hooks/use-camera.ts
251
246
  var import_react6 = require("react");
247
+ var import_core3 = require("@fieldnotes/core");
252
248
  function useCamera() {
253
249
  const viewport = useViewport();
254
- const cachedRef = (0, import_react6.useRef)({ x: 0, y: 0, zoom: 1 });
255
- const subscribe = (0, import_react6.useCallback)(
256
- (onStoreChange) => viewport.camera.onChange(onStoreChange),
257
- [viewport]
258
- );
259
- const getSnapshot = (0, import_react6.useCallback)(() => {
260
- const { position, zoom } = viewport.camera;
261
- const cached = cachedRef.current;
262
- if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
263
- return cached;
264
- }
265
- const next = { x: position.x, y: position.y, zoom };
266
- cachedRef.current = next;
267
- return next;
268
- }, [viewport]);
269
- return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot);
250
+ const source = (0, import_react6.useMemo)(() => (0, import_core3.createCameraSource)(viewport), [viewport]);
251
+ return (0, import_react6.useSyncExternalStore)(source.subscribe, source.getSnapshot);
270
252
  }
271
253
 
272
254
  // src/hooks/use-tool-options.ts
273
255
  var import_react7 = require("react");
274
- function noop() {
275
- }
256
+ var import_core4 = require("@fieldnotes/core");
276
257
  function useToolOptions(toolName) {
277
258
  const viewport = useViewport();
278
- const cachedRef = (0, import_react7.useRef)(null);
259
+ const nameRef = (0, import_react7.useRef)(toolName);
260
+ nameRef.current = toolName;
261
+ const source = (0, import_react7.useMemo)(
262
+ () => (0, import_core4.createToolOptionsSource)(viewport, () => nameRef.current),
263
+ [viewport]
264
+ );
279
265
  const subscribe = (0, import_react7.useCallback)(
280
- (onStoreChange) => {
281
- const tool = viewport.toolManager.getTool(toolName);
282
- if (!tool?.onOptionsChange) return noop;
283
- return tool.onOptionsChange(onStoreChange);
284
- },
285
- [viewport, toolName]
266
+ (onStoreChange) => source.subscribe(onStoreChange),
267
+ [source, toolName]
286
268
  );
287
- const getSnapshot = (0, import_react7.useCallback)(() => {
288
- const tool = viewport.toolManager.getTool(toolName);
289
- if (!tool?.getOptions) return null;
290
- const next = tool.getOptions();
291
- const cached = cachedRef.current;
292
- if (cached && shallowEqual(cached, next)) return cached;
293
- cachedRef.current = next;
294
- return next;
295
- }, [viewport, toolName]);
296
- const options = (0, import_react7.useSyncExternalStore)(subscribe, getSnapshot);
269
+ const options = (0, import_react7.useSyncExternalStore)(subscribe, source.getSnapshot);
297
270
  const setOptions = (0, import_react7.useCallback)(
298
271
  (partial) => {
299
272
  const tool = viewport.toolManager.getTool(toolName);
@@ -303,51 +276,14 @@ function useToolOptions(toolName) {
303
276
  );
304
277
  return [options, setOptions];
305
278
  }
306
- function shallowEqual(a, b) {
307
- const keysA = Object.keys(a);
308
- const keysB = Object.keys(b);
309
- if (keysA.length !== keysB.length) return false;
310
- for (const key of keysA) {
311
- const valA = a[key];
312
- const valB = b[key];
313
- if (valA === valB) continue;
314
- if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
315
- const objA = valA;
316
- const objB = valB;
317
- const innerKeys = Object.keys(objA);
318
- if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
319
- continue;
320
- }
321
- }
322
- return false;
323
- }
324
- return true;
325
- }
326
279
 
327
280
  // src/hooks/use-layers.ts
328
281
  var import_react8 = require("react");
282
+ var import_core5 = require("@fieldnotes/core");
329
283
  function useLayers() {
330
284
  const viewport = useViewport();
331
- const cachedRef = (0, import_react8.useRef)({ layers: [], activeLayerId: "" });
332
- const subscribe = (0, import_react8.useCallback)(
333
- (onStoreChange) => viewport.layerManager.on("change", onStoreChange),
334
- [viewport]
335
- );
336
- const getSnapshot = (0, import_react8.useCallback)(() => {
337
- const layers = viewport.layerManager.getLayers();
338
- const activeLayerId = viewport.layerManager.activeLayerId;
339
- const cached = cachedRef.current;
340
- if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
341
- const next2 = layers[i];
342
- return next2 !== void 0 && l.id === next2.id && l.name === next2.name && l.visible === next2.visible && l.locked === next2.locked && l.order === next2.order && l.opacity === next2.opacity;
343
- })) {
344
- return cached;
345
- }
346
- const next = { layers, activeLayerId };
347
- cachedRef.current = next;
348
- return next;
349
- }, [viewport]);
350
- const snapshot = (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot);
285
+ const source = (0, import_react8.useMemo)(() => (0, import_core5.createLayersSource)(viewport), [viewport]);
286
+ const snapshot = (0, import_react8.useSyncExternalStore)(source.subscribe, source.getSnapshot);
351
287
  const createLayer = (0, import_react8.useCallback)(
352
288
  (name) => viewport.layerManager.createLayer(name),
353
289
  [viewport]
@@ -401,25 +337,11 @@ function useLayers() {
401
337
 
402
338
  // src/hooks/use-history.ts
403
339
  var import_react9 = require("react");
340
+ var import_core6 = require("@fieldnotes/core");
404
341
  function useHistory() {
405
342
  const viewport = useViewport();
406
- const cachedRef = (0, import_react9.useRef)({ canUndo: false, canRedo: false });
407
- const subscribe = (0, import_react9.useCallback)(
408
- (onStoreChange) => viewport.history.onChange(onStoreChange),
409
- [viewport]
410
- );
411
- const getSnapshot = (0, import_react9.useCallback)(() => {
412
- const canUndo = viewport.history.canUndo;
413
- const canRedo = viewport.history.canRedo;
414
- const cached = cachedRef.current;
415
- if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
416
- return cached;
417
- }
418
- const next = { canUndo, canRedo };
419
- cachedRef.current = next;
420
- return next;
421
- }, [viewport]);
422
- const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot);
343
+ const source = (0, import_react9.useMemo)(() => (0, import_core6.createHistorySource)(viewport), [viewport]);
344
+ const snapshot = (0, import_react9.useSyncExternalStore)(source.subscribe, source.getSnapshot);
423
345
  const undo = (0, import_react9.useCallback)(() => viewport.undo(), [viewport]);
424
346
  const redo = (0, import_react9.useCallback)(() => viewport.redo(), [viewport]);
425
347
  return {
@@ -432,186 +354,69 @@ function useHistory() {
432
354
 
433
355
  // src/hooks/use-elements.ts
434
356
  var import_react10 = require("react");
435
- function shallowEqual2(a, b) {
436
- if (Object.is(a, b)) return true;
437
- if (Array.isArray(a) && Array.isArray(b)) {
438
- return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
439
- }
440
- if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
441
- const ka = Object.keys(a);
442
- const kb = Object.keys(b);
443
- return ka.length === kb.length && ka.every(
444
- (k) => k in b && Object.is(a[k], b[k])
445
- );
446
- }
447
- return false;
448
- }
357
+ var import_core7 = require("@fieldnotes/core");
449
358
  function useElements(arg, isEqual) {
450
359
  const viewport = useViewport();
451
- const cachedRef = (0, import_react10.useRef)([]);
452
- const selectedRef = (0, import_react10.useRef)(null);
453
- const subscribe = (0, import_react10.useCallback)(
454
- (onStoreChange) => viewport.store.onChange(onStoreChange),
360
+ const latest = (0, import_react10.useRef)({ arg, isEqual });
361
+ latest.current = { arg, isEqual };
362
+ const elementsSource = (0, import_react10.useMemo)(
363
+ () => (0, import_core7.createElementsSource)(viewport, () => {
364
+ const a = latest.current.arg;
365
+ return typeof a === "function" ? void 0 : a;
366
+ }),
455
367
  [viewport]
456
368
  );
457
- const getSnapshot = (0, import_react10.useCallback)(() => {
458
- if (typeof arg === "function") {
459
- const next2 = arg(viewport.store.getAll());
460
- const prev = selectedRef.current;
461
- const equal = isEqual ?? shallowEqual2;
462
- if (prev !== null && equal(prev.value, next2)) {
463
- return prev.value;
464
- }
465
- selectedRef.current = { value: next2 };
466
- return next2;
467
- }
468
- const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
469
- const cached = cachedRef.current;
470
- if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
471
- return cached;
472
- }
473
- cachedRef.current = next;
474
- return next;
475
- }, [viewport, arg, isEqual]);
476
- return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
369
+ const selectorSource = (0, import_react10.useMemo)(
370
+ () => (0, import_core7.createElementsSelectorSource)(
371
+ viewport,
372
+ // Only read while `arg` is a selector: the source is picked below by `typeof arg`.
373
+ () => latest.current.arg,
374
+ () => latest.current.isEqual
375
+ ),
376
+ [viewport]
377
+ );
378
+ const source = typeof arg === "function" ? selectorSource : elementsSource;
379
+ return (0, import_react10.useSyncExternalStore)(source.subscribe, source.getSnapshot);
477
380
  }
478
381
 
479
382
  // src/hooks/use-selection.ts
480
383
  var import_react11 = require("react");
384
+ var import_core8 = require("@fieldnotes/core");
481
385
  function useSelection() {
482
386
  const viewport = useViewport();
483
- const subscribe = (0, import_react11.useCallback)(
484
- (onChange) => viewport.onSelectionChange(onChange),
485
- [viewport]
486
- );
487
- const getSnapshot = (0, import_react11.useCallback)(() => viewport.getSelectedIds(), [viewport]);
488
- return (0, import_react11.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
387
+ const source = (0, import_react11.useMemo)(() => (0, import_core8.createSelectionSource)(viewport), [viewport]);
388
+ return (0, import_react11.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
489
389
  }
490
390
 
491
391
  // src/hooks/use-selection-style.ts
492
392
  var import_react12 = require("react");
493
-
494
- // src/hooks/style-keys.ts
495
- var STYLE_KEYS = [
496
- "color",
497
- "fillColor",
498
- "strokeWidth",
499
- "opacity",
500
- "fontSize",
501
- "strokeStyle"
502
- ];
503
-
504
- // src/hooks/use-selection-style.ts
505
- function styleEqual(a, b) {
506
- if (a === b) return true;
507
- if (a === null || b === null) return false;
508
- return STYLE_KEYS.every((k) => a[k] === b[k]);
509
- }
393
+ var import_core9 = require("@fieldnotes/core");
510
394
  function useSelectionStyle() {
511
395
  const viewport = useViewport();
512
- const cacheRef = (0, import_react12.useRef)(null);
513
- const subscribe = (0, import_react12.useCallback)(
514
- (onChange) => {
515
- const offSel = viewport.onSelectionChange(onChange);
516
- const offStore = viewport.store.onChange(onChange);
517
- return () => {
518
- offSel();
519
- offStore();
520
- };
521
- },
522
- [viewport]
523
- );
524
- const getSnapshot = (0, import_react12.useCallback)(() => {
525
- const next = viewport.getSelectionStyle();
526
- if (styleEqual(cacheRef.current, next)) return cacheRef.current;
527
- cacheRef.current = next;
528
- return next;
529
- }, [viewport]);
530
- const style = (0, import_react12.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
396
+ const source = (0, import_react12.useMemo)(() => (0, import_core9.createSelectionStyleSource)(viewport), [viewport]);
397
+ const style = (0, import_react12.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
531
398
  const apply = (0, import_react12.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
532
399
  return [style, apply];
533
400
  }
534
401
 
535
402
  // src/hooks/use-selection-style-details.ts
536
403
  var import_react13 = require("react");
537
- function keysEqual(a, b) {
538
- return a.length === b.length && a.every((k, i) => k === b[i]);
539
- }
540
- function detailsEqual(a, b) {
541
- if (a === b) return true;
542
- if (a === null || b === null) return false;
543
- return keysEqual(a.applicable, b.applicable) && keysEqual(a.mixed, b.mixed) && STYLE_KEYS.every((k) => a.common[k] === b.common[k]);
544
- }
404
+ var import_core10 = require("@fieldnotes/core");
545
405
  function useSelectionStyleDetails() {
546
406
  const viewport = useViewport();
547
- const cacheRef = (0, import_react13.useRef)(null);
548
- const subscribe = (0, import_react13.useCallback)(
549
- (onChange) => {
550
- const offSel = viewport.onSelectionChange(onChange);
551
- const offStore = viewport.store.onChange(onChange);
552
- return () => {
553
- offSel();
554
- offStore();
555
- };
556
- },
557
- [viewport]
558
- );
559
- const getSnapshot = (0, import_react13.useCallback)(() => {
560
- const next = viewport.getSelectionStyleDetails();
561
- if (detailsEqual(cacheRef.current, next)) return cacheRef.current;
562
- cacheRef.current = next;
563
- return next;
564
- }, [viewport]);
565
- const details = (0, import_react13.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
407
+ const source = (0, import_react13.useMemo)(() => (0, import_core10.createSelectionStyleDetailsSource)(viewport), [viewport]);
408
+ const details = (0, import_react13.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
566
409
  const apply = (0, import_react13.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
567
410
  return [details, apply];
568
411
  }
569
412
 
570
413
  // src/hooks/use-selection-ops.ts
571
414
  var import_react14 = require("react");
572
- function snapshotEqual(a, b) {
573
- return a.selectedIds === b.selectedIds && a.canGroup === b.canGroup && a.canUngroup === b.canUngroup && a.canAlign === b.canAlign && a.canDistribute === b.canDistribute && a.isLocked === b.isLocked;
574
- }
415
+ var import_core11 = require("@fieldnotes/core");
575
416
  function useSelectionOps() {
576
417
  const viewport = useViewport();
577
- const cacheRef = (0, import_react14.useRef)(null);
578
- const subscribe = (0, import_react14.useCallback)(
579
- (onChange) => {
580
- const offSel = viewport.onSelectionChange(onChange);
581
- const offStore = viewport.store.onChange(onChange);
582
- return () => {
583
- offSel();
584
- offStore();
585
- };
586
- },
587
- [viewport]
588
- );
589
- const getSnapshot = (0, import_react14.useCallback)(() => {
590
- const ids = viewport.getSelectedIds();
591
- const n = ids.length;
592
- let canUngroup = false;
593
- let lockedCount = 0;
594
- for (const id of ids) {
595
- const el = viewport.store.getById(id);
596
- if (!el) continue;
597
- if (el.groupId !== void 0) canUngroup = true;
598
- if (el.locked) lockedCount++;
599
- }
600
- const isLocked = n === 0 ? null : lockedCount === n ? true : lockedCount === 0 ? false : null;
601
- const next = {
602
- selectedIds: ids,
603
- canGroup: n >= 2,
604
- canUngroup,
605
- canAlign: n >= 2,
606
- canDistribute: n >= 3,
607
- isLocked
608
- };
609
- const prev = cacheRef.current;
610
- if (prev && snapshotEqual(prev, next)) return prev;
611
- cacheRef.current = next;
612
- return next;
613
- }, [viewport]);
614
- const snap = (0, import_react14.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
418
+ const source = (0, import_react14.useMemo)(() => (0, import_core11.createSelectionOpsSource)(viewport), [viewport]);
419
+ const snap = (0, import_react14.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
615
420
  const group = (0, import_react14.useCallback)(() => viewport.groupSelection(), [viewport]);
616
421
  const ungroup = (0, import_react14.useCallback)(() => viewport.ungroupSelection(), [viewport]);
617
422
  const toggleLock = (0, import_react14.useCallback)(() => viewport.toggleLockSelection(), [viewport]);
@@ -642,7 +447,7 @@ function useSelectionOps() {
642
447
 
643
448
  // src/hooks/use-element-rects.ts
644
449
  var import_react15 = require("react");
645
- var import_core2 = require("@fieldnotes/core");
450
+ var import_core12 = require("@fieldnotes/core");
646
451
  function useElementRects(match) {
647
452
  const viewport = useViewport();
648
453
  const matchRef = (0, import_react15.useRef)(match);
@@ -652,11 +457,11 @@ function useElementRects(match) {
652
457
  const trackerRef = (0, import_react15.useRef)(null);
653
458
  const subscribe = (0, import_react15.useCallback)(
654
459
  (onStoreChange) => {
655
- const tracker = new import_core2.ElementRectTracker(viewport, { match: stableMatch });
460
+ const tracker = new import_core12.ElementRectTracker(viewport, { match: stableMatch });
656
461
  trackerRef.current = tracker;
657
462
  const next = tracker.getRects();
658
463
  const previous = cacheRef.current;
659
- if (previous === null || !(0, import_core2.elementRectsEqual)(previous, next)) {
464
+ if (previous === null || !(0, import_core12.elementRectsEqual)(previous, next)) {
660
465
  cacheRef.current = next;
661
466
  onStoreChange();
662
467
  }
@@ -674,7 +479,7 @@ function useElementRects(match) {
674
479
  );
675
480
  const getSnapshot = (0, import_react15.useCallback)(() => {
676
481
  if (cacheRef.current === null) {
677
- cacheRef.current = (0, import_core2.computeElementRects)(viewport.store, stableMatch);
482
+ cacheRef.current = (0, import_core12.computeElementRects)(viewport.store, stableMatch);
678
483
  }
679
484
  return cacheRef.current;
680
485
  }, [viewport, stableMatch]);
@@ -687,7 +492,7 @@ function useElementRects(match) {
687
492
 
688
493
  // src/minimap.tsx
689
494
  var import_react16 = require("react");
690
- var import_core3 = require("@fieldnotes/core");
495
+ var import_core13 = require("@fieldnotes/core");
691
496
  var import_jsx_runtime2 = require("react/jsx-runtime");
692
497
  var toggleStyle = {
693
498
  position: "absolute",
@@ -719,7 +524,7 @@ function Minimap({
719
524
  if (collapsed) return;
720
525
  const canvas = canvasRef.current;
721
526
  if (!canvas) return;
722
- const controller = new import_core3.MinimapController(viewport, canvas, {
527
+ const controller = new import_core13.MinimapController(viewport, canvas, {
723
528
  width,
724
529
  height,
725
530
  background,