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:
@@ -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
|
||||
? [
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -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()),
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user