@dxos/react-ui-editor 0.3.10-next.ef70620 → 0.3.11-main.008b7c6

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.
Files changed (219) hide show
  1. package/dist/lib/browser/index.mjs +2814 -1096
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +63 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +9 -4
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +15 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +20 -0
  11. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/index.d.ts +1 -1
  14. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
  16. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  17. package/dist/types/src/extensions/autocomplete.d.ts +10 -0
  18. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  19. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  20. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  21. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  22. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  23. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  24. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/index.d.ts +3 -0
  26. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  27. package/dist/types/src/{automerge/automerge-plugin/PatchSemaphore.d.ts → extensions/automerge/semaphore.d.ts} +8 -5
  28. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  30. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  31. package/dist/types/src/extensions/automerge/update-codemirror.d.ts +5 -0
  32. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  33. package/dist/types/src/extensions/awareness.d.ts +46 -0
  34. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  35. package/dist/types/src/{components/TextEditor/extensions/basic/bundle.d.ts → extensions/basic.d.ts} +3 -2
  36. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  37. package/dist/types/src/extensions/blast.d.ts +25 -0
  38. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  39. package/dist/types/src/extensions/code.d.ts +2 -0
  40. package/dist/types/src/extensions/code.d.ts.map +1 -0
  41. package/dist/types/src/extensions/comments.d.ts +50 -0
  42. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  43. package/dist/types/src/extensions/hr.d.ts +2 -0
  44. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  45. package/dist/types/src/extensions/image.d.ts +4 -0
  46. package/dist/types/src/extensions/image.d.ts.map +1 -0
  47. package/dist/types/src/extensions/index.d.ts +19 -0
  48. package/dist/types/src/extensions/index.d.ts.map +1 -0
  49. package/dist/types/src/extensions/link.d.ts +12 -0
  50. package/dist/types/src/extensions/link.d.ts.map +1 -0
  51. package/dist/types/src/extensions/listener.d.ts +10 -0
  52. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  53. package/dist/types/src/extensions/markdown/bundle.d.ts +17 -0
  54. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  55. package/dist/types/src/extensions/markdown/highlight.d.ts +36 -0
  56. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/index.d.ts +3 -0
  58. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  59. package/dist/types/src/extensions/mention.d.ts +6 -0
  60. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  61. package/dist/types/src/extensions/mermaid.d.ts +3 -0
  62. package/dist/types/src/extensions/mermaid.d.ts.map +1 -0
  63. package/dist/types/src/extensions/table.d.ts +8 -0
  64. package/dist/types/src/extensions/table.d.ts.map +1 -0
  65. package/dist/types/src/extensions/tasklist.d.ts +3 -0
  66. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  67. package/dist/types/src/extensions/typewriter.d.ts +10 -0
  68. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  69. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  70. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  71. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  72. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  73. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  74. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  75. package/dist/types/src/extensions/yjs/yjs.test.d.ts +2 -0
  76. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  77. package/dist/types/src/hooks/automerge.d.ts +3 -0
  78. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  79. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  80. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  81. package/dist/types/src/hooks/index.d.ts +2 -1
  82. package/dist/types/src/hooks/index.d.ts.map +1 -1
  83. package/dist/types/src/hooks/useTextModel.d.ts +23 -8
  84. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  85. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  86. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  87. package/dist/types/src/index.d.ts +3 -0
  88. package/dist/types/src/index.d.ts.map +1 -1
  89. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  90. package/dist/types/src/styles/index.d.ts +1 -0
  91. package/dist/types/src/styles/index.d.ts.map +1 -1
  92. package/dist/types/src/styles/markdown.d.ts +14 -11
  93. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  94. package/dist/types/src/testing/replicator.d.ts +2 -2
  95. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  96. package/dist/types/src/themes/default.d.ts +17 -0
  97. package/dist/types/src/themes/default.d.ts.map +1 -0
  98. package/dist/types/src/themes/index.d.ts +2 -0
  99. package/dist/types/src/themes/index.d.ts.map +1 -0
  100. package/dist/types/src/util/cursor.d.ts +17 -0
  101. package/dist/types/src/util/cursor.d.ts.map +1 -0
  102. package/dist/types/src/util/index.d.ts +3 -0
  103. package/dist/types/src/util/index.d.ts.map +1 -0
  104. package/dist/types/src/util/util.d.ts +5 -0
  105. package/dist/types/src/util/util.d.ts.map +1 -0
  106. package/package.json +29 -33
  107. package/src/components/TextEditor/MarkdownEditor.stories.tsx +426 -0
  108. package/src/components/TextEditor/TextEditor.stories.tsx +70 -43
  109. package/src/components/TextEditor/TextEditor.tsx +98 -52
  110. package/src/{automerge → components/TextEditor}/automerge.stories.tsx +69 -7
  111. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  112. package/src/components/TextEditor/index.ts +1 -1
  113. package/src/components/TextEditor/yjs.stories.tsx +1 -0
  114. package/src/extensions/autocomplete.ts +59 -0
  115. package/src/extensions/automerge/automerge.ts +94 -0
  116. package/src/extensions/automerge/cursor.ts +51 -0
  117. package/src/extensions/automerge/defs.ts +62 -0
  118. package/src/extensions/automerge/index.ts +6 -0
  119. package/src/{automerge/automerge-plugin/PatchSemaphore.ts → extensions/automerge/semaphore.ts} +24 -18
  120. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → extensions/automerge/update-automerge.ts} +8 -6
  121. package/src/{automerge/automerge-plugin/amToCodemirror.ts → extensions/automerge/update-codemirror.ts} +15 -4
  122. package/src/extensions/awareness.ts +316 -0
  123. package/src/{components/TextEditor/extensions/basic/bundle.ts → extensions/basic.ts} +8 -4
  124. package/src/extensions/blast.ts +373 -0
  125. package/src/extensions/code.ts +99 -0
  126. package/src/extensions/comments.ts +421 -0
  127. package/src/extensions/hr.ts +74 -0
  128. package/src/extensions/image.ts +69 -0
  129. package/src/extensions/index.ts +22 -0
  130. package/src/extensions/link.ts +192 -0
  131. package/src/extensions/listener.ts +35 -0
  132. package/src/extensions/markdown/bundle.ts +106 -0
  133. package/src/extensions/markdown/highlight.ts +204 -0
  134. package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +1 -1
  135. package/src/extensions/mention.ts +31 -0
  136. package/src/extensions/mermaid.ts +11 -0
  137. package/src/extensions/table.ts +132 -0
  138. package/src/extensions/tasklist.ts +121 -0
  139. package/src/extensions/typewriter.ts +68 -0
  140. package/src/extensions/yjs/cursor.ts +21 -0
  141. package/src/extensions/yjs/index.ts +5 -0
  142. package/src/extensions/yjs/yjs.test.ts +35 -0
  143. package/src/extensions/yjs/yjs.ts +16 -0
  144. package/src/hooks/automerge.ts +52 -0
  145. package/src/hooks/awareness-provider.ts +120 -0
  146. package/src/hooks/index.ts +2 -1
  147. package/src/hooks/useTextModel.ts +53 -67
  148. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +71 -19
  149. package/src/index.ts +3 -0
  150. package/src/styles/index.ts +1 -0
  151. package/src/styles/markdown.ts +33 -24
  152. package/src/testing/replicator.ts +16 -21
  153. package/src/themes/default.ts +302 -0
  154. package/src/themes/index.ts +5 -0
  155. package/src/util/cursor.ts +29 -0
  156. package/src/util/index.ts +6 -0
  157. package/src/util/util.ts +18 -0
  158. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  159. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  160. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  161. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
  162. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  163. package/dist/types/src/automerge/automerge-plugin/handle.d.ts +0 -9
  164. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  165. package/dist/types/src/automerge/automerge-plugin/index.d.ts +0 -3
  166. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  167. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +0 -26
  168. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  169. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  170. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  171. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  172. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  173. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  174. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  175. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  176. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  177. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  178. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  179. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  180. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  181. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  182. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts +0 -10
  183. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  184. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  185. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  186. package/dist/types/src/components/TextEditor/extensions/index.d.ts +0 -5
  187. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  188. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +0 -8
  189. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  190. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +0 -3
  191. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  192. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  193. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  194. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  195. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  196. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  197. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  198. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  199. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  200. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  201. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  202. package/src/automerge/automerge-plugin/handle.ts +0 -14
  203. package/src/automerge/automerge-plugin/index.ts +0 -6
  204. package/src/automerge/automerge-plugin/plugin.ts +0 -133
  205. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  206. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  207. package/src/components/TextEditor/extensions/change.ts +0 -22
  208. package/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.ts +0 -99
  209. package/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.tsx +0 -61
  210. package/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.tsx +0 -119
  211. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  212. package/src/components/TextEditor/extensions/index.ts +0 -8
  213. package/src/components/TextEditor/extensions/markdown/bundle.ts +0 -99
  214. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  215. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  216. package/src/hooks/useCollaboration.ts +0 -45
  217. package/src/hooks/yjs/index.ts +0 -6
  218. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  219. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
