addon.ts
// 需要引入的插件
import "codemirror/addon/search/searchcursor.js";
import "codemirror/addon/search/search.js";
import "codemirror/addon/scroll/annotatescrollbar.js";
import "codemirror/addon/search/matchesonscrollbar.js";
import "codemirror/addon/search/jump-to-line.js";
import "codemirror/addon/dialog/dialog.js";
import "codemirror/mode/groovy/groovy";
import "codemirror/mode/javascript/javascript";
import "codemirror/addon/dialog/dialog.css";
import "codemirror/addon/search/matchesonscrollbar.css";
/** 主题 */
import "codemirror/theme/eclipse.css";
variable.less
@color: #f9fafc;
@nz-code-prefix-cls: ~"nz-code";
@nz-code-content-prefix-cls: ~"@{nz-code-prefix-cls}-content";
@nz-code-toolbar-prefix-cls: ~"@{nz-code-prefix-cls}-toolbar";
@toolbar-default-height: 32px;
@default-font-size: 12px;
index.less
@import './codemirror.less';
@import './toolbar.less';
// @import './eclipse.less';
// @import './default.less';
.toolbar.less
@import "./variable.less";
.@{nz-code-toolbar-prefix-cls} {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
display: flex;
align-items: center;
width: 100%;
height: @toolbar-default-height;
padding: 0 12px;
line-height: @toolbar-default-height;
background-color: @color;
border: 1px solid rgba(216, 216, 216, 1);
&-both {
justify-content: space-between;
.ant-space {
justify-content: flex-end;
}
}
&-extra {
display: inline-block;
}
.ant-space {
flex: 0 0 100px;
}
.anticon {
cursor: pointer;
}
}
config.ts
export default {
lineNumbers: true,
styleActiveLine: true,
lineWrapping: true,
value: "",
theme: "eclipse",
foldGutter: true,
readOnly: false,
smartIndent: true,
mode: "text/x-groovy",
matchBrackets: true,
autoCloseBrackets: true,
showCursorWhenSelecting: true,
highlightSelectionMatches: {
showToken: /\w/,
annotateScrollbar: true,
},
indentUnit: 2,
tabSize: 2,
autofocus: true,
} as CodeMirror.EditorConfiguration;
// context.ts
import { useContext, createContext } from '@alipay/bigfish/react';
import { CodeEditorContextProps } from './interface';
export const CodeEditorContext = createContext<CodeEditorContextProps>({
ints: null,
});
export const useCodeEditor = () => useContext(CodeEditorContext);
// index.tsx
import React, {
FC,
useRef,
useMemo,
useState,
useEffect,
useCallback,
} from "@alipay/bigfish/react";
import { Spin } from "@alipay/bigfish/antd";
import cs from "@alipay/bigfish/util/classnames";
import { useFullscreen } from "ahooks";
import CodeMirror from "codemirror";
import { ErrorBoundary } from "@alipay/360-components";
import ToolBar from "./component/ToolBar";
import "./addon";
import baseConfig from "./config";
import { CodeEditorProps } from "./interface";
import { CodeEditorContext } from "./context";
import "./style/index.less";
const CodeEditor: FC<CodeEditorProps> = React.memo<CodeEditorProps>((props) => {
const textRef = useRef<HTMLTextAreaElement>(null);
const fullRef = useRef<HTMLDivElement | null>(null);
const [ints, setInts] = useState<CodeMirror.Editor | null>(null);
const intsRef = useRef<CodeMirror.Editor | null>(null);
const [isFullscreen, { toggleFull }] = useFullscreen(fullRef);
const {
style,
onLoaded,
toolbar,
className,
spinning,
value,
extra,
options,
onChange,
} = props;
const config = useMemo(() => {
return {
...baseConfig,
value,
};
}, [value]);
const onTextChange = useCallback(
(cm: CodeMirror.Editor, _changeObj: CodeMirror.EditorChangeLinkedList) => {
const localValue = cm.getDoc().getValue();
if (typeof onChange === "function" && "value" in props) {
onChange(localValue);
}
},
[onChange]
);
const initCodeInts = useCallback(() => {
if (textRef.current) {
const codeInts = CodeMirror.fromTextArea(
textRef.current as HTMLTextAreaElement,
config
);
if (typeof onLoaded === "function") {
onLoaded(codeInts);
}
setInts(codeInts);
intsRef.current = codeInts;
intsRef.current.defaultTextHeight();
intsRef.current.on("change", onTextChange);
}
}, [textRef, config]);
useEffect(() => {
const localValue = ints?.getDoc()?.getValue();
const cursor: any = ints?.getDoc().getCursor();
if (value && value !== localValue) {
ints?.setValue(value);
ints?.setCursor(cursor);
}
}, [value]);
useEffect(() => {
if (textRef.current) {
initCodeInts();
}
return () => {
intsRef.current?.off("change", onTextChange);
};
}, [textRef, intsRef]);
/** options merge */
useEffect(() => {
if (
ints &&
options &&
Object.prototype.toString.call(options) === "[object Object]"
) {
(Object.keys(
options
) as (keyof CodeMirror.EditorConfiguration)[]).forEach((key) => {
if (ints?.getOption(key) !== options[key]) {
ints.setOption(key, options[key]);
}
});
}
}, [props.options, ints]);
const backToTop = useCallback(() => {
if (intsRef.current) {
intsRef.current?.scrollTo?.(0, 0);
}
}, [intsRef]);
const cls = useMemo(() => cs("nz-code", className), [className]);
const renderToolBar = useCallback(() => {
if (!toolbar && !extra) return null;
if (typeof toolbar === "boolean" && toolbar) {
const toolbarCls = extra
? "nz-code-toolbar nz-code-toolbar-both"
: "nz-code-toolbar";
return (
<div className={toolbarCls}>
{extra && <div className="nz-code-toolbar-extra">{extra}</div>}
<ToolBar
backToTop={backToTop}
toggleFull={toggleFull}
isFullscreen={isFullscreen}
/>
</div>
);
}
return (
<div className="nz-code-toolbar">
<ToolBar toggleFull={toggleFull} isFullscreen={isFullscreen} />
</div>
);
}, [toolbar, extra]);
return (
<ErrorBoundary title="代码编辑有误!">
<CodeEditorContext.Provider value={{ ints }}>
<div className={cls} style={style} ref={fullRef}>
<Spin className="nz-code-content" spinning={spinning}>
<textarea ref={textRef} autoComplete="off" defaultValue={value} />
</Spin>
{renderToolBar()}
{props.children}
</div>
</CodeEditorContext.Provider>
</ErrorBoundary>
);
});
CodeEditor.defaultProps = {
spinning: false,
toolbar: true,
};
export { useCodeEditor } from "./context";
export default CodeEditor;
// interface.ts
import { ReactNode } from '@alipay/bigfish/react';
export interface CodeEditorContextProps {
ints: CodeMirror.Editor | null;
}
export interface RenderToolbar {
(): JSX.Element;
}
export interface ToolbarConfig {
/** 全屏 */
fullScreen?: boolean;
/** 切换主题 */
theme?: boolean;
/** 简单配置 */
setting?: boolean;
/** 回到顶部 */
top?: boolean;
}
export interface CodeEditorProps {
spinning?: boolean;
className?: string;
value?: string;
style?: React.CSSProperties;
/** 自定义配置 */
options?: CodeMirror.EditorConfiguration;
onChange?(val?: string): void;
/** 初始化完成 */
onLoaded?(editor: CodeMirror.Editor): void;
children?: JSX.Element | JSX.Element[];
toolbar?: boolean | RenderToolbar | ToolbarConfig;
/** 工具栏其他辅助操作 */
extra?: ReactNode;
}