index.tsx 9.83 KB
import React, { useEffect, useState } from 'react';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import { cloneDeep, flatten, sortBy, size, isEqual, forEach, debounce } from 'lodash-es';
import type { VariableMappingProps } from '../qx-field-setter';
import funObjects from './functions';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import { CloseCircleFilled } from '@ant-design/icons';
import './index.less';

/**
 * 填充到指定位数
 * eg:000000000
 *
 * @param val     原始值
 * @param length  填充后长度
 * @param fill    填充值
 */
const strFill = (val: string | number, length: number, fill: string | number) => {
  return val.toString().padStart(length, fill.toString());
};

type PositionProps = {
  s: number;
  e: number;
};

export type VariableProps = {
  variable: string;
  pos: PositionProps;
};

/**
 * 字符串中提取变量(${xxx})
 * @param code
 */
export const getAllVariable = (code: string) => {
  let codeLocal: string = cloneDeep(code);
  if (!codeLocal) {
    return [];
  }
  const variables: VariableProps[] = [];

  function loopGet() {
    const pos: PositionProps = {
      s: codeLocal.indexOf('${'),
      e: codeLocal.indexOf('}'),
    };
    if (pos.s === -1 && pos.e === -1) {
      return;
    }
    const variable = codeLocal.slice(pos.s, pos.e + 1);
    variables.push({ variable, pos });
    codeLocal = codeLocal.replace(variable, strFill(0, variable.length, 0));

    loopGet();
  }

  loopGet();
  return variables;
};

const domTagGen = (variable: string, text: string, color?: string) => {
  // tag html
  const dom = document.createElement('span');
  // dom.className = 'ant-tag tag';
  dom.style.background = '#c9dffc';
  dom.style.borderRadius = '2px';
  dom.style.margin = '0 1px';
  dom.style.padding = '1px 3px';
  dom.style.fontSize = '96%';
  if (color) {
    dom.style.color = color;
  }
  dom.setAttribute('data-widget', variable);
  dom.innerHTML = text;
  return dom;
};

type CodeHighLightProps = {
  // eg: 'hello ${v1} ${v2}!'
  value: string;
  // eg: {'${v1}': 'hehe', '${v2}': 'enen'}
  variableObj: Record<string, string> | undefined;
  onChange: (val: any) => void;
  className?: any;
  newVariable?: VariableMappingProps;
  style?: any;
  autofocus?: boolean;
  focusFunHandler?: (str: string) => void;
  readOnly?: boolean;
  // 是否使用函数(使用,则执行针对函数关键词的高亮匹配处理)
  isUseFun?: boolean;
  from?: string;
  resetValue?: string;
  allowClear?: boolean | undefined;
};

/**
 * 变量高亮
 *  const inputRef = React.useRef<any>(null);
 * `const cm = inputRef.current.editor;`
 *
 * @param props
 * @constructor
 */
