From ab092ae3e0adf1c4569aadea1b010972dab51c66 Mon Sep 17 00:00:00 2001
From: michi <michael@adornis.de>
Date: Thu, 13 Feb 2025 22:02:47 +0000
Subject: [PATCH 1/2] feat: integrate flat list package + custom stylings per
 config files

---
 .../client/marks/text-align.ts                |   8 +-
 .../nodes/grid/grid-cell/node-grid-cell.ts    |   2 +-
 .../client/nodes/grid/node-grid.ts            |  19 +--
 .../client/nodes/hard-break.ts                |  31 +++++
 .../client/nodes/list/keymapPlugin.ts         |   7 +-
 .../client/nodes/list/list.ts                 | 119 +++++-------------
 .../client/nodes/paragraph.ts                 |   9 +-
 .../client/prosemirror-editor.ts              |  81 +++++++-----
 lab/html-based-buildify/client/starterPack.ts |   6 +-
 lab/html-based-buildify/client/types.ts       |   3 +-
 .../client/x-prosemirror-toolbar.ts           |   7 +-
 lab/html-based-buildify/schema/nodes/list.ts  |  42 -------
 12 files changed, 150 insertions(+), 184 deletions(-)

diff --git a/lab/html-based-buildify/client/marks/text-align.ts b/lab/html-based-buildify/client/marks/text-align.ts
index a5541aac1f..580e48fc8d 100644
--- a/lab/html-based-buildify/client/marks/text-align.ts
+++ b/lab/html-based-buildify/client/marks/text-align.ts
@@ -1,4 +1,4 @@
-import type { EditorState, Transaction } from 'prosemirror-state';
+import { TextSelection, type EditorState, type Transaction } from 'prosemirror-state';
 import type { EditorView } from 'prosemirror-view';
 import { MenuItemGroup, type IMenuItem } from '../types.js';
 import { isInText, updateNodeAttrs } from '../util.js';
@@ -21,7 +21,11 @@ export function toggleTextAlign(alignment: Alignment) {
         transaction = updateNodeAttrs(state, transaction, () => pos, { ...node.attrs, textAlign: alignment });
       }
     });
-    if (dispatch) dispatch(transaction);
+    if (dispatch) {
+      const newSelection = TextSelection.create(transaction.doc, selection.from, selection.to);
+      transaction = transaction.setSelection(newSelection);
+      dispatch(transaction);
+    }
     return applicable;
   };
 }
diff --git a/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts
index 23bdf097f6..bc3a8310b2 100644
--- a/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts
+++ b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts
@@ -103,7 +103,7 @@ export class NodeGridCell extends ChemistryLitElement {
         ':host': {
           border: this.isEditMode() ? '1px solid grey' : '',
           position: 'relative',
-          overflowWrap: 'break-word',
+          //   overflowWrap: 'break-word',
         },
         ':host(.grid-item)': {
           position: 'relative',
diff --git a/lab/html-based-buildify/client/nodes/grid/node-grid.ts b/lab/html-based-buildify/client/nodes/grid/node-grid.ts
index 836af2659c..3b52abafbd 100644
--- a/lab/html-based-buildify/client/nodes/grid/node-grid.ts
+++ b/lab/html-based-buildify/client/nodes/grid/node-grid.ts
@@ -18,6 +18,12 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
     this._calculateOverflowVisuals();
   }
 
+  override updated(changedProperties: PropertyValues): void {
+    super.updated(changedProperties);
+
+    this._calculateOverflowVisuals();
+  }
+
   private _calculateOverflowVisuals() {
     requestAnimationFrame(() => {
       const content = this._data.value;
@@ -26,21 +32,19 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
       const container = this._gridRef.value;
       if (!container) return;
 
-      console.log('1', container);
-
       const width = container.clientWidth; // Sichtbare Breite
       const scrollLeft = container.scrollLeft; // Aktuelle Scroll-Position
       const scrollWidth = container.scrollWidth; // Gesamte Breite des Inhalts
 
       const hasOverflow = width !== scrollWidth;
       const atStart = scrollLeft === 0;
-      const atEnd = scrollLeft + width >= scrollWidth;
-
-      console.log({ width, scrollLeft, scrollWidth });
+      const atEnd = scrollLeft + width >= scrollWidth - 1; // 🔥 Toleranz von 1px
 
       if (!hasOverflow) return (this._scrollInfo = undefined);
 
-      this._scrollInfo = { showStart: !atStart, showEnd: !atEnd };
+      if (this._scrollInfo?.showStart !== !atStart || this._scrollInfo.showEnd !== !atEnd) {
+        this._scrollInfo = { showStart: !atStart, showEnd: !atEnd };
+      }
     });
   }
 
@@ -65,7 +69,7 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
           gridRowGap: content?.gridRowGap ?? '',
           padding: content?.padding ?? '',
           background: content?.backgroundColor ?? '',
-          overflowY: content?.allowHorizontalOverflow ? 'auto' : '',
+          overflowX: content?.allowHorizontalOverflow ? 'auto' : '',
           height: '100%',
         })}
       >
