UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

391 lines (368 loc) 16.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createLeftAnchorWidget = exports.createInlineIndicatorAnchorWidgets = exports.createDocMarginAnchorWidget = exports.createBlockIndicatorAnchorWidgets = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _utils = require("@atlaskit/editor-prosemirror/utils"); var _view = require("@atlaskit/editor-prosemirror/view"); var _decorationKeys = require("./decorationKeys"); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /** * Resolves the doc-level block node (table/expand/layout) for `from`, along with * the position right before it (`beforePos`). Falls back to `$from.nodeAfter` * when there is no depth-1 ancestor (e.g. `from` sits just before the block). */ var resolveDocLevelNode = function resolveDocLevelNode(doc, from) { var _$from$node; var $from = doc.resolve(from); var node = (_$from$node = $from.node(1)) !== null && _$from$node !== void 0 ? _$from$node : $from.nodeAfter; if (!node) { return undefined; } // Content start of the block. For the depth-1 case this is `$from.start(1)`; // for the `nodeAfter` fallback, `from` is the position just before the block // node, so its content starts at `from + 1`. var nodeStart = $from.node(1) ? $from.start(1) : from + 1; return { node: node, nodeStart: nodeStart, // Position of the block node itself (one before its content start). beforePos: nodeStart - 1 }; }; /** * Handles edge cases for block nodes whose inline content can exceed the doc * margin (tables, layouts, expands). Returns the position whose DOM should be * measured to size the left anchor, or `undefined` when the diff is not inside * such a node. */ var edgeCases = function edgeCases(doc, from) { var resolved = resolveDocLevelNode(doc, from); if (!resolved) { return undefined; } var node = resolved.node, nodeStart = resolved.nodeStart, beforePos = resolved.beforePos; /** * All resizable nodes will need dynamic calculations of the block indicator left anchor */ if (node.marks.some(function (mark) { return mark.type.name === 'breakout'; })) { /** * Layouts and Expands have extra padding around the container */ return { beforePos: beforePos, measurePos: beforePos }; } switch (node.type.name) { /** * For resizable blocks, inline content can exceed the doc margin. * The widget is placed before the block; the anchor is sized against the * block's DOM so it doesn't get clipped when the block is resized :') */ case 'table': { // A table with no rows has nothing to measure. if (!node.firstChild) { return undefined; } // Measure the first row (`nodeStart` is just inside the table, i.e. the // position of the first row): its width matches the table's. return { beforePos: beforePos, measurePos: nodeStart }; } case 'layoutSection': case 'expand': // Measure the block itself (the widget is rendered outside the block). return { beforePos: beforePos, leftOffset: 12 }; default: return undefined; } }; /** * Create a widget that marks the start of the doc margin. * This is used to determine the position of the inline indicators * when the inline content exceeds the doc margin. */ var createDocMarginAnchorWidget = exports.createDocMarginAnchorWidget = function createDocMarginAnchorWidget() { return _view.Decoration.widget(0, function () { var span = document.createElement('span'); span.style.setProperty('anchor-name', "--".concat(_decorationKeys.AnchorDocMarginKey)); return span; }, // set the side to -999 so that it is always rendered before any other anchors (0, _decorationKeys.buildAnchorDecorationSpec)({ anchorType: _decorationKeys.AnchorTypeKey.docMargin, side: -999 })); }; /** * Creates an invisible left anchor widget for a diff inside a resizable block * node (table, layout, expand), whose inline content can exceed the doc margin. * Resolves the edge-case position from `doc`/`from`; returns `undefined` when * the diff is not inside such a node and no left anchor is needed. * * The span is given an `anchor-name` (keyed by `diffId`) and positioned in the * doc margin so the `IndicatorBar` can align its left edge against it via CSS * anchor positioning. Shared by inline and node (widget) diff decorations. */ var createLeftAnchorWidget = exports.createLeftAnchorWidget = function createLeftAnchorWidget(_ref) { var doc = _ref.doc, from = _ref.from, diffId = _ref.diffId; var edgeCase = edgeCases(doc, from); if (edgeCase === undefined) { return undefined; } // Render the widget right before the doc-level node so it lives outside the // resizable block container. var beforePos = edgeCase.beforePos; var leftAnchorKey = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.left }); var leftResizeObserver; return _view.Decoration.widget(beforePos, function (view, getPos) { // Outer span stays in the flow but takes up no space. var wrapper = document.createElement('div'); wrapper.style.setProperty('position', 'relative'); wrapper.style.setProperty('width', '100%'); // Inner span is absolutely positioned in the doc margin; it carries the // `anchor-name` the IndicatorBar aligns its left edge against. var anchor = document.createElement('div'); anchor.style.setProperty('anchor-name', "--".concat(leftAnchorKey)); anchor.style.setProperty('position', 'absolute'); anchor.style.setProperty('left', "calc(50% - ".concat((edgeCase === null || edgeCase === void 0 ? void 0 : edgeCase.leftOffset) || 0, "px)")); anchor.style.setProperty('transform', 'translateX(-50%)'); wrapper.appendChild(anchor); var measureWidth = function measureWidth() { if (getPos() === undefined || edgeCase.measurePos === undefined) { return; } var dom = view.nodeDOM(edgeCase.measurePos); if (dom instanceof HTMLElement) { // The left anchor only needs the container width so the // IndicatorBar can align against the block's horizontal extent. anchor.style.setProperty('width', "".concat(dom.offsetWidth, "px")); // Observe the measured element for size changes (e.g. page // resize) so the indicator stays aligned. CCI-17981 if (!leftResizeObserver) { leftResizeObserver = new ResizeObserver(function () { if (getPos() !== undefined) { anchor.style.setProperty('width', "".concat(dom.offsetWidth, "px")); } }); leftResizeObserver.observe(dom); } } }; // The block DOM may not be settled synchronously (e.g. after a // transaction), so defer the measurement like the gap cursor does. requestAnimationFrame(measureWidth); return wrapper; }, _objectSpread(_objectSpread({}, (0, _decorationKeys.buildAnchorDecorationSpec)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.left, side: -999 })), {}, { destroy: function destroy() { var _leftResizeObserver; return (_leftResizeObserver = leftResizeObserver) === null || _leftResizeObserver === void 0 ? void 0 : _leftResizeObserver.disconnect(); } })); }; /** * Creates invisible anchor widgets for a single block-changed diff so that the * `IndicatorBar` can use CSS anchor positioning to align itself with the diff. * * The interface mirrors `createInlineIndicatorAnchorWidgets`: * - A `from` anchor is placed at the start of the node range (top of the bar). * - A `to` anchor is placed at the end of the node range (bottom of the bar). * - An optional `left` anchor is placed inside a resizable container (table, * layout, expand) so the bar aligns within the container boundary. * */ var createBlockIndicatorAnchorWidgets = exports.createBlockIndicatorAnchorWidgets = function createBlockIndicatorAnchorWidgets(_ref2) { var doc = _ref2.doc, from = _ref2.from, to = _ref2.to, diffId = _ref2.diffId; var leftAnchor = createLeftAnchorWidget({ doc: doc, from: from, diffId: diffId }); var maybeLeftAnchor = leftAnchor ? [leftAnchor] : []; /** * A single anchor widget spans the full height of the block node, mimicking * the gap cursor placement logic (see `place-gap-cursor.ts`): an element * whose height is measured from the block's DOM so its box covers the block. * * Because the anchor rect covers the whole block, the `IndicatorBar` can * resolve `top`, `bottom` and `left` against this one anchor (keyed by * `diffId` with no `anchorType`) instead of separate `from`/`to` anchors. */ var blockAnchorKey = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId }); /** * If `from` lands inside a table cell/header or a table row, the widget must * still be rendered *outside* the table (widgets placed inside a table are * clipped/mis-laid-out), but we want the anchor to be sized against the * actual cell/row DOM. So we split into two positions: * - `widgetPos`: where the widget DOM is rendered (outside the table). * - `measurePos`: the closest cell/row whose DOM we measure for the height. */ var $from = doc.resolve(from); var parentTable = (0, _utils.findParentNodeClosestToPos)($from, function (ancestor) { return ancestor.type.name === 'table'; }); var parentCellOrRow = (0, _utils.findParentNodeClosestToPos)($from, function (ancestor) { return ['tableCell', 'tableHeader', 'tableRow'].includes(ancestor.type.name); }); // Render outside the table when inside one; otherwise keep the original pos. var widgetPos = parentTable ? parentTable.pos : from; // Measure the actual cell/row DOM when inside one; otherwise measure the // widget's own position. var measurePos = parentCellOrRow ? parentCellOrRow.pos : from; var blockResizeObserver; var blockWidget = _view.Decoration.widget(widgetPos, function (view, getPos) { // Outer span stays in the flow but takes up no space. var wrapper = document.createElement('span'); wrapper.style.setProperty('position', 'relative'); // Inner span is absolutely positioned and sized to the block height; // it carries the `anchor-name` the IndicatorBar aligns against. var anchor = document.createElement('span'); anchor.style.setProperty('position', 'absolute'); anchor.style.setProperty('anchor-name', "--".concat(blockAnchorKey)); wrapper.appendChild(anchor); var _measureBlock = function measureBlock() { if (getPos() === undefined) { return; } var dom = view.nodeDOM(measurePos); if (dom instanceof HTMLElement) { anchor.style.setProperty('height', "".concat(dom.offsetHeight, "px")); // The wrapper renders outside the table, so there is a vertical // gap between it and the cell/row we're anchoring to. Measure // that delta and offset the (absolutely positioned) anchor by it // so its box lines up with the cell/row. var wrapperTop = wrapper.getBoundingClientRect().top; var domTop = dom.getBoundingClientRect().top; var verticalOffset = domTop - wrapperTop; anchor.style.setProperty('top', "".concat(verticalOffset, "px")); // The offset already accounts for the cell/row's position, so the // margin-top must not be double-applied. anchor.style.setProperty('margin-top', '0px'); // Observe the measured element for size changes (e.g. page // resize) so the indicator stays aligned. CCI-17981 if (!blockResizeObserver) { blockResizeObserver = new ResizeObserver(function () { if (getPos() !== undefined) { _measureBlock(); } }); blockResizeObserver.observe(dom); } } }; // The block DOM may not be settled synchronously (e.g. after a // transaction), so defer the measurement like the gap cursor does. requestAnimationFrame(_measureBlock); return wrapper; }, _objectSpread(_objectSpread({}, (0, _decorationKeys.buildAnchorDecorationSpec)({ diffId: diffId, // Reuse the `from` anchor type slot; the generated key intentionally // omits the anchor type so the single element backs top/bottom/left. anchorType: _decorationKeys.AnchorTypeKey.from, side: -1 })), {}, { destroy: function destroy() { var _blockResizeObserver; return (_blockResizeObserver = blockResizeObserver) === null || _blockResizeObserver === void 0 ? void 0 : _blockResizeObserver.disconnect(); } })); return [blockWidget].concat(maybeLeftAnchor); }; /** * A `from`/`to` on a `tableRow` boundary makes the anchor a direct `<tr>` (CSS grid) * child, adding a phantom column that collapses the cells (EDITOR-8442). Clamp it * inward into the neighbouring cell (`direction: 1` forward, `-1` back). Positions * not on a row boundary are returned as-is. */ var clampAnchorPosIntoCell = function clampAnchorPosIntoCell(doc, pos, direction) { var $pos = doc.resolve(pos); if ($pos.parent.type.name !== 'tableRow') { return pos; } var cell = direction === 1 ? $pos.nodeAfter : $pos.nodeBefore; if (!cell || cell.type.name !== 'tableHeader' && cell.type.name !== 'tableCell') { return pos; } // +2 past the cell and its first child boundary = inside the cell's content; // for the backward case, step back the same amount from the cell's end. return direction === 1 ? pos + 2 : pos - 2; }; /** * Invisible `from`/`to` (and optional `left`) anchor widgets for one inline diff * range, so the `IndicatorBar` can align itself via CSS anchor positioning. */ var createInlineIndicatorAnchorWidgets = exports.createInlineIndicatorAnchorWidgets = function createInlineIndicatorAnchorWidgets(_ref3) { var doc = _ref3.doc, from = _ref3.from, to = _ref3.to, diffId = _ref3.diffId; var leftAnchor = createLeftAnchorWidget({ doc: doc, from: from, diffId: diffId }); var maybeLeftAnchor = leftAnchor ? [leftAnchor] : []; // Keep the start/end anchors out of the table row's grid (see helper above). var fromPos = clampAnchorPosIntoCell(doc, from, 1); var toPos = clampAnchorPosIntoCell(doc, to, -1); /** * Two widgets mark the start and end of the inline range so the * IndicatorBar can determine top/bottom even if * the inline decoration is broken up by marks / between blocks. */ var fromAnchorKey = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.from }); var fromWidget = _view.Decoration.widget(fromPos, function () { var span = document.createElement('span'); span.style.setProperty('anchor-name', "--".concat(fromAnchorKey)); return span; }, (0, _decorationKeys.buildAnchorDecorationSpec)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.from, side: 1 })); var toAnchorKey = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.to }); var toWidget = _view.Decoration.widget(toPos, function () { var span = document.createElement('span'); span.style.setProperty('anchor-name', "--".concat(toAnchorKey)); return span; }, (0, _decorationKeys.buildAnchorDecorationSpec)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.to, side: -1 })); return [fromWidget, toWidget].concat(maybeLeftAnchor); };