sample-ui-component-library 0.0.51-dev → 0.0.53-dev
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/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +6 -6
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Editor/Editor.jsx +51 -16
- package/src/components/Editor/EditorReducer.js +31 -1
- package/src/components/Editor/MonacoInstance/MonacoInstance copy.jsx +184 -0
- package/src/components/Editor/MonacoInstance/MonacoInstance.jsx +133 -161
- package/src/components/Editor/MonacoInstance/MonacoInstance.scss +1 -0
- package/src/components/Editor/Tabs/Tab/Tab.jsx +7 -2
- package/src/components/Editor/Tabs/Tabs.jsx +2 -2
- package/src/stories/Editor.stories.js +13 -8
package/package.json
CHANGED
|
@@ -10,7 +10,8 @@ import React, {
|
|
|
10
10
|
useImperativeHandle,
|
|
11
11
|
useMemo,
|
|
12
12
|
useReducer,
|
|
13
|
-
useContext
|
|
13
|
+
useContext,
|
|
14
|
+
useRef,
|
|
14
15
|
} from "react";
|
|
15
16
|
|
|
16
17
|
import { EditorContext } from "./EditorContext";
|
|
@@ -27,14 +28,19 @@ const MODES = {
|
|
|
27
28
|
*
|
|
28
29
|
* @return {JSX}
|
|
29
30
|
*/
|
|
30
|
-
export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
|
|
31
|
+
export const Editor = forwardRef(({ onSelectAbstraction, onSelectTab }, ref) => {
|
|
31
32
|
const [state, dispatch] = useReducer(editorReducer, initialState);
|
|
33
|
+
const editorRef = useRef();
|
|
32
34
|
|
|
33
35
|
const selectTab = useCallback((id) => {
|
|
34
36
|
dispatch({ type: "SELECT_TAB", payload: id });
|
|
35
|
-
|
|
37
|
+
if (onSelectTab) {
|
|
38
|
+
onSelectTab(id);
|
|
39
|
+
}
|
|
40
|
+
}, [onSelectTab]);
|
|
36
41
|
|
|
37
42
|
const closeTab = useCallback((id) => {
|
|
43
|
+
editorRef.current && editorRef.current.clearModel(id);
|
|
38
44
|
dispatch({ type: "CLOSE_TAB", payload: id });
|
|
39
45
|
}, []);
|
|
40
46
|
|
|
@@ -63,19 +69,46 @@ export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
|
|
|
63
69
|
dispatch({ type: "SET_MAPPED_IDS", payload: ids });
|
|
64
70
|
}, []);
|
|
65
71
|
|
|
72
|
+
const getTabs = useCallback(() => {
|
|
73
|
+
return state.tabs;
|
|
74
|
+
}, [state.tabs]);
|
|
75
|
+
|
|
76
|
+
const getTab = useCallback((id) => {
|
|
77
|
+
return state.tabs.find((tab) => tab.uid === id);
|
|
78
|
+
}, [state.tabs]);
|
|
79
|
+
|
|
80
|
+
const getActiveTab = useCallback(() => {
|
|
81
|
+
return state.tabs.find((tab) => tab.id === state.activeTabId);
|
|
82
|
+
}, [state.tabs, state.activeTabId]);
|
|
83
|
+
|
|
84
|
+
const setUpdatedContent = useCallback((tab, content) => {
|
|
85
|
+
dispatch({ type: "SET_UPDATED_CONTENT", payload: {tab, content} });
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const setContent = useCallback((tab, content) => {
|
|
89
|
+
dispatch({ type: "SET_CONTENT", payload: {tab, content} });
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const api_entries = {
|
|
93
|
+
state,
|
|
94
|
+
addTab,
|
|
95
|
+
setTabGroupId,
|
|
96
|
+
selectTab,
|
|
97
|
+
closeTab,
|
|
98
|
+
moveTab,
|
|
99
|
+
setMapping,
|
|
100
|
+
setMode,
|
|
101
|
+
setMappedIds,
|
|
102
|
+
getTab,
|
|
103
|
+
getTabs,
|
|
104
|
+
getActiveTab,
|
|
105
|
+
setUpdatedContent,
|
|
106
|
+
setContent
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
const api = useMemo(() => {
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
addTab,
|
|
70
|
-
setTabGroupId,
|
|
71
|
-
selectTab,
|
|
72
|
-
closeTab,
|
|
73
|
-
moveTab,
|
|
74
|
-
setMapping,
|
|
75
|
-
setMode,
|
|
76
|
-
setMappedIds
|
|
77
|
-
};
|
|
78
|
-
}, [state, addTab, selectTab, closeTab, moveTab, setTabGroupId, setMapping, setMode, setMappedIds]);
|
|
110
|
+
return api_entries;
|
|
111
|
+
}, Object.values(api_entries));
|
|
79
112
|
|
|
80
113
|
useImperativeHandle(ref, () => api, [api]);
|
|
81
114
|
|
|
@@ -86,7 +119,9 @@ export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
|
|
|
86
119
|
<Tabs />
|
|
87
120
|
</div>
|
|
88
121
|
<div className="monacoContainer">
|
|
89
|
-
<MonacoInstance
|
|
122
|
+
<MonacoInstance
|
|
123
|
+
ref={editorRef}
|
|
124
|
+
onSelectAbstraction={onSelectAbstraction}/>
|
|
90
125
|
</div>
|
|
91
126
|
</div>
|
|
92
127
|
</EditorContext.Provider>
|
|
@@ -7,7 +7,8 @@ export const initialState = {
|
|
|
7
7
|
mode: EDITOR_MODES.DESIGN,
|
|
8
8
|
mapping: new Map(),
|
|
9
9
|
parentTabGroupId: null,
|
|
10
|
-
mappedIds: []
|
|
10
|
+
mappedIds: [],
|
|
11
|
+
modifiedIndicator: false,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export const editorReducer = (state, action) => {
|
|
@@ -24,6 +25,11 @@ export const editorReducer = (state, action) => {
|
|
|
24
25
|
activeTab: tabInfo
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
// Initialize updatedContent and isDirty properties for the tab.
|
|
30
|
+
// These are used to track whether the content of the tab has been modified since it was opened.
|
|
31
|
+
tab.updatedContent = tab.content;
|
|
32
|
+
tab.isDirty = false;
|
|
27
33
|
|
|
28
34
|
// Insert tab at specific location if it was provided.
|
|
29
35
|
let tabs = [...state.tabs];
|
|
@@ -133,6 +139,30 @@ export const editorReducer = (state, action) => {
|
|
|
133
139
|
return initialState;
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
case "SET_UPDATED_CONTENT": {
|
|
143
|
+
const { tab, content } = action.payload;
|
|
144
|
+
tab.updatedContent = content;
|
|
145
|
+
tab.isDirty = (tab.content !== tab.updatedContent);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...state,
|
|
149
|
+
modifiedIndicator: !state.modifiedIndicator,
|
|
150
|
+
tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case "SET_CONTENT": {
|
|
155
|
+
const { tab, content } = action.payload;
|
|
156
|
+
tab.content = content;
|
|
157
|
+
tab.isDirty = (tab.content !== tab.updatedContent);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...state,
|
|
161
|
+
modifiedIndicator: !state.modifiedIndicator,
|
|
162
|
+
tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
136
166
|
default: {
|
|
137
167
|
return state;
|
|
138
168
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
|
|
4
|
+
import EDITOR_MODES from '../EDITOR_MODES';
|
|
5
|
+
|
|
6
|
+
import Editor from '@monaco-editor/react';
|
|
7
|
+
|
|
8
|
+
import "./MonacoInstance.scss"
|
|
9
|
+
|
|
10
|
+
import { useEditor } from "../Editor";
|
|
11
|
+
|
|
12
|
+
export const MonacoInstance = ({onSelectAbstraction, updateContent}) => {
|
|
13
|
+
const { state } = useEditor();
|
|
14
|
+
const [editorContent, setEditorContent] = useState("Loading content...");
|
|
15
|
+
const [showEditor, setShowEditor] = useState(false);
|
|
16
|
+
const activeTabRef = useRef(state.activeTab);
|
|
17
|
+
|
|
18
|
+
const editorRef = useRef(null);
|
|
19
|
+
const content = useRef();
|
|
20
|
+
const containerRef = useRef(null);
|
|
21
|
+
const frameRef = useRef(0);
|
|
22
|
+
|
|
23
|
+
const [overlayDivs, setOverlayDivs] = useState();
|
|
24
|
+
|
|
25
|
+
// When active tab changes, update the editor content and add overlays for the new active tab.
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
if (state.activeTab) {
|
|
28
|
+
console.log(state.activeTab)
|
|
29
|
+
activeTabRef.current = state.activeTab;
|
|
30
|
+
setEditorContent(state.activeTab.updatedContent);
|
|
31
|
+
setShowEditor(true);
|
|
32
|
+
addOverlays();
|
|
33
|
+
} else {
|
|
34
|
+
setShowEditor(false);
|
|
35
|
+
}
|
|
36
|
+
}, [state.activeTab, editorRef]);
|
|
37
|
+
|
|
38
|
+
// When the editor content changes, update the content in the context for the active tab.
|
|
39
|
+
// Setup content change listener for the editor to update the content in the context
|
|
40
|
+
// and track dirty state of the tab.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
content.current = editorContent;
|
|
43
|
+
if (editorRef?.current && content.current !== undefined && content.current !== null) {
|
|
44
|
+
editorRef.current.setValue(content.current);
|
|
45
|
+
editorRef.current.getModel().onDidChangeContent((content) => {
|
|
46
|
+
updateContent(activeTabRef.current, editorRef.current.getValue());
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}, [editorContent]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Callback for when the Monaco Editor is mounted.
|
|
53
|
+
* @param {Object} editor
|
|
54
|
+
* @param {Object} monaco
|
|
55
|
+
*/
|
|
56
|
+
const handleEditorDidMount = useCallback((editor, monaco) => {
|
|
57
|
+
editorRef.current = editor;
|
|
58
|
+
if (content?.current) {
|
|
59
|
+
editorRef.current.setValue(content.current);
|
|
60
|
+
}
|
|
61
|
+
editorRef.current.layout();
|
|
62
|
+
addOverlays();
|
|
63
|
+
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// Add overlays to editor for the given line ranges.
|
|
67
|
+
const addOverlays = useCallback(() => {
|
|
68
|
+
if (!editorRef.current) return;
|
|
69
|
+
|
|
70
|
+
if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
|
|
71
|
+
setOverlayDivs([]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ranges = state.mapping.get(state.activeTab?.name);
|
|
76
|
+
const lineCount = editorRef.current.getModel().getLineCount();
|
|
77
|
+
const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
|
|
78
|
+
const divs = [];
|
|
79
|
+
|
|
80
|
+
ranges.forEach((entry) => {
|
|
81
|
+
const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
|
|
82
|
+
let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
|
|
83
|
+
if (entry.end_line >= lineCount) {
|
|
84
|
+
bottom = bottom + lineHeight;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const style= {
|
|
88
|
+
top: top + "px",
|
|
89
|
+
height: (bottom - top) + "px"
|
|
90
|
+
}
|
|
91
|
+
if (state.mappedIds.includes(entry.uid)) {
|
|
92
|
+
style["backgroundColor"] = "rgba(255, 0, 0, 0.3)";
|
|
93
|
+
}
|
|
94
|
+
const overlayDiv = <div
|
|
95
|
+
className="line-block-overlay"
|
|
96
|
+
onClick={(e) => onSelectAbstraction(entry)}
|
|
97
|
+
style={style}>
|
|
98
|
+
</div>;
|
|
99
|
+
divs.push(overlayDiv);
|
|
100
|
+
|
|
101
|
+
});
|
|
102
|
+
setOverlayDivs(divs);
|
|
103
|
+
}, [editorRef?.current, state, onSelectAbstraction]);
|
|
104
|
+
|
|
105
|
+
// Scroll the editor and update overlays on wheel event.
|
|
106
|
+
const handleWheel = useCallback((e) => {
|
|
107
|
+
if (!editorRef.current) return;
|
|
108
|
+
const deltaY = e.deltaY;
|
|
109
|
+
const currentScrollTop = editorRef.current.getScrollTop();
|
|
110
|
+
editorRef.current.setScrollTop(currentScrollTop + deltaY);
|
|
111
|
+
addOverlays();
|
|
112
|
+
}, [state.activeTab, addOverlays]);
|
|
113
|
+
|
|
114
|
+
// Editor options for Monaco Editor.
|
|
115
|
+
const editorOptions = {
|
|
116
|
+
fontSize: "13px",
|
|
117
|
+
minimap: {
|
|
118
|
+
enabled: false
|
|
119
|
+
},
|
|
120
|
+
padding: {
|
|
121
|
+
top: 10
|
|
122
|
+
},
|
|
123
|
+
renderWhitespace: "none",
|
|
124
|
+
wordWrap: "on",
|
|
125
|
+
scrollBeyondLastLine: false,
|
|
126
|
+
automaticLayout: false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Disable automatic layout and Manually layout the editor to avoid resize observer loops
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!containerRef.current) return;
|
|
132
|
+
|
|
133
|
+
const ro = new ResizeObserver(() => {
|
|
134
|
+
cancelAnimationFrame(frameRef.current);
|
|
135
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
136
|
+
editorRef.current?.layout();
|
|
137
|
+
addOverlays();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
ro.observe(containerRef.current);
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
cancelAnimationFrame(frameRef.current);
|
|
145
|
+
ro.disconnect();
|
|
146
|
+
};
|
|
147
|
+
}, [state.activeTab, addOverlays]);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render the editor if there is an active tab, otherwise render a placeholder message.
|
|
151
|
+
* @returns JSX
|
|
152
|
+
*/
|
|
153
|
+
const renderEditor = () => {
|
|
154
|
+
if (showEditor) {
|
|
155
|
+
return (
|
|
156
|
+
<Editor
|
|
157
|
+
defaultLanguage="python"
|
|
158
|
+
defaultValue={editorContent}
|
|
159
|
+
onMount={handleEditorDidMount}
|
|
160
|
+
theme="vs-dark"
|
|
161
|
+
options={editorOptions}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
return <div className="no-tab">Select file or drag and drop to view.</div>;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="editor-container" ref={containerRef}>
|
|
171
|
+
{renderEditor()}
|
|
172
|
+
{
|
|
173
|
+
state.mode === EDITOR_MODES.MAPPING &&
|
|
174
|
+
<div className="overlay-layer" onWheel={handleWheel}>
|
|
175
|
+
{overlayDivs}
|
|
176
|
+
</div>
|
|
177
|
+
}
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
MonacoInstance.propTypes = {
|
|
183
|
+
editorContent: PropTypes.string,
|
|
184
|
+
}
|
|
@@ -1,174 +1,146 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useMemo,
|
|
9
|
+
useImperativeHandle
|
|
10
|
+
} from "react";
|
|
11
|
+
import PropTypes from "prop-types";
|
|
3
12
|
|
|
4
|
-
import
|
|
13
|
+
import { useEditor } from "../Editor";
|
|
5
14
|
|
|
6
|
-
import
|
|
15
|
+
import EDITOR_MODES from "../EDITOR_MODES";
|
|
7
16
|
|
|
8
|
-
import "
|
|
17
|
+
import Editor from "@monaco-editor/react";
|
|
9
18
|
|
|
10
|
-
import
|
|
19
|
+
import "./MonacoInstance.scss";
|
|
11
20
|
|
|
12
|
-
export const MonacoInstance = ({onSelectAbstraction}) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const containerRef = useRef(null);
|
|
20
|
-
const frameRef = useRef(0);
|
|
21
|
-
|
|
22
|
-
const [overlayDivs, setOverlayDivs] = useState();
|
|
23
|
-
|
|
24
|
-
useLayoutEffect(() => {
|
|
25
|
-
if (state.activeTab) {
|
|
26
|
-
setEditorContent(state.activeTab.content);
|
|
27
|
-
setShowEditor(true);
|
|
28
|
-
addOverlays();
|
|
29
|
-
} else {
|
|
30
|
-
setShowEditor(false);
|
|
31
|
-
}
|
|
32
|
-
}, [state, editorRef]);
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
content.current = editorContent;
|
|
36
|
-
if (editorRef?.current && content.current !== undefined && content.current !== null) {
|
|
37
|
-
editorRef.current.setValue(content.current);
|
|
38
|
-
}
|
|
39
|
-
}, [editorContent]);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Callback for when the Monaco Editor is mounted.
|
|
43
|
-
* @param {Object} editor
|
|
44
|
-
* @param {Object} monaco
|
|
45
|
-
*/
|
|
46
|
-
const handleEditorDidMount = useCallback((editor, monaco) => {
|
|
47
|
-
editorRef.current = editor;
|
|
48
|
-
if (content?.current) {
|
|
49
|
-
editorRef.current.setValue(content.current);
|
|
50
|
-
}
|
|
51
|
-
editorRef.current.layout();
|
|
52
|
-
addOverlays();
|
|
53
|
-
}, [state.activeTab]);
|
|
54
|
-
|
|
55
|
-
// Add overlays to editor for the given line ranges.
|
|
56
|
-
const addOverlays = useCallback(() => {
|
|
57
|
-
if (!editorRef.current) return;
|
|
58
|
-
|
|
59
|
-
if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
|
|
60
|
-
setOverlayDivs([]);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const ranges = state.mapping.get(state.activeTab?.name);
|
|
65
|
-
const lineCount = editorRef.current.getModel().getLineCount();
|
|
66
|
-
const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
|
|
67
|
-
const divs = [];
|
|
68
|
-
|
|
69
|
-
ranges.forEach((entry) => {
|
|
70
|
-
const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
|
|
71
|
-
let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
|
|
72
|
-
if (entry.end_line >= lineCount) {
|
|
73
|
-
bottom = bottom + lineHeight;
|
|
74
|
-
}
|
|
21
|
+
export const MonacoInstance = forwardRef(({ onSelectAbstraction }, ref) => {
|
|
22
|
+
const { state, setUpdatedContent } = useEditor();
|
|
23
|
+
const [showEditor, setShowEditor] = useState(false);
|
|
24
|
+
const activeTabRef = useRef(state.activeTab);
|
|
25
|
+
|
|
26
|
+
const editorRef = useRef(null);
|
|
27
|
+
const modelsRef = useRef(new Map());
|
|
75
28
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
29
|
+
// When active tab changes, update the editor content and add overlays for the new active tab.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (state.activeTab) {
|
|
32
|
+
activeTabRef.current = state.activeTab;
|
|
33
|
+
editorRef.current && editorRef.current.setModel(getModel(state.activeTab));
|
|
34
|
+
setShowEditor(true);
|
|
35
|
+
} else {
|
|
36
|
+
activeTabRef.current = null;
|
|
37
|
+
setShowEditor(false);
|
|
79
38
|
}
|
|
80
|
-
|
|
81
|
-
|
|
39
|
+
}, [state.activeTab, editorRef]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
// Dispose all models on unmount to prevent memory leaks.
|
|
44
|
+
modelsRef.current.forEach((model) => model.dispose());
|
|
45
|
+
modelsRef.current.clear();
|
|
46
|
+
editorRef.current = null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// Get the model given the tab.
|
|
51
|
+
const getModel = useCallback((activeTab) => {
|
|
52
|
+
let model;
|
|
53
|
+
if (modelsRef.current.has(activeTab.uid)) {
|
|
54
|
+
model = modelsRef.current.get(activeTab.uid);
|
|
55
|
+
} else {
|
|
56
|
+
const uri = monaco.Uri.parse(`file:///${activeTab.uid}`);
|
|
57
|
+
model = monaco.editor.createModel(activeTab.content, "python", uri);
|
|
58
|
+
modelsRef.current.set(activeTab.uid, model);
|
|
59
|
+
}
|
|
60
|
+
model.onDidChangeContent((content) => {
|
|
61
|
+
setUpdatedContent(activeTabRef.current, editorRef.current.getValue());
|
|
62
|
+
});
|
|
63
|
+
return model;
|
|
64
|
+
}, [editorRef, modelsRef, setUpdatedContent]);
|
|
65
|
+
|
|
66
|
+
// Setup editor ref, and clear existing models on mount to prevent mem leak from old models.
|
|
67
|
+
const handleEditorDidMount = useCallback((editor, monaco) => {
|
|
68
|
+
editorRef.current = editor;
|
|
69
|
+
editor.layout();
|
|
70
|
+
modelsRef.current = new Map();
|
|
71
|
+
monaco.editor.getModels().forEach((model) => model.dispose());
|
|
72
|
+
|
|
73
|
+
if (activeTabRef.current) {
|
|
74
|
+
editorRef.current.setModel(getModel(activeTabRef.current));
|
|
75
|
+
setShowEditor(true);
|
|
82
76
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const handleWheel = useCallback((e) => {
|
|
96
|
-
if (!editorRef.current) return;
|
|
97
|
-
const deltaY = e.deltaY;
|
|
98
|
-
const currentScrollTop = editorRef.current.getScrollTop();
|
|
99
|
-
editorRef.current.setScrollTop(currentScrollTop + deltaY);
|
|
100
|
-
addOverlays();
|
|
101
|
-
}, [state.activeTab, addOverlays]);
|
|
102
|
-
|
|
103
|
-
// Editor options for Monaco Editor.
|
|
104
|
-
const editorOptions = {
|
|
105
|
-
fontSize: "13px",
|
|
106
|
-
minimap: {
|
|
107
|
-
enabled: false
|
|
108
|
-
},
|
|
109
|
-
padding: {
|
|
110
|
-
top: 10
|
|
111
|
-
},
|
|
112
|
-
renderWhitespace: "none",
|
|
113
|
-
wordWrap: "on",
|
|
114
|
-
scrollBeyondLastLine: false,
|
|
115
|
-
readOnly: true,
|
|
116
|
-
automaticLayout: false
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Disable automatic layout and Manually layout the editor to avoid resize observer loops
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (!containerRef.current) return;
|
|
122
|
-
|
|
123
|
-
const ro = new ResizeObserver(() => {
|
|
124
|
-
cancelAnimationFrame(frameRef.current);
|
|
125
|
-
frameRef.current = requestAnimationFrame(() => {
|
|
126
|
-
editorRef.current?.layout();
|
|
127
|
-
addOverlays();
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// Disable automatic layout and Manually layout the editor to avoid resize observer loops
|
|
80
|
+
const containerRef = useRef(null);
|
|
81
|
+
const frameRef = useRef(0);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!containerRef.current) return;
|
|
84
|
+
const ro = new ResizeObserver(() => {
|
|
85
|
+
cancelAnimationFrame(frameRef.current);
|
|
86
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
87
|
+
editorRef.current?.layout();
|
|
88
|
+
});
|
|
128
89
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*/
|
|
143
|
-
const renderEditor = () => {
|
|
144
|
-
if (showEditor) {
|
|
145
|
-
return (
|
|
146
|
-
<Editor
|
|
147
|
-
defaultLanguage="python"
|
|
148
|
-
defaultValue={editorContent}
|
|
149
|
-
onMount={handleEditorDidMount}
|
|
150
|
-
theme="vs-dark"
|
|
151
|
-
options={editorOptions}
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
return <div className="no-tab">Select file or drag and drop to view.</div>;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return (
|
|
160
|
-
<div className="editor-container" ref={containerRef}>
|
|
161
|
-
{renderEditor()}
|
|
162
|
-
{
|
|
163
|
-
state.mode === EDITOR_MODES.MAPPING &&
|
|
164
|
-
<div className="overlay-layer" onWheel={handleWheel}>
|
|
165
|
-
{overlayDivs}
|
|
166
|
-
</div>
|
|
90
|
+
ro.observe(containerRef.current);
|
|
91
|
+
return () => {
|
|
92
|
+
cancelAnimationFrame(frameRef.current);
|
|
93
|
+
ro.disconnect();
|
|
94
|
+
};
|
|
95
|
+
}, [state.activeTab]);
|
|
96
|
+
|
|
97
|
+
// Setup imperative handle to clear model when tab is closed to prevent mem leak
|
|
98
|
+
const clearModel = useCallback((id) => {
|
|
99
|
+
if (modelsRef.current.has(id)) {
|
|
100
|
+
const model = modelsRef.current.get(id);
|
|
101
|
+
model.dispose();
|
|
102
|
+
modelsRef.current.delete(id);
|
|
167
103
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const api = useMemo(() => {
|
|
107
|
+
return {
|
|
108
|
+
clearModel,
|
|
109
|
+
};
|
|
110
|
+
}, [clearModel]);
|
|
111
|
+
|
|
112
|
+
useImperativeHandle(ref, () => api, [api]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="editor-container" ref={containerRef}>
|
|
116
|
+
{
|
|
117
|
+
showEditor ?
|
|
118
|
+
<Editor
|
|
119
|
+
defaultLanguage="python"
|
|
120
|
+
defaultValue={"Loading content..."}
|
|
121
|
+
onMount={handleEditorDidMount}
|
|
122
|
+
theme="vs-dark"
|
|
123
|
+
options={{
|
|
124
|
+
minimap: { enabled: false },
|
|
125
|
+
padding: { top: 10 },
|
|
126
|
+
renderWhitespace: "none",
|
|
127
|
+
wordWrap: "on",
|
|
128
|
+
scrollBeyondLastLine: false,
|
|
129
|
+
automaticLayout: false,
|
|
130
|
+
}}
|
|
131
|
+
/>:
|
|
132
|
+
<div className="no-tab">Select file or drag and drop to view.</div>
|
|
133
|
+
}
|
|
134
|
+
{state.mode === EDITOR_MODES.MAPPING && (
|
|
135
|
+
<div className="overlay-layer" onWheel={handleWheel}>
|
|
136
|
+
{overlayDivs}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
171
143
|
|
|
172
144
|
MonacoInstance.propTypes = {
|
|
173
145
|
editorContent: PropTypes.string,
|
|
174
|
-
}
|
|
146
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { FileEarmark, XLg } from "react-bootstrap-icons";
|
|
2
|
+
import { FileEarmark, XLg, CircleFill } from "react-bootstrap-icons";
|
|
3
3
|
import { useDraggable } from "@dnd-kit/core";
|
|
4
4
|
import PropTypes from "prop-types";
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ const INACTIVE_TAB_FG_COLOR = "#969690";
|
|
|
19
19
|
* @param {String} label
|
|
20
20
|
* @returns
|
|
21
21
|
*/
|
|
22
|
-
export const Tab = ({ id, parentId, node }) => {
|
|
22
|
+
export const Tab = ({ id, isDirty, parentId, node }) => {
|
|
23
23
|
const [tabStyle, setTabStyle] = useState();
|
|
24
24
|
|
|
25
25
|
const { selectTab, closeTab, state } = useEditor();
|
|
@@ -70,6 +70,11 @@ export const Tab = ({ id, parentId, node }) => {
|
|
|
70
70
|
{...attributes}
|
|
71
71
|
>
|
|
72
72
|
<div className="tab-content">
|
|
73
|
+
{isDirty &&
|
|
74
|
+
<div className="icon">
|
|
75
|
+
<CircleFill size={6}/>
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
73
78
|
<div className="icon">
|
|
74
79
|
<FileEarmark size={14} style={{ pointerEvents: "none" }} />
|
|
75
80
|
</div>
|