UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

195 lines (181 loc) 8.87 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; 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; } import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document'; import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils'; import { Decoration } from '@atlaskit/editor-prosemirror/view'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { addedCellOverlayStyle, deletedRowStyle, deletedCellOverlayStyle } from './colorSchemes/standard'; import { deletedTraditionalRowStyle, deletedTraditionalCellOverlayStyle, traditionalAddedCellOverlayStyle } from './colorSchemes/traditional'; import { findSafeInsertPos } from './utils/findSafeInsertPos'; /** * Extracts information about deleted table rows from a change */ var extractChangedRows = function extractChangedRows(change, originalDoc, newDoc) { var changedRows = []; // Find the table in the original document var $fromPos = originalDoc.resolve(change.fromA); var tableOld = findParentNodeClosestToPos($fromPos, function (node) { return node.type.name === 'table'; }); if (!tableOld) { return changedRows; } var oldTableMap = TableMap.get(tableOld.node); // Find the table in the new document at the insertion point var $newPos = newDoc.resolve(change.fromB); var tableNew = findParentNodeClosestToPos($newPos, function (node) { return node.type.name === 'table'; }); if (!tableNew) { return changedRows; } var newTableMap = TableMap.get(tableNew.node); // If no rows were changed, return empty if (oldTableMap.height <= newTableMap.height || // 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 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' && !isEmptyRow(rowNode)) { 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, 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 return changedRows.filter(function (changedRow) { return !tableNew.node.children.some(function (newRow) { return areNodesEqualIgnoreAttrs(newRow, changedRow.rowNode); }); }); }; /** * Checks if a table row is empty (contains no meaningful content) */ var isEmptyRow = function isEmptyRow(rowNode) { var isEmpty = true; rowNode.descendants(function (node) { if (!isEmpty) { return false; } // If we find any inline content with size > 0, the row is not empty if (node.isInline && node.nodeSize > 0) { isEmpty = false; return false; } // If we find text content, the row is not empty if (node.isText && node.text && node.text.trim() !== '') { isEmpty = false; return false; } return true; }); return isEmpty; }; /** * Creates a DOM representation of a deleted table row */ var createChangedRowDOM = function createChangedRowDOM(rowNode, nodeViewSerializer, colorScheme, isInserted) { var tr = document.createElement('tr'); if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { if (!isInserted) { tr.setAttribute('style', colorScheme === 'traditional' ? deletedTraditionalRowStyle : deletedRowStyle); } } else { tr.setAttribute('style', colorScheme === 'traditional' ? deletedTraditionalRowStyle : deletedRowStyle); } tr.setAttribute('data-testid', 'show-diff-deleted-row'); // Serialize each cell in the row rowNode.content.forEach(function (cellNode) { if (cellNode.type.name === 'tableCell' || cellNode.type.name === 'tableHeader') { var nodeView = nodeViewSerializer.tryCreateNodeView(cellNode); if (nodeView) { if (isInserted && nodeView instanceof HTMLElement && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { var overlay = document.createElement('span'); var overlayStyle = colorScheme === 'traditional' ? isInserted ? traditionalAddedCellOverlayStyle : deletedTraditionalCellOverlayStyle : isInserted ? addedCellOverlayStyle : deletedCellOverlayStyle; 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); } } } }); return tr; }; /** * Expands a diff to include whole changed rows when table rows are affected */ var expandDiffForChangedRows = function expandDiffForChangedRows(changes, originalDoc, 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, originalDoc, newDoc); if (changedRows.length > 0) { rowInfo.push.apply(rowInfo, _toConsumableArray(changedRows)); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return rowInfo; }; /** * Main function to handle deleted rows - computes diff and creates decorations */ export var createChangedRowDecorationWidgets = function createChangedRowDecorationWidgets(_ref) { var changes = _ref.changes, originalDoc = _ref.originalDoc, newDoc = _ref.newDoc, nodeViewSerializer = _ref.nodeViewSerializer, colorScheme = _ref.colorScheme, _ref$isInserted = _ref.isInserted, isInserted = _ref$isInserted === void 0 ? false : _ref$isInserted; // First, expand the changes to include complete deleted rows var changedRows = expandDiffForChangedRows(changes.filter(function (change) { return change.deleted.length > 0; }), originalDoc, newDoc); return changedRows.map(function (changedRow) { var rowDOM = createChangedRowDOM(changedRow.rowNode, nodeViewSerializer, colorScheme, isInserted); // Find safe insertion position for the deleted row var safeInsertPos = findSafeInsertPos(newDoc, changedRow.fromB - 1, // -1 to find the first safe position from the table originalDoc.slice(changedRow.fromA, changedRow.toA)); return Decoration.widget(safeInsertPos, rowDOM, {}); }); };