Schema Definition
Schema definition for the Link mark type:
{
link: {
attrs: {
href: { default: '' },
title: { default: '' }
}
}
}Model Representation
Example model representation:
{
type: 'text',
text: 'Click here',
marks: [{
type: 'link',
attrs: { href: 'https://example.com', title: 'Example' }
}]
}HTML Serialization
Converting model to HTML:
function serializeLinkMark(text, mark) {
const href = escapeHtml(mark.attrs?.href || '');
const title = mark.attrs?.title ?
' title="' + escapeHtml(mark.attrs.title) + '"' : '';
return '<a href="' + href + '"' + title + '>' + text + '</a>';
}HTML Deserialization
Parsing HTML to model:
function extractLinkMark(element) {
if (element.tagName === 'A' && element.hasAttribute('href')) {
return {
type: 'link',
attrs: {
href: element.getAttribute('href') || '',
title: element.getAttribute('title') || ''
}
};
}
return null;
}View Integration
View Integration Notes: Pay special attention to contenteditable behavior, selection handling, and event management when implementing this node type in your view layer.
View integration code:
// Creating/editing links
function createLink(url, text) {
return {
type: 'text',
text: text,
marks: [{
type: 'link',
attrs: { href: url }
}]
};
}
// Updating link URL
function updateLinkUrl(linkMark, newUrl) {
return {
...linkMark,
attrs: { ...linkMark.attrs, href: newUrl }
};
}
// Link click handling
function handleLinkClick(e) {
const link = e.target.closest('a');
if (link && e.ctrlKey || e.metaKey) {
// Allow default (open in new tab)
return;
}
e.preventDefault();
// Handle link navigation in editor
}Common Issues
Common Pitfalls: These are issues frequently encountered when implementing this node type. Review carefully before implementation.
Common issues and solutions:
// Issue: Empty href links
// Solution: Validate href before creating link
function validateLink(link) {
if (!link.attrs.href || link.attrs.href.trim() === '') {
return false;
}
return true;
}
// Issue: Link with other marks
// Solution: Links can coexist with bold, italic, etc.
// But code mark excludes links
function canApplyLinkWithMarks(marks) {
return !marks.some(m => m.type === 'code');
}
// Issue: Relative vs absolute URLs
// Solution: Normalize URLs
function normalizeUrl(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
if (url.startsWith('/')) {
return url; // Relative to root
}
return 'https://' + url; // Assume external
}
// Issue: Link security (XSS)
// Solution: Sanitize href
function sanitizeUrl(url) {
// Remove javascript:, data:, etc.
if (url.startsWith('javascript:') || url.startsWith('data:')) {
return '#';
}
return url;
}Implementation
Complete implementation example:
class LinkMark {
constructor(attrs) {
this.type = 'link';
this.attrs = {
href: attrs?.href || '',
title: attrs?.title || ''
};
}
toDOM() {
const attrs = {};
if (this.attrs.href) attrs.href = this.attrs.href;
if (this.attrs.title) attrs.title = this.attrs.title;
return ['a', attrs, 0];
}
static fromDOM(element) {
if (element.tagName === 'A' && element.hasAttribute('href')) {
return new LinkMark({
href: element.getAttribute('href') || '',
title: element.getAttribute('title') || ''
});
}
return null;
}
}