vzcode 0.23.0 → 0.24.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 +111 -0
- package/dist/assets/{index-c63b4be7.css → index-cf19de3b.css} +1 -1
- package/dist/assets/{worker-d5f0575a.js → worker-b775ddff.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +17 -9
- package/src/client/App.tsx +77 -10
- package/src/client/CodeEditor/getOrCreateEditor.ts +7 -1
- package/src/client/CodeEditor/index.tsx +9 -5
- package/src/client/CodeEditor/json1PresenceBroadcast.js +2 -0
- package/src/client/CodeEditor/json1PresenceDisplay.js +45 -1
- package/src/client/CodeEditor/widgets.ts +71 -0
- package/src/client/PresenceNotifications/index.tsx +145 -0
- package/src/client/PresenceNotifications/style.scss +17 -0
- package/src/client/PrettierErrorOverlay/index.tsx +9 -0
- package/src/client/PrettierErrorOverlay/style.scss +9 -0
- package/src/client/Settings.tsx +11 -1
- package/src/client/Sidebar/FileListing.tsx +0 -1
- package/src/client/Sidebar/Item.tsx +17 -8
- package/src/client/reducer.ts +122 -0
- package/src/client/style.scss +3 -0
- package/src/client/usePrettier/index.ts +32 -14
- package/src/client/usePrettier/worker.ts +1 -1
- package/src/client/useTabsState.ts +42 -5
- package/dist/assets/index-49f71876.js +0 -110
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import './style.scss';
|
|
3
|
+
|
|
4
|
+
type PresenceId = string;
|
|
5
|
+
|
|
6
|
+
// A presence notification is a message that is displayed when a user joins or leaves a session.
|
|
7
|
+
// It has the following properties:
|
|
8
|
+
// - `user` is the username of the user who joined or left the session.
|
|
9
|
+
// - `join` is a boolean value that indicates whether the user joined or left the session.
|
|
10
|
+
// - `true` means user has joined the session.
|
|
11
|
+
// - `false` means user has left the session.
|
|
12
|
+
type PresenceNotification = {
|
|
13
|
+
user: string;
|
|
14
|
+
join: boolean;
|
|
15
|
+
presenceId: PresenceId;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const PresenceNotifications = ({
|
|
19
|
+
docPresence,
|
|
20
|
+
}: {
|
|
21
|
+
docPresence?: any;
|
|
22
|
+
}) => {
|
|
23
|
+
const [presenceNotifications, setPresenceNotifications] =
|
|
24
|
+
useState<Array<PresenceNotification>>([]);
|
|
25
|
+
|
|
26
|
+
// const [presenceIds, setPresenceIds] = useState<
|
|
27
|
+
// Array<any>
|
|
28
|
+
// >([]);
|
|
29
|
+
const alreadyJoinedPresenceIds = useRef<Set<PresenceId>>(
|
|
30
|
+
new Set(),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// console.log('useEffect triggered.');
|
|
35
|
+
if (docPresence) {
|
|
36
|
+
// See https://share.github.io/sharedb/presence
|
|
37
|
+
docPresence.on('receive', (presenceId, update) => {
|
|
38
|
+
// Reason for notifications appearing on every cursor movement:
|
|
39
|
+
// code doesn't check if a notification about a user joining was previously sent
|
|
40
|
+
|
|
41
|
+
// Attempted solutions:
|
|
42
|
+
// - create an array called presenceIds that keeps track of the presences that join and leave
|
|
43
|
+
// - if presenceId is in presenceIds: local user was already alerted that a remote user joined
|
|
44
|
+
// - if presenceId is not in presenceIds: local user needs to be notified about a new user
|
|
45
|
+
// - problems:
|
|
46
|
+
// - the array is always empty inside useEffect (array works outside of useEffect)
|
|
47
|
+
// - causes check to fail
|
|
48
|
+
// - check docPresence.remotePresences, which keeps track of all the remote presences on the doc
|
|
49
|
+
// - see https://share.github.io/sharedb/api/presence
|
|
50
|
+
// - if presenceId is in docPresence.remotePresences: local user was already alerted that a remote user joined
|
|
51
|
+
// - if presenceId is not in docPresence.remotePresences: local user needs to be notified about a new user
|
|
52
|
+
// - problems:
|
|
53
|
+
// - useEffects is called when docPresence changes (in some cases, it's when the remote presence changes)
|
|
54
|
+
// - remotePresences already has the remote presence in the array, so check fails
|
|
55
|
+
// - could add another property to presence (true: already notified, false: need to be notified)
|
|
56
|
+
// - doesn't seem like an ideal solution
|
|
57
|
+
|
|
58
|
+
// console.log('docPresence:', docPresence);
|
|
59
|
+
// console.log('docPresence remotePresences:', docPresence.remotePresences);
|
|
60
|
+
// console.log('check for presence:', docPresence.remotePresences[presenceId]);
|
|
61
|
+
|
|
62
|
+
// TODO figure out how to get username from `presenceId`.
|
|
63
|
+
const user = 'someone';
|
|
64
|
+
|
|
65
|
+
// `true` means user has joined the session.
|
|
66
|
+
// `false` means user has left the session.
|
|
67
|
+
const join = update !== null;
|
|
68
|
+
|
|
69
|
+
console.log('join', join);
|
|
70
|
+
|
|
71
|
+
// Figure out if we have ever seen this user before.
|
|
72
|
+
|
|
73
|
+
// if (join && docPresence.remotePresences[presenceId] === undefined){
|
|
74
|
+
if (join) {
|
|
75
|
+
if (
|
|
76
|
+
!alreadyJoinedPresenceIds.current.has(
|
|
77
|
+
presenceId,
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
alreadyJoinedPresenceIds.current.add(
|
|
81
|
+
presenceId,
|
|
82
|
+
);
|
|
83
|
+
setPresenceNotifications((prev) => [
|
|
84
|
+
...prev,
|
|
85
|
+
{ user, join, presenceId },
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Remove it after 5 seconds.
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
setPresenceNotifications((prev) =>
|
|
91
|
+
prev.filter(
|
|
92
|
+
(notification) =>
|
|
93
|
+
notification.presenceId !==
|
|
94
|
+
presenceId &&
|
|
95
|
+
notification.join !== join,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}, 5000);
|
|
99
|
+
}
|
|
100
|
+
} else if (!join) {
|
|
101
|
+
setPresenceNotifications((prev) => [
|
|
102
|
+
...prev,
|
|
103
|
+
{ user, join, presenceId },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Remove it after 5 seconds.
|
|
107
|
+
// TODO clean up this duplication.
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
setPresenceNotifications((prev) =>
|
|
110
|
+
prev.filter(
|
|
111
|
+
(notification) =>
|
|
112
|
+
notification.presenceId !== presenceId &&
|
|
113
|
+
notification.join !== join,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}, 5000);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, [docPresence]);
|
|
121
|
+
|
|
122
|
+
// console.log(
|
|
123
|
+
// 'presenceNotifications',
|
|
124
|
+
// presenceNotifications,
|
|
125
|
+
// );
|
|
126
|
+
// console.log(
|
|
127
|
+
// 'presenceIds',
|
|
128
|
+
// presenceIds,
|
|
129
|
+
// );
|
|
130
|
+
|
|
131
|
+
return presenceNotifications.length > 0 ? (
|
|
132
|
+
<div className="vz-notification">
|
|
133
|
+
{presenceNotifications.map((notification, i) => {
|
|
134
|
+
const { user, join } = notification;
|
|
135
|
+
return (
|
|
136
|
+
<div key={i} className="vz-notification-item">
|
|
137
|
+
{join
|
|
138
|
+
? `${user} has joined the session.`
|
|
139
|
+
: `${user} has left the session.`}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
) : null;
|
|
145
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.vz-notification {
|
|
2
|
+
position: fixed;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 10px;
|
|
6
|
+
bottom: 20px;
|
|
7
|
+
right: 20px;
|
|
8
|
+
|
|
9
|
+
color: rgb(154, 160, 172);
|
|
10
|
+
padding: 15px;
|
|
11
|
+
|
|
12
|
+
.vz-notification-item {
|
|
13
|
+
background-color: rgb(54, 65, 89);
|
|
14
|
+
padding: 5px;
|
|
15
|
+
border-radius: 5px;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/client/Settings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
2
|
import { Button, Modal, Form } from './bootstrap';
|
|
3
3
|
import { ThemeLabel, themes } from './themes';
|
|
4
4
|
|
|
@@ -9,6 +9,8 @@ import { ThemeLabel, themes } from './themes';
|
|
|
9
9
|
// { value: 30, label: '30 seconds' },
|
|
10
10
|
// ];
|
|
11
11
|
|
|
12
|
+
export let username = 'Anonymous';
|
|
13
|
+
|
|
12
14
|
export const Settings = ({
|
|
13
15
|
show,
|
|
14
16
|
onClose,
|
|
@@ -41,6 +43,12 @@ export const Settings = ({
|
|
|
41
43
|
// [],
|
|
42
44
|
// );
|
|
43
45
|
|
|
46
|
+
const usernameRef = useRef<HTMLInputElement>(null);
|
|
47
|
+
const handleUsernameChange = () => {
|
|
48
|
+
const usernameValue = usernameRef.current?.value;
|
|
49
|
+
username = usernameValue;
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
return show ? (
|
|
45
53
|
<Modal
|
|
46
54
|
className="vz-settings"
|
|
@@ -56,6 +64,8 @@ export const Settings = ({
|
|
|
56
64
|
<Form.Label>Username</Form.Label>
|
|
57
65
|
<Form.Control
|
|
58
66
|
type="text"
|
|
67
|
+
ref={usernameRef}
|
|
68
|
+
onChange={handleUsernameChange}
|
|
59
69
|
placeholder="Enter username"
|
|
60
70
|
/>
|
|
61
71
|
<Form.Text className="text-muted">
|
|
@@ -42,9 +42,13 @@ export const Item = ({
|
|
|
42
42
|
const [showModal, setShowModal] = useState(false);
|
|
43
43
|
|
|
44
44
|
// Function to open the delete file confirmation modal
|
|
45
|
-
const handleModalOpen = useCallback(
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const handleModalOpen = useCallback(
|
|
46
|
+
(event: React.MouseEvent) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
setShowModal(true);
|
|
49
|
+
},
|
|
50
|
+
[],
|
|
51
|
+
);
|
|
48
52
|
|
|
49
53
|
// Function to close the modal
|
|
50
54
|
const handleModalClose = useCallback(() => {
|
|
@@ -52,10 +56,14 @@ export const Item = ({
|
|
|
52
56
|
}, []);
|
|
53
57
|
|
|
54
58
|
// Function to handle confirmation on modal
|
|
55
|
-
const handleConfirmModal = useCallback(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const handleConfirmModal = useCallback(
|
|
60
|
+
(event: React.MouseEvent) => {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
setShowModal(false);
|
|
63
|
+
handleDeleteClick();
|
|
64
|
+
},
|
|
65
|
+
[handleDeleteClick],
|
|
66
|
+
);
|
|
59
67
|
|
|
60
68
|
const handleMouseEnter = useCallback(() => {
|
|
61
69
|
setIsHovered(true);
|
|
@@ -98,7 +106,8 @@ export const Item = ({
|
|
|
98
106
|
<i
|
|
99
107
|
className="bx bxs-edit utilities"
|
|
100
108
|
style={{ color: '#abdafb' }}
|
|
101
|
-
onClick={() => {
|
|
109
|
+
onClick={(event: React.MouseEvent) => {
|
|
110
|
+
event.stopPropagation();
|
|
102
111
|
setIsRenaming(!isRenaming);
|
|
103
112
|
setRenameValue(isDir ? name : children);
|
|
104
113
|
}}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { FileId } from '../types';
|
|
2
|
+
|
|
3
|
+
export const reducer = (state, action) => {
|
|
4
|
+
switch (action.type) {
|
|
5
|
+
// TODO phase this out
|
|
6
|
+
case 'set_tab_list': {
|
|
7
|
+
return { ...state, tabList: action.tabList };
|
|
8
|
+
}
|
|
9
|
+
case 'set_active_fileId': {
|
|
10
|
+
return {
|
|
11
|
+
...state,
|
|
12
|
+
activeFileId: action.activeFileId,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
case 'open_tab': {
|
|
16
|
+
if (!state.tabList.includes(action.fileId)) {
|
|
17
|
+
return {
|
|
18
|
+
...state,
|
|
19
|
+
activeFileId: action.fileId,
|
|
20
|
+
tabList: [...state.tabList, action.fileId],
|
|
21
|
+
};
|
|
22
|
+
} else {
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
activeFileId: action.fileId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
case 'close_tab': {
|
|
30
|
+
const i = state.tabList.findIndex(
|
|
31
|
+
(fileId) => fileId === action.fileIdToRemove,
|
|
32
|
+
);
|
|
33
|
+
if (i != -1) {
|
|
34
|
+
const newTabList = [
|
|
35
|
+
...state.tabList.slice(0, i),
|
|
36
|
+
...state.tabList.slice(i + 1),
|
|
37
|
+
];
|
|
38
|
+
if (state.activeFileId === action.fileIdToRemove) {
|
|
39
|
+
// set the new active file to the next tab over,
|
|
40
|
+
if (newTabList.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
activeFileId:
|
|
44
|
+
i === 0 ? newTabList[i] : newTabList[i - 1],
|
|
45
|
+
tabList: newTabList,
|
|
46
|
+
};
|
|
47
|
+
} else {
|
|
48
|
+
// or clear out the active file
|
|
49
|
+
// if we've closed the last tab.
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
activeFileId: null,
|
|
53
|
+
tabList: newTabList,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
tabList: newTabList,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...state,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case 'multi_close_tab': {
|
|
67
|
+
let newTabList = [...state.tabList]; // Create a copy of the tabList array.
|
|
68
|
+
let newActiveFileId: FileId = state.activeFileId;
|
|
69
|
+
action.idsToDelete.forEach((id) => {
|
|
70
|
+
const i = newTabList.findIndex(
|
|
71
|
+
(fileId) => fileId === id,
|
|
72
|
+
);
|
|
73
|
+
if (i !== -1) {
|
|
74
|
+
// Remove the tab from the tab list.
|
|
75
|
+
newTabList = [
|
|
76
|
+
...newTabList.slice(0, i),
|
|
77
|
+
...newTabList.slice(i + 1),
|
|
78
|
+
];
|
|
79
|
+
// If we are closing the active file,
|
|
80
|
+
if (newActiveFileId === id) {
|
|
81
|
+
// set the new active file to the next tab over,
|
|
82
|
+
if (newTabList.length > 0) {
|
|
83
|
+
newActiveFileId =
|
|
84
|
+
i === 0 ? newTabList[i] : newTabList[i - 1];
|
|
85
|
+
} else {
|
|
86
|
+
// or clear out the active file if we've closed the last tab.
|
|
87
|
+
newActiveFileId = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
tabList: newTabList,
|
|
95
|
+
activeFileId: newActiveFileId,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// TODO eventually
|
|
99
|
+
// case 'init_sharedb': {
|
|
100
|
+
// return {
|
|
101
|
+
// ...state,
|
|
102
|
+
// submitOperation: action.submitOperation
|
|
103
|
+
// }
|
|
104
|
+
// }
|
|
105
|
+
// case 'delete_file': {
|
|
106
|
+
// case 'delete_directory': {
|
|
107
|
+
// case 'create_file': {
|
|
108
|
+
// state.submitOperation((document) => ({
|
|
109
|
+
// ...document,
|
|
110
|
+
// files: {
|
|
111
|
+
// ...document.files,
|
|
112
|
+
// [randomId()]: { name:action.name, text: '' },
|
|
113
|
+
// },
|
|
114
|
+
// }))
|
|
115
|
+
// return state;
|
|
116
|
+
|
|
117
|
+
// }
|
|
118
|
+
// case 'rename_file': {
|
|
119
|
+
// ...
|
|
120
|
+
}
|
|
121
|
+
throw Error('Unknown action: ' + action.type);
|
|
122
|
+
};
|
package/src/client/style.scss
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
ShareDBDoc,
|
|
5
5
|
VZCodeContent,
|
|
6
6
|
} from '../../types';
|
|
7
|
-
import { useEffect, useRef } from 'react';
|
|
7
|
+
import { useEffect, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
// The time in milliseconds by which auto-saving is debounced.
|
|
10
10
|
const autoPrettierDebounceTimeMS = 1200;
|
|
@@ -31,6 +31,14 @@ export const usePrettier = (
|
|
|
31
31
|
// since the last Prettier run.
|
|
32
32
|
const dirtyFiles: Set<FileId> = new Set<FileId>();
|
|
33
33
|
|
|
34
|
+
// State to hold the error from Prettier
|
|
35
|
+
// `null` means no errors
|
|
36
|
+
// If this is a string, it's the
|
|
37
|
+
// error message from Prettier.
|
|
38
|
+
const [prettierError, setPrettierError] = useState<
|
|
39
|
+
string | null
|
|
40
|
+
>(null);
|
|
41
|
+
|
|
34
42
|
// A ref to keep track of whether an op is being applied.
|
|
35
43
|
// This is used to prevent Prettier from running
|
|
36
44
|
// when the op is coming from Prettier itself.
|
|
@@ -46,9 +54,18 @@ export const usePrettier = (
|
|
|
46
54
|
const handleMessage = (event) => {
|
|
47
55
|
const { fileId, error, fileTextPrettified } =
|
|
48
56
|
event.data;
|
|
57
|
+
|
|
58
|
+
// Handle syntax errors
|
|
49
59
|
if (error) {
|
|
50
|
-
|
|
60
|
+
// If there's an error, set the error state
|
|
61
|
+
setPrettierError(error);
|
|
62
|
+
|
|
63
|
+
// Return early as there is no text change to apply.
|
|
51
64
|
return;
|
|
65
|
+
} else {
|
|
66
|
+
// Make sure the error overlay goes away
|
|
67
|
+
// when Prettier runs successfully with no errors.
|
|
68
|
+
setPrettierError(null);
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
isApplyingOpRef.current = true;
|
|
@@ -159,19 +176,20 @@ export const usePrettier = (
|
|
|
159
176
|
} else {
|
|
160
177
|
// console.log('ignoring op from remote', op, isLocal);
|
|
161
178
|
}
|
|
162
|
-
|
|
163
|
-
// In the event that the component unmounts,
|
|
164
|
-
return () => {
|
|
165
|
-
// Remove the event listener
|
|
166
|
-
prettierWorker.removeEventListener(
|
|
167
|
-
'message',
|
|
168
|
-
handleMessage,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// Clear the timeout
|
|
172
|
-
clearTimeout(debounceTimeout);
|
|
173
|
-
};
|
|
174
179
|
},
|
|
175
180
|
);
|
|
181
|
+
|
|
182
|
+
// In the event that the component unmounts,
|
|
183
|
+
return () => {
|
|
184
|
+
// Remove the event listener
|
|
185
|
+
prettierWorker.removeEventListener(
|
|
186
|
+
'message',
|
|
187
|
+
handleMessage,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Clear the timeout
|
|
191
|
+
clearTimeout(debounceTimeout);
|
|
192
|
+
};
|
|
176
193
|
}, [shareDBDoc]);
|
|
194
|
+
return { prettierError }; // Return the errors for use in other files
|
|
177
195
|
};
|
|
@@ -11,6 +11,7 @@ export const useTabsState = (
|
|
|
11
11
|
// Called when a tab is closed.
|
|
12
12
|
const closeTab = useCallback(
|
|
13
13
|
(fileIdToRemove: FileId) => {
|
|
14
|
+
let newActiveFileId: FileId = activeFileId;
|
|
14
15
|
const i = tabList.findIndex(
|
|
15
16
|
(fileId) => fileId === fileIdToRemove,
|
|
16
17
|
);
|
|
@@ -29,16 +30,52 @@ export const useTabsState = (
|
|
|
29
30
|
if (activeFileId === fileIdToRemove) {
|
|
30
31
|
// set the new active file to the next tab over,
|
|
31
32
|
if (newTabList.length > 0) {
|
|
32
|
-
|
|
33
|
-
i === 0 ? newTabList[i] : newTabList[i - 1]
|
|
34
|
-
);
|
|
33
|
+
newActiveFileId =
|
|
34
|
+
i === 0 ? newTabList[i] : newTabList[i - 1];
|
|
35
35
|
} else {
|
|
36
36
|
// or clear out the active file
|
|
37
37
|
// if we've closed the last tab.
|
|
38
|
-
|
|
38
|
+
newActiveFileId = null;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
setActiveFileId(newActiveFileId);
|
|
43
|
+
},
|
|
44
|
+
[tabList, activeFileId],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
//When closing multiple tabs use this function over CloseTab()
|
|
48
|
+
const multiCloseTab = useCallback(
|
|
49
|
+
(idsToDelete: Array<FileId>) => {
|
|
50
|
+
let newTabList = [...tabList]; // Create a copy of the tabList array.
|
|
51
|
+
let newActiveFileId: FileId = activeFileId;
|
|
52
|
+
idsToDelete.forEach((id) => {
|
|
53
|
+
const i = newTabList.findIndex(
|
|
54
|
+
(fileId) => fileId === id,
|
|
55
|
+
);
|
|
56
|
+
if (i !== -1) {
|
|
57
|
+
// Remove the tab from the tab list.
|
|
58
|
+
newTabList = [
|
|
59
|
+
...newTabList.slice(0, i),
|
|
60
|
+
...newTabList.slice(i + 1),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// If we are closing the active file,
|
|
64
|
+
if (newActiveFileId === id) {
|
|
65
|
+
// set the new active file to the next tab over,
|
|
66
|
+
if (newTabList.length > 0) {
|
|
67
|
+
newActiveFileId =
|
|
68
|
+
i === 0 ? newTabList[i] : newTabList[i - 1];
|
|
69
|
+
} else {
|
|
70
|
+
// or clear out the active file if we've closed the last tab.
|
|
71
|
+
newActiveFileId = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
//Run setStates after doing multiple changes to the two variables. Will properly render tabList and activeFileID.
|
|
77
|
+
setActiveFileId(newActiveFileId);
|
|
78
|
+
setTabList(newTabList);
|
|
42
79
|
},
|
|
43
80
|
[tabList, activeFileId],
|
|
44
81
|
);
|
|
@@ -54,5 +91,5 @@ export const useTabsState = (
|
|
|
54
91
|
[tabList],
|
|
55
92
|
);
|
|
56
93
|
|
|
57
|
-
return { tabList, closeTab, openTab };
|
|
94
|
+
return { tabList, closeTab, openTab, multiCloseTab };
|
|
58
95
|
};
|