const CodeEditor: React.FC<CodeHighLightProps> = ({
  value,
  variableObj,
  newVariable,
  className,
  onChange,
  autofocus,
  focusFunHandler,
  readOnly,
  isUseFun,
  from,
  resetValue,
  allowClear,
}) => {
  const [valueLocal, setValueLocal] = useState<string>();
  // 变量值、名称映射关系
  const [variableObjLocal, setVariableObjLocal] = useState<Record<string, string>>({});
  const [codeEditor, setCodeEditor] = useState<any>();
  const [isInitDone, setIsInitDone] = useState<boolean>(false);
  const [focusFun, setFocusFun] = useState<string>();

  const funObjs = flatten(funObjects.map((item: any) => item.children));
  const funObjSimple: any = {};
  funObjs.map((item) => Object.assign(funObjSimple, { [item.title]: item.title }));
  // 以变量字符长度排序,确保`DATAIF`比`IF`优先匹配
  const funObjsSort = sortBy(funObjs, function (o) {
    return 0 - o.title.length;
  });

  useEffect(() => {
    if (value && variableObj) {
      // 限制初始值只设置一次
      if (!isInitDone) {
        setValueLocal(value);
      }

      if (size(variableObj) > 0) {
        setVariableObjLocal(variableObj);
      }
    }
  }, [value, variableObj]);

  useEffect(() => {
    if (typeof focusFunHandler === 'function') {
      focusFunHandler(focusFun || '');
    }
  }, [focusFun]);

  const cmUtils = {
    // 插入变量
    insert: (editor: any, variable: string, isFormula: boolean = false) => {
      if (!editor) {
        return;
      }
      // 光标位置插入新值(变量)
      editor.replaceSelection(variable);
      cmUtils.variableRender(editor);

      // "公式"类型变量时
      if (isFormula) {
        // 光标位置固定插入内容
        editor.replaceSelection('()');
        // 光标左移
        codeEditor.execCommand('goCharLeft');
      }
      // 让编辑器聚集
      editor.focus();
    },

    // 替换变量(变量示例:${xxx})
    variableRender: (editor: any) => {
      if (!editor) {
        return;
      }

      const code = cloneDeep(editor.getValue());
      // 换行分隔
      const codeArr = code.split('\n');
      const variReplace = (cm: any, line: number) => {
        const sIndex = codeArr[line].indexOf('${');
        const eIndex = codeArr[line].indexOf('}');
        if (sIndex === -1 && eIndex === -1) {
          return;
        }

        const variable = codeArr[line].slice(sIndex, eIndex + 1);
        const text: string = variableObjLocal[variable] || '(已缺失)';
        const hasError: boolean = variableObjLocal[variable] === undefined;

        cm?.markText(
          {
            line: line,
            ch: sIndex,
          },
          {
            line: line,
            ch: eIndex + 1,
          },
          {
            replacedWith: domTagGen(variable, text, hasError ? 'red' : '#026be1'),
          },
        );
        codeArr[line] = codeArr[line].replace(variable, strFill(0, variable.length, 0));

        variReplace(cm, line);
      };

      const funReplace = (cm: any, line: number, funStr: string) => {
        const sIndex = codeArr[line].indexOf(funStr);
        const eIndex = sIndex + funStr.length;
        if (sIndex === -1) {
          return;
        }
        const variable = codeArr[line].slice(sIndex, eIndex);
        const text: string = funObjSimple[variable] || '(已缺失)';
        const hasError: boolean = funObjSimple[variable] === undefined;

        cm?.markText(
          {
            line: line,
            ch: sIndex,
          },
          {
            line: line,
            ch: eIndex,
          },
          {
            replacedWith: domTagGen(variable, text, hasError ? 'red' : '#026be1'),
          },
        );
        codeArr[line] = codeArr[line].replace(variable, strFill(0, variable.length, 0));

        funReplace(cm, line, funStr);
      };

      for (let i = 0; i < editor.doc.size; i++) {
        variReplace(editor, i);
        if (Boolean(isUseFun)) {
          funObjsSort.map((v) => funReplace(editor, i, v.title));
        }
      }
    },
  };

  useEffect(() => {
    if (!codeEditor || !newVariable) {
      return;
    }

    setVariableObjLocal(() => {
      return Object.assign(variableObjLocal, {
        [newVariable.key]: newVariable.name,
      });
    });

    cmUtils.insert(codeEditor, newVariable.key, newVariable.type === 'fun');
  }, [newVariable, codeEditor]);

  useEffect(() => {
    // 消息提醒 新增模板时  触发方式变为"定时触发"时 需要将消息内容中的表单字段值清除
    if (resetValue) {
      setValueLocal(resetValue);
    }
    // console.log('--resetValue--',resetValue)
  }, [resetValue]);

  const onValueChange = (editor: any, data: any, val: string) => {
    setCodeEditor(editor);
    cmUtils.variableRender(editor);

    // TODO 默认值bug修复 暂时注释
    // const isSame = isEqual(valueLocal, val);
    // if (isSame) {
    //   return;
    // }

    setIsInitDone(true);
    onChange(val);
  };

  useEffect(() => {
    if (variableObjLocal) {
      cmUtils.variableRender(codeEditor);
    }
  }, [variableObjLocal]);

  // 字符串反转
  const strReverse = (str: string) => {
    return str.split('').reverse().join('');
  };

  /**
   * 查找编辑器光标所对应函数,设置其函数说明信息
   * todo 待优化,规则不够完善
   * @param editor
   */
  const findFocusFun = (editor: any) => {
    const cursor = editor.getCursor();
    const lineStr = editor.getLine(cursor.line);
    const lineStrPart = lineStr.substring(0, cursor.ch);
    const lineStrPartRev = strReverse(lineStrPart);

    let flag: boolean = false;
    let funName: string = '';

    forEach(funObjsSort, (v) => {
      if (flag) {
        return;
      }
      const i = (lineStrPartRev || '').indexOf(strReverse(v.title));
      if (i > -1) {
        flag = true;
        funName = v.title;
        return;
      }
    });
    return funName;
  };

  const handleClear = () => {
    // 清除组件内容
    codeEditor.doc.setValue('');
  };

  return (
    //这里className中的qx-copy-send-cm 是抄送节点--消息内容专用的样式,如果有必要,可将className回滚设置为'qx-formula-cm '  + className
    <div
      className={
        from === 'copySend' ? 'qx-copy-send-cm ' + className : 'qx-formula-cm ' + className
      }
      style={{ height: '100%', padding: 0 }}
    >
      <CodeMirror
        value={(valueLocal || '').toString()}
        editorDidMount={(editor) => setCodeEditor(editor)}
        // onCursorActivity={(e) => console.log('e', e)}
        // onCursorActivity={(e) => e?.showHint()} //没有会报错
        options={{
          // mode: 'text/html',
          lineNumbers: false,
          autofocus,
          readOnly: Boolean(readOnly),
          cursorHeight: Boolean(readOnly) ? 0 : 'auto',
          // 滚动(false,默认)或自动换行
          lineWrapping: true,
        }}
        onChange={debounce(onValueChange, 300)}
        onCursor={(editor) => {
          const funName = findFocusFun(editor);
          setFocusFun(funName);
        }}
      />
      {allowClear && value && (
        <CloseCircleFilled className={'qx-field-setter__clear'} onClick={handleClear} />
      )}
    </div>
  );
};

export default CodeEditor;