Merge pull request #9719 from tchak/attestation-editor

feat(attestation): use tiptap editor controller
This commit is contained in:
Paul Chavard 2023-11-29 17:39:35 +00:00 committed by GitHub
commit a330118929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 727 additions and 265 deletions

View file

@ -1,16 +1,7 @@
#attestation-edit {
.mention {
border: 1px solid var(--text-default-grey);
padding: 8px;
}
.selected {
border: 1px solid #000000;
}
.tiptap {
padding: 8px;
min-height: 300px;
overflow-y: scroll;
min-height: 400px;
}
}

View file

@ -60,6 +60,10 @@
flex-shrink: 0;
}
.flex-gap-1 {
gap: $default-spacer;
}
.flex-gap-2 {
gap: 2 * $default-spacer;
}

View file

@ -24,11 +24,35 @@ module Administrateurs
end
def edit
@buttons = [
[
['Gras', 'bold', 'bold'],
['Italic', 'italic', 'italic'],
['Souligner', 'underline', 'underline']
],
[
['Titre', 'title', 'h-1'],
['Sous titre', 'heading2', 'h-2'],
['Titre de section', 'heading3', 'h-3']
],
[
['Liste à puces', 'bulletList', 'list-unordered'],
['Liste numérotée', 'orderedList', 'list-ordered']
],
[
['Aligner à gauche', 'left', 'align-left'],
['Aligner au centre', 'center', 'align-center'],
['Aligner à droite', 'right', 'align-right']
],
[
['Undo', 'undo', 'arrow-go-back-line'],
['Redo', 'redo', 'arrow-go-forward-line']
]
]
end
def update
@attestation_template
.update(json_body: editor_params)
@attestation_template.update!(editor_params)
end
private
@ -42,7 +66,7 @@ module Administrateurs
end
def editor_params
params.permit(content: [:type, content: [:type, :text, attrs: [:id, :label]]])
params.required(:attestation_template).permit(:tiptap_body)
end
end
end

View file

@ -1,185 +0,0 @@
import { ApplicationController } from './application_controller';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Mention from '@tiptap/extension-mention';
import tippy, { type Instance } from 'tippy.js';
import { httpRequest } from '@utils';
export class AttestationController extends ApplicationController {
static values = {
tags: Array,
url: String
};
static targets = ['editor', 'bold'];
declare readonly tagsValue: string[];
declare readonly urlValue: string;
declare editor: Editor;
declare editorTarget: HTMLElement;
declare boldTarget: HTMLButtonElement;
connect() {
const conf = {
element: this.editorTarget,
editorProps: {
attributes: {
class: 'fr-input'
}
},
extensions: [
StarterKit,
Mention.configure({
HTMLAttributes: {
class: 'mention'
},
suggestion: {
items: ({ query }) => {
return this.tagsValue
.filter((item) =>
item.toLowerCase().startsWith(query.toLowerCase())
)
.slice(0, 5);
},
render: () => {
let popup: Instance;
let div: HTMLElement;
let selectedIndex = 0;
let items: string[];
let command: (props: object) => void;
const makeList = () => {
return items
.map((item, i) => {
if (i == selectedIndex) {
return `<li class='selected'>${item}</li>`;
} else {
return `<li>${item}</li>`;
}
})
.join('');
};
return {
onStart: (props) => {
items = props.items;
command = props.command;
div = document.createElement('UL');
div.innerHTML = makeList();
if (!props.clientRect) {
return;
}
popup = tippy(document.body, {
getReferenceClientRect: () => {
const domrect = props.clientRect?.();
if (!domrect) {
throw new Error('No client rect');
}
return domrect;
},
appendTo: () => this.element,
content: div,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start'
});
},
onUpdate(props) {
command = props.command;
items = props.items;
div.innerHTML = makeList();
if (!props.clientRect) {
return;
}
popup.setProps({
getReferenceClientRect: () => {
const domrect = props.clientRect?.();
if (!domrect) {
throw new Error('No client rect');
}
return domrect;
}
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup.hide();
return true;
}
if (props.event.key === 'ArrowDown') {
selectedIndex = (selectedIndex + 1) % items.length;
div.innerHTML = makeList();
return true;
}
if (props.event.key === 'ArrowUp') {
selectedIndex =
(selectedIndex + items.length - 1) % items.length;
div.innerHTML = makeList();
return true;
}
if (props.event.key === 'Enter') {
const item = items[selectedIndex];
if (item) {
command({ id: item });
}
return true;
}
return false;
},
onExit() {
popup.destroy();
div.remove();
}
};
}
}
})
],
content:
'<p>La situation de M. <span data-type="mention" data-id="nom"></span> dont la demande de logement social</p>'
};
this.editor = new Editor(conf);
this.editor.on('transaction', () => {
this.boldTarget.disabled = !this.editor
.can()
.chain()
.focus()
.toggleBold()
.run();
if (this.editor.isActive('bold')) {
this.boldTarget.classList.add('fr-btn--secondary');
} else {
this.boldTarget.classList.remove('fr-btn--secondary');
}
});
}
bold() {
this.editor.chain().focus().toggleBold().run();
}
send() {
const json = this.editor.getJSON();
httpRequest(this.urlValue, { method: 'put', json }).json();
}
}

View file

@ -43,7 +43,8 @@ export class AutosubmitController extends ApplicationController {
matchInputElement(target, {
date: () => {},
inputable: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY)
inputable: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY),
hidden: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY)
});
}

