<commit_before>import * as React from 'react';
import { Link } from 'react-router';
import { ToolbarComponent } from './toolbar';
import { IMarkdownEntry } from '../../../../../model/trainer/markdownEntry';
import { PanelComponent, PanelItem } from '../../../../../common/components';
import { TextEditorComponent } from '../components/textEditor';
import { MarkDownViewerComponent } from '../../../../../common/components/markdownViewer';
import { panelList } from './panels';
import { SOURCE_LINE_CLASSNAME,
  mapLineToOffset,
  mapOffsetToLine } from '../../../../../common/components/markdownViewer';
import debounce from 'lodash.debounce';

const styles: any = require('./editorStyles.scss');

interface Props {
  content: string;
  cursorStartPosition: number;
  shouldUpdateEditorCursor: boolean;
  className: string;
  showPreview: boolean;
  activePanelId: string;
  onContentChange: (value: string) => void;
  updateEditorCursor: (cursorStartPosition: number) => void;
  togglePreviewMode: () => void;
  setActivePanelId: (panelId: string) => void;
}

// Local state.
interface State {
  mdEntry: IMarkdownEntry;
  editorLineHeight: number;
}

export class EditorComponent extends React.Component<Props, State> {
  constructor() {
    super();

    this.state = {
      mdEntry: {mdCaret: '', caretCursorPosition: 0},
      editorLineHeight: 20,
    };
  }

  private nodeRefPreview: HTMLElement = null;
  private nodeRefEditor: HTMLElement = null;

  private setNodeRefEditor = (ref) => {
    this.nodeRefEditor = ref;
    if (ref) {
      this.nodeRefEditor.onscroll = this.handleScrollEditor;
    }
  }

  private setNodeRefPreview = (ref) => {
    this.nodeRefPreview = ref;
    if (ref) {
      this.nodeRefPreview.onscroll = this.handleScrollPreview;
    }
  }

  private syncScrollEnabled = () => {
    return this.nodeRefEditor && this.nodeRefPreview;
  }

  private handlePanel(panelId) {
    if (panelId !== this.props.activePanelId) {
      this.props.setActivePanelId(panelId);
    } else {
      this.props.setActivePanelId('');
    }
  }

  private insertMarkdownEntry = (markdownEntry: IMarkdownEntry) => {
    if (markdownEntry.panelId && markdownEntry.panelId !== '') {
      this.handlePanel(markdownEntry.panelId);
    } else {
      this.setState({
        ...this.state,
        mdEntry: markdownEntry,
      });
    }
  }

  private mapLineToEditorOffset = (line) => {
    return line * this.state.editorLineHeight;
  }

  private mapEditorOffsetToLine = () => {
    return this.nodeRefEditor.scrollTop / this.state.editorLineHeight;
  }

  private getPreviewPosition = () => {
    const componentPosition = this.nodeRefPreview.getBoundingClientRect().top;
    return componentPosition > 0 ? componentPosition : 0;
  }

  private mapLineToPreviewOffset = (line) => {
    const elements = this.nodeRefPreview.getElementsByClassName(SOURCE_LINE_CLASSNAME);
    return mapLineToOffset(elements, line) - this.getPreviewPosition();
  }

  private mapPreviewOffsetToLine = () => {
    const elements = this.nodeRefPreview.getElementsByClassName(SOURCE_LINE_CLASSNAME);
    return mapOffsetToLine(elements,  this.getPreviewPosition());
  }

  private pauseEditorScrollEvent = debounce(() => {
    this.nodeRefEditor.onscroll = this.nodeRefEditor.onscroll ? null : this.handleScrollEditor;
  }, 250, {leading: true, trailing: true});

  private pausePreviewScrollEvent = debounce(() => {
    this.nodeRefPreview.onscroll = this.nodeRefPreview.onscroll ? null : this.handleScrollPreview;
  }, 250, {leading: true, trailing: true});

  private handleScrollEditor = () => {
    if (!this.syncScrollEnabled()) { return; }
    this.pausePreviewScrollEvent(); this.pausePreviewScrollEvent(); // Call it twice [1]
    window.requestAnimationFrame(() => {
      const line = this.mapEditorOffsetToLine();
      this.nodeRefPreview.scrollTop += this.mapLineToPreviewOffset(line);
    });
  }

