Skip to content

Commit

Permalink
feat: link dialog allows edition of link title
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed Sep 1, 2023
1 parent 637565b commit 2e0611a
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 40 deletions.
81 changes: 48 additions & 33 deletions src/plugins/link-dialog/LinkDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import React from 'react'

import { createCommand, LexicalCommand } from 'lexical'
import CheckIcon from '../../icons/check.svg'
import CloseIcon from '../../icons/close.svg'
import CopyIcon from '../../icons/content_copy.svg'
import EditIcon from '../../icons/edit.svg'
import LinkOffIcon from '../../icons/link_off.svg'
Expand All @@ -23,40 +22,43 @@ export const OPEN_LINK_DIALOG: LexicalCommand<undefined> = createCommand()

interface LinkEditFormProps {
initialUrl: string
onSubmit: (url: string) => void
initialTitle: string
onSubmit: (link: [string, string]) => void
onCancel: () => void
linkAutocompleteSuggestions: string[]
}

const MAX_SUGGESTIONS = 20

export function LinkEditForm({ initialUrl, onSubmit, onCancel, linkAutocompleteSuggestions }: LinkEditFormProps) {
export function LinkEditForm({ initialUrl, initialTitle, onSubmit, onCancel, linkAutocompleteSuggestions }: LinkEditFormProps) {
const [items, setItems] = React.useState(linkAutocompleteSuggestions.slice(0, MAX_SUGGESTIONS))
const [title, setTitle] = React.useState(initialTitle)

const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem } = useCombobox({
initialInputValue: initialUrl,
onInputValueChange({ inputValue }) {
inputValue = inputValue?.toLowerCase() || ''
const matchingItems = []
for (const url of linkAutocompleteSuggestions) {
if (url.toLowerCase().includes(inputValue)) {
matchingItems.push(url)
if (matchingItems.length >= MAX_SUGGESTIONS) {
break
const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem, inputValue } =
useCombobox({
initialInputValue: initialUrl,
onInputValueChange({ inputValue }) {
inputValue = inputValue?.toLowerCase() || ''
const matchingItems = []
for (const url of linkAutocompleteSuggestions) {
if (url.toLowerCase().includes(inputValue)) {
matchingItems.push(url)
if (matchingItems.length >= MAX_SUGGESTIONS) {
break
}
}
}
setItems(matchingItems)
},
items,
itemToString(item) {
return item ?? ''
}
setItems(matchingItems)
},
items,
itemToString(item) {
return item ?? ''
}
})
})

const onSubmitEH = (e: React.FormEvent) => {
e.preventDefault()
onSubmit(selectedItem || '')
onSubmit([inputValue, title])
}

const onKeyDownEH = React.useCallback(
Expand All @@ -65,10 +67,10 @@ export function LinkEditForm({ initialUrl, onSubmit, onCancel, linkAutocompleteS
;(e.target as HTMLInputElement).form?.reset()
} else if (e.key === 'Enter' && (!isOpen || items.length === 0)) {
e.preventDefault()
onSubmit((e.target as HTMLInputElement).value)
onSubmit([(e.target as HTMLInputElement).value, title])
}
},
[isOpen, items, onSubmit]
[isOpen, items, onSubmit, title]
)

const downshiftInputProps = getInputProps()
Expand All @@ -85,9 +87,12 @@ export function LinkEditForm({ initialUrl, onSubmit, onCancel, linkAutocompleteS

return (
<form onSubmit={onSubmitEH} onReset={onCancel} className={classNames(styles.linkDialogEditForm)}>
<div>
<label htmlFor="link-url">URL</label>
</div>
<div className={styles.linkDialogInputContainer}>
<div data-visible-dropdown={dropdownIsVisible} className={styles.linkDialogInputWrapper}>
<input className={styles.linkDialogInput} {...inputProps} autoFocus size={30} data-editor-dialog={true} />
<input id="link-url" className={styles.linkDialogInput} {...inputProps} autoFocus size={40} data-editor-dialog={true} />
<button aria-label="toggle menu" type="button" {...getToggleButtonProps()}>
<DropDownIcon />
</button>
Expand All @@ -108,14 +113,23 @@ export function LinkEditForm({ initialUrl, onSubmit, onCancel, linkAutocompleteS
</ul>
</div>
</div>
<div>
<label htmlFor="link-title">Title</label>
</div>
<div>
<div className={styles.linkDialogInputWrapper}>
<input id="link-title" className={styles.linkDialogInput} size={40} value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
</div>

<ActionButton type="submit" title="Set URL" aria-label="Set URL">
<CheckIcon />
</ActionButton>

<ActionButton type="reset" title="Cancel change" aria-label="Cancel change">
<CloseIcon />
</ActionButton>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-2)' }}>
<button type="reset" title="Cancel change" aria-label="Cancel change" className={classNames(styles.secondaryButton)}>
Cancel
</button>
<button type="submit" title="Set URL" aria-label="Set URL" className={classNames(styles.primaryButton)}>
Save
</button>
</div>
</form>
)
}
Expand Down Expand Up @@ -156,8 +170,8 @@ export const LinkDialog: React.FC = () => {
const theRect = linkDialogState?.rectangle

const onSubmitEH = React.useCallback(
(url: string) => {
updateLinkUrl(url)
(payload: [string, string]) => {
updateLinkUrl(payload)
applyLinkChanges(true)
},
[applyLinkChanges, updateLinkUrl]
Expand Down Expand Up @@ -188,6 +202,7 @@ export const LinkDialog: React.FC = () => {
{linkDialogState.type === 'edit' && (
<LinkEditForm
initialUrl={linkDialogState.url}
initialTitle={linkDialogState.title}
onSubmit={onSubmitEH}
onCancel={cancelLinkEdit.bind(null, true)}
linkAutocompleteSuggestions={linkAutocompleteSuggestions}
Expand Down
29 changes: 24 additions & 5 deletions src/plugins/link-dialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ type PreviewLinkDialog = {
type EditLinkDialog = {
type: 'edit'
initialUrl: string
initialTitle?: string
url: string
title: string
linkNodeKey: string
rectangle: RectData
}
Expand All @@ -59,7 +61,7 @@ const linkDialogSystem = system(
const linkDialogState = r.node<InactiveLinkDialog | PreviewLinkDialog | EditLinkDialog>({ type: 'inactive' }, true)

// actions
const updateLinkUrl = r.node<string>()
const updateLinkUrl = r.node<[string, string]>()
const cancelLinkEdit = r.node<true>()
const applyLinkChanges = r.node<true>()
const switchFromPreviewToLinkEdit = r.node<true>()
Expand All @@ -81,14 +83,18 @@ const linkDialogSystem = system(
r.pub(linkDialogState, {
type: 'edit',
initialUrl: node.getURL(),
initialTitle: node.getTitle() || '',
url: node.getURL(),
title: node.getTitle() || '',
linkNodeKey: node.getKey(),
rectangle
})
} else {
r.pub(linkDialogState, {
type: 'edit',
initialUrl: '',
initialTitle: '',
title: '',
url: '',
linkNodeKey: '',
rectangle
Expand Down Expand Up @@ -123,7 +129,9 @@ const linkDialogSystem = system(
(event) => {
if (event.key === 'k' && (IS_APPLE ? event.metaKey : event.ctrlKey)) {
const selection = $getSelection()
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
// we open the dialog if there's an actual selection
// or if the cursor is inside a link
if ($isRangeSelection(selection) && (getLinkNodeInSelection(selection) || !selection.isCollapsed())) {
r.pub(openLinkEditDialog, true)
event.stopPropagation()
return true
Expand Down Expand Up @@ -161,8 +169,18 @@ const linkDialogSystem = system(
r.sub(r.pipe(applyLinkChanges, r.o.withLatestFrom(linkDialogState, activeEditor)), ([, state, editor]) => {
if (state.type === 'edit') {
const url = state.url.trim()
const title = state.title.trim()

if (url.trim() !== '') {
editor?.dispatchCommand(TOGGLE_LINK_COMMAND, url)
editor?.dispatchCommand(TOGGLE_LINK_COMMAND, { url, title })
// the dispatch command implementation fails to set the link for a fresh link creation.
// Work around with the code below.
setTimeout(() => {
editor?.update(() => {
const node = getLinkNodeInSelection($getSelection() as RangeSelection)
node?.setTitle(title)
})
})
r.pub(linkDialogState, {
type: 'preview',
linkNodeKey: state.linkNodeKey,
Expand All @@ -186,11 +204,12 @@ const linkDialogSystem = system(
r.pipe(
updateLinkUrl,
r.o.withLatestFrom(linkDialogState),
r.o.map(([url, state]) => {
r.o.map(([[url, title], state]) => {
if (state.type === 'edit') {
return {
...state,
url
url,
title
}
} else {
throw new Error('Cannot update link url when not in edit mode')
Expand Down
5 changes: 3 additions & 2 deletions src/styles/ui.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@

.linkDialogEditForm {
display: flex;
align-items: center;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-2);
}

Expand Down Expand Up @@ -498,7 +499,7 @@

.linkDialogInput {
@mixin clear-form-element;
width: 13rem;
width: 20rem;
padding: var(--spacing-2) var(--spacing-3);
font-size: var(--text-sm);
&::placeholder {
Expand Down

0 comments on commit 2e0611a

Please sign in to comment.