kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Lexical: WIP port tables support
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-lexical-ujdd17/deployments/3995
rodzic
06d0f4bcc2
commit
700e7af19d
|
@ -14,16 +14,18 @@ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
||||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
||||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
|
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
|
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
|
||||||
import { $createRemarkExport, $createRemarkImport } from 'lexical-remark';
|
import { $createRemarkExport, $createRemarkImport } from 'lexical-remark';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { importImage } from './handlers/image';
|
import { importImage } from './handlers/image';
|
||||||
import { useNodes } from './nodes';
|
import { useNodes } from './nodes';
|
||||||
|
import TableCellNodes from './nodes/table-cell-nodes';
|
||||||
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
||||||
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
||||||
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
||||||
|
@ -31,6 +33,8 @@ import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-tool
|
||||||
import FocusPlugin from './plugins/focus-plugin';
|
import FocusPlugin from './plugins/focus-plugin';
|
||||||
import MentionPlugin from './plugins/mention-plugin';
|
import MentionPlugin from './plugins/mention-plugin';
|
||||||
import StatePlugin from './plugins/state-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 = [
|
const LINK_MATCHERS = [
|
||||||
createLinkMatcherWithRegExp(
|
createLinkMatcherWithRegExp(
|
||||||
|
@ -53,6 +57,24 @@ interface IComposeEditor {
|
||||||
placeholder?: JSX.Element | string
|
placeholder?: JSX.Element | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
|
||||||
|
mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
|
||||||
|
text: {
|
||||||
|
bold: 'font-bold',
|
||||||
|
code: 'font-mono',
|
||||||
|
italic: 'italic',
|
||||||
|
strikethrough: 'line-through',
|
||||||
|
underline: 'underline',
|
||||||
|
underlineStrikethrough: 'underline-line-through',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
h1: 'text-2xl font-bold',
|
||||||
|
h2: 'text-xl font-bold',
|
||||||
|
h3: 'text-lg font-semibold',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
className,
|
className,
|
||||||
placeholderClassName,
|
placeholderClassName,
|
||||||
|
@ -69,6 +91,9 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const nodes = useNodes();
|
const nodes = useNodes();
|
||||||
|
const instance = useInstance();
|
||||||
|
|
||||||
|
const allowInlineTables = !!instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables']);
|
||||||
|
|
||||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
|
|
||||||
|
@ -76,23 +101,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
namespace: 'ComposeForm',
|
namespace: 'ComposeForm',
|
||||||
onError: console.error,
|
onError: console.error,
|
||||||
nodes,
|
nodes,
|
||||||
theme: {
|
theme,
|
||||||
hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
|
|
||||||
mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
|
|
||||||
text: {
|
|
||||||
bold: 'font-bold',
|
|
||||||
code: 'font-mono',
|
|
||||||
italic: 'italic',
|
|
||||||
strikethrough: 'line-through',
|
|
||||||
underline: 'underline',
|
|
||||||
underlineStrikethrough: 'underline-line-through',
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
h1: 'text-2xl font-bold',
|
|
||||||
h2: 'text-xl font-bold',
|
|
||||||
h3: 'text-lg font-semibold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editorState: dispatch((_, getState) => {
|
editorState: dispatch((_, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const compose = state.compose.get(composeId);
|
const compose = state.compose.get(composeId);
|
||||||
|
@ -124,6 +133,15 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
}),
|
}),
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
|
const cellEditorConfig = useMemo(() => ({
|
||||||
|
namespace: 'ComposeForm',
|
||||||
|
nodes: TableCellNodes,
|
||||||
|
onError: (error: Error) => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
}), []);
|
||||||
|
|
||||||
const [floatingAnchorElem, setFloatingAnchorElem] =
|
const [floatingAnchorElem, setFloatingAnchorElem] =
|
||||||
useState<HTMLDivElement | null>(null);
|
useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
@ -181,6 +199,20 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
<MentionPlugin />
|
<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} />
|
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
|
||||||
<AutoLinkPlugin matchers={LINK_MATCHERS} />
|
<AutoLinkPlugin matchers={LINK_MATCHERS} />
|
||||||
{features.richText && <LinkPlugin />}
|
{features.richText && <LinkPlugin />}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import { AutoLinkNode, LinkNode } from '@lexical/link';
|
||||||
import { ListItemNode, ListNode } from '@lexical/list';
|
import { ListItemNode, ListNode } from '@lexical/list';
|
||||||
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||||
|
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
||||||
|
|
||||||
import { useFeatures, useInstance } from 'soapbox/hooks';
|
import { useFeatures, useInstance } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { EmojiNode } from './emoji-node';
|
import { EmojiNode } from './emoji-node';
|
||||||
import { ImageNode } from './image-node';
|
import { ImageNode } from './image-node';
|
||||||
import { MentionNode } from './mention-node';
|
import { MentionNode } from './mention-node';
|
||||||
|
import { TableNode as NewTableNode } from './table-node';
|
||||||
|
|
||||||
import type { Klass, LexicalNode } from 'lexical';
|
import type { Klass, LexicalNode } from 'lexical';
|
||||||
|
|
||||||
|
@ -32,18 +34,26 @@ const useNodes = () => {
|
||||||
|
|
||||||
if (features.richText) {
|
if (features.richText) {
|
||||||
nodes.push(
|
nodes.push(
|
||||||
QuoteNode,
|
|
||||||
CodeNode,
|
|
||||||
CodeHighlightNode,
|
CodeHighlightNode,
|
||||||
|
CodeNode,
|
||||||
|
HorizontalRuleNode,
|
||||||
LinkNode,
|
LinkNode,
|
||||||
ListItemNode,
|
ListItemNode,
|
||||||
ListNode,
|
ListNode,
|
||||||
HorizontalRuleNode,
|
QuoteNode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_headings'])) nodes.push(HeadingNode);
|
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_images'])) nodes.push(ImageNode);
|
||||||
|
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_tables'])) {
|
||||||
|
nodes.push(
|
||||||
|
NewTableNode,
|
||||||
|
TableCellNode,
|
||||||
|
TableNode,
|
||||||
|
TableRowNode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* 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
|
@ -0,0 +1,424 @@
|
||||||
|
/**
|
||||||
|
* 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,11 +30,13 @@ import { $createImageNode } from '../nodes/image-node';
|
||||||
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
|
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
|
||||||
|
|
||||||
import { ToolbarButton } from './floating-text-format-toolbar-plugin';
|
import { ToolbarButton } from './floating-text-format-toolbar-plugin';
|
||||||
|
import { INSERT_NEW_TABLE_COMMAND } from './table-plugin';
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
createHorizontalLine: { id: 'compose_form.lexical.create_horizontal_line', defaultMessage: 'Create horizontal line' },
|
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' },
|
uploadMedia: { id: 'compose_form.lexical.upload_media', defaultMessage: 'Upload media' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,6 +105,10 @@ const BlockTypeFloatingToolbar = ({
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
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 updateBlockTypeFloatingToolbar = useCallback(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
|
@ -181,6 +187,15 @@ const BlockTypeFloatingToolbar = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createTable = () => {
|
||||||
|
editor.update(() => {
|
||||||
|
editor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, {
|
||||||
|
columns: 3,
|
||||||
|
rows: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const createImage = (src: string) => {
|
const createImage = (src: string) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
|
@ -201,7 +216,14 @@ const BlockTypeFloatingToolbar = ({
|
||||||
>
|
>
|
||||||
{editor.isEditable() && (
|
{editor.isEditable() && (
|
||||||
<>
|
<>
|
||||||
<UploadButton onSelectFile={createImage} />
|
{allowInlineImages && <UploadButton onSelectFile={createImage} />}
|
||||||
|
{allowInlineTables && (
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={createTable}
|
||||||
|
aria-label={intl.formatMessage(messages.createTable)}
|
||||||
|
icon={require('@tabler/icons/table.svg')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={createHorizontalLine}
|
onClick={createHorizontalLine}
|
||||||
aria-label={intl.formatMessage(messages.createHorizontalLine)}
|
aria-label={intl.formatMessage(messages.createHorizontalLine)}
|
||||||
|
|
|
@ -0,0 +1,701 @@
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
21
package.json
21
package.json
|
@ -52,14 +52,17 @@
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@jest/globals": "^29.0.0",
|
"@jest/globals": "^29.0.0",
|
||||||
"@lcdp/offline-plugin": "^5.1.0",
|
"@lcdp/offline-plugin": "^5.1.0",
|
||||||
"@lexical/code": "^0.11.2",
|
"@lexical/clipboard": "^0.11.3",
|
||||||
"@lexical/hashtag": "^0.11.2",
|
"@lexical/code": "^0.11.3",
|
||||||
"@lexical/link": "^0.11.2",
|
"@lexical/hashtag": "^0.11.3",
|
||||||
"@lexical/list": "^0.11.2",
|
"@lexical/html": "^0.11.3",
|
||||||
"@lexical/react": "^0.11.2",
|
"@lexical/link": "^0.11.3",
|
||||||
"@lexical/rich-text": "^0.11.2",
|
"@lexical/list": "^0.11.3",
|
||||||
"@lexical/selection": "^0.11.2",
|
"@lexical/react": "^0.11.3",
|
||||||
"@lexical/utils": "^0.11.2",
|
"@lexical/rich-text": "^0.11.3",
|
||||||
|
"@lexical/selection": "^0.11.3",
|
||||||
|
"@lexical/table": "^0.11.3",
|
||||||
|
"@lexical/utils": "^0.11.3",
|
||||||
"@metamask/providers": "^10.0.0",
|
"@metamask/providers": "^10.0.0",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"@reach/combobox": "^0.18.0",
|
"@reach/combobox": "^0.18.0",
|
||||||
|
@ -134,7 +137,7 @@
|
||||||
"intl-messageformat-parser": "^6.0.0",
|
"intl-messageformat-parser": "^6.0.0",
|
||||||
"intl-pluralrules": "^1.3.1",
|
"intl-pluralrules": "^1.3.1",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lexical": "^0.11.2",
|
"lexical": "^0.11.3",
|
||||||
"lexical-remark": "^0.3.8",
|
"lexical-remark": "^0.3.8",
|
||||||
"libphonenumber-js": "^1.10.8",
|
"libphonenumber-js": "^1.10.8",
|
||||||
"line-awesome": "^1.3.0",
|
"line-awesome": "^1.3.0",
|
||||||
|
|
248
yarn.lock
248
yarn.lock
|
@ -2188,159 +2188,159 @@
|
||||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
||||||
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
||||||
|
|
||||||
"@lexical/clipboard@0.11.2":
|
"@lexical/clipboard@0.11.3", "@lexical/clipboard@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.11.2.tgz#586d02a4b1eb243cb44025809e8daa2f5e4a7bcc"
|
resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.11.3.tgz#f4bfac8fad1f41d43a45c4e1ffc79542757314f9"
|
||||||
integrity sha512-Cq3chsBhX9fJPWQXxqoaNr+MSN9VMbplhQXwEQJOXHYErkAVQgpr4cSlPFVD+MI2MO1TMhYRkMlXWZ33iYmcKQ==
|
integrity sha512-6xggT8b0hd4OQy25mBH+yiJsr3Bm8APHjDOd3yINCGeiiHXIC+2qKQn3MG70euxQQuyzq++tYHcSsFq42g8Jyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/html" "0.11.2"
|
"@lexical/html" "0.11.3"
|
||||||
"@lexical/list" "0.11.2"
|
"@lexical/list" "0.11.3"
|
||||||
"@lexical/selection" "0.11.2"
|
"@lexical/selection" "0.11.3"
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/code@0.11.2", "@lexical/code@^0.11.2":
|
"@lexical/code@0.11.3", "@lexical/code@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.11.2.tgz#add0bfd8fe0c76b0bf764e33c501c83a3fc0cb2d"
|
resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.11.3.tgz#4a5ef193655557859c63dd4a54012c78580585fa"
|
||||||
integrity sha512-xR+K8qtvSeKefNIvRCbDT2iNdl+5UKxDmQLh1ZqUeT5TBefXevnF0Hevek8xs1f+XSYwNeXjK/x1THD/i9e2jQ==
|
integrity sha512-BIMPd2op65iP4N9SkKIUVodZoWeSsnk6skNJ8UHBO/Rg0ZxyAqxLpnBhEgHq2QOoTBbEW6OEFtkc7/+f9LINZg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
prismjs "^1.27.0"
|
prismjs "^1.27.0"
|
||||||
|
|
||||||
"@lexical/dragon@0.11.2":
|
"@lexical/dragon@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.11.2.tgz#9a06676c18edb03644f14a233ed63bcdae8c466f"
|
resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.11.3.tgz#b4254953b09a68c20277ba0fb125548dd6630921"
|
||||||
integrity sha512-zJXeUiViYEbOEL9aXSYZmzku7SytZbD2pQ1FYQJ6H2o7UoK1WkYunRZkyxrx6uclQnZTUB1VO/OM2KuYlzl9wA==
|
integrity sha512-S18uwqOOpV2yIAFVWqSvBdhZ5BGadPQO4ejZF15wP8LUuqkxCs+0I/MjLovQ7tx0Cx34KdDaOXtM6XeG74ixYw==
|
||||||
|
|
||||||
"@lexical/hashtag@0.11.2", "@lexical/hashtag@^0.11.2":
|
"@lexical/hashtag@0.11.3", "@lexical/hashtag@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.11.2.tgz#266d0a8df48abbaac0ffa694b191ba25e71a5f20"
|
resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.11.3.tgz#9ffb2f7b2bb9a62fa9641355fc06feb9d209273a"
|
||||||
integrity sha512-nXtjIW8K+YLUBwh70vtO2oy5IpYiSb1XwM2Wn4c2yrzKk7tOVs7JAVO48Vyag0DsCpfLj1n+F2vd0zsLC147/A==
|
integrity sha512-7auoaWp2QhsX9/Bq0SxLXatUaSwqoT9HlWNTH2vKsw8tdeUBYacTHLuBNncTGrznXLG0/B5+FWoLuM6Pzqq4Ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/history@0.11.2":
|
"@lexical/history@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.11.2.tgz#1bcb9627ee469e038d582e43a3027d1c2bf41ff0"
|
resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.11.3.tgz#bcf7b2e708a5b3b5f90d1a39e07e0ae51bac41df"
|
||||||
integrity sha512-N2oaLhCrCt6AkAIba+sEzhPo5VrjaZDC/2Arn7/2q8vDnQc5WqoyNtEtFaYp+or5oI93C1jM9Qp7FB54lobAXA==
|
integrity sha512-QLJQRH2rbadRwXd4c/U4TqjLWDQna6Q43nCocIZF+SdVG9TlASp7m6dS7hiHfPtV1pkxJUxPhZY6EsB/Ok5WGA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/html@0.11.2":
|
"@lexical/html@0.11.3", "@lexical/html@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.11.2.tgz#d92e8e965cf0032d1ff4f8a1c6942a07264de76a"
|
resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.11.3.tgz#c02b38f512eb808726922c8215dd2374df6b77cc"
|
||||||
integrity sha512-QzPqci+FBoaOJcdqO2XXaiowUDVTkGN8y6ycZPjKYfu+fMDdkfSC6b3fdLfb99v6FluriVih39DJ8WQC0PcC1g==
|
integrity sha512-+8AYnxxml9PneZLkGfdTenqDjE2yD1ZfCmQLrD/L1TEn22OjZh4uvKVHb13wEhgUZTuLKF0PNdnuecko9ON/aQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/selection" "0.11.2"
|
"@lexical/selection" "0.11.3"
|
||||||
|
|
||||||
"@lexical/link@0.11.2", "@lexical/link@^0.11.2":
|
"@lexical/link@0.11.3", "@lexical/link@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.11.2.tgz#6807a62c76e9b8a53f1ac1630d958b56685e1756"
|
resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.11.3.tgz#7efab94e3b061a84639314eaec0b915d3457329c"
|
||||||
integrity sha512-/EYhUkWnQ7cjJDnNrl8SNCT8zNicryOIf8PIkRpDKnDgG3SQMzXV6NT6JRAebam913AenfcBLaje21zBul7m4w==
|
integrity sha512-stAjIrDrF18dPKK25ExPwMCcMe0KKD0FWVzo3F7ejh9DvrQcLFeBPcs8ze71chS3D5fQDB/CzdwvMjEViKmq2A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/list@0.11.2", "@lexical/list@^0.11.2":
|
"@lexical/list@0.11.3", "@lexical/list@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.11.2.tgz#983ba1a6065054ae34d44338a9d8a997ff854a2f"
|
resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.11.3.tgz#d158ac4b4b42d772b30a1cd2a42ca0462a5a154e"
|
||||||
integrity sha512-VK+CeJdWaZS/H2ivELxIZd8UTFAJuqkMMF1XzQqZDxYKus8TJWBsG2SwY3ouADpHifrBTufIZlQrLSwaqxrACA==
|
integrity sha512-Cs9071wDfqi4j1VgodceiR1jTHj13eCoEJDhr3e/FW0x5we7vfbTMtWlOWbveIoryAh+rQNgiD5e8SrAm6Zs3g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/mark@0.11.2":
|
"@lexical/mark@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.11.2.tgz#93df8a2e3a0d088cfc3618c4e44b5417b24ac96a"
|
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.11.3.tgz#7f87a264d44762e275ba7b3d85e18307bbabd9e3"
|
||||||
integrity sha512-OtOjQmsQNraTR7TQhMs6sUCAyT+903EZbP48BLqwIBEZNyRK8FsBh8boI68sdTBpypUmHfYpzn1aAJnLDocTTQ==
|
integrity sha512-0wAtufmaA0rMVFXoiJ0sY/tiJsQbHuDpgywb1Qa8qnZZcg7ZTrQMz9Go0fEWYcbSp8OH2o0cjbDTz3ACS1qCUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/markdown@0.11.2":
|
"@lexical/markdown@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.11.2.tgz#cb550e69cdbbac548b7e4f42a0e7a32e5a44b16b"
|
resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.11.3.tgz#0656a33bdf8b506899c010bb44960f86276e346b"
|
||||||
integrity sha512-4UyPSLuOFT1ezub5DPnwhbLPdaqWSTXSe7UBNw8Qt0/J3koAX4Kg099n+YcXJUwe04yJ00JKScsUjpDQXOOudw==
|
integrity sha512-sF8ow32BDme3UvxaKpf+j+vMc4T/XvDEzteZHmvvP7NX/iUtK3yUkTyT7rKuGwiKLYfMBwQaKMGjU3/nlIOzUg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/code" "0.11.2"
|
"@lexical/code" "0.11.3"
|
||||||
"@lexical/link" "0.11.2"
|
"@lexical/link" "0.11.3"
|
||||||
"@lexical/list" "0.11.2"
|
"@lexical/list" "0.11.3"
|
||||||
"@lexical/rich-text" "0.11.2"
|
"@lexical/rich-text" "0.11.3"
|
||||||
"@lexical/text" "0.11.2"
|
"@lexical/text" "0.11.3"
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/offset@0.11.2":
|
"@lexical/offset@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.11.2.tgz#fd2b409c528fef1eb6f626465f606ccd65f0f134"
|
resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.11.3.tgz#55c2ef5f036235d70f2aa762ed295e32dde9593a"
|
||||||
integrity sha512-9eYK8DV0DM24/mrMdJ9/6lOySIF10QUrePBDq9pzpk9i+S0cfyjnbLszUO+2Fk4/yiDUmMomSYa44K/kndqI3A==
|
integrity sha512-3H9X8iqDSk0LrMOHZuqYuqX4EYGb78TIhtjrFbLJi/OgKmHaSeLx59xcMZdgd5kBdRitzQYMmvbRDvbLfMgWrA==
|
||||||
|
|
||||||
"@lexical/overflow@0.11.2":
|
"@lexical/overflow@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.11.2.tgz#f96674969f67fc556e82b9eebf237b80c4e39ef9"
|
resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.11.3.tgz#a77d72a4fdf8dcc4f558e68a5d0ac210f020472b"
|
||||||
integrity sha512-59c8Kk+ee2jU1RlnosIg323uWrRCIB9aYy7FMsKdH0Hhl+ZhkKI4BVA8rT0SINoq3kA//XwiCWclOQzm2Jmshw==
|
integrity sha512-ShjCG8lICShOBKwrpP+9PjRFKEBCSUUMjbIGZfLnoL//3hyRtGv5aRgRyfJlRgDhCve0ROt5znLJV88EXzGRyA==
|
||||||
|
|
||||||
"@lexical/plain-text@0.11.2":
|
"@lexical/plain-text@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.11.2.tgz#df69ccd1899411ff3ef51742399b3f10a05a77ab"
|
resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.11.3.tgz#ee56c0f0bc10a6333dc9790fb6f27b8d41887da8"
|
||||||
integrity sha512-WVipqB8ltK5tXr/5ZYeJp5ry/SVAXEffVgWqjj30D35nbekNDATkg/qzihm3wUVf1dB87U+nI2oiFJWLoIASuQ==
|
integrity sha512-cQ5Us+GNzShyjjgRqWTnYv0rC+jHJ96LvBA1aSieM77H8/Im5BeoLl6TgBK2NqPkp8fGpj8JnDEdT8h9Qh1jtA==
|
||||||
|
|
||||||
"@lexical/react@^0.11.2":
|
"@lexical/react@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.11.2.tgz#b86683f31f4ee365afafeceb4172819e35299abd"
|
resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.11.3.tgz#e889170cf29bf71e3c4799a104d4f4c99499fa2e"
|
||||||
integrity sha512-lRKdt+uc+MFQ5mQFUr/QpW9GtUdTAyDN0wlG/pLDYFIjU++kTrPhX/fK6SPevSRFm4lsLV2qKd8Ubj7ZOkmmJw==
|
integrity sha512-Rn0Agnrz3uLIWbNyS9PRlkxOxcIDl2kxaVfgBacqQtYKR0ZVB2Hnoi89Cq6VmWPovauPyryx4Q3FC8Y11X7Otg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/clipboard" "0.11.2"
|
"@lexical/clipboard" "0.11.3"
|
||||||
"@lexical/code" "0.11.2"
|
"@lexical/code" "0.11.3"
|
||||||
"@lexical/dragon" "0.11.2"
|
"@lexical/dragon" "0.11.3"
|
||||||
"@lexical/hashtag" "0.11.2"
|
"@lexical/hashtag" "0.11.3"
|
||||||
"@lexical/history" "0.11.2"
|
"@lexical/history" "0.11.3"
|
||||||
"@lexical/link" "0.11.2"
|
"@lexical/link" "0.11.3"
|
||||||
"@lexical/list" "0.11.2"
|
"@lexical/list" "0.11.3"
|
||||||
"@lexical/mark" "0.11.2"
|
"@lexical/mark" "0.11.3"
|
||||||
"@lexical/markdown" "0.11.2"
|
"@lexical/markdown" "0.11.3"
|
||||||
"@lexical/overflow" "0.11.2"
|
"@lexical/overflow" "0.11.3"
|
||||||
"@lexical/plain-text" "0.11.2"
|
"@lexical/plain-text" "0.11.3"
|
||||||
"@lexical/rich-text" "0.11.2"
|
"@lexical/rich-text" "0.11.3"
|
||||||
"@lexical/selection" "0.11.2"
|
"@lexical/selection" "0.11.3"
|
||||||
"@lexical/table" "0.11.2"
|
"@lexical/table" "0.11.3"
|
||||||
"@lexical/text" "0.11.2"
|
"@lexical/text" "0.11.3"
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
"@lexical/yjs" "0.11.2"
|
"@lexical/yjs" "0.11.3"
|
||||||
react-error-boundary "^3.1.4"
|
react-error-boundary "^3.1.4"
|
||||||
|
|
||||||
"@lexical/rich-text@0.11.2", "@lexical/rich-text@^0.11.2":
|
"@lexical/rich-text@0.11.3", "@lexical/rich-text@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.11.2.tgz#12e1cf8bf92191de30f78905240ec3e437f2698d"
|
resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.11.3.tgz#9501789bfe9671c220da7e95bf8589fa92cecf8c"
|
||||||
integrity sha512-/CvQPfmeFwKXSF3nYL5DBT3YLI8RKoIw2Stq0xHjvgo0iFtu0jgfkfbm3b1qxEeENpxkBKkOpq6b9j4GT2avew==
|
integrity sha512-fBFs6wMS7GFLbk+mzIWtwpP+EmnTZZ5bHpveuQ5wXONBuUuLcsYF5KO7UhLxXNLmiViV6lxatZPavEzgZdW7oQ==
|
||||||
|
|
||||||
"@lexical/selection@0.11.2", "@lexical/selection@^0.11.2":
|
"@lexical/selection@0.11.3", "@lexical/selection@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.11.2.tgz#8c9f6f1641611e561e3e7c7193678b390bf3ac24"
|
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.11.3.tgz#f7250fae305a84c6e264a413f5feab056aeabfa3"
|
||||||
integrity sha512-hRkanEZsTxEH4J9MBd6htvV10lXfSZLj2MrQrA8XIduHq5uVNwvCFQBALA+7pvnp12AamAuf/HeROzn93laiLw==
|
integrity sha512-15lQpcKT/vd7XZ5pnF1nb+kpKb72e9Yi1dVqieSxTeXkzt1cAZFKP3NB4RlhOKCv1N+glSBnjSxRwgsFfbD+NQ==
|
||||||
|
|
||||||
"@lexical/table@0.11.2":
|
"@lexical/table@0.11.3", "@lexical/table@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.11.2.tgz#a538611700ff04a26a9068e825e61e44887476ee"
|
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.11.3.tgz#9b1980d828d7a588aaffa4cb8c6bb61401729034"
|
||||||
integrity sha512-MxNKgO1XjbpkVakFgJ8dzkgIsvOJir9/jyJQ/dcePK+6RYnjzsSBHsKoZtdoZCVIWhmaV2HwDdHOdnbKpJ60Kw==
|
integrity sha512-EyRnN39CSPsMceADBR7Kf+sBHNpNQlPEkn/52epeDSnakR6s80woyrA3kIzKo6mLB4afvoqdYc7RfR96M9JLIA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.11.2"
|
"@lexical/utils" "0.11.3"
|
||||||
|
|
||||||
"@lexical/text@0.11.2":
|
"@lexical/text@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.11.2.tgz#1ec1fef938568b7b294d8153b3c5830be9502411"
|
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.11.3.tgz#81ce2b5cd0caa9d89372e52c3548173d9395ad3d"
|
||||||
integrity sha512-XZmALZpzV+k9xmt2bhclKRTGhPlyZIvrDsxVADb+MEx6GpJk+VqYJqLgVpha5RiWtWKEjFXfU4pnPz56L3v/jA==
|
integrity sha512-gCEN8lJyR6b+yaOwKWGj79pbOfCQPWU/PHWyoNFUkEJXn3KydCzr2EYb6ta2cvQWRQU4G2BClKCR56jL4NS+qg==
|
||||||
|
|
||||||
"@lexical/utils@0.11.2", "@lexical/utils@^0.11.2":
|
"@lexical/utils@0.11.3", "@lexical/utils@^0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.11.2.tgz#c8558b99e044a2428eb5aeb6f6ff0fc24ab244ee"
|
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.11.3.tgz#c4eb953289d29943974008c75f80c8d4e294e009"
|
||||||
integrity sha512-4wzEznp+Q4inCfF8XJmr+KWGGqFOk6o8dKH7DmyOBO7YjgzOsbX2Dz/ImVyjt0c3ROkLHisouBFb8GP5iWa0UA==
|
integrity sha512-vC4saCrlcmyIJnvrYKw1uYxZojlD1DCIBsFlgmO8kXyRYXjj+o/8PBdn2dsgSQ3rADrC2mUloOm/maekDcYe9Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/list" "0.11.2"
|
"@lexical/list" "0.11.3"
|
||||||
"@lexical/selection" "0.11.2"
|
"@lexical/selection" "0.11.3"
|
||||||
"@lexical/table" "0.11.2"
|
"@lexical/table" "0.11.3"
|
||||||
|
|
||||||
"@lexical/yjs@0.11.2":
|
"@lexical/yjs@0.11.3":
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.11.2.tgz#69a8202d19ee312320ef2fc21e124ac3d8d43af6"
|
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.11.3.tgz#b7049c0be85945fe0766b614256df8121bb05499"
|
||||||
integrity sha512-4lDAXP4jscNuG72Q8Jgb67YVJKrMO50lEIsKs3aEHKqpV1L3/2sgxekNy4s5uI0fIpMXxbsec4uxG8MuJO3UgQ==
|
integrity sha512-TLDQG2FSEw/aOfppEBb0wRlIuzJ57W//8ImfzyZvckSC12tvU0YKQQX8nQz/rybXdyfRy5eN+8gX5K2EyZx+pQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/offset" "0.11.2"
|
"@lexical/offset" "0.11.3"
|
||||||
|
|
||||||
"@mdn/browser-compat-data@^3.3.14":
|
"@mdn/browser-compat-data@^3.3.14":
|
||||||
version "3.3.14"
|
version "3.3.14"
|
||||||
|
@ -9358,10 +9358,10 @@ lexical-remark@^0.3.8:
|
||||||
unist-util-visit "^4.1.2"
|
unist-util-visit "^4.1.2"
|
||||||
zwitch "^2.0.4"
|
zwitch "^2.0.4"
|
||||||
|
|
||||||
lexical@^0.11.2:
|
lexical@^0.11.3:
|
||||||
version "0.11.2"
|
version "0.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.11.2.tgz#fe930c1cbeea1bec497a7d53c3e5543b6adb67d5"
|
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.11.3.tgz#1ad1a56a657eb55d1b9644733f271bf75a65cbe9"
|
||||||
integrity sha512-9pvMhn28Liac6uRVwhedIy7iw9IYTR6J1A2Ic+sfgdc5GbLBVZm1obCxazdbJzprRoQdrRUh7M4JJfQdCxC0MQ==
|
integrity sha512-xsMKgx/Fa+QHg/nweemU04lCy7TnEr8LyeDtsKUC7fIDN9wH3GqbnQ0+e3Hbg4FmxlhDCiPPt0GcZAROq3R8uw==
|
||||||
|
|
||||||
li@^1.3.0:
|
li@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
|
@ -11995,7 +11995,7 @@ regexp.prototype.flags@^1.3.1:
|
||||||
call-bind "^1.0.2"
|
call-bind "^1.0.2"
|
||||||
define-properties "^1.1.3"
|
define-properties "^1.1.3"
|
||||||
|
|
||||||
regexp.prototype.flags@^1.5.0:
|
regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb"
|
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb"
|
||||||
integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==
|
integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==
|
||||||
|
|
Ładowanie…
Reference in New Issue