UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

398 lines (384 loc) 14.3 kB
import _typeof from "@babel/runtime/helpers/typeof"; import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { LinkMetaStep } from '@atlaskit/adf-schema/steps'; import { TableSortStep } from '@atlaskit/custom-steps'; import { ACTION } from '@atlaskit/editor-common/analytics'; import { getLinkMetadataFromTransaction } from '@atlaskit/editor-common/card'; import { isLinkMark, pmHistoryPluginKey } from '@atlaskit/editor-common/utils'; import { AddMarkStep, RemoveMarkStep } from '@atlaskit/editor-prosemirror/transform'; import { pluginKey } from '../../pm-plugins/plugin-key'; import { getPluginState } from '../../pm-plugins/util/state'; import { EVENT, EVENT_SUBJECT } from './types'; import { appearanceForLink, areSameNodes, findAtPositions, findInNodeRange, getNodeContext, getNodeSubject } from './utils'; /** * Find the links, smartLinks, datasources that were changed in a transaction */ export var findChanged = function findChanged(tr, state) { var schema = tr.doc.type.schema; var removed = []; var inserted = []; /** * Ideally we have the "before" and "after" states of the "entity" * being updated, but if the update is via a "queue for upgrade" * then we no longer have access to the "before" state, because the "before" * state was replaced with a blue link to be upgraded */ var updated = []; var queuedForUpgrade = isTransactionQueuedForUpgrade(tr); var isResolveReplace = isTransactionResolveReplace(tr); var isAutoConvert = isAutoConvertTr(tr); // History var historyMeta = tr.getMeta(pmHistoryPluginKey); var isUndo = isHistoryMeta(historyMeta) && historyMeta.redo === false; var isRedo = isHistoryMeta(historyMeta) && historyMeta.redo === true; var isUpdate = isUpdateTr(tr, isUndo || isRedo); var _loop = function _loop(i) { var _tr$docs$i, _tr$docs; var step = tr.steps[i]; var stepMap = step.getMap(); var removedInStep = []; var insertedInStep = []; var before = (_tr$docs$i = tr.docs[i]) !== null && _tr$docs$i !== void 0 ? _tr$docs$i : tr.before; var after = (_tr$docs = tr.docs[i + 1]) !== null && _tr$docs !== void 0 ? _tr$docs : tr.doc; /** * AddMarkStep and RemoveMarkSteps don't produce stepMap ranges * because there are no "changed tokens" only marks added/removed * So have to check these manually */ if (step instanceof AddMarkStep) { var addMarkStep = step; if (isLinkMark(addMarkStep.mark, schema)) { var node = after.nodeAt(addMarkStep.from); if (node) { /** * For url text pasted on plain text */ insertedInStep.push({ pos: addMarkStep.from, node: node, nodeContext: getNodeContext(after, addMarkStep.from) }); } } } if (step instanceof RemoveMarkStep) { var removeMarkStep = step; if (isLinkMark(removeMarkStep.mark, schema)) { var _node = before.nodeAt(removeMarkStep.from); if (_node) { removedInStep.push({ pos: removeMarkStep.from, node: _node, nodeContext: getNodeContext(before, removeMarkStep.from) }); } } } stepMap.forEach(function (oldStart, oldEnd, newStart, newEnd) { var _tr$docs2; var before = tr.docs[i]; var after = (_tr$docs2 = tr.docs[i + 1]) !== null && _tr$docs2 !== void 0 ? _tr$docs2 : tr.doc; var removedInRange = []; var insertedInRange = []; // Removed removedInRange.push.apply(removedInRange, _toConsumableArray(findInNodeRange(before, oldStart, oldEnd, function (node) { return !!getNodeSubject(node); }))); // Inserted insertedInRange.push.apply(insertedInRange, _toConsumableArray(findInNodeRange(after, newStart, newEnd, function (node) { return !!getNodeSubject(node); }))); removedInStep.push.apply(removedInStep, removedInRange); insertedInStep.push.apply(insertedInStep, insertedInRange); }); var omitRequestsForUpgrade = function omitRequestsForUpgrade(links) { if (!queuedForUpgrade) { return links; } /** * Skip/filter out links that have been queued, they will be tracked later */ var queuedPositions = getQueuedPositions(tr); return links.filter(function (link) { return !queuedPositions.includes(link.pos); }); }; /** * Skip "deletions" when the transaction is relating to * replacing links queued for upgrade to cards, * because the "deleted" link has not actually been * tracked as "created" yet. * Also skip when the transaction is an auto-convert * (e.g. a pasted link being converted to a native embed extension), * because the link is being converted, not deleted by the user. */ if (!isResolveReplace && !isAutoConvert) { removed.push.apply(removed, removedInStep); } inserted.push.apply(inserted, _toConsumableArray(omitRequestsForUpgrade(insertedInStep))); }; for (var i = 0; i < tr.steps.length; i++) { _loop(i); } /** * If there are no links changed but the transaction is a "resolve" action * Then this means we have resolved a link but it has failed to upgrade * We should track all resolved links as now being created */ if (inserted.length === 0 && isResolveReplace) { var positions = getResolvePositions(tr, state); inserted.push.apply(inserted, _toConsumableArray(findAtPositions(tr, positions))); } if (!isUpdate) { var _getLinkMetadataFromT = getLinkMetadataFromTransaction(tr), inputMethod = _getLinkMetadataFromT.inputMethod; /** * If there is no identifiable input method, and the links inserted and removed appear to be the same, * then this transaction likely is not intended to be considered to be the insertion and removal of links */ if (!inputMethod && areSameNodes(removed, inserted)) { return { removed: [], inserted: [], updated: updated }; } return { removed: removed, inserted: inserted, updated: updated }; } var updateInserted = []; var updateRemoved = []; for (var _i = 0; _i < inserted.length; _i++) { if (isResolveReplace) { var newLink = inserted[_i]; // what is the 2nd argument 'assoc = -1' doing here exactly? var mappedPos = tr.mapping.map(newLink.pos, -1); var previousDisplay = getResolveLinkPrevDisplay(state, mappedPos); updated.push({ inserted: inserted[_i], previous: { display: previousDisplay } }); continue; } if (inserted.length === removed.length) { var previousSubject = getNodeSubject(removed[_i].node); var currentSubject = getNodeSubject(inserted[_i].node); if (isDatasourceUpgrade(previousSubject, currentSubject) || isDatasourceDowngrade(previousSubject, currentSubject)) { updateInserted.push(inserted[_i]); updateRemoved.push(removed[_i]); } else { updated.push({ removed: removed[_i], inserted: inserted[_i] }); } } } return { inserted: updateInserted, removed: updateRemoved, updated: updated }; }; /** * List of actions to be considered link "updates" */ var UPDATE_ACTIONS = [ACTION.CHANGED_TYPE, ACTION.UPDATED]; /** * Returns true if the transaction has LinkMetaSteps that indicate the transaction is * intended to be perceived as an update to links, rather than insertion+deletion */ var isUpdateTr = function isUpdateTr(tr, isUndoOrRedo) { return !!tr.steps.find(function (step) { if (!(step instanceof LinkMetaStep)) { return false; } var _step$getMetadata = step.getMetadata(), action = _step$getMetadata.action, cardAction = _step$getMetadata.cardAction; /** * Undo of a resolve step should be considered an update * because the user is choosing to update the url back to the un-upgraded display */ if (cardAction === 'RESOLVE' && isUndoOrRedo) { return true; } if (!action) { return false; } return UPDATE_ACTIONS.includes(action); }); }; var hasType = function hasType(pluginMeta) { return _typeof(pluginMeta) === 'object' && pluginMeta !== null && 'type' in pluginMeta; }; var isTransactionQueuedForUpgrade = function isTransactionQueuedForUpgrade(tr) { var pluginMeta = tr.getMeta(pluginKey); return isMetadataQueue(pluginMeta); }; var isMetadataQueue = function isMetadataQueue(metaData) { return hasType(metaData) && metaData.type === 'QUEUE'; }; var isTransactionResolveReplace = function isTransactionResolveReplace(tr) { var pluginMeta = tr.getMeta(pluginKey); return isMetadataResolve(pluginMeta); }; /** * Checks if the transaction is an auto-convert action * (e.g. a pasted link being converted to a native embed extension node). * In this case the link removal should not be tracked as a deletion. */ var isAutoConvertTr = function isAutoConvertTr(tr) { return !!tr.steps.find(function (step) { if (!(step instanceof LinkMetaStep)) { return false; } return step.getMetadata().cardAction === 'AUTO_CONVERT'; }); }; var isMetadataResolve = function isMetadataResolve(metaData) { return hasType(metaData) && metaData.type === 'RESOLVE'; }; var isHistoryMeta = function isHistoryMeta(meta) { return _typeof(meta) === 'object' && meta !== null && 'redo' in meta; }; var getQueuedPositions = function getQueuedPositions(tr) { var pluginMeta = tr.getMeta(pluginKey); if (!isMetadataQueue(pluginMeta)) { return []; } return pluginMeta.requests.map(function (_ref) { var pos = _ref.pos; return pos; }); }; var getResolvePositions = function getResolvePositions(tr, state) { var cardState = getPluginState(state); if (!cardState) { return []; } var pluginMeta = tr.getMeta(pluginKey); if (!isMetadataResolve(pluginMeta)) { return []; } return cardState.requests.filter(function (request) { return request.url === pluginMeta.url; }).map(function (request) { return request.pos; }); }; var getResolveLinkPrevDisplay = function getResolveLinkPrevDisplay(state, pos) { var _cardState$requests$f; var cardState = getPluginState(state); if (!cardState) { return undefined; } return (_cardState$requests$f = cardState.requests.find(function (request) { return request.pos === pos; })) === null || _cardState$requests$f === void 0 ? void 0 : _cardState$requests$f.previousAppearance; }; var isDatasourceDowngrade = function isDatasourceDowngrade(previousSubject, currentSubject) { return previousSubject === EVENT_SUBJECT.DATASOURCE && currentSubject === EVENT_SUBJECT.LINK; }; var isDatasourceUpgrade = function isDatasourceUpgrade(previousSubject, currentSubject) { return previousSubject === EVENT_SUBJECT.LINK && currentSubject === EVENT_SUBJECT.DATASOURCE; }; export function eventsFromTransaction(tr, state) { var events = []; try { /** * Skip transactions sent by collab (identified by 'isRemote' key) * Skip entire document replace steps * We are only concerned with transactions performed on the document directly by the user */ var isRemote = tr.getMeta('isRemote'); var isReplaceDocument = tr.getMeta('replaceDocument'); var isTableSort = tr.steps.find(function (step) { return step instanceof TableSortStep; }); if (isRemote || isReplaceDocument || isTableSort) { return events; } var historyMeta = tr.getMeta(pmHistoryPluginKey); var isUndo = isHistoryMeta(historyMeta) && historyMeta.redo === false; var isRedo = isHistoryMeta(historyMeta) && historyMeta.redo === true; /** * Retrieve metadata from the LinkMetaStep(s) in the transaction */ var _getLinkMetadataFromT2 = getLinkMetadataFromTransaction(tr), action = _getLinkMetadataFromT2.action, inputMethod = _getLinkMetadataFromT2.inputMethod, sourceEvent = _getLinkMetadataFromT2.sourceEvent; var _findChanged = findChanged(tr, state), removed = _findChanged.removed, inserted = _findChanged.inserted, updated = _findChanged.updated; var MAX_LINK_EVENTS = 50; if ([removed, inserted, updated].some(function (arr) { return arr.length > MAX_LINK_EVENTS; })) { return []; } for (var i = 0; i < updated.length; i++) { var _update$previous$disp; var update = updated[i]; var _inserted = update.inserted; var node = _inserted.node, nodeContext = _inserted.nodeContext; var subject = getNodeSubject(node); /** * Not great, wish we had the previous node but we never stored it */ var previousDisplay = 'removed' in update ? appearanceForLink(update.removed.node) : (_update$previous$disp = update.previous.display) !== null && _update$previous$disp !== void 0 ? _update$previous$disp : 'unknown'; if (subject) { events.push({ event: EVENT.UPDATED, subject: subject, data: { node: node, nodeContext: nodeContext, action: action, inputMethod: inputMethod, sourceEvent: sourceEvent, isUndo: isUndo, isRedo: isRedo, previousDisplay: previousDisplay } }); } } var pushEvents = function pushEvents(entities, event) { for (var _i2 = 0; _i2 < entities.length; _i2++) { var _entities$_i = entities[_i2], _node2 = _entities$_i.node, _nodeContext = _entities$_i.nodeContext; var _subject = getNodeSubject(_node2); if (_subject) { events.push({ event: event, subject: _subject, data: { node: _node2, nodeContext: _nodeContext, action: action, inputMethod: inputMethod, sourceEvent: sourceEvent, isUndo: isUndo, isRedo: isRedo } }); } } }; pushEvents(removed, EVENT.DELETED); pushEvents(inserted, EVENT.CREATED); return events; } catch (err) { return events; } }