diff --git a/resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts b/resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts index 9f832b69e..534663a54 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/unit/HTMLCopyAndPaste.test.ts @@ -82,7 +82,7 @@ describe('HTMLCopyAndPaste tests', () => { pastedHTML: ` 123
456
`, }, { - expectedHTML: ``, + expectedHTML: ``, name: 'google doc checklist', pastedHTML: ``, }, diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts index 43bef7e83..4a3a48950 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts @@ -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 // , , , 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); } diff --git a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts index a57ff3f42..d8525fb36 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTabNode.test.ts @@ -107,7 +107,7 @@ describe('LexicalTabNode tests', () => { $insertDataTransferForRichText(dataTransfer, selection, editor); }); expect(testEnv.innerHTML).toBe( - '

Hello\tworld

Hello\tworld

', + '

Hello\tworld

Hello\tworld

', ); }); diff --git a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts index 57e1dcb3b..b1ea099ac 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts @@ -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()!; + const textNode = $createTextNode('hello'); + paragraph.append(textNode); + + const html = $generateHtmlFromNodes($getEditor(), null); + expect(html).toBe('

hello

'); + }); + }); + + test('simple text wrapped in span if leading or ending spacing', async () => { + + const textByExpectedHtml = { + 'hello ': '

hello

', + ' hello': '

hello

', + ' hello ': '

hello

', + } + + await update(() => { + const paragraph = $getRoot().getFirstChild()!; + 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()!; + 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('

hello

'); + }); + }); + + }); + test('mergeWithSibling', async () => { await update(() => { const paragraph = $getRoot().getFirstChild()!; diff --git a/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts b/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts index afa65708d..c4dedd47d 100644 --- a/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts +++ b/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts @@ -206,7 +206,7 @@ describe('LexicalHeadlessEditor', () => { cleanup(); expect(html).toBe( - '

hello world

', + '

hello world

', ); }); }); diff --git a/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts b/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts index 3dbe5da8b..947e591b4 100644 --- a/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts +++ b/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts @@ -102,7 +102,7 @@ describe('HTML', () => { html = $generateHtmlFromNodes(editor, selection); }); - expect(html).toBe('World'); + expect(html).toBe('World'); }); test(`[Lexical -> HTML]: Default selection (undefined) should serialize entire editor state`, () => { @@ -145,7 +145,7 @@ describe('HTML', () => { }); expect(html).toBe( - '

Hello

World

', + '

Hello

World

', ); }); @@ -175,7 +175,7 @@ describe('HTML', () => { }); expect(html).toBe( - '

Hello world!

', + '

Hello world!

', ); }); @@ -205,7 +205,7 @@ describe('HTML', () => { }); expect(html).toBe( - '

Hello world!

', + '

Hello world!

', ); }); }); diff --git a/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts b/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts index abc509629..6848e5532 100644 --- a/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts +++ b/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts @@ -115,7 +115,7 @@ describe('LexicalTableNode tests', () => { // Make sure paragraph is inserted inside empty cells const emptyCell = '


'; expect(testEnv.innerHTML).toBe( - `${emptyCell}

Hello there

General Kenobi!

Lexical is nice

`, + `${emptyCell}

Hello there

General Kenobi!

Lexical is nice

`, ); }); diff --git a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts index 7655b4540..fd7731f90 100644 --- a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts +++ b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts @@ -330,7 +330,7 @@ describe('LexicalEventHelpers', () => { const suite = [ { expectedHTML: - '

Get schwifty!

', + '

Get schwifty!

', inputs: [ pasteHTML( `Get schwifty!`, @@ -340,7 +340,7 @@ describe('LexicalEventHelpers', () => { }, { expectedHTML: - '

Get schwifty!

', + '

Get schwifty!

', inputs: [ pasteHTML( `Get schwifty!`, @@ -350,7 +350,7 @@ describe('LexicalEventHelpers', () => { }, { expectedHTML: - '

Get schwifty!

', + '

Get schwifty!

', inputs: [ pasteHTML( `Get schwifty!`, @@ -360,7 +360,7 @@ describe('LexicalEventHelpers', () => { }, { expectedHTML: - '

Get schwifty!

', + '

Get schwifty!

', inputs: [ pasteHTML( `Get schwifty!`, diff --git a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts index f04bb5d2e..a70200d63 100644 --- a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts +++ b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts @@ -38,7 +38,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph in between two text nodes', expectedHtml: - '

Hello

world

', + '

Hello

world

', initialHtml: '

Helloworld

', splitOffset: 1, splitPath: [0], @@ -46,7 +46,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph before the first text node', expectedHtml: - '


Helloworld

', + '


Helloworld

', initialHtml: '

Helloworld

', splitOffset: 0, splitPath: [0], @@ -54,7 +54,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph after the last text node', expectedHtml: - '

Helloworld


', + '

Helloworld


', initialHtml: '

Helloworld

', 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: - '
  • Hello
' + - '
  • world
', + '
  • Hello
' + + '
  • world
', initialHtml: '
  • Helloworld
', 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: '
' + - '
  • Helloworld
', + '
  • Helloworld
', initialHtml: '
  • Helloworld
', 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: '
    ' + - '
  • Before
  • ' + - '
    • Hello
  • ' + + '
  • Before
  • ' + + '
    • Hello
  • ' + '
' + '
    ' + - '
    • world
  • ' + - '
  • After
  • ' + + '
    • world
  • ' + + '
  • After
  • ' + '
', initialHtml: '
    ' + diff --git a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts index 9664b2d80..fb04e6284 100644 --- a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts +++ b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts @@ -46,7 +46,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert into paragraph in between two text nodes', expectedHtml: - '

    Hello

    world

    ', + '

    Hello

    world

    ', initialHtml: '

    Helloworld

    ', 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: '
      ' + - '
    • Before
    • ' + - '
      • Hello
    • ' + + '
    • Before
    • ' + + '
      • Hello
    • ' + '
    ' + '' + '
      ' + - '
      • world
    • ' + - '
    • After
    • ' + + '
      • world
    • ' + + '
    • After
    • ' + '
    ', initialHtml: '
      ' + @@ -82,7 +82,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert in the end of paragraph', expectedHtml: - '

      Hello world

      ' + + '

      Hello world

      ' + '' + '


      ', initialHtml: '

      Hello world

      ', @@ -94,7 +94,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { expectedHtml: '


      ' + '' + - '

      Hello world

      ', + '

      Hello world

      ', initialHtml: '

      Hello world

      ', selectionOffset: 0, // Selection on text node after "Hello" world selectionPath: [0, 0], @@ -104,8 +104,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { expectedHtml: '' + '' + - '

      Before

      ' + - '

      After

      ', + '

      Before

      ' + + '

      After

      ', initialHtml: '' + '

      Before

      ' + @@ -116,9 +116,9 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert with selection on root child', expectedHtml: - '

      Before

      ' + + '

      Before

      ' + '' + - '

      After

      ', + '

      After

      ', initialHtml: '

      Before

      After

      ', selectionOffset: 1, selectionPath: [], @@ -126,7 +126,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert with selection on root end', expectedHtml: - '

      Before

      ' + + '

      Before

      ' + '', initialHtml: '

      Before

      ', selectionOffset: 1, diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 31e3533b1..bcd4851e8 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -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 diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 732530375..7c0975da7 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -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]; diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 8bfdb8965..3811f44b9 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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)) { diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index b064a2a9f..35146e5a4 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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), diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 91aef9920..b33cb4d05 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -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; } diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 6e6f7bb7e..426f7961c 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -35,12 +35,15 @@ } @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; } }