substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
213 lines (184 loc) • 6.49 kB
JavaScript
import { Component, domHelpers } from '../dom'
import { platform, getRelativeRect, getSelectionRect, parseKeyEvent } from '../util'
import { EditorSession, createEditorContext } from '../editor'
import SelectableManager from './SelectableManager'
export default class AbstractEditor extends Component {
constructor (...args) {
super(...args)
this._initialize(this.props)
this.handleActions({
executeCommand: this._executeCommand,
scrollSelectionIntoView: this._scrollSelectionIntoView
})
}
_createAPI (archive, editorSession) {
throw new Error('This method is abstract')
}
_getDocument (archive) {
throw new Error('This method is abstract')
}
_getScrollableElement () {
throw new Error('This method is abstract')
}
_initialize (props) {
const { archive } = props
const config = archive.getConfig()
const document = this._getDocumentFromArchive(archive)
this.document = document
const editorSession = new EditorSession('document', document, config, {
overlayId: null
})
this.editorSession = editorSession
const editorState = editorSession.editorState
this.editorState = editorState
const api = this._createAPI(archive, editorSession)
this.api = api
const selectableManager = new SelectableManager(editorState)
this.selectableManager = selectableManager
const context = Object.assign(this.context, createEditorContext(config, editorSession), {
config,
api,
editorSession,
editorState,
archive,
urlResolver: archive,
editable: true,
selectableManager
})
this.context = context
editorSession.setContext(context)
editorSession.initialize()
// HACK: resetting the app state here, because things might get 'dirty' during initialization
// TODO: find out if there is a better way to do this
editorState._reset()
}
willReceiveProps (props) {
if (props.archive !== this.props.archive) {
this._dispose()
this._initialize(props)
this.empty()
}
}
didMount () {
this.editorSession.setRootComponent(this)
this.editorState.addObserver(['selection', 'document'], this._onChangeScrollSelectionIntoView, this, { stage: 'finalize' })
}
dispose () {
this._dispose()
}
handleKeydown (e) {
let handled = false
if (!handled) {
handled = this.editorSession.keyboardManager.onKeydown(e, this.context)
}
if (handled) {
domHelpers.stopAndPrevent(e)
}
return handled
}
_dispose () {
this.editorSession.dispose()
}
_getDocumentType () {}
_getDocumentFromArchive (archive) {
const documentType = this._getDocumentType()
let documentEntry
if (!documentType) {
documentEntry = archive.getDocumentEntries()[0]
} else {
documentEntry = archive.getDocumentEntries().find(entry => entry.type === documentType)
}
if (documentEntry) {
return archive.getDocument(documentEntry.id)
} else {
throw new Error('Could not find main document.')
}
}
_executeCommand (...args) {
return this.editorSession.commandManager.executeCommand(...args)
}
_onChangeScrollSelectionIntoView () {
const sel = this.editorState.selection
this._scrollSelectionIntoView(sel)
}
_scrollSelectionIntoView (sel, options = {}) {
this._scrollRectIntoView(this._getSelectionRect(sel), options)
}
_scrollElementIntoView (el, options = {}) {
const contentEl = this._getScrollableElement()
const contentRect = contentEl.getNativeElement().getBoundingClientRect()
const elRect = el.getNativeElement().getBoundingClientRect()
const rect = getRelativeRect(contentRect, elRect)
this._scrollRectIntoView(rect, options)
return rect.top
}
_scrollRectIntoView (rect, { force }) {
if (!rect) return
const scrollable = this._getScrollableElement()
const height = scrollable.getHeight()
const scrollTop = scrollable.getProperty('scrollTop')
const upperBound = scrollTop
const lowerBound = upperBound + height
const selTop = rect.top + scrollTop
const selBottom = selTop + rect.height
// console.log('upperBound', upperBound, 'lowerBound', lowerBound, 'height', height, 'selTop', selTop, 'selBottom', selBottom)
// TODO: the naming is very confusing cause of the Y-flip of values
if (force || selBottom < upperBound || selTop > lowerBound) {
scrollable.setProperty('scrollTop', selTop)
}
}
_getSelectionRect (sel) {
let selectionRect
if (platform.inBrowser && sel && !sel.isNull()) {
// TODO: here we should use the editor content, i.e. without TOC, or Toolbar
const contentEl = this._getScrollableElement()
const contentRect = contentEl.getNativeElement().getBoundingClientRect()
if (sel.isNodeSelection()) {
const nodeId = sel.nodeId
const nodeEl = contentEl.find(`*[data-id="${nodeId}"]`)
if (nodeEl) {
const nodeRect = nodeEl.getNativeElement().getBoundingClientRect()
selectionRect = getRelativeRect(contentRect, nodeRect)
} else {
console.error(`FIXME: could not find a node with data-id=${nodeId}`)
}
} else if (sel.isCustomSelection()) {
let el
if (sel.customType === 'value') {
el = contentEl.find(`*[data-id="${sel.nodeId}.${sel.data.property}#${sel.data.valueId}"]`)
} else {
el = contentEl.find(`*[data-id="${sel.nodeId}"]`)
}
if (el) {
selectionRect = getRelativeRect(contentRect, el.getNativeElement().getBoundingClientRect())
} else {
console.error(`FIXME: could not find node for custom selection: ${JSON.stringify(sel.toJSON())}`)
}
} else {
selectionRect = getSelectionRect(contentRect)
}
}
return selectionRect
}
// TODO: make sure to add all of the native ones here
_preventNativeKeydownHandlers (event) {
let contentEditableShortcuts
if (platform.isMac) {
contentEditableShortcuts = new Set([
'META+66', // Cmd+Bold
'META+73', // Cmd+Italic
'META+85' // Cmd+Underline
])
} else {
contentEditableShortcuts = new Set([
'CTRL+66', // Ctrl+Bold
'CTRL+73', // Ctrl+Italic
'CTRL+85' // Ctrl+Underline
])
}
const key = parseKeyEvent(event)
if (contentEditableShortcuts.has(key)) {
event.preventDefault()
}
}
}