mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-09 16:01:19 +07:00
A new JsonEditor.vue component wraps CodeMirror 6 + lang-json with line numbers, JSON syntax highlighting, bracket matching, code folding, search (Ctrl+F), undo/redo, lint (red squiggle and gutter icon on invalid JSON), tab indent, and line wrapping. It is wired into the four raw-JSON spots that previously used <a-textarea class="json-editor">: the Xray Advanced Template tab, the Outbound JSON tab, the Balancer Observatory pane, and the Inbound Advanced tab (settings / streamSettings / sniffing). Chrome colors are driven by EditorView.theme so they win the specificity fight cleanly against CodeMirror's own injected styles. A single buildDarkTheme() factory yields a Dark+ palette (#1e1e1e background, #252526 active line, #2d2d30 panels) for the regular dark mode and a near-black variant (#0a0a0a / #141414 / #1f1f1f border) for ultra-dark — both pair with oneDarkHighlightStyle for the syntax colors. Light mode stays on basicSetup's default. CodeMirror lazy-loads as a ~17 kB gzipped chunk that only appears on the Xray/Inbounds bundles.
186 lines
4.9 KiB
Vue
186 lines
4.9 KiB
Vue
<script setup>
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
import { EditorView, basicSetup } from 'codemirror';
|
|
import { EditorState, Compartment } from '@codemirror/state';
|
|
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
|
import { lintGutter, linter } from '@codemirror/lint';
|
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
import { syntaxHighlighting } from '@codemirror/language';
|
|
import { keymap } from '@codemirror/view';
|
|
import { indentWithTab } from '@codemirror/commands';
|
|
|
|
import { theme as themeState } from '@/composables/useTheme.js';
|
|
|
|
const props = defineProps({
|
|
value: { type: String, default: '' },
|
|
minHeight: { type: String, default: '320px' },
|
|
maxHeight: { type: String, default: '600px' },
|
|
readonly: { type: Boolean, default: false },
|
|
});
|
|
|
|
const emit = defineEmits(['update:value', 'change']);
|
|
|
|
const host = ref(null);
|
|
let view = null;
|
|
const themeCompartment = new Compartment();
|
|
const readonlyCompartment = new Compartment();
|
|
|
|
function buildDarkTheme({ bg, panelBg, activeBg, border, selection }) {
|
|
return EditorView.theme(
|
|
{
|
|
'&': { color: '#dcdcdc', backgroundColor: bg },
|
|
'.cm-content': { caretColor: '#dcdcdc' },
|
|
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#dcdcdc' },
|
|
'.cm-gutters': {
|
|
backgroundColor: bg,
|
|
borderRight: `1px solid ${border}`,
|
|
color: '#6a6a6a',
|
|
},
|
|
'.cm-activeLine': { backgroundColor: activeBg },
|
|
'.cm-activeLineGutter': { backgroundColor: activeBg, color: '#dcdcdc' },
|
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
|
{ backgroundColor: selection },
|
|
'.cm-panels': { backgroundColor: panelBg, color: '#dcdcdc' },
|
|
'.cm-panels.cm-panels-top': { borderBottom: `1px solid ${border}` },
|
|
'.cm-panels.cm-panels-bottom': { borderTop: `1px solid ${border}` },
|
|
'.cm-tooltip': {
|
|
backgroundColor: panelBg,
|
|
border: `1px solid ${border}`,
|
|
color: '#dcdcdc',
|
|
},
|
|
},
|
|
{ dark: true },
|
|
);
|
|
}
|
|
|
|
const darkTheme = buildDarkTheme({
|
|
bg: '#1e1e1e',
|
|
panelBg: '#2d2d30',
|
|
activeBg: '#252526',
|
|
border: '#3a3a3c',
|
|
selection: '#3a3a3c',
|
|
});
|
|
|
|
const ultraDarkTheme = buildDarkTheme({
|
|
bg: '#0a0a0a',
|
|
panelBg: '#141414',
|
|
activeBg: '#141414',
|
|
border: '#1f1f1f',
|
|
selection: '#2a2a2a',
|
|
});
|
|
|
|
function themeExtension() {
|
|
if (!themeState.isDark) return [];
|
|
const chrome = themeState.isUltra ? ultraDarkTheme : darkTheme;
|
|
return [chrome, syntaxHighlighting(oneDarkHighlightStyle)];
|
|
}
|
|
|
|
function readonlyExtension() {
|
|
return EditorState.readOnly.of(props.readonly);
|
|
}
|
|
|
|
onMounted(() => {
|
|
const updateListener = EditorView.updateListener.of((u) => {
|
|
if (!u.docChanged) return;
|
|
const next = u.state.doc.toString();
|
|
if (next === props.value) return;
|
|
emit('update:value', next);
|
|
emit('change', next);
|
|
});
|
|
|
|
view = new EditorView({
|
|
parent: host.value,
|
|
state: EditorState.create({
|
|
doc: props.value || '',
|
|
extensions: [
|
|
basicSetup,
|
|
keymap.of([indentWithTab]),
|
|
json(),
|
|
linter(jsonParseLinter()),
|
|
lintGutter(),
|
|
EditorView.lineWrapping,
|
|
updateListener,
|
|
themeCompartment.of(themeExtension()),
|
|
readonlyCompartment.of(readonlyExtension()),
|
|
EditorView.theme({
|
|
'&': { height: '100%' },
|
|
'.cm-scroller': {
|
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
fontSize: '12px',
|
|
minHeight: props.minHeight,
|
|
maxHeight: props.maxHeight,
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
watch(() => props.value, (next) => {
|
|
if (!view) return;
|
|
const current = view.state.doc.toString();
|
|
if (next === current) return;
|
|
view.dispatch({
|
|
changes: { from: 0, to: current.length, insert: next || '' },
|
|
});
|
|
});
|
|
|
|
watch(
|
|
[() => themeState.isDark, () => themeState.isUltra],
|
|
() => {
|
|
if (!view) return;
|
|
view.dispatch({ effects: themeCompartment.reconfigure(themeExtension()) });
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.readonly,
|
|
() => {
|
|
if (!view) return;
|
|
view.dispatch({ effects: readonlyCompartment.reconfigure(readonlyExtension()) });
|
|
},
|
|
);
|
|
|
|
onBeforeUnmount(() => {
|
|
view?.destroy();
|
|
view = null;
|
|
});
|
|
|
|
defineExpose({
|
|
focus: () => view?.focus(),
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="host" class="json-editor-host" />
|
|
</template>
|
|
|
|
<style scoped>
|
|
.json-editor-host {
|
|
border: 1px solid var(--ant-color-border, #d9d9d9);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background: var(--ant-color-bg-container, #fff);
|
|
}
|
|
|
|
.json-editor-host :deep(.cm-editor),
|
|
.json-editor-host :deep(.cm-editor.cm-focused) {
|
|
outline: none;
|
|
}
|
|
|
|
.json-editor-host:focus-within {
|
|
border-color: var(--ant-color-primary, #1677ff);
|
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
|
}
|
|
|
|
:global(body.dark) .json-editor-host {
|
|
border-color: #3a3a3c;
|
|
background: #1e1e1e;
|
|
}
|
|
|
|
:global(html[data-theme="ultra-dark"]) .json-editor-host {
|
|
border-color: #1f1f1f;
|
|
background: #0a0a0a;
|
|
}
|
|
</style>
|