Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ yarn format # 代码格式化

## Active Technologies
- TypeScript ~5.6.3 + @system-ui-js/multi-drag, @system-ui-js/development-base, BroadcastChannel API (001-manager-dispatch)
- TypeScript ~5.6.3 + @system-ui-js/multi-drag, BroadcastChannel API, @system-ui-js/development-base (002-magicdrag-refactor)
- TypeScript 5.6.3 + @system-ui-js/multi-drag, BroadcastChannel API, @system-ui-js/development-base (002-magicdrag-refactor)

## Recent Changes
- 001-manager-dispatch: Added TypeScript ~5.6.3 + @system-ui-js/multi-drag, @system-ui-js/development-base, BroadcastChannel API
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

## Unreleased
## [UnReleased]

### Added
- MagicDrag 扩展点支持静态 onEnterTab 与实例 onOtherTabDrag* 回调
- 引入 coordinator 以解耦 MagicDrag 与 Manager 之间的直接依赖
- 新增跨 Tab 回调与同类实例广播测试覆盖
- 支持按通道名注册事件监听器,并提供全局事件分发接口
- MagicDrag 子类可通过静态 channelName 指定跨标签页通信通道

### Changed
- Manager 按 className/channelName 管理注册表并拒绝通道冲突
- BroadcastChannel 不可用时降级为单 Tab 行为并记录诊断日志
- 未注册 className 的跨 Tab 消息直接忽略并记录 warning
- Manager 统一分发拖拽信号并在无监听器时记录可观测日志
- 跨标签页拖拽 activeTabId 在拖拽开始与进入目标标签页时更新

### Fixed
- 测试覆盖完整的跨 Tab 进入与回调流程
- 移除无用的测试文件 `src/__tests__/sum.test.ts`
- 修复 tsconfig.build.json 配置冲突:将 `noEmit: false` 改为 `emitDeclarationOnly: true`,解决 `allowImportingTsExtensions` 只能在 `noEmit` 或 `emitDeclarationOnly` 时使用的 TypeScript 编译错误
- 修复 ESLint 错误:将 `MagicDrag.channelName` 改为 `readonly`,符合 sonarjs/public-static-readonly 规则
- 修复 ESLint 错误:移除冗余的类型别名 `MagicDragAnyEventListenerStore`,符合 sonarjs/redundant-type-aliases 规则
- 修复拖出所有 Tab 时无法触发 DRAG_ABORT 的问题:在 handleExternalDragMove 中,当鼠标离开当前 Tab 时,如果 `activeTabId` 等于当前 Tab ID,则将其设为 null,确保 MagicDrag.handleDragEnd 能正确判断 abort 条件
- 修复跨 Tab 拖拽时 `onEnterTab` 回调不会被调用的问题:移除 `handleExternalDragEnterTab` 中不必要的 `sourceTabId` 守卫检查,因为该消息已通过初始过滤且语义本身就要求目标 Tab 响应进入事件
- 修复 ESLint 错误:将测试文件中的 `TestCard.onEnterTab` 和 `MinimalCard.channelName` 改为 `readonly`,符合 sonarjs/public-static-readonly 规则
- 修复 TypeScript 类型错误:将 `DemoCard.getClassName()` 从 `protected` 改为 `public`,以匹配基类中的 `public abstract getClassName()` 声明

## 0.1.0 - 2025-11-16

