vzcode 0.24.0 → 0.26.0
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/assets/{index-9c8bbae0.js → index-284b7339.js} +50 -50
- package/dist/assets/index-e160f758.css +1 -0
- package/dist/index.html +2 -6
- package/package.json +7 -5
- package/src/client/App.tsx +47 -213
- package/src/client/CodeEditor/getOrCreateEditor.ts +3 -0
- package/src/client/CodeEditor/index.tsx +36 -6
- package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +5 -3
- package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -3
- package/src/client/CodeEditor/widgets.ts +112 -10
- package/src/client/PresenceNotifications/index.tsx +0 -2
- package/src/client/TabList/Tab.tsx +56 -0
- package/src/client/TabList/index.tsx +11 -62
- package/src/client/TabList/style.scss +21 -9
- package/src/client/{Settings.tsx → VZSettings.tsx} +1 -1
- package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +3 -0
- package/src/client/{Sidebar → VZSidebar}/Item.tsx +10 -7
- package/src/client/{Sidebar → VZSidebar}/Listing.tsx +3 -0
- package/src/client/{Sidebar → VZSidebar}/index.tsx +5 -6
- package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
- package/src/client/bootstrap.ts +3 -0
- package/src/client/style.scss +5 -2
- package/src/client/themes/index.ts +7 -0
- package/src/client/{themes.ts → themes/themes.ts} +5 -5
- package/src/client/{useDynamicTheme.ts → themes/useDynamicTheme.ts} +1 -1
- package/src/client/themes/vizhubTheme/colors.ts +80 -0
- package/src/client/themes/vizhubTheme/index.ts +166 -0
- package/src/client/useActions.ts +74 -0
- package/src/client/useFileCRUD.ts +101 -0
- package/src/client/useOpenDirectories.ts +25 -7
- package/src/client/useSubmitOperation.ts +25 -0
- package/src/client/vzReducer.ts +118 -0
- package/src/index.ts +34 -0
- package/src/server/index.js +13 -2
- package/dist/assets/index-cf19de3b.css +0 -1
- package/src/client/reducer.ts +0 -122
- package/src/client/useTabsState.ts +0 -95
- package/src/client/vizhubTheme.js +0 -1
- /package/src/client/{Sidebar → VZSidebar}/ConfirmationBox.tsx +0 -0
- /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
- /package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +0 -0
package/src/client/App.tsx
CHANGED
|
@@ -9,30 +9,25 @@ import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
|
9
9
|
import { json1Presence } from '../ot';
|
|
10
10
|
import { randomId } from '../randomId';
|
|
11
11
|
import { CodeEditor } from './CodeEditor';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
FileId,
|
|
17
|
-
Files,
|
|
18
|
-
ShareDBDoc,
|
|
19
|
-
VZCodeContent,
|
|
20
|
-
} from '../types';
|
|
12
|
+
import { VZSettings } from './VZSettings';
|
|
13
|
+
import { VZSidebar } from './VZSidebar';
|
|
14
|
+
import { Files, ShareDBDoc, VZCodeContent } from '../types';
|
|
21
15
|
import { TabList } from './TabList';
|
|
22
16
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
23
|
-
import {
|
|
24
|
-
import { ThemeLabel, defaultTheme } from './themes';
|
|
17
|
+
import { defaultTheme, useDynamicTheme } from './themes';
|
|
25
18
|
import { useEditorCache } from './useEditorCache';
|
|
26
|
-
import { useDynamicTheme } from './useDynamicTheme';
|
|
27
19
|
import { usePrettier } from './usePrettier';
|
|
28
20
|
// @ts-ignore
|
|
29
21
|
import PrettierWorker from './usePrettier/worker?worker';
|
|
30
22
|
import { SplitPaneResizeProvider } from './SplitPaneResizeContext';
|
|
31
23
|
import { Resizer } from './Resizer';
|
|
32
24
|
import { PresenceNotifications } from './PresenceNotifications';
|
|
33
|
-
import { reducer } from './reducer';
|
|
34
|
-
import './style.scss';
|
|
35
25
|
import { PrettierErrorOverlay } from './PrettierErrorOverlay';
|
|
26
|
+
import { vzReducer } from './vzReducer';
|
|
27
|
+
import { useActions } from './useActions';
|
|
28
|
+
import { useFileCRUD } from './useFileCRUD';
|
|
29
|
+
import './style.scss';
|
|
30
|
+
import { useSubmitOperation } from './useSubmitOperation';
|
|
36
31
|
|
|
37
32
|
// Instantiate the Prettier worker.
|
|
38
33
|
const prettierWorker = new PrettierWorker();
|
|
@@ -58,20 +53,9 @@ function App() {
|
|
|
58
53
|
const [shareDBDoc, setShareDBDoc] =
|
|
59
54
|
useState<ShareDBDoc<VZCodeContent> | null>(null);
|
|
60
55
|
|
|
61
|
-
// A helper function to submit operations to the ShareDB document
|
|
62
|
-
|
|
63
56
|
const submitOperation: (
|
|
64
57
|
next: (content: VZCodeContent) => VZCodeContent,
|
|
65
|
-
) => void =
|
|
66
|
-
(next) => {
|
|
67
|
-
const content: VZCodeContent = shareDBDoc.data;
|
|
68
|
-
const op = diff(content, next(content));
|
|
69
|
-
if (op && shareDBDoc) {
|
|
70
|
-
shareDBDoc.submitOp(op);
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
[shareDBDoc],
|
|
74
|
-
);
|
|
58
|
+
) => void = useSubmitOperation(shareDBDoc);
|
|
75
59
|
|
|
76
60
|
// Auto-run Pretter after local changes.
|
|
77
61
|
const { prettierError } = usePrettier(
|
|
@@ -80,8 +64,6 @@ function App() {
|
|
|
80
64
|
prettierWorker,
|
|
81
65
|
);
|
|
82
66
|
|
|
83
|
-
console.log('prettierError', prettierError);
|
|
84
|
-
|
|
85
67
|
// Local ShareDB presence, for broadcasting our cursor position
|
|
86
68
|
// so other clients can see it.
|
|
87
69
|
// See https://share.github.io/sharedb/api/local-presence
|
|
@@ -123,6 +105,12 @@ function App() {
|
|
|
123
105
|
// This decouples rendering logic from ShareDB.
|
|
124
106
|
// This callback gets called on each change.
|
|
125
107
|
shareDBDoc.on('op', () => {
|
|
108
|
+
// TODO consider excluding file contents from this,
|
|
109
|
+
// because currently we are re-rendering the entire
|
|
110
|
+
// document on each file change (each keystroke while editing).
|
|
111
|
+
// This is not required, because no part of the app outside
|
|
112
|
+
// of the CodeMirror integration uses those file contents.
|
|
113
|
+
// The Sidebar only needs to know file names, not contents.
|
|
126
114
|
setContent(shareDBDoc.data);
|
|
127
115
|
});
|
|
128
116
|
|
|
@@ -153,76 +141,32 @@ function App() {
|
|
|
153
141
|
};
|
|
154
142
|
}, []);
|
|
155
143
|
|
|
156
|
-
// The id of the currently open file tab.
|
|
157
|
-
// TODO make this a URL param
|
|
158
|
-
// Migrated to useReducer
|
|
159
|
-
//const [activeFileId, setActiveFileId] =
|
|
160
|
-
//useState<FileId | null>(null);
|
|
161
|
-
|
|
162
|
-
// The ordered list of tabs.
|
|
163
|
-
// TODO make this a URL param
|
|
164
144
|
// https://react.dev/reference/react/useReducer
|
|
165
|
-
|
|
166
|
-
const [state, dispatch] = useReducer(reducer, {
|
|
167
|
-
submitOperation,
|
|
145
|
+
const [state, dispatch] = useReducer(vzReducer, {
|
|
168
146
|
tabList: [],
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
activeFileId: null,
|
|
148
|
+
theme: defaultTheme,
|
|
149
|
+
isSettingsOpen: false,
|
|
172
150
|
});
|
|
173
151
|
|
|
174
|
-
//
|
|
175
|
-
const tabList =
|
|
176
|
-
|
|
177
|
-
// TODO phase this out as we complete the refactoring
|
|
178
|
-
// It's here now for backwards compatibility
|
|
179
|
-
const setTabList = (tabList) => {
|
|
180
|
-
dispatch({
|
|
181
|
-
type: 'set_tab_list',
|
|
182
|
-
tabList,
|
|
183
|
-
});
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const setActiveFileId = (activeFileId) => {
|
|
187
|
-
dispatch({
|
|
188
|
-
type: 'set_active_fileId',
|
|
189
|
-
activeFileId,
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
const openTab = (fileId: FileId) => {
|
|
193
|
-
dispatch({
|
|
194
|
-
type: 'open_tab',
|
|
195
|
-
fileId,
|
|
196
|
-
});
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const closeTab = (fileIdToRemove: FileId) => {
|
|
200
|
-
dispatch({
|
|
201
|
-
type: 'close_tab',
|
|
202
|
-
fileIdToRemove,
|
|
203
|
-
});
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const multiCloseTab = (idsToDelete: Array<FileId>) => {
|
|
207
|
-
dispatch({
|
|
208
|
-
type: 'multi_close_tab',
|
|
209
|
-
idsToDelete,
|
|
210
|
-
});
|
|
211
|
-
};
|
|
152
|
+
// Unpack state.
|
|
153
|
+
const { tabList, activeFileId, theme, isSettingsOpen } =
|
|
154
|
+
state;
|
|
212
155
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
const {} = useTabsState(
|
|
216
|
-
activeFileId,
|
|
156
|
+
// Functions for dispatching actions to the reducer.
|
|
157
|
+
const {
|
|
217
158
|
setActiveFileId,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
159
|
+
openTab,
|
|
160
|
+
closeTabs,
|
|
161
|
+
setTheme,
|
|
162
|
+
setIsSettingsOpen,
|
|
163
|
+
closeSettings,
|
|
164
|
+
} = useActions(dispatch);
|
|
221
165
|
|
|
222
|
-
// The
|
|
223
|
-
// TODO
|
|
224
|
-
const
|
|
225
|
-
|
|
166
|
+
// The set of open directories.
|
|
167
|
+
// TODO move this into reducer/useActions
|
|
168
|
+
const { isDirectoryOpen, toggleDirectory } =
|
|
169
|
+
useOpenDirectories();
|
|
226
170
|
|
|
227
171
|
// Cache of CodeMirror editors by file id.
|
|
228
172
|
const editorCache = useEditorCache();
|
|
@@ -230,123 +174,12 @@ function App() {
|
|
|
230
174
|
// Handle dynamic theme changes.
|
|
231
175
|
useDynamicTheme(editorCache, theme);
|
|
232
176
|
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
useOpenDirectories();
|
|
240
|
-
|
|
241
|
-
// TODO move this logic to a hook called `useFileCRUD`
|
|
242
|
-
// Include
|
|
243
|
-
// - `createFile`
|
|
244
|
-
// - `handleRenameFileClick`
|
|
245
|
-
// - `deleteFile`
|
|
246
|
-
// - `handleDeleteFileClick`
|
|
247
|
-
const createFile = useCallback(() => {
|
|
248
|
-
const name = prompt('Enter new file name');
|
|
249
|
-
if (name) {
|
|
250
|
-
submitOperation((document) => ({
|
|
251
|
-
...document,
|
|
252
|
-
files: {
|
|
253
|
-
...document.files,
|
|
254
|
-
[randomId()]: { name, text: '' },
|
|
255
|
-
},
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
}, [submitOperation]);
|
|
259
|
-
|
|
260
|
-
// Called when a file in the sidebar is renamed.
|
|
261
|
-
const handleRenameFileClick = useCallback(
|
|
262
|
-
(fileId: FileId, newName: string) => {
|
|
263
|
-
submitOperation((document) => ({
|
|
264
|
-
...document,
|
|
265
|
-
files: {
|
|
266
|
-
...document.files,
|
|
267
|
-
[fileId]: {
|
|
268
|
-
...document.files[fileId],
|
|
269
|
-
name: newName,
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
}));
|
|
273
|
-
},
|
|
274
|
-
[submitOperation],
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const deleteFile = useCallback(
|
|
278
|
-
(fileId: FileId) => {
|
|
279
|
-
submitOperation((document) => {
|
|
280
|
-
const updatedFiles = { ...document.files };
|
|
281
|
-
delete updatedFiles[fileId];
|
|
282
|
-
return { ...document, files: updatedFiles };
|
|
283
|
-
});
|
|
284
|
-
closeTab(fileId);
|
|
285
|
-
},
|
|
286
|
-
[submitOperation, closeTab],
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
const deleteDirectory = useCallback(
|
|
290
|
-
(path: FileId) => {
|
|
291
|
-
let tabsToDelete: Array<FileId> = [];
|
|
292
|
-
submitOperation((document) => {
|
|
293
|
-
const updatedFiles = { ...document.files };
|
|
294
|
-
for (const key in updatedFiles) {
|
|
295
|
-
if (updatedFiles[key].name.includes(path)) {
|
|
296
|
-
tabsToDelete.push(key);
|
|
297
|
-
delete updatedFiles[key];
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return { ...document, files: updatedFiles };
|
|
301
|
-
});
|
|
302
|
-
multiCloseTab(tabsToDelete);
|
|
303
|
-
},
|
|
304
|
-
[submitOperation, multiCloseTab],
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const handleDeleteClick = useCallback(
|
|
308
|
-
(key: string) => {
|
|
309
|
-
//Regex to identify if the key is a file path or a file id.
|
|
310
|
-
if (/^[0-9]*$/.test(key)) {
|
|
311
|
-
if (key.length == 8) {
|
|
312
|
-
deleteFile(key);
|
|
313
|
-
} else {
|
|
314
|
-
deleteDirectory(key);
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
deleteDirectory(key);
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
[deleteFile, deleteDirectory],
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
const handleSettingsClose = useCallback(() => {
|
|
324
|
-
setIsSettingsOpen(false);
|
|
325
|
-
}, []);
|
|
326
|
-
|
|
327
|
-
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
328
|
-
// via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
|
|
329
|
-
const interactTimeoutRef = useRef(null);
|
|
330
|
-
const handleInteract = useCallback(() => {
|
|
331
|
-
// Set `isInteracting: true` if not already set.
|
|
332
|
-
if (!interactTimeoutRef.current) {
|
|
333
|
-
submitOperation((document) => ({
|
|
334
|
-
...document,
|
|
335
|
-
isInteracting: true,
|
|
336
|
-
}));
|
|
337
|
-
} else {
|
|
338
|
-
clearTimeout(interactTimeoutRef.current);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Set `isInteracting: false` after a delay.
|
|
342
|
-
interactTimeoutRef.current = setTimeout(() => {
|
|
343
|
-
interactTimeoutRef.current = null;
|
|
344
|
-
submitOperation((document) => ({
|
|
345
|
-
...document,
|
|
346
|
-
isInteracting: false,
|
|
347
|
-
}));
|
|
348
|
-
}, 800);
|
|
349
|
-
}, [submitOperation]);
|
|
177
|
+
// Handle file CRUD operations.
|
|
178
|
+
const {
|
|
179
|
+
createFile,
|
|
180
|
+
handleRenameFileClick,
|
|
181
|
+
handleDeleteClick,
|
|
182
|
+
} = useFileCRUD({ submitOperation, closeTabs });
|
|
350
183
|
|
|
351
184
|
// Isolate the files object from the document.
|
|
352
185
|
const files: Files | null = content
|
|
@@ -357,7 +190,7 @@ function App() {
|
|
|
357
190
|
<SplitPaneResizeProvider>
|
|
358
191
|
<div className="app">
|
|
359
192
|
<div className="left">
|
|
360
|
-
<
|
|
193
|
+
<VZSidebar
|
|
361
194
|
createFile={createFile}
|
|
362
195
|
files={files}
|
|
363
196
|
handleRenameFileClick={handleRenameFileClick}
|
|
@@ -366,10 +199,11 @@ function App() {
|
|
|
366
199
|
setIsSettingsOpen={setIsSettingsOpen}
|
|
367
200
|
isDirectoryOpen={isDirectoryOpen}
|
|
368
201
|
toggleDirectory={toggleDirectory}
|
|
202
|
+
activeFileId={activeFileId}
|
|
369
203
|
/>
|
|
370
|
-
<
|
|
204
|
+
<VZSettings
|
|
371
205
|
show={isSettingsOpen}
|
|
372
|
-
onClose={
|
|
206
|
+
onClose={closeSettings}
|
|
373
207
|
theme={theme}
|
|
374
208
|
setTheme={setTheme}
|
|
375
209
|
/>
|
|
@@ -380,16 +214,16 @@ function App() {
|
|
|
380
214
|
tabList={tabList}
|
|
381
215
|
activeFileId={activeFileId}
|
|
382
216
|
setActiveFileId={setActiveFileId}
|
|
383
|
-
|
|
217
|
+
closeTabs={closeTabs}
|
|
384
218
|
/>
|
|
385
219
|
{content && activeFileId ? (
|
|
386
220
|
<CodeEditor
|
|
387
221
|
shareDBDoc={shareDBDoc}
|
|
222
|
+
submitOperation={submitOperation}
|
|
388
223
|
localPresence={localPresence}
|
|
389
224
|
docPresence={docPresence}
|
|
390
225
|
activeFileId={activeFileId}
|
|
391
226
|
theme={theme}
|
|
392
|
-
onInteract={handleInteract}
|
|
393
227
|
editorCache={editorCache}
|
|
394
228
|
/>
|
|
395
229
|
) : null}
|
|
@@ -19,6 +19,7 @@ import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
|
19
19
|
import {
|
|
20
20
|
colorsInTextPlugin,
|
|
21
21
|
highlightWidgets,
|
|
22
|
+
rotationIndicator,
|
|
22
23
|
widgets,
|
|
23
24
|
} from './widgets';
|
|
24
25
|
import {
|
|
@@ -192,6 +193,8 @@ export const getOrCreateEditor = ({
|
|
|
192
193
|
|
|
193
194
|
extensions.push(highlightWidgets);
|
|
194
195
|
|
|
196
|
+
extensions.push(rotationIndicator);
|
|
197
|
+
|
|
195
198
|
const editor = new EditorView({
|
|
196
199
|
state: EditorState.create({
|
|
197
200
|
doc: text,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useLayoutEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
} from 'react';
|
|
2
6
|
import {
|
|
3
7
|
FileId,
|
|
4
8
|
ShareDBDoc,
|
|
@@ -11,20 +15,22 @@ import {
|
|
|
11
15
|
} from '../useEditorCache';
|
|
12
16
|
import { getOrCreateEditor } from './getOrCreateEditor';
|
|
13
17
|
import './style.scss';
|
|
14
|
-
// import { usePrettier } from '../usePrettier';
|
|
15
18
|
|
|
16
19
|
export const CodeEditor = ({
|
|
17
20
|
activeFileId,
|
|
18
21
|
shareDBDoc,
|
|
22
|
+
submitOperation,
|
|
19
23
|
localPresence,
|
|
20
24
|
docPresence,
|
|
21
25
|
theme = defaultTheme,
|
|
22
26
|
filesPath = ['files'],
|
|
23
|
-
onInteract,
|
|
24
27
|
editorCache,
|
|
25
28
|
}: {
|
|
26
29
|
activeFileId: FileId;
|
|
27
30
|
shareDBDoc: ShareDBDoc<VZCodeContent> | null;
|
|
31
|
+
submitOperation: (
|
|
32
|
+
next: (content: VZCodeContent) => VZCodeContent,
|
|
33
|
+
) => void;
|
|
28
34
|
localPresence?: any;
|
|
29
35
|
docPresence?: any;
|
|
30
36
|
theme?: ThemeLabel;
|
|
@@ -32,14 +38,38 @@ export const CodeEditor = ({
|
|
|
32
38
|
// The path of the files object in the ShareDB document.
|
|
33
39
|
// Defaults to `files` if not provided.
|
|
34
40
|
filesPath?: string[];
|
|
35
|
-
onInteract?: () => void;
|
|
36
41
|
editorCache: EditorCache;
|
|
37
42
|
}) => {
|
|
38
43
|
const ref = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
46
|
+
// via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
|
|
47
|
+
const interactTimeoutRef = useRef(null);
|
|
48
|
+
|
|
49
|
+
const onInteract = useCallback(() => {
|
|
50
|
+
// Set `isInteracting: true` if not already set.
|
|
51
|
+
if (!interactTimeoutRef.current) {
|
|
52
|
+
submitOperation((document) => ({
|
|
53
|
+
...document,
|
|
54
|
+
isInteracting: true,
|
|
55
|
+
}));
|
|
56
|
+
} else {
|
|
57
|
+
clearTimeout(interactTimeoutRef.current);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Set `isInteracting: false` after a delay.
|
|
61
|
+
interactTimeoutRef.current = setTimeout(() => {
|
|
62
|
+
interactTimeoutRef.current = null;
|
|
63
|
+
submitOperation((document) => ({
|
|
64
|
+
...document,
|
|
65
|
+
isInteracting: false,
|
|
66
|
+
}));
|
|
67
|
+
}, 800);
|
|
68
|
+
}, [submitOperation]);
|
|
69
|
+
|
|
39
70
|
// Every time the active file switches from one file to another,
|
|
40
71
|
// the editor corresponding to the old file is removed from the DOM,
|
|
41
72
|
// and the editor corresponding to the new file is added to the DOM.
|
|
42
|
-
|
|
43
73
|
useLayoutEffect(() => {
|
|
44
74
|
// Guard against cases where page is still loading.
|
|
45
75
|
if (!ref.current) return;
|
|
@@ -67,7 +97,7 @@ export const CodeEditor = ({
|
|
|
67
97
|
// This happens every time `activeFileId` changes.
|
|
68
98
|
ref.current.removeChild(editorCacheValue.editor.dom);
|
|
69
99
|
};
|
|
70
|
-
}, [shareDBDoc, activeFileId]);
|
|
100
|
+
}, [shareDBDoc, activeFileId, onInteract]);
|
|
71
101
|
|
|
72
102
|
return <div className="vz-code-editor" ref={ref}></div>;
|
|
73
103
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { ViewUpdate } from '@codemirror/view';
|
|
1
2
|
import { EditorView } from 'codemirror';
|
|
2
|
-
import { username } from '../Settings.tsx';
|
|
3
3
|
|
|
4
4
|
// Deals with broadcasting changes in cursor location and selection.
|
|
5
5
|
export const json1PresenceBroadcast = ({
|
|
@@ -7,7 +7,7 @@ export const json1PresenceBroadcast = ({
|
|
|
7
7
|
localPresence,
|
|
8
8
|
}) =>
|
|
9
9
|
// See https://discuss.codemirror.net/t/codemirror-6-proper-way-to-listen-for-changes/2395/10
|
|
10
|
-
EditorView.updateListener.of((viewUpdate) => {
|
|
10
|
+
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
|
11
11
|
// If this update modified the cursor / selection,
|
|
12
12
|
// we broadcast the selection update via ShareDB presence.
|
|
13
13
|
if (viewUpdate.selectionSet) {
|
|
@@ -23,7 +23,9 @@ export const json1PresenceBroadcast = ({
|
|
|
23
23
|
const presence = {
|
|
24
24
|
start: [...path, from],
|
|
25
25
|
end: [...path, to],
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// TODO fill this in with the user's actual username.
|
|
28
|
+
username: 'anonymous',
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// Broadcast presence to remote clients!
|
|
@@ -20,7 +20,12 @@ export const json1PresenceDisplay = ({
|
|
|
20
20
|
}) => [
|
|
21
21
|
ViewPlugin.fromClass(
|
|
22
22
|
class {
|
|
23
|
-
|
|
23
|
+
// The decorations to display.
|
|
24
|
+
// This is a RangeSet of Decoration objects.
|
|
25
|
+
// See https://codemirror.net/6/docs/ref/#view.Decoration
|
|
26
|
+
decorations: RangeSet<Decoration>;
|
|
27
|
+
|
|
28
|
+
constructor(view: EditorView) {
|
|
24
29
|
// Initialize decorations to empty array so CodeMirror doesn't crash.
|
|
25
30
|
this.decorations = RangeSet.of([]);
|
|
26
31
|
|
|
@@ -161,12 +166,13 @@ const pathMatches = (path, presence) => {
|
|
|
161
166
|
|
|
162
167
|
// Displays a single remote presence cursor.
|
|
163
168
|
class PresenceWidget extends WidgetType {
|
|
164
|
-
|
|
169
|
+
id: string;
|
|
170
|
+
constructor(id: string) {
|
|
165
171
|
super();
|
|
166
172
|
this.id = id;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
eq(other) {
|
|
175
|
+
eq(other: PresenceWidget) {
|
|
170
176
|
return other.id === this.id;
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -166,22 +166,27 @@ export const widgets = ({
|
|
|
166
166
|
regexp: /rotate\(-?\d*\.?\d*\)/g,
|
|
167
167
|
cursor: 'move',
|
|
168
168
|
onDragStart(text, setText, e) {
|
|
169
|
-
rotationOrigin = { x: e.
|
|
169
|
+
rotationOrigin = { x: e.clientX, y: e.clientY };
|
|
170
170
|
},
|
|
171
171
|
|
|
172
172
|
onDrag(text, setText, e) {
|
|
173
173
|
if (rotationOrigin == null) return;
|
|
174
|
+
const rotationDegree = Math.round(
|
|
175
|
+
(Math.atan2(
|
|
176
|
+
rotationOrigin.y - e.clientY,
|
|
177
|
+
e.clientX - rotationOrigin.x,
|
|
178
|
+
) *
|
|
179
|
+
180) /
|
|
180
|
+
Math.PI,
|
|
181
|
+
);
|
|
174
182
|
//Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
|
|
175
|
-
setText(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
e.screenX - rotationOrigin.x,
|
|
180
|
-
) *
|
|
181
|
-
180) /
|
|
182
|
-
Math.PI,
|
|
183
|
-
)})`,
|
|
183
|
+
setText(`rotate(${rotationDegree})`);
|
|
184
|
+
const updateDragEvent = new CustomEvent(
|
|
185
|
+
'updateRotateDrag',
|
|
186
|
+
{ detail: rotationDegree },
|
|
184
187
|
);
|
|
188
|
+
|
|
189
|
+
document.dispatchEvent(updateDragEvent);
|
|
185
190
|
},
|
|
186
191
|
onDragEnd(text, setText) {
|
|
187
192
|
rotationOrigin = null;
|
|
@@ -383,3 +388,100 @@ export const highlightWidgets = ViewPlugin.fromClass(
|
|
|
383
388
|
},
|
|
384
389
|
},
|
|
385
390
|
);
|
|
391
|
+
|
|
392
|
+
export const rotationIndicator = ViewPlugin.fromClass(
|
|
393
|
+
class {
|
|
394
|
+
view: EditorView;
|
|
395
|
+
textPosition?: number;
|
|
396
|
+
rotation: number;
|
|
397
|
+
constructor(view) {
|
|
398
|
+
this.view = view;
|
|
399
|
+
this.textPosition = null;
|
|
400
|
+
this.rotation = 0;
|
|
401
|
+
|
|
402
|
+
document.addEventListener(
|
|
403
|
+
'updateRotateDrag',
|
|
404
|
+
(e: CustomEvent) => {
|
|
405
|
+
this.textPosition = this.view.posAtCoords(
|
|
406
|
+
rotationOrigin,
|
|
407
|
+
false,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
this.rotation = e.detail;
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
decorations: (v) => {
|
|
417
|
+
if (rotationOrigin === null) {
|
|
418
|
+
return Decoration.none;
|
|
419
|
+
}
|
|
420
|
+
return Decoration.set([
|
|
421
|
+
{
|
|
422
|
+
from: v.textPosition,
|
|
423
|
+
to: v.textPosition,
|
|
424
|
+
value: Decoration.widget({
|
|
425
|
+
side: -1,
|
|
426
|
+
widget: new RotationCircle(v.rotation),
|
|
427
|
+
}),
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
class RotationCircle extends WidgetType {
|
|
435
|
+
angle: number;
|
|
436
|
+
constructor(angle: number) {
|
|
437
|
+
super();
|
|
438
|
+
this.angle = angle;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
toDOM(view: EditorView): HTMLElement {
|
|
442
|
+
const parent = document.createElement('div');
|
|
443
|
+
|
|
444
|
+
parent.setAttribute('style', 'width:20px;height:20px');
|
|
445
|
+
parent.className = 'color-circle-parent';
|
|
446
|
+
const svg = document.createElementNS(
|
|
447
|
+
'http://www.w3.org/2000/svg',
|
|
448
|
+
'svg',
|
|
449
|
+
);
|
|
450
|
+
const colorCircle = document.createElementNS(
|
|
451
|
+
'http://www.w3.org/2000/svg',
|
|
452
|
+
'circle',
|
|
453
|
+
);
|
|
454
|
+
colorCircle.setAttributeNS(null, 'fill', '#808080');
|
|
455
|
+
colorCircle.setAttributeNS(null, 'r', '5');
|
|
456
|
+
colorCircle.setAttributeNS(null, 'cx', '5');
|
|
457
|
+
colorCircle.setAttributeNS(null, 'cy', '5');
|
|
458
|
+
|
|
459
|
+
const indicatorLine = document.createElementNS(
|
|
460
|
+
'http://www.w3.org/2000/svg',
|
|
461
|
+
'line',
|
|
462
|
+
);
|
|
463
|
+
indicatorLine.setAttributeNS(null, 'x1', '10');
|
|
464
|
+
indicatorLine.setAttributeNS(null, 'x2', '5');
|
|
465
|
+
indicatorLine.setAttributeNS(null, 'y1', '5');
|
|
466
|
+
indicatorLine.setAttributeNS(null, 'y2', '5');
|
|
467
|
+
indicatorLine.setAttributeNS(null, 'stroke', 'black');
|
|
468
|
+
indicatorLine.setAttributeNS(
|
|
469
|
+
null,
|
|
470
|
+
'transform',
|
|
471
|
+
`rotate(${360 - this.angle},5,5)`,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
svg.setAttributeNS(null, 'viewBox', '0 0 10 10');
|
|
475
|
+
svg.setAttributeNS(null, 'width', '20');
|
|
476
|
+
svg.setAttributeNS(null, 'height', '20');
|
|
477
|
+
|
|
478
|
+
svg.appendChild(colorCircle);
|
|
479
|
+
svg.appendChild(indicatorLine);
|
|
480
|
+
|
|
481
|
+
parent.appendChild(svg);
|
|
482
|
+
return parent;
|
|
483
|
+
}
|
|
484
|
+
ignoreEvent() {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -66,8 +66,6 @@ export const PresenceNotifications = ({
|
|
|
66
66
|
// `false` means user has left the session.
|
|
67
67
|
const join = update !== null;
|
|
68
68
|
|
|
69
|
-
console.log('join', join);
|
|
70
|
-
|
|
71
69
|
// Figure out if we have ever seen this user before.
|
|
72
70
|
|
|
73
71
|
// if (join && docPresence.remotePresences[presenceId] === undefined){
|