perf: 编辑器: 表格宽度调整

2.0/email-builder
Lei OT 10 months ago
parent 7b0309aa07
commit dc60dab969

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus"><path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/></svg>

After

Width:  |  Height:  |  Size: 223 B

@ -38,12 +38,12 @@ import { ExtendedTextNode } from './nodes/ExtendedTextNode';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
// import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
// import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
// import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
// import TableCellResizer from './plugins/TableCellResizer';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
import TableCellResizer from './plugins/TableCellResizer';
// import TableHoverActionsPlugin from './plugins/TableHoverActionsPlugin';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
// import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import { TextNode, $getRoot, $getSelection, $createParagraphNode } from 'lexical';
@ -190,13 +190,13 @@ export default function Editor({ isRichText, isDebug, editorRef, onChange, defau
<AutoLinkPlugin />
<LinkPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
{/* <TablePlugin hasCellMerge={tableCellMerge} hasCellBackgroundColor={tableCellBackgroundColor} /> */}
{/* <TableCellResizer /> */}
<TablePlugin hasCellMerge={tableCellMerge} hasCellBackgroundColor={tableCellBackgroundColor} />
<TableCellResizer />
{/* <TableHoverActionsPlugin /> */}
{/* <TableCellActionMenuPlugin
<TableCellActionMenuPlugin
// anchorElem={floatingAnchorElem}
cellMerge={true}
/> */}
/>
<TabFocusPlugin />
<TabIndentationPlugin />
<HorizontalRulePlugin />

@ -0,0 +1,773 @@
/**
* 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 {ElementNode, LexicalEditor} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$getNodeTriplet,
$getTableCellNodeFromLexicalNode,
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableCellNode,
$isTableRowNode,
$isTableSelection,
$unmergeCell,
getTableObserverFromTableElement,
HTMLTableElementWithWithTableSelectionState,
TableCellHeaderStates,
TableCellNode,
TableRowNode,
TableSelection,
} from '@lexical/table';
import {
$createParagraphNode,
$getRoot,
$getSelection,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
$isTextNode,
} from 'lexical';
import * as React from 'react';
import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import useModal from '../../hooks/useModal';
import ColorPicker from '../../ui/ColorPicker';
function computeSelectionCount(selection: TableSelection): {
columns: number;
rows: number;
} {
const selectionShape = selection.getShape();
return {
columns: selectionShape.toX - selectionShape.fromX + 1,
rows: selectionShape.toY - selectionShape.fromY + 1,
};
}
function $canUnmerge(): boolean {
const selection = $getSelection();
if (
($isRangeSelection(selection) && !selection.isCollapsed()) ||
($isTableSelection(selection) && !selection.anchor.is(selection.focus)) ||
(!$isRangeSelection(selection) && !$isTableSelection(selection))
) {
return false;
}
const [cell] = $getNodeTriplet(selection.anchor);
return cell.__colSpan > 1 || cell.__rowSpan > 1;
}
function $cellContainsEmptyParagraph(cell: TableCellNode): boolean {
if (cell.getChildrenSize() !== 1) {
return false;
}
const firstChild = cell.getFirstChildOrThrow();
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
return false;
}
return true;
}
function $selectLastDescendant(node: ElementNode): void {
const lastDescendant = node.getLastDescendant();
if ($isTextNode(lastDescendant)) {
lastDescendant.select();
} else if ($isElementNode(lastDescendant)) {
lastDescendant.selectEnd();
} else if (lastDescendant !== null) {
lastDescendant.selectNext();
}
}
function currentCellBackgroundColor(editor: LexicalEditor): null | string {
return editor.getEditorState().read(() => {
const selection = $getSelection();
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
const [cell] = $getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
return cell.getBackgroundColor();
}
}
return null;
});
}
type TableCellActionMenuProps = Readonly<{
contextRef: {current: null | HTMLElement};
onClose: () => void;
setIsMenuOpen: (isOpen: boolean) => void;
showColorPickerModal: (
title: string,
showModal: (onClose: () => void) => JSX.Element,
) => void;
tableCellNode: TableCellNode;
cellMerge: boolean;
}>;
function TableActionMenu({
onClose,
tableCellNode: _tableCellNode,
setIsMenuOpen,
contextRef,
cellMerge,
showColorPickerModal,
}: TableCellActionMenuProps) {
const [editor] = useLexicalComposerContext();
const dropDownRef = useRef<HTMLDivElement | null>(null);
const [tableCellNode, updateTableCellNode] = useState(_tableCellNode);
const [selectionCounts, updateSelectionCounts] = useState({
columns: 1,
rows: 1,
});
const [canMergeCells, setCanMergeCells] = useState(false);
const [canUnmergeCell, setCanUnmergeCell] = useState(false);
const [backgroundColor, setBackgroundColor] = useState(
() => currentCellBackgroundColor(editor) || '',
);
useEffect(() => {
return editor.registerMutationListener(
TableCellNode,
(nodeMutations) => {
const nodeUpdated =
nodeMutations.get(tableCellNode.getKey()) === 'updated';
if (nodeUpdated) {
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest());
});
setBackgroundColor(currentCellBackgroundColor(editor) || '');
}
},
{skipInitialization: true},
);
}, [editor, tableCellNode]);
useEffect(() => {
editor.getEditorState().read(() => {
const selection = $getSelection();
// Merge cells
if ($isTableSelection(selection)) {
const currentSelectionCounts = computeSelectionCount(selection);
updateSelectionCounts(computeSelectionCount(selection));
setCanMergeCells(
currentSelectionCounts.columns > 1 || currentSelectionCounts.rows > 1,
);
}
// Unmerge cell
setCanUnmergeCell($canUnmerge());
});
}, [editor]);
useEffect(() => {
const menuButtonElement = contextRef.current;
const dropDownElement = dropDownRef.current;
const rootElement = editor.getRootElement();
if (
menuButtonElement != null &&
dropDownElement != null &&
rootElement != null
) {
const rootEleRect = rootElement.getBoundingClientRect();
const menuButtonRect = menuButtonElement.getBoundingClientRect();
dropDownElement.style.opacity = '1';
const dropDownElementRect = dropDownElement.getBoundingClientRect();
const margin = 5;
let leftPosition = menuButtonRect.right + margin;
if (
leftPosition + dropDownElementRect.width > window.innerWidth ||
leftPosition + dropDownElementRect.width > rootEleRect.right
) {
const position =
menuButtonRect.left - dropDownElementRect.width - margin;
leftPosition = (position < 0 ? margin : position) + window.pageXOffset;
}
dropDownElement.style.left = `${leftPosition + window.pageXOffset}px`;
let topPosition = menuButtonRect.top;
if (topPosition + dropDownElementRect.height > window.innerHeight) {
const position = menuButtonRect.bottom - dropDownElementRect.height;
topPosition = (position < 0 ? margin : position) + window.pageYOffset;
}
dropDownElement.style.top = `${topPosition + +window.pageYOffset}px`;
}
}, [contextRef, dropDownRef, editor]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropDownRef.current != null &&
contextRef.current != null &&
!dropDownRef.current.contains(event.target as Node) &&
!contextRef.current.contains(event.target as Node)
) {
setIsMenuOpen(false);
}
}
window.addEventListener('click', handleClickOutside);
return () => window.removeEventListener('click', handleClickOutside);
}, [setIsMenuOpen, contextRef]);
const clearTableSelection = useCallback(() => {
editor.update(() => {
if (tableCellNode.isAttached()) {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableElement = editor.getElementByKey(
tableNode.getKey(),
) as HTMLTableElementWithWithTableSelectionState;
if (!tableElement) {
throw new Error('Expected to find tableElement in DOM');
}
const tableObserver = getTableObserverFromTableElement(tableElement);
if (tableObserver !== null) {
tableObserver.clearHighlight();
}
tableNode.markDirty();
updateTableCellNode(tableCellNode.getLatest());
}
const rootNode = $getRoot();
rootNode.selectStart();
});
}, [editor, tableCellNode]);
const mergeTableCellsAtSelection = () => {
editor.update(() => {
const selection = $getSelection();
if ($isTableSelection(selection)) {
const {columns, rows} = computeSelectionCount(selection);
const nodes = selection.getNodes();
let firstCell: null | TableCellNode = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isTableCellNode(node)) {
if (firstCell === null) {
node.setColSpan(columns).setRowSpan(rows);
firstCell = node;
const isEmpty = $cellContainsEmptyParagraph(node);
let firstChild;
if (
isEmpty &&
$isParagraphNode((firstChild = node.getFirstChild()))
) {
firstChild.remove();
}
} else if ($isTableCellNode(firstCell)) {
const isEmpty = $cellContainsEmptyParagraph(node);
if (!isEmpty) {
firstCell.append(...node.getChildren());
}
node.remove();
}
}
}
if (firstCell !== null) {
if (firstCell.getChildrenSize() === 0) {
firstCell.append($createParagraphNode());
}
$selectLastDescendant(firstCell);
}
onClose();
}
});
};
const unmergeTableCellsAtSelection = () => {
editor.update(() => {
$unmergeCell();
});
};
const insertTableRowAtSelection = useCallback(
(shouldInsertAfter: boolean) => {
editor.update(() => {
$insertTableRow__EXPERIMENTAL(shouldInsertAfter);
onClose();
});
},
[editor, onClose],
);
const insertTableColumnAtSelection = useCallback(
(shouldInsertAfter: boolean) => {
editor.update(() => {
for (let i = 0; i < selectionCounts.columns; i++) {
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter);
}
onClose();
});
},
[editor, onClose, selectionCounts.columns],
);
const deleteTableRowAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableRow__EXPERIMENTAL();
onClose();
});
}, [editor, onClose]);
const deleteTableAtSelection = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
tableNode.remove();
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const deleteTableColumnAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableColumn__EXPERIMENTAL();
onClose();
});
}, [editor, onClose]);
const toggleTableRowIsHeader = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren();
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableRow = tableRows[tableRowIndex];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
const newStyle =
tableCellNode.getHeaderStyles() ^ TableCellHeaderStates.ROW;
tableRow.getChildren().forEach((tableCell) => {
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell');
}
tableCell.setHeaderStyles(newStyle, TableCellHeaderStates.ROW);
});
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const toggleTableColumnIsHeader = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableColumnIndex =
$getTableColumnIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren<TableRowNode>();
const maxRowsLength = Math.max(
...tableRows.map((row) => row.getChildren().length),
);
if (tableColumnIndex >= maxRowsLength || tableColumnIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const newStyle =
tableCellNode.getHeaderStyles() ^ TableCellHeaderStates.COLUMN;
for (let r = 0; r < tableRows.length; r++) {
const tableRow = tableRows[r];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
const tableCells = tableRow.getChildren();
if (tableColumnIndex >= tableCells.length) {
// if cell is outside of bounds for the current row (for example various merge cell cases) we shouldn't highlight it
continue;
}
const tableCell = tableCells[tableColumnIndex];
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell');
}
tableCell.setHeaderStyles(newStyle, TableCellHeaderStates.COLUMN);
}
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const toggleRowStriping = useCallback(() => {
editor.update(() => {
if (tableCellNode.isAttached()) {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
if (tableNode) {
tableNode.setRowStriping(!tableNode.getRowStriping());
}
}
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const handleCellBackgroundColor = useCallback(
(value: string) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
const [cell] = $getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
cell.setBackgroundColor(value);
}
if ($isTableSelection(selection)) {
const nodes = selection.getNodes();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isTableCellNode(node)) {
node.setBackgroundColor(value);
}
}
}
}
});
},
[editor],
);
let mergeCellButton: null | JSX.Element = null;
if (cellMerge) {
if (canMergeCells) {
mergeCellButton = (
<button
type="button"
className="item"
onClick={() => mergeTableCellsAtSelection()}
data-test-id="table-merge-cells">
Merge cells
</button>
);
} else if (canUnmergeCell) {
mergeCellButton = (
<button
type="button"
className="item"
onClick={() => unmergeTableCellsAtSelection()}
data-test-id="table-unmerge-cells">
Unmerge cells
</button>
);
}
}
return createPortal(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="dropdown"
ref={dropDownRef}
onClick={(e) => {
e.stopPropagation();
}}>
{mergeCellButton}
<button
type="button"
className="item"
onClick={() =>
showColorPickerModal('Cell background color', () => (
<ColorPicker
color={backgroundColor}
onChange={handleCellBackgroundColor}
/>
))
}
data-test-id="table-background-color">
<span className="text">Background color</span>
</button>
<button
type="button"
className="item"
onClick={() => toggleRowStriping()}
data-test-id="table-row-striping">
<span className="text">Toggle Row Striping</span>
</button>
<hr />
<button
type="button"
className="item"
onClick={() => insertTableRowAtSelection(false)}
data-test-id="table-insert-row-above">
<span className="text">
Insert{' '}
{selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`}{' '}
above
</span>
</button>
<button
type="button"
className="item"
onClick={() => insertTableRowAtSelection(true)}
data-test-id="table-insert-row-below">
<span className="text">
Insert{' '}
{selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`}{' '}
below
</span>
</button>
<hr />
<button
type="button"
className="item"
onClick={() => insertTableColumnAtSelection(false)}
data-test-id="table-insert-column-before">
<span className="text">
Insert{' '}
{selectionCounts.columns === 1
? 'column'
: `${selectionCounts.columns} columns`}{' '}
left
</span>
</button>
<button
type="button"
className="item"
onClick={() => insertTableColumnAtSelection(true)}
data-test-id="table-insert-column-after">
<span className="text">
Insert{' '}
{selectionCounts.columns === 1
? 'column'
: `${selectionCounts.columns} columns`}{' '}
right
</span>
</button>
<hr />
<button
type="button"
className="item"
onClick={() => deleteTableColumnAtSelection()}
data-test-id="table-delete-columns">
<span className="text">Delete column</span>
</button>
<button
type="button"
className="item"
onClick={() => deleteTableRowAtSelection()}
data-test-id="table-delete-rows">
<span className="text">Delete row</span>
</button>
<button
type="button"
className="item"
onClick={() => deleteTableAtSelection()}
data-test-id="table-delete">
<span className="text">Delete table</span>
</button>
<hr />
<button
type="button"
className="item"
onClick={() => toggleTableRowIsHeader()}>
<span className="text">
{(tableCellNode.__headerState & TableCellHeaderStates.ROW) ===
TableCellHeaderStates.ROW
? 'Remove'
: 'Add'}{' '}
row header
</span>
</button>
<button
type="button"
className="item"
onClick={() => toggleTableColumnIsHeader()}
data-test-id="table-column-header">
<span className="text">
{(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) ===
TableCellHeaderStates.COLUMN
? 'Remove'
: 'Add'}{' '}
column header
</span>
</button>
</div>,
document.body,
);
}
function TableCellActionMenuContainer({
anchorElem,
cellMerge,
}: {
anchorElem: HTMLElement;
cellMerge: boolean;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const menuButtonRef = useRef(null);
const menuRootRef = useRef(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [tableCellNode, setTableMenuCellNode] = useState<TableCellNode | null>(
null,
);
const [colorPickerModal, showColorPickerModal] = useModal();
const $moveMenu = useCallback(() => {
const menu = menuButtonRef.current;
const selection = $getSelection();
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (selection == null || menu == null) {
setTableMenuCellNode(null);
return;
}
const rootElement = editor.getRootElement();
if (
$isRangeSelection(selection) &&
rootElement !== null &&
nativeSelection !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode(
selection.anchor.getNode(),
);
if (tableCellNodeFromSelection == null) {
setTableMenuCellNode(null);
return;
}
const tableCellParentNodeDOM = editor.getElementByKey(
tableCellNodeFromSelection.getKey(),
);
if (tableCellParentNodeDOM == null) {
setTableMenuCellNode(null);
return;
}
setTableMenuCellNode(tableCellNodeFromSelection);
} else if (!activeElement) {
setTableMenuCellNode(null);
}
}, [editor]);
useEffect(() => {
return editor.registerUpdateListener(() => {
editor.getEditorState().read(() => {
$moveMenu();
});
});
});
useEffect(() => {
const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null;
if (menuButtonDOM != null && tableCellNode != null) {
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey());
if (tableCellNodeDOM != null) {
const tableCellRect = tableCellNodeDOM.getBoundingClientRect();
const menuRect = menuButtonDOM.getBoundingClientRect();
const anchorRect = anchorElem.getBoundingClientRect();
const top = tableCellRect.top - anchorRect.top + 4;
const left =
tableCellRect.right - menuRect.width - 10 - anchorRect.left;
menuButtonDOM.style.opacity = '1';
menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`;
} else {
menuButtonDOM.style.opacity = '0';
menuButtonDOM.style.transform = 'translate(-10000px, -10000px)';
}
}
}, [menuButtonRef, tableCellNode, editor, anchorElem]);
const prevTableCellDOM = useRef(tableCellNode);
useEffect(() => {
if (prevTableCellDOM.current !== tableCellNode) {
setIsMenuOpen(false);
}
prevTableCellDOM.current = tableCellNode;
}, [prevTableCellDOM, tableCellNode]);
return (
<div className="table-cell-action-button-container" ref={menuButtonRef}>
{tableCellNode != null && (
<>
<button
type="button"
className="table-cell-action-button chevron-down"
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(!isMenuOpen);
}}
ref={menuRootRef}>
<i className="chevron-down" />
</button>
{colorPickerModal}
{isMenuOpen && (
<TableActionMenu
contextRef={menuRootRef}
setIsMenuOpen={setIsMenuOpen}
onClose={() => setIsMenuOpen(false)}
tableCellNode={tableCellNode}
cellMerge={cellMerge}
showColorPickerModal={showColorPickerModal}
/>
)}
</>
)}
</div>
);
}
export default function TableActionMenuPlugin({
anchorElem = document.body,
cellMerge = false,
}: {
anchorElem?: HTMLElement;
cellMerge?: boolean;
}): null | ReactPortal {
const isEditable = useLexicalEditable();
return createPortal(
isEditable ? (
<TableCellActionMenuContainer
anchorElem={anchorElem}
cellMerge={cellMerge}
/>
) : null,
anchorElem,
);
}

@ -0,0 +1,13 @@
/**
* 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.
*
*
*/
.TableCellResizer__resizer {
position: absolute;
z-index: 1202;
}

@ -0,0 +1,439 @@
/**
* 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 {TableCellNode, TableDOMCell, TableMapType} from '@lexical/table';
import type {LexicalEditor} from 'lexical';
import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
$computeTableMapSkipCellCheck,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$isTableCellNode,
$isTableRowNode,
getDOMCellFromTarget,
TableNode,
} from '@lexical/table';
import {calculateZoomLevel} from '@lexical/utils';
import {$getNearestNodeFromDOMNode} from 'lexical';
import * as React from 'react';
import {
MouseEventHandler,
ReactPortal,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {createPortal} from 'react-dom';
type MousePosition = {
x: number;
y: number;
};
type MouseDraggingDirection = 'right' | 'bottom';
const MIN_ROW_HEIGHT = 33;
const MIN_COLUMN_WIDTH = 92;
function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
const targetRef = useRef<HTMLElement | null>(null);
const resizerRef = useRef<HTMLDivElement | null>(null);
const tableRectRef = useRef<ClientRect | null>(null);
const mouseStartPosRef = useRef<MousePosition | null>(null);
const [mouseCurrentPos, updateMouseCurrentPos] =
useState<MousePosition | null>(null);
const [activeCell, updateActiveCell] = useState<TableDOMCell | null>(null);
const [isMouseDown, updateIsMouseDown] = useState<boolean>(false);
const [draggingDirection, updateDraggingDirection] =
useState<MouseDraggingDirection | null>(null);
const resetState = useCallback(() => {
updateActiveCell(null);
targetRef.current = null;
updateDraggingDirection(null);
mouseStartPosRef.current = null;
tableRectRef.current = null;
}, []);
const isMouseDownOnEvent = (event: MouseEvent) => {
return (event.buttons & 1) === 1;
};
useEffect(() => {
return editor.registerNodeTransform(TableNode, (tableNode) => {
console.dir(TableNode);
console.dir(tableNode);
if (tableNode.getColWidths()) {
return tableNode;
}
const numColumns = tableNode.getColumnCount();
const columnWidth = MIN_COLUMN_WIDTH;
tableNode.setColWidths(Array(numColumns).fill(columnWidth));
return tableNode;
});
}, [editor]);
useEffect(() => {
const onMouseMove = (event: MouseEvent) => {
setTimeout(() => {
const target = event.target;
if (draggingDirection) {
updateMouseCurrentPos({
x: event.clientX,
y: event.clientY,
});
return;
}
updateIsMouseDown(isMouseDownOnEvent(event));
if (resizerRef.current && resizerRef.current.contains(target as Node)) {
return;
}
if (targetRef.current !== target) {
targetRef.current = target as HTMLElement;
const cell = getDOMCellFromTarget(target as HTMLElement);
if (cell && activeCell !== cell) {
editor.update(() => {
const tableCellNode = $getNearestNodeFromDOMNode(cell.elem);
if (!tableCellNode) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode =
$getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableElement = editor.getElementByKey(tableNode.getKey());
if (!tableElement) {
throw new Error('TableCellResizer: Table element not found.');
}
targetRef.current = target as HTMLElement;
tableRectRef.current = tableElement.getBoundingClientRect();
updateActiveCell(cell);
});
} else if (cell == null) {
resetState();
}
}
}, 0);
};
const onMouseDown = (event: MouseEvent) => {
setTimeout(() => {
updateIsMouseDown(true);
}, 0);
};
const onMouseUp = (event: MouseEvent) => {
setTimeout(() => {
updateIsMouseDown(false);
}, 0);
};
const removeRootListener = editor.registerRootListener(
(rootElement, prevRootElement) => {
prevRootElement?.removeEventListener('mousemove', onMouseMove);
prevRootElement?.removeEventListener('mousedown', onMouseDown);
prevRootElement?.removeEventListener('mouseup', onMouseUp);
rootElement?.addEventListener('mousemove', onMouseMove);
rootElement?.addEventListener('mousedown', onMouseDown);
rootElement?.addEventListener('mouseup', onMouseUp);
},
);
return () => {
removeRootListener();
};
}, [activeCell, draggingDirection, editor, resetState]);
const isHeightChanging = (direction: MouseDraggingDirection) => {
if (direction === 'bottom') {
return true;
}
return false;
};
const updateRowHeight = useCallback(
(heightChange: number) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
editor.update(
() => {
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
if (!$isTableCellNode(tableCellNode)) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableRowIndex =
$getTableRowIndexFromTableCellNode(tableCellNode) +
tableCellNode.getRowSpan() -
1;
const tableRows = tableNode.getChildren();
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableRow = tableRows[tableRowIndex];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
let height = tableRow.getHeight();
if (height === undefined) {
const rowCells = tableRow.getChildren<TableCellNode>();
height = Math.min(
...rowCells.map(
(cell) => getCellNodeHeight(cell, editor) ?? Infinity,
),
);
}
const newHeight = Math.max(height + heightChange, MIN_ROW_HEIGHT);
tableRow.setHeight(newHeight);
},
{tag: 'skip-scroll-into-view'},
);
},
[activeCell, editor],
);
const getCellNodeHeight = (
cell: TableCellNode,
activeEditor: LexicalEditor,
): number | undefined => {
const domCellNode = activeEditor.getElementByKey(cell.getKey());
return domCellNode?.clientHeight;
};
const getCellColumnIndex = (
tableCellNode: TableCellNode,
tableMap: TableMapType,
) => {
for (let row = 0; row < tableMap.length; row++) {
for (let column = 0; column < tableMap[row].length; column++) {
if (tableMap[row][column].cell === tableCellNode) {
return column;
}
}
}
};
const updateColumnWidth = useCallback(
(widthChange: number) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
editor.update(
() => {
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
if (!$isTableCellNode(tableCellNode)) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const [tableMap] = $computeTableMapSkipCellCheck(
tableNode,
null,
null,
);
const columnIndex = getCellColumnIndex(tableCellNode, tableMap);
if (columnIndex === undefined) {
throw new Error('TableCellResizer: Table column not found.');
}
const colWidths = tableNode.getColWidths();
if (!colWidths) {
return;
}
const width = colWidths[columnIndex];
if (width === undefined) {
return;
}
const newColWidths = [...colWidths];
const newWidth = Math.max(width + widthChange, MIN_COLUMN_WIDTH);
newColWidths[columnIndex] = newWidth;
tableNode.setColWidths(newColWidths);
},
{tag: 'skip-scroll-into-view'},
);
},
[activeCell, editor],
);
const mouseUpHandler = useCallback(
(direction: MouseDraggingDirection) => {
const handler = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
if (mouseStartPosRef.current) {
const {x, y} = mouseStartPosRef.current;
if (activeCell === null) {
return;
}
const zoom = calculateZoomLevel(event.target as Element);
if (isHeightChanging(direction)) {
const heightChange = (event.clientY - y) / zoom;
updateRowHeight(heightChange);
} else {
const widthChange = (event.clientX - x) / zoom;
updateColumnWidth(widthChange);
}
resetState();
document.removeEventListener('mouseup', handler);
}
};
return handler;
},
[activeCell, resetState, updateColumnWidth, updateRowHeight],
);
const toggleResize = useCallback(
(direction: MouseDraggingDirection): MouseEventHandler<HTMLDivElement> =>
(event) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
mouseStartPosRef.current = {
x: event.clientX,
y: event.clientY,
};
updateMouseCurrentPos(mouseStartPosRef.current);
updateDraggingDirection(direction);
document.addEventListener('mouseup', mouseUpHandler(direction));
},
[activeCell, mouseUpHandler],
);
const getResizers = useCallback(() => {
if (activeCell) {
const {height, width, top, left} =
activeCell.elem.getBoundingClientRect();
const zoom = calculateZoomLevel(activeCell.elem);
const zoneWidth = 10; // Pixel width of the zone where you can drag the edge
const styles = {
bottom: {
backgroundColor: 'none',
cursor: 'row-resize',
height: `${zoneWidth}px`,
left: `${window.pageXOffset + left}px`,
top: `${window.pageYOffset + top + height - zoneWidth / 2}px`,
width: `${width}px`,
},
right: {
backgroundColor: 'none',
cursor: 'col-resize',
height: `${height}px`,
left: `${window.pageXOffset + left + width - zoneWidth / 2}px`,
top: `${window.pageYOffset + top}px`,
width: `${zoneWidth}px`,
},
};
const tableRect = tableRectRef.current;
if (draggingDirection && mouseCurrentPos && tableRect) {
if (isHeightChanging(draggingDirection)) {
styles[draggingDirection].left = `${
window.pageXOffset + tableRect.left
}px`;
styles[draggingDirection].top = `${
window.pageYOffset + mouseCurrentPos.y / zoom
}px`;
styles[draggingDirection].height = '3px';
styles[draggingDirection].width = `${tableRect.width}px`;
} else {
styles[draggingDirection].top = `${
window.pageYOffset + tableRect.top
}px`;
styles[draggingDirection].left = `${
window.pageXOffset + mouseCurrentPos.x / zoom
}px`;
styles[draggingDirection].width = '3px';
styles[draggingDirection].height = `${tableRect.height}px`;
}
styles[draggingDirection].backgroundColor = '#adf';
}
return styles;
}
return {
bottom: null,
left: null,
right: null,
top: null,
};
}, [activeCell, draggingDirection, mouseCurrentPos]);
const resizerStyles = getResizers();
return (
<div ref={resizerRef}>
{activeCell != null && !isMouseDown && (
<>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.right || undefined}
onMouseDown={toggleResize('right')}
/>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.bottom || undefined}
onMouseDown={toggleResize('bottom')}
/>
</>
)}
</div>
);
}
export default function TableCellResizerPlugin(): null | ReactPortal {
const [editor] = useLexicalComposerContext();
const isEditable = useLexicalEditable();
return useMemo(
() =>
isEditable
? createPortal(<TableCellResizer editor={editor} />, document.body)
: null,
[editor, isEditable],
);
}

@ -1097,6 +1097,7 @@ color: #000;
overflow: hidden;
}
.PlaygroundEditorTheme__table,
.editor-table {
border-collapse: collapse;
border-spacing: 0;
@ -1107,6 +1108,7 @@ width: fit-content;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableCell,
.editor-tableCell {
border: 1px solid #bbb;
/* width: 75px; */
@ -1116,3 +1118,52 @@ padding: 6px 8px;
position: relative;
outline: none;
}
.table-cell-action-button-container {
position: absolute;
top: 5px;
left: 15px;
will-change: transform;
z-index: 1201;
}
.table-cell-action-button {
background-color: none;
display: flex;
justify-content: center;
align-items: center;
border: 0;
position: relative;
border-radius: 15px;
color: #222;
display: inline-block;
cursor: pointer;
}
.action-button {
background-color: #eee;
border: 0;
padding: 8px 12px;
position: relative;
margin-left: 5px;
border-radius: 15px;
color: #222;
display: inline-block;
cursor: pointer;
}
.action-button:hover {
background-color: #ddd;
color: #000;
}
.action-button-mic.active {
animation: mic-pulsate-color 3s infinite;
}
button.action-button:disabled {
opacity: 0.6;
background: #eee;
cursor: not-allowed;
}

@ -1,3 +1,6 @@
import './PlaygroundEditorTheme.css';
const exampleTheme = {
ltr: "ltr",
rtl: "rtl",
@ -65,8 +68,24 @@ const exampleTheme = {
url: "editor-tokenOperator",
variable: "editor-tokenVariable"
},
table: 'editor-table',
tableCell: 'editor-tableCell',
// table: 'editor-table',
// tableCell: 'editor-tableCell',
table: 'PlaygroundEditorTheme__table',
tableCell: 'PlaygroundEditorTheme__tableCell',
tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton',
tableCellActionButtonContainer:
'PlaygroundEditorTheme__tableCellActionButtonContainer',
tableCellEditing: 'PlaygroundEditorTheme__tableCellEditing',
tableCellHeader: 'PlaygroundEditorTheme__tableCellHeader',
tableCellPrimarySelected: 'PlaygroundEditorTheme__tableCellPrimarySelected',
tableCellResizer: 'PlaygroundEditorTheme__tableCellResizer',
tableCellSelected: 'PlaygroundEditorTheme__tableCellSelected',
tableCellSortedIndicator: 'PlaygroundEditorTheme__tableCellSortedIndicator',
tableResizeRuler: 'PlaygroundEditorTheme__tableCellResizeRuler',
tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping',
tableScrollableWrapper: 'PlaygroundEditorTheme__tableScrollableWrapper',
tableSelected: 'PlaygroundEditorTheme__tableSelected',
tableSelection: 'PlaygroundEditorTheme__tableSelection',
};
export default exampleTheme;

@ -0,0 +1,467 @@
/**
* 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.
*
*
*/
.PlaygroundEditorTheme__ltr {
text-align: left;
}
.PlaygroundEditorTheme__rtl {
text-align: right;
}
.PlaygroundEditorTheme__paragraph {
margin: 0;
position: relative;
}
.PlaygroundEditorTheme__quote {
margin: 0;
margin-left: 20px;
margin-bottom: 10px;
font-size: 15px;
color: rgb(101, 103, 107);
border-left-color: rgb(206, 208, 212);
border-left-width: 4px;
border-left-style: solid;
padding-left: 16px;
}
.PlaygroundEditorTheme__h1 {
font-size: 24px;
color: rgb(5, 5, 5);
font-weight: 400;
margin: 0;
}
.PlaygroundEditorTheme__h2 {
font-size: 15px;
color: rgb(101, 103, 107);
font-weight: 700;
margin: 0;
text-transform: uppercase;
}
.PlaygroundEditorTheme__h3 {
font-size: 12px;
margin: 0;
text-transform: uppercase;
}
.PlaygroundEditorTheme__indent {
--lexical-indent-base-value: 40px;
}
.PlaygroundEditorTheme__textBold {
font-weight: bold;
}
.PlaygroundEditorTheme__textItalic {
font-style: italic;
}
.PlaygroundEditorTheme__textUnderline {
text-decoration: underline;
}
.PlaygroundEditorTheme__textStrikethrough {
text-decoration: line-through;
}
.PlaygroundEditorTheme__textUnderlineStrikethrough {
text-decoration: underline line-through;
}
.PlaygroundEditorTheme__textSubscript {
font-size: 0.8em;
vertical-align: sub !important;
}
.PlaygroundEditorTheme__textSuperscript {
font-size: 0.8em;
vertical-align: super;
}
.PlaygroundEditorTheme__textCode {
background-color: rgb(240, 242, 245);
padding: 1px 0.25rem;
font-family: Menlo, Consolas, Monaco, monospace;
font-size: 94%;
}
.PlaygroundEditorTheme__hashtag {
background-color: rgba(88, 144, 255, 0.15);
border-bottom: 1px solid rgba(88, 144, 255, 0.3);
}
.PlaygroundEditorTheme__link {
color: rgb(33, 111, 219);
text-decoration: none;
}
.PlaygroundEditorTheme__link:hover {
text-decoration: underline;
cursor: pointer;
}
.PlaygroundEditorTheme__code {
background-color: rgb(240, 242, 245);
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
overflow-x: auto;
position: relative;
tab-size: 2;
}
.PlaygroundEditorTheme__code:before {
content: attr(data-gutter);
position: absolute;
background-color: #eee;
left: 0;
top: 0;
border-right: 1px solid #ccc;
padding: 8px;
color: #777;
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.PlaygroundEditorTheme__tableScrollableWrapper {
overflow-x: auto;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableScrollableWrapper > .PlaygroundEditorTheme__table {
/* Remove the table's margin and put it on the wrapper */
margin: 0;
}
.PlaygroundEditorTheme__table {
border-collapse: collapse;
border-spacing: 0;
overflow-y: scroll;
overflow-x: scroll;
table-layout: fixed;
width: fit-content;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableRowStriping tr:nth-child(even) {
background-color: #f2f5fb;
}
.PlaygroundEditorTheme__tableSelection *::selection {
background-color: transparent;
}
.PlaygroundEditorTheme__tableSelected {
outline: 2px solid rgb(60, 132, 244);
}
.PlaygroundEditorTheme__tableCell {
border: 1px solid #bbb;
width: 75px;
vertical-align: top;
text-align: start;
padding: 6px 8px;
position: relative;
outline: none;
}
.PlaygroundEditorTheme__tableCellSortedIndicator {
display: block;
opacity: 0.5;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: #999;
}
.PlaygroundEditorTheme__tableCellResizer {
position: absolute;
right: -4px;
height: 100%;
width: 8px;
cursor: ew-resize;
/* z-index: 10; */
top: 0;
z-index: 1202;
}
.PlaygroundEditorTheme__tableCellHeader {
background-color: #f2f3f5;
text-align: start;
}
.PlaygroundEditorTheme__tableCellSelected {
background-color: #c9dbf0;
}
.PlaygroundEditorTheme__tableCellPrimarySelected {
border: 2px solid rgb(60, 132, 244);
display: block;
height: calc(100% - 2px);
position: absolute;
width: calc(100% - 2px);
left: -1px;
top: -1px;
z-index: 2;
}
.PlaygroundEditorTheme__tableCellEditing {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
border-radius: 3px;
}
.PlaygroundEditorTheme__tableAddColumns {
position: absolute;
background-color: #eee;
height: 100%;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
z-index: 1202;
}
.PlaygroundEditorTheme__tableAddColumns:after {
background-image: url(../images/icons/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
.PlaygroundEditorTheme__tableAddColumns:hover,
.PlaygroundEditorTheme__tableAddRows:hover {
background-color: #c9dbf0;
}
.PlaygroundEditorTheme__tableAddRows {
position: absolute;
width: calc(100% - 25px);
background-color: #eee;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
z-index: 1202;
}
.PlaygroundEditorTheme__tableAddRows:after {
background-image: url(/images/icons/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
@keyframes table-controls {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.PlaygroundEditorTheme__tableCellResizeRuler {
display: block;
position: absolute;
width: 1px;
background-color: rgb(60, 132, 244);
height: 100%;
top: 0;
z-index: 1202;
}
.PlaygroundEditorTheme__tableCellActionButtonContainer {
display: block;
right: 5px;
top: 6px;
position: absolute;
z-index: 4;
width: 20px;
height: 20px;
}
.PlaygroundEditorTheme__tableCellActionButton {
background-color: #eee;
display: block;
border: 0;
border-radius: 20px;
width: 20px;
height: 20px;
color: #222;
cursor: pointer;
}
.PlaygroundEditorTheme__tableCellActionButton:hover {
background-color: #ddd;
}
.PlaygroundEditorTheme__characterLimit {
display: inline;
background-color: #ffbbbb !important;
}
.PlaygroundEditorTheme__ol1 {
padding: 0;
margin: 0;
list-style-position: outside;
}
.PlaygroundEditorTheme__ol2 {
padding: 0;
margin: 0;
list-style-type: upper-alpha;
list-style-position: outside;
}
.PlaygroundEditorTheme__ol3 {
padding: 0;
margin: 0;
list-style-type: lower-alpha;
list-style-position: outside;
}
.PlaygroundEditorTheme__ol4 {
padding: 0;
margin: 0;
list-style-type: upper-roman;
list-style-position: outside;
}
.PlaygroundEditorTheme__ol5 {
padding: 0;
margin: 0;
list-style-type: lower-roman;
list-style-position: outside;
}
.PlaygroundEditorTheme__ul {
padding: 0;
margin: 0;
list-style-position: outside;
}
.PlaygroundEditorTheme__listItem {
margin: 0 32px;
}
.PlaygroundEditorTheme__listItemChecked,
.PlaygroundEditorTheme__listItemUnchecked {
position: relative;
margin-left: 8px;
margin-right: 8px;
padding-left: 24px;
padding-right: 24px;
list-style-type: none;
outline: none;
}
.PlaygroundEditorTheme__listItemChecked {
text-decoration: line-through;
}
.PlaygroundEditorTheme__listItemUnchecked:before,
.PlaygroundEditorTheme__listItemChecked:before {
content: '';
width: 16px;
height: 16px;
top: 2px;
left: 0;
cursor: pointer;
display: block;
background-size: cover;
position: absolute;
}
.PlaygroundEditorTheme__listItemUnchecked[dir='rtl']:before,
.PlaygroundEditorTheme__listItemChecked[dir='rtl']:before {
left: auto;
right: 0;
}
.PlaygroundEditorTheme__listItemUnchecked:focus:before,
.PlaygroundEditorTheme__listItemChecked:focus:before {
box-shadow: 0 0 0 2px #a6cdfe;
border-radius: 2px;
}
.PlaygroundEditorTheme__listItemUnchecked:before {
border: 1px solid #999;
border-radius: 2px;
}
.PlaygroundEditorTheme__listItemChecked:before {
border: 1px solid rgb(61, 135, 245);
border-radius: 2px;
background-color: #3d87f5;
background-repeat: no-repeat;
}
.PlaygroundEditorTheme__listItemChecked:after {
content: '';
cursor: pointer;
border-color: #fff;
border-style: solid;
position: absolute;
display: block;
top: 6px;
width: 3px;
left: 7px;
right: 7px;
height: 6px;
transform: rotate(45deg);
border-width: 0 2px 2px 0;
}
.PlaygroundEditorTheme__nestedListItem {
list-style-type: none;
}
.PlaygroundEditorTheme__nestedListItem:before,
.PlaygroundEditorTheme__nestedListItem:after {
display: none;
}
.PlaygroundEditorTheme__tokenComment {
color: slategray;
}
.PlaygroundEditorTheme__tokenPunctuation {
color: #999;
}
.PlaygroundEditorTheme__tokenProperty {
color: #905;
}
.PlaygroundEditorTheme__tokenSelector {
color: #690;
}
.PlaygroundEditorTheme__tokenOperator {
color: #9a6e3a;
}
.PlaygroundEditorTheme__tokenAttr {
color: #07a;
}
.PlaygroundEditorTheme__tokenVariable {
color: #e90;
}
.PlaygroundEditorTheme__tokenFunction {
color: #dd4a68;
}
.PlaygroundEditorTheme__mark {
background: rgba(255, 212, 0, 0.14);
border-bottom: 2px solid rgba(255, 212, 0, 0.3);
padding-bottom: 2px;
}
.PlaygroundEditorTheme__markOverlap {
background: rgba(255, 212, 0, 0.3);
border-bottom: 2px solid rgba(255, 212, 0, 0.7);
}
.PlaygroundEditorTheme__mark.selected {
background: rgba(255, 212, 0, 0.5);
border-bottom: 2px solid rgba(255, 212, 0, 1);
}
.PlaygroundEditorTheme__markOverlap.selected {
background: rgba(255, 212, 0, 0.7);
border-bottom: 2px solid rgba(255, 212, 0, 0.7);
}
.PlaygroundEditorTheme__embedBlock {
user-select: none;
}
.PlaygroundEditorTheme__embedBlockFocus {
outline: 2px solid rgb(60, 132, 244);
}
.PlaygroundEditorTheme__layoutContainer {
display: grid;
gap: 10px;
margin: 10px 0;
}
.PlaygroundEditorTheme__layoutItem {
border: 1px dashed #ddd;
padding: 8px 16px;
}
.PlaygroundEditorTheme__autocomplete {
color: #ccc;
}
.PlaygroundEditorTheme__hr {
padding: 2px 2px;
border: none;
margin: 1em 0;
cursor: pointer;
}
.PlaygroundEditorTheme__hr:after {
content: '';
display: block;
height: 2px;
background-color: #ccc;
line-height: 2px;
}
.PlaygroundEditorTheme__hr.selected {
outline: 2px solid rgb(60, 132, 244);
user-select: none;
}
Loading…
Cancel
Save