View file

@ -0,0 +1,96 @@
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'];
declare editorTarget: Element;
declare inputTarget: HTMLInputElement;
declare buttonTargets: HTMLButtonElement[];
declare tagTargets: HTMLElement[];
#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);
this.#editor
.chain()
.focus()
.insertContent({ type: 'mention', attrs: tag })
.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())
});

View file

@ -0,0 +1,116 @@
import { Editor } from '@tiptap/core';
import { z } from 'zod';
type EditorAction = {
run(): void;
isActive(): boolean;
isDisabled(): boolean;
};
export function getAction(
editor: Editor,
button: HTMLButtonElement
): EditorAction {
return tiptapActionSchema.parse(button.dataset)(editor);
}
const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
title: (editor) => ({
run: () => editor.chain().focus(),
isActive: () => editor.isActive('title'),
isDisabled: () => !editor.isActive('title')
}),
heading2: (editor) => ({
run: () => editor.chain().focus().setHeading({ level: 2 }).run(),
isActive: () => editor.isActive('heading', { level: 2 }),
isDisabled: () =>
editor.isActive('title') ||
editor.isActive('header') ||
editor.isActive('footer')
}),
heading3: (editor) => ({
run: () => editor.chain().focus().setHeading({ level: 3 }).run(),
isActive: () => editor.isActive('heading', { level: 3 }),
isDisabled: () =>
editor.isActive('title') ||
editor.isActive('header') ||
editor.isActive('footer')
}),
bold: (editor) => ({
run: () => editor.chain().focus().toggleBold().run(),
isActive: () => editor.isActive('bold'),
isDisabled: () => editor.isActive('heading') || editor.isActive('title')
}),
italic: (editor) => ({
run: () => editor.chain().focus().toggleItalic().run(),
isActive: () => editor.isActive('italic'),
isDisabled: () => false
}),
underline: (editor) => ({
run: () => editor.chain().focus().toggleUnderline().run(),
isActive: () => editor.isActive('underline'),
isDisabled: () => false
}),
strike: (editor) => ({
run: () => editor.chain().focus().toggleStrike().run(),
isActive: () => editor.isActive('strike'),
isDisabled: () => editor.isActive('heading') || editor.isActive('title')
}),
highlight: (editor) => ({
run: () => editor.chain().focus().toggleHighlight().run(),
isActive: () => editor.isActive('highlight'),
isDisabled: () => editor.isActive('heading') || editor.isActive('title')
}),
bulletList: (editor) => ({
run: () => editor.chain().focus().toggleBulletList().run(),
isActive: () => editor.isActive('bulletList'),
isDisabled: () =>
editor.isActive('title') ||
editor.isActive('header') ||
editor.isActive('footer')
}),
orderedList: (editor) => ({
run: () => editor.chain().focus().toggleOrderedList().run(),
isActive: () => editor.isActive('orderedList'),
isDisabled: () =>
editor.isActive('title') ||
editor.isActive('header') ||
editor.isActive('footer')
}),
left: (editor) => ({
run: () => editor.chain().focus().setTextAlign('left').run(),
isActive: () => editor.isActive({ textAlign: 'left' }),
isDisabled: () => false
}),
center: (editor) => ({
run: () => editor.chain().focus().setTextAlign('center').run(),
isActive: () => editor.isActive({ textAlign: 'center' }),
isDisabled: () => false
}),
right: (editor) => ({
run: () => editor.chain().focus().setTextAlign('right').run(),
isActive: () => editor.isActive({ textAlign: 'right' }),
isDisabled: () => false
}),
justify: (editor) => ({
run: () => editor.chain().focus().setTextAlign('justify').run(),
isActive: () => editor.isActive({ textAlign: 'justify' }),
isDisabled: () => false
}),
undo: (editor) => ({
run: () => editor.chain().focus().undo().run(),
isActive: () => false,
isDisabled: () => !editor.can().chain().focus().undo().run()
}),
redo: (editor) => ({
run: () => editor.chain().focus().redo().run(),
isActive: () => false,
isDisabled: () => !editor.can().chain().focus().redo().run()
})
};
const tiptapActionSchema = z
.object({
tiptapAction: z.enum(Object.keys(EDITOR_ACTIONS) as [string, ...string[]])
})
.transform(({ tiptapAction }) => EDITOR_ACTIONS[tiptapAction]);

