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,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Circle } from './Circle';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Circle',
|
||||
component: Circle,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Circle>;
|
||||
|
||||
const Template: ComponentStory<typeof Circle> = (args) => <Circle {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '../../../layout';
|
||||
import PointerListeners from '../../../pointerlisteners';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface CircleProps extends Noodl.ReactProps {
|
||||
size: number;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
|
||||
fillEnabled: boolean;
|
||||
fillColor: Noodl.Color;
|
||||
|
||||
strokeEnabled: boolean;
|
||||
strokeColor: Noodl.Color;
|
||||
strokeWidth: number;
|
||||
strokeLineCap: 'butt' | 'round';
|
||||
|
||||
dom;
|
||||
}
|
||||
|
||||
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
||||
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
||||
|
||||
return {
|
||||
x: centerX + radius * Math.cos(angleInRadians),
|
||||
y: centerY + radius * Math.sin(angleInRadians)
|
||||
};
|
||||
}
|
||||
|
||||
function filledArc(x, y, radius, startAngle, endAngle) {
|
||||
if (endAngle % 360 === startAngle % 360) {
|
||||
endAngle -= 0.0001;
|
||||
}
|
||||
|
||||
const start = polarToCartesian(x, y, radius, endAngle);
|
||||
const end = polarToCartesian(x, y, radius, startAngle);
|
||||
|
||||
const arcSweep = endAngle - startAngle <= 180 ? '0' : '1';
|
||||
|
||||
return [
|
||||
'M',
|
||||
start.x,
|
||||
start.y,
|
||||
'A',
|
||||
radius,
|
||||
radius,
|
||||
0,
|
||||
arcSweep,
|
||||
0,
|
||||
end.x,
|
||||
end.y,
|
||||
'L',
|
||||
x,
|
||||
y,
|
||||
'L',
|
||||
start.x,
|
||||
start.y
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function arc(x, y, radius, startAngle, endAngle) {
|
||||
if (endAngle % 360 === startAngle % 360) {
|
||||
endAngle -= 0.0001;
|
||||
}
|
||||
|
||||
const start = polarToCartesian(x, y, radius, endAngle);
|
||||
const end = polarToCartesian(x, y, radius, startAngle);
|
||||
|
||||
const arcSweep = endAngle - startAngle <= 180 ? '0' : '1';
|
||||
|
||||
return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' ');
|
||||
}
|
||||
|
||||
export class Circle extends React.Component<CircleProps> {
|
||||
constructor(props: CircleProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
//SVG can only do strokes centered on a path, and we want to render it inside.
|
||||
//We'll do it manually by adding another path on top of the filled circle
|
||||
|
||||
let fill;
|
||||
let stroke;
|
||||
|
||||
const r = this.props.size / 2;
|
||||
const { startAngle, endAngle } = this.props;
|
||||
|
||||
if (this.props.fillEnabled) {
|
||||
const r = this.props.size / 2;
|
||||
fill = <path d={filledArc(r, r, r, startAngle, endAngle)} fill={this.props.fillColor} />;
|
||||
}
|
||||
|
||||
if (this.props.strokeEnabled) {
|
||||
const { strokeColor, strokeWidth, strokeLineCap } = this.props;
|
||||
const strokeRadius = r - this.props.strokeWidth / 2;
|
||||
const path = arc(r, r, strokeRadius, startAngle, endAngle);
|
||||
stroke = (
|
||||
<path
|
||||
d={path}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="transparent"
|
||||
strokeLinecap={strokeLineCap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const style = { ...this.props.style };
|
||||
Layout.size(style, this.props);
|
||||
Layout.align(style, this.props);
|
||||
|
||||
if (style.opacity === 0) {
|
||||
style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
//the SVG element lack some properties like offsetLeft, offsetTop that the drag node depends on.
|
||||
//Let's wrap it in a div to make it work properly
|
||||
return (
|
||||
<div className={this.props.className} {...this.props.dom} {...PointerListeners(this.props)} style={style}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={this.props.size} height={this.props.size}>
|
||||
{fill}
|
||||
{stroke}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Circle';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Columns } from './Columns';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Columns',
|
||||
component: Columns,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Columns>;
|
||||
|
||||
const Template: ComponentStory<typeof Columns> = (args) => <Columns {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
|
||||
import { ForEachComponent } from '../../../nodes/std-library/data/foreach';
|
||||
import { Noodl, Slot } from '../../../types';
|
||||
|
||||
export interface ColumnsProps extends Noodl.ReactProps {
|
||||
marginX: string;
|
||||
marginY: string;
|
||||
|
||||
justifyContent: 'flex-start' | 'flex-end' | 'center';
|
||||
direction: 'row' | 'column';
|
||||
minWidth: string;
|
||||
layoutString: string;
|
||||
|
||||
children: Slot;
|
||||
}
|
||||
|
||||
function calcAutofold(layout, minWidths, containerWidth, marginX) {
|
||||
const first = _calcAutofold(layout, minWidths, containerWidth, marginX);
|
||||
const second = _calcAutofold(first.layout, minWidths, containerWidth, marginX);
|
||||
|
||||
if (first.totalFractions === second.totalFractions) {
|
||||
return first;
|
||||
} else {
|
||||
return calcAutofold(second.layout, minWidths, containerWidth, marginX);
|
||||
}
|
||||
}
|
||||
|
||||
function _calcAutofold(layout, minWidth, containerWidth, marginX) {
|
||||
const totalFractions = layout.reduce((a, b) => a + b, 0);
|
||||
const fractionSize = 100 / totalFractions;
|
||||
|
||||
const rowWidth = layout.reduce(
|
||||
(acc, curr, i) => {
|
||||
return {
|
||||
expected: acc.expected + (fractionSize / 100) * containerWidth * curr,
|
||||
min: acc.min + parseFloat(minWidth) + marginX
|
||||
};
|
||||
},
|
||||
{ expected: 0, min: 0, max: null }
|
||||
);
|
||||
|
||||
const newLayout = layout;
|
||||
|
||||
if (rowWidth.expected < rowWidth.min) {
|
||||
newLayout.pop();
|
||||
}
|
||||
|
||||
const newTotalFractions = newLayout.reduce((a, b) => a + b, 0);
|
||||
const newFractionSize = 100 / newTotalFractions;
|
||||
const newColumnAmount = newLayout.length;
|
||||
|
||||
return {
|
||||
layout: newLayout,
|
||||
totalFractions: newTotalFractions,
|
||||
fractionSize: newFractionSize,
|
||||
columnAmount: newColumnAmount
|
||||
};
|
||||
}
|
||||
|
||||
export function Columns(props: ColumnsProps) {
|
||||
if (!props.children) return null;
|
||||
let columnLayout = null;
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const [containerWidth, setContainerWidth] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef?.current;
|
||||
if (!container) return;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
setContainerWidth(container.offsetWidth);
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
switch (typeof props.layoutString) {
|
||||
case 'string':
|
||||
columnLayout = props.layoutString.trim();
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
columnLayout = String(props.layoutString).trim();
|
||||
break;
|
||||
|
||||
default:
|
||||
columnLayout = null;
|
||||
}
|
||||
|
||||
if (!columnLayout) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
// all data for childrens width calculation
|
||||
const targetLayout = columnLayout.split(' ').map((number) => parseInt(number));
|
||||
|
||||
// constraints
|
||||
const { layout, columnAmount, fractionSize } = calcAutofold(
|
||||
targetLayout,
|
||||
props.minWidth,
|
||||
containerWidth,
|
||||
props.marginX
|
||||
);
|
||||
|
||||
let children = [];
|
||||
let forEachComponent = null;
|
||||
|
||||
// ForEachCompoent breaks the layout but is needed to send onMount/onUnmount
|
||||
if (!Array.isArray(props.children)) {
|
||||
children = [props.children];
|
||||
} else {
|
||||
children = props.children.filter((child) => child.type !== ForEachComponent);
|
||||
forEachComponent = props.children.find((child) => child.type === ForEachComponent);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={['columns-container', props.className].join(' ')}
|
||||
ref={containerRef}
|
||||
style={{
|
||||
marginTop: parseFloat(props.marginY) * -1,
|
||||
marginLeft: parseFloat(props.marginX) * -1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: props.justifyContent,
|
||||
flexDirection: props.direction,
|
||||
width: `calc(100% + (${parseFloat(props.marginX)}px)`,
|
||||
boxSizing: 'border-box',
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
{forEachComponent && forEachComponent}
|
||||
|
||||
{children.map((child, i) => {
|
||||
return (
|
||||
<div
|
||||
className="column-item"
|
||||
key={i}
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
paddingTop: props.marginY,
|
||||
paddingLeft: props.marginX,
|
||||
width: layout[i % columnAmount] * fractionSize + '%',
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
minWidth: props.minWidth
|
||||
// maxWidths needs some more thought
|
||||
//maxWidth: getMinMaxInputValues(maxWidths, columnAmount, props.marginX, i)
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(child)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Columns';
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Drag } from './Drag';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Drag',
|
||||
component: Drag,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof Drag>;
|
||||
|
||||
const Template: ComponentStory<typeof Drag> = (args) => <Drag {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
159
packages/noodl-viewer-react/src/components/visual/Drag/Drag.tsx
Normal file
159
packages/noodl-viewer-react/src/components/visual/Drag/Drag.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import EaseCurves from '../../../easecurves';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface DragProps extends Noodl.ReactProps {
|
||||
inputPositionX: number;
|
||||
inputPositionY: number;
|
||||
|
||||
enabled: boolean;
|
||||
scale: number;
|
||||
axis: 'x' | 'y' | 'both';
|
||||
|
||||
useParentBounds: boolean;
|
||||
|
||||
onStart?: () => void;
|
||||
onStop?: () => void;
|
||||
onDrag?: () => void;
|
||||
|
||||
positionX?: (value: number) => void;
|
||||
positionY?: (value: number) => void;
|
||||
deltaX?: (value: number) => void;
|
||||
deltaY?: (value: number) => void;
|
||||
}
|
||||
|
||||
function setDragValues(event, props) {
|
||||
props.positionX && props.positionX(event.x);
|
||||
props.positionY && props.positionY(event.y);
|
||||
props.deltaX && props.deltaX(event.deltaX);
|
||||
props.deltaY && props.deltaY(event.deltaY);
|
||||
}
|
||||
|
||||
type State = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class Drag extends React.Component<DragProps, State> {
|
||||
snapToPositionXTimer: any;
|
||||
snapToPositionYTimer: any;
|
||||
|
||||
constructor(props: DragProps) {
|
||||
super(props);
|
||||
this.state = { x: 0, y: 0 } satisfies State;
|
||||
}
|
||||
|
||||
snapToPosition({ timerScheduler, propCallback, duration, axis, endValue }) {
|
||||
const _this = this;
|
||||
return timerScheduler
|
||||
.createTimer({
|
||||
duration: duration === undefined ? 300 : duration,
|
||||
startValue: this.state[axis],
|
||||
endValue: endValue,
|
||||
ease: EaseCurves.easeOut,
|
||||
onRunning: function (t) {
|
||||
const value = this.ease(this.startValue, this.endValue, t);
|
||||
// @ts-expect-error Either x or y...
|
||||
_this.setState({ [axis]: value });
|
||||
propCallback && propCallback(value);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const x = this.props.inputPositionX ? this.props.inputPositionX : 0;
|
||||
const y = this.props.inputPositionY ? this.props.inputPositionY : 0;
|
||||
|
||||
this.setState({ x, y });
|
||||
setDragValues({ x, y, deltaX: 0, deltaY: 0 }, this.props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: DragProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (props.inputPositionX !== nextProps.inputPositionX) {
|
||||
this.setState({ x: nextProps.inputPositionX });
|
||||
props.positionX && props.positionX(nextProps.inputPositionX);
|
||||
props.deltaX && props.deltaX(nextProps.inputPositionX - props.inputPositionX);
|
||||
}
|
||||
if (props.inputPositionY !== nextProps.inputPositionY) {
|
||||
this.setState({ y: nextProps.inputPositionY });
|
||||
props.positionY && props.positionY(nextProps.inputPositionY);
|
||||
props.deltaY && props.deltaY(nextProps.inputPositionY - props.inputPositionY);
|
||||
}
|
||||
}
|
||||
|
||||
snapToPositionX(x, duration) {
|
||||
if (this.state.x === x) return;
|
||||
|
||||
this.snapToPositionXTimer && this.snapToPositionXTimer.stop();
|
||||
this.snapToPositionXTimer = this.snapToPosition({
|
||||
timerScheduler: this.props.noodlNode.context.timerScheduler,
|
||||
propCallback: this.props.positionX,
|
||||
duration,
|
||||
axis: 'x',
|
||||
endValue: x
|
||||
});
|
||||
}
|
||||
|
||||
snapToPositionY(y, duration) {
|
||||
if (this.state.y === y) return;
|
||||
|
||||
this.snapToPositionYTimer && this.snapToPositionYTimer.stop();
|
||||
this.snapToPositionYTimer = this.snapToPosition({
|
||||
timerScheduler: this.props.noodlNode.context.timerScheduler,
|
||||
propCallback: this.props.positionY,
|
||||
duration,
|
||||
axis: 'y',
|
||||
endValue: y
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const bounds = props.useParentBounds ? 'parent' : undefined;
|
||||
|
||||
let child;
|
||||
if (React.Children.count(props.children) > 0) {
|
||||
child = React.Children.toArray(props.children)[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
axis={props.axis}
|
||||
bounds={bounds}
|
||||
disabled={props.enabled === false}
|
||||
scale={props.scale || 0}
|
||||
position={{ x: this.state.x, y: this.state.y }}
|
||||
onStart={(e, data) => {
|
||||
setDragValues(data, props);
|
||||
props.onStart && props.onStart();
|
||||
this.snapToPositionXTimer && this.snapToPositionXTimer.stop();
|
||||
this.snapToPositionYTimer && this.snapToPositionYTimer.stop();
|
||||
}}
|
||||
onStop={(e, data) => {
|
||||
if (props.axis === 'x' || props.axis === 'both') {
|
||||
this.setState({ x: data.x });
|
||||
}
|
||||
if (props.axis === 'y' || props.axis === 'both') {
|
||||
this.setState({ y: data.y });
|
||||
}
|
||||
props.positionX && props.positionX(data.x);
|
||||
props.positionY && props.positionY(data.y);
|
||||
props.onStop && props.onStop();
|
||||
}}
|
||||
onDrag={(e, data) => {
|
||||
setDragValues(data, props);
|
||||
props.onDrag && props.onDrag();
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(child, { parentLayout: props.parentLayout })}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Drag'
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Group } from './Group';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Group',
|
||||
component: Group,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Group>;
|
||||
|
||||
const Template: ComponentStory<typeof Group> = (args) => <Group {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Group';
|
||||
@@ -0,0 +1,212 @@
|
||||
/*!
|
||||
* better-scroll / nested-scroll
|
||||
* (c) 2016-2020 ustbhuangyi
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
// Slightly modified to for Noodl to fix a problem:
|
||||
// - click bug. Click could be disabled and never enabled again when scrolling
|
||||
// - click bug 2. Click option didn't support the scroller being moved around in the dom tree
|
||||
// It assumed the nested scroll relationships always stay the same
|
||||
// (so doesn't work with delta updates)
|
||||
|
||||
var compatibleFeatures = {
|
||||
duplicateClick: function (_a) {
|
||||
var parentScroll = _a[0],
|
||||
childScroll = _a[1];
|
||||
// no need to make childScroll's click true
|
||||
if (parentScroll.options.click && childScroll.options.click) {
|
||||
childScroll.options.click = false;
|
||||
}
|
||||
},
|
||||
nestedScroll: function (scrollsPair) {
|
||||
var parentScroll = scrollsPair[0],
|
||||
childScroll = scrollsPair[1];
|
||||
var parentScrollX = parentScroll.options.scrollX;
|
||||
var parentScrollY = parentScroll.options.scrollY;
|
||||
var childScrollX = childScroll.options.scrollX;
|
||||
var childScrollY = childScroll.options.scrollY;
|
||||
// vertical nested in vertical scroll and horizontal nested in horizontal
|
||||
// otherwise, no need to handle.
|
||||
if (parentScrollX === childScrollX || parentScrollY === childScrollY) {
|
||||
scrollsPair.forEach(function (scroll, index) {
|
||||
var oppositeScroll = scrollsPair[(index + 1) % 2];
|
||||
scroll.on('scrollStart', function () {
|
||||
if (oppositeScroll.pending) {
|
||||
oppositeScroll.stop();
|
||||
oppositeScroll.resetPosition();
|
||||
}
|
||||
setupData(oppositeScroll);
|
||||
oppositeScroll.disable();
|
||||
});
|
||||
scroll.on('touchEnd', function () {
|
||||
oppositeScroll.enable();
|
||||
});
|
||||
});
|
||||
childScroll.on('scrollStart', function () {
|
||||
if (checkBeyondBoundary(childScroll)) {
|
||||
childScroll.disable();
|
||||
parentScroll.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
var NestedScroll = /** @class */ (function () {
|
||||
function NestedScroll(scroll) {
|
||||
var singleton = NestedScroll.nestedScroll;
|
||||
if (!(singleton instanceof NestedScroll)) {
|
||||
singleton = NestedScroll.nestedScroll = this;
|
||||
singleton.stores = [];
|
||||
}
|
||||
singleton.setup(scroll);
|
||||
singleton.addHooks(scroll);
|
||||
return singleton;
|
||||
}
|
||||
NestedScroll.prototype.setup = function (scroll) {
|
||||
this.appendBScroll(scroll);
|
||||
this.handleContainRelationship();
|
||||
this.handleCompatible();
|
||||
};
|
||||
NestedScroll.prototype.addHooks = function (scroll) {
|
||||
var _this = this;
|
||||
scroll.on('destroy', function () {
|
||||
_this.teardown(scroll);
|
||||
});
|
||||
};
|
||||
NestedScroll.prototype.teardown = function (scroll) {
|
||||
this.removeBScroll(scroll);
|
||||
this.handleContainRelationship();
|
||||
this.handleCompatible();
|
||||
};
|
||||
NestedScroll.prototype.appendBScroll = function (scroll) {
|
||||
this.stores.push(scroll);
|
||||
};
|
||||
NestedScroll.prototype.removeBScroll = function (scroll) {
|
||||
var index = this.stores.indexOf(scroll);
|
||||
if (index === -1) return;
|
||||
scroll.wrapper.isBScrollContainer = undefined;
|
||||
this.stores.splice(index, 1);
|
||||
};
|
||||
NestedScroll.prototype.handleContainRelationship = function () {
|
||||
// bs's length <= 1
|
||||
var stores = this.stores;
|
||||
if (stores.length <= 1) {
|
||||
// there is only a childBScroll left.
|
||||
if (stores[0] && stores[0].__parentInfo) {
|
||||
stores[0].__parentInfo = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
var outerBS;
|
||||
var outerBSWrapper;
|
||||
var innerBS;
|
||||
var innerBSWrapper;
|
||||
// Need two layers of "For loop" to calculate parent-child relationship
|
||||
for (var i = 0; i < stores.length; i++) {
|
||||
outerBS = stores[i];
|
||||
outerBSWrapper = outerBS.wrapper;
|
||||
for (var j = 0; j < stores.length; j++) {
|
||||
innerBS = stores[j];
|
||||
innerBSWrapper = innerBS.wrapper;
|
||||
// same bs
|
||||
if (outerBS === innerBS) continue;
|
||||
// now start calculating
|
||||
if (!innerBSWrapper.contains(outerBSWrapper)) continue;
|
||||
// now innerBS contains outerBS
|
||||
// no parentInfo yet
|
||||
if (!outerBS.__parentInfo) {
|
||||
outerBS.__parentInfo = {
|
||||
parent: innerBS,
|
||||
depth: calculateDepths(outerBSWrapper, innerBSWrapper)
|
||||
};
|
||||
} else {
|
||||
// has parentInfo already!
|
||||
// just judge the "true" parent by depth
|
||||
// we regard the latest node as parent, not the furthest
|
||||
var currentDepths = calculateDepths(outerBSWrapper, innerBSWrapper);
|
||||
var prevDepths = outerBS.__parentInfo.depth;
|
||||
// refresh currentBS as parentScroll
|
||||
if (prevDepths > currentDepths) {
|
||||
outerBS.__parentInfo = {
|
||||
parent: innerBS,
|
||||
depth: currentDepths
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
NestedScroll.prototype.handleCompatible = function () {
|
||||
var pairs = this.availableBScrolls();
|
||||
var keys = ['duplicateClick', 'nestedScroll'];
|
||||
pairs.forEach(function (pair) {
|
||||
keys.forEach(function (key) {
|
||||
compatibleFeatures[key](pair);
|
||||
});
|
||||
});
|
||||
};
|
||||
NestedScroll.prototype.availableBScrolls = function () {
|
||||
var ret = [];
|
||||
ret = this.stores
|
||||
.filter(function (bs) {
|
||||
return !!bs.__parentInfo;
|
||||
})
|
||||
.map(function (bs) {
|
||||
return [bs.__parentInfo.parent, bs];
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
NestedScroll.pluginName = 'nestedScroll';
|
||||
return NestedScroll;
|
||||
})();
|
||||
function calculateDepths(childNode, parentNode) {
|
||||
var depth = 0;
|
||||
var parent = childNode.parentNode;
|
||||
while (parent && parent !== parentNode) {
|
||||
depth++;
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
function checkBeyondBoundary(scroll) {
|
||||
var _a = hasScroll(scroll),
|
||||
hasHorizontalScroll = _a.hasHorizontalScroll,
|
||||
hasVerticalScroll = _a.hasVerticalScroll;
|
||||
var _b = scroll.scroller,
|
||||
scrollBehaviorX = _b.scrollBehaviorX,
|
||||
scrollBehaviorY = _b.scrollBehaviorY;
|
||||
var hasReachLeft = scroll.x >= scroll.minScrollX && scrollBehaviorX.movingDirection === -1;
|
||||
var hasReachRight = scroll.x <= scroll.maxScrollX && scrollBehaviorX.movingDirection === 1;
|
||||
var hasReachTop = scroll.y >= scroll.minScrollY && scrollBehaviorY.movingDirection === -1;
|
||||
var hasReachBottom = scroll.y <= scroll.maxScrollY && scrollBehaviorY.movingDirection === 1;
|
||||
if (hasVerticalScroll) {
|
||||
return hasReachTop || hasReachBottom;
|
||||
} else if (hasHorizontalScroll) {
|
||||
return hasReachLeft || hasReachRight;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function setupData(scroll) {
|
||||
var _a = hasScroll(scroll),
|
||||
hasHorizontalScroll = _a.hasHorizontalScroll,
|
||||
hasVerticalScroll = _a.hasVerticalScroll;
|
||||
var _b = scroll.scroller,
|
||||
actions = _b.actions,
|
||||
scrollBehaviorX = _b.scrollBehaviorX,
|
||||
scrollBehaviorY = _b.scrollBehaviorY;
|
||||
actions.startTime = +new Date();
|
||||
if (hasVerticalScroll) {
|
||||
scrollBehaviorY.startPos = scrollBehaviorY.currentPos;
|
||||
} else if (hasHorizontalScroll) {
|
||||
scrollBehaviorX.startPos = scrollBehaviorX.currentPos;
|
||||
}
|
||||
}
|
||||
function hasScroll(scroll) {
|
||||
return {
|
||||
hasHorizontalScroll: scroll.scroller.scrollBehaviorX.hasScroll,
|
||||
hasVerticalScroll: scroll.scroller.scrollBehaviorY.hasScroll
|
||||
};
|
||||
}
|
||||
|
||||
export default NestedScroll;
|
||||
@@ -0,0 +1,30 @@
|
||||
module.exports = function patchedMomentum(current, start, time, lowerMargin, upperMargin, wrapperSize, options) {
|
||||
if (options === void 0) {
|
||||
options = this.options;
|
||||
}
|
||||
var distance = current - start;
|
||||
var speed = Math.abs(distance) / time;
|
||||
var deceleration = options.deceleration,
|
||||
swipeBounceTime = options.swipeBounceTime,
|
||||
swipeTime = options.swipeTime;
|
||||
var momentumData = {
|
||||
destination: current + (speed / deceleration) * (distance < 0 ? -1 : 1),
|
||||
duration: swipeTime,
|
||||
rate: 15
|
||||
};
|
||||
this.hooks.trigger(this.hooks.eventTypes.momentum, momentumData, distance);
|
||||
if (momentumData.destination < lowerMargin) {
|
||||
momentumData.destination = wrapperSize
|
||||
? Math.max(lowerMargin - wrapperSize / 4, lowerMargin - (wrapperSize / momentumData.rate) * speed)
|
||||
: lowerMargin;
|
||||
momentumData.duration = Math.abs(momentumData.destination - current) / speed;
|
||||
} else if (momentumData.destination > upperMargin) {
|
||||
momentumData.destination = wrapperSize
|
||||
? Math.min(upperMargin + wrapperSize / 4, upperMargin + (wrapperSize / momentumData.rate) * speed)
|
||||
: upperMargin;
|
||||
// momentumData.duration = swipeBounceTime;
|
||||
momentumData.duration = Math.abs(momentumData.destination - current) / speed;
|
||||
}
|
||||
momentumData.destination = Math.round(momentumData.destination);
|
||||
return momentumData;
|
||||
};
|
||||
@@ -0,0 +1,940 @@
|
||||
/*!
|
||||
* better-scroll / slide
|
||||
* (c) 2016-2020 ustbhuangyi
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
//adapted to Noodl's more dynamic delta update environment:
|
||||
// - A scroll refresh doesn't reset the current slide page position
|
||||
// - Horizontal slider doesn't break when there are no children
|
||||
|
||||
function warn(msg) {
|
||||
console.error('[BScroll warn]: ' + msg);
|
||||
}
|
||||
|
||||
// ssr support
|
||||
var inBrowser = typeof window !== 'undefined';
|
||||
var ua = inBrowser && navigator.userAgent.toLowerCase();
|
||||
var isWeChatDevTools = ua && /wechatdevtools/.test(ua);
|
||||
var isAndroid = ua && ua.indexOf('android') > 0;
|
||||
|
||||
function extend(target) {
|
||||
var rest = [];
|
||||
for (var _i = 1; _i < arguments.length; _i++) {
|
||||
rest[_i - 1] = arguments[_i];
|
||||
}
|
||||
for (var i = 0; i < rest.length; i++) {
|
||||
var source = rest[i];
|
||||
for (var key in source) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function fixInboundValue(x, min, max) {
|
||||
if (x < min) {
|
||||
return min;
|
||||
}
|
||||
if (x > max) {
|
||||
return max;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
var elementStyle = inBrowser && document.createElement('div').style;
|
||||
var vendor = (function () {
|
||||
if (!inBrowser) {
|
||||
return false;
|
||||
}
|
||||
var transformNames = {
|
||||
webkit: 'webkitTransform',
|
||||
Moz: 'MozTransform',
|
||||
O: 'OTransform',
|
||||
ms: 'msTransform',
|
||||
standard: 'transform'
|
||||
};
|
||||
for (var key in transformNames) {
|
||||
if (elementStyle[transformNames[key]] !== undefined) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
function prefixStyle(style) {
|
||||
if (vendor === false) {
|
||||
return style;
|
||||
}
|
||||
if (vendor === 'standard') {
|
||||
if (style === 'transitionEnd') {
|
||||
return 'transitionend';
|
||||
}
|
||||
return style;
|
||||
}
|
||||
return vendor + style.charAt(0).toUpperCase() + style.substr(1);
|
||||
}
|
||||
var cssVendor = vendor && vendor !== 'standard' ? '-' + vendor.toLowerCase() + '-' : '';
|
||||
var transform = prefixStyle('transform');
|
||||
var transition = prefixStyle('transition');
|
||||
var hasPerspective = inBrowser && prefixStyle('perspective') in elementStyle;
|
||||
var style = {
|
||||
transform: transform,
|
||||
transition: transition,
|
||||
transitionTimingFunction: prefixStyle('transitionTimingFunction'),
|
||||
transitionDuration: prefixStyle('transitionDuration'),
|
||||
transitionDelay: prefixStyle('transitionDelay'),
|
||||
transformOrigin: prefixStyle('transformOrigin'),
|
||||
transitionEnd: prefixStyle('transitionEnd')
|
||||
};
|
||||
function getRect(el) {
|
||||
if (el instanceof window.SVGElement) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
top: el.offsetTop,
|
||||
left: el.offsetLeft,
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight
|
||||
};
|
||||
}
|
||||
}
|
||||
function prepend(el, target) {
|
||||
var firstChild = target.firstChild;
|
||||
if (firstChild) {
|
||||
before(el, firstChild);
|
||||
} else {
|
||||
target.appendChild(el);
|
||||
}
|
||||
}
|
||||
function before(el, target) {
|
||||
target.parentNode.insertBefore(el, target);
|
||||
}
|
||||
function removeChild(el, child) {
|
||||
el.removeChild(child);
|
||||
}
|
||||
|
||||
var ease = {
|
||||
// easeOutQuint
|
||||
swipe: {
|
||||
style: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
||||
fn: function (t) {
|
||||
return 1 + --t * t * t * t * t;
|
||||
}
|
||||
},
|
||||
// easeOutQuard
|
||||
swipeBounce: {
|
||||
style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
fn: function (t) {
|
||||
return t * (2 - t);
|
||||
}
|
||||
},
|
||||
// easeOutQuart
|
||||
bounce: {
|
||||
style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
|
||||
fn: function (t) {
|
||||
return 1 - --t * t * t * t;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var DEFAULT_INTERVAL = 100 / 60;
|
||||
var windowCompat = inBrowser && window;
|
||||
function noop() {}
|
||||
var requestAnimationFrame = (function () {
|
||||
if (!inBrowser) {
|
||||
/* istanbul ignore if */
|
||||
return noop;
|
||||
}
|
||||
return (
|
||||
windowCompat.requestAnimationFrame ||
|
||||
windowCompat.webkitRequestAnimationFrame ||
|
||||
windowCompat.mozRequestAnimationFrame ||
|
||||
windowCompat.oRequestAnimationFrame ||
|
||||
// if all else fails, use setTimeout
|
||||
function (callback) {
|
||||
return window.setTimeout(callback, (callback.interval || DEFAULT_INTERVAL) / 2); // make interval as precise as possible.
|
||||
}
|
||||
);
|
||||
})();
|
||||
var cancelAnimationFrame = (function () {
|
||||
if (!inBrowser) {
|
||||
/* istanbul ignore if */
|
||||
return noop;
|
||||
}
|
||||
return (
|
||||
windowCompat.cancelAnimationFrame ||
|
||||
windowCompat.webkitCancelAnimationFrame ||
|
||||
windowCompat.mozCancelAnimationFrame ||
|
||||
windowCompat.oCancelAnimationFrame ||
|
||||
function (id) {
|
||||
window.clearTimeout(id);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
var PagesPos = /** @class */ (function () {
|
||||
function PagesPos(scroll, slideOpt) {
|
||||
this.scroll = scroll;
|
||||
this.slideOpt = slideOpt;
|
||||
this.slideEl = null;
|
||||
this.init();
|
||||
}
|
||||
PagesPos.prototype.init = function () {
|
||||
var scrollerIns = this.scroll.scroller;
|
||||
var scrollBehaviorX = scrollerIns.scrollBehaviorX;
|
||||
var scrollBehaviorY = scrollerIns.scrollBehaviorY;
|
||||
var wrapper = getRect(scrollerIns.wrapper);
|
||||
var scroller = getRect(scrollerIns.content);
|
||||
this.wrapperWidth = wrapper.width;
|
||||
this.wrapperHeight = wrapper.height;
|
||||
this.scrollerHeight = scrollBehaviorY.hasScroll ? scroller.height : wrapper.height;
|
||||
this.scrollerWidth = scrollBehaviorX.hasScroll ? scroller.width : wrapper.width;
|
||||
var stepX = this.slideOpt.stepX || this.wrapperWidth;
|
||||
var stepY = this.slideOpt.stepY || this.wrapperHeight;
|
||||
var slideEls = scrollerIns.content;
|
||||
var el = this.slideOpt.el;
|
||||
if (typeof el === 'string') {
|
||||
this.slideEl = slideEls.querySelectorAll(el);
|
||||
}
|
||||
this.pages = this.slideEl ? this.computePagePosInfoByEl(this.slideEl) : this.computePagePosInfo(stepX, stepY);
|
||||
this.xLen = this.pages ? this.pages.length : 0;
|
||||
this.yLen = this.pages && this.pages[0] ? this.pages[0].length : 0;
|
||||
};
|
||||
PagesPos.prototype.hasInfo = function () {
|
||||
if (!this.pages || !this.pages.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
PagesPos.prototype.getPos = function (x, y) {
|
||||
return this.pages[x] ? this.pages[x][y] : null;
|
||||
};
|
||||
PagesPos.prototype.getNearestPage = function (x, y) {
|
||||
if (!this.hasInfo()) {
|
||||
return;
|
||||
}
|
||||
var pageX = 0;
|
||||
var pageY = 0;
|
||||
var l = this.pages.length;
|
||||
for (; pageX < l - 1; pageX++) {
|
||||
if (x >= this.pages[pageX][0].cx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
l = this.pages[pageX].length;
|
||||
for (; pageY < l - 1; pageY++) {
|
||||
if (y >= this.pages[0][pageY].cy) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
pageX: pageX,
|
||||
pageY: pageY
|
||||
};
|
||||
};
|
||||
PagesPos.prototype.computePagePosInfo = function (stepX, stepY) {
|
||||
var pages = [];
|
||||
var x = 0;
|
||||
var y;
|
||||
var cx;
|
||||
var cy;
|
||||
var i = 0;
|
||||
var l;
|
||||
var maxScrollPosX = this.scroll.scroller.scrollBehaviorX.maxScrollPos;
|
||||
var maxScrollPosY = this.scroll.scroller.scrollBehaviorY.maxScrollPos;
|
||||
cx = Math.round(stepX / 2);
|
||||
cy = Math.round(stepY / 2);
|
||||
while (x > -this.scrollerWidth) {
|
||||
pages[i] = [];
|
||||
l = 0;
|
||||
y = 0;
|
||||
while (y > -this.scrollerHeight) {
|
||||
pages[i][l] = {
|
||||
x: Math.max(x, maxScrollPosX),
|
||||
y: Math.max(y, maxScrollPosY),
|
||||
width: stepX,
|
||||
height: stepY,
|
||||
cx: x - cx,
|
||||
cy: y - cy
|
||||
};
|
||||
y -= stepY;
|
||||
l++;
|
||||
}
|
||||
x -= stepX;
|
||||
i++;
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
PagesPos.prototype.computePagePosInfoByEl = function (el) {
|
||||
var pages = [];
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
var cx;
|
||||
var cy;
|
||||
var i = 0;
|
||||
var l = el.length;
|
||||
var m = 0;
|
||||
var n = -1;
|
||||
var rect;
|
||||
var maxScrollX = this.scroll.scroller.scrollBehaviorX.maxScrollPos;
|
||||
var maxScrollY = this.scroll.scroller.scrollBehaviorY.maxScrollPos;
|
||||
for (; i < l; i++) {
|
||||
rect = getRect(el[i]);
|
||||
if (i === 0 || rect.left <= getRect(el[i - 1]).left) {
|
||||
m = 0;
|
||||
n++;
|
||||
}
|
||||
if (!pages[m]) {
|
||||
pages[m] = [];
|
||||
}
|
||||
x = Math.max(-rect.left, maxScrollX);
|
||||
y = Math.max(-rect.top, maxScrollY);
|
||||
cx = x - Math.round(rect.width / 2);
|
||||
cy = y - Math.round(rect.height / 2);
|
||||
pages[m][n] = {
|
||||
x: x,
|
||||
y: y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
cx: cx,
|
||||
cy: cy
|
||||
};
|
||||
if (x > maxScrollX) {
|
||||
m++;
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
return PagesPos;
|
||||
})();
|
||||
|
||||
var PageInfo = /** @class */ (function () {
|
||||
function PageInfo(scroll, slideOpt) {
|
||||
this.scroll = scroll;
|
||||
this.slideOpt = slideOpt;
|
||||
}
|
||||
PageInfo.prototype.init = function () {
|
||||
this.currentPage = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
pageX: 0,
|
||||
pageY: 0
|
||||
};
|
||||
this.pagesPos = new PagesPos(this.scroll, this.slideOpt);
|
||||
this.checkSlideLoop();
|
||||
};
|
||||
PageInfo.prototype.changeCurrentPage = function (newPage) {
|
||||
this.currentPage = newPage;
|
||||
};
|
||||
PageInfo.prototype.change2safePage = function (pageX, pageY) {
|
||||
if (!this.pagesPos.hasInfo()) {
|
||||
return;
|
||||
}
|
||||
if (pageX >= this.pagesPos.xLen) {
|
||||
pageX = this.pagesPos.xLen - 1;
|
||||
} else if (pageX < 0) {
|
||||
pageX = 0;
|
||||
}
|
||||
if (pageY >= this.pagesPos.yLen) {
|
||||
pageY = this.pagesPos.yLen - 1;
|
||||
} else if (pageY < 0) {
|
||||
pageY = 0;
|
||||
}
|
||||
var _a = this.pagesPos.getPos(pageX, pageY);
|
||||
return {
|
||||
pageX: pageX,
|
||||
pageY: pageY,
|
||||
x: _a ? _a.x : 0,
|
||||
y: _a ? _a.y : 0
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.getInitPage = function () {
|
||||
var initPageX = this.loopX ? 1 : 0;
|
||||
var initPageY = this.loopY ? 1 : 0;
|
||||
return {
|
||||
pageX: initPageX,
|
||||
pageY: initPageY
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.getRealPage = function (page) {
|
||||
var fixedPage = function (page, realPageLen) {
|
||||
var pageIndex = [];
|
||||
for (var i = 0; i < realPageLen; i++) {
|
||||
pageIndex.push(i);
|
||||
}
|
||||
pageIndex.unshift(realPageLen - 1);
|
||||
pageIndex.push(0);
|
||||
return pageIndex[page];
|
||||
};
|
||||
var currentPage = page ? extend({}, page) : extend({}, this.currentPage);
|
||||
if (this.loopX) {
|
||||
currentPage.pageX = fixedPage(currentPage.pageX, this.pagesPos.xLen - 2);
|
||||
}
|
||||
if (this.loopY) {
|
||||
currentPage.pageY = fixedPage(currentPage.pageY, this.pagesPos.yLen - 2);
|
||||
}
|
||||
return {
|
||||
pageX: currentPage.pageX,
|
||||
pageY: currentPage.pageY
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.getPageSize = function () {
|
||||
return this.pagesPos.getPos(this.currentPage.pageX, this.currentPage.pageY);
|
||||
};
|
||||
PageInfo.prototype.realPage2Page = function (x, y) {
|
||||
if (!this.pagesPos.hasInfo()) {
|
||||
return;
|
||||
}
|
||||
var lastX = this.pagesPos.xLen - 1;
|
||||
var lastY = this.pagesPos.yLen - 1;
|
||||
var firstX = 0;
|
||||
var firstY = 0;
|
||||
if (this.loopX) {
|
||||
x += 1;
|
||||
firstX = firstX + 1;
|
||||
lastX = lastX - 1;
|
||||
}
|
||||
if (this.loopY) {
|
||||
y += 1;
|
||||
firstY = firstY + 1;
|
||||
lastY = lastY - 1;
|
||||
}
|
||||
x = fixInboundValue(x, firstX, lastX);
|
||||
y = fixInboundValue(y, firstY, lastY);
|
||||
return {
|
||||
realX: x,
|
||||
realY: y
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.nextPage = function () {
|
||||
return this.changedPageNum('positive' /* Positive */);
|
||||
};
|
||||
PageInfo.prototype.prevPage = function () {
|
||||
return this.changedPageNum('negative' /* Negative */);
|
||||
};
|
||||
PageInfo.prototype.nearestPage = function (x, y, directionX, directionY) {
|
||||
var pageInfo = this.pagesPos.getNearestPage(x, y);
|
||||
if (!pageInfo) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
pageX: 0,
|
||||
pageY: 0
|
||||
};
|
||||
}
|
||||
var pageX = pageInfo.pageX;
|
||||
var pageY = pageInfo.pageY;
|
||||
var newX;
|
||||
var newY;
|
||||
if (pageX === this.currentPage.pageX) {
|
||||
pageX += directionX;
|
||||
pageX = fixInboundValue(pageX, 0, this.pagesPos.xLen - 1);
|
||||
}
|
||||
if (pageY === this.currentPage.pageY) {
|
||||
pageY += directionY;
|
||||
pageY = fixInboundValue(pageInfo.pageY, 0, this.pagesPos.yLen - 1);
|
||||
}
|
||||
newX = this.pagesPos.getPos(pageX, 0).x;
|
||||
newY = this.pagesPos.getPos(0, pageY).y;
|
||||
return {
|
||||
x: newX,
|
||||
y: newY,
|
||||
pageX: pageX,
|
||||
pageY: pageY
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.getLoopStage = function () {
|
||||
if (!this.needLoop) {
|
||||
return 'middle' /* Middle */;
|
||||
}
|
||||
if (this.loopX) {
|
||||
if (this.currentPage.pageX === 0) {
|
||||
return 'head' /* Head */;
|
||||
}
|
||||
if (this.currentPage.pageX === this.pagesPos.xLen - 1) {
|
||||
return 'tail' /* Tail */;
|
||||
}
|
||||
}
|
||||
if (this.loopY) {
|
||||
if (this.currentPage.pageY === 0) {
|
||||
return 'head' /* Head */;
|
||||
}
|
||||
if (this.currentPage.pageY === this.pagesPos.yLen - 1) {
|
||||
return 'tail' /* Tail */;
|
||||
}
|
||||
}
|
||||
return 'middle' /* Middle */;
|
||||
};
|
||||
PageInfo.prototype.resetLoopPage = function () {
|
||||
if (this.loopX) {
|
||||
if (this.currentPage.pageX === 0) {
|
||||
return {
|
||||
pageX: this.pagesPos.xLen - 2,
|
||||
pageY: this.currentPage.pageY
|
||||
};
|
||||
}
|
||||
if (this.currentPage.pageX === this.pagesPos.xLen - 1) {
|
||||
return {
|
||||
pageX: 1,
|
||||
pageY: this.currentPage.pageY
|
||||
};
|
||||
}
|
||||
}
|
||||
if (this.loopY) {
|
||||
if (this.currentPage.pageY === 0) {
|
||||
return {
|
||||
pageX: this.currentPage.pageX,
|
||||
pageY: this.pagesPos.yLen - 2
|
||||
};
|
||||
}
|
||||
if (this.currentPage.pageY === this.pagesPos.yLen - 1) {
|
||||
return {
|
||||
pageX: this.currentPage.pageX,
|
||||
pageY: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
PageInfo.prototype.isSameWithCurrent = function (page) {
|
||||
if (page.pageX !== this.currentPage.pageX || page.pageY !== this.currentPage.pageY) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
PageInfo.prototype.changedPageNum = function (direction) {
|
||||
var x = this.currentPage.pageX;
|
||||
var y = this.currentPage.pageY;
|
||||
if (this.slideX) {
|
||||
x = direction === 'negative' /* Negative */ ? x - 1 : x + 1;
|
||||
}
|
||||
if (this.slideY) {
|
||||
y = direction === 'negative' /* Negative */ ? y - 1 : y + 1;
|
||||
}
|
||||
return {
|
||||
pageX: x,
|
||||
pageY: y
|
||||
};
|
||||
};
|
||||
PageInfo.prototype.checkSlideLoop = function () {
|
||||
this.needLoop = this.slideOpt.loop;
|
||||
if (this.pagesPos.xLen > 1) {
|
||||
this.slideX = true;
|
||||
}
|
||||
if (this.pagesPos.pages[0] && this.pagesPos.yLen > 1) {
|
||||
this.slideY = true;
|
||||
}
|
||||
this.loopX = this.needLoop && this.slideX;
|
||||
this.loopY = this.needLoop && this.slideY;
|
||||
if (this.slideX && this.slideY) {
|
||||
warn('slide does not support two direction at the same time.');
|
||||
}
|
||||
};
|
||||
return PageInfo;
|
||||
})();
|
||||
|
||||
var sourcePrefix = 'plugins.slide';
|
||||
var propertiesMap = [
|
||||
{
|
||||
key: 'next',
|
||||
name: 'next'
|
||||
},
|
||||
{
|
||||
key: 'prev',
|
||||
name: 'prev'
|
||||
},
|
||||
{
|
||||
key: 'goToPage',
|
||||
name: 'goToPage'
|
||||
},
|
||||
{
|
||||
key: 'getCurrentPage',
|
||||
name: 'getCurrentPage'
|
||||
}
|
||||
];
|
||||
var propertiesConfig = propertiesMap.map(function (item) {
|
||||
return {
|
||||
key: item.key,
|
||||
sourceKey: sourcePrefix + '.' + item.name
|
||||
};
|
||||
});
|
||||
|
||||
var Slide = /** @class */ (function () {
|
||||
function Slide(scroll) {
|
||||
this.scroll = scroll;
|
||||
this.resetLooping = false;
|
||||
this.isTouching = false;
|
||||
this.scroll.proxy(propertiesConfig);
|
||||
this.scroll.registerType(['slideWillChange']);
|
||||
this.slideOpt = this.scroll.options.slide;
|
||||
this.page = new PageInfo(scroll, this.slideOpt);
|
||||
this.hooksFn = [];
|
||||
this.willChangeToPage = {
|
||||
pageX: 0,
|
||||
pageY: 0
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
Slide.prototype.init = function () {
|
||||
var _this = this;
|
||||
var slide = this.slideOpt;
|
||||
var slideEls = this.scroll.scroller.content;
|
||||
var lazyInitByRefresh = false;
|
||||
if (slide.loop) {
|
||||
var children = slideEls.children;
|
||||
if (children.length > 1) {
|
||||
this.cloneSlideEleForLoop(slideEls);
|
||||
lazyInitByRefresh = true;
|
||||
} else {
|
||||
// Loop does not make any sense if there is only one child.
|
||||
slide.loop = false;
|
||||
}
|
||||
}
|
||||
var shouldRefreshByWidth = this.setSlideWidth(slideEls);
|
||||
var shouldRefreshByHeight = this.setSlideHeight(this.scroll.scroller.wrapper, slideEls);
|
||||
var shouldRefresh = shouldRefreshByWidth || shouldRefreshByHeight;
|
||||
var scrollHooks = this.scroll.hooks;
|
||||
var scrollerHooks = this.scroll.scroller.hooks;
|
||||
this.registorHooks(scrollHooks, 'refresh', this.initSlideState);
|
||||
this.registorHooks(scrollHooks, 'destroy', this.destroy);
|
||||
this.registorHooks(scrollerHooks, 'momentum', this.modifyScrollMetaHandler);
|
||||
// scrollEnd handler should be called before customized handlers
|
||||
this.registorHooks(this.scroll, 'scrollEnd', this.amendCurrentPage);
|
||||
this.registorHooks(scrollerHooks, 'beforeStart', this.setTouchFlag);
|
||||
this.registorHooks(scrollerHooks, 'scroll', this.scrollMoving);
|
||||
this.registorHooks(scrollerHooks, 'resize', this.resize);
|
||||
// for mousewheel event
|
||||
if (this.scroll.eventTypes.mousewheelMove && this.scroll.eventTypes.mousewheelEnd) {
|
||||
this.registorHooks(this.scroll, 'mousewheelMove', function () {
|
||||
// prevent default action of mousewheelMove
|
||||
return true;
|
||||
});
|
||||
this.registorHooks(this.scroll, 'mousewheelEnd', function (delta) {
|
||||
if (delta.directionX === 1 /* Positive */ || delta.directionY === 1 /* Positive */) {
|
||||
_this.next();
|
||||
}
|
||||
if (delta.directionX === -1 /* Negative */ || delta.directionY === -1 /* Negative */) {
|
||||
_this.prev();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (slide.listenFlick !== false) {
|
||||
this.registorHooks(scrollerHooks, 'flick', this.flickHandler);
|
||||
}
|
||||
if (!lazyInitByRefresh && !shouldRefresh) {
|
||||
this.initSlideState();
|
||||
} else {
|
||||
this.scroll.refresh();
|
||||
}
|
||||
};
|
||||
Slide.prototype.resize = function () {
|
||||
var _this = this;
|
||||
var slideEls = this.scroll.scroller.content;
|
||||
var slideWrapper = this.scroll.scroller.wrapper;
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = window.setTimeout(function () {
|
||||
_this.clearSlideWidth(slideEls);
|
||||
_this.clearSlideHeight(slideEls);
|
||||
_this.setSlideWidth(slideEls);
|
||||
_this.setSlideHeight(slideWrapper, slideEls);
|
||||
_this.scroll.refresh();
|
||||
}, this.scroll.options.resizePolling);
|
||||
return true;
|
||||
};
|
||||
Slide.prototype.next = function (time, easing) {
|
||||
var _a = this.page.nextPage(),
|
||||
pageX = _a.pageX,
|
||||
pageY = _a.pageY;
|
||||
this.goTo(pageX, pageY, time, easing);
|
||||
};
|
||||
Slide.prototype.prev = function (time, easing) {
|
||||
var _a = this.page.prevPage(),
|
||||
pageX = _a.pageX,
|
||||
pageY = _a.pageY;
|
||||
this.goTo(pageX, pageY, time, easing);
|
||||
};
|
||||
Slide.prototype.goToPage = function (x, y, time, easing) {
|
||||
var pageInfo = this.page.realPage2Page(x, y);
|
||||
if (!pageInfo) {
|
||||
return;
|
||||
}
|
||||
this.goTo(pageInfo.realX, pageInfo.realY, time, easing);
|
||||
};
|
||||
Slide.prototype.getCurrentPage = function () {
|
||||
return this.page.getRealPage();
|
||||
};
|
||||
Slide.prototype.nearestPage = function (x, y) {
|
||||
var scrollBehaviorX = this.scroll.scroller.scrollBehaviorX;
|
||||
var scrollBehaviorY = this.scroll.scroller.scrollBehaviorY;
|
||||
var triggerThreshold = true;
|
||||
if (
|
||||
Math.abs(x - scrollBehaviorX.absStartPos) <= this.thresholdX &&
|
||||
Math.abs(y - scrollBehaviorY.absStartPos) <= this.thresholdY
|
||||
) {
|
||||
triggerThreshold = false;
|
||||
}
|
||||
if (!triggerThreshold) {
|
||||
return this.page.currentPage;
|
||||
}
|
||||
return this.page.nearestPage(
|
||||
fixInboundValue(x, scrollBehaviorX.maxScrollPos, scrollBehaviorX.minScrollPos),
|
||||
fixInboundValue(y, scrollBehaviorY.maxScrollPos, scrollBehaviorY.minScrollPos),
|
||||
scrollBehaviorX.direction,
|
||||
scrollBehaviorY.direction
|
||||
);
|
||||
};
|
||||
Slide.prototype.destroy = function () {
|
||||
var slideEls = this.scroll.scroller.content;
|
||||
if (this.slideOpt.loop) {
|
||||
var children = slideEls.children;
|
||||
if (children.length > 2) {
|
||||
removeChild(slideEls, children[children.length - 1]);
|
||||
removeChild(slideEls, children[0]);
|
||||
}
|
||||
}
|
||||
this.hooksFn.forEach(function (item) {
|
||||
var hooks = item[0];
|
||||
var hooksName = item[1];
|
||||
var handlerFn = item[2];
|
||||
if (hooks.eventTypes[hooksName]) {
|
||||
hooks.off(hooksName, handlerFn);
|
||||
}
|
||||
});
|
||||
this.hooksFn.length = 0;
|
||||
};
|
||||
Slide.prototype.initSlideState = function () {
|
||||
const prevPage = this.page.currentPage;
|
||||
this.page.init();
|
||||
if (prevPage) {
|
||||
this.page.currentPage = prevPage;
|
||||
} else {
|
||||
var initPage = this.page.getInitPage();
|
||||
this.goTo(initPage.pageX, initPage.pageY, 0);
|
||||
}
|
||||
|
||||
this.initThreshold();
|
||||
};
|
||||
Slide.prototype.initThreshold = function () {
|
||||
var slideThreshold = this.slideOpt.threshold || 0.1;
|
||||
if (slideThreshold % 1 === 0) {
|
||||
this.thresholdX = slideThreshold;
|
||||
this.thresholdY = slideThreshold;
|
||||
} else {
|
||||
var pageSize = this.page.getPageSize();
|
||||
if (pageSize) {
|
||||
this.thresholdX = Math.round(pageSize.width * slideThreshold);
|
||||
this.thresholdY = Math.round(pageSize.height * slideThreshold);
|
||||
}
|
||||
}
|
||||
};
|
||||
Slide.prototype.cloneSlideEleForLoop = function (slideEls) {
|
||||
var children = slideEls.children;
|
||||
prepend(children[children.length - 1].cloneNode(true), slideEls);
|
||||
slideEls.appendChild(children[1].cloneNode(true));
|
||||
};
|
||||
Slide.prototype.amendCurrentPage = function () {
|
||||
this.isTouching = false;
|
||||
if (!this.slideOpt.loop) {
|
||||
return;
|
||||
}
|
||||
// triggered by resetLoop
|
||||
if (this.resetLooping) {
|
||||
this.resetLooping = false;
|
||||
return;
|
||||
}
|
||||
// fix bug: scroll two page or even more page at once and fetch the boundary.
|
||||
// In this case, momentum won't be trigger, so the pageIndex will be wrong and won't be trigger reset.
|
||||
var isScrollToBoundary = false;
|
||||
if (
|
||||
this.page.loopX &&
|
||||
(this.scroll.x === this.scroll.scroller.scrollBehaviorX.minScrollPos ||
|
||||
this.scroll.x === this.scroll.scroller.scrollBehaviorX.maxScrollPos)
|
||||
) {
|
||||
isScrollToBoundary = true;
|
||||
}
|
||||
if (
|
||||
this.page.loopY &&
|
||||
(this.scroll.y === this.scroll.scroller.scrollBehaviorY.minScrollPos ||
|
||||
this.scroll.y === this.scroll.scroller.scrollBehaviorY.maxScrollPos)
|
||||
) {
|
||||
isScrollToBoundary = true;
|
||||
}
|
||||
if (isScrollToBoundary) {
|
||||
var scrollBehaviorX = this.scroll.scroller.scrollBehaviorX;
|
||||
var scrollBehaviorY = this.scroll.scroller.scrollBehaviorY;
|
||||
var newPos = this.page.nearestPage(
|
||||
fixInboundValue(this.scroll.x, scrollBehaviorX.maxScrollPos, scrollBehaviorX.minScrollPos),
|
||||
fixInboundValue(this.scroll.y, scrollBehaviorY.maxScrollPos, scrollBehaviorY.minScrollPos),
|
||||
0,
|
||||
0
|
||||
);
|
||||
var newPage = {
|
||||
x: newPos.x,
|
||||
y: newPos.y,
|
||||
pageX: newPos.pageX,
|
||||
pageY: newPos.pageY
|
||||
};
|
||||
if (!this.page.isSameWithCurrent(newPage)) {
|
||||
this.page.changeCurrentPage(newPage);
|
||||
}
|
||||
}
|
||||
var changePage = this.page.resetLoopPage();
|
||||
if (changePage) {
|
||||
this.resetLooping = true;
|
||||
this.goTo(changePage.pageX, changePage.pageY, 0);
|
||||
return true; // stop trigger chain
|
||||
}
|
||||
// amend willChangeToPage, because willChangeToPage maybe wrong when sliding quickly
|
||||
this.pageWillChangeTo(this.page.currentPage);
|
||||
};
|
||||
Slide.prototype.shouldSetWidthHeight = function (checkType) {
|
||||
var checkMap = {
|
||||
width: ['scrollX', 'disableSetWidth'],
|
||||
height: ['scrollY', 'disableSetHeight']
|
||||
};
|
||||
var checkOption = checkMap[checkType];
|
||||
if (!this.scroll.options[checkOption[0]]) {
|
||||
return false;
|
||||
}
|
||||
if (this.slideOpt[checkOption[1]]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
Slide.prototype.clearSlideWidth = function (slideEls) {
|
||||
if (!this.shouldSetWidthHeight('width')) {
|
||||
return;
|
||||
}
|
||||
var children = slideEls.children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var slideItemDom = children[i];
|
||||
slideItemDom.removeAttribute('style');
|
||||
}
|
||||
slideEls.removeAttribute('style');
|
||||
};
|
||||
Slide.prototype.setSlideWidth = function (slideEls) {
|
||||
if (!this.shouldSetWidthHeight('width')) {
|
||||
return false;
|
||||
}
|
||||
var children = slideEls.children;
|
||||
var slideItemWidth = children[0].clientWidth;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var slideItemDom = children[i];
|
||||
slideItemDom.style.width = slideItemWidth + 'px';
|
||||
}
|
||||
slideEls.style.width = slideItemWidth * children.length + 'px';
|
||||
return true;
|
||||
};
|
||||
Slide.prototype.clearSlideHeight = function (slideEls) {
|
||||
if (!this.shouldSetWidthHeight('height')) {
|
||||
return;
|
||||
}
|
||||
var children = slideEls.children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var slideItemDom = children[i];
|
||||
slideItemDom.removeAttribute('style');
|
||||
}
|
||||
slideEls.removeAttribute('style');
|
||||
};
|
||||
// height change will not effect minScrollY & maxScrollY
|
||||
Slide.prototype.setSlideHeight = function (slideWrapper, slideEls) {
|
||||
if (!this.shouldSetWidthHeight('height')) {
|
||||
return false;
|
||||
}
|
||||
var wrapperHeight = slideWrapper.clientHeight;
|
||||
var children = slideEls.children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var slideItemDom = children[i];
|
||||
slideItemDom.style.height = wrapperHeight + 'px';
|
||||
}
|
||||
slideEls.style.height = wrapperHeight * children.length + 'px';
|
||||
return true;
|
||||
};
|
||||
Slide.prototype.goTo = function (pageX, pageY, time, easing) {
|
||||
if (pageY === void 0) {
|
||||
pageY = 0;
|
||||
}
|
||||
var newPageInfo = this.page.change2safePage(pageX, pageY);
|
||||
if (!newPageInfo) {
|
||||
return;
|
||||
}
|
||||
var scrollEasing = easing || this.slideOpt.easing || ease.bounce;
|
||||
var posX = newPageInfo.x;
|
||||
var posY = newPageInfo.y;
|
||||
var deltaX = posX - this.scroll.scroller.scrollBehaviorX.currentPos;
|
||||
var deltaY = posY - this.scroll.scroller.scrollBehaviorY.currentPos;
|
||||
if (!deltaX && !deltaY) {
|
||||
return;
|
||||
}
|
||||
time = time === undefined ? this.getAnimateTime(deltaX, deltaY) : time;
|
||||
this.page.changeCurrentPage({
|
||||
x: posX,
|
||||
y: posY,
|
||||
pageX: newPageInfo.pageX,
|
||||
pageY: newPageInfo.pageY
|
||||
});
|
||||
this.pageWillChangeTo(this.page.currentPage);
|
||||
this.scroll.scroller.scrollTo(posX, posY, time, scrollEasing);
|
||||
};
|
||||
Slide.prototype.flickHandler = function () {
|
||||
var scrollBehaviorX = this.scroll.scroller.scrollBehaviorX;
|
||||
var scrollBehaviorY = this.scroll.scroller.scrollBehaviorY;
|
||||
var deltaX = scrollBehaviorX.currentPos - scrollBehaviorX.startPos;
|
||||
var deltaY = scrollBehaviorY.currentPos - scrollBehaviorY.startPos;
|
||||
var time = this.getAnimateTime(deltaX, deltaY);
|
||||
this.goTo(
|
||||
this.page.currentPage.pageX + scrollBehaviorX.direction,
|
||||
this.page.currentPage.pageY + scrollBehaviorY.direction,
|
||||
time
|
||||
);
|
||||
};
|
||||
Slide.prototype.getAnimateTime = function (deltaX, deltaY) {
|
||||
if (this.slideOpt.speed) {
|
||||
return this.slideOpt.speed;
|
||||
}
|
||||
return Math.max(Math.max(Math.min(Math.abs(deltaX), 1000), Math.min(Math.abs(deltaY), 1000)), 300);
|
||||
};
|
||||
Slide.prototype.modifyScrollMetaHandler = function (scrollMeta) {
|
||||
var newPos = this.nearestPage(scrollMeta.newX, scrollMeta.newY);
|
||||
scrollMeta.time = this.getAnimateTime(scrollMeta.newX - newPos.x, scrollMeta.newY - newPos.y);
|
||||
scrollMeta.newX = newPos.x;
|
||||
scrollMeta.newY = newPos.y;
|
||||
scrollMeta.easing = this.slideOpt.easing || ease.bounce;
|
||||
this.page.changeCurrentPage({
|
||||
x: scrollMeta.newX,
|
||||
y: scrollMeta.newY,
|
||||
pageX: newPos.pageX,
|
||||
pageY: newPos.pageY
|
||||
});
|
||||
this.pageWillChangeTo(this.page.currentPage);
|
||||
};
|
||||
Slide.prototype.scrollMoving = function (point) {
|
||||
if (this.isTouching) {
|
||||
var newPos = this.nearestPage(point.x, point.y);
|
||||
this.pageWillChangeTo(newPos);
|
||||
}
|
||||
};
|
||||
Slide.prototype.pageWillChangeTo = function (newPage) {
|
||||
var changeToPage = this.page.getRealPage(newPage);
|
||||
if (changeToPage.pageX === this.willChangeToPage.pageX && changeToPage.pageY === this.willChangeToPage.pageY) {
|
||||
return;
|
||||
}
|
||||
this.willChangeToPage = changeToPage;
|
||||
this.scroll.trigger('slideWillChange', this.willChangeToPage);
|
||||
};
|
||||
Slide.prototype.setTouchFlag = function () {
|
||||
this.isTouching = true;
|
||||
};
|
||||
Slide.prototype.registorHooks = function (hooks, name, handler) {
|
||||
hooks.on(name, handler, this);
|
||||
this.hooksFn.push([hooks, name, handler]);
|
||||
};
|
||||
Slide.pluginName = 'slide';
|
||||
return Slide;
|
||||
})();
|
||||
|
||||
export default Slide;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Icon',
|
||||
component: Icon,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof Icon>;
|
||||
|
||||
const Template: ComponentStory<typeof Icon> = (args) => <Icon {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '../../../layout';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface IconProps extends Noodl.ReactProps {
|
||||
iconSourceType: 'image' | 'icon';
|
||||
iconImageSource: Noodl.Image;
|
||||
iconIconSource: Noodl.Icon;
|
||||
iconSize: string;
|
||||
iconColor: Noodl.Color;
|
||||
}
|
||||
|
||||
export function Icon(props: IconProps) {
|
||||
const style: React.CSSProperties = { userSelect: 'none', ...props.style };
|
||||
Layout.size(style, props);
|
||||
Layout.align(style, props);
|
||||
|
||||
function _renderIcon() {
|
||||
const style: React.CSSProperties = {};
|
||||
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined) {
|
||||
style.width = props.iconSize;
|
||||
style.height = props.iconSize;
|
||||
return <img alt="" src={props.iconImageSource} style={style} />;
|
||||
} else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
|
||||
style.fontSize = props.iconSize;
|
||||
style.color = props.iconColor;
|
||||
style.lineHeight = 1;
|
||||
return (
|
||||
<div style={{ lineHeight: 0 }}>
|
||||
{props.iconIconSource.codeAsClass === true ? (
|
||||
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={style}></span>
|
||||
) : (
|
||||
<span className={props.iconIconSource.class} style={style}>
|
||||
{props.iconIconSource.code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let className = 'ndl-visual-icon';
|
||||
if (props.className) className = className + ' ' + props.className;
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
{_renderIcon()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Icon'
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Image } from './Image';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Image',
|
||||
component: Image,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Image>;
|
||||
|
||||
const Template: ComponentStory<typeof Image> = (args) => <Image {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '../../../layout';
|
||||
import PointerListeners from '../../../pointerlisteners';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface ImageProps extends Noodl.ReactProps {
|
||||
dom: {
|
||||
alt?: string;
|
||||
src: string;
|
||||
onLoad?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function Image(props: ImageProps) {
|
||||
const style = { ...props.style };
|
||||
|
||||
Layout.size(style, props);
|
||||
Layout.align(style, props);
|
||||
|
||||
if (style.opacity === 0) {
|
||||
style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
if (props.dom?.src?.startsWith('/')) {
|
||||
// @ts-expect-error missing Noodl typings
|
||||
const baseUrl = Noodl.Env['BaseUrl'];
|
||||
if (baseUrl) {
|
||||
props.dom.src = baseUrl + props.dom.src.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
return <img className={props.className} {...props.dom} {...PointerListeners(props)} style={style} />;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Image';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Text',
|
||||
component: Text,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Text>;
|
||||
|
||||
const Template: ComponentStory<typeof Text> = (args) => <Text {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '../../../layout';
|
||||
import PointerListeners from '../../../pointerlisteners';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface TextProps extends Noodl.ReactProps {
|
||||
as?: keyof JSX.IntrinsicElements | React.ComponentType<unknown>;
|
||||
|
||||
textStyle: Noodl.TextStyle;
|
||||
text: string;
|
||||
|
||||
sizeMode?: Noodl.SizeMode;
|
||||
width?: string;
|
||||
height?: string;
|
||||
fixedWidth?: boolean;
|
||||
fixedHeight?: boolean;
|
||||
|
||||
// Extra Attributes
|
||||
dom: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
const { as: Component = 'div' } = props;
|
||||
|
||||
const style = {
|
||||
...props.textStyle,
|
||||
...props.style
|
||||
};
|
||||
|
||||
Layout.size(style, props);
|
||||
Layout.align(style, props);
|
||||
|
||||
style.color = props.noodlNode.context.styles.resolveColor(style.color);
|
||||
|
||||
// Respect '\n' in the string
|
||||
if (props.sizeMode === 'contentSize' || props.sizeMode === 'contentWidth') {
|
||||
style.whiteSpace = 'pre';
|
||||
} else {
|
||||
style.whiteSpace = 'pre-wrap';
|
||||
style.overflowWrap = 'anywhere';
|
||||
}
|
||||
|
||||
if (style.opacity === 0) {
|
||||
style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={['ndl-visual-text', props.className].join(' ')}
|
||||
{...props.dom}
|
||||
{...PointerListeners(props)}
|
||||
style={style}
|
||||
>
|
||||
{String(props.text)}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Text';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Video } from './Video';
|
||||
|
||||
export default {
|
||||
title: 'CATEGORY_HERE/Video',
|
||||
component: Video,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Video>;
|
||||
|
||||
const Template: ComponentStory<typeof Video> = (args) => <Video {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '../../../layout';
|
||||
import PointerListeners from '../../../pointerlisteners';
|
||||
import { Noodl } from '../../../types';
|
||||
|
||||
export interface VideoProps extends Noodl.ReactProps {
|
||||
objectPositionX: string;
|
||||
objectPositionY: string;
|
||||
|
||||
dom: Exclude<CachedVideoProps, 'innerRef' | 'onCanPlay'>;
|
||||
|
||||
onCanPlay?: () => void;
|
||||
videoWidth?: (value: number) => void;
|
||||
videoHeight?: (value: number) => void;
|
||||
onVideoElementCreated?: (video) => void;
|
||||
}
|
||||
|
||||
export interface CachedVideoProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
|
||||
muted?: boolean;
|
||||
loop?: boolean;
|
||||
volume?: number;
|
||||
autoplay?: boolean;
|
||||
controls?: boolean;
|
||||
src: string;
|
||||
|
||||
innerRef: (video: HTMLVideoElement) => void;
|
||||
onCanPlay: () => void;
|
||||
}
|
||||
|
||||
class CachedVideo extends React.PureComponent<CachedVideoProps> {
|
||||
video: HTMLVideoElement;
|
||||
|
||||
shouldComponentUpdate(nextProps: CachedVideoProps) {
|
||||
if (this.video) {
|
||||
this.video.muted = nextProps.muted;
|
||||
this.video.loop = nextProps.loop;
|
||||
this.video.volume = nextProps.volume;
|
||||
this.video.autoplay = nextProps.autoplay;
|
||||
this.video.controls = nextProps.controls;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
let src = this.props.src ? this.props.src.toString() : undefined;
|
||||
|
||||
if (src) {
|
||||
if (src.indexOf('#t=') === -1) {
|
||||
src += '#t=0.01'; //force Android to render the first frame
|
||||
}
|
||||
if (src.startsWith('/')) {
|
||||
// @ts-expect-error missing Noodl typings
|
||||
const baseUrl = Noodl.Env['BaseUrl'];
|
||||
if (baseUrl) {
|
||||
src = baseUrl + src.substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
{...this.props}
|
||||
playsInline={true}
|
||||
src={src}
|
||||
{...PointerListeners(this.props)}
|
||||
ref={(video) => {
|
||||
this.video = video;
|
||||
this.props.innerRef(video);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Video extends React.Component<VideoProps> {
|
||||
wantToPlay: boolean;
|
||||
canPlay: boolean;
|
||||
video: HTMLVideoElement;
|
||||
|
||||
constructor(props: VideoProps) {
|
||||
super(props);
|
||||
|
||||
this.wantToPlay = false;
|
||||
this.canPlay = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.canPlay = false;
|
||||
}
|
||||
|
||||
setSourceObject(src) {
|
||||
if (this.video.srcObject !== src) {
|
||||
this.video.srcObject = src;
|
||||
this.canPlay = false; //wait for can play event
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
this.wantToPlay = true;
|
||||
if (this.canPlay) {
|
||||
this.video.play();
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.wantToPlay = true;
|
||||
if (this.canPlay) {
|
||||
this.video.currentTime = 0;
|
||||
this.video.play();
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.wantToPlay = false;
|
||||
this.video && this.video.pause();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.wantToPlay = false;
|
||||
if (this.video) {
|
||||
this.video.currentTime = 0;
|
||||
this.video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const style = {
|
||||
...props.style
|
||||
};
|
||||
|
||||
Layout.size(style, props);
|
||||
Layout.align(style, props);
|
||||
|
||||
if (style.opacity === 0) {
|
||||
style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
style.objectPosition = `${props.objectPositionX} ${props.objectPositionY}`;
|
||||
|
||||
return (
|
||||
<CachedVideo
|
||||
{...props.dom}
|
||||
className={props.className}
|
||||
style={style}
|
||||
innerRef={(video) => {
|
||||
this.video = video;
|
||||
this.props.onVideoElementCreated && this.props.onVideoElementCreated(video);
|
||||
}}
|
||||
onCanPlay={() => {
|
||||
this.canPlay = true;
|
||||
if (this.wantToPlay) {
|
||||
this.video.play();
|
||||
}
|
||||
this.props.onCanPlay && this.props.onCanPlay();
|
||||
this.props.videoWidth && this.props.videoWidth(this.video.videoWidth);
|
||||
this.props.videoHeight && this.props.videoHeight(this.video.videoHeight);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Video';
|
||||
Reference in New Issue
Block a user