@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
398 lines (384 loc) • 14.3 kB
JavaScript
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;
}
}