@dxos/react-ui-editor 0.3.10-main.5c4230b → 0.3.10-main.5e11230

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 (148) hide show
  1. package/dist/lib/browser/index.mjs +1644 -791
  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 +56 -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 +7 -3
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
  11. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +3 -2
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  15. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
  17. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
  19. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  21. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  23. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/index.d.ts +11 -2
  25. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  27. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  28. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  29. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +9 -1
  31. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  32. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +37 -0
  33. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  35. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  37. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  39. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +3 -0
  41. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  43. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  44. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  45. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TextEditor/themes/default.d.ts +25 -0
  47. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  48. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  49. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  50. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
  51. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  52. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +16 -0
  53. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  54. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  55. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
  56. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  57. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  58. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  59. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  60. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  61. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  62. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  63. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  64. package/dist/types/src/hooks/index.d.ts +0 -1
  65. package/dist/types/src/hooks/index.d.ts.map +1 -1
  66. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  67. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  68. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -1
  69. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  70. package/dist/types/src/styles/markdown.d.ts +14 -11
  71. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  72. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  73. package/package.json +21 -32
  74. package/src/components/TextEditor/MarkdownEditor.stories.tsx +350 -0
  75. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  76. package/src/components/TextEditor/TextEditor.tsx +71 -45
  77. package/src/components/TextEditor/extensions/autocomplete.ts +59 -0
  78. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +6 -3
  79. package/src/components/TextEditor/extensions/blast.ts +387 -0
  80. package/src/components/TextEditor/extensions/comments.ts +172 -0
  81. package/src/components/TextEditor/extensions/demo.ts +69 -0
  82. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +15 -20
  83. package/src/components/TextEditor/extensions/image.ts +67 -0
  84. package/src/components/TextEditor/extensions/index.ts +11 -2
  85. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +75 -38
  86. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +5 -4
  87. package/src/components/TextEditor/extensions/markdown/bundle.ts +56 -59
  88. package/src/components/TextEditor/extensions/markdown/highlight.ts +207 -0
  89. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  90. package/src/components/TextEditor/extensions/mention.ts +31 -0
  91. package/src/components/TextEditor/extensions/table.ts +122 -0
  92. package/src/components/TextEditor/extensions/tasklist.ts +121 -0
  93. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  94. package/src/components/TextEditor/index.ts +3 -0
  95. package/src/components/TextEditor/themes/default.ts +274 -0
  96. package/src/components/TextEditor/themes/index.ts +5 -0
  97. package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
  98. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +5 -3
  99. package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
  100. package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
  101. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
  102. package/src/hooks/automerge/translation/index.ts +6 -0
  103. package/src/hooks/index.ts +0 -1
  104. package/src/hooks/useTextModel.ts +52 -39
  105. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -1
  106. package/src/styles/markdown.ts +32 -24
  107. package/src/testing/replicator.ts +6 -6
  108. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  109. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  110. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  111. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
  112. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  113. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  114. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  115. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  116. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  117. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  118. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  119. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  120. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  121. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  122. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  123. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  124. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  125. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  126. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  127. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  128. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  131. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  133. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  135. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  137. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  138. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  139. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  140. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  141. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  142. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  143. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  144. package/src/hooks/useCollaboration.ts +0 -45
  145. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  146. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  147. package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  148. package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ // https://codemirror.net/examples/autocompletion
6
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
7
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
8
+
9
+ import {
10
+ autocompletion,
11
+ completionKeymap,
12
+ type Completion,
13
+ type CompletionContext,
14
+ type CompletionResult,
15
+ } from '@codemirror/autocomplete';
16
+ import { keymap } from '@codemirror/view';
17
+
18
+ export type AutocompleteResult = Completion;
19
+
20
+ export type AutocompleteOptions = {
21
+ onSearch: (text: string) => Completion[];
22
+ };
23
+
24
+ /**
25
+ * Autocomplete extension.
26
+ */
27
+ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
28
+ return [
29
+ // https://codemirror.net/docs/ref/#view.keymap
30
+ // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
31
+ keymap.of(completionKeymap),
32
+
33
+ // https://codemirror.net/examples/autocompletion
34
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
35
+ autocompletion({
36
+ // TODO(burdon): Set custom keymap.
37
+ // defaultKeymap: false,
38
+
39
+ // Don't start unless key press.
40
+ activateOnTyping: false,
41
+
42
+ // TODO(burdon): Option to create new page?
43
+ // TODO(burdon): Optional decoration via addToOptions
44
+ override: [
45
+ (context: CompletionContext): CompletionResult | null => {
46
+ const match = context.matchBefore(/\w*/);
47
+ if (!match || (match.from === match.to && !context.explicit)) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ from: match.from,
53
+ options: onSearch(match.text.toLowerCase()),
54
+ };
55
+ },
56
+ ],
57
+ }),
58
+ ];
59
+ };
@@ -11,18 +11,21 @@ import { EditorView, placeholder } from '@codemirror/view';
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
+ // TODO(burdon): Compare with minimalSetup.
22
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
20
23
  bracketMatching(),
