Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
import BScroll from '@better-scroll/core';
import MouseWheel from '@better-scroll/mouse-wheel';
import ScrollBar from '@better-scroll/scroll-bar';
import React from 'react';
import ReactDOM from 'react-dom';
import Layout from '../../../layout';
import PointerListeners from '../../../pointerlisteners';
import { Noodl } from '../../../types';
import NestedScroll from './scroll-plugins/nested-scroll-plugin';
import patchedMomentum from './scroll-plugins/patched-momentum-scroll';
import Slide from './scroll-plugins/slide-scroll-plugin';
BScroll.use(ScrollBar);
BScroll.use(NestedScroll);
BScroll.use(MouseWheel);
BScroll.use(Slide);
export interface GroupProps extends Noodl.ReactProps {
as?: keyof JSX.IntrinsicElements | React.ComponentType<unknown>;
scrollSnapEnabled: boolean;
showScrollbar: boolean;
scrollEnabled: boolean;
nativeScroll: boolean;
scrollSnapToEveryItem: boolean;
flexWrap: 'nowrap' | 'wrap' | 'wrap-reverse';
scrollBounceEnabled: boolean;
clip: boolean;
layout: 'none' | 'row' | 'column';
dom;
onScrollPositionChanged?: (value: number) => void;
onScrollStart?: () => void;
onScrollEnd?: () => void;
}
type ScrollRef = HTMLDivElement & { noodlNode?: Noodl.ReactProps['noodlNode'] };
export class Group extends React.Component<GroupProps> {
scrollNeedsToInit: boolean;
scrollRef: React.RefObject<ScrollRef>;
iScroll?: BScroll;
constructor(props: GroupProps) {
super(props);
this.scrollNeedsToInit = false;
this.scrollRef = React.createRef();
}
componentDidMount() {
if (this.props.scrollEnabled && this.props.nativeScroll !== true) {
this.setupIScroll();
}
//plumbing for the focused signals
this.scrollRef.current.noodlNode = this.props.noodlNode;
}
componentWillUnmount() {
if (this.iScroll) {
this.iScroll.destroy();
this.iScroll = undefined;
}
this.props.noodlNode.context.setNodeFocused(this.props.noodlNode, false);
}
componentDidUpdate() {
if (this.scrollNeedsToInit) {
this.setupIScroll();
this.scrollNeedsToInit = false;
}
if (this.iScroll) {
setTimeout(() => {
this.iScroll && this.iScroll.refresh();
}, 0);
}
}
scrollToIndex(index, duration) {
if (this.iScroll) {
const child = this.scrollRef.current.children[0].children[index] as HTMLElement;
if (child) {
this.iScroll.scrollToElement(child, duration, 0, 0);
}
} else {
const child = this.scrollRef.current.children[index];
child &&
child.scrollIntoView({
behavior: 'smooth'
});
}
}
scrollToElement(noodlChild, duration) {
if (!noodlChild) return;
// eslint-disable-next-line react/no-find-dom-node
const element = ReactDOM.findDOMNode(noodlChild.getRef()) as HTMLElement;
if (element && element.scrollIntoView) {
if (this.iScroll) {
this.iScroll.scrollToElement(element, duration, 0, 0);
} else {
element.scrollIntoView({
behavior: 'smooth'
});
}
}
}
setupIScroll() {
const { scrollSnapEnabled } = this.props;
const scrollDirection = this.getScrollDirection();
const snapOptions = {
disableSetWidth: true,
disableSetHeight: true,
loop: false
};
const domElement = this.scrollRef.current;
this.iScroll = new BScroll(domElement, {
bounceTime: 500,
swipeBounceTime: 300,
scrollbar: this.props.showScrollbar ? {} : undefined,
momentum: scrollSnapEnabled ? !this.props.scrollSnapToEveryItem : true,
bounce: this.props.scrollBounceEnabled && !(scrollSnapEnabled && snapOptions.loop),
scrollX: scrollDirection === 'x' || scrollDirection === 'both',
scrollY: scrollDirection === 'y' || scrollDirection === 'both',
slide: scrollSnapEnabled ? snapOptions : undefined,
probeType: this.props.onScrollPositionChanged ? 3 : 1,
click: true,
nestedScroll: true,
//disable CSS animation, they can cause a flicker on iOS,
//and cause problems with probing the scroll position during an animation
useTransition: false
});
//the scroll behavior when doing a momentum scroll that reaches outside the bounds
//does a slow and unpleasant animation. Let's patch it to make it behave more like iScroll.
const scroller = this.iScroll.scroller;
// @ts-expect-error momentum does exist
scroller.scrollBehaviorX && (scroller.scrollBehaviorX.momentum = patchedMomentum.bind(scroller.scrollBehaviorX));
// @ts-expect-error momentum does exist
scroller.scrollBehaviorY && (scroller.scrollBehaviorY.momentum = patchedMomentum.bind(scroller.scrollBehaviorY));
//refresh the scroll view in case a child has changed height, e.g. an image loaded
//seem to be very performant, no observed problem so far
this.iScroll.on('beforeScrollStart', () => {
this.iScroll.refresh();
});
this.iScroll.on('scrollStart', () => {
this.props.onScrollStart && this.props.onScrollStart();
});
this.iScroll.on('scrollEnd', () => {
this.props.onScrollEnd && this.props.onScrollEnd();
});
if (this.props.onScrollPositionChanged) {
this.iScroll.on('scroll', () => {
this.props.onScrollPositionChanged(scrollDirection === 'x' ? -this.iScroll.x : -this.iScroll.y);
});
}
}
UNSAFE_componentWillReceiveProps(nextProps: GroupProps) {
const scrollHasUpdated =
this.props.scrollSnapEnabled !== nextProps.scrollSnapEnabled ||
this.props.onScrollPositionChanged !== nextProps.onScrollPositionChanged ||
this.props.onScrollStart !== nextProps.onScrollStart ||
this.props.onScrollEnd !== nextProps.onScrollEnd ||
this.props.showScrollbar !== nextProps.showScrollbar ||
this.props.scrollEnabled !== nextProps.scrollEnabled ||
this.props.nativeScroll !== nextProps.nativeScroll ||
this.props.scrollSnapToEveryItem !== nextProps.scrollSnapToEveryItem ||
this.props.layout !== nextProps.layout ||
this.props.flexWrap !== nextProps.flexWrap ||
this.props.scrollBounceEnabled !== nextProps.scrollBounceEnabled;
if (scrollHasUpdated) {
if (this.iScroll) {
this.iScroll.destroy();
this.iScroll = undefined;
}
this.scrollNeedsToInit = nextProps.scrollEnabled && !nextProps.nativeScroll;
}
}
renderIScroll() {
const { flexDirection, flexWrap } = this.props.style;
const childStyle: React.CSSProperties = {
display: 'inline-flex',
flexShrink: 0,
flexDirection,
flexWrap,
touchAction: 'none'
// pointerEvents: this.state.isScrolling ? 'none' : undefined
};
if (flexDirection === 'row') {
if (flexWrap === 'wrap') {
childStyle.width = '100%';
} else {
childStyle.height = '100%';
}
} else {
if (flexWrap === 'wrap') {
childStyle.height = '100%';
} else {
childStyle.width = '100%';
}
}
return (
<div className="scroll-wrapper-internal" style={childStyle}>
{this.props.children}
</div>
);
}
getScrollDirection(): 'x' | 'y' | 'both' {
// TODO: This never returns both, why?
if (this.props.flexWrap === 'wrap' || this.props.flexWrap === 'wrap-reverse') {
return this.props.layout === 'row' ? 'y' : 'x';
}
return this.props.layout === 'row' ? 'x' : 'y';
}
render() {
const {
as: Component = 'div',
...props
} = this.props;
const children = props.scrollEnabled && !props.nativeScroll ? this.renderIScroll() : props.children;
const style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.clip) {
style.overflowX = 'hidden';
style.overflowY = 'hidden';
}
if (props.scrollEnabled && props.nativeScroll) {
const scrollDirection = this.getScrollDirection();
if (scrollDirection === 'y') {
style.overflowY = 'auto';
} else if (scrollDirection === 'x') {
style.overflowX = 'auto';
} else if (scrollDirection === 'both') {
style.overflowX = 'auto';
style.overflowY = 'auto';
}
}
if (style.opacity === 0) {
style.pointerEvents = 'none';
}
return (
<Component
// @ts-expect-error Lets hope that the type passed here is always static!
className={props.className}
{...props.dom}
{...PointerListeners(props)}
style={style}
ref={this.scrollRef}
>
{children}
</Component>
);
}
}