Learnify - PanTool Enhancement Implementation Guide

Overview

This document provides a detailed record of the enhancement process for PanTool, upgrading it from a simple panning tool to a comprehensive tool that supports complete mind map interactions. This enhancement maintains the original panning functionality while adding complete interaction support including hover, click, double-click, and more.

Original PanTool Analysis

Functional Limitations

The original PanTool located at blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts only implemented basic panning functionality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export class PanTool extends BaseTool<PanToolOption> {
static override toolName = 'pan';

private _lastPoint: [number, number] | null = null;
readonly panning$ = new Signal<boolean>(false);

// Only drag-related methods
override dragStart(e: PointerEventState): void {
this._lastPoint = [e.x, e.y];
this.panning$.value = true;
}

override dragMove(e: PointerEventState): void {
if (!this._lastPoint) return;
const { viewport } = this.gfx;
const { zoom } = viewport;
const [lastX, lastY] = this._lastPoint;
const deltaX = lastX - e.x;
const deltaY = lastY - e.y;
this._lastPoint = [e.x, e.y];
viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
}

override dragEnd(_: PointerEventState): void {
this._lastPoint = null;
this.panning$.value = false;
}

// Missing key methods:
// - pointerMove (hover functionality)
// - click (click interaction)
// - pointerDown/pointerUp
// - doubleClick
// - interactivity system integration
}

Problem Analysis

  1. Incomplete event handling: Only processes drag events, ignoring other interaction events
  2. Missing interactivity integration: No integration with BlockSuite's interaction system
  3. Single functionality: Can only pan, unable to perform other operations

Enhancement Design Goals

Functional Goals

  1. Maintain original functionality: Panning functionality remains completely unaffected
  2. Add interaction support: Complete interactions including hover, click, double-click, etc.
  3. Mind map compatibility: Especially mind map expand/collapse functionality
  4. Performance optimization: Avoid unnecessary event processing overhead

Design Principles

  1. Backward compatibility: Do not break existing panning logic
  2. Event dispatch priority: Prioritize dispatching to interactivity system, then handle tool-specific logic
  3. Code reuse: Reuse mature patterns from DefaultTool
  4. Clear responsibility separation: Separate panning logic vs interaction logic

Implementation Steps

1. Import Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Original imports
import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std';
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
import { Signal } from '@preact/signals-core';

// New imports
import {
BaseTool,
InteractivityIdentifier, // New - interactivity system identifier
MouseButton,
type ToolOptions,
} from '@blocksuite/std/gfx';
import { signal } from '@preact/signals-core'; // Updated - using new signal API

Change Notes:

  • Added InteractivityIdentifier for accessing the interaction system
  • Changed Signal to signal to use the new API
  • Organized import order to comply with linting rules

2. Add Interactivity System Support

1
2
3
4
5
6
7
8
export class PanTool extends BaseTool<PanToolOption> {
// ... existing properties ...

// New - interactivity system accessor
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}
}

Key Functions:

  • Provides access to BlockSuite's interaction system
  • Enables the tool to dispatch events to the Extension system
  • Reuses existing interaction logic without reimplementation

3. Enhanced Drag Event Handling

Original Implementation

1
2
3
4
override dragEnd(_: PointerEventState): void {
this._lastPoint = null;
this.panning$.value = false;
}

Enhanced Implementation

1
2
3
4
5
6
7
8
override dragEnd(e: PointerEventState): void {
// Prioritize dispatching to interactivity system
this.interactivity?.dispatchEvent('dragend', e);

// Then handle panning-related logic
this._lastPoint = null;
this.panning$.value = false;
}

Design Considerations:

  • Event dispatch priority: Let interactivity system handle first, as other Extensions might need this event
  • Maintain original logic: Panning state management remains unchanged
  • Parameter usage: Correctly use event parameters to avoid linter warnings

4. Add Complete Interaction Methods

click - Click Interaction

1
2
3
4
override click(e: PointerEventState): void {
// Dispatch click events to interactivity system for mind map expand/collapse functionality
this.interactivity?.dispatchEvent('click', e);
}

Functionality:

  • Mind map node expand/collapse
  • Button click responses
  • Selection operations

