Skip to content
Snippets Groups Projects
TrackChangesProvider.js 7.87 KiB
Newer Older
john's avatar
john committed
import {
  filter,
  find,
  keys,
  last
} from 'lodash'

import {
  createAnnotation,
  expandAnnotation,
  truncateAnnotation
} from 'substance'

class TrackChangesProvider {
  constructor (config) {
    this.config = config
    this.config.documentSession.on('didUpdate', this.handleUndoRedo, this)
john's avatar
john committed
  handleTransaction (options) {
    this.handleAdd(options)
    this.handleDelete(options)
  }

  handleAdd (options) {
    const { event, status } = options
    if (status !== 'add') return

    const isSelectionCollapsed = this.isSelectionCollapsed()
    const mode = this.getMode()

    let selection

    if (isSelectionCollapsed) {
      this.insertText(event)
john's avatar
john committed
      if (mode) return
      selection = this.setSelectionPlusOne('left')
      this.createAddAnnotation(selection)
      this.moveCursorTo('end')
    } else {
      selection = this.getSelection()
      this.createDeleteAnnotation(selection)
      this.moveCursorTo('end')

      this.insertText(event)
      selection = this.setSelectionPlusOne('left')
      this.createAddAnnotation(selection)
      this.separateAnnotations()
      this.moveCursorTo('end')
      return
john's avatar
john committed
  }

  handleDelete (options) {
    const { move, status } = options
john's avatar
john committed
    if (status !== 'delete') return
john's avatar
john committed
    const mode = this.getMode()
    const isSelectionCollapsed = this.isSelectionCollapsed()

john's avatar
john committed
    let selection

    const direction = {
      cursorTo: (move === 'left') ? 'start' : 'end',
      move: move
    }

    if (isSelectionCollapsed) {
      if (!mode) {
        selection = this.setSelectionPlusOne(direction.move)
        this.createDeleteAnnotation(selection)
        this.moveCursorTo(direction.cursorTo)
        return
      }
      if (mode === 'delete') {
        selection = this.setSelectionPlusOne(direction.move)
        const annotation = this.getAnnotationByStatus('delete')
        this.expandTrackAnnotation(selection, annotation)
        this.moveCursorTo(direction.cursorTo)
        return
      }
    } else {
      selection = this.getSelection()
john's avatar
john committed
      this.createDeleteAnnotation(selection)
      this.moveCursorTo(direction.cursorTo)
john's avatar
john committed
  }

  createAddAnnotation (selection) {
    this.createTrackAnnotation(selection, 'add')
  }

  createDeleteAnnotation (selection) {
    this.createTrackAnnotation(selection, 'delete')
  }

  createTrackAnnotation (selection, status) {
    const surface = this.getSurface()

    surface.transaction((tx, args) => {
      const newNode = {
        selection: selection,
        node: {
          status: status,
          type: 'track-change'
        }
      }
      createAnnotation(tx, newNode)
    })
  }

  expandTrackAnnotation (selection, annotation) {
    const surface = this.getSurface()

    surface.transaction((tx, args) => {
      args.selection = selection
      args.anno = annotation

      expandAnnotation(tx, args)
    })
john's avatar
john committed
  separateAnnotations () {
    const surface = this.getSurface()
john's avatar
john committed
    const addAnnotation = this.getAnnotationByStatus('add')
    const deleteAnnotation = this.getAnnotationByStatus('delete')
john's avatar
john committed

    surface.transaction((tx, args) => {
      args.anno = deleteAnnotation
      args.selection = addAnnotation.getSelection()
      truncateAnnotation(tx, args)
    })
john's avatar
john committed
    const surface = this.getSurface()
    surface.transaction(function (tx, args) {
      if (surface.domSelection) surface.domSelection.clear()
      args.text = event.data
      return surface.insertText(tx, args)
    }, { action: 'type' })
john's avatar
john committed
  }

  /*

    ANNOTATION HELPERS

  */

  // is on left edge
  // is on right edge
  // is annotation from the same user

  // TODO -- handle multiple delete and add annotations
  getAnnotationByStatus (status) {
    const annotations = this.getAllExistingTrackAnnotations()
    console.log('annos', annotations)
    const annos = filter(annotations, (annotation) => {
      return annotation.status === status
    })
    return annos[0]
  }

  getExistingAnnotation () {
    const documentSession = this.getDocumentSession()
    const selectionState = documentSession.getSelectionState()
    const annotations = selectionState.getAnnotationsForType('track-change')
    console.log(annotations)
    return annotations[0]
  }

  getAllExistingTrackAnnotations () {
    const documentSession = this.getDocumentSession()
    const selectionState = documentSession.getSelectionState()
    const annotations = selectionState.getAnnotationsForType('track-change')
    return annotations
  }

  // returns whether the selection is on an add / delete tracked change
  isOnAnnotation (status) {
    const annotations = this.getAllExistingTrackAnnotations()
    const annotation = find(annotations, (annotation) => {
      return annotation.status === status
    })

    if (!annotation) return false
    return true
  }

  /**

    HISTORY HANDLERS

  */

  // TODO -- shouldn't run both all the time
  handleUndoRedo (update, info) {
    if (!info.replay) return
    // console.log(update)
    // console.log(info)
    this.handleUndo(update)
    // this.handleRedo(update)
  }

  handleUndo (update) {
    const deleted = update.change.deleted
    const deletedLength = keys(deleted).length

    // console.log(keys(deleted))

    if (deletedLength === 0) return
    if (deletedLength > 1) {
      return console.warn('FIXME: Multiple operations in track changes replay!')
    }

    const deletedOp = deleted[keys(deleted)[0]]
    if (!deletedOp.type === 'track-change') return

    const documentSession = this.getDocumentSession()
    documentSession.undo()
  handleRedo () {
    const documentSession = this.getDocumentSession()
    const undoneChanges = documentSession.undoneChanges
    const lastChange = last(undoneChanges)
    const op = last(lastChange.ops)

    const isTrack = op.path[0].split('-').slice(0, -1).join('-') === 'track-change'
    console.log(isTrack)
john's avatar
john committed
  /*

    SELECTION HANDLERS

  */

  // get delete direction
  // is part of the selection outside the annotation
  // move cursor to

  setSelectionPlusOne (direction) {
    const selection = this.getSelection()

    if (direction === 'left') selection.startOffset -= 1
    if (direction === 'right') selection.endOffset += 1

    return selection
  }

  moveCursorTo (point) {
    const selection = this.getSelection()
    const surface = this.getSurface()

    if (point === 'start') {
      selection.endOffset = selection.startOffset
    } else if (point === 'end') {
      selection.startOffset = selection.endOffset
    } else {
      selection.startOffset = point
      selection.endOffset = point
    }

    surface.setSelection(selection)
  }
  clearSelection (args, status) {
    const selection = args.selection
    if (
      status === 'add' ||
      (status === 'delete' && args.deleteCollapsed)
    ) {
      selection.startOffset = selection.endOffset
    } else if (status === 'delete') {
      selection.endOffset = selection.startOffset
    }
    return selection
  }

  createSelection (args) {
    const selection = args.selection
    selection.startOffset -= 1
    return selection
  }

john's avatar
john committed
  isSelectionCollapsed () {
    const selection = this.getSelection()
    const isCollapsed = selection.isCollapsed()
    return isCollapsed
  }

  /*

    GETTERS

  */

  getCommandManager () {
    return this.config.commandManager
  }

john's avatar
john committed
  getCommandStates () {
    const commandManager = this.getCommandManager()
john's avatar
john committed
    return commandManager.getCommandStates()
  }

  getDocumentSession () {
    return this.config.documentSession
  }

  getMode () {
    const state = this.getTrackState()
    return state.mode
  }

john's avatar
john committed
  getSelection () {
    const surface = this.getSurface()
    return surface.getSelection()
  }

  getSurface () {
    const surfaceManager = this.config.surfaceManager
    const id = this.config.containerId

    return surfaceManager.getSurface(id)
  }
john's avatar
john committed

  getTrackState () {
    const commandStates = this.getCommandStates()
    return commandStates['track-change']
  }
}

export default TrackChangesProvider