Parse custom links defined in inlineContentSpec #2472
Replies: 10 comments
-
The Links are treated pretty oddly in BlockNote though (since they are still in the Tiptap API, not the BlockNote one), so I think you may run into some issues actually setting a priority that is high enough, they seem to set it to be really high by default: https://github.com/ueberdosis/tiptap/blob/35645d94ae9cd73448a564104c2e08f64e9564bc/packages/extension-link/src/link.ts#L197 I think I will change this on the BlockNote side to be set to the same priority as the other default extensions to make it easy for you to override This conversation is likely to be useful in helping understand how we would go about links in BlockNote: #1861 As for the LinkToolbar, it is built only for our links and to make it do more than that would be complicated and not likely to work well. So I'd recommend just re-implementing this for yourself, perhaps you can use our implementation as reference for it. |
Beta Was this translation helpful? Give feedback.
-
|
Hey @nperez0111, thanks for your timely response. I tried adding ["link", "text"] to runsBefore, but that didn't seem to have the effect I was hoping for. Is there another property I can test? I'll give you my complete custom schema for context: export const InternalReference = createReactInlineContentSpec(
{
type: 'internalReference',
propSchema: {
// uuid of the referenced document element
id: {
default: 'unknown-id',
},
// Type of document element, e.g. 'user'
referenceType: {
default: 'unknown',
},
// Attribute of the referenced document element to show as label
referenceAttribute: {
default: 'title',
},
// Style of the reference, i.e. 'reference' or 'inline'
referenceStyle: {
default: 'reference',
},
displayName: {
default: 'Unknown',
},
shouldFetchProperties: {
default: false,
},
},
content: 'none',
} as const,
{
render: props => (
<InternalReferenceRenderer
id={props.inlineContent.props.id}
type={props.inlineContent.props.referenceType as InternalReferenceTypes}
displayedAttribute={props.inlineContent.props.referenceAttribute as InternalReferenceAttributeNames}
shouldFetchProperties={props.inlineContent.props.shouldFetchProperties}
elementAttributes={{} as InternalReferenceElementAttributes}
displayStyle={props.inlineContent.props.referenceStyle as ReferenceDisplayStyles}
onUpdate={update => {
console.log(update);
}}
/>
),
parse: element => {
const isInternalReference =
element.tagName === 'A' && element.className.includes(internalReferenceClassName);
console.log('Parsing element for InternalReference:', element, 'isInternalReference:', isInternalReference);
if (!isInternalReference) {
return undefined;
}
return {
type: 'internalReference',
props: {
id: element.dataset['internal-reference-id'] || 'unknown-id',
referenceType: element.dataset['internal-reference-type'] || 'unknown',
referenceAttribute: element.dataset['internal-reference-attribute'] || 'title',
referenceStyle: element.dataset['internal-reference-style'] || 'reference',
displayName: element.textContent || 'Internal Reference',
// parsed elements should check if update is needed
shouldFetchProperties: true,
},
};
},
toExternalHTML(props) {
const { id, displayName, referenceType, referenceStyle, referenceAttribute } = props.inlineContent.props;
const icon = InternalReferenceIconByType[referenceType as keyof typeof InternalReferenceIconByType];
return (
<a
className={internalReferenceClassName}
href={getLinkToHandbookElement(id, false)}
target="_blank"
data-internal-reference-type={referenceType}
data-internal-reference-id={id}
data-internal-reference-attribute={referenceAttribute}
data-internal-reference-style={referenceStyle}
>
<i className={`${icon.variant} fa-${icon.name}`} />
{displayName || 'Internal Reference'}
</a>
);
},
runsBefore: ['link', 'text'],
},
);It's quite a complex component, that will later have to refetch and update its own data, but I think through the update function that should hopefully work just fine. The log in my parse function never logs any links, unfortunately. I've read through the issue about link, which seems to be sort of my direction, but not quite. On the toolbars: Is there a way to create a custom toolbar for a specific extension? I found the Component.Generic.Popover which works great for nested menus, but I'll need the initial "open me when it's an external extension" similar to how the LinkToolbar works. Thank you so much! |
Beta Was this translation helpful? Give feedback.
-
@JSHSJ, like I said, the link extension is in the Tiptap layer, so this would have no effect, I'm just pointing out that there is a mechanism for this, we are just partway through the migration.
You'll likely need to re-implement this extension, since it is meant for our extension, and therefore only looks for marks named "link", and we wouldn't change that.
You'll need to re-implement what the LinkToolbar does to have the same functionality. In the future we may want to make an API that does this better, but we don't need it at this time for anything than the LinkToolbar, so we don't have an idea of what a good abstraction for this would be. I don't think we would be able to do anything about this in the short term, since it will be a larger refactor to get this to work. You can take a look at the suggestion I made here, and put up a PR with your solution if you need to implement it quickly. The suggestion in the other issue is sort of what you are already doing, but to instead model it as a different piece of content instead of as a link. Like just set your export and parse to use a wrapping div instead or something, so it is not in conflict with the link extension. Setting the priority of the link extension to 100, should cause the default priority of your custom extension to be higher (since they default to 101), but I am not sure that this is the approach that I want for the long run which is why I only put up a draft PR |
Beta Was this translation helpful? Give feedback.
-
|
Thank you very much for your suggestions and the quick actions. I'll see if I can find the time to follow through with your suggestion. Wrapping my extension's element in a would certainly be the easiest way forward, but would require us to migrate all existing links made in our old TipTap editor in some way. I had hoped that would be possible through a parse function, but I understand the issues and welcome your idea of getting rid of the TipTap + linkify implementation :)
|
Beta Was this translation helpful? Give feedback.
-
|
Thanks for your understanding @JSHSJ, perhaps you can have an intermediate parse step that turns the I will keep this open given that links are difficult to customize at this point |
Beta Was this translation helpful? Give feedback.
-
|
Writing a "pre" parse function did address the issue, thank you! When trying to write a custom ToolbarController I had difficulties checking whether my custom inline node is selected. My node is not editable (as in text editable) but should have a toolbar. I believe your LocationAPI implementation in #2044 will address that issue, and allow me to check if my current node is selected, right? Thank you very much! |
Beta Was this translation helpful? Give feedback.
-
|
The location API would probably make that sort of thing easier. For now though, I'd recommend you look at how the linkToolbar works to see how we'd approach it. You can have a BlockNote Extension which has a store which holds whether the current selection is pointing to the node that you want, and then choose to render a menu based on that. Also, another approach is to have a click handler within the custom node's element and then trigger a toolbar based on that. We want to make better APIs for this sort of thing in the future, but the only elements that we have that need this sort of menu are links right now so we felt like we didn't have the right abstraction to recommend to people. What is the custom node you are trying to make? What are you building with BlockNote, btw? |
Beta Was this translation helpful? Give feedback.
-
|
I've been working on re-creating my own toolbar by looking at your implementation. It's a good start, but I haven't found a way to get the current inline content based on cursor position, since they are not marks (I think) and I can't seem to find them in the prosemirror content either. Clicking is possible, but it also needs to be keyboard accessible. My custom node is a sort of dynamic link, that has a configurable text and icon, based on some metadata. In the end it's a reference to another element. At the moment, I'm testing if it is a good fit for our product as a text editor (and replacement for TipTap). We'll be sure to upgrade to the Pro License, since we'll need your pro features :) Thanks for your support and have a good weekend! |
Beta Was this translation helpful? Give feedback.
-
Inline content gets represented as an inline node so to find a node at the current cursor position you could do: editor.transact((tr) => {
const resolvedPos = tr.selection.$anchor;
const inlineContentNode = resolvedPos.node();
if (inlineContentNode.type.name !== "mention") {
return;
}
const nodeRange = {
from: resolvedPos.before(),
to: resolvedPos.after(),
};
return {
range: nodeRange,
node: inlineContentNode,
get text() {
return tr.doc.textBetween(nodeRange.from, nodeRange.to);
},
get position() {
return posToDOMRect(
editor.prosemirrorView,
nodeRange.from,
nodeRange.to,
).toJSON() as DOMRect;
},
};
})Based on this: BlockNote/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts Lines 17 to 49 in ed2c337 |
Beta Was this translation helpful? Give feedback.
-
|
Hmm similar to the mention example in the docs, my node is not actually selectable by cursor. Is there a good way to achieve this? Ideally the cursor would enter the inline content and on the next arrow press, leave it again. Maybe using styled and the content ref on a single character? Edit: That sort of works, but of course makes the note editable and adds the content by itself, making it more unpredictable. 🤔 But that way I can indeed create my custom menus, which is great! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Is your feature request related to a problem? Please describe.
We have a custom link type, called a reference. I tried creating a custom inline content spec for it, but the parse function never gets elements with
tagName === "A". I assume they are parsed by the built-in link inline content before.Describe the solution you'd like
Allow adding a parsing priority, either through their position in
BlockNoteSchema.createor increateReactInlineContentSpec, so I can parse custom links.Describe alternatives you've considered
Of course it might be possible by wrapping the component in a span, but that is not what we want ideally. We're coming from TipTap, so I might try adding it as TipTap Extension, but I prefer your way of defining custom content.
Beta Was this translation helpful? Give feedback.
All reactions