Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
miniEditor.js 7.18 KiB
import {
  ProseEditor,
  Toolbar
} from 'substance'

import ContainerEditor from '../ContainerEditor'
import Comments from '../panes/Comments/CommentBoxList'
import CommentsProvider from '../panes/Comments/CommentsProvider'
import TrackChangesProvider from '../elements/track_change/TrackChangesProvider'
import SimpleExporter from '../SimpleEditorExporter'
import _ from 'lodash'

class MiniEditor extends ProseEditor {

  didMount () {
    const provider = this.getProvider()
    provider.config.miniEditorContext = this.getChildContext()
    this.editorSession.onUpdate('document', this.findNote, this)
    this.on('noteSelected', this.scrollTo, this)
    this.on('notes:inserted', this.createNote, this)
    this.on('notes:deleted', this.removeNote, this)
  }

  render ($$) {
    const el = $$('div').addClass('sc-mini-editor')
    let toolbar = this._renderMiniToolbar($$)
    let editor = this._renderEditor($$)
    let SplitPane = this.componentRegistry.get('split-pane')
    let ScrollPane = this.componentRegistry.get('scroll-pane')
    let Overlay = this.componentRegistry.get('overlay')

    var commentsPane = $$(Comments, {
      comments: this.props.comments,
      fragment: this.props.fragment,
      update: this.props.update,
      user: this.props.user
    }).addClass('sc-comments-pane')

    const editorWithComments = $$(SplitPane, { sizeA: '100%', splitType: 'vertical' })
      .append(
        editor,
        commentsPane
      )

    const contentPanel = $$(ScrollPane, {
      name: 'miniEditorContentPanel',
      scrollbarPosition: 'right'
    })
      .append(editorWithComments, $$(Overlay))
      .attr('id', 'mini-editor-content-panel')
      .ref('miniEditorContentPanel')

    el.append(
      $$(SplitPane, { splitType: 'horizontal' })
        .append(toolbar, contentPanel)
    )

    return el
  }

  _renderMiniToolbar ($$) {
    let commandStates = this.commandManager.getCommandStates()
    return $$('div').addClass('se-toolbar-wrapper').append(
      $$(Toolbar, {
        commandStates: commandStates,
        toolGroups: ['annotations']
      }).ref('mini_toolbar')
    )
  }
  _renderEditor ($$) {
    return $$(ContainerEditor, {
      book: this.props.book,
      comments: this.props.comments,
      containerId: this.props.containerId,
      configurator: this.props.configurator,
      editorSession: this.editorSession,
      disabled: this.props.disabled,
      history: this.props.history,
      fragment: this.props.fragment,
      spellcheck: 'native',
      trackChanges: this.props.trackChanges,
      user: this.props.user
    }).ref('mini_body')
  }

  scrollTo (nodeId) {
    const nodes = this.getIsolatedNodes()
    const note = _.find(nodes, function (c) {
      return c.parentNoteId === nodeId
    })

    this.refs.miniEditorContentPanel.scrollTo(note.id)
  }

  createNodeData (note) {
    return {
      'type': 'isolated-note',
      'content': note['note-content'],
      'parentNoteId': note.id
    }
  }

  createNote (noteId) {
    const provider = this.getProvider()
    const notes = provider.computeEntries()
    const surface = this.getSurface()
    const container = surface.getContainer()

    let note = _.find(notes, function (c) {
      return c.id === noteId
    })

    let findIndex = _.findIndex(notes, ['id', note.id])
    let nodeData = this.createNodeData(note)

    this.editorSession.transaction(function (tx) {
      let blockNode = tx.create(nodeData)
      tx.update(container.getContentPath(), { type: 'insert', pos: findIndex, value: blockNode.id })
    })

    const existingNodes = this.getIsolatedNodes()
    let isolatedNote = _.find(existingNodes, function (c) {
      return c.parentNoteId === noteId
    })
    this.saveNote(isolatedNote)
  }

  removeNote (noteId) {
    const existingNodes = this.getIsolatedNodes()
    const surface = this.getSurface()
    const container = surface.getContainer()

    let note = _.find(existingNodes, function (c) {
      return c.parentNoteId === noteId
    })

    // if Isolated note is focused cannot be deleted. Remove focus
    // before deleting callout
    this.editorSession.setSelection(null)

    // Delete comments from the isolated note before
    // removing the note
    this.removeCommentsFirst(note.id)

    let findIndex = _.findIndex(existingNodes, ['parentNoteId', noteId])

    this.editorSession.transaction(function (tx) {
      tx.update(container.getContentPath(), { type: 'delete', pos: findIndex })
      tx.delete(note.id)
    })
  }

  saveNote (isolatedNote) {
    // If isloated note has no content and you keep pressing backspace,
    // it gets deleted. Set selection to null to prevent that
    const selection = this.editorSession.getSelection()
    if (selection.start && selection.start.offset === 0 && selection.end.offset === 0) {
      this.editorSession.setSelection(null)
    }

    const exporter = new SimpleExporter(this.props.configurator.config)
    const convertedNode = exporter.convertNode(isolatedNote)
    this.context.editorSession.transaction(function (tx, args) {
      const path = [isolatedNote.parentNoteId, 'note-content']
      tx.set(path, convertedNode.outerHTML)
    })
  }

  findNote () {
    const selection = this.editorSession.getSelection()
    if (!selection.end) return

    const isolatedNoteId = selection.end.path[0]
    const isolatedNote = this.editorSession.document.get(isolatedNoteId)
    return this.saveNote(isolatedNote)
  }

  getIsolatedNodes () {
    const doc = this.editorSession.document
    const nodes = doc.getNodes()
    const entries = _.pickBy(nodes, function (value, key) {
      return value.type === 'isolated-note'
    })
    return _.values(entries)
  }

  removeCommentsFirst (noteId) {
    const doc = this.editorSession.document
    const nodes = doc.getNodes()

    const entries = _.pickBy(nodes, function (value, key) {
      return value.type === 'comment'
    })

    if (!entries) return

    _.forEach(entries, function (node, key) {
      if (node.path[0] === noteId) {
        this.editorSession.transaction(function (tx) {
          tx.delete(node.id)
        })
      }
    }.bind(this))
  }

  getProvider () {
    return this.context.notesProvider
  }
  getSurface () {
    const containerId = this.props.containerId
    const provider = this.getProvider()
    return provider.config.miniEditorContext.surfaceManager.getSurface(containerId)
  }

  getChildContext () {
    const oldContext = super.getChildContext()
    const doc = this.doc

    // comments provider
    const commentsProvider = new CommentsProvider(doc, {
      commandManager: this.commandManager,
      comments: this.props.fragment.comments,
      containerId: this.props.containerId,
      controller: this,
      editorSession: this.editorSession,
      fragment: this.props.fragment,
      surfaceManager: this.surfaceManager,
      update: this.props.update
    })

    const trackChangesProvider = new TrackChangesProvider(doc, {
      commandManager: this.commandManager,
      containerId: this.props.containerId,
      controller: this,
      editorSession: this.editorSession,
      surfaceManager: this.surfaceManager,
      user: this.props.user
    })

    // attach all to context
    return {
      ...oldContext,
      commentsProvider,
      trackChangesProvider
    }
  }
}

export default MiniEditor