UNPKG

@atlaskit/editor-plugin-layout

Version:

Layout plugin for @atlaskit/editor-core

244 lines (230 loc) 10.3 kB
import { GapCursorSelection } from '@atlaskit/editor-common/selection'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { fg } from '@atlaskit/platform-feature-flags'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; export var 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 = editorExperiment('advanced_layouts', true) && findSelectedNodeOfType([layoutColumn])(selection); // When selection is on layoutColumn, we want to hide floating toolbar, hence don't return layoutSection node here return isLayoutColumn ? undefined : findParentNodeOfType(layoutSection)(selection) || 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 */ export var 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(TextSelection.near($selectionPos))); } else { view.dispatch(tr.setSelection(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. */ export var getGapCursorTargetForBlankSpaceClick = function getGapCursorTargetForBlankSpaceClick(view, event) { var _columnNode$attrs; if (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. */ export var 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; } };