vzcode 0.19.0 → 0.20.1

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/index.html CHANGED
@@ -28,7 +28,7 @@
28
28
  href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
29
29
  rel="stylesheet"
30
30
  />
31
- <script type="module" crossorigin src="/assets/index-34134182.js"></script>
31
+ <script type="module" crossorigin src="/assets/index-12dbf516.js"></script>
32
32
  <link rel="stylesheet" href="/assets/index-0053b846.css">
33
33
  </head>
34
34
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -49,12 +49,12 @@
49
49
  "@codemirror/lang-css": "^6.2.1",
50
50
  "@codemirror/lang-html": "^6.4.6",
51
51
  "@codemirror/lang-javascript": "^6.2.1",
52
- "@codemirror/lang-markdown": "^6.2.0",
52
+ "@codemirror/lang-markdown": "^6.2.1",
53
53
  "@codemirror/state": "^6.2.1",
54
54
  "@codemirror/theme-one-dark": "^6.1.2",
55
55
  "@replit/codemirror-interact": "^6.3.0",
56
56
  "@teamwork/websocket-json-stream": "^2.0.0",
57
- "@uiw/codemirror-themes-all": "^4.21.13",
57
+ "@uiw/codemirror-themes-all": "^4.21.18",
58
58
  "codemirror": "^6.0.1",
59
59
  "codemirror-ot": "^4.1.0",
60
60
  "d3-array": "^3.2.4",
@@ -65,22 +65,22 @@
65
65
  "ngrok": "^4.3.3",
66
66
  "open": "^9.1.0",
67
67
  "react": "^18.2.0",
68
- "react-bootstrap": "^2.8.0",
68
+ "react-bootstrap": "^2.9.0",
69
69
  "react-dom": "^18.2.0",
70
- "react-select": "^5.7.4",
70
+ "react-select": "^5.7.5",
71
71
  "sharedb": "^4.0.1",
72
72
  "sharedb-client-browser": "^4.3.1",
73
73
  "ot-json1": "^1.0.2",
74
- "ws": "^8.13.0"
74
+ "ws": "^8.14.2"
75
75
  },
76
76
  "devDependencies": {
77
- "@types/react": "^18.2.21",
78
- "@types/react-dom": "^18.2.7",
79
- "@vitejs/plugin-react": "^4.0.4",
77
+ "@types/react": "^18.2.23",
78
+ "@types/react-dom": "^18.2.8",
79
+ "@vitejs/plugin-react": "^4.1.0",
80
80
  "prettier": "^3.0.3",
81
- "sass": "^1.66.1",
81
+ "sass": "^1.68.0",
82
82
  "typescript": "^5.2.2",
83
83
  "vite": "^4.4.9",
84
- "vitest": "^0.34.3"
84
+ "vitest": "^0.34.5"
85
85
  }
86
86
  }
@@ -198,24 +198,19 @@ function App() {
198
198
  }
199
199
  }, [submitOperation]);
200
200
 
