Skip to content
Snippets Groups Projects
ContainerEditor.js 5.25 KiB
Newer Older
import { each, keys, includes } from 'lodash'
john's avatar
john committed
import {
  ContainerEditor as SubstanceContainerEditor,
  // deleteCharacter,
  // deleteSelection,
  keys as keyboardKeys,
john's avatar
john committed
  Surface,
john's avatar
john committed
} from 'substance'

class ContainerEditor extends SubstanceContainerEditor {
  render ($$) {
    // TODO -- call with super
    var el = Surface.prototype.render.call(this, $$)

    var doc = this.getDocument()
    var containerId = this.getContainerId()
    var containerNode = doc.get(containerId)

    if (!containerNode) {
      console.warn('No container node found for ', containerId)
    }

    el.addClass('sc-container-editor container-node ' + containerId)
      .attr({
        spellCheck: false,
        'data-id': containerId
      })

    // if it IS empty, handle in didMount
    if (!this.isEmpty()) {
      containerNode.getNodes().forEach(function (node) {
        el.append(this._renderNode($$, node).ref(node.id))
      }.bind(this))
    }

    // TODO -- should maybe change to isEditable ?
    // or maybe delete it? we never pass a disabled prop explicitly
    if (!this.props.disabled) {
      el.addClass('sm-enabled')
      el.setAttribute('contenteditable', true)
    }

  didMount () {

    if (this.isEmpty()) this.createText()
john's avatar
john committed
    this.focus()
    const documentSession = this.getDocumentSession()

    if (this.isReadOnlyMode()) {
      documentSession.on('didUpdate', this.disableToolbar, this)

      this.addTargetToLinks()
    }

    documentSession.on('didUpdate', this.setCommentstops, this)
  }

  setCommentstops () {
    this.context.commentsProvider.update()
john's avatar
john committed
    if (!this.props.trackChanges) return super.onTextInput(event)

john's avatar
john committed
    this.handleTracking({
john's avatar
john committed
      event: event,
john's avatar
john committed
      status: 'add',
      surfaceEvent: 'input'
    })
  onTextInputShim (event) {
    if (!this.props.trackChanges) return super.onTextInputShim(event)

    this.handleTracking({
      event: event,
      status: 'add',
      surfaceEvent: 'input',
      keypress: true
    })
  }

  _handleDeleteKey (event) {
john's avatar
john committed
    if (!this.props.trackChanges) return super._handleDeleteKey(event)

john's avatar
john committed
    this.handleTracking({
      event: event,
      status: 'delete',
      surfaceEvent: 'delete'
    })
  }

  _handleSpaceKey (event) {
    if (!this.props.trackChanges) return super._handleSpaceKey(event)

    this.handleTracking({
      event: event,
      status: 'add',
      surfaceEvent: 'space'
    })
  }

  shouldIgnoreKeypress (event) {
    // see Surface's onTextInputShim for comments
    if (
      event.which === 0 ||
      event.charCode === 0 ||
      event.keyCode === keys.TAB ||
      event.keyCode === keys.ESCAPE ||
      Boolean(event.metaKey) ||
      (Boolean(event.ctrlKey) ^ Boolean(event.altKey))
    ) {
      return true
    }

    return false
  }

  getTextFromKeypress (event) {
    let character = String.fromCharCode(event.which)
    if (!event.shiftKey) character = character.toLowerCase()
    if (character.length === 0) return null
    return character
  }

john's avatar
john committed
  handleTracking (options) {
    const trackChangesProvider = this.context.trackChangesProvider
    const { event, keypress, surfaceEvent } = options
john's avatar
john committed

    if (!keypress) {
      event.preventDefault()
      event.stopPropagation()
    }
john's avatar
john committed

john's avatar
john committed
    if (surfaceEvent === 'input') {
      if (keypress) {
        if (this.shouldIgnoreKeypress(event)) return
        const text = this.getTextFromKeypress(event)
        event.data = text
        event.preventDefault()
        event.stopPropagation()
      }

      if (!keypress && !event.data) return
john's avatar
john committed
      this._state.skipNextObservation = true
    }
john's avatar
john committed

john's avatar
john committed
    if (surfaceEvent === 'delete') {
      const direction = (event.keyCode === keyboardKeys.BACKSPACE) ? 'left' : 'right'
      options.move = direction
      options.key = (direction === 'left') ? 'BACKSPACE' : 'DELETE'
john's avatar
john committed

john's avatar
john committed
    trackChangesProvider.handleTransaction(options)
  }

  // create an empty paragraph with an empty node
  // then select it for cursor focus
john's avatar
john committed
  createText () {
    var newSel

    this.transaction(function (tx) {
      var container = tx.get(this.props.containerId)
      var textType = tx.getSchema().getDefaultTextType()

      var node = tx.create({
        id: uuid(textType),
        type: textType,
        content: ''
      })

      container.show(node.id)

      newSel = tx.createSelection({
        type: 'property',
        path: [ node.id, 'content' ],
        startOffset: 0,
        endOffset: 0
      })
    }.bind(this))

    this.rerender()
    this.setSelection(newSel)
  }

  // only runs if editor is in read-only mode
  // disables all tools, apart from comments
  disableToolbar () {
    const commandStates = this.getCommandStates()

    each(keys(commandStates), key => {
      const allowed = ['comment', 'note', 'save', 'undo', 'redo', 'track-change-toggle-view']
      if (!includes(allowed, key)) commandStates[key].disabled = true
    })
  }

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

  isReadOnlyMode () {
    return !this.isEditable() && this.isSelectable()
  }

john's avatar
john committed
  addTargetToLinks () {
    const allLinks = this.el.findAll('a')
    each(allLinks, link =>
      link.attr('target', '_blank')
    )
  }
john's avatar
john committed
export default ContainerEditor