Focusing Filament's RichEditor on click with Alpine

Overview

Filament 4 ships with a robust RichEditor using the very powerful TipTap editor (a headless text editor that wraps ProseMirror).

While using this editor in a project, I increased the minimum height of the text area using some custom CSS. However, I noticed users had to click directly on the div with contenteditable="true" in order to focus the editor. Clicking within the margins of rich editor had no effect.

This article explores an approach to automatically focus the underlying editor when clicking anywhere within the rich editor component (including margins). We'll create a custom Alpine.js directive to find the relevant TipTap editor instance and execute a command to focus the editor.

Use case

This approach can be used whenever you'd like to focus the rich editor's editable area upon clicking anywhere within the rich editor component.

The mechanism shown in this article for accessing the TipTap editor instance could easily be expanded to focus the editor for other use cases. For example, when the page loads or a modal's action button is clicked.

Assumptions

You have a working Laravel project with:

  • Filament 4 installed and configured
  • a form schema using Filament's RichEditor

Creating the Alpine directive

Create the following JavaScript file at a resource path that makes sense to you (e.g. resources/js/directives/tiptap-click-focus.js).

📁
resources/js/directives/tiptap-click-focus.js
1document.addEventListener('alpine:init', () => {
2 Alpine.directive('tiptap-click-focus', (el) => {
3 el.addEventListener('click', () => el.querySelector('.tiptap')?.editor?.commands.focus());
4 });
5});

In the above JavaScript snippet, we're using the alpine:init lifecycle hook, which fires:

after Alpine is loaded, but BEFORE it initializes itself on the page

Within the hook's callback, we register a new directive named tiptap-click-focus.

The first argument to the callback, el, is the DOM element the magic function was triggered from. Using this DOM element, we find the contained DOM element that matches the query selector .tiptap. The tiptap CSS class present on the TipTap DOM element out-of-the-box when using Filament's RichEditor.

On the matching DOM element (using optional chaining to be safe) we access the editor property and execute the focus command by invoking the corresponding function on the commands object.

Registering the script as a Filament asset

Now that we have a custom Alpine directive in a dedicated JavaScript file, we need to register the asset using the mechanism provided by Filament. This will cause our JavaScript file to be included wherever the @filamentScripts Blade directive is included.

Add the following in the boot method of a service provider:

📁
app/Providers/AppServiceProvider.php
1<?php
2 
3namespace App\Providers;
4 
5use Filament\Support\Assets\Js;
6use Filament\Support\Facades\FilamentAsset;
7use Illuminate\Support\ServiceProvider;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 public function boot(): void
12 {
13 FilamentAsset::register([
14 Js::make('tiptap-click-focus', __DIR__.'/../../resources/js/directives/tiptap-click-focus.js'),
15 ]);
16 }
17}

Publishing Filament assets

Don't forget to run the following artisan command to publish your updated assets to the /public directory:

1php artisan filament:assets

Using the directive

To use our new Alpine.js directive, we simply need to add the x-tiptap-click-focus (empty) attribute to our RichEditor. We can do this using extraAttributes method provided by Filament.

1RichEditor::make('content')
2 ->extraAttributes(['x-tiptap-click-focus' => '']);

Adding a macro to RichEditor

Filament's RichEditor class uses the Filament\Support\Concerns\Macroable trait which provides support to register custom macros. We can leverage this by adding a focusOnClick macro to add the above Alpine.js directive as an extra attribute on our RichEditor component instance(s).

Add the following in the boot method of a service provider:

📁
app/Providers/AppServiceProvider.php
1<?php
2 
3namespace App\Providers;
4 
5use Filament\Forms\Components\RichEditor;
6use Filament\Support\Assets\Js;
7use Filament\Support\Facades\FilamentAsset;
8use Illuminate\Support\ServiceProvider;
9 
10class AppServiceProvider extends ServiceProvider
11{
12 public function boot(): void
13 {
14 FilamentAsset::register([
15 Js::make('tiptap-click-focus', __DIR__.'/../../resources/js/directives/tiptap-click-focus.js'),
16 ]);
17 
18 RichEditor::macro('focusOnClick', function () {
19 return $this->extraAttributes(['x-tiptap-click-focus' => ''], merge: true);
20 });
21 }
22}
  • Note the merge: true argument which will merge extra attributes instead of replacing them.
Be careful if you're adding additional extra attributes to your component; the merge parameter defaults to false. Either make sure focusOnClick() is called after your additional extraAttributes invocation, or make sure to add merge: true as an argument to merge attributes instead of replacing them.

Using the macro

Now when defining our form's schema, we can simply chain the focusOnClick method on our RichEditor component instance.

1RichEditor::make('content')
2 ->focusOnClick();

Conclusion

By combining Filament, Alpine.js, and TipTap, the RichEditor component is an incredibly powerful form field.

Don't forget you can also extend the editor to further customize your user's experience!

🤖
Did you spot a mistake in this article? Have a suggestion for how something can be improved? Even if you'd just like to comment or chat about something else, I'd love to hear from you! Contact me.

Syntax highlighting by Torchlight.dev

End of article