@atlaskit/editor-plugin-hyperlink
Version:
Hyperlink plugin for @atlaskit/editor-core
307 lines (303 loc) • 17.8 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React, { useRef } from 'react';
import { isSafeUrl } from '@atlaskit/adf-schema';
import { ACTION, ACTION_SUBJECT_ID, INPUT_METHOD, buildVisitedLinkPayload } from '@atlaskit/editor-common/analytics';
import { commandWithMetadata } from '@atlaskit/editor-common/card';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { HyperlinkAddToolbar } from '@atlaskit/editor-common/link';
import { linkMessages, linkToolbarMessages as linkToolbarCommonMessages } from '@atlaskit/editor-common/messages';
import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check';
import { LINKPICKER_HEIGHT_IN_PX, RECENT_SEARCH_HEIGHT_IN_PX, RECENT_SEARCH_WIDTH_IN_PX } from '@atlaskit/editor-common/ui';
import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent';
import { normalizeUrl } from '@atlaskit/editor-common/utils';
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
import EditIcon from '@atlaskit/icon/core/edit';
import LinkBrokenIcon from '@atlaskit/icon/core/link-broken';
import LinkExternalIcon from '@atlaskit/icon/core/link-external';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { editInsertedLink, insertLinkWithAnalytics, onClickAwayCallback, onEscapeCallback, removeLink, updateLink } from '../../editor-commands/commands';
import { stateKey } from '../../pm-plugins/main';
import { toolbarKey } from '../../pm-plugins/toolbar-buttons';
/* type guard for edit links */
function isEditLink(linkMark) {
return linkMark.pos !== undefined;
}
var dispatchAnalytics = function dispatchAnalytics(dispatch, state, analyticsBuilder, editorAnalyticsApi) {
if (dispatch) {
var tr = state.tr;
editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent(analyticsBuilder(ACTION_SUBJECT_ID.HYPERLINK))(tr);
dispatch(tr);
}
};
var visitHyperlink = function visitHyperlink(editorAnalyticsApi) {
return function (state, dispatch) {
dispatchAnalytics(dispatch, state, buildVisitedLinkPayload, editorAnalyticsApi);
return true;
};
};
function getLinkText(activeLinkMark, state) {
if (!activeLinkMark.node) {
return undefined;
}
var textToUrl = normalizeUrl(activeLinkMark.node.text);
var linkMark = activeLinkMark.node.marks.find(function (mark) {
return mark.type === state.schema.marks.link;
});
var linkHref = linkMark && linkMark.attrs.href;
if (textToUrl === linkHref) {
return undefined;
}
return activeLinkMark.node.text;
}
var selector = function selector(states) {
var _states$hyperlinkStat, _states$hyperlinkStat2, _states$hyperlinkStat3;
return {
timesViewed: (_states$hyperlinkStat = states.hyperlinkState) === null || _states$hyperlinkStat === void 0 ? void 0 : _states$hyperlinkStat.timesViewed,
inputMethod: (_states$hyperlinkStat2 = states.hyperlinkState) === null || _states$hyperlinkStat2 === void 0 ? void 0 : _states$hyperlinkStat2.inputMethod,
searchSessionId: (_states$hyperlinkStat3 = states.hyperlinkState) === null || _states$hyperlinkStat3 === void 0 ? void 0 : _states$hyperlinkStat3.searchSessionId
};
};
export function HyperlinkAddToolbarWithState(_ref) {
var _pluginInjectionApi$c;
var _ref$linkPickerOption = _ref.linkPickerOptions,
linkPickerOptions = _ref$linkPickerOption === void 0 ? {} : _ref$linkPickerOption,
onSubmit = _ref.onSubmit,
displayText = _ref.displayText,
displayUrl = _ref.displayUrl,
providerFactory = _ref.providerFactory,
view = _ref.view,
onCancel = _ref.onCancel,
invokeMethod = _ref.invokeMethod,
lpLinkPicker = _ref.lpLinkPicker,
onClose = _ref.onClose,
onEscapeCallback = _ref.onEscapeCallback,
onClickAwayCallback = _ref.onClickAwayCallback,
pluginInjectionApi = _ref.pluginInjectionApi;
var _useSharedPluginState = useSharedPluginStateWithSelector(pluginInjectionApi, ['hyperlink'], selector),
timesViewed = _useSharedPluginState.timesViewed,
inputMethod = _useSharedPluginState.inputMethod,
searchSessionId = _useSharedPluginState.searchSessionId;
// This is constant rather than dynamic - because if someone's already got a hyperlink toolbar open,
// we don't want to dynamically change it on them as this would cause data loss if they've already
// started typing in the fields.
var isOffline = useRef(isOfflineMode(pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$c = pluginInjectionApi.connectivity) === null || _pluginInjectionApi$c === void 0 || (_pluginInjectionApi$c = _pluginInjectionApi$c.sharedState.currentState()) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.mode));
return /*#__PURE__*/React.createElement(HyperlinkAddToolbar, {
linkPickerOptions: linkPickerOptions,
onSubmit: onSubmit,
displayText: displayText,
displayUrl: displayUrl,
providerFactory: providerFactory,
view: view,
onCancel: onCancel,
invokeMethod: invokeMethod,
lpLinkPicker: lpLinkPicker,
onClose: onClose,
onEscapeCallback: onEscapeCallback,
onClickAwayCallback: onClickAwayCallback,
timesViewed: timesViewed,
inputMethod: inputMethod,
searchSessionId: searchSessionId,
isOffline: isOffline.current
});
}
export var getToolbarConfig = function getToolbarConfig(options, pluginInjectionApi) {
return function (state, intl, providerFactory) {
var _pluginInjectionApi$c2, _pluginInjectionApi$a, _options$lpLinkPicker;
if (options.disableFloatingToolbar) {
return;
}
var isToolbarAIFCEnabled = Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar);
var linkState = stateKey.getState(state);
var activeLinkMark = linkState === null || linkState === void 0 ? void 0 : linkState.activeLinkMark;
// If range selection, we don't show hyperlink floating toolbar.
// Text Formattting toolbar is shown instaed.
if (state.selection instanceof TextSelection && state.selection.to !== state.selection.from && (activeLinkMark === null || activeLinkMark === void 0 ? void 0 : activeLinkMark.type) === 'EDIT' && editorExperiment('platform_editor_controls', 'variant1')) {
return;
}
var formatMessage = intl.formatMessage;
var editorCardActions = pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$c2 = pluginInjectionApi.card) === null || _pluginInjectionApi$c2 === void 0 ? void 0 : _pluginInjectionApi$c2.actions;
var editorAnalyticsApi = pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions;
var lpLinkPicker = (_options$lpLinkPicker = options.lpLinkPicker) !== null && _options$lpLinkPicker !== void 0 ? _options$lpLinkPicker : true;
if (activeLinkMark) {
var hyperLinkToolbar = {
title: 'Hyperlink floating controls',
nodeType: [state.schema.nodes.text, state.schema.nodes.paragraph, state.schema.nodes.heading, state.schema.nodes.taskItem, state.schema.nodes.decisionItem, state.schema.nodes.caption].filter(function (nodeType) {
return !!nodeType;
}),
// Use only the node types existing in the schema ED-6745
align: 'left',
className: activeLinkMark.type.match('INSERT|EDIT_INSERTED') ? 'hyperlink-floating-toolbar' : '',
// getDomRef by default uses view.state.selection.from to position the toolbar.
// However, when the user clicks in right after the link the view.state.selection.from references to the dom after the selection.
// So instead we want to use the activeLinkMark.pos which has been calculated as the position before the click so that would be the link node
getDomRef: activeLinkMark && (activeLinkMark.type === 'EDIT_INSERTED' || activeLinkMark.type === 'EDIT') && editorExperiment('platform_editor_controls', 'variant1') ? function (view) {
var domRef = findDomRefAtPos(activeLinkMark.pos, view.domAtPos.bind(view));
return domRef instanceof HTMLElement ? domRef : undefined;
} : undefined
};
switch (activeLinkMark.type) {
case 'EDIT':
{
var _pluginInjectionApi$c3, _cardActions$getStart, _cardActions$getEndin;
var pos = activeLinkMark.pos,
node = activeLinkMark.node;
var linkMark = node.marks.filter(function (mark) {
return mark.type === state.schema.marks.link;
});
var link = linkMark[0] && linkMark[0].attrs.href;
var isValidUrl = isSafeUrl(link);
var labelOpenLink = formatMessage(isValidUrl ? linkMessages.openLink : linkToolbarCommonMessages.unableToOpenLink);
// TODO: ED-14403 - investigate why these are not translating?
var labelUnlink = formatMessage(linkToolbarCommonMessages.unlink);
var editLink = formatMessage(linkToolbarCommonMessages.editLink);
var metadata = {
url: link,
title: ''
};
if (activeLinkMark.node.text) {
metadata.title = activeLinkMark.node.text;
}
var areAnyNewToolbarFlagsEnabled = areToolbarFlagsEnabled(Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar));
var cardActions = pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$c3 = pluginInjectionApi.card) === null || _pluginInjectionApi$c3 === void 0 ? void 0 : _pluginInjectionApi$c3.actions;
var startingToolbarItems = (_cardActions$getStart = cardActions === null || cardActions === void 0 ? void 0 : cardActions.getStartingToolbarItems(intl, link, editInsertedLink(editorAnalyticsApi), metadata, areAnyNewToolbarFlagsEnabled ? state : undefined)) !== null && _cardActions$getStart !== void 0 ? _cardActions$getStart : [{
id: 'editor.link.edit',
testId: 'editor.link.edit',
type: 'button',
onClick: editInsertedLink(editorAnalyticsApi),
title: editLink,
showTitle: areAnyNewToolbarFlagsEnabled ? false : true,
metadata: metadata,
icon: areAnyNewToolbarFlagsEnabled ? EditIcon : undefined
}, {
type: 'separator'
}];
var openLinkButton = {
id: 'editor.link.openLink',
testId: 'editor.link.openLink',
type: 'button',
disabled: !isValidUrl,
target: '_blank',
href: isValidUrl ? link : undefined,
onClick: visitHyperlink(editorAnalyticsApi),
title: labelOpenLink,
icon: LinkExternalIcon,
className: 'hyperlink-open-link',
metadata: metadata,
tabIndex: null
};
var unlinkButton = {
id: 'editor.link.unlink',
testId: 'editor.link.unlink',
type: 'button',
onClick: commandWithMetadata(removeLink(pos, editorAnalyticsApi), {
inputMethod: INPUT_METHOD.FLOATING_TB
}),
title: labelUnlink,
icon: LinkBrokenIcon,
tabIndex: null
};
var items = [].concat(_toConsumableArray(startingToolbarItems), _toConsumableArray(areAnyNewToolbarFlagsEnabled ? [unlinkButton, {
type: 'separator',
fullHeight: true
}, openLinkButton, {
type: 'separator',
fullHeight: true
}] : [openLinkButton, {
type: 'separator'
}, unlinkButton, {
type: 'separator'
}]), [{
type: 'copy-button',
items: [{
state: state,
formatMessage: formatMessage,
markType: state.schema.marks.link
}]
}], _toConsumableArray((_cardActions$getEndin = cardActions === null || cardActions === void 0 ? void 0 : cardActions.getEndingToolbarItems(intl, link)) !== null && _cardActions$getEndin !== void 0 ? _cardActions$getEndin : []));
return _objectSpread(_objectSpread({}, hyperLinkToolbar), {}, {
height: 32,
width: 250,
items: items,
scrollable: true
});
}
case 'EDIT_INSERTED':
case 'INSERT':
{
var _link;
if (isEditLink(activeLinkMark) && activeLinkMark.node) {
var _linkMark = activeLinkMark.node.marks.filter(function (mark) {
return mark.type === state.schema.marks.link;
});
_link = _linkMark[0] && _linkMark[0].attrs.href;
}
var displayText = isEditLink(activeLinkMark) ? getLinkText(activeLinkMark, state) : linkState.activeText;
var popupHeight = lpLinkPicker ? LINKPICKER_HEIGHT_IN_PX : RECENT_SEARCH_HEIGHT_IN_PX;
// Removing popupWidth to ensure that we the popup always positions setting positon left instead of flipping to position right
// inside of a narrow space like Preview panel
var popupWidth = !lpLinkPicker ? undefined : RECENT_SEARCH_WIDTH_IN_PX;
return _objectSpread(_objectSpread({}, hyperLinkToolbar), {}, {
preventPopupOverflow: true,
height: popupHeight,
width: popupWidth,
items: [{
type: 'custom',
fallback: [],
disableArrowNavigation: true,
render: function render(view, idx) {
if (!view) {
return null;
}
var Toolbar = /*#__PURE__*/React.createElement(HyperlinkAddToolbarWithState, {
pluginInjectionApi: pluginInjectionApi,
view: view,
key: idx,
linkPickerOptions: options === null || options === void 0 ? void 0 : options.linkPicker,
lpLinkPicker: lpLinkPicker,
displayUrl: _link,
displayText: displayText || '',
providerFactory: providerFactory
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onCancel: function onCancel() {
return view.focus();
},
onEscapeCallback: onEscapeCallback(editorCardActions),
onClickAwayCallback: onClickAwayCallback
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onSubmit: function onSubmit(href) {
var _toolbarKey$getState$, _toolbarKey$getState;
var title = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var displayText = arguments.length > 2 ? arguments[2] : undefined;
var inputMethod = arguments.length > 3 ? arguments[3] : undefined;
var analytic = arguments.length > 4 ? arguments[4] : undefined;
var isEdit = isEditLink(activeLinkMark);
var action = isEdit ? ACTION.UPDATED : ACTION.INSERTED;
var 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;
var command = isEdit ? commandWithMetadata(updateLink(href, displayText || title, activeLinkMark.pos), {
action: action,
inputMethod: inputMethod,
sourceEvent: analytic
}) : insertLinkWithAnalytics(inputMethod, activeLinkMark.from, activeLinkMark.to, href, editorCardActions, editorAnalyticsApi, title, displayText, skipAnalytics, analytic);
command(view.state, view.dispatch, view);
view.focus();
}
});
return isToolbarAIFCEnabled ? /*#__PURE__*/React.createElement(UserIntentPopupWrapper, {
api: pluginInjectionApi
}, Toolbar) : Toolbar;
}
}]
});
}
}
}
return;
};
};