Required Reading
Before you attempt to create any Bard extensions, it would be wise to learn how to write a TipTap extension first. Otherwise you’d be trying to learn how to ride a motorcycle before you can even ride a bike. Or a unicyle before you can juggle. To have a better understanding of how to write a TipTap extension, you’d in turn benefit greatly on reading about how ProseMirror works.
Writing custom extensions for Bard is pretty complicated, but can be rewarding and give you powerful results.
In short, here’s a quickstart of the things you should probably start with:
- The ProseMirror guide — Yes, it’s really long, but you should at least pretend to read it
- Checking out the code samples for the core TipTap extensions, so you can understand how TipTap relates to ProseMirror
- If you don’t know how to extend the control panel yet, go ahead and read up on that first. The code snippets later will be part of your extension to the control panel. Alternatively, you may also extend the control panel through the creation of an addon.
- Come back here again and keep on going.
Extensions
Adding New Extensions
You may add your own TipTap extensions to Bard using the addExtension
method. (Previously extend
.) The callback may return a single extension, or an array of them.
Statamic.$bard.addExtension(({ mark, node }) => mark(new MyExtension));
Statamic.$bard.addExtension(({ mark, node }) => { return [ mark(new MyExtension), node(new AnotherExtension) ]});
The classes you return should be wrapped using the provided helper functions (i.e. mark
or node
like in the example above).
If you want to replace an existing extension, read below.
Extension Classes
Your extension class should look like a TipTap extension (see an example here) except it should not extend another class, and you should use methods instead of getters.
export default class MyExtension { constructor(options = {}) { this.options = options } name() { return 'myextension'; } schema() { // Your schema stuff } commands({type}) { // Your command stuff } inputRules({type}) { return [] // Input rules if you want } plugins() { return [] } pasteRules() { return [] }}
Replacing Existing Extensions
If you’d like to replace a native extension (e.g. headings or paragraphs) you can use the replaceExtension
method. It takes the name
of the extension, and a callback that returns a single extension instance.
The callback will provide you with the existing extension instance.
Statamic.$bard.replaceExtension('heading', ({ mark, node, extension }) => { return node(new CustomHeadingExtension(extension.options));})
If you are doing simple tweaks to an extension (e.g. adding tailwind classes to headings) you can use the native extension classes directly by importing them through $bard.tiptap.extensions
. Then you don’t need to author an entire class and use the mark
or node
helpers.
const { Heading } = Statamic.$bard.tiptap.extensions; class CustomHeading extends Heading { get schema() { return { ...super.schema, toDOM: node => [`h${node.attrs.level}`, { class: 'font-bold' }, 0], } }} Statamic.$bard.replaceExtension('heading', ({ mark, node, extension }) => { return new CustomHeadingExtension(extension.options);})
Marks and Nodes
The addExtension
and replaceExtension
callbacks will provide the mark
and node
functions to you. Use it to wrap your class, and under the hood it will convert it to an actual TipTap extension class
to be used by Bard.
Within your class, Statamic will provide commonly used functions along with the arguments you’d get in a TipTap extension. This prevents you from needing to import the entire TipTap library into your build. For example:
// markcommands({ type, toggleMark }) { return () => toggleMark(type)} // nodecommands({ type, toggleBlockType }) { return () => toggleBlockType(type)}
If you need more TipTap methods than the ones passed into the arguments, you can use our TipTap API.
If you’re providing a new mark or node and intend to use this Bard field on the front-end, you will also need to create a Mark or Node class to be used by the PHP renderer.
Buttons
To add a button to the toolbar, provide a callback to the buttons
method.
The callback will receive two arguments:
-
buttons
- an array of the existing buttons in the toolbar (more about that in a moment) -
button
- a function that wraps your button objects
The callback may return a button
object, or an array of them.
Statamic.$bard.buttons((buttons, button) => { return button({ name: 'bold', text: __('Bold'), command: 'bold', icon: 'bold' });});
Statamic.$bard.buttons((buttons, button) => [ button({ name: 'bold', text: __('Bold'), command: 'bold', icon: 'bold' }), button({ name: 'italic', text: __('Italic'), command: 'italic', icon: 'italic' }),]);
Returning values to the buttons
method will push them onto the end. If you need more control, you can manipulate the supplied buttons
argument, and then return nothing. For example, we’ll add a button after wherever the existing bold button happens to be:
Statamic.$bard.buttons((buttons, button) => { const indexOfBold = _.findIndex(buttons, { command: 'bold' }); buttons.splice(indexOfBold + 1, 0, button({ name: 'italic', text: 'Italic', command: 'italic', icon: 'italic' }));});
Using the button()
method will make the button only appear if the Bard field has been configured to show your button.
If you’d like your button to appear on all Bard fields, regardless of whether it’s been configured to use that button, you can just return an object. Don’t wrap with button()
.
TipTap API
In your extensions, you may need to use functions from the tiptap
library. Rather than importing the library yourself and bloating your JS files, you may use methods through our API.
Statamic.$bard.tiptap.core; // 'tiptap'Statamic.$bard.tiptap.commands; // 'tiptap-commands'Statamic.$bard.tiptap.utils; // 'tiptap-utils'
You could shorten things up by using destructuring. For example:
const { core: tiptap, commands, utils } = Statamic.$bard.tiptap;const selection = new tiptap.TextSelection(...);commands.insertText(...);utils.getMarkAttrs(...);
ProseMirror Rendering
If you have created an extension on the JS side to be used inside the Bard fieldtype, you will need to be able to render it on the PHP side (in your views).
The Bard Augmentor
class is responsible for converting the ProseMirror structure to HTML.
You can use the addExtension
or replaceExtension
methods to bind an extension class into the renderer. Your service provider’s boot
method
is a good place to do this.
use Statamic\Fieldtypes\Bard\Augmentor; public function boot(){ // Pass an object Augmentor::addExtension('myExtension', new MyExtension); // or a closure. You will be passed the bard fieldtype and an array of options as arguments. Augmentor::addExtension('myExtension', function ($bard, $options) { return new MyExtension(['foo' => $bard->config('should_foo')]; }); // Same for replacing extensions. Augmentor::replaceExtension('paragraph', new MyCustomParagraph); // Closures too. There will be an additional argument at the front which is the existing extension. Augmentor::replaceExtension('paragraph', function ($existing, $bard, $options) { return new CustomParagraph; });}