Skip to content

Commit edcf2ca

Browse files
authored
983 lajout de lextension custom tiptap ara (#1132)
* feat(tiptap): make links accessible * when editing: no `target="_blank"`, no icon, no extra span * when not editable: `target="blank"` forced, icon + extra hidden span " (nouvelle fenêtre) * chore(tiptap): remove useless CSS rule * chore(tiptap): fix names and comments * chore: lint * Update CHANGELOG.md
1 parent f99892a commit edcf2ca

6 files changed

Lines changed: 157 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
<h2 class="fr-sr-only" id="2025">2025</h2>
66

7+
### 26/06/2025
8+
9+
#### <span aria-hidden="true">🐛</span> Corrections
10+
11+
- Améliore l’accessibilité des liens affichés dans le rapport d’audit (ceux ajoutés par l’utilisateur dans les éditeurs riches lors de l’audit) ([#1132](https://github.com/DISIC/Ara/pull/1132))
12+
713
### 18/06/2025
814

915
#### <span aria-hidden="true">🚀</span> Nouvelles fonctionnalités
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Extension } from "@tiptap/core";
2+
import { Plugin, PluginKey } from "@tiptap/pm/state";
3+
import { Decoration, DecorationSet } from "prosemirror-view";
4+
5+
export interface AraTiptapRenderedExtensionOptions {
6+
uniqueId: string;
7+
}
8+
9+
/**
10+
* Plugin making links opening in a new window accessible by adding an invisible
11+
* span (`<span class="fr-sr-only"> (nouvelle fenêtre)</span>"`)
12+
*
13+
* ## Note
14+
* In the report, all links are converted to links openning in a new window.
15+
*
16+
* ## Warning
17+
* Use this plugin in read-only mode only, as it creates an issue when cursor
18+
* goes beyond the last character of the link:
19+
* - editor acts as if there were an extra "ghost character"
20+
* - cursor disappears when reaching that "ghost character"
21+
*
22+
* When the editor is editable, we decided to:
23+
* - remove `target="_blank"`
24+
* - not use this plugin (not necessary anymore)
25+
* - not open links on click
26+
* That way, it's easier to edit, with no extra icon and no issue with cursor.
27+
*/
28+
const accessibleLinkPlugin = new Plugin({
29+
key: new PluginKey("linkDecoration"),
30+
31+
props: {
32+
decorations: ({ doc }) => {
33+
const decorations: Decoration[] = [];
34+
// Walk through the document to find all link marks
35+
doc.descendants((node, pos) => {
36+
if (node.isText && node.marks) {
37+
node.marks.forEach((mark) => {
38+
if (mark.type.name === "link") {
39+
// Find the end position of the link text
40+
const linkEnd = pos + node.nodeSize;
41+
// Create a decoration that adds the span after the link
42+
const decoration = Decoration.widget(
43+
linkEnd,
44+
() => {
45+
const span = document.createElement("span");
46+
span.className = "fr-sr-only";
47+
span.textContent = " (nouvelle fenêtre)";
48+
return span;
49+
},
50+
{
51+
side: -1, // to be inside the `<a>` element and not after
52+
key: `link-decoration-${pos}` // Unique key for each decoration
53+
}
54+
);
55+
decorations.push(decoration);
56+
}
57+
});
58+
}
59+
});
60+
return DecorationSet.create(doc, decorations);
61+
}
62+
}
63+
});
64+
65+
/**
66+
* Extension AraTiptapRenderedExtension
67+
*
68+
* Tiptap extension for Ara specificities when editor is not editable:
69+
* - make links openning in a new window accessible
70+
*/
71+
export const AraTiptapRenderedExtension =
72+
Extension.create<AraTiptapRenderedExtensionOptions>({
73+
name: "ara",
74+
addProseMirrorPlugins() {
75+
return [accessibleLinkPlugin];
76+
}
77+
});

confiture-web-app/src/components/tiptap/TiptapEditor.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Editor, EditorContent, useEditor } from "@tiptap/vue-3";
44
import { onBeforeUnmount, ShallowRef, watch } from "vue";
55
66
import { useUniqueId } from "../../composables/useUniqueId";
7-
import { displayedHeadings, tiptapExtensions } from "./tiptap-extensions";
7+
import { displayedHeadings, tiptapEditorExtensions } from "./tiptap-extensions";
88
import TiptapButton from "./TiptapButton.vue";
99
1010
export interface Props {
@@ -88,7 +88,7 @@ const editor = useEditor({
8888
},
8989
editable: props.editable && !props.disabled,
9090
content: getContent(),
91-
extensions: tiptapExtensions,
91+
extensions: tiptapEditorExtensions,
9292
onUpdate({ editor }) {
9393
// The content has changed.
9494
emit("update:modelValue", JSON.stringify(editor.getJSON()));

confiture-web-app/src/components/tiptap/TiptapRenderer.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Editor, EditorContent, useEditor } from "@tiptap/vue-3";
33
import hljs from "highlight.js";
44
import { computed, onMounted, ref, ShallowRef } from "vue";
55
6-
import { tiptapExtensions } from "./tiptap-extensions";
6+
import { tiptapRenderedExtensions } from "./tiptap-extensions";
77
88
const props = defineProps<{
99
document: string;
@@ -23,7 +23,7 @@ const editor = useEditor({
2323
},
2424
editable: false,
2525
content: parsedDocument.value,
26-
extensions: tiptapExtensions
26+
extensions: tiptapRenderedExtensions
2727
}) as ShallowRef<Editor>;
2828
2929
const contentRef = ref<HTMLDivElement>();
Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
Extensions,
3-
mergeAttributes,
4-
textblockTypeInputRule
5-
} from "@tiptap/core";
1+
import { Extensions, textblockTypeInputRule } from "@tiptap/core";
62
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
73
import DropCursor from "@tiptap/extension-dropcursor";
84
import { Heading, type Level } from "@tiptap/extension-heading";
@@ -17,6 +13,8 @@ import html from "highlight.js/lib/languages/xml";
1713
import { common, createLowlight } from "lowlight";
1814
import { Markdown } from "tiptap-markdown";
1915

16+
import { AraTiptapRenderedExtension } from "./AraTiptapRenderedExtension";
17+
2018
// Define needed heading levels
2119
export const displayedHeadings = [4, 5, 6] as Array<Level>;
2220

@@ -28,7 +26,45 @@ lowlight.register("css", css);
2826
lowlight.register("js", js);
2927
lowlight.register("ts", ts);
3028

31-
export const tiptapExtensions: Extensions = [
29+
const extendedLink = Link.extend({
30+
addAttributes() {
31+
// Default attributes are useful when pasting links in editor for example.
32+
return {
33+
...this.parent?.(),
34+
// "class" is always reset
35+
class: {
36+
default: null,
37+
renderHTML: () => {
38+
return {
39+
class: null
40+
};
41+
}
42+
},
43+
// "rel" is always reset to "noopener noreferrer"
44+
rel: {
45+
default: null,
46+
renderHTML: () => {
47+
return {
48+
rel: "noopener noreferrer"
49+
};
50+
}
51+
},
52+
// "target" is reset to:
53+
// - null (removed) when editing
54+
// - "_blank" when rendered
55+
target: {
56+
default: null,
57+
renderHTML: () => {
58+
return {
59+
target: this.options.HTMLAttributes.target
60+
};
61+
}
62+
}
63+
};
64+
}
65+
});
66+
67+
const commonExtensions: Extensions = [
3268
Heading.extend({
3369
// prevent all marks from being applied to headings
3470
marks: "",
@@ -56,33 +92,6 @@ export const tiptapExtensions: Extensions = [
5692
heading: false
5793
}),
5894
CodeBlockLowlight.configure({ lowlight, defaultLanguage: "html" }),
59-
Link.extend({
60-
addAttributes() {
61-
return {
62-
...this.parent?.(),
63-
class: {
64-
default: null,
65-
renderHTML: () => {
66-
return { class: null }; // reset class when copy pasting for example
67-
}
68-
},
69-
title: {
70-
default: null,
71-
renderHTML: (attributes) => {
72-
return {
73-
title: attributes.title
74-
};
75-
}
76-
}
77-
};
78-
},
79-
renderHTML({ HTMLAttributes }) {
80-
return ["a", mergeAttributes(HTMLAttributes), 0];
81-
}
82-
}).configure({
83-
openOnClick: false,
84-
defaultProtocol: "https"
85-
}),
8695
Typography.configure({
8796
openDoubleQuote: "« ",
8897
closeDoubleQuote: " »"
@@ -91,3 +100,29 @@ export const tiptapExtensions: Extensions = [
91100
Image,
92101
DropCursor.configure({ color: "var(--dsfr-outline)", width: 3 })
93102
];
103+
104+
export const tiptapEditorExtensions: Extensions = [
105+
...commonExtensions,
106+
extendedLink.configure({
107+
openOnClick: false,
108+
defaultProtocol: "https",
109+
shouldAutoLink: () => true,
110+
HTMLAttributes: {
111+
// Links do not open when editing, so not "new window"…
112+
// Advantage: no extra icon when editing
113+
target: null
114+
}
115+
})
116+
];
117+
118+
export const tiptapRenderedExtensions: Extensions = [
119+
...commonExtensions,
120+
extendedLink.configure({
121+
openOnClick: true,
122+
HTMLAttributes: {
123+
// Links open in a new window when displaying the editor in read-only mode
124+
target: "_blank"
125+
}
126+
}),
127+
...[AraTiptapRenderedExtension]
128+
];

confiture-web-app/src/components/tiptap/tiptap.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
}
3030
}
3131

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

3636
.tiptap pre code {

0 commit comments

Comments
 (0)