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

class ContainerEditor extends SubstanceContainerEditor {
  constructor (...args) {
    super(...args)
    this.controlBackButton = this.controlBackButton.bind(this)
john's avatar
john committed
  render ($$) {
    let el = super.render($$)
john's avatar
john committed

chris's avatar
chris committed
    // native spellcheck
    // TODO -- there is a hasNativeSpellcheck fn
    const isSpellcheckNative = (this.props.spellcheck === 'native')
    el.attr('spellcheck', isSpellcheckNative)
    // 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)
    }

chris's avatar
chris committed

  didMount () {
    super.didMount()
john's avatar
john committed
    // TODO -- why are we checking for disabled as well?
    if (this.isEmpty() && !this.props.disabled) {
chris's avatar
chris committed
      this.createText()
      // TODO -- why this and not this.focus ?
      this.el.focus()
    }
john's avatar
john committed

    // Check is this is working properly as the isReadOnlyMode is unstable
chris's avatar
chris committed
    if (this.isReadOnlyMode()) {
chris's avatar
chris committed
      this.editorSession.onUpdate('', this.disableToolbar, this)
chris's avatar
chris committed
      this.addTargetToLinks()
    }
    // TODO -- this.props.history is deprecated and gives a warning
    if (this.props.history) {
      this.props.history.listenBefore((location, callback) => {
john's avatar
john committed
        // TODO -- body is hardcoded here
chris's avatar
chris committed
        if (this.props.containerId === 'body' && this.editorSession.hasUnsavedChanges()) {
          const editor = this.getEditor()
          editor.send('changesNotSaved')
chris's avatar
chris committed
          editor.emit('send:route', {location: location.pathname, back: false})
          return callback(false)
        }
    // window.history.pushState(null, null, document.URL)
    window.addEventListener('popstate', this.controlBackButton)
  // TODO -- review // messes up browser history
  controlBackButton () {
john's avatar
john committed
    // TODO -- why are we pushing this url?
    // it is not necessary that that's where the user wants to go
    const url = '/books/' + this.props.book.id + '/book-builder'

    window.removeEventListener('popstate', this.controlBackButton)

john's avatar
john committed
    // TODO -- body is hardcoded here
chris's avatar
chris committed
    if (this.props.containerId === 'body' && this.editorSession.hasUnsavedChanges()) {
      const editor = this.getEditor()

      window.history.pushState(null, null, document.URL)
      editor.send('changesNotSaved')
john's avatar
john committed

      editor.emit('send:route', {
        back: true,
        location: url
      })
      this.props.history.push(url)
john's avatar
john committed

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 () {
    this.editorSession.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',
        // TODO -- both id's ??
        containerId: this.props.containerId,
        surfaceId: this.props.containerId,
        path: [ node.id, 'content' ],
        startOffset: 0,
        endOffset: 0
      })
    }.bind(this))
    this.rerender()
    this.editorSession.setSelection(newSel)

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

    const allowed = [
      'comment',
      'redo',
      'save',
      'track-change-enable',
      'track-change-toggle-view',
      'undo'
    ]

    each(keys(commandStates), key => {
      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')
    )
  }

  getEditor () {
    return this.context.editor
  }
john's avatar
john committed
export default ContainerEditor