@@ -0,0 +1,316 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Annotation, Facet, type Extension, RangeSet, type Range } from '@codemirror/state';
6
+ import {
7
+ Decoration,
8
+ type DecorationSet,
9
+ EditorView,
10
+ type PluginValue,
11
+ ViewPlugin,
12
+ type ViewUpdate,
13
+ WidgetType,
14
+ } from '@codemirror/view';
15
+
16
+ import { Event } from '@dxos/async';
17
+ import { Context } from '@dxos/context';
18
+
19
+ import { CursorConverter } from '../util';
20
+
21
+ export interface AwarenessProvider {
22
+ remoteStateChange: Event<void>;
23
+
24
+ open(): void;
25
+ close(): void;
26
+
27
+ update(position: AwarenessPosition | undefined): void;
28
+ getRemoteStates(): AwarenessState[];
29
+ }
30
+
31
+ const dummyProvider: AwarenessProvider = {
32
+ remoteStateChange: new Event(),
33
+
34
+ open: () => {},
35
+ close: () => {},
36
+
37
+ update: () => {},
38
+ getRemoteStates: () => [],
39
+ };
40
+
41
+ export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
42
+ combine: (providers) => providers[0] ?? dummyProvider,
43
+ });
44
+
45
+ // TODO(dmaretskyi): Specify the users that actually changed. Currently, we recalculate positions for every user.
46
+ const RemoteSelectionChangedAnnotation = Annotation.define();
47
+
48
+ export type AwarenessPosition = {
49
+ anchor?: string;
50
+ head?: string;
51
+ };
52
+
53
+ export type AwarenessInfo = {
54
+ displayName?: string;
55
+ // TODO(burdon): Rename light/dark.
56
+ color?: string;
57
+ lightColor?: string;
58
+ };
59
+
60
+ export type AwarenessState = {
61
+ position?: AwarenessPosition;
62
+ peerId: string;
63
+ info: AwarenessInfo;
64
+ };
65
+
66
+ /**
67
+ * Extension provides presence information about other peers.
68
+ */
69
+ // TODO(burdon): Why provide default?
70
+ export const awareness = (provider = dummyProvider): Extension => {
71
+ return [
72
+ AwarenessProvider.of(provider),
73
+ ViewPlugin.fromClass(RemoteSelectionsDecorator, {
74
+ decorations: (value) => value.decorations,
75
+ }),
76
+ styles,
77
+ ];
78
+ };
79
+
80
+ /**
81
+ * Generates selection decorations from remote peers.
82
+ */
83
+ export class RemoteSelectionsDecorator implements PluginValue {
84
+ public decorations: DecorationSet = RangeSet.of([]);
85
+
86
+ private readonly _ctx = new Context();
87
+
88
+ private _cursorConverter: CursorConverter;
89
+ private _provider: AwarenessProvider;
90
+ private _lastAnchor?: number = undefined;
91
+ private _lastHead?: number = undefined;
92
+
93
+ constructor(view: EditorView) {
94
+ this._cursorConverter = view.state.facet(CursorConverter);
95
+ this._provider = view.state.facet(AwarenessProvider);
96
+ this._provider.open();
97
+ this._provider.remoteStateChange.on(this._ctx, () => {
98
+ view.dispatch({ annotations: [RemoteSelectionChangedAnnotation.of([])] });
99
+ });
100
+ }
101
+
102
+ destroy() {
103
+ void this._ctx.dispose();
104
+ this._provider.close();
105
+ }
106
+
107
+ update(update: ViewUpdate) {
108
+ this._updateLocalSelection(update);
109
+ this._updateRemoteSelections(update);
110
+ }
111
+
112
+ private _updateLocalSelection(update: ViewUpdate) {
113
+ const hasFocus = update.view.hasFocus && update.view.dom.ownerDocument.hasFocus();
114
+ const { anchor = undefined, head = undefined } = hasFocus ? update.state.selection.main : {};
115
+ if (this._lastAnchor === anchor && this._lastHead === head) {
116
+ return;
117
+ }
118
+
119
+ this._lastAnchor = anchor;
120
+ this._lastHead = head;
121
+
122
+ this._provider.update(
123
+ anchor !== undefined && head !== undefined
124
+ ? {
125
+ anchor: this._cursorConverter.toCursor(anchor),
126
+ head: this._cursorConverter.toCursor(head),
127
+ }
128
+ : undefined,
129
+ );
130
+ }
131
+
132
+ private _updateRemoteSelections(update: ViewUpdate) {
133
+ const decorations: Range<Decoration>[] = [];
134
+ const awarenessStates = this._provider.getRemoteStates();
135
+ for (const state of awarenessStates) {
136
+ const anchor = state.position?.anchor ? this._cursorConverter.fromCursor(state.position.anchor) : null;
137
+ const head = state.position?.head ? this._cursorConverter.fromCursor(state.position.head) : null;
138
+ if (anchor == null || head == null) {
139
+ continue;
140
+ }
141
+
142
+ const start = Math.min(Math.min(anchor, head), update.view.state.doc.length);
143
+ const end = Math.min(Math.max(anchor, head), update.view.state.doc.length);
144
+
145
+ const startLine = update.view.state.doc.lineAt(start);
146
+ const endLine = update.view.state.doc.lineAt(end);
147
+
148
+ // TODO(burdon): Factor out styles.
149
+ const color = state.info.color ?? '#30bced';
150
+ const lightColor = state.info.lightColor ?? color + '33';
151
+
152
+ if (startLine.number === endLine.number) {
153
+ // Selected content in a single line.
154
+ decorations.push({
155
+ from: start,
156
+ to: end,
157
+ value: Decoration.mark({
158
+ attributes: { style: `background-color: ${lightColor}` },
159
+ class: 'cm-ySelection',
160
+ }),
161
+ });
162
+ } else {
163
+ // Selected content in multiple lines; first, render text-selection in the first line.
164
+ decorations.push({
165
+ from: start,
166
+ to: startLine.from + startLine.length,
167
+ value: Decoration.mark({
168
+ attributes: { style: `background-color: ${color}` },
169
+ class: 'cm-ySelection',
170
+ }),
171
+ });
172
+
173
+ // Render text-selection in the last line.
174
+ decorations.push({
175
+ from: endLine.from,
176
+ to: end,
177
+ value: Decoration.mark({
178
+ attributes: { style: `background-color: ${color}` },
179
+ class: 'cm-ySelection',
180
+ }),
181
+ });
182
+
183
+ for (let i = startLine.number + 1; i < endLine.number; i++) {
184
+ const linePos = update.view.state.doc.line(i).from;
185
+ decorations.push({
186
+ from: linePos,
187
+ to: linePos,
188
+ value: Decoration.line({
189
+ attributes: { style: `background-color: ${color}`, class: 'cm-yLineSelection' },
190
+ }),
191
+ });
192
+ }
193
+ }
194
+
195
+ decorations.push({
196
+ from: head,
197
+ to: head,
198
+ value: Decoration.widget({
199
+ side: head - anchor > 0 ? -1 : 1, // The local cursor should be rendered outside the remote selection.
200
+ block: false,
201
+ widget: new RemoteCaretWidget(state.info.displayName ?? 'Anonymous', color),
202
+ }),
203
+ });
204
+ }
205
+
206
+ this.decorations = Decoration.set(decorations, true);
207
+ }
208
+ }
209
+
210
+ class RemoteCaretWidget extends WidgetType {
211
+ constructor(public name: string, public color: string) {
212
+ super();
213
+ this.name = name;
214
+ this.color = color;
215
+ }
216
+
217
+ toDOM(): HTMLElement {
218
+ const span = document.createElement('span');
219
+ span.className = 'cm-ySelectionCaret';
220
+ span.style.backgroundColor = this.color;
221
+ span.style.borderColor = this.color;
222
+
223
+ const dot = document.createElement('div');
224
+ dot.className = 'cm-ySelectionCaretDot';
225
+
226
+ const info = document.createElement('div');
227
+ info.className = 'cm-ySelectionInfo';
228
+ info.innerText = this.name;
229
+
230
+ span.appendChild(document.createTextNode('\u2060'));
231
+ span.appendChild(dot);
232
+ span.appendChild(document.createTextNode('\u2060'));
233
+ span.appendChild(info);
234
+ span.appendChild(document.createTextNode('\u2060'));
235
+
236
+ return span;
237
+ }
238
+
239
+ override eq(widget: this) {
240
+ return widget.color === this.color;
241
+ }
242
+
243
+ compare(widget: this) {
244
+ return widget.color === this.color;
245
+ }
246
+
247
+ override updateDOM() {
248
+ return false;
249
+ }
250
+
251
+ override get estimatedHeight() {
252
+ return -1;
253
+ }
254
+
255
+ override ignoreEvent() {
256
+ return true;
257
+ }
258
+ }
259
+
260
+ // TODO(burdon): Rename prefix (y for yjs?)
261
+ const styles = EditorView.baseTheme({
262
+ '.cm-ySelection': {},
263
+ '.cm-yLineSelection': {
264
+ padding: 0,
265
+ margin: '0px 2px 0px 4px',
266
+ },
267
+ '.cm-ySelectionCaret': {
268
+ position: 'relative',
269
+ borderLeft: '1px solid black',
270
+ borderRight: '1px solid black',
271
+ marginLeft: '-1px',
272
+ marginRight: '-1px',
273
+ boxSizing: 'border-box',
274
+ display: 'inline',
275
+ },
276
+ '.cm-ySelectionCaretDot': {
277
+ borderRadius: '50%',
278
+ position: 'absolute',
279
+ width: '.4em',
280
+ height: '.4em',
281
+ top: '-.2em',
282
+ left: '-.2em',
283
+ backgroundColor: 'inherit',
284
+ transition: 'transform .3s ease-in-out',
285
+ boxSizing: 'border-box',
286
+ },
287
+ '.cm-ySelectionCaret:hover > .cm-ySelectionCaretDot': {
288
+ transformOrigin: 'bottom center',
289
+ transform: 'scale(0)',
290
+ },
291
+ '.cm-ySelectionInfo': {
292
+ position: 'absolute',
293
+ top: '-1.05em',
294
+ left: '-1px',
295
+ fontSize: '.75em',
296
+ fontFamily: 'serif',
297
+ fontStyle: 'normal',
298
+ fontWeight: 'normal',
299
+ lineHeight: 'normal',
300
+ userSelect: 'none',
301
+ color: 'white',
302
+ paddingLeft: '2px',
303
+ paddingRight: '2px',
304
+ zIndex: 101,
305
+ transition: 'opacity .3s ease-in-out',
306
+ backgroundColor: 'inherit',
307
+ // these should be separate
308
+ opacity: 0,
309
+ transitionDelay: '0s',
310
+ whiteSpace: 'nowrap',
311
+ },
312
+ '.cm-ySelectionCaret:hover > .cm-ySelectionInfo': {
313
+ opacity: 1,
314
+ transitionDelay: '0s',
315
+ },
316
+ });
@@ -6,23 +6,27 @@ import { closeBrackets } from '@codemirror/autocomplete';
6
6
  import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
