refactor missing model row reconciliation
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Window } from 'happy-dom';
|
||||
import {
|
||||
html,
|
||||
normalizePathIdentity,
|
||||
@@ -2173,6 +2174,10 @@ test('content-preserving Missing Models refresh patches the browser instead of c
|
||||
missingBrowserMethodsSource,
|
||||
'patchMissingModelRowElement'
|
||||
);
|
||||
const reconcileMissingModelRows = extractMethod(
|
||||
missingBrowserMethodsSource,
|
||||
'reconcileMissingModelRows'
|
||||
);
|
||||
const setupMissingModelsVirtualizer = extractMethod(
|
||||
missingBrowserMethodsSource,
|
||||
'setupMissingModelsVirtualizer'
|
||||
@@ -2188,6 +2193,10 @@ test('content-preserving Missing Models refresh patches the browser instead of c
|
||||
|
||||
assert.match(
|
||||
patchMissingModelsBrowserElement,
|
||||
/reconcileMissingModelRows\(/
|
||||
);
|
||||
assert.match(
|
||||
reconcileMissingModelRows,
|
||||
/patchMissingModelRowElement\(currentRow, nextRow\)/
|
||||
);
|
||||
assert.match(
|
||||
@@ -2200,7 +2209,7 @@ test('content-preserving Missing Models refresh patches the browser instead of c
|
||||
);
|
||||
assert.match(
|
||||
setupMissingModelsVirtualizer,
|
||||
/patchMissingModelRowElement\(currentRow, nextRow\)/
|
||||
/reconcileMissingModelRows\(rowsHost, nextRows\)/
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
setupMissingModelsVirtualizer,
|
||||
@@ -2224,7 +2233,7 @@ test('content-preserving Missing Models refresh patches the browser instead of c
|
||||
);
|
||||
assert.match(
|
||||
patchMissingModelsBrowserElement,
|
||||
/currentList\.appendChild\(row\)/
|
||||
/currentRowsHost === currentList \? currentList : currentRowsHost/
|
||||
);
|
||||
assert.match(
|
||||
patchMissingModelsBrowserElement,
|
||||
@@ -2244,6 +2253,77 @@ test('content-preserving Missing Models refresh patches the browser instead of c
|
||||
);
|
||||
});
|
||||
|
||||
test('Missing Models browser reuses rows by key, slot, and index while removing stale rows', () => {
|
||||
const patchMissingModelsBrowserElement = eval(
|
||||
`(${extractMethod(missingBrowserMethodsSource, 'patchMissingModelsBrowserElement')})`
|
||||
);
|
||||
const patchMissingModelRowElement = eval(
|
||||
`(${extractMethod(missingBrowserMethodsSource, 'patchMissingModelRowElement')})`
|
||||
);
|
||||
const reconcileMissingModelRows = eval(
|
||||
`(${extractMethod(missingBrowserMethodsSource, 'reconcileMissingModelRows')})`
|
||||
);
|
||||
const window = new Window();
|
||||
const previousDocument = globalThis.document;
|
||||
const container = window.document.createElement('div');
|
||||
globalThis.document = window.document;
|
||||
|
||||
try {
|
||||
container.innerHTML = `
|
||||
<div class="mr-missing-browser">
|
||||
<div class="mr-missing-list">
|
||||
<div class="mr-missing-list-virtual-scroll">
|
||||
<div class="mr-missing-list-virtual-rows">
|
||||
<div class="mr-missing-list-row" data-missing-key="key-a" data-missing-index="0" data-missing-slot-keys="slot-a">old-a</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="key-b" data-missing-index="1" data-missing-slot-keys="slot-b">old-b</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="key-c" data-missing-index="2" data-missing-slot-keys="slot-c">old-c</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="stale" data-missing-index="3" data-missing-slot-keys="slot-stale">stale</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const keyRow = container.querySelector('[data-missing-key="key-a"]');
|
||||
const slotRow = container.querySelector('[data-missing-key="key-b"]');
|
||||
const indexRow = container.querySelector('[data-missing-key="key-c"]');
|
||||
const dialog = {
|
||||
_wiredMissingModelRows: new Set(),
|
||||
patchMissingModelRowElement,
|
||||
reconcileMissingModelRows,
|
||||
};
|
||||
|
||||
const patched = patchMissingModelsBrowserElement.call(dialog, container, `
|
||||
<div class="mr-missing-browser">
|
||||
<div class="mr-missing-list">
|
||||
<div class="mr-missing-list-virtual-scroll">
|
||||
<div class="mr-missing-list-virtual-rows">
|
||||
<div class="mr-missing-list-row" data-missing-key="key-a" data-missing-index="10" data-missing-slot-keys="slot-new-a">new-a</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="new-b" data-missing-index="11" data-missing-slot-keys="slot-b">new-b</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="new-c" data-missing-index="2" data-missing-slot-keys="slot-new-c">new-c</div>
|
||||
<div class="mr-missing-list-row" data-missing-key="new-d" data-missing-index="12" data-missing-slot-keys="slot-new-d">new-d</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
assert.equal(patched, true);
|
||||
const rows = Array.from(container.querySelectorAll('.mr-missing-list-row'));
|
||||
assert.deepEqual(
|
||||
rows.map(row => row.dataset.missingKey),
|
||||
['key-a', 'new-b', 'new-c', 'new-d']
|
||||
);
|
||||
assert.equal(container.querySelector('[data-missing-key="key-a"]'), keyRow);
|
||||
assert.equal(container.querySelector('[data-missing-key="new-b"]'), slotRow);
|
||||
assert.equal(container.querySelector('[data-missing-key="new-c"]'), indexRow);
|
||||
assert.equal(keyRow.textContent, 'new-a');
|
||||
assert.equal(slotRow.textContent, 'new-b');
|
||||
assert.equal(indexRow.textContent, 'new-c');
|
||||
} finally {
|
||||
globalThis.document = previousDocument;
|
||||
}
|
||||
});
|
||||
|
||||
test('cached Missing Models refresh preserves browser geometry after linking a local match', () => {
|
||||
const refreshMissingModelsBrowserFromCache = eval(
|
||||
`(${extractMethod(missingBrowserMethodsSource, 'refreshMissingModelsBrowserFromCache')})`
|
||||
|
||||
@@ -682,37 +682,6 @@ export const missingBrowserMethods = {
|
||||
state.start = nextStart;
|
||||
state.end = nextEnd;
|
||||
rowsHost.style.transform = `translateY(${Math.round(nextStart * rowHeight)}px)`;
|
||||
const currentRows = new Map(
|
||||
Array.from(rowsHost.querySelectorAll('.mr-missing-list-row'))
|
||||
.map(row => [row.dataset.missingKey || '', row])
|
||||
.filter(([key]) => key)
|
||||
);
|
||||
const currentRowsBySlot = new Map();
|
||||
const currentRowsByIndex = new Map();
|
||||
currentRows.forEach((row) => {
|
||||
const index = row.dataset.missingIndex || '';
|
||||
if (index && !currentRowsByIndex.has(index)) currentRowsByIndex.set(index, row);
|
||||
String(row.dataset.missingSlotKeys || '')
|
||||
.split('|')
|
||||
.filter(Boolean)
|
||||
.forEach(slotKey => {
|
||||
if (!currentRowsBySlot.has(slotKey)) currentRowsBySlot.set(slotKey, row);
|
||||
});
|
||||
});
|
||||
const retainedRows = new Set();
|
||||
const findCurrentRow = (nextRow) => {
|
||||
const byKey = currentRows.get(nextRow.dataset.missingKey || '');
|
||||
if (byKey && !retainedRows.has(byKey)) return byKey;
|
||||
|
||||
const bySlot = String(nextRow.dataset.missingSlotKeys || '')
|
||||
.split('|')
|
||||
.map(slotKey => currentRowsBySlot.get(slotKey))
|
||||
.find(row => row && !retainedRows.has(row));
|
||||
if (bySlot) return bySlot;
|
||||
|
||||
const byIndex = currentRowsByIndex.get(nextRow.dataset.missingIndex || '');
|
||||
return byIndex && !retainedRows.has(byIndex) ? byIndex : null;
|
||||
};
|
||||
const nextRowsTemplate = document.createElement('template');
|
||||
nextRowsTemplate.innerHTML = state.models
|
||||
.slice(nextStart, nextEnd)
|
||||
@@ -727,17 +696,7 @@ export const missingBrowserMethods = {
|
||||
nextRowsTemplate.content.querySelectorAll('.mr-missing-list-row')
|
||||
);
|
||||
|
||||
nextRows.forEach((nextRow) => {
|
||||
const currentRow = findCurrentRow(nextRow);
|
||||
const row = currentRow
|
||||
? this.patchMissingModelRowElement(currentRow, nextRow)
|
||||
: nextRow;
|
||||
retainedRows.add(row);
|
||||
rowsHost.appendChild(row);
|
||||
});
|
||||
currentRows.forEach((row) => {
|
||||
if (!retainedRows.has(row)) row.remove();
|
||||
});
|
||||
this.reconcileMissingModelRows(rowsHost, nextRows);
|
||||
this.wireVisibleMissingModelRows(container, state.data, state.models);
|
||||
};
|
||||
|
||||
@@ -919,37 +878,7 @@ export const missingBrowserMethods = {
|
||||
return html;
|
||||
},
|
||||
|
||||
patchMissingModelsBrowserElement(container, html) {
|
||||
const currentBrowser = container?.querySelector?.('.mr-missing-browser');
|
||||
if (!currentBrowser || typeof document === 'undefined') return false;
|
||||
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = String(html || '').trim();
|
||||
const nextBrowser = template.content.firstElementChild;
|
||||
const currentList = currentBrowser.querySelector('.mr-missing-list');
|
||||
const nextList = nextBrowser?.querySelector?.('.mr-missing-list');
|
||||
if (!nextBrowser || !currentList || !nextList) return false;
|
||||
|
||||
currentBrowser.style.cssText = nextBrowser.style.cssText;
|
||||
const currentToolbar = currentBrowser.querySelector('.mr-missing-list-toolbar');
|
||||
const nextToolbar = nextBrowser.querySelector('.mr-missing-list-toolbar');
|
||||
if (currentToolbar && nextToolbar) {
|
||||
currentToolbar.replaceWith(nextToolbar);
|
||||
}
|
||||
|
||||
const currentHead = currentList.querySelector('.mr-missing-list-head');
|
||||
const nextHead = nextList.querySelector('.mr-missing-list-head');
|
||||
if (currentHead && nextHead) {
|
||||
currentHead.replaceWith(nextHead);
|
||||
}
|
||||
|
||||
const currentVirtualScroll = currentList.querySelector('.mr-missing-list-virtual-scroll');
|
||||
const nextVirtualScroll = nextList.querySelector('.mr-missing-list-virtual-scroll');
|
||||
if (currentVirtualScroll && nextVirtualScroll) {
|
||||
currentVirtualScroll.style.height = nextVirtualScroll.style.height;
|
||||
}
|
||||
const currentRowsHost = currentVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || currentList;
|
||||
const nextRowsHost = nextVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || nextList;
|
||||
reconcileMissingModelRows(currentRowsHost, nextRows, appendTarget = currentRowsHost) {
|
||||
const currentRows = new Map(
|
||||
Array.from(currentRowsHost.querySelectorAll('.mr-missing-list-row'))
|
||||
.map(row => [row.dataset.missingKey || '', row])
|
||||
@@ -981,21 +910,56 @@ export const missingBrowserMethods = {
|
||||
const byIndex = currentRowsByIndex.get(nextRow.dataset.missingIndex || '');
|
||||
return byIndex && !retainedRows.has(byIndex) ? byIndex : null;
|
||||
};
|
||||
nextRowsHost.querySelectorAll('.mr-missing-list-row').forEach((nextRow) => {
|
||||
|
||||
nextRows.forEach((nextRow) => {
|
||||
const currentRow = findCurrentRow(nextRow);
|
||||
const row = currentRow
|
||||
? this.patchMissingModelRowElement(currentRow, nextRow)
|
||||
: nextRow;
|
||||
retainedRows.add(row);
|
||||
if (currentRowsHost === currentList) {
|
||||
currentList.appendChild(row);
|
||||
} else {
|
||||
currentRowsHost.appendChild(row);
|
||||
}
|
||||
appendTarget.appendChild(row);
|
||||
});
|
||||
currentRows.forEach((row) => {
|
||||
if (!retainedRows.has(row)) row.remove();
|
||||
});
|
||||
},
|
||||
|
||||
patchMissingModelsBrowserElement(container, html) {
|
||||
const currentBrowser = container?.querySelector?.('.mr-missing-browser');
|
||||
if (!currentBrowser || typeof document === 'undefined') return false;
|
||||
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = String(html || '').trim();
|
||||
const nextBrowser = template.content.firstElementChild;
|
||||
const currentList = currentBrowser.querySelector('.mr-missing-list');
|
||||
const nextList = nextBrowser?.querySelector?.('.mr-missing-list');
|
||||
if (!nextBrowser || !currentList || !nextList) return false;
|
||||
|
||||
currentBrowser.style.cssText = nextBrowser.style.cssText;
|
||||
const currentToolbar = currentBrowser.querySelector('.mr-missing-list-toolbar');
|
||||
const nextToolbar = nextBrowser.querySelector('.mr-missing-list-toolbar');
|
||||
if (currentToolbar && nextToolbar) {
|
||||
currentToolbar.replaceWith(nextToolbar);
|
||||
}
|
||||
|
||||
const currentHead = currentList.querySelector('.mr-missing-list-head');
|
||||
const nextHead = nextList.querySelector('.mr-missing-list-head');
|
||||
if (currentHead && nextHead) {
|
||||
currentHead.replaceWith(nextHead);
|
||||
}
|
||||
|
||||
const currentVirtualScroll = currentList.querySelector('.mr-missing-list-virtual-scroll');
|
||||
const nextVirtualScroll = nextList.querySelector('.mr-missing-list-virtual-scroll');
|
||||
if (currentVirtualScroll && nextVirtualScroll) {
|
||||
currentVirtualScroll.style.height = nextVirtualScroll.style.height;
|
||||
}
|
||||
const currentRowsHost = currentVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || currentList;
|
||||
const nextRowsHost = nextVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || nextList;
|
||||
this.reconcileMissingModelRows(
|
||||
currentRowsHost,
|
||||
Array.from(nextRowsHost.querySelectorAll('.mr-missing-list-row')),
|
||||
currentRowsHost === currentList ? currentList : currentRowsHost
|
||||
);
|
||||
|
||||
const currentSplitter = currentBrowser.querySelector('.mr-missing-browser-splitter');
|
||||
const nextSplitter = nextBrowser.querySelector('.mr-missing-browser-splitter');
|
||||
|
||||
Reference in New Issue
Block a user