pointerMove - Hover Functionality

1
2
3
4
override pointerMove(e: PointerEventState): void {
// Dispatch pointer move events to interactivity system for hover functionality
this.interactivity?.dispatchEvent('pointermove', e);
}

Functionality:

  • Element hover highlighting
  • Collapse button show/hide
  • Cursor style changes
  • Tooltip display

pointerDown/pointerUp - Complete Mouse Lifecycle

1
2
3
4
5
6
7
8
9
override pointerDown(e: PointerEventState): void {
// Dispatch pointerdown events to interactivity system
this.interactivity?.dispatchEvent('pointerdown', e);
}

override pointerUp(e: PointerEventState): void {
// Dispatch pointerup events to interactivity system
this.interactivity?.dispatchEvent('pointerup', e);
}

Functionality:

  • Press/release state management
  • Drag preparation and ending
  • Complex interaction state machines

doubleClick - Double-Click Operations

1
2
3
4
override doubleClick(e: PointerEventState): void {
// Dispatch double-click events to interactivity system
this.interactivity?.dispatchEvent('dblclick', e);
}

Functionality:

  • Quick editing
  • Special operation triggers
  • Context-related double-click behaviors

5. Signal System Update

Original Implementation

1
readonly panning$ = new Signal<boolean>(false);

Updated Implementation

1
readonly panning$ = signal(false);

Reasons for Change:

  • Use the new signal API
  • More concise syntax
  • Better type inference

How It Works

Event Handling Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sequenceDiagram
participant User as User Operation
participant Browser as Browser
participant TC as ToolController
participant PT as PanTool
participant IS as Interactivity System
participant MV as MindMapView

User->>Browser: Mouse move
Browser->>TC: pointermove event
TC->>PT: pointerMove(e)
PT->>IS: dispatchEvent('pointermove', e)
IS->>MV: Handle hover logic
MV->>Browser: Update visual feedback

User->>Browser: Drag start
Browser->>TC: dragstart event
TC->>PT: dragStart(e)
PT->>PT: Set _lastPoint, panning$=true

User->>Browser: Drag move
Browser->>TC: dragmove event
TC->>PT: dragMove(e)
PT->>PT: Calculate delta and pan view

User->>Browser: Drag end
Browser->>TC: dragend event
TC->>PT: dragEnd(e)
PT->>IS: dispatchEvent('dragend', e)
PT->>PT: Clean state _lastPoint=null, panning$=false

Maintaining Panning Functionality

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override dragMove(e: PointerEventState): void {
// Panning functionality completely maintains original logic
if (!this._lastPoint) return;

const { viewport } = this.gfx;
const { zoom } = viewport;

const [lastX, lastY] = this._lastPoint;
const deltaX = lastX - e.x;
const deltaY = lastY - e.y;

this._lastPoint = [e.x, e.y];

viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
}

Reasons for Maintaining Unchanged:

  • Panning logic is already mature
  • Performance optimization is good
  • User experience is consistent

Enhanced Interaction Functionality

Mind map interactions now work properly:

  1. Hover effects: pointerMoveinteractivityMindMapView → Show collapse button
  2. Click expand: clickinteractivity → Related Extension → Expand/collapse node
  3. Cursor changes: Cursor becomes pointer on hover, returns to default on leave

Testing and Validation

Functional Testing

1. Panning Functionality Test

1
2
3
4
5
6
7
8
9
10
// Test drag panning
const startEvent = { x: 100, y: 100 };
const moveEvent = { x: 150, y: 120 };

panTool.dragStart(startEvent);
panTool.dragMove(moveEvent);

// Verify view actually panned
assert(viewport.centerX !== originalCenterX);
assert(viewport.centerY !== originalCenterY);

2. Interaction Functionality Test

1
2
3
4
5
6
7
8
9
10
11
12
// Test hover functionality
const hoverEvent = { x: nodeX, y: nodeY };
panTool.pointerMove(hoverEvent);

// Verify collapse button display
assert(collapseButton.opacity > 0);

// Test click functionality
panTool.click(hoverEvent);

// Verify node state change
assert(node.collapsed !== originalCollapsed);

Performance Testing

