@atlaskit/editor-plugin-hyperlink
Version:
Hyperlink plugin for @atlaskit/editor-core
245 lines • 8.73 kB
JavaScript
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;
};