Created
January 25, 2024 08:44
-
-
Save dmitrysurkin/807a81d7a750225f3361fee56fb060bc to your computer and use it in GitHub Desktop.
scroll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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