UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

607 lines (595 loc) 25.1 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; 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 isEqual from 'lodash/isEqual'; import { isSafeUrl } from '@atlaskit/adf-schema'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, SMART_LINK_TYPE, unlinkPayload } from '@atlaskit/editor-common/analytics'; import { addLinkMetadata } from '@atlaskit/editor-common/card'; import { getActiveLinkMark } from '@atlaskit/editor-common/link'; import { getAnnotationMarksForPos, getLinkCreationAnalyticsEvent, isFromCurrentDomain, nodesBetweenChanged, processRawValue } from '@atlaskit/editor-common/utils'; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { closeHistory } from '@atlaskit/prosemirror-history'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { hideDatasourceModal, queueCards, removeDatasourceStash, resolveCard, setDatasourceStash } from './actions'; import { pluginKey } from './plugin-key'; import { shouldReplaceLink } from './shouldReplaceLink'; import { appearanceForNodeType, isDatasourceConfigEditable, isDatasourceNode, selectedCardAppearance } from './utils'; /** * Attempt to replace the link into the respective card. */ function replaceLinksToCards(tr, cardAdf, schema, request) { var inlineCard = schema.nodes.inlineCard; var url = request.url; if (!isSafeUrl(url)) { return; } // replace all the outstanding links with their cards var pos = tr.mapping.map(request.pos); var $pos = tr.doc.resolve(pos); var $head = tr.selection.$head; var node = tr.doc.nodeAt(pos); if (!node || !node.type.isText) { return; } var replaceLink = request.shouldReplaceLink || shouldReplaceLink(node, request.compareLinkText, url); if (!replaceLink) { return; } // ED-5638: add an extra space after inline cards to avoid re-rendering them var nodes = [cardAdf]; if (cardAdf.type === inlineCard) { nodes.push(schema.text(' ')); } tr.replaceWith(pos, pos + (node.text || url).length, nodes); var annotationMarksForPos = getAnnotationMarksForPos($head); if (annotationMarksForPos && annotationMarksForPos.length > 0) { annotationMarksForPos.forEach(function (annotationMark) { // Add the annotation mark on to the inlineCard node and the trailing space node. tr.addMark(pos, pos + nodes[0].nodeSize + nodes[1].nodeSize, annotationMark); }); } return $pos.node($pos.depth - 1).type.name; } export var replaceQueuedUrlWithCard = function replaceQueuedUrlWithCard(url, cardData, analyticsAction, editorAnalyticsApi, createAnalyticsEvent, embedCardNodeTransformer) { return function (editorState, dispatch) { var state = pluginKey.getState(editorState); if (!state) { return false; } // find the requests for this URL var requests = state.requests.filter(function (req) { return req.url === url; }); // try to transform response to ADF var schema = editorState.schema; var cardAdf = null; // If an embed card transformer is provided and the resolved card is an embedCard, // attempt to transform it into an alternative node representation first. if (cardData.type === 'embedCard' && embedCardNodeTransformer) { var _embedCardNodeTransfo; cardAdf = (_embedCardNodeTransfo = embedCardNodeTransformer(schema, cardData.attrs)) !== null && _embedCardNodeTransfo !== void 0 ? _embedCardNodeTransfo : null; } if (!cardAdf) { var _processRawValue; cardAdf = (_processRawValue = processRawValue(schema, cardData)) !== null && _processRawValue !== void 0 ? _processRawValue : null; } var tr = editorState.tr; if (cardAdf) { // Should prevent any other node than cards? [inlineCard, blockCard].includes(cardAdf.type) var nodeContexts = requests.map(function (request) { return replaceLinksToCards(tr, cardAdf, schema, request); }).filter(function (context) { return !!context; }); // context exist // Send analytics information if (nodeContexts.length) { var nodeContext = nodeContexts.every(function (context) { return context === nodeContexts[0]; }) ? nodeContexts[0] : 'mixed'; /** For block links v1, default to inline links */ var nodeType = 'inlineCard'; var _url$split = url.split('/'), _url$split2 = _slicedToArray(_url$split, 3), domainName = _url$split2[2]; if (state.smartLinkEvents) { state.smartLinkEvents.insertSmartLink(domainName, 'inline', createAnalyticsEvent); } /** * TODO: * What if each request has a different source? * Unlikely, but need to define behaviour. * Ignore analytics event? take first? provide 'mixed' as well? */ var inputMethod = requests[0].source; var sourceEvent = requests[0].sourceEvent; editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({ // eslint-disable-next-line @typescript-eslint/no-explicit-any action: analyticsAction || ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.SMART_LINK, eventType: EVENT_TYPE.TRACK, attributes: { inputMethod: inputMethod, nodeType: nodeType, nodeContext: nodeContext, fromCurrentDomain: isFromCurrentDomain(url) }, nonPrivacySafeAttributes: { domainName: domainName } })(tr); addLinkMetadata(editorState.selection, tr, { action: analyticsAction, inputMethod: inputMethod, cardAction: 'RESOLVE', sourceEvent: sourceEvent }); } } if (dispatch) { dispatch(resolveCard(url)(closeHistory(tr))); } return true; }; }; export var handleFallbackWithAnalytics = function handleFallbackWithAnalytics(request, editorAnalyticsApi) { return function (state, dispatch) { var cardState = pluginKey.getState(state); if (!cardState) { return false; } var tr = state.tr; if (request.source !== INPUT_METHOD.FLOATING_TB) { editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(request.source, request.url))(tr); } addLinkMetadata(state.selection, tr, { action: request.analyticsAction, inputMethod: request.source, sourceEvent: request.sourceEvent }); if (dispatch) { dispatch(resolveCard(request.url)(tr)); } return true; }; }; export var queueCardsFromChangedTr = function queueCardsFromChangedTr(state, tr, source, analyticsAction) { var normalizeLinkText = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; var sourceEvent = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : undefined; var appearance = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : 'inline'; var schema = state.schema; var link = schema.marks.link; var requests = []; nodesBetweenChanged(tr, function (node, pos) { if (!node.isText) { return true; } var linkMark = node.marks.find(function (mark) { return mark.type === link; }); if (linkMark) { if (!shouldReplaceLink(node, normalizeLinkText)) { return false; } requests.push({ url: linkMark.attrs.href, pos: pos, appearance: appearance, compareLinkText: normalizeLinkText, source: source, analyticsAction: analyticsAction, sourceEvent: sourceEvent }); } return false; }); if (analyticsAction) { addLinkMetadata(state.selection, tr, { action: analyticsAction }); } return queueCards(requests)(tr); }; export var queueCardFromChangedTr = function queueCardFromChangedTr(state, tr, source, analyticsAction) { var normalizeLinkText = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; var sourceEvent = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : undefined; var previousAppearance = arguments.length > 6 ? arguments[6] : undefined; var schema = state.schema; var link = schema.marks.link; var requests = []; nodesBetweenChanged(tr, function (node, pos) { if (!node.isText) { return true; } var linkMark = node.marks.find(function (mark) { return mark.type === link; }); if (linkMark) { if (!shouldReplaceLink(node, normalizeLinkText)) { return false; } requests.push({ url: linkMark.attrs.href, pos: pos, appearance: 'inline', previousAppearance: previousAppearance, compareLinkText: normalizeLinkText, source: source, analyticsAction: analyticsAction, sourceEvent: sourceEvent }); } return false; }); addLinkMetadata(state.selection, tr, { action: analyticsAction }); return queueCards(requests)(tr); }; export var convertHyperlinkToSmartCard = function convertHyperlinkToSmartCard(state, source, appearance) { var normalizeLinkText = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; var schema = state.schema; var link = schema.marks.link; var requests = []; var createRequest = function createRequest(linkMark, pos) { return { url: linkMark.attrs.href, pos: pos, appearance: appearance, previousAppearance: 'url', compareLinkText: normalizeLinkText, source: source, analyticsAction: ACTION.CHANGED_TYPE, shouldReplaceLink: true }; }; if (editorExperiment('platform_editor_controls', 'variant1')) { var activeLinkMark = getActiveLinkMark(state); if (activeLinkMark) { var linkMark = activeLinkMark.node.marks.find(function (mark) { return mark.type === link; }); if (linkMark) { requests.push(createRequest(linkMark, activeLinkMark.pos)); } } } else { state.tr.doc.nodesBetween(state.selection.from, state.selection.to, function (node, pos) { var linkMark = node.marks.find(function (mark) { return mark.type === link; }); if (linkMark) { requests.push(createRequest(linkMark, pos)); } }); } addLinkMetadata(state.selection, state.tr, { action: ACTION.CHANGED_TYPE }); return queueCards(requests)(state.tr); }; export var changeSelectedCardToLink = function changeSelectedCardToLink(text, href, sendAnalytics, node, pos, editorAnalyticsApi) { return function (state, dispatch) { var selectedNode = state.selection instanceof NodeSelection ? state.selection.node : undefined; var tr; if (node && pos) { tr = cardNodeToLinkWithTransaction(state, text, href, node, pos); } else { tr = cardToLinkWithTransaction(state, text, href); } updateDatasourceStash(tr, selectedNode); if (sendAnalytics) { if (selectedNode) { editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({ action: ACTION.CHANGED_TYPE, actionSubject: ACTION_SUBJECT.SMART_LINK, eventType: EVENT_TYPE.TRACK, attributes: { newType: SMART_LINK_TYPE.URL, previousType: appearanceForNodeType(selectedNode.type) } })(tr); } } if (dispatch) { dispatch(tr.scrollIntoView()); } return true; }; }; export var changeSelectedCardToLinkFallback = function changeSelectedCardToLinkFallback(text, href, sendAnalytics, node, pos, editorAnalyticsApi) { return function (state, dispatch) { var tr; if (node && pos) { tr = cardNodeToLinkWithTransaction(state, text, href, node, pos); } else { tr = cardToLinkWithTransaction(state, text, href); } if (sendAnalytics) { editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({ action: ACTION.ERRORED, actionSubject: ACTION_SUBJECT.SMART_LINK, eventType: EVENT_TYPE.OPERATIONAL, attributes: { error: 'Smart card falling back to link.' } })(tr); } if (dispatch) { dispatch(tr.setMeta('addToHistory', false)); } return true; }; }; export var updateCard = function updateCard(href, sourceEvent) { return function (state, dispatch) { var selectedNode = state.selection instanceof NodeSelection && state.selection.node; if (!selectedNode) { return false; } var cardAppearance = selectedCardAppearance(state); var tr = cardToLinkWithTransaction(state, href, href); queueCardFromChangedTr(state, tr, INPUT_METHOD.MANUAL, ACTION.UPDATED, undefined, sourceEvent, cardAppearance); if (dispatch) { dispatch(tr.scrollIntoView()); } return true; }; }; function cardToLinkWithTransaction(state, text, href) { var selectedNode = state.selection instanceof NodeSelection && state.selection.node; if (!selectedNode) { return state.tr; } var link = state.schema.marks.link; var url = selectedNode.attrs.url || selectedNode.attrs.data.url; var tr = state.tr.replaceSelectionWith(state.schema.text(text || url, [link.create({ href: href || url })]), false); return tr; } function cardNodeToLinkWithTransaction(state, text, href, node, pos) { var link = state.schema.marks.link; var url = node.attrs.url || node.attrs.data.url; return state.tr.replaceWith(pos, pos + node.nodeSize, state.schema.text(text || url, [link.create({ href: href || url })])); } export var changeSelectedCardToText = function changeSelectedCardToText(text, editorAnalyticsApi) { return function (state, dispatch) { var selectedNode = state.selection instanceof NodeSelection && state.selection.node; if (!selectedNode) { return false; } var tr = state.tr.replaceSelectionWith(state.schema.text(text), false); if (dispatch) { addLinkMetadata(state.selection, tr, { action: ACTION.UNLINK }); tr.scrollIntoView(); editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent(unlinkPayload(ACTION_SUBJECT_ID.CARD_INLINE))(tr); dispatch(tr); } return true; }; }; export var setSelectedCardAppearance = function setSelectedCardAppearance(appearance, editorAnalyticsApi) { return function (state, dispatch) { var _selectedNode$attrs$d, _previousNode$type; var selectedNode = state.selection instanceof NodeSelection ? state.selection.node : undefined; if (!selectedNode) { // When there is no selected node, we insert a new one // and replace the existing blue link var _tr = convertHyperlinkToSmartCard(state, INPUT_METHOD.FLOATING_TB, appearance); if (dispatch) { addLinkMetadata(state.selection, _tr, { action: ACTION.CHANGED_TYPE }); dispatch(_tr.scrollIntoView()); } return false; } if (appearanceForNodeType(selectedNode.type) === appearance && !selectedNode.attrs.datasource) { return false; } var attrs = editorExperiment('platform_synced_block', true) ? getAttrsForAppearance(appearance, selectedNode, state.selection.$from.parent.type.name === 'bodiedSyncBlock') : getAttrsForAppearance(appearance, selectedNode); var _state$selection = state.selection, from = _state$selection.from, to = _state$selection.to; var nodeType = getLinkNodeType(appearance, state.schema.nodes); var tr = state.tr.setNodeMarkup(from, nodeType, attrs, selectedNode.marks); // If switching to embed appearance, attempt to use a registered transform command // to create an alternative node representation (e.g. a native embed). if (appearance === 'embed' && (selectedNode.attrs.url || (_selectedNode$attrs$d = selectedNode.attrs.data) !== null && _selectedNode$attrs$d !== void 0 && _selectedNode$attrs$d.url)) { var _cardState$embedCardT; var cardState = pluginKey.getState(state); var createEmbedCardTransformCommand = cardState === null || cardState === void 0 || (_cardState$embedCardT = cardState.embedCardTransformers) === null || _cardState$embedCardT === void 0 ? void 0 : _cardState$embedCardT.createEmbedCardTransformCommand; if (createEmbedCardTransformCommand) { var transformCommand = createEmbedCardTransformCommand({ editorAnalyticsApi: editorAnalyticsApi, augmentTransaction: function augmentTransaction(augmentTr) { updateDatasourceStash(augmentTr, selectedNode); editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({ action: ACTION.CHANGED_TYPE, actionSubject: ACTION_SUBJECT.SMART_LINK, eventType: EVENT_TYPE.TRACK, attributes: { newType: appearance, previousType: appearanceForNodeType(selectedNode.type) } })(augmentTr); addLinkMetadata(state.selection, augmentTr, { action: ACTION.CHANGED_TYPE }); } }); if (transformCommand(state, dispatch)) { return true; } } } updateDatasourceStash(tr, selectedNode); // When the selected card is the last element in the doc we add a new paragraph after it for consistent replacement if (tr.doc.nodeSize - 2 === to) { tr.insertText(' ', to); } tr.setSelection(TextSelection.create(tr.doc, to + 1)); var previousNodePos = from - 1 > 0 ? from - 1 : 0; var previousNode = tr.doc.nodeAt(previousNodePos); if ((previousNode === null || previousNode === void 0 || (_previousNode$type = previousNode.type) === null || _previousNode$type === void 0 ? void 0 : _previousNode$type.name) === 'paragraph') { tr.delete(previousNodePos, from); } editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({ action: ACTION.CHANGED_TYPE, actionSubject: ACTION_SUBJECT.SMART_LINK, eventType: EVENT_TYPE.TRACK, attributes: { newType: appearance, previousType: appearanceForNodeType(selectedNode.type) } })(tr); addLinkMetadata(state.selection, tr, { action: ACTION.CHANGED_TYPE }); if (dispatch) { dispatch(tr.scrollIntoView()); } return true; }; }; export var getLinkNodeType = function getLinkNodeType(appearance, linkNodes) { switch (appearance) { case 'inline': return linkNodes.inlineCard; case 'block': return linkNodes.blockCard; case 'embed': return linkNodes.embedCard; } }; // Apply an update made from a datasource ui interaction export var updateCardViaDatasource = function updateCardViaDatasource(args) { var state = args.state, node = args.node, newAdf = args.newAdf, view = args.view, sourceEvent = args.sourceEvent, isDeletingConfig = args.isDeletingConfig, inputMethod = args.inputMethod; var tr = state.tr, _state$selection2 = state.selection, from = _state$selection2.from, to = _state$selection2.to, schemaNodes = state.schema.nodes; if (newAdf.type === 'blockCard') { var _node$attrs, _newAdf$attrs; if ((_node$attrs = node.attrs) !== null && _node$attrs !== void 0 && _node$attrs.datasource && (_newAdf$attrs = newAdf.attrs) !== null && _newAdf$attrs !== void 0 && _newAdf$attrs.datasource) { var _ref3, _ref6, _oldViews$properties, _newViews$properties; var newAttrs = newAdf.attrs; var oldAttrs = node.attrs; var _ref = (_ref3 = newAttrs.datasource.views) !== null && _ref3 !== void 0 ? _ref3 : [], _ref2 = _slicedToArray(_ref, 1), newViews = _ref2[0]; var _ref4 = (_ref6 = oldAttrs.datasource.views) !== null && _ref6 !== void 0 ? _ref6 : [], _ref5 = _slicedToArray(_ref4, 1), oldViews = _ref5[0]; var isColumnChange = !isEqual(oldViews === null || oldViews === void 0 || (_oldViews$properties = oldViews.properties) === null || _oldViews$properties === void 0 ? void 0 : _oldViews$properties.columns, newViews === null || newViews === void 0 || (_newViews$properties = newViews.properties) === null || _newViews$properties === void 0 ? void 0 : _newViews$properties.columns); var isUrlChange = newAttrs.url !== oldAttrs.url; if (isColumnChange || isUrlChange) { tr.setNodeMarkup(from, schemaNodes.blockCard, _objectSpread(_objectSpread({}, oldAttrs), newAdf.attrs)); } } else if (node.type.isText) { // url to datasource var link; state.doc.nodesBetween(from, to, function (node, pos) { // get the actual start position of a link within the node var linkMark = node.marks.find(function (mark) { return mark.type === state.schema.marks.link; }); if (linkMark) { link = { url: linkMark.attrs.href, text: node.text, pos: pos }; return false; } return true; }); if (link) { var newNode = schemaNodes.blockCard.createChecked(newAdf.attrs); tr.replaceWith(link.pos, link.pos + (link.text || link.url).length, [newNode]); } } else { // inline or blockCard to datasource tr.setNodeMarkup(from, schemaNodes.blockCard, newAdf.attrs); } } else if (newAdf.type === 'inlineCard') { // card type to inlineCard tr.setNodeMarkup(from, schemaNodes.inlineCard, newAdf.attrs); } addLinkMetadata(state.selection, tr, { action: ACTION.UPDATED, sourceEvent: sourceEvent, inputMethod: inputMethod }); if (isDeletingConfig) { if (typeof node.attrs.url === 'string') { removeDatasourceStash(tr, node.attrs.url); } } else { hideDatasourceModal(tr); } view.dispatch(tr.scrollIntoView()); }; export var insertDatasource = function insertDatasource(state, adf, view, sourceEvent) { var tr = state.tr, from = state.selection.from, schemaNodes = state.schema.nodes; var attrs = adf.attrs, type = adf.type; var schemaNode = type === 'inlineCard' ? schemaNodes.inlineCard : schemaNodes.blockCard; var newNode = schemaNode.createChecked(attrs); // in future, if we decide to do datasource insertion from the main toolbar, we should probably consider editor-plugin-content-insertion instead of tr.insert // this will allow us to deal with insertions from multiple paths in a more consistent way newNode && tr.insert(from, newNode); hideDatasourceModal(tr); addLinkMetadata(state.selection, tr, { action: ACTION.INSERTED, sourceEvent: sourceEvent }); view.dispatch(tr.scrollIntoView()); }; /** * Get attributes for new Card Appearance */ export var getAttrsForAppearance = function getAttrsForAppearance(appearance, selectedNode) { var isInsideBodiedSyncBlock = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; if (appearance === 'embed') { var _selectedNode$attrs$w; return _objectSpread(_objectSpread({}, selectedNode.attrs), {}, { layout: 'center' }, isInsideBodiedSyncBlock ? // When converting to embed, width attribute is set to null and when the document is published, the width attribute is set to 100 as per schema default // For editor, width is not required to render the embed card, but it's required in renderer // Because sync block has nested renderer in editor, we need width to be defined even in editor so embed in reference sync block can be rendered properly { width: (_selectedNode$attrs$w = selectedNode.attrs.width) !== null && _selectedNode$attrs$w !== void 0 ? _selectedNode$attrs$w : 100 } : {}); } if (isDatasourceNode(selectedNode)) { return { url: selectedNode.attrs.url }; } return selectedNode.attrs; }; var updateDatasourceStash = function updateDatasourceStash(tr, selectedNode) { if (isDatasourceNode(selectedNode) && !isDatasourceConfigEditable(selectedNode.attrs.datasource.id) && selectedNode.attrs.url) { setDatasourceStash(tr, { url: selectedNode.attrs.url, views: selectedNode.attrs.datasource.views }); } };