Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .playwright/tests/links.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const sel = {
selectionStart: '[data-testid="test-links-selection-start"]',
selectionEnd: '[data-testid="test-links-selection-end"]',
applySelection: '[data-testid="test-links-apply-selection-button"]',
applySetLinkFromSelection:
'[data-testid="test-links-apply-setlink-from-selection-button"]',
selectionPayload: '[data-testid="test-links-selection-payload"]',
onLinkDetectedPayload: '[data-testid="on-link-detected-payload"]',
editorInner: '[data-testid="test-links-editor"] .eti-editor',
editorScreenshot: '[data-testid="test-links-editor"]',
Expand Down Expand Up @@ -251,6 +254,61 @@ test.describe('test-links setLink table', () => {
}
});

test.describe('test-links setLink round-trips onChangeSelection text', () => {
test('linking a selection across a block boundary keeps both paragraphs', async ({
page,
}) => {
await gotoTestLinks(page);
await setTestLinksEditorHtml(page, '<html><p>hello</p><p>world</p></html>');

await page.fill(sel.selectionStart, '3');
await page.fill(sel.selectionEnd, '8');
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
await page.click(sel.applySelection);

await expect
.poll(async () => page.locator(sel.selectionPayload).textContent())
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));

await page.click(sel.applySetLinkFromSelection);

await expect
.poll(async () => getTestLinksSerializedHtml(page))
.toContain(
'<p>hel<a href="https://swmansion.com">lo</a></p>' +
'<p><a href="https://swmansion.com">wo</a>rld</p>'
);
});

test('linking a selection across a block boundary preserves inline marks', async ({
page,
}) => {
await gotoTestLinks(page);
await setTestLinksEditorHtml(
page,
'<html><p>hel<b>lo</b></p><p>world</p></html>'
);

await page.fill(sel.selectionStart, '3');
await page.fill(sel.selectionEnd, '8');
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
await page.click(sel.applySelection);

await expect
.poll(async () => page.locator(sel.selectionPayload).textContent())
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));

await page.click(sel.applySetLinkFromSelection);

await expect
.poll(async () => getTestLinksSerializedHtml(page))
.toContain(
'<p>hel<a href="https://swmansion.com"><b>lo</b></a></p>' +
'<p><a href="https://swmansion.com">wo</a>rld</p>'
);
});
});

test.describe('test-links removeLink table', () => {
const cases: {
name: string;
Expand Down
15 changes: 15 additions & 0 deletions apps/example-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,21 @@ function App() {
Push Text
</button>

<button
className="btn btn-full"
onClick={() => {
if (!selection) return;
ref.current?.setLink(
selection.start,
selection.end,
'',
'swmansion.com'
);
}}
>
setLink(text="")
</button>
Comment thread
hejsztynx marked this conversation as resolved.

Comment on lines +336 to +350

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for manual test purposes only. Will be removed on merge

{showHtmlOutput && <HtmlOutputPanel html={currentHtml} />}

<TextRenderer htmlValue={enrichedTextValue} />
Expand Down
25 changes: 25 additions & 0 deletions apps/example-web/src/testScreens/TestLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EnrichedTextInput,
type EnrichedInputStyle,
type EnrichedTextInputInstance,
type OnChangeSelectionEvent,
type OnLinkDetected,
} from 'react-native-enriched-html';
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
Expand Down Expand Up @@ -34,6 +35,8 @@ export function TestLinks() {
const [selEndInput, setSelEndInput] = useState('0');
const [lastOnLinkDetected, setLastOnLinkDetected] =
useState<OnLinkDetected | null>(null);
const [lastSelection, setLastSelection] =
useState<OnChangeSelectionEvent | null>(null);

useEffect(() => {
setLinkRegexError('');
Expand Down Expand Up @@ -68,6 +71,9 @@ export function TestLinks() {
onLinkDetected={(e) => {
setLastOnLinkDetected(e);
}}
onChangeSelection={(e) => {
setLastSelection(e.nativeEvent);
}}
linkRegex={appliedLinkRegex}
/>
</div>
Expand Down Expand Up @@ -223,8 +229,27 @@ export function TestLinks() {
>
setSelection
</button>
<button
type="button"
data-testid="test-links-apply-setlink-from-selection-button"
onClick={() => {
if (!lastSelection) return;
ref.current?.setLink(
lastSelection.start,
lastSelection.end,
lastSelection.text,
linkUrlInput
);
}}
>
setLink from selection
</button>
</div>

<pre data-testid="test-links-selection-payload">
{JSON.stringify(lastSelection)}
</pre>
Comment thread
hejsztynx marked this conversation as resolved.

<pre data-testid="on-link-detected-payload">
{JSON.stringify(lastOnLinkDetected)}
</pre>
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/api-reference/enriched-text-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,12 @@ details.
- `text: string` - displayed text of the link
- `url: string` - URL of the link

:::note

Using this method with `text=""` will result in removal of the text marked by the `start` and `end` indexes.

;;;

### `.removeLink()`

```ts
Expand Down
18 changes: 13 additions & 5 deletions src/web/formats/EnrichedLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link, { type LinkOptions } from '@tiptap/extension-link';
import { mergeAttributes, type CommandProps } from '@tiptap/core';
import type { Editor } from '@tiptap/react';

import { nativePosToTiptapPos } from '../positionMapping';
import { nativeLeafText, nativePosToTiptapPos } from '../positionMapping';
import { isLinkBlocked } from './formatRules';
import { findAutolinkRangesInWord } from '../pmPlugins/AutolinkPlugin/autolinkRegex';

Expand Down Expand Up @@ -138,9 +138,6 @@ export function setLink(
text: string,
url: string
) {
if (url.length === 0 || text.length === 0) {
return;
}
const { state } = editor;
const doc = state.doc;
const from = nativePosToTiptapPos(doc, start);
Expand All @@ -150,6 +147,17 @@ export function setLink(
return;
}

if (text.length === 0) {
if (from !== to) {
editor.chain().focus().deleteRange({ from, to }).run();
}
return;
}

if (url.length === 0) {
return;
}

const linkType = state.schema.marks.link;
if (!linkType) return;
const linkMark = linkType.create({ href: url });
Expand All @@ -162,7 +170,7 @@ export function setLink(
const marksWithLink = linkMark.addToSet(marksAtRangeStart);
tr.insert(from, s.schema.text(text, marksWithLink));
} else {
const currentText = doc.textBetween(from, to);
const currentText = nativeLeafText(doc, from, to);

if (text !== currentText) {
const marksAtRangeStart = doc.resolve(from).marks();
Expand Down
4 changes: 2 additions & 2 deletions src/web/useOnLinkDetected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getMarkRange, getMarksBetween } from '@tiptap/core';
import type { EditorState } from '@tiptap/pm/state';
import type { MarkType } from '@tiptap/pm/model';
import { emitLinkDetected, type LinkEmitterRef } from './emitLinkDetected';
import { tiptapPosToNativePos } from './positionMapping';
import { nativeLeafText, tiptapPosToNativePos } from './positionMapping';

function findLinkRangeAt(
state: EditorState,
Expand Down Expand Up @@ -53,7 +53,7 @@ export const useOnLinkDetected = (
if (!linkMark) return;

emitLinkDetected(ref.current, {
text: state.doc.textBetween(range.from, range.to, '\n'),
text: nativeLeafText(state.doc, range.from, range.to),
url: (linkMark.attrs.href as string | undefined) ?? '',
start: tiptapPosToNativePos(state.doc, range.from),
end: tiptapPosToNativePos(state.doc, range.to),
Expand Down
Loading