mirror of
https://github.com/facebook/lexical.git
synced 2025-08-26 02:39:24 +08:00
Add UI components to Lexical Playground (#1211)
* Add UI components to Lexical Playground * Add autofocus plugin * Fix test
This commit is contained in:

committed by
acywatson

parent
1b6e5d254e
commit
fa107c93fb
@ -17,6 +17,10 @@ describe('Selection', () => {
|
||||
return document.activeElement === editorElement;
|
||||
});
|
||||
const {page} = e2e;
|
||||
await evaluate(page, () => {
|
||||
const editorElement = document.querySelector('div[contenteditable="true"]');
|
||||
return editorElement.blur();
|
||||
});
|
||||
expect(await editorHasFocus()).toEqual(false);
|
||||
await sleep(500);
|
||||
expect(await editorHasFocus()).toEqual(false);
|
||||
|
@ -39,6 +39,7 @@ import ContentEditable from './ui/ContentEditable';
|
||||
import AutoLinkPlugin from './plugins/AutoLinkPlugin';
|
||||
import PollPlugin from './plugins/PollPlugin';
|
||||
import {useSettings} from './context/SettingsContext';
|
||||
import AutoFocusPlugin from './plugins/AutoFocusPlugin';
|
||||
|
||||
const skipCollaborationInit =
|
||||
window.parent != null && window.parent.frames.right === window;
|
||||
@ -69,6 +70,7 @@ export default function Editor(): React$Node {
|
||||
className={`editor-container ${showTreeView ? 'tree-view' : ''} ${
|
||||
!isRichText ? 'plain-text' : ''
|
||||
}`}>
|
||||
<AutoFocusPlugin />
|
||||
<StickyPlugin />
|
||||
<MentionsPlugin />
|
||||
<EmojisPlugin />
|
||||
|
48
packages/lexical-playground/src/hooks/useModal.js
Normal file
48
packages/lexical-playground/src/hooks/useModal.js
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
export default function useModal(): [
|
||||
React$Node,
|
||||
(string, (() => void) => React$Node) => void,
|
||||
] {
|
||||
const [modalContent, setModalContent] = useState<null | {
|
||||
content: React$Node,
|
||||
title: string,
|
||||
}>(null);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setModalContent(null);
|
||||
}, []);
|
||||
|
||||
const modal = useMemo(() => {
|
||||
if (modalContent === null) {
|
||||
return null;
|
||||
}
|
||||
const {title, content} = modalContent;
|
||||
return (
|
||||
<Modal onClose={onClose} title={title}>
|
||||
{content}
|
||||
</Modal>
|
||||
);
|
||||
}, [modalContent, onClose]);
|
||||
|
||||
const showModal = useCallback(
|
||||
(title, getContent: (() => void) => React$Node) => {
|
||||
setModalContent({title, content: getContent(onClose)});
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
return [modal, showModal];
|
||||
}
|
@ -35,7 +35,7 @@ header h1 {
|
||||
.editor-shell {
|
||||
margin: 20px auto 20px auto;
|
||||
border-radius: 2px;
|
||||
width: 940px;
|
||||
width: 960px;
|
||||
color: #000;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
@ -62,7 +62,7 @@ header h1 {
|
||||
|
||||
.test-recorder-output {
|
||||
margin: 20px auto 20px auto;
|
||||
width: 940px;
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
22
packages/lexical-playground/src/plugins/AutoFocusPlugin.js
Normal file
22
packages/lexical-playground/src/plugins/AutoFocusPlugin.js
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export default function AutoFocusPlugin(): null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
editor.focus();
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
}
|
@ -40,6 +40,19 @@ import {getCodeLanguages, getDefaultCodeLanguage} from './CodeHighlightPlugin';
|
||||
import {$getNearestNodeOfType} from '@lexical/helpers/nodes';
|
||||
// $FlowFixMe
|
||||
import {createPortal} from 'react-dom';
|
||||
import useModal from '../hooks/useModal';
|
||||
import Input from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import stylex from 'stylex';
|
||||
|
||||
const styles = stylex.create({
|
||||
dialogActions: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'right',
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const LowPriority: CommandListenerLowPriority = 1;
|
||||
|
||||
@ -221,6 +234,32 @@ function FloatingLinkEditor({editor}: {editor: LexicalEditor}): React$Node {
|
||||
);
|
||||
}
|
||||
|
||||
function InsertTableDialog({
|
||||
activeEditor,
|
||||
onClose,
|
||||
}: {
|
||||
activeEditor: LexicalEditor,
|
||||
onClose: () => void,
|
||||
}): React$Node {
|
||||
const [rows, setRows] = useState('3');
|
||||
const [columns, setColumns] = useState('3');
|
||||
|
||||
const onClick = () => {
|
||||
activeEditor.execCommand('insertTable', {rows, columns});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input label="No of rows" onChange={setRows} value={rows} />
|
||||
<Input label="No of columns" onChange={setColumns} value={columns} />
|
||||
<div className={stylex(styles.dialogActions)}>
|
||||
<Button onClick={onClick}>Confirm</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockOptionsDropdownList({
|
||||
editor,
|
||||
blockType,
|
||||
@ -438,6 +477,7 @@ export default function ToolbarPlugin(): React$Node {
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [modal, showModal] = useModal();
|
||||
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] =
|
||||
useState(false);
|
||||
const [codeLanguage, setCodeLanguage] = useState<string>('');
|
||||
@ -740,7 +780,12 @@ export default function ToolbarPlugin(): React$Node {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
activeEditor.execCommand('insertTable');
|
||||
showModal('Insert Table', (onClose) => (
|
||||
<InsertTableDialog
|
||||
activeEditor={activeEditor}
|
||||
onClose={onClose}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
className="toolbar-item"
|
||||
aria-label="Insert Table">
|
||||
@ -807,6 +852,7 @@ export default function ToolbarPlugin(): React$Node {
|
||||
aria-label="Indent">
|
||||
<i className="format indent" />
|
||||
</button>
|
||||
{modal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
41
packages/lexical-playground/src/ui/Button.js
Normal file
41
packages/lexical-playground/src/ui/Button.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import stylex from 'stylex';
|
||||
|
||||
const styles = stylex.create({
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
border: 0,
|
||||
backgroundColor: '#eee',
|
||||
borderRadius: 5,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: '#ddd',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default function Button({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React$Node,
|
||||
onClick: () => void,
|
||||
}): React$Node {
|
||||
return (
|
||||
<button className={stylex(styles.root)} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
60
packages/lexical-playground/src/ui/Input.js
Normal file
60
packages/lexical-playground/src/ui/Input.js
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import stylex from 'stylex';
|
||||
|
||||
const styles = stylex.create({
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
label: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
color: '#666',
|
||||
},
|
||||
input: {
|
||||
display: 'flex',
|
||||
flex: 2,
|
||||
border: '1px solid #999',
|
||||
paddingTop: 7,
|
||||
paddingBottom: 7,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
fontSize: 16,
|
||||
borderRadius: 5,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Input({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string,
|
||||
onChange: (string) => void,
|
||||
value: string,
|
||||
}): React$Node {
|
||||
return (
|
||||
<div className={stylex(styles.wrapper)}>
|
||||
<label className={stylex(styles.label)}>{label}</label>
|
||||
<input
|
||||
className={stylex(styles.input)}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
140
packages/lexical-playground/src/ui/Modal.js
Normal file
140
packages/lexical-playground/src/ui/Modal.js
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
// $FlowFixMe
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import stylex from 'stylex';
|
||||
|
||||
const styles = stylex.create({
|
||||
overlay: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
flexDirection: 'column',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(40, 40, 40, 0.6)',
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
zIndex: 100,
|
||||
},
|
||||
modal: {
|
||||
padding: 20,
|
||||
minHeight: 100,
|
||||
minWidth: 400,
|
||||
display: 'flex',
|
||||
flexGrow: 0,
|
||||
backgroundColor: '#fff',
|
||||
maxHeight: 300,
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
boxShadow: '0 0 20px 0 #444',
|
||||
borderRadius: 10,
|
||||
},
|
||||
title: {
|
||||
color: '#444',
|
||||
margin: 0,
|
||||
paddingBottom: 10,
|
||||
borderBottom: '1px solid #ccc',
|
||||
},
|
||||
closeButton: {
|
||||
border: 0,
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
width: 30,
|
||||
height: 30,
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#eee',
|
||||
':hover': {
|
||||
backgroundColor: '#ddd',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
paddingTop: 20,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
});
|
||||
|
||||
function PortalImpl({
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
onClose: () => void,
|
||||
children: React$Node,
|
||||
title: string,
|
||||
}) {
|
||||
const modalRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalRef.current !== null) {
|
||||
modalRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className={stylex(styles.overlay)} onClick={() => onClose()}>
|
||||
<div
|
||||
className={stylex(styles.modal)}
|
||||
tabIndex={-1}
|
||||
ref={modalRef}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className={stylex(styles.title)}>{title}</h2>
|
||||
<button
|
||||
className={stylex(styles.closeButton)}
|
||||
aria-label="Close modal"
|
||||
onClick={() => onClose()}>
|
||||
X
|
||||
</button>
|
||||
<div className={stylex(styles.content)}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
onClose: () => void,
|
||||
children: React$Node,
|
||||
title: string,
|
||||
}): React$Node {
|
||||
return createPortal(
|
||||
<PortalImpl onClose={onClose} title={title}>
|
||||
{children}
|
||||
</PortalImpl>,
|
||||
document.body,
|
||||
);
|
||||
}
|
@ -26,8 +26,9 @@ export default function TablePlugin(): React$Node {
|
||||
useEffect(() => {
|
||||
const removeCommandListener = editor.addListener(
|
||||
'command',
|
||||
(type) => {
|
||||
(type, payload) => {
|
||||
if (type === 'insertTable') {
|
||||
const {columns, rows} = payload;
|
||||
$log('handleAddTable');
|
||||
const selection = $getSelection();
|
||||
if (selection === null) {
|
||||
@ -37,7 +38,7 @@ export default function TablePlugin(): React$Node {
|
||||
|
||||
if (focusNode !== null) {
|
||||
const topLevelNode = focusNode.getTopLevelElementOrThrow();
|
||||
const tableNode = $createTableNodeWithDimensions(3, 3);
|
||||
const tableNode = $createTableNodeWithDimensions(rows, columns);
|
||||
topLevelNode.insertAfter(tableNode);
|
||||
tableNode.insertAfter($createParagraphNode());
|
||||
const firstCell = tableNode
|
||||
|
Reference in New Issue
Block a user