UNPKG

@atlaskit/editor-plugin-hyperlink

Version:

Hyperlink plugin for @atlaskit/editor-core

113 lines (105 loc) 5.02 kB
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;