201
- // Called when a file in the sidebar is double-clicked.
201
+ // Called when a file in the sidebar is renamed.
202
202
  const handleRenameFileClick = useCallback(
203
- (fileId: FileId) => {
204
- // TODO better UX, maybe Bootstrap modal? Maybe edit inline?
205
- const newName = prompt('Enter new name');
206
-
207
- if (newName) {
208
- submitOperation((document) => ({
209
- ...document,
210
- files: {
211
- ...document.files,
212
- [fileId]: {
213
- ...document.files[fileId],
214
- name: newName,
215
- },
203
+ (fileId: FileId, newName: string) => {
204
+ submitOperation((document) => ({
205
+ ...document,
206
+ files: {
207
+ ...document.files,
208
+ [fileId]: {
209
+ ...document.files[fileId],
210
+ name: newName,
216
211
  },
217
- }));
218
- }
212
+ },
213
+ }));
219
214
  },
220
215
  [submitOperation],
221
216
  );
@@ -232,12 +227,8 @@ function App() {
232
227
  [submitOperation, closeTab],
233
228
  );
234
229
 
235
- // TODO prompt the user "Are you sure?"
236
230
  const handleDeleteFileClick = useCallback(
237
- (fileId: FileId, event: React.MouseEvent) => {
238
- // Stop propagation so that the outer listener doesn't fire,
239
- // which would try to open this file in a tab.
240
- event.stopPropagation();
231
+ (fileId: FileId) => {
241
232
  deleteFile(fileId);
242
233
  },
243
234
  [deleteFile],
@@ -47,17 +47,32 @@ export const json1PresenceDisplay = ({
47
47
  const { start, end } = presence;
48
48
  const from = start[start.length - 1];
49
49
  // TODO support selection ranges (first attempt introduced layout errors)
50
- //const to = end[end.length - 1];
51
- return {
52
- from,
53
- //to,
54
- to: from, // Temporary meaure
55
- value: Decoration.widget({
56
- side: -1,
57
- block: false,
58
- widget: new PresenceWidget(id),
59
- }),
60
- };
50
+ const to = end[end.length - 1];
51
+ if (from === to) {
52
+ return {
53
+ from,
54
+ to,
55
+ value: Decoration.widget({
56
+ side: -1,
57
+ block: false,
58
+ widget: new PresenceWidget(id),
59
+ }),
60
+ };
61
+ } else {
62
+ return {
63
+ from,
64
+ to,
65
+ value: Decoration.mark({
66
+ class: 'cm-json1-presence',
67
+ attributes: {
68
+ style: `
69
+ background-color: rgba(255, 255, 0, 0.5);
70
+ box-shadow: 1px 0 0 yellow;
71
+ `,
72
+ },
73
+ }),
74
+ };
75
+ }
61
76
  }),
62
77
  // Without this argument, we get the following error:
63
78
  // Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
@@ -142,6 +157,5 @@ const presenceTheme = EditorView.baseTheme({
142
157
  left: '0',
143
158
  right: '0',
144
159
  borderLeft: '1px solid yellow',
145
- //borderRight: '1px solid black',
146
160
  },
147
161
  });
@@ -43,6 +43,61 @@ export const widgets = ({
43
43
  }
44
44
  },
45
45
  },
46
+ //vec2 slider
47
+ // Inspired by: https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L61
48
+ {
49
+ regexp:
50
+ /vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)/g,
51
+ cursor: 'move',
52
+
53
+ onDrag: (text, setText, e) => {
54
+ const res =
55
+ /vec2\((?<x>-?\b\d+\.?\d*\b)\s*(,\s*(?<y>-?\b\d+\.?\d*\b))?\)/.exec(
56
+ text,
57
+ );
58
+ const x = Number(res?.groups?.x);
59
+ let y = Number(res?.groups?.y);
60
+ if (isNaN(x)) return;
61
+ if (isNaN(y)) y = x;
62
+ setText(
63
+ `vec2(${x + e.movementX}, ${y - e.movementY})`,
64
+ );
65
+ },
66
+ },
67
+ //color picker
68
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
69
+ //TODO: create color picker for hsl colors
70
+ {
71
+ regexp: /rgb\(.*\)/g,
72
+ cursor: 'pointer',
73
+ onClick: (text, setText, e) => {
74
+ const res =
75
+ /rgb\((?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)\)/.exec(
76
+ text,
77
+ );
78
+ const r = Number(res?.groups?.r);
79
+ const g = Number(res?.groups?.g);
80
+ const b = Number(res?.groups?.b);
81
+
82
+ //sel will open the color picker when sel.click is called.
83
+ const sel = document.createElement('input');
84
+ sel.type = 'color';
85
+ if (!isNaN(r + g + b))
86
+ sel.value = rgb2Hex(r, g, b);
87
+
88
+ const updateRGB = (e: Event) => {
89
+ const el = e.target as HTMLInputElement;
90
+ if (el.value) {
91
+ const [r, g, b] = hex2RGB(el.value);
92
+ setText(`rgb(${r}, ${g}, ${b})`);
93
+ }
94
+ sel.removeEventListener('change', updateRGB);
95
+ };
96
+
97
+ sel.addEventListener('change', updateRGB);
98
+ sel.click();
99
+ },
100
+ },
46
101
  // url clicker
47
102
  {
48
103
  regexp: /https?:\/\/[^ "]+/g,
@@ -51,5 +106,45 @@ export const widgets = ({
51
106
  window.open(text);
52
107
  },
53
108
  },
109
+
110
+ //Set rotation to the angle between the x-axis and a line from the word "rotate" to the mouse pointer (while dragging).
111
+ //The rotation is in range (-180,180]
112
+ {
113
+ regexp: /rotate\(-?\d*\.?\d*\)/g,
114
+ cursor: 'move',
115
+ onDragStart(text, setText, e) {
116
+ rotationOrigin = { x: e.screenX, y: e.screenY };
117
+ },
118
+
119
+ onDrag(text, setText, e) {
120
+ if (rotationOrigin == null) return;
121
+ //Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
122
+ setText(
123
+ `rotate(${Math.round(
124
+ (Math.atan2(
125
+ rotationOrigin.y - e.screenY,
126
+ e.screenX - rotationOrigin.x,
127
+ ) *
128
+ 180) /
129
+ Math.PI,
130
+ )})`,
131
+ );
132
+ },
133
+ onDragEnd(text, setText) {
134
+ rotationOrigin = null;
135
+ },
136
+ },
54
137
  ],
55
138
  });