View file

@ -0,0 +1,139 @@
import Document from '@tiptap/extension-document';
import Hystory from '@tiptap/extension-history';
import TextAlign from '@tiptap/extension-text-align';
import Gapcursor from '@tiptap/extension-gapcursor';
import Paragraph from '@tiptap/extension-paragraph';
import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
import ListItem from '@tiptap/extension-list-item';
import Text from '@tiptap/extension-text';
import Highlight from '@tiptap/extension-highlight';
import Underline from '@tiptap/extension-underline';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import Strike from '@tiptap/extension-strike';
import Mention from '@tiptap/extension-mention';
import Typography from '@tiptap/extension-typography';
import Heading from '@tiptap/extension-heading';
import {
Editor,
type EditorOptions,
type JSONContent,
type Extensions
} from '@tiptap/core';
import {
DocumentWithHeader,
Title,
Header,
Footer,
HeaderColumn
} from './nodes';
import { createSuggestionMenu, type TagSchema } from './tags';
export function createEditor({
editorElement,
content,
tags,
buttons,
onChange
}: {
editorElement: Element;
content?: JSONContent;
tags: TagSchema[];
buttons: string[];
onChange(change: { editor: Editor }): void;
}): Editor {
const options = getEditorOptions(editorElement, tags, buttons, content);
const editor = new Editor(options);
editor.on('transaction', onChange);
return editor;
}
function getEditorOptions(
element: Element,
tags: TagSchema[],
actions: string[],
content?: JSONContent
): Partial<EditorOptions> {
const extensions: Extensions = [];
for (const action of actions) {
switch (action) {
case 'bold':
extensions.push(Bold);
break;
case 'italic':
extensions.push(Italic);
break;
case 'underline':
extensions.push(Underline);
break;
case 'strike':
extensions.push(Strike);
break;
case 'highlight':
extensions.push(Highlight);
break;
case 'bulletList':
extensions.push(BulletList);
break;
case 'orderedList':
extensions.push(OrderedList);
break;
case 'left':
case 'center':
case 'right':
case 'justify':
extensions.push(
TextAlign.configure({
types: actions.includes('title')
? ['headerColumn', 'title', 'footer', 'heading', 'paragraph']
: ['heading', 'paragraph']
})
);
break;
case 'title':
extensions.push(Header, HeaderColumn, Title, Footer);
break;
case 'heading2':
case 'heading3':
extensions.push(Heading.configure({ levels: [2, 3] }));
break;
}
}
if (actions.includes('bulletList') || actions.includes('orderedList')) {
extensions.push(ListItem);
}
if (tags.length > 0) {
extensions.push(
Mention.configure({
renderLabel({ node }) {
return `--${node.attrs.label}--`;
},
HTMLAttributes: {
class: 'fr-badge fr-badge--sm fr-badge--info fr-badge--no-icon'
},
suggestion: createSuggestionMenu(tags, element)
})
);
}
return {
element,
content,
editorProps: { attributes: { class: 'fr-input' } },
extensions: [
actions.includes('title') ? DocumentWithHeader : Document,
Hystory,
Typography,
Gapcursor,
Paragraph,
Text,
...extensions
]
};
}

