UNPKG

@edugouvfr/ngx-dsfr-ext

Version:

NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)

1 lines 98.6 kB
{"version":3,"file":"edugouvfr-ngx-dsfr-ext-editor.mjs","sources":["../../../projects/ngx-dsfr-ext/editor/src/lib/components/toolbar/editor-toolbar.model.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/components/toolbar/editor-toolbar-default-options.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/editor.service.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/components/content/editor-content.component.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/components/content/editor-content.component.html","../../../projects/ngx-dsfr-ext/editor/src/lib/directives/unfocusable.directive.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/components/toolbar/editor-toolbar.component.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/components/toolbar/editor-toolbar.component.html","../../../projects/ngx-dsfr-ext/editor/src/lib/editor.component.ts","../../../projects/ngx-dsfr-ext/editor/src/lib/editor.component.html","../../../projects/ngx-dsfr-ext/editor/src/public-api.ts","../../../projects/ngx-dsfr-ext/editor/src/edugouvfr-ngx-dsfr-ext-editor.ts"],"sourcesContent":["/**\n * Constantes des identifiants d'actions de formatage supportés.\n */\nexport enum FormatConst {\n ALIGN = 'align',\n BACKGROUND = 'background',\n BLOCKQUOTE = 'blockquote',\n BOLD = 'bold',\n CLEAN = 'clean',\n CODE_BLOCK = 'code-block',\n COLOR = 'color',\n DIRECTION = 'direction',\n HEADER = 'header',\n IMAGE = 'image',\n INDENT = 'indent',\n ITALIC = 'italic',\n LINK = 'link',\n LIST = 'list',\n SCRIPT = 'script',\n SIZE = 'size',\n STRIKE = 'strike',\n UNDERLINE = 'underline',\n}\n\n/**\n * Type correspondant aux identifiants d'actions de formatage supportés.\n */\nexport type Format = `${FormatConst}`;\n\n/**\n * Mapping des clés d'internationalisation des libellés des actions de formatage.\n *\n * Selon le contexte, le libellé sera utilisé comme tooltip, libellé de champ ou transcription accessible.\n */\nexport const FORMAT_I18N_KEYS: Record<Format, string | Record<string, string>> = {\n 'align': {\n normal: 'editor.controls.align.left',\n center: 'editor.controls.align.center',\n right: 'editor.controls.align.right',\n justify: 'editor.controls.align.justify',\n },\n 'background': 'editor.controls.background',\n 'blockquote': 'editor.controls.blockquote',\n 'clean': 'editor.controls.clean',\n 'bold': 'editor.controls.bold',\n 'code-block': 'editor.controls.code',\n 'color': 'editor.controls.color',\n 'direction': { normal: 'editor.controls.direction.normal', rtl: 'editor.controls.direction.rtl' },\n 'header': {\n 'label': 'editor.controls.header.label',\n '0': 'editor.controls.header.0',\n '1': 'editor.controls.header.1',\n '2': 'editor.controls.header.2',\n '3': 'editor.controls.header.3',\n '4': 'editor.controls.header.4',\n '5': 'editor.controls.header.5',\n '6': 'editor.controls.header.6',\n },\n 'image': 'editor.controls.image.label',\n 'indent': { '-1': 'editor.controls.indent.-1', '+1': 'editor.controls.indent.+1' },\n 'italic': 'editor.controls.italic',\n 'link': 'editor.controls.link.label',\n 'list': {\n ordered: 'editor.controls.list.ordered',\n bullet: 'editor.controls.list.bullet',\n unchecked: 'editor.controls.list.checkbox',\n },\n 'script': { sub: 'editor.controls.script.sub', super: 'editor.controls.script.super' },\n 'size': {\n label: 'editor.controls.size.label',\n small: 'editor.controls.size.small',\n normal: 'editor.controls.size.normal',\n large: 'editor.controls.size.large',\n huge: 'editor.controls.size.huge',\n },\n 'strike': 'editor.controls.strike',\n 'underline': 'editor.controls.underline',\n};\n\n/**\n * Mapping des icônes par défaut correspondant aux actions de formatage supportées.\n *\n * Note de conception (RPA+KGE) pour l'instant on décide de maintenir les icônes dans une structure à part afin de\n * verrouiller le modèle. Si demain on souhaite donner à l'utilisateur la possibilité de surcharger les icônes des\n * actions de formatage alors on pourra réintégrer les icônes au modèle en augmentant l'interface `BaseFormatControl`\n * d'une propriété `icon` (rétrocompatible car la propriété aura une valeur par défaut dans le modèle par défaut)\n */\nexport const FORMAT_ICON: Record<Format, string | Record<string, string> | undefined> = {\n 'align': {\n normal: 'fr-icon-align-left',\n center: 'fr-icon-align-center',\n right: 'fr-icon-align-right',\n justify: 'fr-icon-align-justify',\n },\n 'background': 'fr-icon-brush-3-line',\n 'blockquote': 'fr-icon-quote-line',\n 'clean': 'fr-icon-clean',\n 'bold': 'fr-icon-bold',\n 'code-block': 'fr-icon-code-view',\n 'color': 'fr-icon-font-color',\n 'direction': 'fr-icon-direction-rtl',\n 'header': undefined,\n 'image': 'fr-icon-image-line',\n 'indent': { '-1': 'fr-icon-indent-decrease', '+1': 'fr-icon-indent-increase' },\n 'italic': 'fr-icon-italic',\n 'link': 'fr-icon-link',\n 'list': { ordered: 'fr-icon-list-ordered', bullet: 'fr-icon-list-unordered', unchecked: 'fr-icon-list-check' },\n 'script': { sub: 'fr-icon-subscript', super: 'fr-icon-superscript' },\n 'size': undefined,\n 'strike': 'fr-icon-strike',\n 'underline': 'fr-icon-underline',\n};\n\n/**\n * Les différents mode supportés par les contrôles de formatage à valeur unique.\n */\nexport enum ControlMode {\n /**\n * Active/désactive le format.\n */\n TOGGLE = 'toggle',\n /**\n * Le format dépend de la capture d'un paramètre utilisateur.\n */\n PROMPT = 'prompt',\n /**\n * Action issue d'une des options d'une action multivaluée érigée en action unique autonome.\n * Ex. je veux un bouton permettant de formater en Titre de niveau 1 (H1)\n */\n CUSTOM = 'custom',\n /**\n * Permet de représenter l'une des options d'une action multivaluée.\n */\n VALUE = 'valued',\n /**\n * Dénote une action de formatage multivaluée à afficher sous forme de selecteur.\n */\n SELECT = 'select',\n /**\n * Dénote une action de formatage multivaluée à afficher sous de groupe de bouton exclusif.\n */\n RADIO = 'radio',\n /**\n * Mode spécial dénotant le contrôle de réinitialisation.\n */\n CLEAN = 'clean',\n}\n\n/**\n * Les valeurs possibles pour le format 'size'.\n */\nexport enum SizeValue {\n SMALL = 'small',\n NORMAL = 'normal',\n LARGE = 'large',\n HUGE = 'huge',\n}\n\n/**\n * Les valeurs possibles pour le format 'header'.\n */\nexport enum HeaderValue {\n NORMAL = 0,\n H1 = 1,\n H2 = 2,\n H3 = 3,\n H4 = 4,\n H5 = 5,\n H6 = 6,\n}\n\n/**\n * Les valeurs possibles pour le format 'align'.\n */\nexport enum AlignValue {\n LEFT = 'normal',\n CENTER = 'center',\n RIGHT = 'right',\n JUSTIFY = 'justify',\n}\n\n/**\n * Les valeurs possibles pour le format 'list'.\n */\nexport enum ListValue {\n ORDERED = 'ordered',\n BULLET = 'bullet',\n CHECKBOX = 'unchecked',\n}\n\n/**\n * Les valeurs possibles pour le format 'script'.\n */\nexport enum ScriptValue {\n /**\n * Subscript.\n */\n SUBSCRIPT = 'sub',\n /**\n * Superscript.\n */\n SUPERSCRIPT = 'super',\n}\n\n/**\n * Les valeurs possibles pour le format 'indent'.\n */\nexport enum IndentValue {\n /**\n * Decrease indent.\n */\n DECREASE = '-1',\n /**\n * Increase indent.\n */\n INCREASE = '+1',\n}\n\n/**\n * Les valeurs possibles pour le format 'direction'.\n */\nexport enum DirectionValue {\n /**\n * De gauche à droite.\n */\n NORMAL = 'normal',\n\n /**\n * De droite à gauche.\n */\n RTL = 'rtl',\n}\n\n/**\n * Représente une tâche de formatage de base.\n */\nexport interface DsfrBaseFormatControl {\n /**\n * L'identifiant Quill du format.\n */\n name: Format;\n\n /**\n * Le mode du contrôle de format.\n */\n mode: ControlMode;\n}\n\n/**\n * Représente une action simple (bouton).\n */\nexport interface DsfrSingleFormatControl extends DsfrBaseFormatControl {\n /**\n * Optionnel : permet de surcharger l'icône illustrative.\n * Doit être une classe CSS Remix wrappée DSFR. Ex. 'fr-icon-bold'.\n */\n icon?: string;\n}\n\n/**\n * Permet de sortir une des options de formatage d'une action multivaluée et d el'ériger en action unique autonome.\n */\n//TODO: pas encore opérationnel, à re-travailler notamment sur la fourniture icône, libellés i18n, etc.\nexport class DsfrCustomFormatControl implements DsfrSingleFormatControl {\n /**\n * Le mode d'une action avec valeur prédéfinie est forcément 'valued'.\n */\n mode = ControlMode.CUSTOM;\n\n /**\n * Construit une action de formatage valuée.\n *\n * @param name le format de cette action\n * @param value la valeur spécifique de cette action de formatage\n */\n constructor(\n public name: Format,\n public value: string | number | false,\n ) {}\n}\n\n/**\n *\n */\nexport class DsfrToggleFormatControl implements DsfrSingleFormatControl {\n /**\n * Le mode d'une action avec valeur prédéfinie est forcément 'toggle'.\n */\n mode = ControlMode.TOGGLE;\n\n constructor(public name: Format) {}\n}\n\n/**\n *\n */\nexport class DsfrPromptFormatControl implements DsfrSingleFormatControl {\n /**\n * Le mode d'une action avec valeur prédéfinie est forcément 'prompt'.\n */\n mode = ControlMode.PROMPT;\n\n constructor(public name: Format) {}\n}\n\n/**\n * Représente une des actions possibles d'une méta-action multi-valuée.\n */\nexport class DsfrValueFormatControl implements Omit<DsfrSingleFormatControl, 'name'> {\n /**\n * Le mode d'une action avec valeur prédéfinie est forcément 'valued'.\n */\n mode = ControlMode.VALUE;\n\n /**\n * Construit une action de formatage valuée.\n *\n * @param value la valeur spécifique de cette action de formatage\n */\n constructor(public value: string | number | false) {}\n}\n\n/**\n * Représente une méta-tâche qui s'applique à travers la sélection d'une valeur concrète au sein d'une liste de valeurs.\n */\nexport interface DsfrMultivaluedFormatControl extends DsfrBaseFormatControl {\n /**\n * Permet d'indiquer si le contrôle multivalué doit s'afficher au sein d'un groupe dans la toolbar.\n */\n group: boolean;\n\n /**\n * Valeurs possibles pour cette action de formatage.\n */\n values: DsfrValueFormatControl[];\n}\n\n/**\n * Représente une action multi-valuée en mode radio.\n */\nexport class RadioFormatControl implements DsfrMultivaluedFormatControl {\n mode = ControlMode.RADIO;\n\n /**\n * Construit un control de type radio.\n *\n * @param values les différentes valeurs possibles pour le control de type radio\n */\n constructor(\n public name: Format,\n public values: DsfrValueFormatControl[],\n public group: boolean,\n ) {}\n}\n\n/**\n * Représente une action multi-valuée en mode select.\n */\nexport class SelectFormatControl implements DsfrMultivaluedFormatControl {\n mode = ControlMode.SELECT;\n group = false;\n\n /**\n * Construit un control de type select.\n *\n * @param values les différentes valeurs possibles pour le control de type select.\n */\n constructor(\n public name: Format,\n public values: DsfrValueFormatControl[],\n ) {}\n}\n\n/**\n * Type générique encapsulant toutes les types de tâches de formatage.\n */\nexport type FormatControl = DsfrSingleFormatControl | DsfrMultivaluedFormatControl;\n\n/**\n * Type décrivant un élément de la barre d'outil qui peut être soit une tâche unitaire soit un regroupement de tâches.\n */\nexport type ToolbarControl = FormatControl | FormatControl[];\n","import {\n AlignValue,\n ControlMode,\n DsfrPromptFormatControl,\n DsfrToggleFormatControl,\n DsfrValueFormatControl,\n FormatConst,\n HeaderValue,\n IndentValue,\n ListValue,\n RadioFormatControl,\n ScriptValue,\n SelectFormatControl,\n SizeValue,\n ToolbarControl,\n} from './editor-toolbar.model';\n\n/**\n * Options par défaut de la toolbar.\n */\nexport const DEFAULT_TOOLBAR_OPTIONS: ToolbarControl[] = [\n // niveau de titre\n new SelectFormatControl(FormatConst.HEADER, [\n new DsfrValueFormatControl(HeaderValue.NORMAL),\n new DsfrValueFormatControl(HeaderValue.H1),\n new DsfrValueFormatControl(HeaderValue.H2),\n new DsfrValueFormatControl(HeaderValue.H3),\n new DsfrValueFormatControl(HeaderValue.H4),\n new DsfrValueFormatControl(HeaderValue.H5),\n new DsfrValueFormatControl(HeaderValue.H6),\n ]),\n // taille du texte\n new SelectFormatControl(FormatConst.SIZE, [\n new DsfrValueFormatControl(SizeValue.SMALL),\n new DsfrValueFormatControl(SizeValue.NORMAL),\n new DsfrValueFormatControl(SizeValue.LARGE),\n new DsfrValueFormatControl(SizeValue.HUGE),\n ]),\n // mise en forme du texte (gras, italique, etc.)\n [\n new DsfrToggleFormatControl(FormatConst.BOLD),\n new DsfrToggleFormatControl(FormatConst.ITALIC),\n new DsfrToggleFormatControl(FormatConst.UNDERLINE),\n new DsfrToggleFormatControl(FormatConst.STRIKE),\n ],\n new RadioFormatControl(\n FormatConst.SCRIPT,\n [new DsfrValueFormatControl(ScriptValue.SUPERSCRIPT), new DsfrValueFormatControl(ScriptValue.SUBSCRIPT)],\n true,\n ),\n // couleurs\n [new DsfrPromptFormatControl(FormatConst.COLOR), new DsfrPromptFormatControl(FormatConst.BACKGROUND)],\n // mise en forme avancée\n [\n new DsfrToggleFormatControl(FormatConst.BLOCKQUOTE),\n new DsfrToggleFormatControl(FormatConst.CODE_BLOCK),\n new DsfrPromptFormatControl(FormatConst.LINK),\n new DsfrPromptFormatControl(FormatConst.IMAGE),\n ],\n // listes\n new RadioFormatControl(\n FormatConst.LIST,\n [\n new DsfrValueFormatControl(ListValue.ORDERED),\n new DsfrValueFormatControl(ListValue.BULLET),\n // new ValueFormatControl(ListValue.CHECKBOX), //RPA: pas proposé par défaut car non standard au sens html\n ],\n true,\n ),\n // indentation des lignes de texte\n new RadioFormatControl(\n FormatConst.INDENT,\n [new DsfrValueFormatControl(IndentValue.DECREASE), new DsfrValueFormatControl(IndentValue.INCREASE)],\n true,\n ),\n\n // alignement des lignes de texte\n new RadioFormatControl(\n FormatConst.ALIGN,\n [\n new DsfrValueFormatControl(AlignValue.LEFT), // même effet que clean, faut-il le conserver ? ça fait un peu bizarre sans\n new DsfrValueFormatControl(AlignValue.CENTER),\n new DsfrValueFormatControl(AlignValue.RIGHT),\n new DsfrValueFormatControl(AlignValue.JUSTIFY),\n ],\n true,\n ),\n // reset du format\n {\n name: FormatConst.CLEAN,\n mode: ControlMode.CLEAN,\n },\n];\n","export const MESSAGES_EN = {\n editor: {\n placeholder: 'Veuillez éditer votre contenu',\n controls: {\n align: { left: 'Left', center: 'Center', right: 'Right', justify: 'Justify' },\n background: `Background color`,\n blockquote: 'Quote',\n clean: 'Reset format',\n bold: 'Bold',\n code: 'Code block',\n color: 'Text color',\n direction: { normal: 'Left to right', rtl: 'Right to left' },\n header: {\n 'label': 'Heading level',\n '0': 'Normal',\n '1': 'Heading 1',\n '2': 'Heading 2',\n '3': 'Heading 3',\n '4': 'Heading 4',\n '5': 'Heading 5',\n '6': 'Heading 6',\n },\n image: { label: 'Image', prompt: `Image path:` },\n indent: { '-1': `Decrease indentation`, '+1': `Increase indentation` },\n italic: 'Italic',\n link: { label: 'Link', prompt: `Link URL:` },\n list: { label: 'List', ordered: 'Ordered', bullet: 'Unordered', checkbox: 'Checklist' },\n script: { sub: 'Subscript', super: 'Superscript' },\n size: { label: 'Text size', small: 'Small', normal: 'Normal', large: 'Large', huge: 'Huge' },\n strike: 'Strike',\n underline: 'Underline',\n },\n },\n};\n","// Ordre alphabétique requis\nexport const MESSAGES_FR = {\n editor: {\n placeholder: 'Veuillez éditer votre contenu',\n controls: {\n align: {\n label: 'Alignement du texte',\n left: 'Aligner à gauche',\n center: 'Centrer',\n right: 'Aligner à droite',\n justify: 'Justifier',\n },\n background: `Couleur du fond`,\n blockquote: 'Guillemets',\n clean: 'Réinitialiser le formatage',\n bold: 'Gras',\n code: 'Bloc de code',\n color: 'Couleur du texte',\n direction: { label: 'Direction', normal: 'De gauche à droite', rtl: 'De droite à gauche' },\n header: {\n 'label': 'Niveau de titre',\n '0': 'Normal',\n '1': 'Titre 1',\n '2': 'Titre 2',\n '3': 'Titre 3',\n '4': 'Titre 4',\n '5': 'Titre 5',\n '6': 'Titre 6',\n },\n image: { label: 'Image', prompt: `Chemin vers l'image :` },\n indent: { 'label': 'Indentation', '-1': `Réduire l'indentation`, '+1': `Augmenter l'indentation` },\n italic: 'Italique',\n link: { label: 'Lien', prompt: `URL du lien :` },\n list: { label: 'Liste', ordered: 'Liste numérotée', bullet: 'Liste à puces', checkbox: 'Liste à cocher' },\n script: { sub: 'Indice', super: 'Exposant' },\n size: { label: 'Taille du texte', small: 'Petit', normal: 'Normal', large: 'Large', huge: 'Énorme' },\n strike: 'Barré',\n underline: 'Souligné',\n },\n },\n};\n","import { Injectable } from '@angular/core';\nimport { Observable, ReplaySubject } from 'rxjs';\n\nexport declare type Formatting = { [format: string]: unknown } | undefined;\n\nexport declare type FormattingRequest = { action: string; value: any } | undefined;\n\n/**\n * Service permettant de gérer la communication bidirectionnelle entre la toolbar et son éditeur.\n */\n@Injectable()\nexport class DsfrEditorService {\n /**\n * Observable sur le formatage de la sélection courante.\n */\n readonly currentFormatting$: Observable<Formatting>;\n\n /**\n * Observable sur les demandes de mises en forme émise depuis la toolbar.\n */\n readonly formattingRequest$: Observable<FormattingRequest>;\n\n /**\n * Permet de notifier les changements opérés de l'extérieur sur le modèle HTML.\n */\n readonly modelChange$: Observable<string>;\n\n /**\n * Permet de notifier les changements au niveau du contenu de l'éditeur qui impacteront le modèle HTML.\n */\n readonly editorChange$: Observable<string>;\n\n /**\n * Permet d'émettre une demande de prise de focus sur le contenu de l'éditeur HTML.\n */\n readonly focusOnEditorRequest$: Observable<void>;\n\n /**\n * Subject sur les formats de la sélection courante.\n */\n private _currentFormattingSubject: ReplaySubject<Formatting>;\n\n /**\n * Subject sur les demandes de formatage.\n */\n private _formattingRequestSubject: ReplaySubject<FormattingRequest>;\n\n /**\n * Subject sur les changements externes sur le modèle HTML.\n */\n private _modelChangeSubject: ReplaySubject<string>;\n\n /**\n * Subject sur les changements de la valeur de l'éditeur HTML.\n */\n private _editorChangeSubject: ReplaySubject<string>;\n\n /**\n * Subject sur les demandes de prise de focus sur l'éditeur.\n */\n private _focusOnEditorRequestSubject: ReplaySubject<void>;\n\n constructor() {\n // RPA: j'utilise des ReplaySubject(1) pour ne pas être parasité par la première émission de la valeur initiale\n // du BehaviorSubject, par contre comme pour ce dernier les souscripteurs seront notifiés de la dernière valeur\n // émise _à partir du moment où une valeur a été explicitement émise_\n // source: https://stackoverflow.com/a/43119768/8737817\n this._currentFormattingSubject = new ReplaySubject<Formatting>(1);\n this._formattingRequestSubject = new ReplaySubject<FormattingRequest>(1);\n this._modelChangeSubject = new ReplaySubject<string>(1);\n this._editorChangeSubject = new ReplaySubject<string>(1);\n this._focusOnEditorRequestSubject = new ReplaySubject<void>(1);\n\n this.currentFormatting$ = this._currentFormattingSubject.asObservable();\n this.formattingRequest$ = this._formattingRequestSubject.asObservable();\n this.modelChange$ = this._modelChangeSubject.asObservable();\n this.editorChange$ = this._editorChangeSubject.asObservable();\n this.focusOnEditorRequest$ = this._focusOnEditorRequestSubject.asObservable();\n }\n\n /**\n * Permet de positionner la mise en forme de la sélection courante.\n */\n setCurrentFormatting(formats: Formatting) {\n this._currentFormattingSubject.next(formats);\n }\n\n /**\n * Permet de demander une nouvelle mise en forme sur la sélection courante.\n */\n requestFormatting(request: FormattingRequest) {\n this._formattingRequestSubject.next(request);\n }\n\n /**\n * Permet de demander la prise de focus sur l'éditeur.\n */\n requestFocusOnEditorContent() {\n this._focusOnEditorRequestSubject.next();\n }\n\n /**\n * Permet de notifier un changement externe sur le modèle HTML.\n */\n emitModelChange(value: string) {\n this._modelChangeSubject.next(value);\n }\n\n /**\n * Permet de notifier un changement interne du contenu de l'éditeur HTML.\n */\n emitEditorChange(value: string) {\n this._editorChangeSubject.next(value);\n }\n}\n","import { CommonModule } from '@angular/common';\nimport {\n AfterViewInit,\n Component,\n CUSTOM_ELEMENTS_SCHEMA,\n inject,\n Input,\n OnChanges,\n OnDestroy,\n SimpleChanges,\n ViewEncapsulation,\n} from '@angular/core';\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nimport Quill, { QuillOptions, Range } from 'quill';\n\nimport { DsfrI18nService, DsfrSeverity, DsfrSeverityConst } from '@edugouvfr/ngx-dsfr';\nimport { Delta } from 'quill/core';\n// eslint-disable-next-line @typescript-eslint/naming-convention\nimport Emitter from 'quill/core/emitter';\nimport { Subscription } from 'rxjs';\nimport { DsfrEditorService, FormattingRequest } from '../../editor.service';\n\ndeclare type Formatting = { [format: string]: unknown };\n\n@Component({\n selector: 'edu-editor-content',\n encapsulation: ViewEncapsulation.None,\n templateUrl: './editor-content.component.html',\n styleUrls: ['./editor-content.component.scss'],\n standalone: true,\n imports: [CommonModule],\n\n schemas: [CUSTOM_ELEMENTS_SCHEMA],\n})\nexport class EduEditorContentComponent implements AfterViewInit, OnDestroy, OnChanges {\n /**\n * L'identifiant du tag hôte pour le composant éditeur.\n *\n * Attention : cet input est statique et doit être connu à l'initialisation du composant.\n */\n @Input() editorId = 'editor';\n\n /**\n * Renseigner cette proprité positionne un attribut aria-labelledby sur l'élément hôte de l'éditeur.\n */\n @Input() ariaLabelledBy: string;\n\n /**\n * Active la lecure seule.\n */\n @Input() readOnly = false;\n\n /**\n * Message d'information associé au composant.\n */\n @Input() message: string;\n\n /**\n * Représente la sévérité du message.\n */\n @Input() messageSeverity: DsfrSeverity;\n\n /**\n * Id de l'élément affichant les messages associé à l'éditeur. A renseigner si vous souhaitez positionner un attribut\n * `aria-describedby` référençant votre message au niveau de l'élément hôte de l'éditeur.\n */\n @Input() messagesGroupId: string;\n\n /**\n * Dev/debug : permet d'afficher le code HTML interne de l'éditeur (i.e. celui qui sert pour l'affichage WYSIWYG).\n */\n @Input() showEditorHtmlDebugPanel = false;\n\n /**\n * Dev/debug : permet d'afficher le code HTML qui sera récupéré en sortie.\n */\n @Input() showSemanticHtmlDebugPanel = false;\n\n /** @internal */\n public i18n = inject(DsfrI18nService);\n\n /**\n * Instance Quill.\n */\n protected _quill: Quill;\n\n /**\n * Pour pouvoir utiliser DsfrSeverityConst côté template.\n */\n protected readonly severityConst = DsfrSeverityConst;\n\n /**\n * Options de configuration de l'éditeur Quill.\n */\n private _options: QuillOptions = {\n readOnly: this.readOnly,\n modules: {\n toolbar: false,\n },\n placeholder: this.i18n.t('editor.placeholder'),\n theme: 'snow',\n };\n\n private _subscriptions: Subscription[] = [];\n\n private _editorSelectionHandler: Emitter;\n\n private _editorChangeHandler: Emitter;\n\n private readonly editorService = inject(DsfrEditorService);\n\n constructor() {}\n\n ngAfterViewInit(): void {\n this._quill = new Quill('#' + this.editorId, this._options);\n this._quill.root.setAttribute('role', 'textbox');\n this._quill.root.setAttribute('aria-multiline', 'true');\n this._quill.root.setAttribute('spellcheck', 'false');\n // fix a11y : par défaut, le key-binding Quill sur la touche Tab empêche de sortir de l'éditeur en utilisant Tab\n delete (this._quill.getModule('keyboard') as any)?.bindings['Tab'];\n\n if (this.ariaLabelledBy && this.ariaLabelledBy.length) {\n this._quill.root.setAttribute('aria-labelledby', this.ariaLabelledBy);\n }\n if (this.message && this.message.length && this.messagesGroupId && this.messagesGroupId.length) {\n this._quill.root.setAttribute('aria-describedby', this.messagesGroupId);\n }\n // On se met à l'écoute de la sélection utilisateur\n this._editorSelectionHandler = this._quill.on(Emitter.events.SELECTION_CHANGE, (range) =>\n this._onEditorSelectionChange(range),\n );\n // Détecte le changement de contenu de l'éditeur.\n this._editorChangeHandler = this._quill.on(\n Emitter.events.EDITOR_CHANGE,\n (eventName: any, delta: Delta, oldContents: Delta, source: string) =>\n this._onEditorChange(eventName, delta, oldContents, source),\n );\n\n this._subscriptions.push(\n this.editorService.formattingRequest$.subscribe((req: FormattingRequest) => {\n const range = this._quill.getSelection();\n if (req === undefined) {\n if (range) {\n // remove formatting\n this._quill.removeFormat(range.index, range.length);\n }\n } else if ('image' === req.action) {\n // perform special image format\n if (range) {\n this._quill.uploader.upload(range, req.value);\n }\n } else {\n // perform standart formatting\n this._quill.format(req.action, req.value);\n }\n this.editorService.setCurrentFormatting(this._getCurrentFormatting());\n }),\n );\n // Écoute les mises à jour du modèle externe.\n this._subscriptions.push(\n this.editorService.modelChange$.subscribe((newValue: string) => {\n this.setEditorContent(newValue);\n }),\n );\n // Écoute les demandes de prise de focus\n this._subscriptions.push(\n this.editorService.focusOnEditorRequest$.subscribe(() => {\n this._quill.focus();\n }),\n );\n }\n\n ngOnDestroy(): void {\n this._subscriptions.forEach((s) => s.unsubscribe());\n this._quill.off(Emitter.events.SELECTION_CHANGE, this._editorSelectionHandler.off);\n this._quill.off(Emitter.events.EDITOR_CHANGE, this._editorChangeHandler.off);\n }\n\n ngOnChanges({ ariaLabelledBy, message, messagesGroupId }: SimpleChanges): void {\n if (ariaLabelledBy) {\n if (this.ariaLabelledBy && this.ariaLabelledBy.length) {\n this._quill?.root.setAttribute('aria-labelledby', this.ariaLabelledBy);\n } else {\n this._quill?.root.removeAttribute('aria-labelledby');\n }\n }\n if (message || messagesGroupId) {\n if (this.message && this.message.length && this.messagesGroupId && this.messagesGroupId.length) {\n this._quill?.root.setAttribute('aria-describedby', this.messagesGroupId);\n } else {\n this._quill?.root.removeAttribute('aria-describedby');\n }\n }\n }\n\n protected hasMessage(severity: DsfrSeverity): boolean {\n return !this.isStringEmptyOrNull(this.message) && severity === this.messageSeverity;\n }\n\n private setEditorContent(newValue: string) {\n this._quill.setContents(this._quill.clipboard.convert({ html: newValue }), 'silent');\n }\n\n //FIXME: utiliser les utils de la lib ngx-dsfr\n private isStringEmptyOrNull(s: string | null | undefined): boolean {\n return !s || s.trim() === '';\n }\n\n private _onEditorSelectionChange(range: { index: number; length: number }) {\n // RPA: vu avec SAB le 25/09/2024 : on considère que la toolbar reflète l'état courant relatif à la position du\n // curseur et/ou la sélection active dans l'éditeur\n // si on change d'avis et qu'on on veut conserver l'état de la toolbar correspondant à la dernière position du\n // curseur avant une perte de focus de l'éditeur alors il ne faut pas émettre d'event si range==null\n this.editorService.setCurrentFormatting(this._getCurrentFormatting());\n }\n\n /**\n * Permet de se mettre à l'écoute du contenu de l'éditeur afin de reset la toolbar si l'éditeur est vidé.\n */\n private _onEditorChange(eventName: string, delta: Delta, oldContents: Delta, source: string) {\n if (eventName === 'text-change') {\n // Note even when Quill is empty, there is still a blank line represented by '\\n', so getLength will return 1.\n if (this._quill.getLength() === 1) {\n this.editorService.setCurrentFormatting(undefined);\n } else {\n this.editorService.setCurrentFormatting(this._getCurrentFormatting());\n }\n // on signale le changement à l'extérieur\n this.editorService.emitEditorChange(this._quill.getSemanticHTML());\n }\n }\n\n private _getCurrentFormatting(): Formatting | undefined {\n const range: Range | null = this._quill.getSelection();\n return range ? this._quill.getFormat(range.index, range.length) : undefined;\n }\n}\n","<div id=\"{{ editorId }}\"></div>\n\n<div class=\"fr-messages-group\" [id]=\"messagesGroupId\" aria-live=\"polite\">\n @if (message) {\n <p\n class=\"fr-message\"\n [ngClass]=\"{\n 'fr-message--error': hasMessage(severityConst.ERROR),\n 'fr-message--valid': hasMessage(severityConst.SUCCESS),\n 'fr-message--info': hasMessage(severityConst.INFO)\n }\">\n {{ message }}\n </p>\n }\n</div>\n\n@if (showEditorHtmlDebugPanel) {\n <div class=\"debug-panel\">\n <p>Editor source</p>\n <code>{{ _quill?.root?.innerHTML }}</code>\n </div>\n}\n\n@if (showSemanticHtmlDebugPanel) {\n <div class=\"debug-panel\">\n <p>HTML output</p>\n <code>{{ _quill?.getSemanticHTML() }}</code>\n </div>\n}\n","import { AfterViewInit, Directive, ElementRef, Input, Renderer2 } from '@angular/core';\n\n/**\n * Permet de rendre l'élément déonoté par le sélecteur explicitement non-focusable. Cela sert au niveau de la toolbar\n * sur les dsfr-bouton et les dsfr-form-select qui embarquent des éléments nativement focusable tels que button et\n * select.\n */\n//TODO: (RPA) à réfléchir s'il ne faudrait pas plutôt prévoir d'ajouter un input `'unfocusable' permettant de gérer ça\n// directement aux niveaux des composants eux-même (?)\n@Directive({\n selector: '[unfocusable]',\n standalone: true,\n})\nexport class UnfocusableDirective implements AfterViewInit {\n /**\n * Le sélecteur à utiliser pour récupérer l'élément à rendre non focusable.\n */\n // RPA: j'utilise l'alias du même nom que la directive pour pouvoir paramétrer directement la directive elle-même\n // tout en disposant d'un nom plus explicite (selector) en interne.\n @Input('unfocusable')\n public selector: string;\n\n constructor(\n private _elementRef: ElementRef,\n private _renderer: Renderer2,\n ) {}\n\n // RPA: Je me mets sur AfterViewInit car sur OnInit je n'ai pas encore le select lorsque je requête le DOM\n // (pour le bouton par contre cela fonctionnait avec OnInit)\n ngAfterViewInit(): void {\n let el: HTMLElement = this._elementRef.nativeElement.querySelector(this.selector);\n if (el) {\n this._renderer.setAttribute(el, 'tabindex', '-1');\n } else {\n //FIXME: (RPA) utiliser LoggerService lorsque celui-ci sera exporté depuis ngx-dsfr\n console.warn('UnfocusableDirective > aucun élément remontée par le sélecteur', this.selector);\n }\n }\n}\n","import { CommonModule } from '@angular/common';\nimport {\n Component,\n CUSTOM_ELEMENTS_SCHEMA,\n ElementRef,\n inject,\n Input,\n NgZone,\n OnDestroy,\n OnInit,\n Renderer2,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport {\n DsfrButtonModule,\n DsfrFormSelectModule,\n DsfrI18nPipe,\n DsfrI18nService,\n DsfrSelectOption,\n DsfrSkiplinksModule,\n} from '@edugouvfr/ngx-dsfr';\n\nimport { Subscription } from 'rxjs';\nimport { UnfocusableDirective } from '../../directives/unfocusable.directive';\nimport { DsfrEditorService, Formatting } from '../../editor.service';\nimport { DEFAULT_TOOLBAR_OPTIONS } from './editor-toolbar-default-options';\nimport {\n ControlMode,\n DsfrBaseFormatControl,\n DsfrMultivaluedFormatControl,\n DsfrValueFormatControl,\n Format,\n FORMAT_I18N_KEYS,\n FORMAT_ICON,\n FormatConst,\n FormatControl,\n ToolbarControl,\n} from './editor-toolbar.model';\n\n/**\n * Les quatre modalités de présentation supportées par la toolbar pour afficher les contrôles de format.\n */\ntype ControlViewType = 'btn' | 'group' | 'radio' | 'select';\n\n@Component({\n selector: 'edu-editor-toolbar',\n encapsulation: ViewEncapsulation.None,\n templateUrl: './editor-toolbar.component.html',\n styleUrls: ['./editor-toolbar.component.scss'],\n standalone: true,\n imports: [\n CommonModule,\n FormsModule,\n ReactiveFormsModule,\n DsfrSkiplinksModule,\n DsfrFormSelectModule,\n DsfrButtonModule,\n UnfocusableDirective,\n DsfrI18nPipe,\n ],\n schemas: [CUSTOM_ELEMENTS_SCHEMA],\n})\nexport class EduEditorToolbarComponent implements OnInit, OnDestroy {\n /**\n * L'identifiant du tag hôte pour le composant éditeur.\n */\n @Input() editorId = 'editor';\n\n /**\n * Active la lecure seule.\n */\n //TODO: @Input() readOnly = false;\n\n /**\n * Options de la toolbar.\n */\n @Input() toolbarOptions: ToolbarControl[] = DEFAULT_TOOLBAR_OPTIONS;\n\n /** @internal */\n @ViewChild('controlsParent') controlsParent: ElementRef;\n\n /**\n * Modèle de présentation spécifique pour les select (header+size).\n * RPA: je passe par un modèle de présentation spécifique et je n'utilise pas directement un binding sur\n * currentSelectionFormats car j'ai besoin de reflêter l'état de sélection utilisateur sinon voici ce qui ce passe :\n * l'utilisateur sélectionne une portion de texte et sélectionnz un niveau de titre H2, puis il place le curseur dans\n * un paragraphe quelconque, le select reste sur H2 car la propriété interne 'value' n'est pas mise à jour lors de\n * la sélection utilisateur (binding non bi-directionnel). C'est pourquoi il est nécessaire de passer par un modèle\n * de présentation dédié à la gestion des select.\n */\n protected selectModel: any = { header: null, size: null };\n\n /**\n * Permet de gérer l'affichage d'un outline custom lorsque la toolbar prend le focus.\n */\n protected toolbarFocused = false;\n\n /**\n * Pointeur sur le dernier élément de contrôle actif, permet de restaurer le focus directement sur celui-ci au\n * SHIFT+TAB.\n */\n protected lastActiveControlElement?: HTMLElement;\n\n /**\n * Modèle de présentation interne exposant les formats de la sélection courante.\n */\n private _currentSelectionFormats: Formatting | undefined;\n\n private _unlistenFn: Function;\n\n private _sub: Subscription;\n\n private readonly ngZone = inject(NgZone);\n private readonly renderer2 = inject(Renderer2);\n private readonly elementRef = inject(ElementRef);\n private readonly i18n = inject(DsfrI18nService);\n private readonly editorService = inject(DsfrEditorService);\n\n constructor() {}\n\n ngOnInit(): void {\n this._sub = this.editorService.currentFormatting$.subscribe((formatting: Formatting) => {\n this._updateToolbar(formatting);\n });\n // Hors zone Angular pour des raisons de perf\n this.ngZone.runOutsideAngular(() => {\n this._unlistenFn = this.renderer2.listen('document', 'click', (e) => {\n this.clickOutside(e);\n });\n });\n }\n\n ngOnDestroy(): void {\n this._sub?.unsubscribe();\n this._unlistenFn();\n }\n\n /**\n * Handler sur l'input file associé à une inserion d'image.\n */\n //FIXME : erreur dans la console : ParchmentError: [Parchment] Cannot wrap imag\n protected onImageInputFileChange(event: Event) {\n this._uploadImage(event);\n }\n\n /**\n * Permet de conditionner la structure HTML selon le type contrôle.\n */\n protected getButtonType(control: FormatControl | FormatControl[]): ControlViewType {\n let result: ControlViewType = 'btn';\n if (Array.isArray(control) && control.length) {\n result = 'group';\n } else if ('values' in control && control.values && control.values.length) {\n switch ((control as DsfrBaseFormatControl).mode) {\n case ControlMode.RADIO:\n result = 'radio';\n break;\n\n case ControlMode.SELECT:\n result = 'select';\n break;\n\n default:\n break;\n }\n }\n\n return result;\n }\n\n /**\n * Retourne l'icône correspondant au format passé en paramètre.\n *\n * @param format le format à considérer\n * @param value la vaaleur à prendre en compte le cas échéant\n * @returns la classe css correspondant à l'icône\n */\n protected getIcon(format: Format, value = undefined): string | undefined {\n let icon: any = FORMAT_ICON[format];\n // la valeur peut être falsy d'où le test explicite sur not(undefined)\n if (icon && value !== undefined) {\n // on doit aller chercher l'icône en utilisant la valeur comme clé\n icon = icon[value];\n }\n\n return icon;\n }\n\n /**\n * Retourne la clé du libellé internationalisé.\n * @param control le contôle de formatage\n */\n protected getI18nKey(format: Format, value: string | boolean | undefined = undefined): string {\n let key = FORMAT_I18N_KEYS[format];\n if (value !== undefined) {\n key = key as Record<string, string>;\n key = key[String(value)];\n }\n\n return key as string;\n }\n\n /**\n * Détermine si l'option de format doit être activée ou pas.\n *\n * @param format le nom de contrôle de format\n * @param value la valeur de l'option considérée\n * @returns vrai si le couple format/value fait partie des formats de la sélection courante, faux sinon\n */\n protected isValueActive(format: Format, value: string): boolean {\n // @ts-ignore (RPA) le test sur currentSelectionFormats est effectué à l'intérieur de la fonction isFormatActive\n return this.isFormatActive(format) ? this._currentSelectionFormats[format] === value : false;\n }\n\n /**\n * Retourne la liste des optiosn à affichier au sein du contrôle multivalué (select).\n *\n * @param control le contrôle multivalué à considéré\n */\n protected getSelectOptions(control: DsfrMultivaluedFormatControl): DsfrSelectOption[] {\n return control.values.map((v: DsfrValueFormatControl) => this._buildSelectOption(control.name, v));\n }\n\n /**\n * Permet d'indiquer si le contrôle de ce format doit être activé au niveau de la barre d'outils.\n *\n * @param format le format à considérer vis-à-vis des formats de la sélection courante\n * @returns vrai si ce format doit être actif, faux sinon\n */\n protected isFormatActive(format: Format): boolean {\n return this._currentSelectionFormats ? format in this._currentSelectionFormats : false;\n }\n\n /**\n * Permet de réagir aux actions utilisateur sur la toolbar.\n */\n protected onToolbarControlPerform(event: Event, format: FormatControl, value?: string) {\n event.preventDefault();\n this._applyFormat(format, value);\n }\n\n /**\n * Permet d'écouter la capture d'une couleur par l'utilisateur.\n */\n protected onColorPickerChange(event: Event, action: string) {\n const target = <HTMLInputElement>event.target;\n const value = target.value ?? undefined;\n if (value) {\n this.performAction(action, value);\n }\n }\n\n /**\n * Permet d'écouter les changement sur le format \"list\".\n *\n * @param action\n * @param event\n */\n protected onListSelect(action: string, event: Event) {\n this.performAction(action, event);\n }\n\n /**\n * Permet de forcer la prise de focus sur le premier élément focusable de la barre d'outil lorsque le conteneur de\n * la toolbar reçoit le focus.\n */\n protected onToolbarFocus() {\n this.toolbarFocused = true;\n if (this.lastActiveControlElement) {\n this.lastActiveControlElement.focus();\n } else {\n const nextElem: HTMLElement = this.elementRef.nativeElement.querySelector('.item-editor, .fr-select');\n nextElem?.focus();\n }\n }\n\n /**\n * Permet de gérer la sortie de focus\n */\n protected onToolbarBlur(event: any) {\n // si le focus est pris par un élément de contrôle, on conserve l'outline custom\n if (!event.currentTarget?.contains(event.relatedTarget)) {\n this.toolbarFocused = false;\n }\n }\n\n /**\n * Permey de se mettre à l'écoute des actions utilisateur sur la barre d'outils.\n */\n protected onToolbarKeydown(event: KeyboardEvent) {\n const key = event.key;\n switch (key) {\n case 'Tab':\n event.preventDefault(); // évite la perte de focus asynchrone sur l'éditeur due à la propagation de l'event\n this.editorService.requestFocusOnEditorContent();\n this.toolbarFocused = false;\n break;\n\n case 'ArrowRight':\n case 'ArrowLeft':\n event.preventDefault(); // évite la prise de focus sur l'éditeur suite propagation event\n let focusableElt = key === 'ArrowRight' ? this.findNextSiblingControl() : this.findPrevSiblingControl();\n if (focusableElt) {\n focusableElt.focus();\n this.lastActiveControlElement = focusableElt;\n }\n break;\n\n default:\n break;\n }\n }\n\n private performAction(action: any, value?: any) {\n if (action) {\n this.editorService.requestFormatting({ action: action, value: value });\n } else {\n this.editorService.requestFormatting(undefined);\n }\n\n this.toolbarFocused = false;\n }\n\n /**\n * Détecte le clic en dehors du composant pour retirer le marqueur de focus custom sur la toolbar.\n */\n private clickOutside(event: any) {\n // comme on s'est mis à l'écoute de l'event hors Angular pour des raisons de perf, on doit mettre à jour l'input\n // en s'exécutant dans la zone Angular\n this.ngZone.run(() => {\n if (!this.elementRef.nativeElement.contains(event.target)) {\n this.toolbarFocused = false;\n }\n });\n }\n\n private _buildSelectOption(format: Format, control: DsfrValueFormatControl) {\n return { label: this._getOptionLabel(format, control.value), value: control.value };\n }\n\n private _getOptionLabel(format: Format, value: any): string {\n const record: Record<string, string> = FORMAT_I18N_KEYS[format] as Record<string, string>;\n const key = record[String(value)];\n return this.i18n.t(key);\n }\n\n private findNextSiblingControl(): HTMLElement | undefined {\n return this.findSiblingControl('nextElementSibling', () => this.getFirstToolbarControl());\n }\n\n private findPrevSiblingControl(): HTMLElement | undefined {\n return this.findSiblingControl('previousElementSibling', () => this.getLastToolbarControl());\n }\n\n /**\n * Recherche l'élément de contrôle focusable adjaçant à l'élément de contrôle actif.\n *\n * @param siblingProperty La propriété permettant de récupérer le voisin suivant ou précédent (selon)\n * @param failoverFunction La fonction permettant de récupérer un élément focusable lorsqu'il n'y plus de suivant\n * @returns Une élément HTML sur lequel il sera possible de prendre le focus\n */\n private findSiblingControl(\n siblingProperty: 'nextElementSibling' | 'previousElementSibling',\n failoverFunction: Function,\n ): HTMLElement | undefined {\n let result: HTMLElement | undefined = undefined;\n let activeElt: Element | null = document.activeElement;\n //TODO: imbrication conditionnelle complexe > à mettre sous harnais de tests pour ensuite tenter une simplification\n if (activeElt) {\n if (activeElt.tagName === 'BUTTON') {\n if (activeElt.closest('dsfr-buttons-group')) {\n const dsfrButtonsGroup = activeElt.parentElement;\n if (dsfrButtonsGroup && dsfrButtonsGroup[siblingProperty])\n // directly try to get next sibling\n result = dsfrButtonsGroup[siblingProperty] as HTMLElement;\n } else if (activeElt.closest('span.group')) {\n const spanControl = activeElt.closest('span.control');\n if (spanControl && spanControl[siblingProperty]) {\n // directly try to get parent span to get next sibling\n result = spanControl[siblingProperty] as HTMLElement;\n }\n }\n }\n if (result === undefined) {\n while (activeElt.parentElement !== null && !activeElt.parentElement.classList.contains('editor-toolbar')) {\n activeElt = activeElt.parentElement;\n }\n if (activeElt.parentElement?.classList.contains('editor-toolbar')) {\n result = activeElt[siblingProperty] as HTMLElement;\n }\n }\n }\n\n return result ? this.getFocusableControl(result, siblingProperty === 'previousElementSibling') : failoverFunction();\n }\n\n private getFirstToolbarControl(): HTMLElement | undefined {\n return this.getFocusableControl(this.controlsParent.nativeElement);\n }\n\n private getLastToolbarControl(): HTMLElement | undefined {\n let lastOne = this.controlsParent.nativeElement.lastElementChild;\n return this.getFocusableControl(lastOne, true);\n }\n\n /**\n * Search focusable control (depth first traversal).\n */\n private getFocusableControl(elt: HTMLElement, reverse = false): HTMLElement | undefined {\n if (elt.tagName === 'BUTTON' || elt.tagName === 'SELECT') {\n return elt;\n } else if (elt.children.length !== 0) {\n let children = Array.from(elt.children);\n if (reverse) {\n children = children.reverse();\n }\n for (const c of children) {\n const focusable = this.getFocusableControl(c as HTMLElement);\n if (focusable) {\n return focusable;\n }\n }\n }\n return undefined;\n }\n\n /**\n * Répercute les formats actifs de la sélection courante vers la toolbar.\n */\n private _updateToolbar(formats: Formatting) {\n // on force l'exécution dans zonejs car l'événement provient de Quill et n'est pas géré par Angular\n this.ngZone.run(() => {\n this._currentSelectionFormats = formats;\n this.selectModel.header = this._getSelectedOption(FormatConst.HEADER);\n this.selectModel.size = this._getSelectedOption(FormatConst.SIZE);\n });\n // on met à jour les états aria-pressed sur les boutons\n //TODO: à remplacer par un input ariaPressed sur dsfr-button cf. ngx-dsfr#\n const dsfrButtons: NodeList = this.elementRef.nativeElement.querySelectorAll('dsfr-button');\n dsfrButtons.forEach((n) => {\n const e = n as HTMLElement;\n const controlInfo = e.getAttribute('data-testid')?.split('-');\n if (controlInfo) {\n const pressed =\n controlInfo.length === 1\n ? this.isFormatActive(controlInfo[0] as Format)\n : this.isValueActive(controlInfo[0] as Format, controlInfo[1] as string);\n\n if (pressed) {\n e.querySelector('button')?.setAttribute('aria-pressed', 'true');\n } else {\n e.querySelector('button')?.removeAttribute('aria-pressed');\n }\n }\n });\n }\n\n /**\n * Retourne la valeur à sélectionner dans le cas de contôle multivalué (select).\n *\n * @internal\n */\n private _getSelectedOption(format: Format): unknown | null {\n const debug =\n this._currentSelectionFormats && this._currentSelectionFormats[format]\n ? this._currentSelectionFormats[format]\n : null;\n return debug;\n }\n\n /**\n * Supprime toutes les mises en forme sur la sélection courante.\n */\n private _removeSelectionFormats() {\n this.performAction(undefined);\n }\n\n /**\n * Applique le format défini par l'utilisateur via la toolbar sur la sélection courante de l'éditeur.\n */\n private _applyFormat(control: FormatControl, value?: any) {\n // Cas particulier du clean\n if (control.name === FormatConst.CLEAN) {\n this._removeSelectionFormats();\n } else {\n const baseControl: DsfrBaseFormatControl = control as DsfrBaseFormatControl;\n switch (baseControl.mode) {\n case ControlMode.TOGGLE:\n // cas d'une toggle action, on détermine la valeur à transmettre à l'éditeur\n value = this._currentSelectionFormats && control.name in this._currentSelectionFormats ? false : true;\n break;\n\n case ControlMode.RADIO:\n // si l'action envoyée est déjà présente dans la sélection il faut envoyer \"false\" comme valeur\n value =\n this._currentSelectionFormats &&\n control.name in this._currentSelectionFormats &&\n this._currentSelectionFormats[control.name] === value\n ? false\n : value;\n break;\n\n case ControlMode.PROMPT:\n switch (baseControl.name) {\n case FormatConst.IMAGE:\n //TODO: à remplacer par quelque chose de plus sexy\n this._displayPromptElement('#prompt-' + FormatConst.IMAGE);\n break;\n\n case FormatConst.LINK:\n //TODO: à remplacer par quelque chose de