@fieldnotes/core 0.11.2 → 0.12.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.cjs CHANGED
@@ -898,6 +898,12 @@ var InputFilter = class _InputFilter {
898
898
  }
899
899
  };
900
900
 
901
+ // src/elements/create-id.ts
902
+ var counter = 0;
903
+ function createId(prefix) {
904
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
905
+ }
906
+
901
907
  // src/canvas/input-handler.ts
902
908
  var ZOOM_SENSITIVITY = 1e-3;
903
909
  var MIDDLE_BUTTON = 1;
@@ -927,6 +933,8 @@ var InputHandler = class {
927
933
  inputFilter = new InputFilter();
928
934
  deferredDown = null;
929
935
  abortController = new AbortController();
936
+ clipboard = [];
937
+ pasteCount = 0;
930
938
  setToolManager(toolManager, toolContext) {
931
939
  this.toolManager = toolManager;
932
940
  this.toolContext = toolContext;
@@ -1050,6 +1058,14 @@ var InputHandler = class {
1050
1058
  e.preventDefault();
1051
1059
  this.handleRedo();
1052
1060
  }
1061
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1062
+ e.preventDefault();
1063
+ this.handleCopy();
1064
+ }
1065
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1066
+ e.preventDefault();
1067
+ this.handlePaste();
1068
+ }
1053
1069
  };
