perf: 编辑器: 表格宽度调整
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 |
@ -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],
|
||||||
|
);
|
||||||
|
}
|
@ -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…
Reference in New Issue