View file

@ -0,0 +1,64 @@
import { Node, mergeAttributes } from '@tiptap/core';
export const DocumentWithHeader = Node.create({
name: 'doc',
topNode: true,
content: 'header title block+ footer'
});
export const Title = Node.create({
name: 'title',
content: 'inline*',
defining: true,
marks: 'italic underline',
parseHTML() {
return [{ tag: `h1`, attrs: { level: 1 } }];
},
renderHTML({ HTMLAttributes }) {
return ['h1', HTMLAttributes, 0];
}
});
export const Header = Node.create({
name: 'header',
content: 'headerColumn headerColumn',
defining: true,
parseHTML() {
return [{ tag: `header` }];
},
renderHTML({ HTMLAttributes }) {
return [
'header',
mergeAttributes(HTMLAttributes, { class: 'header flex' }),
0
];
}
});
export const Footer = Node.create({
name: 'footer',
content: 'paragraph+',
defining: true,
parseHTML() {
return [{ tag: `footer` }];
},
renderHTML({ HTMLAttributes }) {
return ['footer', mergeAttributes(HTMLAttributes, { class: 'footer' }), 0];
}
});
export const HeaderColumn = Node.create({
name: 'headerColumn',
content: 'paragraph',
defining: true,
parseHTML() {
return [{ tag: `div` }];
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes, { class: 'flex-1' }), 0];
}
});

View file

@ -0,0 +1,173 @@
import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion';
import { z } from 'zod';
import tippy, { type Instance as TippyInstance } from 'tippy.js';
import { matchSorter } from 'match-sorter';
export const tagSchema = z
.object({ tagLabel: z.string(), tagId: z.string() })
.transform(({ tagId, tagLabel }) => ({ label: tagLabel, id: tagId }));
export type TagSchema = z.infer<typeof tagSchema>;
class SuggestionMenu {
#selectedIndex = 0;
#props: SuggestionProps<TagSchema>;
#element?: Element;
#popup?: TippyInstance;
constructor(props: SuggestionProps<TagSchema>, editorElement: Element) {
this.#props = props;
this.render();
this.init(editorElement);
}
init(editorElement: Element) {
if (!this.#props.clientRect) {
return;
}
this.#popup = tippy(document.body, {
getReferenceClientRect: () => {
const domRect = this.#props?.clientRect?.();
if (!domRect) {
throw new Error('domRect is null');
}
return domRect;
},
appendTo: editorElement,
content: this.#element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start'
});
}
update(props: SuggestionProps<TagSchema>) {
this.#props = props;
if (!this.#props.clientRect) {
return;
}
this.#popup?.setProps({
getReferenceClientRect: () => {
const domRect = props.clientRect?.();
if (!domRect) {
throw new Error('domRect is null');
}
return domRect;
}
});
this.render();
}
onKeyDown(key: string) {
switch (key) {
case 'ArrowDown':
this.down();
return true;
case 'ArrowUp':
this.up();
return true;
case 'Escape':
this.escape();
return true;
case 'Enter':
this.enter();
return true;
}
return false;
}
destroy() {
this.#popup?.destroy();
this.#element?.remove();
}
private render() {
this.#element ??= this.createMenu();
const list = this.#element.firstChild as HTMLUListElement;
const html = this.#props.items
.map((item, i) => {
return `<li class="fr-badge fr-badge--sm fr-badge--no-icon${
i == this.#selectedIndex ? ' fr-badge--info' : ''
}">${item.label}</li>`;
})
.join('');
this.#element.classList.add('fr-menu__list');
list.innerHTML = html;
list.querySelector<HTMLElement>('.selected')?.focus();
}
private createMenu() {
const menu = document.createElement('div');
const list = document.createElement('ul');
menu.classList.add('fr-menu');
list.classList.add('fr-menu__list');
menu.appendChild(list);
return menu;
}
private up() {
this.#selectedIndex =
(this.#selectedIndex + this.#props.items.length - 1) %
this.#props.items.length;
this.render();
}
private down() {
this.#selectedIndex = (this.#selectedIndex + 1) % this.#props.items.length;
this.render();
}
private enter() {
const item = this.#props.items[this.#selectedIndex];
if (item) {
this.#props.command(item);
}
}
private escape() {
this.#popup?.hide();
this.#selectedIndex = 0;
}
}
export function createSuggestionMenu(
tags: TagSchema[],
editorElement: Element
): Omit<SuggestionOptions<TagSchema>, 'editor'> {
return {
char: '@',
items: ({ query }) => {
return matchSorter(tags, query, { keys: ['label'] }).slice(0, 6);
},
render: () => {
let menu: SuggestionMenu;
return {
onStart: (props) => {
menu = new SuggestionMenu(props, editorElement);
},
onUpdate(props) {
menu.update(props);
},
onKeyDown(props) {
return menu.onKeyDown(props.event.key);
},
onExit() {
menu.destroy();
}
};
}
};
}

