@blorkfield/blork-tabs 0.3.0 → 0.4.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
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  SnapPreview: () => SnapPreview,
27
27
  TabManager: () => TabManager,
28
28
  areInSameChain: () => areInSameChain,
29
+ createDebugLog: () => createDebugLog,
29
30
  createDebugPanelContent: () => createDebugPanelContent,
30
31
  createDebugPanelInterface: () => createDebugPanelInterface,
31
32
  createPanelElement: () => createPanelElement,
@@ -38,16 +39,19 @@ __export(index_exports, {
38
39
  getDefaultZIndex: () => getDefaultZIndex,
39
40
  getDragZIndex: () => getDragZIndex,
40
41
  getLeftmostPanel: () => getLeftmostPanel,
42
+ getMovingGroupRespectingPins: () => getMovingGroupRespectingPins,
41
43
  getPanelDimensions: () => getPanelDimensions,
42
44
  getPanelPosition: () => getPanelPosition,
43
45
  getRightmostPanel: () => getRightmostPanel,
44
46
  hidePanel: () => hidePanel,
45
47
  setPanelPosition: () => setPanelPosition,
46
48
  setPanelZIndex: () => setPanelZIndex,
49
+ setupHoverEnlarge: () => setupHoverEnlarge,
47
50
  showPanel: () => showPanel,
48
51
  snapPanels: () => snapPanels,
49
52
  snapPanelsToTarget: () => snapPanelsToTarget,
50
53
  toggleCollapse: () => toggleCollapse,
54
+ togglePin: () => togglePin,
51
55
  unsnap: () => unsnap,
52
56
  updateSnappedPositions: () => updateSnappedPositions
53
57
  });
@@ -65,11 +69,24 @@ function createDebugPanelContent(_config, classes) {
65
69
  };
66
70
  }
