6f49dd892d
- no menu when no matching tags - insert a space after clicking a button - allow no space before mention
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { Editor, type JSONContent } from '@tiptap/core';
|
|
import { isButtonElement, isHTMLElement } from '@coldwired/utils';
|
|
import { z } from 'zod';
|
|
|
|
import { ApplicationController } from './application_controller';
|
|
import { getAction } from '../shared/tiptap/actions';
|
|
import { tagSchema, type TagSchema } from '../shared/tiptap/tags';
|
|
import { createEditor } from '../shared/tiptap/editor';
|
|
|
|
export class TiptapController extends ApplicationController {
|
|
static targets = ['editor', 'input', 'button', 'tag'];
|
|
static values = {
|
|
insertAfterTag: { type: String, default: '' }
|
|
};
|
|
|
|
declare editorTarget: Element;
|
|
declare inputTarget: HTMLInputElement;
|
|
declare buttonTargets: HTMLButtonElement[];
|
|
declare tagTargets: HTMLElement[];
|
|
declare insertAfterTagValue: string;
|
|
|
|
#initializing = true;
|
|
#editor?: Editor;
|
|
|
|
connect(): void {
|
|
this.#editor = createEditor({
|
|
editorElement: this.editorTarget,
|
|
content: this.content,
|
|
tags: this.tags,
|
|
buttons: this.menuButtons,
|
|
onChange: ({ editor }) => {
|
|
for (const button of this.buttonTargets) {
|
|
const action = getAction(editor, button);
|
|
button.classList.toggle('fr-btn--secondary', !action.isActive());
|
|
button.disabled = action.isDisabled();
|
|
}
|
|
|
|
const previousValue = this.inputTarget.value;
|
|
const value = JSON.stringify(editor.getJSON());
|
|
this.inputTarget.value = value;
|
|
|
|
// Dispatch input event only if the value has changed and not during initialization
|
|
if (this.#initializing) {
|
|
this.#initializing = false;
|
|
} else if (value != previousValue) {
|
|
this.dispatch('input', { target: this.inputTarget, prefix: '' });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
disconnect(): void {
|
|
this.#editor?.destroy();
|
|
}
|
|
|
|
menuButton(event: MouseEvent) {
|
|
if (this.#editor && isButtonElement(event.target)) {
|
|
getAction(this.#editor, event.target).run();
|
|
}
|
|
}
|
|
|
|
insertTag(event: MouseEvent) {
|
|
if (this.#editor && isHTMLElement(event.target)) {
|
|
const tag = tagSchema.parse(event.target.dataset);
|
|
const editor = this.#editor
|
|
.chain()
|
|
.focus()
|
|
.insertContent({ type: 'mention', attrs: tag });
|
|
|
|
if (this.insertAfterTagValue != '') {
|
|
editor.insertContent({ type: 'text', text: this.insertAfterTagValue });
|
|
}
|
|
editor.run();
|
|
}
|
|
}
|
|
|
|
private get content() {
|
|
const value = this.inputTarget.value;
|
|
if (value) {
|
|
return jsonContentSchema.parse(JSON.parse(value));
|
|
}
|
|
}
|
|
|
|
private get tags(): TagSchema[] {
|
|
return this.tagTargets.map((tag) => tagSchema.parse(tag.dataset));
|
|
}
|
|
|
|
private get menuButtons() {
|
|
return this.buttonTargets.map(
|
|
(menuButton) => menuButton.dataset.tiptapAction as string
|
|
);
|
|
}
|
|
}
|
|
|
|
const jsonContentSchema: z.ZodType<JSONContent> = z.object({
|
|
type: z.string().optional(),
|
|
text: z.string().optional(),
|
|
attrs: z.record(z.any()).optional(),
|
|
marks: z
|
|
.object({ type: z.string(), attrs: z.record(z.any()).optional() })
|
|
.array()
|
|
.optional(),
|
|
content: z.lazy(() => z.array(jsonContentSchema).optional())
|
|
});
|