View file

@ -99,6 +99,14 @@ class AttestationTemplate < ApplicationRecord
signature.attached? ? signature.filename : nil
end
def tiptap_body
json_body&.to_json
end
def tiptap_body=(json)
self.json_body = JSON.parse(json)
end
private
def signature_to_render(groupe_instructeur)

View file

@ -14,8 +14,16 @@ class TiptapService
def node_to_html(node, tags)
case node
in type: 'header', content:
"<header>#{children(content, tags)}</header>"
in type: 'footer', content:, **rest
"<footer#{text_align(rest[:attrs])}>#{children(content, tags)}</footer>"
in type: 'headerColumn', content:, **rest
"<div#{text_align(rest[:attrs])} class=\"column\">#{children(content, tags)}</div>"
in type: 'paragraph', content:, **rest
"<p#{text_align(rest[:attrs])}>#{children(content, tags)}</p>"
in type: 'title', content:, **rest
"<h1#{text_align(rest[:attrs])}>#{children(content, tags)}</h1>"
in type: 'heading', attrs: { level:, **attrs }, content:
"<h#{level}#{text_align(attrs)}>#{children(content, tags)}</h#{level}>"
in type: 'bulletList', content:

View file

@ -1,10 +1,16 @@
#attestation-edit.fr-container.mt-2{
data: {
controller: 'attestation',
attestation_tags_value: @attestation_template.tags.map { _1[:libelle] },
attestation_url_value: admin_procedure_attestation_template_v2_path(@procedure)} }
#attestation-edit.fr-container.mt-2{ data: { controller: 'tiptap' } }
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), data: { turbo: 'true', controller: 'autosubmit' } do |form|
.flex.flex-gap-2
- @buttons.each do |buttons|
.flex.flex-gap-1
- buttons.each do |(label, action, icon)|
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label
%button.fr-btn{ data: { action: 'click->attestation#bold', attestation_target: 'bold' } } Gras
.editor.mt-2{ data: { attestation_target: 'editor' } }
.editor.mt-2{ data: { tiptap_target: 'editor' } }
= form.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
%button.fr-btn.mt-2{ data: { action: 'click->attestation#send' } } Envoyer
%ul.mt-2.flex.wrap.flex-gap-1
- @attestation_template.tags.each do |tag|
%li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } }
= tag[:libelle]

View file

