@atlaskit/editor-plugin-hyperlink
Version:
Hyperlink plugin for @atlaskit/editor-core
113 lines (105 loc) • 5.02 kB
JavaScript
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { addLinkMetadata } from '@atlaskit/editor-common/card';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { createRule, findFilepaths, getLinkCreationAnalyticsEvent, isLinkInMatches, LinkMatcher, normalizeUrl, shouldAutoLinkifyMatch } from '@atlaskit/editor-common/utils';
import { createPlugin } from '@atlaskit/prosemirror-input-rules';
import { toolbarKey } from './toolbar-buttons';
/**
* Called when space after link, but not on enter
*/
export function createLinkInputRule(regexp, editorAnalyticsApi) {
// Plain typed text (eg, typing 'www.google.com') should convert to a hyperlink
return createRule(regexp, (state, match, start, end) => {
var _toolbarKey$getState$, _toolbarKey$getState;
const {
schema
} = state;
if (state.doc.rangeHasMark(start, end, schema.marks.link)) {
return null;
}
const link = match;
// Property 'url' does not exist on type 'RegExpExecArray', the type of `match`.
// This check is in case the match is not a Linkify match, which has a url property.
if (link.url === undefined) {
return null;
}
if (!shouldAutoLinkifyMatch(link)) {
return null;
}
const url = normalizeUrl(link.url);
// Not previously handled; don't create a link if the URL is empty.
// This will only happen if the `regexp` matches more links than the normalizeUrl validation;
// if they both use the same linkify instance this shouldn't happen.
if (url === '') {
return null;
}
const markType = schema.mark('link', {
href: url
});
// Need access to complete text to check if last URL is part of a filepath before linkifying
const nodeBefore = state.selection.$from.nodeBefore;
if (!nodeBefore || !nodeBefore.isText || !nodeBefore.text) {
return null;
}
const filepaths = findFilepaths(nodeBefore.text,
// The position referenced by 'start' is relative to the start of the document, findFilepaths deals with index in a node only.
start - (nodeBefore.text.length - link.text.length) // (start of link match) - (whole node text length - link length) gets start of text node, which is used as offset
);
if (isLinkInMatches(start, filepaths)) {
const tr = state.tr;
return tr;
}
const from = start;
const to = Math.min(start + link.text.length, state.doc.content.size);
const tr = state.tr.addMark(from, to, markType);
// Keep old behavior that will delete the space after the link
if (to === end) {
tr.insertText(' ');
}
addLinkMetadata(state.selection, tr, {
inputMethod: INPUT_METHOD.AUTO_DETECT
});
const skipAnalytics = (_toolbarKey$getState$ = (_toolbarKey$getState = toolbarKey.getState(state)) === null || _toolbarKey$getState === void 0 ? void 0 : _toolbarKey$getState.skipAnalytics) !== null && _toolbarKey$getState$ !== void 0 ? _toolbarKey$getState$ : false;
if (skipAnalytics) {
return tr;
}
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(INPUT_METHOD.AUTO_DETECT, url))(tr);
return tr;
});
}
export function createInputRulePlugin(schema, editorAnalyticsApi, autoLinkOnBlur = false) {
if (!schema.marks.link) {
return;
}
const urlWithASpaceRule = createLinkInputRule(LinkMatcher.create(), editorAnalyticsApi);
// [something](link) should convert to a hyperlink
// eslint-disable-next-line require-unicode-regexp
const markdownLinkRule = createRule(/(^|[^!])\[(.*?)\]\((\S+)\)$/, (state, match, start, end) => {
var _toolbarKey$getState$2, _toolbarKey$getState2;
const {
schema
} = state;
const [, prefix, linkText, linkUrl] = match;
// We don't filter this match here by shouldAutoLinkifyMatch
// because the intent of creating a link is clear
const url = normalizeUrl(linkUrl).trim();
const markType = schema.mark('link', {
href: url
});
const tr = state.tr.replaceWith(start + prefix.length, end, schema.text((linkText || '').trim(), [markType]));
addLinkMetadata(state.selection, tr, {
inputMethod: INPUT_METHOD.FORMATTING
});
const skipAnalytics = (_toolbarKey$getState$2 = (_toolbarKey$getState2 = toolbarKey.getState(state)) === null || _toolbarKey$getState2 === void 0 ? void 0 : _toolbarKey$getState2.skipAnalytics) !== null && _toolbarKey$getState$2 !== void 0 ? _toolbarKey$getState$2 : false;
if (skipAnalytics) {
return tr;
}
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(INPUT_METHOD.FORMATTING, url))(tr);
return tr;
});
return new SafePlugin(createPlugin('hyperlink', [urlWithASpaceRule, markdownLinkRule], autoLinkOnBlur ? {
checkOnBlur: true,
appendTextOnBlur: ' '
} : undefined));
}
export default createInputRulePlugin;