@ngroznykh/papirus 0.4.0 → 0.5.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/papirus.js CHANGED
@@ -570,6 +570,14 @@ class SelectionManager extends EventEmitter {
570
570
  }
571
571
  return;
572
572
  }
573
+ const node = element;
574
+ if (typeof node.getBadgeAtPoint === "function") {
575
+ const badge = node.getBadgeAtPoint(point);
576
+ if (badge !== null) {
577
+ this.renderer.emit("nodeBadgeClick", element.id, badge.id);
578
+ return;
579
+ }
580
+ }
573
581
  if (event.ctrlKey || event.metaKey) {
574
582
  this.toggleSelection(element.id);
575
583
  } else {
@@ -5002,6 +5010,7 @@ class InteractionManager {
5002
5010
  this.overlayDragSession = null;
5003
5011
  this.handledOverlayMouseDown = false;
5004
5012
  this.renderer = options.renderer;
5013
+ this.navigationOnly = options.navigationOnly ?? false;
5005
5014
  this.inputHandler = new InputHandler({
5006
5015
  canvas: this.renderer.getCanvas(),
5007
5016
  screenToWorld: (x, y) => this.renderer.screenToWorld(x, y)
@@ -5153,13 +5162,15 @@ class InteractionManager {
5153
5162
  setupEvents(options) {
5154
5163
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
5155
5164
  this.selectionManager.renderSelectionRect(ctx);
5156
- this.dragManager.renderAlignmentGuides(ctx);
5157
- this.connectionManager.renderPreview(ctx);
5158
- this.connectionManager.renderHoverAnchors(ctx);
5159
- for (const id of this.selectionManager.selectedIds) {
5160
- const node = this.renderer.getNode(id);
5161
- if (node) {
5162
- node.renderResizeHandles(ctx);
5165
+ if (!this.navigationOnly) {
5166
+ this.dragManager.renderAlignmentGuides(ctx);
5167
+ this.connectionManager.renderPreview(ctx);
5168
+ this.connectionManager.renderHoverAnchors(ctx);
5169
+ for (const id of this.selectionManager.selectedIds) {
5170
+ const node = this.renderer.getNode(id);
5171
+ if (node) {
5172
+ node.renderResizeHandles(ctx);
5173
+ }
5163
5174
  }
5164
5175
  }
5165
5176
  });
@@ -5173,68 +5184,70 @@ class InteractionManager {
5173
5184
  this.inputHandler.on("pinch", (event) => this.handlePinch(event));
5174
5185
  this.inputHandler.on("keydown", (event) => this.handleKeyDown(event, options));
5175
5186
  this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
5176
- this.dragManager.on("dragstart", (nodeIds) => {
5177
- this.connectionManager.disableHover();
5178
- this.dragStartPositions.clear();
5179
- for (const id of nodeIds) {
5180
- const node = this.renderer.getNode(id);
5181
- if (node) {
5182
- this.dragStartPositions.set(id, { x: node.x, y: node.y });
5183
- }
5184
- }
5185
- });
5186
- this.dragManager.on("dragend", (nodeIds) => {
5187
- this.connectionManager.enableHover();
5188
- const nodePositions = /* @__PURE__ */ new Map();
5189
- for (const id of nodeIds) {
5190
- const node = this.renderer.getNode(id);
5191
- const before = this.dragStartPositions.get(id);
5192
- if (!node || !before) continue;
5193
- const after = { x: node.x, y: node.y };
5194
- if (before.x !== after.x || before.y !== after.y) {
5195
- nodePositions.set(id, { before, after });
5196
- }
5197
- }
5198
- if (nodePositions.size > 0) {
5199
- this.historyManager.execute(
5200
- new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5201
- );
5202
- }
5203
- });
5204
- this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5205
- this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5206
- });
5207
- this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5208
- const origin = this.reconnectOrigins.get(edge.id);
5209
- if (!origin || origin.endpoint !== endpoint) {
5210
- return;
5211
- }
5212
- const before = origin.original;
5213
- const after = endpoint === "start" ? edge.from : edge.to;
5214
- if (this.endpointsEqual(before, after)) {
5215
- this.reconnectOrigins.delete(edge.id);
5216
- return;
5217
- }
5218
- this.historyManager.execute({
5219
- execute: () => {
5220
- if (endpoint === "start") {
5221
- edge.from = { ...after };
5222
- } else {
5223
- edge.to = { ...after };
5187
+ if (!this.navigationOnly) {
5188
+ this.dragManager.on("dragstart", (nodeIds) => {
5189
+ this.connectionManager.disableHover();
5190
+ this.dragStartPositions.clear();
5191
+ for (const id of nodeIds) {
5192
+ const node = this.renderer.getNode(id);
5193
+ if (node) {
5194
+ this.dragStartPositions.set(id, { x: node.x, y: node.y });
5224
5195
  }
5225
- this.renderer.markDirty();
5226
- },
5227
- undo: () => {
5228
- if (endpoint === "start") {
5229
- edge.from = { ...before };
5230
- } else {
5231
- edge.to = { ...before };
5196
+ }
5197
+ });
5198
+ this.dragManager.on("dragend", (nodeIds) => {
5199
+ this.connectionManager.enableHover();
5200
+ const nodePositions = /* @__PURE__ */ new Map();
5201
+ for (const id of nodeIds) {
5202
+ const node = this.renderer.getNode(id);
5203
+ const before = this.dragStartPositions.get(id);
5204
+ if (!node || !before) continue;
5205
+ const after = { x: node.x, y: node.y };
5206
+ if (before.x !== after.x || before.y !== after.y) {
5207
+ nodePositions.set(id, { before, after });
5232
5208
  }
5233
- this.renderer.markDirty();
5209
+ }
5210
+ if (nodePositions.size > 0) {
5211
+ this.historyManager.execute(
5212
+ new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5213
+ );
5234
5214
  }
5235
5215
  });
5236
- this.reconnectOrigins.delete(edge.id);
5237
- });
5216
+ this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5217
+ this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5218
+ });
5219
+ this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5220
+ const origin = this.reconnectOrigins.get(edge.id);
5221
+ if (!origin || origin.endpoint !== endpoint) {
5222
+ return;
5223
+ }
5224
+ const before = origin.original;
5225
+ const after = endpoint === "start" ? edge.from : edge.to;
5226
+ if (this.endpointsEqual(before, after)) {
5227
+ this.reconnectOrigins.delete(edge.id);
5228
+ return;
5229
+ }
5230
+ this.historyManager.execute({
5231
+ execute: () => {
5232
+ if (endpoint === "start") {
5233
+ edge.from = { ...after };
5234
+ } else {
5235
+ edge.to = { ...after };
5236
+ }
5237
+ this.renderer.markDirty();
5238
+ },
5239
+ undo: () => {
5240
+ if (endpoint === "start") {
5241
+ edge.from = { ...before };
5242
+ } else {
5243
+ edge.to = { ...before };
5244
+ }
5245
+ this.renderer.markDirty();
5246
+ }
5247
+ });
5248
+ this.reconnectOrigins.delete(edge.id);
5249
+ });
5250
+ }
5238
5251
  this.historyManager.on("change", () => {
5239
5252
  this.renderer.markDirty();
5240
5253
  });
@@ -5242,14 +5255,16 @@ class InteractionManager {
5242
5255
  handleMouseDown(event) {
5243
5256
  this.handledScrollbarMouseDown = false;
5244
5257
  this.handledOverlayMouseDown = false;
5245
- if (this.resizeManager.handleMouseDown(event)) {
5246
- return;
5247
- }
5248
- if (this.connectionManager.tryStartReconnection(event)) {
5249
- return;
5250
- }
5251
- if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5252
- return;
5258
+ if (!this.navigationOnly) {
5259
+ if (this.resizeManager.handleMouseDown(event)) {
5260
+ return;
5261
+ }
5262
+ if (this.connectionManager.tryStartReconnection(event)) {
5263
+ return;
5264
+ }
5265
+ if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5266
+ return;
5267
+ }
5253
5268
  }
5254
5269
  const overlayDrag = this.renderer.beginOverlayDrag(event.screenX, event.screenY);
5255
5270
  if (overlayDrag) {
@@ -5283,6 +5298,9 @@ class InteractionManager {
5283
5298
  if (this.navigationManager.handleMouseDown(event)) {
5284
5299
  return;
5285
5300
  }
5301
+ if (this.navigationOnly) {
5302
+ return;
5303
+ }
5286
5304
  if (this.dragManager.handleMouseDown(event)) {
5287
5305
  return;
5288
5306
  }
@@ -5302,6 +5320,9 @@ class InteractionManager {
5302
5320
  }
5303
5321
  }
5304
5322
  const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
5323
+ this.renderer.updateBadgeHover(
5324
+ overScrollbar ? { x: -1e9, y: -1e9 } : { x: event.worldX, y: event.worldY }
5325
+ );
5305
5326
  if (this.overlayDragSession) {
5306
5327
  const moved = this.renderer.updateOverlayDrag(
5307
5328
  this.overlayDragSession,
@@ -5330,14 +5351,16 @@ class InteractionManager {
5330
5351
  if (overScrollbar) {
5331
5352
  return;
5332
5353
  }
5333
- if (this.resizeManager.handleMouseMove(event)) {
5334
- return;
5335
- }
5336
- if (this.connectionManager.handleMouseMove(event)) {
5337
- return;
5338
- }
5339
- if (this.dragManager.handleMouseMove(event)) {
5340
- return;
5354
+ if (!this.navigationOnly) {
5355
+ if (this.resizeManager.handleMouseMove(event)) {
5356
+ return;
5357
+ }
5358
+ if (this.connectionManager.handleMouseMove(event)) {
5359
+ return;
5360
+ }
5361
+ if (this.dragManager.handleMouseMove(event)) {
5362
+ return;
5363
+ }
5341
5364
  }
5342
5365
  if (this.selectionManager.selectionRectangle !== null) {
5343
5366
  this.selectionManager.updateSelectionRect({ x: event.worldX, y: event.worldY });
@@ -5356,14 +5379,16 @@ class InteractionManager {
5356
5379
  this.renderer.setScrollbarActiveAxis(null);
5357
5380
  return;
5358
5381
  }
5359
- if (this.resizeManager.handleMouseUp()) {
5360
- return;
5361
- }
5362
- if (this.connectionManager.handleMouseUp(event)) {
5363
- return;
5364
- }
5365
- if (this.dragManager.handleMouseUp(event)) {
5366
- return;
5382
+ if (!this.navigationOnly) {
5383
+ if (this.resizeManager.handleMouseUp()) {
5384
+ return;
5385
+ }
5386
+ if (this.connectionManager.handleMouseUp(event)) {
5387
+ return;
5388
+ }
5389
+ if (this.dragManager.handleMouseUp(event)) {
5390
+ return;
5391
+ }
5367
5392
  }
5368
5393
  if (this.selectionManager.selectionRectangle !== null) {
5369
5394
  this.selectionManager.endSelectionRect();
@@ -5389,6 +5414,9 @@ class InteractionManager {
5389
5414
  if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
5390
5415
  return;
5391
5416
  }
5417
+ if (this.navigationOnly) {
5418
+ return;
5419
+ }
5392
5420
  if (this.connectionManager.handleDoubleClick(event)) {
5393
5421
  return;
5394
5422
  }
@@ -5451,16 +5479,19 @@ class InteractionManager {
5451
5479
  handleKeyDown(event, options) {
5452
5480
  const isCtrlOrMeta = event.ctrlKey || event.metaKey;
5453
5481
  const key = event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
5482
+ this.navigationManager.handleKeyDown(event);
5483
+ if (this.handleViewportNavigationKey(event)) {
5484
+ return;
5485
+ }
5486
+ if (this.navigationOnly) {
5487
+ return;
5488
+ }
5454
5489
  if (isCtrlOrMeta && (key === "z" || key === "y")) {
5455
5490
  this.flushPendingPropertyChanges();
5456
5491
  }
5457
5492
  if (this.historyManager.handleKeyDown(event)) {
5458
5493
  return;
5459
5494
  }
5460
- this.navigationManager.handleKeyDown(event);
5461
- if (this.handleViewportNavigationKey(event)) {
5462
- return;
5463
- }
5464
5495
  if (this.keymap.deleteKeys.includes(event.key)) {
5465
5496
  event.preventDefault();
5466
5497
  this.deleteSelection();
@@ -6713,6 +6744,30 @@ class DiagramRenderer extends EventEmitter {
6713
6744
  }
6714
6745
  return void 0;
6715
6746
  }
6747
+ /**
6748
+ * Update badge hover state and canvas cursor based on pointer position.
6749
+ * Call from mousemove to show hover highlight and pointer cursor over badges.
6750
+ */
6751
+ updateBadgeHover(worldPoint) {
6752
+ const element = this.getElementAtPoint(worldPoint);
6753
+ let hoveredNodeId = null;
6754
+ let hoveredIndex = -1;
6755
+ const node = element;
6756
+ if (element && typeof node.getBadgeAtPoint === "function") {
6757
+ const badge = node.getBadgeAtPoint(worldPoint);
6758
+ if (badge !== null) {
6759
+ hoveredNodeId = node.id;
6760
+ hoveredIndex = badge.index;
6761
+ }
6762
+ }
6763
+ const cursor = hoveredNodeId !== null ? "pointer" : "";
6764
+ if (this.canvas.style.cursor !== cursor) {
6765
+ this.canvas.style.cursor = cursor;
6766
+ }
6767
+ for (const n of this._nodes.values()) {
6768
+ n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
6769
+ }
6770
+ }
6716
6771
  /**
6717
6772
  * Mark the diagram as needing re-render
6718
6773
  */
@@ -8025,11 +8080,11 @@ function tintSvg(svgText, strokeColor, fillColor) {
8025
8080
  const all = [root, ...Array.from(root.querySelectorAll("*"))];
8026
8081
  for (const el of all) {
8027
8082
  const stroke = el.getAttribute("stroke");
8028
- if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
8083
+ const fill = el.getAttribute("fill");
8084
+ if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) {
8029
8085
  el.setAttribute("stroke", strokeColor);
8030
8086
  }
8031
- const fill = el.getAttribute("fill");
8032
- if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
8087
+ if (fillColor && (fill === null || fill.toLowerCase() !== "none")) {
8033
8088
  el.setAttribute("fill", fillColor);
8034
8089
  }
8035
8090
  const style = el.getAttribute("style");
@@ -8196,6 +8251,9 @@ class NodeImage {
8196
8251
  };
8197
8252
  }
8198
8253
  }
8254
+ const BADGE_SIZE = 15;
8255
+ const BADGE_OFFSET = 4;
8256
+ const BADGE_GAP = 4;
8199
8257
  function isValidAnchorId(id) {
8200
8258
  return /^(top|right|bottom|left):\d+$/.test(id);
8201
8259
  }
@@ -8217,7 +8275,10 @@ class Node extends Element {
8217
8275
  styleClass: options.styleClass
8218
8276
  });
8219
8277
  this._ports = [];
8278
+ this._badges = [];
8220
8279
  this._anchorCache = null;
8280
+ this._badgeImageCache = /* @__PURE__ */ new Map();
8281
+ this._hoveredBadgeIndex = -1;
8221
8282
  this._defaultSize = { width: options.width, height: options.height };
8222
8283
  this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
8223
8284
  this._showPortsAlways = options.showPortsAlways ?? false;
@@ -8242,6 +8303,10 @@ class Node extends Element {
8242
8303
  this.addPort(portOptions);
8243
8304
  }
8244
8305
  }
8306
+ if (options.badges !== void 0 && options.badges.length > 0) {
8307
+ this._badges = options.badges.map((b) => ({ id: b.id, iconUrl: b.iconUrl }));
8308
+ this.ensureBadgeImagesLoaded();
8309
+ }
8245
8310
  }
8246
8311
  /**
8247
8312
  * Node style
@@ -8307,6 +8372,46 @@ class Node extends Element {
8307
8372
  }
8308
8373
  this.markDirty();
8309
8374
  }
8375
+ /**
8376
+ * Badges shown in top-left corner of node (e.g. interactive property icons)
8377
+ */
8378
+ get badges() {
8379
+ return this._badges;
8380
+ }
8381
+ set badges(value) {
8382
+ this._badges = Array.isArray(value) ? value.map((b) => ({ id: b.id, iconUrl: b.iconUrl })) : [];
8383
+ this.ensureBadgeImagesLoaded();
8384
+ this.markDirty();
8385
+ }
8386
+ /**
8387
+ * Set which badge index is under the pointer (-1 for none). Used for hover highlight and cursor.
8388
+ */
8389
+ setBadgeHover(index) {
8390
+ if (this._hoveredBadgeIndex === index) return;
8391
+ this._hoveredBadgeIndex = index;
8392
+ this.markDirty();
8393
+ }
8394
+ /**
8395
+ * Return badge at world point, or null if point is not over a badge.
8396
+ */
8397
+ getBadgeAtPoint(worldPoint) {
8398
+ if (this._badges.length === 0) {
8399
+ return null;
8400
+ }
8401
+ const bounds = this.getBounds();
8402
+ const contentBounds = this.getLabelContainerBounds(bounds);
8403
+ const localX = worldPoint.x - (contentBounds.x + BADGE_OFFSET);
8404
+ const localY = worldPoint.y - (contentBounds.y + BADGE_OFFSET);
8405
+ for (let i = 0; i < this._badges.length; i++) {
8406
+ const badge = this._badges[i];
8407
+ if (badge === void 0) continue;
8408
+ const x = i * (BADGE_SIZE + BADGE_GAP);
8409
+ if (localX >= x && localX <= x + BADGE_SIZE && localY >= 0 && localY <= BADGE_SIZE) {
8410
+ return { id: badge.id, index: i };
8411
+ }
8412
+ }
8413
+ return null;
8414
+ }
8310
8415
  /**
8311
8416
  * Add a port to this node
8312
8417
  */
@@ -8526,6 +8631,7 @@ class Node extends Element {
8526
8631
  ctx.setLineDash([]);
8527
8632
  ctx.lineDashOffset = 0;
8528
8633
  let bounds = this.getBounds();
8634
+ this.renderBadges(ctx, bounds);
8529
8635
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8530
8636
  if (this._label) {
8531
8637
  this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
@@ -8549,6 +8655,67 @@ class Node extends Element {
8549
8655
  this.renderLabel(ctx, labelBounds);
8550
8656
  this.renderPorts(ctx);
8551
8657
  }
8658
+ ensureBadgeImagesLoaded() {
8659
+ for (const badge of this._badges) {
8660
+ const url = badge.iconUrl;
8661
+ if (!url || this._badgeImageCache.has(url)) {
8662
+ continue;
8663
+ }
8664
+ const img = new Image();
8665
+ img.decoding = "async";
8666
+ this._badgeImageCache.set(url, { img, loaded: false });
8667
+ img.onload = () => {
8668
+ const entry = this._badgeImageCache.get(url);
8669
+ if (entry) {
8670
+ entry.loaded = true;
8671
+ this.markDirty();
8672
+ }
8673
+ };
8674
+ img.onerror = () => {
8675
+ this.markDirty();
8676
+ };
8677
+ img.src = url;
8678
+ }
8679
+ }
8680
+ renderBadges(ctx, bounds) {
8681
+ if (this._badges.length === 0) {
8682
+ return;
8683
+ }
8684
+ const contentBounds = this.getLabelContainerBounds(bounds);
8685
+ const x0 = contentBounds.x + BADGE_OFFSET;
8686
+ const y0 = contentBounds.y + BADGE_OFFSET;
8687
+ const radius = 2;
8688
+ for (let i = 0; i < this._badges.length; i++) {
8689
+ const badge = this._badges[i];
8690
+ if (badge === void 0) continue;
8691
+ const x = x0 + i * (BADGE_SIZE + BADGE_GAP);
8692
+ const isHovered = this._hoveredBadgeIndex === i;
8693
+ if (isHovered) {
8694
+ ctx.save();
8695
+ ctx.fillStyle = "rgba(0, 0, 0, 0.08)";
8696
+ ctx.beginPath();
8697
+ ctx.roundRect(x, y0, BADGE_SIZE, BADGE_SIZE, radius);
8698
+ ctx.fill();
8699
+ ctx.restore();
8700
+ }
8701
+ const entry = this._badgeImageCache.get(badge.iconUrl);
8702
+ if (entry?.loaded && entry.img.naturalWidth > 0) {
8703
+ ctx.save();
8704
+ const img = entry.img;
8705
+ const sw = img.naturalWidth;
8706
+ const sh = img.naturalHeight;
8707
+ const scale = Math.min(BADGE_SIZE / sw, BADGE_SIZE / sh, 1);
8708
+ const dw = sw * scale;
8709
+ const dh = sh * scale;
8710
+ const dx = x + (BADGE_SIZE - dw) / 2;
8711
+ const dy = y0 + (BADGE_SIZE - dh) / 2;
8712
+ ctx.imageSmoothingEnabled = true;
8713
+ ctx.imageSmoothingQuality = "high";
8714
+ ctx.drawImage(img, 0, 0, sw, sh, dx, dy, dw, dh);
8715
+ ctx.restore();
8716
+ }
8717
+ }
8718
+ }
8552
8719
  /**
8553
8720
  * Minimal size required to fit current contents
8554
8721
  */