@ -20,9 +20,25 @@
"@sentry/browser": "7.66.0",
"@stimulus/polyfills": "^2.0.0",
"@tiptap/core": "^2.1.12",
"@tiptap/extension-bold": "^2.1.12",
"@tiptap/extension-bullet-list": "^2.1.12",
"@tiptap/extension-document": "^2.1.12",
"@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-highlight": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-italic": "^2.1.12",
"@tiptap/extension-link": "^2.1.12",
"@tiptap/extension-list-item": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-ordered-list": "^2.1.12",
"@tiptap/extension-paragraph": "^2.1.12",
"@tiptap/extension-strike": "^2.1.12",
"@tiptap/extension-text": "^2.1.12",
"@tiptap/extension-text-align": "^2.1.12",
"@tiptap/extension-typography": "^2.1.12",
"@tiptap/extension-underline": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"@tiptap/suggestion": "^2.1.12",
"@tmcw/togeojson": "^5.6.0",
"chartkick": "^5.0.1",

View file

@ -4,6 +4,23 @@ RSpec.describe TiptapService do
{
type: 'doc',
content: [
{
type: 'header',
content: [
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Left' }]
},
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Right' }]
}
]
},
{
type: 'title',
content: [{ type: 'text', text: 'Title' }]
},
{
type: 'paragraph',
attrs: { textAlign: 'right' },
@ -118,6 +135,10 @@ RSpec.describe TiptapService do
]
}
]
},
{
type: 'footer',
content: [{ type: 'text', text: 'Footer' }]
}
]
}
@ -125,13 +146,16 @@ RSpec.describe TiptapService do
let(:tags) { { 'name' => 'Paul' } }
let(:html) do
[
'<header><div class="column">Left</div><div class="column">Right</div></header>',
'<h1>Title</h1>',
'<p style="text-align: right">Hello world!</p>',
'<p><s><em>Bonjour </em></s><u><strong>Paul</strong></u> <mark>!</mark></p>',
'<h1>Heading 1</h1>',
'<h2 style="text-align: center">Heading 2</h2>',
'<h3>Heading 3</h3>',
'<ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul>',
'<ol><li><p>Item 1</p></li><li><p>Item 2</p></li></ol>'
'<ol><li><p>Item 1</p></li><li><p>Item 2</p></li></ol>',
'<footer>Footer</footer>'
].join
end

View file

