@dxos/react-ui-editor 0.6.2-main.fb91371 → 0.6.2

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.
@@ -101,29 +101,47 @@ export const commentsState = StateField.define<CommentsState>({
101
101
  //
102
102
 
103
103
  const styles = EditorView.baseTheme({
104
- '&light .cm-comment, &light .cm-comment-current': { mixBlendMode: 'darken' },
105
- '&dark .cm-comment, &dark .cm-comment-current': { mixBlendMode: 'plus-lighter' },
104
+ '.cm-comment, .cm-comment-current': {
105
+ cursor: 'pointer',
106
+ borderWidth: '1px',
107
+ borderStyle: 'solid',
108
+ borderRadius: '2px',
109
+ transition: 'background-color 0.1s ease',
110
+ },
111
+ '&light .cm-comment, &light .cm-comment-current': {
112
+ mixBlendMode: 'darken',
113
+ borderColor: getToken('extend.colors.yellow.100'),
114
+ },
115
+ '&dark .cm-comment, &dark .cm-comment-current': {
116
+ mixBlendMode: 'plus-lighter',
117
+ borderColor: getToken('extend.colors.yellow.900'),
118
+ },
106
119
  '&light .cm-comment': {
107
120
  backgroundColor: getToken('extend.colors.yellow.50'),
108
121
  },
109
- '&light .cm-comment-current': {
110
- backgroundColor: getToken('extend.colors.yellow.100'),
111
- },
122
+ '&light .cm-comment:hover': { backgroundColor: getToken('extend.colors.yellow.100') },
123
+ '&light .cm-comment-current': { backgroundColor: getToken('extend.colors.yellow.100') },
124
+ '&light .cm-comment-current:hover': { backgroundColor: getToken('extend.colors.yellow.150') },
112
125
  '&dark .cm-comment': {
113
126
  color: getToken('extend.colors.yellow.50'),
114
127
  backgroundColor: getToken('extend.colors.yellow.900'),
115
128
  },
129
+ '&dark .cm-comment:hover': { backgroundColor: getToken('extend.colors.yellow.800') },
116
130
  '&dark .cm-comment-current': {
117
131
  color: getToken('extend.colors.yellow.100'),
118
132
  backgroundColor: getToken('extend.colors.yellow.950'),
119
133
  },
134
+ '&dark .cm-comment-current:hover': { backgroundColor: getToken('extend.colors.yellow.900') },
120
135
  });
121
136
 
122
- const commentMark = Decoration.mark({ class: 'cm-comment', attributes: { 'data-testid': 'cm-comment' } });
123
- const commentCurrentMark = Decoration.mark({
124
- class: 'cm-comment-current',
125
- attributes: { 'data-testid': 'cm-comment' },
126
- });
137
+ const createCommentMark = (id: string, isCurrent: boolean) =>
138
+ Decoration.mark({
139
+ class: isCurrent ? 'cm-comment-current' : 'cm-comment',
140
+ attributes: {
141
+ 'data-testid': 'cm-comment',
142
+ 'data-comment-id': id,
143
+ },
144
+ });
127
145
 
128
146
  /**
129
147
  * Decorate ranges.
@@ -145,17 +163,39 @@ const commentsDecorations = EditorView.decorations.compute([commentsState], (sta
145
163
  return undefined;
146
164
  }
147
165
 
148
- if (comment.comment.id === current) {
149
- return commentCurrentMark.range(range.from, range.to);
150
- } else {
151
- return commentMark.range(range.from, range.to);
152
- }
166
+ const mark = createCommentMark(comment.comment.id, comment.comment.id === current);
167
+ return mark.range(range.from, range.to);
153
168
  })
154
169
  .filter(nonNullable);
155
170
 
156
171
  return Decoration.set(decorations);
157
172
  });
158
173
 
174
+ const commentClickedEffect = StateEffect.define<string>();
175
+
176
+ const handleCommentClick = EditorView.domEventHandlers({
177
+ click: (event, view) => {
178
+ let target = event.target as HTMLElement;
179
+ const editorRoot = view.dom;
180
+
181
+ // Traverse up the DOM tree looking for an element with data-comment-id
182
+ // Stop if we reach the editor root or find the comment id
183
+ while (target && target !== editorRoot && !target.hasAttribute('data-comment-id')) {
184
+ target = target.parentElement as HTMLElement;
185
+ }
186
+
187
+ // Check if we found a comment id and are still within the editor
188
+ if (target && target !== editorRoot) {
189
+ const commentId = target.getAttribute('data-comment-id');
190
+ if (commentId) {
191
+ view.dispatch({ effects: commentClickedEffect.of(commentId) });
192
+ return true;
193
+ }
194
+ }
195
+
196
+ return false;
197
+ },
198
+ });
159
199
  //
160
200
  // Cut-and-paste.
161
201
  //
@@ -372,6 +412,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
372
412
  documentId.of(options.id),
373
413
  commentsState,
374
414
  commentsDecorations,
415
+ handleCommentClick,
375
416
  styles,
376
417
 
377
418
  //
@@ -503,17 +544,28 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
503
544
  if (!comment?.comment.cursor) {
504
545
  return;
505
546
  }
506
-
507
547
  const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
508
548
  if (range) {
509
- view.dispatch({
510
- selection: { anchor: range.from },
511
- effects: [
512
- //
513
- EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
514
- setSelection.of({ current: id }),
515
- ],
516
- });
549
+ const currentSelection = view.state.selection.main;
550
+ const currentScrollPosition = view.scrollDOM.scrollTop;
551
+ const targetScrollPosition = view.coordsAtPos(range.from)?.top;
552
+
553
+ const needsScroll =
554
+ targetScrollPosition !== undefined &&
555
+ (targetScrollPosition < currentScrollPosition ||
556
+ targetScrollPosition > currentScrollPosition + view.scrollDOM.clientHeight);
557
+
558
+ const needsSelectionUpdate = currentSelection.from !== range.from || currentSelection.to !== range.from;
559
+
560
+ if (needsScroll || needsSelectionUpdate) {
561
+ view.dispatch({
562
+ selection: needsSelectionUpdate ? { anchor: range.from } : undefined,
563
+ effects: [
564
+ needsScroll ? EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined) : [],
565
+ needsSelectionUpdate ? setSelection.of({ current: id }) : [],
566
+ ].flat(),
567
+ });
568
+ }
517
569
  }
518
570
  };
519
571
 
@@ -533,6 +585,13 @@ export const selectionOverlapsComment = (state: EditorState): boolean => {
533
585
  return false;
534
586
  };
535
587
 
588
+ /**
589
+ * Check if there is one or more active (non-empty) selections in the editor state.
590
+ */
591
+ const hasActiveSelection = (state: EditorState): boolean => {
592
+ return state.selection.ranges.some((range) => !range.empty);
593
+ };
594
+
536
595
  /**
537
596
  * Update comments state field.
538
597
  */
@@ -554,18 +613,45 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
554
613
  * Hook provides an extension to compute the current comment state under the selection.
555
614
  * NOTE(Zan): I think this conceptually belongs in 'formatting.ts' but we can't import ESM modules there atm.
556
615
  */
557
- export const useCommentState = (): [boolean, Extension] => {
558
- const [comment, setComment] = useState<boolean>(false);
616
+ export const useCommentState = (): [{ comment: boolean; selection: boolean }, Extension] => {
617
+ const [state, setState] = useState<{ comment: boolean; selection: boolean }>({
618
+ comment: false,
619
+ selection: false,
620
+ });
559
621
 
560
622
  const observer = useMemo(
561
623
  () =>
562
624
  EditorView.updateListener.of((update) => {
563
625
  if (update.docChanged || update.selectionSet) {
564
- setComment(() => selectionOverlapsComment(update.state));
626
+ setState({
627
+ comment: selectionOverlapsComment(update.state),
628
+ selection: hasActiveSelection(update.state),
629
+ });
565
630
  }
566
631
  }),
567
632
  [],
568
633
  );
569
634
 
570
- return [comment, observer];
635
+ return [state, observer];
636
+ };
637
+
638
+ /**
639
+ * Hook provides an extension to listen for comment clicks and invoke a handler.
640
+ */
641
+ export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
642
+ const observer = useMemo(
643
+ () =>
644
+ EditorView.updateListener.of((update) => {
645
+ update.transactions.forEach((transaction) => {
646
+ transaction.effects.forEach((effect) => {
647
+ if (effect.is(commentClickedEffect)) {
648
+ onCommentClick(effect.value);
649
+ }
650
+ });
651
+ });
652
+ }),
653
+ [onCommentClick],
654
+ );
655
+
656
+ return observer;
571
657
  };