@andymcloid/trakk 1.0.1 → 1.0.2
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/README.md +14 -1
- package/dist/trakk.esm.js +1 -1
- package/dist/trakk.esm.js.map +1 -1
- package/dist/trakk.js +1 -1
- package/dist/trakk.js.map +1 -1
- package/package.json +1 -1
- package/src/trakk.js +213 -13
package/src/trakk.js
CHANGED
|
@@ -33,7 +33,8 @@ export class Trakk extends HTMLElement {
|
|
|
33
33
|
hideCursor: false,
|
|
34
34
|
disableDrag: false,
|
|
35
35
|
gridSnap: true,
|
|
36
|
-
grid: 1
|
|
36
|
+
grid: 1,
|
|
37
|
+
allowOverlap: false
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
// Callback functions
|
|
@@ -64,6 +65,7 @@ export class Trakk extends HTMLElement {
|
|
|
64
65
|
this.isPlaying = false;
|
|
65
66
|
this._scrollX = 0;
|
|
66
67
|
this._scrollY = 0;
|
|
68
|
+
this._selectedAction = null; // Currently selected action {action, row}
|
|
67
69
|
|
|
68
70
|
// DOM refs
|
|
69
71
|
this.timeAreaEl = null;
|
|
@@ -284,6 +286,165 @@ export class Trakk extends HTMLElement {
|
|
|
284
286
|
}));
|
|
285
287
|
}
|
|
286
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Select an action (block)
|
|
291
|
+
* @param {Object|null} action - The action to select, or null to deselect
|
|
292
|
+
* @param {Object|null} row - The row containing the action
|
|
293
|
+
*/
|
|
294
|
+
selectAction(action, row = null) {
|
|
295
|
+
// Remove previous selection
|
|
296
|
+
const prevSelected = this.querySelector('.timeline-editor-action.selected');
|
|
297
|
+
if (prevSelected) {
|
|
298
|
+
prevSelected.classList.remove('selected');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Update state
|
|
302
|
+
const previousSelection = this._selectedAction;
|
|
303
|
+
this._selectedAction = action ? { action, row } : null;
|
|
304
|
+
|
|
305
|
+
// Add selection class to new action
|
|
306
|
+
if (action) {
|
|
307
|
+
const actionEl = this.querySelector(`[data-action-id="${action.id}"]`);
|
|
308
|
+
if (actionEl) {
|
|
309
|
+
actionEl.classList.add('selected');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Emit select event
|
|
314
|
+
this._emitSelect(action, row);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get the currently selected action
|
|
319
|
+
* @returns {Object|null} - The selected action and row, or null
|
|
320
|
+
*/
|
|
321
|
+
getSelectedAction() {
|
|
322
|
+
return this._selectedAction;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Deselect any selected action
|
|
327
|
+
*/
|
|
328
|
+
deselectAction() {
|
|
329
|
+
this.selectAction(null, null);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Emit select event
|
|
334
|
+
*/
|
|
335
|
+
_emitSelect(action, row) {
|
|
336
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
337
|
+
detail: action ? { action, row } : null
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if a time range would overlap with other actions in the same row
|
|
343
|
+
* @param {Object} row - The row to check
|
|
344
|
+
* @param {string} actionId - The action being moved/resized (to exclude from check)
|
|
345
|
+
* @param {number} start - Proposed start time
|
|
346
|
+
* @param {number} end - Proposed end time
|
|
347
|
+
* @returns {boolean} - True if there would be an overlap
|
|
348
|
+
*/
|
|
349
|
+
_wouldOverlap(row, actionId, start, end) {
|
|
350
|
+
if (!row.blocks) return false;
|
|
351
|
+
|
|
352
|
+
for (const block of row.blocks) {
|
|
353
|
+
// Skip the action being moved/resized
|
|
354
|
+
if (block.id === actionId) continue;
|
|
355
|
+
|
|
356
|
+
// Check for overlap: ranges overlap if start1 < end2 AND end1 > start2
|
|
357
|
+
if (start < block.end && end > block.start) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get valid (non-overlapping) position for an action
|
|
366
|
+
* @param {Object} row - The row containing the action
|
|
367
|
+
* @param {string} actionId - The action being moved/resized
|
|
368
|
+
* @param {number} proposedStart - Proposed start time
|
|
369
|
+
* @param {number} proposedEnd - Proposed end time
|
|
370
|
+
* @param {string} mode - 'move' or 'resize-left' or 'resize-right'
|
|
371
|
+
* @returns {{start: number, end: number}} - Valid start and end times
|
|
372
|
+
*/
|
|
373
|
+
_getValidPosition(row, actionId, proposedStart, proposedEnd, mode = 'move') {
|
|
374
|
+
// If overlap is allowed, just return proposed values
|
|
375
|
+
if (this.config.allowOverlap) {
|
|
376
|
+
return { start: proposedStart, end: proposedEnd };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const duration = proposedEnd - proposedStart;
|
|
380
|
+
|
|
381
|
+
// If no overlap, return proposed values
|
|
382
|
+
if (!this._wouldOverlap(row, actionId, proposedStart, proposedEnd)) {
|
|
383
|
+
return { start: proposedStart, end: proposedEnd };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Find the blocking actions
|
|
387
|
+
const otherBlocks = (row.blocks || [])
|
|
388
|
+
.filter(b => b.id !== actionId)
|
|
389
|
+
.sort((a, b) => a.start - b.start);
|
|
390
|
+
|
|
391
|
+
if (mode === 'move') {
|
|
392
|
+
// For move: find the closest valid position
|
|
393
|
+
let bestStart = proposedStart;
|
|
394
|
+
let bestEnd = proposedEnd;
|
|
395
|
+
let minDistance = Infinity;
|
|
396
|
+
|
|
397
|
+
// Try snapping to left edge of each blocking action
|
|
398
|
+
for (const block of otherBlocks) {
|
|
399
|
+
// Try placing before this block
|
|
400
|
+
const candidateEnd = block.start;
|
|
401
|
+
const candidateStart = candidateEnd - duration;
|
|
402
|
+
if (candidateStart >= 0 && !this._wouldOverlap(row, actionId, candidateStart, candidateEnd)) {
|
|
403
|
+
const distance = Math.abs(proposedStart - candidateStart);
|
|
404
|
+
if (distance < minDistance) {
|
|
405
|
+
minDistance = distance;
|
|
406
|
+
bestStart = candidateStart;
|
|
407
|
+
bestEnd = candidateEnd;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Try placing after this block
|
|
412
|
+
const candidateStart2 = block.end;
|
|
413
|
+
const candidateEnd2 = candidateStart2 + duration;
|
|
414
|
+
if (!this._wouldOverlap(row, actionId, candidateStart2, candidateEnd2)) {
|
|
415
|
+
const distance = Math.abs(proposedStart - candidateStart2);
|
|
416
|
+
if (distance < minDistance) {
|
|
417
|
+
minDistance = distance;
|
|
418
|
+
bestStart = candidateStart2;
|
|
419
|
+
bestEnd = candidateEnd2;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { start: bestStart, end: bestEnd };
|
|
425
|
+
} else if (mode === 'resize-left') {
|
|
426
|
+
// For resize-left: find the maximum start we can have
|
|
427
|
+
let maxStart = proposedStart;
|
|
428
|
+
for (const block of otherBlocks) {
|
|
429
|
+
if (block.end > proposedStart && block.end <= proposedEnd) {
|
|
430
|
+
maxStart = Math.max(maxStart, block.end);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { start: maxStart, end: proposedEnd };
|
|
434
|
+
} else if (mode === 'resize-right') {
|
|
435
|
+
// For resize-right: find the minimum end we can have
|
|
436
|
+
let minEnd = proposedEnd;
|
|
437
|
+
for (const block of otherBlocks) {
|
|
438
|
+
if (block.start < proposedEnd && block.start >= proposedStart) {
|
|
439
|
+
minEnd = Math.min(minEnd, block.start);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { start: proposedStart, end: minEnd };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return { start: proposedStart, end: proposedEnd };
|
|
446
|
+
}
|
|
447
|
+
|
|
287
448
|
/**
|
|
288
449
|
* Expand timeline if an action extends beyond current bounds
|
|
289
450
|
* @param {number} endTime - The end time to check against
|
|
@@ -675,6 +836,14 @@ export class Trakk extends HTMLElement {
|
|
|
675
836
|
this._updateCursorPosition();
|
|
676
837
|
});
|
|
677
838
|
|
|
839
|
+
// Click on edit area (not on action) deselects
|
|
840
|
+
editArea.addEventListener('click', (e) => {
|
|
841
|
+
// Only deselect if clicking directly on edit area or row, not on an action
|
|
842
|
+
if (this._selectedAction) {
|
|
843
|
+
this.deselectAction();
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
678
847
|
return editArea;
|
|
679
848
|
}
|
|
680
849
|
|
|
@@ -1073,8 +1242,23 @@ export class Trakk extends HTMLElement {
|
|
|
1073
1242
|
actionEl.appendChild(deleteBtn);
|
|
1074
1243
|
}
|
|
1075
1244
|
|
|
1076
|
-
//
|
|
1245
|
+
// Select on mousedown (immediate feedback, only if track is not locked)
|
|
1246
|
+
if (!row.locked) {
|
|
1247
|
+
actionEl.addEventListener('mousedown', (e) => {
|
|
1248
|
+
// Ignore if clicking on resize handles or delete button
|
|
1249
|
+
if (e.target.classList.contains('timeline-editor-action-left-stretch') ||
|
|
1250
|
+
e.target.classList.contains('timeline-editor-action-right-stretch') ||
|
|
1251
|
+
e.target.classList.contains('timeline-editor-action-delete')) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
this.selectAction(action, row);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Click event (for callback only, selection handled in mousedown)
|
|
1077
1259
|
actionEl.addEventListener('click', (e) => {
|
|
1260
|
+
e.stopPropagation(); // Prevent deselection from edit area click
|
|
1261
|
+
|
|
1078
1262
|
if (this.callbacks.onClickAction) {
|
|
1079
1263
|
const rect = this.editAreaEl.getBoundingClientRect();
|
|
1080
1264
|
// Edit area starts at 0, account for contentPadding, add startLeft for correct time conversion
|
|
@@ -1623,8 +1807,14 @@ export class Trakk extends HTMLElement {
|
|
|
1623
1807
|
this.dragState.currentLeft = newLeft;
|
|
1624
1808
|
this.dragState.deltaX = this.dragState.deltaX % grid;
|
|
1625
1809
|
|
|
1626
|
-
const startTime = parserPixelToTime(newLeft, this.config);
|
|
1627
1810
|
const duration = action.end - action.start;
|
|
1811
|
+
let startTime = parserPixelToTime(newLeft, this.config);
|
|
1812
|
+
let endTime = startTime + duration;
|
|
1813
|
+
|
|
1814
|
+
// Check for overlap and get valid position
|
|
1815
|
+
const validPos = this._getValidPosition(row, action.id, startTime, endTime, 'move');
|
|
1816
|
+
startTime = validPos.start;
|
|
1817
|
+
endTime = validPos.end;
|
|
1628
1818
|
|
|
1629
1819
|
// Callback
|
|
1630
1820
|
if (this.callbacks.onActionMoving) {
|
|
@@ -1632,13 +1822,13 @@ export class Trakk extends HTMLElement {
|
|
|
1632
1822
|
action,
|
|
1633
1823
|
row,
|
|
1634
1824
|
start: startTime,
|
|
1635
|
-
end:
|
|
1825
|
+
end: endTime
|
|
1636
1826
|
});
|
|
1637
1827
|
if (result === false) return;
|
|
1638
1828
|
}
|
|
1639
1829
|
|
|
1640
1830
|
action.start = startTime;
|
|
1641
|
-
action.end =
|
|
1831
|
+
action.end = endTime;
|
|
1642
1832
|
|
|
1643
1833
|
// Expand timeline if action extends beyond current bounds
|
|
1644
1834
|
this._expandTimelineIfNeeded(action.end);
|
|
@@ -1683,20 +1873,25 @@ export class Trakk extends HTMLElement {
|
|
|
1683
1873
|
this.dragState.currentWidth = newWidth;
|
|
1684
1874
|
this.dragState.deltaX = this.dragState.deltaX % grid;
|
|
1685
1875
|
|
|
1686
|
-
|
|
1876
|
+
let startTime = Math.max(0, parserPixelToTime(newLeft, this.config));
|
|
1877
|
+
const endTime = action.end;
|
|
1878
|
+
|
|
1879
|
+
// Check for overlap and get valid position
|
|
1880
|
+
const validPos = this._getValidPosition(row, action.id, startTime, endTime, 'resize-left');
|
|
1881
|
+
startTime = validPos.start;
|
|
1687
1882
|
|
|
1688
1883
|
// Callback
|
|
1689
1884
|
if (this.callbacks.onActionResizing) {
|
|
1690
1885
|
const result = this.callbacks.onActionResizing({
|
|
1691
1886
|
action,
|
|
1692
1887
|
row,
|
|
1693
|
-
start:
|
|
1694
|
-
end:
|
|
1888
|
+
start: startTime,
|
|
1889
|
+
end: endTime
|
|
1695
1890
|
});
|
|
1696
1891
|
if (result === false) return;
|
|
1697
1892
|
}
|
|
1698
1893
|
|
|
1699
|
-
action.start =
|
|
1894
|
+
action.start = startTime;
|
|
1700
1895
|
|
|
1701
1896
|
this._updateActionElement(action, this.dragState.rowIndex);
|
|
1702
1897
|
}
|
|
@@ -1734,20 +1929,25 @@ export class Trakk extends HTMLElement {
|
|
|
1734
1929
|
this.dragState.deltaX = this.dragState.deltaX % grid;
|
|
1735
1930
|
|
|
1736
1931
|
const endPixel = this.dragState.currentLeft + newWidth;
|
|
1737
|
-
const
|
|
1932
|
+
const startTime = action.start;
|
|
1933
|
+
let endTime = Math.max(startTime + 0.1, parserPixelToTime(endPixel, this.config));
|
|
1934
|
+
|
|
1935
|
+
// Check for overlap and get valid position
|
|
1936
|
+
const validPos = this._getValidPosition(row, action.id, startTime, endTime, 'resize-right');
|
|
1937
|
+
endTime = validPos.end;
|
|
1738
1938
|
|
|
1739
1939
|
// Callback
|
|
1740
1940
|
if (this.callbacks.onActionResizing) {
|
|
1741
1941
|
const result = this.callbacks.onActionResizing({
|
|
1742
1942
|
action,
|
|
1743
1943
|
row,
|
|
1744
|
-
start:
|
|
1745
|
-
end:
|
|
1944
|
+
start: startTime,
|
|
1945
|
+
end: endTime
|
|
1746
1946
|
});
|
|
1747
1947
|
if (result === false) return;
|
|
1748
1948
|
}
|
|
1749
1949
|
|
|
1750
|
-
action.end =
|
|
1950
|
+
action.end = endTime;
|
|
1751
1951
|
|
|
1752
1952
|
// Expand timeline if action extends beyond current bounds
|
|
1753
1953
|
this._expandTimelineIfNeeded(action.end);
|