feat: ability to drag super list items; secure password generation blocks (#2039)

* feat: ability to drag list item nodes

* fix: issue where editor focus would scroll to bottom

* fix: improve drag icon and prevent from interfering with selection

* fix(super): add 'current' as keyword for bringing up date block options

* fix(super): issue with autocomplete menu width on large screens

* feat(super): ability to generate secure random passwords
This commit is contained in:
Mo
2022-11-19 08:53:30 -06:00
committed by GitHub
parent 7f28876047
commit c39c72da7a
19 changed files with 238 additions and 62 deletions

View File

@@ -98,7 +98,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<div className="editor" ref={onRef}> <div className="editor" ref={onRef}>
<ContentEditable <ContentEditable
id={SuperEditorContentId} id={SuperEditorContentId}
className={`ContentEditable__root ${className}`} className={`ContentEditable__root overflow-y-auto ${className}`}
spellCheck={spellcheck} spellCheck={spellcheck}
/> />
</div> </div>

View File

@@ -1,18 +1,21 @@
.draggable-block-menu { .draggable-block-menu {
border-radius: 4px; border-radius: 4px;
padding: 2px 1px; padding: 3px 1px;
cursor: grab; cursor: grab;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
will-change: transform; will-change: transform;
transition: opacity 0.3s;
} }
.draggable-block-menu .icon { .draggable-block-menu .icon {
width: 1rem; width: 0.8rem;
height: 1rem; height: 1.1rem;
opacity: 0.4; opacity: 0.2;
padding-left: 4.75px;
padding-top: 2px;
} }
.draggable-block-menu:active { .draggable-block-menu:active {
@@ -21,7 +24,6 @@
.draggable-block-menu:hover { .draggable-block-menu:hover {
background-color: var(--sn-stylekit-contrast-background-color); background-color: var(--sn-stylekit-contrast-background-color);
padding: 3px;
} }
.draggable-block-target-line { .draggable-block-target-line {
@@ -32,5 +34,6 @@
left: 0; left: 0;
top: 0; top: 0;
opacity: 0; opacity: 0;
will-change: transform; will-change: transform, opacity;
transition: opacity 0.15s;
} }

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
* *
*/ */
import {$createListNode, $isListNode} from '@lexical/list';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {eventFiles} from '@lexical/rich-text'; import {eventFiles} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';
@@ -19,17 +20,17 @@ import {
} from 'lexical'; } from 'lexical';
import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react'; import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom'; import {createPortal} from 'react-dom';
import {LexicalDraggableBlockMenu} from '@standardnotes/icons'; import {BlockIcon} from '@standardnotes/icons';
import {isHTMLElement} from '../../Utils/guard'; import {isHTMLElement} from '../../Utils/guard';
import {Point} from '../../Utils/point'; import {Point} from '../../Utils/point';
import {Rect} from '../../Utils/rect'; import {ContainsPointReturn, Rect} from '../../Utils/rect';
const SPACE = 4; const SPACE = 4;
const TARGET_LINE_HALF_HEIGHT = 2; const TARGET_LINE_HALF_HEIGHT = 2;
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'; const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'; const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
const TEXT_BOX_HORIZONTAL_PADDING = 28; const TEXT_BOX_HORIZONTAL_PADDING = 24;
const Downward = 1; const Downward = 1;
const Upward = -1; const Upward = -1;
@@ -53,12 +54,56 @@ function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
return root ? root.__children : []; return root ? root.__children : [];
} }
function elementContainingEventLocation(
anchorElem: HTMLElement,
element: HTMLElement,
event: MouseEvent,
): {contains: ContainsPointReturn; element: HTMLElement} {
const anchorElementRect = anchorElem.getBoundingClientRect();
const eventLocation = new Point(event.x, event.y);
const elementDomRect = Rect.fromDOM(element);
const {marginTop, marginBottom} = window.getComputedStyle(element);
const rect = elementDomRect.generateNewRect({
bottom: elementDomRect.bottom + parseFloat(marginBottom),
left: anchorElementRect.left,
right: anchorElementRect.right,
top: elementDomRect.top - parseFloat(marginTop),
});
const children = Array.from(element.children);
const shouldRecurseIntoChildren = ['UL', 'OL', 'LI'].includes(
element.tagName,
);
if (shouldRecurseIntoChildren) {
for (const child of children) {
const isLeaf = child.children.length === 0;
if (isLeaf) {
continue;
}
const childResult = elementContainingEventLocation(
anchorElem,
child as HTMLElement,
event,
);
if (childResult.contains.result) {
return childResult;
}
}
}
return {contains: rect.contains(eventLocation), element: element};
}
function getBlockElement( function getBlockElement(
anchorElem: HTMLElement, anchorElem: HTMLElement,
editor: LexicalEditor, editor: LexicalEditor,
event: MouseEvent, event: MouseEvent,
): HTMLElement | null { ): HTMLElement | null {
const anchorElementRect = anchorElem.getBoundingClientRect();
const topLevelNodeKeys = getTopLevelNodeKeys(editor); const topLevelNodeKeys = getTopLevelNodeKeys(editor);
let blockElem: HTMLElement | null = null; let blockElem: HTMLElement | null = null;
@@ -73,32 +118,22 @@ function getBlockElement(
if (elem === null) { if (elem === null) {
break; break;
} }
const point = new Point(event.x, event.y); const {contains, element} = elementContainingEventLocation(
const domRect = Rect.fromDOM(elem); anchorElem,
const {marginTop, marginBottom} = window.getComputedStyle(elem); elem,
event,
);
const rect = domRect.generateNewRect({ if (contains.result) {
bottom: domRect.bottom + parseFloat(marginBottom), blockElem = element;
left: anchorElementRect.left,
right: anchorElementRect.right,
top: domRect.top - parseFloat(marginTop),
});
const {
result,
reason: {isOnTopSide, isOnBottomSide},
} = rect.contains(point);
if (result) {
blockElem = elem;
prevIndex = index; prevIndex = index;
break; break;
} }
if (direction === Indeterminate) { if (direction === Indeterminate) {
if (isOnTopSide) { if (contains.reason.isOnTopSide) {
direction = Upward; direction = Upward;
} else if (isOnBottomSide) { } else if (contains.reason.isOnBottomSide) {
direction = Downward; direction = Downward;
} else { } else {
// stop search block element // stop search block element
@@ -124,7 +159,6 @@ function setMenuPosition(
) { ) {
if (!targetElem) { if (!targetElem) {
floatingElem.style.opacity = '0'; floatingElem.style.opacity = '0';
floatingElem.style.transform = 'translate(-10000px, -10000px)';
return; return;
} }
@@ -186,13 +220,12 @@ function setTargetLine(
targetLineElem.style.width = `${ targetLineElem.style.width = `${
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2 anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
}px`; }px`;
targetLineElem.style.opacity = '.4'; targetLineElem.style.opacity = '.6';
} }
function hideTargetLine(targetLineElem: HTMLElement | null) { function hideTargetLine(targetLineElem: HTMLElement | null) {
if (targetLineElem) { if (targetLineElem) {
targetLineElem.style.opacity = '0'; targetLineElem.style.opacity = '0';
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
} }
} }
@@ -284,18 +317,30 @@ function useDraggableBlockMenu(
return false; return false;
} }
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem); const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
if (!targetNode) { if (!targetNode) {
return false; return false;
} }
if (targetNode === draggedNode) { if (targetNode === draggedNode) {
return true; return true;
} }
let nodeToInsert = draggedNode;
const targetParent = targetNode.getParent();
const sourceParent = draggedNode.getParent();
if ($isListNode(sourceParent) && !$isListNode(targetParent)) {
const newList = $createListNode(sourceParent.getListType());
newList.append(draggedNode);
nodeToInsert = newList;
}
const {top, height} = targetBlockElem.getBoundingClientRect(); const {top, height} = targetBlockElem.getBoundingClientRect();
const shouldInsertAfter = pageY - top > height / 2; const shouldInsertAfter = pageY - top > height / 2;
if (shouldInsertAfter) { if (shouldInsertAfter) {
targetNode.insertAfter(draggedNode); targetNode.insertAfter(nodeToInsert);
} else { } else {
targetNode.insertBefore(draggedNode); targetNode.insertBefore(nodeToInsert);
} }
setDraggableBlockElem(null); setDraggableBlockElem(null);
@@ -349,7 +394,7 @@ function useDraggableBlockMenu(
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd}> onDragEnd={onDragEnd}>
<div className={isEditable ? 'icon' : ''}> <div className={isEditable ? 'icon' : ''}>
<LexicalDraggableBlockMenu className="text-text pointer-events-none" /> <BlockIcon className="text-text pointer-events-none" />
</div> </div>
</div> </div>
<div className="draggable-block-target-line" ref={targetLineRef} /> <div className="draggable-block-target-line" ref={targetLineRef} />

View File

@@ -110,8 +110,8 @@ export function InsertTableDialog({
return ( return (
<> <>
<TextInput label="No of rows" onChange={setRows} value={rows} /> <TextInput label="Number of rows" onChange={setRows} value={rows} />
<TextInput label="No of columns" onChange={setColumns} value={columns} /> <TextInput label="Number of columns" onChange={setColumns} value={columns} />
<DialogActions data-test-id="table-model-confirm-insert"> <DialogActions data-test-id="table-model-confirm-insert">
<Button onClick={onClick}>Confirm</Button> <Button onClick={onClick}>Confirm</Button>
</DialogActions> </DialogActions>

View File

@@ -13,13 +13,13 @@
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
border: 0px; border: 0px;
background-color: #eee; background-color: var(--sn-stylekit-contrast-background-color);
border-radius: 5px;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
} }
.Button__root:hover { .Button__root:hover {
background-color: #ddd; background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
} }
.Button__small { .Button__small {
padding-top: 5px; padding-top: 5px;
@@ -32,5 +32,5 @@
cursor: not-allowed; cursor: not-allowed;
} }
.Button__disabled:hover { .Button__disabled:hover {
background-color: #eee; background-color: var(--sn-stylekit-secondary-background-color);
} }

View File

@@ -17,16 +17,17 @@
display: flex; display: flex;
flex: 1; flex: 1;
color: #666; color: #666;
margin-right: 20px;
} }
.Input__input { .Input__input {
display: flex; display: flex;
flex: 2; flex: 2;
border: 1px solid #999; border: 1px solid var(--sn-stylekit-contrast-border-color);
background-color: var(--sn-stylekit-contrast-background-color);
padding-top: 7px; padding-top: 7px;
padding-bottom: 7px; padding-bottom: 7px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
font-size: 16px; font-size: 16px;
border-radius: 5px;
min-width: 0; min-width: 0;
} }

View File

@@ -17,7 +17,7 @@
bottom: 0px; bottom: 0px;
left: 0px; left: 0px;
right: 0px; right: 0px;
background-color: rgba(40, 40, 40, 0.6); background-color: rgba(0, 0, 0, 0.7);
flex-grow: 0px; flex-grow: 0px;
flex-shrink: 1px; flex-shrink: 1px;
z-index: 100; z-index: 100;
@@ -28,22 +28,23 @@
min-width: 300px; min-width: 300px;
display: flex; display: flex;
flex-grow: 0px; flex-grow: 0px;
background-color: #fff; background-color: var(--sn-stylekit-background-color);
flex-direction: column; flex-direction: column;
position: relative; position: relative;
box-shadow: 0 0 20px 0 #444; box-shadow: 0 0px 0 var(--sn-stylekit-shadow-color);
border-radius: 10px; border-radius: 0px;
} }
.Modal__title { .Modal__title {
color: #444; color:var(--sn-stylekit-foreground-color);
margin: 0px; margin: 0px;
padding-bottom: 10px; padding-bottom: 15px;
border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--sn-stylekit-border-color);
} }
.Modal__closeButton { .Modal__closeButton {
border: 0px; border: 0px;
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 15px;
border-radius: 20px; border-radius: 20px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -52,10 +53,11 @@
height: 30px; height: 30px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
background-color: #eee; background-color: var(--sn-stylekit-contrast-background-color);
} }
.Modal__closeButton:hover { .Modal__closeButton:hover {
background-color: #ddd; background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
} }
.Modal__content { .Modal__content {
padding-top: 20px; padding-top: 20px;

View File

@@ -73,7 +73,7 @@ function PortalImpl({
aria-label="Close modal" aria-label="Close modal"
type="button" type="button"
onClick={onClose}> onClick={onClose}>
X
</button> </button>
<div className="Modal__content">{children}</div> <div className="Modal__content">{children}</div>
</div> </div>

View File

@@ -0,0 +1,7 @@
<svg width="9px" height="14px" viewBox="0 0 9 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2" fill="currentColor" fill-rule="nonzero">
<path d="M1.6666,3.33333 C1.2246,3.33333 0.8007,3.15774 0.4881,2.84518 C0.1755,2.53262 0,2.1087 0,1.66667 C0,1.22464 0.1755,0.80072 0.4881,0.48816 C0.8007,0.1756 1.2246,0 1.6666,0 C2.1086,0 2.5326,0.1756 2.8451,0.48816 C3.1577,0.80072 3.3333,1.22464 3.3333,1.66667 C3.3333,2.1087 3.1577,2.53262 2.8451,2.84518 C2.5326,3.15774 2.1086,3.33333 1.6666,3.33333 Z M1.6666,8.33333 C1.2246,8.33333 0.8007,8.15773 0.4881,7.84513 C0.1755,7.53263 0,7.10873 0,6.66663 C0,6.22463 0.1755,5.80073 0.4881,5.48813 C0.8007,5.17563 1.2246,5.00003 1.6666,5.00003 C2.1086,5.00003 2.5326,5.17563 2.8451,5.48813 C3.1577,5.80073 3.3333,6.22463 3.3333,6.66663 C3.3333,7.10873 3.1577,7.53263 2.8451,7.84513 C2.5326,8.15773 2.1086,8.33333 1.6666,8.33333 Z M1.6666,13.33333 C1.2246,13.33333 0.8007,13.15773 0.4881,12.84513 C0.1755,12.53263 0,12.10873 0,11.66663 C0,11.22463 0.1755,10.80073 0.4881,10.48813 C0.8007,10.17563 1.2246,10.00003 1.6666,10.00003 C2.1086,10.00003 2.5326,10.17563 2.8451,10.48813 C3.1577,10.80073 3.3333,11.22463 3.3333,11.66663 C3.3333,12.10873 3.1577,12.53263 2.8451,12.84513 C2.5326,13.15773 2.1086,13.33333 1.6666,13.33333 L1.6666,13.33333 Z M6.6666,3.33333 C6.2246,3.33333 5.8007,3.15774 5.4881,2.84518 C5.1755,2.53262 5,2.1087 5,1.66667 C5,1.22464 5.1755,0.80072 5.4881,0.48816 C5.8007,0.1756 6.2246,0 6.6666,0 C7.1086,0 7.5326,0.1756 7.8451,0.48816 C8.1577,0.80072 8.3333,1.22464 8.3333,1.66667 C8.3333,2.1087 8.1577,2.53262 7.8451,2.84518 C7.5326,3.15774 7.1086,3.33333 6.6666,3.33333 Z M6.6666,8.33333 C6.2246,8.33333 5.8007,8.15773 5.4881,7.84513 C5.1755,7.53263 5,7.10873 5,6.66663 C5,6.22463 5.1755,5.80073 5.4881,5.48813 C5.8007,5.17563 6.2246,5.00003 6.6666,5.00003 C7.1086,5.00003 7.5326,5.17563 7.8451,5.48813 C8.1577,5.80073 8.3333,6.22463 8.3333,6.66663 C8.3333,7.10873 8.1577,7.53263 7.8451,7.84513 C7.5326,8.15773 7.1086,8.33333 6.6666,8.33333 Z M6.6666,13.33333 C6.2246,13.33333 5.8007,13.15773 5.4881,12.84513 C5.1755,12.53263 5,12.10873 5,11.66663 C5,11.22463 5.1755,10.80073 5.4881,10.48813 C5.8007,10.17563 6.2246,10.00003 6.6666,10.00003 C7.1086,10.00003 7.5326,10.17563 7.8451,10.48813 C8.1577,10.80073 8.3333,11.22463 8.3333,11.66663 C8.3333,12.10873 8.1577,12.53263 7.8451,12.84513 C7.5326,13.15773 7.1086,13.33333 6.6666,13.33333 L6.6666,13.33333 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -21,6 +21,7 @@ import AttachmentFileIcon from './ic-attachment-file.svg'
import AuthenticatorIcon from './ic-authenticator.svg' import AuthenticatorIcon from './ic-authenticator.svg'
import AuthenticatorVariantIcon from './ic-authenticator-variant.svg' import AuthenticatorVariantIcon from './ic-authenticator-variant.svg'
import BackIosIcon from './ic-back-ios.svg' import BackIosIcon from './ic-back-ios.svg'
import BlockIcon from './ic-block.svg'
import BlueDotIcon from './blue-dot.svg' import BlueDotIcon from './blue-dot.svg'
import BoldIcon from './ic-bold.svg' import BoldIcon from './ic-bold.svg'
import BoxFilledIcon from './ic-box-filled.svg' import BoxFilledIcon from './ic-box-filled.svg'
@@ -224,6 +225,7 @@ export {
AuthenticatorIcon, AuthenticatorIcon,
AuthenticatorVariantIcon, AuthenticatorVariantIcon,
BackIosIcon, BackIosIcon,
BlockIcon,
BlueDotIcon, BlueDotIcon,
BoldIcon, BoldIcon,
BoxFilledIcon, BoxFilledIcon,

View File

@@ -11,6 +11,7 @@ import { GetBulletedListBlock } from './Blocks/BulletedList'
import { GetChecklistBlock } from './Blocks/Checklist' import { GetChecklistBlock } from './Blocks/Checklist'
import { GetDividerBlock } from './Blocks/Divider' import { GetDividerBlock } from './Blocks/Divider'
import { GetCollapsibleBlock } from './Blocks/Collapsible' import { GetCollapsibleBlock } from './Blocks/Collapsible'
import { GetDynamicPasswordBlocks, GetPasswordBlocks } from './Blocks/Password'
import { GetParagraphBlock } from './Blocks/Paragraph' import { GetParagraphBlock } from './Blocks/Paragraph'
import { GetHeadingsBlocks } from './Blocks/Headings' import { GetHeadingsBlocks } from './Blocks/Headings'
import { GetQuoteBlock } from './Blocks/Quote' import { GetQuoteBlock } from './Blocks/Quote'
@@ -49,11 +50,15 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
GetDividerBlock(editor), GetDividerBlock(editor),
...GetDatetimeBlocks(editor), ...GetDatetimeBlocks(editor),
...GetAlignmentBlocks(editor), ...GetAlignmentBlocks(editor),
...GetPasswordBlocks(editor),
GetCollapsibleBlock(editor), GetCollapsibleBlock(editor),
...GetEmbedsBlocks(editor), ...GetEmbedsBlocks(editor),
] ]
const dynamicOptions = GetDynamicTableBlocks(editor, queryString || '') const dynamicOptions = [
...GetDynamicTableBlocks(editor, queryString || ''),
...GetDynamicPasswordBlocks(editor, queryString || ''),
]
return queryString return queryString
? [ ? [

View File

@@ -6,17 +6,17 @@ export function GetDatetimeBlocks(editor: LexicalEditor) {
return [ return [
new BlockPickerOption('Current date and time', { new BlockPickerOption('Current date and time', {
iconName: 'authenticator', iconName: 'authenticator',
keywords: ['date'], keywords: ['date', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_DATETIME_COMMAND, 'datetime'), onSelect: () => editor.dispatchCommand(INSERT_DATETIME_COMMAND, 'datetime'),
}), }),
new BlockPickerOption('Current time', { new BlockPickerOption('Current time', {
iconName: 'authenticator', iconName: 'authenticator',
keywords: ['time'], keywords: ['time', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_TIME_COMMAND, 'datetime'), onSelect: () => editor.dispatchCommand(INSERT_TIME_COMMAND, 'datetime'),
}), }),
new BlockPickerOption('Current date', { new BlockPickerOption('Current date', {
iconName: 'authenticator', iconName: 'authenticator',
keywords: ['date'], keywords: ['date', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_DATE_COMMAND, 'datetime'), onSelect: () => editor.dispatchCommand(INSERT_DATE_COMMAND, 'datetime'),
}), }),
] ]

View File

@@ -0,0 +1,42 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_PASSWORD_COMMAND } from '../../Commands'
const DEFAULT_PASSWORD_LENGTH = 16
const MIN_PASSWORD_LENGTH = 8
export function GetPasswordBlocks(editor: LexicalEditor) {
return [
new BlockPickerOption('Generate cryptographically secure password', {
iconName: 'password',
keywords: ['password', 'secure'],
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, String(DEFAULT_PASSWORD_LENGTH)),
}),
]
}
export function GetDynamicPasswordBlocks(editor: LexicalEditor, queryString: string) {
if (queryString == null) {
return []
}
const lengthRegex = /^\d+$/
const match = lengthRegex.exec(queryString)
if (!match) {
return []
}
const length = parseInt(match[0], 10)
if (length < MIN_PASSWORD_LENGTH) {
return []
}
return [
new BlockPickerOption(`Generate ${length}-character cryptographically secure password`, {
iconName: 'password',
keywords: ['password', 'secure'],
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, length.toString()),
}),
]
}

View File

@@ -1,8 +1,8 @@
import { classNames } from '@/Utils/ConcatenateClassNames' import { classNames } from '@/Utils/ConcatenateClassNames'
export const PopoverClassNames = classNames( export const PopoverClassNames = classNames(
'z-dropdown-menu w-full min-w-80', 'z-dropdown-menu w-full',
'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto md:max-w-xs h-auto overflow-y-scroll', 'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto h-auto overflow-y-scroll',
) )
export const PopoverItemClassNames = classNames( export const PopoverItemClassNames = classNames(

View File

@@ -5,3 +5,4 @@ export const INSERT_BUBBLE_COMMAND: LexicalCommand<string> = createCommand('INSE
export const INSERT_TIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_TIME_COMMAND') export const INSERT_TIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_TIME_COMMAND')
export const INSERT_DATE_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATE_COMMAND') export const INSERT_DATE_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATE_COMMAND')
export const INSERT_DATETIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATETIME_COMMAND') export const INSERT_DATETIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATETIME_COMMAND')
export const INSERT_PASSWORD_COMMAND: LexicalCommand<string> = createCommand('INSERT_PASSWORD_COMMAND')

View File

@@ -0,0 +1,26 @@
const LOWER_CASE_LETTERS = 'abcdefghijklmnopqrstuvwxyz'.split('')
const UPPER_CASE_LETTERS = LOWER_CASE_LETTERS.map((l) => l.toUpperCase())
const SPECIAL_SYMBOLS = '!£$%^&*()@~:;,./?{}=-_'.split('')
const CHARACTER_SET = [...LOWER_CASE_LETTERS, ...UPPER_CASE_LETTERS, ...SPECIAL_SYMBOLS]
const CHARACTER_SET_LENGTH = CHARACTER_SET.length
function isValidPassword(password: string) {
const containsSymbols = SPECIAL_SYMBOLS.some((symbol) => password.includes(symbol))
const containsUpperCase = UPPER_CASE_LETTERS.some((upperLetter) => password.includes(upperLetter))
const containsLowerCase = LOWER_CASE_LETTERS.some((lowerLetter) => password.includes(lowerLetter))
return containsLowerCase && containsUpperCase && containsSymbols
}
export function generatePassword(length: number): string {
const buffer = new Uint8Array(length)
let generatedPassword = ''
do {
window.crypto.getRandomValues(buffer)
generatedPassword = [...buffer].map((x) => CHARACTER_SET[x % CHARACTER_SET_LENGTH]).join('')
} while (!isValidPassword(generatedPassword))
return generatedPassword
}

View File

@@ -0,0 +1,40 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
COMMAND_PRIORITY_EDITOR,
$createTextNode,
$getSelection,
$isRangeSelection,
$createParagraphNode,
} from 'lexical'
import { useEffect } from 'react'
import { INSERT_PASSWORD_COMMAND } from '../Commands'
import { mergeRegister } from '@lexical/utils'
import { generatePassword } from './Generator'
export default function PasswordPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()
useEffect(() => {
return mergeRegister(
editor.registerCommand<string>(
INSERT_PASSWORD_COMMAND,
(lengthString) => {
const length = Number(lengthString)
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return false
}
const paragraph = $createParagraphNode()
const password = generatePassword(length)
paragraph.append($createTextNode(password))
selection.insertNodes([paragraph])
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor])
return null
}

View File

@@ -21,6 +21,7 @@ import {
ChangeContentCallbackPlugin, ChangeContentCallbackPlugin,
ChangeEditorFunction, ChangeEditorFunction,
} from './Plugins/ChangeContentCallback/ChangeContentCallback' } from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
const NotePreviewCharLimit = 160 const NotePreviewCharLimit = 160
@@ -102,7 +103,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<BlocksEditor <BlocksEditor
onChange={handleChange} onChange={handleChange}
ignoreFirstChange={true} ignoreFirstChange={true}
className="relative h-full resize-none px-5 py-4 text-base focus:shadow-none focus:outline-none" className="relative h-full resize-none px-6 py-4 text-base focus:shadow-none focus:outline-none"
previewLength={NotePreviewCharLimit} previewLength={NotePreviewCharLimit}
spellcheck={spellcheck} spellcheck={spellcheck}
> >
@@ -111,6 +112,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<ItemBubblePlugin /> <ItemBubblePlugin />
<BlockPickerMenuPlugin /> <BlockPickerMenuPlugin />
<DatetimePlugin /> <DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin /> <AutoLinkPlugin />
<ChangeContentCallbackPlugin <ChangeContentCallbackPlugin
providerCallback={(callback) => (changeEditorFunction.current = callback)} providerCallback={(callback) => (changeEditorFunction.current = callback)}

View File

@@ -93,7 +93,7 @@ const Popover = ({
anchorElement={anchorElement} anchorElement={anchorElement}
anchorPoint={anchorPoint} anchorPoint={anchorPoint}
childPopovers={childPopovers} childPopovers={childPopovers}
className={className} className={`popover-content-container ${className ?? ''}`}
id={popoverId.current} id={popoverId.current}
overrideZIndex={overrideZIndex} overrideZIndex={overrideZIndex}
side={side} side={side}