@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
407 lines (393 loc) • 18.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.moveToLayout = void 0;
var _analytics = require("@atlaskit/editor-common/analytics");
var _monitoring = require("@atlaskit/editor-common/monitoring");
var _model = require("@atlaskit/editor-prosemirror/model");
var _state = require("@atlaskit/editor-prosemirror/state");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
var _analytics2 = require("../pm-plugins/utils/analytics");
var _checkFragment = require("../pm-plugins/utils/check-fragment");
var _consts = require("../pm-plugins/utils/consts");
var _removeFromSource = require("../pm-plugins/utils/remove-from-source");
var _selection = require("../pm-plugins/utils/selection");
var _validation = require("../pm-plugins/utils/validation");
var _consts2 = require("../ui/consts");
var createNewLayout = function createNewLayout(schema, layoutContents) {
if (layoutContents.length === 0 || layoutContents.length > (0, _consts.maxLayoutColumnSupported)()) {
return null;
}
var width = _consts2.DEFAULT_COLUMN_DISTRIBUTIONS[layoutContents.length];
if (!width) {
return null;
}
var _ref = schema.nodes || {},
layoutSection = _ref.layoutSection,
layoutColumn = _ref.layoutColumn;
try {
var layoutContent = _model.Fragment.fromArray(layoutContents.map(function (layoutContent) {
return layoutColumn.createChecked({
width: width
}, layoutContent);
}));
var layoutSectionNode = layoutSection.createChecked(undefined, layoutContent);
return layoutSectionNode;
} catch (error) {
(0, _monitoring.logException)(error, {
location: 'editor-plugin-block-controls/move-to-layout'
});
}
return null;
};
var moveToExistingLayout = function moveToExistingLayout(toLayout, toLayoutPos, sourceContent, from, to, tr, $originalFrom, $originalTo, api, selectMovedNode) {
var isSameLayout = (0, _validation.isInSameLayout)($originalFrom, $originalTo);
var sourceContentEndPos = -1;
var isMultiSelect = (0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true);
var sourceNodeTypes, hasSelectedMultipleNodes;
if (isMultiSelect) {
if (sourceContent instanceof _model.Fragment) {
sourceContentEndPos = from + sourceContent.size;
var attributes = (0, _analytics2.getMultiSelectAnalyticsAttributes)(tr, from, sourceContentEndPos);
hasSelectedMultipleNodes = attributes.hasSelectedMultipleNodes;
sourceNodeTypes = attributes.nodeTypes;
}
} else {
if (sourceContent instanceof _model.Node) {
sourceContentEndPos = from + sourceContent.nodeSize;
}
}
if (sourceContentEndPos === -1) {
return tr;
}
if (isSameLayout) {
var _$originalFrom$nodeAf;
// reorder columns
tr.delete(from, sourceContentEndPos);
var mappedTo = tr.mapping.map(to);
tr.insert(mappedTo, sourceContent);
if (selectMovedNode) {
tr.setSelection(new _state.NodeSelection(tr.doc.resolve(mappedTo))).scrollIntoView();
}
(0, _analytics2.attachMoveNodeAnalytics)(tr, _analytics.INPUT_METHOD.DRAG_AND_DROP, $originalFrom.depth, ((_$originalFrom$nodeAf = $originalFrom.nodeAfter) === null || _$originalFrom$nodeAf === void 0 ? void 0 : _$originalFrom$nodeAf.type.name) || '', 1, 'layoutSection', true, api, sourceNodeTypes, hasSelectedMultipleNodes);
} else if (toLayout.childCount < (0, _consts.maxLayoutColumnSupported)()) {
var _$originalFrom$nodeAf2;
(0, _removeFromSource.removeFromSource)(tr, tr.doc.resolve(from), sourceContentEndPos);
insertToDestinationNoWidthUpdate(tr, tr.mapping.map(to), sourceContent);
(0, _analytics2.attachMoveNodeAnalytics)(tr, _analytics.INPUT_METHOD.DRAG_AND_DROP, $originalFrom.depth, ((_$originalFrom$nodeAf2 = $originalFrom.nodeAfter) === null || _$originalFrom$nodeAf2 === void 0 ? void 0 : _$originalFrom$nodeAf2.type.name) || '', 1, 'layoutSection', false, api, sourceNodeTypes, hasSelectedMultipleNodes);
}
return tr;
};
/**
* This function is similar to insertToDestination
* But without update width step, mainly rely on the append transaction from layout.
* @param tr
* @param to
* @param sourceNode
* @returns
*/
var insertToDestinationNoWidthUpdate = function insertToDestinationNoWidthUpdate(tr, to, sourceContent) {
var _ref2 = tr.doc.type.schema.nodes || {},
layoutColumn = _ref2.layoutColumn;
var content = null;
try {
if ((0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true)) {
if (sourceContent instanceof _model.Fragment) {
var _sourceFragment$first;
var sourceFragment = sourceContent;
content = layoutColumn.createChecked({
width: 0
}, (0, _checkFragment.isFragmentOfType)(sourceFragment, 'layoutColumn') ? (_sourceFragment$first = sourceFragment.firstChild) === null || _sourceFragment$first === void 0 ? void 0 : _sourceFragment$first.content : sourceFragment);
}
} else {
if (sourceContent instanceof _model.Node) {
var sourceNode = sourceContent;
content = layoutColumn.createChecked({
width: 0
}, sourceNode.type.name === 'layoutColumn' ? sourceNode.content : sourceNode);
}
}
} catch (error) {
(0, _monitoring.logException)(error, {
location: 'editor-plugin-block-controls/move-to-layout'
});
}
if (content) {
tr.insert(to, content);
}
return tr;
};
/**
* Check if the node at `from` can be moved to node at `to` to create/expand a layout.
* Returns the source and destination nodes and positions if it's a valid move, otherwise, undefined
*/
var canMoveToLayout = function canMoveToLayout(api, from, to, tr, moveNodeAtCursorPos) {
if (from === to) {
return;
}
var _ref3 = tr.doc.type.schema.nodes || {},
layoutSection = _ref3.layoutSection,
layoutColumn = _ref3.layoutColumn,
doc = _ref3.doc,
bodiedSyncBlock = _ref3.bodiedSyncBlock;
// layout plugin does not exist
if (!layoutSection || !layoutColumn) {
return;
}
var $to = tr.doc.resolve(to);
var allowedParentTypes = [doc, layoutSection];
if (bodiedSyncBlock && (0, _experiments.editorExperiment)('platform_synced_block', true) && (0, _experiments.editorExperiment)('platform_synced_block_patch_6', true, {
exposure: true
})) {
allowedParentTypes.push(bodiedSyncBlock);
}
// drop at invalid position, not top level, or not a layout column
if (!$to.nodeAfter || !allowedParentTypes.includes($to.parent.type)) {
return;
}
var $from = tr.doc.resolve(from);
var isMultiSelect = (0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true);
// invalid from position or dragging a layout
if (!$from.nodeAfter || $from.nodeAfter.type === layoutSection) {
return;
}
var sourceContent = $from.nodeAfter;
var sourceFrom = from;
var sourceTo = from + sourceContent.nodeSize;
if (isMultiSelect && !moveNodeAtCursorPos) {
var _getMultiSelectionIfP = (0, _selection.getMultiSelectionIfPosInside)(api, from),
anchor = _getMultiSelectionIfP.anchor,
head = _getMultiSelectionIfP.head;
if (anchor !== undefined && head !== undefined) {
sourceFrom = Math.min(anchor, head);
sourceTo = Math.max(anchor, head);
sourceContent = tr.doc.slice(sourceFrom, sourceTo).content;
// TODO: ED-26959 - this might become expensive for large content, consider removing it if check has been done beforehand
if ((0, _checkFragment.containsNodeOfType)(sourceContent, 'layoutSection')) {
return;
}
} else {
sourceContent = _model.Fragment.from($from.nodeAfter);
}
}
var toNode = $to.nodeAfter;
return {
toNode: toNode,
$to: $to,
sourceContent: sourceContent,
$sourceFrom: tr.doc.resolve(sourceFrom),
sourceTo: sourceTo
};
};
var removeBreakoutMarks = function removeBreakoutMarks(tr, $from, to) {
var fromContentWithoutBreakout = $from.nodeAfter;
var _ref4 = tr.doc.type.schema.marks || {},
breakout = _ref4.breakout;
if ((0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true)) {
tr.doc.nodesBetween($from.pos, to, function (node, pos, parent) {
// should never remove breakout from previous layoutSection
if ((0, _expValEquals.expValEquals)('platform_editor_breakout_resizing', 'isEnabled', true)) {
if (node.type.name === 'layoutSection') {
return false;
}
}
// breakout doesn't exist on nested nodes
if ((parent === null || parent === void 0 ? void 0 : parent.type.name) === 'doc' && node.marks.some(function (m) {
return m.type === breakout;
})) {
tr.removeNodeMark(pos, breakout);
}
// descending is not needed as breakout doesn't exist on nested nodes
return false;
});
// resolve again the source content after node updated (remove breakout marks)
fromContentWithoutBreakout = tr.doc.slice($from.pos, to).content;
} else {
if (breakout && $from.nodeAfter && $from.nodeAfter.marks.some(function (m) {
return m.type === breakout;
})) {
tr.removeNodeMark($from.pos, breakout);
// resolve again the source node after node updated (remove breakout marks)
fromContentWithoutBreakout = tr.doc.resolve($from.pos).nodeAfter;
}
}
return fromContentWithoutBreakout;
};
var getBreakoutMode = function getBreakoutMode(content, breakout) {
if ((0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true)) {
if (content instanceof _model.Node) {
var _content$marks$find;
return (_content$marks$find = content.marks.find(function (m) {
return m.type === breakout;
})) === null || _content$marks$find === void 0 ? void 0 : _content$marks$find.attrs.mode;
} else if (content instanceof _model.Fragment) {
// Find the first breakout mode in the fragment
var firstBreakoutMode;
for (var i = 0; i < content.childCount; i++) {
var child = content.child(i);
var breakoutMark = child.marks.find(function (m) {
return m.type === breakout;
});
if (breakoutMark) {
firstBreakoutMode = breakoutMark.attrs.mode;
break;
}
}
return firstBreakoutMode;
}
} else {
// Without multi-select support, we can assume source content is of type PMNode
if (content instanceof _model.Node) {
var _content$marks$find2;
return (_content$marks$find2 = content.marks.find(function (m) {
return m.type === breakout;
})) === null || _content$marks$find2 === void 0 ? void 0 : _content$marks$find2.attrs.mode;
}
}
};
var getBreakoutModeAndWidth = function getBreakoutModeAndWidth(content, breakout) {
var findBreakoutMark = function findBreakoutMark(node) {
return node.marks.find(function (m) {
return m.type === breakout;
});
};
var extractBreakoutAttributes = function extractBreakoutAttributes(mark) {
return mark ? {
breakoutMode: mark.attrs.mode,
breakoutWidth: mark.attrs.width
} : null;
};
if ((0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true)) {
if (content instanceof _model.Node) {
return extractBreakoutAttributes(findBreakoutMark(content));
} else if (content instanceof _model.Fragment) {
// Find the first breakout mode in the fragment
for (var i = 0; i < content.childCount; i++) {
var child = content.child(i);
var breakoutMark = findBreakoutMark(child);
if (breakoutMark) {
return extractBreakoutAttributes(breakoutMark);
}
}
}
} else {
// Without multi-select support, we can assume source content is of type PMNode
if (content instanceof _model.Node) {
return extractBreakoutAttributes(findBreakoutMark(content));
}
}
return null;
};
// TODO: ED-26959 - As part of platform_editor_element_drag_and_drop_multiselect clean up,
// source content variable that has type of `PMNode | Fragment` should be updated to `Fragment` only
var moveToLayout = exports.moveToLayout = function moveToLayout(api) {
return function (from, to, options) {
return function (_ref5) {
var tr = _ref5.tr;
if (!api) {
return tr;
}
var canMove = canMoveToLayout(api, from, to, tr, options === null || options === void 0 ? void 0 : options.moveNodeAtCursorPos);
if (!canMove) {
return tr;
}
var toNode = canMove.toNode,
$to = canMove.$to,
sourceContent = canMove.sourceContent,
$sourceFrom = canMove.$sourceFrom,
sourceTo = canMove.sourceTo;
var _ref6 = tr.doc.type.schema.nodes || {},
layoutSection = _ref6.layoutSection,
layoutColumn = _ref6.layoutColumn;
var _ref7 = tr.doc.type.schema.marks || {},
breakout = _ref7.breakout;
// get breakout mode from destination node,
// if not found, get from source node,
var breakoutMode;
var breakoutWidth;
if ((0, _expValEquals.expValEquals)('platform_editor_breakout_resizing', 'isEnabled', true)) {
var _ref8 = getBreakoutModeAndWidth(toNode, breakout) || getBreakoutModeAndWidth(sourceContent, breakout) || {};
breakoutMode = _ref8.breakoutMode;
breakoutWidth = _ref8.breakoutWidth;
} else {
breakoutMode = getBreakoutMode(toNode, breakout) || getBreakoutMode(sourceContent, breakout);
}
// we don't want to remove marks when moving/re-ordering layoutSection
var shouldRemoveMarks = !($sourceFrom.node().type === layoutSection && (0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true));
var fromContentBeforeBreakoutMarksRemoved = (0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true) ? tr.doc.slice($sourceFrom.pos, sourceTo).content : $sourceFrom.nodeAfter;
// remove breakout from source content
var fromContentWithoutBreakout = shouldRemoveMarks ? removeBreakoutMarks(tr, $sourceFrom, sourceTo) : fromContentBeforeBreakoutMarksRemoved;
if (!fromContentWithoutBreakout) {
return tr;
}
if ((0, _platformFeatureFlags.fg)('platform_editor_ease_of_use_metrics')) {
var _api$metrics;
api === null || api === void 0 || (_api$metrics = api.metrics) === null || _api$metrics === void 0 || _api$metrics.commands.setContentMoved()({
tr: tr
});
}
var isMultiSelect = (0, _experiments.editorExperiment)('platform_editor_element_drag_and_drop_multiselect', true);
if (toNode.type === layoutSection) {
var toPos = options !== null && options !== void 0 && options.moveToEnd ? to + toNode.nodeSize - 1 : to + 1;
return moveToExistingLayout(toNode, to, fromContentWithoutBreakout, $sourceFrom.pos, toPos, tr, $sourceFrom, $to, api, options === null || options === void 0 ? void 0 : options.selectMovedNode);
} else if (toNode.type === layoutColumn) {
var toLayout = $to.parent;
var toLayoutPos = to - $to.parentOffset - 1;
var _toPos = options !== null && options !== void 0 && options.moveToEnd ? to + toNode.nodeSize : to;
return moveToExistingLayout(toLayout, toLayoutPos, fromContentWithoutBreakout, $sourceFrom.pos, _toPos, tr, $sourceFrom, $to, api, options === null || options === void 0 ? void 0 : options.selectMovedNode);
} else {
var toNodeWithoutBreakout = toNode;
// remove breakout from node;
if (breakout && $to.nodeAfter && $to.nodeAfter.marks.some(function (m) {
return m.type === breakout;
})) {
tr.removeNodeMark(to, breakout);
// resolve again the source node after node updated (remove breakout marks)
toNodeWithoutBreakout = tr.doc.resolve(to).nodeAfter || toNode;
}
if (isMultiSelect) {
if ((0, _checkFragment.isFragmentOfType)(fromContentWithoutBreakout, 'layoutColumn') && fromContentWithoutBreakout.firstChild) {
fromContentWithoutBreakout = fromContentWithoutBreakout.firstChild.content;
}
} else {
if (fromContentWithoutBreakout instanceof _model.Node && fromContentWithoutBreakout.type.name === 'layoutColumn') {
fromContentWithoutBreakout = fromContentWithoutBreakout.content;
}
}
var layoutContents = options !== null && options !== void 0 && options.moveToEnd ? [toNodeWithoutBreakout, fromContentWithoutBreakout] : [fromContentWithoutBreakout, toNodeWithoutBreakout];
var newLayout = createNewLayout(tr.doc.type.schema, layoutContents);
if (newLayout) {
var sourceNodeTypes, hasSelectedMultipleNodes;
if (isMultiSelect) {
var attributes = (0, _analytics2.getMultiSelectAnalyticsAttributes)(tr, $sourceFrom.pos, sourceTo);
hasSelectedMultipleNodes = attributes.hasSelectedMultipleNodes;
sourceNodeTypes = attributes.nodeTypes;
}
tr = (0, _removeFromSource.removeFromSource)(tr, $sourceFrom, sourceTo);
var mappedTo = tr.mapping.map(to);
tr.delete(mappedTo, mappedTo + toNodeWithoutBreakout.nodeSize).insert(mappedTo, newLayout);
if ((0, _expValEquals.expValEquals)('platform_editor_breakout_resizing', 'isEnabled', true)) {
breakoutMode && tr.setNodeMarkup(mappedTo, newLayout.type, newLayout.attrs, [breakout.create({
mode: breakoutMode,
width: breakoutWidth
})]);
} else {
breakoutMode && tr.setNodeMarkup(mappedTo, newLayout.type, newLayout.attrs, [breakout.create({
mode: breakoutMode
})]);
}
if ((0, _platformFeatureFlags.fg)('platform_editor_column_count_analytics')) {
// layout created via drag and drop will always be 2 columns
(0, _analytics2.fireInsertLayoutAnalytics)(tr, api, sourceNodeTypes, hasSelectedMultipleNodes, 2);
} else {
(0, _analytics2.fireInsertLayoutAnalytics)(tr, api, sourceNodeTypes, hasSelectedMultipleNodes);
}
}
return tr;
}
};
};
};