From 48b60fc90509a291bbbb6380e71c821bdf8cc8c7 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 20 Nov 2024 21:19:18 -0700 Subject: [PATCH] chore(richtext-lexical): enable strict: true (#9394) Thanks to @GermanJablo for doing most of the work by enabling `noImplicitAny` and `strictNullChecks` in previous PRs --- .../blocks/client/component/index.tsx | 2 +- .../blocks/server/graphQLPopulationPromise.ts | 4 +- .../html/converter/defaultConverters.ts | 2 +- .../plugins/TableCellResizerPlugin/index.tsx | 48 ++++++---- .../plugins/TableHoverActionsPlugin/index.tsx | 2 + .../client/utils/useDebounce.ts | 40 ++++---- .../features/lists/checklist/server/index.ts | 2 +- .../lists/orderedList/server/index.ts | 2 +- .../lists/unorderedList/server/index.ts | 2 +- .../server/nodes/RelationshipNode.tsx | 9 +- .../toolbars/fixed/client/Toolbar/index.tsx | 45 ++++----- .../toolbars/inline/client/Toolbar/index.tsx | 22 ++--- .../toolbars/shared/ToolbarButton/index.tsx | 7 +- .../shared/ToolbarDropdown/DropDown.tsx | 8 +- .../toolbars/shared/ToolbarDropdown/index.tsx | 29 +++--- .../src/features/typesClient.ts | 95 ++++++++++--------- .../src/features/typesServer.ts | 1 - .../upload/client/nodes/UploadNode.tsx | 4 +- .../features/upload/client/plugin/index.tsx | 6 +- .../upload/server/nodes/UploadNode.tsx | 4 +- .../src/lexical/config/client/sanitize.ts | 2 +- .../LexicalMenu.tsx | 9 +- .../src/lexical/plugins/SlashMenu/index.tsx | 15 +-- packages/richtext-lexical/tsconfig.json | 1 + test/fields/payload-types.ts | 4 +- 25 files changed, 197 insertions(+), 168 deletions(-) diff --git a/packages/richtext-lexical/src/features/blocks/client/component/index.tsx b/packages/richtext-lexical/src/features/blocks/client/component/index.tsx index 9e3cd3873f6..f91e922b8e0 100644 --- a/packages/richtext-lexical/src/features/blocks/client/component/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/component/index.tsx @@ -162,7 +162,7 @@ export const BlockComponent: React.FC = (props) => { const { i18n, t } = useTranslation() const onChange = useCallback( - async ({ formState: prevFormState, submit }: { formState: FormState; submit: boolean }) => { + async ({ formState: prevFormState, submit }: { formState: FormState; submit?: boolean }) => { abortAndIgnore(onChangeAbortControllerRef.current) const controller = new AbortController() diff --git a/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts index 158fc0efdad..79c12486835 100644 --- a/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts +++ b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts @@ -9,7 +9,9 @@ import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/re export const blockPopulationPromiseHOC = ( blocks: Block[], ): PopulationPromise => { - const blockPopulationPromise: PopulationPromise = ({ + const blockPopulationPromise: PopulationPromise< + SerializedBlockNode | SerializedInlineBlockNode + > = ({ context, currentDepth, depth, diff --git a/packages/richtext-lexical/src/features/converters/html/converter/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/html/converter/defaultConverters.ts index 848d895a4ab..8802e10c1d8 100644 --- a/packages/richtext-lexical/src/features/converters/html/converter/defaultConverters.ts +++ b/packages/richtext-lexical/src/features/converters/html/converter/defaultConverters.ts @@ -4,7 +4,7 @@ import { LinebreakHTMLConverter } from './converters/linebreak.js' import { ParagraphHTMLConverter } from './converters/paragraph.js' import { TextHTMLConverter } from './converters/text.js' -export const defaultHTMLConverters: HTMLConverter[] = [ +export const defaultHTMLConverters: HTMLConverter[] = [ ParagraphHTMLConverter, TextHTMLConverter, LinebreakHTMLConverter, diff --git a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableCellResizerPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableCellResizerPlugin/index.tsx index 3d6b25a1e0c..4a35614e71b 100644 --- a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableCellResizerPlugin/index.tsx +++ b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableCellResizerPlugin/index.tsx @@ -314,7 +314,19 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { [activeCell, mouseUpHandler], ) - const getResizers = useCallback(() => { + const [resizerStyles, setResizerStyles] = useState<{ + bottom?: null | React.CSSProperties + left?: null | React.CSSProperties + right?: null | React.CSSProperties + top?: null | React.CSSProperties + }>({ + bottom: null, + left: null, + right: null, + top: null, + }) + + useEffect(() => { if (activeCell) { const { height, left, top, width } = activeCell.elem.getBoundingClientRect() const zoom = calculateZoomLevel(activeCell.elem) @@ -324,16 +336,16 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { backgroundColor: 'none', cursor: 'row-resize', height: `${zoneWidth}px`, - left: `${window.pageXOffset + left}px`, - top: `${window.pageYOffset + top + height - zoneWidth / 2}px`, + left: `${window.scrollX + left}px`, + top: `${window.scrollY + top + height - zoneWidth / 2}px`, width: `${width}px`, }, right: { backgroundColor: 'none', cursor: 'col-resize', height: `${height}px`, - left: `${window.pageXOffset + left + width - zoneWidth / 2}px`, - top: `${window.pageYOffset + top}px`, + left: `${window.scrollX + left + width - zoneWidth / 2}px`, + top: `${window.scrollY + top}px`, width: `${zoneWidth}px`, }, } @@ -342,13 +354,13 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { if (draggingDirection && mouseCurrentPos && tableRect) { if (isHeightChanging(draggingDirection)) { - styles[draggingDirection].left = `${window.pageXOffset + tableRect.left}px` - styles[draggingDirection].top = `${window.pageYOffset + mouseCurrentPos.y / zoom}px` + styles[draggingDirection].left = `${window.scrollX + tableRect.left}px` + styles[draggingDirection].top = `${window.scrollY + mouseCurrentPos.y / zoom}px` styles[draggingDirection].height = '3px' styles[draggingDirection].width = `${tableRect.width}px` } else { - styles[draggingDirection].top = `${window.pageYOffset + tableRect.top}px` - styles[draggingDirection].left = `${window.pageXOffset + mouseCurrentPos.x / zoom}px` + styles[draggingDirection].top = `${window.scrollY + tableRect.top}px` + styles[draggingDirection].left = `${window.scrollX + mouseCurrentPos.x / zoom}px` styles[draggingDirection].width = '3px' styles[draggingDirection].height = `${tableRect.height}px` } @@ -356,19 +368,17 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { styles[draggingDirection].backgroundColor = '#adf' } - return styles - } - - return { - bottom: null, - left: null, - right: null, - top: null, + setResizerStyles(styles) + } else { + setResizerStyles({ + bottom: null, + left: null, + right: null, + top: null, + }) } }, [activeCell, draggingDirection, mouseCurrentPos]) - const resizerStyles = getResizers() - return (
{activeCell != null && !isMouseDown && ( diff --git a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx index 74336268edd..ca912b4fccc 100644 --- a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx @@ -218,6 +218,7 @@ function TableHoverActionsContainer({ className={editorConfig.editorConfig.lexical.theme.tableAddRows} onClick={() => insertAction(true)} style={{ ...position }} + type="button" /> )} {isShownColumn && ( @@ -225,6 +226,7 @@ function TableHoverActionsContainer({ className={editorConfig.editorConfig.lexical.theme.tableAddColumns} onClick={() => insertAction(false)} style={{ ...position }} + type="button" /> )} diff --git a/packages/richtext-lexical/src/features/experimental_table/client/utils/useDebounce.ts b/packages/richtext-lexical/src/features/experimental_table/client/utils/useDebounce.ts index 5549cc0b6cf..ff472735d15 100644 --- a/packages/richtext-lexical/src/features/experimental_table/client/utils/useDebounce.ts +++ b/packages/richtext-lexical/src/features/experimental_table/client/utils/useDebounce.ts @@ -1,27 +1,35 @@ 'use client' -import { useMemo, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' import debounce from './debounce.js' +// Define the type for debounced function that includes cancel method +interface DebouncedFunction any> { + (...args: Parameters): ReturnType + cancel: () => void +} + export function useDebounce void>( fn: T, ms: number, maxWait?: number, ) { - const funcRef = useRef(null) - funcRef.current = fn + // Update the ref type to include cancel method + const debouncedRef = useRef | null>(null) + + useEffect(() => { + debouncedRef.current = debounce(fn, ms, { maxWait }) as DebouncedFunction + + return () => { + debouncedRef.current?.cancel() + } + }, [fn, ms, maxWait]) + + const callback = useCallback((...args: Parameters) => { + if (debouncedRef.current) { + debouncedRef.current(...args) + } + }, []) - return useMemo( - () => - debounce( - (...args: Parameters) => { - if (funcRef.current) { - funcRef.current(...args) - } - }, - ms, - { maxWait }, - ), - [ms, maxWait], - ) + return callback } diff --git a/packages/richtext-lexical/src/features/lists/checklist/server/index.ts b/packages/richtext-lexical/src/features/lists/checklist/server/index.ts index d97d215a3b4..5ddaa68a5d0 100644 --- a/packages/richtext-lexical/src/features/lists/checklist/server/index.ts +++ b/packages/richtext-lexical/src/features/lists/checklist/server/index.ts @@ -18,7 +18,7 @@ export const ChecklistFeature = createServerFeature({ : [ createNode({ converters: { - html: ListHTMLConverter, + html: ListHTMLConverter as any, // ListHTMLConverter uses a different generic type than ListNode[exportJSON], thus we need to cast as any }, node: ListNode, }), diff --git a/packages/richtext-lexical/src/features/lists/orderedList/server/index.ts b/packages/richtext-lexical/src/features/lists/orderedList/server/index.ts index b681b5dd197..a497edb52bf 100644 --- a/packages/richtext-lexical/src/features/lists/orderedList/server/index.ts +++ b/packages/richtext-lexical/src/features/lists/orderedList/server/index.ts @@ -17,7 +17,7 @@ export const OrderedListFeature = createServerFeature({ : [ createNode({ converters: { - html: ListHTMLConverter, + html: ListHTMLConverter as any, // ListHTMLConverter uses a different generic type than ListNode[exportJSON], thus we need to cast as any }, node: ListNode, }), diff --git a/packages/richtext-lexical/src/features/lists/unorderedList/server/index.ts b/packages/richtext-lexical/src/features/lists/unorderedList/server/index.ts index e9907ef2ed5..6ed27d42768 100644 --- a/packages/richtext-lexical/src/features/lists/unorderedList/server/index.ts +++ b/packages/richtext-lexical/src/features/lists/unorderedList/server/index.ts @@ -14,7 +14,7 @@ export const UnorderedListFeature = createServerFeature({ nodes: [ createNode({ converters: { - html: ListHTMLConverter, + html: ListHTMLConverter as any, // ListHTMLConverter uses a different generic type than ListNode[exportJSON], thus we need to cast as any }, node: ListNode, }), diff --git a/packages/richtext-lexical/src/features/relationship/server/nodes/RelationshipNode.tsx b/packages/richtext-lexical/src/features/relationship/server/nodes/RelationshipNode.tsx index e5620790c7b..48ccb24f40d 100644 --- a/packages/richtext-lexical/src/features/relationship/server/nodes/RelationshipNode.tsx +++ b/packages/richtext-lexical/src/features/relationship/server/nodes/RelationshipNode.tsx @@ -104,13 +104,16 @@ export class RelationshipServerNode extends DecoratorBlockNode { return false } - decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null { + decorate(_editor: LexicalEditor, _config: EditorConfig): JSX.Element | null { return null } exportDOM(): DOMExportOutput { const element = document.createElement('div') - element.setAttribute('data-lexical-relationship-id', String(this.__data?.value)) + element.setAttribute( + 'data-lexical-relationship-id', + String(typeof this.__data?.value === 'object' ? this.__data?.value?.id : this.__data?.value), + ) element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo) const text = document.createTextNode(this.getTextContent()) @@ -132,7 +135,7 @@ export class RelationshipServerNode extends DecoratorBlockNode { } getTextContent(): string { - return `${this.__data?.relationTo} relation to ${this.__data?.value}` + return `${this.__data?.relationTo} relation to ${typeof this.__data?.value === 'object' ? this.__data?.value?.id : this.__data?.value}` } setData(data: RelationshipData): void { diff --git a/packages/richtext-lexical/src/features/toolbars/fixed/client/Toolbar/index.tsx b/packages/richtext-lexical/src/features/toolbars/fixed/client/Toolbar/index.tsx index 9f1a46cb74e..a2e5b670002 100644 --- a/packages/richtext-lexical/src/features/toolbars/fixed/client/Toolbar/index.tsx +++ b/packages/richtext-lexical/src/features/toolbars/fixed/client/Toolbar/index.tsx @@ -8,7 +8,7 @@ import { useMemo } from 'react' import type { EditorConfigContextType } from '../../../../../lexical/config/client/EditorConfigProvider.js' import type { SanitizedClientEditorConfig } from '../../../../../lexical/config/types.js' -import type { PluginComponentWithAnchor } from '../../../../typesClient.js' +import type { PluginComponent } from '../../../../typesClient.js' import type { ToolbarGroup, ToolbarGroupItem } from '../../../types.js' import type { FixedToolbarFeatureProps } from '../../server/index.js' @@ -58,7 +58,7 @@ function ToolbarGroupComponent({ group: ToolbarGroup index: number }): React.ReactNode { - const { i18n } = useTranslation() + const { i18n } = useTranslation<{}, string>() const { fieldProps: { featureClientSchemaMap, schemaPath }, } = useEditorConfigContext() @@ -106,9 +106,8 @@ function ToolbarGroupComponent({ return (
- {group.type === 'dropdown' && - group.items.length && - (DropdownIcon ? ( + {group.type === 'dropdown' && group.items.length ? ( + DropdownIcon ? ( - ))} - {group.type === 'buttons' && - group.items.length && - group.items.map((item) => { - return ( - - ) - })} + ) + ) : null} + {group.type === 'buttons' && group.items.length + ? group.items.map((item) => { + return ( + + ) + }) + : null} {index < editorConfig.features.toolbarFixed?.groups.length - 1 &&
}
) @@ -196,14 +196,18 @@ function FixedToolbar({ ) if (overlapping) { - currentToolbarElem.className = 'fixed-toolbar fixed-toolbar--overlapping' - parentToolbarElem.className = 'fixed-toolbar fixed-toolbar--hide' + currentToolbarElem.classList.remove('fixed-toolbar') + currentToolbarElem.classList.add('fixed-toolbar', 'fixed-toolbar--overlapping') + parentToolbarElem.classList.remove('fixed-toolbar') + parentToolbarElem.classList.add('fixed-toolbar', 'fixed-toolbar--hide') } else { if (!currentToolbarElem.classList.contains('fixed-toolbar--overlapping')) { return } - currentToolbarElem.className = 'fixed-toolbar' - parentToolbarElem.className = 'fixed-toolbar' + currentToolbarElem.classList.remove('fixed-toolbar--overlapping') + currentToolbarElem.classList.add('fixed-toolbar') + parentToolbarElem.classList.remove('fixed-toolbar--hide') + parentToolbarElem.classList.add('fixed-toolbar') } }, 50, @@ -256,10 +260,7 @@ const getParentEditorWithFixedToolbar = ( return false } -export const FixedToolbarPlugin: PluginComponentWithAnchor = ({ - anchorElem, - clientProps, -}) => { +export const FixedToolbarPlugin: PluginComponent = ({ clientProps }) => { const [currentEditor] = useLexicalComposerContext() const editorConfigContext = useEditorConfigContext() @@ -287,7 +288,7 @@ export const FixedToolbarPlugin: PluginComponentWithAnchor - {group.type === 'dropdown' && - group.items.length && - (DropdownIcon ? ( + {group.type === 'dropdown' && group.items.length ? ( + DropdownIcon ? ( - ))} - {group.type === 'buttons' && - group.items.length && - group.items.map((item) => { - return ( - - ) - })} + ) + ) : null} + {group.type === 'buttons' && group.items.length + ? group.items.map((item) => { + return ( + + ) + }) + : null} {index < editorConfig.features.toolbarInline?.groups.length - 1 && (
)} diff --git a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarButton/index.tsx b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarButton/index.tsx index 1ab09fb2525..91ca2ad90e9 100644 --- a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarButton/index.tsx +++ b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarButton/index.tsx @@ -2,7 +2,7 @@ import type { LexicalEditor } from 'lexical' import { mergeRegister } from '@lexical/utils' -import { $getSelection } from 'lexical' +import { $addUpdateTag, $getSelection } from 'lexical' import React, { useCallback, useEffect, useState } from 'react' import type { ToolbarGroupItem } from '../../types.js' @@ -84,9 +84,10 @@ export const ToolbarButton = ({ className={className} onClick={() => { if (enabled !== false) { - editor._updateTags = new Set(['toolbar', ...editor._updateTags]) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored. - editor.focus(() => { + editor.update(() => { + $addUpdateTag('toolbar') + }) // We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called. item.onSelect?.({ editor, diff --git a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/DropDown.tsx b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/DropDown.tsx index 1daae08ad94..5d616f745bf 100644 --- a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/DropDown.tsx +++ b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/DropDown.tsx @@ -1,7 +1,6 @@ 'use client' -import type { LexicalEditor } from 'lexical' - import { Button } from '@payloadcms/ui' +import { $addUpdateTag, type LexicalEditor } from 'lexical' import React, { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' @@ -74,9 +73,10 @@ export function DropDownItem({ iconStyle="none" onClick={() => { if (enabled !== false) { - editor._updateTags = new Set(['toolbar', ...editor._updateTags]) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored. - editor.focus(() => { + editor.update(() => { + $addUpdateTag('toolbar') + }) // We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called. item.onSelect?.({ editor, diff --git a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/index.tsx b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/index.tsx index a78e265798d..344fd4dbab9 100644 --- a/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/index.tsx +++ b/packages/richtext-lexical/src/features/toolbars/shared/ToolbarDropdown/index.tsx @@ -28,7 +28,7 @@ const ToolbarItem = ({ enabled?: boolean item: ToolbarGroupItem }) => { - const { i18n } = useTranslation() + const { i18n } = useTranslation<{}, string>() const { fieldProps: { featureClientSchemaMap, schemaPath }, } = useEditorConfigContext() @@ -173,19 +173,20 @@ export const ToolbarDropdown = ({ key={groupKey} label={label} > - {items.length && - items.map((item) => { - return ( - - ) - })} + {items.length + ? items.map((item) => { + return ( + + ) + }) + : null} ) } diff --git a/packages/richtext-lexical/src/features/typesClient.ts b/packages/richtext-lexical/src/features/typesClient.ts index e329c2fba30..57bfe2d3465 100644 --- a/packages/richtext-lexical/src/features/typesClient.ts +++ b/packages/richtext-lexical/src/features/typesClient.ts @@ -1,6 +1,7 @@ import type { Klass, LexicalEditor, LexicalNode, LexicalNodeReplacement } from 'lexical' import type { RichTextFieldClient } from 'payload' import type React from 'react' +import type { JSX } from 'react' import type { ClientEditorConfig } from '../lexical/config/types.js' import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js' @@ -47,6 +48,52 @@ export type PluginComponentWithAnchor = React.FC<{ clientProps: ClientFeatureProps }> +/** + * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality + */ +export type SanitizedPlugin = + | { + clientProps: any + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: PluginComponent + key: string + position: 'bottom' // Determines at which position the Component will be added. + } + | { + clientProps: any + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: PluginComponent + key: string + position: 'normal' // Determines at which position the Component will be added. + } + | { + clientProps: any + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: PluginComponent + key: string + position: 'top' // Determines at which position the Component will be added. + } + | { + clientProps: any + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: PluginComponentWithAnchor + desktopOnly?: boolean + key: string + position: 'floatingAnchorElem' // Determines at which position the Component will be added. + } + | { + clientProps: any + Component: PluginComponent + key: string + position: 'aboveContainer' + } + | { + clientProps: any + Component: PluginComponent + key: string + position: 'belowContainer' + } + export type ClientFeature = { markdownTransformers?: ( | ((props: { @@ -93,7 +140,7 @@ export type ClientFeature = { /** * Client Features can register their own providers, which will be nested below the EditorConfigProvider */ - providers?: Array + providers?: Array> /** * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to */ @@ -154,52 +201,6 @@ export type ResolvedClientFeatureMap = Map> export type ClientFeatureProviderMap = Map> -/** - * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality - */ -export type SanitizedPlugin = - | { - clientProps: any - // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality - Component: PluginComponent - key: string - position: 'bottom' // Determines at which position the Component will be added. - } - | { - clientProps: any - // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality - Component: PluginComponent - key: string - position: 'normal' // Determines at which position the Component will be added. - } - | { - clientProps: any - // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality - Component: PluginComponent - key: string - position: 'top' // Determines at which position the Component will be added. - } - | { - clientProps: any - // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality - Component: PluginComponentWithAnchor - desktopOnly?: boolean - key: string - position: 'floatingAnchorElem' // Determines at which position the Component will be added. - } - | { - clientProps: any - Component: PluginComponent - key: string - position: 'aboveContainer' - } - | { - clientProps: any - Component: PluginComponent - key: string - position: 'belowContainer' - } - export type SanitizedClientFeatures = { /** The keys of all enabled features */ enabledFeatures: string[] diff --git a/packages/richtext-lexical/src/features/typesServer.ts b/packages/richtext-lexical/src/features/typesServer.ts index 5cc53c6f248..0b3b7c99cbe 100644 --- a/packages/richtext-lexical/src/features/typesServer.ts +++ b/packages/richtext-lexical/src/features/typesServer.ts @@ -12,7 +12,6 @@ import type { Field, FieldSchemaMap, JsonObject, - Payload, PayloadComponent, PayloadRequest, PopulateType, diff --git a/packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx b/packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx index 0bafd3b0af2..e414e37cd59 100644 --- a/packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx +++ b/packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx @@ -57,9 +57,9 @@ export class UploadNode extends UploadServerNode { return super.getType() } - static importDOM(): DOMConversionMap | null { + static importDOM(): DOMConversionMap { return { - img: (node: HTMLImageElement) => ({ + img: (node) => ({ conversion: $convertUploadElement, priority: 0, }), diff --git a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx index 6ad94774db4..c3c6d08407b 100644 --- a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx @@ -14,7 +14,7 @@ import { } from 'lexical' import React, { useEffect } from 'react' -import type { PluginComponentWithAnchor } from '../../../typesClient.js' +import type { PluginComponent } from '../../../typesClient.js' import type { UploadData } from '../../server/nodes/UploadNode.js' import type { UploadFeaturePropsClient } from '../feature.client.js' @@ -26,9 +26,7 @@ export type InsertUploadPayload = Readonly & Partial = createCommand('INSERT_UPLOAD_COMMAND') -export const UploadPlugin: PluginComponentWithAnchor = ({ - clientProps, -}) => { +export const UploadPlugin: PluginComponent = ({ clientProps }) => { const [editor] = useLexicalComposerContext() const { config: { collections }, diff --git a/packages/richtext-lexical/src/features/upload/server/nodes/UploadNode.tsx b/packages/richtext-lexical/src/features/upload/server/nodes/UploadNode.tsx index a3cb8ce3948..ce3965cfd16 100644 --- a/packages/richtext-lexical/src/features/upload/server/nodes/UploadNode.tsx +++ b/packages/richtext-lexical/src/features/upload/server/nodes/UploadNode.tsx @@ -97,9 +97,9 @@ export class UploadServerNode extends DecoratorBlockNode { return 'upload' } - static importDOM(): DOMConversionMap | null { + static importDOM(): DOMConversionMap { return { - img: (node: HTMLImageElement) => ({ + img: (node) => ({ conversion: $convertUploadServerElement, priority: 0, }), diff --git a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts index b1c7d660fea..9ba4847f080 100644 --- a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts +++ b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts @@ -49,7 +49,7 @@ export const sanitizeClientFeatures = ( feature.plugins.forEach((plugin, i) => { sanitized.plugins?.push({ clientProps: feature.sanitizedClientFeatureProps, - Component: plugin.Component, + Component: plugin.Component as any, // Appeases strict: true key: feature.key + i, position: plugin.position, }) diff --git a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx index b442780fcd4..9c4e5d54b39 100644 --- a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx +++ b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx @@ -147,12 +147,13 @@ function isTriggerVisibleInNearestScrollContainer( // Reposition the menu on scroll, window resize, and element resize. export function useDynamicPositioning( resolution: MenuResolution | null, - targetElement: HTMLElement | null, + targetElementRef: RefObject, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void, ) { const [editor] = useLexicalComposerContext() useEffect(() => { + const targetElement = targetElementRef.current if (targetElement != null && resolution != null) { const rootElement = editor.getRootElement() const rootScrollParent = @@ -186,12 +187,12 @@ export function useDynamicPositioning( }) resizeObserver.observe(targetElement) return () => { - resizeObserver.unobserve(targetElement) + resizeObserver.disconnect() window.removeEventListener('resize', onReposition) document.removeEventListener('scroll', handleScroll, true) } } - }, [targetElement, editor, onVisibilityChange, onReposition, resolution]) + }, [editor, onVisibilityChange, onReposition, resolution, targetElementRef]) } export const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND: LexicalCommand<{ @@ -529,7 +530,7 @@ export function useMenuAnchorRef( [resolution, setResolution], ) - useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange) + useDynamicPositioning(resolution, anchorElementRef, positionMenu, onVisibilityChange) return anchorElementRef } diff --git a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.tsx b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.tsx index 53dcd7dc78a..c652c77f448 100644 --- a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.tsx +++ b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.tsx @@ -1,6 +1,4 @@ 'use client' -import type { TextNode } from 'lexical' - import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' import { useTranslation } from '@payloadcms/ui' import { useCallback, useMemo, useState } from 'react' @@ -26,18 +24,20 @@ function SlashMenuItem({ item, onClick, onMouseEnter, + ref, }: { index: number isSelected: boolean item: SlashMenuItemInternal onClick: () => void onMouseEnter: () => void + ref?: React.Ref }) { const { fieldProps: { featureClientSchemaMap, schemaPath }, } = useEditorConfigContext() - const { i18n } = useTranslation() + const { i18n } = useTranslation<{}, string>() let className = `${baseClass}__item ${baseClass}__item-${item.key}` if (isSelected) { @@ -64,9 +64,7 @@ function SlashMenuItem({ key={item.key} onClick={onClick} onMouseEnter={onMouseEnter} - ref={(element) => { - item.ref = { current: element } - }} + ref={ref} role="option" tabIndex={-1} type="button" @@ -86,7 +84,7 @@ export function SlashMenuPlugin({ const [editor] = useLexicalComposerContext() const [queryString, setQueryString] = useState(null) const { editorConfig } = useEditorConfigContext() - const { i18n } = useTranslation() + const { i18n } = useTranslation<{}, string>() const { fieldProps: { featureClientSchemaMap, schemaPath }, } = useEditorConfigContext() @@ -231,6 +229,9 @@ export function SlashMenuPlugin({ onMouseEnter={() => { setSelectedItemKey(item.key) }} + ref={(el) => { + ;(item as SlashMenuItemInternal).ref = { current: el } + }} /> ))}
diff --git a/packages/richtext-lexical/tsconfig.json b/packages/richtext-lexical/tsconfig.json index 3d9f932f35e..7a795018fd4 100644 --- a/packages/richtext-lexical/tsconfig.json +++ b/packages/richtext-lexical/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitAny": true, "outDir": "./dist" /* Specify an output folder for all emitted files. */, "rootDir": "./src" /* Specify the root folder within your source files. */, + "strict": true }, "exclude": [ "dist", diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 218df9acd38..68ad61a01d7 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -121,9 +121,9 @@ export interface Config { user: User & { collection: 'users'; }; - jobs?: { + jobs: { tasks: unknown; - workflows?: unknown; + workflows: unknown; }; } export interface UserAuthOperations {