Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"@babel/eslint-parser": "^7.24.5",
"@babel/helper-module-imports": "^7.24.1",
"@babel/plugin-transform-optional-catch-binding": "^7.24.1",
"@babel/preset-env": "^7.28.6",
"@babel/preset-flow": "^7.24.1",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
Expand Down
42 changes: 3 additions & 39 deletions packages/lexical-markdown/src/MarkdownExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,47 +293,11 @@ function exportTextFormat(
return closingTagsBefore + output;
}

// Get next or previous text sibling a text node, including cases
// when it's a child of inline element (e.g. link)
function getTextSibling(node: TextNode, backward: boolean): TextNode | null {
let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
const sibling = backward ? node.getPreviousSibling() : node.getNextSibling();

if (!sibling) {
const parent = node.getParentOrThrow();

if (parent.isInline()) {
sibling = backward
? parent.getPreviousSibling()
: parent.getNextSibling();
}
}

while (sibling) {
if ($isElementNode(sibling)) {
if (!sibling.isInline()) {
break;
}

const descendant = backward
? sibling.getLastDescendant()
: sibling.getFirstDescendant();

if ($isTextNode(descendant)) {
return descendant;
} else {
sibling = backward
? sibling.getPreviousSibling()
: sibling.getNextSibling();
}
}

if ($isTextNode(sibling)) {
return sibling;
}

if (!$isElementNode(sibling)) {
return null;
}
if ($isTextNode(sibling)) {
return sibling;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,25 +604,25 @@ describe('Markdown', () => {
html: '<p><span style="white-space: pre-wrap;">Text </span><b><strong style="white-space: pre-wrap;">boldstart </strong></b><a href="https://lexical.dev"><b><strong style="white-space: pre-wrap;">text</strong></b></a><b><strong style="white-space: pre-wrap;"> boldend</strong></b><span style="white-space: pre-wrap;"> text</span></p>',
md: 'Text **boldstart [text](https://lexical.dev) boldend** text',
mdAfterExport:
'Text **boldstart&#32;[text](https://lexical.dev)&#32;boldend** text',
'Text **boldstart&#32;**[**text**](https://lexical.dev)**&#32;boldend** text',
},
{
html: '<p><span style="white-space: pre-wrap;">Text </span><b><strong style="white-space: pre-wrap;">boldstart </strong></b><a href="https://lexical.dev"><b><code spellcheck="false" style="white-space: pre-wrap;"><strong>text</strong></code></b></a><b><strong style="white-space: pre-wrap;"> boldend</strong></b><span style="white-space: pre-wrap;"> text</span></p>',
md: 'Text **boldstart [`text`](https://lexical.dev) boldend** text',
mdAfterExport:
'Text **boldstart&#32;[`text`](https://lexical.dev)&#32;boldend** text',
'Text **boldstart&#32;**[**`text`**](https://lexical.dev)**&#32;boldend** text',
},
{
html: '<p><span style="white-space: pre-wrap;">It </span><s><i><b><strong style="white-space: pre-wrap;">works </strong></b></i></s><a href="https://lexical.io"><s><i><b><strong style="white-space: pre-wrap;">with links</strong></b></i></s></a><span style="white-space: pre-wrap;"> too</span></p>',
md: 'It ~~___works [with links](https://lexical.io)___~~ too',
mdAfterExport:
'It ***~~works&#32;[with links](https://lexical.io)~~*** too',
'It ***~~works&#32;~~***[***~~with links~~***](https://lexical.io) too',
},
{
html: '<p><span style="white-space: pre-wrap;">It </span><s><i><b><strong style="white-space: pre-wrap;">works </strong></b></i></s><a href="https://lexical.io"><s><i><b><strong style="white-space: pre-wrap;">with links</strong></b></i></s></a><s><i><b><strong style="white-space: pre-wrap;"> too</strong></b></i></s><span style="white-space: pre-wrap;">!</span></p>',
md: 'It ~~___works [with links](https://lexical.io) too___~~!',
mdAfterExport:
'It ***~~works&#32;[with links](https://lexical.io)&#32;too~~***!',
'It ***~~works&#32;~~***[***~~with links~~***](https://lexical.io)***~~&#32;too~~***!',
},
{
html: '<p><a href="https://lexical.dev"><span style="white-space: pre-wrap;">link</span></a><a href="https://lexical.dev"><span style="white-space: pre-wrap;">link2</span></a></p>',
Expand Down Expand Up @@ -706,6 +706,16 @@ describe('Markdown', () => {
html: '<p><span style="white-space: pre-wrap;">[](https://lexical.dev)</span></p>',
md: '[](https://lexical.dev)',
},
{
html: '<p><a href="https://lexical.dev"><b><strong style="white-space: pre-wrap;">link</strong></b></a><b><strong style="white-space: pre-wrap;"> text</strong></b></p>',
md: '[**link**](https://lexical.dev)** text**',
mdAfterExport: '[**link**](https://lexical.dev)**&#32;text**',
},
{
html: '<p><b><strong style="white-space: pre-wrap;">text </strong></b><a href="https://lexical.dev"><b><strong style="white-space: pre-wrap;">link</strong></b></a></p>',
md: '**text [link](https://lexical.dev)**',
mdAfterExport: '**text&#32;**[**link**](https://lexical.dev)',
},
];

for (const {
Expand Down
181 changes: 181 additions & 0 deletions packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7152,6 +7152,187 @@ test.describe.parallel('Tables', () => {
focus: {x: 2, y: 2},
});
});

test('Drag-select column in 2x2 table selects all cells in that column', async ({
page,
isPlainText,
isCollab,
}) => {
test.skip(isPlainText);
await initialize({isCollab, page});

await focusEditor(page);

// Insert a 2x2 table
await insertTable(page, 2, 2);
const pageOrFrame = getPageOrFrame(page);

await pageOrFrame.waitForFunction(() => {
const cell = document.querySelector(
'table:first-of-type td, table:first-of-type th',
);
// eslint-disable-next-line no-underscore-dangle
return Boolean(cell && cell._cell);
});

const readTableSelectionCoordinates = async () => {
return await pageOrFrame.evaluate(() => {
const editor = window.lexicalEditor;
if (!editor) {
return null;
}
const selection = editor.getEditorState()._selection;
if (!selection || selection.tableKey == null) {
return null;
}
const anchorElement = editor.getElementByKey(selection.anchor.key);
const focusElement = editor.getElementByKey(selection.focus.key);
const anchorCell = anchorElement?._cell;
const focusCell = focusElement?._cell;
if (!anchorCell || !focusCell) {
return null;
}
return {
anchor: {x: anchorCell.x, y: anchorCell.y},
focus: {x: focusCell.x, y: focusCell.y},
};
});
};

const matchesExpected = (coords, expected) => {
if (!coords) {
return false;
}
const anchorMatches =
expected.anchor == null ||
((expected.anchor.x === undefined ||
coords.anchor.x === expected.anchor.x) &&
(expected.anchor.y === undefined ||
coords.anchor.y === expected.anchor.y));
const focusMatches =
expected.focus == null ||
((expected.focus.x === undefined ||
coords.focus.x === expected.focus.x) &&
(expected.focus.y === undefined ||
coords.focus.y === expected.focus.y));
return anchorMatches && focusMatches;
};

const waitForTableSelectionCoordinates = async (expected) => {
for (let i = 0; i < 20; i++) {
const coords = await readTableSelectionCoordinates();
if (matchesExpected(coords, expected)) {
return true;
}
await sleep(50);
}
return false;
};

const dispatchPointerDrag = async (dragStart, dragEnd) => {
return await pageOrFrame.evaluate(
({endPoint, startPoint}) => {
const startTarget = document.elementFromPoint(
startPoint.x,
startPoint.y,
);
const endTarget = document.elementFromPoint(endPoint.x, endPoint.y);
if (!startTarget || !endTarget) {
return false;
}
const baseEvent = {
bubbles: true,
button: 0,
buttons: 1,
isPrimary: true,
pointerId: 1,
pointerType: 'mouse',
};
startTarget.dispatchEvent(
new PointerEvent('pointerdown', {
...baseEvent,
clientX: startPoint.x,
clientY: startPoint.y,
}),
);
endTarget.dispatchEvent(
new PointerEvent('pointermove', {
...baseEvent,
clientX: endPoint.x,
clientY: endPoint.y,
}),
);
endTarget.dispatchEvent(
new PointerEvent('pointerup', {
...baseEvent,
buttons: 0,
clientX: endPoint.x,
clientY: endPoint.y,
}),
);
return true;
},
{endPoint: dragEnd, startPoint: dragStart},
);
};

const dragAndAssertSelection = async (fromBox, toBox, expected) => {
await dragMouse(page, fromBox, toBox, {slow: true});
if (await waitForTableSelectionCoordinates(expected)) {
return;
}
const start = {
x: fromBox.x + fromBox.width / 2,
y: fromBox.y + fromBox.height / 2,
};
const end = {
x: toBox.x + toBox.width / 2,
y: toBox.y + toBox.height / 2,
};
await dispatchPointerDrag(start, end);
if (await waitForTableSelectionCoordinates(expected)) {
return;
}
const coords = await readTableSelectionCoordinates();
throw new Error(
`Expected table selection ${JSON.stringify(
expected,
)} but got ${JSON.stringify(coords)}`,
);
};

// Test first column: straight drag from top to bottom (no click first)
// This tests the fix for issue #8079 - straight drag should work
const firstColTop = await selectorBoundingBox(
page,
'table:first-of-type > tr:nth-of-type(1) > th:nth-child(1)',
);
const firstColBottom = await selectorBoundingBox(
page,
'table:first-of-type > tr:nth-of-type(2) > th:nth-child(1)',
);

await dragAndAssertSelection(firstColTop, firstColBottom, {
anchor: {x: 0, y: 0},
focus: {x: 0, y: 1},
});

// Test second column: straight drag from top to bottom on FIRST attempt
// This was the bug: first drag missed top cell, subsequent drags worked
const secondColTop = await selectorBoundingBox(
page,
'table:first-of-type > tr:nth-of-type(1) > th:nth-child(2)',
);
const secondColBottom = await selectorBoundingBox(
page,
'table:first-of-type > tr:nth-of-type(2) > td:nth-child(2)',
);

await dragAndAssertSelection(secondColTop, secondColBottom, {
anchor: {x: 1, y: 0},
focus: {x: 1, y: 1},
});
});
});

const TABLE_WITH_MERGED_CELLS = `
Expand Down
Loading
Loading