mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 23:26:16 +08:00
Migrate to TS strict mode 6/n (#2496)
* Make suggested amends from @trueadm * Migrate files from playground
This commit is contained in:
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()}),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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);
|
||||
|
@ -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)';
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 && {
|
||||
|
@ -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]);
|
||||
|
@ -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!
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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])) {
|
||||
|
@ -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
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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])) {
|
||||
|
@ -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])) {
|
||||
|
@ -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());
|
||||
|
@ -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')}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -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 />
|
||||
|
@ -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(() => {
|
||||
|
@ -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) {
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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],
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
|
@ -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({
|
||||
|
@ -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 || {}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -6,6 +6,8 @@
|
||||
*
|
||||
*/
|
||||
|
||||
export default function joinClasses(...args) {
|
||||
export default function joinClasses(
|
||||
...args: Array<string | boolean | null | undefined>
|
||||
) {
|
||||
return args.filter(Boolean).join(' ');
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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<{
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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/**",
|
||||
|
@ -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"
|
||||
}
|
||||
|
Reference in New Issue
Block a user