Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CommentBubble.js 3.67 KiB
import {
  createAnnotation,
  DefaultDOMElement,
  FontAwesomeIcon as Icon,
  Tool
} from 'substance'

class CommentBubble extends Tool {
  render ($$) {
    const mode = this.getMode()
    const title = 'Create a new comment'

    if (mode === 'delete') return $$('div')

    // console.log('bubble commands', commands.mode)
    const icon = $$(Icon, { icon: 'fa-comment' })
        .addClass('sc-comment-icon')

    const bubble = $$('div')
      .attr('title', title)
      .addClass('sc-overlay-bubble')
      .addClass('sc-overlay-bubble-hidden')
      .on('click', this.createComment)
      .append(icon)

    return bubble
  }

  // reset bubble position on window resize
  // only works on the horizontal level, as the vertical position gets
  // calculated relative to the overlay container, which gets positioned
  // wrong on resize (substance bug -- TODO)
  didMount () {
    this.position()
    DefaultDOMElement.getBrowserWindow().on('resize', this.didUpdate, this)
  }

  didUpdate () {
    this.position()
  }

  dispose () {
    DefaultDOMElement.getBrowserWindow().off(this)
  }

  position () {
    if (this.el.getChildCount() === 0) return
    this.setBubblePosition()
  }

  setBubblePosition () {
    // without this check, the editor will break on first load
    const surface = this.getSurface()
    if (!surface) return

    const documentElement = document.querySelector('.se-content')
    const overlayContainer = document.querySelector('.sc-overlay-container')

    setTimeout(() => { // read comment below
      const documentElementWidth = documentElement.offsetWidth
      const overlayContainerLeft = overlayContainer.offsetLeft
      const left = documentElementWidth - overlayContainerLeft - 15

      // unhide it first, as the bubble has no height otherwise
      this.el.removeClass('sc-overlay-bubble-hidden')

      const hints = surface.getBoundingRectangleForSelection()
      const selectionHeight = hints.height
      const bubbleHeight = this.el.getHeight()
      const cheat = 3
      const moveUp = (selectionHeight / 2) + (bubbleHeight / 2) + cheat
      const top = '-' + moveUp + 'px'

      this.el.css('left', left)
      this.el.css('top', top)
    })

    /*
      There is a race condition with overlayContainer's position.
      If it gets rendered fast enough, this is fine.
      Otherwise, the overlayContainerLeft variable won't have been updated by
      substance for us to get the correct value.
      There is no event letting us know that this has been updated,
      and it's probably not worth creating a listener.
    */
  }

  getCommentState () {
    const { commandManager } = this.context
    const commandStates = commandManager.getCommandStates()
    return commandStates.comment
  }

  getDocumentSession () {
    return this.context.documentSession
  }

  getMode () {
    const commentState = this.getCommentState()
    return commentState.mode
  }

  getProvider () {
    return this.context.commentsProvider
  }

  getSelection () {
    const documentSession = this.getDocumentSession()
    return documentSession.getSelection()
  }

  getSurface () {
    const surfaceManager = this.context.surfaceManager
    return surfaceManager.getFocusedSurface()
  }

  // TODO -- move to provider?
  createComment () {
    const mode = this.getMode()
    const provider = this.getProvider()
    const selection = this.getSelection()
    const surface = this.getSurface()

    if (mode === 'delete') return

    const newNode = {
      selection: selection,
      node: { type: 'comment' }
    }

    surface.transaction((tx, args) => {
      const annotation = createAnnotation(tx, newNode)
      provider.focusTextArea(annotation.node.id)
    })
  }
}

export default CommentBubble