@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.cjs CHANGED
@@ -124,7 +124,13 @@ var EventBus = class {
124
124
  this.listeners.get(event)?.delete(listener);
125
125
  }
126
126
  emit(event, data) {
127
- this.listeners.get(event)?.forEach((listener) => listener(data));
127
+ this.listeners.get(event)?.forEach((listener) => {
128
+ try {
129
+ listener(data);
130
+ } catch (err) {
131
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
132
+ }
133
+ });
128
134
  }
129
135
  clear() {
130
136
  this.listeners.clear();
@@ -928,6 +934,7 @@ var KeyboardActions = class {
928
934
  clipboard = [];
929
935
  pasteCount = 0;
930
936
  nudgeTimer = null;
937
+ nudgeTxId = null;
931
938
  dispose() {
932
939
  this.flushPendingNudge();
933
940
  }
@@ -946,7 +953,9 @@ var KeyboardActions = class {
946
953
  if (sel.tool.selectedIds.length === 0) return false;
947
954
  const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
948
955
  if (this.nudgeTimer === null) {
949
- this.deps.getHistoryRecorder()?.begin();
956
+ const recorder = this.deps.getHistoryRecorder();
957
+ recorder?.begin();
958
+ this.nudgeTxId = recorder?.currentTransactionId ?? null;
950
959
  } else {
951
960
  clearTimeout(this.nudgeTimer);
952
961
  }
@@ -958,9 +967,14 @@ var KeyboardActions = class {
958
967
  if (this.nudgeTimer === null) return;
959
968
  clearTimeout(this.nudgeTimer);
960
969
  this.nudgeTimer = null;
961
- this.deps.getHistoryRecorder()?.commit();
970
+ const recorder = this.deps.getHistoryRecorder();
971
+ if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
972
+ recorder?.commit();
973
+ }
974
+ this.nudgeTxId = null;
962
975
  }
963
976
  deleteSelected() {
977
+ if (this.deps.isToolActive()) return;
964
978
  this.flushPendingNudge();
965
979
  const sel = this.selectTool();
966
980
  if (!sel) return;
@@ -975,6 +989,7 @@ var KeyboardActions = class {
975
989
  sel.ctx.requestRender();
976
990
  }
977
991
  undo() {
992
+ if (this.deps.isToolActive()) return;
978
993
  this.flushPendingNudge();
979
994
  const ctx = this.deps.getToolContext();
980
995
  const stack = this.deps.getHistoryStack();
@@ -986,6 +1001,7 @@ var KeyboardActions = class {
986
1001
  ctx.requestRender();
987
1002
  }
988
1003
  redo() {
1004
+ if (this.deps.isToolActive()) return;
989
1005
  this.flushPendingNudge();
990
1006
  const ctx = this.deps.getToolContext();
991
1007
  const stack = this.deps.getHistoryStack();
@@ -1059,6 +1075,7 @@ var KeyboardActions = class {
1059
1075
  this.deps.fitToContent?.();
1060
1076
  }
1061
1077
  zOrder(operation) {
1078
+ if (this.deps.isToolActive()) return;
1062
1079
  this.flushPendingNudge();
1063
1080
  const sel = this.selectTool();
1064
1081
  if (!sel) return;
@@ -1133,14 +1150,158 @@ var KeyboardActions = class {
1133
1150
  }
1134
1151
  };
1135
1152
 
1153
+ // src/canvas/shortcut-map.ts
1154
+ var DEFAULT_BINDINGS = [
1155
+ ["delete", ["delete", "backspace"]],
1156
+ ["deselect", ["escape"]],
1157
+ ["undo", ["mod+z"]],
1158
+ ["redo", ["mod+y", "mod+shift+z"]],
1159
+ ["select-all", ["mod+a"]],
1160
+ ["copy", ["mod+c"]],
1161
+ ["paste", ["mod+v"]],
1162
+ ["duplicate", ["mod+d"]],
1163
+ ["z-forward", ["]"]],
1164
+ ["z-backward", ["["]],
1165
+ ["z-front", ["mod+]"]],
1166
+ ["z-back", ["mod+["]],
1167
+ ["zoom-fit", ["shift+1"]],
1168
+ ["nudge-left", ["arrowleft"]],
1169
+ ["nudge-right", ["arrowright"]],
1170
+ ["nudge-up", ["arrowup"]],
1171
+ ["nudge-down", ["arrowdown"]],
1172
+ ["tool:select", ["v"]],
1173
+ ["tool:hand", ["h"]],
1174
+ ["tool:pencil", ["p"]],
1175
+ ["tool:eraser", ["e"]],
1176
+ ["tool:arrow", ["a"]],
1177
+ ["tool:note", ["n"]],
1178
+ ["tool:text", ["t"]],
1179
+ ["tool:shape", ["s"]],
1180
+ ["tool:measure", ["m"]],
1181
+ ["tool:template", ["g"]]
1182
+ ];
1183
+ var ALLOW_SHIFT = /* @__PURE__ */ new Set(["nudge-left", "nudge-right", "nudge-up", "nudge-down"]);
1184
+ var MODIFIERS = /* @__PURE__ */ new Set(["mod", "ctrl", "meta", "shift", "alt"]);
1185
+ function parseBinding(binding) {
1186
+ const parts = binding.toLowerCase().split("+");
1187
+ const key = parts.pop();
1188
+ if (key === void 0 || key.length === 0 || MODIFIERS.has(key)) {
1189
+ throw new Error(`Invalid shortcut binding "${binding}": missing key`);
1190
+ }
1191
+ const normalizedKey = key === "space" ? " " : key;
1192
+ const parsed = {
1193
+ mod: false,
1194
+ ctrl: false,
1195
+ meta: false,
1196
+ shift: false,
1197
+ alt: false,
1198
+ key: normalizedKey,
1199
+ digit: /^[0-9]$/.test(normalizedKey)
1200
+ };
1201
+ for (const part of parts) {
1202
+ switch (part) {
1203
+ case "mod":
1204
+ parsed.mod = true;
1205
+ break;
1206
+ case "ctrl":
1207
+ parsed.ctrl = true;
1208
+ break;
1209
+ case "meta":
1210
+ parsed.meta = true;
1211
+ break;
1212
+ case "shift":
1213
+ parsed.shift = true;
1214
+ break;
1215
+ case "alt":
1216
+ parsed.alt = true;
1217
+ break;
1218
+ default:
1219
+ throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
1220
+ }
1221
+ }
1222
+ return parsed;
1223
+ }
1224
+ function bindingMatches(p, e, allowShift) {
1225
+ if (p.mod) {
1226
+ if (!e.ctrlKey && !e.metaKey) return false;
1227
+ } else if (e.ctrlKey !== p.ctrl || e.metaKey !== p.meta) {
1228
+ return false;
1229
+ }
1230
+ if (!allowShift && e.shiftKey !== p.shift) return false;
1231
+ if (e.altKey !== p.alt) return false;
1232
+ return p.digit ? e.code === `Digit${p.key}` : e.key.toLowerCase() === p.key;
1233
+ }
1234
+ function toArray(bindings) {
1235
+ if (bindings === null) return [];
1236
+ return Array.isArray(bindings) ? [...bindings] : [bindings];
1237
+ }
1238
+ var ShortcutMap = class {
1239
+ raw = /* @__PURE__ */ new Map();
1240
+ parsed = /* @__PURE__ */ new Map();
1241
+ constructor(overrides) {
1242
+ this.applyDefaults();
1243
+ if (overrides) {
1244
+ for (const [action, bindings] of Object.entries(overrides)) {
1245
+ this.rebind(action, bindings);
1246
+ }
1247
+ }
1248
+ }
1249
+ /** First matching action in registration order wins when bindings conflict. */
1250
+ match(e) {
1251
+ for (const [action, parsedList] of this.parsed) {
1252
+ const allowShift = ALLOW_SHIFT.has(action);
1253
+ for (const p of parsedList) {
1254
+ if (bindingMatches(p, e, allowShift)) return action;
1255
+ }
1256
+ }
1257
+ return null;
1258
+ }
1259
+ rebind(action, bindings) {
1260
+ const list = toArray(bindings);
1261
+ const parsedList = list.map(parseBinding);
1262
+ this.raw.set(action, list);
1263
+ this.parsed.set(action, parsedList);
1264
+ }
1265
+ disable(action) {
1266
+ this.rebind(action, null);
1267
+ }
1268
+ reset(action) {
1269
+ if (action === void 0) {
1270
+ this.raw.clear();
1271
+ this.parsed.clear();
1272
+ this.applyDefaults();
1273
+ return;
1274
+ }
1275
+ const def = DEFAULT_BINDINGS.find(([name]) => name === action);
1276
+ if (def) {
1277
+ this.rebind(action, [...def[1]]);
1278
+ } else if (this.raw.has(action)) {
1279
+ this.raw.delete(action);
1280
+ this.parsed.delete(action);
1281
+ }
1282
+ }
1283
+ getBindings() {
1284
+ const out = {};
1285
+ for (const [action, list] of this.raw) {
1286
+ out[action] = [...list];
1287
+ }
1288
+ return out;
1289
+ }
1290
+ applyDefaults() {
1291
+ for (const [action, bindings] of DEFAULT_BINDINGS) {
1292
+ this.rebind(action, [...bindings]);
1293
+ }
1294
+ }
1295
+ };
1296
+
1136
1297
  // src/canvas/input-handler.ts
1137
1298
  var ZOOM_SENSITIVITY = 1e-3;
1138
1299
  var MIDDLE_BUTTON = 1;
1139
- var NUDGE_KEYS = {
1140
- ArrowLeft: [-1, 0],
1141
- ArrowRight: [1, 0],
1142
- ArrowUp: [0, -1],
1143
- ArrowDown: [0, 1]
1300
+ var NUDGE_DELTAS = {
1301
+ "nudge-left": [-1, 0],
1302
+ "nudge-right": [1, 0],
1303
+ "nudge-up": [0, -1],
1304
+ "nudge-down": [0, 1]
1144
1305
  };
1145
1306
  var InputHandler = class {
1146
1307
  constructor(element, camera, options = {}) {
@@ -1158,7 +1319,13 @@ var InputHandler = class {
1158
1319
  isToolActive: () => this.isToolActive,
1159
1320
  fitToContent: options.fitToContent
1160
1321
  });
1322
+ this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1323
+ this.scope = options.shortcuts?.scope ?? "focus";
1161
1324
  this.element.style.touchAction = "none";
1325
+ if (this.scope === "focus") {
1326
+ this.element.tabIndex = 0;
1327
+ this.element.style.outline = "none";
1328
+ }
1162
1329
  this.bind();
1163
1330
  }
1164
1331
  isPanning = false;
@@ -1177,6 +1344,8 @@ var InputHandler = class {
1177
1344
  deferredDown = null;
1178
1345
  abortController = new AbortController();
1179
1346
  actions;
1347
+ shortcutMap;
1348
+ scope;
1180
1349
  setToolManager(toolManager, toolContext) {
1181
1350
  this.toolManager = toolManager;
1182
1351
  this.toolContext = toolContext;
@@ -1184,6 +1353,9 @@ var InputHandler = class {
1184
1353
  flushPendingHistory() {
1185
1354
  this.actions.flushPendingNudge();
1186
1355
  }
1356
+ get shortcuts() {
1357
+ return this.shortcutMap;
1358
+ }
1187
1359
  destroy() {
1188
1360
  this.actions.dispose();
1189
1361
  this.abortController.abort();
@@ -1213,6 +1385,7 @@ var InputHandler = class {
1213
1385
  });
1214
1386
  };
1215
1387
  onPointerDown = (e) => {
1388
+ this.focusSelf();
1216
1389
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1217
1390
  this.element.setPointerCapture?.(e.pointerId);
1218
1391
  if (this.activePointers.size === 2) {
@@ -1295,57 +1468,13 @@ var InputHandler = class {
1295
1468
  if (target?.isContentEditable) return;
1296
1469
  const tag = target?.tagName;
1297
1470
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1471
+ if (!this.isInScope()) return;
1298
1472
  if (e.key === " ") {
1299
1473
  this.spaceHeld = true;
1300
1474
  }
1301
- if (e.key === "Delete" || e.key === "Backspace") {
1302
- this.actions.deleteSelected();
1303
- }
1304
- if (e.key === "Escape") {
1305
- this.actions.deselect();
1306
- }
1307
- if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1308
- e.preventDefault();
1309
- this.actions.undo();
1310
- }
1311
- if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
1312
- e.preventDefault();
1313
- this.actions.redo();
1314
- }
1315
- if ((e.ctrlKey || e.metaKey) && e.key === "a") {
1316
- e.preventDefault();
1317
- this.actions.selectAll();
1318
- }
1319
- if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1320
- e.preventDefault();
1321
- this.actions.copy();
1322
- }
1323
- if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1324
- e.preventDefault();
1325
- this.actions.paste();
1326
- }
1327
- if ((e.ctrlKey || e.metaKey) && e.key === "d") {
1328
- e.preventDefault();
1329
- this.actions.duplicate();
1330
- }
1331
- if (e.key === "]") {
1332
- e.preventDefault();
1333
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1334
- }
1335
- if (e.key === "[") {
1336
- e.preventDefault();
1337
- this.actions.zOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1338
- }
1339
- if (e.shiftKey && e.code === "Digit1" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1340
- e.preventDefault();
1341
- this.actions.zoomToFit();
1342
- }
1343
- const nudgeDelta = NUDGE_KEYS[e.key];
1344
- if (nudgeDelta) {
1345
- const [dx, dy] = nudgeDelta;
1346
- if (this.actions.nudge(dx, dy, e.shiftKey)) {
1347
- e.preventDefault();
1348
- }
1475
+ const action = this.shortcutMap.match(e);
1476
+ if (action !== null) {
1477
+ this.runAction(action, e);
1349
1478
  }
1350
1479
  };
1351
1480
  onKeyUp = (e) => {
@@ -1360,6 +1489,79 @@ var InputHandler = class {
1360
1489
  }
1361
1490
  }
1362
1491
  };
1492
+ runAction(action, e) {
1493
+ switch (action) {
1494
+ case "delete":
1495
+ e.preventDefault();
1496
+ this.actions.deleteSelected();
1497
+ return;
1498
+ case "deselect":
1499
+ this.actions.deselect();
1500
+ return;
1501
+ case "undo":
1502
+ e.preventDefault();
1503
+ this.actions.undo();
1504
+ return;
1505
+ case "redo":
1506
+ e.preventDefault();
1507
+ this.actions.redo();
1508
+ return;
1509
+ case "select-all":
1510
+ e.preventDefault();
1511
+ this.actions.selectAll();
1512
+ return;
1513
+ case "copy":
1514
+ e.preventDefault();
1515
+ this.actions.copy();
1516
+ return;
1517
+ case "paste":
1518
+ e.preventDefault();
1519
+ this.actions.paste();
1520
+ return;
1521
+ case "duplicate":
1522
+ e.preventDefault();
1523
+ this.actions.duplicate();
1524
+ return;
1525
+ case "z-forward":
1526
+ e.preventDefault();
1527
+ this.actions.zOrder("forward");
1528
+ return;
1529
+ case "z-backward":
1530
+ e.preventDefault();
1531
+ this.actions.zOrder("backward");
1532
+ return;
1533
+ case "z-front":
1534
+ e.preventDefault();
1535
+ this.actions.zOrder("front");
1536
+ return;
1537
+ case "z-back":
1538
+ e.preventDefault();
1539
+ this.actions.zOrder("back");
1540
+ return;
1541
+ case "zoom-fit":
1542
+ e.preventDefault();
1543
+ this.actions.zoomToFit();
1544
+ return;
1545
+ case "nudge-left":
1546
+ case "nudge-right":
1547
+ case "nudge-up":
1548
+ case "nudge-down": {
1549
+ const delta = NUDGE_DELTAS[action];
1550
+ if (delta && this.actions.nudge(delta[0], delta[1], e.shiftKey)) {
1551
+ e.preventDefault();
1552
+ }
1553
+ return;
1554
+ }
1555
+ default:
1556
+ if (action.startsWith("tool:")) {
1557
+ if (this.isToolActive) return;
1558
+ e.preventDefault();
1559
+ this.toolContext?.switchTool?.(action.slice("tool:".length));
1560
+ return;
1561
+ }
1562
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1563
+ }
1564
+ }
1363
1565
  startPinch() {
1364
1566
  this.inputFilter.reset();
1365
1567
  this.deferredDown = null;
@@ -1430,6 +1632,15 @@ var InputHandler = class {
1430
1632
  this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1431
1633
  this.historyRecorder?.commit();
1432
1634
  }
1635
+ isInScope() {
1636
+ if (this.scope === "window") return true;
1637
+ const active = document.activeElement;
1638
+ return active === this.element || this.element.contains(active);
1639
+ }
1640
+ focusSelf() {
1641
+ if (this.scope !== "focus" || this.isInScope()) return;
1642
+ this.element.focus({ preventScroll: true });
1643
+ }
1433
1644
  cancelToolIfActive(e) {
1434
1645
  if (this.isToolActive) {
1435
1646
  this.dispatchToolUp(e);
@@ -3618,6 +3829,7 @@ var HistoryRecorder = class {
3618
3829
  }
3619
3830
  recording = true;
3620
3831
  transaction = null;
3832
+ generation = 0;
3621
3833
  updateSnapshots = /* @__PURE__ */ new Map();
3622
3834
  unsubscribers;
3623
3835
  pause() {
@@ -3632,6 +3844,10 @@ var HistoryRecorder = class {
3632
3844
  }
3633
3845
  this.transaction = [];
3634
3846
  this.updateSnapshots.clear();
3847
+ this.generation += 1;
3848
+ }
3849
+ get currentTransactionId() {
3850
+ return this.transaction !== null ? this.generation : null;
3635
3851
  }
3636
3852
  commit() {
3637
3853
  if (!this.transaction) return;
@@ -4819,7 +5035,8 @@ var Viewport = class {
4819
5035
  toolContext: this.toolContext,
4820
5036
  historyRecorder: this.historyRecorder,
4821
5037
  historyStack: this.history,
4822
- fitToContent: () => this.fitToContent()
5038
+ fitToContent: () => this.fitToContent(),
5039
+ shortcuts: options.shortcuts
4823
5040
  });
4824
5041
  this.domNodeManager = new DomNodeManager({
4825
5042
  domLayer: this.domLayer,
@@ -4990,6 +5207,12 @@ var Viewport = class {
4990
5207
  loadJSON(json) {
4991
5208
  this.loadState(parseState(json));
4992
5209
  }
5210
+ setTool(name) {
5211
+ this.toolManager.setTool(name, this.toolContext);
5212
+ }
5213
+ get shortcuts() {
5214
+ return this.inputHandler.shortcuts;
5215
+ }
4993
5216
  undo() {
4994
5217
  this.inputHandler.flushPendingHistory();
4995
5218
  this.historyRecorder.pause();
@@ -7028,7 +7251,7 @@ var TemplateTool = class {
7028
7251
  };
7029
7252
 
7030
7253
  // src/index.ts
7031
- var VERSION = "0.17.0";
7254
+ var VERSION = "0.19.0";
7032
7255
  // Annotate the CommonJS export names for ESM import in node:
7033
7256
  0 && (module.exports = {
7034
7257
  AddElementCommand,
@@ -7116,3 +7339,4 @@ var VERSION = "0.17.0";
7116
7339
  unbindArrow,
7117
7340
  updateBoundArrow
7118
7341
  });
7342
+ //# sourceMappingURL=index.cjs.map