Skip to content
Closed
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
47 changes: 47 additions & 0 deletions docs/content/docs/features/blocks/inline-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,53 @@ type Link = {
};
```

### Customizing Links

You can customize how links are rendered and how they respond to clicks with the `links` editor option.

```ts
const editor = BlockNoteEditor.create({
links: {
HTMLAttributes: {
class: "my-link-class",
target: "_blank",
},
onClick: (event) => {
// Custom click logic, e.g. routing without a page reload.
},
},
});
```
Comment thread
matthewlipski marked this conversation as resolved.

#### `HTMLAttributes`

Additional HTML attributes that should be added to rendered link elements.

```ts
const editor = BlockNoteEditor.create({
links: {
HTMLAttributes: {
class: "my-link-class",
target: "_blank",
},
},
});
```

#### `onClick`

Custom handler invoked when a link is clicked. If left `undefined`, links are opened in a new window on click (the default behavior). If provided, that default behavior is disabled and this function is called instead.

```ts
const editor = BlockNoteEditor.create({
links: {
onClick: (event) => {
// Do something when a link is clicked.
},
},
});
```

## Default Styles

The default text formatting options in BlockNote are represented by the `Styles` in the default schema:
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ export interface BlockNoteEditorOptions<
NoInfer<SSchema>
>[];

/**
* Options for configuring how links behave in the editor.
*/
links?: {
/**
* HTML attributes to add to rendered link elements.
*
* @default {}
* @example { class: "my-link-class", target: "_blank" }
*/
HTMLAttributes?: Record<string, any>;
/**
* Custom handler invoked when a link is clicked. If left `undefined`,
* links are opened in a new window on click. If provided, the default
* open-on-click behavior is disabled and this function is called instead.
*/
onClick?: (event: MouseEvent) => void;
};

/**
* @deprecated, provide placeholders via dictionary instead
* @internal
Expand Down
93 changes: 89 additions & 4 deletions packages/core/src/editor/managers/ExtensionManager/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
AnyExtension as AnyTiptapExtension,
extensions,
getAttributes,
Node,
Extension as TiptapExtension,
} from "@tiptap/core";
import { Gapcursor } from "@tiptap/extensions/gap-cursor";
import { Link } from "@tiptap/extension-link";
import { Text } from "@tiptap/extension-text";
import { Plugin, PluginKey } from "prosemirror-state";
import { createDropFileExtension } from "../../../api/clipboard/fromClipboard/fileDropExtension.js";
import { createPasteFromClipboardExtension } from "../../../api/clipboard/fromClipboard/pasteExtension.js";
import { createCopyToClipboardExtension } from "../../../api/clipboard/toClipboard/copyExtension.js";
Expand Down Expand Up @@ -91,12 +93,95 @@ export function getDefaultTiptapExtensions(
delete (attrs as Record<string, unknown>).title;
return attrs;
},
addProseMirrorPlugins() {
const plugins = this.parent?.() || [];

const markType = this.type;
const tiptapEditor = this.editor;
// Copied from @tiptap/extension-link's `clickHandler` helper, but
// calls `onClick` if the editor option is defined. Otherwise, uses
// default behaviour.
// https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/src/helpers/clickHandler.ts
plugins.push(
new Plugin({
key: new PluginKey("linkClickHandler"),
props: {
handleClick: (view, _pos, event) => {
if (event.button !== 0) {
return false;
}

if (!view.editable) {
return false;
}

let link: HTMLAnchorElement | null = null;

if (event.target instanceof HTMLAnchorElement) {
link = event.target;
} else {
const target = event.target as HTMLElement | null;
if (!target) {
return false;
}

const root = tiptapEditor.view.dom;

// Intentionally limit the lookup to the editor root.
// Using tag names like DIV as boundaries breaks with custom NodeViews.
link = target.closest<HTMLAnchorElement>("a");

if (link && !root.contains(link)) {
link = null;
}
}

if (!link) {
return false;
}

let handled = false;

// `enableClickSelection` is always disabled.
// if (options.enableClickSelection) {
// const commandResult =
// tiptapEditor.commands.extendMarkRange(
// markType.name,
// );
// handled = commandResult;
// }

if (options.links?.onClick) {
options.links.onClick(event);
handled = true;
} else {
const attrs = getAttributes(view.state, markType.name);
const href = link.href ?? attrs.href;
const target = link.target ?? attrs.target;

if (href) {
window.open(href, target);
handled = true;
Comment thread
matthewlipski marked this conversation as resolved.
}
Comment thread
matthewlipski marked this conversation as resolved.
}

return handled;
},
Comment thread
matthewlipski marked this conversation as resolved.
},
}),
);

return plugins;
},
})
.configure({
defaultProtocol: DEFAULT_LINK_PROTOCOL,
// only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450
protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS,
}),
defaultProtocol: DEFAULT_LINK_PROTOCOL,
// only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450
protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS,
HTMLAttributes: options.links?.HTMLAttributes ?? {},
// Always false as we handle clicks ourselves above.
openOnClick: false,
}),
...(Object.values(editor.schema.styleSpecs).map((styleSpec) => {
return styleSpec.implementation.mark.configure({
editor: editor,
Expand Down
Loading