UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

321 lines (320 loc) 15.1 kB
import React, { useContext, useEffect } from 'react'; import { cardMessages as messages } from '@atlaskit/editor-common/messages'; import { IconDatasourceAssetsObjects, IconDatasourceConfluenceSearch, IconDatasourceJiraIssue } from '@atlaskit/editor-common/quick-insert'; import { canRenderDatasource } from '@atlaskit/editor-common/utils'; import { ASSETS_LIST_OF_LINKS_DATASOURCE_ID, CONFLUENCE_SEARCH_DATASOURCE_ID } from '@atlaskit/link-datasource'; import { SmartCardContext } from '@atlaskit/link-provider/context'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValNoExposure } from '@atlaskit/tmp-editor-statsig/expVal'; import { blockCardSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/blockCard'; import { embedCardSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/embedCard'; import { inlineCardSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/inlineCard'; import { hideLinkToolbar, setProvider, showDatasourceModal } from './pm-plugins/actions'; import { queueCardsFromChangedTr, queueCardsFromRange } from './pm-plugins/doc'; import { cardKeymap } from './pm-plugins/keymap'; import { createPlugin } from './pm-plugins/main'; import { pluginKey } from './pm-plugins/plugin-key'; import { isDatasourceConfigEditable } from './pm-plugins/utils'; import { createEventsQueue } from './ui/analytics/create-events-queue'; import DatasourceModalWithState from './ui/DatasourceModal/ModalWithState'; import { EditorLinkingPlatformAnalytics } from './ui/EditorLinkingPlatformAnalytics'; import { EditorSmartCardEvents } from './ui/EditorSmartCardEvents'; // Ignored via go/ees005 // eslint-disable-next-line import/no-named-as-default import LayoutButton from './ui/LayoutButton'; import { getPasteDisplayAsMenuComponents } from './ui/PasteDisplayAsMenu'; import { floatingToolbar, getEndingToolbarItems, getStartingToolbarItems } from './ui/toolbar'; const PasteMenuSmartCardClientSync = ({ clientRef }) => { var _smartCardContext$con2; const smartCardContext = useContext(SmartCardContext); useEffect(() => { var _smartCardContext$con; clientRef.current = smartCardContext === null || smartCardContext === void 0 ? void 0 : (_smartCardContext$con = smartCardContext.connections) === null || _smartCardContext$con === void 0 ? void 0 : _smartCardContext$con.client; }, [clientRef, smartCardContext === null || smartCardContext === void 0 ? void 0 : (_smartCardContext$con2 = smartCardContext.connections) === null || _smartCardContext$con2 === void 0 ? void 0 : _smartCardContext$con2.client]); return null; }; export const cardPlugin = ({ config: options = {}, api }) => { var _api$base, _options$lpLinkPicker; let previousCardProvider; const cardPluginEvents = createEventsQueue(); let instanceEmbedCardTransformers = options.embedCardTransformers; let editorViewForPasteMenu; const pasteMenuSmartCardClientRef = { current: undefined }; const pasteMenuVariant = expValNoExposure('platform_editor_paste_actions_menu_v2', 'variant', 'control'); const shouldRegisterPasteDisplayAsMenu = options.enablePasteDisplayAsMenu && ['hasSpellingAndGrammar', 'hasAltAiActions'].includes(pasteMenuVariant); if (shouldRegisterPasteDisplayAsMenu) { var _api$uiControlRegistr, _options$allowBlockCa; api === null || api === void 0 ? void 0 : (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 ? void 0 : _api$uiControlRegistr.actions.register(getPasteDisplayAsMenuComponents({ api, allowBlockCards: options.onlyInlineCards ? false : (_options$allowBlockCa = options.allowBlockCards) !== null && _options$allowBlockCa !== void 0 ? _options$allowBlockCa : true, allowEmbeds: options.onlyInlineCards ? false : options.allowEmbeds, getEditorView: () => editorViewForPasteMenu, smartCardClientRef: pasteMenuSmartCardClientRef })); } api === null || api === void 0 ? void 0 : (_api$base = api.base) === null || _api$base === void 0 ? void 0 : _api$base.actions.registerMarks(({ tr, node, pos }) => { const { doc } = tr; const { schema } = doc.type; const { inlineCard: inlineCardNodeType } = schema.nodes; if (node.type === inlineCardNodeType) { const newText = node.attrs.url; const currentPos = tr.mapping.map(pos); tr.replaceWith(currentPos, currentPos + node.nodeSize, schema.text(newText, node.marks)); } }); return { name: 'card', getSharedState(editorState) { if (!editorState) { return null; } return pluginKey.getState(editorState) || null; }, nodes() { const nodes = [{ name: 'inlineCard', node: inlineCardSpecWithFixedToDOM() }]; // `onlyInlineCards` is a hard gate: when set, blockCard/embedCard // stay out of the schema regardless of allowBlockCards/allowEmbeds. if (options.onlyInlineCards) { return nodes; } nodes.push({ name: 'blockCard', node: blockCardSpecWithFixedToDOM() }); if (options.allowEmbeds) { nodes.push({ name: 'embedCard', node: embedCardSpecWithFixedToDOM() }); } return nodes; }, pmPlugins() { var _options$allowBlockCa2, _options$allowResizin, _options$useAlternati, _options$allowWrappin, _options$allowAlignme, _options$allowDatasou, _options$showUpgradeD; // onlyInlineCards forces block/embed off regardless of caller-passed flags, // keeping the schema gate (in nodes()) and the runtime gate in sync. const allowBlockCards = options.onlyInlineCards ? false : (_options$allowBlockCa2 = options.allowBlockCards) !== null && _options$allowBlockCa2 !== void 0 ? _options$allowBlockCa2 : true; const allowEmbeds = options.onlyInlineCards ? false : options.allowEmbeds; const allowResizing = (_options$allowResizin = options.allowResizing) !== null && _options$allowResizin !== void 0 ? _options$allowResizin : true; const useAlternativePreloader = (_options$useAlternati = options.useAlternativePreloader) !== null && _options$useAlternati !== void 0 ? _options$useAlternati : true; const allowWrapping = (_options$allowWrappin = options.allowWrapping) !== null && _options$allowWrappin !== void 0 ? _options$allowWrappin : true; const allowAlignment = (_options$allowAlignme = options.allowAlignment) !== null && _options$allowAlignme !== void 0 ? _options$allowAlignme : true; const allowDatasource = (_options$allowDatasou = options.allowDatasource) !== null && _options$allowDatasou !== void 0 ? _options$allowDatasou : false; const showUpgradeDiscoverability = (_options$showUpgradeD = options.showUpgradeDiscoverability) !== null && _options$showUpgradeD !== void 0 ? _options$showUpgradeD : true; const plugins = [{ name: 'card', plugin: createPlugin({ ...options, allowBlockCards, allowEmbeds, allowResizing, useAlternativePreloader, allowWrapping, allowAlignment, allowDatasource, cardPluginEvents, embedCardTransformers: instanceEmbedCardTransformers, showUpgradeDiscoverability }, api) }]; plugins.push({ name: 'cardKeymap', plugin: ({ featureFlags }) => { return cardKeymap(featureFlags); } }); return plugins; }, contentComponent({ editorView, popupsMountPoint, popupsScrollableElement, popupsBoundariesElement }) { if (!editorView) { return null; } editorViewForPasteMenu = editorView; const breakoutEnabled = options.editorAppearance === 'full-page'; return /*#__PURE__*/React.createElement(React.Fragment, null, shouldRegisterPasteDisplayAsMenu && /*#__PURE__*/React.createElement(PasteMenuSmartCardClientSync, { clientRef: pasteMenuSmartCardClientRef }), /*#__PURE__*/React.createElement(EditorSmartCardEvents, { editorView: editorView }), /*#__PURE__*/React.createElement(EditorLinkingPlatformAnalytics, { cardPluginEvents: cardPluginEvents, editorView: editorView }), breakoutEnabled && /*#__PURE__*/React.createElement(LayoutButton, { api: api, editorView: editorView, mountPoint: popupsMountPoint, scrollableElement: popupsScrollableElement, boundariesElement: popupsBoundariesElement }), /*#__PURE__*/React.createElement(DatasourceModalWithState, { api: api, editorView: editorView })); }, commands: { queueCardsFromRange: (from, to, source, analyticsAction, normalizeLinkText, sourceEvent, appearance) => ({ tr }) => queueCardsFromRange( // Synthesise the minimal EditorState shape the impl actually reads. { schema: tr.doc.type.schema, selection: tr.selection }, tr, from, to, source, analyticsAction, normalizeLinkText, sourceEvent, appearance) }, actions: { setProvider: async providerPromise => { var _api$core$actions$exe; const provider = await providerPromise; // Prevent someone trying to set the exact same provider twice for performance reasons if (previousCardProvider === provider || (options === null || options === void 0 ? void 0 : options.provider) === providerPromise) { return false; } previousCardProvider = provider; return (_api$core$actions$exe = api === null || api === void 0 ? void 0 : api.core.actions.execute(({ tr }) => setProvider(provider)(tr))) !== null && _api$core$actions$exe !== void 0 ? _api$core$actions$exe : false; }, hideLinkToolbar, queueCardsFromChangedTr, queueCardsFromRange, registerEmbedCardTransformer: transformers => { instanceEmbedCardTransformers = transformers; }, resolveShortLinkUrl: async url => { if (!options.provider) { return undefined; } try { const provider = await options.provider; // EditorCardProvider holds a CardClient instance internally. We access it // directly to call fetchData, which gives us the raw ORS JSON-LD response // including the canonical url field — something provider.resolve() does not // surface. This is intentionally a structural cast rather than a typed // dependency on EditorCardProvider's private API. const clientHolder = provider; const client = clientHolder.cardClient; if (!client) { return undefined; } const response = await client.fetchData(url); // ORS returns a JsonLd.Response whose data union includes BaseData, // BaseCollectionData, and BaseCollectionPage. We only care about the // `url` field which is present on BaseData (Primitives.Object). Cast // to a minimal shape rather than accessing through the full union type. const data = response === null || response === void 0 ? void 0 : response.data; if (!data) { return undefined; } const urlField = data.url; // Property<string | Link> can be a string, a Link object, or an array. // Short-link responses always return a plain string URL. if (typeof urlField === 'string' && urlField !== url) { return urlField; } // Fallback: Link object shape with href or @id if (urlField && typeof urlField === 'object' && !Array.isArray(urlField)) { var _linkObj$Id; const linkObj = urlField; const id = (_linkObj$Id = linkObj['@id']) !== null && _linkObj$Id !== void 0 ? _linkObj$Id : linkObj['href']; if (typeof id === 'string' && id !== url) { return id; } } } catch { // Swallow errors — callers handle undefined } return undefined; }, getStartingToolbarItems: getStartingToolbarItems(options, api), getEndingToolbarItems: getEndingToolbarItems(options, api) }, pluginsOptions: { floatingToolbar: floatingToolbar(options, (_options$lpLinkPicker = options.lpLinkPicker) !== null && _options$lpLinkPicker !== void 0 ? _options$lpLinkPicker : false, options.linkPicker, api, options.disableFloatingToolbar), quickInsert: ({ formatMessage }) => { const quickInsertArray = []; if (!options.allowDatasource) { return quickInsertArray; } quickInsertArray.push({ id: 'datasource', title: formatMessage(fg('confluence-issue-terminology-refresh') ? messages.datasourceJiraIssueIssueTermRefresh : messages.datasourceJiraIssue), description: formatMessage(fg('confluence-issue-terminology-refresh') ? messages.datasourceJiraIssueDescriptionIssueTermRefresh : messages.datasourceJiraIssueDescription), isDisabledOffline: true, categories: ['external-content', 'development'], keywords: ['jira'], featured: true, ...(fg('jim-lower-ranking-in-jira-macro-search') && { priority: 500 }), icon: () => /*#__PURE__*/React.createElement(IconDatasourceJiraIssue, null), action(insert) { const tr = insert(undefined); showDatasourceModal('jira')(tr); return tr; } }); if (canRenderDatasource(ASSETS_LIST_OF_LINKS_DATASOURCE_ID)) { quickInsertArray.push({ id: 'datasource', title: formatMessage(messages.datasourceAssetsObjectsGeneralAvailability), description: formatMessage(messages.datasourceAssetsObjectsDescription), isDisabledOffline: true, categories: ['external-content', 'development'], keywords: ['assets'], icon: () => /*#__PURE__*/React.createElement(IconDatasourceAssetsObjects, null), action(insert) { const tr = insert(undefined); showDatasourceModal('assets')(tr); return tr; } }); } if (isDatasourceConfigEditable(CONFLUENCE_SEARCH_DATASOURCE_ID)) { quickInsertArray.push({ id: 'datasource', title: formatMessage(messages.datasourceConfluenceSearch), description: formatMessage(messages.datasourceConfluenceSearchDescription), isDisabledOffline: true, categories: ['external-content', 'development'], keywords: ['confluence'], featured: true, icon: () => /*#__PURE__*/React.createElement(IconDatasourceConfluenceSearch, null), action(insert) { const tr = insert(undefined); showDatasourceModal('confluence-search')(tr); return tr; } }); } return quickInsertArray; } } }; };