@ptengine/inline-editor 0.1.9 → 0.1.10
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.md +14 -0
- package/dist/locales.d.ts +12 -0
- package/dist/locales.js +9 -3
- package/dist/panels/CodeMirrorField.d.ts +32 -0
- package/dist/panels/CodeMirrorField.js +160 -0
- package/dist/panels/CodePanel.d.ts +2 -11
- package/dist/panels/CodePanel.js +45 -10
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.10
|
|
4
|
+
|
|
5
|
+
新增依赖:`@codemirror/*`(state / view / commands / language / autocomplete / lint / lang-css / lang-javascript)与 `js-beautify`。
|
|
6
|
+
`@ptengine/inline-core` 依赖不变,可以直接发。
|
|
7
|
+
|
|
8
|
+
**变更**
|
|
9
|
+
|
|
10
|
+
- 自定义代码面板的输入框换成 CodeMirror 6(与老编辑器同一个库):语法高亮、行号、括号配对与自动闭合、Tab 缩进、编辑器内撤销。
|
|
11
|
+
- 语法错误提示:按语言解析树标出写错的位置(行号旁 + 波浪线,同一行只报一条),**只提醒不拦**保存(老编辑器同口径)。不引 ESLint / CSSLint。
|
|
12
|
+
- 自动补全:CSS 属性与值;JS 里敲 `ptKit.` 补全六个工具函数(query / queryAll / querySelector / querySelectorAll / observe / moveElement,带片段与说明)。
|
|
13
|
+
- 「格式化」按钮:按两格缩进整理当前页签的代码(js-beautify,老编辑器同款)。
|
|
14
|
+
- CodeMirror 与 js-beautify 都**按需加载**:打开面板才下载,不进宿主首屏 / 编辑器主分块;下载完成前先给等宽文本框,可照常输入。
|
|
15
|
+
- 产物检查的外部依赖清单改为从 package.json 的 dependencies + peerDependencies 读取。
|
|
16
|
+
|
|
3
17
|
## 0.1.9
|
|
4
18
|
|
|
5
19
|
依赖不变(`@ptengine/inline-core` ~0.2.1),可以直接发。
|
package/dist/locales.d.ts
CHANGED
|
@@ -101,6 +101,8 @@ export declare const zhCN: {
|
|
|
101
101
|
title: string;
|
|
102
102
|
desc: string;
|
|
103
103
|
cssHint: string;
|
|
104
|
+
format: string;
|
|
105
|
+
formatHint: string;
|
|
104
106
|
jsHint: string;
|
|
105
107
|
};
|
|
106
108
|
version: {
|
|
@@ -295,6 +297,8 @@ export declare const enUS: {
|
|
|
295
297
|
title: string;
|
|
296
298
|
desc: string;
|
|
297
299
|
cssHint: string;
|
|
300
|
+
format: string;
|
|
301
|
+
formatHint: string;
|
|
298
302
|
jsHint: string;
|
|
299
303
|
};
|
|
300
304
|
version: {
|
|
@@ -489,6 +493,8 @@ export declare const jaJP: {
|
|
|
489
493
|
title: string;
|
|
490
494
|
desc: string;
|
|
491
495
|
cssHint: string;
|
|
496
|
+
format: string;
|
|
497
|
+
formatHint: string;
|
|
492
498
|
jsHint: string;
|
|
493
499
|
};
|
|
494
500
|
version: {
|
|
@@ -733,6 +739,8 @@ export declare const inlineEditorLocales: {
|
|
|
733
739
|
title: string;
|
|
734
740
|
desc: string;
|
|
735
741
|
cssHint: string;
|
|
742
|
+
format: string;
|
|
743
|
+
formatHint: string;
|
|
736
744
|
jsHint: string;
|
|
737
745
|
};
|
|
738
746
|
readonly version: {
|
|
@@ -976,6 +984,8 @@ export declare const inlineEditorLocales: {
|
|
|
976
984
|
title: string;
|
|
977
985
|
desc: string;
|
|
978
986
|
cssHint: string;
|
|
987
|
+
format: string;
|
|
988
|
+
formatHint: string;
|
|
979
989
|
jsHint: string;
|
|
980
990
|
};
|
|
981
991
|
readonly version: {
|
|
@@ -1219,6 +1229,8 @@ export declare const inlineEditorLocales: {
|
|
|
1219
1229
|
title: string;
|
|
1220
1230
|
desc: string;
|
|
1221
1231
|
cssHint: string;
|
|
1232
|
+
format: string;
|
|
1233
|
+
formatHint: string;
|
|
1222
1234
|
jsHint: string;
|
|
1223
1235
|
};
|
|
1224
1236
|
readonly version: {
|
package/dist/locales.js
CHANGED
|
@@ -90,7 +90,9 @@ export const zhCN = {
|
|
|
90
90
|
button: '追加代码',
|
|
91
91
|
title: '自定义代码',
|
|
92
92
|
desc: '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。',
|
|
93
|
-
cssHint: 'CSS
|
|
93
|
+
cssHint: 'CSS 会实时应用到画布。语法错误会在行号旁标出,不会阻止保存。',
|
|
94
|
+
format: '格式化',
|
|
95
|
+
formatHint: '按标准缩进整理当前页签的代码',
|
|
94
96
|
jsHint: 'JavaScript 不会在编辑器画布里执行,请通过预览或发布后查看效果。可使用 ptKit 工具函数(如 ptKit.query)等待元素出现。'
|
|
95
97
|
},
|
|
96
98
|
version: {
|
|
@@ -284,7 +286,9 @@ export const enUS = {
|
|
|
284
286
|
button: 'Custom code',
|
|
285
287
|
title: 'Custom code',
|
|
286
288
|
desc: 'Add CSS / JavaScript to customize your page more freely.',
|
|
287
|
-
cssHint: 'CSS is applied to the canvas in real time.',
|
|
289
|
+
cssHint: 'CSS is applied to the canvas in real time. Syntax errors are marked next to the line numbers and do not block saving.',
|
|
290
|
+
format: 'Format',
|
|
291
|
+
formatHint: 'Re-indent the code in this tab',
|
|
288
292
|
jsHint: 'JavaScript does not run in the editor canvas. Use preview or publish to see its effect. ptKit helpers (e.g. ptKit.query) can wait for elements to appear.'
|
|
289
293
|
},
|
|
290
294
|
version: {
|
|
@@ -478,7 +482,9 @@ export const jaJP = {
|
|
|
478
482
|
button: 'コード追加',
|
|
479
483
|
title: 'カスタムコード',
|
|
480
484
|
desc: 'CSS / JavaScript を追加して、ページをより自由に編集できます。',
|
|
481
|
-
cssHint: 'CSS
|
|
485
|
+
cssHint: 'CSS はキャンバスにリアルタイムで反映されます。構文エラーは行番号の横に表示されますが、保存は妨げません。',
|
|
486
|
+
format: '整形',
|
|
487
|
+
formatHint: 'このタブのコードを標準のインデントで整えます',
|
|
482
488
|
jsHint: 'JavaScript はエディタのキャンバスでは実行されません。プレビューまたは公開後に効果を確認してください。ptKit のヘルパー(例: ptKit.query)で要素の表示を待つことができます。'
|
|
483
489
|
},
|
|
484
490
|
version: {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EditorState } from '@codemirror/state';
|
|
2
|
+
import { type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
|
3
|
+
import { type Diagnostic } from '@codemirror/lint';
|
|
4
|
+
/**
|
|
5
|
+
* 自定义代码面板里的代码编辑器(CodeMirror 6)—— 语法高亮、语法错误提示、自动补全、括号配对、Tab 缩进。
|
|
6
|
+
*
|
|
7
|
+
* 与老编辑器(engage-editor `PtCodeMirror.vue`,同样是 CodeMirror 6)对齐的几样:CSS / JS 两种语言、
|
|
8
|
+
* 错误只**提醒不拦**(老编辑器的 lint 也只出警告、从不挡保存)、JS 里补全 `ptKit.*` 工具函数。
|
|
9
|
+
* 有意不同的一处:语法检查用语言自带的解析树(Lezer)找出错节点,不引 ESLint / CSSLint ——
|
|
10
|
+
* 那两个浏览器版加起来上 MB,而面板里要的只是「这里写错了」。
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ 本模块**只经 `CodePanel` 的动态 import 加载**:CodeMirror 只在用户打开面板时才下载,
|
|
13
|
+
* 不进宿主的首屏包。不要从 `index.ts` 或别的模块静态 import 它。
|
|
14
|
+
*/
|
|
15
|
+
export interface CodeMirrorFieldProps {
|
|
16
|
+
language: 'css' | 'javascript';
|
|
17
|
+
value: string;
|
|
18
|
+
onChange: (value: string) => void;
|
|
19
|
+
onBlur?: () => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
'data-testid'?: string;
|
|
23
|
+
}
|
|
24
|
+
/** ptKit 工具函数(与后端 `engagev5/ptkit.ts`、SDK 同一套)—— JS 里敲 `ptKit.` 时补全。 */
|
|
25
|
+
export declare const PT_KIT_COMPLETIONS: import("@codemirror/autocomplete").Completion[];
|
|
26
|
+
export declare function ptKitCompletion(context: CompletionContext): CompletionResult | null;
|
|
27
|
+
/**
|
|
28
|
+
* 语法错误提示:解析树里的出错节点(Lezer 遇到写不下去的地方会插一个 error 节点)。
|
|
29
|
+
* 同一行只报一条,免得一处少个括号整片飘红。
|
|
30
|
+
*/
|
|
31
|
+
export declare function findSyntaxErrors(state: EditorState, message: string): Diagnostic[];
|
|
32
|
+
export default function CodeMirrorField({ language, value, onChange, onBlur, placeholder, ariaLabel, 'data-testid': testId }: CodeMirrorFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { EditorState } from '@codemirror/state';
|
|
4
|
+
import { EditorView, drawSelection, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, placeholder as placeholderExt } from '@codemirror/view';
|
|
5
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
|
6
|
+
import { bracketMatching, defaultHighlightStyle, foldGutter, indentOnInput, ensureSyntaxTree, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
|
7
|
+
import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap, snippetCompletion } from '@codemirror/autocomplete';
|
|
8
|
+
import { lintGutter, linter } from '@codemirror/lint';
|
|
9
|
+
import { css } from '@codemirror/lang-css';
|
|
10
|
+
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
|
11
|
+
export const PT_KIT_COMPLETIONS = [
|
|
12
|
+
snippetCompletion("query('${selector}').then(function (el) {\n\t${}\n});", {
|
|
13
|
+
label: 'query',
|
|
14
|
+
detail: '(selector, options?) → Promise<Element>',
|
|
15
|
+
info: '轮询等待元素出现(默认最长 60 秒、每 500ms 查一次),找到后 resolve。'
|
|
16
|
+
}),
|
|
17
|
+
snippetCompletion("queryAll('${selector}').then(function (list) {\n\t${}\n});", {
|
|
18
|
+
label: 'queryAll',
|
|
19
|
+
detail: '(selector, options?) → Promise<Element[]>',
|
|
20
|
+
info: '轮询等待至少一个匹配元素出现,resolve 全部匹配元素。'
|
|
21
|
+
}),
|
|
22
|
+
snippetCompletion("querySelector('${selector}')", {
|
|
23
|
+
label: 'querySelector',
|
|
24
|
+
detail: '(selector, context?) → Element | null',
|
|
25
|
+
info: '立即查找一个元素(含 Ptengine 弹窗的 Shadow DOM)。'
|
|
26
|
+
}),
|
|
27
|
+
snippetCompletion("querySelectorAll('${selector}')", {
|
|
28
|
+
label: 'querySelectorAll',
|
|
29
|
+
detail: '(selector, context?) → Element[]',
|
|
30
|
+
info: '立即查找全部匹配元素(含 Ptengine 弹窗的 Shadow DOM)。'
|
|
31
|
+
}),
|
|
32
|
+
snippetCompletion("observe('${selector}', function (el) {\n\t${}\n});", {
|
|
33
|
+
label: 'observe',
|
|
34
|
+
detail: '(selector, callback, options?)',
|
|
35
|
+
info: '元素已在或之后被插入页面时执行回调(MutationObserver)。'
|
|
36
|
+
}),
|
|
37
|
+
snippetCompletion("moveElement('${target}', '${relative}', '${after}');", {
|
|
38
|
+
label: 'moveElement',
|
|
39
|
+
detail: "(target, relative, 'before' | 'after', options?)",
|
|
40
|
+
info: '等两个元素都出现后,把 target 移到 relative 的前面或后面。'
|
|
41
|
+
})
|
|
42
|
+
];
|
|
43
|
+
export function ptKitCompletion(context) {
|
|
44
|
+
const before = context.matchBefore(/ptKit\.\w*/);
|
|
45
|
+
if (!before)
|
|
46
|
+
return null;
|
|
47
|
+
return { from: before.from + 'ptKit.'.length, options: PT_KIT_COMPLETIONS, validFor: /^\w*$/ };
|
|
48
|
+
}
|
|
49
|
+
export function findSyntaxErrors(state, message) {
|
|
50
|
+
const diagnostics = [];
|
|
51
|
+
const seenLines = new Set();
|
|
52
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
|
|
53
|
+
tree.iterate({
|
|
54
|
+
enter: node => {
|
|
55
|
+
if (!node.type.isError)
|
|
56
|
+
return;
|
|
57
|
+
const line = state.doc.lineAt(node.from).number;
|
|
58
|
+
if (seenLines.has(line))
|
|
59
|
+
return;
|
|
60
|
+
seenLines.add(line);
|
|
61
|
+
const to = Math.max(node.to, Math.min(node.from + 1, state.doc.length));
|
|
62
|
+
diagnostics.push({ from: node.from, to, severity: 'warning', message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return diagnostics;
|
|
66
|
+
}
|
|
67
|
+
const syntaxLinter = (message) => linter(view => findSyntaxErrors(view.state, message), { delay: 400 });
|
|
68
|
+
const theme = EditorView.theme({
|
|
69
|
+
'&': { height: '100%', fontSize: '13px', backgroundColor: '#fff' },
|
|
70
|
+
'&.cm-focused': { outline: 'none' },
|
|
71
|
+
'.cm-scroller': {
|
|
72
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
73
|
+
lineHeight: '20px'
|
|
74
|
+
},
|
|
75
|
+
'.cm-gutters': {
|
|
76
|
+
backgroundColor: '#fafbfc',
|
|
77
|
+
borderRight: '1px solid #EBECF0',
|
|
78
|
+
color: '#97A0AF'
|
|
79
|
+
},
|
|
80
|
+
'.cm-activeLine': { backgroundColor: '#f4f5f7' },
|
|
81
|
+
'.cm-activeLineGutter': { backgroundColor: '#eef0f3' },
|
|
82
|
+
'.cm-tooltip': { fontSize: '12px' }
|
|
83
|
+
});
|
|
84
|
+
function languageExtensions(language, errorMessage) {
|
|
85
|
+
if (language === 'css')
|
|
86
|
+
return [css(), syntaxLinter(errorMessage)];
|
|
87
|
+
return [
|
|
88
|
+
javascript(),
|
|
89
|
+
javascriptLanguage.data.of({ autocomplete: ptKitCompletion }),
|
|
90
|
+
syntaxLinter(errorMessage)
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
export default function CodeMirrorField({ language, value, onChange, onBlur, placeholder, ariaLabel, 'data-testid': testId }) {
|
|
94
|
+
const hostRef = useRef(null);
|
|
95
|
+
const viewRef = useRef(null);
|
|
96
|
+
const onChangeRef = useRef(onChange);
|
|
97
|
+
onChangeRef.current = onChange;
|
|
98
|
+
const onBlurRef = useRef(onBlur);
|
|
99
|
+
onBlurRef.current = onBlur;
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!hostRef.current)
|
|
102
|
+
return;
|
|
103
|
+
const view = new EditorView({
|
|
104
|
+
parent: hostRef.current,
|
|
105
|
+
state: EditorState.create({
|
|
106
|
+
doc: value,
|
|
107
|
+
extensions: [
|
|
108
|
+
lineNumbers(),
|
|
109
|
+
highlightActiveLineGutter(),
|
|
110
|
+
foldGutter(),
|
|
111
|
+
lintGutter(),
|
|
112
|
+
history(),
|
|
113
|
+
drawSelection(),
|
|
114
|
+
indentOnInput(),
|
|
115
|
+
bracketMatching(),
|
|
116
|
+
closeBrackets(),
|
|
117
|
+
autocompletion(),
|
|
118
|
+
highlightActiveLine(),
|
|
119
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
120
|
+
EditorState.tabSize.of(2),
|
|
121
|
+
keymap.of([
|
|
122
|
+
...closeBracketsKeymap,
|
|
123
|
+
...defaultKeymap,
|
|
124
|
+
...historyKeymap,
|
|
125
|
+
...completionKeymap,
|
|
126
|
+
indentWithTab
|
|
127
|
+
]),
|
|
128
|
+
...languageExtensions(language, language === 'css' ? 'CSS 语法错误' : 'JavaScript 语法错误'),
|
|
129
|
+
...(placeholder ? [placeholderExt(placeholder)] : []),
|
|
130
|
+
EditorView.contentAttributes.of({
|
|
131
|
+
'aria-label': ariaLabel ?? language,
|
|
132
|
+
spellcheck: 'false'
|
|
133
|
+
}),
|
|
134
|
+
EditorView.updateListener.of(update => {
|
|
135
|
+
if (update.docChanged)
|
|
136
|
+
onChangeRef.current(update.state.doc.toString());
|
|
137
|
+
if (update.focusChanged && !update.view.hasFocus)
|
|
138
|
+
onBlurRef.current?.();
|
|
139
|
+
}),
|
|
140
|
+
theme
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
viewRef.current = view;
|
|
145
|
+
return () => {
|
|
146
|
+
view.destroy();
|
|
147
|
+
viewRef.current = null;
|
|
148
|
+
};
|
|
149
|
+
}, [language]);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const view = viewRef.current;
|
|
152
|
+
if (!view)
|
|
153
|
+
return;
|
|
154
|
+
const current = view.state.doc.toString();
|
|
155
|
+
if (current === value)
|
|
156
|
+
return;
|
|
157
|
+
view.dispatch({ changes: { from: 0, to: current.length, insert: value } });
|
|
158
|
+
}, [value]);
|
|
159
|
+
return _jsx("div", { ref: hostRef, "data-testid": testId, className: "h-full min-h-0 overflow-hidden" });
|
|
160
|
+
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { ExperienceCode } from '@ptengine/inline-core/protocol';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* - **改即生效,没有「应用」按钮**(与老编辑器一致):输入停顿 `FLUSH_MS` 后把整份代码交给宿主,
|
|
6
|
-
* 宿主写回 `code` 属性,编辑器据此把 CSS 注入画布。关面板 / 换版本时立即交出未落的那一下。
|
|
7
|
-
* - JavaScript **只保存、画布里不执行**(老编辑器同样关掉了:编辑器里的页面环境不对、
|
|
8
|
-
* 客户页也没有 SDK 的 `ptKit`),预览或发布后才生效 —— 面板里说清楚。
|
|
9
|
-
* - 不进撤销历史(老编辑器也不进)。
|
|
10
|
-
*
|
|
11
|
-
* 编辑控件用包里现成的等宽文本框,不引代码编辑器库(体积);Tab 键插两个空格而不是跳走焦点。
|
|
12
|
-
*/
|
|
2
|
+
/** 格式化(js-beautify,按需加载)。失败(语法错得太离谱)原样返回,不吞用户的代码。 */
|
|
3
|
+
export declare function formatCode(type: 'css' | 'javascript', code: string): Promise<string>;
|
|
13
4
|
export interface CodePanelProps {
|
|
14
5
|
code: ExperienceCode[];
|
|
15
6
|
onChange: (code: ExperienceCode[]) => void;
|
package/dist/panels/CodePanel.js
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Suspense, lazy, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { X } from 'lucide-react';
|
|
4
|
+
import { AlignLeft, X } from 'lucide-react';
|
|
5
5
|
import { Button } from '@ptengine/design-components';
|
|
6
6
|
import { ChromeSegment, ChromeSegmented } from '../chrome/topbarPrimitives.js';
|
|
7
7
|
import { CodeField } from '../dialogs/CodeField.js';
|
|
8
|
+
const CodeMirrorField = lazy(() => import('./CodeMirrorField.js'));
|
|
9
|
+
export async function formatCode(type, code) {
|
|
10
|
+
if (!code.trim())
|
|
11
|
+
return code;
|
|
12
|
+
const mod = (await import('js-beautify'));
|
|
13
|
+
const lib = mod.default ?? mod;
|
|
14
|
+
const opts = {
|
|
15
|
+
indent_size: 2,
|
|
16
|
+
end_with_newline: false,
|
|
17
|
+
preserve_newlines: true,
|
|
18
|
+
max_preserve_newlines: 2
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
return (type === 'css' ? lib.css : lib.js)(code, opts);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function placeholderOf(type) {
|
|
28
|
+
return type === 'css'
|
|
29
|
+
? '.hero-title { color: #e60012; }'
|
|
30
|
+
: "ptKit.query('.cta').then(function (el) { el.textContent = '…'; });";
|
|
31
|
+
}
|
|
8
32
|
const FLUSH_MS = 400;
|
|
9
33
|
function textOf(code, type) {
|
|
10
34
|
return code
|
|
@@ -63,6 +87,19 @@ export function CodePanel({ code, onChange, onClose }) {
|
|
|
63
87
|
clearTimeout(timerRef.current);
|
|
64
88
|
timerRef.current = setTimeout(flush, FLUSH_MS);
|
|
65
89
|
};
|
|
90
|
+
const [formatting, setFormatting] = useState(false);
|
|
91
|
+
const format = async () => {
|
|
92
|
+
const type = tab;
|
|
93
|
+
setFormatting(true);
|
|
94
|
+
try {
|
|
95
|
+
const next = await formatCode(type, draftRef.current[type]);
|
|
96
|
+
if (next !== draftRef.current[type])
|
|
97
|
+
update(type, next);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setFormatting(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
66
103
|
const onKeyDown = (event) => {
|
|
67
104
|
if (event.key !== 'Tab' || event.shiftKey)
|
|
68
105
|
return;
|
|
@@ -75,15 +112,13 @@ export function CodePanel({ code, onChange, onClose }) {
|
|
|
75
112
|
el.selectionStart = el.selectionEnd = selectionStart + 2;
|
|
76
113
|
});
|
|
77
114
|
};
|
|
78
|
-
return (_jsxs("aside", { "data-testid": "inline-editor-code-panel", className: "absolute bottom-3 right-3 top-3 z-30 flex w-[
|
|
115
|
+
return (_jsxs("aside", { "data-testid": "inline-editor-code-panel", className: "absolute bottom-3 right-3 top-3 z-30 flex w-[480px] max-w-[calc(100%-24px)] flex-col\n rounded-lg border border-[#EBECF0] bg-white shadow-[0_8px_24px_rgba(9,30,66,0.16)]", children: [_jsxs("header", { className: "flex h-11 shrink-0 items-center justify-between border-b border-[#f1f2f3] pl-3 pr-1.5", children: [_jsx("span", { className: "text-[13px] font-medium text-[#1a1a1a]", children: t('inlineCanvas.code.title', '自定义代码') }), _jsx(Button, { size: "sm", variant: "ghost", "data-testid": "inline-editor-code-close", "aria-label": t('inlineCanvas.chrome.close', '关闭'), onClick: () => {
|
|
79
116
|
flush();
|
|
80
117
|
onClose();
|
|
81
|
-
}, children: _jsx(X, { className: "size-4" }) })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-3", children: [_jsx("p", { className: "text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.code.desc', '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。') }), _jsx(ChromeSegmented, { "data-testid": "inline-editor-code-tabs", className: "self-start", children: [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
? '.
|
|
86
|
-
: "ptKit.query('.cta').then(function (el) { el.textContent = '…'; });", onChange: event => update(tab, event.target.value), onKeyDown: onKeyDown, onBlur: flush }, tab), _jsx("p", { "data-testid": "inline-editor-code-hint", className: "text-xs leading-5 text-[#6B778C]", children: tab === 'css'
|
|
87
|
-
? t('inlineCanvas.code.cssHint', 'CSS 会实时应用到画布。')
|
|
118
|
+
}, children: _jsx(X, { className: "size-4" }) })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-3", children: [_jsx("p", { className: "text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.code.desc', '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。') }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(ChromeSegmented, { "data-testid": "inline-editor-code-tabs", className: "self-start", children: [
|
|
119
|
+
['css', 'CSS'],
|
|
120
|
+
['javascript', 'JavaScript']
|
|
121
|
+
].map(([value, label]) => (_jsx(ChromeSegment, { "data-testid": `inline-editor-code-tab-${value}`, active: tab === value, onClick: () => setTab(value), children: label }, value))) }), _jsxs(Button, { size: "sm", variant: "ghost", "data-testid": "inline-editor-code-format", disabled: formatting || !draft[tab].trim(), title: t('inlineCanvas.code.formatHint', '按标准缩进整理当前页签的代码'), onClick: () => void format(), children: [_jsx(AlignLeft, { className: "size-3.5" }), t('inlineCanvas.code.format', '格式化')] })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden rounded-md border border-[#DFE1E6]", children: _jsx(Suspense, { fallback: _jsx(CodeField, { "data-testid": `inline-editor-code-input-${tab}`, "aria-label": tab === 'css' ? 'CSS' : 'JavaScript', className: "h-full resize-none rounded-none border-0", value: draft[tab], placeholder: placeholderOf(tab), onChange: event => update(tab, event.target.value), onKeyDown: onKeyDown, onBlur: flush }, `${tab}-plain`), children: _jsx(CodeMirrorField, { "data-testid": `inline-editor-code-input-${tab}`, language: tab, ariaLabel: tab === 'css' ? 'CSS' : 'JavaScript', value: draft[tab], placeholder: placeholderOf(tab), onChange: value => update(tab, value), onBlur: flush }, tab) }) }), _jsx("p", { "data-testid": "inline-editor-code-hint", className: "text-xs leading-5 text-[#6B778C]", children: tab === 'css'
|
|
122
|
+
? t('inlineCanvas.code.cssHint', 'CSS 会实时应用到画布。语法错误会在行号旁标出,不会阻止保存。')
|
|
88
123
|
: t('inlineCanvas.code.jsHint', 'JavaScript 不会在编辑器画布里执行,请通过预览或发布后查看效果。可使用 ptKit 工具函数(如 ptKit.query)等待元素出现。') })] })] }));
|
|
89
124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ptengine/inline-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Ptengine Inline 编辑器:画布会话编排、图层树、属性面板与组件库(含内部 canvas-ui)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
"react-i18next": ">=13"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@codemirror/autocomplete": "^6.20.4",
|
|
37
|
+
"@codemirror/commands": "^6.11.1",
|
|
38
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
39
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
40
|
+
"@codemirror/language": "^6.13.1",
|
|
41
|
+
"@codemirror/lint": "^6.9.7",
|
|
42
|
+
"@codemirror/state": "^6.7.6",
|
|
43
|
+
"@codemirror/view": "^6.43.15",
|
|
44
|
+
"js-beautify": "^2.0.3",
|
|
36
45
|
"lucide-react": "^0.577.0",
|
|
37
46
|
"@ptengine/inline-core": "~0.2.1"
|
|
38
47
|
},
|
|
@@ -40,6 +49,7 @@
|
|
|
40
49
|
"@ptengine/design-components": "^0.10.0",
|
|
41
50
|
"@testing-library/react": "^16.0.0",
|
|
42
51
|
"@types/chrome": "^0.0.268",
|
|
52
|
+
"@types/js-beautify": "^1.14.3",
|
|
43
53
|
"@types/node": "^22.0.0",
|
|
44
54
|
"@types/react": "^19.1.16",
|
|
45
55
|
"@types/react-dom": "^19.1.9",
|