kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Remove tables support as it's not supported by lexical-remark (yet)
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-lexical-ujdd17/deployments/3995
rodzic
700e7af19d
commit
0521d16eae
|
@ -14,18 +14,16 @@ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|||
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
||||
import clsx from 'clsx';
|
||||
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
|
||||
import { $createRemarkExport, $createRemarkImport } from 'lexical-remark';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import { importImage } from './handlers/image';
|
||||
import { useNodes } from './nodes';
|
||||
import TableCellNodes from './nodes/table-cell-nodes';
|
||||
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
||||
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
||||
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
||||
|
@ -33,8 +31,6 @@ import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-tool
|
|||
import FocusPlugin from './plugins/focus-plugin';
|
||||
import MentionPlugin from './plugins/mention-plugin';
|
||||
import StatePlugin from './plugins/state-plugin';
|
||||
import TableActionMenuPlugin from './plugins/table-action-menu-plugin';
|
||||
import { TablePlugin as NewTablePlugin } from './plugins/table-plugin';
|
||||
|
||||
const LINK_MATCHERS = [
|
||||
createLinkMatcherWithRegExp(
|
||||
|
@ -91,9 +87,6 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
|||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const nodes = useNodes();
|
||||
const instance = useInstance();
|
||||
|
||||
const allowInlineTables = !!instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables']);
|
||||
|
||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||
|
||||
|
@ -133,15 +126,6 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
|||
}),
|
||||
}), []);
|
||||
|
||||
const cellEditorConfig = useMemo(() => ({
|
||||
namespace: 'ComposeForm',
|
||||
nodes: TableCellNodes,
|
||||
onError: (error: Error) => {
|
||||
throw error;
|
||||
},
|
||||
theme,
|
||||
}), []);
|
||||
|
||||
const [floatingAnchorElem, setFloatingAnchorElem] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
|
||||
|
@ -199,20 +183,6 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
|||
<HistoryPlugin />
|
||||
<HashtagPlugin />
|
||||
<MentionPlugin />
|
||||
{allowInlineTables && <TablePlugin />}
|
||||
{allowInlineTables && (
|
||||
<NewTablePlugin cellEditorConfig={cellEditorConfig}>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className='outline-none' />}
|
||||
placeholder={null}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<HashtagPlugin />
|
||||
<MentionPlugin />
|
||||
</NewTablePlugin>
|
||||
)}
|
||||
{allowInlineTables && <TableActionMenuPlugin anchorElem={floatingAnchorElem} cellMerge />}
|
||||
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
|
||||
<AutoLinkPlugin matchers={LINK_MATCHERS} />
|
||||
{features.richText && <LinkPlugin />}
|
||||
|
|
|
@ -10,14 +10,12 @@ import { AutoLinkNode, LinkNode } from '@lexical/link';
|
|||
import { ListItemNode, ListNode } from '@lexical/list';
|
||||
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
||||
|
||||
import { useFeatures, useInstance } from 'soapbox/hooks';
|
||||
|
||||
import { EmojiNode } from './emoji-node';
|
||||
import { ImageNode } from './image-node';
|
||||
import { MentionNode } from './mention-node';
|
||||
import { TableNode as NewTableNode } from './table-node';
|
||||
|
||||
import type { Klass, LexicalNode } from 'lexical';
|
||||
|
||||
|
@ -46,14 +44,6 @@ const useNodes = () => {
|
|||
|
||||
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_headings'])) nodes.push(HeadingNode);
|
||||
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_images'])) nodes.push(ImageNode);
|
||||
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables'])) {
|
||||
nodes.push(
|
||||
NewTableNode,
|
||||
TableCellNode,
|
||||
TableNode,
|
||||
TableRowNode,
|
||||
);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* This source code is derived from code from Meta Platforms, Inc.
|
||||
* and affiliates, licensed under the MIT license located in the
|
||||
* LICENSE file in the /app/soapbox/features/compose/editor directory.
|
||||
*/
|
||||
|
||||
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
||||
import { HashtagNode } from '@lexical/hashtag';
|
||||
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
||||
import { ListItemNode, ListNode } from '@lexical/list';
|
||||
import { QuoteNode } from '@lexical/rich-text';
|
||||
|
||||
import { EmojiNode } from './emoji-node';
|
||||
import { MentionNode } from './mention-node';
|
||||
|
||||
import type { Klass, LexicalNode } from 'lexical';
|
||||
|
||||
const TableCellNodes: Array<Klass<LexicalNode>> = [
|
||||
AutoLinkNode,
|
||||
HashtagNode,
|
||||
EmojiNode,
|
||||
MentionNode,
|
||||
QuoteNode,
|
||||
CodeNode,
|
||||
CodeHighlightNode,
|
||||
LinkNode,
|
||||
ListItemNode,
|
||||
ListNode,
|
||||
];
|
||||
|
||||
export default TableCellNodes;
|
Plik diff jest za duży
Load Diff
|
@ -1,424 +0,0 @@
|
|||
/**
|
||||
* This source code is derived from code from Meta Platforms, Inc.
|
||||
* and affiliates, licensed under the MIT license located in the
|
||||
* LICENSE file in the /app/soapbox/features/compose/editor directory.
|
||||
*/
|
||||
|
||||
import { DecoratorNode } from 'lexical';
|
||||
import * as React from 'react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
} from 'lexical';
|
||||
|
||||
export type Cell = {
|
||||
colSpan: number
|
||||
json: string
|
||||
type: 'normal' | 'header'
|
||||
id: string
|
||||
width: number | null
|
||||
};
|
||||
|
||||
export type Row = {
|
||||
cells: Array<Cell>
|
||||
height: null | number
|
||||
id: string
|
||||
};
|
||||
|
||||
export type Rows = Array<Row>;
|
||||
|
||||
export const cellHTMLCache: Map<string, string> = new Map();
|
||||
export const cellTextContentCache: Map<string, string> = new Map();
|
||||
|
||||
const emptyEditorJSON =
|
||||
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
||||
|
||||
const plainTextEditorJSON = (text: string) =>
|
||||
text === ''
|
||||
? emptyEditorJSON
|
||||
: `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`;
|
||||
|
||||
const TableComponent = React.lazy(
|
||||
// @ts-ignore
|
||||
() => import('./table-component'),
|
||||
);
|
||||
|
||||
export function createUID(): string {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace(/[^a-z]+/g, '')
|
||||
.substr(0, 5);
|
||||
}
|
||||
|
||||
function createCell(type: 'normal' | 'header'): Cell {
|
||||
return {
|
||||
colSpan: 1,
|
||||
id: createUID(),
|
||||
json: emptyEditorJSON,
|
||||
type,
|
||||
width: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRow(): Row {
|
||||
return {
|
||||
cells: [],
|
||||
height: null,
|
||||
id: createUID(),
|
||||
};
|
||||
}
|
||||
|
||||
export type SerializedTableNode = Spread<
|
||||
{
|
||||
rows: Rows
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export function extractRowsFromHTML(tableElem: HTMLTableElement): Rows {
|
||||
const rowElems = tableElem.querySelectorAll('tr');
|
||||
const rows: Rows = [];
|
||||
for (let y = 0; y < rowElems.length; y++) {
|
||||
const rowElem = rowElems[y];
|
||||
const cellElems = rowElem.querySelectorAll('td,th');
|
||||
if (!cellElems || cellElems.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const cells: Array<Cell> = [];
|
||||
for (let x = 0; x < cellElems.length; x++) {
|
||||
const cellElem = cellElems[x] as HTMLElement;
|
||||
const isHeader = cellElem.nodeName === 'TH';
|
||||
const cell = createCell(isHeader ? 'header' : 'normal');
|
||||
cell.json = plainTextEditorJSON(
|
||||
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
||||
);
|
||||
cells.push(cell);
|
||||
}
|
||||
const row = createRow();
|
||||
row.cells = cells;
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
|
||||
const rowElems = domNode.querySelectorAll('tr');
|
||||
if (!rowElems || rowElems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const rows: Rows = [];
|
||||
for (let y = 0; y < rowElems.length; y++) {
|
||||
const rowElem = rowElems[y];
|
||||
const cellElems = rowElem.querySelectorAll('td,th');
|
||||
if (!cellElems || cellElems.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const cells: Array<Cell> = [];
|
||||
for (let x = 0; x < cellElems.length; x++) {
|
||||
const cellElem = cellElems[x] as HTMLElement;
|
||||
const isHeader = cellElem.nodeName === 'TH';
|
||||
const cell = createCell(isHeader ? 'header' : 'normal');
|
||||
cell.json = plainTextEditorJSON(
|
||||
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
||||
);
|
||||
cells.push(cell);
|
||||
}
|
||||
const row = createRow();
|
||||
row.cells = cells;
|
||||
rows.push(row);
|
||||
}
|
||||
return { node: $createTableNode(rows) };
|
||||
}
|
||||
|
||||
export function exportTableCellsToHTML(
|
||||
rows: Rows,
|
||||
rect?: {startX: number, endX: number, startY: number, endY: number},
|
||||
): HTMLElement {
|
||||
const table = document.createElement('table');
|
||||
const colGroup = document.createElement('colgroup');
|
||||
const tBody = document.createElement('tbody');
|
||||
const firstRow = rows[0];
|
||||
|
||||
for (
|
||||
let x = rect ? rect.startX : 0;
|
||||
x < (rect ? rect.endX + 1 : firstRow.cells.length);
|
||||
x++
|
||||
) {
|
||||
const col = document.createElement('col');
|
||||
colGroup.append(col);
|
||||
}
|
||||
|
||||
for (
|
||||
let y = rect ? rect.startY : 0;
|
||||
y < (rect ? rect.endY + 1 : rows.length);
|
||||
y++
|
||||
) {
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const rowElem = document.createElement('tr');
|
||||
|
||||
for (
|
||||
let x = rect ? rect.startX : 0;
|
||||
x < (rect ? rect.endX + 1 : cells.length);
|
||||
x++
|
||||
) {
|
||||
const cell = cells[x];
|
||||
const cellElem = document.createElement(
|
||||
cell.type === 'header' ? 'th' : 'td',
|
||||
);
|
||||
cellElem.innerHTML = cellHTMLCache.get(cell.json) || '';
|
||||
rowElem.appendChild(cellElem);
|
||||
}
|
||||
tBody.appendChild(rowElem);
|
||||
}
|
||||
|
||||
table.appendChild(colGroup);
|
||||
table.appendChild(tBody);
|
||||
return table;
|
||||
}
|
||||
|
||||
export class TableNode extends DecoratorNode<JSX.Element> {
|
||||
|
||||
__rows: Rows;
|
||||
|
||||
static getType(): string {
|
||||
return 'tablesheet';
|
||||
}
|
||||
|
||||
static clone(node: TableNode): TableNode {
|
||||
return new TableNode(Array.from(node.__rows), node.__key);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTableNode): TableNode {
|
||||
return $createTableNode(serializedNode.rows);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTableNode {
|
||||
return {
|
||||
rows: this.__rows,
|
||||
type: 'tablesheet',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
table: (_node: Node) => ({
|
||||
conversion: convertTableElement,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
return { element: exportTableCellsToHTML(this.__rows) };
|
||||
}
|
||||
|
||||
constructor(rows?: Rows, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__rows = rows || [];
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
mergeRows(startX: number, startY: number, mergeRows: Rows): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
const endY = Math.min(rows.length, startY + mergeRows.length);
|
||||
for (let y = startY; y < endY; y++) {
|
||||
const row = rows[y];
|
||||
const mergeRow = mergeRows[y - startY];
|
||||
const cells = row.cells;
|
||||
const cellsClone = Array.from(cells);
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
const mergeCells = mergeRow.cells;
|
||||
const endX = Math.min(cells.length, startX + mergeCells.length);
|
||||
for (let x = startX; x < endX; x++) {
|
||||
const cell = cells[x];
|
||||
const mergeCell = mergeCells[x - startX];
|
||||
const cellClone = { ...cell, json: mergeCell.json, type: mergeCell.type };
|
||||
cellsClone[x] = cellClone;
|
||||
}
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
}
|
||||
|
||||
updateCellJSON(x: number, y: number, json: string): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cell = cells[x];
|
||||
const cellsClone = Array.from(cells);
|
||||
const cellClone = { ...cell, json };
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
cellsClone[x] = cellClone;
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
|
||||
updateCellType(x: number, y: number, type: 'header' | 'normal'): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cell = cells[x];
|
||||
const cellsClone = Array.from(cells);
|
||||
const cellClone = { ...cell, type };
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
cellsClone[x] = cellClone;
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
|
||||
insertColumnAt(x: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cellsClone = Array.from(cells);
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
const type = (cells[x] || cells[x - 1]).type;
|
||||
cellsClone.splice(x, 0, createCell(type));
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
}
|
||||
|
||||
deleteColumnAt(x: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cellsClone = Array.from(cells);
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
cellsClone.splice(x, 1);
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
}
|
||||
|
||||
addColumns(count: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cellsClone = Array.from(cells);
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
const type = cells[cells.length - 1].type;
|
||||
for (let x = 0; x < count; x++) {
|
||||
cellsClone.push(createCell(type));
|
||||
}
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
}
|
||||
|
||||
insertRowAt(y: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
const prevRow = rows[y] || rows[y - 1];
|
||||
const cellCount = prevRow.cells.length;
|
||||
const row = createRow();
|
||||
for (let x = 0; x < cellCount; x++) {
|
||||
const cell = createCell(prevRow.cells[x].type);
|
||||
row.cells.push(cell);
|
||||
}
|
||||
rows.splice(y, 0, row);
|
||||
}
|
||||
|
||||
deleteRowAt(y: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
rows.splice(y, 1);
|
||||
}
|
||||
|
||||
addRows(count: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
const prevRow = rows[rows.length - 1];
|
||||
const cellCount = prevRow.cells.length;
|
||||
|
||||
for (let y = 0; y < count; y++) {
|
||||
const row = createRow();
|
||||
for (let x = 0; x < cellCount; x++) {
|
||||
const cell = createCell(prevRow.cells[x].type);
|
||||
row.cells.push(cell);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
updateColumnWidth(x: number, width: number): void {
|
||||
const self = this.getWritable();
|
||||
const rows = self.__rows;
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cells = row.cells;
|
||||
const cellsClone = Array.from(cells);
|
||||
const rowClone = { ...row, cells: cellsClone };
|
||||
cellsClone[x].width = width;
|
||||
rows[y] = rowClone;
|
||||
}
|
||||
}
|
||||
|
||||
decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
return (
|
||||
<Suspense>
|
||||
<TableComponent
|
||||
nodeKey={this.__key}
|
||||
theme={config.theme}
|
||||
rows={this.__rows}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
isInline(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function $isTableNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is TableNode {
|
||||
return node instanceof TableNode;
|
||||
}
|
||||
|
||||
export function $createTableNode(rows: Rows): TableNode {
|
||||
return new TableNode(rows);
|
||||
}
|
||||
|
||||
export function $createTableNodeWithDimensions(
|
||||
rowCount: number,
|
||||
columnCount: number,
|
||||
includeHeaders = true,
|
||||
): TableNode {
|
||||
const rows: Rows = [];
|
||||
for (let y = 0; y < columnCount; y++) {
|
||||
const row: Row = createRow();
|
||||
rows.push(row);
|
||||
for (let x = 0; x < rowCount; x++) {
|
||||
row.cells.push(
|
||||
createCell(
|
||||
includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return new TableNode(rows);
|
||||
}
|
|
@ -30,13 +30,11 @@ import { $createImageNode } from '../nodes/image-node';
|
|||
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
|
||||
|
||||
import { ToolbarButton } from './floating-text-format-toolbar-plugin';
|
||||
import { INSERT_NEW_TABLE_COMMAND } from './table-plugin';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
createHorizontalLine: { id: 'compose_form.lexical.create_horizontal_line', defaultMessage: 'Create horizontal line' },
|
||||
createTable: { id: 'compose_form.lexical.create_table', defaultMessage: 'Create table' },
|
||||
uploadMedia: { id: 'compose_form.lexical.upload_media', defaultMessage: 'Upload media' },
|
||||
});
|
||||
|
||||
|
@ -108,7 +106,6 @@ const BlockTypeFloatingToolbar = ({
|
|||
const instance = useInstance();
|
||||
|
||||
const allowInlineImages = instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_images']);
|
||||
const allowInlineTables = instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables']);
|
||||
|
||||
const updateBlockTypeFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
@ -187,15 +184,6 @@ const BlockTypeFloatingToolbar = ({
|
|||
});
|
||||
};
|
||||
|
||||
const createTable = () => {
|
||||
editor.update(() => {
|
||||
editor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, {
|
||||
columns: 3,
|
||||
rows: 3,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createImage = (src: string) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
@ -217,13 +205,6 @@ const BlockTypeFloatingToolbar = ({
|
|||
{editor.isEditable() && (
|
||||
<>
|
||||
{allowInlineImages && <UploadButton onSelectFile={createImage} />}
|
||||
{allowInlineTables && (
|
||||
<ToolbarButton
|
||||
onClick={createTable}
|
||||
aria-label={intl.formatMessage(messages.createTable)}
|
||||
icon={require('@tabler/icons/table.svg')}
|
||||
/>
|
||||
)}
|
||||
<ToolbarButton
|
||||
onClick={createHorizontalLine}
|
||||
aria-label={intl.formatMessage(messages.createHorizontalLine)}
|
||||
|
|
|
@ -1,701 +0,0 @@
|
|||
/**
|
||||
* This source code is derived from code from Meta Platforms, Inc.
|
||||
* and affiliates, licensed under the MIT license located in the
|
||||
* LICENSE file in the /app/soapbox/features/compose/editor directory.
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import useLexicalEditable from '@lexical/react/useLexicalEditable';
|
||||
import {
|
||||
$deleteTableColumn__EXPERIMENTAL,
|
||||
$deleteTableRow__EXPERIMENTAL,
|
||||
$getTableCellNodeFromLexicalNode,
|
||||
$getTableColumnIndexFromTableCellNode,
|
||||
$getTableNodeFromLexicalNodeOrThrow,
|
||||
$getTableRowIndexFromTableCellNode,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableCellNode,
|
||||
$isTableRowNode,
|
||||
$unmergeCell,
|
||||
getTableSelectionFromTableElement,
|
||||
HTMLTableElementWithWithTableSelectionState,
|
||||
TableCellHeaderStates,
|
||||
TableCellNode,
|
||||
type TableRowNode,
|
||||
} from '@lexical/table';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$isParagraphNode,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
DEPRECATED_$getNodeTriplet,
|
||||
DEPRECATED_$isGridCellNode,
|
||||
DEPRECATED_$isGridSelection,
|
||||
GridSelection,
|
||||
} from 'lexical';
|
||||
import * as React from 'react';
|
||||
import { ReactPortal, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { DEPRECATED_GridCellNode, ElementNode } from 'lexical';
|
||||
|
||||
function computeSelectionCount(selection: GridSelection): {
|
||||
columns: number
|
||||
rows: number
|
||||
} {
|
||||
const selectionShape = selection.getShape();
|
||||
return {
|
||||
columns: selectionShape.toX - selectionShape.fromX + 1,
|
||||
rows: selectionShape.toY - selectionShape.fromY + 1,
|
||||
};
|
||||
}
|
||||
|
||||
// This is important when merging cells as there is no good way to re-merge weird shapes (a result
|
||||
// of selecting merged cells and non-merged)
|
||||
function isGridSelectionRectangular(selection: GridSelection): boolean {
|
||||
const nodes = selection.getNodes();
|
||||
const currentRows: Array<number> = [];
|
||||
let currentRow: TableRowNode | null = null;
|
||||
let expectedColumns: number | null = null;
|
||||
let currentColumns = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if ($isTableCellNode(node)) {
|
||||
const row = node.getParentOrThrow();
|
||||
if (!$isTableRowNode(row)) throw new Error('Expected CellNode to have a RowNode parent');
|
||||
if (currentRow !== row) {
|
||||
if (expectedColumns !== null && currentColumns !== expectedColumns) {
|
||||
return false;
|
||||
}
|
||||
if (currentRow !== null) {
|
||||
expectedColumns = currentColumns;
|
||||
}
|
||||
currentRow = row;
|
||||
currentColumns = 0;
|
||||
}
|
||||
const colSpan = node.__colSpan;
|
||||
for (let j = 0; j < colSpan; j++) {
|
||||
if (currentRows[currentColumns + j] === undefined) {
|
||||
currentRows[currentColumns + j] = 0;
|
||||
}
|
||||
currentRows[currentColumns + j] += node.__rowSpan;
|
||||
}
|
||||
currentColumns += colSpan;
|
||||
}
|
||||
}
|
||||
return (
|
||||
(expectedColumns === null || currentColumns === expectedColumns) &&
|
||||
currentRows.every((v) => v === currentRows[0])
|
||||
);
|
||||
}
|
||||
|
||||
function $canUnmerge(): boolean {
|
||||
const selection = $getSelection();
|
||||
if (
|
||||
($isRangeSelection(selection) && !selection.isCollapsed()) ||
|
||||
(DEPRECATED_$isGridSelection(selection) &&
|
||||
!selection.anchor.is(selection.focus)) ||
|
||||
(!$isRangeSelection(selection) && !DEPRECATED_$isGridSelection(selection))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
|
||||
return cell.__colSpan > 1 || cell.__rowSpan > 1;
|
||||
}
|
||||
|
||||
function $cellContainsEmptyParagraph(cell: DEPRECATED_GridCellNode): 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();
|
||||
}
|
||||
}
|
||||
|
||||
type TableCellActionMenuProps = Readonly<{
|
||||
contextRef: {current: null | HTMLElement}
|
||||
onClose: () => void
|
||||
setIsMenuOpen: (isOpen: boolean) => void
|
||||
tableCellNode: TableCellNode
|
||||
cellMerge: boolean
|
||||
}>;
|
||||
|
||||
function TableActionMenu({
|
||||
onClose,
|
||||
tableCellNode: _tableCellNode,
|
||||
setIsMenuOpen,
|
||||
contextRef,
|
||||
cellMerge,
|
||||
}: 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);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
|
||||
const nodeUpdated =
|
||||
nodeMutations.get(tableCellNode.getKey()) === 'updated';
|
||||
|
||||
if (nodeUpdated) {
|
||||
editor.getEditorState().read(() => {
|
||||
updateTableCellNode(tableCellNode.getLatest());
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [editor, tableCellNode]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
// Merge cells
|
||||
if (DEPRECATED_$isGridSelection(selection)) {
|
||||
const currentSelectionCounts = computeSelectionCount(selection);
|
||||
updateSelectionCounts(computeSelectionCount(selection));
|
||||
setCanMergeCells(
|
||||
isGridSelectionRectangular(selection) &&
|
||||
(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 && dropDownElement && rootElement) {
|
||||
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 &&
|
||||
contextRef.current &&
|
||||
!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 tableSelection = getTableSelectionFromTableElement(tableElement);
|
||||
if (tableSelection !== null) {
|
||||
tableSelection.clearHighlight();
|
||||
}
|
||||
|
||||
tableNode.markDirty();
|
||||
updateTableCellNode(tableCellNode.getLatest());
|
||||
}
|
||||
|
||||
const rootNode = $getRoot();
|
||||
rootNode.selectStart();
|
||||
});
|
||||
}, [editor, tableCellNode]);
|
||||
|
||||
const mergeTableCellsAtSelection = () => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (DEPRECATED_$isGridSelection(selection)) {
|
||||
const { columns, rows } = computeSelectionCount(selection);
|
||||
const nodes = selection.getNodes();
|
||||
let firstCell: null | DEPRECATED_GridCellNode = null;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (DEPRECATED_$isGridCellNode(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 (DEPRECATED_$isGridCellNode(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');
|
||||
}
|
||||
|
||||
tableRow.getChildren().forEach((tableCell) => {
|
||||
if (!$isTableCellNode(tableCell)) {
|
||||
throw new Error('Expected table cell');
|
||||
}
|
||||
|
||||
tableCell.toggleHeaderStyle(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();
|
||||
|
||||
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 || tableColumnIndex < 0) {
|
||||
throw new Error('Expected table cell to be inside of table row.');
|
||||
}
|
||||
|
||||
const tableCell = tableCells[tableColumnIndex];
|
||||
|
||||
if (!$isTableCellNode(tableCell)) {
|
||||
throw new Error('Expected table cell');
|
||||
}
|
||||
|
||||
tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN);
|
||||
}
|
||||
|
||||
clearTableSelection();
|
||||
onClose();
|
||||
});
|
||||
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
||||
|
||||
let mergeCellButton: null | JSX.Element = null;
|
||||
if (cellMerge) {
|
||||
if (canMergeCells) {
|
||||
mergeCellButton = (
|
||||
<button
|
||||
className='item'
|
||||
onClick={() => mergeTableCellsAtSelection()}
|
||||
data-test-id='table-merge-cells'
|
||||
>
|
||||
Merge cells
|
||||
</button>
|
||||
);
|
||||
} else if (canUnmergeCell) {
|
||||
mergeCellButton = (
|
||||
<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}
|
||||
<hr />
|
||||
<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
|
||||
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
|
||||
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
|
||||
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
|
||||
className='item'
|
||||
onClick={() => deleteTableColumnAtSelection()}
|
||||
data-test-id='table-delete-columns'
|
||||
>
|
||||
<span className='text'>Delete column</span>
|
||||
</button>
|
||||
<button
|
||||
className='item'
|
||||
onClick={() => deleteTableRowAtSelection()}
|
||||
data-test-id='table-delete-rows'
|
||||
>
|
||||
<span className='text'>Delete row</span>
|
||||
</button>
|
||||
<button
|
||||
className='item'
|
||||
onClick={() => deleteTableAtSelection()}
|
||||
data-test-id='table-delete'
|
||||
>
|
||||
<span className='text'>Delete table</span>
|
||||
</button>
|
||||
<hr />
|
||||
<button className='item' onClick={() => toggleTableRowIsHeader()}>
|
||||
<span className='text'>
|
||||
{(tableCellNode.__headerState & TableCellHeaderStates.ROW) ===
|
||||
TableCellHeaderStates.ROW
|
||||
? 'Remove'
|
||||
: 'Add'}{' '}
|
||||
row header
|
||||
</span>
|
||||
</button>
|
||||
<button className='item' onClick={() => toggleTableColumnIsHeader()}>
|
||||
<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 moveMenu = useCallback(() => {
|
||||
const menu = menuButtonRef.current;
|
||||
const selection = $getSelection();
|
||||
const nativeSelection = window.getSelection();
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (!selection || !menu) {
|
||||
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) {
|
||||
setTableMenuCellNode(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const tableCellParentNodeDOM = editor.getElementByKey(
|
||||
tableCellNodeFromSelection.getKey(),
|
||||
);
|
||||
|
||||
if (!tableCellParentNodeDOM) {
|
||||
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 && tableCellNode) {
|
||||
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey());
|
||||
|
||||
if (tableCellNodeDOM) {
|
||||
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 && (
|
||||
<>
|
||||
<button
|
||||
className='table-cell-action-button chevron-down'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
}}
|
||||
ref={menuRootRef}
|
||||
>
|
||||
<i className='chevron-down' />
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<TableActionMenu
|
||||
contextRef={menuRootRef}
|
||||
setIsMenuOpen={setIsMenuOpen}
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
tableCellNode={tableCellNode}
|
||||
cellMerge={cellMerge}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TableActionMenuPlugin({
|
||||
anchorElem,
|
||||
cellMerge = false,
|
||||
}: {
|
||||
anchorElem?: HTMLElement | null
|
||||
cellMerge?: boolean
|
||||
}): null | ReactPortal {
|
||||
const isEditable = useLexicalEditable();
|
||||
return createPortal(
|
||||
isEditable ? (
|
||||
<TableCellActionMenuContainer
|
||||
anchorElem={anchorElem || document.body}
|
||||
cellMerge={cellMerge}
|
||||
/>
|
||||
) : null,
|
||||
anchorElem || document.body,
|
||||
);
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
/**
|
||||
* This source code is derived from code from Meta Platforms, Inc.
|
||||
* and affiliates, licensed under the MIT license located in the
|
||||
* LICENSE file in the /app/soapbox/features/compose/editor directory.
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
EditorThemeClasses,
|
||||
Klass,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
} from 'lexical';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { $createTableNodeWithDimensions, TableNode } from '../nodes/table-node';
|
||||
|
||||
export type InsertTableCommandPayload = Readonly<{
|
||||
columns: number
|
||||
rows: number
|
||||
includeHeaders?: boolean
|
||||
}>;
|
||||
|
||||
export type CellContextShape = {
|
||||
cellEditorConfig: null | CellEditorConfig
|
||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>
|
||||
set: (
|
||||
cellEditorConfig: null | CellEditorConfig,
|
||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>,
|
||||
) => void
|
||||
};
|
||||
|
||||
export type CellEditorConfig = Readonly<{
|
||||
namespace: string
|
||||
nodes?: ReadonlyArray<Klass<LexicalNode>>
|
||||
onError: (error: Error, editor: LexicalEditor) => void
|
||||
readOnly?: boolean
|
||||
theme?: EditorThemeClasses
|
||||
}>;
|
||||
|
||||
export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
|
||||
createCommand('INSERT_NEW_TABLE_COMMAND');
|
||||
|
||||
export const CellContext = createContext<CellContextShape>({
|
||||
cellEditorConfig: null,
|
||||
cellEditorPlugins: null,
|
||||
set: () => {
|
||||
// Empty
|
||||
},
|
||||
});
|
||||
|
||||
export function TableContext({ children }: {children: JSX.Element}) {
|
||||
const [contextValue, setContextValue] = useState<{
|
||||
cellEditorConfig: null | CellEditorConfig
|
||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>
|
||||
}>({
|
||||
cellEditorConfig: null,
|
||||
cellEditorPlugins: null,
|
||||
});
|
||||
return (
|
||||
<CellContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
cellEditorConfig: contextValue.cellEditorConfig,
|
||||
cellEditorPlugins: contextValue.cellEditorPlugins,
|
||||
set: (cellEditorConfig, cellEditorPlugins) => {
|
||||
setContextValue({ cellEditorConfig, cellEditorPlugins });
|
||||
},
|
||||
}),
|
||||
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</CellContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TablePlugin({
|
||||
cellEditorConfig,
|
||||
children,
|
||||
}: {
|
||||
cellEditorConfig: CellEditorConfig
|
||||
children: JSX.Element | Array<JSX.Element>
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const cellContext = useContext(CellContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([TableNode])) {
|
||||
throw new Error('TablePlugin: TableNode is not registered on editor');
|
||||
}
|
||||
|
||||
cellContext.set(cellEditorConfig, children);
|
||||
|
||||
return editor.registerCommand<InsertTableCommandPayload>(
|
||||
INSERT_NEW_TABLE_COMMAND,
|
||||
({ columns, rows, includeHeaders }) => {
|
||||
const tableNode = $createTableNodeWithDimensions(
|
||||
rows,
|
||||
columns,
|
||||
includeHeaders,
|
||||
);
|
||||
$insertNodes([tableNode]);
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
);
|
||||
}, [cellContext, cellEditorConfig, children, editor]);
|
||||
|
||||
return null;
|
||||
}
|
Ładowanie…
Reference in New Issue