-
Notifications
You must be signed in to change notification settings - Fork 46
Technical description
This documentation was last updated for [email protected]. This document is only valid for versions >= 8.0.0, after the old jQuery-version was rewritten as a React component. The aim of this document is to outline some of the technical choices that are not immediately apparent from the code itself - for a better understanding of the editor's features, you should refer to its component test suite.
RTE uses a React Context to hold some shared functions and parts of the state, and to handle communication across the components. Most significantly, it manages the hydration, creation and removal of equation editors. The boundary of what lives in the context and what is in the components is not very well defined, but generally things that need to be called across components are in the context.
This is the core of the editor, where the user fills in their answer. Under the hood, it is a <div>
with the attribute contenteditable
. This already natively makes it into a (pretty bad) rich text editor that is capable of rendering and editing arbitrary HTML content. The user cannot just write HTML into it and expect it to be hydrated into proper DOM elements though - elements need to be added either programmatically or via pasting it as HTML from another source. We sanitize any pasted content, more on that later. Technically, this component also controls the rendering of the other components: The toolbar, the help dialog and opened equation editors.
Since the content is plain HTML, we need to do some special tricks to make everything work cleanly with React. Most significantly, equations (images with a source that contains /math.svg
) need to have click handlers that create an equation editor component. To make this magic work, you'll see snippets like this in different parts of the codebase:
setTimeout(() => {
editor.initMathImages()
setTimeout(() => {
editor.onAnswerChange()
}, 0)
}, 0)
Performing an action after a timeout of 0 milliseconds delays the action until the next event loop. This means that possible straight up DOM manipulations are done, and we're able to do our handling based on them (i.e. make pasted equation images in the DOM clickable, and then take the editor's innerHTML
and pass it to the provided answer saving function).
There are three shortcuts that the text area listens for:
-
Ctrl + E
: This opens a new equation at the cursor position (replacing selected content, if a selection exists) -
Ctrl + Z
: Undo. Due to browsers doing their own thing, this uses a custom history implementation -
Ctrl + Y
: Redo. Same as above.
The user can paste three kinds of content to the editor. Each type is handled differently before being added to the answer, but in the end every type will be added to the DOM with document.execCommand('insertHTML', ...)
. A single copied element may be represented as several of these types on the clipboard, but they'll be processed in the order outlined below (e.g. if the element is a piece of HTML, it will be represented as both the actual copied HTML and a text representation of the same content and we will handle it as HTML). The pasted content may include HTML that needs to be hydrated after the pasting is done.
RTE allows defining the allowed file types and defaults to PNG and JPG. In practice, these will mostly be screenshots taken from software used in the exam. The file blob is passed to a function passed to the editor as a prop (getPasteSource
), and the URL returned by the function will be used as the src
for a newly created <img>
.
All pasted HTML is sanitized. Sanitization options can be passed to the component, but in practice we only allow <img>
, <br>
, <span>
, <div>
and <p>
tags. During the sanitization, <div>
and <p>
tags get processed in a way that strips the tags and adds line breaks in a way that emulates paragraphs. For all other tags, the tag is stripped and replaced with its possible text contents. Note that the browser may still add these tags when the user adds line breaks - trying to prevent that would not be worth the added complexity, and these will be removed when the answer is sanitized for save anyway.
Just text, this is not processed in any way.
The native browser undo/redo history does not completely work with the way we do DOM manipulation in the editor, so we need to use our custom implementation. In a nutshell, the history is represented as an array of strings representing the answer's innerHTML
at different points, and a pointer that points to the current value. Just like pasting, this also requires a hydration step as an equation recovered from the history would otherwise be just a plain image. The main text area's history is updated with a debounce of 500ms. The history is stored in the context.
The WYSIWYG-LaTeX-editor-in-editor used to create and modify equations. Comprises of two parts, the WYSIWYG equation field and the raw plain LaTeX field. This is a React component that is rendered into a wrapper <span>
that holds a React Portal - this allows us to render the component into an arbitrary HTML node that is not rendered via React, and it is still inside the same context as the other parts so we can use it to communicate both ways.
There can only be one editor at a time, always associated with a single equation. The editor is created by the context, when an equation image is clicked. The hydration done after direct DOM changes adds the necessary handlers and attributes to images that allow this to work. Changes to the equation are rendered live, but the changes are saved when the equation editor closes.
This is provided by MathQuill (MQ). We provide some callbacks and do syncing between this and the raw field, but otherwise this is mostly out of our hands. This is not a React component; we pass an Element
to a function, it makes it into a WYSIWYG editor and returns us a handle we can use for manipulating it. The handle for the currently open editor is stored in the context.
Boring old text field. The changes here are reflected in the WYSIWYG equation field and vice versa.
When an equation editor is open, we render and additional toolbar that provides LaTeX commands and buttons for undo and redo. The LaTeX buttons send commands to MQ via the context.