@ -2286,11 +2286,6 @@
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.12.tgz#904fdf147e91b5e60561c76e7563c1b5a32f54ab"
integrity sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==
"@tiptap/extension-blockquote@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.1.12.tgz#97b43419606acf9bfd93b9f482a1827dcac8c3e9"
integrity sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg==
"@tiptap/extension-bold@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.1.12.tgz#5dbf41105fc0fbde8adbff629312187fbebc39b0"
@ -2301,56 +2296,43 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz#7c905a577ce30ef2cb335870a23f9d24fd26f6aa"
integrity sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==
"@tiptap/extension-code-block@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.12.tgz#20416baef1b5fc839490a8416e97fdcbb5fdf918"
integrity sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==
"@tiptap/extension-code@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.12.tgz#86d2eb5f63725af472c5fd858e5a9c7ccae06ef3"
integrity sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw==
"@tiptap/extension-document@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.12.tgz#e19e4716dfad60cbeb6abaf2f362fed759963529"
integrity sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==
"@tiptap/extension-dropcursor@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.1.12.tgz#9da0c275291c9d47497d3db41b4d70d96366b4ff"
integrity sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A==
"@tiptap/extension-gapcursor@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.12.tgz#63844c3abd1a38af915839cf0c097b6d2e5a86fe"
integrity sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==
"@tiptap/extension-hard-break@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.1.12.tgz#54d0c9996e1173594852394975a9356eec98bc9a"
integrity sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw==
"@tiptap/extension-heading@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.1.12.tgz#05ae4684d6f29ae611495ab114038e14a5d1dff6"
integrity sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==
"@tiptap/extension-highlight@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.1.12.tgz#184efb75238c9cbc6c18d523b735de4329f78ecc"
integrity sha512-buen31cYPyiiHA2i0o2i/UcjRTg/42mNDCizGr1OJwvv3AELG3qOFc4Y58WJWIvWNv+1Dr4ZxHA3GNVn0ANWyg==
"@tiptap/extension-history@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.1.12.tgz#03bcb9422e8ea2b82dc45207d1a1b0bc0241b055"
integrity sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==
"@tiptap/extension-horizontal-rule@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.12.tgz#2191d4ff68ed39381d65971ad8e2aa1be43e6d6b"
integrity sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A==
"@tiptap/extension-italic@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.12.tgz#e99480eb77f8b4e5444fc236add8a831d5aa2343"
integrity sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==
"@tiptap/extension-link@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.12.tgz#a18f83a0b54342e6274ff9e5a5907ef7f15aa723"
integrity sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ==
dependencies:
linkifyjs "^4.1.0"
"@tiptap/extension-list-item@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.12.tgz#3eb28dc998490a98f14765783770b3cf6587d39e"
@ -2376,11 +2358,26 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.12.tgz#2b049aedf2985e9c9e3c3f1cc0b203a574c85bd8"
integrity sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==
"@tiptap/extension-text-align@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.1.12.tgz#962dca4d284ce57ed345fd4b94ddb3a97944e0d1"
integrity sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ==
"@tiptap/extension-text@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.12.tgz#466e3244bdd9b2db2304c0c9a1d51ce59f5327d0"
integrity sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==
"@tiptap/extension-typography@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-typography/-/extension-typography-2.1.12.tgz#0bf3ba500b49b4f239205e76dda512bda7d3b4f7"
integrity sha512-OFkQHmUbKQwVO0b/NP2MLuuqQIWxw/gHaWQF/atgZf3mG0YDV2x3P/u+RBpKnsIujPZFvoEBRJGnstvEAB7zfQ==
"@tiptap/extension-underline@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.1.12.tgz#abd59c4b6c8434dbadb4ff9bff23eefcc6bc095e"
integrity sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==
"@tiptap/pm@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.12.tgz#88a4b19be0eabb13d42ddd540c19ba1bbe74b322"
@ -2405,31 +2402,6 @@
prosemirror-transform "^1.7.0"
prosemirror-view "^1.28.2"
"@tiptap/starter-kit@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.1.12.tgz#2bf28091ed08dc8f7b903ba92925e4ffe06257ea"
integrity sha512-+RoP1rWV7rSCit2+3wl2bjvSRiePRJE/7YNKbvH8Faz/+AMO23AFegHoUFynR7U0ouGgYDljGkkj35e0asbSDA==
dependencies:
"@tiptap/core" "^2.1.12"
"@tiptap/extension-blockquote" "^2.1.12"
"@tiptap/extension-bold" "^2.1.12"
"@tiptap/extension-bullet-list" "^2.1.12"
"@tiptap/extension-code" "^2.1.12"
"@tiptap/extension-code-block" "^2.1.12"
"@tiptap/extension-document" "^2.1.12"
"@tiptap/extension-dropcursor" "^2.1.12"
"@tiptap/extension-gapcursor" "^2.1.12"
"@tiptap/extension-hard-break" "^2.1.12"
"@tiptap/extension-heading" "^2.1.12"
"@tiptap/extension-history" "^2.1.12"
"@tiptap/extension-horizontal-rule" "^2.1.12"
"@tiptap/extension-italic" "^2.1.12"
"@tiptap/extension-list-item" "^2.1.12"
"@tiptap/extension-ordered-list" "^2.1.12"
"@tiptap/extension-paragraph" "^2.1.12"
"@tiptap/extension-strike" "^2.1.12"
"@tiptap/extension-text" "^2.1.12"
"@tiptap/suggestion@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.12.tgz#a13782d1e625ec03b3f61b6839ecc95b6b685d3f"
@ -5213,6 +5185,11 @@ linkify-it@^4.0.1:
dependencies:
uc.micro "^1.0.1"
linkifyjs@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.2.tgz#48fadb05ddf5a5f7065510a385a500ca1ac4e65e"
integrity sha512-1elJrH8MwUgr77Rgmx4JgB/nBgISYVoGossH6pAfCeHG+07TblTn6RWKx0MKozEMJU6NCFYHRih9M8ZtV3YZ+Q==
local-pkg@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963"