[MM-63799] Fix the high number of detached nodes in post list in dynamic-virtualized-list (#30864)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ee5926f03c
Коммит
c6a11763a8
@@ -71,7 +71,7 @@ describe('Collapsed Reply Threads', () => {
|
|||||||
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
|
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
|
||||||
|
|
||||||
// # Post a reply post as current user
|
// # Post a reply post as current user
|
||||||
cy.postMessageAs({sender: testUser, message: 'reply!', channelId: testChannel.id, rootId: rootPost.id});
|
cy.postMessageAs({sender: testUser, message: 'reply to root post', channelId: testChannel.id, rootId: rootPost.id});
|
||||||
|
|
||||||
// # Get thread footer of last post
|
// # Get thread footer of last post
|
||||||
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
|
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ describe('Leave Channel Command', () => {
|
|||||||
it('Should be redirected to last channel when user leaves channel with /leave command', () => {
|
it('Should be redirected to last channel when user leaves channel with /leave command', () => {
|
||||||
// # Go to newly created channel
|
// # Go to newly created channel
|
||||||
cy.get('#sidebarItem_' + testChannel.name).click({force: true});
|
cy.get('#sidebarItem_' + testChannel.name).click({force: true});
|
||||||
cy.findAllByTestId('postView').should('be.visible');
|
|
||||||
|
cy.findAllByTestId('postView').last().scrollIntoView().should('be.visible');
|
||||||
|
|
||||||
// # Post /leave command in center channel
|
// # Post /leave command in center channel
|
||||||
cy.postMessage('/leave ');
|
cy.postMessage('/leave ');
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ describe('Teams Suite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cy.get('#sidebarItem_off-topic').should('be.visible').click({force: true});
|
cy.get('#sidebarItem_off-topic').should('be.visible').click({force: true});
|
||||||
|
|
||||||
|
cy.findAllByTestId('postView').last().scrollIntoView();
|
||||||
|
|
||||||
cy.getLastPost().wait(TIMEOUTS.HALF_SEC).then(($el) => {
|
cy.getLastPost().wait(TIMEOUTS.HALF_SEC).then(($el) => {
|
||||||
cy.wrap($el).get('.user-popover').
|
cy.wrap($el).get('.user-popover').
|
||||||
should('be.visible').
|
should('be.visible').
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ Cypress.Commands.add('uiClickSidebarItem', (name) => {
|
|||||||
});
|
});
|
||||||
cy.get('#tutorial-threads-mobile-header span.Button_label').contains('Followed threads');
|
cy.get('#tutorial-threads-mobile-header span.Button_label').contains('Followed threads');
|
||||||
} else {
|
} else {
|
||||||
cy.findAllByTestId('postView').should('be.visible');
|
cy.findAllByTestId('postView').last().scrollIntoView().should('be.visible');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.dynamic_virtualized_list {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-anchor: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
.innerList[role='list'] {
|
||||||
|
padding: 14px 0px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem[role='listitem'] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import {createElement, PureComponent} from 'react';
|
import {createElement, PureComponent} from 'react';
|
||||||
|
|
||||||
import ItemMeasurer from './item_measurer';
|
import ListItem from './item_row_shared';
|
||||||
|
|
||||||
const atBottomMargin = 10;
|
const atBottomMargin = 10;
|
||||||
|
|
||||||
@@ -612,11 +612,9 @@ export class DynamicVirtualizedList extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_renderItems = () => {
|
_renderItems = () => {
|
||||||
const {children, direction, itemData, loaderId, visibleId} =
|
|
||||||
this.props;
|
|
||||||
const width = this.innerRefWidth;
|
const width = this.innerRefWidth;
|
||||||
const [startIndex, stopIndex] = this._getRangeToRender();
|
const [startIndex, stopIndex] = this._getRangeToRender();
|
||||||
const itemCount = itemData.length;
|
const itemCount = this.props.itemData.length;
|
||||||
const items = [];
|
const items = [];
|
||||||
if (itemCount > 0) {
|
if (itemCount > 0) {
|
||||||
for (let index = itemCount - 1; index >= 0; index--) {
|
for (let index = itemCount - 1; index >= 0; index--) {
|
||||||
@@ -636,9 +634,9 @@ export class DynamicVirtualizedList extends PureComponent {
|
|||||||
index < localOlderPostsToRenderStopIndex + 1 &&
|
index < localOlderPostsToRenderStopIndex + 1 &&
|
||||||
localOlderPostsToRenderStartIndex === stopIndex + 1;
|
localOlderPostsToRenderStartIndex === stopIndex + 1;
|
||||||
|
|
||||||
const isLoader = itemData[index] === loaderId;
|
const isLoader = this.props.itemData[index] === this.props.loaderId;
|
||||||
const isVisible = itemData[index] === visibleId;
|
const isVisible = this.props.itemData[index] === this.props.visibleId;
|
||||||
const itemId = itemData[index];
|
const itemId = this.props.itemData[index];
|
||||||
|
|
||||||
// It's important to read style after fetching item metadata.
|
// It's important to read style after fetching item metadata.
|
||||||
// getItemMetadata() will clear stale styles.
|
// getItemMetadata() will clear stale styles.
|
||||||
@@ -649,24 +647,22 @@ export class DynamicVirtualizedList extends PureComponent {
|
|||||||
isLoader ||
|
isLoader ||
|
||||||
isVisible
|
isVisible
|
||||||
) {
|
) {
|
||||||
const item = createElement(children, {
|
const item = createElement(this.props.children, {
|
||||||
data: itemData,
|
data: this.props.itemData,
|
||||||
itemId,
|
itemId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always wrap children in a ItemMeasurer to detect changes in size.
|
// Always wrap children in a ItemRow to detect changes in size.
|
||||||
items.push(
|
items.push(
|
||||||
createElement(ItemMeasurer, {
|
createElement(ListItem, {
|
||||||
direction,
|
key: itemId,
|
||||||
handleNewMeasurements: this._handleNewMeasurements,
|
|
||||||
index,
|
index,
|
||||||
item,
|
item,
|
||||||
key: itemId,
|
|
||||||
size,
|
|
||||||
itemId,
|
itemId,
|
||||||
|
height: size,
|
||||||
width,
|
width,
|
||||||
|
onHeightChange: this._handleNewMeasurements,
|
||||||
onUnmount: this._onItemRowUnmount,
|
onUnmount: this._onItemRowUnmount,
|
||||||
itemCount,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -718,6 +714,7 @@ export class DynamicVirtualizedList extends PureComponent {
|
|||||||
{
|
{
|
||||||
ref: innerRef,
|
ref: innerRef,
|
||||||
role: 'list',
|
role: 'list',
|
||||||
|
className: 'innerList',
|
||||||
style: innerListStyle,
|
style: innerListStyle,
|
||||||
},
|
},
|
||||||
items,
|
items,
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
/* eslint-disable no-underscore-dangle */
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {findDOMNode} from 'react-dom';
|
|
||||||
|
|
||||||
import {isSafari} from 'utils/user_agent';
|
|
||||||
|
|
||||||
const scrollBarWidth = 8;
|
|
||||||
const scrollableContainerStyles = {
|
|
||||||
display: 'inline',
|
|
||||||
width: '0px',
|
|
||||||
height: '0px',
|
|
||||||
zIndex: '-1',
|
|
||||||
overflow: 'hidden',
|
|
||||||
margin: '0px',
|
|
||||||
padding: '0px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollableWrapperStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
flex: '0 0 auto',
|
|
||||||
overflow: 'hidden',
|
|
||||||
visibility: 'hidden',
|
|
||||||
zIndex: '-1',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
left: '0px',
|
|
||||||
top: '0px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandShrinkContainerStyles = {
|
|
||||||
flex: '0 0 auto',
|
|
||||||
overflow: 'hidden',
|
|
||||||
zIndex: '-1',
|
|
||||||
visibility: 'hidden',
|
|
||||||
left: `-${scrollBarWidth + 1}px`, //8px(scrollbar width) + 1px
|
|
||||||
bottom: `-${scrollBarWidth}px`, //8px because of scrollbar width
|
|
||||||
right: `-${scrollBarWidth}px`, //8px because of scrollbar width
|
|
||||||
top: `-${scrollBarWidth + 1}px`, //8px(scrollbar width) + 1px
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandShrinkStyles = {
|
|
||||||
position: 'absolute',
|
|
||||||
flex: '0 0 auto',
|
|
||||||
visibility: 'hidden',
|
|
||||||
overflow: 'scroll',
|
|
||||||
zIndex: '-1',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
};
|
|
||||||
|
|
||||||
const shrinkChildStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
height: '200%',
|
|
||||||
width: '200%',
|
|
||||||
};
|
|
||||||
|
|
||||||
//values below need to be changed when scrollbar width changes
|
|
||||||
const shrinkScrollDelta = (2 * scrollBarWidth) + 1; // 17 = 2* scrollbar width(8px) + 1px as buffer
|
|
||||||
|
|
||||||
// 27 = 2* scrollbar width(8px) + 1px as buffer + 10px(this value is based of off lib(Link below). Probably not needed but doesnt hurt to leave)
|
|
||||||
//https://github.com/wnr/element-resize-detector/blob/27983e59dce9d8f1296d8f555dc2340840fb0804/src/detection-strategy/scroll.js#L246
|
|
||||||
const expandScrollDelta = shrinkScrollDelta + 10;
|
|
||||||
|
|
||||||
export default class ItemMeasurer extends PureComponent {
|
|
||||||
_node = null;
|
|
||||||
_resizeSensorExpand = React.createRef();
|
|
||||||
_resizeSensorShrink = React.createRef();
|
|
||||||
_positionScrollbarsRef = null;
|
|
||||||
_measureItemAnimFrame = null;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// eslint-disable-next-line react/no-find-dom-node
|
|
||||||
this._node = findDOMNode(this);
|
|
||||||
|
|
||||||
// Force sync measure for the initial mount.
|
|
||||||
// This is necessary to support the DynamicSizeList layout logic.
|
|
||||||
if (isSafari() && this.props.size) {
|
|
||||||
this._measureItemAnimFrame = requestAnimationFrame(() => {
|
|
||||||
this._measureItem(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._measureItem(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.size) {
|
|
||||||
// Don't wait for positioning scrollbars when we have size
|
|
||||||
// This is needed triggering an event for remounting a post
|
|
||||||
this.positionScrollBars();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if ((prevProps.size === 0 && this.props.size !== 0) || prevProps.size !== this.props.size) {
|
|
||||||
this.positionScrollBars();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_measureItem = (forceScrollCorrection) => {
|
|
||||||
const {handleNewMeasurements, size: oldSize, itemId} = this.props;
|
|
||||||
|
|
||||||
const node = this._node;
|
|
||||||
|
|
||||||
if (node && node.ownerDocument && node.ownerDocument.defaultView && node instanceof node.ownerDocument.defaultView.HTMLElement) {
|
|
||||||
const newSize = Math.ceil(node.offsetHeight);
|
|
||||||
|
|
||||||
if (oldSize !== newSize) {
|
|
||||||
handleNewMeasurements(itemId, newSize, forceScrollCorrection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
positionScrollBars = (height = this.props.size) => {
|
|
||||||
//we are position these hidden div scroll bars to the end so they can emit
|
|
||||||
//scroll event when height in the div changes
|
|
||||||
//Heavily inspired from https://github.com/marcj/css-element-queries/blob/master/src/ResizeSensor.js
|
|
||||||
//and https://github.com/wnr/element-resize-detector/blob/master/src/detection-strategy/scroll.js
|
|
||||||
//For more info http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/#comment-244
|
|
||||||
if (this._positionScrollbarsRef) {
|
|
||||||
window.cancelAnimationFrame(this._positionScrollbarsRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._positionScrollbarsRef = window.requestAnimationFrame(() => {
|
|
||||||
this._resizeSensorExpand.current.scrollTop = height + expandScrollDelta;
|
|
||||||
this._resizeSensorShrink.current.scrollTop = (2 * height) + shrinkScrollDelta;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollingDiv = (event) => {
|
|
||||||
if (event.target.offsetHeight !== this.props.size) {
|
|
||||||
this._measureItem(event.target.offsetWidth !== this.props.width);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderItems = () => {
|
|
||||||
const item = this.props.item;
|
|
||||||
|
|
||||||
const expandChildStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
left: '0',
|
|
||||||
top: '0',
|
|
||||||
height: `${this.props.size + expandScrollDelta}px`,
|
|
||||||
width: '100%',
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderItem = (
|
|
||||||
<div
|
|
||||||
role='listitem'
|
|
||||||
style={{position: 'relative'}}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
<div style={scrollableContainerStyles}>
|
|
||||||
<div
|
|
||||||
dir='ltr'
|
|
||||||
style={scrollableWrapperStyle}
|
|
||||||
>
|
|
||||||
<div style={expandShrinkContainerStyles}>
|
|
||||||
<div
|
|
||||||
style={expandShrinkStyles}
|
|
||||||
ref={this._resizeSensorExpand}
|
|
||||||
onScroll={this.scrollingDiv}
|
|
||||||
>
|
|
||||||
<div style={expandChildStyle}/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={expandShrinkStyles}
|
|
||||||
ref={this._resizeSensorShrink}
|
|
||||||
onScroll={this.scrollingDiv}
|
|
||||||
>
|
|
||||||
<div style={shrinkChildStyle}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return renderItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._positionScrollbarsRef) {
|
|
||||||
window.cancelAnimationFrame(this._positionScrollbarsRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._measureItemAnimFrame) {
|
|
||||||
window.cancelAnimationFrame(this._measureItemAnimFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {onUnmount, itemId, index} = this.props;
|
|
||||||
if (onUnmount) {
|
|
||||||
onUnmount(itemId, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.renderItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import React, {memo, useEffect, useRef} from 'react';
|
||||||
|
|
||||||
|
import {ListItemSizeObserver} from './item_row_size_observer';
|
||||||
|
|
||||||
|
const RESIZE_DEBOUNCE_TIME = 200; // in ms
|
||||||
|
|
||||||
|
const listItemSizeObserver = ListItemSizeObserver.getInstance();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: ReactNode;
|
||||||
|
itemId: string;
|
||||||
|
index: number;
|
||||||
|
height: number;
|
||||||
|
width?: number; // Its optional since it may not be available when the parent component is mounted
|
||||||
|
onHeightChange: (itemId: string, height: number, forceScrollCorrection: boolean) => void;
|
||||||
|
onUnmount: (itemId: string, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is used to measure the height of a row and update the height of the row when it changes.
|
||||||
|
* Uses a shared ResizeObserver instance for better performance with many items.
|
||||||
|
*/
|
||||||
|
const ListItem = (props: Props) => {
|
||||||
|
const rowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const heightRef = useRef(props.height);
|
||||||
|
const widthRef = useRef(props.width);
|
||||||
|
const indexRef = useRef(props.index);
|
||||||
|
|
||||||
|
// This prevents stale closures in the ResizeObserver callback
|
||||||
|
// and ensures event handlers always have the latest values
|
||||||
|
// we also update these refs whenever their source values change
|
||||||
|
useEffect(() => {
|
||||||
|
heightRef.current = props.height;
|
||||||
|
widthRef.current = props.width;
|
||||||
|
indexRef.current = props.index;
|
||||||
|
}, [props.itemId, props.height, props.width, props.index]);
|
||||||
|
|
||||||
|
// This effect is used to measure the height of the row as soon as the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
const newHeight = Math.ceil(rowRef?.current?.offsetHeight ?? 0);
|
||||||
|
props.onHeightChange(props.itemId, newHeight, false);
|
||||||
|
}, [props.itemId]);
|
||||||
|
|
||||||
|
// This effects adds the observer which calls height change callback debounced
|
||||||
|
useEffect(() => {
|
||||||
|
const debouncedOnHeightChange = debounce((changedHeight: number) => {
|
||||||
|
// If width of container has changed then scroll bar position will be out of sync
|
||||||
|
// so we need to force a scroll correction
|
||||||
|
const forceScrollCorrection = rowRef.current?.offsetWidth !== widthRef.current;
|
||||||
|
|
||||||
|
heightRef.current = changedHeight;
|
||||||
|
|
||||||
|
props.onHeightChange(props.itemId, changedHeight, forceScrollCorrection);
|
||||||
|
}, RESIZE_DEBOUNCE_TIME);
|
||||||
|
|
||||||
|
function itemRowSizeObserverCallback(changedHeight: number) {
|
||||||
|
if (!rowRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedHeight !== heightRef.current) {
|
||||||
|
debouncedOnHeightChange(changedHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanupSizeObserver: () => void;
|
||||||
|
|
||||||
|
// We add the observer here to a row
|
||||||
|
if (rowRef.current) {
|
||||||
|
cleanupSizeObserver = listItemSizeObserver.observe(props.itemId, rowRef.current, itemRowSizeObserverCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We remove the observer here from a row
|
||||||
|
return () => {
|
||||||
|
if (cleanupSizeObserver) {
|
||||||
|
cleanupSizeObserver();
|
||||||
|
}
|
||||||
|
props.onUnmount(props.itemId, indexRef.current);
|
||||||
|
};
|
||||||
|
}, [props.itemId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={rowRef}
|
||||||
|
role='listitem'
|
||||||
|
className='item_measurer'
|
||||||
|
>
|
||||||
|
{props.item}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ListItem);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
type TrackedItemCallback = (changedHeight: number) => void;
|
||||||
|
type TrackedItemData = {element: Element; callback: TrackedItemCallback};
|
||||||
|
type TrackedItemsMap = Map<string, TrackedItemData>;
|
||||||
|
|
||||||
|
export class ListItemSizeObserver {
|
||||||
|
private observer: ResizeObserver;
|
||||||
|
|
||||||
|
private trackedItems: TrackedItemsMap = new Map();
|
||||||
|
|
||||||
|
private static instance: ListItemSizeObserver | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.observer = new ResizeObserver(this.handleResizeObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ListItemSizeObserver {
|
||||||
|
if (!ListItemSizeObserver.instance) {
|
||||||
|
// Following class based singleton pattern to avoid multiple instances of the observer
|
||||||
|
ListItemSizeObserver.instance = new ListItemSizeObserver();
|
||||||
|
}
|
||||||
|
return ListItemSizeObserver.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResizeObserver = (resizeEntries: ResizeObserverEntry[]) => {
|
||||||
|
resizeEntries.forEach((resizeEntry) => {
|
||||||
|
const resizedElement = resizeEntry.target;
|
||||||
|
|
||||||
|
let itemData: TrackedItemData | undefined;
|
||||||
|
for (const [, trackedItemData] of this.trackedItems.entries()) {
|
||||||
|
// Reverse lookup by element to get the item's data
|
||||||
|
if (trackedItemData.element === resizedElement) {
|
||||||
|
itemData = trackedItemData;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedHeight = Math.ceil(resizeEntry.borderBoxSize[0].blockSize);
|
||||||
|
itemData.callback(changedHeight);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public observe(itemId: string, element: Element, callback: TrackedItemCallback): () => void {
|
||||||
|
this.trackedItems.set(itemId, {element, callback});
|
||||||
|
this.observer.observe(element);
|
||||||
|
|
||||||
|
return () => this.unobserve(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unobserve(itemId: string): void {
|
||||||
|
const trackedItemToUnobserve = this.trackedItems.get(itemId);
|
||||||
|
if (trackedItemToUnobserve) {
|
||||||
|
this.observer.unobserve(trackedItemToUnobserve.element);
|
||||||
|
this.trackedItems.delete(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ссылка в новой задаче
Block a user