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

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

View File

@@ -6,17 +6,17 @@ export function GetDatetimeBlocks(editor: LexicalEditor) {
return [
new BlockPickerOption('Current date and time', {
iconName: 'authenticator',
keywords: ['date'],
keywords: ['date', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_DATETIME_COMMAND, 'datetime'),
}),
new BlockPickerOption('Current time', {
iconName: 'authenticator',
keywords: ['time'],
keywords: ['time', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_TIME_COMMAND, 'datetime'),
}),
new BlockPickerOption('Current date', {
iconName: 'authenticator',
keywords: ['date'],
keywords: ['date', 'current'],
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'
export const PopoverClassNames = classNames(
'z-dropdown-menu w-full min-w-80',
'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto md:max-w-xs h-auto overflow-y-scroll',
'z-dropdown-menu w-full',
'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto h-auto overflow-y-scroll',
)
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_DATE_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATE_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,
ChangeEditorFunction,
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
const NotePreviewCharLimit = 160
@@ -102,7 +103,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<BlocksEditor
onChange={handleChange}
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}
spellcheck={spellcheck}
>
@@ -111,6 +112,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin />
<ChangeContentCallbackPlugin
providerCallback={(callback) => (changeEditorFunction.current = callback)}

View File

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