Expand Down
303 changes: 303 additions & 0 deletions specs/002-magicdrag-refactor/contracts/magic-drag-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
openapi: 3.0.3
info:
title: MagicDrag Cross-Tab Events
version: "0.1"
paths:
/events/drag-start:
post:
summary: Broadcast drag start event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragStartMessage"
responses:
"202":
description: Accepted
/events/drag-move:
post:
summary: Broadcast drag move event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragMoveMessage"
responses:
"202":
description: Accepted
/events/drag-end:
post:
summary: Broadcast drag end event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragEndMessage"
responses:
"202":
description: Accepted
/events/drag-enter-tab:
post:
summary: Broadcast drag enter event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragEnterTabMessage"
responses:
"202":
description: Accepted
/events/drag-leave-tab:
post:
summary: Broadcast drag leave event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragLeaveTabMessage"
responses:
"202":
description: Accepted
/events/drag-drop:
post:
summary: Broadcast drag drop event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragDropMessage"
responses:
"202":
description: Accepted
/events/drag-abort:
post:
summary: Broadcast drag abort event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DragAbortMessage"
responses:
"202":
description: Accepted
/events/heartbeat:
post:
summary: Broadcast heartbeat event to other tabs
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/HeartbeatMessage"
responses:
"202":
description: Accepted
components:
schemas:
ScreenPosition:
type: object
required: [screenX, screenY]
properties:
screenX:
type: number
screenY:
type: number
DragOffset:
type: object
required: [x, y]
properties:
x:
type: number
y:
type: number
Pose:
type: object
required: [x, y, width, height, rotate, scaleX, scaleY]
properties:
x: { type: number }
y: { type: number }
width: { type: number }
height: { type: number }
rotate: { type: number }
scaleX: { type: number }
scaleY: { type: number }
SerializedData:
type: object
required: [instanceId, className, pose, customData, dragOffset]
properties:
instanceId:
type: string
className:
type: string
pose:
$ref: "#/components/schemas/Pose"
customData:
type: object
additionalProperties: true
dragOffset:
$ref: "#/components/schemas/DragOffset"
DragStartMessage:
type: object
required: [type, instanceId, sourceTabId, payload]
properties:
type:
type: string
enum: [DRAG_START]
instanceId:
type: string
sourceTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
DragMoveMessage:
type: object
required: [type, instanceId, sourceTabId, payload]
properties:
type:
type: string
enum: [DRAG_MOVE]
instanceId:
type: string
sourceTabId:
type: string
payload:
type: object
required: [serializedData, screenPosition, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
screenPosition:
$ref: "#/components/schemas/ScreenPosition"
timestamp:
type: number
DragEndMessage:
type: object
required: [type, instanceId, sourceTabId, payload]
properties:
type:
type: string
enum: [DRAG_END]
instanceId:
type: string
sourceTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
DragEnterTabMessage:
type: object
required: [type, instanceId, sourceTabId, targetTabId, payload]
properties:
type:
type: string
enum: [DRAG_ENTER_TAB]
instanceId:
type: string
sourceTabId:
type: string
targetTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
DragLeaveTabMessage:
type: object
required: [type, instanceId, sourceTabId, targetTabId, payload]
properties:
type:
type: string
enum: [DRAG_LEAVE_TAB]
instanceId:
type: string
sourceTabId:
type: string
targetTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
DragDropMessage:
type: object
required: [type, instanceId, sourceTabId, targetTabId, payload]
properties:
type:
type: string
enum: [DRAG_DROP]
instanceId:
type: string
sourceTabId:
type: string
targetTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
DragAbortMessage:
type: object
required: [type, instanceId, sourceTabId, payload]
properties:
type:
type: string
enum: [DRAG_ABORT]
instanceId:
type: string
sourceTabId:
type: string
payload:
type: object
required: [serializedData, timestamp]
properties:
serializedData:
$ref: "#/components/schemas/SerializedData"
timestamp:
type: number
HeartbeatMessage:
type: object
required: [type, instanceId, sourceTabId, payload]
properties:
type:
type: string
enum: [HEARTBEAT]
instanceId:
type: string
sourceTabId:
type: string
payload:
type: object
required: [timestamp]
properties:
timestamp:
type: number
30 changes: 30 additions & 0 deletions specs/002-magicdrag-refactor/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Data Model

## Entity: MagicDrag 子类
- Fields:
- className: string(静态标识,注册 key)
- channelName: string(静态,用于跨 Tab 通信)
- onEnterTab?: (payload) => void(静态扩展点)
- onOtherTabDragStart?: (payload) => void(实例扩展点)
- onOtherTabDragMove?: (payload) => void(实例扩展点)
- onOtherTabDragEnd?: (payload) => void(实例扩展点)
- Relationships:
- 由 Manager 注册并与 channelName 绑定

## Entity: Manager 注册表
- Fields:
- classesByName: Map<string, MagicDragConstructor>
- classNameByChannel: Map<string, string>
- instancesByClassName: Map<string, Set<MagicDrag>>
- Validation Rules:
- channelName 必须唯一(冲突拒绝注册)
- 未注册 className 的消息必须被忽略并记录 warning

## Entity: 跨 Tab 拖拽事件
- Fields:
- type: DRAG_START | DRAG_MOVE | DRAG_END | DRAG_ENTER_TAB
- className: string
- payload: serialized data + screenPosition + dragOffset
- State Transitions:
- DRAG_START → DRAG_MOVE* → DRAG_END
- DRAG_ENTER_TAB(独立触发)
Loading