Browse Source

fix(admin-ui): Allow target attribute on <a> tags in rich text editor

Fixes #2281
Michael Bromley 2 years ago
parent
commit
8f72e1e9cf
20 changed files with 123 additions and 57 deletions
  1. 27 27
      packages/admin-ui/i18n-coverage.json
  2. 19 7
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/link-dialog/link-dialog.component.html
  3. 5 3
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/link-dialog/link-dialog.component.ts
  4. 40 1
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/custom-nodes.ts
  5. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.service.ts
  6. 6 0
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.scss
  7. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  8. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  9. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  10. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  11. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  12. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  13. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  14. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  15. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  16. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  17. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  18. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  19. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  20. 0 6
      packages/admin-ui/src/lib/static/styles/global/_overrides.scss

+ 27 - 27
packages/admin-ui/i18n-coverage.json

@@ -1,70 +1,70 @@
 {
-  "generatedOn": "2023-07-03T12:30:16.598Z",
-  "lastCommit": "8669ef4b27abfdc1e4446442a1a9d65cb1fd1927",
+  "generatedOn": "2023-07-12T12:33:44.002Z",
+  "lastCommit": "4d01ab53b3d078f4cc2478d764d3dc87d61520da",
   "translationStatus": {
     "cs": {
-      "tokenCount": 739,
+      "tokenCount": 740,
       "translatedCount": 545,
       "percentage": 74
     },
     "de": {
-      "tokenCount": 739,
-      "translatedCount": 738,
+      "tokenCount": 740,
+      "translatedCount": 739,
       "percentage": 100
     },
     "en": {
-      "tokenCount": 739,
-      "translatedCount": 738,
+      "tokenCount": 740,
+      "translatedCount": 739,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 739,
-      "translatedCount": 738,
+      "tokenCount": 740,
+      "translatedCount": 739,
       "percentage": 100
     },
     "fr": {
-      "tokenCount": 739,
-      "translatedCount": 734,
+      "tokenCount": 740,
+      "translatedCount": 735,
       "percentage": 99
     },
     "it": {
-      "tokenCount": 739,
-      "translatedCount": 569,
+      "tokenCount": 740,
+      "translatedCount": 570,
       "percentage": 77
     },
     "pl": {
-      "tokenCount": 739,
-      "translatedCount": 379,
+      "tokenCount": 740,
+      "translatedCount": 380,
       "percentage": 51
     },
     "pt_BR": {
-      "tokenCount": 739,
-      "translatedCount": 738,
+      "tokenCount": 740,
+      "translatedCount": 739,
       "percentage": 100
     },
     "pt_PT": {
-      "tokenCount": 739,
-      "translatedCount": 578,
+      "tokenCount": 740,
+      "translatedCount": 579,
       "percentage": 78
     },
     "ru": {
-      "tokenCount": 739,
-      "translatedCount": 568,
+      "tokenCount": 740,
+      "translatedCount": 569,
       "percentage": 77
     },
     "uk": {
-      "tokenCount": 739,
-      "translatedCount": 568,
+      "tokenCount": 740,
+      "translatedCount": 569,
       "percentage": 77
     },
     "zh_Hans": {
-      "tokenCount": 739,
-      "translatedCount": 514,
+      "tokenCount": 740,
+      "translatedCount": 515,
       "percentage": 70
     },
     "zh_Hant": {
-      "tokenCount": 739,
-      "translatedCount": 359,
+      "tokenCount": 740,
+      "translatedCount": 360,
       "percentage": 49
     }
   }

+ 19 - 7
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/link-dialog/link-dialog.component.html

@@ -1,14 +1,26 @@
 <form [formGroup]="form">
-    <vdr-form-field [label]="'editor.link-href' | translate" for="href">
-        <input id="href" type="text" formControlName="href" />
-    </vdr-form-field>
-    <vdr-form-field [label]="'editor.link-title' | translate" for="title">
-        <input id="title" type="text" formControlName="title" />
-    </vdr-form-field>
+    <div class="form-grid">
+        <vdr-form-field [label]="'editor.link-href' | translate" for="href" class="form-grid-span">
+            <input id="href" type="text" formControlName="href"/>
+        </vdr-form-field>
+        <vdr-form-field [label]="'editor.link-title' | translate" for="title">
+            <input id="title" type="text" formControlName="title"/>
+        </vdr-form-field>
+        <vdr-form-field [label]="'editor.link-target' | translate" for="target">
+            <select id="target" formControlName="target">
+                <option value=""></option>
+                <option value="_self">_self</option>
+                <option value="_blank">_blank</option>
+                <option value="_parent">_parent</option>
+                <option value="_top">_top</option>
+            </select>
+        </vdr-form-field>
+    </div>
 </form>
 <ng-template vdrDialogButtons>
     <button type="button" class="btn btn-secondary" (click)="remove()" *ngIf="existing">
-        <clr-icon shape="unlink"></clr-icon> {{ 'editor.remove-link' | translate }}
+        <clr-icon shape="unlink"></clr-icon>
+        {{ 'editor.remove-link' | translate }}
     </button>
     <button type="submit" (click)="select()" class="btn btn-primary" [disabled]="form.invalid">
         {{ 'editor.set-link' | translate }}

+ 5 - 3
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/link-dialog/link-dialog.component.ts

@@ -1,11 +1,12 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
+import { FormControl, UntypedFormGroup, Validators } from '@angular/forms';
 
 import { Dialog } from '../../../../providers/modal/modal.types';
 
 export interface LinkAttrs {
     href: string;
     title: string;
+    target?: string;
 }
 
 @Component({
@@ -22,8 +23,9 @@ export class LinkDialogComponent implements OnInit, Dialog<LinkAttrs> {
 
     ngOnInit(): void {
         this.form = new UntypedFormGroup({
-            href: new UntypedFormControl(this.existing ? this.existing.href : '', Validators.required),
-            title: new UntypedFormControl(this.existing ? this.existing.title : ''),
+            href: new FormControl(this.existing ? this.existing.href : '', Validators.required),
+            title: new FormControl(this.existing ? this.existing.title : ''),
+            target: new FormControl(this.existing ? this.existing.target : null),
         });
     }
 

+ 40 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/custom-nodes.ts

@@ -1,4 +1,4 @@
-import { Attrs, DOMParser, DOMSerializer, Node, NodeSpec } from 'prosemirror-model';
+import { Attrs, DOMParser, DOMSerializer, Node, NodeSpec, Mark, MarkSpec } from 'prosemirror-model';
 import { NodeViewConstructor } from 'prosemirror-view';
 
 export const iframeNode: NodeSpec = {
@@ -58,3 +58,42 @@ export const iframeNodeView: NodeViewConstructor = (node, view, getPos, decorati
         dom: wrapper,
     };
 };
+
+export const linkMark: MarkSpec = {
+    attrs: {
+        href: {},
+        title: { default: null },
+        target: { default: null },
+        rel: { default: null },
+        download: { default: null },
+        type: { default: null },
+    },
+    inclusive: false,
+    parseDOM: [
+        {
+            tag: 'a[href]',
+            getAttrs(dom: HTMLElement | string) {
+                if (typeof dom !== 'string') {
+                    return {
+                        href: dom.getAttribute('href'),
+                        title: dom.getAttribute('title'),
+                        target: dom.getAttribute('target'),
+                        rel: dom.getAttribute('rel'),
+                        download: dom.getAttribute('download'),
+                        type: dom.getAttribute('type'),
+                    };
+                } else {
+                    return null;
+                }
+            },
+        },
+    ],
+    toDOM(node) {
+        const { href, title, target, rel, download, type } = node.attrs;
+        const attrs = { href, title, rel, download, type };
+        if (target) {
+            attrs['target'] = target;
+        }
+        return ['a', attrs, 0];
+    },
+};

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.service.ts

@@ -15,7 +15,7 @@ import { Observable } from 'rxjs';
 import { ModalService } from '../../../../providers/modal/modal.service';
 
 import { ContextMenuService } from './context-menu/context-menu.service';
-import { iframeNode, iframeNodeView } from './custom-nodes';
+import { iframeNode, iframeNodeView, linkMark } from './custom-nodes';
 import { buildInputRules } from './inputrules';
 import { buildKeymap } from './keymap';
 import { customMenuPlugin } from './menu/menu-plugin';
@@ -41,7 +41,7 @@ export class ProsemirrorService {
         nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block')
             .append(getTableNodes() as any)
             .addToEnd('iframe', iframeNode),
-        marks: schema.spec.marks,
+        marks: schema.spec.marks.update('link', linkMark),
     });
     private enabled = true;
     /**

+ 6 - 0
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.scss

@@ -67,6 +67,12 @@ label.rich-text-label {
         max-width: 100%;
     }
 
+    a:link,
+    a:visited {
+        color: var(--color-primary-700);
+        text-decoration: none;
+    }
+
     .iframe-wrapper {
         width: 100%;
         text-align: center;

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -459,6 +459,7 @@
     "image-title": "Titulek",
     "insert-image": "Vložit obrázek",
     "link-href": "Odkaz",
+    "link-target": "",
     "link-title": "Odkaz titulky",
     "remove-link": "Odebrat odkaz",
     "set-link": "Nastavit odkaz"

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -459,6 +459,7 @@
     "image-title": "Titel",
     "insert-image": "Bild einfügen",
     "link-href": "Link 'href'",
+    "link-target": "",
     "link-title": "Link 'title'",
     "remove-link": "Entfernen",
     "set-link": "Link setzen"
@@ -770,4 +771,4 @@
     "job-result": "Job-Ergebnis",
     "job-state": "Job-Status"
   }
-}
+}

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -459,6 +459,7 @@
     "image-title": "Title",
     "insert-image": "Insert image",
     "link-href": "Link href",
+    "link-target": "Link target",
     "link-title": "Link title",
     "remove-link": "Remove",
     "set-link": "Set link"

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -459,6 +459,7 @@
     "image-title": "Título",
     "insert-image": "Insertar imagen",
     "link-href": "Enlace href",
+    "link-target": "",
     "link-title": "Título del enlace",
     "remove-link": "Eliminar",
     "set-link": "Establecer enlace"
@@ -770,4 +771,4 @@
     "job-result": "Resultado",
     "job-state": "Estado"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -459,6 +459,7 @@
     "image-title": "Titre",
     "insert-image": "Insérer image",
     "link-href": "Adresse du lien",
+    "link-target": "",
     "link-title": "Titre du lien",
     "remove-link": "Retirer le lien",
     "set-link": "Définir lien"
@@ -770,4 +771,4 @@
     "job-result": "Résultat de la tâche",
     "job-state": "Etat de la tâche"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -459,6 +459,7 @@
     "image-title": "Titolo",
     "insert-image": "Inserisci immagine",
     "link-href": "Link",
+    "link-target": "",
     "link-title": "Titolo link",
     "remove-link": "Rimuovi",
     "set-link": "Imposta link"
@@ -770,4 +771,4 @@
     "job-result": "Risultato operazione",
     "job-state": "Stato operazione"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -459,6 +459,7 @@
     "image-title": "Tytuł",
     "insert-image": "Wstaw obraz",
     "link-href": "Link href",
+    "link-target": "",
     "link-title": "Tytuł linku",
     "remove-link": "Usuń",
     "set-link": "Ustaw link"
@@ -770,4 +771,4 @@
     "job-result": "Rezultat zlecenia",
     "job-state": "Status zlecenia"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -459,6 +459,7 @@
     "image-title": "Título",
     "insert-image": "Inserir imagem",
     "link-href": "Link de referência",
+    "link-target": "",
     "link-title": "Título do link",
     "remove-link": "Remover",
     "set-link": "Setar link"
@@ -770,4 +771,4 @@
     "job-result": "Resultado do trabalho",
     "job-state": "Estado do trabalho"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -459,6 +459,7 @@
     "image-title": "Título",
     "insert-image": "Inserir imagem",
     "link-href": "Link de referência",
+    "link-target": "",
     "link-title": "Título do link",
     "remove-link": "Remover",
     "set-link": "Atribuir link"
@@ -770,4 +771,4 @@
     "job-result": "Resultado do trabalho",
     "job-state": "Estado do trabalho"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -459,6 +459,7 @@
     "image-title": "Заголовок",
     "insert-image": "Вставить изображение",
     "link-href": "Ссылка",
+    "link-target": "",
     "link-title": "Заголовок ссылки",
     "remove-link": "Удалять",
     "set-link": "Установить ссылку"
@@ -770,4 +771,4 @@
     "job-result": "Результат задания",
     "job-state": "Состояние задания"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -459,6 +459,7 @@
     "image-title": "Заголовок",
     "insert-image": "Вставити зображення",
     "link-href": "Посилання",
+    "link-target": "",
     "link-title": "Заголовок посилання",
     "remove-link": "Удалить",
     "set-link": "Встановити посилання"
@@ -770,4 +771,4 @@
     "job-result": "Результат завдання",
     "job-state": "Стан завдання"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -459,6 +459,7 @@
     "image-title": "图片标题",
     "insert-image": "插入图片",
     "link-href": "链接",
+    "link-target": "",
     "link-title": "链接标题",
     "remove-link": "删除链接",
     "set-link": "设置链接"
@@ -770,4 +771,4 @@
     "job-result": "任务结果",
     "job-state": "任务状态"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -459,6 +459,7 @@
     "image-title": "圖片標題",
     "insert-image": "插入圖片",
     "link-href": "連結",
+    "link-target": "",
     "link-title": "連結標題",
     "remove-link": "移除連結",
     "set-link": "設定連結"
@@ -770,4 +771,4 @@
     "job-result": "",
     "job-state": ""
   }
-}
+}

+ 0 - 6
packages/admin-ui/src/lib/static/styles/global/_overrides.scss

@@ -17,7 +17,6 @@ h6:not([cds-text]) {
 
 a:link,
 a:visited {
-    color: var(--clr-global-link-color);
     text-decoration: none;
 }
 
@@ -35,11 +34,6 @@ a:visited {
     cursor: pointer;
 }
 
-a:link,
-a:visited {
-    color: var(--clr-btn-link-color);
-}
-
 a:focus,
 button:focus {
     outline-color: var(--color-primary-400);