UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

187 lines (180 loc) 8.34 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.wasExtensionInsertedInBodiedSyncBlock = exports.sliceFullyContainsNode = exports.isBodiedSyncBlockNode = exports.findSyncBlockOrBodiedSyncBlock = exports.findSyncBlock = exports.findBodiedSyncBlock = exports.deferDispatch = exports.canBeConvertedToSyncBlock = void 0; var _selection = require("@atlaskit/editor-common/selection"); var _model = require("@atlaskit/editor-prosemirror/model"); var _transform = require("@atlaskit/editor-prosemirror/transform"); var _utils = require("@atlaskit/editor-prosemirror/utils"); var _experiments = require("@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. */ var deferDispatch = exports.deferDispatch = function deferDispatch(fn) { queueMicrotask(fn); }; var findSyncBlock = exports.findSyncBlock = function findSyncBlock(schema, selection) { var syncBlock = schema.nodes.syncBlock; return (0, _utils.findSelectedNodeOfType)(syncBlock)(selection); }; var findBodiedSyncBlock = exports.findBodiedSyncBlock = function findBodiedSyncBlock(schema, selection) { return (0, _utils.findSelectedNodeOfType)(schema.nodes.bodiedSyncBlock)(selection) || (0, _utils.findParentNodeOfType)(schema.nodes.bodiedSyncBlock)(selection); }; var findSyncBlockOrBodiedSyncBlock = exports.findSyncBlockOrBodiedSyncBlock = function findSyncBlockOrBodiedSyncBlock(schema, selection) { return findSyncBlock(schema, selection) || findBodiedSyncBlock(schema, selection); }; var isBodiedSyncBlockNode = exports.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 */ var canBeConvertedToSyncBlock = exports.canBeConvertedToSyncBlock = function canBeConvertedToSyncBlock(selection) { var _expandSelectionToBlo = (0, _selection.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 _model.Fragment.from(nodes); }; var sliceFullyContainsNode = exports.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 ((0, _experiments.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. */ var wasExtensionInsertedInBodiedSyncBlock = exports.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 _transform.ReplaceStep || step instanceof _transform.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 = (0, _utils.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 ((0, _experiments.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 = (0, _utils.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 || ((0, _experiments.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; };