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