139
+
140
+ let rotationOrigin: { x: number; y: number } = null;
141
+
142
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L108
143
+ const hex2RGB = (hex: string): [number, number, number] => {
144
+ const v = parseInt(hex.substring(1), 16);
145
+ return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
146
+ };
147
+
148
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L117
149
+ const rgb2Hex = (r: number, g: number, b: number): string =>
150
+ '#' + r.toString(16) + g.toString(16) + b.toString(16);
@@ -1,7 +1,5 @@
1
1
  import { useCallback } from 'react';
2
- import Modal from 'react-bootstrap/Modal';
3
- import Form from 'react-bootstrap/Form';
4
- import Button from 'react-bootstrap/Button';
2
+ import { Button, Modal, Form } from './bootstrap';
5
3
  import Select from 'react-select';
6
4
  import {
7
5
  ThemeLabel,
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { Button, Modal } from '../bootstrap';
3
+
4
+ // Inspired by https://react-bootstrap.netlify.app/docs/components/modal/#live-demo
5
+ export const ConfirmationBox = ({
6
+ show,
7
+ onClose,
8
+ onConfirm,
9
+ }) => (
10
+ <Modal
11
+ show={show}
12
+ onHide={onClose}
13
+ // confirm={onConfirm}
14
+ centered
15
+ >
16
+ <Modal.Header closeButton onClick={onClose}>
17
+ <Modal.Title>Delete File</Modal.Title>
18
+ </Modal.Header>
19
+
20
+ <Modal.Body>
21
+ <p>Are you sure you want to delete this file?</p>
22
+ </Modal.Body>
23
+
24
+ <Modal.Footer>
25
+ <Button variant="primary" onClick={onConfirm}>
26
+ Yes
27
+ </Button>
28
+ <Button variant="secondary" onClick={onClose}>
29
+ No
30
+ </Button>
31
+ </Modal.Footer>
32
+ </Modal>
33
+ );
@@ -17,7 +17,10 @@ export const DirectoryListing = ({
17
17
  name: string;
18
18
  path: string;
19
19
  children: Array<FileTree | FileTreeFile>;
20
- handleRenameFileClick: (fileId: string) => void;
20
+ handleRenameFileClick: (
21
+ fileId: string,
22
+ newName: string,
23
+ ) => void;
21
24
  handleDeleteFileClick: (
22
25
  fileId: string,
23
26
  event: React.MouseEvent,
@@ -31,10 +34,12 @@ export const DirectoryListing = ({
31
34
  }, [toggleDirectory]);
32
35
 
33
36
  const handleDeleteClick = useCallback(() => {
37
+ // https://github.com/vizhub-core/vzcode/issues/102
34
38
  console.log('TODO handleDeleteDirectoryClick');
35
39
  }, []);
36
40
 
37
41
  const handleRenameClick = useCallback(() => {
42
+ // https://github.com/vizhub-core/vzcode/issues/103
38
43
  console.log('TODO handleRenameDirectoryClick');
39
44
  }, []);
40
45
 
@@ -50,6 +55,7 @@ export const DirectoryListing = ({
50
55
  handleClick={handleClick}
51
56
  handleDeleteClick={handleDeleteClick}
52
57
  handleRenameClick={handleRenameClick}
58
+ isDir={true}
53
59
  >
54
60
  <div
55
61
  className="arrow-wrapper"
@@ -11,7 +11,10 @@ export const FileListing = ({
11
11
  }: {
12
12
  name: string;
13
13
  fileId: FileId;
14
- handleRenameFileClick: (fileId: FileId) => void;
14
+ handleRenameFileClick: (
15
+ fileId: FileId,
16
+ newName: string,
17
+ ) => void;
15
18
  handleDeleteFileClick: (
16
19
  fileId: FileId,
17
20
  event: React.MouseEvent,
@@ -23,21 +26,25 @@ export const FileListing = ({
23
26
  }, [fileId, handleFileClick]);
24
27
 
25
28
  const handleDeleteClick = useCallback(
26
- (event) => {
29
+ (event: React.MouseEvent) => {
27
30
  handleDeleteFileClick(fileId, event);
28
31
  },
29
- [fileId],
32
+ [fileId, handleDeleteFileClick],
30
33
  );
31
34
 
32
- const handleRenameClick = useCallback(() => {
33
- handleRenameFileClick(fileId);
34
- }, [fileId]);
35
+ const handleRenameClick = useCallback(
36
+ (newName: string) => {
37
+ handleRenameFileClick(fileId, newName);
38
+ },
39
+ [fileId, handleRenameFileClick],
40
+ );
35
41
 
36
42
  return (
37
43
  <Item
38
44
  handleClick={handleClick}
39
45
  handleDeleteClick={handleDeleteClick}
40
46
  handleRenameClick={handleRenameClick}
47
+ isDir={false}
41
48
  >
42
49
  {name}
43
50
  </Item>
@@ -0,0 +1,121 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import { ConfirmationBox } from './ConfirmationBox';
3
+
4
+ export const Item = ({
5
+ name,
6
+ children,
7
+ handleClick,
8
+ handleRenameClick,
9
+ handleDeleteClick,
10
+ isDir,
11
+ }) => {
12
+ // Tracks whether the mouse is hovering over the file or directory
13
+ const [isHovered, setIsHovered] = useState(false);
14
+
15
+ // Tracks whether the file is being renamed (inline text input)
16
+ const [isRenaming, setIsRenaming] = useState(false);
17
+
18
+ // Tracks the value of the input field when renaming
19
+ const [renameValue, setRenameValue] = useState('');
20
+
21
+ const onKeyDown = useCallback((event) => {
22
+ // TODO make 'Escape' cancel the changes - do not commit the rename
23
+ // https://github.com/vizhub-core/vzcode/issues/207
24
+ if (event.key === 'Enter' || event.key === 'Escape') {
25
+ event.target.blur();
26
+ }
27
+ }, []);
28
+
29
+ const onBlur = useCallback(
30
+ (event: React.FocusEvent<HTMLInputElement>) => {
31
+ if (event.target.value.trim() === '') {
32
+ // setEditingValue(value);
33
+ console.log('TODO: handle empty rename');
34
+ } else {
35
+ setIsRenaming(false);
36
+ handleRenameClick(event.target.value);
37
+ }
38
+ },
39
+ [handleRenameClick],
40
+ );
41
+
42
+ const [showModal, setShowModal] = useState(false);
43
+
44
+ // Function to open the delete file confirmation modal
45
+ const handleModalOpen = useCallback(() => {
46
+ setShowModal(true);
47
+ }, []);
48
+
49
+ // Function to close the modal
50
+ const handleModalClose = useCallback(() => {
51
+ setShowModal(false);
52
+ }, []);
53
+
54
+ // Function to handle confirmation on modal
55
+ const handleConfirmModal = useCallback(() => {
56
+ setShowModal(false);
57
+ handleDeleteClick();
58
+ }, [handleDeleteClick]);
59
+
60
+ const handleMouseEnter = useCallback(() => {
61
+ setIsHovered(true);
62
+ }, []);
63
+
64
+ const handleMouseLeave = useCallback(() => {
65
+ setIsHovered(false);
66
+ }, []);
67
+
68
+ const onChange = useCallback(
69
+ (event: React.ChangeEvent<HTMLInputElement>) => {
70
+ setRenameValue(event.target.value);
71
+ },
72
+ [],
73
+ );
74
+
75
+ return (
76
+ <div
77
+ className="full-box file-or-directory"
78
+ onClick={isRenaming ? null : handleClick}
79
+ onMouseEnter={handleMouseEnter}
80
+ onMouseLeave={handleMouseLeave}
81
+ >
82
+ <div className="name">
83
+ {isRenaming ? (
84
+ <input
85
+ type="text"
86
+ aria-label="Field name"
87
+ value={renameValue}
88
+ onKeyDown={onKeyDown}
89
+ onBlur={onBlur}
90
+ onChange={onChange}
91
+ />
92
+ ) : (
93
+ children
94
+ )}
95
+ </div>
96
+ {isHovered ? (
97
+ <div className="utils">
98
+ <i
99
+ className="bx bxs-edit utilities"
100
+ style={{ color: '#abdafb' }}
101
+ onClick={() => {
102
+ setIsRenaming(!isRenaming);
103
+ setRenameValue(isDir ? name : children);
104
+ }}
105
+ ></i>
106
+ <i
107
+ className="bx bx-trash"
108
+ style={{ color: '#eb336c' }}
109
+ onClick={handleModalOpen}
110
+ ></i>
111
+ </div>
112
+ ) : null}
113
+
114
+ <ConfirmationBox
115
+ show={showModal}
116
+ onClose={handleModalClose}
117
+ onConfirm={handleConfirmModal}
118
+ />
119
+ </div>
120
+ );
121
+ };
@@ -17,7 +17,10 @@ export const Listing = ({
17
17
  toggleDirectory,
18
18
  }: {
19
19
  entity: FileTree | FileTreeFile;
20
- handleRenameFileClick: (fileId: FileId) => void;
20
+ handleRenameFileClick: (
21
+ fileId: FileId,
22
+ newName: string,
23
+ ) => void;
21
24
  handleDeleteFileClick: (
22
25
  fileId: FileId,
23
26
  event: React.MouseEvent,
@@ -24,7 +24,10 @@ export const Sidebar = ({
24
24
  }: {
25
25
  files: Files;
26
26
  createFile?: () => void;
27
- handleRenameFileClick?: (fileId: FileId) => void;
27
+ handleRenameFileClick?: (
28
+ fileId: FileId,
29
+ newName: string,
30
+ ) => void;
28
31
  handleDeleteFileClick?: (
29
32
  fileId: FileId,
30
33
  event: React.MouseEvent,
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import { useCallback, useMemo, useEffect } from 'react';
2
2
  import { FileId, Files } from '../../types';
3
3
  import './style.scss';
4
4
 
@@ -69,6 +69,28 @@ export const TabList = ({
69
69
  setActiveFileId: (fileId: FileId) => void;
70
70
  closeTab: (fileId: FileId) => void;
71
71
  }) => {
72
+ //Create a function to perform the logic to delete the current tab
73
+ const handleKeyPress = useCallback(
74
+ (event: { altKey: boolean; key: string }) => {
75
+ if (event.altKey == true) {
76
+ if (event.key == 'w') {
77
+ closeTab(activeFileId);
78
+ }
79
+ }
80
+ },
81
+ [closeTab, activeFileId],
82
+ );
83
+ useEffect(() => {
84
+ // attach the event listener
85
+ document.addEventListener('keydown', handleKeyPress);
86
+ // remove the event listener
87
+ return () => {
88
+ document.removeEventListener(
89
+ 'keydown',
90
+ handleKeyPress,
91
+ );
92
+ };
93
+ }, [handleKeyPress]);
72
94
  return (
73
95
  <div className="vz-tab-list">
74
96
  {files &&
@@ -0,0 +1,8 @@
1
+ // A central place where we import what we need from react-bootstrap.
2
+ // This strange looking way of importing makes it work with Vite's SSR build.
3
+ // Otherwise we get `Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import`
4
+ import Form from 'react-bootstrap/cjs/Form.js';
5
+ import Button from 'react-bootstrap/cjs/Button.js';
6
+ import Modal from 'react-bootstrap/cjs/Modal.js';
7
+
8
+ export { Form, Button, Modal };
@@ -109,7 +109,7 @@ const save = () => {
109
109
 
110
110
  // Handle renaming files.
111
111
  if (previous.name !== current.name) {
112
- fs.renamesync(previous.name, current.name);
112
+ fs.renameSync(previous.name, current.name);
113
113
  }
114
114
  }
115
115