@atlaskit/editor-plugin-layout
Version:
Layout plugin for @atlaskit/editor-core
251 lines (235 loc) • 10.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.selectIntoLayout = exports.isParagraphBlankSpaceTarget = exports.getMaybeLayoutSection = exports.getGapCursorTargetForBlankSpaceClick = void 0;
var _selection = require("@atlaskit/editor-common/selection");
var _state = require("@atlaskit/editor-prosemirror/state");
var _utils = require("@atlaskit/editor-prosemirror/utils");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
var getMaybeLayoutSection = exports.getMaybeLayoutSection = function getMaybeLayoutSection(state) {
var _state$schema$nodes = state.schema.nodes,
layoutSection = _state$schema$nodes.layoutSection,
layoutColumn = _state$schema$nodes.layoutColumn,
selection = state.selection;
var isLayoutColumn = (0, _experiments.editorExperiment)('advanced_layouts', true) && (0, _utils.findSelectedNodeOfType)([layoutColumn])(selection);
// When selection is on layoutColumn, we want to hide floating toolbar, hence don't return layoutSection node here
return isLayoutColumn ? undefined : (0, _utils.findParentNodeOfType)(layoutSection)(selection) || (0, _utils.findSelectedNodeOfType)([layoutSection])(selection);
};
/**
* The depth of the layout column inside the layout section.
* As per the current implementation, the layout column ALWAYS has a depth of 1.
*/
var LAYOUT_COLUMN_DEPTH = 1;
/**
* This helper function is used to select a position inside a layout section.
* @param view editor view instance
* @param posOfLayout the starting position of the layout
* @param childIndex the index of the child node in the layout section
* @returns Transaction or undefined
*/
var selectIntoLayout = exports.selectIntoLayout = function selectIntoLayout(view, posOfLayout) {
var _$maybeLayoutSection$;
var childIndex = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var $maybeLayoutSection = view.state.doc.resolve(posOfLayout);
if (((_$maybeLayoutSection$ = $maybeLayoutSection.nodeAfter) === null || _$maybeLayoutSection$ === void 0 ? void 0 : _$maybeLayoutSection$.type.name) === 'layoutSection') {
var _layoutSectionNode$fi;
var layoutSectionNode = $maybeLayoutSection.nodeAfter;
// check if the childIndex is valid
if (childIndex < 0 || childIndex >= layoutSectionNode.childCount) {
return;
}
var childPos = $maybeLayoutSection.posAtIndex(childIndex, LAYOUT_COLUMN_DEPTH);
var tr = view.state.tr;
var $selectionPos = tr.doc.resolve(childPos);
if (((_layoutSectionNode$fi = layoutSectionNode.firstChild) === null || _layoutSectionNode$fi === void 0 ? void 0 : _layoutSectionNode$fi.type.name) === 'paragraph') {
view.dispatch(tr.setSelection(_state.TextSelection.near($selectionPos)));
} else {
view.dispatch(tr.setSelection(_selection.GapCursorSelection.near($selectionPos)));
}
return tr;
}
};
/**
* For a blank-space click inside a layout column — above the first child (middle/bottom-aligned
* columns) or below the last child (any alignment) — return the ProseMirror position and side
* for a gap cursor. Returns `undefined` when the kill switch is ON, the click is outside a
* layoutColumn, or the Y coordinate is not in blank space.
*
* The `advanced_layouts` / `platform_editor_layout_column_menu` gates live in the caller
* (`applyBlankSpaceGapCursor`); only the kill switch is checked here.
*/
var getGapCursorTargetForBlankSpaceClick = exports.getGapCursorTargetForBlankSpaceClick = function getGapCursorTargetForBlankSpaceClick(view, event) {
var _columnNode$attrs;
if ((0, _platformFeatureFlags.fg)('platform_editor_layout_column_menu_kill_switch_1')) {
return undefined;
}
// Resolve the column from the DOM target so it works even when posAtCoords returns null
// (nothing rendered at the clicked Y).
var target = event.target;
var columnEl = target === null || target === void 0 ? void 0 : target.closest('[data-layout-column]');
if (!columnEl) {
return undefined;
}
var columnStartPos;
try {
columnStartPos = view.posAtDOM(columnEl, 0);
} catch (_unused) {
return undefined;
}
// posAtDOM resolves at varying depths, so walk up to find the layoutColumn.
var $columnStart = view.state.doc.resolve(columnStartPos);
var depth = -1;
for (var d = $columnStart.depth; d >= 0; d--) {
if ($columnStart.node(d).type.name === 'layoutColumn') {
depth = d;
break;
}
}
if (depth < 0) {
return undefined;
}
var columnNode = $columnStart.node(depth);
if (columnNode.childCount === 0) {
return undefined;
}
var columnContentStart = $columnStart.start(depth);
var columnEndPos = $columnStart.end(depth);
var getChildDom = function getChildDom(nodePos) {
try {
var dom = view.nodeDOM(nodePos);
return dom instanceof Element ? dom : null;
} catch (_unused2) {
return null;
}
};
var valign = (_columnNode$attrs = columnNode.attrs) === null || _columnNode$attrs === void 0 ? void 0 : _columnNode$attrs.valign;
var isNonTopAligned = valign && valign !== 'top';
// Use the column rect (not child rects) for above/below detection: it stays stable as
// gap-cursor widgets shift child DOM positions between repeated clicks.
var columnRect = columnEl.getBoundingClientRect();
// Click ABOVE the first child (only for middle/bottom-aligned columns).
var firstChildPos = columnContentStart;
var firstChildDom = getChildDom(firstChildPos);
if (isNonTopAligned && firstChildDom) {
var rect = firstChildDom.getBoundingClientRect();
if (event.clientY < rect.top && event.clientY >= columnRect.top) {
return {
pos: firstChildPos,
side: 'left'
};
}
}
// Click BELOW the last child (for any column alignment).
var lastChild = columnNode.lastChild;
var lastChildEndPos = columnEndPos;
var lastChildStartPos = lastChild ? lastChildEndPos - lastChild.nodeSize : columnContentStart;
var lastChildDom = lastChild ? getChildDom(lastChildStartPos) : null;
if (lastChild && lastChildDom) {
var _rect = lastChildDom.getBoundingClientRect();
if (event.clientY > _rect.bottom && event.clientY <= columnRect.bottom) {
return {
pos: lastChildEndPos,
side: 'right'
};
}
}
// Fallback: click lands ON a single atomic child that fills the column (mediaSingle/expand),
// so the above/below checks never fired.
if (columnNode.childCount === 1) {
var onlyChild = columnNode.firstChild;
// Exclude `panel`: its wrapper makes `view.nodeDOM` non-null and intercepts clicks, so the
// guard below would wrongly fire for in-panel blank-space clicks (which have their own
// native gap cursor).
if (onlyChild && onlyChild.type.name !== 'paragraph' && onlyChild.type.name !== 'panel') {
// Bail when the click is on the child's own content. For media the wrapper is full-width
// so test against the <img> rect; resolve it only for a direct mediaSingle child (else
// getContentRect could grab an image nested in an expand and break its toggle).
var contentRect = onlyChild.type.name === 'mediaSingle' ? getContentRect(firstChildDom) : null;
if (contentRect) {
var insideImage = event.clientX >= contentRect.left && event.clientX <= contentRect.right && event.clientY >= contentRect.top && event.clientY <= contentRect.bottom;
if (insideImage) {
return undefined;
}
} else {
// Other atomics: bail when posAtCoords resolves strictly inside the node range.
var coordPos = null;
try {
coordPos = view.posAtCoords({
left: event.clientX,
top: event.clientY
});
} catch (_unused3) {
coordPos = null;
}
if (coordPos && coordPos.pos > firstChildPos && coordPos.pos < lastChildEndPos) {
return undefined;
}
}
// Fire when the child DOM is resolvable, or when it's null (media not yet loaded) but
// the click target is the column itself (no node view intercepted it).
var targetEl = event.target;
var targetIsColumn = targetEl === columnEl;
var shouldUseFallback = firstChildDom !== null || targetIsColumn;
if (shouldUseFallback) {
var side = getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, event.clientX, event.clientY);
return side === 'left' ? {
pos: firstChildPos,
side: 'left'
} : {
pos: lastChildEndPos,
side: 'right'
};
}
}
}
return undefined;
};
/**
* The tight `<img>` content rect (or `null`). The outer wrapper often fills the whole column
* width, so the `<img>` rect is needed to tell "beside the image" from "on the image".
*/
var getContentRect = function getContentRect(firstChildDom) {
var img = firstChildDom === null || firstChildDom === void 0 ? void 0 : firstChildDom.querySelector('img');
return img ? img.getBoundingClientRect() : null;
};
/**
* Which side of an atomic child a blank-space click belongs to. Prefers the tight content (image)
* rect when available — using its midpoint so it's direction-agnostic (handles RTL right-aligned
* images) — otherwise falls back to the column's vertical midpoint.
*/
var getGapCursorSideForBlankSpaceClick = function getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, clientX, clientY) {
var contentRect = getContentRect(firstChildDom);
if (contentRect) {
if (clientY < contentRect.top) {
return 'left';
}
if (clientY > contentRect.bottom) {
return 'right';
}
if (clientX < (contentRect.left + contentRect.right) / 2) {
return 'left';
}
return 'right';
}
var columnMidY = columnRect.top + columnRect.height / 2;
return clientY < columnMidY ? 'left' : 'right';
};
/**
* True when the blank-space click target child is a paragraph, so the caller uses a TextSelection
* instead of a gap cursor. LEFT inspects the first child, RIGHT the last child.
*/
var isParagraphBlankSpaceTarget = exports.isParagraphBlankSpaceTarget = function isParagraphBlankSpaceTarget(view, gapTarget) {
var pos = gapTarget.pos,
side = gapTarget.side;
var doc = view.state.doc;
try {
var $pos = doc.resolve(pos);
var childNode = side === 'left' ? $pos.nodeAfter : $pos.nodeBefore;
return (childNode === null || childNode === void 0 ? void 0 : childNode.type.name) === 'paragraph';
} catch (_unused4) {
return false;
}
};