mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge branch 'master' into monaco
This commit is contained in:
93 files changed
+1414
-736
No files matched your search
@@ -0,0 +1,277 @@
|
||||
import $ from 'jquery';
|
||||
import type { editor } from 'monaco-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import { i18n } from 'vj/utils';
|
||||
import uploadFiles from '../upload';
|
||||
|
||||
interface MonacoOptions {
|
||||
language?: string;
|
||||
onChange?: (val: string) => any;
|
||||
theme?: string;
|
||||
model?: string;
|
||||
autoResize?: boolean;
|
||||
autoLayout?: boolean;
|
||||
value?: string;
|
||||
hide?: string[];
|
||||
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
||||
}
|
||||
type Options = MonacoOptions;
|
||||
|
||||
export default class Editor extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjEditorInstance';
|
||||
model: editor.IModel;
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
markdownEditor: any;
|
||||
valueCache?: string;
|
||||
setMarkdownEditorValue?: (v: string) => void;
|
||||
reactRoot?: ReactDOM.Root;
|
||||
isValid: boolean;
|
||||
|
||||
constructor($dom, public options: Options = {}) {
|
||||
super($dom);
|
||||
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
||||
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
||||
else this.initMarkdownEditor();
|
||||
}
|
||||
|
||||
async initMonaco() {
|
||||
const { load } = await import('vj/components/monaco/loader');
|
||||
const {
|
||||
onChange, language = 'markdown',
|
||||
theme = UserContext.monacoTheme || 'vs-light',
|
||||
model = `file://model-${Math.random().toString(16)}`,
|
||||
autoResize = true, autoLayout = true,
|
||||
hide = [], lineNumbers = 'on',
|
||||
} = this.options;
|
||||
const { monaco } = await load([language]);
|
||||
const { $dom } = this;
|
||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||
const origin = $dom.get(0);
|
||||
const ele = document.createElement('div');
|
||||
$(ele).width('100%').addClass('textbox');
|
||||
if (!autoResize && $dom.height()) $(ele).height($dom.height());
|
||||
$dom.hide();
|
||||
origin.parentElement.appendChild(ele);
|
||||
const value = this.options.value || $dom.val();
|
||||
this.model = typeof model === 'string'
|
||||
? monaco.editor.getModel(monaco.Uri.parse(model))
|
||||
|| monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model))
|
||||
: model;
|
||||
if (!this.options.model) this.model.setValue(value);
|
||||
const cfg: editor.IStandaloneEditorConstructionOptions = {
|
||||
theme,
|
||||
lineNumbers,
|
||||
glyphMargin: true,
|
||||
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
|
||||
model: this.model,
|
||||
minimap: { enabled: false },
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerLanes: 0,
|
||||
overviewRulerBorder: false,
|
||||
fontFamily: UserContext.codeFontFamily,
|
||||
fontLigatures: '',
|
||||
unicodeHighlight: {
|
||||
ambiguousCharacters: language !== 'markdown',
|
||||
},
|
||||
};
|
||||
if (autoLayout) cfg.automaticLayout = true;
|
||||
let prevHeight = 0;
|
||||
const updateEditorHeight = () => {
|
||||
const editorElement = this.editor.getDomNode();
|
||||
if (!editorElement) return;
|
||||
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const lineCount = this.editor.getModel()?.getLineCount() || 1;
|
||||
let height = this.editor.getTopForLineNumber(lineCount + 1) + lineHeight;
|
||||
if (prevHeight !== height) {
|
||||
if (window.innerHeight * 1.5 < height) {
|
||||
height = window.innerHeight;
|
||||
this.editor.updateOptions({
|
||||
scrollbar: {
|
||||
vertical: 'auto',
|
||||
horizontal: 'auto',
|
||||
handleMouseWheel: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.editor.updateOptions({
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
handleMouseWheel: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
prevHeight = height;
|
||||
editorElement.style.height = `${height}px`;
|
||||
this.editor.layout();
|
||||
}
|
||||
};
|
||||
if (autoResize) {
|
||||
cfg.wordWrap = 'bounded';
|
||||
cfg.scrollBeyondLastLine = false;
|
||||
}
|
||||
this.editor = monaco.editor.create(ele, cfg);
|
||||
if (hide.length) {
|
||||
const ranges = [];
|
||||
for (const text of hide) {
|
||||
const found = this.model.findMatches(text, true, false, true, '', true);
|
||||
ranges.push(...found.map((i) => i.range));
|
||||
}
|
||||
this.editor.deltaDecorations([], ranges.map((range) => ({ range, options: { inlineClassName: 'decoration-hide' } })));
|
||||
}
|
||||
// registerAction(this.editor, this.model, this.$dom);
|
||||
if (autoResize) {
|
||||
this.editor.onDidChangeModelDecorations(() => {
|
||||
updateEditorHeight(); // typing
|
||||
requestAnimationFrame(updateEditorHeight); // folding
|
||||
});
|
||||
}
|
||||
this.editor.onDidChangeModelContent(() => {
|
||||
const val = this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
$dom.val(val);
|
||||
$dom.text(val);
|
||||
if (onChange) onChange(val);
|
||||
});
|
||||
this.isValid = true;
|
||||
if (hasFocus) this.focus();
|
||||
if (autoResize) updateEditorHeight();
|
||||
// @ts-ignore
|
||||
window.model = this.model;
|
||||
// @ts-ignore
|
||||
window.editor = this.editor;
|
||||
}
|
||||
|
||||
async initMarkdownEditor() {
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
const isProblemEdit = pagename === 'problem_edit';
|
||||
const that = this;
|
||||
const { $dom } = this;
|
||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||
const origin = $dom.get(0);
|
||||
const ele = document.createElement('div');
|
||||
const value = $dom.val();
|
||||
const { onChange } = this.options;
|
||||
const { MdEditor } = await import('./mdeditor');
|
||||
|
||||
const renderCallback = (ref) => {
|
||||
this.markdownEditor = ref;
|
||||
};
|
||||
|
||||
function EditorComponent() {
|
||||
const [val, setVal] = React.useState(value);
|
||||
that.setMarkdownEditorValue = setVal;
|
||||
return <MdEditor
|
||||
className='textbox'
|
||||
autoFocus={hasFocus}
|
||||
codeTheme='github'
|
||||
codeStyleReverse={false}
|
||||
ref={renderCallback}
|
||||
modelValue={val}
|
||||
noMermaid
|
||||
noPrettier
|
||||
autoDetectCode
|
||||
toolbarsExclude={[
|
||||
// 'bold',
|
||||
// 'underline',
|
||||
// 'italic',
|
||||
// '-',
|
||||
// 'strikeThrough',
|
||||
// 'title',
|
||||
// 'sub',
|
||||
// 'sup',
|
||||
// 'quote',
|
||||
// 'unorderedList',
|
||||
// 'orderedList',
|
||||
// 'task',
|
||||
// '-',
|
||||
// 'codeRow',
|
||||
// 'code',
|
||||
// 'link',
|
||||
// 'image',
|
||||
// 'table',
|
||||
'mermaid',
|
||||
// 'katex',
|
||||
// '-',
|
||||
// 'revoke',
|
||||
// 'next',
|
||||
'save',
|
||||
// '=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
// 'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
// 'catalog',
|
||||
'github',
|
||||
]}
|
||||
onChange={(v) => {
|
||||
that.valueCache = v;
|
||||
setVal(v);
|
||||
$dom.val(v);
|
||||
$dom.text(v);
|
||||
onChange?.(v);
|
||||
}}
|
||||
onUploadImg={async (files, callback) => {
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
|
||||
type: isProblemEdit ? 'additional_file' : undefined,
|
||||
filenameCallback: () => filename,
|
||||
}).then(() => {
|
||||
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
|
||||
}).catch(() => {
|
||||
callback([]);
|
||||
});
|
||||
return null;
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
this.reactRoot = ReactDOM.createRoot(ele);
|
||||
this.reactRoot.render(<EditorComponent />);
|
||||
$dom.hide();
|
||||
origin.parentElement.appendChild(ele);
|
||||
this.isValid = true;
|
||||
if (hasFocus) this.focus();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.detach();
|
||||
if (this.reactRoot) this.reactRoot.unmount();
|
||||
else if (this.editor?.dispose) this.editor.dispose();
|
||||
}
|
||||
|
||||
ensureValid() {
|
||||
if (!this.isValid) throw new Error('Editor is not loaded');
|
||||
}
|
||||
|
||||
value(val?: string) {
|
||||
this.ensureValid();
|
||||
if (typeof val === 'string') {
|
||||
if (this.editor) return this.editor.setValue(val);
|
||||
this.setMarkdownEditorValue?.(val);
|
||||
this.markdownEditor?.resetHistory?.();
|
||||
}
|
||||
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
return this.valueCache;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.ensureValid();
|
||||
if (!this.editor || !this.model) return;
|
||||
this.editor.focus();
|
||||
const range = this.model.getFullModelRange();
|
||||
this.editor.setPosition({ lineNumber: range.endLineNumber, column: range.endColumn });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user