1054
1070
  onKeyUp = (e) => {
1055
1071
  if (e.key === " ") {
@@ -1106,7 +1122,8 @@ var InputHandler = class {
1106
1122
  x: e.clientX - rect.left,
1107
1123
  y: e.clientY - rect.top,
1108
1124
  pressure: e.pressure,
1109
- pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse"
1125
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1126
+ shiftKey: e.shiftKey
1110
1127
  };
1111
1128
  }
1112
1129
  dispatchToolDown(e) {
@@ -1159,6 +1176,72 @@ var InputHandler = class {
1159
1176
  this.historyRecorder?.resume();
1160
1177
  this.toolContext.requestRender();
1161
1178
  }
1179
+ handleCopy() {
1180
+ if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1181
+ const tool = this.toolManager.activeTool;
1182
+ if (tool?.name !== "select") return;
1183
+ const selectTool = tool;
1184
+ const ids = selectTool.selectedIds;
1185
+ if (ids.length === 0) return;
1186
+ this.clipboard = [];
1187
+ for (const id of ids) {
1188
+ const el = this.toolContext.store.getById(id);
1189
+ if (el) this.clipboard.push(structuredClone(el));
1190
+ }
1191
+ this.pasteCount = 0;
1192
+ }
1193
+ handlePaste() {
1194
+ if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1195
+ return;
1196
+ const tool = this.toolManager.activeTool;
1197
+ if (tool?.name !== "select") return;
1198
+ const selectTool = tool;
1199
+ this.pasteCount++;
1200
+ const offset = this.pasteCount * 20;
1201
+ const idMap = /* @__PURE__ */ new Map();
1202
+ for (const el of this.clipboard) {
1203
+ idMap.set(el.id, createId(el.type));
1204
+ }
1205
+ const newIds = [];
1206
+ this.historyRecorder?.begin();
1207
+ for (const el of this.clipboard) {
1208
+ const clone = structuredClone(el);
1209
+ const newId = idMap.get(el.id);
1210
+ if (!newId) continue;
1211
+ clone.id = newId;
1212
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1213
+ if (clone.type === "arrow") {
1214
+ const arrow = clone;
1215
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1216
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1217
+ delete arrow.cachedControlPoint;
1218
+ if (arrow.fromBinding) {
1219
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1220
+ if (newTarget) {
1221
+ arrow.fromBinding = { elementId: newTarget };
1222
+ } else {
1223
+ delete arrow.fromBinding;
1224
+ }
1225
+ }
1226
+ if (arrow.toBinding) {
1227
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1228
+ if (newTarget) {
1229
+ arrow.toBinding = { elementId: newTarget };
1230
+ } else {
1231
+ delete arrow.toBinding;
1232
+ }
1233
+ }
1234
+ }
1235
+ if (this.toolContext.activeLayerId) {
1236
+ clone.layerId = this.toolContext.activeLayerId;
1237
+ }
1238
+ this.toolContext.store.add(clone);
1239
+ newIds.push(clone.id);
1240
+ }
1241
+ this.historyRecorder?.commit();
1242
+ selectTool.setSelection(newIds);
1243
+ this.toolContext.requestRender();
1244
+ }
1162
1245
  cancelToolIfActive(e) {
1163
1246
  if (this.isToolActive) {
1164
1247
  this.dispatchToolUp(e);
@@ -1412,19 +1495,23 @@ var ElementStore = class {
1412
1495
  bus = new EventBus();
1413
1496
  layerOrderMap = /* @__PURE__ */ new Map();
1414
1497
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1498
+ sortedCache = null;
1415
1499
  get count() {
1416
1500
  return this.elements.size;
1417
1501
  }
1418
1502
  setLayerOrder(order) {
1419
1503
  this.layerOrderMap = new Map(order);
1504
+ this.sortedCache = null;
1420
1505
  }
1421
1506
  getAll() {
1422
- return [...this.elements.values()].sort((a, b) => {
1507
+ if (this.sortedCache) return this.sortedCache;
1508
+ this.sortedCache = [...this.elements.values()].sort((a, b) => {
1423
1509
  const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1424
1510
  const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1425
1511
  if (layerA !== layerB) return layerA - layerB;
1426
1512
  return a.zIndex - b.zIndex;
1427
1513
  });
1514
+ return this.sortedCache;
1428
1515
  }
1429
1516
  getById(id) {
1430
1517
  return this.elements.get(id);
@@ -1435,6 +1522,7 @@ var ElementStore = class {
1435
1522
  );
1436
1523
  }
1437
1524
  add(element) {
1525
+ this.sortedCache = null;
1438
1526
  this.elements.set(element.id, element);
1439
1527
  const bounds = getElementBounds(element);
1440
1528
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1443,11 +1531,15 @@ var ElementStore = class {
1443
1531
  update(id, partial) {
1444
1532
  const existing = this.elements.get(id);
1445
1533
  if (!existing) return;
1534
+ this.sortedCache = null;
1446
1535
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1447
1536
  if (updated.type === "arrow") {
1448
1537
  const arrow = updated;
1449
1538
  arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1450
1539
  }
1540
+ if (updated.type === "note" && "text" in partial) {
1541
+ updated.text = sanitizeNoteHtml(updated.text);
1542
+ }
1451
1543
  this.elements.set(id, updated);
1452
1544
  const newBounds = getElementBounds(updated);
1453
1545
  if (newBounds) {
@@ -1458,11 +1550,13 @@ var ElementStore = class {
1458
1550
  remove(id) {
1459
1551
  const element = this.elements.get(id);
1460
1552
  if (!element) return;
1553
+ this.sortedCache = null;
1461
1554
  this.elements.delete(id);
1462
1555
  this.spatialIndex.remove(id);
1463
1556
  this.bus.emit("remove", element);
1464
1557
  }
1465
1558
  clear() {
1559
+ this.sortedCache = null;
1466
1560
  this.elements.clear();
1467
1561
  this.spatialIndex.clear();
1468
1562
  this.bus.emit("clear", null);
@@ -1471,6 +1565,7 @@ var ElementStore = class {
1471
1565
  return this.getAll().map((el) => ({ ...el }));
1472
1566
  }
1473
1567
  loadSnapshot(elements) {
1568
+ this.sortedCache = null;
1474
1569
  this.elements.clear();
1475
1570
  this.spatialIndex.clear();
1476
1571
  for (const el of elements) {
@@ -1478,6 +1573,10 @@ var ElementStore = class {
1478
1573
  const bounds = getElementBounds(el);
1479
1574
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1480
1575
  }
1576
+ this.bus.emit("clear", null);
1577
+ for (const el of elements) {
1578
+ this.bus.emit("add", el);
1579
+ }
1481
1580
  }
1482
1581
  queryRect(rect) {
1483
1582
  const ids = this.spatialIndex.query(rect);
@@ -2527,12 +2626,6 @@ var ElementRenderer = class {
2527
2626
  }
2528
2627
  };
2529
2628
 
2530
- // src/elements/create-id.ts
2531
- var counter = 0;
2532
- function createId(prefix) {
2533
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2534
- }
2535
-
2536
2629
  // src/elements/element-factory.ts
2537
2630
  var DEFAULT_NOTE_FONT_SIZE = 18;
2538
2631
  function createStroke(input) {
@@ -2558,7 +2651,7 @@ function createNote(input) {
2558
2651
  locked: input.locked ?? false,
2559
2652
  layerId: input.layerId ?? "",
2560
2653
  size: input.size ?? { w: 200, h: 100 },
2561
- text: input.text ?? "",
2654
+ text: sanitizeNoteHtml(input.text ?? ""),
2562
2655
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
2563
2656
  textColor: input.textColor ?? "#000000",
2564
2657
  fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
@@ -5083,9 +5176,15 @@ var SelectTool = class {
5083
5176
  lastWorld = { x: 0, y: 0 };
5084
5177
  currentWorld = { x: 0, y: 0 };
5085
5178
  ctx = null;
5179
+ pendingSingleSelectId = null;
5180
+ hasDragged = false;
5086
5181
  get selectedIds() {
5087
5182
  return [...this._selectedIds];
5088
5183
  }
5184
+ setSelection(ids) {
5185
+ this._selectedIds = ids;
5186
+ this.ctx?.requestRender();
5187
+ }
5089
5188
  get isMarqueeActive() {
5090
5189
  return this.mode.type === "marquee";
5091
5190
  }
@@ -5134,13 +5233,27 @@ var SelectTool = class {
5134
5233
  return;
5135
5234
  }
5136
5235
  }
5236
+ this.pendingSingleSelectId = null;
5237
+ this.hasDragged = false;
5137
5238
  const hit = this.hitTest(world, ctx);
5138
5239
  if (hit) {
5139
5240
  const alreadySelected = this._selectedIds.includes(hit.id);
5140
- if (!alreadySelected) {
5141
- this._selectedIds = [hit.id];
5241
+ if (state.shiftKey) {
5242
+ if (alreadySelected) {
5243
+ this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
5244
+ this.mode = { type: "idle" };
5245
+ } else {
5246
+ this._selectedIds = [...this._selectedIds, hit.id];
5247
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5248
+ }
5249
+ } else {
5250
+ if (!alreadySelected) {
5251
+ this._selectedIds = [hit.id];
5252
+ } else if (this._selectedIds.length > 1) {
5253
+ this.pendingSingleSelectId = hit.id;
5254
+ }
5255
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5142
5256
  }
5143
- this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5144
5257
  } else {
5145
5258
  this._selectedIds = [];
5146
5259
  this.mode = { type: "marquee", start: world };
@@ -5166,6 +5279,7 @@ var SelectTool = class {
5166
5279
  return;
5167
5280
  }
5168
5281
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
5282
+ this.hasDragged = true;
5169
5283
  ctx.setCursor?.("move");
5170
5284
  const snapped = this.snap(world, ctx);
5171
5285
  const dx = snapped.x - this.lastWorld.x;
@@ -5234,6 +5348,11 @@ var SelectTool = class {
5234
5348
  }
5235
5349
  ctx.requestRender();
5236
5350
  }
5351
+ if (!this.hasDragged && this.pendingSingleSelectId !== null) {
5352
+ this._selectedIds = [this.pendingSingleSelectId];
5353
+ }
5354
+ this.pendingSingleSelectId = null;
5355
+ this.hasDragged = false;
5237
5356
  this.mode = { type: "idle" };
5238
5357
  ctx.setCursor?.("default");
5239
5358
  }
@@ -6414,7 +6533,7 @@ var UpdateLayerCommand = class {
6414
6533
  };
6415
6534
 
6416
6535
  // src/index.ts
6417
- var VERSION = "0.11.2";
6536
+ var VERSION = "0.12.0";
6418
6537
  // Annotate the CommonJS export names for ESM import in node:
6419
6538
  0 && (module.exports = {
6420
6539
  AddElementCommand,
package/dist/index.d.cts CHANGED
@@ -200,6 +200,7 @@ declare class ElementStore {
200
200
  private bus;
201
201
  private layerOrderMap;
202
202
  private spatialIndex;
203
+ private sortedCache;
203
204
  get count(): number;
204
205
  setLayerOrder(order: Map<string, number>): void;
205
206
  getAll(): CanvasElement[];
@@ -239,6 +240,7 @@ interface PointerState {
239
240
  y: number;
240
241
  pressure: number;
241
242
  pointerType: 'mouse' | 'touch' | 'pen';
243
+ shiftKey: boolean;
242
244
  }
243
245
  interface Tool {
244
246
  readonly name: string;
@@ -428,6 +430,8 @@ declare class InputHandler {
428
430
  private readonly inputFilter;
429
431
  private deferredDown;
430
432
  private readonly abortController;
433
+ private clipboard;
434
+ private pasteCount;
431
435
  constructor(element: HTMLElement, camera: Camera, options?: InputHandlerOptions);
432
436
  setToolManager(toolManager: ToolManager, toolContext: ToolContext): void;
433
437
  destroy(): void;
@@ -451,6 +455,8 @@ declare class InputHandler {
451
455
  private deleteSelected;
452
456
  private handleUndo;
453
457
  private handleRedo;
458
+ private handleCopy;
459
+ private handlePaste;
454
460
  private cancelToolIfActive;
455
461
  }
456
462
 
@@ -899,7 +905,10 @@ declare class SelectTool implements Tool {
899
905
  private lastWorld;
900
906
  private currentWorld;
901
907
  private ctx;
908
+ private pendingSingleSelectId;
909
+ private hasDragged;
902
910
  get selectedIds(): string[];
911
+ setSelection(ids: string[]): void;
903
912
  get isMarqueeActive(): boolean;
904
913
  onActivate(ctx: ToolContext): void;
905
914
  onDeactivate(ctx: ToolContext): void;
@@ -1144,6 +1153,6 @@ declare class UpdateLayerCommand implements Command {
1144
1153
  undo(_store: ElementStore): void;
1145
1154
  }
1146
1155
 
1147
- declare const VERSION = "0.11.2";
1156
+ declare const VERSION = "0.12.0";
1148
1157
 
1149
1158
  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, 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, 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
@@ -200,6 +200,7 @@ declare class ElementStore {
200
200
  private bus;
201
201
  private layerOrderMap;
202
202
  private spatialIndex;
203
+ private sortedCache;
203
204
  get count(): number;
204
205
  setLayerOrder(order: Map<string, number>): void;
205
206
  getAll(): CanvasElement[];
@@ -239,6 +240,7 @@ interface PointerState {
239
240
  y: number;
240
241
  pressure: number;
241
242
  pointerType: 'mouse' | 'touch' | 'pen';
243
+ shiftKey: boolean;
242
244
  }
243
245
  interface Tool {
244
246
  readonly name: string;
@@ -428,6 +430,8 @@ declare class InputHandler {
428
430
  private readonly inputFilter;
429
431
  private deferredDown;
430
432
  private readonly abortController;
433
+ private clipboard;
434
+ private pasteCount;
431
435
  constructor(element: HTMLElement, camera: Camera, options?: InputHandlerOptions);
432
436
  setToolManager(toolManager: ToolManager, toolContext: ToolContext): void;
433
437
  destroy(): void;
@@ -451,6 +455,8 @@ declare class InputHandler {
451
455
  private deleteSelected;
452
456
  private handleUndo;
453
457
  private handleRedo;
458
+ private handleCopy;
459
+ private handlePaste;
454
460
  private cancelToolIfActive;
455
461
  }
456
462
 
@@ -899,7 +905,10 @@ declare class SelectTool implements Tool {
899
905
  private lastWorld;
900
906
  private currentWorld;
901
907
  private ctx;
908
+ private pendingSingleSelectId;
909
+ private hasDragged;
902
910
  get selectedIds(): string[];
911
+ setSelection(ids: string[]): void;
903
912
  get isMarqueeActive(): boolean;
904
913
  onActivate(ctx: ToolContext): void;
905
914
  onDeactivate(ctx: ToolContext): void;
@@ -1144,6 +1153,6 @@ declare class UpdateLayerCommand implements Command {
1144
1153
  undo(_store: ElementStore): void;
1145
1154
  }
1146
1155
 
1147
- declare const VERSION = "0.11.2";
1156
+ declare const VERSION = "0.12.0";
1148
1157
 
1149
1158
  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, 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, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -791,6 +791,12 @@ var InputFilter = class _InputFilter {
791
791
  }
792
792
  };
793
793
 
794
+ // src/elements/create-id.ts
795
+ var counter = 0;
796
+ function createId(prefix) {
797
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
798
+ }
799
+
794
800
  // src/canvas/input-handler.ts
795
801
  var ZOOM_SENSITIVITY = 1e-3;
796
802
  var MIDDLE_BUTTON = 1;
@@ -820,6 +826,8 @@ var InputHandler = class {
820
826
  inputFilter = new InputFilter();
821
827
  deferredDown = null;
822
828
  abortController = new AbortController();
829
+ clipboard = [];
830
+ pasteCount = 0;
823
831
  setToolManager(toolManager, toolContext) {
824
832
  this.toolManager = toolManager;
825
833
  this.toolContext = toolContext;
@@ -943,6 +951,14 @@ var InputHandler = class {
943
951
  e.preventDefault();
944
952
  this.handleRedo();
945
953
  }
954
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
955
+ e.preventDefault();
956
+ this.handleCopy();
957
+ }
958
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
959
+ e.preventDefault();
960
+ this.handlePaste();
961
+ }
946
962
  };
947
963
  onKeyUp = (e) => {
948
964
  if (e.key === " ") {
@@ -999,7 +1015,8 @@ var InputHandler = class {
999
1015
  x: e.clientX - rect.left,
1000
1016
  y: e.clientY - rect.top,
1001
1017
  pressure: e.pressure,
1002
- pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse"
1018
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1019
+ shiftKey: e.shiftKey
1003
1020
  };
1004
1021
  }
1005
1022
  dispatchToolDown(e) {
@@ -1052,6 +1069,72 @@ var InputHandler = class {
1052
1069
  this.historyRecorder?.resume();
1053
1070
  this.toolContext.requestRender();
1054
1071
  }
1072
+ handleCopy() {
1073
+ if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1074
+ const tool = this.toolManager.activeTool;
1075
+ if (tool?.name !== "select") return;
1076
+ const selectTool = tool;
1077
+ const ids = selectTool.selectedIds;
1078
+ if (ids.length === 0) return;
1079
+ this.clipboard = [];
1080
+ for (const id of ids) {
1081
+ const el = this.toolContext.store.getById(id);
1082
+ if (el) this.clipboard.push(structuredClone(el));
1083
+ }
1084
+ this.pasteCount = 0;
1085
+ }
1086
+ handlePaste() {
1087
+ if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1088
+ return;
1089
+ const tool = this.toolManager.activeTool;
1090
+ if (tool?.name !== "select") return;
1091
+ const selectTool = tool;
1092
+ this.pasteCount++;
1093
+ const offset = this.pasteCount * 20;
1094
+ const idMap = /* @__PURE__ */ new Map();
1095
+ for (const el of this.clipboard) {
1096
+ idMap.set(el.id, createId(el.type));
1097
+ }
1098
+ const newIds = [];
1099
+ this.historyRecorder?.begin();
1100
+ for (const el of this.clipboard) {
1101
+ const clone = structuredClone(el);
1102
+ const newId = idMap.get(el.id);
1103
+ if (!newId) continue;
1104
+ clone.id = newId;
1105
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1106
+ if (clone.type === "arrow") {
1107
+ const arrow = clone;
1108
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1109
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1110
+ delete arrow.cachedControlPoint;
1111
+ if (arrow.fromBinding) {
1112
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1113
+ if (newTarget) {
1114
+ arrow.fromBinding = { elementId: newTarget };
1115
+ } else {
1116
+ delete arrow.fromBinding;
1117
+ }
1118
+ }
1119
+ if (arrow.toBinding) {
1120
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1121
+ if (newTarget) {
1122
+ arrow.toBinding = { elementId: newTarget };
1123
+ } else {
1124
+ delete arrow.toBinding;
1125
+ }
1126
+ }
1127
+ }
1128
+ if (this.toolContext.activeLayerId) {
1129
+ clone.layerId = this.toolContext.activeLayerId;
1130
+ }
1131
+ this.toolContext.store.add(clone);
1132
+ newIds.push(clone.id);
1133
+ }
1134
+ this.historyRecorder?.commit();
1135
+ selectTool.setSelection(newIds);
1136
+ this.toolContext.requestRender();
1137
+ }
1055
1138
  cancelToolIfActive(e) {
1056
1139
  if (this.isToolActive) {
1057
1140
  this.dispatchToolUp(e);
@@ -1305,19 +1388,23 @@ var ElementStore = class {
1305
1388
  bus = new EventBus();
1306
1389
  layerOrderMap = /* @__PURE__ */ new Map();
1307
1390
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1391
+ sortedCache = null;
1308
1392
  get count() {
1309
1393
  return this.elements.size;
1310
1394
  }
1311
1395
  setLayerOrder(order) {
1312
1396
  this.layerOrderMap = new Map(order);
1397
+ this.sortedCache = null;
1313
1398
  }
1314
1399
  getAll() {
1315
- return [...this.elements.values()].sort((a, b) => {
1400
+ if (this.sortedCache) return this.sortedCache;
1401
+ this.sortedCache = [...this.elements.values()].sort((a, b) => {
1316
1402
  const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1317
1403
  const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1318
1404
  if (layerA !== layerB) return layerA - layerB;
1319
1405
  return a.zIndex - b.zIndex;
1320
1406
  });
1407
+ return this.sortedCache;
1321
1408
  }
1322
1409
  getById(id) {
1323
1410
  return this.elements.get(id);
@@ -1328,6 +1415,7 @@ var ElementStore = class {
1328
1415
  );
1329
1416
  }
1330
1417
  add(element) {
1418
+ this.sortedCache = null;
1331
1419
  this.elements.set(element.id, element);
1332
1420
  const bounds = getElementBounds(element);
1333
1421
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1336,11 +1424,15 @@ var ElementStore = class {
1336
1424
  update(id, partial) {
1337
1425
  const existing = this.elements.get(id);
1338
1426
  if (!existing) return;
1427
+ this.sortedCache = null;
1339
1428
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1340
1429
  if (updated.type === "arrow") {
1341
1430
  const arrow = updated;
1342
1431
  arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1343
1432
  }
1433
+ if (updated.type === "note" && "text" in partial) {
1434
+ updated.text = sanitizeNoteHtml(updated.text);
1435
+ }
1344
1436
  this.elements.set(id, updated);
1345
1437
  const newBounds = getElementBounds(updated);
1346
1438
  if (newBounds) {
@@ -1351,11 +1443,13 @@ var ElementStore = class {
1351
1443
  remove(id) {
1352
1444
  const element = this.elements.get(id);
1353
1445
  if (!element) return;
1446
+ this.sortedCache = null;
1354
1447
  this.elements.delete(id);
1355
1448
  this.spatialIndex.remove(id);
1356
1449
  this.bus.emit("remove", element);
1357
1450
  }
1358
1451
  clear() {
1452
+ this.sortedCache = null;
1359
1453
  this.elements.clear();
1360
1454
  this.spatialIndex.clear();
1361
1455
  this.bus.emit("clear", null);
@@ -1364,6 +1458,7 @@ var ElementStore = class {
1364
1458
  return this.getAll().map((el) => ({ ...el }));
1365
1459
  }
1366
1460
  loadSnapshot(elements) {
1461
+ this.sortedCache = null;
1367
1462
  this.elements.clear();
1368
1463
  this.spatialIndex.clear();
1369
1464
  for (const el of elements) {
@@ -1371,6 +1466,10 @@ var ElementStore = class {
1371
1466
  const bounds = getElementBounds(el);
1372
1467
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1373
1468
  }
1469
+ this.bus.emit("clear", null);
1470
+ for (const el of elements) {
1471
+ this.bus.emit("add", el);
1472
+ }
1374
1473
  }
1375
1474
  queryRect(rect) {
1376
1475
  const ids = this.spatialIndex.query(rect);
@@ -2420,12 +2519,6 @@ var ElementRenderer = class {
2420
2519
  }
2421
2520
  };
2422
2521
 
2423
- // src/elements/create-id.ts
2424
- var counter = 0;
2425
- function createId(prefix) {
2426
- return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
2427
- }
2428
-
2429
2522
  // src/elements/element-factory.ts
2430
2523
  var DEFAULT_NOTE_FONT_SIZE = 18;
2431
2524
  function createStroke(input) {
@@ -2451,7 +2544,7 @@ function createNote(input) {
2451
2544
  locked: input.locked ?? false,
2452
2545
  layerId: input.layerId ?? "",
2453
2546
  size: input.size ?? { w: 200, h: 100 },
2454
- text: input.text ?? "",
2547
+ text: sanitizeNoteHtml(input.text ?? ""),
2455
2548
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
2456
2549
  textColor: input.textColor ?? "#000000",
2457
2550
  fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
@@ -4976,9 +5069,15 @@ var SelectTool = class {
4976
5069
  lastWorld = { x: 0, y: 0 };
4977
5070
  currentWorld = { x: 0, y: 0 };
4978
5071
  ctx = null;
5072
+ pendingSingleSelectId = null;
5073
+ hasDragged = false;
4979
5074
  get selectedIds() {
4980
5075
  return [...this._selectedIds];
4981
5076
  }
5077
+ setSelection(ids) {
5078
+ this._selectedIds = ids;
5079
+ this.ctx?.requestRender();
5080
+ }
4982
5081
  get isMarqueeActive() {
4983
5082
  return this.mode.type === "marquee";
4984
5083
  }
@@ -5027,13 +5126,27 @@ var SelectTool = class {
5027
5126
  return;
5028
5127
  }
5029
5128
  }
5129
+ this.pendingSingleSelectId = null;
5130
+ this.hasDragged = false;
5030
5131
  const hit = this.hitTest(world, ctx);
5031
5132
  if (hit) {
5032
5133
  const alreadySelected = this._selectedIds.includes(hit.id);
5033
- if (!alreadySelected) {
5034
- this._selectedIds = [hit.id];
5134
+ if (state.shiftKey) {
5135
+ if (alreadySelected) {
5136
+ this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
5137
+ this.mode = { type: "idle" };
5138
+ } else {
5139
+ this._selectedIds = [...this._selectedIds, hit.id];
5140
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5141
+ }
5142
+ } else {
5143
+ if (!alreadySelected) {
5144
+ this._selectedIds = [hit.id];
5145
+ } else if (this._selectedIds.length > 1) {
5146
+ this.pendingSingleSelectId = hit.id;
5147
+ }
5148
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5035
5149
  }
5036
- this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
5037
5150
  } else {
5038
5151
  this._selectedIds = [];
5039
5152
  this.mode = { type: "marquee", start: world };
@@ -5059,6 +5172,7 @@ var SelectTool = class {
5059
5172
  return;
5060
5173
  }
5061
5174
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
5175
+ this.hasDragged = true;
5062
5176
  ctx.setCursor?.("move");
5063
5177
  const snapped = this.snap(world, ctx);
5064
5178
  const dx = snapped.x - this.lastWorld.x;
@@ -5127,6 +5241,11 @@ var SelectTool = class {
5127
5241
  }
5128
5242
  ctx.requestRender();
5129
5243
  }
5244
+ if (!this.hasDragged && this.pendingSingleSelectId !== null) {
5245
+ this._selectedIds = [this.pendingSingleSelectId];
5246
+ }
5247
+ this.pendingSingleSelectId = null;
5248
+ this.hasDragged = false;
5130
5249
  this.mode = { type: "idle" };
5131
5250
  ctx.setCursor?.("default");
5132
5251
  }
@@ -6307,7 +6426,7 @@ var UpdateLayerCommand = class {
6307
6426
  };
6308
6427
 
6309
6428
  // src/index.ts
6310
- var VERSION = "0.11.2";
6429
+ var VERSION = "0.12.0";
6311
6430
  export {
6312
6431
  AddElementCommand,
6313
6432
  ArrowTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/core",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Vanilla TypeScript infinite canvas engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",