From ac4590378f5dd2b702ac31cad52b25bf5ae60aa3 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Thu, 2 Dec 2021 17:13:55 +0100
Subject: [PATCH] [editor] Add hanging indent functionality to TextEditor

---
 .../src/app/text-editor/hanging-indent.ts     | 82 +++++++++++++++++++
 .../rich-text-editor.component.html           |  8 ++
 .../text-editor/rich-text-editor.component.ts | 12 ++-
 3 files changed, 101 insertions(+), 1 deletion(-)
 create mode 100644 projects/editor/src/app/text-editor/hanging-indent.ts

diff --git a/projects/editor/src/app/text-editor/hanging-indent.ts b/projects/editor/src/app/text-editor/hanging-indent.ts
new file mode 100644
index 000000000..5b7af447d
--- /dev/null
+++ b/projects/editor/src/app/text-editor/hanging-indent.ts
@@ -0,0 +1,82 @@
+import { Command, Extension } from '@tiptap/core';
+import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
+
+declare module '@tiptap/core' {
+  interface Commands<ReturnType> {
+    hangingIndent: {
+      hangIndent: () => ReturnType;
+      unhangIndent: () => ReturnType;
+    };
+  }
+}
+
+export const HangingIndent = Extension.create({
+  name: 'hangingIndent',
+
+  defaultOptions: {
+    types: ['paragraph']
+  },
+
+  addGlobalAttributes() {
+    return [
+      {
+        types: this.options.types,
+        attributes: {
+          hangingIndent: {
+            default: false,
+            renderHTML: attributes => (attributes.hangingIndent ?
+              { style: 'text-indent: -20px' } : { style: 'text-indent: 0px' }
+            ),
+            parseHTML: element => element.style.textIndent === '-20px'
+          }
+        }
+      }
+    ];
+  },
+
+  addCommands() {
+    const setNodeIndentMarkup = (tr: Transaction, pos: number, indent: boolean): Transaction => {
+      const node = tr?.doc?.nodeAt(pos);
+
+      if (node) {
+        if (indent !== node.attrs.indent) {
+          const nodeAttrs = { ...node.attrs, hangingIndent: indent };
+          return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);
+        }
+      }
+      return tr;
+    };
+
+    const updateIndentLevel = (tr: Transaction, indent: boolean): Transaction => {
+      const { doc, selection } = tr;
+
+      if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) {
+        const { from, to } = selection;
+        doc.nodesBetween(from, to, (node, pos) => {
+          setNodeIndentMarkup(tr, pos, indent);
+          return false;
+        });
+      }
+
+      return tr;
+    };
+    const applyIndent: (indent: boolean) => () => Command =
+      indent => () => ({ tr, state, dispatch }) => {
+        const { selection } = state;
+        tr = tr.setSelection(selection);
+        tr = updateIndentLevel(tr, indent);
+
+        if (tr.docChanged) {
+          dispatch?.(tr);
+          return true;
+        }
+
+        return false;
+      };
+
+    return {
+      hangIndent: applyIndent(true),
+      unhangIndent: applyIndent(false)
+    };
+  }
+});
diff --git a/projects/editor/src/app/text-editor/rich-text-editor.component.html b/projects/editor/src/app/text-editor/rich-text-editor.component.html
index a0ac6502a..b70bd8eca 100644
--- a/projects/editor/src/app/text-editor/rich-text-editor.component.html
+++ b/projects/editor/src/app/text-editor/rich-text-editor.component.html
@@ -125,6 +125,14 @@
               (click)="outdent()">
         <mat-icon>format_indent_decrease</mat-icon>
       </button>
+      <button mat-icon-button matTooltip="Hängender Einzug" [matTooltipShowDelay]="300"
+              (click)="hangIndent()">
+        <mat-icon>segment</mat-icon>
+      </button>
+      <button mat-icon-button matTooltip="Hängenden Einzug entfernen" [matTooltipShowDelay]="300"
+              (click)="unhangIndent()">
+        <mat-icon>view_headline</mat-icon>
+      </button>
     </div>
     <div fxLayout="row">
       <div class="combo-button" fxLayout="row" [style.background-color]="editor.isActive('bulletList') ? 'lightgrey' : ''">
diff --git a/projects/editor/src/app/text-editor/rich-text-editor.component.ts b/projects/editor/src/app/text-editor/rich-text-editor.component.ts
index a3e8f26a9..a94b144af 100644
--- a/projects/editor/src/app/text-editor/rich-text-editor.component.ts
+++ b/projects/editor/src/app/text-editor/rich-text-editor.component.ts
@@ -13,6 +13,7 @@ import { Highlight } from '@tiptap/extension-highlight';
 import { TextAlign } from '@tiptap/extension-text-align';
 import { Heading } from '@tiptap/extension-heading';
 import { Indent } from './indent';
+import { HangingIndent } from './hanging-indent';
 import { customParagraph } from './paragraph-extension';
 import { fontSizeExtension } from './font-size-extension';
 import { bulletListExtension } from './bulletList-extension';
@@ -57,7 +58,8 @@ export class RichTextEditorComponent implements AfterViewInit {
       customParagraph,
       fontSizeExtension,
       bulletListExtension,
-      orderedListExtension
+      orderedListExtension,
+      HangingIndent
     ]
   });
 
@@ -155,4 +157,12 @@ export class RichTextEditorComponent implements AfterViewInit {
   insertSpecialChar(char: string): void {
     this.editor.chain().insertContent(char).focus().run();
   }
+
+  hangIndent() {
+    this.editor.commands.hangIndent();
+  }
+
+  unhangIndent() {
+    this.editor.commands.unhangIndent();
+  }
 }
-- 
GitLab