chore: update some lexical plugins (#2297)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,390 +0,0 @@
|
|||||||
import {
|
|
||||||
DOMConversionMap,
|
|
||||||
DOMConversionOutput,
|
|
||||||
DOMExportOutput,
|
|
||||||
EditorConfig,
|
|
||||||
LexicalEditor,
|
|
||||||
LexicalNode,
|
|
||||||
NodeKey,
|
|
||||||
SerializedLexicalNode,
|
|
||||||
Spread,
|
|
||||||
DecoratorNode,
|
|
||||||
} from 'lexical'
|
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
import { Suspense } from 'react'
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
return 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(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
() => import('./TableComponent'),
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
type: 'tablesheet'
|
|
||||||
version: 1
|
|
||||||
},
|
|
||||||
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 != null ? rect.startX : 0; x < (rect != null ? rect.endX + 1 : firstRow.cells.length); x++) {
|
|
||||||
const col = document.createElement('col')
|
|
||||||
colGroup.append(col)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let y = rect != null ? rect.startY : 0; y < (rect != null ? rect.endY + 1 : rows.length); y++) {
|
|
||||||
const row = rows[y]
|
|
||||||
const cells = row.cells
|
|
||||||
const rowElem = document.createElement('tr')
|
|
||||||
|
|
||||||
for (let x = rect != null ? rect.startX : 0; x < (rect != null ? 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 override getType(): string {
|
|
||||||
return 'tablesheet'
|
|
||||||
}
|
|
||||||
|
|
||||||
static override clone(node: TableNode): TableNode {
|
|
||||||
return new TableNode(Array.from(node.__rows), node.__key)
|
|
||||||
}
|
|
||||||
|
|
||||||
static override importJSON(serializedNode: SerializedTableNode): TableNode {
|
|
||||||
return $createTableNode(serializedNode.rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
override exportJSON(): SerializedTableNode {
|
|
||||||
return {
|
|
||||||
rows: this.__rows,
|
|
||||||
type: 'tablesheet',
|
|
||||||
version: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
|
||||||
return {
|
|
||||||
table: (_node: Node) => ({
|
|
||||||
conversion: convertTableElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override exportDOM(): DOMExportOutput {
|
|
||||||
return { element: exportTableCellsToHTML(this.__rows) }
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(rows?: Rows, key?: NodeKey) {
|
|
||||||
super(key)
|
|
||||||
this.__rows = rows || []
|
|
||||||
}
|
|
||||||
|
|
||||||
override createDOM(): HTMLElement {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.style.display = 'contents'
|
|
||||||
return div
|
|
||||||
}
|
|
||||||
|
|
||||||
override 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Suspense>
|
|
||||||
<TableComponent nodeKey={this.__key} theme={config.theme} rows={this.__rows} />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,17 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { EditorConfig, ElementFormatType, LexicalEditor, LexicalNode, NodeKey, Spread } from 'lexical'
|
import type {
|
||||||
|
DOMConversionMap,
|
||||||
|
DOMConversionOutput,
|
||||||
|
DOMExportOutput,
|
||||||
|
EditorConfig,
|
||||||
|
ElementFormatType,
|
||||||
|
LexicalEditor,
|
||||||
|
LexicalNode,
|
||||||
|
NodeKey,
|
||||||
|
Spread,
|
||||||
|
} from 'lexical'
|
||||||
|
|
||||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
@@ -46,6 +56,15 @@ export type SerializedYouTubeNode = Spread<
|
|||||||
SerializedDecoratorBlockNode
|
SerializedDecoratorBlockNode
|
||||||
>
|
>
|
||||||
|
|
||||||
|
function convertYoutubeElement(domNode: HTMLElement): null | DOMConversionOutput {
|
||||||
|
const videoID = domNode.getAttribute('data-lexical-youtube')
|
||||||
|
if (videoID) {
|
||||||
|
const node = $createYouTubeNode(videoID)
|
||||||
|
return { node }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export class YouTubeNode extends DecoratorBlockNode {
|
export class YouTubeNode extends DecoratorBlockNode {
|
||||||
__id: string
|
__id: string
|
||||||
|
|
||||||
@@ -72,6 +91,36 @@ export class YouTubeNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportDOM(): DOMExportOutput {
|
||||||
|
const element = document.createElement('iframe')
|
||||||
|
element.setAttribute('data-lexical-youtube', this.__id)
|
||||||
|
element.setAttribute('width', '560')
|
||||||
|
element.setAttribute('height', '315')
|
||||||
|
element.setAttribute('src', `https://www.youtube.com/embed/${this.__id}`)
|
||||||
|
element.setAttribute('frameborder', '0')
|
||||||
|
element.setAttribute(
|
||||||
|
'allow',
|
||||||
|
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||||
|
)
|
||||||
|
element.setAttribute('allowfullscreen', 'true')
|
||||||
|
element.setAttribute('title', 'YouTube video')
|
||||||
|
return { element }
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM(): DOMConversionMap | null {
|
||||||
|
return {
|
||||||
|
iframe: (domNode: HTMLElement) => {
|
||||||
|
if (!domNode.hasAttribute('data-lexical-youtube')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conversion: convertYoutubeElement,
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||||
super(format, key)
|
super(format, key)
|
||||||
this.__id = id
|
this.__id = id
|
||||||
|
|||||||
@@ -33,5 +33,7 @@ export const IS_IOS: boolean = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.
|
|||||||
|
|
||||||
// Keep these in case we need to use them in the future.
|
// Keep these in case we need to use them in the future.
|
||||||
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
||||||
// export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
|
export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent)
|
||||||
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
|
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
|
||||||
|
|
||||||
|
export const IS_APPLE_WEBKIT = CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
URL_MATCHER,
|
URL_MATCHER,
|
||||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import * as ReactDOM from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import useModal from '../../Lexical/Hooks/useModal'
|
import useModal from '../../Lexical/Hooks/useModal'
|
||||||
@@ -174,6 +174,18 @@ function AutoEmbedMenu({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debounce = (callback: (text: string) => void, delay: number) => {
|
||||||
|
let timeoutId: number
|
||||||
|
|
||||||
|
return (text: string) => {
|
||||||
|
window.clearTimeout(timeoutId)
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
callback(text)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function AutoEmbedDialog({
|
export function AutoEmbedDialog({
|
||||||
embedConfig,
|
embedConfig,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -183,14 +195,26 @@ export function AutoEmbedDialog({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
const [embedResult, setEmbedResult] = useState<EmbedMatchResult | null>(null)
|
||||||
|
|
||||||
const urlMatch = URL_MATCHER.exec(text)
|
const validateText = useMemo(
|
||||||
const embedResult = text != null && urlMatch != null ? embedConfig.parseUrl(text) : null
|
() =>
|
||||||
|
debounce((inputText: string) => {
|
||||||
|
const urlMatch = URL_MATCHER.exec(inputText)
|
||||||
|
if (embedConfig != null && inputText != null && urlMatch != null) {
|
||||||
|
void Promise.resolve(embedConfig.parseUrl(inputText)).then((parseResult) => {
|
||||||
|
setEmbedResult(parseResult)
|
||||||
|
})
|
||||||
|
} else if (embedResult != null) {
|
||||||
|
setEmbedResult(null)
|
||||||
|
}
|
||||||
|
}, 200),
|
||||||
|
[embedConfig, embedResult],
|
||||||
|
)
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = () => {
|
||||||
const result = await embedResult
|
if (embedResult != null) {
|
||||||
if (result != null) {
|
embedConfig.insertNode(editor, embedResult)
|
||||||
embedConfig.insertNode(editor, result)
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +229,9 @@ export function AutoEmbedDialog({
|
|||||||
value={text}
|
value={text}
|
||||||
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setText(e.target.value)
|
const { value } = e.target
|
||||||
|
setText(value)
|
||||||
|
validateText(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,55 +1,28 @@
|
|||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
/**
|
||||||
import { AutoLinkPlugin as LexicalAutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
import { COMMAND_PRIORITY_EDITOR, KEY_MODIFIER_COMMAND, $getSelection } from 'lexical'
|
*
|
||||||
import { useEffect } from 'react'
|
* This source code is licensed under the MIT license found in the
|
||||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
* LICENSE file in the root directory of this source tree.
|
||||||
import { mergeRegister } from '@lexical/utils'
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
const URL_MATCHER =
|
import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin'
|
||||||
|
|
||||||
|
const URL_REGEX =
|
||||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
|
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
|
||||||
|
|
||||||
|
const EMAIL_REGEX =
|
||||||
|
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
|
||||||
|
|
||||||
const MATCHERS = [
|
const MATCHERS = [
|
||||||
(text: string) => {
|
createLinkMatcherWithRegExp(URL_REGEX, (text) => {
|
||||||
const match = URL_MATCHER.exec(text)
|
return text.startsWith('http') ? text : `https://${text}`
|
||||||
if (match === null) {
|
}),
|
||||||
return null
|
createLinkMatcherWithRegExp(EMAIL_REGEX, (text) => {
|
||||||
}
|
return `mailto:${text}`
|
||||||
const fullMatch = match[0]
|
}),
|
||||||
return {
|
|
||||||
index: match.index,
|
|
||||||
length: fullMatch.length,
|
|
||||||
text: fullMatch,
|
|
||||||
url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function AutoLinkPlugin(): JSX.Element | null {
|
export default function LexicalAutoLinkPlugin(): JSX.Element {
|
||||||
const [editor] = useLexicalComposerContext()
|
return <AutoLinkPlugin matchers={MATCHERS} />
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return mergeRegister(
|
|
||||||
editor.registerCommand(
|
|
||||||
KEY_MODIFIER_COMMAND,
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
const isCmdK = event.key === 'k' && !event.altKey && (event.metaKey || event.ctrlKey)
|
|
||||||
if (isCmdK) {
|
|
||||||
const selection = $getSelection()
|
|
||||||
if (selection) {
|
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, selection.getTextContent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LexicalAutoLinkPlugin matchers={MATCHERS} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLEle
|
|||||||
const linkParent = $findMatchingParent(node, $isLinkNode)
|
const linkParent = $findMatchingParent(node, $isLinkNode)
|
||||||
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
|
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
|
||||||
|
|
||||||
if (linkParent != null || autoLinkParent != null) {
|
if (linkParent != null && autoLinkParent == null) {
|
||||||
setIsLink(true)
|
setIsLink(true)
|
||||||
} else {
|
} else {
|
||||||
setIsLink(false)
|
setIsLink(false)
|
||||||
|
|||||||
@@ -6,90 +6,13 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
||||||
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||||
import {
|
import { LexicalEditor } from 'lexical'
|
||||||
$createNodeSelection,
|
import { useState } from 'react'
|
||||||
$createParagraphNode,
|
|
||||||
$getSelection,
|
|
||||||
$isRangeSelection,
|
|
||||||
$isRootOrShadowRoot,
|
|
||||||
$setSelection,
|
|
||||||
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 invariant from '../Lexical/Shared/invariant'
|
|
||||||
import { $createTableNodeWithDimensions, TableNode } from '../Lexical/Nodes/TableNode'
|
|
||||||
import Button from '../Lexical/UI/Button'
|
import Button from '../Lexical/UI/Button'
|
||||||
import { DialogActions } from '../Lexical/UI/Dialog'
|
import { DialogActions } from '../Lexical/UI/Dialog'
|
||||||
import TextInput from '../Lexical/UI/TextInput'
|
import TextInput from '../Lexical/UI/TextInput'
|
||||||
|
|
||||||
export type InsertTableCommandPayload = Readonly<{
|
|
||||||
columns: string
|
|
||||||
rows: string
|
|
||||||
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')
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: not sure why TS doesn't like using null as the value?
|
|
||||||
export const CellContext: React.Context<CellContextShape> = createContext({
|
|
||||||
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 InsertTableDialog({
|
export function InsertTableDialog({
|
||||||
activeEditor,
|
activeEditor,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -115,91 +38,3 @@ export function InsertTableDialog({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InsertNewTableDialog({
|
|
||||||
activeEditor,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
activeEditor: LexicalEditor
|
|
||||||
onClose: () => void
|
|
||||||
}): JSX.Element {
|
|
||||||
const [rows, setRows] = useState('5')
|
|
||||||
const [columns, setColumns] = useState('5')
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, { columns, rows })
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextInput label="No of rows" onChange={setRows} value={rows} />
|
|
||||||
<TextInput label="No of columns" onChange={setColumns} value={columns} />
|
|
||||||
<DialogActions data-test-id="table-model-confirm-insert">
|
|
||||||
<Button onClick={onClick}>Confirm</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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])) {
|
|
||||||
invariant(false, 'TablePlugin: TableNode is not registered on editor')
|
|
||||||
}
|
|
||||||
|
|
||||||
cellContext.set(cellEditorConfig, children)
|
|
||||||
|
|
||||||
return editor.registerCommand<InsertTableCommandPayload>(
|
|
||||||
INSERT_TABLE_COMMAND,
|
|
||||||
({ columns, rows, includeHeaders }) => {
|
|
||||||
const selection = $getSelection()
|
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const focus = selection.focus
|
|
||||||
const focusNode = focus.getNode()
|
|
||||||
|
|
||||||
if (focusNode !== null) {
|
|
||||||
const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders)
|
|
||||||
|
|
||||||
if ($isRootOrShadowRoot(focusNode)) {
|
|
||||||
const target = focusNode.getChildAtIndex(focus.offset)
|
|
||||||
|
|
||||||
if (target !== null) {
|
|
||||||
target.insertBefore(tableNode)
|
|
||||||
} else {
|
|
||||||
focusNode.append(tableNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
tableNode.insertBefore($createParagraphNode())
|
|
||||||
} else {
|
|
||||||
const topLevelNode = focusNode.getTopLevelElementOrThrow()
|
|
||||||
topLevelNode.insertAfter(tableNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
tableNode.insertAfter($createParagraphNode())
|
|
||||||
const nodeSelection = $createNodeSelection()
|
|
||||||
nodeSelection.add(tableNode.getKey())
|
|
||||||
$setSelection(nodeSelection)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
}, [cellContext, cellEditorConfig, children, editor])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user