Skip to content

Commit dfea22a

Browse files
authored
Merge pull request #50 from retejs/feature/comments-history
Feature/comments history
2 parents 96b4c38 + 5ca65a5 commit dfea22a

9 files changed

Lines changed: 677 additions & 132 deletions

src/comment-node-sync.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { NodeId } from 'rete'
2+
import { BaseAreaPlugin } from 'rete-area-plugin'
3+
4+
import { Comment } from './comment'
5+
import { FrameComment } from './frame-comment'
6+
import { detachNodeFromFrame, FrameMembership, syncFrameOnNodeDrop } from './frame-membership'
7+
import type { Produces } from './index'
8+
import { InlineComment } from './inline-comment'
9+
import type { ExpectedSchemes, MembershipState, Position } from './types'
10+
import { trackDragging, trackedTranslate, trackedTranslateComment } from './utils'
11+
12+
export type CommentNodeSyncDeps<Schemes extends ExpectedSchemes, K> = {
13+
area: BaseAreaPlugin<Schemes, K>
14+
comments: Map<Comment['id'], Comment>
15+
membershipState: MembershipState
16+
emit: (signal: Produces) => Promise<unknown>
17+
}
18+
19+
export function createCommentNodeSync<Schemes extends ExpectedSchemes, K>(deps: CommentNodeSyncDeps<Schemes, K>) {
20+
const { area, comments, membershipState, emit } = deps
21+
const { translate, isTranslating } = trackedTranslate({ area })
22+
const commentTracker = trackedTranslateComment(comments)
23+
const dragging = trackDragging()
24+
const nodeDragPrevious = new Map<NodeId, Position>()
25+
26+
const collectFrameMembership = async (nodeId: NodeId) => {
27+
const frames: FrameMembership[] = []
28+
29+
for (const comment of Array.from(comments.values())) {
30+
if (comment instanceof FrameComment) {
31+
const entry = await syncFrameOnNodeDrop(comment, nodeId)
32+
33+
if (entry) frames.push(entry)
34+
}
35+
}
36+
37+
return frames
38+
}
39+
40+
const emitMembership = async (
41+
frames: FrameMembership[],
42+
node?: { id: NodeId, previous: Position, current: Position }
43+
) => {
44+
if (!frames.length) return
45+
46+
await emit({
47+
type: 'commentmembershipchanged',
48+
data: node
49+
? { node, frames }
50+
: { frames }
51+
})
52+
}
53+
54+
return {
55+
beginNodeDrag(nodeId: NodeId) {
56+
dragging.start(nodeId)
57+
const view = area.nodeViews.get(nodeId)
58+
59+
if (view) nodeDragPrevious.set(nodeId, { ...view.position })
60+
},
61+
62+
reorderLinkedComments(element: HTMLElement) {
63+
const views = Array.from(area.nodeViews.entries())
64+
const matchedView = views.find(([, view]) => view.element === element)
65+
66+
if (!matchedView) return
67+
68+
const linkedComments = Array.from(comments.entries())
69+
.filter(([, comment]) => comment.linkedTo(matchedView[0]))
70+
71+
for (const [, comment] of linkedComments) {
72+
if (comment instanceof InlineComment) {
73+
area.area.content.reorder(comment.element, matchedView[1].element.nextElementSibling)
74+
}
75+
if (comment instanceof FrameComment) {
76+
area.area.content.reorder(comment.element, area.area.content.holder.firstChild)
77+
}
78+
}
79+
},
80+
81+
async onNodeTranslated(data: { id: NodeId, position: Position, previous: Position }) {
82+
if (membershipState.restoring) return
83+
84+
const { id, position, previous } = data
85+
const dx = position.x - previous.x
86+
const dy = position.y - previous.y
87+
88+
await Promise.all(Array.from(comments.values())
89+
.filter(comment => comment.linkedTo(id))
90+
.map(async comment => {
91+
if (comment instanceof InlineComment && !commentTracker.isTranslating(comment.id)) {
92+
await commentTracker.translate(comment.id, dx, dy, [id])
93+
}
94+
if (comment instanceof FrameComment
95+
&& !dragging.isDragging(id)
96+
&& !commentTracker.isResizing(comment.id)) {
97+
await commentTracker.resize(comment.id)
98+
}
99+
}))
100+
},
101+
102+
async onCommentTranslated(data: { id: Comment['id'], dx: number, dy: number, sources?: NodeId[] }) {
103+
const { id, dx, dy, sources } = data
104+
const comment = comments.get(id)
105+
106+
if (!(comment instanceof FrameComment)) return
107+
108+
await Promise.all(comment.links
109+
.filter(linkId => !sources?.includes(linkId))
110+
.map(linkId => ({ linkId, view: area.nodeViews.get(linkId) }))
111+
.map(async ({ linkId, view }) => {
112+
if (!view) return
113+
// prevent an infinite loop if a node is selected and translated along with the selected comment
114+
if (!await emit({ type: 'commentlinktranslate', data: { id, link: linkId } })) return
115+
116+
if (!isTranslating(linkId)) await translate(linkId, view.position.x + dx, view.position.y + dy)
117+
}))
118+
},
119+
120+
async finalizeNodeDrag(nodeId: NodeId) {
121+
if (membershipState.restoring) return
122+
123+
const nodePrevious = nodeDragPrevious.get(nodeId)
124+
const nodeView = area.nodeViews.get(nodeId)
125+
const frames = await collectFrameMembership(nodeId)
126+
127+
if (frames.length && nodePrevious && nodeView) {
128+
await emitMembership(frames, {
129+
id: nodeId,
130+
previous: nodePrevious,
131+
current: { ...nodeView.position }
132+
})
133+
}
134+
135+
nodeDragPrevious.delete(nodeId)
136+
dragging.stop(nodeId)
137+
},
138+
139+
async cleanupOnNodeRemoved(nodeId: NodeId) {
140+
dragging.stop(nodeId)
141+
nodeDragPrevious.delete(nodeId)
142+
143+
const frames: FrameMembership[] = []
144+
145+
for (const comment of Array.from(comments.values())) {
146+
if (comment instanceof InlineComment && comment.linkedTo(nodeId)) {
147+
comment.linkTo([])
148+
}
149+
if (comment instanceof FrameComment) {
150+
const entry = await detachNodeFromFrame(comment, nodeId)
151+
152+
if (entry) frames.push(entry)
153+
}
154+
}
155+
156+
await emitMembership(frames)
157+
}
158+
}
159+
}

