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
2 changes: 2 additions & 0 deletions packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
import TableCellResizer from './plugins/TableCellResizer';
import TableHoverActionsPlugin from './plugins/TableHoverActionsPlugin';
import TableOfContentsPlugin from './plugins/TableOfContentsPlugin';
import TableScrollShadowPlugin from './plugins/TableScrollShadowPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';
import TwitterPlugin from './plugins/TwitterPlugin';
Expand Down Expand Up @@ -241,6 +242,7 @@ export default function Editor(): JSX.Element {
hasNestedTables={hasNestedTables}
/>
<TableCellResizer />
<TableScrollShadowPlugin />
<ImagesPlugin />
<LinkPlugin hasLinkAttributes={hasLinkAttributes} />
<PollPlugin />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* 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 {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useEffect} from 'react';

const SCROLLABLE_WRAPPER_CLASS =
'PlaygroundEditorTheme__tableScrollableWrapper';
const HAS_SCROLL_RIGHT_CLASS = 'PlaygroundEditorTheme__tableScrollRight';
const HAS_SCROLL_LEFT_CLASS = 'PlaygroundEditorTheme__tableScrollLeft';
const HAS_SCROLL_MIDDLE_CLASS = 'PlaygroundEditorTheme__tableScrollMiddle';

function updateTableScrollState(element: HTMLElement): void {
const hasScroll = element.scrollWidth > element.clientWidth;
// Adding and removing 1 and -1 for floating point precision
const isScrolledToRight =
element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
const isScrolledToLeft = element.scrollLeft <= 1;

// Remove all scroll classes first
element.classList.remove(HAS_SCROLL_RIGHT_CLASS);
element.classList.remove(HAS_SCROLL_LEFT_CLASS);
element.classList.remove(HAS_SCROLL_MIDDLE_CLASS);

if (hasScroll) {
// Middle state: not at either edge
if (!isScrolledToLeft && !isScrolledToRight) {
element.classList.add(HAS_SCROLL_MIDDLE_CLASS);
}
// Right edge
else if (isScrolledToLeft && !isScrolledToRight) {
element.classList.add(HAS_SCROLL_RIGHT_CLASS);
}
// Left edge
else if (!isScrolledToLeft && isScrolledToRight) {
element.classList.add(HAS_SCROLL_LEFT_CLASS);
}
}
}

export default function TableScrollShadowPlugin(): null {
const [editor] = useLexicalComposerContext();

useEffect(() => {
const editorElement = editor.getRootElement();
if (!editorElement) {
return;
}

const updateAllTableScrollStates = () => {
const wrappers = editorElement.querySelectorAll<HTMLElement>(
`.${SCROLLABLE_WRAPPER_CLASS}`,
);
wrappers.forEach(updateTableScrollState);
};

// Initial check
updateAllTableScrollStates();

// Watch for new table wrappers being added
const observer = new MutationObserver(() => {
updateAllTableScrollStates();
});

observer.observe(editorElement, {
childList: true,
subtree: true,
});

// Also watch for resize events on the wrappers themselves
const resizeObserver = new ResizeObserver(() => {
updateAllTableScrollStates();
});

const scrollHandlers = new Map<HTMLElement, () => void>();

const addScrollListener = (wrapper: HTMLElement) => {
if (scrollHandlers.has(wrapper)) {
return; // Already has a listener
}
const handler = () => {
updateTableScrollState(wrapper);
};
wrapper.addEventListener('scroll', handler, {passive: true});
scrollHandlers.set(wrapper, handler);
};

const wrappers = editorElement.querySelectorAll<HTMLElement>(
`.${SCROLLABLE_WRAPPER_CLASS}`,
);
wrappers.forEach((wrapper) => {
resizeObserver.observe(wrapper);
addScrollListener(wrapper);
});

// Watch for new wrappers to observe
const wrapperObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement) {
if (node.classList.contains(SCROLLABLE_WRAPPER_CLASS)) {
resizeObserver.observe(node);
addScrollListener(node);
updateTableScrollState(node);
}

const childWrappers = node.querySelectorAll<HTMLElement>(
`.${SCROLLABLE_WRAPPER_CLASS}`,
);
childWrappers.forEach((wrapper) => {
resizeObserver.observe(wrapper);
addScrollListener(wrapper);
updateTableScrollState(wrapper);
});
}
});
});
});

wrapperObserver.observe(editorElement, {
childList: true,
subtree: true,
});

return () => {
observer.disconnect();
resizeObserver.disconnect();
wrapperObserver.disconnect();

scrollHandlers.forEach((handler, wrapper) => {
wrapper.removeEventListener('scroll', handler);
});
scrollHandlers.clear();
};
}, [editor]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@
overflow-x: auto;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableScrollRight {
box-shadow: inset -10px 0 10px rgba(0, 0, 0, 0.15);
}
.PlaygroundEditorTheme__tableScrollLeft {
box-shadow: inset 10px 0 10px rgba(0, 0, 0, 0.15);
}
.PlaygroundEditorTheme__tableScrollMiddle {
box-shadow:
inset 10px 0 10px rgba(0, 0, 0, 0.15),
inset -10px 0 10px rgba(0, 0, 0, 0.15);
}
.PlaygroundEditorTheme__tableScrollableWrapper > .PlaygroundEditorTheme__table {
/* Remove the table's vertical margin and put it on the wrapper */
margin-top: 0;
Expand Down
Loading