PCX Forum TestversionTestversion des neuen PCX Forums. Beiträge, Nachrichten und Änderungen hier sind nur zum Ausprobieren und werden beim nächsten Abgleich mit dem alten Forum überschrieben. Mails werden nicht verschickt.
Für dies und das

Threads

  1. Der große Apple Thread - Part 2 Matze 291
  2. Steam V Pascal Parvex 973
  3. Hi ZeroCool 0
  4. Der Witze Thread 1 Pascal Parvex 141
  5. Grand Theft Auto VI Mindeye 40
  6. Der 2. RIP-Thread Mindeye 1012
  7. Der I. "ich hab Keys über" Thread Schallmauer 692
  8. Best Price 4 Mindeye 353
  9. Dies und das und jenes!-Thread Teil 2 Smooth 857
  10. Der Politik-Thread IV ossi_osram 542
  11. Neue, aber dennoch alte Hardware: Neo-Geo AES+ reality.sys 9
  12. No Man's Sky Killersepp 300
  13. Erinnert sich sonst noch wer an den Textmodus-Snake-Klon "Kibowurm"? Pascal Parvex 9
  14. Welches Auto fahrt ihr? reality.sys 21
  15. Das PCX-Forum informiert ZeroCool 868
  16. Nvidia RTX wiede 495
  17. Witcher 3 - Ankündigung im Februar? Mindeye 545
  18. Wann hattet ihr das letzte Mal einen Bluescreen? Pascal Parvex 1
  19. Dinge, die gar nicht gehen - Nummer 19 Pascal Parvex 254
  20. Ich bin jetzt auf 8K-Gaming umgestiegen. Pascal Parvex 393
  21. Seine Jörglangerigkeit mal wieder Famou$ 634
  22. Trittst im Morgenrot daher... Pascal Parvex 25
  23. Der KI-Thread hellbringer 50
  24. Fußball-Thread 2015/2016 Border 973
  25. Der Musik-Tipp-Thread Border 366
  26. "Frogger" Demo von Hasbro Interactive von 1997 Pascal Parvex 0
  27. was gefällt! ossi_osram 1005
  28. Der Geburtstagsthread Pascal Parvex 41
  29. Diablo 2 (Resurrected) Christoph 9
  30. Cyberpunk 2077 - neues CDP Spiel Mindeye 169
  31. Der Foto-Thread 2 Pascal Parvex 882
  32. Bei AMD ist heute Weihnachten wiede 288
  33. Technische Fragen - Wake on LAN und VMD-Raid Lupo 2
  34. Civilization VII Pascal Parvex 9
  35. Der Intel-Thread Singler 65
  36. Der 2. Film-Thread (Kino / Heimkino / TV) Matze 455
  37. Das Maniac Forum Teh Sachiel 206
  38. Was wird/wurde gespielt? - Nr1 Mindeye 802
  39. GOG - I ossi_osram 273
  40. Weiterentwicklung Forum (technisch) TrifftNix 28
  41. WoW SauerCrowd TrifftNix 11
  42. Monitor gesucht TrifftNix 3
  43. FF7 Renmake für PC (und auch PS3) Schallmauer 13
  44. Ich (als Mod) veranstalte mal wieder ein Giveaway auf r/3ddruck... Singler 6
  45. Frohes neues Jahr 2026 reality.sys 4
  46. Switch 2 hellbringer 37
  47. Star Wars - Fate of the old Republic Goran 3
  48. Internet-Geschwindigkeitsvergleich Pascal Parvex 245
  49. Anthem: Kein Thema? MrAnchovy 22
  50. Ubisoft verschiebt Geschäftszahlen ossi_osram 2
  51. 21 Bud Spencer und Terence Hill Filme gratis bis Ende Jahr Pascal Parvex 17
  52. 007 First Light Border 1
  53. Der Adventure-Thread Joschi 676
  54. Frag das PCX-Forum 4 Pascal Parvex 345
  55. Jemand Interesse an 3DDruckern? Ihr könnt einen gewinnen Singler 1
  56. TMP aktiviert - kein Windows 11 Lupo 5
  57. Battlefield 6 Mindeye 2
  58. VR-Gear MatD 110
  59. Star Citizen Mindeye 73
  60. Anno 117: Pax Romana Mindeye 3
  61. Windows 11 reality.sys 37
  62. Asus ROG Ally vs. Steam Deck Mindeye 2
  63. Borderlands 4 kommt am 12. September wiede 1
  64. The Witcher 4 (Reveal Trailer) Border 4
  65. Windows Surface Problem Lupo 5
  66. Kingdom Come Deliverance II ZeroCool 9
  67. WLAN Mesh Repeater als "Kabelersatz" Lupo 7
  68. Stalker 2 - Entwicklung geht weiter Slochy 33
  69. For Sale Topic reality.sys 92
  70. Path of Exile Schallmauer 26
  71. MiSTer FPGA - gibt es hier Erfahrungen? reality.sys 2
  72. US-Wahlen 2024 Pascal Parvex 6
  73. Star Wars trifft Goonies trifft 80iger Musik Singler 1
  74. Welche Betriebssysteme nutzt ihr? Carnivore 9
  75. Zu doof ich bin? Rechner geht nicht... Lupo 21

Weiterentwicklung Forum (technisch)

28 Antworten

Re:WYSIWYG Editor

von TrifftNix am

Ich bin eben auf den tiptap gewechselt. Das geht deutlich besser -> siehe Testforum. Bei der Gelegenheit habe ich gleich Mentions mit eingebaut. Läuft auch.
Quote und Messageparser müssen noch angepasst werden. Da steht eine Entscheidung an. Bleibt es beim Quoten mit > pro Zeile oder wird doch gleich auf [q:....] umgestellt, was der Funktionsweise der Editoren entspricht, aber mit der Messagehistorie bricht. Da würden Zitate nicht mehr grau angezeigt, sondern nur noch mit den > davor. ...oder ein Migrationsscript muss her.
Über die Sache mit HTML direkt in der DB habe ich auch noch mal nachgedacht. Es hätte neben Sicherheitsaspekten noch einen anderen Nachteil. Wenn sich z.B. bei Youtube mal die Syntax ändert oder man es anders aufrufen will steht alles falsch in der DB. Ich denke ich bleibe bei der PXM Syntax. Läuft problemlos mit dem Editor.

Das als Anregung für die KI:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
import Underline from '@tiptap/extension-underline'
import Mention from '@tiptap/extension-mention'
import suggestion from './mention-suggestion.js'

/**
* Tiptap to PXM Converter
* Konvertiert Tiptap JSON zu PXM-Syntax
* Basierend auf der Stack-basierten PXM-Syntax:
*   text → <b>text</b>
*   text → <b><i>text</i></b>
*/
function tiptapToPxm(editor) {
 const json = editor.getJSON();

 const convertNode = (node) => {
   // Text-Node mit Marks
   if (node.type === 'text') {
     let text = node.text;

     // Marks von außen nach innen anwenden (Stack-basiert)
     if (node.marks) {
       // Reverse, damit innerste Marks zuerst applied werden
       node.marks.slice().reverse().forEach(mark => {
         switch(mark.type) {
           case 'bold':
             text = `${text}`;
             break;
           case 'italic':
             text = `${text}`;
             break;
           case 'underline':
             text = `${text}`;
             break;
           case 'strike':
             text = `${text}`;
             break;
           case 'link':
             // Link-Format: [url]
             const href = mark.attrs.href;
             if (href.startsWith('http://') || href.startsWith('https://')) {
               text = `[${href}]`;
             } else if (href.startsWith('www.')) {
               text = `[${href}]`;
             } else if (href.startsWith('mailto:')) {
               text = `[${href}]`;
             }
             break;
         }
       });
     }
     return text;
   }

   // Mention-Node handling: Convert to PXM format [user:id]
   if (node.type === 'mention') {
     const userId = node.attrs?.id || 0;
     return `[user:${userId}]`;
   }

   // Block-Nodes rekursiv verarbeiten
   const content = node.content
     ? node.content.map(convertNode).join('')
     : '';

   switch(node.type) {
     case 'paragraph':
       // Paragraph = Content + Newline
       return content + '\n';

     case 'blockquote':
       // Quote: Jede Zeile mit > prefixen
       return content.split('\n')
         .map(line => line ? `>${line}` : '')
         .filter(line => line)  // Leere Zeilen entfernen
         .join('\n') + '\n';

     case 'hardBreak':
       return '\n';

     case 'image':
       // Bild-Format: [img:url]
       const src = node.attrs?.src || '';
       return `[img:${src}]`;

     case 'doc':
       return content;

     default:
       console.warn('Unbekannter Node-Typ:', node.type);
       return content;
   }
 };

 return convertNode(json).trim();
}

/**
* Editor-Instanzen werden außerhalb von Alpine's Reactive-System gespeichert,
* damit ProseMirror's State-Management nicht durch Alpine's Proxy gestört wird.
*/
const editorInstances = new WeakMap();

function getEditor(el) {
 return editorInstances.get(el);
}

/**
* Alpine.js Component für Tiptap Editor
*/
let pxmEditorRegistered = false;

function initPxmEditor() {
 if (pxmEditorRegistered) {
   return;
 }

 if (typeof window.Alpine !== 'undefined') {
   pxmEditorRegistered = true;

   window.Alpine.data('pxmEditor', (initialHtmlContent = '') => ({
     pxmContent: '',
     loading: true,
     error: null,

     init() {
       const el = this.$refs.editor;
       if (!el || getEditor(el)) {
         return;
       }

       try {
         const component = this;
         const editor = new Editor({
           element: el,
           extensions: [
             StarterKit.configure({
               link: false,
               underline: false,
             }),
             Underline,
             Link.configure({
               openOnClick: false,
               HTMLAttributes: {
                 target: '_blank',
                 rel: 'noopener noreferrer'
               }
             }),
             Image.configure({
               inline: false,
               HTMLAttributes: {
                 class: 'max-w-full h-auto'
               }
             }),
             Mention.configure({
               HTMLAttributes: {
                 class: 'mention',
               },
               suggestion: suggestion,
               renderLabel({ node }) {
                 return `@${node.attrs.label}`;
               }
             })
           ],
           content: initialHtmlContent,
           onUpdate: ({ editor }) => {
             component.pxmContent = tiptapToPxm(editor);
           },
           editorProps: {
             attributes: {
               class: 'prose prose-sm max-w-none focus:outline-none min-h-[200px] p-4'
             }
           }
         });

         editorInstances.set(el, editor);
         this.pxmContent = tiptapToPxm(editor);
         this.loading = false;
       } catch (error) {
         console.error('Editor-Initialisierung fehlgeschlagen:', error);
         this.error = 'Editor konnte nicht geladen werden. Fehler: ' + error.message;
         this.loading = false;
       }
     },

     destroy() {
       const el = this.$refs.editor;
       const editor = el && getEditor(el);
       if (editor && !editor.isDestroyed) {
         editor.destroy();
       }
       if (el) {
         editorInstances.delete(el);
       }
     },

     // Toolbar-Aktionen
     toggleBold() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.toggleBold();
     },

     toggleItalic() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.toggleItalic();
     },

     toggleUnderline() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.toggleUnderline();
     },

     toggleStrike() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.toggleStrike();
     },

     setLink() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       const url = window.prompt('URL:');
       if (url) {
         editor.commands.setLink({ href: url });
       }
     },

     unsetLink() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.unsetLink();
     },

     insertImage() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       const url = window.prompt('Bild-URL:');
       if (url) {
         editor.commands.setImage({ src: url });
       }
     },

     toggleBlockquote() {
       const editor = getEditor(this.$refs.editor);
       if (!editor || editor.isDestroyed) return;
       editor.commands.toggleBlockquote();
     },

     // Status-Prüfungen für Toolbar Buttons
     isBold() {
       return getEditor(this.$refs.editor)?.isActive('bold');
     },

     isItalic() {
       return getEditor(this.$refs.editor)?.isActive('italic');
     },

     isUnderline() {
       return getEditor(this.$refs.editor)?.isActive('underline');
     },

     isStrike() {
       return getEditor(this.$refs.editor)?.isActive('strike');
     },

     isLink() {
       return getEditor(this.$refs.editor)?.isActive('link');
     },

     isBlockquote() {
       return getEditor(this.$refs.editor)?.isActive('blockquote');
     }
   }));
 }
}

// Versuche mehrere Event-Strategien, aber registriere nur einmal
if (document.readyState === 'loading') {
 document.addEventListener('DOMContentLoaded', initPxmEditor);
} else {
 initPxmEditor();
}

// Fallback: Alpine-spezifisches Event
document.addEventListener('alpine:init', initPxmEditor);