@seatlayer/core 0.10.1 → 0.11.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 +243 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +243 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -637,6 +637,7 @@ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
|
637
637
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
638
638
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
639
639
|
var MAX_LABELS = 700;
|
|
640
|
+
var MARQUEE_RING_CAP = 2500;
|
|
640
641
|
var ISO_ANGLE_DEG = -11.5;
|
|
641
642
|
var ISO_SQUASH = 0.58;
|
|
642
643
|
var LIFT_PER_STEP = 58;
|
|
@@ -863,8 +864,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
863
864
|
this.panLast = null;
|
|
864
865
|
/** Cumulative gesture movement in px — clicks are suppressed after a real pan/pinch. */
|
|
865
866
|
this.moved = 0;
|
|
867
|
+
/**
|
|
868
|
+
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
869
|
+
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
870
|
+
* selection band. Null except during an active manage-mode drag.
|
|
871
|
+
*/
|
|
872
|
+
this.marquee = null;
|
|
873
|
+
this.marqueeCur = null;
|
|
866
874
|
// ---- keyboard navigation (accessibility) ----------------------------------
|
|
867
875
|
this.onKeyDown = (e) => {
|
|
876
|
+
if (this.opts.manageMode) {
|
|
877
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "a" || e.key === "A")) {
|
|
878
|
+
e.preventDefault();
|
|
879
|
+
this.opts.onMarquee?.(this.selectAllSelectable());
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (e.key === "Escape" && this.selection.size) {
|
|
883
|
+
e.preventDefault();
|
|
884
|
+
this.clearSelection();
|
|
885
|
+
this.opts.onMarquee?.([]);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
868
889
|
const dir = e.key === "ArrowLeft" ? { x: -1, y: 0 } : e.key === "ArrowRight" ? { x: 1, y: 0 } : e.key === "ArrowUp" ? { x: 0, y: -1 } : e.key === "ArrowDown" ? { x: 0, y: 1 } : null;
|
|
869
890
|
if (dir) {
|
|
870
891
|
e.preventDefault();
|
|
@@ -887,9 +908,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
887
908
|
this.pointers.set(e.pointerId, this.toLocal(e));
|
|
888
909
|
if (this.pointers.size === 1) {
|
|
889
910
|
this.moved = 0;
|
|
890
|
-
this.panLast = this.toLocal(e);
|
|
891
911
|
this.pinch = null;
|
|
912
|
+
if (this.opts.manageMode && this.opts.marqueeSelect && e.pointerType !== "touch" && e.button === 0 && this.getRung() === "seats") {
|
|
913
|
+
this.beginMarquee(this.toLocal(e));
|
|
914
|
+
this.panLast = null;
|
|
915
|
+
} else {
|
|
916
|
+
this.panLast = this.toLocal(e);
|
|
917
|
+
}
|
|
892
918
|
} else if (this.pointers.size === 2) {
|
|
919
|
+
if (this.marquee) this.cancelMarquee();
|
|
893
920
|
const [a, b] = [...this.pointers.values()];
|
|
894
921
|
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
895
922
|
const s = this.stage.scaleX();
|
|
@@ -908,6 +935,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
908
935
|
const prev = this.pointers.get(e.pointerId);
|
|
909
936
|
this.moved += Math.hypot(p.x - prev.x, p.y - prev.y);
|
|
910
937
|
this.pointers.set(e.pointerId, p);
|
|
938
|
+
if (this.marquee) {
|
|
939
|
+
this.updateMarquee(p);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
911
942
|
if (this.pinch && this.pointers.size >= 2) {
|
|
912
943
|
const [a, b] = [...this.pointers.values()];
|
|
913
944
|
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
@@ -933,6 +964,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
933
964
|
}
|
|
934
965
|
};
|
|
935
966
|
this.onPointerEnd = (e) => {
|
|
967
|
+
if (this.marquee) {
|
|
968
|
+
this.finishMarquee();
|
|
969
|
+
this.pointers.delete(e.pointerId);
|
|
970
|
+
if (this.pointers.size === 0) this.panLast = null;
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
936
973
|
this.pointers.delete(e.pointerId);
|
|
937
974
|
if (this.pointers.size < 2) this.pinch = null;
|
|
938
975
|
if (this.pointers.size === 1) {
|
|
@@ -1168,6 +1205,144 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1168
1205
|
}
|
|
1169
1206
|
if (changed) this.overlayLayer.batchDraw();
|
|
1170
1207
|
}
|
|
1208
|
+
// ---- manage-mode bulk selection (SDK SeatManager) -------------------------
|
|
1209
|
+
// All option-gated: no-ops (or empty) unless `manageMode` is set, so buyer
|
|
1210
|
+
// surfaces that never pass the flag can't reach any of this.
|
|
1211
|
+
/** Select every selectable seat on the chart (⌘A). Returns the added seats. */
|
|
1212
|
+
selectAllSelectable() {
|
|
1213
|
+
if (!this.opts.manageMode) return [];
|
|
1214
|
+
const ids = [];
|
|
1215
|
+
for (const seat of this.seats) if (this.isSelectable(seat.id)) ids.push(seat.id);
|
|
1216
|
+
return this.selectMany(ids);
|
|
1217
|
+
}
|
|
1218
|
+
/** Select the selectable seats matching these public labels (category/row/
|
|
1219
|
+
* section bulk resolves to labels host-side). Returns the newly added seats. */
|
|
1220
|
+
selectByLabels(labels) {
|
|
1221
|
+
if (!this.opts.manageMode) return [];
|
|
1222
|
+
const want = new Set(labels);
|
|
1223
|
+
const ids = [];
|
|
1224
|
+
for (const seat of this.seats) {
|
|
1225
|
+
if (want.has(seat.label) && this.isSelectable(seat.id)) ids.push(seat.id);
|
|
1226
|
+
}
|
|
1227
|
+
return this.selectMany(ids);
|
|
1228
|
+
}
|
|
1229
|
+
/** Selectable seats in a section OR zone id — pure read (no selection change). */
|
|
1230
|
+
getSelectableInSection(sectionId) {
|
|
1231
|
+
const out = [];
|
|
1232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1233
|
+
for (const sec of this.sections) {
|
|
1234
|
+
if (sec.id !== sectionId && sec.zone !== sectionId) continue;
|
|
1235
|
+
for (const id of sec.memberIds) {
|
|
1236
|
+
if (seen.has(id)) continue;
|
|
1237
|
+
seen.add(id);
|
|
1238
|
+
if (!this.isSelectable(id)) continue;
|
|
1239
|
+
const s = this.seatById.get(id);
|
|
1240
|
+
if (s) out.push(s);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return out;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Union `ids` into the selection in one batched pass. Beyond MARQUEE_RING_CAP
|
|
1247
|
+
* total selected we skip the per-seat ring nodes (fill paint still marks the
|
|
1248
|
+
* seats) so a whole-arena select doesn't spawn thousands of Konva shapes.
|
|
1249
|
+
* Returns the full current selection.
|
|
1250
|
+
*/
|
|
1251
|
+
selectMany(ids) {
|
|
1252
|
+
const fresh = ids.filter((id) => !this.selection.has(id));
|
|
1253
|
+
if (!fresh.length) return this.getSelection();
|
|
1254
|
+
const drawRings = this.selection.size + fresh.length <= MARQUEE_RING_CAP;
|
|
1255
|
+
for (const id of fresh) {
|
|
1256
|
+
if (drawRings) {
|
|
1257
|
+
this.setSelected(id, true, true);
|
|
1258
|
+
} else {
|
|
1259
|
+
this.selection.add(id);
|
|
1260
|
+
const c = this.circleById.get(id);
|
|
1261
|
+
if (c) this.paintSeat(c, id);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (!this.cached) this.seatLayer.batchDraw();
|
|
1265
|
+
this.overlayLayer.batchDraw();
|
|
1266
|
+
return this.getSelection();
|
|
1267
|
+
}
|
|
1268
|
+
// ---- manage-mode marquee gesture ------------------------------------------
|
|
1269
|
+
beginMarquee(clientPt) {
|
|
1270
|
+
const w = this.screenToWorld(clientPt);
|
|
1271
|
+
const s = this.stage.scaleX() || 1;
|
|
1272
|
+
const rect = new Rect({
|
|
1273
|
+
x: w.x,
|
|
1274
|
+
y: w.y,
|
|
1275
|
+
width: 0,
|
|
1276
|
+
height: 0,
|
|
1277
|
+
stroke: this.effSelection,
|
|
1278
|
+
strokeWidth: 1.5 / s,
|
|
1279
|
+
// world units → ~constant on-screen px under the stage scale
|
|
1280
|
+
dash: [5 / s, 4 / s],
|
|
1281
|
+
fill: "rgba(110,123,255,0.10)",
|
|
1282
|
+
listening: false,
|
|
1283
|
+
perfectDrawEnabled: false,
|
|
1284
|
+
shadowForStrokeEnabled: false
|
|
1285
|
+
});
|
|
1286
|
+
this.overlayLayer.add(rect);
|
|
1287
|
+
this.marquee = { start: w, rect };
|
|
1288
|
+
this.marqueeCur = w;
|
|
1289
|
+
this.overlayLayer.batchDraw();
|
|
1290
|
+
}
|
|
1291
|
+
updateMarquee(clientPt) {
|
|
1292
|
+
if (!this.marquee) return;
|
|
1293
|
+
const w = this.screenToWorld(clientPt);
|
|
1294
|
+
const { start, rect } = this.marquee;
|
|
1295
|
+
rect.setAttrs({
|
|
1296
|
+
x: Math.min(start.x, w.x),
|
|
1297
|
+
y: Math.min(start.y, w.y),
|
|
1298
|
+
width: Math.abs(w.x - start.x),
|
|
1299
|
+
height: Math.abs(w.y - start.y)
|
|
1300
|
+
});
|
|
1301
|
+
this.marqueeCur = w;
|
|
1302
|
+
this.overlayLayer.batchDraw();
|
|
1303
|
+
}
|
|
1304
|
+
cancelMarquee() {
|
|
1305
|
+
if (!this.marquee) return;
|
|
1306
|
+
this.marquee.rect.destroy();
|
|
1307
|
+
this.marquee = null;
|
|
1308
|
+
this.marqueeCur = null;
|
|
1309
|
+
this.overlayLayer.batchDraw();
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Pointer-up: hit-test the marquee world-rect against the in-memory seat
|
|
1313
|
+
* centres (NOT the Konva hit-graph — that's a cached bitmap when zoomed and
|
|
1314
|
+
* far slower), keep only SELECTABLE seats, union them into the selection and
|
|
1315
|
+
* fire `onMarquee`. A near-zero drag reads as a click: clear the selection.
|
|
1316
|
+
*/
|
|
1317
|
+
finishMarquee() {
|
|
1318
|
+
const m = this.marquee;
|
|
1319
|
+
this.marquee = null;
|
|
1320
|
+
if (!m) return;
|
|
1321
|
+
m.rect.destroy();
|
|
1322
|
+
this.overlayLayer.batchDraw();
|
|
1323
|
+
const cur = this.marqueeCur ?? m.start;
|
|
1324
|
+
this.marqueeCur = null;
|
|
1325
|
+
const x0 = Math.min(m.start.x, cur.x);
|
|
1326
|
+
const x1 = Math.max(m.start.x, cur.x);
|
|
1327
|
+
const y0 = Math.min(m.start.y, cur.y);
|
|
1328
|
+
const y1 = Math.max(m.start.y, cur.y);
|
|
1329
|
+
const s = this.stage.scaleX() || 1;
|
|
1330
|
+
if ((x1 - x0) * s < 4 && (y1 - y0) * s < 4) {
|
|
1331
|
+
if (this.selection.size) {
|
|
1332
|
+
this.clearSelection();
|
|
1333
|
+
this.opts.onMarquee?.([]);
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const ids = [];
|
|
1338
|
+
for (const seat of this.seats) {
|
|
1339
|
+
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
1340
|
+
if (!this.isSelectable(seat.id)) continue;
|
|
1341
|
+
ids.push(seat.id);
|
|
1342
|
+
}
|
|
1343
|
+
this.selectMany(ids);
|
|
1344
|
+
this.opts.onMarquee?.(this.getSelection());
|
|
1345
|
+
}
|
|
1171
1346
|
flashSeat(seatId, color = "#f43f5e") {
|
|
1172
1347
|
const seat = this.seatById.get(seatId);
|
|
1173
1348
|
if (!seat) return;
|
|
@@ -2849,14 +3024,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2849
3024
|
this.seatLayer.batchDraw();
|
|
2850
3025
|
}
|
|
2851
3026
|
}
|
|
2852
|
-
|
|
3027
|
+
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
3028
|
+
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
3029
|
+
rebuildSeatCache() {
|
|
2853
3030
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
2854
3031
|
this.seatLayer.clearCache();
|
|
2855
3032
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
2856
3033
|
this.seatLayer.listening(false);
|
|
2857
3034
|
this.cached = true;
|
|
3035
|
+
}
|
|
3036
|
+
cacheSeatLayer() {
|
|
3037
|
+
this.rebuildSeatCache();
|
|
2858
3038
|
this.seatLayer.batchDraw();
|
|
2859
3039
|
}
|
|
3040
|
+
/**
|
|
3041
|
+
* SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
|
|
3042
|
+
* ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
|
|
3043
|
+
* Konva's batchDraw) on hidden/backgrounded/occluded tabs, so seat-status
|
|
3044
|
+
* deltas applied via setStatus() mutate the scene graph but never reach the
|
|
3045
|
+
* canvas until the tab is foregrounded. This flushes the cache-debounce and
|
|
3046
|
+
* paints every layer setStatus() can touch, immediately.
|
|
3047
|
+
*
|
|
3048
|
+
* Additive: the foreground path never calls this, so foreground behaviour is
|
|
3049
|
+
* byte-identical.
|
|
3050
|
+
*/
|
|
3051
|
+
forceDraw() {
|
|
3052
|
+
if (this.recacheTimer) {
|
|
3053
|
+
clearTimeout(this.recacheTimer);
|
|
3054
|
+
this.recacheTimer = null;
|
|
3055
|
+
this.rebuildSeatCache();
|
|
3056
|
+
}
|
|
3057
|
+
this.bgLayer.draw();
|
|
3058
|
+
this.seatLayer.draw();
|
|
3059
|
+
this.overlayLayer.draw();
|
|
3060
|
+
}
|
|
2860
3061
|
updateLabels() {
|
|
2861
3062
|
const show = this.effScale() > LABEL_SCALE;
|
|
2862
3063
|
this.labelGroup.destroyChildren();
|
|
@@ -2992,6 +3193,10 @@ var PickerController = class {
|
|
|
2992
3193
|
this.reconnectTimer = null;
|
|
2993
3194
|
this.attempt = 0;
|
|
2994
3195
|
this.closed = false;
|
|
3196
|
+
/** Bound visibilitychange listener (buyer catch-up on tab regain). Kept on the
|
|
3197
|
+
* instance so destroy() can detach it; null when not attached (guards double
|
|
3198
|
+
* mounts / non-DOM environments). */
|
|
3199
|
+
this.onVisibilityChange = null;
|
|
2995
3200
|
// hold state
|
|
2996
3201
|
this.hold_ = null;
|
|
2997
3202
|
this.liveStatuses = /* @__PURE__ */ new Map();
|
|
@@ -3117,6 +3322,7 @@ var PickerController = class {
|
|
|
3117
3322
|
this.closedSections = new Set(closedIds);
|
|
3118
3323
|
if (closedIds.length) renderer.setClosedSections?.(closedIds);
|
|
3119
3324
|
if (seats) this.applySeatsMap(seats);
|
|
3325
|
+
this.attachVisibilityListener();
|
|
3120
3326
|
this.connect();
|
|
3121
3327
|
return {
|
|
3122
3328
|
doc: res.doc,
|
|
@@ -3543,6 +3749,7 @@ var PickerController = class {
|
|
|
3543
3749
|
}
|
|
3544
3750
|
destroy() {
|
|
3545
3751
|
this.closed = true;
|
|
3752
|
+
this.detachVisibilityListener();
|
|
3546
3753
|
if (this.reconnectTimer) {
|
|
3547
3754
|
clearTimeout(this.reconnectTimer);
|
|
3548
3755
|
this.reconnectTimer = null;
|
|
@@ -3739,6 +3946,37 @@ var PickerController = class {
|
|
|
3739
3946
|
}
|
|
3740
3947
|
}
|
|
3741
3948
|
// ---- realtime socket ------------------------------------------------------
|
|
3949
|
+
/**
|
|
3950
|
+
* Buyer catch-up on tab regain. While a tab is hidden Chrome pauses rAF, so
|
|
3951
|
+
* seat-status deltas that arrived over the WS mutated the scene graph but the
|
|
3952
|
+
* canvas colors never repainted. When the tab becomes visible again we (1)
|
|
3953
|
+
* resnapshot to pull authoritative state for anything the socket may have
|
|
3954
|
+
* missed while backgrounded, then (2) forceDraw() a synchronous repaint so the
|
|
3955
|
+
* seat colors are correct immediately rather than on the next incidental draw.
|
|
3956
|
+
*
|
|
3957
|
+
* Attached once per mount; guarded against double-attach and non-DOM
|
|
3958
|
+
* environments; detached in destroy() (no leaks).
|
|
3959
|
+
*/
|
|
3960
|
+
attachVisibilityListener() {
|
|
3961
|
+
if (this.onVisibilityChange) return;
|
|
3962
|
+
if (typeof document === "undefined" || typeof document.addEventListener !== "function") return;
|
|
3963
|
+
const handler = () => {
|
|
3964
|
+
if (this.closed) return;
|
|
3965
|
+
if (document.visibilityState !== "visible") return;
|
|
3966
|
+
void this.resnapshot().then(() => {
|
|
3967
|
+
if (!this.closed) this.renderer?.forceDraw();
|
|
3968
|
+
});
|
|
3969
|
+
};
|
|
3970
|
+
this.onVisibilityChange = handler;
|
|
3971
|
+
document.addEventListener("visibilitychange", handler);
|
|
3972
|
+
}
|
|
3973
|
+
detachVisibilityListener() {
|
|
3974
|
+
if (!this.onVisibilityChange) return;
|
|
3975
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
|
|
3976
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
3977
|
+
}
|
|
3978
|
+
this.onVisibilityChange = null;
|
|
3979
|
+
}
|
|
3742
3980
|
connect() {
|
|
3743
3981
|
if (this.closed) return;
|
|
3744
3982
|
let ws;
|
|
@@ -3784,6 +4022,9 @@ var PickerController = class {
|
|
|
3784
4022
|
}
|
|
3785
4023
|
r.setStatus([id], next);
|
|
3786
4024
|
}
|
|
4025
|
+
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
4026
|
+
r.forceDraw();
|
|
4027
|
+
}
|
|
3787
4028
|
this.clearBookedHoldIfSettled();
|
|
3788
4029
|
this.opts.onStatusChange?.();
|
|
3789
4030
|
}
|