UNPKG

@atlaskit/editor-plugin-hyperlink

Version:

Hyperlink plugin for @atlaskit/editor-core

307 lines (303 loc) 17.8 kB
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; }; };