Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use custom Tiptap extensions with Editor component #746

Open
philippbuerger opened this issue Nov 26, 2024 · 12 comments
Open

Can't use custom Tiptap extensions with Editor component #746

philippbuerger opened this issue Nov 26, 2024 · 12 comments
Assignees

Comments

@philippbuerger
Copy link

How can I access the current editor object in a custom toolbar button? I'm trying to implement the unset all marks function from tip tap.

@jeffchown
Copy link

@philippbuerger With a little experimentation, I was able to access the editor object and create a Clear content button to execute the TipTap clearContent() command (https://tiptap.dev/docs/editor/api/commands/content/clear-content) using the following code:

<div>
    <form>
        <flux:editor x-ref="myeditor" wire:model="content" />

        <div class="mt-4">
            <flux:button type="button" x-on:click="$refs.myeditor.editor.commands.clearContent()">
                Clear editor
            </flux:button>
        </div>
    </form>
</div>

Maybe this can help as a starting point for your exploration into custom toolbar buttons.

@philippbuerger
Copy link
Author

@jeffchown thanks for the hint. I was able implement the same logic with $el.closest('[data-flux-editor]').editor.commands.clearContent(). So there's no need for an extra ref. But your way also works perfect.

@philippbuerger
Copy link
Author

Thinking about what is the best way to install custom extensions for tiptap for e.g. https://tiptap.dev/docs/editor/extensions/nodes/table and using it? Any suggestions?

@jeffchown
Copy link

Not yet. I have some experimentation to do with the new Fluxified version of TipTap.
I have installed custom TipTap extensions in the past, but not yet in the Flux version.

I won't have time to experiment with this until next week, but will post any findings here when I do.

@mauritskorse
Copy link

Yes, I mentioned this in #739 (comment) as well.
But it would require to be able to extend on the editor js bundled with flux, wich doesnt seem to be possible yet.

@joshhanley
Copy link
Member

joshhanley commented Nov 28, 2024

@philippbuerger can you provide code for how you are currently trying to add a custom button or how you expect it to work, so we can replicate it and see what is the best way? Thanks!

@calebporzio
Copy link
Contributor

@philippbuerger - Your $el.closest('[data-flux-editor]').editor.commands.clearContent() is great as a solution for the original issue.

However, you're right, having a way to add your own extensions would be nice.

We could add an event hook or something that allows you to append your own extension object onto ours.

The tricky thing is that I think you have to include extensions when you initialize the editor (and can't after initialization).

This means we would need some kind of event-driven setup that allows you to add it at the right time in the boot process.

Something like this maybe?:

import TextAlign from '@tiptap/extension-text-align'

document.addEventListener('flux:editor', e => {
    e.target.appendExtension(TextAlign.configure({
        types: ['paragraph', 'heading'],
        alignments: ['left', 'center', 'right'],
    }),)
})

This would be part of your app bundle and you would have to make sure it executes before Flux's JS... (which may actually be difficult)


Can anyone else think of any other way out-of-the-box alternatives for this story?

I could see

  • Allowing people to bundle their own Flux build (there be dragons here)
  • Providing some kind of more limited PHP-side affordance or something. I don't know...

@jeffchown
Copy link

@calebporzio On first think, the event-driven/hook approach will probably be the best.

Anyone who wants to customize flux:editor to this degree will probably be comfortable (enough) to use the https://livewire.laravel.com/docs/installation#manually-bundling-livewire-and-alpine approach to loading Livewire/Alpine - which I assume will be necessary to install any bundle before Flux's JS.

The first of last two possibles you present:

  • Allowing people to bundle their own Flux build (there be dragons here)
    I think could turn into a mess of non-standard app configurations that could lead to challenging and very time-consuming support issues while also potentially causing unexpected side-effects effecting other aspects/components within the LAF ecosystem (bad acronym for Livewire, Alpine, Flux, I know, but better than 'FAL' or 'FLA' - and made me laugh on this Friday 😂)

@mauritskorse
Copy link

mauritskorse commented Nov 29, 2024

Just a quick thought, and not sure if it could work as I haven't any experience on building webcomponents: I noticed that flux extends the webcomponent classes, and that (if i'm correct) the js that defines the webcomponent is loaded using AssetManager::editorScripts()

So wouldn't it be possible to create a custom editor webcomponent class that extends the base editor class. And possibly through a property on de flux:editor component the js file of that custom class is passed to editorScripts() to make sure the correct script is loaded? In case none is passed, editorScripts will use the default editor implementation.

The base class could look something like this:

class BaseEditor extends UIControl {
    constructor() {
        super();
        this.editor = null;
    }

    connectedCallback() {
        this.initializeEditor();
        this.setupEventListeners();
        this.setupToolbar();
        this.setupAccessibility();
    }

    initializeEditor() {
        const defaultExtensions = [
            // Default extensions
        ];

        this.editor = new Editor({
            extensions: [
               ...defaultExtensions, 
               ...this.getCustomExtensions()
           ],
            content: getContent()
        });
    }

    getContent(){
       return this.innerHTML
    }

    getCustomExtensions(){
      return [
         // the custom extensions, method can be overridden
      ] 
   }

    setupEventListeners() {
        this.addEventListener('keydown', e => {
            // 
        });
    }

    setupToolbar() {
        initializeToolbar(this.editor, this.querySelector('ui-toolbar'));
    }

    setupAccessibility() {
        this._disableable.onInitAndChange(disabled => {
           //
        });
    }

    // and other methods getValue, setValue, mount, focus, ...  
}

The custom class could look something like this

import TextAlign from '@tiptap/extension-text-align'
class CustomEditor extends BaseEditor {
    constructor() {
        super();
    }

    // simply pass the extensions through getCustomExtensions
    getCustomExtensions() {
        // Return an array of custom extensions
        return [
            TextAlign.configure({
                types: ['paragraph', 'heading'],
                alignments: ['left', 'center', 'right'],
            })
        ];
    }

    // or redefine initializeEditor() completely if necessary
    initializeEditor() {
        const defaultExtensions = [
            // Default extensions
        ];

        this.editor = new Editor({
            extensions: [
               ...defaultExtensions,
               this.getCustomExtensions()
            ],
            content: this.innerHTML,
        });
    }

    getValue() {
        // Override getValue method
        return this.editor.getJSON(); // Example of returning JSON instead of HTML
    }

    setValue(value) {
        // Override setValue method
        this.editor.commands.setContent(value, false); // Example of setting content without replacing all
    }
}

which is then passed to flux like this

<flux:editor custom="/resources/js/myCustomEditor.js" />

@megawubs
Copy link

megawubs commented Dec 2, 2024

How about providing a global array of editor extension and once the flux editor initializes, it looks for this global array and appends it to its extensions? This way it can be part of your app bundle and you won't have to figure out when to load it.

Example:

import TextAlign from '@tiptap/extension-text-align'

window.fluxEditorExtensions = [
  TextAlign.configure({
          types: ['paragraph', 'heading'],
          alignments: ['left', 'center', 'right'],
      }
]

@joshhanley joshhanley changed the title Inject editor object in custom toolbar button CamInject editor object in custom toolbar button Dec 19, 2024
@joshhanley joshhanley changed the title CamInject editor object in custom toolbar button Can't use custom Tiptap extensions with Editor component Dec 19, 2024
@calebporzio calebporzio self-assigned this Dec 19, 2024
@gdebrauwer
Copy link

gdebrauwer commented Jan 7, 2025

This would be part of your app bundle and you would have to make sure it executes before Flux's JS... (which may actually be difficult)

@calebporzio I think we can use the event-driven setup and ensure it executes before Flux's Js by allowing users to provide a script url to the flux editor using a 'customize' attribute (or 'configure or 'script' attribute? don't know what the best name would be)

<flux:editor :customize="Vite::asset('resources/js/flux-editor-customizations.js') />

In the flux/editor/index.blade.php file, the provided file could be added to the @assets ... @endassets section:

<!-- In flux/editor/index.blade.php -->

@assets
	@if ($attributes->has('customize'))
        <script src="{{ $attributes->get('customize') }}"></script>
    @endif

    <flux:editor.scripts />
    <flux:editor.styles />
@endassets

If the custom script is not deferred, then that script will always be loaded before it even starts to download and load the flux editor script file if I'm not mistaken 🤔

Maybe it is even possible to make the customize script deferred by waiting for it to be loaded in Flux editor js file 🤔

// In Flux Editor JS

var customizeScript = document.querySelector('#customize-flux-editor-script');

if (customizeScript) {
    customizeScript.addEventListener('load', function() {
        // start executing the flux editor js code ...
    });
} else {
    // start executing the flux editor js code ...
}

The custom script can then use the event-driven setup you previously suggested:

// In resources/js/flux-editor-customizations.js of your laravel project

import TextAlign from '@tiptap/extension-text-align'

document.addEventListener('flux:editor', e => {
    e.target.appendExtension(TextAlign.configure({
        types: ['paragraph', 'heading'],
        alignments: ['left', 'center', 'right'],
    }),)
})

@gdebrauwer
Copy link

It should also be possible to customize the existing extensions.

In a project, I added custom protocols to the link extension using the following code:

window.customElements.whenDefined('ui-editor').then(() => {
    document.querySelectorAll('ui-editor').forEach(uiEditor => {
        let editor = uiEditor.editor;
        let toolbar = uiEditor.querySelector('ui-toolbar');

        editor.extensionManager.extensions.filter(extension => extension.name == 'link')[0].options.protocols = ['mailto', 'page', 'procedure', 'legislation'];
        editor.setOptions();
    });
})

This works if the editor does not have any content yet. If the editor is loaded with existing content and the content contains links with those custom protocols, then the links with those custom protocols are purged by TipTap. This is because those custom protocols are added to the extension's configuration after loading the initial editor.

To fix that, the Flux editor should have the ability to edit its default extensions while also adding new extensions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants