Переработка списков #5
@@ -67,6 +67,39 @@ function drop(row: HTMLTableRowElement): void {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Положение строк ОТНОСИТЕЛЬНО их <tbody>, а не окна.
|
||||
*
|
||||
* Это принципиально: getBoundingClientRect() отдаёт координату относительно
|
||||
* ОКНА, а прокрутка страницы не вызывает ре-рендер — значит, запомненные с
|
||||
* прошлого рендера позиции остаются от прежнего положения прокрутки. Стоит
|
||||
* пользователю прокрутить страницу к маршруту и нажать стрелку, как в сдвиг
|
||||
* попадает вся дельта прокрутки (сотни пикселей), и строки улетают за пределы
|
||||
* таблицы. Именно так выглядел баг «первое нажатие ломает, дальше нормально»:
|
||||
* повторные клики уже сравнивались с позициями, замеренными при текущей
|
||||
* прокрутке, поэтому шли правильно.
|
||||
*
|
||||
* Относительная база от этого избавляет: и строка, и <tbody> замеряются в
|
||||
* одном кадре, поэтому прокрутка сокращается. Заодно это чинит и случай, когда
|
||||
* блок маршрута целиком уезжает из-за изменений НАД ним (например, свернулся
|
||||
* блок вложений): tbody уезжает вместе со строками, и мнимого сдвига нет.
|
||||
*/
|
||||
function measureTops(nodes: Map<string, HTMLTableRowElement>): Map<string, number> {
|
||||
const tops = new Map<string, number>();
|
||||
|
||||
let firstRow: HTMLTableRowElement | undefined;
|
||||
nodes.forEach((el) => {
|
||||
if (!firstRow) firstRow = el;
|
||||
});
|
||||
if (!firstRow) return tops;
|
||||
|
||||
const container = (firstRow as HTMLTableRowElement).parentElement;
|
||||
const base = container ? container.getBoundingClientRect().top : 0;
|
||||
|
||||
nodes.forEach((el, key) => tops.set(key, el.getBoundingClientRect().top - base));
|
||||
return tops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает функцию, которая по ключу строки отдаёт ref-колбэк для её <tr>.
|
||||
* @param activeKeys ключи строк того шага, который двигает пользователь —
|
||||
@@ -126,31 +159,28 @@ export function useFlipRows(
|
||||
if (!orderChanged) {
|
||||
// Порядок тот же — просто обновляем базовую геометрию для следующего
|
||||
// ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе.
|
||||
const current = new Map<string, number>();
|
||||
nodes.current.forEach((el, key) => current.set(key, el.getBoundingClientRect().top));
|
||||
prevTops.current = current;
|
||||
prevTops.current = measureTops(nodes.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// Сначала запоминаем, где строка ВИДНА сейчас: при быстрых повторных кликах
|
||||
// она в этот момент ещё летит, и новая анимация обязана стартовать от её
|
||||
// текущего положения на экране, иначе будет рывок.
|
||||
const visualTops = new Map<string, number>();
|
||||
nodes.current.forEach((el, key) => visualTops.set(key, el.getBoundingClientRect().top));
|
||||
const visualTops = measureTops(nodes.current);
|
||||
|
||||
running.current.forEach((a) => a.cancel());
|
||||
running.current = [];
|
||||
nodes.current.forEach(drop);
|
||||
|
||||
const previous = prevTops.current;
|
||||
const current = new Map<string, number>();
|
||||
// После отмены анимаций transform снят — это истинные новые положения.
|
||||
const current = measureTops(nodes.current);
|
||||
const moved: Array<{ el: HTMLTableRowElement; key: string; shift: number }> = [];
|
||||
const entered: HTMLTableRowElement[] = [];
|
||||
|
||||
nodes.current.forEach((el, key) => {
|
||||
// После отмены анимаций transform снят — это истинное новое положение.
|
||||
const layoutTop = el.getBoundingClientRect().top;
|
||||
current.set(key, layoutTop);
|
||||
const layoutTop = current.get(key);
|
||||
if (layoutTop === undefined) return;
|
||||
|
||||
const before = previous.get(key);
|
||||
if (before === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user