Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "prosemirror-view";

export interface AraTiptapRenderedExtensionOptions {
uniqueId: string;
}

/**
* Plugin making links opening in a new window accessible by adding an invisible
* span (`<span class="fr-sr-only"> (nouvelle fenêtre)</span>"`)
*
* ## Note
* In the report, all links are converted to links openning in a new window.
*
* ## Warning
* Use this plugin in read-only mode only, as it creates an issue when cursor
* goes beyond the last character of the link:
* - editor acts as if there were an extra "ghost character"
* - cursor disappears when reaching that "ghost character"
*
* When the editor is editable, we decided to:
* - remove `target="_blank"`
* - not use this plugin (not necessary anymore)
* - not open links on click
* That way, it's easier to edit, with no extra icon and no issue with cursor.
*/
const accessibleLinkPlugin = new Plugin({
key: new PluginKey("linkDecoration"),

props: {
decorations: ({ doc }) => {
const decorations: Decoration[] = [];
// Walk through the document to find all link marks
doc.descendants((node, pos) => {
if (node.isText && node.marks) {
node.marks.forEach((mark) => {
if (mark.type.name === "link") {
// Find the end position of the link text
const linkEnd = pos + node.nodeSize;
// Create a decoration that adds the span after the link
const decoration = Decoration.widget(
linkEnd,
() => {
const span = document.createElement("span");
span.className = "fr-sr-only";
span.textContent = " (nouvelle fenêtre)";
return span;
},
{
side: -1, // to be inside the `<a>` element and not after
key: `link-decoration-${pos}` // Unique key for each decoration
}
);
decorations.push(decoration);
}
});
}
});
return DecorationSet.create(doc, decorations);
}
}
});

/**
* Extension AraTiptapRenderedExtension
*
* Tiptap extension for Ara specificities when editor is not editable:
* - make links openning in a new window accessible
*/
export const AraTiptapRenderedExtension =
Extension.create<AraTiptapRenderedExtensionOptions>({
name: "ara",
addProseMirrorPlugins() {
return [accessibleLinkPlugin];
}
});
4 changes: 2 additions & 2 deletions confiture-web-app/src/components/tiptap/TiptapEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Editor, EditorContent, useEditor } from "@tiptap/vue-3";
import { onBeforeUnmount, ShallowRef, watch } from "vue";

import { useUniqueId } from "../../composables/useUniqueId";
import { displayedHeadings, tiptapExtensions } from "./tiptap-extensions";
import { displayedHeadings, tiptapEditorExtensions } from "./tiptap-extensions";
import TiptapButton from "./TiptapButton.vue";

export interface Props {
Expand Down Expand Up @@ -88,7 +88,7 @@ const editor = useEditor({
},
editable: props.editable && !props.disabled,
content: getContent(),
extensions: tiptapExtensions,
extensions: tiptapEditorExtensions,
onUpdate({ editor }) {
// The content has changed.
emit("update:modelValue", JSON.stringify(editor.getJSON()));
Expand Down
4 changes: 2 additions & 2 deletions confiture-web-app/src/components/tiptap/TiptapRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Editor, EditorContent, useEditor } from "@tiptap/vue-3";
import hljs from "highlight.js";
import { computed, onMounted, ref, ShallowRef } from "vue";

import { tiptapExtensions } from "./tiptap-extensions";
import { tiptapRenderedExtensions } from "./tiptap-extensions";

const props = defineProps<{
document: string;
Expand All @@ -23,7 +23,7 @@ const editor = useEditor({
},
editable: false,
content: parsedDocument.value,
extensions: tiptapExtensions
extensions: tiptapRenderedExtensions
}) as ShallowRef<Editor>;

const contentRef = ref<HTMLDivElement>();
Expand Down
101 changes: 68 additions & 33 deletions confiture-web-app/src/components/tiptap/tiptap-extensions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
Extensions,
mergeAttributes,
textblockTypeInputRule
} from "@tiptap/core";
import { Extensions, textblockTypeInputRule } from "@tiptap/core";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import DropCursor from "@tiptap/extension-dropcursor";
import { Heading, type Level } from "@tiptap/extension-heading";
Expand All @@ -17,6 +13,8 @@ import html from "highlight.js/lib/languages/xml";
import { common, createLowlight } from "lowlight";
import { Markdown } from "tiptap-markdown";

import { AraTiptapRenderedExtension } from "./AraTiptapRenderedExtension";

// Define needed heading levels
export const displayedHeadings = [4, 5, 6] as Array<Level>;

Expand All @@ -28,7 +26,45 @@ lowlight.register("css", css);
lowlight.register("js", js);
lowlight.register("ts", ts);

export const tiptapExtensions: Extensions = [
const extendedLink = Link.extend({
addAttributes() {
// Default attributes are useful when pasting links in editor for example.
return {
...this.parent?.(),
// "class" is always reset
class: {
default: null,
renderHTML: () => {
return {
class: null
};
}
},
// "rel" is always reset to "noopener noreferrer"
rel: {
default: null,
renderHTML: () => {
return {
rel: "noopener noreferrer"
};
}
},
// "target" is reset to:
// - null (removed) when editing
// - "_blank" when rendered
target: {
default: null,
renderHTML: () => {
return {
target: this.options.HTMLAttributes.target
};
}
}
};
}
});

const commonExtensions: Extensions = [
Heading.extend({
// prevent all marks from being applied to headings
marks: "",
Expand Down Expand Up @@ -56,33 +92,6 @@ export const tiptapExtensions: Extensions = [
heading: false
}),
CodeBlockLowlight.configure({ lowlight, defaultLanguage: "html" }),
Link.extend({
addAttributes() {
return {
...this.parent?.(),
class: {
default: null,
renderHTML: () => {
return { class: null }; // reset class when copy pasting for example
}
},
title: {
default: null,
renderHTML: (attributes) => {
return {
title: attributes.title
};
}
}
};
},
renderHTML({ HTMLAttributes }) {
return ["a", mergeAttributes(HTMLAttributes), 0];
}
}).configure({
openOnClick: false,
defaultProtocol: "https"
}),
Typography.configure({
openDoubleQuote: "« ",
closeDoubleQuote: " »"
Expand All @@ -91,3 +100,29 @@ export const tiptapExtensions: Extensions = [
Image,
DropCursor.configure({ color: "var(--dsfr-outline)", width: 3 })
];

export const tiptapEditorExtensions: Extensions = [
...commonExtensions,
extendedLink.configure({
openOnClick: false,
defaultProtocol: "https",
shouldAutoLink: () => true,
HTMLAttributes: {
// Links do not open when editing, so not "new window"…
// Advantage: no extra icon when editing
target: null
}
})
];

export const tiptapRenderedExtensions: Extensions = [
...commonExtensions,
extendedLink.configure({
openOnClick: true,
HTMLAttributes: {
// Links open in a new window when displaying the editor in read-only mode
target: "_blank"
}
}),
...[AraTiptapRenderedExtension]
];
4 changes: 2 additions & 2 deletions confiture-web-app/src/components/tiptap/tiptap.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
}
}

.tiptap p {
vertical-align: middle;
.tiptap:not(.tiptap--rendered) [href] {
cursor: text;
}

.tiptap pre code {
Expand Down