1. Event Handling Overhead

1
2
3
4
5
6
const startTime = performance.now();
for (let i = 0; i < 1000; i++) {
panTool.pointerMove({ x: i % 100, y: i % 100 });
}
const endTime = performance.now();
console.log(`1000 pointerMove events: ${endTime - startTime}ms`);

2. Memory Usage

  • No significant memory overhead added
  • Reuses existing interactivity system
  • Event dispatch is lightweight

Compatibility Considerations

Backward Compatibility

  1. API compatibility: All existing public methods and properties remain unchanged
  2. Behavior compatibility: Panning behavior is completely consistent
  3. Middle-click panning: Temporary panning functionality maintains original logic

Middle-Click Panning Functionality

The existing middle-click panning functionality in the mounted() method remains completely unchanged:

1
2
3
4
5
6
7
8
9
10
11
override mounted(): void {
this.addHook('pointerDown', evt => {
const shouldPanWithMiddle = evt.raw.button === MouseButton.MIDDLE;

if (!shouldPanWithMiddle) {
return;
}

// ... middle-click panning logic remains unchanged ...
});
}

Debugging and Monitoring

Event Tracking

Debug code added during development:

1
2
3
4
5
6
7
8
9
override pointerMove(e: PointerEventState): void {
console.log('PanTool.pointerMove - dispatching to interactivity');
this.interactivity?.dispatchEvent('pointermove', e);
}

override click(e: PointerEventState): void {
console.log('PanTool.click - dispatching to interactivity');
this.interactivity?.dispatchEvent('click', e);
}

These logs help confirm events are properly dispatched.

Performance Monitoring

1
2
3
4
5
6
7
8
9
private _eventCount = 0;

override pointerMove(e: PointerEventState): void {
this._eventCount++;
if (this._eventCount % 100 === 0) {
console.log(`PanTool processed ${this._eventCount} pointer moves`);
}
this.interactivity?.dispatchEvent('pointermove', e);
}

Known Issues and Solutions

1. Linter Errors

Issue: Unused parameter warnings

1
override dragEnd(_: PointerEventState): void // '_' is defined but never used

Solution: Properly use parameters for event dispatch

1
2
3
4
override dragEnd(e: PointerEventState): void {
this.interactivity?.dispatchEvent('dragend', e);
// ...
}

2. Import Order

Issue: ESLint import sorting warnings

Solution: Organize imports in alphabetical order

1
import { BaseTool, InteractivityIdentifier, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';

3. Signal API Update

Issue: Old Signal constructor

1
readonly panning$ = new Signal<boolean>(false); // Outdated API

Solution: Use new signal function

1
readonly panning$ = signal(false); // Modern API

Best Practices Summary

Tool Design Principles

  1. Functional completeness: Tools should provide complete user interaction experience
  2. Event dispatch priority: Prioritize dispatching to interactivity system, then handle tool-specific logic
  3. Backward compatibility: Keep existing functionality unaffected
  4. Performance considerations: Avoid unnecessary calculations and DOM operations

Code Organization

  1. Clear method separation: Panning logic vs interaction logic
  2. Consistent naming: Follow BlockSuite naming conventions
  3. Adequate comments: Explain the purpose and design considerations of each method

Testing Strategy

  1. Functional testing: Ensure all functionality works properly
  2. Performance testing: Verify performance is not significantly affected
  3. Compatibility testing: Ensure backward compatibility
  4. User experience testing: Verify consistent interaction experience

File Comparison

Before vs After Modifications

Functionality Before After
Panning functionality ✅ Complete ✅ Unchanged
Hover effects ❌ None ✅ Full support
Click interaction ❌ None ✅ Full support
Double-click ops ❌ None ✅ Full support
Middle-click panning ✅ Complete ✅ Unchanged
Interactivity integration ❌ None ✅ Fully integrated
  • blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts - Enhanced PanTool implementation
  • blocksuite/affine/blocks/surface/src/tool/default-tool.ts - Reference implementation
  • blocksuite/framework/std/src/gfx/tool/tool-controller.ts - Tool controller
  • blocksuite/affine/gfx/mindmap/src/view/view.ts - Mind map view implementation

Reference Documentation