@dxos/react-ui-editor 0.6.2-next.5b5129c → 0.6.3-main.0af171d
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/dist/lib/browser/index.mjs +70 -41
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/extensions/comments.d.ts +8 -8
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/package.json +25 -25
- package/src/extensions/comments.ts +91 -35
|
@@ -12,11 +12,21 @@ import {
|
|
|
12
12
|
type ChangeDesc,
|
|
13
13
|
type EditorState,
|
|
14
14
|
} from '@codemirror/state';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
hoverTooltip,
|
|
17
|
+
keymap,
|
|
18
|
+
type Command,
|
|
19
|
+
Decoration,
|
|
20
|
+
EditorView,
|
|
21
|
+
type Rect,
|
|
22
|
+
type PluginValue,
|
|
23
|
+
ViewPlugin,
|
|
24
|
+
} from '@codemirror/view';
|
|
16
25
|
import sortBy from 'lodash.sortby';
|
|
17
26
|
import { useEffect, useMemo, useState } from 'react';
|
|
18
27
|
|
|
19
|
-
import {
|
|
28
|
+
import { type ThreadType } from '@braneframe/types';
|
|
29
|
+
import { debounce, type UnsubscribeCallback } from '@dxos/async';
|
|
20
30
|
import { log } from '@dxos/log';
|
|
21
31
|
import { nonNullable } from '@dxos/util';
|
|
22
32
|
|
|
@@ -134,15 +144,14 @@ const styles = EditorView.baseTheme({
|
|
|
134
144
|
'&dark .cm-comment-current:hover': { backgroundColor: getToken('extend.colors.yellow.900') },
|
|
135
145
|
});
|
|
136
146
|
|
|
137
|
-
const createCommentMark = (id: string, isCurrent: boolean) =>
|
|
138
|
-
|
|
147
|
+
const createCommentMark = (id: string, isCurrent: boolean) =>
|
|
148
|
+
Decoration.mark({
|
|
139
149
|
class: isCurrent ? 'cm-comment-current' : 'cm-comment',
|
|
140
150
|
attributes: {
|
|
141
151
|
'data-testid': 'cm-comment',
|
|
142
152
|
'data-comment-id': id,
|
|
143
153
|
},
|
|
144
154
|
});
|
|
145
|
-
};
|
|
146
155
|
|
|
147
156
|
/**
|
|
148
157
|
* Decorate ranges.
|
|
@@ -545,17 +554,28 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
|
|
|
545
554
|
if (!comment?.comment.cursor) {
|
|
546
555
|
return;
|
|
547
556
|
}
|
|
548
|
-
|
|
549
557
|
const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
|
|
550
558
|
if (range) {
|
|
551
|
-
view.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
+
const currentSelection = view.state.selection.main;
|
|
560
|
+
const currentScrollPosition = view.scrollDOM.scrollTop;
|
|
561
|
+
const targetScrollPosition = view.coordsAtPos(range.from)?.top;
|
|
562
|
+
|
|
563
|
+
const needsScroll =
|
|
564
|
+
targetScrollPosition !== undefined &&
|
|
565
|
+
(targetScrollPosition < currentScrollPosition ||
|
|
566
|
+
targetScrollPosition > currentScrollPosition + view.scrollDOM.clientHeight);
|
|
567
|
+
|
|
568
|
+
const needsSelectionUpdate = currentSelection.from !== range.from || currentSelection.to !== range.from;
|
|
569
|
+
|
|
570
|
+
if (needsScroll || needsSelectionUpdate) {
|
|
571
|
+
view.dispatch({
|
|
572
|
+
selection: needsSelectionUpdate ? { anchor: range.from } : undefined,
|
|
573
|
+
effects: [
|
|
574
|
+
needsScroll ? EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined) : [],
|
|
575
|
+
needsSelectionUpdate ? setSelection.of({ current: id }) : [],
|
|
576
|
+
].flat(),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
559
579
|
}
|
|
560
580
|
};
|
|
561
581
|
|
|
@@ -575,34 +595,52 @@ export const selectionOverlapsComment = (state: EditorState): boolean => {
|
|
|
575
595
|
return false;
|
|
576
596
|
};
|
|
577
597
|
|
|
578
|
-
/**
|
|
579
|
-
* Check if there is one or more active (non-empty) selections in the editor state.
|
|
580
|
-
*/
|
|
581
598
|
const hasActiveSelection = (state: EditorState): boolean => {
|
|
582
599
|
return state.selection.ranges.some((range) => !range.empty);
|
|
583
600
|
};
|
|
584
601
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
602
|
+
class ExternalCommentSync implements PluginValue {
|
|
603
|
+
private unsubscribe: () => void;
|
|
604
|
+
|
|
605
|
+
constructor(
|
|
606
|
+
view: EditorView,
|
|
607
|
+
id: string,
|
|
608
|
+
subscribe: (sink: () => void) => UnsubscribeCallback,
|
|
609
|
+
getThreads: () => ThreadType[],
|
|
610
|
+
) {
|
|
611
|
+
const updateComments = () => {
|
|
612
|
+
const threads = getThreads();
|
|
613
|
+
const comments = threads
|
|
614
|
+
.filter(nonNullable)
|
|
615
|
+
.filter((thread) => thread.anchor)
|
|
616
|
+
.map((thread) => ({ id: thread.id, cursor: thread.anchor! }));
|
|
617
|
+
|
|
593
618
|
if (id === view.state.facet(documentId)) {
|
|
594
|
-
view.dispatch({
|
|
595
|
-
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
596
|
-
});
|
|
619
|
+
queueMicrotask(() => view.dispatch({ effects: setComments.of({ id, comments }) }));
|
|
597
620
|
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
this.unsubscribe = subscribe(updateComments);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
destroy = () => {
|
|
627
|
+
this.unsubscribe();
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export const createExternalCommentSync = (
|
|
632
|
+
id: string,
|
|
633
|
+
subscribe: (sink: () => void) => UnsubscribeCallback,
|
|
634
|
+
getThreads: () => ThreadType[],
|
|
635
|
+
): Extension =>
|
|
636
|
+
ViewPlugin.fromClass(
|
|
637
|
+
class {
|
|
638
|
+
constructor(view: EditorView) {
|
|
639
|
+
return new ExternalCommentSync(view, id, subscribe, getThreads);
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
);
|
|
601
643
|
|
|
602
|
-
/**
|
|
603
|
-
* Hook provides an extension to compute the current comment state under the selection.
|
|
604
|
-
* NOTE(Zan): I think this conceptually belongs in 'formatting.ts' but we can't import ESM modules there atm.
|
|
605
|
-
*/
|
|
606
644
|
export const useCommentState = (): [{ comment: boolean; selection: boolean }, Extension] => {
|
|
607
645
|
const [state, setState] = useState<{ comment: boolean; selection: boolean }>({
|
|
608
646
|
comment: false,
|
|
@@ -625,6 +663,24 @@ export const useCommentState = (): [{ comment: boolean; selection: boolean }, Ex
|
|
|
625
663
|
return [state, observer];
|
|
626
664
|
};
|
|
627
665
|
|
|
666
|
+
/**
|
|
667
|
+
* @deprecated This hook will be removed in future versions. Use the new comment sync extension instead.
|
|
668
|
+
* Update comments state field.
|
|
669
|
+
*/
|
|
670
|
+
export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
if (view) {
|
|
673
|
+
// Check same document.
|
|
674
|
+
// NOTE: Hook might be called before editor state is updated.
|
|
675
|
+
if (id === view.state.facet(documentId)) {
|
|
676
|
+
view.dispatch({
|
|
677
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
|
|
628
684
|
/**
|
|
629
685
|
* Hook provides an extension to listen for comment clicks and invoke a handler.
|
|
630
686
|
*/
|