4.0.2 • Published 5 months ago

@webselect/editor v4.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

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.

ParamTypeDefaultDescription
elementElement-The Element that the Editor will be mounted to.
contentstring-Optional: Initial HTML content to populate the Editor with on instantiation.
editorClassesstring-Optional: Additional classes to be added to the TipTap editor.
defaultColourstring'#000000'Optional: The default text colour of the Editor.
swatchesstring[]-Optional: Swatches that will be shown in the colour picker.
fontFamiliesstring[]-Optional: Font families that can be used in the Editor.
headingLevelsnumber[][1, 2, 3, 4, 5, 6]Optional: Permitted heading levels in the editor.
elementBindingsElement | 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.

ParamTypeDescription
contentstringThe 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:

VariableDefaultDescription
--wse-toolbar-bg-colourtransparentThe background colour of the toolbar.
--wse-toolbar-text-colour#000The text colour of the toolbar.
--wse-toolbar-padding10pxThe toolbar padding.
--wse-toolbar-gap8pxThe gap between controls in the toolbar.
--wse-toolbar-border1px solid #999The border underneath the toolbar.
--wse-control-bordernoneThe border around toolbar controls.
--wse-control-radius5pxThe border radius of toolbar controls.
--wse-control-padding-x6pxThe horizontal padding of toolbar controls.
--wse-control-padding-y0pxThe vertical padding of toolbar controls.
--wse-control-bg-colour#eeeThe background colour of toolbar controls.
--wse-control-text-colour#000The text colour of toolbar controls.
--wse-control-hover-bg-colour#dddThe hover background colour of toolbar controls.
--wse-control-hover-text-colour#000The hover text colour of toolbar controls.
--wse-control-highlight-bg-colour#222The highlighted background colour of toolbar controls.
--wse-control-highlight-text-colour#fffThe highlighted text colour of toolbar controls.
--wse-control-icon-size16pxThe size of toolbar control icons.
--wse-dialog-bg-colour#222The background colour of control dialogs.
--wse-dialog-text-colour#fffThe text colour of control dialogs.
--wse-dialog-padding15pxThe padding of control dialogs.
--wse-dialog-radius5pxThe border radius of control dialogs.
--wse-dialog-gap6pxThe gap between elements inside control dialogs.
--wse-dialog-shadownoneThe drop shadow underneath control dialogs.
--wse-select-max-heightnoneThe maximum height of the select.
--wse-select-option-gap6pxThe gap between options.
--wse-select-option-padding-x8pxThe horizontal padding of each option.
--wse-select-option-padding-y6pxThe vertical padding of each option.
--wse-select-option-radius5pxThe border radius of each option.
--wse-select-option-highlight-bg-colour#fffThe highlighted background colour of each option
--wse-select-option-highlight-text-colour#222The highlighted text colour of each option.
--wse-select-option-hover-bg-colourrgb(white, 0.2)The hover background colour of each option.
--wse-select-option-hover-text-colour#fffThe hover text colour of each option.
--wse-editor-bg-colour#fffThe background colour of the editor.
--wse-editor-min-height200pxThe minimum height of the editor.
--wse-editor-max-heightnoneThe maximum height of the editor.
--wse-editor-padding20pxThe padding of the editor.
--wse-editor-bordernoneThe border around the editor.
--wse-editor-radius0pxThe border radius of the editor.

Text colour component styles

VariableDefaultDescription
--wse-default-swatch-radius50%The radius of the swatches.
--wse-default-swatch-highlight-outline#fffThe highlight colour of the swatches.
--wse-default-swatches-per-line8The 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, which represents a clickable item in the editor toolbar. This is used for simple extensions such as toggling bold or italic text.

Some extensions require additional UI, and this is where ControlButtonWithDialog is useful, as it lets us show a dialog (styled as a dropdown). More on Dialog in the next section.

As a stylised replacement for the native <select> element, we also have ControlButtonWithSelect, which implements ControlButtonWithDialog under the hood, but requires less setup when dealing with simple option lists.

Lastly we have the top-level controls that implement one of the aforementioned abstract classes. 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 abstract Control* classes and implement according to the TipTap docs.
  • Import and add a new instance of the control to the controls array (in main.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 a submit event will automatically be bound to it, which will call event.preventDefault() and then, if you have provided an onSubmit property, it'll be called.
  • If your HTML contains a <button> element with a data-type="clear" attribute (inside a <form> element) then a click event will be automatically bound to it, which will call onClear 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>