Skip to content
Open
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
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@types/child-process-promise": "^2.2.6",
"@types/jest": "^29.5.12",
"@types/jsdom": "^21.1.6",
"@types/jsdom": "^27.0.0",
"@types/katex": "^0.16.7",
"@types/node": "^17.0.31",
"@types/prettier": "^2.7.3",
Expand Down
10 changes: 5 additions & 5 deletions packages/lexical-clipboard/src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
BaseSelection,
COMMAND_PRIORITY_CRITICAL,
COPY_COMMAND,
getDOMSelection,
getDOMSelectionForEditor,
getWindow,
isSelectionWithinEditor,
LexicalEditor,
LexicalNode,
Expand Down Expand Up @@ -450,9 +451,9 @@ export async function copyToClipboard(
}

const rootElement = editor.getRootElement();
const editorWindow = editor._window || window;
const editorWindow = getWindow(editor);
const windowDocument = editorWindow.document;
const domSelection = getDOMSelection(editorWindow);
const domSelection = getDOMSelectionForEditor(editor);
if (rootElement === null || domSelection === null) {
return false;
}
Expand Down Expand Up @@ -501,13 +502,12 @@ function $copyToClipboardEvent(
data?: LexicalClipboardData,
): boolean {
if (data === undefined) {
const domSelection = getDOMSelection(editor._window);
const domSelection = getDOMSelectionForEditor(editor);
const selection = $getSelection();

if (!selection || selection.isCollapsed()) {
return false;
}

if (!domSelection) {
return false;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import TwitterPlugin from './plugins/TwitterPlugin';
import {VersionsPlugin} from './plugins/VersionsPlugin';
import YouTubePlugin from './plugins/YouTubePlugin';
import ContentEditable from './ui/ContentEditable';
import ShadowDOMWrapper from './ui/ShadowDOMWrapper';

const COLLAB_DOC_ID = 'main';

Expand All @@ -105,6 +106,7 @@ export default function Editor(): JSX.Element {
hasNestedTables,
isCharLimitUtf8,
isRichText,
isShadowDOM,
showTreeView,
showTableOfContents,
shouldUseLexicalContextMenu,
Expand Down Expand Up @@ -170,7 +172,9 @@ export default function Editor(): JSX.Element {
setIsLinkEditMode={setIsLinkEditMode}
/>
)}
<div
<ShadowDOMWrapper
key={`shadow-${isShadowDOM}`}
enabled={isShadowDOM}
className={`editor-container ${showTreeView ? 'tree-view' : ''} ${
!isRichText ? 'plain-text' : ''
}`}>
Expand Down Expand Up @@ -304,7 +308,7 @@ export default function Editor(): JSX.Element {
shouldPreserveNewLinesInMarkdown={shouldPreserveNewLinesInMarkdown}
useCollabV2={useCollabV2}
/>
</div>
</ShadowDOMWrapper>
{showTreeView && <TreeViewPlugin />}
</>
);
Expand Down
6 changes: 6 additions & 0 deletions packages/lexical-playground/src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function Settings(): JSX.Element {
isCharLimit,
isCharLimitUtf8,
isAutocomplete,
isShadowDOM,
showTreeView,
showNestedEditorTreeView,
// disableBeforeInput,
Expand Down Expand Up @@ -148,6 +149,11 @@ export default function Settings(): JSX.Element {
checked={isAutocomplete}
text="Autocomplete"
/>
<Switch
onClick={() => setOption('isShadowDOM', !isShadowDOM)}
checked={isShadowDOM}
text="Shadow DOM"
/>
{/* <Switch
onClick={() => {
setOption('disableBeforeInput', !disableBeforeInput);
Expand Down
1 change: 1 addition & 0 deletions packages/lexical-playground/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const DEFAULT_SETTINGS = {
isCollab: false,
isMaxLength: false,
isRichText: true,
isShadowDOM: false,
listStrictIndent: false,
measureTypingPerf: false,
selectionAlwaysOnDisplay: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_NORMAL,
createCommand,
getDOMSelection,
getDOMSelectionForEditor,
KEY_ESCAPE_COMMAND,
} from 'lexical';
import {
Expand Down Expand Up @@ -933,7 +933,7 @@ export default function CommentPlugin({
editor.registerCommand(
INSERT_INLINE_COMMAND,
() => {
const domSelection = getDOMSelection(editor._window);
const domSelection = getDOMSelectionForEditor(editor);
if (domSelection !== null) {
domSelection.removeAllRanges();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_LOW,
getDOMSelection,
getDOMSelectionForEditor,
KEY_ESCAPE_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
Expand Down Expand Up @@ -104,7 +104,7 @@ function FloatingLinkEditor({
}

const editorElem = editorRef.current;
const nativeSelection = getDOMSelection(editor._window);
const nativeSelection = getDOMSelectionForEditor(editor);
const activeElement = document.activeElement;

if (editorElem === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
$isTextNode,
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
getDOMSelection,
getDOMSelectionForEditor,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
Expand Down Expand Up @@ -122,7 +122,7 @@ function TextFormatFloatingToolbar({
const selection = $getSelection();

const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
const nativeSelection = getDOMSelection(editor._window);
const nativeSelection = getDOMSelectionForEditor(editor);

if (popupCharStylesEditorElem === null) {
return;
Expand Down Expand Up @@ -342,7 +342,7 @@ function useFloatingTextFormatToolbar(
return;
}
const selection = $getSelection();
const nativeSelection = getDOMSelection(editor._window);
const nativeSelection = getDOMSelectionForEditor(editor);
const rootElement = editor.getRootElement();

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
$isTextNode,
$setSelection,
COMMAND_PRIORITY_CRITICAL,
getDOMSelection,
getDOMSelectionForEditor,
isDOMNode,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
Expand Down Expand Up @@ -729,7 +729,7 @@ function TableCellActionMenuContainer({
const $moveMenu = useCallback(() => {
const menu = menuButtonRef.current;
const selection = $getSelection();
const nativeSelection = getDOMSelection(editor._window);
const nativeSelection = getDOMSelectionForEditor(editor);
const activeElement = document.activeElement;
function disable() {
if (menu) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
$createParagraphNode,
$createTextNode,
$getRoot,
getDOMSelection,
getDOMSelectionForEditor,
} from 'lexical';
import * as React from 'react';
import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
Expand Down Expand Up @@ -172,7 +172,7 @@ function useTestRecorder(

const generateTestContent = useCallback(() => {
const rootElement = editor.getRootElement();
const browserSelection = getDOMSelection(editor._window);
const browserSelection = getDOMSelectionForEditor(editor);

if (
rootElement == null ||
Expand Down Expand Up @@ -327,7 +327,7 @@ ${steps.map(formatStep).join(`\n`)}
dirtyElements.size === 0 &&
!skipNextSelectionChange
) {
const browserSelection = getDOMSelection(editor._window);
const browserSelection = getDOMSelectionForEditor(editor);
if (
browserSelection &&
(browserSelection.anchorNode == null ||
Expand Down Expand Up @@ -384,7 +384,7 @@ ${steps.map(formatStep).join(`\n`)}
if (!isRecording) {
return;
}
const browserSelection = getDOMSelection(getCurrentEditor()._window);
const browserSelection = getDOMSelectionForEditor(getCurrentEditor());
if (
browserSelection === null ||
browserSelection.anchorNode == null ||
Expand Down
107 changes: 107 additions & 0 deletions packages/lexical-playground/src/ui/ShadowDOMWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* 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 type {JSX, ReactNode} from 'react';

import {useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';

type ShadowDOMWrapperProps = {
children: ReactNode;
enabled: boolean;
className?: string;
};

export default function ShadowDOMWrapper({
children,
enabled,
className,
}: ShadowDOMWrapperProps): JSX.Element {
const hostRef = useRef<HTMLDivElement>(null);
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
const [stylesAdded, setStylesAdded] = useState(false);

useEffect(() => {
if (!enabled || !hostRef.current) {
setShadowRoot(null);
setStylesAdded(false);
return;
}

const host = hostRef.current;

// Create shadow DOM (should be safe with fresh element due to key prop)
try {
const shadow = host.attachShadow({mode: 'open'});
setShadowRoot(shadow);
} catch (error) {
// If shadow already exists, use existing one
if (error instanceof DOMException && error.name === 'NotSupportedError') {
const existingShadow = host.shadowRoot;
if (existingShadow) {
setShadowRoot(existingShadow);
// Clear existing content
existingShadow.innerHTML = '';
}
} else {
console.error('Error creating shadow DOM:', error);
return;
}
}

const shadow = host.shadowRoot;
if (!shadow) {
return;
}

// Copy all document styles to shadow DOM
const documentStyles = Array.from(
document.head.querySelectorAll('style, link[rel="stylesheet"]'),
);

documentStyles.forEach((styleElement) => {
const clonedStyle = styleElement.cloneNode(true) as HTMLElement;
shadow.appendChild(clonedStyle);
});

setStylesAdded(true);

return () => {
// Cleanup is automatic when host element is removed
};
}, [enabled]);

// If shadow DOM is not enabled, render children normally
if (!enabled) {
return <div className={className}>{children}</div>;
}

// Return the host element and portal to shadow DOM
return (
<div style={{position: 'relative'}}>
<div
ref={hostRef}
className={className}
style={{
position: 'relative',
}}
/>
{shadowRoot &&
stylesAdded &&
createPortal(
<div
style={{
display: 'contents', // This ensures the children render properly
}}>
{children}
</div>,
shadowRoot,
)}
</div>
);
}
Loading