From e96ee55b749c0fed8ac03d49044dd5594f0bc32e Mon Sep 17 00:00:00 2001 From: Sherry Date: Sat, 26 Oct 2024 00:47:28 +0800 Subject: [PATCH 1/2] [lexical-react] replace abstract component (#6752) --- .flowconfig | 3 ++- libdefs/yjs.js | 1 + package-lock.json | 15 +++++++------- package.json | 2 +- .../flow/LexicalContentEditable.js.flow | 20 +++++++++---------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.flowconfig b/.flowconfig index 8d966996cb3..04806f560c0 100644 --- a/.flowconfig +++ b/.flowconfig @@ -17,6 +17,7 @@ untyped-type-import=error [options] server.max_workers=4 exact_by_default=true +component_syntax=true ;; [generated-start update-flowconfig] module.name_mapper='^lexical$' -> '/packages/lexical/flow/Lexical.js.flow' @@ -105,4 +106,4 @@ nonstrict-import unclear-type [version] -^0.226.0 +^0.250.0 diff --git a/libdefs/yjs.js b/libdefs/yjs.js index b58fc7a834d..8912a4461e9 100644 --- a/libdefs/yjs.js +++ b/libdefs/yjs.js @@ -644,6 +644,7 @@ declare module 'yjs' { }; declare type StackItem = { + // $FlowFixMe: perhaps add generic typing instead of mixed meta: Map, type: 'undo' | 'redo', }; diff --git a/package-lock.json b/package-lock.json index 97f74d56156..d0e83273504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-sort-keys-fix": "^1.1.2", - "flow-bin": "^0.226.0", + "flow-bin": "^0.250.0", "fs-extra": "^10.0.0", "glob": "^10.4.1", "google-closure-compiler": "^20220202.0.0", @@ -17265,10 +17265,11 @@ "dev": true }, "node_modules/flow-bin": { - "version": "0.226.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.226.0.tgz", - "integrity": "sha512-q8hXSRhZ+I14jS0KGDDsPYCvPufvBexk6nJXSOsSP6DgCuXbvCOByWhsXRAjPtmXKmO8v9RKSJm1kRaWaf0fZw==", + "version": "0.250.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.250.0.tgz", + "integrity": "sha512-OYEPzpgSzvV+33kBuOOA1C0AjQkzIjrmbS/324CRRijnU1tABKyM5unzf4KIkyN5IQutgxqsSRZ1GsixC8+xIQ==", "dev": true, + "license": "MIT", "bin": { "flow": "cli.js" }, @@ -49129,9 +49130,9 @@ "dev": true }, "flow-bin": { - "version": "0.226.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.226.0.tgz", - "integrity": "sha512-q8hXSRhZ+I14jS0KGDDsPYCvPufvBexk6nJXSOsSP6DgCuXbvCOByWhsXRAjPtmXKmO8v9RKSJm1kRaWaf0fZw==", + "version": "0.250.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.250.0.tgz", + "integrity": "sha512-OYEPzpgSzvV+33kBuOOA1C0AjQkzIjrmbS/324CRRijnU1tABKyM5unzf4KIkyN5IQutgxqsSRZ1GsixC8+xIQ==", "dev": true }, "flow-enums-runtime": { diff --git a/package.json b/package.json index f1d0d0d401c..e8b913dd2cd 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-sort-keys-fix": "^1.1.2", - "flow-bin": "^0.226.0", + "flow-bin": "^0.250.0", "fs-extra": "^10.0.0", "glob": "^10.4.1", "google-closure-compiler": "^20220202.0.0", diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 3c50a112513..46fa8ccb950 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -12,11 +12,10 @@ import type { LexicalEditor } from 'lexical'; import type {TRefFor} from 'CoreTypes.flow'; import * as React from 'react'; -import type { AbstractComponent } from "react"; type InlineStyle = { - [key: string]: mixed; -} + [key: string]: mixed, +}; // Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement type HTMLDivElementDOMProps = $ReadOnly<{ @@ -29,7 +28,7 @@ type HTMLDivElementDOMProps = $ReadOnly<{ 'aria-invalid'?: void | boolean, 'aria-owns'?: void | string, 'title'?: void | string, - onClick?: void | (e: SyntheticEvent) => mixed, + onClick?: void | ((e: SyntheticEvent) => mixed), autoCapitalize?: void | boolean, autoComplete?: void | boolean, autoCorrect?: void | boolean, @@ -72,11 +71,10 @@ export type Props = $ReadOnly<{ ariaOwns?: string, ariaRequired?: string, autoCapitalize?: boolean, - ref?: TRefFor, - ...PlaceholderProps -}> + ...PlaceholderProps, +}>; -declare export var ContentEditable: AbstractComponent< - Props, - HTMLDivElement, ->; +declare export var ContentEditable: component( + ref: React.RefSetter, + ...Props +); From d64d395ce9db90912684346d0b06b57024dcc8a6 Mon Sep 17 00:00:00 2001 From: Taro Shono Date: Sat, 26 Oct 2024 02:55:44 +0900 Subject: [PATCH 2/2] [lexical-react] Feature: add attributes in LinkPlugin (#6760) --- .../__tests__/e2e/Links.spec.mjs | 43 +++++++++++++++++++ .../__tests__/utils/index.mjs | 3 ++ packages/lexical-playground/src/Editor.tsx | 3 +- packages/lexical-playground/src/Settings.tsx | 6 +++ .../lexical-playground/src/appSettings.ts | 1 + .../src/plugins/LinkPlugin/index.tsx | 22 +++++++++- .../lexical-react/src/LexicalLinkPlugin.ts | 28 +++++++++--- 7 files changed, 96 insertions(+), 10 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index 695ee28d8e6..d0db3462c25 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -12,6 +12,7 @@ import { moveRight, moveToLineBeginning, moveToLineEnd, + paste, selectAll, selectCharacters, STANDARD_KEYPRESS_DELAY_MS, @@ -2076,6 +2077,48 @@ test.describe.parallel('Links', () => { }); }); +test.describe('Link attributes', () => { + test.use({hasLinkAttributes: true}); + test.beforeEach(({isCollab, hasLinkAttributes, page}) => + initialize({hasLinkAttributes, isCollab, page}), + ); + test('Can add attributes with paste', async ({ + page, + context, + hasLinkAttributes, + browserName, + }) => { + if (browserName === 'chromium') { + await focusEditor(page); + await page.keyboard.type('Hello awesome'); + await focusEditor(page); + await selectAll(page); + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.evaluate(() => + navigator.clipboard.writeText('https://facebook.com'), + ); + await paste(page); + await assertHTML( + page, + html` +

+ + Hello awesome + +

+ `, + ); + } + }); +}); + async function setURL(page, url) { await click(page, '.link-edit'); await focus(page, '.link-input'); diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 907db4b110a..c2f4d921ff9 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -58,6 +58,7 @@ export async function initialize({ isCharLimit, isCharLimitUtf8, isMaxLength, + hasLinkAttributes, showNestedEditorTreeView, tableCellMerge, tableCellBackgroundColor, @@ -85,6 +86,7 @@ export async function initialize({ appSettings.isCharLimit = !!isCharLimit; appSettings.isCharLimitUtf8 = !!isCharLimitUtf8; appSettings.isMaxLength = !!isMaxLength; + appSettings.hasLinkAttributes = !!hasLinkAttributes; if (tableCellMerge !== undefined) { appSettings.tableCellMerge = tableCellMerge; } @@ -140,6 +142,7 @@ async function exposeLexicalEditor(page) { } export const test = base.extend({ + hasLinkAttributes: false, isCharLimit: false, isCharLimitUtf8: false, isCollab: IS_COLLAB, diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 9d03a71c21e..31d8a38d433 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -83,6 +83,7 @@ export default function Editor(): JSX.Element { isAutocomplete, isMaxLength, isCharLimit, + hasLinkAttributes, isCharLimitUtf8, isRichText, showTreeView, @@ -185,7 +186,7 @@ export default function Editor(): JSX.Element { - + diff --git a/packages/lexical-playground/src/Settings.tsx b/packages/lexical-playground/src/Settings.tsx index a8e622dc6b0..b015f570d9d 100644 --- a/packages/lexical-playground/src/Settings.tsx +++ b/packages/lexical-playground/src/Settings.tsx @@ -22,6 +22,7 @@ export default function Settings(): JSX.Element { isCollab, isRichText, isMaxLength, + hasLinkAttributes, isCharLimit, isCharLimitUtf8, isAutocomplete, @@ -116,6 +117,11 @@ export default function Settings(): JSX.Element { checked={isCharLimitUtf8} text="Char Limit (UTF-8)" /> + setOption('hasLinkAttributes', !hasLinkAttributes)} + checked={hasLinkAttributes} + text="Link Attributes" + /> setOption('isMaxLength', !isMaxLength)} checked={isMaxLength} diff --git a/packages/lexical-playground/src/appSettings.ts b/packages/lexical-playground/src/appSettings.ts index 61892c2946e..d698af382c2 100644 --- a/packages/lexical-playground/src/appSettings.ts +++ b/packages/lexical-playground/src/appSettings.ts @@ -14,6 +14,7 @@ export const isDevPlayground: boolean = export const DEFAULT_SETTINGS = { disableBeforeInput: false, emptyEditor: isDevPlayground, + hasLinkAttributes: false, isAutocomplete: false, isCharLimit: false, isCharLimitUtf8: false, diff --git a/packages/lexical-playground/src/plugins/LinkPlugin/index.tsx b/packages/lexical-playground/src/plugins/LinkPlugin/index.tsx index 1f3dc437994..68799491dc5 100644 --- a/packages/lexical-playground/src/plugins/LinkPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/LinkPlugin/index.tsx @@ -11,6 +11,24 @@ import * as React from 'react'; import {validateUrl} from '../../utils/url'; -export default function LinkPlugin(): JSX.Element { - return ; +type Props = { + hasLinkAttributes?: boolean; +}; + +export default function LinkPlugin({ + hasLinkAttributes = false, +}: Props): JSX.Element { + return ( + + ); } diff --git a/packages/lexical-react/src/LexicalLinkPlugin.ts b/packages/lexical-react/src/LexicalLinkPlugin.ts index e5f5b0afdc1..1d5da4844a2 100644 --- a/packages/lexical-react/src/LexicalLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalLinkPlugin.ts @@ -6,7 +6,12 @@ * */ -import {$toggleLink, LinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; +import { + $toggleLink, + LinkAttributes, + LinkNode, + TOGGLE_LINK_COMMAND, +} from '@lexical/link'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {mergeRegister, objectKlassEquals} from '@lexical/utils'; import { @@ -20,9 +25,10 @@ import {useEffect} from 'react'; type Props = { validateUrl?: (url: string) => boolean; + attributes?: LinkAttributes; }; -export function LinkPlugin({validateUrl}: Props): null { +export function LinkPlugin({validateUrl, attributes}: Props): null { const [editor] = useLexicalComposerContext(); useEffect(() => { @@ -38,13 +44,18 @@ export function LinkPlugin({validateUrl}: Props): null { return true; } else if (typeof payload === 'string') { if (validateUrl === undefined || validateUrl(payload)) { - $toggleLink(payload); + $toggleLink(payload, attributes); return true; } return false; } else { const {url, target, rel, title} = payload; - $toggleLink(url, {rel, target, title}); + $toggleLink(url, { + ...attributes, + rel, + target, + title, + }); return true; } }, @@ -73,7 +84,10 @@ export function LinkPlugin({validateUrl}: Props): null { } // If we select nodes that are elements then avoid applying the link. if (!selection.getNodes().some((node) => $isElementNode(node))) { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, clipboardText); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, { + ...attributes, + url: clipboardText, + }); event.preventDefault(); return true; } @@ -82,10 +96,10 @@ export function LinkPlugin({validateUrl}: Props): null { COMMAND_PRIORITY_LOW, ) : () => { - // Don't paste arbritrary text as a link when there's no validate function + // Don't paste arbitrary text as a link when there's no validate function }, ); - }, [editor, validateUrl]); + }, [editor, validateUrl, attributes]); return null; }