@@ -145,7 +149,6 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
         },
         '::slotted(*)': {
           boxSizing: 'border-box',
-          overflowY: this._data.value?.allowHorizontalOverflow ? '' : 'hidden !important',
         },
       },
     ] as Styles[];
diff --git a/lab/html-based-buildify/client/nodes/hard-break.ts b/lab/html-based-buildify/client/nodes/hard-break.ts
index 4dde70cd52..5caf77fc75 100644
--- a/lab/html-based-buildify/client/nodes/hard-break.ts
+++ b/lab/html-based-buildify/client/nodes/hard-break.ts
@@ -1,9 +1,40 @@
+import { keymap } from 'prosemirror-keymap';
+import type { EditorState, Transaction } from 'prosemirror-state';
 import { HARD_BREAK_KEY, HARD_BREAK_SCHEMA } from '../../schema/nodes/hard-break.js';
 import type { INodeConfig } from '../types.js';
 
+function insertHardBreak(state: EditorState, dispatch?: (tr: Transaction) => void) {
+  if (dispatch) {
+    const { schema, selection } = state;
+    const { $from } = selection;
+    let tr = state.tr.insert($from.pos, schema.nodes[HARD_BREAK_KEY]!.create());
+    dispatch(tr);
+  }
+  return true;
+}
+
 export const hardBreak: INodeConfig = {
   node: {
     name: HARD_BREAK_KEY,
     schema: HARD_BREAK_SCHEMA,
   },
+  menuItems: [
+    {
+      icon: 'insert_page_break',
+      shouldVisualize(editorView) {
+        return insertHardBreak(editorView.state);
+      },
+      label: 'Zeilenumbruch',
+      run(editorView) {
+        return insertHardBreak(editorView.state, editorView.dispatch);
+      },
+    },
+  ],
+  plugins(schema) {
+    return [
+      keymap({
+        'Shift-Enter': insertHardBreak,
+      }),
+    ];
+  },
 };
diff --git a/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts b/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
index 51eeae140d..377cdbdcb9 100644
--- a/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
+++ b/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
@@ -1,14 +1,13 @@
 import { keymap } from 'prosemirror-keymap';
 import { Schema } from 'prosemirror-model';
-import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
 import { LIST_ITEM_KEY } from '../../../schema/nodes/list.js';
 
 export const listKeymapPlugin = (schema: Schema) => {
   const listItem = schema.nodes[LIST_ITEM_KEY];
   if (!listItem) throw new Error('list item not found in schema');
   return keymap({
-    Enter: splitListItem(listItem),
-    Tab: sinkListItem(listItem),
-    'Shift-Tab': liftListItem(listItem),
+    // Enter: splitListItem(listItem),
+    // Tab: sinkListItem(listItem),
+    // 'Shift-Tab': liftListItem(listItem),
   });
 };
diff --git a/lab/html-based-buildify/client/nodes/list/list.ts b/lab/html-based-buildify/client/nodes/list/list.ts
index 7fb870d176..aa77b0d415 100644
--- a/lab/html-based-buildify/client/nodes/list/list.ts
+++ b/lab/html-based-buildify/client/nodes/list/list.ts
@@ -1,98 +1,37 @@
-import type { Schema } from 'prosemirror-model';
-import { wrapInList } from 'prosemirror-schema-list';
-import { Plugin } from 'prosemirror-state';
-import type { EditorView } from 'prosemirror-view';
-import {
-  LIST_ITEM_KEY,
-  LIST_ITEM_SCHEMA,
-  LIST_KEY,
-  LIST_SCHEMA,
-  ORDERED_LIST_KEY,
-  ORDERED_LIST_SCHEMA,
-} from '../../../schema/nodes/list.js';
+// @ts-expect-error default flat list stylings
+import ListStyles from 'text:prosemirror-flat-list/dist/style.css';
+
+// others
+import { css, unsafeCSS } from 'lit';
+import { createListPlugins, createListSpec, listInputRules, listKeymap } from 'prosemirror-flat-list';
+import { inputRules } from 'prosemirror-inputrules';
+import { keymap } from 'prosemirror-keymap';
 import type { INodeConfig } from '../../types.js';
