Skip to content

Commit

Permalink
fix: regression for anchor links
Browse files Browse the repository at this point in the history
  • Loading branch information
splincode committed Nov 27, 2024
1 parent 6e433db commit d5d1d33
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 76 deletions.
45 changes: 45 additions & 0 deletions projects/demo-playwright/tests/anchors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,49 @@ test.describe('Anchors', () => {
await expect(page).toHaveScreenshot(`Anchors-02-${anchor}.png`);
}
});

test('make anchor', async ({page}) => {
const editor = page.locator('[contenteditable]').nth(0);
const fullExample = page.locator('tui-doc-example#anchors');

await editor.focus();
await editor.fill('');

await expect(fullExample).toHaveScreenshot('Anchors-03.png');

await editor.focus();
await editor.fill('Hello\n\n\nLink to anchor\n');
await editor.getByText('Hello').selectText();
await page.getByTestId('tui-doc-example').getByRole('button').nth(3).click();
await page.waitForTimeout(300);

await page.keyboard.press('H');
await page.keyboard.press('e');
await page.keyboard.press('l');
await page.keyboard.press('l');
await page.keyboard.press('o');

await expect(fullExample).toHaveScreenshot('Anchors-04.png');

await page.keyboard.press('Enter');
await page.waitForTimeout(300);

await expect(fullExample).toHaveScreenshot('Anchors-05.png');

await editor.getByText('Link to anchor').selectText();
await page.getByTestId('toolbar__link-button').click();
await page.waitForTimeout(300);

await expect(fullExample).toHaveScreenshot('Anchors-06.png');

await page.getByRole('button', {name: '#Hello'}).click();
await page.waitForTimeout(300);

await expect(fullExample).toHaveScreenshot('Anchors-07.png');

await page.mouse.click(0, 0);
await page.waitForTimeout(300);

await expect(fullExample).toHaveScreenshot('Anchors-08.png');
});
});
1 change: 1 addition & 0 deletions projects/editor/common/hack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TUI_TIPTAP_WHITESPACE_HACK = '<span style="font-size: 15px"> </span>'; // require: `@tiptap/extension-text-style`
1 change: 1 addition & 0 deletions projects/editor/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './editor-tool';
export * from './editor-value-transformer';
export * from './files-loader';
export * from './gradient-direction';
export * from './hack';
export * from './i18n';
export * from './iframe';
export * from './image';
Expand Down
23 changes: 18 additions & 5 deletions projects/editor/components/edit-link/edit-link.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ export class TuiEditLink {
protected setAnchor(anchor: string): void {
this.url = anchor;
this.changePrefix(true);
this.addLink.emit(this.href);
}

protected changePrefix(isPrefix: boolean): void {
this.prefix = isPrefix ? TUI_EDITOR_LINK_HASH_PREFIX : this.defaultProtocol;
protected changePrefix(useHash: boolean): void {
this.prefix = useHash ? TUI_EDITOR_LINK_HASH_PREFIX : this.defaultProtocol;
}

protected onSave(): void {
Expand Down Expand Up @@ -161,9 +162,14 @@ export class TuiEditLink {
protected onBlur(url: string): void {
const range = this.editor?.getSelectionSnapshot();

if (range && !url) {
if (range && !url && !this.url) {
this.editor?.setTextSelection({from: range.anchor, to: range.head});
this.editor?.toggleLink('');

if (this.anchorMode) {
this.editor?.removeAnchor();
} else {
this.editor?.toggleLink('');
}
}
}

Expand All @@ -182,6 +188,10 @@ export class TuiEditLink {
.prefix as TuiEditorLinkPrefix) || this.defaultProtocol;

if (a) {
if (this.isOnlyAnchorMode) {
return TUI_EDITOR_LINK_HASH_PREFIX;
}

return (!a.getAttribute('href') && a.getAttribute('id')) ||
a.getAttribute('href')?.startsWith(TUI_EDITOR_LINK_HASH_PREFIX)
? TUI_EDITOR_LINK_HASH_PREFIX
Expand All @@ -194,7 +204,10 @@ export class TuiEditLink {
private detectAnchorMode(): boolean {
const a = this.getAnchorElement();

return !a?.href && !!a?.getAttribute('id');
return (
!a?.href &&
(!!a?.getAttribute('id') || a?.getAttribute('data-type') === 'jump-anchor')
);
}

private getFocusedParentElement(): HTMLElement | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
*ngFor="let id of anchorIds | tuiFilterAnchors: prefix : url"
type="button"
class="t-anchor"
(click)="setAnchor(id)"
(mousedown)="setAnchor(id)"
>
#{{ id }}
</button>
Expand Down
13 changes: 12 additions & 1 deletion projects/editor/components/toolbar/toolbar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
TuiTextColor,
} from '@taiga-ui/editor/components/toolbar-tools';
import {TuiTiptapEditorService} from '@taiga-ui/editor/directives';
import {tuiGetCurrentWordBounds} from '@taiga-ui/editor/utils';
import {take} from 'rxjs';

import {TuiToolbarNavigationManager} from './toolbar-navigation-manager.directive';
Expand Down Expand Up @@ -275,7 +276,17 @@ export class TuiToolbar {

protected onLink(url?: string): void {
this.editor?.takeSelectionSnapshot();
this.editor?.toggleLink(url ?? '');

if (url === '#') {
const range = this.editor?.getSelectionSnapshot();
const editor = this.editor?.getOriginTiptapEditor();
const {from = range?.anchor} = editor ? tuiGetCurrentWordBounds(editor) : {};

this.editor?.setAnchor('');
this.editor?.getOriginTiptapEditor()?.commands.focus((from ?? 0) + 1);
} else {
this.editor?.toggleLink(url ?? '');
}
}

protected enabled(tool: TuiEditorToolType): boolean {
Expand Down
23 changes: 19 additions & 4 deletions projects/editor/extensions/jump-anchor/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {TUI_TIPTAP_WHITESPACE_HACK} from '@taiga-ui/editor/common';
import {tuiGetCurrentWordBounds, tuiGetSlicedFragment} from '@taiga-ui/editor/utils';
import {Mark, mergeAttributes} from '@tiptap/core';

declare module '@tiptap/core' {
Expand Down Expand Up @@ -42,11 +44,24 @@ export const TuiJumpAnchor = Mark.create({
return {
setAnchor:
(id) =>
({chain}) =>
chain()
({chain, state, editor}) => {
const {from, to} = tuiGetCurrentWordBounds(editor);
const sliced = tuiGetSlicedFragment(state, to);
const forwardSymbolIsWhitespace = sliced === ' ';
const jumpAnchorMark = chain()
.setTextSelection({from, to})
.extendMarkRange('jumpAnchor')
.setMark('jumpAnchor', {id})
.run(),
.setMark('jumpAnchor', {id});

return (
forwardSymbolIsWhitespace
? jumpAnchorMark.setTextSelection(to - 1)
: jumpAnchorMark
.setTextSelection(to)
.insertContent(TUI_TIPTAP_WHITESPACE_HACK)
.setTextSelection(to - 1)
).run();
},

removeAnchor:
() =>
Expand Down
76 changes: 11 additions & 65 deletions projects/editor/extensions/link/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,14 @@
import {tuiParseNodeAttributes} from '@taiga-ui/editor/utils';
import type {Editor, KeyboardShortcutCommand, Range} from '@tiptap/core';
import {getHTMLFromFragment, markPasteRule} from '@tiptap/core';
import {TUI_TIPTAP_WHITESPACE_HACK} from '@taiga-ui/editor/common';
import {
tuiGetCurrentWordBounds,
tuiGetSlicedFragment,
tuiParseNodeAttributes,
} from '@taiga-ui/editor/utils';
import type {KeyboardShortcutCommand} from '@tiptap/core';
import {markPasteRule} from '@tiptap/core';
import {Link} from '@tiptap/extension-link';
import {find} from 'linkifyjs';

function getCurrentWordBounds(editor: Editor): Range {
const {state} = editor;
const {selection} = state;
const {$anchor, empty} = selection;

if (!empty) {
return {
from: selection.from,
to: selection.to,
};
}

if ($anchor) {
const {pos} = $anchor;
const start = $anchor.start();
const parent = $anchor.parent;
const textBefore = parent.textBetween(0, pos - start, undefined, '\uFFFC');
const textAfter = parent.textBetween(
pos - start,
parent.content.size,
undefined,
'\uFFFC',
);

const wordBefore = textBefore
// eslint-disable-next-line unicorn/prefer-string-replace-all
.replaceAll(/\uFFFC/g, '')
.split(/\b/)
.pop();
const wordAfter = textAfter
// eslint-disable-next-line unicorn/prefer-string-replace-all
.replaceAll(/\uFFFC/g, '')
.split(/\b/)
.shift();

const from = pos - (wordBefore?.length ?? 0);
const to = pos + (wordAfter?.length ?? 0);

return {
from,
to,
};
}

return {
from: selection.to,
to: selection.to + 1,
};
}

export const TuiLink = Link.extend({
addAttributes() {
return {
Expand All @@ -70,14 +25,8 @@ export const TuiLink = Link.extend({
({chain, state, editor}) => {
// eslint-disable-next-line no-lone-blocks
{
const {doc, schema} = state;
const {from, to} = getCurrentWordBounds(editor);
const selected = doc.cut(to, to + 1);
const sliced = getHTMLFromFragment(
selected.content,
schema,
).replaceAll(/<\/?[^>]+(>|$)/g, '');

const {from, to} = tuiGetCurrentWordBounds(editor);
const sliced = tuiGetSlicedFragment(state, to);
const forwardSymbolIsWhitespace = sliced === ' ';

const toggleMark = chain()
Expand All @@ -91,10 +40,7 @@ export const TuiLink = Link.extend({
? toggleMark.setTextSelection(to - 1)
: toggleMark
.setTextSelection(to)
.insertContent(
// require: `@tiptap/extension-text-style`
'<span style="font-size: 15px"> </span>',
)
.insertContent(TUI_TIPTAP_WHITESPACE_HACK)
.setTextSelection(to - 1)
).run();
}
Expand Down
51 changes: 51 additions & 0 deletions projects/editor/utils/get-current-word-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {Editor, Range} from '@tiptap/core';

export function tuiGetCurrentWordBounds(editor: Editor): Range {
const {state} = editor;
const {selection} = state;
const {$anchor, empty} = selection;

if (!empty) {
return {
from: selection.from,
to: selection.to,
};
}

if ($anchor) {
const {pos} = $anchor;
const start = $anchor.start();
const parent = $anchor.parent;
const textBefore = parent.textBetween(0, pos - start, undefined, '\uFFFC');
const textAfter = parent.textBetween(
pos - start,
parent.content.size,
undefined,
'\uFFFC',
);

const wordBefore = textBefore
// eslint-disable-next-line unicorn/prefer-string-replace-all
.replaceAll(/\uFFFC/g, '')
.split(/\b/)
.pop();
const wordAfter = textAfter
// eslint-disable-next-line unicorn/prefer-string-replace-all
.replaceAll(/\uFFFC/g, '')
.split(/\b/)
.shift();

const from = pos - (wordBefore?.length ?? 0);
const to = pos + (wordAfter?.length ?? 0);

return {
from,
to,
};
}

return {
from: selection.to,
to: selection.to + 1,
};
}
12 changes: 12 additions & 0 deletions projects/editor/utils/get-sliced-fragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {getHTMLFromFragment} from '@tiptap/core';
import type {EditorState} from '@tiptap/pm/state';

export function tuiGetSlicedFragment(state: EditorState, to: number): string {
const {doc, schema} = state;
const selected = doc.cut(to, to + 1);

return getHTMLFromFragment(selected.content, schema).replaceAll(
/<\/?[^>]+(>|$)/g,
'',
);
}
2 changes: 2 additions & 0 deletions projects/editor/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './delete-nodes';
export * from './get-current-word-bounds';
export * from './get-mark-range';
export * from './get-nested-nodes';
export * from './get-selected-content';
export * from './get-selection-state';
export * from './get-sliced-fragment';
export * from './is-selection-in';
export * from './legacy-converter';
export * from './parse-node-attributes';
Expand Down

0 comments on commit d5d1d33

Please sign in to comment.