Skip to content

Commit

Permalink
Merge branch 'main' into feature/add-keyboard-shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
bedre7 committed Nov 5, 2024
2 parents e84877f + 2c1a8f1 commit 30bb9a6
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 9 deletions.
9 changes: 8 additions & 1 deletion packages/lexical-markdown/src/MarkdownImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,14 @@ function importTextMatchTransformers(
}

const startIndex = match.index || 0;
const endIndex = startIndex + match[0].length;
const endIndex = transformer.getEndIndex
? transformer.getEndIndex(textNode, match)
: startIndex + match[0].length;

if (endIndex === false) {
continue;
}

let replaceNode, newTextNode;

if (startIndex === 0) {
Expand Down
9 changes: 9 additions & 0 deletions packages/lexical-markdown/src/MarkdownTransformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ export type TextMatchTransformer = Readonly<{
* Determines how the matched markdown text should be transformed into a node during the markdown import process
*/
replace?: (node: TextNode, match: RegExpMatchArray) => void;
/**
* For import operations, this function can be used to determine the end index of the match, after `importRegExp` has matched.
* Without this function, the end index will be determined by the length of the match from `importRegExp`. Manually determining the end index can be useful if
* the match from `importRegExp` is not the entire text content of the node. That way, `importRegExp` can be used to match only the start of the node, and `getEndIndex`
* can be used to match the end of the node.
*
* @returns The end index of the match, or false if the match was unsuccessful and a different transformer should be tried.
*/
getEndIndex?: (node: TextNode, match: RegExpMatchArray) => number | false;
/**
* Single character that allows the transformer to trigger when typed in the editor. This does not affect markdown imports outside of the markdown shortcut plugin.
* If the trigger is matched, the `regExp` will be used to match the text in the second step.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {$createCodeNode, CodeNode} from '@lexical/code';
import {createHeadlessEditor} from '@lexical/headless';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {LinkNode} from '@lexical/link';
import {$createLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {$createTextNode, $getRoot, $insertNodes} from 'lexical';
Expand All @@ -28,6 +28,51 @@ import {
normalizeMarkdown,
} from '../../MarkdownTransformers';

const SIMPLE_INLINE_JSX_MATCHER: TextMatchTransformer = {
dependencies: [LinkNode],
getEndIndex(node, match) {
// Find the closing tag. Count the number of opening and closing tags to find the correct closing tag.
// For simplicity, this will only count the opening and closing tags without checking for "MyTag" specifically.
let openedSubStartMatches = 0;
const start = (match.index ?? 0) + match[0].length;
let endIndex = start;
const line = node.getTextContent();

for (let i = start; i < line.length; i++) {
const char = line[i];
if (char === '<') {
const nextChar = line[i + 1];
if (nextChar === '/') {
if (openedSubStartMatches === 0) {
endIndex = i + '</MyTag>'.length;
break;
}
openedSubStartMatches--;
} else {
openedSubStartMatches++;
}
}
}
return endIndex;
},
importRegExp: /<(MyTag)\s*>/,
regExp: /__ignore__/,
replace: (textNode, match) => {
const linkNode = $createLinkNode('simple-jsx');

const textStart = match[0].length + (match.index ?? 0);
const textEnd =
(match.index ?? 0) + textNode.getTextContent().length - '</MyTag>'.length;
const text = match.input?.slice(textStart, textEnd);

const linkTextNode = $createTextNode(text);
linkTextNode.setFormat(textNode.getFormat());
linkNode.append(linkTextNode);
textNode.replace(linkNode);
},
type: 'text-match',
};

// Matches html within a mdx file
const MDX_HTML_TRANSFORMER: MultilineElementTransformer = {
dependencies: [CodeNode],
Expand Down Expand Up @@ -461,6 +506,12 @@ describe('Markdown', () => {
md: '```ts\nCode\n```ts\nSub Code\n```\n```',
skipExport: true,
},
{
customTransformers: [SIMPLE_INLINE_JSX_MATCHER],
html: '<p><span style="white-space: pre-wrap;">Hello </span><a href="simple-jsx"><span style="white-space: pre-wrap;">One &lt;MyTag&gt;Two&lt;/MyTag&gt;</span></a><span style="white-space: pre-wrap;"> there</span></p>',
md: 'Hello <MyTag>One <MyTag>Two</MyTag></MyTag> there',
skipExport: true,
},
];

const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = {
Expand Down
4 changes: 3 additions & 1 deletion packages/lexical-react/src/LexicalContextMenuPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ export function LexicalContextMenuPlugin<TOption extends MenuOption>({
return () => document.removeEventListener('click', handleClick);
}, [editor, handleClick]);

return resolution === null || editor === null ? null : (
return anchorElementRef.current === null ||
resolution === null ||
editor === null ? null : (
<LexicalMenu
close={closeNodeMenu}
resolution={resolution}
Expand Down
4 changes: 3 additions & 1 deletion packages/lexical-react/src/LexicalNodeMenuPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ export function LexicalNodeMenuPlugin<TOption extends MenuOption>({
}
}, [editor, positionOrCloseMenu, nodeKey]);

return resolution === null || editor === null ? null : (
return anchorElementRef.current === null ||
resolution === null ||
editor === null ? null : (
<LexicalMenu
close={closeNodeMenu}
resolution={resolution}
Expand Down
4 changes: 3 additions & 1 deletion packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
openTypeahead,
]);

return resolution === null || editor === null ? null : (
return resolution === null ||
editor === null ||
anchorElementRef.current === null ? null : (
<LexicalMenu
close={closeTypeahead}
resolution={resolution}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {createTestEditor} from 'lexical/src/__tests__/utils';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'shared/react-test-utils';

import {useMenuAnchorRef} from '../../shared/LexicalMenu';

jest.mock('@lexical/react/LexicalComposerContext', () => ({
useLexicalComposerContext: () => [createTestEditor()],
}));

jest.mock('shared/canUseDOM', () => ({
CAN_USE_DOM: false,
}));

describe('useMenuAnchorRef', () => {
let container: HTMLDivElement | null = null;
let reactRoot: Root;

beforeEach(() => {
container = document.createElement('div');
reactRoot = createRoot(container);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should return null if CAN_USE_DOM is false', async () => {
let anchorElementRef;

function App() {
const resolution = null;
const setResolution = jest.fn();
const anchorClassName = 'some-class';
const parent = undefined;
const shouldIncludePageYOffset__EXPERIMENTAL = true;

anchorElementRef = useMenuAnchorRef(
resolution,
setResolution,
anchorClassName,
parent,
shouldIncludePageYOffset__EXPERIMENTAL,
);

return null;
}

await ReactTestUtils.act(async () => {
reactRoot.render(<App />);
});

expect(anchorElementRef!.current).toBeNull();
});
});
14 changes: 10 additions & 4 deletions packages/lexical-react/src/shared/LexicalMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
useRef,
useState,
} from 'react';
import {CAN_USE_DOM} from 'shared/canUseDOM';
import useLayoutEffect from 'shared/useLayoutEffect';

export type MenuTextMatch = {
Expand Down Expand Up @@ -267,7 +268,7 @@ export function LexicalMenu<TOption extends MenuOption>({
}: {
close: () => void;
editor: LexicalEditor;
anchorElementRef: MutableRefObject<HTMLElement>;
anchorElementRef: MutableRefObject<HTMLElement | null>;
resolution: MenuResolution;
options: Array<TOption>;
shouldSplitNodeWithQuery?: boolean;
Expand Down Expand Up @@ -481,12 +482,17 @@ export function useMenuAnchorRef(
resolution: MenuResolution | null,
setResolution: (r: MenuResolution | null) => void,
className?: string,
parent: HTMLElement = document.body,
parent: HTMLElement | undefined = CAN_USE_DOM ? document.body : undefined,
shouldIncludePageYOffset__EXPERIMENTAL: boolean = true,
): MutableRefObject<HTMLElement> {
): MutableRefObject<HTMLElement | null> {
const [editor] = useLexicalComposerContext();
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'));
const anchorElementRef = useRef<HTMLElement | null>(
CAN_USE_DOM ? document.createElement('div') : null,
);
const positionMenu = useCallback(() => {
if (anchorElementRef.current === null || parent === undefined) {
return;
}
anchorElementRef.current.style.top = anchorElementRef.current.style.bottom;
const rootElement = editor.getRootElement();
const containerDiv = anchorElementRef.current;
Expand Down

0 comments on commit 30bb9a6

Please sign in to comment.