import { each, keys, includes } from 'lodash' import { ContainerEditor as SubstanceContainerEditor, keys as keyboardKeys, uuid } from 'substance' class ContainerEditor extends SubstanceContainerEditor { constructor (...args) { super(...args) this.controlBackButton = this.controlBackButton.bind(this) } render ($$) { let el = super.render($$) // native spellcheck // TODO -- there is a hasNativeSpellcheck fn const isSpellcheckNative = (this.props.spellcheck === 'native') el.attr('spellcheck', isSpellcheckNative) // open for editing // 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) } return el } didMount () { super.didMount() // TODO -- why are we checking for disabled as well? if (this.isEmpty() && !this.props.disabled) { this.createText() // TODO -- why this and not this.focus ? this.el.focus() } // Check is this is working properly as the isReadOnlyMode is unstable if (this.isReadOnlyMode()) { this.editorSession.onUpdate('', this.disableToolbar, this) this.addTargetToLinks() } // TODO -- this.props.history is deprecated and gives a warning if (this.props.history) { this.props.history.listenBefore((location, callback) => { // TODO -- body is hardcoded here if (this.props.containerId === 'body' && this.editorSession.hasUnsavedChanges()) { const editor = this.getEditor() editor.send('changesNotSaved') editor.emit('send:route', {location: location.pathname, back: false}) return callback(false) } return callback() }) } // window.history.pushState(null, null, document.URL) window.addEventListener('popstate', this.controlBackButton) } // TODO -- review // messes up browser history controlBackButton () { // 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) // TODO -- body is hardcoded here if (this.props.containerId === 'body' && this.editorSession.hasUnsavedChanges()) { const editor = this.getEditor() window.history.pushState(null, null, document.URL) editor.send('changesNotSaved') editor.emit('send:route', { back: true, location: url }) } else { this.props.history.push(url) } } onTextInput (event) { if (!this.props.trackChanges) return super.onTextInput(event) this.handleTracking({ event: event, 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) { if (!this.props.trackChanges) return super._handleDeleteKey(event) 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 } handleTracking (options) { const trackChangesProvider = this.context.trackChangesProvider const { event, keypress, surfaceEvent } = options if (!keypress) { event.preventDefault() event.stopPropagation() } 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 this._state.skipNextObservation = true } if (surfaceEvent === 'delete') { const direction = (event.keyCode === keyboardKeys.BACKSPACE) ? 'left' : 'right' options.move = direction options.key = (direction === 'left') ? 'BACKSPACE' : 'DELETE' } trackChangesProvider.handleTransaction(options) } // create an empty paragraph with an empty node // then select it for cursor focus createText () { var newSel 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() } addTargetToLinks () { const allLinks = this.el.findAll('a') each(allLinks, link => link.attr('target', '_blank') ) } getEditor () { return this.context.editor } } export default ContainerEditor