0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Add support for auto-linking URLs and emails within pasted rich text

This commit is contained in:
Thibaud Colas 2024-10-21 10:41:39 +01:00 committed by Sage Abdullah
parent 611d942c10
commit ba6360446e
No known key found for this signature in database
GPG Key ID: EB1A33CC51CC0217

View File

@ -1,5 +1,12 @@
import PropTypes from 'prop-types';
import { Modifier, EditorState, RichUtils } from 'draft-js';
import {
Modifier,
EditorState,
RichUtils,
convertFromHTML,
ContentState,
CharacterMetadata,
} from 'draft-js';
import React from 'react';
import { gettext } from '../../../utils/gettext';
@ -69,6 +76,11 @@ const djangoEmail = new RegExp(
*/
const urlPattern = /(?:http|ftp)s?:\/\/[^\s]+/;
// Find URLs and email addresses within text, ready to use with RegExp g flag.
// Enforce URLs start at word boundaries, and end with whitespace.
// Essential so URLs can be auto-linked in a space-separated succession.
const linkPatternSource = `\\b(${urlPattern.source}|${djangoUser.source}@${djangoDomain.source})(\\s|$)`;
export const getValidLinkURL = (text) => {
if (djangoEmail.test(text)) {
return `mailto:${text}`;
@ -114,6 +126,57 @@ const insertSingleLink = (editorState, text, url) => {
return RichUtils.toggleLink(editorState, selection, entityKey);
};
/**
* Insert the pasted HTML or text, auto-linking URLs and emails.
*/
const insertContentWithLinks = (editorState, htmlOrText) => {
const selection = editorState.getSelection();
let content = editorState.getCurrentContent();
const { contentBlocks, entityMap } = convertFromHTML(htmlOrText);
const blockMap = ContentState.createFromBlockArray(
contentBlocks,
entityMap,
).getBlockMap();
const blocks = blockMap.map((block) => {
const blockText = block.getText();
const pattern = new RegExp(linkPatternSource, 'ig');
// Find matches in the block, confirm the URL, create the entity, store the range.
const matches = Array.from(blockText.matchAll(pattern), (match) => {
const url = getValidLinkURL(match[1]);
if (!url) return {};
content = content.createEntity('LINK', 'MUTABLE', { url });
return {
start: match.index,
end: match.index + match[1].length,
key: content.getLastCreatedEntityKey(),
};
});
// Attach the link to the correct characters based on the matches ranges.
const chars = block.getCharacterList().map((char, i) => {
const match = matches.find(({ start, end }) => i >= start && i < end);
if (match) {
return CharacterMetadata.applyEntity(char, match.key);
}
return char;
});
return block.set('characterList', chars);
});
content = Modifier.replaceWithFragment(
content,
selection,
blockMap.merge(blocks),
);
return EditorState.push(editorState, content, 'insert-characters');
};
export const onPasteLink = (text, html, editorState, { setEditorState }) => {
const url = getValidLinkURL(text);
@ -122,6 +185,12 @@ export const onPasteLink = (text, html, editorState, { setEditorState }) => {
return 'handled';
}
if (new RegExp(linkPatternSource, 'gi').test(text)) {
// Prefer the multi-line HTML clipboard data if present.
setEditorState(insertContentWithLinks(editorState, html || text));
return 'handled';
}
return 'not-handled';
};