import React, { useRef, useEffect, useState } from "react"; import classNames from "classnames"; import { useLast } from "../_util/use-last"; import { StyledProps } from "../_type"; import { mergeStyle } from "../_util/merge-style"; import { ErrorTip, LoadingTip } from "../tips"; import { injectValue } from "../_util/inject-value"; import { useConfig } from "../_util/config-context"; type BasicType = string | boolean | number | (string | boolean | number)[]; type ExtractBasic = Extract; interface Message { type: string; payload?: any; } type PickBasic = { [key in keyof T]: T[key] extends { [key: string]: any } ? PickBasic : ExtractBasic; }; type IEditorOptions = any; type IStandaloneEditorConstructionOptions = any; /** * `options` 详见: [IStandaloneEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html) */ export interface CodeEditorOptions extends PickBasic {} export interface CodeEditorProps extends StyledProps { /** * 编辑器配置,会合并下面的默认值: ```js { "language": "javascript" } ``` * 该配置只有在初始渲染时传入有效,后续传入不再生效。 * 支持的配置请参考: [IStandaloneEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html) * * > 注意:由于 `options` 经过 JSON 序列化给到 iframe,所以配置中传入函数的方式都会无效 */ options?: CodeEditorOptions; /** * 是否自动获得焦点 * @default false */ autoFocus?: boolean; /** * 加载时显示的文本 * @default */ loadingPlaceholder?: React.ReactNode; /** * 出现错误或超时显示的文本 * @docType React.ReactNode | ((retry: () => void) => React.ReactNode) * @default retry => */ errorPlaceholder?: React.ReactNode | ((retry: () => void) => React.ReactNode); /** * 发生编辑时回调 */ onEdit?: (editor: CodeEditorInstance) => void; /** * 发生保存(Ctrl + S)时回调 */ onSave?: (editor: CodeEditorInstance) => void; /** * 编辑器可用时回调 */ onReady?: (editor: CodeEditorInstance) => void; /** * 加载编辑器发生错误时回调 */ onError?: (error: Error) => void; /** * 接收到 iframe 消息时回调,若用户调用了 event.preventDefault, 则不执行内部的默认行为 */ onMessage?: (message: Message, event: MessageEvent) => void; /** * 加载编辑器超时时间(ms) * @default 10000 */ timeout?: number; /** * 加载编辑器超时时回调 */ onTimeout?: () => void; /* * 供 iframe 加载的 URL 地址 * * 当默认提供的 Editor 不满足要求时,可定制页面供 iframe 加载 */ src?: string; } export interface CodeEditorInstance { /** * 异步获取编辑器当前文本 */ getValue(options?: { preserveBOM: boolean; lineEnding: "\n" | "\r\n"; }): Promise; /** * 设置编辑器当前文本 * @param value */ setValue(value: string): void; /** * 聚焦编辑器 */ focus(): void; /** * 更新配置 */ updateOptions(newOptions: IEditorOptions): void; /** * 向编辑器内部传递消息 */ sendMessage(type: string, payload?: any): void; } // 获取值时自增引用 let nextValueKey = 0; export function CodeEditor(props: CodeEditorProps) { const { classPrefix } = useConfig(); const [ready, setReady] = useState(false); const [error, setError] = useState(false); const timerRef = useRef(null); const { options, autoFocus, className, style, onEdit, onReady, onSave, onError, onMessage, timeout = 10000, onTimeout, loadingPlaceholder = , errorPlaceholder = retry => , src = "https://cloudcache.tencentcs.com/qcloud/vendors/monaco/editor.html", } = props; const handler = useLast({ onEdit, onReady, onSave, onError, onMessage }); const iframeRef = useRef(null); const valueCallbackMap = useRef(new Map()); useEffect(() => { if (!iframeRef.current) { return () => null; } let instance: CodeEditorInstance; const callHandler = (method: keyof typeof handler.current, ...args) => { if (handler.current && typeof handler.current[method] === "function") { if (method === "onError") { handler.current[method](args[0]); return; } if (method === "onMessage") { handler.current[method](args[0], args[1]); return; } handler.current[method](instance); } }; const receive = (evt: MessageEvent) => { if ( !iframeRef.current || evt.source !== iframeRef.current.contentWindow ) { return; } const { source } = evt; const message = decodeMessage(evt.data); if (!message) { return; } let isEventPrevented = false; if (onMessage) { const preventDefault = evt.preventDefault.bind(evt); callHandler( "onMessage", message, Object.assign(evt, { preventDefault() { preventDefault(); isEventPrevented = true; }, }) ); } if (isEventPrevented) { return; } const send = (type: string, payload?: any) => { (source as WindowProxy).postMessage( encodeMessage(type, payload), src.replace(/(\w)\/(.*)/, "$1") ); }; const { type, payload } = message; switch (type) { case "ready": { send("create", { language: "javascript", autoFocus, ...(options || null), }); instance = { focus: () => send("focus"), getValue: option => new Promise(resolve => { const key = nextValueKey; nextValueKey += 1; send("get-value", { key, option }); valueCallbackMap.current.set(key.toString(), resolve); }), setValue: value => send("set-value", { value }), updateOptions: options => send("update-options", { options }), sendMessage: send, }; removeTimeoutListener(); setReady(true); callHandler("onReady"); break; } case "value": { const { key, value } = payload; const resolve = valueCallbackMap.current.get(String(key)); if (resolve) { valueCallbackMap.current.delete(String(key)); resolve(value); } break; } case "edit": { callHandler("onEdit"); break; } case "save": { callHandler("onSave"); break; } case "error": { removeTimeoutListener(); setError(true); callHandler("onError", new Error(payload.message)); break; } } }; addTimeoutListener(); window.addEventListener("message", receive); return () => { removeTimeoutListener(); window.removeEventListener("message", receive); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps function addTimeoutListener() { removeTimeoutListener(); timerRef.current = setTimeout(() => { setError(true); if (typeof onTimeout === "function") { onTimeout(); } }, timeout); } function removeTimeoutListener() { clearTimeout(timerRef.current); } if (error) { return (
{injectValue(errorPlaceholder)(() => { setError(false); addTimeoutListener(); })}
); } return (