<commit_before>/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {bool, func} from 'prop-types'
import React from 'react'
import RichContentEditor from '@canvas/rce/RichContentEditor'
import {Submission} from '@canvas/assignments/graphql/student/Submission'
import LoadingIndicator from '@canvas/loading-indicator'

// This is the interval (in ms) at which we check for changes to the text
// content
const checkForChangesIntervalMS = 250

// This is how long we wait to see that changes have stopped before actually
// saving the draft
const saveDraftDelayMS = 1000

export default class TextEntry extends React.Component {
  static propTypes = {
    createSubmissionDraft: func,
    onContentsChanged: func,
    submission: Submission.shape,
    readOnly: bool
  }

  state = {
    editorLoaded: false,
    renderingEditor: false
  }

  _isMounted = false

  _saveDraftTimer = null

  _checkForChangesTimer = null

  _lastSavedContent = null

  getDraftBody = () => {
    const {submission} = this.props
    if (['graded', 'submitted'].includes(submission.state)) {
      // If this attempt has been submitted/graded, use it
      return submission.body
    } else if (submission.submissionDraft != null) {
      // If a draft object exists, get the submission contents from it
      return submission.submissionDraft.body
    }

    // If the submission is marked as unsubmitted and there's no draft object,
    // the user has started a new attempt but not entered any text, so return
    // an empty string. The body attribute may contain the contents of a
    // previous attempt, which we don't want.
    return ''
  }

  componentDidMount() {
    this._isMounted = true
    window.addEventListener('beforeunload', this.beforeunload.bind(this))

    if (this.getDraftBody() != null && !this.state.editorLoaded) {
      this.loadRCE()
    }
  }

  componentDidUpdate(prevProps) {
    if (this._tinyeditor == null) {
      return
    }

    if (this.props.readOnly !== prevProps.readOnly) {
      this._tinyeditor.mode.set(this.props.readOnly ? 'readonly' : 'design')
    }

    if (this.props.submission.attempt !== prevProps.submission.attempt) {
      const body = this.getDraftBody()
      this._tinyeditor.setContent(body)
      this._lastSavedContent = body

      if (!this.props.readOnly) {
        this.handleEditorIframeFocus()
        this.handleEditorFocus()
      }
    }
  }

  componentWillUnmount() {
    this._isMounted = false
    window.removeEventListener('beforeunload', this.beforeunload.bind(this))

    if (this.state.editorLoaded) {
      this.unloadRCE()
    }
  }

  beforeunload(e) {
    if (this.state.editorLoaded && this.getDraftBody() !== this.getRCEText()) {
      e.preventDefault()
      e.returnValue = true
    }
  }

  // Note: I believe there's a bug in tinymce, that
  // if you set focus:true to give the editor focus on init,
  // then the internal bookkeeping doesn't know it has focus
  // and it does not handle the focusout event correctly.
  // Start w/o focus, then give it focus after initialization
  // in this.handleRCEInit
  loadRCE() {
    this.setState({editorLoaded: true, renderingEditor: true}, () => {
      RichContentEditor.loadNewEditor(this._textareaRef, {
        focus: false,
        manageParent: false,
        tinyOptions: {
          init_instance_callback: this.handleRCEInit,
          height: 300<commit_msg>Remove RCE confirm-unsaved-changes behavior<commit_after>/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {bool, func} from 'prop-types'
import React from 'react'
import RichContentEditor from '@canvas/rce/RichContentEditor'
import {Submission} from '@canvas/assignments/graphql/student/Submission'
import LoadingIndicator from '@canvas/loading-indicator'

// This is the interval (in ms) at which we check for changes to the text
// content
const checkForChangesIntervalMS = 250

// This is how long we wait to see that changes have stopped before actually
// saving the draft
const saveDraftDelayMS = 1000

export default class TextEntry extends React.Component {
  static propTypes = {
    createSubmissionDraft: func,
    onContentsChanged: func,
    submission: Submission.shape,
    readOnly: bool
  }

  state = {
    editorLoaded: false,
    renderingEditor: false
  }

  _isMounted = false

  _saveDraftTimer = null

  _checkForChangesTimer = null

  _lastSavedContent = null

  getDraftBody = () => {
    const {submission} = this.props
    if (['graded', 'submitted'].includes(submission.state)) {
      // If this attempt has been submitted/graded, use it
      return submission.body
    } else if (submission.submissionDraft != null) {
      // If a draft object exists, get the submission contents from it
      return submission.submissionDraft.body
    }

    // If the submission is marked as unsubmitted and there's no draft object,
    // the user has started a new attempt but not entered any text, so return
    // an empty string. The body attribute may contain the contents of a
    // previous attempt, which we don't want.
    return ''
  }

  componentDidMount() {
    this._isMounted = true

    if (this.getDraftBody() != null && !this.state.editorLoaded) {
      this.loadRCE()
    }
  }

  componentDidUpdate(prevProps) {
    if (this._tinyeditor == null) {
      return
    }

    if (this.props.readOnly !== prevProps.readOnly) {
      this._tinyeditor.mode.set(this.props.readOnly ? 'readonly' : 'design')
    }

    if (this.props.submission.attempt !== prevProps.submission.attempt) {
      const body = this.getDraftBody()
      this._tinyeditor.setContent(body)
      this._lastSavedContent = body

      if (!this.props.readOnly) {
        this.handleEditorIframeFocus()
        this.handleEditorFocus()
      }
    }
  }

  componentWillUnmount() {
    this._isMounted = false

    if (this.state.editorLoaded) {
      this.unloadRCE()
    }
  }

  // Note: I believe there's a bug in tinymce, that
  // if you set focus:true to give the editor focus on init,
  // then the internal bookkeeping doesn't know it has focus
  // and it does not handle the focusout event correctly.
  // Start w/o focus, then give it focus after initialization
  // in this.handleRCEInit
  loadRCE() {
    this.setState({editorLoaded: true, renderingEditor: true}, () => {
      RichContentEditor.loadNewEditor(this._textareaRef, {
        focus: false,
        manageParent: false,
        tinyOptions: {
          init_instance_callback: this.handleRCEInit,
          height: 300
{'fraction_non_alphanumeric': 0.09074596257369905, 'fraction_numerical': 0.003845167905665214, 'mean_word_length': 3.115506329113924, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, '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': '5015163', 'n_tokens_mistral': 2262, 'n_tokens_neox': 2187, 'n_words': 764}