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 {