src/comment.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class Comment {
1414
element!: HTMLElement
1515
nested!: HTMLElement
1616
prevPosition: null | Position = null
17+
private dragOrigin: null | Position = null
1718

1819
constructor(
1920
public text: string,
@@ -23,9 +24,14 @@ export class Comment {
2324
pick?: null | (() => void)
2425
translate?: null | ((dx: number, dy: number, sources?: NodeId[]) => Promise<void>)
2526
drag?: null | (() => void)
26-
}
27+
dragged?: null | ((
28+
previous: Position,
29+
prevLinks: string[]
30+
) => Promise<void>)
31+
},
32+
options?: { id?: string }
2733
) {
28-
this.id = getUID()
34+
this.id = options?.id ?? getUID()
2935
this.element = document.createElement('div')
3036
this.nested = document.createElement('div')
3137
this.element.appendChild(this.nested)
@@ -41,6 +47,7 @@ export class Comment {
4147
},
4248
{
4349
start: () => {
50+
this.dragOrigin = { x: this.x, y: this.y }
4451
this.prevPosition = { ...area.area.pointer }
4552

4653
if (this.events?.pick) {
@@ -58,10 +65,7 @@ export class Comment {
5865
}
5966
},
6067
drag: () => {
61-
this.prevPosition = null
62-
if (this.events?.drag) {
63-
this.events.drag()
64-
}
68+
void this.finishDrag()
6569
}
6670
}
6771
)
@@ -111,4 +115,25 @@ export class Comment {
111115
destroy() {
112116
this.dragHandler.destroy()
113117
}
118+
119+
private async finishDrag() {
120+
this.prevPosition = null
121+
122+
const origin = this.dragOrigin
123+
124+
this.dragOrigin = null
125+
126+
if (!origin) {
127+
this.events?.drag?.()
128+
return
129+
}
130+
131+
const prevLinks = [...this.links]
132+
133+
if (this.events?.drag) {
134+
this.events.drag()
135+
}
136+
137+
await this.events?.dragged?.(origin, prevLinks)
138+
}
114139
}

src/frame-comment.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NodeEditor, NodeId } from 'rete'
22
import { BaseAreaPlugin } from 'rete-area-plugin'
33

44
import { Comment } from './comment'
5-
import { ExpectedSchemes } from './types'
5+
import { ExpectedSchemes, Position } from './types'
66
import { containsRect, intersectRect, nodesBBox, Rect } from './utils'
77

88
export class FrameComment extends Comment {
@@ -18,7 +18,13 @@ export class FrameComment extends Comment {
1818
contextMenu?: (comment: FrameComment) => void
1919
pick?: (comment: FrameComment) => void
2020
translate?: (comment: FrameComment, dx: number, dy: number, sources?: NodeId[]) => Promise<void>
21-
}
21+
dragged?: (
22+
comment: FrameComment,
23+
previous: Position,
24+
prevLinks: string[]
25+
) => Promise<void>
26+
},
27+
options?: { id?: string }
2228
) {
2329
super(text, area, {
2430
contextMenu: () => {
@@ -30,8 +36,11 @@ export class FrameComment extends Comment {
3036
translate: async (dx, dy, sources) => {
3137
if (events?.translate) await events.translate(this, dx, dy, sources)
3238
},
33-
drag: () => 1
34-
})
39+
drag: () => 1,
40+
dragged: async (previous, prevLinks) => {
41+
if (events?.dragged) await events.dragged(this, previous, prevLinks)
42+
}
43+
}, options)
3544

3645
this.nested.className = 'frame-comment'
3746
}

