chore: update some lexical plugins (#2297)
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
URL_MATCHER,
|
||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
|
||||
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({
|
||||
embedConfig,
|
||||
onClose,
|
||||
@@ -183,14 +195,26 @@ export function AutoEmbedDialog({
|
||||
}): JSX.Element {
|
||||
const [text, setText] = useState('')
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [embedResult, setEmbedResult] = useState<EmbedMatchResult | null>(null)
|
||||
|
||||
const urlMatch = URL_MATCHER.exec(text)
|
||||
const embedResult = text != null && urlMatch != null ? embedConfig.parseUrl(text) : null
|
||||
const validateText = useMemo(
|
||||
() =>
|
||||
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 result = await embedResult
|
||||
if (result != null) {
|
||||
embedConfig.insertNode(editor, result)
|
||||
const onClick = () => {
|
||||
if (embedResult != null) {
|
||||
embedConfig.insertNode(editor, embedResult)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
@@ -205,7 +229,9 @@ export function AutoEmbedDialog({
|
||||
value={text}
|
||||
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value)
|
||||
const { value } = e.target
|
||||
setText(value)
|
||||
validateText(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,28 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { AutoLinkPlugin as LexicalAutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
|
||||
import { COMMAND_PRIORITY_EDITOR, KEY_MODIFIER_COMMAND, $getSelection } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
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()@:%_+.~#?&//=]*)/
|
||||
|
||||
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 = [
|
||||
(text: string) => {
|
||||
const match = URL_MATCHER.exec(text)
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
const fullMatch = match[0]
|
||||
return {
|
||||
index: match.index,
|
||||
length: fullMatch.length,
|
||||
text: fullMatch,
|
||||
url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
|
||||
}
|
||||
},
|
||||
createLinkMatcherWithRegExp(URL_REGEX, (text) => {
|
||||
return text.startsWith('http') ? text : `https://${text}`
|
||||
}),
|
||||
createLinkMatcherWithRegExp(EMAIL_REGEX, (text) => {
|
||||
return `mailto:${text}`
|
||||
}),
|
||||
]
|
||||
|
||||
export default function AutoLinkPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
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} />
|
||||
</>
|
||||
)
|
||||
export default function LexicalAutoLinkPlugin(): JSX.Element {
|
||||
return <AutoLinkPlugin matchers={MATCHERS} />
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLEle
|
||||
const linkParent = $findMatchingParent(node, $isLinkNode)
|
||||
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
|
||||
|
||||
if (linkParent != null || autoLinkParent != null) {
|
||||
if (linkParent != null && autoLinkParent == null) {
|
||||
setIsLink(true)
|
||||
} else {
|
||||
setIsLink(false)
|
||||
|
||||
@@ -6,90 +6,13 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||
import {
|
||||
$createNodeSelection,
|
||||
$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 { LexicalEditor } from 'lexical'
|
||||
import { useState } from 'react'
|
||||
import Button from '../Lexical/UI/Button'
|
||||
import { DialogActions } from '../Lexical/UI/Dialog'
|
||||
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({
|
||||
activeEditor,
|
||||
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