Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lexical editor integration #2940

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 15 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@ 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: [
{
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$" }]

},
},
{
Expand All @@ -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'],
},
},
],
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions packages/gamut/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
"version": "57.2.2",
"author": "Codecademy Engineering <[email protected]>",
"dependencies": {
"@lexical/clipboard": "0.16.0",
"@lexical/code": "0.16.0",
"@lexical/file": "0.16.0",
"@lexical/hashtag": "0.16.0",
"@lexical/link": "0.16.0",
"@lexical/list": "0.16.0",
"@lexical/mark": "0.16.0",
"@lexical/overflow": "0.16.0",
"@lexical/plain-text": "0.16.0",
"@lexical/react": "0.16.0",
"@lexical/rich-text": "0.16.0",
"@lexical/selection": "0.16.0",
"@lexical/table": "0.16.0",
"@lexical/utils": "0.16.0",
"lexical": "0.16.0",
"@codecademy/gamut-icons": "9.31.0",
"@codecademy/gamut-illustrations": "0.48.0",
"@codecademy/gamut-patterns": "0.9.14",
Expand Down
218 changes: 218 additions & 0 deletions packages/gamut/src/LexicalEditor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* 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 {ClickableLinkPlugin} from '@lexical/react/LexicalClickableLinkPlugin';
// 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 {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import * as React from 'react';
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 ImagesPlugin from './plugins/ImagesPlugin';
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 TableCellResizer from './plugins/TableCellResizer';
import TableOfContentsPlugin from './plugins/TableOfContentsPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';
import ContentEditable from './ui/ContentEditable';
import Placeholder from './ui/Placeholder';

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,
shouldPreserveNewLinesInMarkdown,
tableCellMerge,
tableCellBackgroundColor,
},
} = useSettings();
const isEditable = useLexicalEditable();
const text = isCollab
? 'Enter some collaborative rich text...'
: isRichText
? 'Enter some rich text...'
: 'Enter some plain text...';
const placeholder = <Placeholder>{text}</Placeholder>;
const [floatingAnchorElem, setFloatingAnchorElem] =
useState<HTMLDivElement | null>(null);
const [isSmallWidthViewport, setIsSmallWidthViewport] =
useState<boolean>(false);
const [isLinkEditMode, setIsLinkEditMode] = useState<boolean>(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);
};
}, [isSmallWidthViewport]);

return (
<>
{isRichText && <ToolbarPlugin setIsLinkEditMode={setIsLinkEditMode} />}
<div
className={`editor-container ${showTreeView ? 'tree-view' : ''} ${
!isRichText ? 'plain-text' : ''
}`}>
{isMaxLength && <MaxLengthPlugin maxLength={30} />}
<AutoFocusPlugin />
<ClearEditorPlugin />
<ComponentPickerPlugin />
{/* <AutoEmbedPlugin /> */}

<MentionsPlugin />
<HashtagPlugin />
<KeywordsPlugin />
<AutoLinkPlugin />
{isRichText ? (
<>
{/* {isCollab ? (
// <CollaborationPlugin
// id="main"
// providerFactory={createWebsocketProvider}
// shouldBootstrap={!skipCollaborationInit}
// />
) : (
<HistoryPlugin externalHistoryState={historyState} />
)} */}
<RichTextPlugin
contentEditable={
<div className="editor-scroller">
<div className="editor" ref={onRef}>
<ContentEditable />
</div>
</div>
}
placeholder={placeholder}
ErrorBoundary={LexicalErrorBoundary}
/>
<MarkdownShortcutPlugin />
<CodeHighlightPlugin />
<ListPlugin />
<CheckListPlugin />
<ListMaxIndentLevelPlugin maxDepth={7} />
<TablePlugin
hasCellMerge={tableCellMerge}
hasCellBackgroundColor={tableCellBackgroundColor}
/>
<TableCellResizer />
<LinkPlugin />
<ClickableLinkPlugin disabled={isEditable} />
<HorizontalRulePlugin />
<TabFocusPlugin />
<TabIndentationPlugin />
<LayoutPlugin />
{floatingAnchorElem && !isSmallWidthViewport && (
<>
{/* <CodeActionMenuPlugin anchorElem={floatingAnchorElem} /> */}
<FloatingLinkEditorPlugin
anchorElem={floatingAnchorElem}
isLinkEditMode={isLinkEditMode}
setIsLinkEditMode={setIsLinkEditMode}
/>
<TableCellActionMenuPlugin
anchorElem={floatingAnchorElem}
cellMerge={true}
/>
<FloatingTextFormatToolbarPlugin
anchorElem={floatingAnchorElem}
setIsLinkEditMode={setIsLinkEditMode}
/>
</>
)}
</>
) : (
<>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={placeholder}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin externalHistoryState={historyState} />
</>
)}
{(isCharLimit || isCharLimitUtf8) && (
<CharacterLimitPlugin
charset={isCharLimit ? 'UTF-16' : 'UTF-8'}
maxLength={5}
/>
)}
{isAutocomplete && <AutocompletePlugin />}
<div>{showTableOfContents && <TableOfContentsPlugin />}</div>
{shouldUseLexicalContextMenu && <ContextMenuPlugin />}
{/* <ActionsPlugin
isRichText={isRichText}
shouldPreserveNewLinesInMarkdown={shouldPreserveNewLinesInMarkdown}
/> */}
</div>
{showTreeView && <TreeViewPlugin />}
</>
);
}
Loading
Loading