diff --git a/CHANGELOG.md b/CHANGELOG.md
index f76afd36e..9128e0745 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@
2025
+### 26/06/2025
+
+#### đ Corrections
+
+- 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))
+
### 18/06/2025
#### đ Nouvelles fonctionnalitĂ©s
diff --git a/confiture-web-app/src/components/tiptap/AraTiptapRenderedExtension.ts b/confiture-web-app/src/components/tiptap/AraTiptapRenderedExtension.ts
new file mode 100644
index 000000000..ba6832b29
--- /dev/null
+++ b/confiture-web-app/src/components/tiptap/AraTiptapRenderedExtension.ts
@@ -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 (` (nouvelle fenĂȘtre)"`)
+ *
+ * ## 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 `` 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({
+ name: "ara",
+ addProseMirrorPlugins() {
+ return [accessibleLinkPlugin];
+ }
+ });
diff --git a/confiture-web-app/src/components/tiptap/TiptapEditor.vue b/confiture-web-app/src/components/tiptap/TiptapEditor.vue
index 2b97aa783..dad9413d6 100644
--- a/confiture-web-app/src/components/tiptap/TiptapEditor.vue
+++ b/confiture-web-app/src/components/tiptap/TiptapEditor.vue
@@ -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 {
@@ -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()));
diff --git a/confiture-web-app/src/components/tiptap/TiptapRenderer.vue b/confiture-web-app/src/components/tiptap/TiptapRenderer.vue
index ff636f7f6..a0e1f0adc 100644
--- a/confiture-web-app/src/components/tiptap/TiptapRenderer.vue
+++ b/confiture-web-app/src/components/tiptap/TiptapRenderer.vue
@@ -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;
@@ -23,7 +23,7 @@ const editor = useEditor({
},
editable: false,
content: parsedDocument.value,
- extensions: tiptapExtensions
+ extensions: tiptapRenderedExtensions
}) as ShallowRef;
const contentRef = ref();
diff --git a/confiture-web-app/src/components/tiptap/tiptap-extensions.ts b/confiture-web-app/src/components/tiptap/tiptap-extensions.ts
index 96a033d99..196e20115 100644
--- a/confiture-web-app/src/components/tiptap/tiptap-extensions.ts
+++ b/confiture-web-app/src/components/tiptap/tiptap-extensions.ts
@@ -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";
@@ -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;
@@ -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: "",
@@ -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: " »"
@@ -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]
+];
diff --git a/confiture-web-app/src/components/tiptap/tiptap.css b/confiture-web-app/src/components/tiptap/tiptap.css
index 8c3755745..d5fa5c895 100644
--- a/confiture-web-app/src/components/tiptap/tiptap.css
+++ b/confiture-web-app/src/components/tiptap/tiptap.css
@@ -29,8 +29,8 @@
}
}
-.tiptap p {
- vertical-align: middle;
+.tiptap:not(.tiptap--rendered) [href] {
+ cursor: text;
}
.tiptap pre code {