@fieldnotes/core 0.17.0 → 0.19.0

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/dist/index.d.cts CHANGED
@@ -411,12 +411,14 @@ declare class HistoryRecorder {
411
411
  private readonly layerManager?;
412
412
  private recording;
413
413
  private transaction;
414
+ private generation;
414
415
  private updateSnapshots;
415
416
  private unsubscribers;
416
417
  constructor(store: ElementStore, stack: HistoryStack, layerManager?: LayerManager | undefined);
417
418
  pause(): void;
418
419
  resume(): void;
419
420
  begin(): void;
421
+ get currentTransactionId(): number | null;
420
422
  commit(): void;
421
423
  rollback(): void;
422
424
  destroy(): void;
@@ -430,12 +432,25 @@ declare class HistoryRecorder {
430
432
  private flushUpdateSnapshots;
431
433
  }
432
434
 
435
+ type ShortcutBindings = Record<string, string | string[] | null>;
436
+ interface ShortcutOptions {
437
+ scope?: 'focus' | 'window';
438
+ bindings?: ShortcutBindings;
439
+ }
440
+ interface ShortcutsApi {
441
+ rebind(action: string, bindings: string | string[] | null): void;
442
+ disable(action: string): void;
443
+ reset(action?: string): void;
444
+ getBindings(): Record<string, string[]>;
445
+ }
446
+
433
447
  interface InputHandlerOptions {
434
448
  toolManager?: ToolManager;
435
449
  toolContext?: ToolContext;
436
450
  historyRecorder?: HistoryRecorder;
437
451
  historyStack?: HistoryStack;
438
452
  fitToContent?: () => void;
453
+ shortcuts?: ShortcutOptions;
439
454
  }
440
455
  declare class InputHandler {
441
456
  private readonly element;
@@ -456,9 +471,12 @@ declare class InputHandler {
456
471
  private deferredDown;
457
472
  private readonly abortController;
458
473
  private readonly actions;
474
+ private readonly shortcutMap;
475
+ private readonly scope;
459
476
  constructor(element: HTMLElement, camera: Camera, options?: InputHandlerOptions);
460
477
  setToolManager(toolManager: ToolManager, toolContext: ToolContext): void;
461
478
  flushPendingHistory(): void;
479
+ get shortcuts(): ShortcutsApi;
462
480
  destroy(): void;
463
481
  private bind;
464
482
  private onWheel;
@@ -467,6 +485,7 @@ declare class InputHandler {
467
485
  private onPointerUp;
468
486
  private onKeyDown;
469
487
  private onKeyUp;
488
+ private runAction;
470
489
  private startPinch;
471
490
  private handlePinchMove;
472
491
  private getPinchPoints;
@@ -477,6 +496,8 @@ declare class InputHandler {
477
496
  private dispatchToolMove;
478
497
  private dispatchToolHover;
479
498
  private dispatchToolUp;
499
+ private isInScope;
500
+ private focusSelf;
480
501
  private cancelToolIfActive;
481
502
  }
482
503
 
@@ -567,6 +588,7 @@ interface ViewportOptions {
567
588
  background?: BackgroundOptions;
568
589
  fontSizePresets?: FontSizePreset[];
569
590
  toolbar?: boolean;
591
+ shortcuts?: ShortcutOptions;
570
592
  onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
571
593
  onDrop?: (event: DragEvent, worldPosition: {
572
594
  x: number;
@@ -614,6 +636,8 @@ declare class Viewport {
614
636
  exportImage(options?: ExportImageOptions): Promise<Blob | null>;
615
637
  loadState(state: CanvasState): void;
616
638
  loadJSON(json: string): void;
639
+ setTool(name: string): void;
640
+ get shortcuts(): ShortcutsApi;
617
641
  undo(): boolean;
618
642
  redo(): boolean;
619
643
  addImage(src: string, position: {
@@ -1209,6 +1233,6 @@ declare class UpdateLayerCommand implements Command {
1209
1233
  undo(_store: ElementStore): void;
1210
1234
  }
1211
1235
 
1212
- declare const VERSION = "0.17.0";
1236
+ declare const VERSION = "0.19.0";
1213
1237
 
1214
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1238
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -411,12 +411,14 @@ declare class HistoryRecorder {
411
411
  private readonly layerManager?;
412
412
  private recording;
413
413
  private transaction;
414
+ private generation;
414
415
  private updateSnapshots;
415
416
  private unsubscribers;
416
417
  constructor(store: ElementStore, stack: HistoryStack, layerManager?: LayerManager | undefined);
417
418
  pause(): void;
418
419
  resume(): void;
419
420
  begin(): void;
421
+ get currentTransactionId(): number | null;
420
422
  commit(): void;
421
423
  rollback(): void;
422
424
  destroy(): void;
@@ -430,12 +432,25 @@ declare class HistoryRecorder {
430
432
  private flushUpdateSnapshots;
431
433
  }
432
434
 
435
+ type ShortcutBindings = Record<string, string | string[] | null>;
436
+ interface ShortcutOptions {
437
+ scope?: 'focus' | 'window';
438
+ bindings?: ShortcutBindings;
439
+ }
440
+ interface ShortcutsApi {
441
+ rebind(action: string, bindings: string | string[] | null): void;
442
+ disable(action: string): void;
443
+ reset(action?: string): void;
444
+ getBindings(): Record<string, string[]>;
445
+ }
446
+
433
447
  interface InputHandlerOptions {
434
448
  toolManager?: ToolManager;
435
449
  toolContext?: ToolContext;
436
450
  historyRecorder?: HistoryRecorder;
437
451
  historyStack?: HistoryStack;
438
452
  fitToContent?: () => void;
453
+ shortcuts?: ShortcutOptions;
439
454
  }
440
455
  declare class InputHandler {
441
456
  private readonly element;
@@ -456,9 +471,12 @@ declare class InputHandler {
456
471
  private deferredDown;
457
472
  private readonly abortController;
458
473
  private readonly actions;
474
+ private readonly shortcutMap;
475
+ private readonly scope;
459
476
  constructor(element: HTMLElement, camera: Camera, options?: InputHandlerOptions);
460
477
  setToolManager(toolManager: ToolManager, toolContext: ToolContext): void;
461
478
  flushPendingHistory(): void;
479
+ get shortcuts(): ShortcutsApi;
462
480
  destroy(): void;
463
481
  private bind;
464
482
  private onWheel;
@@ -467,6 +485,7 @@ declare class InputHandler {
467
485
  private onPointerUp;
468
486
  private onKeyDown;
469
487
  private onKeyUp;
488
+ private runAction;
470
489
  private startPinch;
471
490
  private handlePinchMove;
472
491
  private getPinchPoints;
@@ -477,6 +496,8 @@ declare class InputHandler {
477
496
  private dispatchToolMove;
478
497
  private dispatchToolHover;
479
498
  private dispatchToolUp;
499
+ private isInScope;
500
+ private focusSelf;
480
501
  private cancelToolIfActive;
481
502
  }
482
503
 
@@ -567,6 +588,7 @@ interface ViewportOptions {
567
588
  background?: BackgroundOptions;
568
589
  fontSizePresets?: FontSizePreset[];
569
590
  toolbar?: boolean;
591
+ shortcuts?: ShortcutOptions;
570
592
  onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
571
593
  onDrop?: (event: DragEvent, worldPosition: {
572
594
  x: number;
@@ -614,6 +636,8 @@ declare class Viewport {
614
636
  exportImage(options?: ExportImageOptions): Promise<Blob | null>;
615
637
  loadState(state: CanvasState): void;
616
638
  loadJSON(json: string): void;
639
+ setTool(name: string): void;
640
+ get shortcuts(): ShortcutsApi;
617
641
  undo(): boolean;
618
642
  redo(): boolean;
619
643
  addImage(src: string, position: {
@@ -1209,6 +1233,6 @@ declare class UpdateLayerCommand implements Command {
1209
1233
  undo(_store: ElementStore): void;
1210
1234
  }
1211
1235
 
1212
- declare const VERSION = "0.17.0";
1236
+ declare const VERSION = "0.19.0";
1213
1237
 
1214
- export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
1238
+ export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -15,7 +15,13 @@ var EventBus = class {
15
15
  this.listeners.get(event)?.delete(listener);
16
16
  }
17
17
  emit(event, data) {
18
- this.listeners.get(event)?.forEach((listener) => listener(data));
18
+ this.listeners.get(event)?.forEach((listener) => {
19
+ try {
20
+ listener(data);
21
+ } catch (err) {
22
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
23
+ }
24
+ });
19
25
  }
20
26
  clear() {
21
27
  this.listeners.clear();
@@ -819,6 +825,7 @@ var KeyboardActions = class {
819
825
  clipboard = [];
820
826
  pasteCount = 0;
821
827
  nudgeTimer = null;
828
+ nudgeTxId = null;
822
829
  dispose() {
823
830
  this.flushPendingNudge();
824
831
  }
@@ -837,7 +844,9 @@ var KeyboardActions = class {
837
844
  if (sel.tool.selectedIds.length === 0) return false;
838
845
  const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
839
846
  if (this.nudgeTimer === null) {
840
- this.deps.getHistoryRecorder()?.begin();
847
+ const recorder = this.deps.getHistoryRecorder();
848
+ recorder?.begin();
849
+ this.nudgeTxId = recorder?.currentTransactionId ?? null;
841
850
  } else {
842
851
  clearTimeout(this.nudgeTimer);
843
852
  }
@@ -849,9 +858,14 @@ var KeyboardActions = class {
849
858
  if (this.nudgeTimer === null) return;
850
859
  clearTimeout(this.nudgeTimer);
851
860
  this.nudgeTimer = null;
852
- this.deps.getHistoryRecorder()?.commit();
861
+ const recorder = this.deps.getHistoryRecorder();
862
+ if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
863
+ recorder?.commit();
864
+ }
865
+ this.nudgeTxId = null;
853
866
  }
854
867
  deleteSelected() {
868
+ if (this.deps.isToolActive()) return;
855
869
  this.flushPendingNudge();
856
870
  const sel = this.selectTool();
857
871
  if (!sel) return;
@@ -866,6 +880,7 @@ var KeyboardActions = class {
866
880
  sel.ctx.requestRender();
867
881
  }
868
882
  undo() {
883
+ if (this.deps.isToolActive()) return;
869
884
  this.flushPendingNudge();
870
885
  const ctx = this.deps.getToolContext();
871
886
  const stack = this.deps.getHistoryStack();
@@ -877,6 +892,7 @@ var KeyboardActions = class {
877
892
  ctx.requestRender();
878
893
  }
879
894
  redo() {
895
+ if (this.deps.isToolActive()) return;
880
896
  this.flushPendingNudge();
881
897
  const ctx = this.deps.getToolContext();
882
898
  const stack = this.deps.getHistoryStack();
@@ -950,6 +966,7 @@ var KeyboardActions = class {
950
966
  this.deps.fitToContent?.();
951
967
  }
952
968
  zOrder(operation) {
969
+ if (this.deps.isToolActive()) return;
953
970
  this.flushPendingNudge();
954
971
  const sel = this.selectTool();
955
972
  if (!sel) return;
@@ -1024,14 +1041,158 @@ var KeyboardActions = class {
1024
1041
  }
1025
1042
  };
1026
1043
 
1044
+ // src/canvas/shortcut-map.ts
1045
+ var DEFAULT_BINDINGS = [
1046
+ ["delete", ["delete", "backspace"]],
1047
+ ["deselect", ["escape"]],
1048
+ ["undo", ["mod+z"]],
1049
+ ["redo", ["mod+y", "mod+shift+z"]],
1050
+ ["select-all", ["mod+a"]],
1051
+ ["copy", ["mod+c"]],
1052
+ ["paste", ["mod+v"]],
1053
+ ["duplicate", ["mod+d"]],
1054
+ ["z-forward", ["]"]],
1055
+ ["z-backward", ["["]],
1056
+ ["z-front", ["mod+]"]],
1057
+ ["z-back", ["mod+["]],
1058
+ ["zoom-fit", ["shift+1"]],
1059
+ ["nudge-left", ["arrowleft"]],
1060
+ ["nudge-right", ["arrowright"]],
1061
+ ["nudge-up", ["arrowup"]],
1062
+ ["nudge-down", ["arrowdown"]],
1063
+ ["tool:select", ["v"]],
1064
+ ["tool:hand", ["h"]],
1065
+ ["tool:pencil", ["p"]],
1066
+ ["tool:eraser", ["e"]],
1067
+ ["tool:arrow", ["a"]],
1068
+ ["tool:note", ["n"]],
1069
+ ["tool:text", ["t"]],
1070
+ ["tool:shape", ["s"]],
1071
+ ["tool:measure", ["m"]],
1072
+ ["tool:template", ["g"]]
1073
+ ];
1074
+ var ALLOW_SHIFT = /* @__PURE__ */ new Set(["nudge-left", "nudge-right", "nudge-up", "nudge-down"]);
1075
+ var MODIFIERS = /* @__PURE__ */ new Set(["mod", "ctrl", "meta", "shift", "alt"]);
1076
+ function parseBinding(binding) {
1077
+ const parts = binding.toLowerCase().split("+");
1078
+ const key = parts.pop();
1079
+ if (key === void 0 || key.length === 0 || MODIFIERS.has(key)) {
1080
+ throw new Error(`Invalid shortcut binding "${binding}": missing key`);
1081
+ }
1082
+ const normalizedKey = key === "space" ? " " : key;
1083
+ const parsed = {
1084
+ mod: false,
1085
+ ctrl: false,
1086
+ meta: false,
1087
+ shift: false,
1088
+ alt: false,
1089
+ key: normalizedKey,
1090
+ digit: /^[0-9]$/.test(normalizedKey)
1091
+ };
1092
+ for (const part of parts) {
1093
+ switch (part) {
1094
+ case "mod":
1095
+ parsed.mod = true;
1096
+ break;
1097
+ case "ctrl":
1098
+ parsed.ctrl = true;
1099
+ break;
1100
+ case "meta":
1101
+ parsed.meta = true;
1102
+ break;
1103
+ case "shift":
1104
+ parsed.shift = true;
1105
+ break;
1106
+ case "alt":
1107
+ parsed.alt = true;
1108
+ break;
1109
+ default:
1110
+ throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
1111
+ }
1112
+ }
1113
+ return parsed;
1114
+ }
1115
+ function bindingMatches(p, e, allowShift) {
1116
+ if (p.mod) {
1117
+ if (!e.ctrlKey && !e.metaKey) return false;
1118
+ } else if (e.ctrlKey !== p.ctrl || e.metaKey !== p.meta) {
1119
+ return false;
1120
+ }
1121
+ if (!allowShift && e.shiftKey !== p.shift) return false;
1122
+ if (e.altKey !== p.alt) return false;
1123
+ return p.digit ? e.code === `Digit${p.key}` : e.key.toLowerCase() === p.key;
1124
+ }
1125
+ function toArray(bindings) {
1126
+ if (bindings === null) return [];
1127
+ return Array.isArray(bindings) ? [...bindings] : [bindings];
1128
+ }
1129
+ var ShortcutMap = class {
1130
+ raw = /* @__PURE__ */ new Map();
1131
+ parsed = /* @__PURE__ */ new Map();
1132
+ constructor(overrides) {
1133
+ this.applyDefaults();
1134
+ if (overrides) {
1135
+ for (const [action, bindings] of Object.entries(overrides)) {
1136
+ this.rebind(action, bindings);
1137
+ }
1138
+ }
1139
+ }
1140
+ /** First matching action in registration order wins when bindings conflict. */
1141
+ match(e) {
1142
+ for (const [action, parsedList] of this.parsed) {
1143
+ const allowShift = ALLOW_SHIFT.has(action);
1144
+ for (const p of parsedList) {
1145
+ if (bindingMatches(p, e, allowShift)) return action;
1146
+ }
1147
+ }
1148
+ return null;
1149
+ }
1150
+ rebind(action, bindings) {
1151
+ const list = toArray(bindings);
1152
+ const parsedList = list.map(parseBinding);
1153
+ this.raw.set(action, list);
1154
+ this.parsed.set(action, parsedList);
1155
+ }
1156
+ disable(action) {
1157
+ this.rebind(action, null);
1158
+ }
1159
+ reset(action) {
1160
+ if (action === void 0) {
1161
+ this.raw.clear();
1162
+ this.parsed.clear();
1163
+ this.applyDefaults();
1164
+ return;
1165
+ }
1166
+ const def = DEFAULT_BINDINGS.find(([name]) => name === action);
1167
+ if (def) {
1168
+ this.rebind(action, [...def[1]]);
1169
+ } else if (this.raw.has(action)) {
1170
+ this.raw.delete(action);
1171
+ this.parsed.delete(action);
1172
+ }
1173
+ }
1174
+ getBindings() {
1175
+ const out = {};
1176
+ for (const [action, list] of this.raw) {
1177
+ out[action] = [...list];
1178
+ }
1179
+ return out;
1180
+ }
1181
+ applyDefaults() {
1182
+ for (const [action, bindings] of DEFAULT_BINDINGS) {
1183
+ this.rebind(action, [...bindings]);
1184
+ }
1185
+ }
1186
+ };
1187
+
1027
1188
  // src/canvas/input-handler.ts
1028
1189
  var ZOOM_SENSITIVITY = 1e-3;
1029
1190
  var MIDDLE_BUTTON = 1;
1030
- var NUDGE_KEYS = {
1031
- ArrowLeft: [-1, 0],
1032
- ArrowRight: [1, 0],
1033
- ArrowUp: [0, -1],
1034
- ArrowDown: [0, 1]
1191
+ var NUDGE_DELTAS = {
1192
+ "nudge-left": [-1, 0],
1193
+ "nudge-right": [1, 0],
1194
+ "nudge-up": [0, -1],
1195
+ "nudge-down": [0, 1]
1035
1196
  };
1036
1197
  var InputHandler = class {
1037
1198
  constructor(element, camera, options = {}) {
@@ -1049,7 +1210,13 @@ var InputHandler = class {
1049
1210
  isToolActive: () => this.isToolActive,
1050
1211
  fitToContent: options.fitToContent
1051
1212
  });
1213
+ this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1214
+ this.scope = options.shortcuts?.scope ?? "focus";
1052
1215
  this.element.style.touchAction = "none";
1216
+ if (this.scope === "focus") {
1217
+ this.element.tabIndex = 0;
1218
+ this.element.style.outline = "none";
1219
+ }
1053
1220
  this.bind();
1054
1221
  }
1055
1222
  isPanning = false;
@@ -1068,6 +1235,8 @@ var InputHandler = class {
1068
1235
  deferredDown = null;
1069
1236
  abortController = new AbortController();
1070
1237
  actions;
1238
+ shortcutMap;
1239
+ scope;
1071
1240
  setToolManager(toolManager, toolContext) {
1072
1241
  this.toolManager = toolManager;
1073
1242
  this.toolContext = toolContext;
@@ -1075,6 +1244,9 @@ var InputHandler = class {
1075
1244
  flushPendingHistory() {
1076
1245
  this.actions.flushPendingNudge();
1077
1246
  }
1247
+ get shortcuts() {
1248
+ return this.shortcutMap;
1249
+ }
1078
1250
  destroy() {
1079
1251
  this.actions.dispose();
1080
1252
  this.abortController.abort();
@@ -1104,6 +1276,7 @@ var InputHandler = class {
1104
1276
  });
1105
1277
  };
1106
1278
  onPointerDown = (e) => {
1279
+ this.focusSelf();
1107
1280
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1108
1281
  this.element.setPointerCapture?.(e.pointerId);
1109
1282
  if (this.activePointers.size === 2) {
@@ -1186,57 +1359,13 @@ var InputHandler = class {
1186
1359
  if (target?.isContentEditable) return;
1187
1360
  const tag = target?.tagName;
1188
1361
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1362
+ if (!this.isInScope()) return;
1189
1363
  if (e.key === " ") {
1190
1364
  this.spaceHeld = true;
1191
1365
  }
1192
- if (e.key === "Delete" || e.key === "Backspace") {
1193
- this.actions.deleteSelected();
1194
- }
1195
- if (e.key === "Escape") {
1196
- this.actions.deselect();
1197
- }
1198
- if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1199
- e.preventDefault();
1200
- this.actions.undo();
1201
- }
1202
- if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
1203
- e.preventDefault();
1204
- this.actions.redo();
1205
- }
1206
- if ((e.ctrlKey || e.metaKey) && e.key === "a") {
1207
- e.preventDefault();
1208
- this.actions.selectAll();
1209
- }
1210
- if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1211
- e.preventDefault();
1212
- this.actions.copy();
1213
- }
1214
- if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1215
- e.preventDefault();
1216
- this.actions.paste();
1217
- }
1218
- if ((e.ctrlKey || e.metaKey) && e.key === "d") {
1219
- e.preventDefault();
1220
- this.actions.duplicate();
1221
- }
1222
- if (e.key === "]") {
1223
- e.preventDefault();
1224
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1225
- }
1226
- if (e.key === "[") {
1227
- e.preventDefault();
1228
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1229
- }
1230
- if (e.shiftKey && e.code === "Digit1" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1231
- e.preventDefault();
1232
- this.actions.zoomToFit();
1233
- }
1234
- const nudgeDelta = NUDGE_KEYS[e.key];
1235
- if (nudgeDelta) {
1236
- const [dx, dy] = nudgeDelta;
1237
- if (this.actions.nudge(dx, dy, e.shiftKey)) {
1238
- e.preventDefault();
1239
- }
1366
+ const action = this.shortcutMap.match(e);
1367
+ if (action !== null) {
1368
+ this.runAction(action, e);
1240
1369
  }
1241
1370
  };
1242
1371
  onKeyUp = (e) => {
@@ -1251,6 +1380,79 @@ var InputHandler = class {
1251
1380
  }
1252
1381
  }
1253
1382
  };
1383
+ runAction(action, e) {
1384
+ switch (action) {
1385
+ case "delete":
1386
+ e.preventDefault();
1387
+ this.actions.deleteSelected();
1388
+ return;
1389
+ case "deselect":
1390
+ this.actions.deselect();
1391
+ return;
1392
+ case "undo":
1393
+ e.preventDefault();
1394
+ this.actions.undo();
1395
+ return;
1396
+ case "redo":
1397
+ e.preventDefault();
1398
+ this.actions.redo();
1399
+ return;
1400
+ case "select-all":
1401
+ e.preventDefault();
1402
+ this.actions.selectAll();
1403
+ return;
1404
+ case "copy":
1405
+ e.preventDefault();
1406
+ this.actions.copy();
1407
+ return;
1408
+ case "paste":
1409
+ e.preventDefault();
1410
+ this.actions.paste();
1411
+ return;
1412
+ case "duplicate":
1413
+ e.preventDefault();
1414
+ this.actions.duplicate();
1415
+ return;
1416
+ case "z-forward":
1417
+ e.preventDefault();
1418
+ this.actions.zOrder("forward");
1419
+ return;
1420
+ case "z-backward":
1421
+ e.preventDefault();
1422
+ this.actions.zOrder("backward");
1423
+ return;
1424
+ case "z-front":
1425
+ e.preventDefault();
1426
+ this.actions.zOrder("front");
1427
+ return;
1428
+ case "z-back":
1429
+ e.preventDefault();
1430
+ this.actions.zOrder("back");
1431
+ return;
1432
+ case "zoom-fit":
1433
+ e.preventDefault();
1434
+ this.actions.zoomToFit();
1435
+ return;
1436
+ case "nudge-left":
1437
+ case "nudge-right":
1438
+ case "nudge-up":
1439
+ case "nudge-down": {
1440
+ const delta = NUDGE_DELTAS[action];
1441
+ if (delta && this.actions.nudge(delta[0], delta[1], e.shiftKey)) {
1442
+ e.preventDefault();
1443
+ }
1444
+ return;
1445
+ }
1446
+ default:
1447
+ if (action.startsWith("tool:")) {
1448
+ if (this.isToolActive) return;
1449
+ e.preventDefault();
1450
+ this.toolContext?.switchTool?.(action.slice("tool:".length));
1451
+ return;
1452
+ }
1453
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1454
+ }
1455
+ }
1254
1456
  startPinch() {
1255
1457
  this.inputFilter.reset();
1256
1458
  this.deferredDown = null;
@@ -1321,6 +1523,15 @@ var InputHandler = class {
1321
1523
  this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1322
1524
  this.historyRecorder?.commit();
1323
1525
  }
1526
+ isInScope() {
1527
+ if (this.scope === "window") return true;
1528
+ const active = document.activeElement;
1529
+ return active === this.element || this.element.contains(active);
1530
+ }
1531
+ focusSelf() {
1532
+ if (this.scope !== "focus" || this.isInScope()) return;
1533
+ this.element.focus({ preventScroll: true });
1534
+ }
1324
1535
  cancelToolIfActive(e) {
1325
1536
  if (this.isToolActive) {
1326
1537
  this.dispatchToolUp(e);
@@ -3509,6 +3720,7 @@ var HistoryRecorder = class {
3509
3720
  }
3510
3721
  recording = true;
3511
3722
  transaction = null;
3723
+ generation = 0;
3512
3724
  updateSnapshots = /* @__PURE__ */ new Map();
3513
3725
  unsubscribers;
3514
3726
  pause() {
@@ -3523,6 +3735,10 @@ var HistoryRecorder = class {
3523
3735
  }
3524
3736
  this.transaction = [];
3525
3737
  this.updateSnapshots.clear();
3738
+ this.generation += 1;
3739
+ }
3740
+ get currentTransactionId() {
3741
+ return this.transaction !== null ? this.generation : null;
3526
3742
  }
3527
3743
  commit() {
3528
3744
  if (!this.transaction) return;
@@ -4710,7 +4926,8 @@ var Viewport = class {
4710
4926
  toolContext: this.toolContext,
4711
4927
  historyRecorder: this.historyRecorder,
4712
4928
  historyStack: this.history,
4713
- fitToContent: () => this.fitToContent()
4929
+ fitToContent: () => this.fitToContent(),
4930
+ shortcuts: options.shortcuts
4714
4931
  });
4715
4932
  this.domNodeManager = new DomNodeManager({
4716
4933
  domLayer: this.domLayer,
@@ -4881,6 +5098,12 @@ var Viewport = class {
4881
5098
  loadJSON(json) {
4882
5099
  this.loadState(parseState(json));
4883
5100
  }
5101
+ setTool(name) {
5102
+ this.toolManager.setTool(name, this.toolContext);
5103
+ }
5104
+ get shortcuts() {
5105
+ return this.inputHandler.shortcuts;
5106
+ }
4884
5107
  undo() {
4885
5108
  this.inputHandler.flushPendingHistory();
4886
5109
  this.historyRecorder.pause();
@@ -6919,7 +7142,7 @@ var TemplateTool = class {
6919
7142
  };
6920
7143
 
6921
7144
  // src/index.ts
6922
- var VERSION = "0.17.0";
7145
+ var VERSION = "0.19.0";
6923
7146
  export {
6924
7147
  AddElementCommand,
6925
7148
  ArrowTool,
@@ -7006,3 +7229,4 @@ export {
7006
7229
  unbindArrow,
7007
7230
  updateBoundArrow
7008
7231
  };
7232
+ //# sourceMappingURL=index.js.map