-import { handleKeyDown } from './handleKeyDown.js';
-import { listKeymapPlugin } from './keymapPlugin.js';
 
-export const listItem: INodeConfig = {
+export const listConfig: INodeConfig = {
   node: {
-    name: LIST_ITEM_KEY,
-    schema: LIST_ITEM_SCHEMA,
+    name: 'list',
+    schema: createListSpec(),
   },
-};
-
-export const bulletList: INodeConfig = {
-  node: {
-    name: LIST_KEY,
-    schema: LIST_SCHEMA,
-  },
-  plugins: (schema: Schema) => [
-    new Plugin({
-      props: {
-        handleTextInput(view: EditorView, from, to, text) {
-          const { state, dispatch } = view;
-          const { $from } = state.selection;
-          const paragraphNode = $from.parent;
-
-          // checken ob der Input valid ist
-          const checkText = paragraphNode.textBetween(0, $from.parentOffset, null, '\n');
-          const shouldWrapInList = checkText === '-';
-          if (!shouldWrapInList) return false;
-
-          const tr = state.tr;
-
-          // Lösche das eingegebene Zeichen "-"
-          tr.delete(from - 1, to);
-
-          // Erstelle eine Bullet-Liste
-          dispatch(tr);
-
-          const bulletList = state.schema.nodes[LIST_KEY];
-          if (!bulletList) throw new Error('node not found in schema');
-
-          wrapInList(bulletList)(view.state, dispatch);
-          return true;
-        },
-        handleKeyDown,
-      },
-    }),
-    listKeymapPlugin(schema),
-  ],
-};
-
-export const orderedList: INodeConfig = {
-  node: {
-    name: ORDERED_LIST_KEY,
-    schema: ORDERED_LIST_SCHEMA,
+  plugins: schema => {
+    const listKeymapPlugin = keymap({
+      ...listKeymap,
+      'Shift-Tab': listKeymap['Mod-['],
+      Tab: listKeymap['Mod-]'],
+    });
+    const listInputRulePlugin = inputRules({ rules: listInputRules });
+    const listPlugins = createListPlugins({ schema });
+    return [listKeymapPlugin, listInputRulePlugin, ...listPlugins];
   },
-  plugins: (schema: Schema) => [
-    new Plugin({
-      props: {
-        handleTextInput(view, from, to, text) {
-          const { state, dispatch } = view;
-          const { $from } = state.selection;
-          const paragraphNode = $from.parent;
-
-          // Prüfe auf "1. ", "2. " usw. (Ordered List)
-          const orderedListMatch = paragraphNode.textBetween(0, $from.parentOffset, null, '\n').match(/^(\d+)\.$/);
-          if (!orderedListMatch) return false; // Wenn kein Muster erkannt wurde
-
-          const tr = state.tr;
-
-          // Lösche die Eingabe "1."
-          tr.delete(from - orderedListMatch[0].length, to);
+  stylings: css`
+    ${unsafeCSS(ListStyles)}
 
-          // Erstelle eine Ordered-Liste
-          dispatch(tr);
+    .prosemirror-flat-list {
+      margin-left: 18px;
+    }
 
-          const orderedList = state.schema.nodes[ORDERED_LIST_KEY];
-          if (!orderedList) throw new Error('ordered list not found in schema');
-          wrapInList(orderedList)(view.state, dispatch);
-          return true;
-        },
-        handleKeyDown,
-      },
-    }),
-    listKeymapPlugin(schema),
-  ],
+    .prosemirror-flat-list[data-list-kind='ordered']::before {
+      transform: translate(-6px, -1px);
+    }
+  `,
 };
diff --git a/lab/html-based-buildify/client/nodes/paragraph.ts b/lab/html-based-buildify/client/nodes/paragraph.ts
index 897a15c1a5..6c261ef203 100644
--- a/lab/html-based-buildify/client/nodes/paragraph.ts
+++ b/lab/html-based-buildify/client/nodes/paragraph.ts
@@ -42,10 +42,14 @@ class ParagraphNodeView {
     this.placeholder.style.left = '0';
     this.placeholder.style.top = '50%';
     this.placeholder.style.transform = 'translateY(-50%)';
+    this.placeholder.contentEditable = 'false';
 
     const contentWrapper = document.createElement('span');
     contentWrapper.style.minHeight = '1em';
     contentWrapper.style.display = 'inline-block';
+    contentWrapper.style.width = '100%';
+    contentWrapper.style.outline = 'none';
+    contentWrapper.contentEditable = 'true';
 
     element.appendChild(this.placeholder);
     element.appendChild(contentWrapper);
@@ -65,9 +69,10 @@ class ParagraphNodeView {
   }
 
   updatePlaceholderVisibility() {
-    const isEmpty = this.node.textContent.length === 0;
+    const isEditorEmpty = this.view.state.doc.textContent.trim() === '';
+    const isThisElementEmpty = this.node.textContent.length === 0;
 
-    if (isEmpty) {
+    if (isEditorEmpty && isThisElementEmpty) {
       this.placeholder.style.display = 'block';
     } else {
       this.placeholder.style.display = 'none';
diff --git a/lab/html-based-buildify/client/prosemirror-editor.ts b/lab/html-based-buildify/client/prosemirror-editor.ts
index 7ae87108b1..02245758a2 100644
--- a/lab/html-based-buildify/client/prosemirror-editor.ts
+++ b/lab/html-based-buildify/client/prosemirror-editor.ts
@@ -14,7 +14,7 @@ import '@adornis/chemistry/elements/components/x-flex';
 import '@adornis/chemistry/elements/components/x-icon';
 import { FormField } from '@adornis/formfield/form-field.js';
 import { ContextProvider, createContext, type Context } from '@lit/context';
-import { html, type PropertyValues } from 'lit';
+import { CSSResult, html, css as litCSS, unsafeCSS, type PropertyValues } from 'lit';
 import { customElement, property, state } from 'lit/decorators.js';
 import { baseKeymap } from 'prosemirror-commands';
 import { dropCursor } from 'prosemirror-dropcursor';
@@ -48,9 +48,14 @@ import './x-prosemirror-toolbar';
 
 const EDITOR_ID = 'editor';
 
-function getHtmlString(doc) {
+function getHtmlString(doc, stylings: CSSResult[]) {
   const serializer = DOMSerializer.fromSchema(doc.type.schema);
   const docElement = serializer.serializeNode(doc);
+  const styleElement = document.createElement('style');
+  styleElement.textContent = litCSS`
+    ${unsafeCSS(stylings)}
+  `.cssText;
+  docElement.appendChild(styleElement);
   const wrapper = document.createElement('div');
   wrapper.appendChild(docElement);
   return wrapper.innerHTML;
@@ -163,6 +168,15 @@ export class ProsemirrorEditor extends FormField<string> {
     return plugins;
   }
 
+  getStylings(): CSSResult[] {
+    const stylings: CSSResult[] = [];
+    for (const config of this.configs.value) {
+      if (!config.stylings) continue;
+      stylings.push(config.stylings);
+    }
+    return stylings;
+  }
+
   generateSchema() {
     const nodes: Record<string, NodeSpec> = {};
     const marks: Record<string, MarkSpec> = {};
@@ -264,7 +278,7 @@ export class ProsemirrorEditor extends FormField<string> {
       const node = document.createElement('div');
       node.innerHTML = html;
 
-      if (getHtmlString(this.view.state.doc) === html) {
+      if (getHtmlString(this.view.state.doc, this.getStylings()) === html) {
         return;
       }
 
@@ -305,10 +319,10 @@ export class ProsemirrorEditor extends FormField<string> {
     if (!view) return;
     const { state } = view;
     const value = JSON.stringify(state.doc.toJSON());
-    this.value.next(getHtmlString(state.doc));
+    this.value.next(getHtmlString(state.doc, this.getStylings()));
     this.dispatchEvent(
       new CustomEvent('value-changed', {
-        detail: { value, valueHTML: getHtmlString(state.doc) },
+        detail: { value, valueHTML: getHtmlString(state.doc, this.getStylings()) },
       }),
     );
   }
@@ -333,29 +347,7 @@ export class ProsemirrorEditor extends FormField<string> {
         : '100%';
 
     return html`
-      <style>
-        ::selection {
-          color: inherit !important;
-          background-color: #eee;
-        }
-
-        .ProseMirror.ProseMirror-focused {
-          outline: none;
-        }
-
-        .ProseMirror {
-          outline: none;
-          padding: 0px 8px 16px 8px;
-          /* prosemirror warns us if we don't set this */
-          /* // TODO do we want to load prosemirror's CSS? Has Michi avoided that for a reason? */
-          white-space: pre-wrap;
-        }
-
-        .ProseMirror-selectednode {
-          position: relative;
-          outline: 2px solid ${this.colors.accent};
-        }
-      </style>
+      ${this._getEditorStylings()}
       <x-buildify-size-picker
         ${css({ marginBottom: '16px', display: 'block' })}
         .value=${this._selectedSize}
@@ -380,6 +372,39 @@ export class ProsemirrorEditor extends FormField<string> {
     `;
   }
 
+  private _getEditorStylings() {
+    const styleElement = document.createElement('style');
+    styleElement.textContent = unsafeCSS(`
+      /* others */
+      ::selection {
+        color: inherit !important;
+        background-color: #eee;
+      }
+
+      .ProseMirror.ProseMirror-focused {
+        outline: none;
+      }
+
+      .ProseMirror {
+        outline: none;
+        padding: 0px 8px 16px 8px;
+        /* prosemirror warns us if we don't set this */
+        /* // TODO do we want to load prosemirror's CSS? Has Michi avoided that for a reason? */
+        white-space: pre-wrap;
+      }
+
+      .ProseMirror-selectednode {
+        position: relative;
+        outline: 2px solid ${this.colors.accent};
+      }
+
+      /* custom stylings from configs */
+      ${this.getStylings()}
+    `).cssText;
+
+    return styleElement;
+  }
+
   override styles() {
     return [
       ...super.styles(),
diff --git a/lab/html-based-buildify/client/starterPack.ts b/lab/html-based-buildify/client/starterPack.ts
index c593a99472..42e5b0680b 100644
--- a/lab/html-based-buildify/client/starterPack.ts
+++ b/lab/html-based-buildify/client/starterPack.ts
@@ -20,7 +20,7 @@ import { icon } from './nodes/icon.js';
 import { iconText, iconWrapper, textWrapper } from './nodes/iconText.js';
 import { iframe } from './nodes/iframe.js';
 import { image } from './nodes/image.js';
-import { bulletList, listItem, orderedList } from './nodes/list/list.js';
+import { listConfig } from './nodes/list/list.js';
 import { paragraph } from './nodes/paragraph.js';
 import { section } from './nodes/section.js';
 import { spacing } from './nodes/spacing.js';
@@ -58,9 +58,7 @@ export const startedPack: Array<INodeConfig | IMarkConfig> = [
   iconWrapper,
   textWrapper,
   image,
-  listItem,
-  bulletList,
-  orderedList,
+  listConfig,
   section,
   spacing,
   tag,
diff --git a/lab/html-based-buildify/client/types.ts b/lab/html-based-buildify/client/types.ts
index 060302bac2..180c0b36cc 100644
--- a/lab/html-based-buildify/client/types.ts
+++ b/lab/html-based-buildify/client/types.ts
@@ -2,7 +2,7 @@ import type { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-eleme
 import '@adornis/chemistry/elements/components/x-icon';
 import type { TranslationController } from '@adornis/translation-core/client/translation-controller.js';
 import type { TranslationDictionary } from '@adornis/translation-core/translation.js';
-import type { TemplateResult } from 'lit';
+import type { CSSResult, TemplateResult } from 'lit';
 import type { MarkSpec, NodeSpec, Schema } from 'prosemirror-model';
 import type { Plugin } from 'prosemirror-state';
 import type { EditorView } from 'prosemirror-view';
@@ -38,6 +38,7 @@ export interface IEditorConfigBase {
   menuItems?: IMenuItem[];
   plugins?: (schema: Schema) => Array<Plugin<any>>;
   editor?: EditorFunc<any, any>;
+  stylings?: CSSResult;
 }
 
 export interface INodeConfig extends IEditorConfigBase {
diff --git a/lab/html-based-buildify/client/x-prosemirror-toolbar.ts b/lab/html-based-buildify/client/x-prosemirror-toolbar.ts
index 77175edae4..43289947a2 100644
--- a/lab/html-based-buildify/client/x-prosemirror-toolbar.ts
+++ b/lab/html-based-buildify/client/x-prosemirror-toolbar.ts
@@ -34,9 +34,10 @@ export function renderMenuItemIcon({
         borderRadius: '4px',
         color: color ?? '#333',
       })}
-      @click=${() => {
+      @click=${e => {
         if (!run) return;
         run(view);
+        view.focus();
       }}
     >
       ${icon}
@@ -52,6 +53,7 @@ export function renderMenuItem(item: IMenuItem, view: EditorView): TemplateResul
         @click=${() => {
           if (!item.run) return;
           item.run(view);
+          view.focus();
         }}
       >
         ${item.render(view)}
@@ -77,9 +79,10 @@ export function renderMenuItem(item: IMenuItem, view: EditorView): TemplateResul
                   background: isActive ? '' : '#F2F2F1',
                 },
               })}
-              @click=${() => {
+              @click=${e => {
                 if (!item.run) return;
                 item.run(view);
+                view.focus();
               }}
             >
               <x-text> ${item.label} </x-text>
diff --git a/lab/html-based-buildify/schema/nodes/list.ts b/lab/html-based-buildify/schema/nodes/list.ts
index fcc39b9e34..e69de29bb2 100644
--- a/lab/html-based-buildify/schema/nodes/list.ts
+++ b/lab/html-based-buildify/schema/nodes/list.ts
@@ -1,42 +0,0 @@
-import type { NodeSpec } from 'prosemirror-model';
-
-export const LIST_ITEM_KEY = 'list_item';
-export const LIST_KEY = 'list';
-export const ORDERED_LIST_KEY = 'ordered_list';
-
-export const LIST_ITEM_SCHEMA: NodeSpec = {
-  group: 'block',
-  content: 'paragraph block*',
-  toDOM() {
-    return ['li', 0];
-  },
-  parseDOM: [{ tag: 'li' }],
-};
-
-export const LIST_SCHEMA: NodeSpec = {
-  group: 'block',
-  draggable: true,
-  content: `${LIST_ITEM_KEY}+`,
-  toDOM() {
-    return ['ul', { style: 'padding-inline-start: 18px;' }, 0];
-  },
-  parseDOM: [{ tag: 'ul' }],
-};
-
-export const ORDERED_LIST_SCHEMA: NodeSpec = {
-  group: 'block',
-  draggable: true,
-  content: `${LIST_ITEM_KEY}+`,
-  attrs: { order: { default: 1 } },
-  toDOM(node) {
-    return ['ol', { start: node.attrs.order, style: 'padding-inline-start: 18px;' }, 0];
-  },
-  parseDOM: [
-    {
-      tag: 'ol',
-      getAttrs: dom => ({
-        order: dom.hasAttribute('start') ? +dom.getAttribute('start')! : 1,
-      }),
-    },
-  ],
-};
-- 
GitLab


From 68de0d8b495cf5960c9117fe9312a3166af2683b Mon Sep 17 00:00:00 2001
From: michi <michael@adornis.de>
Date: Thu, 13 Feb 2025 22:10:44 +0000
Subject: [PATCH 2/2] refactor: remove old list node

---
 .../client/nodes/grid/node-grid.ts            |   4 +-
 .../client/nodes/{list => }/list.ts           |   2 +-
 .../client/nodes/list/handleKeyDown.ts        | 122 ------------------
 .../client/nodes/list/keymapPlugin.ts         |  13 --
 lab/html-based-buildify/client/starterPack.ts |   2 +-
 lab/html-based-buildify/migration.ts          |   3 -
 lab/html-based-buildify/schema/nodes/list.ts  |   0
 7 files changed, 4 insertions(+), 142 deletions(-)
 rename lab/html-based-buildify/client/nodes/{list => }/list.ts (95%)
 delete mode 100644 lab/html-based-buildify/client/nodes/list/handleKeyDown.ts
 delete mode 100644 lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
 delete mode 100644 lab/html-based-buildify/schema/nodes/list.ts

diff --git a/lab/html-based-buildify/client/nodes/grid/node-grid.ts b/lab/html-based-buildify/client/nodes/grid/node-grid.ts
index 3b52abafbd..1466af9abe 100644
--- a/lab/html-based-buildify/client/nodes/grid/node-grid.ts
+++ b/lab/html-based-buildify/client/nodes/grid/node-grid.ts
@@ -102,7 +102,7 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
                       top: '0',
                       bottom: '0',
                       width: '40px',
-                      background: 'linear-gradient(to right, rgba(206,206,206, 1) 40%, rgba(0, 0, 0, 0))',
+                      background: 'linear-gradient(to right, rgba(175, 202, 0, 1) 40%, rgba(0, 0, 0, 0))',
                       color: '#333',
                       fontSize: '24px',
                     })}
@@ -122,7 +122,7 @@ export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> {
                       top: '0',
                       bottom: '0',
                       width: '40px',
-                      background: 'linear-gradient(to left, rgba(206,206,206, 1) 40%, rgba(0, 0, 0, 0))',
+                      background: 'linear-gradient(to left, rgba(175, 202, 0, 1) 40%, rgba(0, 0, 0, 0))',
                       color: '#333',
                       fontSize: '24px',
                     })}
diff --git a/lab/html-based-buildify/client/nodes/list/list.ts b/lab/html-based-buildify/client/nodes/list.ts
similarity index 95%
rename from lab/html-based-buildify/client/nodes/list/list.ts
rename to lab/html-based-buildify/client/nodes/list.ts
index aa77b0d415..073ae5fd0f 100644
--- a/lab/html-based-buildify/client/nodes/list/list.ts
+++ b/lab/html-based-buildify/client/nodes/list.ts
@@ -6,7 +6,7 @@ import { css, unsafeCSS } from 'lit';
 import { createListPlugins, createListSpec, listInputRules, listKeymap } from 'prosemirror-flat-list';
 import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from 'prosemirror-keymap';
-import type { INodeConfig } from '../../types.js';
+import type { INodeConfig } from '../types.js';
 
 export const listConfig: INodeConfig = {
   node: {
diff --git a/lab/html-based-buildify/client/nodes/list/handleKeyDown.ts b/lab/html-based-buildify/client/nodes/list/handleKeyDown.ts
deleted file mode 100644
index d48bb11156..0000000000
--- a/lab/html-based-buildify/client/nodes/list/handleKeyDown.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { splitListItem } from 'prosemirror-schema-list';
-import { TextSelection, type EditorState } from 'prosemirror-state';
-import type { EditorView } from 'prosemirror-view';
-import { LIST_KEY, ORDERED_LIST_KEY } from '../../../schema/nodes/list.js';
-import { PARAGRAPH_KEY } from '../../../schema/nodes/paragraph.js';
-
-export const handleKeyDown = (view, event) => {
-  const { state } = view;
-  const { schema } = state;
-  const { selection } = state;
-
-  // Prüfen, ob eine Auswahl existiert und der Cursor nicht erweitert ist
-  if (!selection || !selection.empty) {
-    return false;
-  }
-
-  // BACKSPACE-Taste
-  if (event.key === 'Backspace') {
-    const { $anchor } = selection;
-
-    // Prüfen, ob der Cursor sich innerhalb eines Listenelements befindet
-    if (isCursorInListItem($anchor, schema)) {
-      return handleBackspaceInListItem(view, state, schema, $anchor);
-    }
-
-    // Prüfen, ob der Cursor in einem leeren Paragraphen nach einer Liste steht
-    if (isEmptyParagraphAfterList(view)) {
-      return handleBackspaceInEmptyParagraphAfterList(view, state, $anchor);
-    }
-  }
-
-  return false; // Standardverhalten für alle anderen Tasten
-};
-
-/**
- * Prüft, ob sich der Cursor in einem Listenelement befindet.
- */
-const isCursorInListItem = ($anchor, schema) => {
-  return $anchor.node(-1).type === schema.nodes.list_item && $anchor.parentOffset === 0;
-};
-
-/**
- * Behandelt die Backspace-Taste, wenn der Cursor am Anfang eines Listenelements steht.
- */
-const handleBackspaceInListItem = (view, state, schema, $anchor) => {
-  const listItem = schema.nodes.list_item;
-  if (!listItem) {
-    throw new Error('list_item not found in schema');
-  }
-
-  console.log('Backspace pressed at the start of a list item', listItem);
-  const changed = splitListItem(listItem)(state, view.dispatch);
-
-  if (!changed) {
-    console.log('No change, simulating Enter key press');
-    simulateEnterKeyPress(view);
-  }
-
-  return true;
-};
-
-/**
- * Prüft, ob der Cursor in einem leeren Paragraphen steht, der direkt nach einer Liste kommt.
- */
-const isEmptyParagraphAfterList = (view: EditorView) => {
-  const { state } = view;
-  const { selection, schema } = state;
-
-  const { from, to } = selection;
-  if (from !== to) return false; // Kein gültiger Cursor
-
-  // Node an der aktuellen Position auflösen
-  const resolvedPos = state.doc.resolve(from);
-  const resolvedNode = resolvedPos.node();
-
-  if (resolvedPos.start() - 2 < 0) return false;
-
-  const posBefore = state.doc.resolve(resolvedPos.start() - 2);
-  const nodeBefore = posBefore.node();
-
-  return (
-    resolvedNode.type === schema.nodes[PARAGRAPH_KEY] &&
-    resolvedNode.content.size === 0 &&
-    nodeBefore &&
-    (nodeBefore.type === schema.nodes[ORDERED_LIST_KEY] || nodeBefore.type === schema.nodes[LIST_KEY])
-  );
-};
-
-/**
- * Behandelt die Backspace-Taste in einem leeren Paragraphen, der direkt nach einer Liste steht.
- */
-const handleBackspaceInEmptyParagraphAfterList = (view: EditorView, state: EditorState, $anchor) => {
-  console.log('Backspace pressed in an empty paragraph after a list');
-
-  console.log('anchor: ', $anchor, $anchor.before(), $anchor.after());
-
-  const tr = state.tr;
-  tr.delete($anchor.before(), $anchor.after()); // Leeren Paragraph löschen
-
-  // Position in das letzte Listenelement setzen
-  const listEndPos = $anchor.before() - 1;
-  tr.setSelection(TextSelection.create(tr.doc, listEndPos, listEndPos));
-
-  view.dispatch(tr);
-  return true;
-};
-
-/**
- * Simuliert das Drücken der Enter-Taste.
- */
-const simulateEnterKeyPress = view => {
-  const enterEvent = new KeyboardEvent('keydown', {
-    key: 'Enter',
-    code: 'Enter',
-    keyCode: 13,
-    which: 13,
-    bubbles: true,
-    cancelable: true,
-  });
-
-  view.dom.dispatchEvent(enterEvent);
-};
diff --git a/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts b/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
deleted file mode 100644
index 377cdbdcb9..0000000000
--- a/lab/html-based-buildify/client/nodes/list/keymapPlugin.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { keymap } from 'prosemirror-keymap';
-import { Schema } from 'prosemirror-model';
-import { LIST_ITEM_KEY } from '../../../schema/nodes/list.js';
-
-export const listKeymapPlugin = (schema: Schema) => {
-  const listItem = schema.nodes[LIST_ITEM_KEY];
-  if (!listItem) throw new Error('list item not found in schema');
-  return keymap({
-    // Enter: splitListItem(listItem),
-    // Tab: sinkListItem(listItem),
-    // 'Shift-Tab': liftListItem(listItem),
-  });
-};
diff --git a/lab/html-based-buildify/client/starterPack.ts b/lab/html-based-buildify/client/starterPack.ts
index 42e5b0680b..8aadb6325b 100644
--- a/lab/html-based-buildify/client/starterPack.ts
+++ b/lab/html-based-buildify/client/starterPack.ts
@@ -20,7 +20,7 @@ import { icon } from './nodes/icon.js';
 import { iconText, iconWrapper, textWrapper } from './nodes/iconText.js';
 import { iframe } from './nodes/iframe.js';
 import { image } from './nodes/image.js';
-import { listConfig } from './nodes/list/list.js';
+import { listConfig } from './nodes/list.js';
 import { paragraph } from './nodes/paragraph.js';
 import { section } from './nodes/section.js';
 import { spacing } from './nodes/spacing.js';
diff --git a/lab/html-based-buildify/migration.ts b/lab/html-based-buildify/migration.ts
index ae5c7a61e9..46bba1eb6b 100644
--- a/lab/html-based-buildify/migration.ts
+++ b/lab/html-based-buildify/migration.ts
@@ -35,7 +35,6 @@ import { GRID_CELL_KEY, GRID_KEY } from './schema/nodes/grid.js';
 import { HARD_BREAK_KEY } from './schema/nodes/hard-break.js';
 import { ICON_TEXT_KEY, ICON_WRAPPER_KEY, TEXT_WRAPPER_KEY } from './schema/nodes/icon-text.js';
 import { IMAGE_KEY } from './schema/nodes/image.js';
-import { LIST_ITEM_KEY, LIST_KEY } from './schema/nodes/list.js';
 import { SECTION_KEY } from './schema/nodes/section.js';
 import { SPACING_KEY } from './schema/nodes/spacing.js';
 import { TAG_KEY } from './schema/nodes/tag.js';
@@ -107,8 +106,6 @@ const PARSE_CONTENT_TEXT_TO_JSON = (content: BaseText, otherContents, customData
   let text = content.text;
   text = text.replaceAll('textStyle', FONT_SIZE_KEY);
   text = text.replaceAll('strike', STRIKE_THROUGH_KEY);
-  text = text.replaceAll('listItem', LIST_ITEM_KEY);
-  text = text.replaceAll('bulletList', LIST_KEY);
   text = text.replaceAll('hardBreak', HARD_BREAK_KEY);
   text = text.replaceAll('"fontSize":8', '"fontSize":"8px"');
   text = text.replaceAll('"fontSize":12', '"fontSize":"12px"');
diff --git a/lab/html-based-buildify/schema/nodes/list.ts b/lab/html-based-buildify/schema/nodes/list.ts
deleted file mode 100644
index e69de29bb2..0000000000
-- 
GitLab