@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
182 lines (175 loc) • 7.74 kB
JavaScript
import { expandSelectionToBlockRange } from '@atlaskit/editor-common/selection';
import { Fragment } from '@atlaskit/editor-prosemirror/model';
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { findParentNodeOfType, findParentNodeOfTypeClosestToPos, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
/**
* Defers a callback to the next microtask (when gated) or next macrotask via setTimeout(0).
* Used to avoid re-entrant ProseMirror dispatch cycles.
*/
export var deferDispatch = function deferDispatch(fn) {
queueMicrotask(fn);
};
export var findSyncBlock = function findSyncBlock(schema, selection) {
var syncBlock = schema.nodes.syncBlock;
return findSelectedNodeOfType(syncBlock)(selection);
};
export var findBodiedSyncBlock = function findBodiedSyncBlock(schema, selection) {
return findSelectedNodeOfType(schema.nodes.bodiedSyncBlock)(selection) || findParentNodeOfType(schema.nodes.bodiedSyncBlock)(selection);
};
export var findSyncBlockOrBodiedSyncBlock = function findSyncBlockOrBodiedSyncBlock(schema, selection) {
return findSyncBlock(schema, selection) || findBodiedSyncBlock(schema, selection);
};
export var isBodiedSyncBlockNode = function isBodiedSyncBlockNode(node, bodiedSyncBlock) {
return node.type === bodiedSyncBlock;
};
var UNSUPPORTED_NODE_TYPES = new Set(['inlineExtension', 'extension', 'bodiedExtension', 'syncBlock', 'bodiedSyncBlock']);
/**
* Checks whether the selection can be converted to sync block
*
* @param selection - the current editor selection to validate for sync block conversion
* @returns A fragment containing the content to include in the synced block,
* stripping out unsupported marks (breakout on codeblock/expand/layout), as well as from and to positions,
* or false if conversion is not possible
*/
export var canBeConvertedToSyncBlock = function canBeConvertedToSyncBlock(selection) {
var _expandSelectionToBlo = expandSelectionToBlockRange(selection),
$from = _expandSelectionToBlo.$from,
range = _expandSelectionToBlo.range;
if (!range) {
return false;
}
var from = range.start;
var to = range.end;
var canBeConverted = true;
$from.doc.nodesBetween(from, to, function (node) {
if (UNSUPPORTED_NODE_TYPES.has(node.type.name)) {
canBeConverted = false;
return false;
}
});
if (!canBeConverted) {
return false;
}
var contentToInclude = removeBreakoutMarks($from.doc.slice(from, to).content);
return {
contentToInclude: contentToInclude,
from: from,
to: to
};
};
var removeBreakoutMarks = function removeBreakoutMarks(content) {
var nodes = [];
// we only need to recurse at the top level, because breakout has to be on a top level
content.forEach(function (node) {
var filteredMarks = node.marks.filter(function (mark) {
return mark.type.name !== 'breakout';
});
if (node.isText) {
nodes.push(node);
} else {
var newNode = node.type.create(node.attrs, node.content, filteredMarks);
nodes.push(newNode);
}
});
return Fragment.from(nodes);
};
export var sliceFullyContainsNode = function sliceFullyContainsNode(slice, node) {
var _slice$content$firstC, _slice$content$firstC2, _slice$content$lastCh, _slice$content$lastCh2;
var isFirstChild = ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type) === node.type && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.attrs.resourceId) === node.attrs.resourceId;
var isLastChild = ((_slice$content$lastCh = slice.content.lastChild) === null || _slice$content$lastCh === void 0 ? void 0 : _slice$content$lastCh.type) === node.type && ((_slice$content$lastCh2 = slice.content.lastChild) === null || _slice$content$lastCh2 === void 0 ? void 0 : _slice$content$lastCh2.attrs.resourceId) === node.attrs.resourceId;
var isOpenAtStart = isFirstChild && slice.openStart > 0;
var isOpenAtEnd = isLastChild && slice.openEnd > 0;
if (isOpenAtStart || isOpenAtEnd) {
return false;
}
return true;
};
// even though extension and bodiedExtension are explicitly not allowed by the schema, they can still be inserted nested inside other nodes e.g. layouts
var EXTENSION_NODES = new Set(['inlineExtension', 'extension', 'bodiedExtension']);
var _fragmentContainsExtension = function fragmentContainsExtension(fragment) {
var found = false;
fragment.forEach(function (node) {
if (found) {
return;
}
if (editorExperiment('platform_synced_block_patch_6', true, {
exposure: true
}) ? EXTENSION_NODES.has(node.type.name) : node.type.name === 'inlineExtension') {
found = true;
} else if (node.content.size) {
if (_fragmentContainsExtension(node.content)) {
found = true;
}
}
});
return found;
};
var sliceContainsExtension = function sliceContainsExtension(slice) {
return _fragmentContainsExtension(slice.content);
};
/**
* Returns the resourceId of the bodied sync block where an inline extension was inserted, or undefined.
* Used to show a warning flag only on the first instance per sync block.
*/
export var wasExtensionInsertedInBodiedSyncBlock = function wasExtensionInsertedInBodiedSyncBlock(tr, state) {
if (!tr.docChanged || tr.getMeta('isRemote')) {
return undefined;
}
var bodiedSyncBlock = state.schema.nodes.bodiedSyncBlock;
if (!bodiedSyncBlock) {
return undefined;
}
var docs = tr.docs;
// When docs is available (e.g. from history plugin), check each replace step
if (docs && docs.length > 0) {
for (var i = 0; i < tr.steps.length; i++) {
var _docs;
var step = tr.steps[i];
var isReplaceStep = step instanceof ReplaceStep || step instanceof ReplaceAroundStep;
if (!isReplaceStep || !('slice' in step) || !('from' in step)) {
continue;
}
var replaceStep = step;
if (!sliceContainsExtension(replaceStep.slice)) {
continue;
}
var docAfterStep = (_docs = docs[i + 1]) !== null && _docs !== void 0 ? _docs : tr.doc;
try {
var $pos = docAfterStep.resolve(replaceStep.from);
var parent = findParentNodeOfTypeClosestToPos($pos, bodiedSyncBlock);
if (parent !== null && parent !== void 0 && parent.node.attrs.resourceId) {
return parent.node.attrs.resourceId;
}
} catch (_unused) {
// resolve() can throw if position is invalid
}
}
return undefined;
}
// Fallback: scan final doc for inline extensions inside bodied sync block that were added
var resourceId;
tr.doc.descendants(function (node, pos) {
if (resourceId !== undefined) {
return false;
}
if (editorExperiment('platform_synced_block_patch_6', true, {
exposure: true
}) ? EXTENSION_NODES.has(node.type.name) : node.type.name === 'inlineExtension') {
var _$pos = tr.doc.resolve(pos);
var _parent = findParentNodeOfTypeClosestToPos(_$pos, bodiedSyncBlock);
if (_parent !== null && _parent !== void 0 && _parent.node.attrs.resourceId) {
var mappedPos = tr.mapping.invert().map(pos);
var nodeBefore = state.doc.nodeAt(mappedPos);
if (!nodeBefore || (editorExperiment('platform_synced_block_patch_6', true, {
exposure: true
}) ? EXTENSION_NODES.has(nodeBefore.type.name) : nodeBefore.type.name !== 'inlineExtension')) {
resourceId = _parent.node.attrs.resourceId;
return false;
}
}
}
return true;
});
return resourceId;
};