67
71
  function createDebugPanelInterface(panel, elements, config, classes) {
72
+ const debugLog = createDebugLogInterface(elements.logContainer, config, classes);
73
+ return {
74
+ panel,
75
+ ...debugLog
76
+ };
77
+ }
78
+ function escapeHtml(str) {
79
+ const div = document.createElement("div");
80
+ div.textContent = str;
81
+ return div.innerHTML;
82
+ }
83
+ function createDebugLogInterface(logContainer, config, classes) {
68
84
  const maxEntries = config.maxEntries ?? 50;
69
85
  const showTimestamps = config.showTimestamps ?? false;
86
+ const entryClass = classes.debugLogEntry;
70
87
  function addEntry(level, eventName, data) {
71
88
  const entry = document.createElement("div");
72
- entry.className = classes.debugLogEntry;
89
+ entry.className = entryClass;
73
90
  if (level === "warn") entry.classList.add(classes.debugLogEntryWarn);
74
91
  if (level === "error") entry.classList.add(classes.debugLogEntryError);
75
92
  let html = "";
@@ -88,30 +105,104 @@ function createDebugPanelInterface(panel, elements, config, classes) {
88
105
  html += `<span class="${classes.debugLogData}">${escapeHtml(dataStr)}</span>`;
89
106
  }
90
107
  entry.innerHTML = html;
91
- elements.logContainer.appendChild(entry);
92
- elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
93
- while (elements.logContainer.children.length > maxEntries) {
94
- elements.logContainer.removeChild(elements.logContainer.children[0]);
108
+ logContainer.appendChild(entry);
109
+ logContainer.scrollTop = logContainer.scrollHeight;
110
+ const entries = logContainer.querySelectorAll(`.${entryClass}`);
111
+ if (entries.length > maxEntries) {
112
+ const toRemove = entries.length - maxEntries;
113
+ for (let i = 0; i < toRemove; i++) {
114
+ entries[i].remove();
115
+ }
95
116
  }
96
117
  }
118
+ function clearEntries() {
119
+ const entries = logContainer.querySelectorAll(`.${entryClass}`);
120
+ entries.forEach((entry) => entry.remove());
121
+ }
97
122
  return {
98
- panel,
99
123
  log: (name, data) => addEntry("log", name, data),
100
124
  info: (name, data) => addEntry("info", name, data),
101
125
  warn: (name, data) => addEntry("warn", name, data),
102
126
  error: (name, data) => addEntry("error", name, data),
103
- clear: () => {
104
- elements.logContainer.innerHTML = "";
105
- }
127
+ clear: clearEntries
106
128
  };
107
129
  }
108
- function escapeHtml(str) {
109
- const div = document.createElement("div");
110
- div.textContent = str;
111
- return div.innerHTML;
130
+ function createDebugLog(container, config, classes) {
131
+ const logContainer = document.createElement("div");
132
+ logContainer.className = classes.debugLog;
133
+ container.appendChild(logContainer);
134
+ const debugLog = createDebugLogInterface(logContainer, config, classes);
135
+ return { debugLog, logContainer };
136
+ }
137
+ function setupHoverEnlarge(config) {
138
+ const { logContainer, hoverDelay, backdropContainer, classes, onHoverStart, onHoverEnd, onClose } = config;
139
+ logContainer.classList.add(classes.debugPanel);
140
+ const closeBtn = document.createElement("button");
141
+ closeBtn.className = classes.debugClearButton;
142
+ closeBtn.textContent = "\xD7";
143
+ closeBtn.title = "Close enlarged view";
144
+ logContainer.appendChild(closeBtn);
145
+ let hoverTimeout = null;
146
+ let isEnlarged = false;
147
+ let backdrop = null;
148
+ let originalParent = null;
149
+ let placeholder = null;
150
+ const closeEnlarged = () => {
151
+ if (!isEnlarged) return;
152
+ isEnlarged = false;
153
+ logContainer.classList.remove(classes.debugPanelEnlarged);
154
+ if (originalParent && placeholder) {
155
+ originalParent.insertBefore(logContainer, placeholder);
156
+ placeholder.remove();
157
+ placeholder = null;
158
+ originalParent = null;
159
+ }
160
+ if (backdrop) {
161
+ backdrop.remove();
162
+ backdrop = null;
163
+ }
164
+ onClose?.();
165
+ };
166
+ const openEnlarged = () => {
167
+ if (isEnlarged) return;
168
+ isEnlarged = true;
169
+ backdrop = document.createElement("div");
170
+ backdrop.className = classes.debugBackdrop;
171
+ backdropContainer.appendChild(backdrop);
172
+ backdrop.addEventListener("click", closeEnlarged);
173
+ originalParent = logContainer.parentElement;
174
+ placeholder = document.createComment("debug-log-placeholder");
175
+ originalParent?.insertBefore(placeholder, logContainer);
176
+ backdropContainer.appendChild(logContainer);
177
+ logContainer.classList.add(classes.debugPanelEnlarged);
178
+ };
179
+ logContainer.addEventListener("mouseenter", () => {
180
+ onHoverStart?.();
181
+ if (isEnlarged) return;
182
+ if (hoverDelay > 0) {
183
+ hoverTimeout = setTimeout(() => {
184
+ openEnlarged();
185
+ }, hoverDelay);
186
+ }
187
+ });
188
+ logContainer.addEventListener("mouseleave", () => {
189
+ if (hoverTimeout) {
190
+ clearTimeout(hoverTimeout);
191
+ hoverTimeout = null;
192
+ }
193
+ if (!isEnlarged) {
194
+ onHoverEnd?.();
195
+ }
196
+ });
197
+ closeBtn.addEventListener("click", (e) => {
198
+ e.stopPropagation();
199
+ closeEnlarged();
200
+ });
112
201
  }
113
202
 
114
203
  // src/Panel.ts
204
+ var PIN_ICON_UNPINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
205
+ var PIN_ICON_PINNED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;transform:rotate(90deg)"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17H19V16L17 11V4H7L5 11V16Z"/><line x1="5" y1="11" x2="19" y2="11"/></svg>`;
115
206
  function createPanelElement(config, classes) {
116
207
  const element = document.createElement("div");
117
208
  element.className = classes.panel;
@@ -134,6 +225,14 @@ function createPanelElement(config, classes) {
134
225
  title.textContent = config.title;
135
226
  header.appendChild(title);
136
227
  }
228
+ let pinButton = null;
229
+ if (config.pinnable === true) {
230
+ pinButton = document.createElement("button");
231
+ pinButton.className = classes.pinButton;
232
+ pinButton.id = `${config.id}-pin-btn`;
233
+ pinButton.innerHTML = config.startPinned === true ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
234
+ header.appendChild(pinButton);
235
+ }
137
236
  let collapseButton = null;
138
237
  if (config.collapsible !== false) {
139
238
  collapseButton = document.createElement("button");
@@ -160,6 +259,7 @@ function createPanelElement(config, classes) {
160
259
  return {
161
260
  element,
162
261
  dragHandle: header,
262
+ pinButton,
163
263
  collapseButton,
164
264
  contentWrapper,
165
265
  detachGrip
@@ -168,12 +268,14 @@ function createPanelElement(config, classes) {
168
268
  function createPanelState(config, classes, globalConfig) {
169
269
  let element;
170
270
  let dragHandle;
271
+ let pinButton;
171
272
  let collapseButton;
172
273
  let contentWrapper;
173
274
  let detachGrip;
174
275
  if (config.element) {
175
276
  element = config.element;
176
277
  dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
278
+ pinButton = config.pinButton ?? element.querySelector(`.${classes.pinButton}`);
177
279
  collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
178
280
  contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
179
281
  detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
@@ -181,6 +283,7 @@ function createPanelState(config, classes, globalConfig) {
181
283
  const created = createPanelElement(config, classes);
182
284
  element = created.element;
183
285
  dragHandle = created.dragHandle;
286
+ pinButton = created.pinButton;
184
287
  collapseButton = created.collapseButton;
185
288
  contentWrapper = created.contentWrapper;
186
289
  detachGrip = created.detachGrip;
@@ -194,9 +297,11 @@ function createPanelState(config, classes, globalConfig) {
194
297
  id: config.id,
195
298
  element,
196
299
  dragHandle,
300
+ pinButton,
197
301
  collapseButton,
198
302
  contentWrapper,
199
303
  detachGrip,
304
+ isPinned: config.startPinned === true,
200
305
  isCollapsed: config.startCollapsed !== false,
201
306
  snappedTo: null,
202
307
  snappedFrom: null,
@@ -218,6 +323,14 @@ function toggleCollapse(state, classes, collapsed) {
218
323
  }
219
324
  return newState;
220
325
  }
326
+ function togglePin(state, pinned) {
327
+ const newState = pinned ?? !state.isPinned;
328
+ state.isPinned = newState;
329
+ if (state.pinButton) {
330
+ state.pinButton.innerHTML = newState ? PIN_ICON_PINNED : PIN_ICON_UNPINNED;
331
+ }
332
+ return newState;
333
+ }
221
334
  function showPanel(state, classes) {
222
335
  if (!state.isHidden) return;
223
336
  state.isHidden = false;
@@ -395,6 +508,28 @@ function snapPanels(leftPanel, rightPanel) {
395
508
  leftPanel.snappedTo = rightPanel.id;
396
509
  rightPanel.snappedFrom = leftPanel.id;
397
510
  }
511
+ function getMovingGroupRespectingPins(grabbedPanel, panels) {
512
+ if (grabbedPanel.isPinned) return [];
513
+ const fullGroup = getConnectedGroup(grabbedPanel, panels);
514
+ const grabbedIndex = fullGroup.indexOf(grabbedPanel);
515
+ const leftPanels = [];
516
+ for (let i = grabbedIndex; i >= 0; i--) {
517
+ if (fullGroup[i].isPinned) {
518
+ unsnap(fullGroup[i], fullGroup[i + 1]);
519
+ break;
520
+ }
521
+ leftPanels.unshift(fullGroup[i]);
522
+ }
523
+ const rightPanels = [];
524
+ for (let i = grabbedIndex + 1; i < fullGroup.length; i++) {
525
+ if (fullGroup[i].isPinned) {
526
+ unsnap(fullGroup[i - 1], fullGroup[i]);
527
+ break;
528
+ }
529
+ rightPanels.push(fullGroup[i]);
530
+ }
531
+ return [...leftPanels, ...rightPanels];
532
+ }
398
533
  function unsnap(leftPanel, rightPanel) {
399
534
  if (leftPanel.snappedTo === rightPanel.id) {
400
535
  leftPanel.snappedTo = null;
@@ -422,18 +557,18 @@ var DragManager = class {
422
557
  startDrag(e, panel, mode) {
423
558
  e.preventDefault();
424
559
  e.stopPropagation();
425
- const connectedPanels = getConnectedGroup(panel, this.panels);
426
- const initialGroupPositions = /* @__PURE__ */ new Map();
427
- for (const p of connectedPanels) {
428
- const rect2 = p.element.getBoundingClientRect();
429
- initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
430
- }
431
560
  let movingPanels;
432
561
  if (mode === "single") {
433
562
  detachFromGroup(panel, this.panels);
434
563
  movingPanels = [panel];
435
564
  } else {
436
- movingPanels = connectedPanels;
565
+ movingPanels = getMovingGroupRespectingPins(panel, this.panels);
566
+ if (movingPanels.length === 0) return;
567
+ }
568
+ const initialGroupPositions = /* @__PURE__ */ new Map();
569
+ for (const p of movingPanels) {
570
+ const rect2 = p.element.getBoundingClientRect();
571
+ initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
437
572
  }
438
573
  const rect = panel.element.getBoundingClientRect();
439
574
  this.activeDrag = {
@@ -901,6 +1036,7 @@ var SnapPreview = class {
901
1036
  var AutoHideManager = class {
902
1037
  constructor(panels, classes, callbacks) {
903
1038
  this.hideTimers = /* @__PURE__ */ new Map();
1039
+ this.pausedPanels = /* @__PURE__ */ new Set();
904
1040
  this.listenersAttached = false;
905
1041
  this.panels = panels;
906
1042
  this.classes = classes;
@@ -922,6 +1058,8 @@ var AutoHideManager = class {
922
1058
  */
923
1059
  handleActivity() {
924
1060
  for (const panel of this.panels.values()) {
1061
+ if (panel.isPinned) continue;
1062
+ if (this.pausedPanels.has(panel.id)) continue;
925
1063
  if (panel.resolvedAutoHideDelay !== void 0 || panel.isHidden) {
926
1064
  this.show(panel, "activity");
927
1065
  if (panel.resolvedAutoHideDelay !== void 0) {
@@ -951,6 +1089,23 @@ var AutoHideManager = class {
951
1089
  this.hideTimers.delete(panelId);
952
1090
  }
953
1091
  }
1092
+ /**
1093
+ * Pause auto-hide timer for a panel (e.g., during debug hover)
1094
+ */
1095
+ pauseTimer(panelId) {
1096
+ this.clearTimer(panelId);
1097
+ this.pausedPanels.add(panelId);
1098
+ }
1099
+ /**
1100
+ * Resume auto-hide timer for a panel
1101
+ */
1102
+ resumeTimer(panelId) {
1103
+ this.pausedPanels.delete(panelId);
1104
+ const panel = this.panels.get(panelId);
1105
+ if (panel && panel.resolvedAutoHideDelay !== void 0) {
1106
+ this.scheduleHide(panel);
1107
+ }
1108
+ }
954
1109
  /**
955
1110
  * Show a panel
956
1111
  */
@@ -964,9 +1119,27 @@ var AutoHideManager = class {
964
1119
  */
965
1120
  hide(panel, trigger) {
966
1121
  if (panel.isHidden) return;
1122
+ if (panel.isPinned) return;
967
1123
  hidePanel(panel, this.classes);
968
1124
  this.callbacks.onHide?.(panel, trigger);
969
1125
  }
1126
+ /**
1127
+ * Called when a panel's pin state changes.
1128
+ * Pinning cancels the hide timer and reveals the panel if hidden.
1129
+ * Unpinning restarts the timer if the panel participates in auto-hide.
1130
+ */
1131
+ onPanelPinChanged(panel) {
1132
+ if (panel.isPinned) {
1133
+ this.clearTimer(panel.id);
1134
+ if (panel.isHidden) {
1135
+ this.show(panel, "api");
1136
+ }
1137
+ } else {
1138
+ if (!panel.isHidden && panel.resolvedAutoHideDelay !== void 0) {
1139
+ this.scheduleHide(panel);
1140
+ }
1141
+ }
1142
+ }
970
1143
  /**
971
1144
  * Initialize a newly added panel's auto-hide state
972
1145
  */
@@ -1021,6 +1194,7 @@ function generateClasses(prefix) {
1021
1194
  panelContent: `${prefix}-content`,
1022
1195
  panelContentCollapsed: `${prefix}-content-collapsed`,
1023
1196
  detachGrip: `${prefix}-detach-grip`,
1197
+ pinButton: `${prefix}-pin-btn`,
1024
1198
  collapseButton: `${prefix}-collapse-btn`,
1025
1199
  snapPreview: `${prefix}-snap-preview`,
1026
1200
  snapPreviewVisible: `${prefix}-snap-preview-visible`,
@@ -1111,6 +1285,7 @@ var TabManager = class {
1111
1285
  id,
1112
1286
  element,
1113
1287
  dragHandle: options.dragHandle,
1288
+ pinButton: options.pinButton,
1114
1289
  collapseButton: options.collapseButton,
1115
1290
  contentWrapper: options.contentWrapper,
1116
1291
  detachGrip: options.detachGrip,
@@ -1157,58 +1332,38 @@ var TabManager = class {
1157
1332
  startCollapsed: config.startCollapsed ?? true
1158
1333
  };
1159
1334
  const state = this.addPanel(panelConfig);
1160
- state.element.classList.add(this.classes.debugPanel);
1161
- const closeBtn = document.createElement("button");
1162
- closeBtn.className = this.classes.debugClearButton;
1163
- closeBtn.textContent = "\xD7";
1164
- closeBtn.title = "Close enlarged view";
1165
- if (state.collapseButton) {
1166
- state.collapseButton.parentElement?.insertBefore(closeBtn, state.collapseButton);
1167
- }
1168
- elements.clearButton = closeBtn;
1169
1335
  this.debugPanelElements.set(state.id, elements);
1170
1336
  const debugPanel = createDebugPanelInterface(state, elements, config, this.classes);
1171
- let hoverTimeout = null;
1172
- let isEnlarged = false;
1173
- let backdrop = null;
1174
- const enlargedClass = this.classes.debugPanelEnlarged;
1175
- const backdropClass = this.classes.debugBackdrop;
1176
- const closeEnlarged = () => {
1177
- if (!isEnlarged) return;
1178
- isEnlarged = false;
1179
- state.element.classList.remove(enlargedClass);
1180
- if (backdrop) {
1181
- backdrop.remove();
1182
- backdrop = null;
1183
- }
1184
- };
1185
- const openEnlarged = () => {
1186
- if (isEnlarged) return;
1187
- isEnlarged = true;
1188
- backdrop = document.createElement("div");
1189
- backdrop.className = backdropClass;
1190
- this.config.container.appendChild(backdrop);
1191
- backdrop.addEventListener("click", closeEnlarged);
1192
- state.element.classList.add(enlargedClass);
1193
- };
1194
- state.element.addEventListener("mouseenter", () => {
1195
- if (isEnlarged) return;
1196
- hoverTimeout = setTimeout(() => {
1197
- openEnlarged();
1198
- }, 5e3);
1199
- });
1200
- state.element.addEventListener("mouseleave", () => {
1201
- if (hoverTimeout) {
1202
- clearTimeout(hoverTimeout);
1203
- hoverTimeout = null;
1204
- }
1205
- });
1206
- closeBtn.addEventListener("click", (e) => {
1207
- e.stopPropagation();
1208
- closeEnlarged();
1209
- });
1337
+ const hoverDelay = config.hoverDelay ?? 5e3;
1338
+ if (hoverDelay > 0) {
1339
+ setupHoverEnlarge({
1340
+ logContainer: elements.logContainer,
1341
+ hoverDelay,
1342
+ backdropContainer: this.config.container,
1343
+ classes: this.classes,
1344
+ onHoverStart: () => this.autoHideManager.pauseTimer(state.id),
1345
+ onHoverEnd: () => this.autoHideManager.resumeTimer(state.id),
1346
+ onClose: () => this.autoHideManager.resumeTimer(state.id)
1347
+ });
1348
+ }
1210
1349
  return debugPanel;
1211
1350
  }
1351
+ /**
1352
+ * Create an embeddable debug log in any container element
1353
+ */
1354
+ createDebugLog(container, config = {}) {
1355
+ const { debugLog, logContainer } = createDebugLog(container, config, this.classes);
1356
+ const hoverDelay = config.hoverDelay ?? 5e3;
1357
+ if (hoverDelay > 0) {
1358
+ setupHoverEnlarge({
1359
+ logContainer,
1360
+ hoverDelay,
1361
+ backdropContainer: this.config.container,
1362
+ classes: this.classes
1363
+ });
1364
+ }
1365
+ return debugLog;
1366
+ }
1212
1367
  /**
1213
1368
  * Set up event handlers for a panel
1214
1369
  */
@@ -1220,15 +1375,25 @@ var TabManager = class {
1220
1375
  this.emit("panel:collapse", { panel: state, isCollapsed: newState });
1221
1376
  });
1222
1377
  }
1378
+ if (state.pinButton) {
1379
+ state.pinButton.addEventListener("click", () => {
1380
+ const isPinned = togglePin(state);
1381
+ this.autoHideManager.onPanelPinChanged(state);
1382
+ this.emit("panel:pin", { panel: state, isPinned });
1383
+ });
1384
+ }
1223
1385
  if (state.detachGrip) {
1224
1386
  state.detachGrip.addEventListener("mousedown", (e) => {
1387
+ if (state.isPinned) return;
1225
1388
  this.dragManager.startDrag(e, state, "single");
1226
1389
  });
1227
1390
  }
1228
1391
  state.dragHandle.addEventListener("mousedown", (e) => {
1229
- if (e.target === state.collapseButton || e.target === state.detachGrip) {
1392
+ const target = e.target;
1393
+ if (e.target === state.collapseButton || e.target === state.detachGrip || state.pinButton && state.pinButton.contains(target)) {
1230
1394
  return;
1231
1395
  }
1396
+ if (state.isPinned) return;
1232
1397
  this.dragManager.startDrag(e, state, "group");
1233
1398
  });
1234
1399
  }
@@ -1476,6 +1641,7 @@ var TabManager = class {
1476
1641
  SnapPreview,
1477
1642
  TabManager,
1478
1643
  areInSameChain,
1644
+ createDebugLog,
1479
1645
  createDebugPanelContent,
1480
1646
  createDebugPanelInterface,
1481
1647
  createPanelElement,
@@ -1488,16 +1654,19 @@ var TabManager = class {
1488
1654
  getDefaultZIndex,
1489
1655
  getDragZIndex,
1490
1656
  getLeftmostPanel,
1657
+ getMovingGroupRespectingPins,
1491
1658
  getPanelDimensions,
1492
1659
  getPanelPosition,
1493
1660
  getRightmostPanel,
1494
1661
  hidePanel,
1495
1662
  setPanelPosition,
1496
1663
  setPanelZIndex,
1664
+ setupHoverEnlarge,
1497
1665
  showPanel,
1498
1666
  snapPanels,
1499
1667
  snapPanelsToTarget,
1500
1668
  toggleCollapse,
1669
+ togglePin,
1501
1670
  unsnap,
1502
1671
  updateSnappedPositions
1503
1672
  });