From 5e52b8fa5895197a481dacadd14ed10deeea9b60 Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 8 Aug 2024 17:16:40 +0200 Subject: [PATCH 1/5] Introduce configuration interface for inline links --- packages/slate-editor/src/extensions/inline-links/index.ts | 1 + packages/slate-editor/src/extensions/inline-links/types.ts | 6 ++++++ .../slate-editor/src/modules/editor/getEnabledExtensions.ts | 3 ++- packages/slate-editor/src/modules/editor/types.ts | 3 ++- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/slate-editor/src/extensions/inline-links/types.ts diff --git a/packages/slate-editor/src/extensions/inline-links/index.ts b/packages/slate-editor/src/extensions/inline-links/index.ts index 97b8e7d1f..245d4dfe4 100644 --- a/packages/slate-editor/src/extensions/inline-links/index.ts +++ b/packages/slate-editor/src/extensions/inline-links/index.ts @@ -1,3 +1,4 @@ export { InlineLinksExtension, EXTENSION_ID } from './InlineLinksExtension'; export { createLink, unwrapLink, wrapInLink } from './lib'; +export type { InlineLinksExtensionConfiguration } from './types'; diff --git a/packages/slate-editor/src/extensions/inline-links/types.ts b/packages/slate-editor/src/extensions/inline-links/types.ts new file mode 100644 index 000000000..cf584267b --- /dev/null +++ b/packages/slate-editor/src/extensions/inline-links/types.ts @@ -0,0 +1,6 @@ +export interface InlineLinksExtensionConfiguration { + predefinedLinks?: { + label: string; + options: { label: string; value: string }[]; + }; +} diff --git a/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts b/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts index 4b99f72c2..d808c986b 100644 --- a/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts +++ b/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts @@ -186,7 +186,8 @@ export function* getEnabledExtensions(parameters: Parameters): Generator Date: Thu, 8 Aug 2024 17:19:11 +0200 Subject: [PATCH 2/5] Allow rendering href as plain text --- .../LinkWithTooltip/LinkWithTooltip.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/slate-editor/src/modules/components/LinkWithTooltip/LinkWithTooltip.tsx b/packages/slate-editor/src/modules/components/LinkWithTooltip/LinkWithTooltip.tsx index 63225bda4..bd1711922 100644 --- a/packages/slate-editor/src/modules/components/LinkWithTooltip/LinkWithTooltip.tsx +++ b/packages/slate-editor/src/modules/components/LinkWithTooltip/LinkWithTooltip.tsx @@ -15,9 +15,10 @@ interface Props { children: TooltipV2.TooltipProps['children']; enabled?: boolean; href: string; + textOnly?: boolean; } -export function LinkWithTooltip({ children, enabled = true, href }: Props) { +export function LinkWithTooltip({ children, enabled = true, href, textOnly = false }: Props) { return ( - {href} - + textOnly ? ( + href + ) : ( + + {href} + + ) } > {children} From d1e90cd1a235e7b983052f2deb2df3e026a8681e Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 8 Aug 2024 17:21:27 +0200 Subject: [PATCH 3/5] Add email regexp and helper function for humanizing mailto links --- .../slate-editor/src/lib/humanFriendlyEmailUrl.ts | 5 +++++ packages/slate-editor/src/lib/index.ts | 11 ++++++++++- packages/slate-editor/src/lib/urls.ts | 12 ++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 packages/slate-editor/src/lib/humanFriendlyEmailUrl.ts diff --git a/packages/slate-editor/src/lib/humanFriendlyEmailUrl.ts b/packages/slate-editor/src/lib/humanFriendlyEmailUrl.ts new file mode 100644 index 000000000..c323c0216 --- /dev/null +++ b/packages/slate-editor/src/lib/humanFriendlyEmailUrl.ts @@ -0,0 +1,5 @@ +const MAILTO_PREFIX = /^(mailto):/; + +export function humanFriendlyEmailUrl(url: string): string { + return url.replace(MAILTO_PREFIX, ''); +} diff --git a/packages/slate-editor/src/lib/index.ts b/packages/slate-editor/src/lib/index.ts index 6884f448f..29a2a92ea 100644 --- a/packages/slate-editor/src/lib/index.ts +++ b/packages/slate-editor/src/lib/index.ts @@ -15,6 +15,7 @@ export { ensureElementInView } from './ensureElementInView'; export { ensureRangeInView } from './ensureRangeInView'; export { formatBytes } from './formatBytes'; export { getScrollParent } from './getScrollParent'; +export { humanFriendlyEmailUrl } from './humanFriendlyEmailUrl'; export { humanFriendlyUrl } from './humanFriendlyUrl'; export { isCorsEnabledOrigin } from './isCorsEnabledOrigin'; export { isGoogleDocsWrapper } from './isGoogleDocsWrapper'; @@ -28,5 +29,13 @@ export { scrollTo } from './scrollTo'; export * from './isDeletingEvent'; export * from './stripTags'; export * as utils from './utils'; -export { HREF_REGEXP, URL_WITH_OPTIONAL_PROTOCOL_REGEXP, normalizeHref, matchUrls } from './urls'; +export { + EMAIL_REGEXP, + HREF_REGEXP, + MAILTO_REGEXP, + URL_WITH_OPTIONAL_PROTOCOL_REGEXP, + normalizeHref, + normalizeMailtoHref, + matchUrls, +} from './urls'; export { withResetFormattingOnBreak } from './withResetFormattingOnBreak'; diff --git a/packages/slate-editor/src/lib/urls.ts b/packages/slate-editor/src/lib/urls.ts index 044b4214b..ee35a3f95 100644 --- a/packages/slate-editor/src/lib/urls.ts +++ b/packages/slate-editor/src/lib/urls.ts @@ -1,7 +1,8 @@ export const URL_PLACEHOLDER_REGEXP = new RegExp('%release\\.url%|%release\\.shorturl%'); -export const MAILTO_REGEXP = new RegExp( - 'mailto:[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*', +export const EMAIL_REGEXP = new RegExp( + '[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*', ); +export const MAILTO_REGEXP = new RegExp(`mailto:${EMAIL_REGEXP.source}`); /** * @see https://regex101.com/r/W0NkQE/2 * For problems, blame Ivan & Lukas :) @@ -40,6 +41,13 @@ export function normalizeHref(href: string): string { return href; } +export function normalizeMailtoHref(href: string): string { + if (full(EMAIL_REGEXP).test(href)) { + return `mailto:${href}`; + } + return href; +} + /** * Convert an unbound RegExp object to the same pattern, bound to the string start and end boundaries. * From f9db936ab8cd0df94404351f9eda18fd5d1d09ff Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 8 Aug 2024 17:24:46 +0200 Subject: [PATCH 4/5] Improve link menu by allowing predefined variables, also handle emails better --- .../inline-links/InlineLinksExtension.tsx | 11 +- .../inline-links/components/LinkElement.tsx | 11 +- .../rich-formatting-menu/LinkMenu.module.scss | 3 + .../modules/rich-formatting-menu/LinkMenu.tsx | 141 +++++++++++++++--- .../RichFormattingMenu.tsx | 10 +- .../src/modules/rich-formatting-menu/types.ts | 2 + 6 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 packages/slate-editor/src/modules/rich-formatting-menu/LinkMenu.module.scss diff --git a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx index 4c5285c42..b1c8788fb 100644 --- a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx +++ b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx @@ -17,10 +17,13 @@ import { normalizeRedundantLinkAttributes, parseSerializedLinkElement, } from './lib'; +import type { InlineLinksExtensionConfiguration } from './types'; export const EXTENSION_ID = 'InlineLinksExtension'; -export const InlineLinksExtension = (): Extension => ({ +export const InlineLinksExtension = ({ + predefinedLinks, +}: InlineLinksExtensionConfiguration): Extension => ({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -46,7 +49,11 @@ export const InlineLinksExtension = (): Extension => ({ renderElement: ({ attributes, children, element }: RenderElementProps) => { if (isLinkNode(element)) { return ( - + {children} ); diff --git a/packages/slate-editor/src/extensions/inline-links/components/LinkElement.tsx b/packages/slate-editor/src/extensions/inline-links/components/LinkElement.tsx index f33562c4c..451b9991f 100644 --- a/packages/slate-editor/src/extensions/inline-links/components/LinkElement.tsx +++ b/packages/slate-editor/src/extensions/inline-links/components/LinkElement.tsx @@ -6,14 +6,18 @@ import { ReactEditor, useSlateStatic } from 'slate-react'; import { LinkWithTooltip } from '#modules/components'; +import type { InlineLinksExtensionConfiguration } from '../types'; + import styles from './LinkElement.module.scss'; interface Props extends RenderElementProps { element: LinkNode; + predefinedLinks: InlineLinksExtensionConfiguration['predefinedLinks']; } -export function LinkElement({ attributes, children, element }: Props) { +export function LinkElement({ attributes, children, element, predefinedLinks }: Props) { const editor = useSlateStatic(); + const predefinedLink = predefinedLinks?.options.find(({ value }) => value === element.href); function onMouseUp() { if (editor.selection && Range.isCollapsed(editor.selection)) { @@ -28,7 +32,10 @@ export function LinkElement({ attributes, children, element }: Props) { // a failed `ReactEditor.toSlateNode` in Slate's Editable onClick handler. // For more details, see https://github.com/prezly/prezly/pull/8016#discussion_r454190469 - + {({ ariaAttributes, onHide, onShow, setReferenceElement }) => ( [] = [ { @@ -35,6 +45,7 @@ interface Props { onClose: () => void; onConvert?: (presentation: Presentation) => void; onUnlink: () => void; + predefinedLinks: InlineLinksExtensionConfiguration['predefinedLinks']; } export function LinkMenu({ @@ -47,19 +58,59 @@ export function LinkMenu({ onClose, onConvert, onUnlink, + predefinedLinks, }: Props) { const rootRef = React.useRef(null); const [href, setHref] = useState(node?.href ?? ''); - const [new_tab, setNewTab] = useState(node?.new_tab ?? true); + const [newTab, setNewTab] = useState(node?.new_tab ?? true); + const [type, setType] = useState(detectLinkType(href, predefinedLinks)); + + const isConvertable = type === 'url'; function handleSave() { - onChange({ href: normalizeHref(href), new_tab }); + let normalizedHref = href; + if (type === 'url') { + normalizedHref = normalizeHref(href); + } + + if (type === 'email') { + normalizedHref = normalizeMailtoHref(href); + } + + onChange({ + href: normalizedHref, + new_tab: newTab, + }); + } + + function handleChangeType(nextType: LinkType) { + if (nextType === 'predefined' && predefinedLinks) { + setType(nextType); + setHref(predefinedLinks.options[0].value); + return; + } + + setType(nextType); + setHref(''); + } + + function getTypeOptions(): OptionsGroupOption[] { + const options: OptionsGroupOption[] = [ + { label: 'Web', value: 'url' }, + { label: 'Email', value: 'email' }, + ]; + + if (predefinedLinks) { + return [...options, { label: predefinedLinks.label, value: 'predefined' }]; + } + + return options; } useRootClose(rootRef, onBlur); return ( - + Link settings @@ -73,21 +124,51 @@ export function LinkMenu({ - Link - Link to + + {type === 'url' && ( + + )} + {type === 'email' && ( + + )} + {type === 'predefined' && predefinedLinks && ( + + )} {withNewTabOption && ( - + Open in new tab )} @@ -95,7 +176,7 @@ export function LinkMenu({ - {withConversionOptions && onConvert && ( + {withConversionOptions && onConvert && isConvertable && ( - @@ -127,3 +215,18 @@ export function LinkMenu({ ); } + +function detectLinkType( + href: string, + predefinedLinks: InlineLinksExtensionConfiguration['predefinedLinks'], +): LinkType { + if (predefinedLinks && predefinedLinks.options.some(({ value }) => href === value)) { + return 'predefined'; + } + + if (MAILTO_REGEXP.test(href)) { + return 'email'; + } + + return 'url'; +} diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx b/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx index 02e98eaf5..793384b08 100644 --- a/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx +++ b/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx @@ -12,6 +12,7 @@ import { Menu, TextSelectionPortalV2 } from '#components'; import { decorateSelectionFactory } from '#extensions/decorate-selection'; import { unwrapLink, wrapInLink } from '#extensions/inline-links'; +import type { InlineLinksExtensionConfiguration } from '#extensions/inline-links'; import { MarkType } from '#extensions/text-styling'; import { useDecorationFactory } from '#modules/decorations'; import { EventsEditor } from '#modules/events'; @@ -38,7 +39,7 @@ interface Props { withCallouts: boolean; withConversionOptions?: false | { fetchOembed: FetchOEmbedFn }; withHeadings: boolean; - withInlineLinks: boolean; + withInlineLinks: boolean | InlineLinksExtensionConfiguration; withLists: boolean; withNewTabOption: boolean; withTextHighlight: boolean; @@ -219,6 +220,11 @@ export function RichFormattingMenu({ onConvert={handleConvert} onClose={onClose} onUnlink={unlinkSelection} + predefinedLinks={ + typeof withInlineLinks === 'object' + ? withInlineLinks.predefinedLinks + : undefined + } /> ); @@ -273,7 +279,7 @@ export function RichFormattingMenu({ withCallouts={withCallouts} withHeadings={withHeadings && !isInsideTable} withInlineLinks={ - isTitleSelected || isSubtitleSelected ? false : withInlineLinks + isTitleSelected || isSubtitleSelected ? false : Boolean(withInlineLinks) } withLists={withLists} withParagraphs={withParagraphs} diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/types.ts b/packages/slate-editor/src/modules/rich-formatting-menu/types.ts index 5402352b9..e300146f5 100644 --- a/packages/slate-editor/src/modules/rich-formatting-menu/types.ts +++ b/packages/slate-editor/src/modules/rich-formatting-menu/types.ts @@ -22,6 +22,8 @@ export type FetchOEmbedFn = (url: string) => Promise; export type Presentation = 'card' | 'embed' | 'link'; +export type LinkType = 'url' | 'email' | 'predefined'; + export type RichFormattedTextElement = | ParagraphNode | HeadingNode From 2e9b5d0405258956b7ce8249433a23f92eb9724f Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 8 Aug 2024 17:35:07 +0200 Subject: [PATCH 5/5] Make inline links config optional --- .../src/extensions/inline-links/InlineLinksExtension.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx index b1c8788fb..252fc70aa 100644 --- a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx +++ b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx @@ -23,7 +23,7 @@ export const EXTENSION_ID = 'InlineLinksExtension'; export const InlineLinksExtension = ({ predefinedLinks, -}: InlineLinksExtensionConfiguration): Extension => ({ +}: InlineLinksExtensionConfiguration = {}): Extension => ({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({