  private handleScrollPreview = () => {
    if (!this.syncScrollEnabled()) { return; }
    this.pauseEditorScrollEvent(); this.pauseEditorScrollEvent(); // Call it twice [1]
    window.requestAnimationFrame(() => {
      const line = this.mapPreviewOffsetToLine();
      this.nodeRefEditor.scrollTop = this.mapLineToEditorOffset(line);
    });
  }

  private handleOnLineHeightChange = (lineHeight) => {
    this.setState({
      ...this.state,
      editorLineHeight: lineHeight,
    });
  }

  public render() {
    return (
      <div className={this.props.className}>
        <ToolbarComponent
          insertMarkdownEntry={this.insertMarkdownEntry}
          togglePreviewMode={this.props.togglePreviewMode}
        />
        <div className={styles.editorContainer}>
          <PanelComponent activePanelId={this.props.activePanelId} panelList={panelList} />
          <div className={styles.markdownArea}>
            <TextEditorComponent registerRef={this.setNodeRefEditor}
              className={styles.textArea}
              content={this.props.content}
              cursorStartPosition={this.props.cursorStartPosition}
              shouldUpdateEditorCursor={this.props.shouldUpdateEditorCursor}
              onContentChange={this.props.onContentChange}
              updateEditorCursor={this.props.updateEditorCursor}
              onLineHeightChange={this.handleOnLineHeightChange}
              markdownEntry={this.state.mdEntry}
            />
            {
              this.props.showPreview === true ?
                <MarkDownViewerComponent registerRef={this.setNodeRefPreview}
                  className={styles.previewArea}
                  content={this.props.content}
                />
              : null
            }
          </div>
        </div>
      </div>
    );
  }
}

// [1]. This is not the most elegant solution but it is effective. As an alternative
// we can avoid the double call by implementing a custom debounce function with leading
// and trailing edge also for a single call. Lodash does not perform the trailing edge
// when only a single call to the debounced function has been made.<commit_msg>A better approach to avoid calling debounced function twice<commit_after>import * as React from 'react';
import { Link } from 'react-router';
import { ToolbarComponent } from './toolbar';
import { IMarkdownEntry } from '../../../../../model/trainer/markdownEntry';
import { PanelComponent, PanelItem } from '../../../../../common/components';
import { TextEditorComponent } from '../components/textEditor';
import { MarkDownViewerComponent } from '../../../../../common/components/markdownViewer';
import { panelList } from './panels';
import { SOURCE_LINE_CLASSNAME,
  mapLineToOffset,
  mapOffsetToLine } from '../../../../../common/components/markdownViewer';
import debounce from 'lodash.debounce';

const styles: any = require('./editorStyles.scss');

interface Props {
  content: string;
  cursorStartPosition: number;
  shouldUpdateEditorCursor: boolean;
  className: string;
  showPreview: boolean;
  activePanelId: string;
  onContentChange: (value: string) => void;
  updateEditorCursor: (cursorStartPosition: number) => void;
  togglePreviewMode: () => void;
  setActivePanelId: (panelId: string) => void;
}

// Local state.
interface State {
  mdEntry: IMarkdownEntry;
  editorLineHeight: number;
}

export class EditorComponent extends React.Component<Props, State> {
  constructor() {
    super();

    this.state = {
      mdEntry: {mdCaret: '', caretCursorPosition: 0},
      editorLineHeight: 20,
    };
  }

  private nodeRefPreview: HTMLElement = null;
  private nodeRefEditor: HTMLElement = null;

  private setNodeRefEditor = (ref) => {
    this.nodeRefEditor = ref;
    if (ref) {
      this.nodeRefEditor.onscroll = this.handleScrollEditor;
    }
  }

  private setNodeRefPreview = (ref) => {
    this.nodeRefPreview = ref;
    if (ref) {
      this.nodeRefPreview.onscroll = this.handleScrollPreview;
    }
  }

  private syncScrollEnabled = () => {
    return this.nodeRefEditor && this.nodeRefPreview;
  }

