]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Updated toolbar & text node exporting
authorDan Brown <redacted>
Mon, 23 Sep 2024 16:36:16 +0000 (17:36 +0100)
committerDan Brown <redacted>
Mon, 23 Sep 2024 16:36:16 +0000 (17:36 +0100)
- Updated toolbar to match existing editor, including dynamic RTL/LTR
  controls.
- Updated text node handling to not include spans and extra classes when
  not needed. Added & update tests to cover.

16 files changed:
resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts
resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts
resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts
resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts
resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts
resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts
resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts
resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts
resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/index.ts
resources/js/wysiwyg/ui/toolbars.ts
resources/sass/_editor.scss
resources/sass/_pages.scss

index 9f832b69e4064791dde1fbe84690991783fc0978..534663a54b48cc65cfe643b036e469f29ed368e1 100644 (file)
@@ -82,7 +82,7 @@ describe('HTMLCopyAndPaste tests', () => {
           pastedHTML: ` <span>123<div>456</div></span>`,
         },
         {
-          expectedHTML: `<ul><li role="checkbox" tabindex="-1" aria-checked="true" value="1"><span data-lexical-text="true">done</span></li><li role="checkbox" tabindex="-1" aria-checked="false" value="2"><span data-lexical-text="true">todo</span></li><li value="3"><ul><li role="checkbox" tabindex="-1" aria-checked="true" value="1"><span data-lexical-text="true">done</span></li><li role="checkbox" tabindex="-1" aria-checked="false" value="2"><span data-lexical-text="true">todo</span></li></ul></li><li role="checkbox" tabindex="-1" aria-checked="false" value="3"><span data-lexical-text="true">todo</span></li></ul>`,
+          expectedHTML: `<ul><li role="checkbox" tabindex="-1" aria-checked="true" value="1"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">done</span></li><li role="checkbox" tabindex="-1" aria-checked="false" value="2"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">todo</span></li><li value="3"><ul><li role="checkbox" tabindex="-1" aria-checked="true" value="1"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">done</span></li><li role="checkbox" tabindex="-1" aria-checked="false" value="2"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">todo</span></li></ul></li><li role="checkbox" tabindex="-1" aria-checked="false" value="3"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">todo</span></li></ul>`,
           name: 'google doc checklist',
           pastedHTML: `<meta charset='utf-8'><meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-1980f960-7fff-f4df-4ba3-26c6e1508542"><ul style="margin-top:0;margin-bottom:0;padding-inline-start:28px;"><li role="checkbox" aria-checked="true" style="list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;" aria-level="1"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAABbElEQVR4Ae3bsU4CYRDEcRsxodZE8Q0BbS258l5MwESJNL6HOfrPKdhyxeBcwk5mkn9F98sGIOSuPM/zPM/zPI+xG/SEtuiAWpEOaIOWaDIWziP6RK14OzSjX44ITvTBvqRn1MRaMIHeBIE2TKBBEGhgArWkKmtJBjKQgQxkIANd/Aw0NVC+O7RHvYFynHasN1COE/UGynGiXgOIjxOtdIH4OGJAfBwxID6OGBAfRwiIjyMARMCpCjRF5+72Dzhd5R+rHfpC92NeTlWgLl5PkQg4RYBynBSJgFMGKMNJkQg4lYFeUDuFRMCpBXQOEgGnDtA/kPg4xT7m2y/tCd9zKgOdviTC5RQEIiAFjh4QASlw9IAISIEjCURAWvmf1UDKcQwUSDmOgWLdMcxA7BnIQAYykIEM5EcRvplAW0GgNRNoKQg0ZwJN0E4I5x1dI+pmgSSA84BG2QQt0LrYG/eAXtGccjme53me53me9wPjPWZWjhktAQAAAABJRU5ErkJggg==" width="18.4px" height="18.4px" alt="checked" aria-roledescription="checkbox" style="margin-right:3px;" /><p style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">done</span></p></li><li role="checkbox" aria-checked="false" style="list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" width="18.4px" height="18.4px" alt="unchecked" aria-roledescription="checkbox" style="margin-right:3px;" /><p style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">todo</span></p></li><ul style="margin-top:0;margin-bottom:0;padding-inline-start:28px;"><li role="checkbox" aria-checked="true" style="list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;" aria-level="2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAABbElEQVR4Ae3bsU4CYRDEcRsxodZE8Q0BbS258l5MwESJNL6HOfrPKdhyxeBcwk5mkn9F98sGIOSuPM/zPM/zPI+xG/SEtuiAWpEOaIOWaDIWziP6RK14OzSjX44ITvTBvqRn1MRaMIHeBIE2TKBBEGhgArWkKmtJBjKQgQxkIANd/Aw0NVC+O7RHvYFynHasN1COE/UGynGiXgOIjxOtdIH4OGJAfBwxID6OGBAfRwiIjyMARMCpCjRF5+72Dzhd5R+rHfpC92NeTlWgLl5PkQg4RYBynBSJgFMGKMNJkQg4lYFeUDuFRMCpBXQOEgGnDtA/kPg4xT7m2y/tCd9zKgOdviTC5RQEIiAFjh4QASlw9IAISIEjCURAWvmf1UDKcQwUSDmOgWLdMcxA7BnIQAYykIEM5EcRvplAW0GgNRNoKQg0ZwJN0E4I5x1dI+pmgSSA84BG2QQt0LrYG/eAXtGccjme53me53me9wPjPWZWjhktAQAAAABJRU5ErkJggg==" width="18.4px" height="18.4px" alt="checked" aria-roledescription="checkbox" style="margin-right:3px;" /><p style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">done</span></p></li><li role="checkbox" aria-checked="false" style="list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" width="18.4px" height="18.4px" alt="unchecked" aria-roledescription="checkbox" style="margin-right:3px;" /><p style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">todo</span></p></li></ul><li role="checkbox" aria-checked="false" style="list-style-type:none;font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" width="18.4px" height="18.4px" alt="unchecked" aria-roledescription="checkbox" style="margin-right:3px;" /><p style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11.5pt;font-family:'Optimistic Text',sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">todo</span></p></li></ul></b>`,
         },
index 43bef7e83c0a960a6dfb56b742204101411d13d2..4a3a489504e1d6a054d352566467372d778b5090 100644 (file)
@@ -624,7 +624,28 @@ export class TextNode extends LexicalNode {
       element !== null && isHTMLElement(element),
       'Expected TextNode createDOM to always return a HTMLElement',
     );
-    element.style.whiteSpace = 'pre-wrap';
+
+    // Wrap up to retain space if head/tail whitespace exists
+    const text = this.getTextContent();
+    if (/^\s|\s$/.test(text)) {
+      element.style.whiteSpace = 'pre-wrap';
+    }
+
+    // Strip editor theme classes
+    for (const className of Array.from(element.classList.values())) {
+      if (className.startsWith('editor-theme-')) {
+        element.classList.remove(className);
+      }
+    }
+    if (element.classList.length === 0) {
+      element.removeAttribute('class');
+    }
+
+    // Remove placeholder tag if redundant
+    if (element.nodeName === 'SPAN' && !element.getAttribute('style')) {
+      element = document.createTextNode(text);
+    }
+
     // This is the only way to properly add support for most clients,
     // even if it's semantically incorrect to have to resort to using
     // <b>, <u>, <s>, <i> elements.
@@ -632,7 +653,7 @@ export class TextNode extends LexicalNode {
       element = wrapElementWith(element, 'b');
     }
     if (this.hasFormat('italic')) {
-      element = wrapElementWith(element, 'i');
+      element = wrapElementWith(element, 'em');
     }
     if (this.hasFormat('strikethrough')) {
       element = wrapElementWith(element, 's');
@@ -1329,6 +1350,10 @@ function applyTextFormatFromStyle(
   // Google Docs uses span tags + vertical-align to specify subscript and superscript
   const verticalAlign = style.verticalAlign;
 
+  // Styles to copy to node
+  const color = style.color;
+  const backgroundColor = style.backgroundColor;
+
   return (lexicalNode: LexicalNode) => {
     if (!$isTextNode(lexicalNode)) {
       return lexicalNode;
@@ -1355,6 +1380,18 @@ function applyTextFormatFromStyle(
       lexicalNode.toggleFormat('superscript');
     }
 
+    // Apply styles
+    let style = lexicalNode.getStyle();
+    if (color) {
+      style += `color: ${color};`;
+    }
+    if (backgroundColor && backgroundColor !== 'transparent') {
+      style += `background-color: ${backgroundColor};`;
+    }
+    if (style) {
+      lexicalNode.setStyle(style);
+    }
+
     if (shouldApply && !lexicalNode.hasFormat(shouldApply)) {
       lexicalNode.toggleFormat(shouldApply);
     }
index a57ff3f42f6b833e7b8910ad359ca1a8e0e88699..d8525fb369f901c8af1451314dd3492380a67965 100644 (file)
@@ -107,7 +107,7 @@ describe('LexicalTabNode tests', () => {
         $insertDataTransferForRichText(dataTransfer, selection, editor);
       });
       expect(testEnv.innerHTML).toBe(
-        '<p><span data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></p><p><span data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></p>',
+        '<p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span style="color: rgb(0, 0, 0);" data-lexical-text="true">world</span></p><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span style="color: rgb(0, 0, 0);" data-lexical-text="true">world</span></p>',
       );
     });
 
index 57e1dcb3baf59bd8c120924bf687f4e29e64327a..b1ea099ac1ecd763fdf1d64f2e5c78844bb6552a 100644 (file)
@@ -8,7 +8,7 @@
 
 import {
   $createParagraphNode,
-  $createTextNode,
+  $createTextNode, $getEditor,
   $getNodeByKey,
   $getRoot,
   $getSelection,
@@ -41,6 +41,9 @@ import {
   $setCompositionKey,
   getEditorStateTextContent,
 } from '../../../LexicalUtils';
+import {Text} from "@codemirror/state";
+import {$generateHtmlFromNodes} from "@lexical/html";
+import {formatBold} from "@lexical/selection/__tests__/utils";
 
 const editorConfig = Object.freeze({
   namespace: '',
@@ -792,6 +795,58 @@ describe('LexicalTextNode tests', () => {
     );
   });
 
+  describe('exportDOM()', () => {
+
+    test('simple text exports as a text node', async () => {
+      await update(() => {
+        const paragraph = $getRoot().getFirstChild<ElementNode>()!;
+        const textNode = $createTextNode('hello');
+        paragraph.append(textNode);
+
+        const html = $generateHtmlFromNodes($getEditor(), null);
+        expect(html).toBe('<p>hello</p>');
+      });
+    });
+
+    test('simple text wrapped in span if leading or ending spacing', async () => {
+
+      const textByExpectedHtml = {
+        'hello ': '<p><span style="white-space: pre-wrap;">hello </span></p>',
+        ' hello': '<p><span style="white-space: pre-wrap;"> hello</span></p>',
+        ' hello ': '<p><span style="white-space: pre-wrap;"> hello </span></p>',
+      }
+
+      await update(() => {
+        const paragraph = $getRoot().getFirstChild<ElementNode>()!;
+        for (const [text, expectedHtml] of Object.entries(textByExpectedHtml)) {
+          paragraph.getChildren().forEach(c => c.remove(true));
+          const textNode = $createTextNode(text);
+          paragraph.append(textNode);
+
+          const html = $generateHtmlFromNodes($getEditor(), null);
+          expect(html).toBe(expectedHtml);
+        }
+      });
+    });
+
+    test('text with formats exports using format elements instead of classes', async () => {
+      await update(() => {
+        const paragraph = $getRoot().getFirstChild<ElementNode>()!;
+        const textNode = $createTextNode('hello');
+        textNode.toggleFormat('bold');
+        textNode.toggleFormat('subscript');
+        textNode.toggleFormat('italic');
+        textNode.toggleFormat('underline');
+        textNode.toggleFormat('code');
+        paragraph.append(textNode);
+
+        const html = $generateHtmlFromNodes($getEditor(), null);
+        expect(html).toBe('<p><u><em><b><code spellcheck="false"><strong>hello</strong></code></b></em></u></p>');
+      });
+    });
+
+  });
+
   test('mergeWithSibling', async () => {
     await update(() => {
       const paragraph = $getRoot().getFirstChild<ElementNode>()!;
index afa65708d4bd2379b821bff4899ce86832d04ad2..c4dedd47d137dc8b93b3466c0c162754b2e35bfd 100644 (file)
@@ -206,7 +206,7 @@ describe('LexicalHeadlessEditor', () => {
     cleanup();
 
     expect(html).toBe(
-      '<p dir="ltr"><span style="white-space: pre-wrap;">hello world</span></p>',
+      '<p>hello world</p>',
     );
   });
 });
index 3dbe5da8b3e10850945250b0496e2cdb0109ef97..947e591b4ff633796722c00c5cc65e2d32b76358 100644 (file)
@@ -102,7 +102,7 @@ describe('HTML', () => {
       html = $generateHtmlFromNodes(editor, selection);
     });
 
-    expect(html).toBe('<span style="white-space: pre-wrap;">World</span>');
+    expect(html).toBe('World');
   });
 
   test(`[Lexical -> HTML]: Default selection (undefined) should serialize entire editor state`, () => {
@@ -145,7 +145,7 @@ describe('HTML', () => {
     });
 
     expect(html).toBe(
-      '<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">World</span></p>',
+      '<p>Hello</p><p>World</p>',
     );
   });
 
@@ -175,7 +175,7 @@ describe('HTML', () => {
     });
 
     expect(html).toBe(
-      '<p style="text-align: center;"><span style="white-space: pre-wrap;">Hello world!</span></p>',
+      '<p style="text-align: center;">Hello world!</p>',
     );
   });
 
@@ -205,7 +205,7 @@ describe('HTML', () => {
     });
 
     expect(html).toBe(
-      '<p style="text-align: center;"><span style="white-space: pre-wrap;">Hello world!</span></p>',
+      '<p style="text-align: center;">Hello world!</p>',
     );
   });
 });
