Core concepts of Serlo Editor - serlo/documentation GitHub Wiki
Core Concepts
The term “editor”
An editor can mean different things regarding the context. They often refer to react components which are used in edit mode. There are
- Slate editor: The editor component of the slate library which we use to render and edit rich text inside the text-plugin
- Serlo editor: The react component of the whole editor we are writing in this project
- Plugin editor: The react component of a plugin used in edit mode, i.e. in the rows plugin there is a RowsEditor component which we call the “editor of the rows plugin”
Editor & InnerDocument
The Editor
component (.../serlo-editor/core/editor.tsx) represents the entire Serlo editor.
Within is a InnerDocument
component.
SubDocument, Plugin-Toolbar and Plugin-Settings
The entire document state is saved in a tree like structure. Each node has a plugin type and a state. In the box plugin for example, the state contains information like boxType
but also a rows plugin for the box content.
For each node in this document tree there is an entry in the documents
array in the redux state.
The component SubDocument
takes an id of an entry in the documents
array, obtains the plugin type and state from the redux state and renders this element. When this element contains other plugins as children, they will be rendered as well within their own SubDocument
component.
Each SubDocument
in turn contains three elements ...
- Plugin-Settings
- Plugin-Toolbar
- The editor of the plugin (e.g.
BoxEditor
)
... bundled by the DocumentEditor
component.
Plugin-Settings
Plugin-Toolbar
Sub-Document (Plugin + Plugin-Toolbar)
Plugin
The content in the editor can be divided into a hierarchy of blocks. Every text block, image, geogebra applet and so on is its own block within the content. We call those blocks "plugins". Plugins can be nested within each other. A multimedia plugin for example can contain a text plugin on the left and an image plugin on the right.
A plugin is always defined by a PluginState
and a PluginConfig
and the PluginComponent
.
PluginState
: contains the content of the plugin, e.g. for sc-mc these are answers of the exercise. The content is read from the Redux store or the database.PluginConfig
: contains global configuration options or defaults for the plugin, e.g. default feedback if no feedback defined in PluginState.PluginComponent
: The React component rendering the plugin
Plugins usually have at least:
index.tsx
defines the plugin state, config etc.editor.tsx
renders the editing view and logicstatic.tsx
contains only the necessary code to render a component from the static documents formatrenderer.tsx
contains only the markup and styling that can be shared betweenstatic
andeditor
Plugin configuration
Plugins can be configured to behave & look differently. For each plugin type there is a default configuration which is set in the config
property at startup and serves as a base configuration for all instantiated plugin components of that type. See .../serlo-editor/plugins/[plugin type]/index.tsx
.
Plugins need to be configured differently in certain cases. For example, the text plugin in the header of a box plugin should not allow the user to create lists.
Ways to customize the configuration:
- When a plugins default (state, config, component)-triplet are created at startup, the state can also contain default configuration for the nested plugins. A box plugin for example can configure its text plugin in the box title to now allow lists in the title. See
createBoxState
in.../serlo-editor/plugins/box/index.tsx
- You can pass configuration as a prop to the
SubDocument
component using proppluginProps.config
. This is usually done throughChildStateType.render({ config: ... })
. This function is for example called when rendering the box plugin title. It takes a configuration object, merges it with the initial configuration and instantiates a newSubDocument
component passing the merged configuration.
In the end, a newly instantiated plugin component (for example BoxEditor
) will receive the resulting configuration as a prop and change its look & behavior accordingly.
Static / Store state
- Static state: Editor state as it is saved in the database and that is used by the StaticRenderer (JSON tree structure)
- Store state: Editor state as it is stored in the Redux Store and in the editor (flat structure with ids instead of tree)
Store
The redux store handles temporary changes to the documents and undo/redo. Whenever a change happens in the editor, an event is triggered and a so-called action is dispatched to the store. An action for a change is of the following form:
{action: 'change', pluginId: 'xyz' }
where pluginId
refers to the plugin where the change occurred. Actions are resolved by reducers who define how the state is transformed.
In the redux store, we store the list of plugins, the documents as records, the focus, a clipboard (used for copy and paste) and a history. Combining the initialState of the history and the actions on the undoStack and the redoStack allows us to compute the current state.
type Plugin = { Component: React.Component, state: State, config: {} }
export interface ScopeState {
plugins: {
defaultPlugin: string
plugins: Record<string, Plugin>
}
documents: Record<string, DefaultState>
focus: string | null
root: string | null // atm. always 'root'
clipboard: DocumentState[]
history: HistoryState
}
export interface HistoryState {
initialState?: {
documents: ScopeState['documents']
}
undoStack: PureAction[][]
redoStack: PureAction[][]
pendingChanges: number
}