src/frame-membership.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { NodeId } from 'rete'
2+
3+
import { FrameComment } from './frame-comment'
4+
5+
export type FrameBounds = { x: number, y: number, width: number, height: number }
6+
7+
export type FrameMembership = {
8+
id: FrameComment['id']
9+
links: { prev: string[], next: string[] }
10+
previous: FrameBounds
11+
current: FrameBounds
12+
}
13+
14+
export function frameBounds(comment: FrameComment): FrameBounds {
15+
return {
16+
x: comment.x,
17+
y: comment.y,
18+
width: comment.width,
19+
height: comment.height
20+
}
21+
}
22+
23+
function sameLinkSet(prev: string[], next: string[]) {
24+
return prev.length === next.length && prev.every(linkId => next.includes(linkId))
25+
}
26+
27+
function nextLinks(frame: FrameComment, nodeId: NodeId) {
28+
const links = frame.links.filter(id => id !== nodeId)
29+
30+
return frame.intersects(nodeId)
31+
? [...links, nodeId]
32+
: links
33+
}
34+
35+
export async function syncFrameOnNodeDrop(frame: FrameComment, nodeId: NodeId) {
36+
const prevLinks = [...frame.links]
37+
const next = nextLinks(frame, nodeId)
38+
39+
if (sameLinkSet(prevLinks, next)) {
40+
if (frame.linkedTo(nodeId)) await frame.resize()
41+
return null
42+
}
43+
44+
const previous = frameBounds(frame)
45+
46+
frame.linkTo(next)
47+
await frame.resize()
48+
49+
return {
50+
id: frame.id,
51+
links: { prev: prevLinks, next: [...frame.links] },
52+
previous,
53+
current: frameBounds(frame)
54+
}
55+
}
56+
57+
export async function detachNodeFromFrame(frame: FrameComment, nodeId: NodeId) {
58+
if (!frame.linkedTo(nodeId)) return null
59+
60+
const prevLinks = [...frame.links]
61+
const previous = frameBounds(frame)
62+
63+
frame.linkTo(frame.links.filter(id => id !== nodeId))
64+
await frame.resize()
65+
66+
return {
67+
id: frame.id,
68+
links: { prev: prevLinks, next: [...frame.links] },
69+
previous,
70+
current: frameBounds(frame)
71+
}
72+
}

src/handlers.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NodeId } from 'rete'
2+
import { BaseAreaPlugin } from 'rete-area-plugin'
3+
4+
import { FrameComment } from './frame-comment'
5+
import type { Produces } from './index'
6+
import { InlineComment } from './inline-comment'
7+
import type { ExpectedSchemes, Position } from './types'
8+
9+
export type CommentHandlerDeps = {
10+
area: BaseAreaPlugin<ExpectedSchemes, unknown>
11+
emit: (signal: Produces) => Promise<unknown>
12+
editComment: (id: string) => Promise<void>
13+
}
14+
15+
export function createInlineHandlers(deps: CommentHandlerDeps) {
16+
const { area, emit, editComment } = deps
17+
18+
return {
19+
contextMenu: ({ id }: InlineComment) => void editComment(id),
20+
pick: (item: InlineComment) => {
21+
area.area.content.reorder(item.element, null)
22+
void emit({ type: 'commentselected', data: item })
23+
},
24+
translate: async ({ id }: InlineComment, dx: number, dy: number, sources?: NodeId[]) => {
25+
await emit({ type: 'commenttranslated', data: { id, dx, dy, sources } })
26+
},
27+
dragged: async (item: InlineComment, previous: Position, prevLinks: string[]) => {
28+
await emit({
29+
type: 'commentdragged',
30+
data: { id: item.id, previous, prevLinks }
31+
})
32+
}
33+
}
34+
}
35+
36+
export function createFrameHandlers(deps: CommentHandlerDeps) {
37+
const { area, emit, editComment } = deps
38+
39+
return {
40+
contextMenu: ({ id }: FrameComment) => void editComment(id),
41+
pick: (item: FrameComment) => {
42+
area.area.content.reorder(item.element, area.area.content.holder.firstChild)
43+
void emit({ type: 'commentselected', data: item })
44+
},
45+
translate: async ({ id }: FrameComment, dx: number, dy: number, sources?: NodeId[]) => {
46+
await emit({ type: 'commenttranslated', data: { id, dx, dy, sources } })
47+
},
48+
dragged: async (item: FrameComment, previous: Position, prevLinks: string[]) => {
49+
await emit({
50+
type: 'commentdragged',
51+
data: { id: item.id, previous, prevLinks }
52+
})
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)