addon.ts

    1. // 需要引入的插件
    2. import "codemirror/addon/search/searchcursor.js";
    3. import "codemirror/addon/search/search.js";
    4. import "codemirror/addon/scroll/annotatescrollbar.js";
    5. import "codemirror/addon/search/matchesonscrollbar.js";
    6. import "codemirror/addon/search/jump-to-line.js";
    7. import "codemirror/addon/dialog/dialog.js";
    8. import "codemirror/mode/groovy/groovy";
    9. import "codemirror/mode/javascript/javascript";
    10. import "codemirror/addon/dialog/dialog.css";
    11. import "codemirror/addon/search/matchesonscrollbar.css";
    12. /** 主题 */
    13. import "codemirror/theme/eclipse.css";

    variable.less

    1. @color: #f9fafc;
    2. @nz-code-prefix-cls: ~"nz-code";
    3. @nz-code-content-prefix-cls: ~"@{nz-code-prefix-cls}-content";
    4. @nz-code-toolbar-prefix-cls: ~"@{nz-code-prefix-cls}-toolbar";
    5. @toolbar-default-height: 32px;
    6. @default-font-size: 12px;

    index.less

    1. @import './codemirror.less';
    2. @import './toolbar.less';
    3. // @import './eclipse.less';
    4. // @import './default.less';

    .toolbar.less

    1. @import "./variable.less";
    2. .@{nz-code-toolbar-prefix-cls} {
    3. position: absolute;
    4. top: 0;
    5. right: 0;
    6. bottom: 0;
    7. left: 0;
    8. z-index: 10;
    9. display: flex;
    10. align-items: center;
    11. width: 100%;
    12. height: @toolbar-default-height;
    13. padding: 0 12px;
    14. line-height: @toolbar-default-height;
    15. background-color: @color;
    16. border: 1px solid rgba(216, 216, 216, 1);
    17. &-both {
    18. justify-content: space-between;
    19. .ant-space {
    20. justify-content: flex-end;
    21. }
    22. }
    23. &-extra {
    24. display: inline-block;
    25. }
    26. .ant-space {
    27. flex: 0 0 100px;
    28. }
    29. .anticon {
    30. cursor: pointer;
    31. }
    32. }

    config.ts

    1. export default {
    2. lineNumbers: true,
    3. styleActiveLine: true,
    4. lineWrapping: true,
    5. value: "",
    6. theme: "eclipse",
    7. foldGutter: true,
    8. readOnly: false,
    9. smartIndent: true,
    10. mode: "text/x-groovy",
    11. matchBrackets: true,
    12. autoCloseBrackets: true,
    13. showCursorWhenSelecting: true,
    14. highlightSelectionMatches: {
    15. showToken: /\w/,
    16. annotateScrollbar: true,
    17. },
    18. indentUnit: 2,
    19. tabSize: 2,
    20. autofocus: true,
    21. } as CodeMirror.EditorConfiguration;
    1. // context.ts
    2. import { useContext, createContext } from '@alipay/bigfish/react';
    3. import { CodeEditorContextProps } from './interface';
    4. export const CodeEditorContext = createContext<CodeEditorContextProps>({
    5. ints: null,
    6. });
    7. export const useCodeEditor = () => useContext(CodeEditorContext);
    1. // index.tsx
    2. import React, {
    3. FC,
    4. useRef,
    5. useMemo,
    6. useState,
    7. useEffect,
    8. useCallback,
    9. } from "@alipay/bigfish/react";
    10. import { Spin } from "@alipay/bigfish/antd";
    11. import cs from "@alipay/bigfish/util/classnames";
    12. import { useFullscreen } from "ahooks";
    13. import CodeMirror from "codemirror";
    14. import { ErrorBoundary } from "@alipay/360-components";
    15. import ToolBar from "./component/ToolBar";
    16. import "./addon";
    17. import baseConfig from "./config";
    18. import { CodeEditorProps } from "./interface";
    19. import { CodeEditorContext } from "./context";
    20. import "./style/index.less";
    21. const CodeEditor: FC<CodeEditorProps> = React.memo<CodeEditorProps>((props) => {
    22. const textRef = useRef<HTMLTextAreaElement>(null);
    23. const fullRef = useRef<HTMLDivElement | null>(null);
    24. const [ints, setInts] = useState<CodeMirror.Editor | null>(null);
    25. const intsRef = useRef<CodeMirror.Editor | null>(null);
    26. const [isFullscreen, { toggleFull }] = useFullscreen(fullRef);
    27. const {
    28. style,
    29. onLoaded,
    30. toolbar,
    31. className,
    32. spinning,
    33. value,
    34. extra,
    35. options,
    36. onChange,
    37. } = props;
    38. const config = useMemo(() => {
    39. return {
    40. ...baseConfig,
    41. value,
    42. };
    43. }, [value]);
    44. const onTextChange = useCallback(
    45. (cm: CodeMirror.Editor, _changeObj: CodeMirror.EditorChangeLinkedList) => {
    46. const localValue = cm.getDoc().getValue();
    47. if (typeof onChange === "function" && "value" in props) {
    48. onChange(localValue);
    49. }
    50. },
    51. [onChange]
    52. );
    53. const initCodeInts = useCallback(() => {
    54. if (textRef.current) {
    55. const codeInts = CodeMirror.fromTextArea(
    56. textRef.current as HTMLTextAreaElement,
    57. config
    58. );
    59. if (typeof onLoaded === "function") {
    60. onLoaded(codeInts);
    61. }
    62. setInts(codeInts);
    63. intsRef.current = codeInts;
    64. intsRef.current.defaultTextHeight();
    65. intsRef.current.on("change", onTextChange);
    66. }
    67. }, [textRef, config]);
    68. useEffect(() => {
    69. const localValue = ints?.getDoc()?.getValue();
    70. const cursor: any = ints?.getDoc().getCursor();
    71. if (value && value !== localValue) {
    72. ints?.setValue(value);
    73. ints?.setCursor(cursor);
    74. }
    75. }, [value]);
    76. useEffect(() => {
    77. if (textRef.current) {
    78. initCodeInts();
    79. }
    80. return () => {
    81. intsRef.current?.off("change", onTextChange);
    82. };
    83. }, [textRef, intsRef]);
    84. /** options merge */
    85. useEffect(() => {
    86. if (
    87. ints &&
    88. options &&
    89. Object.prototype.toString.call(options) === "[object Object]"
    90. ) {
    91. (Object.keys(
    92. options
    93. ) as (keyof CodeMirror.EditorConfiguration)[]).forEach((key) => {
    94. if (ints?.getOption(key) !== options[key]) {
    95. ints.setOption(key, options[key]);
    96. }
    97. });
    98. }
    99. }, [props.options, ints]);
    100. const backToTop = useCallback(() => {
    101. if (intsRef.current) {
    102. intsRef.current?.scrollTo?.(0, 0);
    103. }
    104. }, [intsRef]);
    105. const cls = useMemo(() => cs("nz-code", className), [className]);
    106. const renderToolBar = useCallback(() => {
    107. if (!toolbar && !extra) return null;
    108. if (typeof toolbar === "boolean" && toolbar) {
    109. const toolbarCls = extra
    110. ? "nz-code-toolbar nz-code-toolbar-both"
    111. : "nz-code-toolbar";
    112. return (
    113. <div className={toolbarCls}>
    114. {extra && <div className="nz-code-toolbar-extra">{extra}</div>}
    115. <ToolBar
    116. backToTop={backToTop}
    117. toggleFull={toggleFull}
    118. isFullscreen={isFullscreen}
    119. />
    120. </div>
    121. );
    122. }
    123. return (
    124. <div className="nz-code-toolbar">
    125. <ToolBar toggleFull={toggleFull} isFullscreen={isFullscreen} />
    126. </div>
    127. );
    128. }, [toolbar, extra]);
    129. return (
    130. <ErrorBoundary title="代码编辑有误!">
    131. <CodeEditorContext.Provider value={{ ints }}>
    132. <div className={cls} style={style} ref={fullRef}>
    133. <Spin className="nz-code-content" spinning={spinning}>
    134. <textarea ref={textRef} autoComplete="off" defaultValue={value} />
    135. </Spin>
    136. {renderToolBar()}
    137. {props.children}
    138. </div>
    139. </CodeEditorContext.Provider>
    140. </ErrorBoundary>
    141. );
    142. });
    143. CodeEditor.defaultProps = {
    144. spinning: false,
    145. toolbar: true,
    146. };
    147. export { useCodeEditor } from "./context";
    148. export default CodeEditor;
    1. // interface.ts
    2. import { ReactNode } from '@alipay/bigfish/react';
    3. export interface CodeEditorContextProps {
    4. ints: CodeMirror.Editor | null;
    5. }
    6. export interface RenderToolbar {
    7. (): JSX.Element;
    8. }
    9. export interface ToolbarConfig {
    10. /** 全屏 */
    11. fullScreen?: boolean;
    12. /** 切换主题 */
    13. theme?: boolean;
    14. /** 简单配置 */
    15. setting?: boolean;
    16. /** 回到顶部 */
    17. top?: boolean;
    18. }
    19. export interface CodeEditorProps {
    20. spinning?: boolean;
    21. className?: string;
    22. value?: string;
    23. style?: React.CSSProperties;
    24. /** 自定义配置 */
    25. options?: CodeMirror.EditorConfiguration;
    26. onChange?(val?: string): void;
    27. /** 初始化完成 */
    28. onLoaded?(editor: CodeMirror.Editor): void;
    29. children?: JSX.Element | JSX.Element[];
    30. toolbar?: boolean | RenderToolbar | ToolbarConfig;
    31. /** 工具栏其他辅助操作 */
    32. extra?: ReactNode;
    33. }