What is the Weka editor?
Weka is a new content editor introduced in Totara 13. It can be selected and used as a default editor along with Atto, plain text, and other editor plugins. Along with the editor itself, Weka brings a new format type into Totara format types (FORMAT_JSON_EDITOR) in addition to the existing HTML, plain text, Markdown and Moodle formats.
A big motivating factor for creating a new editor was that existing formats had significant limitations for mobile rendering. This editor and its format were built to be agnostic of the output rendering, and allows proper rendering of all content types on a mobile client. This also brings some limitations to formatting capabilities and styling, because each node implemented for Weka must also be implemented on a mobile client for each supported platform and checked that it is displaying correctly.
However, as Weka is a pluggable editor, the number of node types and styles will grow over time in line with users' needs and requirements. Custom plugins can also be added when needed.
On the web, Weka content can be rendered in three formats: Tui, regular HTML, and plain text. Tui is a front-end component library made for Totara and based on the Vue.js framework. Most standard Weka plugins are made using Tui and allow the adding and displaying of interactive content. In places where Tui rendering is not used, such as standard course pages or dashboards, content created in Weka will be displayed in a simplified HTML format, which is the default output from format_text().
How the Weka editor works
Weka structures the content being edited as a tree of nodes. Each of the nodes represents a single piece of content, such as a paragraph, a run of text, a user mention, or an embedded video.
As Weka does not edit HTML, but rather an abstract node structure, HTML is just one possible way to render the node structure. For instance, the Totara Mobile app renders Weka content as React Native elements.
The Weka editor is implemented using the ProseMirror editor toolkit. ProseMirror provides an immutable data model, tools for configuring the document schema, and handles rendering the actual editor control for our node-based data structure. Reading through the ProseMirror guide is highly recommended to understand how Weka works if you are looking at extending it.
Internally, the document and other editor state is represented as an immutable tree. Whenever a change is made, a transaction is dispatched which updates the state by generating a new immutable tree.
This same model is used for programmatic changes to the editor content as well, e.g. to add new node when a toolbar button is clicked, we dispatch a transaction that describes the change we want to make. These actions are encapsulated as commands, which are just functions that take the editor state (used to construct the transaction), and an optional dispatch function to execute the transaction.
Upon saving, the node structure will be converted to JSON, and that JSON content will be saved into the database. During content rendering, the json_editor formatter converts the JSON to HTML or text. To do that, the formatter needs to know all the node types declared in the system. Each of the node types has a structure described in ProseMirror's documentation.
Note that the JSON editor format is a part of the core, while editors are plugins. Weka editor is an editor plugin that generates FORMAT_JSON content. This means that the JSON schema for each node must always be the same across the system, and future editors will need to support the same JSON schema.
Using the Weka editor
Rather than using Weka directly, we recommend using the Editor component, which will reflect system and user preferences:
<template> <Editor v-model="content" aria-label="My editor" /> </template> <script> import Editor from 'tui/components/editor/Editor'; import { EditorContent, Format } from 'tui/editor'; export default { components: { Editor }, data() { return { content: new EditorContent({ // Optional: pass `format` to lock in a format. Weka is the only editor supporting JSON_EDITOR, so it will be used. // If format is left out, <Editor> will pick the most preferred editor that has Tui support. Currently this is just Weka and the plain text fallback editor. format: Format.JSON_EDITOR, // optional: pass fileItemId to enable file uploads fileItemId: this.draftId, }), // If you have existing content, you can pass it in like this: content: new EditorContent({ content: this.description, format: this.descriptionFormat, // format must always be passed when providing content fileItemId: this.draftId, }), }; }, }; </script>
See the Editor sample page at /totara/tui/
in a Totara installation for more examples of the available options.
EditorContent is an opaque wrapper for the editor content. It has .format
and .fileItemId
properties, and the content can be retrieved by calling .getContent()
. This has a small performance cost so you should not do it on every change - instead, convert when the value is needed, e.g. to submit to the server.
You should always pass either aria-label or aria-labelledby
. If using the editor inside a FormRow
, you can get the labelId
to pass to aria-labelledby
from the slot.
You can also use Weka directly if needed:
<template> <Weka v-model="doc" :file-item-id="draftFile" aria-label="My editor" /> </template> <script> import Weka from 'editor_weka/components/Weka'; import WekaValue from 'editor_weka/WekaValue'; export default { components: { Weka }, data() { return { doc: WekaValue.empty(), // or from existing content: doc: WekaValue.fromDoc(JSON.parse(this.description)) }; }, } </script>
Adjusting Weka configuration and enabled features
'Variants' are an editor concept, currently only used by Weka, that allow embedders to select a set of editor features to enable. In Weka, this means which 'extensions' (editor plugins) are enabled, which implement things like links and bulleted lists.
There are four core variants to choose from:
- full: Full editor functionality. Used for long-form text such as course pages, Totara Engage resource content, etc.
- standard: The default variant. Excludes extensions that are only relevant for long-form content, such as layout.
- basic: Basic text editing. Only includes b/i/u, links, lists, and emoji. This is the variant used for comments.
- simple: This is a very simple editor containing only b/i/u.
You can also provide the name of additional extensions to load at the site of use:
<template> <Editor v-model="doc" variant="standard" :extra-extensions="['mention']" /> </template>
The mention and hashtag extensions are built-in, but must be enabled manually in Totara 17 and later through the extra-extensions prop, as they require back-end support in each area to use.
This can also be useful for one-off pieces of custom functionality, e.g. the centralised notifications system uses this to add a placeholder suggestion extension, which wouldn't make sense if it were enabled in every Weka editor.
Extending Weka (Totara 17+)
Weka is built up from a series of 'extensions', each adding new nodes and features to the editor. Out of the box, the editor is only capable of editing plain text, and almost all other features are added through built-in extensions.
Custom extensions can also be defined in plugins. In order to add new functionality to Weka in a plugin, there are generally three things that need to be created, as outlined below.
weka_myplugin frontend
The first is the front-end Tui component for the plugin at client/component/weka_myplugin
.
This contains the extension to the front end of Weka editor, contained in a src/js/extension.js
file, and any components used by the extension.
Extensions can provide the following:
- nodes() - map of node names to an object containing a ProseMirror node definition under the schema key, and optionally a Vue component under the component key
- marks() - same as nodes, except for ProseMirror mark definitions
- plugins() - array of ProseMirror plugins to add
- toolbarItems() - array of items to add to the Weka toolbar
- keymap(bind) - called to set up keyboard shortcuts
- inputRules() - array of ProseMirror input rules, e.g. used to automatically create a list when you type an asterisk
Each ProseMirror node schema has a few key properties:
- group - Used in content expressions that control where nodes can be placed. In Weka, there are two main groups: block, which can appear as direct children of the document, but can also appear as list item content for example, and inline, which can appear as the children of paragraphs and headings.
- parseDOM - Defines rules used to parse HTML and convert to this node during a paste. Read more in the ProseMirror documentation.
- toDOM - Called to convert the node to HTML. This is used for copying to the clipboard, and is used to render the node if no component is provided.
- inline - Causes ProseMirror to treat this as an inline node. This should be set to true if
group
is inline. - attrs - Used to define the possible attributes for this node. This is an object of the form
attrs: { someAttr: {}, anotherAttr: { default: null } }
. In this example,someAttr
is required as it does not have a default, whereasanotherAttr
is optional. - See the ProseMirror documentation for the full list of node schema properties.
You can also provide a component to render the node. Note that the component property does not go inside the schema object, it goes one level up - see the sample below. The component should extend editor_weka/components/nodes/BaseNode
. The node attr values will be available as this.attrs
, and componentContext as this.context
.
A good way to understand each of these is to look at the existing extensions in client/component/editor_weka/src/js/extensions
- ruler and list are good ones to start with.
import BaseExtension from 'editor_weka/extensions/Base'; import Marquee from 'weka_myplugin/components/nodes/Marquee'; class MypluginExtension extends BaseExtension { nodes() { return { // "myplugin" here comes from the name of the jsoneditor plugin described in a section below 'myplugin/marquee': { // ProseMirror node spec // https://prosemirror.net/docs/guide/#schema // https://prosemirror.net/docs/ref/#model.NodeSpec schema: { group: 'block', content: 'inline*', parseDOM: [{ tag: 'marquee' }], toDOM() { return ['marquee', 0]; }, }, // Component to use for rendering in the editor. // If this is not specified, toDOM() will be used for rendering. component: Marquee, // You can also optionally pass "context" to the component. // componentContext: { // replaceWithFoo: this.replaceWithFoo.bind(this), // } }, }; } } export default opt => new MypluginExtension(opt);
weka_myplugin backend
The corresponding back-end Totara component at server/lib/editor/weka/extensions/myplugin
must also be created.
This contains a classes/extension.php
that defines the JS path for the Weka front-end extension.
namespace weka_myplugin; use editor_weka\extension\extension as base_extension; class extension extends base_extension { public function get_js_path(): string { return "weka_myplugin/extension"; } }
This extension will automatically be loaded in the exclude-based core Weka variants (standard and full).
In order to add your custom extension to the basic variant as well, you can override the get_included_in_variants() method to tell Weka to load it:
public static function get_included_in_variants(): array { return ['basic']; }
Avoid adding extensions to the simple variant, as that variant is typically used in specialised use cases.
To prevent your extension from being added to standard and full extensions, you can implement the specific_custom_extension interface to signal to Weka that this extension is use-case specific and needs to be enabled manually – either through get_included_in_variants or extra-extensions.
jsoneditor_myplugin backend plugin
Finally, if your Weka plugin needs new node types, they should be defined in the jsoneditor
plugin, e.g. at server/text_format/json_editor/extensions/myplugin
. They do not need to share a name.
Nodes cannot be defined in a Weka plugin, as the format is independent of any specific editor.
Nodes are defined by creating a class, e.g. jsoneditor_myplugin/json_editor/node/marquee.php
.
Plugin example
To see how this all fits together in a working example, take a look at the example weka_marquee
plugin: https://git.totaralearning.com/projects/EXAMPLE/repos/weka_marquee/browse
This plugin implements support for the <marquee> tag in the Weka editor. This could serve as a starting point for your own plugin, or just as a reference.
You can also look at how the core Weka extensions are implemented in client/component/editor_weka/src/js/extensions/
.