Migrate to TS strict mode 6/n (#2496)

* Make suggested amends from @trueadm

* Migrate files from playground
This commit is contained in:
John Flockton
2022-06-23 08:34:35 +01:00
committed by GitHub
parent d50b9fc4f4
commit b70d217b28
57 changed files with 541 additions and 332 deletions

View File

@ -175,7 +175,9 @@ function $createNodesFromDOM(
let currentLexicalNode = null;
const transformFunction = getConversionFunction(node, editor);
const transformOutput = transformFunction ? transformFunction(node) : null;
const transformOutput = transformFunction
? transformFunction(node as HTMLElement)
: null;
let postTransform = null;
if (transformOutput !== null) {

View File

@ -122,7 +122,7 @@ function App(): JSX.Element {
: prepopulatedRichText,
namespace: 'Playground',
nodes: [...PlaygroundNodes],
onError: (error) => {
onError: (error: Error) => {
throw error;
},
theme: PlaygroundEditorTheme,
@ -141,8 +141,8 @@ function App(): JSX.Element {
<Editor />
</div>
<Settings />
{isDevPlayground && <TestRecorderPlugin />}
{measureTypingPerf && <TypingPerfPlugin />}
{isDevPlayground ? <TestRecorderPlugin /> : null}
{measureTypingPerf ? <TypingPerfPlugin /> : null}
</SharedAutocompleteContext>
</SharedHistoryContext>
</LexicalComposer>

View File

@ -12,7 +12,13 @@ import {TOGGLE_CONNECT_COMMAND} from '@lexical/yjs';
import {COMMAND_PRIORITY_EDITOR} from 'lexical';
import {useEffect, useState} from 'react';
import {WebsocketProvider} from 'y-websocket';
import {Array as YArray, Map as YMap, YArrayEvent} from 'yjs';
import {
Array as YArray,
Map as YMap,
Transaction,
YArrayEvent,
YEvent,
} from 'yjs';
export type Comment = {
author: string;
@ -109,7 +115,9 @@ export class CommentStore {
offset?: number,
): void {
const nextComments = Array.from(this._comments);
const sharedCommentsArray = this._getCollabComments();
// The YJS types explicitly use `any` as well.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sharedCommentsArray: YArray<any> | null = this._getCollabComments();
if (thread !== undefined && commentOrThread.type === 'comment') {
for (let i = 0; i < nextComments.length; i++) {
@ -118,7 +126,7 @@ export class CommentStore {
const newThread = cloneThread(comment);
nextComments.splice(i, 1, newThread);
const insertOffset = offset || newThread.comments.length;
if (this.isCollaborative()) {
if (this.isCollaborative() && sharedCommentsArray !== null) {
const parentSharedArray = sharedCommentsArray
.get(i)
.get('comments');
@ -133,7 +141,7 @@ export class CommentStore {
}
} else {
const insertOffset = offset || nextComments.length;
if (this.isCollaborative()) {
if (this.isCollaborative() && sharedCommentsArray !== null) {
this._withRemoteTransaction(() => {
const sharedMap = this._createCollabSharedMap(commentOrThread);
sharedCommentsArray.insert(nextComments.length, [sharedMap]);
@ -147,7 +155,9 @@ export class CommentStore {
deleteComment(commentOrThread: Comment | Thread, thread?: Thread): void {
const nextComments = Array.from(this._comments);
const sharedCommentsArray = this._getCollabComments();
// The YJS types explicitly use `any` as well.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sharedCommentsArray: YArray<any> | null = this._getCollabComments();
if (thread !== undefined) {
for (let i = 0; i < nextComments.length; i++) {
@ -158,7 +168,7 @@ export class CommentStore {
const threadComments = newThread.comments;
if (threadComments.length === 1) {
const threadIndex = nextComments.indexOf(newThread);
if (this.isCollaborative()) {
if (this.isCollaborative() && sharedCommentsArray !== null) {
this._withRemoteTransaction(() => {
sharedCommentsArray.delete(threadIndex);
});
@ -166,7 +176,7 @@ export class CommentStore {
nextComments.splice(threadIndex, 1);
} else {
const index = threadComments.indexOf(commentOrThread as Comment);
if (this.isCollaborative()) {
if (this.isCollaborative() && sharedCommentsArray !== null) {
const parentSharedArray = sharedCommentsArray
.get(i)
.get('comments');
@ -181,7 +191,7 @@ export class CommentStore {
}
} else {
const index = nextComments.indexOf(commentOrThread);
if (this.isCollaborative()) {
if (this.isCollaborative() && sharedCommentsArray !== null) {
this._withRemoteTransaction(() => {
sharedCommentsArray.delete(index);
});
@ -292,7 +302,12 @@ export class CommentStore {
COMMAND_PRIORITY_EDITOR,
);
const onSharedCommentChanges = (events, transaction) => {
const onSharedCommentChanges = (
// The YJS types explicitly use `any` as well.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
events: Array<YEvent<any>>,
transaction: Transaction,
) => {
if (transaction.origin !== this) {
for (let i = 0; i < events.length; i++) {
const event = events[i];
@ -329,12 +344,13 @@ export class CommentStore {
map
.get('comments')
.toArray()
.map((innerComment) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((innerComment: Map<string, string | number>) =>
createComment(
innerComment.get('content'),
innerComment.get('author'),
innerComment.get('id'),
innerComment.get('timeStamp'),
innerComment.get('content') as string,
innerComment.get('author') as string,
innerComment.get('id') as string,
innerComment.get('timeStamp') as number,
),
),
id,
@ -346,7 +362,9 @@ export class CommentStore {
map.get('timeStamp'),
);
this._withLocalTransaction(() => {
this.addComment(commentOrThread, parentThread, offset);
if (parentThread !== undefined && parentThread !== false) {
this.addComment(commentOrThread, parentThread, offset);
}
});
});
} else if (typeof retain === 'number') {
@ -354,11 +372,13 @@ export class CommentStore {
} else if (typeof del === 'number') {
for (let d = 0; d < del; d++) {
const commentOrThread =
parentThread === undefined
parentThread === undefined || parentThread === false
? this._comments[offset]
: parentThread.comments[offset];
this._withLocalTransaction(() => {
this.deleteComment(commentOrThread, parentThread);
if (parentThread !== undefined && parentThread !== false) {
this.deleteComment(commentOrThread, parentThread);
}
});
offset++;
}
@ -369,6 +389,10 @@ export class CommentStore {
}
};
if (sharedCommentsArray === null) {
return () => null;
}
sharedCommentsArray.observeDeep(onSharedCommentChanges);
connect();

View File

@ -9,7 +9,14 @@
import type {SettingName} from '../appSettings';
import * as React from 'react';
import {createContext, useCallback, useContext, useMemo, useState} from 'react';
import {
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import {DEFAULT_SETTINGS} from '../appSettings';
@ -19,7 +26,7 @@ type SettingsContextShape = {
};
const Context: React.Context<SettingsContextShape> = createContext({
setOption: () => {
setOption: (name: SettingName, value: boolean) => {
return;
},
settings: DEFAULT_SETTINGS,
@ -28,9 +35,10 @@ const Context: React.Context<SettingsContextShape> = createContext({
export const SettingsContext = ({
children,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
}): JSX.Element => {
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const setOption = useCallback((setting: SettingName, value: boolean) => {
setSettings((options) => ({
...options,
@ -42,9 +50,11 @@ export const SettingsContext = ({
setURLParam(setting, value);
}
}, []);
const contextValue = useMemo(() => {
return {setOption, settings};
}, [setOption, settings]);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};

View File

@ -7,7 +7,14 @@
*/
import * as React from 'react';
import {createContext, useContext, useEffect, useMemo, useState} from 'react';
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
type Suggestion = null | string;
type CallbackFn = (newSuggestion: Suggestion) => void;
@ -16,15 +23,22 @@ type PublishFn = (newSuggestion: Suggestion) => void;
type ContextShape = [SubscribeFn, PublishFn];
type HookShape = [suggestion: Suggestion, setSuggestion: PublishFn];
const Context: React.Context<ContextShape> = createContext(null);
const Context: React.Context<ContextShape> = createContext([
(_cb) => () => {
return;
},
(_newSuggestion: Suggestion) => {
return;
},
]);
export const SharedAutocompleteContext = ({
children,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
}): JSX.Element => {
const context: ContextShape = useMemo(() => {
let suggestion = null;
let suggestion: Suggestion | null = null;
const listeners: Set<CallbackFn> = new Set();
return [
(cb: (newSuggestion: Suggestion) => void) => {
@ -47,7 +61,7 @@ export const SharedAutocompleteContext = ({
export const useSharedAutocompleteContext = (): HookShape => {
const [subscribe, publish]: ContextShape = useContext(Context);
const [suggestion, setSuggestion] = useState(null);
const [suggestion, setSuggestion] = useState<Suggestion>(null);
useEffect(() => {
return subscribe((newSuggestion: Suggestion) => {
setSuggestion(newSuggestion);

View File

@ -10,20 +10,18 @@ import type {HistoryState} from '@lexical/react/LexicalHistoryPlugin';
import {createEmptyHistoryState} from '@lexical/react/LexicalHistoryPlugin';
import * as React from 'react';
import {createContext, useContext, useMemo} from 'react';
import {createContext, ReactNode, useContext, useMemo} from 'react';
type ContextShape = {
historyState?: HistoryState;
};
const Context: React.Context<ContextShape> = createContext({
historyState: {current: null, redoStack: [], undoStack: []},
});
const Context: React.Context<ContextShape> = createContext({});
export const SharedHistoryContext = ({
children,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
}): JSX.Element => {
const historyContext = useMemo(
() => ({historyState: createEmptyHistoryState()}),

View File

@ -12,7 +12,7 @@ import * as React from 'react';
import Modal from '../ui/Modal';
export default function useModal(): [
JSX.Element,
JSX.Element | null,
(title: string, showModal: (onClose: () => void) => JSX.Element) => void,
] {
const [modalContent, setModalContent] = useState<null | {
@ -42,7 +42,7 @@ export default function useModal(): [
const showModal = useCallback(
(
title,
title: string,
// eslint-disable-next-line no-shadow
getContent: (onClose: () => void) => JSX.Element,
closeOnClickOutside = false,

View File

@ -34,7 +34,9 @@ const getElement = (): HTMLElement => {
export default function useReport(): (arg0: string) => NodeJS.Timeout {
const timer = useRef<NodeJS.Timeout | null>(null);
const cleanup = useCallback(() => {
clearTimeout(timer.current);
if (timer !== null) {
clearTimeout(timer.current as NodeJS.Timeout);
}
if (document.body) {
document.body.removeChild(getElement());
@ -50,7 +52,7 @@ export default function useReport(): (arg0: string) => NodeJS.Timeout {
// eslint-disable-next-line no-console
console.log(content);
const element = getElement();
clearTimeout(timer.current);
clearTimeout(timer.current as NodeJS.Timeout);
element.innerHTML = content;
timer.current = setTimeout(cleanup, 1000);
return timer.current;

View File

@ -15,7 +15,7 @@ import {createRoot} from 'react-dom/client';
import App from './App';
// Handle runtime errors
const showErrorOverlay = (err) => {
const showErrorOverlay = (err: Event) => {
const ErrorOverlay = customElements.get('vite-error-overlay');
if (!ErrorOverlay) {
return;
@ -32,7 +32,7 @@ window.addEventListener('unhandledrejection', ({reason}) =>
showErrorOverlay(reason),
);
createRoot(document.getElementById('root')).render(
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,

View File

@ -28,7 +28,7 @@ export type SerializedAutocompleteNode = Spread<
SerializedLexicalNode
>;
export class AutocompleteNode extends DecoratorNode<JSX.Element> {
export class AutocompleteNode extends DecoratorNode<JSX.Element | null> {
// TODO add comment
__uuid: string;
@ -73,7 +73,7 @@ export class AutocompleteNode extends DecoratorNode<JSX.Element> {
return document.createElement('span');
}
decorate(): JSX.Element {
decorate(): JSX.Element | null {
if (this.__uuid !== UUID) {
return null;
}

View File

@ -54,7 +54,7 @@ type Props = {
// exportToSvg has fonts from excalidraw.com
// We don't want them to be used in open source
const removeStyleFromSvg_HACK = (svg) => {
const removeStyleFromSvg_HACK = (svg: SVGElement) => {
const styleTag = svg?.firstElementChild?.firstElementChild;
// Generated SVG is getting double-sized by height and width attributes
@ -81,11 +81,15 @@ export default function ExcalidrawImage({
appState = null,
rootClassName = null,
}: Props): JSX.Element {
const [Svg, setSvg] = useState<Element | null>(null);
const [Svg, setSvg] = useState<SVGElement | null>(null);
useEffect(() => {
const setContent = async () => {
const svg: Element = await exportToSvg({
if (appState === null) {
return;
}
const svg: SVGElement = await exportToSvg({
appState,
elements,
files: null,

View File

@ -57,8 +57,7 @@ export default function ExcalidrawModal({
onHide,
onDelete,
}: Props): ReactPortal | null {
const excalidrawRef = useRef(null);
const excaliDrawModelRef = useRef(null);
const excaliDrawModelRef = useRef<HTMLDivElement | null>(null);
const [discardModalOpen, setDiscardModalOpen] = useState(false);
const [elements, setElements] =
useState<ReadonlyArray<ExcalidrawElementFragment>>(initialElements);
@ -70,12 +69,12 @@ export default function ExcalidrawModal({
}, []);
useEffect(() => {
let modalOverlayElement = null;
let modalOverlayElement: HTMLElement | null = null;
const clickOutsideHandler = (event: MouseEvent) => {
const target = event.target;
if (
excaliDrawModelRef.current !== null &&
!excaliDrawModelRef.current.contains(target) &&
!excaliDrawModelRef.current.contains(target as Node) &&
closeOnClickOutside
) {
onDelete();
@ -143,15 +142,11 @@ export default function ExcalidrawModal({
);
}
useEffect(() => {
excalidrawRef?.current?.updateScene({elements: initialElements});
}, [initialElements]);
if (isShown === false) {
return null;
}
const onChange = (els) => {
const onChange = (els: ReadonlyArray<ExcalidrawElementFragment>) => {
setElements(els);
};

View File

@ -57,9 +57,8 @@ function ExcalidrawComponent({
const [isResizing, setIsResizing] = useState<boolean>(false);
const onDelete = useCallback(
(payload) => {
(event: KeyboardEvent) => {
if (isSelected && $isNodeSelection($getSelection())) {
const event: KeyboardEvent = payload;
event.preventDefault();
editor.update(() => {
const node = $getNodeByKey(nodeKey);
@ -153,7 +152,7 @@ function ExcalidrawComponent({
setIsResizing(true);
};
const onResizeEnd = (nextWidth, nextHeight) => {
const onResizeEnd = () => {
// Delay hiding the resize bars for click case
setTimeout(() => {
setIsResizing(false);
@ -212,7 +211,9 @@ export type SerializedExcalidrawNode = Spread<
SerializedLexicalNode
>;
function convertExcalidrawElement(domNode: HTMLElement): DOMConversionOutput {
function convertExcalidrawElement(
domNode: HTMLElement,
): DOMConversionOutput | null {
const excalidrawData = domNode.getAttribute('data-lexical-excalidraw-json');
if (excalidrawData) {
const node = $createExcalidrawNode();
@ -267,9 +268,9 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
return false;
}
static importDOM(): DOMConversionMap | null {
static importDOM(): DOMConversionMap<HTMLSpanElement> | null {
return {
span: (domNode: HTMLElement) => {
span: (domNode: HTMLSpanElement) => {
if (!domNode.hasAttribute('data-lexical-excalidraw-json')) {
return null;
}
@ -285,7 +286,10 @@ export class ExcalidrawNode extends DecoratorNode<JSX.Element> {
const element = document.createElement('span');
const content = editor.getElementByKey(this.getKey());
if (content !== null) {
element.innerHTML = content.querySelector('svg').outerHTML;
const svg = content.querySelector('svg');
if (svg !== null) {
element.innerHTML = svg.outerHTML;
}
}
element.setAttribute('data-lexical-excalidraw-json', this.__data);
return {element};

View File

@ -8,9 +8,12 @@
import type {
EditorConfig,
GridSelection,
LexicalEditor,
LexicalNode,
NodeKey,
NodeSelection,
RangeSelection,
SerializedEditor,
SerializedLexicalNode,
Spread,
@ -104,7 +107,7 @@ function LazyImage({
useSuspenseImage(src);
return (
<img
className={className}
className={className || undefined}
src={src}
alt={altText}
ref={imageRef}
@ -146,7 +149,9 @@ function ImageComponent({
const {yjsDocMap} = useCollaborationContext();
const [editor] = useLexicalComposerContext();
const isCollab = yjsDocMap.get('main') !== undefined;
const [selection, setSelection] = useState(null);
const [selection, setSelection] = useState<
RangeSelection | NodeSelection | GridSelection | null
>(null);
const onDelete = useCallback(
(payload: KeyboardEvent) => {
@ -219,7 +224,10 @@ function ImageComponent({
});
};
const onResizeEnd = (nextWidth, nextHeight) => {
const onResizeEnd = (
nextWidth: 'inherit' | number,
nextHeight: 'inherit' | number,
) => {
// Delay hiding the resize bars for click case
setTimeout(() => {
setIsResizing(false);

View File

@ -28,11 +28,19 @@ export type SerializedMentionNode = Spread<
SerializedTextNode
>;
function convertMentionElement(domNode: HTMLElement): DOMConversionOutput {
const node = $createMentionNode(domNode.textContent);
return {
node,
};
function convertMentionElement(
domNode: HTMLElement,
): DOMConversionOutput | null {
const textContent = domNode.textContent;
if (textContent !== null) {
const node = $createMentionNode(textContent);
return {
node,
};
}
return null;
}
const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';

View File

@ -22,10 +22,13 @@ import {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
GridSelection,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
LexicalNode,
NodeKey,
NodeSelection,
RangeSelection,
SerializedLexicalNode,
Spread,
} from 'lexical';
@ -87,7 +90,7 @@ function PollOptionComponent({
option: Option;
options: Options;
totalVotes: number;
withPollNode: (cb: (PollNode) => void) => void;
withPollNode: (cb: (pollNode: PollNode) => void) => void;
}): JSX.Element {
const {clientID} = useCollaborationContext();
const checkboxRef = useRef(null);
@ -166,7 +169,9 @@ function PollComponent({
const totalVotes = useMemo(() => getTotalVotes(options), [options]);
const [isSelected, setSelected, clearSelection] =
useLexicalNodeSelection(nodeKey);
const [selection, setSelection] = useState(null);
const [selection, setSelection] = useState<
RangeSelection | NodeSelection | GridSelection | null
>(null);
const ref = useRef(null);
const onDelete = useCallback(
@ -276,10 +281,13 @@ export type SerializedPollNode = Spread<
SerializedLexicalNode
>;
function convertPollElement(domNode: HTMLElement): DOMConversionOutput {
function convertPollElement(domNode: HTMLElement): DOMConversionOutput | null {
const question = domNode.getAttribute('data-lexical-poll-question');
const node = $createPollNode(question);
return {node};
if (question !== null) {
const node = $createPollNode(question);
return {node};
}
return null;
}
export class PollNode extends DecoratorNode<JSX.Element> {

View File

@ -42,7 +42,19 @@ import StickyEditorTheme from '../themes/StickyEditorTheme';
import ContentEditable from '../ui/ContentEditable';
import Placeholder from '../ui/Placeholder';
function positionSticky(stickyElem: HTMLElement, positioning): void {
type Positioning = {
isDragging: boolean;
offsetX: number;
offsetY: number;
rootElementRect: null | ClientRect;
x: number;
y: number;
};
function positionSticky(
stickyElem: HTMLElement,
positioning: Positioning,
): void {
const style = stickyElem.style;
const rootElementRect = positioning.rootElementRect;
const rectLeft = rootElementRect !== null ? rootElementRect.left : 0;
@ -66,14 +78,7 @@ function StickyComponent({
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const stickyContainerRef = useRef<null | HTMLDivElement>(null);
const positioningRef = useRef<{
isDragging: boolean;
offsetX: number;
offsetY: number;
rootElementRect: null | ClientRect;
x: number;
y: number;
}>({
const positioningRef = useRef<Positioning>({
isDragging: false,
offsetX: 0,
offsetY: 0,

View File

@ -44,10 +44,15 @@ type TweetComponentProps = Readonly<{
tweetID: string;
}>;
function convertTweetElement(domNode: HTMLElement): null | DOMConversionOutput {
function convertTweetElement(
domNode: HTMLDivElement,
): DOMConversionOutput | null {
const id = domNode.getAttribute('data-lexical-tweet-id');
const node = $createTweetNode(id);
return {node};
if (id) {
const node = $createTweetNode(id);
return {node};
}
return null;
}
function TweetComponent({
@ -91,7 +96,9 @@ function TweetComponent({
script.async = true;
document.body?.appendChild(script);
script.onload = createTweet;
script.onerror = onError;
if (onError) {
script.onerror = onError as OnErrorEventHandler;
}
} else {
createTweet();
}
@ -151,9 +158,9 @@ export class TweetNode extends DecoratorBlockNode {
};
}
static importDOM(): DOMConversionMap | null {
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
return {
div: (domNode: HTMLElement) => {
div: (domNode: HTMLDivElement) => {
if (!domNode.hasAttribute('data-lexical-tweet-id')) {
return null;
}
@ -171,7 +178,7 @@ export class TweetNode extends DecoratorBlockNode {
return {element};
}
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key);
this.__id = id;
}

View File

@ -91,7 +91,7 @@ export class YouTubeNode extends DecoratorBlockNode {
};
}
constructor(id: string, format?: ElementFormatType | null, key?: NodeKey) {
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key);
this.__id = id;
}

View File

@ -16,7 +16,7 @@ const EMAIL_MATCHER =
/(([^<>()[\]\\.,;:\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) => {
(text: string) => {
const match = URL_MATCHER.exec(text);
return (
match && {
@ -27,7 +27,7 @@ const MATCHERS = [
}
);
},
(text) => {
(text: string) => {
const match = EMAIL_MATCHER.exec(text);
return (
match && {

View File

@ -83,7 +83,7 @@ function useQuery(): (searchText: string) => SearchPromise {
}, []);
}
export default function AutocompletePlugin(): JSX.Element {
export default function AutocompletePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const [, setSuggestion] = useSharedAutocompleteContext();
const query = useQuery();
@ -94,7 +94,10 @@ export default function AutocompletePlugin(): JSX.Element {
let lastSuggestion: null | string = null;
let searchPromise: null | SearchPromise = null;
function $clearSuggestion() {
const autocompleteNode = $getNodeByKey(autocompleteNodeKey);
const autocompleteNode =
autocompleteNodeKey !== null
? $getNodeByKey(autocompleteNodeKey)
: null;
if (autocompleteNode !== null && autocompleteNode.isAttached()) {
autocompleteNode.remove();
autocompleteNodeKey = null;
@ -160,9 +163,11 @@ export default function AutocompletePlugin(): JSX.Element {
$clearSuggestion();
searchPromise = query(match);
searchPromise.promise
.then((newSuggestion) =>
updateAsyncSuggestion(searchPromise, newSuggestion),
)
.then((newSuggestion) => {
if (searchPromise !== null) {
updateAsyncSuggestion(searchPromise, newSuggestion);
}
})
.catch((e) => {
console.error(e);
});
@ -170,7 +175,7 @@ export default function AutocompletePlugin(): JSX.Element {
});
}
function $handleAutocompleteIntent(): boolean {
if (lastSuggestion === null) {
if (lastSuggestion === null || autocompleteNodeKey === null) {
return false;
}
const autocompleteNode = $getNodeByKey(autocompleteNodeKey);
@ -202,6 +207,9 @@ export default function AutocompletePlugin(): JSX.Element {
$clearSuggestion();
});
}
const rootElem = editor.getRootElement();
return mergeRegister(
editor.registerNodeTransform(
AutocompleteNode,
@ -218,7 +226,9 @@ export default function AutocompletePlugin(): JSX.Element {
$handleKeypressCommand,
COMMAND_PRIORITY_LOW,
),
addSwipeRightListener(editor.getRootElement(), handleSwipeRight),
...(rootElem !== null
? [addSwipeRightListener(rootElem, handleSwipeRight)]
: []),
unmountSuggestion,
);
}, [editor, query, setSuggestion]);

View File

@ -26,7 +26,7 @@ export default function ClickableLinkPlugin({
}: {
filter?: LinkFilter;
newTab?: boolean;
}): JSX.Element {
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
function onClick(e: Event) {
@ -69,10 +69,12 @@ export default function ClickableLinkPlugin({
}
try {
window.open(
href,
newTab || event.metaKey || event.ctrlKey ? '_blank' : '_self',
);
if (href !== null) {
window.open(
href,
newTab || event.metaKey || event.ctrlKey ? '_blank' : '_self',
);
}
} catch {
// It didn't work, which is better than throwing an exception!
}

View File

@ -77,7 +77,7 @@ function AddCommentBox({
editor: LexicalEditor;
onAddComment: () => void;
}): JSX.Element {
const boxRef = useRef(null);
const boxRef = useRef<HTMLDivElement>(null);
const updatePosition = useCallback(() => {
const boxElem = boxRef.current;
@ -135,7 +135,7 @@ function EditorRefPlugin({
function EscapeHandlerPlugin({
onEscape,
}: {
onEscape: (KeyboardEvent) => boolean;
onEscape: (e: KeyboardEvent) => boolean;
}): null {
const [editor] = useLexicalComposerContext();
@ -164,13 +164,13 @@ function PlainTextEditor({
className?: string;
editorRef?: {current: null | LexicalEditor};
onChange: (editorState: EditorState, editor: LexicalEditor) => void;
onEscape: (KeyboardEvent) => boolean;
onEscape: (e: KeyboardEvent) => boolean;
placeholder?: string;
}) {
const initialConfig = {
namespace: 'Commenting',
nodes: [],
onError: (error) => {
onError: (error: Error) => {
throw error;
},
theme: CommentEditorTheme,
@ -194,7 +194,10 @@ function PlainTextEditor({
);
}
function useOnChange(setContent, setCanSubmit) {
function useOnChange(
setContent: (text: string) => void,
setCanSubmit: (canSubmit: boolean) => void,
) {
return useCallback(
(editorState: EditorState, _editor: LexicalEditor) => {
editorState.read(() => {
@ -220,7 +223,7 @@ function CommentInputBox({
}) {
const [content, setContent] = useState('');
const [canSubmit, setCanSubmit] = useState(false);
const boxRef = useRef(null);
const boxRef = useRef<HTMLDivElement>(null);
const selectionState = useMemo(
() => ({
container: document.createElement('div'),
@ -256,12 +259,13 @@ function CommentInputBox({
boxElem.style.left = `${correctedLeft}px`;
boxElem.style.top = `${bottom + 20}px`;
const selectionRectsLength = selectionRects.length;
const {elements, container} = selectionState;
const {container} = selectionState;
const elements: Array<HTMLSpanElement> = selectionState.elements;
const elementsLength = elements.length;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
let elem = elements[i];
let elem: HTMLSpanElement = elements[i];
if (elem === undefined) {
elem = document.createElement('span');
elements[i] = elem;
@ -365,7 +369,7 @@ function CommentsComposer({
}) {
const [content, setContent] = useState('');
const [canSubmit, setCanSubmit] = useState(false);
const editorRef = useRef(null);
const editorRef = useRef<LexicalEditor>(null);
const author = useCollabAuthorName();
const onChange = useOnChange(setContent, setCanSubmit);
@ -375,7 +379,7 @@ function CommentsComposer({
submitAddComment(createComment(content, author), false, thread);
const editor = editorRef.current;
if (editor !== null) {
editor.dispatchCommand(CLEAR_EDITOR_COMMAND);
editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
}
}
};
@ -553,7 +557,7 @@ function CommentsPanelList({
{comments.map((commentOrThread) => {
const id = commentOrThread.id;
if (commentOrThread.type === 'thread') {
const handleClickThread = (event) => {
const handleClickThread = () => {
const markNodeKeys = markNodeMap.get(id);
if (
markNodeKeys !== undefined &&
@ -644,8 +648,8 @@ function CommentsPanel({
thread?: Thread,
) => void;
}): JSX.Element {
const footerRef = useRef(null);
const listRef = useRef(null);
const footerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const isEmpty = comments.length === 0;
useLayoutEffect(() => {
@ -713,7 +717,7 @@ export default function CommentPlugin({
const markNodeMap = useMemo<Map<string, Set<NodeKey>>>(() => {
return new Map();
}, []);
const [activeAnchorKey, setActiveAnchorKey] = useState(null);
const [activeAnchorKey, setActiveAnchorKey] = useState<NodeKey | null>();
const [activeIDs, setActiveIDs] = useState<Array<string>>([]);
const [showCommentInput, setShowCommentInput] = useState(false);
const [showComments, setShowComments] = useState(false);
@ -797,7 +801,7 @@ export default function CommentPlugin({
);
useEffect(() => {
const changedElems = [];
const changedElems: Array<HTMLElement> = [];
for (let i = 0; i < activeIDs.length; i++) {
const id = activeIDs[i];
const keys = markNodeMap.get(id);
@ -842,7 +846,7 @@ export default function CommentPlugin({
editor.getEditorState().read(() => {
for (const [key, mutation] of mutations) {
const node: null | MarkNode = $getNodeByKey(key);
let ids = [];
let ids: NodeKey[] = [];
if (mutation === 'destroyed') {
ids = markNodeKeysToIDs.get(key) || [];
@ -916,7 +920,9 @@ export default function CommentPlugin({
INSERT_INLINE_COMMAND,
() => {
const domSelection = window.getSelection();
domSelection.removeAllRanges();
if (domSelection !== null) {
domSelection.removeAllRanges();
}
setShowCommentInput(true);
return true;
},
@ -941,6 +947,7 @@ export default function CommentPlugin({
document.body,
)}
{activeAnchorKey !== null &&
activeAnchorKey !== undefined &&
!showCommentInput &&
createPortal(
<AddCommentBox

View File

@ -51,7 +51,7 @@ function findAndTransformEmoji(node: TextNode): null | TextNode {
}
function textNodeTransform(node: TextNode): void {
let targetNode = node;
let targetNode: TextNode | null = node;
while (targetNode !== null) {
if (!targetNode.isSimpleText()) {
@ -72,7 +72,7 @@ function useEmojis(editor: LexicalEditor): void {
}, [editor]);
}
export default function EmojisPlugin(): JSX.Element {
export default function EmojisPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEmojis(editor);
return null;

View File

@ -19,7 +19,7 @@ import {useEffect} from 'react';
import {$createExcalidrawNode, ExcalidrawNode} from '../nodes/ExcalidrawNode';
export const INSERT_EXCALIDRAW_COMMAND: LexicalCommand<void> = createCommand();
export default function ExcalidrawPlugin(): JSX.Element {
export default function ExcalidrawPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([ExcalidrawNode])) {

View File

@ -38,7 +38,7 @@ export type InsertImagePayload = Readonly<ImagePayload>;
export const INSERT_IMAGE_COMMAND: LexicalCommand<InsertImagePayload> =
createCommand();
export default function ImagesPlugin(): JSX.Element {
export default function ImagesPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
@ -150,7 +150,9 @@ function onDrop(event: DragEvent, editor: LexicalEditor): boolean {
const range = getDragSelection(event);
node.remove();
const rangeSelection = $createRangeSelection();
rangeSelection.applyDOMRange(range);
if (range !== null && range !== undefined) {
rangeSelection.applyDOMRange(range);
}
$setSelection(rangeSelection);
editor.dispatchCommand(INSERT_IMAGE_COMMAND, data);
}
@ -198,12 +200,12 @@ function canDropImage(event: DragEvent): boolean {
);
}
function getDragSelection(event: DragEvent): Range {
function getDragSelection(event: DragEvent): Range | null | undefined {
let range;
const domSelection = getSelection();
if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(event.clientX, event.clientY);
} else if (event.rangeParent) {
} else if (event.rangeParent && domSelection !== null) {
domSelection.collapse(event.rangeParent, event.rangeOffset || 0);
range = domSelection.getRangeAt(0);
} else {

File diff suppressed because one or more lines are too long

View File

@ -9,14 +9,14 @@
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {trimTextContentFromAnchor} from '@lexical/selection';
import {$restoreEditorState} from '@lexical/utils';
import {$getSelection, $isRangeSelection, RootNode} from 'lexical';
import {$getSelection, $isRangeSelection, EditorState, RootNode} from 'lexical';
import {useEffect} from 'react';
export function MaxLengthPlugin({maxLength}: {maxLength: number}): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
let lastRestoredEditorState = null;
let lastRestoredEditorState: EditorState | null = null;
return editor.registerNodeTransform(RootNode, (rootNode: RootNode) => {
const selection = $getSelection();

View File

@ -21,7 +21,14 @@ import {
KEY_ESCAPE_COMMAND,
KEY_TAB_COMMAND,
} from 'lexical';
import {startTransition, useCallback, useEffect, useRef, useState} from 'react';
import {
ReactPortal,
startTransition,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';
import useLayoutEffect from 'shared/useLayoutEffect';
@ -532,7 +539,7 @@ const dummyLookupService = {
},
};
function useMentionLookupService(mentionString) {
function useMentionLookupService(mentionString: string) {
const [results, setResults] = useState<Array<string> | null>(null);
useEffect(() => {
@ -599,8 +606,8 @@ function MentionsTypeahead({
close: () => void;
editor: LexicalEditor;
resolution: Resolution;
}): JSX.Element {
const divRef = useRef(null);
}): JSX.Element | null {
const divRef = useRef<HTMLDivElement>(null);
const match = resolution.match;
const results = useMentionLookupService(match.matchingString);
const [selectedIndex, setSelectedIndex] = useState<null | number>(null);
@ -635,7 +642,7 @@ function MentionsTypeahead({
}, [close, match, editor, results, selectedIndex]);
const updateSelectedIndex = useCallback(
(index) => {
(index: number) => {
const rootElem = editor.getRootElement();
if (rootElem !== null) {
rootElem.setAttribute(
@ -785,8 +792,8 @@ function MentionsTypeahead({
}
function checkForCapitalizedNameMentions(
text,
minMatchLength,
text: string,
minMatchLength: number,
): MentionMatch | null {
const match = CapitalizedNameMentionsRegex.exec(text);
if (match !== null) {
@ -806,7 +813,10 @@ function checkForCapitalizedNameMentions(
return null;
}
function checkForAtSignMentions(text, minMatchLength): MentionMatch | null {
function checkForAtSignMentions(
text: string,
minMatchLength: number,
): MentionMatch | null {
let match = AtSignMentionsRegex.exec(text);
if (match === null) {
@ -829,7 +839,7 @@ function checkForAtSignMentions(text, minMatchLength): MentionMatch | null {
return null;
}
function getPossibleMentionMatch(text): MentionMatch | null {
function getPossibleMentionMatch(text: string): MentionMatch | null {
const match = checkForAtSignMentions(text, 1);
return match === null ? checkForCapitalizedNameMentions(text, 3) : match;
}
@ -859,8 +869,10 @@ function tryToPositionRange(match: MentionMatch, range: Range): boolean {
const startOffset = match.leadOffset;
const endOffset = domSelection.anchorOffset;
try {
range.setStart(anchorNode, startOffset);
range.setEnd(anchorNode, endOffset);
if (anchorNode) {
range.setStart(anchorNode, startOffset);
range.setEnd(anchorNode, endOffset);
}
} catch (error) {
return false;
}
@ -976,7 +988,7 @@ function isSelectionOnEntityBoundary(
});
}
function useMentions(editor: LexicalEditor): JSX.Element {
function useMentions(editor: LexicalEditor): ReactPortal | null {
const [resolution, setResolution] = useState<Resolution | null>(null);
useEffect(() => {
@ -987,7 +999,7 @@ function useMentions(editor: LexicalEditor): JSX.Element {
useEffect(() => {
let activeRange: Range | null = document.createRange();
let previousText = null;
let previousText: string | null = null;
const updateListener = () => {
const range = activeRange;
@ -1044,7 +1056,7 @@ function useMentions(editor: LexicalEditor): JSX.Element {
);
}
export default function MentionsPlugin(): JSX.Element {
export default function MentionsPlugin(): ReactPortal | null {
const [editor] = useLexicalComposerContext();
return useMentions(editor);
}

View File

@ -22,7 +22,7 @@ import {$createPollNode, PollNode} from '../nodes/PollNode';
export const INSERT_POLL_COMMAND: LexicalCommand<string> = createCommand();
export default function PollPlugin(): JSX.Element {
export default function PollPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([PollNode])) {

View File

@ -11,7 +11,7 @@ import {useEffect} from 'react';
import {StickyNode} from '../nodes/StickyNode';
export default function StickyPlugin(): JSX.Element {
export default function StickyPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([StickyNode])) {

View File

@ -37,7 +37,7 @@ import {createPortal} from 'react-dom';
type TableCellActionMenuProps = Readonly<{
contextRef: {current: null | HTMLElement};
onClose: () => void;
setIsMenuOpen: (boolean) => void;
setIsMenuOpen: (isOpen: boolean) => void;
tableCellNode: TableCellNode;
}>;
@ -48,7 +48,7 @@ function TableActionMenu({
contextRef,
}: TableCellActionMenuProps) {
const [editor] = useLexicalComposerContext();
const dropDownRef = useRef<HTMLDivElement>();
const dropDownRef = useRef<HTMLDivElement | null>(null);
const [tableCellNode, updateTableCellNode] = useState(_tableCellNode);
const [selectionCounts, updateSelectionCounts] = useState({
columns: 1,
@ -103,12 +103,12 @@ function TableActionMenu({
}, [contextRef, dropDownRef]);
useEffect(() => {
function handleClickOutside(event) {
function handleClickOutside(event: MouseEvent) {
if (
dropDownRef.current != null &&
contextRef.current != null &&
!dropDownRef.current.contains(event.target) &&
!contextRef.current.contains(event.target)
!dropDownRef.current.contains(event.target as Node) &&
!contextRef.current.contains(event.target as Node)
) {
setIsMenuOpen(false);
}
@ -132,7 +132,9 @@ function TableActionMenu({
}
const tableSelection = getTableSelectionFromTableElement(tableElement);
tableSelection.clearHighlight();
if (tableSelection !== null) {
tableSelection.clearHighlight();
}
tableNode.markDirty();
updateTableCellNode(tableCellNode.getLatest());
@ -143,7 +145,7 @@ function TableActionMenu({
}, [editor, tableCellNode]);
const insertTableRowAtSelection = useCallback(
(shouldInsertAfter) => {
(shouldInsertAfter: boolean) => {
editor.update(() => {
const selection = $getSelection();
@ -179,7 +181,7 @@ function TableActionMenu({
);
const insertTableColumnAtSelection = useCallback(
(shouldInsertAfter) => {
(shouldInsertAfter: boolean) => {
editor.update(() => {
const selection = $getSelection();
@ -427,6 +429,7 @@ function TableCellActionMenuContainer(): JSX.Element {
if (
$isRangeSelection(selection) &&
rootElement !== null &&
nativeSelection !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode(
@ -462,7 +465,7 @@ function TableCellActionMenuContainer(): JSX.Element {
});
useEffect(() => {
const menuButtonDOM = menuButtonRef.current;
const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null;
if (menuButtonDOM != null && tableCellNode != null) {
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey());

View File

@ -28,6 +28,7 @@ import {
} from 'lexical';
import * as React from 'react';
import {
MouseEventHandler,
ReactPortal,
useCallback,
useEffect,
@ -224,57 +225,58 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
);
const toggleResize = useCallback(
(direction) => (event) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
if (draggingDirection === direction && mouseStartPosRef.current) {
const {x, y} = mouseStartPosRef.current;
(direction: MouseDraggingDirection): MouseEventHandler<HTMLDivElement> =>
(event) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
return;
throw new Error('TableCellResizer: Expected active cell.');
}
const {height, width} = activeCell.elem.getBoundingClientRect();
if (draggingDirection === direction && mouseStartPosRef.current) {
const {x, y} = mouseStartPosRef.current;
if (isHeightChanging(direction)) {
const heightChange = Math.abs(event.clientY - y);
if (activeCell === null) {
return;
}
const isShrinking = direction === 'bottom' && y > event.clientY;
const {height, width} = activeCell.elem.getBoundingClientRect();
updateRowHeight(
Math.max(
isShrinking ? height - heightChange : heightChange + height,
MIN_ROW_HEIGHT,
),
);
if (isHeightChanging(direction)) {
const heightChange = Math.abs(event.clientY - y);
const isShrinking = direction === 'bottom' && y > event.clientY;
updateRowHeight(
Math.max(
isShrinking ? height - heightChange : heightChange + height,
MIN_ROW_HEIGHT,
),
);
} else {
const widthChange = Math.abs(event.clientX - x);
const isShrinking = direction === 'right' && x > event.clientX;
updateColumnWidth(
Math.max(
isShrinking ? width - widthChange : widthChange + width,
MIN_COLUMN_WIDTH,
),
);
}
resetState();
} else {
const widthChange = Math.abs(event.clientX - x);
const isShrinking = direction === 'right' && x > event.clientX;
updateColumnWidth(
Math.max(
isShrinking ? width - widthChange : widthChange + width,
MIN_COLUMN_WIDTH,
),
);
mouseStartPosRef.current = {
x: event.clientX,
y: event.clientY,
};
updateMouseCurrentPos(mouseStartPosRef.current);
updateDraggingDirection(direction);
}
resetState();
} else {
mouseStartPosRef.current = {
x: event.clientX,
y: event.clientY,
};
updateMouseCurrentPos(mouseStartPosRef.current);
updateDraggingDirection(direction);
}
},
},
[
activeCell,
draggingDirection,
@ -353,13 +355,13 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
<>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.right}
style={resizerStyles.right || undefined}
onMouseDown={toggleResize('right')}
onMouseUp={toggleResize('right')}
/>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.bottom}
style={resizerStyles.bottom || undefined}
onMouseDown={toggleResize('bottom')}
onMouseUp={toggleResize('bottom')}
/>

View File

@ -6,7 +6,12 @@
*
*/
import type {LexicalEditor} from 'lexical';
import type {
GridSelection,
LexicalEditor,
NodeSelection,
RangeSelection,
} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
@ -46,8 +51,8 @@ const download = (filename: string, text: string | null) => {
document.body?.removeChild(a);
};
const formatStep = (step) => {
const formatOneStep = (name, value) => {
const formatStep = (step: Step) => {
const formatOneStep = (name: string, value: Step['value']) => {
switch (name) {
case 'click': {
return ` await page.mouse.click(${value.x}, ${value.y});`;
@ -99,7 +104,7 @@ export function isSelectAll(event: KeyboardEvent): boolean {
}
// stolen from LexicalSelection-test
function sanitizeSelection(selection) {
function sanitizeSelection(selection: Selection) {
const {anchorNode, focusNode} = selection;
let {anchorOffset, focusOffset} = selection;
if (anchorOffset !== 0) {
@ -111,15 +116,17 @@ function sanitizeSelection(selection) {
return {anchorNode, anchorOffset, focusNode, focusOffset};
}
function getPathFromNodeToEditor(node: Node, rootElement) {
let currentNode = node;
function getPathFromNodeToEditor(node: Node, rootElement: HTMLElement | null) {
let currentNode: Node | null | undefined = node;
const path = [];
while (currentNode !== rootElement) {
path.unshift(
Array.from(currentNode?.parentNode?.childNodes ?? []).indexOf(
currentNode as ChildNode,
),
);
if (currentNode !== null && currentNode !== undefined) {
path.unshift(
Array.from(currentNode?.parentNode?.childNodes ?? []).indexOf(
currentNode as ChildNode,
),
);
}
currentNode = currentNode?.parentNode;
}
return path;
@ -137,21 +144,26 @@ const keyPresses = new Set([
]);
type Step = {
value: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
count: number;
name: string;
};
type Steps = Step[];
function useTestRecorder(editor: LexicalEditor): [JSX.Element, JSX.Element] {
function useTestRecorder(
editor: LexicalEditor,
): [JSX.Element, JSX.Element | null] {
const [steps, setSteps] = useState<Steps>([]);
const [isRecording, setIsRecording] = useState(false);
const [, setCurrentInnerHTML] = useState('');
const [templatedTest, setTemplatedTest] = useState('');
const previousSelectionRef = useRef(null);
const previousSelectionRef = useRef<
RangeSelection | GridSelection | NodeSelection | null
>(null);
const skipNextSelectionChangeRef = useRef(false);
const preRef = useRef(null);
const preRef = useRef<HTMLPreElement>(null);
const getCurrentEditor = useCallback(() => {
return editor;
@ -188,6 +200,8 @@ import {
repeat,
} from '../utils';
import {selectAll} from '../keyboardShortcuts';
import { RangeSelection } from 'lexical';
import { NodeSelection } from 'lexical';
describe('Test case', () => {
initializeE2E((e2e) => {
@ -204,7 +218,7 @@ ${steps.map(formatStep).join(`\n`)}
// just a wrapper around inserting new actions so that we can
// coalesce some actions like insertText/moveNativeSelection
const pushStep = useCallback(
(name, value) => {
(name: string, value: Step['value']) => {
setSteps((currentSteps) => {
// trying to group steps
const currentIndex = steps.length - 1;
@ -287,7 +301,10 @@ ${steps.map(formatStep).join(`\n`)}
useEffect(() => {
if (steps) {
setTemplatedTest(generateTestContent());
const testContent = generateTestContent();
if (testContent !== null) {
setTemplatedTest(testContent);
}
if (preRef.current) {
preRef.current.scrollTo(0, preRef.current.scrollHeight);
}
@ -311,8 +328,9 @@ ${steps.map(formatStep).join(`\n`)}
) {
const browserSelection = window.getSelection();
if (
browserSelection.anchorNode == null ||
browserSelection.focusNode == null
browserSelection &&
(browserSelection.anchorNode == null ||
browserSelection.focusNode == null)
) {
return;
}
@ -320,7 +338,10 @@ ${steps.map(formatStep).join(`\n`)}
previousSelectionRef.current = currentSelection;
}
skipNextSelectionChangeRef.current = false;
setTemplatedTest(generateTestContent());
const testContent = generateTestContent();
if (testContent !== null) {
setTemplatedTest(testContent);
}
},
);
return removeUpdateListener;
@ -342,7 +363,7 @@ ${steps.map(formatStep).join(`\n`)}
// clear editor and start recording
const toggleEditorSelection = useCallback(
(currentEditor) => {
(currentEditor: LexicalEditor) => {
if (!isRecording) {
currentEditor.update(() => {
const root = $getRoot();
@ -364,6 +385,7 @@ ${steps.map(formatStep).join(`\n`)}
}
const browserSelection = window.getSelection();
if (
browserSelection === null ||
browserSelection.anchorNode == null ||
browserSelection.focusNode == null
) {
@ -371,14 +393,15 @@ ${steps.map(formatStep).join(`\n`)}
}
const {anchorNode, anchorOffset, focusNode, focusOffset} =
sanitizeSelection(browserSelection);
const anchorPath = getPathFromNodeToEditor(
anchorNode,
getCurrentEditor().getRootElement(),
);
const focusPath = getPathFromNodeToEditor(
focusNode,
getCurrentEditor().getRootElement(),
);
const rootElement = getCurrentEditor().getRootElement();
let anchorPath;
if (anchorNode !== null) {
anchorPath = getPathFromNodeToEditor(anchorNode, rootElement);
}
let focusPath;
if (focusNode !== null) {
focusPath = getPathFromNodeToEditor(focusNode, rootElement);
}
pushStep('snapshot', {
anchorNode,
anchorOffset,

View File

@ -7,7 +7,12 @@
*/
import type {InsertImagePayload} from './ImagesPlugin';
import type {LexicalEditor, RangeSelection} from 'lexical';
import type {
GridSelection,
LexicalEditor,
NodeSelection,
RangeSelection,
} from 'lexical';
import './ToolbarPlugin.css';
@ -28,6 +33,7 @@ import {
$createHeadingNode,
$createQuoteNode,
$isHeadingNode,
HeadingTagType,
} from '@lexical/rich-text';
import {
$getSelectionStyleValueForProperty,
@ -58,6 +64,7 @@ import {
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
NodeKey,
OUTDENT_CONTENT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
@ -65,7 +72,7 @@ import {
UNDO_COMMAND,
} from 'lexical';
import * as React from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';
import {ChangeEvent, useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {IS_APPLE} from 'shared/environment';
@ -87,21 +94,6 @@ import {INSERT_POLL_COMMAND} from './PollPlugin';
import {INSERT_TWEET_COMMAND} from './TwitterPlugin';
import {INSERT_YOUTUBE_COMMAND} from './YouTubePlugin';
const supportedBlockTypes = new Set([
'paragraph',
'quote',
'code',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'bullet',
'number',
'check',
]);
const blockTypeToBlockName = {
bullet: 'Bulleted List',
check: 'Check List',
@ -160,7 +152,7 @@ function getSelectedNode(selection: RangeSelection): TextNode | ElementNode {
function positionEditorElement(
editor: HTMLElement,
rect: ClientRect,
rect: ClientRect | null,
rootElement: HTMLElement,
): void {
if (rect === null) {
@ -184,10 +176,12 @@ function positionEditorElement(
function FloatingLinkEditor({editor}: {editor: LexicalEditor}): JSX.Element {
const editorRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef(null);
const inputRef = useRef<HTMLInputElement>(null);
const [linkUrl, setLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState(null);
const [lastSelection, setLastSelection] = useState<
RangeSelection | GridSelection | NodeSelection | null
>(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
@ -213,6 +207,7 @@ function FloatingLinkEditor({editor}: {editor: LexicalEditor}): JSX.Element {
const rootElement = editor.getRootElement();
if (
selection !== null &&
nativeSelection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
@ -232,7 +227,9 @@ function FloatingLinkEditor({editor}: {editor: LexicalEditor}): JSX.Element {
positionEditorElement(editorElem, rect, rootElement);
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== 'link-input') {
positionEditorElement(editorElem, null, rootElement);
if (rootElement !== null) {
positionEditorElement(editorElem, null, rootElement);
}
setLastSelection(null);
setEditMode(false);
setLinkUrl('');
@ -381,7 +378,7 @@ function InsertImageUploadedDialogBody({
const isDisabled = src === '';
const loadImage = (files: FileList) => {
const loadImage = (files: FileList | null) => {
const reader = new FileReader();
reader.onload = function () {
if (typeof reader.result === 'string') {
@ -389,7 +386,9 @@ function InsertImageUploadedDialogBody({
}
return '';
};
reader.readAsDataURL(files[0]);
if (files !== null) {
reader.readAsDataURL(files[0]);
}
};
return (
@ -632,7 +631,7 @@ function BlockFormatDropDown({
editor,
blockType,
}: {
blockType: string;
blockType: keyof typeof blockTypeToBlockName;
editor: LexicalEditor;
}): JSX.Element {
const formatParagraph = () => {
@ -647,7 +646,7 @@ function BlockFormatDropDown({
}
};
const formatHeading = (headingSize) => {
const formatHeading = (headingSize: HeadingTagType) => {
if (blockType !== headingSize) {
editor.update(() => {
const selection = $getSelection();
@ -791,7 +790,7 @@ function Select({
value,
}: {
className: string;
onChange: (event: {target: {value: string}}) => void;
onChange: (e: ChangeEvent) => void;
options: [string, string][];
value: string;
}): JSX.Element {
@ -809,8 +808,11 @@ function Select({
export default function ToolbarPlugin(): JSX.Element {
const [editor] = useLexicalComposerContext();
const [activeEditor, setActiveEditor] = useState(editor);
const [blockType, setBlockType] = useState('paragraph');
const [selectedElementKey, setSelectedElementKey] = useState(null);
const [blockType, setBlockType] =
useState<keyof typeof blockTypeToBlockName>('paragraph');
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(
null,
);
const [fontSize, setFontSize] = useState<string>('15px');
const [fontColor, setFontColor] = useState<string>('#000');
const [bgColor, setBgColor] = useState<string>('#fff');
@ -874,9 +876,12 @@ export default function ToolbarPlugin(): JSX.Element {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
setBlockType(type);
if (type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName);
}
if ($isCodeNode(element)) {
const language = element.getLanguage();
const language =
element.getLanguage() as keyof typeof CODE_LANGUAGE_MAP;
setCodeLanguage(
language ? CODE_LANGUAGE_MAP[language] || language : '',
);
@ -974,8 +979,8 @@ export default function ToolbarPlugin(): JSX.Element {
}, [activeEditor]);
const onFontSizeSelect = useCallback(
(e) => {
applyStyleText({'font-size': e.target.value});
(e: ChangeEvent) => {
applyStyleText({'font-size': (e.target as HTMLSelectElement).value});
},
[applyStyleText],
);
@ -995,8 +1000,8 @@ export default function ToolbarPlugin(): JSX.Element {
);
const onFontFamilySelect = useCallback(
(e) => {
applyStyleText({'font-family': e.target.value});
(e: ChangeEvent) => {
applyStyleText({'font-family': (e.target as HTMLSelectElement).value});
},
[applyStyleText],
);
@ -1010,12 +1015,12 @@ export default function ToolbarPlugin(): JSX.Element {
}, [editor, isLink]);
const onCodeLanguageSelect = useCallback(
(e) => {
(e: ChangeEvent) => {
activeEditor.update(() => {
if (selectedElementKey !== null) {
const node = $getNodeByKey(selectedElementKey);
if ($isCodeNode(node)) {
node.setLanguage(e.target.value);
node.setLanguage((e.target as HTMLSelectElement).value);
}
}
});
@ -1049,7 +1054,7 @@ export default function ToolbarPlugin(): JSX.Element {
<i className="format redo" />
</button>
<Divider />
{supportedBlockTypes.has(blockType) && activeEditor === editor && (
{blockType in blockTypeToBlockName && activeEditor === editor && (
<>
<BlockFormatDropDown blockType={blockType} editor={editor} />
<Divider />

View File

@ -24,7 +24,7 @@ import {$createTweetNode, TweetNode} from '../nodes/TweetNode';
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand();
export default function TwitterPlugin(): JSX.Element {
export default function TwitterPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {

View File

@ -29,13 +29,13 @@ const validInputTypes = new Set([
'deleteSoftLineForward',
]);
export default function TypingPerfPlugin(): JSX.Element {
export default function TypingPerfPlugin(): JSX.Element | null {
const report = useReport();
useEffect(() => {
let start = 0;
let timerId = null;
let keyPressTimerId = null;
let log = [];
let timerId: NodeJS.Timeout | null;
let keyPressTimerId: number | null;
let log: Array<DOMHighResTimeStamp> = [];
let invalidatingEvent = false;
const measureEventEnd = function logKeyPress() {
@ -73,7 +73,7 @@ export default function TypingPerfPlugin(): JSX.Element {
start = performance.now();
};
const beforeInputHandler = function beforeInputHandler(event) {
const beforeInputHandler = function beforeInputHandler(event: InputEvent) {
if (!validInputTypes.has(event.inputType) || invalidatingEvent) {
invalidatingEvent = false;
return;
@ -82,7 +82,7 @@ export default function TypingPerfPlugin(): JSX.Element {
measureEventStart();
};
const keyDownHandler = function keyDownHandler(event) {
const keyDownHandler = function keyDownHandler(event: KeyboardEvent) {
const keyCode = event.keyCode;
if (keyCode === 8 || keyCode === 13) {

View File

@ -24,7 +24,7 @@ import {$createYouTubeNode, YouTubeNode} from '../nodes/YouTubeNode';
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand();
export default function YouTubePlugin(): JSX.Element {
export default function YouTubePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {

View File

@ -6,7 +6,7 @@
*
*/
import {DEFAULT_SETTINGS} from './appSettings';
import {DEFAULT_SETTINGS, Settings} from './appSettings';
// override default options with query parameters if any
const urlSearchParams = new URLSearchParams(window.location.search);
@ -15,7 +15,7 @@ for (const param of Object.keys(DEFAULT_SETTINGS)) {
if (urlSearchParams.has(param)) {
try {
const value = JSON.parse(urlSearchParams.get(param) ?? 'true');
DEFAULT_SETTINGS[param] = Boolean(value);
DEFAULT_SETTINGS[param as keyof Settings] = Boolean(value);
} catch (error) {
console.warn(`Unable to parse query parameter "${param}"`);
}
@ -23,5 +23,6 @@ for (const param of Object.keys(DEFAULT_SETTINGS)) {
}
if (DEFAULT_SETTINGS.disableBeforeInput) {
// @ts-expect-error
delete window.InputEvent.prototype.getTargetRanges;
}

View File

@ -9,6 +9,7 @@
import './Button.css';
import * as React from 'react';
import {ReactNode} from 'react';
import joinClasses from '../utils/join-classes';
@ -22,7 +23,7 @@ export default function Button({
title,
}: {
'data-test-id'?: string;
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
className?: string;
disabled?: boolean;
onClick: () => void;

View File

@ -8,7 +8,7 @@
import './ColorPicker.css';
import {useEffect, useMemo, useRef, useState} from 'react';
import {ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import * as React from 'react';
import DropDown from './DropDown';
@ -19,8 +19,8 @@ interface ColorPickerProps {
buttonClassName: string;
buttonIconClassName?: string;
buttonLabel?: string;
color?: string;
children?: JSX.Element;
color: string;
children?: ReactNode;
onChange?: (color: string) => void;
title?: string;
}
@ -100,7 +100,7 @@ export default function ColorPicker({
useEffect(() => {
// Check if the dropdown is actually active
if (innerDivRef.current !== null) {
if (innerDivRef.current !== null && onChange) {
onChange(selfColor.hex);
setInputColor(selfColor.hex);
}
@ -269,14 +269,15 @@ export function toHex(value: string): string {
}
function hex2rgb(hex: string): RGB {
const rbgArr = hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => '#' + r + r + g + g + b + b,
)
.substring(1)
.match(/.{2}/g)
.map((x) => parseInt(x, 16));
const rbgArr = (
hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => '#' + r + r + g + g + b + b,
)
.substring(1)
.match(/.{2}/g) || []
).map((x) => parseInt(x, 16));
return {
b: rbgArr[2],

View File

@ -6,7 +6,14 @@
*
*/
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';
@ -133,7 +140,7 @@ export default function DropDown({
buttonClassName: string;
buttonIconClassName?: string;
buttonLabel?: string;
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
stopCloseOnClickSelf?: boolean;
}): JSX.Element {
const dropDownRef = useRef<HTMLDivElement>(null);

View File

@ -15,7 +15,7 @@ type BaseEquationEditorProps = {
equation: string;
inline: boolean;
inputRef: {current: null | HTMLInputElement | HTMLTextAreaElement};
setEquation: (string) => void;
setEquation: (equation: string) => void;
};
export default function EquationEditor({
@ -24,8 +24,8 @@ export default function EquationEditor({
inline,
inputRef,
}: BaseEquationEditorProps): JSX.Element {
const onChange = (event) => {
setEquation(event.target.value);
const onChange = (event: ChangeEvent) => {
setEquation((event.target as HTMLInputElement).value);
};
const props = {

View File

@ -14,7 +14,7 @@ type Props = Readonly<{
'data-test-id'?: string;
accept?: string;
label: string;
onChange: (files: FileList) => void;
onChange: (files: FileList | null) => void;
}>;
export default function FileInput({

View File

@ -36,7 +36,7 @@ export default function ImageResizer({
maxWidth?: number;
onResizeEnd: (width: 'inherit' | number, height: 'inherit' | number) => void;
onResizeStart: () => void;
setShowCaption: (boolean) => void;
setShowCaption: (show: boolean) => void;
showCaption: boolean;
}): JSX.Element {
const buttonRef = useRef(null);

View File

@ -16,7 +16,7 @@ import KatexRenderer from './KatexRenderer';
type Props = {
initialEquation?: string;
onConfirm: (string, boolean) => void;
onConfirm: (equation: string, inline: boolean) => void;
};
export default function KatexEquationAlterer({

View File

@ -9,10 +9,17 @@
import './LinkPreview.css';
import * as React from 'react';
import {Suspense} from 'react';
import {CSSProperties, Suspense} from 'react';
type Preview = {
title: string;
description: string;
img: string;
domain: string;
} | null;
// Cached responses or running request promises
const PREVIEW_CACHE = {};
const PREVIEW_CACHE: Record<string, Promise<Preview> | {preview: Preview}> = {};
const URL_MATCHER =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
@ -49,7 +56,7 @@ function LinkPreviewContent({
url,
}: Readonly<{
url: string;
}>): JSX.Element {
}>): JSX.Element | null {
const {preview} = useSuspenseRequest(url);
if (preview === null) {
return null;
@ -78,12 +85,15 @@ function LinkPreviewContent({
);
}
function Glimmer(props): JSX.Element {
function Glimmer(props: {style: CSSProperties; index: number}): JSX.Element {
return (
<div
className="LinkPreview__glimmer"
{...props}
style={{animationDelay: (props.index || 0) * 300, ...(props.style || {})}}
style={{
animationDelay: String((props.index || 0) * 300),
...(props.style || {}),
}}
/>
);
}

View File

@ -9,7 +9,7 @@
import './Modal.css';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {ReactNode, useEffect, useRef} from 'react';
import {createPortal} from 'react-dom';
function PortalImpl({
@ -18,12 +18,12 @@ function PortalImpl({
title,
closeOnClickOutside,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
closeOnClickOutside: boolean;
onClose: () => void;
title: string;
}) {
const modalRef = useRef<HTMLDivElement>();
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (modalRef.current !== null) {
@ -32,8 +32,8 @@ function PortalImpl({
}, []);
useEffect(() => {
let modalOverlayElement = null;
const handler = (event) => {
let modalOverlayElement: HTMLElement | null = null;
const handler = (event: KeyboardEvent) => {
if (event.keyCode === 27) {
onClose();
}
@ -88,7 +88,7 @@ export default function Modal({
title,
closeOnClickOutside = false,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
closeOnClickOutside?: boolean;
onClose: () => void;
title: string;

View File

@ -9,12 +9,13 @@
import './Placeholder.css';
import * as React from 'react';
import {ReactNode} from 'react';
export default function Placeholder({
children,
className,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
className?: string;
}): JSX.Element {
return <div className={className || 'Placeholder__root'}>{children}</div>;

View File

@ -6,6 +6,8 @@
*
*/
export default function joinClasses(...args) {
export default function joinClasses(
...args: Array<string | boolean | null | undefined>
) {
return args.filter(Boolean).join(' ');
}

View File

@ -17,7 +17,7 @@ type ElementValues = {
const elements = new WeakMap<HTMLElement, ElementValues>();
function readTouch(e: TouchEvent): [number, number] {
function readTouch(e: TouchEvent): [number, number] | null {
const touch = e.changedTouches[0];
if (touch === undefined) {
return null;
@ -30,16 +30,23 @@ function addListener(element: HTMLElement, cb: Listener): () => void {
if (elementValues === undefined) {
const listeners = new Set<Listener>();
const handleTouchstart = (e: TouchEvent) => {
elementValues.start = readTouch(e);
if (elementValues !== undefined) {
elementValues.start = readTouch(e);
}
};
const handleTouchend = (e: TouchEvent) => {
if (elementValues === undefined) {
return;
}
const start = elementValues.start;
if (start === null) {
return;
}
const end = readTouch(e);
for (const listener of listeners) {
listener([end[0] - start[0], end[1] - start[1]], e);
if (end !== null) {
listener([end[0] - start[0], end[1] - start[1]], e);
}
}
};
element.addEventListener('touchstart', handleTouchstart);
@ -59,6 +66,9 @@ function addListener(element: HTMLElement, cb: Listener): () => void {
function deleteListener(element: HTMLElement, cb: Listener): void {
const elementValues = elements.get(element);
if (elementValues === undefined) {
return;
}
const listeners = elementValues.listeners;
listeners.delete(cb);
if (listeners.size === 0) {
@ -68,7 +78,10 @@ function deleteListener(element: HTMLElement, cb: Listener): void {
}
}
export function addSwipeLeftListener(element, cb) {
export function addSwipeLeftListener(
element: HTMLElement,
cb: (_force: number, e: TouchEvent) => void,
) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (x < 0 && -x > Math.abs(y)) {
@ -77,7 +90,10 @@ export function addSwipeLeftListener(element, cb) {
});
}
export function addSwipeRightListener(element, cb) {
export function addSwipeRightListener(
element: HTMLElement,
cb: (_force: number, e: TouchEvent) => void,
) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (x > 0 && x > Math.abs(y)) {
@ -86,7 +102,10 @@ export function addSwipeRightListener(element, cb) {
});
}
export function addSwipeUpListener(element, cb) {
export function addSwipeUpListener(
element: HTMLElement,
cb: (_force: number, e: TouchEvent) => void,
) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (y < 0 && -y > Math.abs(x)) {
@ -95,7 +114,10 @@ export function addSwipeUpListener(element, cb) {
});
}
export function addSwipeDownListener(element, cb) {
export function addSwipeDownListener(
element: HTMLElement,
cb: (_force: number, e: TouchEvent) => void,
) {
return addListener(element, (force, e) => {
const [x, y] = force;
if (y > 0 && y > Math.abs(x)) {

View File

@ -28,10 +28,10 @@ import {
KEY_DELETE_COMMAND,
} from 'lexical';
import * as React from 'react';
import {useCallback, useEffect, useRef} from 'react';
import {ReactNode, useCallback, useEffect, useRef} from 'react';
type Props = Readonly<{
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
format: ElementFormatType | null | undefined;
nodeKey: NodeKey;
className: Readonly<{

View File

@ -15,7 +15,7 @@ import {
LexicalComposerContext,
} from '@lexical/react/LexicalComposerContext';
import * as React from 'react';
import {useContext, useMemo} from 'react';
import {ReactNode, useContext, useMemo} from 'react';
import invariant from 'shared/invariant';
export function LexicalNestedComposer({
@ -23,7 +23,7 @@ export function LexicalNestedComposer({
children,
initialTheme,
}: {
children: JSX.Element | string | (JSX.Element | string)[];
children: ReactNode;
initialEditor: LexicalEditor;
initialTheme?: EditorThemeClasses;
}): JSX.Element {

View File

@ -115,7 +115,7 @@ export function TablePlugin(): JSX.Element | null {
} else if (mutation === 'destroyed') {
const tableSelection = tableSelections.get(nodeKey);
if (tableSelection) {
if (tableSelection !== undefined) {
tableSelection.removeListeners();
tableSelections.delete(nodeKey);
}

View File

@ -121,24 +121,24 @@ export function $getNodeByKeyOrThrow<N extends LexicalNode>(key: NodeKey): N {
return node;
}
export type DOMConversion = {
conversion: DOMConversionFn;
export type DOMConversion<T extends HTMLElement = HTMLElement> = {
conversion: DOMConversionFn<T>;
priority: 0 | 1 | 2 | 3 | 4;
};
export type DOMConversionFn = (
element: Node,
export type DOMConversionFn<T extends HTMLElement = HTMLElement> = (
element: T,
parent?: Node,
) => DOMConversionOutput;
) => DOMConversionOutput | null;
export type DOMChildConversion = (
lexicalNode: LexicalNode,
parentLexicalNode: LexicalNode | null | undefined,
) => LexicalNode | null | undefined;
export type DOMConversionMap = Record<
export type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<
NodeName,
<T extends HTMLElement>(node: T) => DOMConversion | null
(node: T) => DOMConversion<T> | null
>;
type NodeName = string;

View File

@ -6,7 +6,7 @@
"moduleResolution": "node",
"downlevelIteration": true,
"noEmit": true,
"strict": false,
"strict": true,
"baseUrl": ".",
"typeRoots": ["node_modules/@types", "libdefs/globals"],
"skipLibCheck": true,
@ -185,6 +185,7 @@
},
"include": ["./libdefs", "./packages"],
"exclude": [
"**/__tests__/**",
"**/dist/**",
"**/npm/**",
"**/node_modules/**",

View File

@ -1,7 +1,15 @@
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true
"esModuleInterop": true,
"strict": false
},
"include": ["./libdefs", "./packages"],
"exclude": [
"**/dist/**",
"**/npm/**",
"**/node_modules/**",
"./packages/playwright-core/**"
],
"extends": "./tsconfig.json"
}