UNPKG

@atlaskit/editor-plugin-hyperlink

Version:

Hyperlink plugin for @atlaskit/editor-core

245 lines 8.73 kB
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, buildEditLinkPayload, EVENT_TYPE, INPUT_METHOD, unlinkPayload } from '@atlaskit/editor-common/analytics'; import { addLinkMetadata, commandWithMetadata } from '@atlaskit/editor-common/card'; import { withAnalytics } from '@atlaskit/editor-common/editor-analytics'; import { isTextAtPos, LinkAction } from '@atlaskit/editor-common/link'; import { editorCommandToPMCommand } from '@atlaskit/editor-common/preset'; import { getLinkCreationAnalyticsEvent, normalizeUrl } from '@atlaskit/editor-common/utils'; import { Selection } from '@atlaskit/editor-prosemirror/state'; import { stateKey } from '../pm-plugins/main'; function setLinkHrefEditorCommand(href, pos, editorAnalyticsApi, to, isTabPressed) { return ({ tr }) => { if (!isTextAtPos(pos)({ tr })) { return null; } const $pos = tr.doc.resolve(pos); const node = tr.doc.nodeAt(pos); const linkMark = tr.doc.type.schema.marks.link; const mark = linkMark.isInSet(node.marks); const url = normalizeUrl(href); if (mark && mark.attrs.href === url) { return null; } const rightBound = to && pos !== to ? to : pos - $pos.textOffset + node.nodeSize; tr.removeMark(pos, rightBound, linkMark); if (href.trim()) { tr.addMark(pos, rightBound, linkMark.create({ ...(mark && mark.attrs || {}), href: url })); } else { editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(unlinkPayload(ACTION_SUBJECT_ID.HYPERLINK))(tr); } if (!isTabPressed) { tr.setMeta(stateKey, { type: LinkAction.HIDE_TOOLBAR }); } return tr; }; } export function setLinkHref(href, pos, editorAnalyticsApi, to, isTabPressed) { return editorCommandToPMCommand(setLinkHrefEditorCommand(href, pos, editorAnalyticsApi, to, isTabPressed)); } export function updateLinkEditorCommand(href, text, pos, to) { return ({ tr }) => { const $pos = tr.doc.resolve(pos); const node = tr.doc.nodeAt(pos); if (!node) { return null; } const url = normalizeUrl(href); const mark = tr.doc.type.schema.marks.link.isInSet(node.marks); const linkMark = tr.doc.type.schema.marks.link; const rightBound = to && pos !== to ? to : pos - $pos.textOffset + node.nodeSize; if (!url && text) { tr.removeMark(pos, rightBound, linkMark); tr.insertText(text, pos, rightBound); } else if (!url) { return null; } else { tr.insertText(text, pos, rightBound); // Casting to LinkAttributes to prevent wrong attributes been passed (Example ED-7951) const linkAttrs = { ...(mark && mark.attrs || {}), href: url }; tr.addMark(pos, pos + text.length, linkMark.create(linkAttrs)); tr.setMeta(stateKey, { type: LinkAction.HIDE_TOOLBAR }); } return tr; }; } export function updateLink(href, text, pos, to) { return editorCommandToPMCommand(updateLinkEditorCommand(href, text, pos, to)); } export function insertLink(from, to, incomingHref, incomingTitle, displayText, source, sourceEvent, appearance = 'inline', cardApiActions) { return (state, dispatch) => { const link = state.schema.marks.link; const { tr } = state; if (incomingHref.trim()) { var _stateKey$getState; const normalizedUrl = normalizeUrl(incomingHref); // NB: in this context, `currentText` represents text which has been // highlighted in the Editor, upon which a link is is being added. const currentText = (_stateKey$getState = stateKey.getState(state)) === null || _stateKey$getState === void 0 ? void 0 : _stateKey$getState.activeText; let markEnd = to; const text = displayText || incomingTitle || incomingHref; if (!displayText || displayText !== currentText) { tr.insertText(text, from, to); // new block created to wrap the link if (tr.mapping.map(from) === from + text.length + 2) { // +1 is for the block's opening tag markEnd = from + text.length + 1; } else { markEnd = from + text.length; } } tr.addMark(from, markEnd, link.create({ href: normalizedUrl })); tr.setSelection(Selection.near(tr.doc.resolve(markEnd))); if (!displayText || displayText === incomingHref) { const queueCardsFromChangedTr = cardApiActions === null || cardApiActions === void 0 ? void 0 : cardApiActions.queueCardsFromChangedTr; if (queueCardsFromChangedTr) { queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion source, ACTION.INSERTED, false, sourceEvent, appearance); } else { addLinkMetadata(state.selection, tr, { action: ACTION.INSERTED, inputMethod: source, sourceEvent }); } } else { /** * Add link metadata because queue cards would have otherwise handled this for us */ addLinkMetadata(state.selection, tr, { action: ACTION.INSERTED, inputMethod: source, sourceEvent }); } tr.setMeta(stateKey, { type: LinkAction.HIDE_TOOLBAR }); if (dispatch) { dispatch(tr); } return true; } tr.setMeta(stateKey, { type: LinkAction.HIDE_TOOLBAR }); if (dispatch) { dispatch(tr); } return false; }; } export const insertLinkWithAnalytics = (inputMethod, from, to, href, cardActions, editorAnalyticsApi, title, displayText, cardsAvailable = false, sourceEvent = undefined, appearance) => { // If smart cards are available, we send analytics for hyperlinks when a smart link is rejected. if (cardsAvailable && !title && !displayText) { return insertLink(from, to, href, title, displayText, inputMethod, sourceEvent, appearance, cardActions); } return withAnalytics(editorAnalyticsApi, getLinkCreationAnalyticsEvent(inputMethod, href))(insertLink(from, to, href, title, displayText, inputMethod, sourceEvent, appearance, cardActions)); }; export function removeLink(pos, editorAnalyticsApi) { return commandWithMetadata(setLinkHref('', pos, editorAnalyticsApi), { action: ACTION.UNLINK }); } export function removeLinkEditorCommand(pos, editorAnalyticsApi) { return ({ tr }) => { setLinkHrefEditorCommand('', pos, editorAnalyticsApi)({ tr }); addLinkMetadata(tr.selection, tr, { action: ACTION.UNLINK }); return tr; }; } export function editInsertedLink(editorAnalyticsApi) { return (state, dispatch) => { if (dispatch) { const { tr } = state; tr.setMeta(stateKey, { type: LinkAction.EDIT_INSERTED_TOOLBAR, inputMethod: INPUT_METHOD.FLOATING_TB }); editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(buildEditLinkPayload(ACTION_SUBJECT_ID.HYPERLINK))(tr); dispatch(tr); } return true; }; } export function showLinkToolbar(inputMethod, editorAnalyticsApi) { return ({ tr }) => { const newTr = tr.setMeta(stateKey, { type: LinkAction.SHOW_INSERT_TOOLBAR, inputMethod }); editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent({ action: ACTION.INVOKED, actionSubject: ACTION_SUBJECT.TYPEAHEAD, actionSubjectId: ACTION_SUBJECT_ID.TYPEAHEAD_LINK, attributes: { inputMethod }, eventType: EVENT_TYPE.UI })(newTr); return newTr; }; } export function hideLinkToolbar() { return function (state, dispatch) { if (dispatch) { dispatch(hideLinkToolbarSetMeta(state.tr)); } return true; }; } export const hideLinkToolbarSetMeta = tr => { return tr.setMeta(stateKey, { type: LinkAction.HIDE_TOOLBAR }); }; export const onEscapeCallback = cardActions => (state, dispatch) => { var _cardActions$hideLink; const { tr } = state; hideLinkToolbarSetMeta(tr); cardActions === null || cardActions === void 0 ? void 0 : (_cardActions$hideLink = cardActions.hideLinkToolbar) === null || _cardActions$hideLink === void 0 ? void 0 : _cardActions$hideLink.call(cardActions, tr); if (dispatch) { dispatch(tr); return true; } return false; }; export const onClickAwayCallback = (state, dispatch) => { if (dispatch) { hideLinkToolbar()(state, dispatch); return true; } return false; };