index abc50962949038aba038c02cb073d5dbce17b375..6848e55325debae123ae209e62801f6b7532863e 100644 (file)
@@ -115,7 +115,7 @@ describe('LexicalTableNode tests', () => {
         // Make sure paragraph is inserted inside empty cells
         const emptyCell = '<td><p><br></p></td>';
         expect(testEnv.innerHTML).toBe(
-          `<table><tr><td><p><span data-lexical-text="true">Hello there</span></p></td><td><p><span data-lexical-text="true">General Kenobi!</span></p></td></tr><tr><td><p><span data-lexical-text="true">Lexical is nice</span></p></td>${emptyCell}</tr></table>`,
+          `<table><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello there</span></p></td><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">General Kenobi!</span></p></td></tr><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Lexical is nice</span></p></td>${emptyCell}</tr></table>`,
         );
       });
 
index 7655b45405342bb4453a6afb03dd4f789c2018d9..fd7731f906103598a3b3f37e756fa49348e28799 100644 (file)
@@ -330,7 +330,7 @@ describe('LexicalEventHelpers', () => {
       const suite = [
         {
           expectedHTML:
-            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Get schwifty!</span></p></div>',
+            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</span></p></div>',
           inputs: [
             pasteHTML(
               `<b style="font-weight:normal;" id="docs-internal-guid-2c706577-7fff-f54a-fe65-12f480020fac"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
@@ -340,7 +340,7 @@ describe('LexicalEventHelpers', () => {
         },
         {
           expectedHTML:
-            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">Get schwifty!</strong></p></div>',
+            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><strong class="editor-text-bold" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</strong></p></div>',
           inputs: [
             pasteHTML(
               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
@@ -350,7 +350,7 @@ describe('LexicalEventHelpers', () => {
         },
         {
           expectedHTML:
-            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><em class="editor-text-italic" data-lexical-text="true">Get schwifty!</em></p></div>',
+            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><em class="editor-text-italic" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</em></p></div>',
           inputs: [
             pasteHTML(
               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
@@ -360,7 +360,7 @@ describe('LexicalEventHelpers', () => {
         },
         {
           expectedHTML:
-            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span class="editor-text-strikethrough" data-lexical-text="true">Get schwifty!</span></p></div>',
+            '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span class="editor-text-strikethrough" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</span></p></div>',
           inputs: [
             pasteHTML(
               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
index f04bb5d2e46fe18ae803c78e0946e5e6dcd074a2..a70200d634901c1f101340952df67a6c4a9eda77 100644 (file)
@@ -38,7 +38,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph in between two text nodes',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">world</span></p>',
+        '<p>Hello</p><p>world</p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 1,
       splitPath: [0],
@@ -46,7 +46,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph before the first text node',
       expectedHtml:
-        '<p><br></p><p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p>',
+        '<p><br></p><p>Helloworld</p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 0,
       splitPath: [0],
@@ -54,7 +54,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph after the last text node',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p><p><br></p>',
+        '<p>Helloworld</p><p><br></p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 2, // Any offset that is higher than children size
       splitPath: [0],
@@ -62,8 +62,8 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split list items between two text nodes',
       expectedHtml:
-        '<ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul>' +
-        '<ul><li><span style="white-space: pre-wrap;">world</span></li></ul>',
+        '<ul><li>Hello</li></ul>' +
+        '<ul><li>world</li></ul>',
       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
       splitOffset: 1, // Any offset that is higher than children size
       splitPath: [0, 0],
@@ -72,7 +72,7 @@ describe('LexicalUtils#splitNode', () => {
       _: 'split list items before the first text node',
       expectedHtml:
         '<ul><li></li></ul>' +
-        '<ul><li><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></li></ul>',
+        '<ul><li>Helloworld</li></ul>',
       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
       splitOffset: 0, // Any offset that is higher than children size
       splitPath: [0, 0],
@@ -81,12 +81,12 @@ describe('LexicalUtils#splitNode', () => {
       _: 'split nested list items',
       expectedHtml:
         '<ul>' +
-        '<li><span style="white-space: pre-wrap;">Before</span></li>' +
-        '<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
+        '<li>Before</li>' +
+        '<li><ul><li>Hello</li></ul></li>' +
         '</ul>' +
         '<ul>' +
-        '<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
-        '<li><span style="white-space: pre-wrap;">After</span></li>' +
+        '<li><ul><li>world</li></ul></li>' +
+        '<li>After</li>' +
         '</ul>',
       initialHtml:
         '<ul>' +
index 9664b2d80ca3f1718199eb69d8a4fda6f0875bee..fb04e6284137a0ff1fa5125fd7cd92b8cb15ebbf 100644 (file)
@@ -46,7 +46,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert into paragraph in between two text nodes',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Hello</span></p><test-decorator></test-decorator><p><span style="white-space: pre-wrap;">world</span></p>',
+        '<p>Hello</p><test-decorator></test-decorator><p>world</p>',
       initialHtml: '<p><span>Helloworld</span></p>',
       selectionOffset: 5, // Selection on text node after "Hello" world
       selectionPath: [0, 0],
@@ -55,13 +55,13 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
       _: 'insert into nested list items',
       expectedHtml:
         '<ul>' +
-        '<li><span style="white-space: pre-wrap;">Before</span></li>' +
-        '<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
+        '<li>Before</li>' +
+        '<li><ul><li>Hello</li></ul></li>' +
         '</ul>' +
         '<test-decorator></test-decorator>' +
         '<ul>' +
-        '<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
-        '<li><span style="white-space: pre-wrap;">After</span></li>' +
+        '<li><ul><li>world</li></ul></li>' +
+        '<li>After</li>' +
         '</ul>',
       initialHtml:
         '<ul>' +
@@ -82,7 +82,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert in the end of paragraph',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Hello world</span></p>' +
+        '<p>Hello world</p>' +
         '<test-decorator></test-decorator>' +
         '<p><br></p>',
       initialHtml: '<p>Hello world</p>',
@@ -94,7 +94,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
       expectedHtml:
         '<p><br></p>' +
         '<test-decorator></test-decorator>' +
-        '<p><span style="white-space: pre-wrap;">Hello world</span></p>',
+        '<p>Hello world</p>',
       initialHtml: '<p>Hello world</p>',
       selectionOffset: 0, // Selection on text node after "Hello" world
       selectionPath: [0, 0],
@@ -104,8 +104,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
       expectedHtml:
         '<test-decorator></test-decorator>' +
         '<test-decorator></test-decorator>' +
-        '<p><span style="white-space: pre-wrap;">Before</span></p>' +
-        '<p><span style="white-space: pre-wrap;">After</span></p>',
+        '<p>Before</p>' +
+        '<p>After</p>',
       initialHtml:
         '<test-decorator></test-decorator>' +
         '<p><span>Before</span></p>' +
@@ -116,9 +116,9 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert with selection on root child',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Before</span></p>' +
+        '<p>Before</p>' +
         '<test-decorator></test-decorator>' +
-        '<p><span style="white-space: pre-wrap;">After</span></p>',
+        '<p>After</p>',
       initialHtml: '<p>Before</p><p>After</p>',
       selectionOffset: 1,
       selectionPath: [],
@@ -126,7 +126,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert with selection on root end',
       expectedHtml:
-        '<p><span style="white-space: pre-wrap;">Before</span></p>' +
+        '<p>Before</p>' +
         '<test-decorator></test-decorator>',
       initialHtml: '<p>Before</p>',
       selectionOffset: 1,
index 31e3533b1e0a98cc68e5491671c233c4e7d1ac64..bcd4851e80555fe90bf713fdd539b24ea6d3aba0 100644 (file)
@@ -7,7 +7,6 @@
 ## Main Todo
 
 - Mac: Shortcut support via command.
-- Update toolbar overflows to match existing editor, incl. direction dynamic controls
 
 ## Secondary Todo
 
@@ -16,9 +15,9 @@
 - Table caption text support
 - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
 - Deep check of translation coverage
+- About button & view
+- Mobile display and handling
 
 ## Bugs
 
-- Editor theme classes remain on items after export
-- List selection can get lost on nesting/unnesting
-- Content not properly saving on new pages
\ No newline at end of file
+- List selection can get lost on nesting/unnesting
\ No newline at end of file
index 7325303758337871dbec98d60e252d558f9311ef..7c0975da7e7193cf055bc8e29df3f97ff53db62a 100644 (file)
@@ -163,6 +163,10 @@ export class EditorUIManager {
         });
     }
 
+    getDefaultDirection(): 'rtl' | 'ltr' {
+        return this.getContext().options.textDirection === 'rtl' ? 'rtl' : 'ltr';
+    }
+
     protected updateContextToolbars(update: EditorUiStateUpdate): void {
         for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {
             const toolbar = this.activeContextToolbars[i];
index 8bfdb8965d2ebcf9db50a52b1bad972cf3d86be6..3811f44b9bfae2a17772bcae87a4d3f89e895139 100644 (file)
@@ -32,7 +32,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
     manager.setContext(context);
 
     // Create primary toolbar
-    manager.setToolbar(getMainEditorFullToolbar());
+    manager.setToolbar(getMainEditorFullToolbar(context));
 
     // Register modals
     for (const key of Object.keys(modals)) {
index b064a2a9f1b0f67a19a0d7fb8bad86da810ab585..35146e5a440aecca04097a3477c01f57e19f3ea4 100644 (file)
@@ -1,5 +1,5 @@
 import {EditorButton} from "./framework/buttons";
-import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
+import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext, EditorUiElement} from "./framework/core";
 import {EditorFormatMenu} from "./framework/blocks/format-menu";
 import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
 import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
@@ -80,7 +80,10 @@ import {el} from "../utils/dom";
 import {EditorButtonWithMenu} from "./framework/blocks/button-with-menu";
 import {EditorSeparator} from "./framework/blocks/separator";
 
-export function getMainEditorFullToolbar(): EditorContainerUiElement {
+export function getMainEditorFullToolbar(context: EditorUiContext): EditorContainerUiElement {
+
+    const inRtlMode = context.manager.getDefaultDirection() === 'rtl';
+
     return new EditorSimpleClassContainer('editor-toolbar-main', [
 
         // History state
@@ -124,17 +127,17 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
         ]),
 
         // Alignment
-        new EditorOverflowContainer(6, [ // TODO - Dynamic
+        new EditorOverflowContainer(6, [
             new EditorButton(alignLeft),
             new EditorButton(alignCenter),
             new EditorButton(alignRight),
             new EditorButton(alignJustify),
-            new EditorButton(directionLTR), // TODO - Dynamic
-            new EditorButton(directionRTL), // TODO - Dynamic
-        ]),
+            inRtlMode ? new EditorButton(directionLTR) : null,
+            inRtlMode ? new EditorButton(directionRTL) : null,
+        ].filter(x => x !== null)),
 
         // Lists
-        new EditorOverflowContainer(5, [
+        new EditorOverflowContainer(3, [
             new EditorButton(bulletList),
             new EditorButton(numberList),
             new EditorButton(taskList),
index 91aef9920861cbf26ab7e67c989cd2d200f26fc5..b33cb4d055870b4aa96e6f67f184d96e0e7a118f 100644 (file)
@@ -27,6 +27,7 @@ body.editor-is-fullscreen {
 }
 .editor-content-area {
   min-height: 100%;
+  padding-block: 1rem;
   &:focus {
     outline: 0;
   }
@@ -136,7 +137,6 @@ body.editor-is-fullscreen {
   background-color: #FFF;
   box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.15);
   z-index: 99;
-  min-width: 120px;
   display: flex;
   flex-direction: row;
 }
index 6e6f7bb7e21bf9607f55375fee2eadcd264fe375..426f7961c727896414cc65caa8aa26371ba8b313 100755 (executable)
 }
 
 @include larger-than($xxl) {
+  .page-editor-wysiwyg2024 .page-edit-toolbar,
+  .page-editor-wysiwyg2024 .page-editor-page-area,
   .page-editor-wysiwyg .page-edit-toolbar,
   .page-editor-wysiwyg .page-editor-page-area {
     max-width: 1140px;
   }
 
-  .page-editor-wysiwyg .floating-toolbox {
+  .page-editor-wysiwyg .floating-toolbox,
+  .page-editor-wysiwyg2024 .floating-toolbox {
     position: absolute;
   }
 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.