@edugouvfr/ngx-dsfr-ext
Version:
NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)
1 lines • 111 kB
Source Map (JSON)
{"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/add-title.directive.ts","../../../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 label: 'editor.controls.align.label',\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 label: 'editor.controls.list.label',\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 de l'é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 * Action de type interrupteur on/off avec valeur booléenne.\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 * Représente un groupe d'actions.\n */\nexport class DsfrGroupFormatControl {\n group = true;\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 i18nLabelKey: string,\n public controls: DsfrSingleFormatControl[],\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 * Tableau permettant de regrouper des contrôles uniques avec un libellé accessible.\n */\nexport class ControlArray extends Array<DsfrSingleFormatControl> {\n name: string;\n constructor(name: string, ...items: DsfrSingleFormatControl[]) {\n super(...items);\n this.name = name;\n }\n}\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 | ControlArray;\n","import {\n AlignValue,\n ControlArray,\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 new ControlArray(\n 'text',\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 ControlArray(\n 'palettes',\n new DsfrPromptFormatControl(FormatConst.COLOR),\n new DsfrPromptFormatControl(FormatConst.BACKGROUND),\n ),\n // mise en forme avancée\n new ControlArray(\n 'advanced',\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 legend: `Editing area`,\n hint: `Depending on the actions used, formatting will target either the current selection (e.g., text size or style,\n script, colors, link) or the line on which the cursor is positioned (e.g., heading level, quotation, code, \n indentation, lists, alignments).`,\n announce: {\n pre: `The `,\n applied: ` formatting has been successfully applied`,\n removed: ` formatting has been successfully removed`,\n cleaned: 'All formatting has been successfully removed from the selection',\n },\n placeholder: 'Please edit your content',\n controls: {\n align: { label: 'Align', left: 'Left', center: 'Center', right: 'Right', justify: 'Justify' },\n background: `Background color`,\n blockquote: 'Quote',\n clean: 'Remove all formatting from the selection',\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: { label: '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 text: 'Text style',\n advanced: 'Advanced styles',\n palettes: 'Palettes',\n },\n },\n};\n","export const MESSAGES_FR = {\n editor: {\n legend: `Zone d'édition`,\n hint: `Selon les actions utilisées, la mise en forme ciblera soit la sélection courante (ex. : taille ou style du \n texte, script, couleurs, lien) soit la ligne sur laquelle le curseur est positionné (ex. : niveau de titre, \n citation, code, indentation, listes, alignements).`,\n announce: {\n pre: `La mise en forme `,\n applied: ` a été appliquée avec succès`,\n removed: ` a été supprimée avec succès`,\n cleaned: 'Toutes les mises en forme ont été supprimées sur la sélection',\n },\n placeholder: 'Veuillez éditer votre contenu',\n controls: {\n align: {\n label: 'Alignement',\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: 'Supprimer toutes les mises en forme sur la sélection',\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: { label: '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 text: 'Style du texte',\n advanced: 'Styles avancés',\n palettes: 'Palettes',\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 * Observable permettant de notifier la prise en charge effective d'une action de formatage.\n */\n readonly editorActionPerformed$: Observable<FormattingRequest>;\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 /**\n * Subject sur les demandes de prise de focus sur l'éditeur.\n */\n private _editorActionPerformedSubject: ReplaySubject<FormattingRequest>;\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 this._editorActionPerformedSubject = new ReplaySubject<FormattingRequest>(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 this.editorActionPerformed$ = this._editorActionPerformedSubject.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 /**\n * Permet de notifier l'application effective d'uen action de formatage dans l'éditeur.\n */\n emitEditorActionPerformed(value: FormattingRequest) {\n this._editorActionPerformedSubject.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 { DsfrI18nPipe, DsfrI18nService, DsfrSeverity, DsfrSeverityConst, LangService } 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 imports: [CommonModule, DsfrI18nPipe],\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 * Permet de masquer visuellement la légende en le réservant aux dispositifs de restitution.\n */\n @Input() legendSrOnly = true;\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 /** @internal */\n public langSvc = inject(LangService);\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 this._quill.root.setAttribute('aria-labelledby', this.editorId + '-legend');\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.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 this.editorService.emitEditorActionPerformed(req);\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 // Écoute les changements de langue pour maj le placeholder\n this._subscriptions.push(\n this.langSvc.langChange$.subscribe(() => {\n this._quill.root.dataset['placeholder'] = this.i18n.t('editor.placeholder');\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({ message, messagesGroupId }: SimpleChanges): void {\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","<fieldset role=\"presentation\">\n <legend id=\"{{ editorId }}-legend\" [class.fr-sr-only]=\"legendSrOnly\">{{ 'editor.legend' | dsfrI18n }}</legend>\n <div id=\"{{ editorId }}\"></div>\n</fieldset>\n\n<div class=\"fr-messages-group\" [attr.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, inject } from '@angular/core';\n\n@Directive({\n selector: '[addTitle]',\n standalone: true,\n})\nexport class AddTitleDirective implements AfterViewInit {\n /**\n * La texte du tooltip.\n */\n @Input('addTitle') text = '';\n\n /**\n * Le sélecteur à utilsier pour identifier l'élément cible sur lequel positionner l'attribut `title`.\n */\n @Input('addTitleSelector') selector?: string = undefined;\n\n private host = inject<ElementRef<HTMLElement>>(ElementRef);\n\n ngAfterViewInit(): void {\n if (this.text && this.text.length) {\n this.applyTitle();\n }\n }\n\n private applyTitle(): void {\n const elem: Element | null = this.selector ? this.host.nativeElement.querySelector<Element>(this.selector) : null;\n if (elem) {\n elem.setAttribute('title', this.text);\n }\n }\n}\n","import { AfterViewInit, Directive, ElementRef, Input, Renderer2, inject } 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 private _elementRef = inject(ElementRef);\n private _renderer = inject(Renderer2);\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 { FocusMonitor } from '@angular/cdk/a11y';\nimport { Subscription } from 'rxjs';\nimport { AddTitleDirective } from '../../directives/add-title.directive';\nimport { UnfocusableDirective } from '../../directives/unfocusable.directive';\nimport { DsfrEditorService, Formatting } from '../../editor.service';\nimport { DEFAULT_TOOLBAR_OPTIONS } from './editor-toolbar-default-options';\nimport {\n ControlArray,\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' | 'array' | '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 imports: [\n CommonModule,\n FormsModule,\n ReactiveFormsModule,\n DsfrSkiplinksModule,\n DsfrFormSelectModule,\n DsfrButtonModule,\n UnfocusableDirective,\n DsfrI18nPipe,\n AddTitleDirective,\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('toolbar') toolbar!: 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 la valeur tabindex de la toolbar.\n */\n protected focusable = true;\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 private focusMonitor = inject(FocusMonitor);\n private el = inject(ElementRef);\n\n ngOnInit(): void {\n this._sub.push(\n this.editorService.currentFormatting$.subscribe((formatting: Formatting) => {\n this._updateToolbar(formatting);\n }),\n );\n // cdk permet de positionner des classes css sur l'hôte indiquant si l'utilisateur utilise la souris ou le clavier\n this.focusMonitor.monitor(this.el, true);\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.forEach((sub: Subscription) => sub.unsubscribe());\n this._unlistenFn();\n }\n\n /**\n * Retourne la clé du libellé internationalisé.\n * @param control le contôle de formatage\n */\n getI18nKey(format: Format, value: any = undefined): string {\n let key = FORMAT_I18N_KEYS[format];\n // si action multivaluée (radio ou select) il faut aller regarder la valeur pour obtenir la clé i18n\n // sauf si la valeur est false ce qui correspond à une suppression et on ne peut pas connaître la valeur exacte\n if (value !== undefined && this.isMultiValuedControl(format)) {\n // console.log('getI18nKey', value);\n key = key as Record<string, string>;\n key = key[typeof value === 'boolean' ? 'label' : String(value)];\n }\n\n return key as string;\n }\n\n /**\n * Construit la clé i18n correspondant au nom du tableau de contrôles passé en paramètre.\n */\n protected getArrayI18nKey(controlArray: ControlArray) {\n return 'editor.controls.' + controlArray.name;\n }\n\n /**\n * Construit la clé i18n correspondant au groupe de contrôles multivalués passé en paramètre.\n */\n protected getGroupI18nKey(format: Format) {\n return 'editor.controls.' + format + '.label';\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 // Tableau de contrôles\n if (Array.isArray(control)) {\n return control.length ? 'array' : 'btn';\n }\n\n // Contrôle multivalué\n if ('values' in control && control.values?.length) {\n const mapping: Partial<Record<ControlMode, ControlViewType>> = {\n [ControlMode.RADIO]: 'radio',\n [ControlMode.SELECT]: 'select',\n };\n\n return mapping[control.mode] ?? 'btn';\n }\n\n return 'btn';\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 * 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(event: FocusEvent) {\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 * Handler sur la prise de focus d'un élément de la toolbar (gestion du tabindex de la toolbar).\n */\n protected onToolbarFocusIn(event: FocusEvent): void {\n // lorsqu'un élément de la toolbar obtient le focus alors la toolbar ne doit plus être focusable\n this.focusable = false;\n }\n\n /**\n * Handler sur la perte de focus d'un élément de la toolbar (gestion du tabindex de la toolbar).\n */\n protected onToolbarFocusOut(event: FocusEvent): void {\n // lorsqu'un élément de la toolbar perd le focus\n // il faut regarder si le focus a été positionné sur un autre élément de la toolbar\n // si ce n'est pas le cas la toolbar doit être de nouveau focusable\n this.focusable = !this.toolbar.nativeElement.contains(document.activeElement);\n }\n\n /**\n * Permet 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 if (!event.shiftKey) {\n event.preventDefault(); // évite la perte de focus asynchrone sur l'éditeur due à la propagation de l'event\n this.editorService.requestFocusOnEditorContent();\n }\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 /**\n * Retourne vrai si le format correspond à un contrôle d'action multivaluée, faux sinon.\n *\n * @param format le descripteur de format à considérer\n */\n private isMultiValuedControl(format: Format): boolean {\n let control: any | undefined = this.toolbarOptions.flat(Infinity).find((c) => c.name === format);\n\n return control && control.values && control.values.length;\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\n return this.i18n.t(key);\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\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.focusable = true;\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 findNextSiblingControl(): HTMLElement | null {\n return this.findSiblingControl('nextElementSibling', () => this.getFirstToolbarControl());\n }\n\n private findPrevSiblingControl(): HTMLElement | null {\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 * Cette fonction est utilisée pour pouvoir déplacer le focus lors de la navigation au clavier.\n *\n * @param s