index.js 3.94 KB
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as React from "react";
import { useEffect, useRef, SFC } from "react";
import { useConfig } from "../../../../docz-lib/docz/dist";
import { Controlled as BaseCodeMirror } from "react-codemirror2";
import PerfectScrollbar from "react-perfect-scrollbar";
import styled from "styled-components";

import { get } from "~utils/theme";

import { ScrollbarStyles } from "./ps-scrollbar";
import * as themes from "./themes";

import "codemirror/mode/markdown/markdown";
import "codemirror/mode/javascript/javascript";
import "codemirror/mode/jsx/jsx";
import "codemirror/mode/css/css";
import "codemirror/addon/edit/matchbrackets";
import "codemirror/addon/edit/closetag";
import "codemirror/addon/fold/xml-fold";

const Scrollbar = styled(PerfectScrollbar)`
  overflow: auto;
  position: relative;
  max-height: ${p => 25 * p.linesToScroll}px;

  .ps__rail-y {
    z-index: 9;
    opacity: 0.4;
  }
`;

const preStyles = get("styles.pre");
const EditorStyled = styled(BaseCodeMirror)`
  ${themes.dark};
  ${themes.light};
  position: relative;
  flex: 1;

  .CodeMirror {
    max-width: 100%;
    height: 100%;
  }

  .CodeMirror-gutters {
    left: 1px !important;
  }

  .CodeMirror-lines {
    padding: 10px 0;
    box-sizing: content-box;
  }

  .CodeMirror-line {
    padding: 0 10px;
  }

  .CodeMirror-linenumber {
    padding: 0 7px 0 5px;
  }

  &,
  .CodeMirror pre {
    ${preStyles};
  }
`;

const scrollbarOpts = {
  wheelSpeed: 2,
  wheelPropagation: true,
  minScrollbarLength: 20,
  suppressScrollX: true
};

const noCurrent = (val: any) => !val || !val.current;

const CodeMirror: SFC<any> = props => {
  const { themeConfig } = useConfig();
  const editor = useRef < any > null;
  const forceUpdateEditorTimeout = useRef(0);
  const previousEditor = useRef(0);
  const linesToScroll = themeConfig.linesToScrollEditor || 14;

  const editorProps = {
    ...props,
    editorDidMount: (codemirror: any) => {
      props.editorDidMount && props.editorDidMount(codemirror);
      editor.current = codemirror;
    }
  };

  const refreshCodeMirror = () => {
    if (noCurrent(editor)) return;
    editor.current.refresh();
  };

  const clearForceUpdateCodeMirror = () => {
    if (noCurrent(forceUpdateEditorTimeout)) return;
    clearTimeout(forceUpdateEditorTimeout.current);
  };

  const forceUpdateCodeMirror = () => {
    if (noCurrent(editor)) return;
    clearForceUpdateCodeMirror();

    forceUpdateEditorTimeout.current = setTimeout(() => {
      const currentHeight = editor.current.getScrollInfo().height || 0;
      const hasNoHeight = currentHeight <= 0;

      // Don't refresh if no height (CodeMirror is not visible) or
      // Don't refresh if same height
      if (hasNoHeight || previousEditor === currentHeight) return;
      refreshCodeMirror();
      previousEditor.current = editor.current.getScrollInfo().height || 0;
    });
  };

  useEffect(() => {
    forceUpdateCodeMirror();
    return () => clearForceUpdateCodeMirror();
  }, []);

  return (
    <React.Fragment>
      <ScrollbarStyles />
      <Scrollbar options={scrollbarOpts} linesToScroll={linesToScroll}>
        <EditorStyled {...editorProps} />
      </Scrollbar>
    </React.Fragment>
  );
};

export default CodeMirror;