@podlite/editor-react 0.0.41 → 0.0.43
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/CHANGELOG.podlite +17 -0
- package/README.md +7 -83
- package/esm/Editor.css +19 -0
- package/esm/Editor.d.ts +2 -0
- package/esm/Editor.js +92 -62
- package/esm/Editor.js.map +1 -1
- package/esm/Wrapper.d.ts +5 -3
- package/esm/Wrapper.js +59 -75
- package/esm/Wrapper.js.map +1 -1
- package/esm/foldPodlite.js +40 -1
- package/esm/foldPodlite.js.map +1 -1
- package/lib/Editor.css +19 -0
- package/lib/Editor.d.ts +2 -0
- package/lib/Editor.js +92 -62
- package/lib/Editor.js.map +1 -1
- package/lib/Wrapper.d.ts +5 -3
- package/lib/Wrapper.js +58 -74
- package/lib/Wrapper.js.map +1 -1
- package/lib/foldPodlite.js +40 -1
- package/lib/foldPodlite.js.map +1 -1
- package/package.json +2 -1
package/esm/Wrapper.js
CHANGED
|
@@ -1,87 +1,48 @@
|
|
|
1
|
-
import React, { useRef } from 'react';
|
|
2
|
-
const WindowWrapper = ({ children, title = '' }) => {
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
const WindowWrapper = ({ children, title = '', enableCopyPng = false }) => {
|
|
3
|
+
const windowRef = useRef(null);
|
|
4
|
+
const copyBtnRef = useRef(null);
|
|
5
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
6
|
+
const handleCopyPng = async () => {
|
|
7
|
+
if (!windowRef.current)
|
|
6
8
|
return;
|
|
7
|
-
const html = wrapperRef.current.outerHTML;
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<foreignObject width="100%" height="100%">
|
|
12
|
-
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
13
|
-
${html}
|
|
14
|
-
</div>
|
|
15
|
-
</foreignObject>
|
|
16
|
-
</svg>
|
|
17
|
-
`.trim();
|
|
18
|
-
const dataURL = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`;
|
|
19
|
-
await navigator.clipboard.writeText(dataURL);
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
console.error('Copy failed:', err);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
const copyElementToClipboard = async () => {
|
|
26
|
-
if (!wrapperRef.current)
|
|
27
|
-
return;
|
|
28
|
-
try {
|
|
29
|
-
// setIsLoading(true);
|
|
30
|
-
const canvas = document.createElement('canvas');
|
|
31
|
-
const context = canvas.getContext('2d');
|
|
32
|
-
const element = wrapperRef.current;
|
|
33
|
-
const rect = element.getBoundingClientRect();
|
|
34
|
-
canvas.width = rect.width * 2;
|
|
35
|
-
canvas.height = rect.height * 2;
|
|
36
|
-
const svgData = `
|
|
37
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${rect.width}" height="${rect.height}">
|
|
38
|
-
<foreignObject width="100%" height="100%">
|
|
39
|
-
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
40
|
-
${element.outerHTML}
|
|
41
|
-
</div>
|
|
42
|
-
</foreignObject>
|
|
43
|
-
</svg>
|
|
44
|
-
`;
|
|
45
|
-
console.log('1');
|
|
46
|
-
const base64Data = btoa(unescape(encodeURIComponent(svgData)));
|
|
47
|
-
// const base64Data = btoa(encodeURIComponent(svgData));
|
|
48
|
-
const dataUrl = `data:image/svg+xml;base64,${base64Data}`;
|
|
49
|
-
console.log(dataUrl);
|
|
50
|
-
const image = new Image();
|
|
51
|
-
image.crossOrigin = 'anonymous';
|
|
52
|
-
console.log('2');
|
|
53
|
-
await new Promise((resolve, reject) => {
|
|
54
|
-
image.onload = resolve;
|
|
55
|
-
image.onerror = e => {
|
|
56
|
-
console.error('Image load error:', e);
|
|
57
|
-
reject(e);
|
|
58
|
-
};
|
|
59
|
-
image.src = dataUrl;
|
|
60
|
-
});
|
|
61
|
-
console.log('3');
|
|
62
|
-
if (context) {
|
|
63
|
-
context.scale(2, 2);
|
|
64
|
-
context.drawImage(image, 0, 0);
|
|
10
|
+
if (copyBtnRef.current) {
|
|
11
|
+
copyBtnRef.current.style.display = 'none';
|
|
65
12
|
}
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
13
|
+
const domtoimage = (await import('dom-to-image-more')).default;
|
|
14
|
+
const blob = await domtoimage.toBlob(windowRef.current, { quality: 1.0 });
|
|
15
|
+
if (copyBtnRef.current) {
|
|
16
|
+
copyBtnRef.current.style.display = '';
|
|
70
17
|
}
|
|
18
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
19
|
+
setIsCopied(true);
|
|
20
|
+
setTimeout(() => setIsCopied(false), 2000);
|
|
71
21
|
}
|
|
72
22
|
catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
23
|
+
if (copyBtnRef.current) {
|
|
24
|
+
copyBtnRef.current.style.display = '';
|
|
25
|
+
}
|
|
26
|
+
console.error('Copy PNG failed:', err);
|
|
77
27
|
}
|
|
78
28
|
};
|
|
79
|
-
return (React.createElement("div", { className: "window", ref:
|
|
80
|
-
React.createElement("div", { className: "
|
|
81
|
-
React.createElement("div", { className: "
|
|
82
|
-
|
|
83
|
-
|
|
29
|
+
return (React.createElement("div", { className: "window-capture", ref: windowRef },
|
|
30
|
+
React.createElement("div", { className: "window" },
|
|
31
|
+
React.createElement("div", { className: "titlebar" },
|
|
32
|
+
React.createElement("div", { className: "buttons" }),
|
|
33
|
+
title && React.createElement("span", { className: "title" }, title),
|
|
34
|
+
React.createElement("div", { className: "titlebar-spacer" }),
|
|
35
|
+
enableCopyPng && (React.createElement("button", { ref: copyBtnRef, className: "copy-png-btn", onClick: handleCopyPng, title: "Copy as PNG" }, isCopied ? '\u2713' : '\u2398'))),
|
|
36
|
+
React.createElement("div", { className: "content" }, children)),
|
|
84
37
|
React.createElement("style", null, `
|
|
38
|
+
.window-capture {
|
|
39
|
+
padding: 1.5rem;
|
|
40
|
+
display: inline-block;
|
|
41
|
+
margin: 0.5rem;
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
}
|
|
45
|
+
|
|
85
46
|
.window {
|
|
86
47
|
position: relative;
|
|
87
48
|
margin: 2rem;
|
|
@@ -89,6 +50,8 @@ const WindowWrapper = ({ children, title = '' }) => {
|
|
|
89
50
|
background: white;
|
|
90
51
|
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.2);
|
|
91
52
|
border: 1px solid #ccc;
|
|
53
|
+
max-width: 100%;
|
|
54
|
+
box-sizing: border-box;
|
|
92
55
|
}
|
|
93
56
|
|
|
94
57
|
.titlebar {
|
|
@@ -101,6 +64,10 @@ const WindowWrapper = ({ children, title = '' }) => {
|
|
|
101
64
|
align-items: center;
|
|
102
65
|
}
|
|
103
66
|
|
|
67
|
+
.titlebar-spacer {
|
|
68
|
+
flex: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
104
71
|
.buttons {
|
|
105
72
|
display: flex;
|
|
106
73
|
gap: 6px;
|
|
@@ -140,6 +107,23 @@ const WindowWrapper = ({ children, title = '' }) => {
|
|
|
140
107
|
.content {
|
|
141
108
|
padding: 16px;
|
|
142
109
|
}
|
|
110
|
+
|
|
111
|
+
.copy-png-btn {
|
|
112
|
+
background: none;
|
|
113
|
+
border: 1px solid #ccc;
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
padding: 2px 8px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 14px;
|
|
118
|
+
color: #666;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
transition: background 0.15s, color 0.15s;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.copy-png-btn:hover {
|
|
124
|
+
background: #e0e0e0;
|
|
125
|
+
color: #333;
|
|
126
|
+
}
|
|
143
127
|
`)));
|
|
144
128
|
};
|
|
145
129
|
export default WindowWrapper;
|
package/esm/Wrapper.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Wrapper.js","sourceRoot":"","sources":["../src/Wrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Wrapper.js","sourceRoot":"","sources":["../src/Wrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQ/C,MAAM,aAAa,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAK,GAAG,EAAE,EAAE,aAAa,GAAG,KAAK,EAAsB,EAAE,EAAE;IAC5F,MAAM,SAAS,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAC9C,MAAM,UAAU,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAClD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE/C,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,IAAI,CAAC,SAAS,CAAC,OAAO;YAAE,OAAM;QAC9B,IAAI;YACF,IAAI,UAAU,CAAC,OAAO,EAAE;gBACtB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAA;aAC1C;YACD,MAAM,UAAU,GAAG,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAA;YAC9D,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;YACzE,IAAI,UAAU,CAAC,OAAO,EAAE;gBACtB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;aACtC;YACD,MAAM,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,aAAa,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;YAC3E,WAAW,CAAC,IAAI,CAAC,CAAA;YACjB,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;SAC3C;QAAC,OAAO,GAAG,EAAE;YACZ,IAAI,UAAU,CAAC,OAAO,EAAE;gBACtB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;aACtC;YACD,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;SACvC;IACH,CAAC,CAAA;IAED,OAAO,CACL,6BAAK,SAAS,EAAC,gBAAgB,EAAC,GAAG,EAAE,SAAS;QAC5C,6BAAK,SAAS,EAAC,QAAQ;YACrB,6BAAK,SAAS,EAAC,UAAU;gBACvB,6BAAK,SAAS,EAAC,SAAS,GAAO;gBAC9B,KAAK,IAAI,8BAAM,SAAS,EAAC,OAAO,IAAE,KAAK,CAAQ;gBAChD,6BAAK,SAAS,EAAC,iBAAiB,GAAO;gBACtC,aAAa,IAAI,CAChB,gCAAQ,GAAG,EAAE,UAAU,EAAE,SAAS,EAAC,cAAc,EAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAC,aAAa,IAC1F,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CACxB,CACV,CACG;YACN,6BAAK,SAAS,EAAC,SAAS,IAAE,QAAQ,CAAO,CACrC;QACN,mCAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0FP,CAAS,CACN,CACP,CAAA;AACH,CAAC,CAAA;AAED,eAAe,aAAa,CAAA"}
|
package/esm/foldPodlite.js
CHANGED
|
@@ -4,11 +4,44 @@ const beginRe = /^(\s*)=begin\s+(\S+)/;
|
|
|
4
4
|
const endRe = /^(\s*)=end\s+(\S+)/;
|
|
5
5
|
// =headN (level 1-6)
|
|
6
6
|
const headRe = /^(\s*)=head(\d+)\s/;
|
|
7
|
+
// Verbatim block types where content should not be folded
|
|
8
|
+
const verbatimTypes = new Set(['code', 'comment', 'data', 'input', 'output']);
|
|
7
9
|
// Parse =begin line, return block name or null
|
|
8
10
|
function parseBeginLine(lineText) {
|
|
9
11
|
const m = lineText.match(beginRe);
|
|
10
12
|
return m ? m[2] ?? null : null;
|
|
11
13
|
}
|
|
14
|
+
// Cache verbatim ranges per document version to avoid rescanning on every fold query
|
|
15
|
+
let _cachedVerbatimDoc = null;
|
|
16
|
+
let _cachedVerbatimRanges = [];
|
|
17
|
+
function getVerbatimRanges(state) {
|
|
18
|
+
// EditorState.doc is immutable — same reference means same content
|
|
19
|
+
if (_cachedVerbatimDoc === state.doc)
|
|
20
|
+
return _cachedVerbatimRanges;
|
|
21
|
+
const ranges = [];
|
|
22
|
+
for (let i = 1; i <= state.doc.lines; i++) {
|
|
23
|
+
const line = state.doc.line(i);
|
|
24
|
+
const m = line.text.match(beginRe);
|
|
25
|
+
if (m && verbatimTypes.has(m[2])) {
|
|
26
|
+
for (let j = i + 1; j <= state.doc.lines; j++) {
|
|
27
|
+
const endLine = state.doc.line(j);
|
|
28
|
+
const em = endLine.text.match(endRe);
|
|
29
|
+
if (em && em[2] === m[2]) {
|
|
30
|
+
if (i + 1 <= j - 1) {
|
|
31
|
+
ranges.push({ from: i + 1, to: j - 1 });
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
_cachedVerbatimDoc = state.doc;
|
|
39
|
+
_cachedVerbatimRanges = ranges;
|
|
40
|
+
return ranges;
|
|
41
|
+
}
|
|
42
|
+
function isInsideVerbatim(lineNumber, ranges) {
|
|
43
|
+
return ranges.some(r => lineNumber >= r.from && lineNumber <= r.to);
|
|
44
|
+
}
|
|
12
45
|
// Fold =begin Name ... =end Name: fold from end of =begin line to start of =end line
|
|
13
46
|
function foldBeginEnd(state, lineStart) {
|
|
14
47
|
const line = state.doc.lineAt(lineStart);
|
|
@@ -65,5 +98,11 @@ function foldHeadSection(state, lineStart) {
|
|
|
65
98
|
return null;
|
|
66
99
|
return { from: line.to, to: endPos };
|
|
67
100
|
}
|
|
68
|
-
export const podliteFoldService = foldService.of((state, lineStart) =>
|
|
101
|
+
export const podliteFoldService = foldService.of((state, lineStart) => {
|
|
102
|
+
const line = state.doc.lineAt(lineStart);
|
|
103
|
+
const verbatimRanges = getVerbatimRanges(state);
|
|
104
|
+
if (isInsideVerbatim(line.number, verbatimRanges))
|
|
105
|
+
return null;
|
|
106
|
+
return foldBeginEnd(state, lineStart) ?? foldHeadSection(state, lineStart);
|
|
107
|
+
});
|
|
69
108
|
//# sourceMappingURL=foldPodlite.js.map
|
package/esm/foldPodlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foldPodlite.js","sourceRoot":"","sources":["../src/foldPodlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"foldPodlite.js","sourceRoot":"","sources":["../src/foldPodlite.ts"],"names":[],"mappings":"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"}
|
package/lib/Editor.css
CHANGED
|
@@ -53,3 +53,22 @@
|
|
|
53
53
|
.podlite-editor h6 a {
|
|
54
54
|
display: none;
|
|
55
55
|
}
|
|
56
|
+
.podlite-editor .podlite-editor-preview > .content {
|
|
57
|
+
margin-top: 0;
|
|
58
|
+
}
|
|
59
|
+
.podlite-editor .podlite-editor-preview li,
|
|
60
|
+
.podlite-editor .podlite-editor-preview .line-src,
|
|
61
|
+
.podlite-editor .podlite-editor-preview ul {
|
|
62
|
+
margin-top: 0;
|
|
63
|
+
}
|
|
64
|
+
.podlite-editor-preview .content > :first-child,
|
|
65
|
+
.podlite-editor-preview .content > .line-src:first-child {
|
|
66
|
+
margin-top: 0;
|
|
67
|
+
}
|
|
68
|
+
.podlite-editor-preview .content > .line-src:first-child > * {
|
|
69
|
+
margin-top: 0;
|
|
70
|
+
}
|
|
71
|
+
/* Compensate for missing fold gutter width when folding is disabled */
|
|
72
|
+
.podlite-editor .cm-editor:not(:has(.cm-foldGutter)) .cm-content {
|
|
73
|
+
padding-left: 11px;
|
|
74
|
+
}
|
package/lib/Editor.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface IPodliteEditor extends ReactCodeMirrorProps {
|
|
|
28
28
|
onOpenLink?: (url: string) => void;
|
|
29
29
|
/** Enable code highlighting */
|
|
30
30
|
enableHighlighting?: boolean;
|
|
31
|
+
/** Enable code folding (fold gutter + fold keymap) @default `true` */
|
|
32
|
+
enableFolding?: boolean;
|
|
31
33
|
/** Initial editor session state to restore (cursor, scroll, folds) */
|
|
32
34
|
initialEditorState?: EditorSessionState;
|
|
33
35
|
/** Called when editor state changes (cursor move, scroll, fold/unfold) */
|
package/lib/Editor.js
CHANGED
|
@@ -33,10 +33,10 @@ function PodliteEditorInternal(props, ref) {
|
|
|
33
33
|
if (!isBrowser)
|
|
34
34
|
return null;
|
|
35
35
|
const { onChange, enableScroll = true, enablePreview = true, extensions = [], previewWidth = '50%', onPreviewMode, isFullscreen, makePreviewComponent, startLinePreview = 1, // start line number
|
|
36
|
-
enableAutocompletion = false, onOpenLink, enableHighlighting = false, initialEditorState, onEditorStateChange, ...codemirrorProps } = props;
|
|
36
|
+
enableAutocompletion = false, onOpenLink, enableHighlighting = false, enableFolding = true, initialEditorState, onEditorStateChange, ...codemirrorProps } = props;
|
|
37
37
|
const full_preview = previewWidth === '100%';
|
|
38
38
|
const [value, setValue] = (0, react_1.useState)(props.value || '');
|
|
39
|
-
const
|
|
39
|
+
const pendingTextRef = (0, react_1.useRef)(props.value || '');
|
|
40
40
|
const codeMirror = (0, react_1.useRef)(null);
|
|
41
41
|
const container = (0, react_1.useRef)(null);
|
|
42
42
|
const containerEditor = (0, react_1.useRef)(null);
|
|
@@ -45,6 +45,14 @@ function PodliteEditorInternal(props, ref) {
|
|
|
45
45
|
const topLine = (0, react_1.useRef)(startLinePreview);
|
|
46
46
|
const $viewHeight = (0, react_1.useRef)(0);
|
|
47
47
|
const initialStateApplied = (0, react_1.useRef)(false);
|
|
48
|
+
// Stable refs for callbacks used in memoized extensions
|
|
49
|
+
// Stable refs for callbacks used in memoized extensions
|
|
50
|
+
const onEditorStateChangeRef = (0, react_1.useRef)(onEditorStateChange);
|
|
51
|
+
onEditorStateChangeRef.current = onEditorStateChange;
|
|
52
|
+
const onOpenLinkRef = (0, react_1.useRef)(onOpenLink);
|
|
53
|
+
onOpenLinkRef.current = onOpenLink;
|
|
54
|
+
// Stable basicSetup config to prevent CodeMirror reconfiguration on re-render
|
|
55
|
+
const basicSetupConfig = react_1.default.useMemo(() => ({ defaultKeymap: false }), []);
|
|
48
56
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
49
57
|
editor: codeMirror,
|
|
50
58
|
preview: preview,
|
|
@@ -60,7 +68,11 @@ function PodliteEditorInternal(props, ref) {
|
|
|
60
68
|
}
|
|
61
69
|
return ranges;
|
|
62
70
|
}, []);
|
|
63
|
-
//
|
|
71
|
+
// Reset applied flag when initialEditorState changes (new file opened)
|
|
72
|
+
(0, react_1.useEffect)(() => {
|
|
73
|
+
initialStateApplied.current = false;
|
|
74
|
+
}, [initialEditorState]);
|
|
75
|
+
// Restore editor session state once after mount or file change
|
|
64
76
|
(0, react_1.useEffect)(() => {
|
|
65
77
|
if (initialStateApplied.current || !initialEditorState)
|
|
66
78
|
return;
|
|
@@ -126,7 +138,9 @@ function PodliteEditorInternal(props, ref) {
|
|
|
126
138
|
: codemirrorProps.height;
|
|
127
139
|
const preValue = props.value;
|
|
128
140
|
(0, react_1.useEffect)(() => setValue(preValue ?? ''), [preValue]);
|
|
129
|
-
(0, react_1.useEffect)(() =>
|
|
141
|
+
(0, react_1.useEffect)(() => {
|
|
142
|
+
pendingTextRef.current = preValue ?? '';
|
|
143
|
+
}, [preValue]);
|
|
130
144
|
const fullRef = (0, react_1.useRef)(isFullscreen);
|
|
131
145
|
(0, react_1.useEffect)(() => {
|
|
132
146
|
fullRef.current = isFullscreen;
|
|
@@ -281,8 +295,9 @@ function PodliteEditorInternal(props, ref) {
|
|
|
281
295
|
},
|
|
282
296
|
...commands_1.defaultKeymap,
|
|
283
297
|
]);
|
|
284
|
-
//
|
|
285
|
-
|
|
298
|
+
// Memoized extensions to prevent CodeMirror reconfiguration on re-render
|
|
299
|
+
// (reconfiguration closes panels like search)
|
|
300
|
+
const modKeyClass = react_1.default.useMemo(() => view_1.EditorView.domEventHandlers({
|
|
286
301
|
keydown(e, view) {
|
|
287
302
|
if (e.metaKey || e.ctrlKey)
|
|
288
303
|
view.dom.classList.add('cm-mod-pressed');
|
|
@@ -294,12 +309,11 @@ function PodliteEditorInternal(props, ref) {
|
|
|
294
309
|
blur(_, view) {
|
|
295
310
|
view.dom.classList.remove('cm-mod-pressed');
|
|
296
311
|
},
|
|
297
|
-
// defensive: if mouse leaves, clear the class (helps when key is released elsewhere)
|
|
298
312
|
mouseleave(_, view) {
|
|
299
313
|
view.dom.classList.remove('cm-mod-pressed');
|
|
300
314
|
},
|
|
301
|
-
});
|
|
302
|
-
const openStyledLinkOnClick = view_1.EditorView.domEventHandlers({
|
|
315
|
+
}), []);
|
|
316
|
+
const openStyledLinkOnClick = react_1.default.useMemo(() => view_1.EditorView.domEventHandlers({
|
|
303
317
|
click(event, view) {
|
|
304
318
|
const target = event.target;
|
|
305
319
|
if (!target)
|
|
@@ -307,60 +321,42 @@ function PodliteEditorInternal(props, ref) {
|
|
|
307
321
|
const el = target.closest('.cm-clickable-link');
|
|
308
322
|
if (!el)
|
|
309
323
|
return;
|
|
310
|
-
// require Cmd/Ctrl for activation
|
|
311
324
|
const modPressed = event.metaKey || event.ctrlKey;
|
|
312
325
|
if (!modPressed)
|
|
313
326
|
return;
|
|
314
327
|
const from = view.posAtDOM(el, 0);
|
|
315
328
|
const to = view.posAtDOM(el, el.childNodes.length);
|
|
316
329
|
const text = view.state.doc.sliceString(from, to).trim();
|
|
317
|
-
|
|
318
|
-
onOpenLink(text); // i.e window.open(text, '_blank', 'noopener')
|
|
319
|
-
}
|
|
330
|
+
onOpenLinkRef.current?.(text);
|
|
320
331
|
event.preventDefault();
|
|
321
332
|
event.stopPropagation();
|
|
322
333
|
},
|
|
323
|
-
});
|
|
334
|
+
}), []);
|
|
324
335
|
// Track fold/unfold and cursor changes to emit editor state
|
|
325
|
-
const stateChangeListener = onEditorStateChange
|
|
336
|
+
const stateChangeListener = react_1.default.useMemo(() => onEditorStateChange
|
|
326
337
|
? view_1.EditorView.updateListener.of(update => {
|
|
327
|
-
// Emit on fold/unfold (transactions with fold effects) or cursor move
|
|
328
338
|
const hasFoldChange = update.transactions.some(tr => tr.effects.some(e => e.is(language_1.foldEffect) || e.is(language_1.unfoldEffect)));
|
|
329
339
|
const hasCursorChange = update.selectionSet;
|
|
330
340
|
if (hasFoldChange || hasCursorChange) {
|
|
331
|
-
|
|
341
|
+
onEditorStateChangeRef.current?.({
|
|
332
342
|
cursorOffset: update.state.selection.main.head,
|
|
333
343
|
scrollTop: update.view.scrollDOM.scrollTop,
|
|
334
344
|
foldedRanges: serializeFoldedRanges(update.view),
|
|
335
345
|
});
|
|
336
346
|
}
|
|
337
347
|
})
|
|
338
|
-
: [];
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
stateChangeListener,
|
|
346
|
-
listContinuation_1.itemLevelKeymap,
|
|
347
|
-
listContinuation_1.listContinuationKeymap,
|
|
348
|
-
preventToggleComment,
|
|
349
|
-
];
|
|
350
|
-
if (onOpenLink) {
|
|
351
|
-
extensionsData.push(modKeyClass, openStyledLinkOnClick, view_1.EditorView.theme({
|
|
352
|
-
'&.cm-editor.cm-mod-pressed .cm-clickable-link:hover': {
|
|
353
|
-
cursor: 'pointer',
|
|
354
|
-
},
|
|
355
|
-
}));
|
|
356
|
-
}
|
|
357
|
-
if (enableScroll) {
|
|
358
|
-
extensionsData.push(scrollExtensions);
|
|
359
|
-
}
|
|
360
|
-
const makeApply = (text) => (editor, completion, from, to) => {
|
|
348
|
+
: [], [!!onEditorStateChange, serializeFoldedRanges]);
|
|
349
|
+
const linkTheme = react_1.default.useMemo(() => view_1.EditorView.theme({
|
|
350
|
+
'&.cm-editor.cm-mod-pressed .cm-clickable-link:hover': {
|
|
351
|
+
cursor: 'pointer',
|
|
352
|
+
},
|
|
353
|
+
}), []);
|
|
354
|
+
const makeApply = (0, react_1.useCallback)((text) => (editor, completion, from, to) => {
|
|
361
355
|
return (0, autocomplete_1.snippet)(text)(editor, completion, from - 1, to);
|
|
362
|
-
};
|
|
363
|
-
|
|
356
|
+
}, []);
|
|
357
|
+
const autocompletionExt = react_1.default.useMemo(() => {
|
|
358
|
+
if (!enableAutocompletion)
|
|
359
|
+
return null;
|
|
364
360
|
const langDict = dict_1.default.filter(({ lang = 'pod6' }) => lang === 'pod6');
|
|
365
361
|
const completions = langDict.map(({ displayText, text }) => {
|
|
366
362
|
function cleanBraces(text) {
|
|
@@ -375,8 +371,6 @@ function PodliteEditorInternal(props, ref) {
|
|
|
375
371
|
});
|
|
376
372
|
function myCompletions(context) {
|
|
377
373
|
let before = context.matchBefore(/^\s*=\w*/);
|
|
378
|
-
// If completion wasn't explicitly started and there
|
|
379
|
-
// is no word before the cursor, don't open completions.
|
|
380
374
|
if (!context.explicit && !before)
|
|
381
375
|
return null;
|
|
382
376
|
return {
|
|
@@ -385,11 +379,51 @@ function PodliteEditorInternal(props, ref) {
|
|
|
385
379
|
validFor: /^=\w*$/,
|
|
386
380
|
};
|
|
387
381
|
}
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
382
|
+
return (0, autocomplete_1.autocompletion)({ override: [myCompletions] });
|
|
383
|
+
}, [enableAutocompletion, makeApply]);
|
|
384
|
+
const extensionsData = react_1.default.useMemo(() => {
|
|
385
|
+
const exts = [
|
|
386
|
+
(0, podlite_2.podliteLang)(),
|
|
387
|
+
view_1.EditorView.lineWrapping,
|
|
388
|
+
stateChangeListener,
|
|
389
|
+
listContinuation_1.itemLevelKeymap,
|
|
390
|
+
listContinuation_1.listContinuationKeymap,
|
|
391
|
+
preventToggleComment,
|
|
392
|
+
];
|
|
393
|
+
if (enableFolding) {
|
|
394
|
+
exts.push(foldPodlite_1.podliteFoldService, (0, language_1.foldGutter)(), view_1.keymap.of(language_1.foldKeymap));
|
|
395
|
+
}
|
|
396
|
+
if (onOpenLink) {
|
|
397
|
+
exts.push(modKeyClass, openStyledLinkOnClick, linkTheme);
|
|
398
|
+
}
|
|
399
|
+
if (enableScroll) {
|
|
400
|
+
exts.push(scrollExtensions);
|
|
401
|
+
}
|
|
402
|
+
if (autocompletionExt) {
|
|
403
|
+
exts.push(autocompletionExt);
|
|
404
|
+
}
|
|
405
|
+
return exts;
|
|
406
|
+
}, [
|
|
407
|
+
stateChangeListener,
|
|
408
|
+
modKeyClass,
|
|
409
|
+
openStyledLinkOnClick,
|
|
410
|
+
linkTheme,
|
|
411
|
+
!!onOpenLink,
|
|
412
|
+
enableScroll,
|
|
413
|
+
scrollExtensions,
|
|
414
|
+
preventToggleComment,
|
|
415
|
+
autocompletionExt,
|
|
416
|
+
enableFolding,
|
|
417
|
+
]);
|
|
418
|
+
// Debounced preview update: check ref every 300ms, update value state only when text changed
|
|
419
|
+
(0, react_1.useEffect)(() => {
|
|
420
|
+
const interval = setInterval(() => {
|
|
421
|
+
if (pendingTextRef.current !== value) {
|
|
422
|
+
setValue(pendingTextRef.current);
|
|
423
|
+
}
|
|
424
|
+
}, 300);
|
|
425
|
+
return () => clearInterval(interval);
|
|
426
|
+
}, [value]);
|
|
393
427
|
(0, react_1.useEffect)(() => {
|
|
394
428
|
if (!isBrowser || !container.current)
|
|
395
429
|
return;
|
|
@@ -427,18 +461,14 @@ function PodliteEditorInternal(props, ref) {
|
|
|
427
461
|
}, 100);
|
|
428
462
|
return () => clearTimeout(timer);
|
|
429
463
|
}, [startLinePreview, enablePreview]); // Only run when startLinePreview or enablePreview changes
|
|
430
|
-
const handleChange = (value, viewUpdate) => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
foldedRanges: serializeFoldedRanges(view),
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
};
|
|
464
|
+
const handleChange = (0, react_1.useCallback)((value, viewUpdate) => {
|
|
465
|
+
pendingTextRef.current = value;
|
|
466
|
+
// Defer parent callback to next frame so CodeMirror textupdate handler
|
|
467
|
+
// completes immediately and the typed character appears without delay
|
|
468
|
+
requestAnimationFrame(() => {
|
|
469
|
+
onChange?.(value, viewUpdate);
|
|
470
|
+
});
|
|
471
|
+
}, [onChange]);
|
|
442
472
|
(0, react_1.useEffect)(() => {
|
|
443
473
|
if (preview.current) {
|
|
444
474
|
const $preview = preview.current;
|
|
@@ -589,7 +619,7 @@ function PodliteEditorInternal(props, ref) {
|
|
|
589
619
|
};
|
|
590
620
|
const conentView = (react_1.default.createElement("div", { className: `podlite-editor-content`, style: { height: codemirrorProps?.height } },
|
|
591
621
|
react_1.default.createElement("div", { className: `podlite-editor-content-editor`, ref: containerEditor },
|
|
592
|
-
react_1.default.createElement(react_codemirror_1.default, { theme: theme_1.defaultTheme, indentWithTab: false,
|
|
622
|
+
react_1.default.createElement(react_codemirror_1.default, { theme: theme_1.defaultTheme, indentWithTab: false, autoFocus: true, ...codemirrorProps, basicSetup: basicSetupConfig, className: `podlite-editor-inner`, extensions: extensionsData, height: height, ref: codeMirror, onChange: handleChange })),
|
|
593
623
|
enablePreview && (react_1.default.createElement("div", { className: clsPreview, style: { overflow: full_preview ? 'visible' : 'hidden' }, ref: preview }, previewContent()))));
|
|
594
624
|
return (react_1.default.createElement("div", null,
|
|
595
625
|
react_1.default.createElement("div", { className: cls, ref: container }, conentView),
|