21
24
  closeBrackets(),
22
- _placeholder && placeholder(_placeholder),
23
-
24
25
  EditorView.lineWrapping,
25
26
 
27
+ _placeholder && placeholder(_placeholder),
28
+
26
29
  // TODO(burdon): Is this required?
27
30
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
28
31
  ].filter(Boolean) as Extension[];
@@ -0,0 +1,387 @@
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, StateField } 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>): 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.view.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);
85
+ if (element && point) {
86
+ blaster.spawn({ element, point });
87
+ }
88
+ }
89
+ }),
90
+
91
+ // Document changed.
92
+ StateField.define({
93
+ create: () => null,
94
+ update: (_value, transaction) => {
95
+ if (blaster && transaction.docChanged) {
96
+ // TODO(burdon): Only on delete.
97
+ const view = EditorView.findFromDOM(blaster.node)!;
98
+ const { element, point } = getPoint(view, transaction.selection!.main.head);
99
+ if (element && point) {
100
+ blaster.spawn({ element, point });
101
+ }
102
+ }
103
+
104
+ return null;
105
+ },
106
+ }),
107
+
108
+ keymap.of([
109
+ {
110
+ any: (view, event) => {
111
+ if (blaster && event.key === 'Enter' && event.shiftKey) {
112
+ blaster.shake({ time: 0.4 });
113
+ }
114
+
115
+ return false;
116
+ },
117
+ },
118
+ ]),
119
+ ];
120
+ };
121
+
122
+ //
123
+ // Blaster (no CM deps).
124
+ //
125
+
126
+ class Blaster {
127
+ _canvas: HTMLCanvasElement | undefined;
128
+ _ctx: CanvasRenderingContext2D | undefined | null;
129
+
130
+ private readonly _effect: Effect;
131
+
132
+ _running = false;
133
+ _lastTime: number | undefined;
134
+ _shakeTime = 0;
135
+ _shakeTimeMax = 0;
136
+
137
+ _particles: Particle[] = [];
138
+ _particlePointer = 0;
139
+
140
+ _lastPoint = { x: 0, y: 0 };
141
+
142
+ constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
143
+ this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
144
+ }
145
+
146
+ get node() {
147
+ return this._node;
148
+ }
149
+
150
+ initialize() {
151
+ // console.log('initialize');
152
+ invariant(!this._canvas && !this._ctx);
153
+
154
+ this._canvas = document.createElement('canvas');
155
+ this._canvas.id = 'code-blast-canvas';
156
+ this._canvas.style.position = 'absolute';
157
+ this._canvas.style.zIndex = '0';
158
+ this._canvas.style.pointerEvents = 'none';
159
+ // this._canvas.style.border = '2px solid red';
160
+
161
+ this._ctx = this._canvas.getContext('2d');
162
+
163
+ const parent = this._node.parentElement?.parentElement;
164
+ parent?.appendChild(this._canvas);
165
+
166
+ this.resize();
167
+ }
168
+
169
+ destroy() {
170
+ this.stop();
171
+ // console.log('destroy');
172
+ if (this._canvas) {
173
+ this._canvas.remove();
174
+ this._canvas = undefined;
175
+ this._ctx = undefined;
176
+ }
177
+ }
178
+
179
+ resize() {
180
+ if (this._node.parentElement && this._canvas) {
181
+ const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
182
+ this._canvas.style.top = `${y}px`;
183
+ this._canvas.style.left = `${x}px`;
184
+ this._canvas.width = width;
185
+ this._canvas.height = height;
186
+ }
187
+ }
188
+
189
+ start() {
190
+ // console.log('start');
191
+ invariant(this._canvas && this._ctx);
192
+ this._running = true;
193
+ this.loop();
194
+ }
195
+
196
+ stop() {
197
+ // console.log('stop');
198
+ this._running = false;
199
+ this._node.style.transform = 'translate(0px, 0px)';
200
+ }
201
+
202
+ loop() {
203
+ if (!this._running || !this._canvas || !this._ctx) {
204
+ return;
205
+ }
206
+
207
+ this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
208
+
209
+ // Calculate the delta from the previous frame.
210
+ const now = new Date().getTime();
211
+ this._lastTime ??= now;
212
+ const dt = (now - this._lastTime) / 1000;
213
+ this._lastTime = now;
214
+
215
+ if (this._shakeTime > 0) {
216
+ this._shakeTime -= dt;
217
+ const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
218
+ const shakeX = random(-magnitude, magnitude);
219
+ const shakeY = random(-magnitude, magnitude);
220
+ this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
221
+ }
222
+
223
+ this.drawParticles();
224
+
225
+ requestAnimationFrame(this.loop.bind(this));
226
+ }
227
+
228
+ shake = throttle<{ time: number }>(({ time }) => {
229
+ this._shakeTime = this._shakeTimeMax;
230
+ this._shakeTimeMax = time;
231
+ }, 100);
232
+
233
+ spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
234
+ const color = getRGBComponents(element, this._options.color);
235
+ const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
236
+ const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
237
+ this._lastPoint = point;
238
+ for (let i = numParticles; i--; i > 0) {
239
+ this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
240
+ this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
241
+ }
242
+ }, 100);
243
+
244
+ drawParticles() {
245
+ for (let i = this._particles.length; i--; i > 0) {
246
+ const particle = this._particles[i];
247
+ if (!particle) {
248
+ continue;
249
+ }
250
+
251
+ if (particle.alpha < 0.01 || particle.size <= 0.5) {
252
+ continue;
253
+ }
254
+
255
+ this._effect.update(this._ctx!, particle);
256
+ }
257
+ }
258
+ }
259
+
260
+ //
261
+ // Effects
262
+ //
263
+
264
+ type Particle = {
265
+ x: number;
266
+ y: number;
267
+ vx: number;
268
+ vy: number;
269
+ size: number;
270
+ color: (string | number)[];
271
+ alpha: number;
272
+ theta?: number;
273
+ drag?: number;
274
+ wander?: number;
275
+ };
276
+
277
+ abstract class Effect {
278
+ constructor(protected readonly _options: BlastOptions) {}
279
+ abstract create(x: number, y: number, color: Particle['color']): Particle;
280
+ abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
281
+ }
282
+
283
+ class Effect1 extends Effect {
284
+ create(x: number, y: number, color: Particle['color']) {
285
+ return {
286
+ x,
287
+ y: y + 10,
288
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
289
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
290
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
291
+ color,
292
+ alpha: 1,
293
+ };
294
+ }
295
+
296
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
297
+ particle.vy += this._options.particleGravity;
298
+ particle.x += particle.vx;
299
+ particle.y += particle.vy;
300
+ particle.alpha *= this._options.particleAlphaFadeout;
301
+
302
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
303
+ ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Based on Soulwire's demo.
309
+ * http://codepen.io/soulwire/pen/foktm
310
+ */
311
+ class Effect2 extends Effect {
312
+ create(x: number, y: number, color: Particle['color']) {
313
+ return {
314
+ x,
315
+ y: y + 10,
316
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
317
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
318
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
319
+ color,
320
+ alpha: 1,
321
+ theta: (random(0, 360) * Math.PI) / 180,
322
+ drag: 0.92,
323
+ wander: 0.15,
324
+ };
325
+ }
326
+
327
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
328
+ particle.vy += this._options.particleGravity;
329
+ particle.x += particle.vx;
330
+ particle.y += particle.vy;
331
+ particle.vx *= particle.drag!;
332
+ particle.vy *= particle.drag!;
333
+ particle.theta! += random(-0.5, 0.5);
334
+ particle.vx += Math.sin(particle.theta!) * 0.1;
335
+ particle.vy += Math.cos(particle.theta!) * 0.1;
336
+ particle.size *= this._options.particleShrinkRate;
337
+ particle.alpha *= this._options.particleAlphaFadeout;
338
+
339
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
340
+ ctx.beginPath();
341
+ ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
342
+ ctx.fill();
343
+ }
344
+ }
345
+
346
+ //
347
+ // Utils
348
+ //
349
+
350
+ function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
351
+ let wait = false;
352
+ return function (...args: any[]) {
353
+ if (!wait) {
354
+ // @ts-ignore
355
+ callback.apply(this, args);
356
+ wait = true;
357
+ setTimeout(() => {
358
+ wait = false;
359
+ }, limit);
360
+ }
361
+ };
362
+ }
363
+
364
+ const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
365
+ if (typeof color === 'function') {
366
+ return color();
367
+ }
368
+
369
+ const bgColor = getComputedStyle(node).color;
370
+ if (bgColor) {
371
+ const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
372
+ if (x) {
373
+ return x;
374
+ }
375
+ }
376
+
377
+ return [50, 50, 50];
378
+ };
379
+
380
+ const random = (min: number, max: number) => {
381
+ if (!max) {
382
+ max = min;
383
+ min = 0;
384
+ }
385
+
386
+ return min + ~~(Math.random() * (max - min + 1));
387
+ };
@@ -0,0 +1,172 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import {
7
+ keymap,
8
+ type DecorationSet,
9
+ type Rect,
10
+ type ViewUpdate,
11
+ Decoration,
12
+ EditorView,
13
+ MatchDecorator,
14
+ ViewPlugin,
15
+ WidgetType,
16
+ } from '@codemirror/view';
17
+
18
+ import { debounce } from '@dxos/async';
19
+ import { invariant } from '@dxos/invariant';
20
+
21
+ // TODO(burdon): Reconcile with theme.
22
+ const styles = EditorView.baseTheme({
23
+ '& .cm-bookmark': {
24
+ cursor: 'pointer',
25
+ margin: '4px',
26
+ padding: '4px',
27
+ backgroundColor: 'yellow',
28
+ },
29
+ '& .cm-bookmark-selected': {
30
+ backgroundColor: 'orange',
31
+ },
32
+ });
33
+
34
+ class BookmarkWidget extends WidgetType {
35
+ constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
36
+ super();
37
+ invariant(this._id);
38
+ }
39
+
40
+ override eq(other: BookmarkWidget) {
41
+ return this._id === other._id;
42
+ }
43
+
44
+ override toDOM() {
45
+ const span = document.createElement('span');
46
+ span.className = 'cm-bookmark';
47
+ span.textContent = '§';
48
+ span.onclick = () => this._handleClick();
49
+ return span;
50
+ }
51
+ }
52
+
53
+ type CommentsInfo = {
54
+ active?: string;
55
+ items?: { id: string; pos: number; location: Rect | null }[];
56
+ };
57
+
58
+ export type CommentsOptions = {
59
+ key?: string;
60
+ onCreate?: () => string | void;
61
+ onUpdate?: (info: CommentsInfo) => void;
62
+ };
63
+
64
+ // https://www.markdownguide.org/extended-syntax/#footnotes
65
+ // TODO(burdon): Record span in automerge model?
66
+ // TODO(burdon): Extended markdown: https://www.markdownguide.org/extended-syntax
67
+ export const comments = (options: CommentsOptions = {}): Extension => {
68
+ let active: string | undefined;
69
+
70
+ const bookmarkMatcher = new MatchDecorator({
71
+ regexp: /\[\^(\w+)\]/g,
72
+ decoration: (match, view, pos) => {
73
+ const id = match[1];
74
+ return Decoration.replace({
75
+ id,
76
+ widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
77
+ });
78
+ },
79
+ });
80
+
81
+ const bookmarks = ViewPlugin.fromClass(
82
+ class {
83
+ bookmarks: DecorationSet;
84
+ constructor(view: EditorView) {
85
+ this.bookmarks = bookmarkMatcher.createDeco(view);
86
+ }
87
+
88
+ update(update: ViewUpdate) {
89
+ this.bookmarks = bookmarkMatcher.updateDeco(update, this.bookmarks);
90
+ }
91
+ },
92
+ {
93
+ decorations: (instance) => instance.bookmarks,
94
+ provide: (plugin) =>
95
+ EditorView.atomicRanges.of((view) => {
96
+ return view.plugin(plugin)?.bookmarks || Decoration.none;
97
+ }),
98
+ },
99
+ );
100
+
101
+ const handleUpdate = debounce((data: CommentsInfo) => {
102
+ options.onUpdate?.(data);
103
+ }, 200);
104
+
105
+ return [
106
+ keymap.of([
107
+ {
108
+ key: options?.key ?? 'shift-meta-c',
109
+ run: (view) => {
110
+ // Insert footnote.
111
+ const id = options.onCreate?.();
112
+ if (id) {
113
+ const pos = view.state.selection.main.head;
114
+ const tag = `[^${id}]`;
115
+ view.dispatch({
116
+ changes: { from: pos, insert: tag },
117
+ selection: { anchor: pos + tag.length },
118
+ });
119
+
120
+ return true;
121
+ }
122
+
123
+ return false;
124
+ },
125
+ },
126
+ ]),
127
+
128
+ bookmarks,
129
+
130
+ // Monitor cursor movement.
131
+ EditorView.updateListener.of((update) => {
132
+ active = undefined;
133
+ if (!options.onUpdate) {
134
+ return;
135
+ }
136
+
137
+ const view = update.view;
138
+ const pos = view.state.selection.main.head;
139
+
140
+ const decorations: { from: number; to: number; value: Decoration }[] = [];
141
+ const rangeSet = view.plugin(bookmarks)?.bookmarks;
142
+ let closest = Infinity;
143
+ // TODO(burdon): Handle multiple visible ranges in large documents.
144
+ const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: view.state.doc.length };
145
+ rangeSet?.between(from, to, (from, to, value) => {
146
+ decorations.push({ from, to, value });
147
+ const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
148
+ if (d < closest) {
149
+ // TODO(burdon): Update class of selected.
150
+ active = value.spec.id;
151
+ closest = d;
152
+ }
153
+ });
154
+
155
+ if (decorations.length) {
156
+ handleUpdate({
157
+ active,
158
+ items: decorations.map(
159
+ ({
160
+ from,
161
+ value: {
162
+ spec: { id },
163
+ },
164
+ }) => ({ id: id as string, pos: from, location: view.coordsAtPos(from) }),
165
+ ),
166
+ });
167
+ }
168
+ }),
169
+
170
+ styles,
171
+ ];
172
+ };
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+
8
+ // TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
9
+
10
+ export type DemoOptions = {
11
+ delay?: number;
12
+ items?: string[];
13
+ };
14
+
15
+ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
16
+
17
+ /**
18
+ * Demo script.
19
+ * Configurable plugin that let's user cycle through pre-configured input script.
20
+ */
21
+ export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
22
+ let t: any;
23
+ let idx = 0;
24
+
25
+ return [
26
+ keymap.of([
27
+ {
28
+ // Reset.
29
+ key: "alt-meta-'",
30
+ run: (view) => {
31
+ clearTimeout(t);
32
+ idx = 0;
33
+ return true;
34
+ },
35
+ },
36
+ {
37
+ // Next prompt.
38
+ // TODO(burdon): Press 1-9 to select prompt?
39
+ key: "shift-meta-'",
40
+ run: (view) => {
41
+ clearTimeout(t);
42
+ // TODO(burdon): Add space if needed.
43
+ const text = items[idx++];
44
+ if (idx === items?.length) {
45
+ idx = 0;
46
+ }
47
+
48
+ let i = 0;
49
+ const insert = (d = 0) => {
50
+ t = setTimeout(() => {
51
+ const pos = view.state.selection.main.head;
52
+ view.dispatch({
53
+ changes: { from: pos, insert: text[i++] },
54
+ selection: { anchor: pos + 1 },
55
+ });
56
+
57
+ if (i < text.length) {
58
+ insert(Math.random() * delay * (text[i] === ' ' ? 2 : 1));
59
+ }
60
+ }, d);
61
+ };
62
+
63
+ insert();
64
+ return true;
65
+ },
66
+ },
67
+ ]),
68
+ ];
69
+ };