  private handlePanel(panelId) {
    if (panelId !== this.props.activePanelId) {
      this.props.setActivePanelId(panelId);
    } else {
      this.props.setActivePanelId('');
    }
  }

  private insertMarkdownEntry = (markdownEntry: IMarkdownEntry) => {
    if (markdownEntry.panelId && markdownEntry.panelId !== '') {
      this.handlePanel(markdownEntry.panelId);
    } else {
      this.setState({
        ...this.state,
        mdEntry: markdownEntry,
      });
    }
  }

  private mapLineToEditorOffset = (line) => {
    return line * this.state.editorLineHeight;
  }

  private mapEditorOffsetToLine = () => {
    return this.nodeRefEditor.scrollTop / this.state.editorLineHeight;
  }

  private getPreviewPosition = () => {
    const componentPosition = this.nodeRefPreview.getBoundingClientRect().top;
    return componentPosition > 0 ? componentPosition : 0;
  }

  private mapLineToPreviewOffset = (line) => {
    const elements = this.nodeRefPreview.getElementsByClassName(SOURCE_LINE_CLASSNAME);
    return mapLineToOffset(elements, line) - this.getPreviewPosition();
  }

  private mapPreviewOffsetToLine = () => {
    const elements = this.nodeRefPreview.getElementsByClassName(SOURCE_LINE_CLASSNAME);
    return mapOffsetToLine(elements,  this.getPreviewPosition());
  }

  private attachEditorScrollEvent = debounce(() => {
    this.nodeRefEditor.onscroll = this.handleScrollEditor;
  }, 250, {leading: false, trailing: true});

  private attachPreviewScrollEvent = debounce(() => {
    this.nodeRefPreview.onscroll = this.handleScrollPreview;
  }, 250, {leading: false, trailing: true});

  private handleScrollEditor = () => {
    if (!this.syncScrollEnabled()) { return; }
    window.requestAnimationFrame(() => {
      const line = this.mapEditorOffsetToLine();
      this.nodeRefPreview.onscroll = null;
      this.nodeRefPreview.scrollTop += this.mapLineToPreviewOffset(line);
      this.attachPreviewScrollEvent();
    });
  }

  private handleScrollPreview = () => {
    if (!this.syncScrollEnabled()) { return; }
    window.requestAnimationFrame(() => {
      const line = this.mapPreviewOffsetToLine();
      this.nodeRefEditor.onscroll = null;
      this.nodeRefEditor.scrollTop = this.mapLineToEditorOffset(line);
      this.attachEditorScrollEvent();
    });
  }

  private handleOnLineHeightChange = (lineHeight) => {
    this.setState({
      ...this.state,
      editorLineHeight: lineHeight,
    });
  }

  public render() {
    return (
      <div className={this.props.className}>
        <ToolbarComponent
          insertMarkdownEntry={this.insertMarkdownEntry}
          togglePreviewMode={this.props.togglePreviewMode}
        />
        <div className={styles.editorContainer}>
          <PanelComponent activePanelId={this.props.activePanelId} panelList={panelList} />
          <div className={styles.markdownArea}>
            <TextEditorComponent registerRef={this.setNodeRefEditor}
              className={styles.textArea}
              content={this.props.content}
              cursorStartPosition={this.props.cursorStartPosition}
              shouldUpdateEditorCursor={this.props.shouldUpdateEditorCursor}
              onContentChange={this.props.onContentChange}
              updateEditorCursor={this.props.updateEditorCursor}
              onLineHeightChange={this.handleOnLineHeightChange}
              markdownEntry={this.state.mdEntry}
            />
            {
              this.props.showPreview === true ?
                <MarkDownViewerComponent registerRef={this.setNodeRefPreview}
                  className={styles.previewArea}
                  content={this.props.content}
                />
              : null
            }
          </div>
        </div>
      </div>
    );
  }
}
{'fraction_non_alphanumeric': 0.12353241449719245, 'fraction_numerical': 0.002126935511315297, 'mean_word_length': 3.118780658724597, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2398737', 'n_tokens_mistral': 3305, 'n_tokens_neox': 3248, 'n_words': 633}