Skip to content
Snippets Groups Projects
CommentBubble.js 4.56 KiB
Newer Older
john's avatar
john committed
import {
  DefaultDOMElement,
  FontAwesomeIcon as Icon,
  Tool
} from 'substance'

class CommentBubble extends Tool {
  render ($$) {
    if (!this.canCreateComment()) return $$('div')
    const title = 'Create a new comment'
chris's avatar
chris committed
    const iconPlus = $$(Icon, { icon: 'fa-plus' })
        .addClass('sc-comment-icon-plus')

    const iconBubble = $$(Icon, { icon: 'fa-comment-o' })
        .addClass('sc-comment-icon-bubble')
john's avatar
john committed

    const bubble = $$('div')
      .attr('title', title)
      .addClass('sc-overlay-bubble')
      .addClass('sc-overlay-bubble-hidden')
      .on('click', this.createComment)
chris's avatar
chris committed
      .append(iconBubble)
      .append(iconPlus)
john's avatar
john committed
    return bubble
  }
john's avatar
john committed
  // 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.context.editorSession.onUpdate('', this.position, this)
    this.position()
john's avatar
john committed
    DefaultDOMElement.getBrowserWindow().on('resize', this.didUpdate, this)
  }
john's avatar
john committed
  didUpdate () {
    this.position()
john's avatar
john committed
  }
john's avatar
john committed
  dispose () {
    DefaultDOMElement.getBrowserWindow().off(this)
  }
  position () {
    if (this.el.getChildCount() === 0) return
    this.setBubblePosition()
  }

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

    setTimeout(() => { // read comment below
      let documentElement = document.querySelector('.se-content')
      let overlayContainer = document.querySelector('.sc-overlay')
      let fix = 15
      if (parseInt(overlayContainer.offsetLeft) === 0) {
chris's avatar
chris committed
        const minEditorContentPanelChildren = document.getElementById('notes-editor-content-panel').children
        const temp = minEditorContentPanelChildren[0].children
        documentElement = temp[0]
        const children = temp[0].children
        overlayContainer = children[1]
      }
chris's avatar
chris committed

chris's avatar
chris committed
      const documentElementWidth = documentElement.offsetWidth / 1.85
john's avatar
john committed
      const overlayContainerLeft = overlayContainer.offsetLeft
      const left = documentElementWidth - overlayContainerLeft - fix
john's avatar
john committed
      // unhide it first, as the bubble has no height otherwise
      this.el.removeClass('sc-overlay-bubble-hidden')
      let wsel = window.getSelection()
      let wrange = wsel.getRangeAt(0)
      const hints = wrange.getBoundingClientRect()
      const selectionHeight = hints.height
      const bubbleHeight = this.el.getHeight()
      const cheat = 3
      const moveUp = (selectionHeight / 2) + (bubbleHeight / 2) + cheat
      const top = '-' + moveUp + 'px'
chris's avatar
chris committed

john's avatar
john committed
      this.el.css('left', left)
      this.el.css('top', top)
john's avatar
john committed
    })

    /*
      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.
    */
  }
john's avatar
john committed
  getCommentState () {
    const { commandManager } = this.context
    const commandStates = commandManager.getCommandStates()
    return commandStates.comment
  getEditorSession () {
    return this.context.editorSession
john's avatar
john committed
  }

  getMode () {
    const commentState = this.getCommentState()
    return commentState.mode
  }
john's avatar
john committed
  getProvider () {
    return this.context.commentsProvider
john's avatar
john committed
  getSelection () {
    const editorSession = this.getEditorSession()
    return editorSession.getSelection()
john's avatar
john committed
  // TODO -- get from provider
john's avatar
john committed
  getSurface () {
    const surfaceManager = this.context.surfaceManager
    return surfaceManager.getFocusedSurface()
  }

  isSelectionLargerThanComments () {
    const provider = this.getProvider()
    return provider.isSelectionLargerThanComments()
  }

  canCreateComment () {
    const mode = this.getMode()

    if (mode === 'create') return true
    if (!this.isSelectionLargerThanComments()) return false
john's avatar
john committed
  // TODO -- move to provider
john's avatar
john committed
  createComment () {
    if (!this.canCreateComment()) return

john's avatar
john committed
    const provider = this.getProvider()
    const selection = this.getSelection()
    const surface = this.getSurface()

    const newNode = {
      selection: selection,
      type: 'comment',
      path: selection.path,
      start: selection.start,
      end: selection.end
    surface.editorSession.transaction((tx, args) => {
      const annotation = tx.create(newNode)
      provider.focusTextArea(annotation.id)
john's avatar
john committed
    })
john's avatar
john committed
export default CommentBubble