diff --git a/.eslintignore b/.eslintignore index 9152f93127..745508c629 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,3 +9,6 @@ docs packages/gamut-icons/src/icons packages/gamut-patterns/src/patterns .nx +packages/gamut/src/LexicalEditor +packages/gamut/src/LexicalEditor/**/*.tsx +packages/gamut/src/LexicalEditor/**/*.js diff --git a/.eslintrc.js b/.eslintrc.js index 50b5fa7d66..eaf528a968 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,13 +6,15 @@ module.exports = { 'plugin:react/jsx-runtime', ], - plugins: ['eslint-plugin-gamut'], + plugins: [], rules: { - 'gamut/prefer-themed': 'error', - 'gamut/no-css-standalone': 'error', - 'gamut/import-paths': 'error', + 'gamut/prefer-themed': 'warn', + 'gamut/no-css-standalone': 'warn', + 'gamut/import-paths': 'warn', 'import/no-extraneous-dependencies': 'off', + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^React$" }] + }, overrides: [ @@ -20,12 +22,18 @@ module.exports = { files: ['**/typings/*', '*.d.ts'], rules: { '@typescript-eslint/no-namespace': 'off', + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^React$" }] + }, }, { files: ['*.mdx'], rules: { 'gamut/import-paths': 'off', + "react/*": "off", + "react/react-in-jsx-scope": "off", + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^React$" }] + }, }, { @@ -46,13 +54,15 @@ module.exports = { '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^React$" }] + }, }, { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], plugins: ['lodash'], rules: { - 'lodash/import-scope': ['error', 'method'], + 'lodash/import-scope': ['warn', 'method'], }, }, ], diff --git a/package.json b/package.json index 990e6ad875..d69b9ca8bb 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "lerna": "7.2.0", "lint-staged": "14.0.1", "lodash": "^4.17.5", + "lodash-es": "^4.17.21", "micromatch": "^4.0.5", "mutationobserver-shim": "^0.3.3", "nx": "16.8.1", @@ -67,7 +68,9 @@ "ts-jest": "29.1.1", "ts-node": "10.9.1", "tslib": "2.4.0", - "typescript": "5.1.3" + "typescript": "5.1.3", + "y-websocket": "^2.0.4", + "yjs": "^13.6.19" }, "devDependencies": { "eslint-plugin-lodash": "^7.4.0", diff --git a/packages/gamut/package.json b/packages/gamut/package.json index 42f7d632b5..1e6cc18c73 100644 --- a/packages/gamut/package.json +++ b/packages/gamut/package.json @@ -4,6 +4,21 @@ "version": "57.2.2", "author": "Codecademy Engineering ", "dependencies": { + "@lexical/clipboard": "0.13.1", + "@lexical/code": "0.13.1", + "@lexical/file": "0.13.1", + "@lexical/hashtag": "0.13.1", + "@lexical/link": "0.13.1", + "@lexical/list": "0.13.1", + "@lexical/mark": "0.13.1", + "@lexical/overflow": "0.13.1", + "@lexical/plain-text": "0.13.1", + "@lexical/react": "0.13.1", + "@lexical/rich-text": "0.13.1", + "@lexical/selection": "0.13.1", + "@lexical/table": "0.13.1", + "@lexical/utils": "0.13.1", + "lexical": "0.13.1", "@codecademy/gamut-icons": "9.31.0", "@codecademy/gamut-illustrations": "0.48.0", "@codecademy/gamut-patterns": "0.9.14", diff --git a/packages/gamut/src/LexicalEditor/Editor.tsx b/packages/gamut/src/LexicalEditor/Editor.tsx new file mode 100644 index 0000000000..f35400b56a --- /dev/null +++ b/packages/gamut/src/LexicalEditor/Editor.tsx @@ -0,0 +1,202 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin'; +import {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin'; +import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin'; +import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin'; +// import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin'; +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; +import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin'; +import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; +import {HorizontalRulePlugin} from '@lexical/react/LexicalHorizontalRulePlugin'; +import {ListPlugin} from '@lexical/react/LexicalListPlugin'; +import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin'; +import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; +import {TabIndentationPlugin} from '@lexical/react/LexicalTabIndentationPlugin'; +import {useEffect, useState} from 'react'; +// import {CAN_USE_DOM} from './shared/canUseDOM'; + +// import {createWebsocketProvider} from './collaboration'; +import {useSettings} from './context/SettingsContext'; +import {useSharedHistoryContext} from './context/SharedHistoryContext'; +// import ActionsPlugin from './plugins/ActionsPlugin'; +import AutocompletePlugin from './plugins/AutocompletePlugin'; +// import AutoEmbedPlugin from './plugins/AutoEmbedPlugin'; +import AutoLinkPlugin from './plugins/AutoLinkPlugin'; +// import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin'; +import CodeHighlightPlugin from './plugins/CodeHighlightPlugin'; +import ComponentPickerPlugin from './plugins/ComponentPickerPlugin'; +import ContextMenuPlugin from './plugins/ContextMenuPlugin'; +import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin'; +import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin'; +import KeywordsPlugin from './plugins/KeywordsPlugin'; +import {LayoutPlugin} from './plugins/LayoutPlugin/LayoutPlugin'; +import LinkPlugin from './plugins/LinkPlugin'; +import ListMaxIndentLevelPlugin from './plugins/ListMaxIndentLevelPlugin'; +import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin'; +import {MaxLengthPlugin} from './plugins/MaxLengthPlugin'; +import MentionsPlugin from './plugins/MentionsPlugin'; +import TabFocusPlugin from './plugins/TabFocusPlugin'; +import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; +import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'; +import ToolbarPlugin from './plugins/ToolbarPlugin'; +import TreeViewPlugin from './plugins/TreeViewPlugin'; +import ContentEditable from './ui/ContentEditable'; + +const skipCollaborationInit = + // @ts-expect-error + window.parent != null && window.parent.frames.right === window; + +export default function Editor(): JSX.Element { + const {historyState} = useSharedHistoryContext(); + const { + settings: { + isCollab, + isAutocomplete, + isMaxLength, + isCharLimit, + isCharLimitUtf8, + isRichText, + showTreeView, + showTableOfContents, + shouldUseLexicalContextMenu, + }, + } = useSettings(); + const text = isCollab + ? 'Enter some collaborative rich text...' + : isRichText + ? 'Enter some rich text...' + : 'Enter some plain text...'; + const [floatingAnchorElem, setFloatingAnchorElem] = + useState(null); + const [isLinkEditMode, setIsLinkEditMode] = useState(false); + + const onRef = (_floatingAnchorElem: HTMLDivElement) => { + if (_floatingAnchorElem !== null) { + setFloatingAnchorElem(_floatingAnchorElem); + } + }; + + useEffect(() => { + const updateViewPortWidth = () => { + // const isNextSmallWidthViewport = + // CAN_USE_DOM && window.matchMedia('(max-width: 1025px)').matches; + + // if (isNextSmallWidthViewport !== isSmallWidthViewport) { + // setIsSmallWidthViewport(isNextSmallWidthViewport); + // } + }; + updateViewPortWidth(); + window.addEventListener('resize', updateViewPortWidth); + + return () => { + window.removeEventListener('resize', updateViewPortWidth); + }; + }, []); + + return ( + <> + {isRichText && } +
+ {isMaxLength && } + + + + {/* */} + + + + + + {isRichText ? ( + <> + {/* {isCollab ? ( + // + ) : ( + + )} */} + +
+ +
+
+ } + placeholder={<>} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + {/* */} + + + + + + {floatingAnchorElem && ( + <> + {/* */} + + + + + )} + + ) : ( + <> + } + placeholder={<>} + ErrorBoundary={LexicalErrorBoundary} + /> + + + )} + {(isCharLimit || isCharLimitUtf8) && ( + + )} + {isAutocomplete && } +
{showTableOfContents && }
+ {shouldUseLexicalContextMenu && } + {/* */} + + {showTreeView && } + + ); +} diff --git a/packages/gamut/src/LexicalEditor/LexicalEditor.tsx b/packages/gamut/src/LexicalEditor/LexicalEditor.tsx new file mode 100644 index 0000000000..71a909e560 --- /dev/null +++ b/packages/gamut/src/LexicalEditor/LexicalEditor.tsx @@ -0,0 +1,192 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {$createLinkNode} from '@lexical/link'; +import {$createListItemNode, $createListNode} from '@lexical/list'; +import {LexicalComposer} from '@lexical/react/LexicalComposer'; +import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text'; +import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical'; + +import {FlashMessageContext} from './context/FlashMessageContext'; +import {SettingsContext, useSettings} from './context/SettingsContext'; +import {SharedAutocompleteContext} from './context/SharedAutocompleteContext'; +import {SharedHistoryContext} from './context/SharedHistoryContext'; +import Editor from './Editor'; +import logo from './images/logo.svg'; +import PlaygroundNodes from './nodes/PlaygroundNodes'; +import {TableContext} from './plugins/TablePlugin'; +import Settings from './Settings'; +import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme'; + +console.warn( + 'If you are profiling the playground app, please ensure you turn off the debug view. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.', +); + +function $prepopulatedRichText() { + const root = $getRoot(); + if (root.getFirstChild() === null) { + const heading = $createHeadingNode('h1'); + heading.append($createTextNode('Welcome to the playground')); + root.append(heading); + const quote = $createQuoteNode(); + quote.append( + $createTextNode( + `In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. ` + + `You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.`, + ), + ); + root.append(quote); + const paragraph = $createParagraphNode(); + paragraph.append( + $createTextNode('The playground is a demo environment built with '), + $createTextNode('@lexical/react').toggleFormat('code'), + $createTextNode('.'), + $createTextNode(' Try typing in '), + $createTextNode('some text').toggleFormat('bold'), + $createTextNode(' with '), + $createTextNode('different').toggleFormat('italic'), + $createTextNode(' formats.'), + ); + root.append(paragraph); + const paragraph2 = $createParagraphNode(); + paragraph2.append( + $createTextNode( + 'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!', + ), + ); + root.append(paragraph2); + const paragraph3 = $createParagraphNode(); + paragraph3.append( + $createTextNode(`If you'd like to find out more about Lexical, you can:`), + ); + root.append(paragraph3); + const list = $createListNode('bullet'); + list.append( + $createListItemNode().append( + $createTextNode(`Visit the `), + $createLinkNode('https://lexical.dev/').append( + $createTextNode('Lexical website'), + ), + $createTextNode(` for documentation and more information.`), + ), + $createListItemNode().append( + $createTextNode(`Check out the code on our `), + $createLinkNode('https://github.com/facebook/lexical').append( + $createTextNode('GitHub repository'), + ), + $createTextNode(`.`), + ), + $createListItemNode().append( + $createTextNode(`Playground code can be found `), + $createLinkNode( + 'https://github.com/facebook/lexical/tree/main/packages/lexical-playground', + ).append($createTextNode('here')), + $createTextNode(`.`), + ), + $createListItemNode().append( + $createTextNode(`Join our `), + $createLinkNode('https://discord.com/invite/KmG4wQnnD9').append( + $createTextNode('Discord Server'), + ), + $createTextNode(` and chat with the team.`), + ), + ); + root.append(list); + const paragraph4 = $createParagraphNode(); + paragraph4.append( + $createTextNode( + `Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).`, + ), + ); + root.append(paragraph4); + } +} + +function App(): JSX.Element { + const { + settings: {isCollab, emptyEditor, measureTypingPerf}, + } = useSettings(); + + const initialConfig = { + editorState: isCollab + ? null + : emptyEditor + ? undefined + : $prepopulatedRichText, + namespace: 'Playground', + nodes: [...PlaygroundNodes], + onError: (error: Error) => { + throw error; + }, + theme: PlaygroundEditorTheme, + }; + + return ( + + + + +
+ + Lexical Logo + +
+
+ +
+ +
+
+
+
+ ); +} + +export function PlaygroundApp(): JSX.Element { + return ( + + + + + + + + + ); +} \ No newline at end of file diff --git a/packages/gamut/src/LexicalEditor/Settings.tsx b/packages/gamut/src/LexicalEditor/Settings.tsx new file mode 100644 index 0000000000..a8e622dc6b --- /dev/null +++ b/packages/gamut/src/LexicalEditor/Settings.tsx @@ -0,0 +1,168 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {CAN_USE_BEFORE_INPUT} from '@lexical/utils'; +import {useEffect, useMemo, useState} from 'react'; + +import {INITIAL_SETTINGS, isDevPlayground} from './appSettings'; +import {useSettings} from './context/SettingsContext'; +import Switch from './ui/Switch'; + +export default function Settings(): JSX.Element { + const windowLocation = window.location; + const { + setOption, + settings: { + measureTypingPerf, + isCollab, + isRichText, + isMaxLength, + isCharLimit, + isCharLimitUtf8, + isAutocomplete, + showTreeView, + showNestedEditorTreeView, + disableBeforeInput, + showTableOfContents, + shouldUseLexicalContextMenu, + shouldPreserveNewLinesInMarkdown, + }, + } = useSettings(); + useEffect(() => { + if (INITIAL_SETTINGS.disableBeforeInput && CAN_USE_BEFORE_INPUT) { + console.error( + `Legacy events are enabled (disableBeforeInput) but CAN_USE_BEFORE_INPUT is true`, + ); + } + }, []); + const [showSettings, setShowSettings] = useState(false); + const [isSplitScreen, search] = useMemo(() => { + const parentWindow = window.parent; + const _search = windowLocation.search; + const _isSplitScreen = + parentWindow && parentWindow.location.pathname === '/split/'; + return [_isSplitScreen, _search]; + }, [windowLocation]); + + return ( + <> +