React 18 and Vite (#4510)
This commit is contained in:
parent
1bb1b13f6d
commit
21613fa602
244 changed files with 4547 additions and 8933 deletions
206
resources/scripts/components/elements/editor/Editor.tsx
Normal file
206
resources/scripts/components/elements/editor/Editor.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
indentOnInput,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
LanguageDescription,
|
||||
indentUnit,
|
||||
} from '@codemirror/language';
|
||||
import { languages } from '@codemirror/language-data';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
EditorView,
|
||||
} from '@codemirror/view';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { ayuMirageHighlightStyle, ayuMirageTheme } from './theme';
|
||||
|
||||
function findLanguageByFilename(filename: string): LanguageDescription | undefined {
|
||||
const language = LanguageDescription.matchFilename(languages, filename);
|
||||
if (language !== null) {
|
||||
return language;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultExtensions: Extension = [
|
||||
// Ayu Mirage
|
||||
ayuMirageTheme,
|
||||
syntaxHighlighting(ayuMirageHighlightStyle),
|
||||
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
indentWithTab,
|
||||
]),
|
||||
EditorState.tabSize.of(4),
|
||||
indentUnit.of('\t'),
|
||||
];
|
||||
|
||||
export interface EditorProps {
|
||||
// DOM
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
|
||||
// CodeMirror Config
|
||||
extensions?: Extension[];
|
||||
language?: LanguageDescription;
|
||||
|
||||
// Options
|
||||
filename?: string;
|
||||
initialContent?: string;
|
||||
|
||||
// ?
|
||||
fetchContent?: (callback: () => Promise<string>) => void;
|
||||
|
||||
// Events
|
||||
onContentSaved?: () => void;
|
||||
onLanguageChanged?: (language: LanguageDescription | undefined) => void;
|
||||
}
|
||||
|
||||
export function Editor(props: EditorProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [view, setView] = useState<EditorView>();
|
||||
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [languageConfig] = useState(new Compartment());
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [keybindings] = useState(new Compartment());
|
||||
|
||||
const createEditorState = () =>
|
||||
EditorState.create({
|
||||
doc: props.initialContent,
|
||||
extensions: [
|
||||
defaultExtensions,
|
||||
props.extensions === undefined ? [] : props.extensions,
|
||||
languageConfig.of([]),
|
||||
keybindings.of([]),
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setView(
|
||||
new EditorView({
|
||||
state: createEditorState(),
|
||||
parent: ref.current,
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.destroy();
|
||||
setView(undefined);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.setState(createEditorState());
|
||||
}, [props.initialContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const language = props.language ?? findLanguageByFilename(props.filename ?? '');
|
||||
if (language === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
void language.load().then(language => {
|
||||
view.dispatch({
|
||||
effects: languageConfig.reconfigure(language),
|
||||
});
|
||||
});
|
||||
|
||||
if (props.onLanguageChanged !== undefined) {
|
||||
props.onLanguageChanged(language);
|
||||
}
|
||||
}, [view, props.filename, props.language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.fetchContent === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
props.fetchContent(async () => {
|
||||
throw new Error('no editor session has been configured');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { onContentSaved } = props;
|
||||
if (onContentSaved !== undefined) {
|
||||
view.dispatch({
|
||||
effects: keybindings.reconfigure(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Mod-s',
|
||||
run() {
|
||||
onContentSaved();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
props.fetchContent(async () => view.state.doc.toJSON().join('\n'));
|
||||
}, [view, props.fetchContent, props.onContentSaved]);
|
||||
|
||||
return <div ref={ref} className={`relative ${props.className ?? ''}`.trimEnd()} style={props.style} />;
|
||||
}
|
1
resources/scripts/components/elements/editor/index.ts
Normal file
1
resources/scripts/components/elements/editor/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Editor } from './Editor';
|
148
resources/scripts/components/elements/editor/theme.ts
Normal file
148
resources/scripts/components/elements/editor/theme.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
import { HighlightStyle } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
const highlightBackground = 'transparent';
|
||||
const background = '#1F2430';
|
||||
const selection = '#34455A';
|
||||
const cursor = '#FFCC66';
|
||||
|
||||
export const ayuMirageTheme: Extension = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
color: '#CBCCC6',
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-cursor': { borderLeftColor: cursor },
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: '#232834', color: '#CBCCC6' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'.cm-matchingBracket, .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#FF3333',
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
'.cm-gutterElement': {
|
||||
color: 'rgba(61, 66, 77, 99)',
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
border: '1px solid #181a1f',
|
||||
backgroundColor: '#232834',
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: '#CBCCC6',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
export const ayuMirageHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
tag: t.keyword,
|
||||
color: '#FFA759',
|
||||
},
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.function(t.variableName), t.labelName],
|
||||
color: '#CBCCC6',
|
||||
},
|
||||
{
|
||||
tag: [t.color, t.constant(t.name), t.standard(t.name)],
|
||||
color: '#F29E74',
|
||||
},
|
||||
{
|
||||
tag: [t.definition(t.name), t.separator],
|
||||
color: '#CBCCC6B3',
|
||||
},
|
||||
{
|
||||
tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
|
||||
color: '#FFCC66',
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.meta, t.comment],
|
||||
color: '#5C6773',
|
||||
},
|
||||
{
|
||||
tag: t.strong,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
{
|
||||
tag: t.emphasis,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
{
|
||||
tag: t.strikethrough,
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
{
|
||||
tag: t.link,
|
||||
color: '#FF3333',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
{
|
||||
tag: t.heading,
|
||||
fontWeight: 'bold',
|
||||
color: '#BAE67E',
|
||||
},
|
||||
{
|
||||
tag: [t.atom, t.bool, t.special(t.variableName)],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.processingInstruction, t.string, t.inserted],
|
||||
color: '#BAE67E',
|
||||
},
|
||||
{
|
||||
tag: t.invalid,
|
||||
color: '#FF3333',
|
||||
},
|
||||
]);
|
Loading…
Add table
Add a link
Reference in a new issue