petitviolet_blog

@petitviolet blog

ReactからAceEditorを使ってリッチなエディタを表示する

これはなに

リッチなエディタとしてajaxorg/aceというものがあり、Reactから使おうとするとsecuringsincity/react-aceが便利。

react-aceが提供するコンポーネントをラップするような独自のコンポーネントを実装する。

具体的には、以下の三点。

  • ファイルタイプに応じて動的にrequireする
  • React hooksを使う
  • readonlyモードを用意する

実装

大した量でもないので実装を貼り付ける

import * as React from 'react';
import AceEditor from 'react-ace';

// AceEditor周りで使うデータ
export type EditorProps = {
  fileType: string;
  contents: string;
  readOnly: boolean;
  onChange?: (content: string) => void;
  theme?: string;
};

const MIN_LINE = 10;
const MAX_LINE = 50;

const DEFAULT_THEME = 'monokai';

export const EditorComponent = (props: EditorProps) => {
  const theme = props.theme || DEFAULT_THEME;

  // load theme
  React.useMemo(() => {
    try {
      require(`ace-builds/src-noconflict/theme-${theme}`);
    } catch (e) {
      console.log(`error new theme(${theme}): ${e}`);
    }
  }, [props.theme]);

  // load mode
  React.useMemo(() => {
    if (props.fileType == null) {
      return;
    }
    try {
      require(`ace-builds/src-noconflict/mode-${props.fileType}`);
    } catch (e) {
      console.log(`error new mode(${props.fileType}): ${e}`);
    }
  }, [props.fileType]);

  const onChange: (string) => void = props.onChange ? props.onChange : () => {};

  if (props.readOnly) {
    return (
      <AceEditor
        mode={props.fileType}
        theme={theme}
        value={props.contents}
        width={null} // nullにしておくとwidthを外から変えられる
        minLines={MIN_LINE}
        maxLines={MAX_LINE}
        readOnly={true}
        focus={false}
        highlightActiveLine={false}
        enableBasicAutocompletion={false}
        onLoad={editor => {
          // readOnlyでも表示されるカーソルを消す
          editor.renderer.$cursorLayer.element.style.opacity = 0;
        }}
      />
    );
  } else {
    return (
      <AceEditor
        mode={props.fileType}
        theme={theme}
        value={props.contents}
        width={null}
        minLines={MIN_LINE}
        maxLines={MAX_LINE}
        readOnly={props.readOnly}
        enableBasicAutocompletion={true}
        onChange={onChange}
      />
    );
  }
};

動的なtheme, modeの読み込み

aceで用意されているthemeやmodeはここから見ることが出来る。 https://github.com/ajaxorg/ace-builds/tree/master/src-min-noconflict

本当にこれでいいのかはよくわからんが、ace-builds/src-noconflict/配下をrequireすることで動的に読み込み出来ている様子。

そしてtheme, modeの読み込みにはReact.useMemoを使用しているが、初期化が遅いとかではなく副作用を起こすようなものなのだからReact.useEffectの方が適切では?と思うかもしれないが、実行タイミングが異なり、useMemoは同期的だがuseEffectコンポーネントの初期化が終わったあとに実行される。
ドキュメントにもあるように従来のcomponentDidMountと同じに思えばよい。 そのためAceEditorコンポーネントレンダリングする前にthemeやmodeは読み込み終わっていてほしいため、useEffectだとタイミングが遅すぎるため、useMemo(あるいはuseContext)の方が適切となる。

readonlyモード

readOnlytrueを与えればreadonlyにはなるが、それは単に更新は出来ないだけでカーソルはフォーカス出来てしまう。 そこでonLoadでstyleを弄って透明にしてしまうことで見た目上はフォーカスされていないように見せかけることで対応。

readOnly={true}
focus={false}
highlightActiveLine={false}
onLoad={editor => {
  // readOnlyでも表示されるカーソルを消す
  editor.renderer.$cursorLayer.element.style.opacity = 0;
}}

随分古いissueのコメントを参考にした。 Option to make editor disabled · Issue #266 · ajaxorg/ace