Skip to content

Commit e1f1c19

Browse files
authored
Merge pull request #110 from shotstack/fix/dbl-click
fix: move canvas double-click detection to Edit layer
2 parents 464e917 + 4be78b1 commit e1f1c19

9 files changed

Lines changed: 340 additions & 77 deletions

File tree

src/components/canvas/players/player.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export enum PlayerType {
5959
*/
6060
export abstract class Player extends Entity {
6161
private static readonly DiscardedFrameCount = 0;
62-
private static readonly DoubleClickThresholdMs = 350;
6362

6463
public layer: number;
6564
public shouldDispose: boolean;
@@ -493,13 +492,8 @@ export abstract class Player extends Entity {
493492
return this.getPlaybackTime() < Player.DiscardedFrameCount;
494493
}
495494

496-
/** Timestamp of last single-click on this player, used for double-click detection. */
497-
private lastClickAt = 0;
498-
499495
/**
500-
* Handle pointer down - emit click event for selection handling.
501-
* Two clicks within DoubleClickThresholdMs on the same player also emit
502-
* CanvasClipDoubleClicked so text-clip editing can be triggered from the canvas.
496+
* Handle pointer down — emit click event for selection handling.
503497
* All drag/resize/rotate interaction is handled by SelectionHandles.
504498
*/
505499
private onPointerDown(event: pixi.FederatedPointerEvent): void {
@@ -508,14 +502,6 @@ export abstract class Player extends Entity {
508502
}
509503

510504
this.edit.getInternalEvents().emit(InternalEvent.CanvasClipClicked, { player: this });
511-
512-
const now = performance.now();
513-
if (now - this.lastClickAt <= Player.DoubleClickThresholdMs) {
514-
this.edit.getInternalEvents().emit(InternalEvent.CanvasClipDoubleClicked, { player: this });
515-
this.lastClickAt = 0;
516-
} else {
517-
this.lastClickAt = now;
518-
}
519505
}
520506

521507
private clipHasKeyframes(): boolean {

src/core/edit-session.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2236,15 +2236,34 @@ export class Edit {
22362236
return bestMatch;
22372237
}
22382238

2239+
private lastClipClick: { player: Player; at: number } | null = null;
2240+
private static readonly DoubleClickThresholdMs = 500;
2241+
22392242
// ─── Intent Listeners ────────────────────────────────────────────────────────
22402243

22412244
private setupIntentListeners(): void {
22422245
this.internalEvents.on(InternalEvent.CanvasClipClicked, data => {
2243-
this.selectPlayer(data.player);
2246+
const wasSelected = this.getSelectedClipInfo()?.player === data.player;
2247+
2248+
if (!wasSelected) {
2249+
this.selectPlayer(data.player);
2250+
this.lastClipClick = null;
2251+
return;
2252+
}
2253+
2254+
const now = performance.now();
2255+
const within = this.lastClipClick && now - this.lastClipClick.at <= Edit.DoubleClickThresholdMs;
2256+
if (this.lastClipClick?.player === data.player && within) {
2257+
this.internalEvents.emit(InternalEvent.CanvasClipDoubleClicked, { player: data.player });
2258+
this.lastClipClick = null;
2259+
} else {
2260+
this.lastClipClick = { player: data.player, at: now };
2261+
}
22442262
});
22452263

22462264
this.internalEvents.on(InternalEvent.CanvasBackgroundClicked, () => {
22472265
this.clearSelection();
2266+
this.lastClipClick = null;
22482267
});
22492268
}
22502269

src/core/ui/base-toolbar.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ export const TOOLBAR_ICONS = {
4343
chevron: `<path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/>`,
4444
transition: `<path d="M12 3v18"/><path d="M5 12H2l3-3 3 3H5"/><path d="M19 12h3l-3 3-3-3h3"/>`,
4545
effect: `<circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M1 12h4M19 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>`,
46-
trash: `<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>`,
47-
textCursor: `<path d="M12 4v16"/><path d="M8 4h8"/><path d="M8 20h8"/>`
46+
trash: `<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>`
4847
};
4948

5049
/**
@@ -256,9 +255,11 @@ export abstract class BaseToolbar {
256255
*/
257256
protected setupOutsideClickHandler(): void {
258257
this.clickOutsideHandler = (e: MouseEvent) => {
259-
if (!this.container?.contains(e.target as Node)) {
260-
this.closeAllPopups();
261-
}
258+
const target = e.target as HTMLElement | null;
259+
if (!target) return;
260+
if (this.container?.contains(target)) return;
261+
if (target.tagName === "CANVAS") return;
262+
this.closeAllPopups();
262263
};
263264
document.addEventListener("click", this.clickOutsideHandler);
264265
}

src/core/ui/rich-text-toolbar.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { injectShotstackStyles } from "@styles/inject";
88
import { GOOGLE_FONTS_BY_FILENAME } from "../fonts/google-fonts";
99

1010
import { BackgroundColorPicker } from "./background-color-picker";
11-
import { BaseToolbar, FONT_SIZES, TOOLBAR_ICONS } from "./base-toolbar";
11+
import { BaseToolbar, FONT_SIZES } from "./base-toolbar";
1212
import { EffectPanel } from "./composites/EffectPanel";
1313
import { SpacingPanel } from "./composites/SpacingPanel";
1414
import { StylePanel, type StylePanelOptions } from "./composites/StylePanel";
@@ -127,24 +127,6 @@ export class RichTextToolbar extends BaseToolbar {
127127
</div>
128128
<div class="ss-toolbar-mode-divider"></div>
129129
130-
<div class="ss-toolbar-dropdown">
131-
<button data-action="text-edit-toggle" class="ss-toolbar-btn ss-toolbar-btn--text-edit" title="Edit text">
132-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
133-
${TOOLBAR_ICONS.textCursor}
134-
</svg>
135-
<span>Edit text</span>
136-
</button>
137-
<div data-text-edit-popup class="ss-toolbar-popup ss-toolbar-popup--text-edit">
138-
<div class="ss-toolbar-popup-header">Edit Text</div>
139-
<div class="ss-toolbar-text-area-wrapper">
140-
<textarea data-text-edit-area class="ss-toolbar-text-area" rows="4" placeholder="Enter text..."></textarea>
141-
<div class="ss-autocomplete-popup" data-autocomplete-popup>
142-
<div class="ss-autocomplete-items" data-autocomplete-items></div>
143-
</div>
144-
</div>
145-
</div>
146-
</div>
147-
148130
<div class="ss-toolbar-group ss-toolbar-group--bordered">
149131
<button data-action="size-down" class="ss-toolbar-btn" title="Decrease font size">
150132
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -203,6 +185,23 @@ export class RichTextToolbar extends BaseToolbar {
203185
204186
<div class="ss-toolbar-divider"></div>
205187
188+
<div class="ss-toolbar-dropdown">
189+
<button data-action="text-edit-toggle" class="ss-toolbar-btn ss-toolbar-btn--text-edit ss-toolbar-btn--primary" title="Edit text">
190+
<span>Edit text</span>
191+
</button>
192+
<div data-text-edit-popup class="ss-toolbar-popup ss-toolbar-popup--text-edit">
193+
<div class="ss-toolbar-popup-header">Edit Text</div>
194+
<div class="ss-toolbar-text-area-wrapper">
195+
<textarea data-text-edit-area class="ss-toolbar-text-area" rows="4" placeholder="Enter text..."></textarea>
196+
<div class="ss-autocomplete-popup" data-autocomplete-popup>
197+
<div class="ss-autocomplete-items" data-autocomplete-items></div>
198+
</div>
199+
</div>
200+
</div>
201+
</div>
202+
203+
<div class="ss-toolbar-divider"></div>
204+
206205
<!-- Formatting Group -->
207206
<button data-action="align-cycle" class="ss-toolbar-btn" title="Text alignment">
208207
<svg data-align-icon width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
@@ -835,12 +834,10 @@ export class RichTextToolbar extends BaseToolbar {
835834

836835
// Double-clicking the canvas text opens the edit popup for the current
837836
// selection — same path as the "Edit text" toolbar button.
838-
this.unsubCanvasDoubleClick = this.edit.getInternalEvents().on(InternalEvent.CanvasClipDoubleClicked, ({ player }) => {
837+
this.unsubCanvasDoubleClick = this.edit.getInternalEvents().on(InternalEvent.CanvasClipDoubleClicked, () => {
839838
if (this.selectedTrackIdx < 0 || this.selectedClipIdx < 0) return;
840-
const selectedClipId = this.edit.getClipId(this.selectedTrackIdx, this.selectedClipIdx);
841-
if (selectedClipId && player.clipId === selectedClipId) {
842-
this.openTextEditPopup();
843-
}
839+
if (!this.container || !this.container.classList.contains("visible")) return;
840+
this.openTextEditPopup();
844841
});
845842
}
846843

@@ -1133,13 +1130,12 @@ export class RichTextToolbar extends BaseToolbar {
11331130
}
11341131

11351132
/** Open the edit-text popup and focus its textarea. Idempotent if already open. */
1136-
public openTextEditPopup(): void {
1133+
private openTextEditPopup(): void {
11371134
if (!this.isPopupOpen(this.textEditPopup)) {
11381135
this.toggleTextEditPopup();
1139-
} else {
1140-
this.textEditArea?.focus();
1141-
this.textEditArea?.select();
11421136
}
1137+
this.textEditArea?.focus();
1138+
this.textEditArea?.select();
11431139
}
11441140

11451141
private debouncedApplyTextEdit(): void {

src/core/ui/selection-handles.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@ import {
99
detectCornerZone,
1010
detectEdgeZone
1111
} from "@core/interaction/clip-interaction";
12-
import { SELECTION_CONSTANTS, CURSOR_BASE_ANGLES, type CornerName, buildResizeCursor, buildRotationCursor, calculateHitArea } from "@core/interaction/selection-overlay";
13-
import { type ClipBounds, createClipBounds, createSnapContext, filterContainedClips, snap, snapRotation, visualToLogical } from "@core/interaction/snap-system";
12+
import {
13+
SELECTION_CONSTANTS,
14+
CURSOR_BASE_ANGLES,
15+
type CornerName,
16+
buildResizeCursor,
17+
buildRotationCursor,
18+
calculateHitArea
19+
} from "@core/interaction/selection-overlay";
20+
import {
21+
type ClipBounds,
22+
createClipBounds,
23+
createSnapContext,
24+
filterContainedClips,
25+
snap,
26+
snapRotation,
27+
visualToLogical
28+
} from "@core/interaction/snap-system";
1429
import { updateSvgViewBox, isSimpleRectSvg } from "@core/shared/svg-utils";
1530
import { Pointer } from "@inputs/pointer";
1631
import type { Size, Vector } from "@layouts/geometry";
@@ -456,7 +471,13 @@ export class SelectionHandles implements CanvasOverlayRegistration {
456471

457472
// Check if inside player bounds for drag
458473
if (localPoint.x >= 0 && localPoint.x <= size.width && localPoint.y >= 0 && localPoint.y <= size.height) {
474+
this.edit.getInternalEvents().emit(InternalEvent.CanvasClipClicked, { player: this.selectedPlayer });
459475
this.startDrag(event);
476+
return;
477+
}
478+
479+
if (event.target === this.outline || event.target === this.app?.stage) {
480+
this.edit.getInternalEvents().emit(InternalEvent.CanvasBackgroundClicked);
460481
}
461482
}
462483

@@ -536,8 +557,7 @@ export class SelectionHandles implements CanvasOverlayRegistration {
536557
}
537558

538559
// Auto-set fit to "contain" for image/video clips when resizing
539-
if ((this.scaleDirection || this.edgeDragDirection) &&
540-
(finalClip.asset?.type === "image" || finalClip.asset?.type === "video")) {
560+
if ((this.scaleDirection || this.edgeDragDirection) && (finalClip.asset?.type === "image" || finalClip.asset?.type === "video")) {
541561
finalClip.fit = "contain";
542562
this.edit.updateClipInDocument(this.selectedClipId, { fit: "contain" });
543563
this.edit.resolveClip(this.selectedClipId);

src/core/ui/text-toolbar.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,6 @@ export class TextToolbar extends BaseToolbar {
104104
</div>
105105
<div class="ss-toolbar-mode-divider"></div>
106106
107-
<div class="ss-toolbar-dropdown">
108-
<button data-action="text-edit-toggle" class="ss-toolbar-btn ss-toolbar-btn--text-edit" title="Edit text">
109-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
110-
${TOOLBAR_ICONS.textCursor}
111-
</svg>
112-
<span>Edit text</span>
113-
</button>
114-
<div data-text-edit-popup class="ss-toolbar-popup ss-toolbar-popup--text-edit">
115-
<div class="ss-toolbar-popup-header">Edit Text</div>
116-
<div class="ss-toolbar-text-area-wrapper">
117-
<textarea data-text-edit-area class="ss-toolbar-text-area" rows="4" placeholder="Enter text..."></textarea>
118-
</div>
119-
</div>
120-
</div>
121-
122107
<div class="ss-toolbar-group ss-toolbar-group--bordered">
123108
<button data-action="size-down" class="ss-toolbar-btn" title="Decrease font size">
124109
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -195,6 +180,20 @@ export class TextToolbar extends BaseToolbar {
195180
196181
<div class="ss-toolbar-divider"></div>
197182
183+
<div class="ss-toolbar-dropdown">
184+
<button data-action="text-edit-toggle" class="ss-toolbar-btn ss-toolbar-btn--text-edit ss-toolbar-btn--primary" title="Edit text">
185+
<span>Edit text</span>
186+
</button>
187+
<div data-text-edit-popup class="ss-toolbar-popup ss-toolbar-popup--text-edit">
188+
<div class="ss-toolbar-popup-header">Edit Text</div>
189+
<div class="ss-toolbar-text-area-wrapper">
190+
<textarea data-text-edit-area class="ss-toolbar-text-area" rows="4" placeholder="Enter text..."></textarea>
191+
</div>
192+
</div>
193+
</div>
194+
195+
<div class="ss-toolbar-divider"></div>
196+
198197
<button data-action="align-cycle" class="ss-toolbar-btn" title="Text alignment">
199198
<svg data-align-icon width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
200199
${TOOLBAR_ICONS.alignCenter}
@@ -343,7 +342,7 @@ export class TextToolbar extends BaseToolbar {
343342
}
344343

345344
/** Open the edit-text popup and focus its textarea. Idempotent if already open. */
346-
public openTextEditPopup(): void {
345+
private openTextEditPopup(): void {
347346
if (!this.isPopupOpen(this.textEditPopup)) {
348347
this.togglePopup(this.textEditPopup);
349348
}
@@ -390,14 +389,13 @@ export class TextToolbar extends BaseToolbar {
390389
this.strokeColorInput?.addEventListener("input", () => this.handleStrokeChange());
391390

392391
// Double-clicking the text on the canvas opens the edit popup — same
393-
// path as clicking the toolbar's "Edit text" button. Guarded against
394-
// firing for clips other than the currently-selected one.
395-
this.unsubCanvasDoubleClick = this.edit.getInternalEvents().on(InternalEvent.CanvasClipDoubleClicked, ({ player }) => {
392+
// path as clicking the toolbar's "Edit text" button. The Edit layer only
393+
// emits CanvasClipDoubleClicked for the *current* selection, so the
394+
// toolbar just needs to be visible (i.e. it has an active selection).
395+
this.unsubCanvasDoubleClick = this.edit.getInternalEvents().on(InternalEvent.CanvasClipDoubleClicked, () => {
396396
if (this.selectedTrackIdx < 0 || this.selectedClipIdx < 0) return;
397-
const selectedClipId = this.edit.getClipId(this.selectedTrackIdx, this.selectedClipIdx);
398-
if (selectedClipId && player.clipId === selectedClipId) {
399-
this.openTextEditPopup();
400-
}
397+
if (!this.container || !this.container.classList.contains("visible")) return;
398+
this.openTextEditPopup();
401399
});
402400

403401
// Mount composite panels

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Edit as EditSchema } from "@schemas";
22
import { Timeline } from "@timeline/index";
33

4-
import template from "./templates/caption.json";
4+
import template from "./templates/test.json";
55

66
import { Edit, Canvas, Controls, UIController } from "./index";
77

src/styles/ui/rich-text-toolbar.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,23 @@
464464
white-space: nowrap;
465465
}
466466

467+
.ss-toolbar-btn.ss-toolbar-btn--primary {
468+
padding: 0 16px;
469+
font-size: 13px;
470+
font-weight: 600;
471+
letter-spacing: -0.01em;
472+
color: rgba(255, 255, 255, 0.95);
473+
background: rgba(255, 255, 255, 0.08);
474+
}
475+
.ss-toolbar-btn.ss-toolbar-btn--primary:hover {
476+
background: rgba(255, 255, 255, 0.16);
477+
color: #fff;
478+
}
479+
.ss-toolbar-btn.ss-toolbar-btn--primary.active {
480+
background: rgba(255, 255, 255, 0.22);
481+
color: #fff;
482+
}
483+
467484
.ss-toolbar-popup--text-edit {
468485
min-width: 280px;
469486
padding: 14px 16px;

0 commit comments

Comments
 (0)