7
  import { type Extension } from '@codemirror/state';
8
8
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
- import { EditorView, placeholder } from '@codemirror/view';
9
+ import { drawSelection, EditorView, placeholder } from '@codemirror/view';
10
10
 
11
11
  import type { ThemeMode } from '@dxos/react-ui';
12
12
 
13
13
  export type BasicBundleOptions = {
14
+ readonly?: boolean;
14
15
  themeMode?: ThemeMode;
15
16
  placeholder?: string;
16
17
  };
17
18
 
18
- export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
+ export const basicBundle = ({ readonly, themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
20
  [
21
+ EditorView.lineWrapping,
22
+
23
+ // TODO(burdon): Compare with minimalSetup.
24
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
20
25
  bracketMatching(),
21
26
  closeBrackets(),
27
+ drawSelection(),
22
28
  _placeholder && placeholder(_placeholder),
23
29
 
24
- EditorView.lineWrapping,
25
-
26
30
  // TODO(burdon): Is this required?
27
31
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
28
32
  ].filter(Boolean) as Extension[];
@@ -0,0 +1,373 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Based on a demo by Joel Besada and Kushagra Gour.
4
+ // https://twitter.com/JoelBesada/status/670343885655293952
5
+ // https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
6
+ //
7
+
8
+ import { type Extension } from '@codemirror/state';
9
+ import { EditorView, keymap } from '@codemirror/view';
10
+ import defaultsDeep from 'lodash.defaultsdeep';
11
+
12
+ import { invariant } from '@dxos/invariant';
13
+
14
+ export type BlastOptions = {
15
+ effect?: number;
16
+ maxParticles: number;
17
+ particleGravity: number;
18
+ particleAlphaFadeout: number;
19
+ particleSize: { min: number; max: number };
20
+ particleNumRange: { min: number; max: number };
21
+ particleVelocityRange: { x: [number, number]; y: [number, number] };
22
+ particleShrinkRate: number;
23
+ shakeIntensity: number;
24
+ color?: () => number[];
25
+ };
26
+
27
+ export const defaultOptions: BlastOptions = {
28
+ effect: 2,
29
+ maxParticles: 200,
30
+ particleGravity: 0.08,
31
+ particleAlphaFadeout: 0.996,
32
+ particleSize: { min: 2, max: 8 },
33
+ particleNumRange: { min: 5, max: 10 },
34
+ particleVelocityRange: { x: [-1, 1], y: [-3.5, -1.5] },
35
+ particleShrinkRate: 0.95,
36
+ shakeIntensity: 5,
37
+ };
38
+
39
+ export const blast = (options: Partial<BlastOptions> = defaultOptions): Extension => {
40
+ let blaster: Blaster | undefined;
41
+ let last = 0;
42
+
43
+ const getPoint = (view: EditorView, pos: number) => {
44
+ const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
45
+ const element = document.elementFromPoint(x, y);
46
+
47
+ const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
48
+ const point = { x: x - left, y: y - top };
49
+
50
+ return { element, point };
51
+ };
52
+
53
+ return [
54
+ // Cursor moved.
55
+ EditorView.updateListener.of((update) => {
56
+ // NOTE: The MarkdownEditor may recreated the EditorView.
57
+ if (blaster?.node !== update.view.scrollDOM) {
58
+ if (blaster) {
59
+ blaster.destroy();
60
+ }
61
+
62
+ blaster = new Blaster(
63
+ update.view.scrollDOM,
64
+ defaultsDeep(
65
+ {
66
+ particleGravity: 0.2,
67
+ particleShrinkRate: 0.995,
68
+ color: () => [100 + Math.random() * 100, 0, 0],
69
+ },
70
+ options,
71
+ defaultOptions,
72
+ ),
73
+ );
74
+ blaster.initialize();
75
+ blaster.start(); // TODO(burdon): Stop/clean-up.
76
+ } else {
77
+ blaster.resize();
78
+ }
79
+
80
+ const current = update.state.selection.main.head;
81
+ if (current !== last) {
82
+ last = current;
83
+ // TODO(burdon): Null if end of line.
84
+ const { element, point } = getPoint(update.view, current - 1);
85
+ if (element && point) {
86
+ blaster.spawn({ element, point });
87
+ }
88
+ }
89
+ }),
90
+
91
+ keymap.of([
92
+ {
93
+ any: (_, event) => {
94
+ if (blaster) {
95
+ if (event.key === 'Enter' && event.shiftKey) {
96
+ blaster.shake({ time: 0.8 });
97
+ return true;
98
+ }
99
+ }
100
+
101
+ return false;
102
+ },
103
+ },
104
+ ]),
105
+ ];
106
+ };
107
+
108
+ //
109
+ // Blaster (no CM deps).
110
+ //
111
+
112
+ class Blaster {
113
+ private readonly _effect: Effect;
114
+
115
+ _canvas: HTMLCanvasElement | undefined;
116
+ _ctx: CanvasRenderingContext2D | undefined | null;
117
+
118
+ _running = false;
119
+ _lastTime: number | undefined;
120
+ _shakeTime = 0;
121
+ _shakeTimeMax = 0;
122
+
123
+ _particles: Particle[] = [];
124
+ _particlePointer = 0;
125
+
126
+ _lastPoint = { x: 0, y: 0 };
127
+
128
+ constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
129
+ this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
130
+ }
131
+
132
+ get node() {
133
+ return this._node;
134
+ }
135
+
136
+ initialize() {
137
+ // console.log('initialize');
138
+ invariant(!this._canvas && !this._ctx);
139
+
140
+ this._canvas = document.createElement('canvas');
141
+ this._canvas.id = 'code-blast-canvas';
142
+ this._canvas.style.position = 'absolute';
143
+ this._canvas.style.zIndex = '0';
144
+ this._canvas.style.pointerEvents = 'none';
145
+ // this._canvas.style.border = '2px dashed red';
146
+
147
+ this._ctx = this._canvas.getContext('2d');
148
+
149
+ const parent = this._node.parentElement?.parentElement;
150
+ parent?.appendChild(this._canvas);
151
+
152
+ this.resize();
153
+ }
154
+
155
+ destroy() {
156
+ this.stop();
157
+ // console.log('destroy');
158
+ if (this._canvas) {
159
+ this._canvas.remove();
160
+ this._canvas = undefined;
161
+ this._ctx = undefined;
162
+ }
163
+ }
164
+
165
+ resize() {
166
+ if (this._node.parentElement && this._canvas) {
167
+ const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
168
+ this._canvas.style.top = `${y}px`;
169
+ this._canvas.style.left = `${x}px`;
170
+ this._canvas.width = width;
171
+ this._canvas.height = height;
172
+ }
173
+ }
174
+
175
+ start() {
176
+ // console.log('start');
177
+ invariant(this._canvas && this._ctx);
178
+ this._running = true;
179
+ this.loop();
180
+ }
181
+
182
+ stop() {
183
+ // console.log('stop');
184
+ this._running = false;
185
+ this._node.style.transform = 'translate(0px, 0px)';
186
+ }
187
+
188
+ loop() {
189
+ if (!this._running || !this._canvas || !this._ctx) {
190
+ return;
191
+ }
192
+
193
+ this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
194
+
195
+ // Calculate the delta from the previous frame.
196
+ const now = new Date().getTime();
197
+ this._lastTime ??= now;
198
+ const dt = (now - this._lastTime) / 1000;
199
+ this._lastTime = now;
200
+
201
+ if (this._shakeTime > 0) {
202
+ this._shakeTime -= dt;
203
+ const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
204
+ const shakeX = random(-magnitude, magnitude);
205
+ const shakeY = random(-magnitude, magnitude);
206
+ this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
207
+ }
208
+
209
+ this.drawParticles();
210
+
211
+ requestAnimationFrame(this.loop.bind(this));
212
+ }
213
+
214
+ shake = throttle<{ time: number }>(({ time }) => {
215
+ this._shakeTime = this._shakeTimeMax || time;
216
+ this._shakeTimeMax = time;
217
+ }, 100);
218
+
219
+ spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
220
+ const color = getRGBComponents(element, this._options.color);
221
+ const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
222
+ const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
223
+ this._lastPoint = point;
224
+ for (let i = numParticles; i--; i > 0) {
225
+ this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
226
+ this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
227
+ }
228
+ }, 100);
229
+
230
+ drawParticles() {
231
+ for (let i = this._particles.length; i--; i > 0) {
232
+ const particle = this._particles[i];
233
+ if (!particle) {
234
+ continue;
235
+ }
236
+
237
+ if (particle.alpha < 0.01 || particle.size <= 0.5) {
238
+ continue;
239
+ }
240
+
241
+ this._effect.update(this._ctx!, particle);
242
+ }
243
+ }
244
+ }
245
+
246
+ //
247
+ // Effects
248
+ //
249
+
250
+ type Particle = {
251
+ x: number;
252
+ y: number;
253
+ vx: number;
254
+ vy: number;
255
+ size: number;
256
+ color: (string | number)[];
257
+ alpha: number;
258
+ theta?: number;
259
+ drag?: number;
260
+ wander?: number;
261
+ };
262
+
263
+ abstract class Effect {
264
+ constructor(protected readonly _options: BlastOptions) {}
265
+ abstract create(x: number, y: number, color: Particle['color']): Particle;
266
+ abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
267
+ }
268
+
269
+ class Effect1 extends Effect {
270
+ create(x: number, y: number, color: Particle['color']) {
271
+ return {
272
+ x,
273
+ y: y + 10,
274
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
275
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
276
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
277
+ color,
278
+ alpha: 1,
279
+ };
280
+ }
281
+
282
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
283
+ particle.vy += this._options.particleGravity;
284
+ particle.x += particle.vx;
285
+ particle.y += particle.vy;
286
+ particle.alpha *= this._options.particleAlphaFadeout;
287
+
288
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
289
+ ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Based on Soulwire's demo.
295
+ * http://codepen.io/soulwire/pen/foktm
296
+ */
297
+ class Effect2 extends Effect {
298
+ create(x: number, y: number, color: Particle['color']) {
299
+ return {
300
+ x,
301
+ y: y + 10,
302
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
303
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
304
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
305
+ color,
306
+ alpha: 1,
307
+ theta: (random(0, 360) * Math.PI) / 180,
308
+ drag: 0.92,
309
+ wander: 0.15,
310
+ };
311
+ }
312
+
313
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
314
+ particle.vy += this._options.particleGravity;
315
+ particle.x += particle.vx;
316
+ particle.y += particle.vy;
317
+ particle.vx *= particle.drag!;
318
+ particle.vy *= particle.drag!;
319
+ particle.theta! += random(-0.5, 0.5);
320
+ particle.vx += Math.sin(particle.theta!) * 0.1;
321
+ particle.vy += Math.cos(particle.theta!) * 0.1;
322
+ particle.size *= this._options.particleShrinkRate;
323
+ particle.alpha *= this._options.particleAlphaFadeout;
324
+
325
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
326
+ ctx.beginPath();
327
+ ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
328
+ ctx.fill();
329
+ }
330
+ }
331
+
332
+ //
333
+ // Utils
334
+ //
335
+
336
+ function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
337
+ let wait = false;
338
+ return function (...args: any[]) {
339
+ if (!wait) {
340
+ // @ts-ignore
341
+ callback.apply(this, args);
342
+ wait = true;
343
+ setTimeout(() => {
344
+ wait = false;
345
+ }, limit);
346
+ }
347
+ };
348
+ }
349
+
350
+ const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
351
+ if (typeof color === 'function') {
352
+ return color();
353
+ }
354
+
355
+ const bgColor = getComputedStyle(node).color;
356
+ if (bgColor) {
357
+ const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
358
+ if (x) {
359
+ return x;
360
+ }
361
+ }
362
+
363
+ return [50, 50, 50];
364
+ };
365
+
366
+ const random = (min: number, max: number) => {
367
+ if (!max) {
368
+ max = min;
369
+ min = 0;
370
+ }
371
+
372
+ return min + ~~(Math.random() * (max - min + 1));
373
+ };