@webselect/editor
v3.5.1
Published
A text editor UI built on top of TipTap
Downloads
7
Readme
Webselect text editor
The editor is a UI built on top of TipTap, which itself is a high-level abstraction of ProseMirror.
Installation
npm i @webselect/editor
Usage
ESM environments
This can be implemented natively in the browser:
<link rel="stylesheet" href="./path/to/editor.css">
<script type="module">
import { Editor } from './path/to/editor.mjs';
const editor = new Editor({ ... });
</script>
Or as part of a build pipeline / bundler:
// Optional depending on setup.
// Explained in the "Styling" section.
import './path/to/editor.css';
import { Editor } from '@webselect/editor';
const editor = new Editor({ ... });
Legacy environments
This works in a similar way to libraries such as jQuery, which expose a global variable. In this case that variable is WebselectEditor
.
<script src="./path/to/editor.browser.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const editor = new WebselectEditor.Editor({ ... });
});
</script>
Creating a new Editor instance
const editor = new Editor({
element: document.querySelector('.editor'),
});
The Editor
constructor takes a single options object.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| element | Element
| - | The Element that the Editor will be mounted to. |
| [content] | string
| - | Optional: Initial HTML content to populate the Editor with on instantiation. |
| [editorClasses] | string
| - | Optional: Additional classes to be added to the TipTap editor. |
| [defaultColour] | string
| '#000000'
| Optional: The default text colour of the Editor. |
| [swatches] | string[]
| - | Optional: Swatches that will be shown in the colour picker. |
| [fontFamilies] | string[]
| - | Optional: Font families that can be used in the Editor. |
| [headingLevels] | number[]
| [1, 2, 3, 4, 5, 6]
| Optional: Permitted heading levels in the editor. |
| [elementBindings] | Element
| Element[]
| - | Optional: Any Element(s) that will be automatically bound to the value of the Editor. |
| [onUpdate] | (editor: Editor) ⇒ void
| - | Optional: Callback function that fires whenever the state of the Editor is changed. |
The Editor
instance
The underlying TipTap instance is available as property tiptap
.
const editor = new Editor({ ... });
// TipTap instance
editor.tiptap;
The following instance methods are also available. In some cases these simply proxy TipTap methods for ease of use.
destroy() ⇒ void
Destroy the editor along with its TipTap instance.
getHTML() ⇒ string
Get the editor contents as HTML.
isEmpty() ⇒ boolean
Check if the editor is empty or not.
setContent(content) ⇒ void
Replace the editor with new content.
| Param | Type | Description |
| --- | --- | --- |
| content | string
| The new content. |
Example:
editor.setContent('<p>Example text</p>');
closeAllDialogs() ⇒ void
Close any open dialogs.
Styling
If your pipeline/setup allows it, for example using Webpack along with the css-loader
plugin, you can import the CSS directly in the module:
import './path/to/editor.css';
Otherwise you'll want to copy editor.css
from the package into your distribution folder and reference it in your HTML like normal.
The Editor comes with a basic, neutral layout that is based on CSS variables. These can be overridden to customise the editor as needed. The following table lists the available variables and their functions:
| Variable | Default | Description |
| --- | --- | --- |
| --wse-toolbar-bg-colour
| transparent
| The background colour of the toolbar. |
| --wse-toolbar-text-colour
| #000
| The text colour of the toolbar. |
| --wse-toolbar-padding
| 10px
| The toolbar padding. |
| --wse-toolbar-gap
| 8px
| The gap between controls in the toolbar. |
| --wse-toolbar-border
| 1px solid #999
| The border underneath the toolbar. |
| --wse-control-border
| none
| The border around toolbar controls. |
| --wse-control-radius
| 5px
| The border radius of toolbar controls. |
| --wse-control-padding
| 8px
| The padding of toolbar controls. |
| --wse-control-bg-colour
| #eee
| The background colour of toolbar controls. |
| --wse-control-text-colour
| #000
| The text colour of toolbar controls. |
| --wse-control-hover-bg-colour
| #ddd
| The hover background colour of toolbar controls. |
| --wse-control-hover-text-colour
| #000
| The hover text colour of toolbar controls. |
| --wse-control-highlight-bg-colour
| #222
| The highlighted background colour of toolbar controls. |
| --wse-control-highlight-text-colour
| #fff
| The highlighted text colour of toolbar controls. |
| --wse-control-icon-size
| 16px
| The size of toolbar control icons. |
| --wse-dialog-bg-colour
| #222
| The background colour of control dialogs. |
| --wse-dialog-text-colour
| #fff
| The text colour of control dialogs. |
| --wse-dialog-padding
| 15px
| The padding of control dialogs. |
| --wse-dialog-radius
| 5px
| The border radius of control dialogs. |
| --wse-dialog-gap
| 6px
| The gap between elements inside control dialogs. |
| --wse-dialog-shadow
| none
| The drop shadow underneath control dialogs. |
| --wse-editor-bg-colour
| #fff
| The background colour of the editor. |
| --wse-editor-min-height
| 200px
| The minimum height of the editor. |
| --wse-editor-max-height
| none
| The maximum height of the editor. |
| --wse-editor-padding
| 20px
| The padding of the editor. |
| --wse-editor-border
| none
| The border around the editor. |
| --wse-editor-radius
| 0px
| The border radius of the editor. |
Text colour component styles
| Variable | Default | Description |
| --- | --- | --- |
| --wse-default-swatch-radius
| 50%
| The radius of the swatches. |
| --wse-default-swatch-highlight-outline
| #fff
| The highlight colour of the swatches. |
| --wse-default-swatches-per-line
| 8
| The number of swatches per row. |
Demo pages
Demo pages can be found under the demos
directory. You'll need to spin up a server in the project root, and not under demos
, so that the distribution files can be referenced properly. I recommend using serve.
Contributing
TipTap is headless and totally modular, so out of the box it does virtually nothing. You can intitialise it with "extensions", which enable features such as bold or italic text. In some cases this will include markdown functionality, but it does not include any UI. That's down to us to implement ourselves.
In most cases the requirements for a feature UI are as follows:
- Have a button or some kind of interactive element that will invoke the functionality of an "extension".
- Provide a subsequent UI if needed (eg. a colour picker)
- The element will react to changes in TipTap's state (eg. a bold button will highlight if the user selects bold text in the editor).
To that end the editor is based on class inheritance, with common functionality delegated to abstract classes. These can be found under components/abstract/*
.
The base class, Control<T>
, does very little other than define methods that must be implemented by subclasses, and expose the current editor instance. This will probably never need to be touched.
Next in the heirarchy is ControlButton
and ControlSelect
. Hopefully these are self-explanatory; they contain common logic for button and select UI elements. This includes creating the corresponding elements in memory so that they can be appended to the toolbar later. These should be the only elements we need.
Above ControlButton
is the optional ControlButtonWithDialog
class, which handles the creation of a Dialog
component. More on Dialog
in the next section.
Lastly we have the top-level controls that implement either ControlButton
, ControlButtonWithDialog
or ControlSelect
. These are found under components/Control{Name}.ts
(note the Control
prefix for consistency).
A simple example is the control for bold text:
import icon from '../../img/bold.svg';
import { highlightIf } from '../utils';
import { ControlButton } from './abstract/ControlButton';
class ControlBold extends ControlButton {
constructor() {
super(icon);
}
public override update(): void {
highlightIf(this.tiptap.isActive('bold'), this.element);
}
protected override onClick(): void {
this.tiptap.chain().focus().toggleBold().run();
}
}
export { ControlBold };
Currently the icons are provided by Flaticon. There's a collection in our account named "Text editor". Any new icons should follow the same style if possible.
The update()
method is mandatory and is called when TipTap fires its transaction
event, which essentially means the state has changed somehow. This includes the user making selections or simply changing the caret position.
The highlightIf()
method is a simple utility function that will apply some styles to the element if the condition is truthy. What's important is the condition itself: this.tiptap.isActive('bold')
. If the caret is within the bounds of a block of bold text, or the current selection contains any bold markers, this will return true
and our UI element will be highlighted.
The onClick()
method is fired when the UI element is clicked. This then hooks directly into the TipTap API to enable bold text. This action will also trigger the transaction
event mentioned previously.
Note that not all extensions have the same methods, so you'll need to reference the TipTap docs when implementing any new extensions. The concept, however, is the same.
For a more complex example, it's worth having a look at ControlTextColour.ts
, as this requires an additional UI for displaying a colour picker.
In a nutshell, the process for adding a new UI control is as follows:
- Install the corresponding TipTap extension.
- Register the extension when initialising TipTap (in
main.ts
). - Find a new icon and add it to the
img
directory. - Create a new
Control{Name}.ts
that extends one of the abstractControlButton
,ControlButtonWithDialog
orControlSelect
classes and implement according to the TipTap docs. - Import and add a new instance of the control to the
controls
array (inmain.ts
).
Dialog
component
The Dialog
component creates a floating dialog beneath a Control
. You'll need to pass in the HTML to render in the dialog, but there are a couple of handy conventions to be aware of:
- If your HTML contains a
<form>
element then asubmit
event will automatically be bound to it, which will callevent.preventDefault()
and then, if you have provided anonSubmit
property, it'll be called. - If your HTML contains a
<button>
element with adata-type="clear"
attribute (inside a<form>
element) then aclick
event will be automatically bound to it, which will callonClear
if you have provided it.
Development mode
Run watch
to begin development mode.
Publishing
Before publishing a new version, please make sure to update CHANGELOG.md
.
The prePublishOnly
task runs automatically and builds the project for production (including code formatting and generating TypeScript types), so you shouldn't have to do any heavy lifting.
I recommend using np for publishing, but it's entirely up to you.
You can bump just the version number in package.json
and package-lock.json
by running the following command:
npm version --commit-hooks false --git-tag-version false <major|minor|patch>