mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 15:22:55 +01:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user