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:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
7
packages/icons/src/Icons/ic-block.svg
Normal file
7
packages/icons/src/Icons/ic-block.svg
Normal 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 |
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -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'),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'
|
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(
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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,
|
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)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user