Skip to content

Instantly share code, notes, and snippets.

@dmitrysurkin
Created January 25, 2024 08:44
Show Gist options
  • Save dmitrysurkin/807a81d7a750225f3361fee56fb060bc to your computer and use it in GitHub Desktop.
Save dmitrysurkin/807a81d7a750225f3361fee56fb060bc to your computer and use it in GitHub Desktop.
scroll
import React, { Fragment, PureComponent } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import loadable from '@loadable/component';
import { Text } from '@avito/mobile-components';
import { role } from '@avito/utils';
import { withToggles } from '@avito-core/toggles';
import { throttle } from '@avito/utils/src/helpers';
import { sources } from '@plugins/withFavorite';
import { getLogParams } from '@plugins/withLogObserver';
import withABCentral from '@plugins/withABCentral';
import withUserInfo from '@plugins/withUserInfo';
import { OpenMessengerContext } from '@plugins/withOpenMessenger';
import { MeasureContent } from '@modules/CustomMetrics/MeasureContent';
import { WIDGET_TYPE } from '@modules/Search/constants'
import RecommendationCarouselWidget from '@modules/Widgets/components/RecommendationCarouselWidget/RecommendationCarouselWidget.tsx';
import ItemsCarouselWidget from '@modules/Widgets/components/ItemsCarouselWidget/ItemsCarouselWidget';
// Components
import LazyLoadComponent from '@components/LazyLoad/LazyLoadComponent';
import trackWindowScroll from '@components/LazyLoad/TrackWindowScroll';
import Loader from '@components/Loader/Loader';
import JobVacancyDisclaimer from '@components/JobVacancyDisclaimer/JobVacancyDisclaimer';
import RenderChunks from '@components/RenderChunks/RenderChunks';
import DevelopmentsCatalogPromo from '@components/DevelopmentsCatalogPromo/DevelopmentsCatalogPromo';
import DevelopmentsAdviceButtons from '@components/DevelopmentsAdviceButtons/DevelopmentsAdviceButtons';
import DevelopmentsAdviceCarousel from '@components/DevelopmentsAdviceCarousel/DevelopmentsAdviceCarousel';
import QueryTags from '@components/QueryTags/QueryTags';
import CrossCategoryWidget from '@components/CrossCategoryWidget/CrossCategoryWidget';
import RecentQuerySearch from '@components/RecentQuerySearch/RecentQuerySearch';
import { BrandspaceWidget } from '../BrandspaceWidget';
// Utils
import clickStream from '@utils/ClickStream';
import { getSearchKeysMapForAB } from '../../utils/ads/keyMaps';
import { getTestWithRelevantAdsForGoods } from '@utils/AbCentral';
import {
GOODS_DISABLED_POSITIONS_TOGGLE,
isAdsPositionEnabled,
REALTY_DISABLED_POSITIONS_TOGGLE,
SERVICES_DISABLED_POSITIONS_TOGGLE,
TRANSPORT_DISABLED_POSITIONS_TOGGLE
} from '../../utils/ads/disabledAdsPosition';
import { CATEGORY_ID_VACANCY } from '@constants/category';
import {
JOB_DISCLAIMER_ENABLED_TOGGLE,
SHOW_ITEMS_CAROUSEL_WIDGET_SERP
} from '@constants/toggles';
import Item from '../Item/Item';
import WarningTile from '../WarningTile/WarningTile';
import PrimaryFlatButton from '../PrimaryFlatButton/PrimaryFlatButton';
import SearchGroupTitle from '../SearchGroupTitle/SearchGroupTitle';
import SearchNoResult from '../SearchNoResult/SearchNoResult';
import MapBanner from '../MapBanner/MapBanner';
import Witcher from '../Witcher/Witcher';
import SnakeTab from '../SnakeTab/SnakeTab';
import Snippet from '../Snippet/Snippet';
import { FilterTab } from '../FilterTab/FilterTab';
import { Observer } from '../Observer/Observer';
import { SellerItem } from '../SellerItem';
// Styles
import skeletonAnimationStyles from '@components/Skeletons/AnimatedSkeleton.css';
import styles from './Items.css';
// Lazy components
const SearchTitle = loadable(() => import('@components/SearchTitle/SearchTitle'));
const MAX_SIZE_SKELETON = 10;
const createSkeletonItems = (length) => {
return new Array(length).fill({ type: 'skeleton' });
};
// тип группировок - дубли
const EXPANDED_DOUBLES = 4;
// Максиальное кол-во страниц (далее появляется кнопка "Загрузить еще")
const MAX_PAGE = 4;
// Отступ до начала подгрузки
const THRESHOLD = 320;
// Максиальное кол-во загружаемых объявлений
const LIMIT_ITEMS = 5000;
// Кол-во объявлений на одной странице
const PAGE_SIZE = 30;
const JOB_VACANCY_DISCLAIMER_ITEM_TYPE = 'jobVacancyDisclaimer';
class Items extends PureComponent {
static propTypes = {
user: PropTypes.object,
items: PropTypes.array,
itemsLength: PropTypes.number,
isFixedSizeSkeleton: PropTypes.bool,
isRich: PropTypes.bool,
isVacancy: PropTypes.bool,
isVerticalMain: PropTypes.bool,
isReVertical: PropTypes.bool,
threshold: PropTypes.number,
expanded: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
shouldRenderMapBanner: PropTypes.bool,
shouldRenderDividers: PropTypes.bool,
hasPaddingForLoad: PropTypes.bool,
isFullMap: PropTypes.bool,
isFetch: PropTypes.bool,
isFetchFailed: PropTypes.bool,
onLoadMore: PropTypes.func,
onCall: PropTypes.func,
onClickWitcher: PropTypes.func,
onClickMapBanner: PropTypes.func,
onClickExpandedLink: PropTypes.func,
notMoreItems: PropTypes.bool,
renderAds: PropTypes.func,
activePage: PropTypes.number,
categoryId: PropTypes.number,
segment: PropTypes.string,
headerHeight: PropTypes.number,
navHeight: PropTypes.number,
history: PropTypes.object,
visibleByDefaultCount: PropTypes.number,
parentNode: PropTypes.object,
showAdsPlaceholders: PropTypes.bool,
showSkeleton: PropTypes.bool,
onViewItem: PropTypes.func,
abCentral: PropTypes.object,
mobileInfo: PropTypes.object,
renderInChunks: PropTypes.bool,
displayType: PropTypes.string,
onItemsReachLimit: PropTypes.func,
onSendSnippetBannerEvent: PropTypes.func,
onScroll: PropTypes.func,
getItems: PropTypes.func,
hideRecentSearch: PropTypes.func,
// Для избранного
onClickFavorite: PropTypes.func,
favorites: PropTypes.object,
// HOC trackWindowScroll
scrollPosition: PropTypes.any,
toggles: PropTypes.object,
xHash: PropTypes.string,
isBackendAdvMixing: PropTypes.bool,
isLoading: PropTypes.bool,
ignorePaddings: PropTypes.bool,
ignoreScroll: PropTypes.bool
};
static defaultProps = {
favorites: {},
mobileInfo: {},
isRich: false,
isVacancy: false,
isReVertical: false,
expanded: false,
threshold: THRESHOLD,
shouldRenderDividers: false,
hasPaddingForLoad: false,
visibleByDefaultCount: 0,
showAdsPlaceholders: false,
showSkeleton: false,
isBackendAdvMixing: false,
isLoading: false,
ignorePaddings: false,
ignoreScroll: false,
onItemsReachLimit: () => { },
onScroll: () => { }
};
constructor(props) {
super(props);
this.handleScroll = throttle(this.handleScroll, 50);
this.adsKeysMapStatic = getSearchKeysMapForAB(getTestWithRelevantAdsForGoods(props.categoryId, props.abCentral));
this.itemsComponentRef = React.createRef();
}
state = {
headerTotalHeight: 0,
chunksIsReady: true,
isShowAddButton: false,
scrollParentNode: false,
arePaddingsIncluded: true
};
componentDidMount() {
const { scrollParentNode } = this.state;
const { ignoreScroll, parentNode } = this.props;
if (!ignoreScroll) {
window.addEventListener('scroll', this.handleScroll, { passive: true });
}
if (this.props.isVerticalMain) {
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const key = entry.target.getAttribute('data-key');
const logParams = getLogParams(key) || {};
this.observer.unobserve(entry.target);
clickStream.sendEvent(4920, 3, {
position: logParams.position,
cid: logParams.categoryId,
from_page: logParams.source, // eslint-disable-line camelcase
target_page: logParams.target || '', // eslint-disable-line camelcase
x: logParams.xHash
});
}
});
});
}
if (parentNode && !scrollParentNode && !ignoreScroll) {
this.setState({ scrollParentNode: true });
parentNode.addEventListener('scroll', this.handleScroll, { passive: true });
}
const { nextSibling, previousSibling } = this.itemsComponentRef.current;
if (
nextSibling?.childNodes[0]?.dataset?.key?.includes(WIDGET_TYPE.addresses) ||
previousSibling?.childNodes[0]?.dataset?.key?.includes(WIDGET_TYPE.addresses)
) {
this.setState({ arePaddingsIncluded: false });
}
}
componentDidUpdate() {
const { scrollParentNode } = this.state;
const { activePage, ignoreScroll, onItemsReachLimit, parentNode, notMoreItems } = this.props;
if (parentNode && !scrollParentNode && !ignoreScroll) {
this.setState({ scrollParentNode: true });
parentNode.addEventListener('scroll', this.handleScroll, { passive: true });
}
if (activePage >= MAX_PAGE || notMoreItems) {
onItemsReachLimit();
}
}
componentWillUnmount() {
const { scrollParentNode } = this.state;
const { parentNode } = this.props;
window.removeEventListener('scroll', this.handleScroll);
if (this.props.isVerticalMain) {
this.observer.disconnect();
}
if (parentNode && scrollParentNode) {
this.setState({ scrollParentNode: false });
}
if (parentNode) {
parentNode.removeEventListener('scroll', this.handleScroll);
}
}
render() {
// TODO Нужно кнопку вынести в родительский компонент. Сделать в рамках переиспользования общего items.
let isShowButton = false;
const { isShowAddButton, chunksIsReady } = this.state;
const {
isFetch,
isFetchFailed,
hasPaddingForLoad,
notMoreItems,
isRich,
isVacancy,
isReVertical,
ignorePaddings
} = this.props;
if (isShowAddButton && !isFetch && chunksIsReady) {
isShowButton = true;
}
return (
<OpenMessengerContext.Provider>
<div ref={this.itemsComponentRef} className={styles.wrapper}>
<div
className={cx(
styles.root,
hasPaddingForLoad && !isFetch && styles.root_paddingForLoading,
hasPaddingForLoad && isShowButton && styles.root_paddingForButton,
{ [styles.rootPaddings]: this.state.arePaddingsIncluded && !ignorePaddings }
)}
{...role(this.props)}>
<div
ref={ref => this.containerNode = ref}
className={cx(
styles.container,
{
[styles.richContainer]: isRich,
[styles.whiteBackground]: isVacancy,
[styles.realVertical]: isReVertical,
[styles.containerPaddings]: this.state.arePaddingsIncluded && !ignorePaddings,
[styles.containerWithoutPaddings]: ignorePaddings
}
)}
{...role(this.props, 'list')}>
{this.renderItems()}
</div>
{
isFetchFailed &&
<SnakeTab
retry={this.loadMore}
message='Не удалось загрузить объявления.' />
}
{this.renderLoader()}
{!notMoreItems && isShowButton &&
<div className={styles['add-button']}>
<PrimaryFlatButton
{...role(this.props, 'add-button')}
text='Загрузить еще'
onAction={this.handleMoreButton} />
</div>
}
</div>
</div>
</OpenMessengerContext.Provider>
);
}
renderLoader = () => {
const { isFetch, items } = this.props;
const { chunksIsReady } = this.state;
const showLoader = (isFetch && chunksIsReady) && items.length;
if (!showLoader) {
return null;
}
return (
<div className={styles.loader}>
<Loader size='middle' />
</div>
);
};
renderItems = () => {
const {
isLoading,
items,
itemsLength,
visibleByDefaultCount,
isFetch,
isFixedSizeSkeleton,
showSkeleton: forceSkeleton,
abCentral,
renderInChunks,
isBackendAdvMixing
} = this.props;
const showSkeleton = isLoading || ((isFetch || forceSkeleton) && !items.length);
const length = !isFixedSizeSkeleton && itemsLength > 0 && itemsLength < MAX_SIZE_SKELETON ? itemsLength : MAX_SIZE_SKELETON;
if (!items.length && !showSkeleton) {
return null;
}
// 1. Добавляем рекламу
let itemsWithAds = [];
if (isBackendAdvMixing) {
itemsWithAds = showSkeleton ? createSkeletonItems(length) : items;
} else {
itemsWithAds = this.enrichWithAds(showSkeleton ? createSkeletonItems(length) : items);
}
// 2. Разбиваем на пакеты
const size = 12;
const subItems = [];
for (let i = 0; i < Math.ceil(itemsWithAds.length / size); i++) {
subItems[i] = itemsWithAds.slice((i * size), (i * size) + size);
}
if (this.isJobVacancyDisclaimerVisible && subItems.length !== 0) {
const firstItemIndex = subItems[0].findIndex((item) => ['item', 'xlItem'].includes(item.type));
subItems[0].splice(firstItemIndex, 0, { type: JOB_VACANCY_DISCLAIMER_ITEM_TYPE });
}
const Container = showSkeleton ? Fragment : MeasureContent;
return (
<Container>
<RenderChunks
abCentral={abCentral}
renderInChunks={renderInChunks}
onRenderChunk={this.handleRenderingChunksProgress}>
{subItems.map((arr, index) => {
return (
<div key={index} className={styles.itemsLayer}>
{this.renderSubItems(arr, index, index === 0 && visibleByDefaultCount > 0)}
</div>
);
})}
</RenderChunks>
</Container>
);
};
renderSubItems = (items, layerIndex, useVisibleByDefaultCount) => {
const { visibleByDefaultCount, categoryId } = this.props;
let visibleCount = visibleByDefaultCount;
for (let i = 0; i < visibleCount && i < items.length - 1; ++i) {
// large blocks
if (items[i].type === 'xlItem' || items[i].type === 'vip' || items[i].type === 'witcher') {
visibleCount -= 1;
}
}
return items.map((item, index) => {
const visibleByDefault = useVisibleByDefaultCount && index < visibleCount;
switch (item.type) {
case 'xlItem':
return this.renderXLItem(item, `${layerIndex}_${index}`, visibleByDefault, index);
case 'warning':
return this.renderWarningItem(item, `${layerIndex}_${index}`);
case 'ads':
case 'banner':
return this.renderAds(item, `${layerIndex}_${index}`);
case 'skeleton':
return this.renderSkeletonItem(`${layerIndex}_${index}`);
case 'placeholder':
return this.renderPlaceholder(item);
case 'groupTitle':
return this.renderGroupTitle(item, `${layerIndex}_${index}`);
case 'witcher':
return this.renderWitcher(item, `${layerIndex}_${index}`, index, visibleByDefault, item.type);
case 'snippet':
return this.renderSnippet(item);
case 'reformulationsWidget':
return this.renderQueryTags(item);
case 'recentQuerySearchWidget':
return this.renderRecentQuerySearch(item);
case 'crossCategoryWidget':
return this.renderCrossCategoryWidget({ widget: item, position: index, cid: categoryId });
case 'mapBanner':
return this.renderMapBanner(item, `${item.type}_${index}`);
case 'header':
return this.renderHeader(item, `${item.type}_${index}`);
case 'filtersTabs':
return this.renderFilterTabs(item, `${item.type}_${index}`);
case 'developmentsCatalogPromo':
return this.renderDevelopmentsCatalogPromo(item, `${item.type}_${index}`);
case 'development':
return this.renderDevelopment(item, `${item.type}_${index}`);
case 'developmentsAdviceButtonsWidget':
return this.renderDevelopmentsAdviceButtons(item, `${item.type}_${index}`);
case 'developmentsAdviceCarouselWidget':
return this.renderDevelopmentsAdviceCarousel(item, `${item.type}_${index}`);
case 'xlDevelopment':
return this.renderDevelopment(item, `${item.type}_${index}`, true);
case 'itemsWidget':
return this.renderRecommendationCarousel(item, index);
case 'itemsCarouselWidget':
return this.renderItemsCarouselWidget(item, index);
case 'sellerItem':
return this.renderSellerItem(item, `${item.type}_${index}`);
case 'brandspaceWidget':
return this.renderBrandspaceWidget(item, `${item.type}_${index}`)
case JOB_VACANCY_DISCLAIMER_ITEM_TYPE:
return this.renderJobVacancyDisclaimer(item, `${item.type}_${index}`);
default:
return this.renderDefaultItem(item, `${layerIndex}_${index}`, visibleByDefault, index);
}
});
};
renderSkeletonItem = (key) => {
const { isVacancy, isReVertical } = this.props;
return (
<Item
key={key}
skeletonClass={skeletonAnimationStyles.animatedSkeleton}
isVacancy={isVacancy}
isReVertical={isReVertical}
isSkeleton />
);
}
renderSnippet = (banner) => {
const { value = {} } = banner;
return (
<Snippet
onSendSnippetBannerEvent={this.props.onSendSnippetBannerEvent}
{...value} />
);
};
renderQueryTags = (widget) => {
const { xHash, categoryId } = this.props;
return (
<QueryTags
categoryId={categoryId}
title={widget?.value?.titleText}
items={widget?.value?.list}
style={widget?.value?.style}
xHash={xHash} />
);
}
renderRecentQuerySearch = (widget) => {
const { isFullMap, categoryId, hideRecentSearch } = this.props;
return (
<RecentQuerySearch
isFullMap={isFullMap}
categoryId={categoryId}
title={widget?.value?.title}
query={widget?.value?.query}
description={widget?.value?.description}
linkText={widget?.value?.action?.title}
url={widget?.value?.action?.url}
onClose={hideRecentSearch} />
);
}
renderCrossCategoryWidget = ({ widget, position }) => {
const { xHash } = this.props;
if (!widget?.value || !widget?.value?.title || !widget?.value?.query || !widget?.value?.action) {
return null;
}
return (
<CrossCategoryWidget
position={position}
title={widget?.value?.title}
data={{
cid: widget?.value?.analyticParams?.cid,
crossCategoryId: widget?.value?.analyticParams?.crossCategoryId,
query: widget?.value?.query,
image: widget?.value?.image,
url: widget?.value?.action?.url
}}
xHash={xHash} />
);
}
renderXLItem = (item, index, visibleByDefault, position) => {
const {
headerHeight,
navHeight,
favorites,
isRich,
isVacancy,
expanded,
scrollPosition,
onViewItem,
onCall,
isReVertical
} = this.props;
const rootMargin = `-${headerHeight + navHeight}px 0px 0px 0px`;
return (
<Fragment key={`${item.value.id}_${index}`}>
<Observer rootMargin={rootMargin}>
{
(setRef, { isVisible }) => (
<>
<Item
isXL
isVisible={isVisible}
isVacancy={isVacancy}
isRich={isRich}
isReVertical={isReVertical}
expanded={expanded}
item={item}
favorites={favorites}
visibleByDefault={visibleByDefault}
scrollPosition={scrollPosition}
position={position}
setRef={setRef}
onView={onViewItem}
onCall={onCall}
onClickFavorite={this.handleClickFavorite} />
{this.renderDivider()}
</>
)}
</Observer>
</Fragment>
);
};
renderDevelopmentsCatalogPromo = (item, key) => {
if (!item?.value) {
return null;
}
return (
<Fragment key={key}>
<DevelopmentsCatalogPromo {...item.value} />
{this.renderDivider()}
</Fragment>
);
}
renderDevelopment = (item, index, isXL) => {
const {
favorites,
expanded,
scrollPosition,
onViewItem,
onCall,
isReVertical
} = this.props;
return (
<Fragment key={`${item.value.id}_${index}`}>
<Item
isDevelopment
isXL={isXL}
isReVertical={isReVertical}
expanded={expanded}
item={item}
favorites={favorites}
// visibleByDefault={visibleByDefault}
scrollPosition={scrollPosition}
onView={onViewItem}
onCall={onCall}
onClickFavorite={this.handleClickFavorite} />
{this.renderDivider()}
</Fragment>
);
};
renderDevelopmentsAdviceButtons = (item, key) => {
if (!item?.value) {
return null;
}
return (
<Fragment key={key}>
<DevelopmentsAdviceButtons
{...item.value}
xHash={this.props.xHash} />
{this.renderDivider()}
</Fragment>
);
};
renderDevelopmentsAdviceCarousel = (item, key) => {
if (!item?.value) {
return null;
}
return (
<Fragment key={key}>
<DevelopmentsAdviceCarousel
{...item.value}
xHash={this.props.xHash} />
{this.renderDivider()}
</Fragment>
);
};
renderSellerItem = (item, index) => {
return (
<Fragment key={`${item.value.id}_${index}`}>
<SellerItem value={item.value} />
</Fragment>
);
};
renderDivider = () => {
const { shouldRenderDividers } = this.props;
return shouldRenderDividers && (
<div className={styles.divider} />
);
}
renderAds = (item, index) => {
const {
renderAds,
scrollPosition,
isRich,
isBackendAdvMixing,
isReVertical
} = this.props;
const bannerKey = !isBackendAdvMixing ?
this.adsKeysMapStatic[item.bannerIndex] :
item?.value?.code;
return (
<Fragment key={`${bannerKey}_${index}`}>
<div
className={cx(styles.ads, { [styles.adsRich]: isRich, [styles.adsRealVertical]: isReVertical })}
{...role({ marker: 'items/ads' }, bannerKey)}>
<LazyLoadComponent
scrollPosition={scrollPosition}
expand='500'
placeholder={this.renderAdsPlaceholder()}>
{renderAds(bannerKey, this.renderAdsPlaceholder)}
</LazyLoadComponent>
</div>
{this.renderDivider()}
</Fragment>
);
};
renderPlaceholder = ({ value }) => {
return (
<SearchNoResult title={value.title} />
);
};
renderAdsPlaceholder = () => {
const { isFetch } = this.props;
return (
<div className={styles.adsPlaceholder}>
<div
className={cx(styles.innerAdsPlaceholder, { [skeletonAnimationStyles.animatedSkeleton]: isFetch })} />
</div>);
};
renderRecommendationCarousel = (item, index) => {
const { xHash, categoryId } = this.props;
return (
<RecommendationCarouselWidget
{...item.value}
isSearch
xHash={xHash}
categoryId={categoryId}
position={index} />
);
};
renderItemsCarouselWidget = (item) => {
const {
toggles = {},
isVerticalMain,
categoryId,
xHash,
segment
} = this.props;
if (!toggles[SHOW_ITEMS_CAROUSEL_WIDGET_SERP]) {
return null;
}
return (
<div className={styles.carouselWidget}>
<ItemsCarouselWidget
page='search'
{...item.value}
isVerticalMain={isVerticalMain}
xHash={xHash}
segment={segment}
categoryId={categoryId} />
</div>
);
};
renderGroupTitle = ({ value }, index) => {
const { items } = this.props;
const noResult = items.some(({ type }) => type === 'placeholder');
return (
<SearchGroupTitle key={index} title={value.title} noResult={noResult} />
);
};
renderWitcher = ({ value }, index, itemIndex, visibleByDefault, type) => {
const {
favorites,
onClickWitcher
} = this.props;
const {
title_text: titleText,
button_text: buttonText,
selection_type: selectionType,
deeplink,
items
} = value;
const coreParams = this.getCoreParams(type, itemIndex);
return (
<Witcher
key={index}
{...coreParams}
title={titleText}
buttonText={buttonText}
selectionType={selectionType}
favorites={favorites}
deeplink={deeplink}
items={items}
index={index}
visibleByDefault={visibleByDefault}
onClick={onClickWitcher}
onClickItem={this.handleClickWitcherItem({ ...coreParams, title: titleText })}
onClickFavorite={this.handleClickFavorite} />
);
};
renderWarningItem = (item, index) => {
return (
<div
key={`${item.value.id}_${index}`}
className={cx(styles.item, styles['item_type-warning'])}
{...role({
marker: 'item-wrapper',
markerId: item.value.id
})}>
<WarningTile
title={item.value.title}
actions={item.value.actions}
history={this.props.history}
marker='item'
markerid={item.value.id} />
</div>
);
};
renderDefaultItem = (item, index, visibleByDefault, position) => {
// TODO У вип объявления странный формат
if (item.type === 'vip') {
item = item.value.list[0];
item.type = 'vip';
}
const {
headerHeight,
navHeight,
favorites,
isRich,
isVacancy,
isReVertical,
scrollPosition,
mobileInfo,
onViewItem,
onClickExpandedLink,
onCall,
displayType
} = this.props;
const rootMargin = `-${headerHeight + navHeight}px 0px 0px 0px`;
return (
<Fragment key={`${item.value.id}_${index}`}>
<Observer rootMargin={rootMargin}>
{
(setRef, { isVisible }) => (
<Item
mobileInfo={mobileInfo}
isVisible={isVisible}
item={item}
isRich={isRich}
isVacancy={isVacancy}
isReVertical={isReVertical}
favorites={favorites}
visibleByDefault={visibleByDefault}
scrollPosition={scrollPosition}
position={position}
displayType={displayType}
setRef={setRef}
onView={onViewItem}
onClickExpandedLink={onClickExpandedLink}
onClickFavorite={this.handleClickFavorite}
onCall={onCall} />
)
}
</Observer>
{this.renderDivider()}
</Fragment>
);
};
renderMapBanner = (item, key) => {
const {
shouldRenderMapBanner,
onClickMapBanner
} = this.props;
// на экране карты не показываем баннер
if (!shouldRenderMapBanner) {
return null;
}
return (
<div key={key} className={styles['item_type-mapBanner']}>
<MapBanner marker='map-banner' data={item.value} onClickMapBanner={onClickMapBanner} />
</div>
);
}
renderHeader = (item, key) => {
const { expanded } = this.props;
const { value: itemsHeader } = item;
if (!itemsHeader?.title) {
return null;
}
const classNames = expanded ? {
title: styles.expandedTitle,
subtitle: styles.expandedSubtitle,
subtitleItem: styles.expandedSubtitleItem,
container: styles.expandedContainer
} : {};
// кастомный заголовок выдачи для разгруппированных позиций
return (
<div key={key}>
<SearchTitle
title={itemsHeader.title}
subtitle={itemsHeader.descriptions}
classNames={classNames}
marker='search-title' />
</div>
);
}
renderFilterTabs = (item, key) => {
return (
<FilterTab key={key} item={item} getItems={this.props.getItems} />
);
}
renderJobVacancyDisclaimer = (item, key) => {
return (
<Text
key={key}
pt={10}
pb={12}
pr={17}
pl={16}
mt={16}
mb={16}
bg='gray4'
borderRadius={5}
size='m'>
<JobVacancyDisclaimer />
</Text>
);
}
renderBrandspaceWidget = (item, key) => {
return (
<Fragment key={key}>
<BrandspaceWidget {...item.value} />
{this.renderDivider()}
</Fragment>
)
}
handleMoreButton = () => {
this.loadMore();
};
handleScroll = () => {
if (!this.containerNode?.offsetHeight) {
return;
}
const {
isFetchFailed,
isFetch,
items,
threshold,
activePage,
parentNode,
onScroll
} = this.props;
const {
offsetHeight,
offsetTop
} = this.containerNode;
const scrollY = parentNode ? parentNode.scrollTop : window.scrollY;
const innerHeight = parentNode ? parentNode.offsetHeight : window.innerHeight;
const deltaOffsetTop = offsetHeight + offsetTop - scrollY - innerHeight;
const isPositionForFetch = scrollY > 1 && deltaOffsetTop < threshold;
onScroll();
if (!isFetch && !isFetchFailed && isPositionForFetch && items.length) {
if (activePage <= MAX_PAGE) {
this.loadMore();
this.setState({ isShowAddButton: false });
} else {
this.setState({ isShowAddButton: items.length < LIMIT_ITEMS });
}
}
};
handleClickFavorite = (params) => {
const { onClickFavorite, xHash, categoryId } = this.props;
if (onClickFavorite) {
onClickFavorite({ ...params, source: sources.snippet, xHash, categoryId });
}
};
handleRenderingChunksProgress = (inProgress) => {
this.setState({ chunksIsReady: !inProgress });
if (!inProgress && this.runLoadMoreOnChunksReady) {
this.runLoadMoreOnChunksReady = false;
this.loadMore();
}
};
handleClickWitcherItem = ({ type, title, position, categoryId, xHash }) => (index) => {
const { isVerticalMain } = this.props;
if (!isVerticalMain) {
return;
}
clickStream.sendEvent(4921, 4, {
position,
from_page: type, // eslint-disable-line camelcase
target_page: title, // eslint-disable-line camelcase
option_number: index, // eslint-disable-line camelcase
cid: categoryId,
x: xHash
});
}
enrichWithAds = (items) => {
const { categoryId, showAdsPlaceholders, expanded, toggles } = this.props;
// TODO: убрать при переносе данных о рекламе в api/11/items
const isExpandedDuplicates = expanded === EXPANDED_DOUBLES;
const isDevelopmentItems = items.find(({ type }) => ['xlDevelopment', 'development'].includes(type));
const shouldShowAds = showAdsPlaceholders && !isExpandedDuplicates && !isDevelopmentItems;
const resultItems = shouldShowAds ? [] : items;
if (shouldShowAds) {
let index = 0;
items.forEach((item) => {
const bannerIndex = index % PAGE_SIZE;
if (this.adsKeysMapStatic[bannerIndex] && isAdsPositionEnabled(categoryId, this.adsKeysMapStatic[bannerIndex], toggles, null)) {
resultItems.push({
type: 'ads',
bannerIndex
});
index++;
}
resultItems.push(item);
index++;
});
}
return resultItems;
};
loadMore = () => {
const { onLoadMore } = this.props;
const { chunksIsReady } = this.state;
if (chunksIsReady) {
onLoadMore();
} else {
this.runLoadMoreOnChunksReady = true;
}
};
runLoadMoreOnChunksReady = false;
getCoreParams = (type, index) => {
const { categoryId, xHash } = this.props;
return {
observer: this.observer,
type: type,
position: index,
categoryId,
xHash
};
}
get isJobVacancy() {
return this.props.categoryId === CATEGORY_ID_VACANCY;
}
get isJobVacancyDisclaimerVisible() {
const hasJobVacancyDisclaimer = this.props.toggles?.[JOB_DISCLAIMER_ENABLED_TOGGLE];
const isAuthorized = Boolean(this.props.user);
return this.isJobVacancy && hasJobVacancyDisclaimer && isAuthorized;
}
}
export default withToggles(trackWindowScroll(
withUserInfo(withABCentral(Items))),
[
GOODS_DISABLED_POSITIONS_TOGGLE,
SERVICES_DISABLED_POSITIONS_TOGGLE,
REALTY_DISABLED_POSITIONS_TOGGLE,
TRANSPORT_DISABLED_POSITIONS_TOGGLE,
JOB_DISCLAIMER_ENABLED_TOGGLE,
SHOW_ITEMS_CAROUSEL_WIDGET_SERP
]
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment