From 4b24b58f2152a5e55a328003708f607b2ea3ea4d Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sun, 6 Aug 2023 02:43:35 +0530 Subject: [PATCH] feat: Improved link editing and creation in Super notes (#2382) [skip e2e] --- .../Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx | 7 +- .../FloatingLinkEditorPlugin/LinkEditor.tsx | 6 +- .../LinkTextEditor.tsx | 123 ++++++++++++++++++ .../FloatingTextFormatToolbarPlugin/index.tsx | 102 +++++++++++++-- .../MobileToolbarPlugin.tsx | 9 +- .../src/javascripts/Constants/Constants.ts | 3 + 6 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/LinkTextEditor.tsx diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx index 23bf6583a..9fe41c9ee 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx @@ -6,14 +6,9 @@ * */ +import { URL_REGEX, EMAIL_REGEX } from '@/Constants/Constants' import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin' -const URL_REGEX = - /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/ - -const EMAIL_REGEX = - /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/ - const MATCHERS = [ createLinkMatcherWithRegExp(URL_REGEX, (text) => { return text.startsWith('http') ? text : `https://${text}` diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/LinkEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/LinkEditor.tsx index 7d037e1e5..1d4de9efd 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/LinkEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingLinkEditorPlugin/LinkEditor.tsx @@ -4,7 +4,7 @@ import { KeyboardKey } from '@standardnotes/ui-services' import { IconComponent } from '../../Lexical/Theme/IconComponent' import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl' import { TOGGLE_LINK_COMMAND } from '@lexical/link' -import { useCallback, useState, useRef } from 'react' +import { useCallback, useState, useRef, useEffect } from 'react' import { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical' import { classNames } from '@standardnotes/snjs' @@ -36,6 +36,10 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i } }, []) + useEffect(() => { + setEditedLinkUrl(linkUrl) + }, [linkUrl]) + return isEditMode ? (
, selection: RangeSelection) => { + const parent = node.getParent() + return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode() +} + +const LinkTextEditor = ({ linkText, editor, lastSelection }: Props) => { + const [editedLinkText, setEditedLinkText] = useState(() => linkText) + const [isEditMode, setEditMode] = useState(false) + const editModeContainer = useRef(null) + + useEffect(() => { + setEditedLinkText(linkText) + }, [linkText]) + + const focusInput = useCallback((input: HTMLInputElement | null) => { + if (input) { + input.focus() + } + }, []) + + const handleLinkTextSubmission = () => { + editor.update(() => { + if ($isRangeSelection(lastSelection)) { + const node = getSelectedNode(lastSelection) + if (!$isLinkTextNode(node, lastSelection)) { + return + } + node.setTextContent(editedLinkText) + } + }) + setEditMode(false) + } + + return isEditMode ? ( +
+ { + setEditedLinkText(event.target.value) + }} + onKeyDown={(event) => { + if (event.key === KeyboardKey.Enter) { + event.preventDefault() + handleLinkTextSubmission() + } else if (event.key === KeyboardKey.Escape) { + event.preventDefault() + setEditMode(false) + } + }} + onBlur={(event) => { + if (!editModeContainer.current?.contains(event.relatedTarget as Node)) { + setEditMode(false) + } + }} + className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]" + /> + + +
+ ) : ( +
+ +
+ Link text: + {linkText} +
+ +
+ ) +} + +export default LinkTextEditor diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingTextFormatToolbarPlugin/index.tsx index db546be5f..2439b64c5 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -23,6 +23,9 @@ import { RangeSelection, GridSelection, NodeSelection, + KEY_MODIFIER_COMMAND, + COMMAND_PRIORITY_NORMAL, + createCommand, } from 'lexical' import { $isHeadingNode } from '@lexical/rich-text' import { @@ -49,13 +52,14 @@ import { ListNumbered, } from '@standardnotes/icons' import { IconComponent } from '../../Lexical/Theme/IconComponent' -import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl' import { classNames } from '@standardnotes/snjs' import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect' import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles' import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal' import LinkEditor from '../FloatingLinkEditorPlugin/LinkEditor' import { movePopoverToFitInsideRect } from '@/Components/Popover/Utils/movePopoverToFitInsideRect' +import LinkTextEditor, { $isLinkTextNode } from '../FloatingLinkEditorPlugin/LinkTextEditor' +import { URL_REGEX } from '@/Constants/Constants' const blockTypeToBlockName = { bullet: 'Bulleted List', @@ -74,6 +78,8 @@ const blockTypeToBlockName = { const IconSize = 15 +const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand('TOGGLE_LINK_AND_EDIT_COMMAND') + const ToolbarButton = ({ active, ...props }: { active?: boolean } & ComponentPropsWithoutRef<'button'>) => { return (