UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

408 lines (394 loc) • 21.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createChangedRowDecorationWidgets = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _document = require("@atlaskit/editor-common/utils/document"); var _utils = require("@atlaskit/editor-prosemirror/utils"); var _view = require("@atlaskit/editor-prosemirror/view"); var _tableMap = require("@atlaskit/editor-tables/table-map"); var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled"); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _factory = require("./colorSchemes/factory"); var _schemes = require("./colorSchemes/schemes"); var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets"); var _createChangedRowDecorationWidgetsStyles = require("./createChangedRowDecorationWidgets.styles.legacy"); var _createContributorTagWidget = require("./createContributorTagWidget"); var _decorationKeys = require("./decorationKeys"); var _findSafeInsertPos = require("./utils/findSafeInsertPos"); var _tableCellEdgeAttrs = require("./utils/tableCellEdgeAttrs"); var _wrapBlockNodeView = require("./utils/wrapBlockNodeView"); 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; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } /** * Extracts information about deleted table rows from a change */ var extractChangedRows = function extractChangedRows(_ref) { var change = _ref.change, originalDoc = _ref.originalDoc, newDoc = _ref.newDoc; var changedRows = []; // Find the table in the original document var $fromPos = originalDoc.resolve(change.fromA); var tableOld = (0, _utils.findParentNodeClosestToPos)($fromPos, function (node) { return node.type.name === 'table'; }); if (!tableOld) { return changedRows; } var oldTableMap = _tableMap.TableMap.get(tableOld.node); // Find the table in the new document at the insertion point var $newPos = newDoc.resolve(change.fromB); var tableNew = (0, _utils.findParentNodeClosestToPos)($newPos, function (node) { return node.type.name === 'table'; }); if (!tableNew) { return changedRows; } var newTableMap = _tableMap.TableMap.get(tableNew.node); // A row DELETION makes the table shorter. An in-place whole-row REPLACEMENT (`replaceNode` on a // `tableRow`, which AI suggested edits emits) leaves the row count untouched — and this helper // was written for deletions only, so it used to discard every replacement and the changed row's // content was never rendered anywhere. The reviewer was left with just the removed side. var isRowDeletion = oldTableMap.height > newTableMap.height; var handleRowReplacement = oldTableMap.height === newTableMap.height && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1'); // If no rows were changed, return empty if (!(isRowDeletion || handleRowReplacement) || // For now ignore if there are column deletions as well oldTableMap.width !== newTableMap.width) { return changedRows; } // Find which rows were changed by analyzing the change range var changeStartInTable = change.fromA - tableOld.pos - 1; var changeEndInTable = change.toA - tableOld.pos - 1; var edgeAttrsByOffset = (0, _expValEquals.expValEquals)('platform_editor_table_q4_loveability', 'isEnabled', true) && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_table_diff_rounded_corners') ? (0, _tableCellEdgeAttrs.getTableTopAndBottomCellEdgeAttrs)(oldTableMap) : undefined; var currentOffset = 0; var rowIndex = 0; tableOld.node.content.forEach(function (rowNode) { var rowStart = currentOffset; var rowEnd = currentOffset + rowNode.nodeSize; // Check if this row overlaps with the deletion range var rowOverlapsChange = rowStart >= changeStartInTable && rowStart < changeEndInTable || rowEnd > changeStartInTable && rowEnd <= changeEndInTable || rowStart < changeStartInTable && rowEnd > changeEndInTable; if (rowOverlapsChange && rowNode.type.name === 'tableRow') { var rowLocalId = rowNode.attrs.localId; var isTableRowReplacement = rowLocalId && tableNew.node.children.some(function (candidateRow) { return candidateRow.type.name === 'tableRow' && candidateRow.attrs.localId === rowLocalId; }); var cellEdgeAttrs = edgeAttrsByOffset ? (0, _tableCellEdgeAttrs.getRowCellEdgeAttrs)({ edgeAttrsByOffset: edgeAttrsByOffset, rowNode: rowNode, rowStart: rowStart }) : undefined; var startOfRow = newTableMap.mapByRow.slice().reverse().find(function (row) { return row[0] + tableNew.pos < change.fromB && change.fromB < row[row.length - 1] + tableNew.pos; }); changedRows.push({ rowIndex: rowIndex, rowNode: rowNode, isTableRowReplacement: isTableRowReplacement, cellEdgeAttrs: cellEdgeAttrs, fromA: tableOld.pos + 1 + rowStart, toA: tableOld.pos + 1 + rowEnd, fromB: startOfRow ? startOfRow[0] + tableNew.start : change.fromB }); } currentOffset += rowNode.nodeSize; if (rowNode.type.name === 'tableRow') { rowIndex++; } }); // Filter changes that never truly got deleted. Compare the number of equivalent rows rather // than only checking whether one still exists: deleting two of three identical empty rows should // retain two changed rows even though the third remains in the new table. var equivalentRowDeletionCounts = []; return changedRows.filter(function (changedRow) { // A replacement leaves every sibling row in place, so asking "does this row still exist // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a // sibling. Compare it against the row at the same index instead. if (handleRowReplacement) { var counterpart = tableNew.node.maybeChild(changedRow.rowIndex); return !counterpart || !(0, _document.areNodesEqualIgnoreAttrs)(counterpart, changedRow.rowNode); } var deletionCount = equivalentRowDeletionCounts.find(function (_ref2) { var rowNode = _ref2.rowNode; return (0, _document.areNodesEqualIgnoreAttrs)(rowNode, changedRow.rowNode); }); if (!deletionCount) { var oldRowCount = tableOld.node.children.filter(function (oldRow) { return (0, _document.areNodesEqualIgnoreAttrs)(oldRow, changedRow.rowNode); }).length; var newRowCount = tableNew.node.children.filter(function (newRow) { return (0, _document.areNodesEqualIgnoreAttrs)(newRow, changedRow.rowNode); }).length; deletionCount = { remaining: Math.max(0, oldRowCount - newRowCount), rowNode: changedRow.rowNode }; equivalentRowDeletionCounts.push(deletionCount); } if (deletionCount.remaining === 0) { return false; } deletionCount.remaining--; return true; }); }; /** * Creates a DOM representation of a deleted table row */ var createChangedRowDOM = function createChangedRowDOM(_ref3) { var rowNode = _ref3.rowNode, cellEdgeAttrs = _ref3.cellEdgeAttrs, nodeViewSerializer = _ref3.nodeViewSerializer, isTableRowReplacement = _ref3.isTableRowReplacement, colorScheme = _ref3.colorScheme, isInserted = _ref3.isInserted, hasAnchoredRemovedLozenge = _ref3.hasAnchoredRemovedLozenge, intl = _ref3.intl, isActive = _ref3.isActive; var tr = document.createElement('tr'); if (isTableRowReplacement && (0, _fg.fg)('platform_editor_ai_show_diff_patch_2')) { tr.dataset.showDiffTableRowReplacement = 'true'; } var colors = _schemes.colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : 'standard']; var deletedTreatment = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_color_scheme_refactor') ? (0, _factory.buildDeletedRowStyle)(colors) : (0, _createChangedRowDecorationWidgetsStyles.resolveDeletedRowStyleLegacy)((0, _schemes.getLegacyColorScheme)(colorScheme)); var hostsRemovedLozenge = Boolean(intl) && !isInserted && !hasAnchoredRemovedLozenge; // Inserted rows keep their natural styling; the row strikethrough is deletions only. if (!isInserted) { tr.setAttribute('style', deletedTreatment); } // Mirrors the strikethrough condition above: an `isInserted` row is ADDED content, so it must not claim the "deleted" testid — page models match that as // removed content (same reasoning as `createTableCellContentWidgets`). tr.setAttribute('data-testid', isInserted && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1') ? 'show-diff-changed-row' : 'show-diff-deleted-row'); if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) { tr.style.setProperty('scroll-margin-top', _decorationKeys.scrollMarginTopValue); } // Serialize each cell in the row var cellIndex = 0; rowNode.content.forEach(function (cellNode) { if (cellNode.type.name === 'tableCell' || cellNode.type.name === 'tableHeader') { var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode, 0, undefined, isInserted, colorScheme); if (nodeView) { if (nodeView instanceof HTMLElement) { (0, _tableCellEdgeAttrs.applyCellEdgeAttrs)(nodeView, cellEdgeAttrs === null || cellEdgeAttrs === void 0 ? void 0 : cellEdgeAttrs[cellIndex]); var overlay = document.createElement('span'); var isRoundedTable = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_table_diff_rounded_corners'); var overlayStyle = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_color_scheme_refactor') ? isInserted ? isRoundedTable ? (0, _factory.buildAddedCellOverlayRoundedStyle)(colors) : (0, _factory.buildAddedCellOverlayStyle)(colors) : isRoundedTable ? (0, _factory.buildDeletedCellOverlayRoundedStyle)(colors) : (0, _factory.buildDeletedCellOverlayStyle)(colors) : (0, _createChangedRowDecorationWidgetsStyles.resolveCellOverlayStyleLegacy)({ colorScheme: (0, _schemes.getLegacyColorScheme)(colorScheme), isInserted: isInserted, isRoundedTable: isRoundedTable }); overlay.setAttribute('style', overlayStyle); nodeView.appendChild(overlay); } tr.appendChild(nodeView); } else { // Fallback to fragment serialization var serializedContent = nodeViewSerializer.serializeFragment(cellNode.content); if (serializedContent) { tr.appendChild(serializedContent); } } cellIndex++; } }); // A "Removed" label on the row's last cell, so it reads at the row's top right corner. Deletions // only: an inserted row is added content and carries no removal label. if (intl && hostsRemovedLozenge) { var lastCell = tr.lastElementChild; if (lastCell instanceof HTMLElement) { lastCell.style.position = 'relative'; // Append, never prepend. Editor CSS resets the top margin of a cell's first child, so a // prepended label pushes the paragraph out of that reset and the row grows taller. lastCell.append((0, _wrapBlockNodeView.createRemovedLozenge)(intl, isActive, colorScheme, true)); } } return tr; }; var supportsAnchorPositioning = function supportsAnchorPositioning() { return typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('top', 'anchor(--a top)') && CSS.supports('position-anchor', '--a'); }; var createAnchoredRemovedLozengeWidget = function createAnchoredRemovedLozengeWidget(_ref4) { var anchorName = _ref4.anchorName, colorScheme = _ref4.colorScheme, from = _ref4.from, intl = _ref4.intl, isActive = _ref4.isActive; var lozenge = (0, _wrapBlockNodeView.createRemovedLozenge)(intl, isActive, colorScheme, true); var inset = lozenge.style.top; lozenge.style.setProperty('position', 'fixed'); // Fixed labels only follow their default anchor on scroll, and escape scroll-container clipping. lozenge.style.setProperty('position-anchor', "--".concat(anchorName)); lozenge.style.setProperty('position-visibility', 'anchors-visible'); lozenge.style.setProperty('top', "calc(anchor(--".concat(anchorName, " top) + ").concat(inset, ")")); lozenge.style.setProperty('left', "calc(anchor(--".concat(anchorName, " right) - ").concat(inset, ")")); lozenge.style.setProperty('right', 'auto'); lozenge.style.setProperty('transform', 'translateX(-100%)'); return _view.Decoration.widget(from, lozenge, { key: "removed-lozenge-".concat(anchorName), side: -1 }); }; /** * Expands a diff to include whole changed rows when table rows are affected */ var expandDiffForChangedRows = function expandDiffForChangedRows(_ref5) { var changes = _ref5.changes, originalDoc = _ref5.originalDoc, newDoc = _ref5.newDoc; var rowInfo = []; var _iterator = _createForOfIteratorHelper(changes), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var change = _step.value; // Check if this change affects table content var changedRows = extractChangedRows({ change: change, originalDoc: originalDoc, newDoc: newDoc }); if (changedRows.length > 0) { rowInfo.push.apply(rowInfo, (0, _toConsumableArray2.default)(changedRows)); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return rowInfo; }; /** * Main function to handle deleted rows - computes diff and creates decorations */ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidgets = function createChangedRowDecorationWidgets(_ref6) { var attributionKey = _ref6.attributionKey, changes = _ref6.changes, originalDoc = _ref6.originalDoc, newDoc = _ref6.newDoc, nodeViewSerializer = _ref6.nodeViewSerializer, colorScheme = _ref6.colorScheme, isActive = _ref6.isActive, _ref6$isInserted = _ref6.isInserted, isInserted = _ref6$isInserted === void 0 ? false : _ref6$isInserted, leftAnchorId = _ref6.leftAnchorId, intl = _ref6.intl, _ref6$showIndicators = _ref6.showIndicators, showIndicators = _ref6$showIndicators === void 0 ? false : _ref6$showIndicators, _ref6$showContributor = _ref6.showContributorTags, showContributorTags = _ref6$showContributor === void 0 ? false : _ref6$showContributor, tagMountContext = _ref6.tagMountContext; // First, expand the changes to include complete deleted rows var changedRows = expandDiffForChangedRows({ changes: changes.filter(function (change) { return change.deleted.length > 0; }), originalDoc: originalDoc, newDoc: newDoc }); return changedRows.flatMap(function (changedRow) { // Keep the ID stable while contributor tags are enabled so recalculation preserves their state. var diffId = showContributorTags ? "widget-row-".concat(changedRow.fromA, "-").concat(changedRow.toA) : crypto.randomUUID(); var hasAnchoredRemovedLozenge = Boolean(intl) && !isInserted && supportsAnchorPositioning(); var rowDOM = createChangedRowDOM({ rowNode: changedRow.rowNode, isTableRowReplacement: changedRow.isTableRowReplacement, cellEdgeAttrs: changedRow.cellEdgeAttrs, nodeViewSerializer: nodeViewSerializer, colorScheme: colorScheme, isInserted: isInserted, hasAnchoredRemovedLozenge: hasAnchoredRemovedLozenge, intl: intl, isActive: isActive }); // Find safe insertion position for the deleted row var safeInsertPos = (0, _findSafeInsertPos.findSafeInsertPos)(newDoc, changedRow.fromB - 1, // -1 to find the first safe position from the table originalDoc.slice(changedRow.fromA, changedRow.toA)); var decorations = []; var rowAnchorNames = []; // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar // is still created but can never resolve a position, so it silently does not render and the // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same // property on its own widget DOM for non-table content; this path returns before reaching it. if (showIndicators && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) { rowAnchorNames.push((0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId })); // A table's content can extend past the doc margin, so the bar also needs a left anchor // measured against the table itself or it is clipped once the table is resized. var leftAnchor = (0, _createAnchorDecorationWidgets.createLeftAnchorWidget)({ doc: newDoc, from: safeInsertPos, diffId: diffId, leftAnchorId: leftAnchorId }); if (leftAnchor) { decorations.push(leftAnchor); } } var tagAnchorName = showContributorTags && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() ? (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.tag }) : undefined; var removedLozengeAnchorName = hasAnchoredRemovedLozenge ? tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.tag }) : undefined; var rowLabelAnchorName = tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : removedLozengeAnchorName; var tagWidget = tagAnchorName ? (0, _createContributorTagWidget.createContributorTagWidget)({ anchorAtRangeStart: true, anchorName: tagAnchorName, diffId: diffId, doc: newDoc, from: safeInsertPos, mountContext: tagMountContext, to: safeInsertPos }) : undefined; if (rowLabelAnchorName) { // Labels stay outside the table row so they do not inherit its deletion treatment. rowAnchorNames.push(rowLabelAnchorName); } if (rowAnchorNames.length > 0) { rowDOM.style.setProperty('anchor-name', rowAnchorNames.map(function (anchorName) { return "--".concat(anchorName); }).join(', ')); } if (tagWidget) { // Lets the contributor tag reveal when any part of the deleted row is hovered. rowDOM.setAttribute('data-diff-id', diffId); } var rowWidget = _view.Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({ attributionKey: tagWidget ? attributionKey : undefined, colorScheme: colorScheme, decorationType: 'widget', diffId: diffId, leftAnchorId: leftAnchorId, isActive: isActive, isInserted: isInserted }))); decorations.push(rowWidget); if (intl && removedLozengeAnchorName) { decorations.push(createAnchoredRemovedLozengeWidget({ anchorName: removedLozengeAnchorName, colorScheme: colorScheme, from: safeInsertPos, intl: intl, isActive: isActive })); } if (tagWidget) { decorations.push.apply(decorations, (0, _toConsumableArray2.default)(tagWidget)); } return decorations; }); };