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 ...

  1. Plugin-Settings
  2. Plugin-Toolbar
  3. 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 logic
  • static.tsx contains only the necessary code to render a component from the static documents format
  • renderer.tsx contains only the markup and styling that can be shared between static and editor

overview of editor plugin files

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 prop pluginProps.config. This is usually done through ChildStateType.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 new SubDocument 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
}