Development
This field is deceivingly complex with a lot of moving parts. For the sanity of future maintainers here are some notes on the more complicated areas
Basic flow of the field
- Form load
- Display progress spinner
- Async load available formats, load libraries for available formats and editors
- Remove progress spinner and show editor with format selector
Render, render, and render again
The rich_text field being a functional React component means it renders a lot by default but add in the async data fetching necessary to display even a basic render of the field and things get messy... as can be seen in the code. The takeaway here is that the component must always be able to cleanly render without the libraries loaded from /rjsf_rich_text/attachments
or the format data from rich_text_formats
JSON:RPC method. This messy and slow process is hidden behind a progress spinner for the user but developers and maintainers should add consideration of the boostrap render to their mental model of how the component works.
Editor attach
In order to attach an editor the textarea must exist and all the Drupal libraries must be loaded onto the page. For a decoupled form this presents a problem as it has no idea there is a rich text field to render until it renders itself and we can't just include all editor config with every form. To work around this a new custom end point at /rjsf_rich_text/attachments
takes the current user and field config then returns the settings and libraries required for the available editor. Once the needed attachments are retrieved they are loaded on to the page using Drupal.ajax
.
WYSIWYG editors don't render in the textarea
Most WYSIWYG editors do not actually render themselves into the textarea
element they are collecting data for instead they render their own set of DOM elements or an iframe to be presented as the "editor" while the textarea is hidden from the user view and updated only when the WYSIWYG library determines necessary. Fortunately for us Drupal has abstracted this nuance into the Drupal.editorAttach()
and Drupal.editorDetach()
methods which rich_text makes use of to handle any change in editor configuration.
Deciding on available formats, user default format, and fallback format
To decide what formats a user should be able to choose from involves negotiating between values from the form data, field config, and user permissions. To determine which formats are available, which format is the user default, and the fallback format. The rich_text field contains the logic to decide on the default value for a field as well as determining which formats are displayed for selection but which formats a user has access to is determined by the JSON:RPC method rich_text_formats
during the initial render of the field.
Changing formats
The changing of formats with Drupal editors is dangerous because what one format supports, another format may strip out as dangerous or unsupported tags. This is why Drupal core's editor field and this RJSF rich_text field display a popup warning the user that changing the format may cause them to lose data. The important part to understand when changing formats is that it requires a full editor detach and attach cycle.
- Initial form and field render completes
- User changes format
- Show warning modal asking for confirmation
- Move the editor value from the editor to the textarea
- Store the potential new format in the state of the component
- User confirms change
- Hide everything but the title and description
- Run Drupal.editorDetach()
- Run Drupal.editorAttach()
- Show the editor and format selector again
Propagating the value from the textarea to the RJSF form on submit
Keeping the React component and textarea in sync is a bit tricky because it's not the user updating the element but the Drupal.editorDetach
method setting the value via js. This process it turns out this does not trigger the onChange React handler so the module overrides the Drupal.editorDetach
method. The overridden method first makes a call to the parent(core) method so all normal behavior is maintained then an input event is triggered via javascript:
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeInputValueSetter.call(field, field.value);
var ev = new Event('input', { bubbles: true});
field.dispatchEvent(ev);
Backend validation
Core's validation of rich text is rather light on form submit instead it relies on the editors in the frontend to clean up unwanted tags and the filter process during render to catch anything that slips past. This module follows the same set of assumptions so on submit the rich_text Opis filter provided by RJSF runs a call to check_format
, checks the submitted format is one that is allowed for the field and also one the user has permission to use.