-
Notifications
You must be signed in to change notification settings - Fork 50
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
Comments
@philippbuerger With a little experimentation, I was able to access the editor object and create a
Maybe this can help as a starting point for your exploration into custom toolbar buttons. |
@jeffchown thanks for the hint. I was able implement the same logic with |
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? |
Not yet. I have some experimentation to do with the new Fluxified version of TipTap. I won't have time to experiment with this until next week, but will post any findings here when I do. |
Yes, I mentioned this in #739 (comment) as well. |
@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! |
@philippbuerger - Your 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?:
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
|
@calebporzio On first think, the event-driven/hook approach will probably be the best. Anyone who wants to customize The first of last two possibles you present:
|
This comment has been minimized.
This comment has been minimized.
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'],
}
] |
@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 <!-- 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'],
}),)
}) |
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. |
I have a little different working approach. It uses the native The default config file is set in the components blade file and can be changed by passing a different config file in the Below is my implementation. editor/index.blade.php @props([
'name' => $attributes->whereStartsWith('wire:model')->first(),
'toolbar' => null,
'invalid' => null,
+ 'src' => 'resources/js/vendor/flux/editor/default.module.min.js',
'format' => 'json',
])
data-flux-editor
+ src="{{ Vite::asset( $src ) }}"
format="{{ $format }}" {{-- html or json --}}
>
<?php if ($slot->isEmpty()): ?>
<flux:editor.toolbar :items="$toolbar" /> in editor.js there are quite some changes to make this work boot() {
// Extract initial content. If the content is coming from Livewire, we need to use
// the value attribute, otherwise load it from the editor content element...
- let content = this.value ?? this.querySelector('ui-editor-content').innerHTML
+ this.content = this.getAttribute('content') || this.querySelector('ui-editor-content').innerHTML || '';
this.format = this.getAttribute('format') || 'html';
toolbar = this.querySelector('ui-toolbar')
commands = this.querySelector('ui-commands')
this.querySelector('ui-editor-content').innerHTML = ''
this._controllable = new Controllable(this)
this._disableable = new Disableable(this)
this._droppable = new Droppable(this)
this.$wire = this.closest('[wire\\:id]')?.__livewire.$wire;
+ }
+
+ initializeEditor(config) {
- this.editor = new Editor({
- element: this.querySelector('ui-editor-content'),
- content: content,
+ // Initialize the TipTap editor with the provided configuration
+ const defaultConfig = {
injectCSS: false,
editable: true,
editorProps: { // Prevent the default ProseMirror input event so we can dispatch our own...
handleDOMEvents: { input: (view, event) => { event.stopPropagation(); return true; } }
},
- extensions: [
- StarterKit.configure({
- history: true,
- paragraph: {
- keepOnSplit: false,
- allowNesting: true,
- },
- }),
- TextAlign.configure({
- types: ['paragraph', 'heading'],
- alignments: ['left', 'center', 'right'],
- }),
- Underline,
- Link.configure({
- openOnClick: false,
- linkOnPaste: true,
- }),
- Highlight,
- Subscript,
- Superscript,
- Placeholder.configure({
- placeholder: this.getAttribute('placeholder'),
- }),
- ],
onBlur: () => {
this.dispatchEvent(new Event('blur', {
bubbles: false,
cancelable: true,
}))
},
onUpdate: () => {
this._controllable.dispatch()
},
onCreate: ({ editor }) => {
editor.view.dom.setAttribute('data-slot', 'content')
+ // Dispatch a custom event to signal nested components such as the toolbar and commands that the editor is ready.
+ this.dispatchEvent(new CustomEvent('editor-ready', {
+ detail: { editor: this.editor },
+ bubbles: true, // so that children can catch it via this.closest()
+ }));
// Tiptap handles most keyboard shortcuts, however we need to implement link shortcuts...
this.addEventListener('keydown', e => {
let cmdIsHeld = e.metaKey || e.ctrlKey
if (cmdIsHeld && e.key.toLowerCase() === 'k') {
e.preventDefault(); e.stopPropagation();
this.querySelector('[data-editor="link"] [data-match-target]')?.click()
}
})
toolbar && initializeToolbar(editor, toolbar)
}
}
+ // merge default config with the provided config, so that the provided config can recursively override the default one
+ const mergedConfig = { ...defaultConfig, ...config }
+
+ this.editor = new Editor({
+ element: this.querySelector('ui-editor-content'),
+ content: this.content,
+ ...mergedConfig,
+ })
// This ensures that Tiptap doesn't emit an update event when the editor component is being booted. This
// fixes an issue with `wire:model.live` is dispatching an update request on editor load, and fixes the
// editor content being set to empty when Livewire and Alpine are manually bundled in `app.js`...
let shouldEmitUpdate = false
add // is a lifecycle method that is called when the element is added to the DOM.
async connectedCallback() {
// Gather all attributes into an object
const options = {};
for (const attr of this.attributes) {
options[attr.name] = attr.value;
}
const configSrc = this.getAttribute('src');
if (configSrc) {
try {
const module = await import(/* @vite-ignore */configSrc);
// Get the config factory function from the module.
const createConfig = module.default;
// Pass all attributes and get the config
const config = createConfig(options);
this.initializeEditor(config);
} catch (err) {
console.error('Error loading TipTap config:', err);
}
} else {
console.warn('No configuration source provided for tiptap-editor.');
}
} replace UIEditorContent in editor.js with this: class UIEditorContent extends UIElement {
boot() {
const parent = this.closest('ui-richeditor');
if (!parent) {
throw `ui-editor-content: no parent ui-richeditor element found`;
}
// Check if the editor instance is already set.
if (parent.editor) {
this.editor = parent.editor;
} else {
// Listen for the custom event when the editor becomes available.
parent.addEventListener('editor-ready', event => {
this.editor = event.detail.editor;
// You can also perform any further initialization here.
});
}
}
} Then the default.module.js or your custom config.module.js import Placeholder from '@tiptap/extension-placeholder'
import Superscript from '@tiptap/extension-superscript'
import TextAlign from '@tiptap/extension-text-align'
import Highlight from '@tiptap/extension-highlight'
import Underline from '@tiptap/extension-underline'
import Subscript from '@tiptap/extension-subscript'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
// options are all attributes set on the ui-editor tag.
export default function createConfig(options = {}) {
return {
// The array of extensions (TipTap expects functions/classes for extensions)
extensions: [
StarterKit.configure({
history: true,
paragraph: {
keepOnSplit: false,
allowNesting: true,
},
// issue "Error loading TipTap config: RangeError: Adding different instances of a keyed plugin (plugin$)
// therefore disabled Dropcursor and Gapcursor
dropcursor: false,
gapcursor: false,
}),
TextAlign.configure({
types: ['paragraph', 'heading'],
alignments: ['left', 'center', 'right'],
}),
Underline,
Link.configure({
openOnClick: false,
linkOnPaste: true,
}),
Highlight,
Subscript,
Superscript,
Placeholder.configure({
placeholder: options.placeholder || 'Type here...',
}),
]
};
} |
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.
The text was updated successfully, but these errors were encountered: