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,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { NoHomeError } from './NoHomeError';
export default {
title: 'Common/NoHomeError',
component: NoHomeError,
argTypes: {}
} as ComponentMeta<typeof NoHomeError>;
const Template: ComponentStory<typeof NoHomeError> = () => <NoHomeError />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,34 @@
import React from 'react';
export function NoHomeError() {
return (
<div
style={{
overflowY: 'auto',
padding: '24px',
fontFamily: 'Open Sans',
fontSize: '16px',
width: '100vw',
height: '100vh',
backgroundColor: '#F57569'
}}
>
<div style={{ marginBottom: '50px', fontWeight: 'bold', display: 'flex', alignItems: 'center' }}>
<span>ERROR</span>
<img src="ndl_assets/noodl-logo-black.svg" style={{ marginLeft: 'auto' }} />
</div>
<div style={{ margin: '0 auto', alignItems: 'center', display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '24px', textAlign: 'center', marginBottom: '50px' }}>
No <img src="ndl_assets/home-icon.svg" style={{ marginRight: '-6px' }} />{' '}
<span style={{ fontWeight: 'bold' }}>HOME</span> component selected
</div>
<div style={{ textAlign: 'center' }}>
Click <span style={{ fontWeight: 'bold' }}>Make home</span> as shown below.
</div>
<img style={{ marginTop: '24px' }} srcSet="ndl_assets/make-home-instructions@2x.png 2x" />
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './NoHomeError';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Button } from './Button';
export default {
title: 'CATEGORY_HERE/Button',
component: Button,
argTypes: {}
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,110 @@
import React from 'react';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl, Slot } from '../../../types';
export interface ButtonProps extends Noodl.ReactProps {
enabled: boolean;
buttonType: 'button' | 'submit';
textStyle: Noodl.TextStyle;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
onClick: () => void;
children: Slot;
}
export function Button(props: ButtonProps) {
let style: React.CSSProperties = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.textStyle !== undefined) {
// Apply text style
style = Object.assign({}, props.textStyle, style);
style.color = props.noodlNode.context.styles.resolveColor(style.color);
}
function _renderIcon() {
const iconStyle: React.CSSProperties = {};
if (props.useLabel) {
if (props.iconPlacement === 'left' || props.iconPlacement === undefined) {
iconStyle.marginRight = props.iconSpacing;
} else {
iconStyle.marginLeft = props.iconSpacing;
}
}
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined) {
iconStyle.width = props.iconSize;
iconStyle.height = props.iconSize;
return <img alt="" src={props.iconImageSource} style={iconStyle} />;
} else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
iconStyle.fontSize = props.iconSize;
iconStyle.color = props.iconColor;
if (props.iconIconSource.codeAsClass === true) {
return (
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={iconStyle}></span>
);
} else {
return (
<span className={props.iconIconSource.class} style={iconStyle}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
}
let className = 'ndl-controls-button';
if (props.className) className = className + ' ' + props.className;
let content = null;
if (props.useLabel && props.useIcon) {
content = (
<>
{props.iconPlacement === 'left' ? _renderIcon() : null}
{String(props.label)}
{props.iconPlacement === 'right' ? _renderIcon() : null}
</>
);
} else if (props.useLabel) {
content = String(props.label);
} else if (props.useIcon) {
content = _renderIcon();
}
return (
<button
className={className}
disabled={!props.enabled}
{...Utils.controlEvents(props)}
type={props.buttonType}
style={style}
onClick={props.onClick}
>
{content}
{props.children}
</button>
);
}

View File

@@ -0,0 +1 @@
export * from './Button';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Checkbox } from './Checkbox';
export default {
title: 'CATEGORY_HERE/Checkbox',
component: Checkbox,
argTypes: {}
} as ComponentMeta<typeof Checkbox>;
const Template: ComponentStory<typeof Checkbox> = (args) => <Checkbox {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,148 @@
import React, { useEffect, useState } from 'react';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl } from '../../../types';
export interface CheckboxProps extends Noodl.ReactProps {
id: string;
enabled: boolean;
checked: boolean;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
checkedChanged: (checked: boolean) => void;
}
export function Checkbox(props: CheckboxProps) {
const [checked, setChecked] = useState(props.checked);
// Report initial values when mounted
useEffect(() => {
setChecked(!!props.checked);
}, []);
useEffect(() => {
setChecked(!!props.checked);
}, [props.checked]);
const style: React.CSSProperties = { ...props.style };
if (props.parentLayout === 'none') {
style.position = 'absolute';
}
Layout.align(style, props);
const inputProps = {
id: props.id,
className: [props.className, 'ndl-controls-checkbox-2'].join(' '),
disabled: !props.enabled,
style: {
width: props.styles.checkbox.width,
height: props.styles.checkbox.height
}
};
const inputWrapperStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
position: 'relative',
...props.styles.checkbox
};
if (!props.useLabel) {
Object.assign(inputProps, Utils.controlEvents(props));
Object.assign(inputWrapperStyle, style);
}
function _renderIcon() {
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined)
return (
<img
alt=""
src={props.iconImageSource}
style={{
width: props.iconSize,
height: props.iconSize,
position: 'absolute'
}}
/>
);
else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
const style: React.CSSProperties = {
fontSize: props.iconSize,
color: props.iconColor,
position: 'absolute'
};
if (props.iconIconSource.codeAsClass === true) {
return (
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={style}></span>
);
} else {
return (
<span className={props.iconIconSource.class} style={style}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
}
const checkbox = (
<div className="ndl-controls-pointer" style={inputWrapperStyle} noodl-style-tag="checkbox">
{props.useIcon ? _renderIcon() : null}
<input
type="checkbox"
{...inputProps}
checked={checked}
onChange={(e) => {
setChecked(e.target.checked);
props.checkedChanged && props.checkedChanged(e.target.checked);
}}
/>
</div>
);
if (props.useLabel) {
const labelStyle: React.CSSProperties = {
marginLeft: props.labelSpacing,
...props.labeltextStyle,
...props.styles.label
};
if (!props.enabled) {
labelStyle.cursor = 'default';
}
labelStyle.color = props.noodlNode.context.styles.resolveColor(labelStyle.color);
return (
<div style={{ display: 'flex', alignItems: 'center', ...style }} {...Utils.controlEvents(props)}>
{checkbox}
<label className="ndl-controls-pointer" style={labelStyle} htmlFor={props.id} noodl-style-tag="label">
{props.label}
</label>
</div>
);
} else {
return checkbox;
}
}

View File

@@ -0,0 +1 @@
export * from './Checkbox'

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { RadioButton } from './RadioButton';
export default {
title: 'Controls/Radio Button',
component: RadioButton,
argTypes: {}
} as ComponentMeta<typeof RadioButton>;
const Template: ComponentStory<typeof RadioButton> = (args) => <RadioButton {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,157 @@
import React, { useContext } from 'react';
import RadioButtonContext from '../../../contexts/radiobuttoncontext';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl } from '../../../types';
export interface RadioButtonProps extends Noodl.ReactProps {
id: string;
enabled: boolean;
value: string;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
fillSpacing: string;
checkedChanged: (value: boolean) => void;
}
function isPercentage(size /* string */) {
return size && size[size.length - 1] === '%';
}
export function RadioButton(props: RadioButtonProps) {
const radioButtonGroup = useContext(RadioButtonContext);
const style = { ...props.style };
if (props.parentLayout === 'none') {
style.position = 'absolute';
}
Layout.align(style, props);
props.checkedChanged && props.checkedChanged(radioButtonGroup ? radioButtonGroup.selected === props.value : false);
const inputProps = {
id: props.id,
disabled: !props.enabled,
className: [props.className, 'ndl-controls-radio-2'].join(' '),
style: {
width: props.styles.radio.width,
height: props.styles.radio.height
}
};
const inputWrapperStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
flexShrink: 0,
...props.styles.radio
};
if (props.useLabel) {
if (isPercentage(props.styles.radio.width)) {
delete inputWrapperStyle.width;
inputWrapperStyle.flexGrow = 1;
}
} else {
Object.assign(inputProps, Utils.controlEvents(props));
Object.assign(inputWrapperStyle, style);
}
function _renderIcon() {
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined)
return <img alt="" src={props.iconImageSource} style={{ width: props.iconSize, height: props.iconSize }} />;
else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
const style = { fontSize: props.iconSize, color: props.iconColor };
if (props.iconIconSource.codeAsClass === true) {
return (
<span
className={['ndl-controls-abs-center', props.iconIconSource.class, props.iconIconSource.code].join(' ')}
style={style}
></span>
);
} else {
return (
<span className={['ndl-controls-abs-center', props.iconIconSource.class].join(' ')} style={style}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
}
const fillStyle: React.CSSProperties = {
left: props.fillSpacing,
right: props.fillSpacing,
top: props.fillSpacing,
bottom: props.fillSpacing,
backgroundColor: props.styles.fill.backgroundColor,
borderRadius: 'inherit',
position: 'absolute'
};
const radioButton = (
<div className="ndl-controls-pointer" style={inputWrapperStyle} noodl-style-tag="radio">
<div style={fillStyle} noodl-style-tag="fill" />
{props.useIcon ? _renderIcon() : null}
<input
type="radio"
name={radioButtonGroup ? radioButtonGroup.name : undefined}
{...inputProps}
checked={radioButtonGroup ? radioButtonGroup.selected === props.value : false}
onChange={(e) => {
radioButtonGroup && radioButtonGroup.checkedChanged && radioButtonGroup.checkedChanged(props.value);
}}
/>
</div>
);
if (props.useLabel) {
const labelStyle = {
marginLeft: props.labelSpacing,
...props.labeltextStyle,
...props.styles.label,
cursor: props.enabled ? undefined : 'default'
};
labelStyle.color = props.noodlNode.context.styles.resolveColor(labelStyle.color);
const wrapperStyle = { display: 'flex', alignItems: 'center', ...style };
if (isPercentage(props.styles.radio.width)) {
wrapperStyle.width = props.styles.radio.width;
}
if (isPercentage(props.styles.radio.height)) {
wrapperStyle.height = props.styles.radio.height;
}
return (
<div style={wrapperStyle} {...Utils.controlEvents(props)}>
{radioButton}
<label className="ndl-controls-pointer" style={labelStyle} htmlFor={props.id} noodl-style-tag="label">
{props.label}
</label>
</div>
);
} else {
return radioButton;
}
}

View File

@@ -0,0 +1 @@
export * from './RadioButton';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { RadioButtonGroup } from './RadioButtonGroup';
export default {
title: 'Controls/RadioButtonGroup',
component: RadioButtonGroup,
argTypes: {}
} as ComponentMeta<typeof RadioButtonGroup>;
const Template: ComponentStory<typeof RadioButtonGroup> = (args) => <RadioButtonGroup {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react';
import RadioButtonContext from '../../../contexts/radiobuttoncontext';
import Layout from '../../../layout';
import { Noodl, Slot } from '../../../types';
export interface RadioButtonGroupProps extends Noodl.ReactProps {
name: string;
value: string;
valueChanged?: (value: string) => void;
children: Slot;
}
export function RadioButtonGroup(props: RadioButtonGroupProps) {
const [selected, setSelected] = useState(props.value);
const context = {
selected: selected,
name: props.name,
checkedChanged: (value) => {
setSelected(value);
props.valueChanged && props.valueChanged(value);
}
};
useEffect(() => {
setSelected(props.value);
}, [props.value]);
const style: React.CSSProperties = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
let className = 'ndl-controls-radiobuttongroup';
if (props.className) className = className + ' ' + props.className;
return (
<RadioButtonContext.Provider value={context}>
<div className={className} style={style}>
{props.children}
</div>
</RadioButtonContext.Provider>
);
}

View File

@@ -0,0 +1 @@
export * from './RadioButtonGroup';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Select } from './Select';
export default {
title: 'Controls/Select',
component: Select,
argTypes: {}
} as ComponentMeta<typeof Select>;
const Template: ComponentStory<typeof Select> = (args) => <Select {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,197 @@
import React, { useEffect, useState } from 'react';
import type { TSFixme } from '../../../../typings/global';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl } from '../../../types';
export interface SelectProps extends Noodl.ReactProps {
id: string;
value: string;
enabled: boolean;
textStyle: Noodl.TextStyle;
items: TSFixme;
placeholder: string;
placeholderOpacity: string;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
onClick: () => void;
valueChanged: (value: string) => void;
}
export function Select(props: SelectProps) {
const [value, setValue] = useState(props.value);
useEffect(() => {
setValue(props.value);
props.valueChanged(props.value);
}, [props.value, props.items]);
let style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.textStyle !== undefined) {
// Apply text style
style = Object.assign({}, props.textStyle, style);
style.color = props.noodlNode.context.styles.resolveColor(style.color);
}
// Hide label if there is no selected value, of if value is not in the items array
const selectedIndex = !props.items || value === undefined ? -1 : props.items.findIndex((i) => i.Value === value);
const { height, ...otherStyles } = style;
function _renderIcon() {
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined)
return <img alt="" src={props.iconImageSource} style={{ width: props.iconSize, height: props.iconSize }}></img>;
else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
const style: React.CSSProperties = { fontSize: props.iconSize, color: props.iconColor };
if (props.iconPlacement === 'left' || props.iconPlacement === undefined) style.marginRight = props.iconSpacing;
else style.marginLeft = props.iconSpacing;
if (props.iconIconSource.codeAsClass === true) {
return (
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={style}></span>
);
} else {
return (
<span className={props.iconIconSource.class} style={style}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
}
const inputProps = {
id: props.id,
className: props.className,
style: {
inset: 0,
opacity: 0,
position: 'absolute',
textTransform: 'inherit',
cursor: props.enabled ? '' : 'default',
'-webkit-appearance': 'none' //this makes styling possible on Safari, otherwise the size will be incorrect as it will use the native styling
},
onClick: props.onClick
};
const inputWrapperStyle = {
display: 'flex',
alignItems: 'center',
...props.styles.inputWrapper,
cursor: props.enabled ? '' : 'default'
};
const heightInPercent = height && height[String(height).length - 1] === '%';
if (props.useLabel) {
if (heightInPercent) {
inputWrapperStyle.flexGrow = 1;
} else {
inputWrapperStyle.height = height;
}
} else {
Object.assign(inputWrapperStyle, otherStyles);
inputWrapperStyle.height = height;
}
let options = [];
if (props.items) {
options = props.items.map((i) => (
<option key={i.Value} value={i.Value} disabled={i.Disabled === 'true' || i.Disabled === true ? true : undefined}>
{i.Label}
</option>
));
// options.unshift();
}
let label = null;
if (selectedIndex >= 0 && selectedIndex < props.items.items.length) {
label = <span>{props.items.items[selectedIndex].Label}</span>;
} else if (props.placeholder) {
label = <span style={{ opacity: props.placeholderOpacity }}>{props.placeholder}</span>;
}
//A hidden first option is preselected and added to the list of options, it makes it possible to select the first item in the dropdown
const inputWrapper = (
<div className="ndl-controls-pointer" style={inputWrapperStyle} noodl-style-tag="inputWrapper">
{props.useIcon && props.iconPlacement === 'left' ? _renderIcon() : null}
<div
style={{
width: '100%',
height: '100%',
alignItems: 'center',
display: 'flex'
}}
>
{label}
</div>
{props.useIcon && props.iconPlacement === 'right' ? _renderIcon() : null}
<select
{...inputProps}
disabled={!props.enabled}
value={options.find((i) => i.props.value === value) ? value : undefined}
{...Utils.controlEvents(props)}
onChange={(e) => {
setValue(e.target.value);
props.valueChanged && props.valueChanged(e.target.value);
}}
>
<option value="" disabled selected hidden />
{options}
</select>
</div>
);
if (props.useLabel) {
const outerWrapperStyle: React.CSSProperties = {
...otherStyles,
display: 'flex',
flexDirection: 'column'
};
if (heightInPercent) {
outerWrapperStyle.height = height;
}
return (
<div style={outerWrapperStyle}>
<label
htmlFor={props.id}
style={{
...props.labeltextStyle,
...props.styles.label,
marginBottom: props.labelSpacing
}}
noodl-style-tag="label"
>
{props.label}
</label>
{inputWrapper}
</div>
);
} else {
return inputWrapper;
}
}

View File

@@ -0,0 +1 @@
export * from './Select';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Slider } from './Slider';
export default {
title: 'Controls/Slider',
component: Slider,
argTypes: {}
} as ComponentMeta<typeof Slider>;
const Template: ComponentStory<typeof Slider> = (args) => <Slider {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,183 @@
import React, { useEffect, useState } from 'react';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl } from '../../../types';
export interface SliderProps extends Noodl.ReactProps {
_nodeId: string;
id: string;
enabled: boolean;
value: number;
min: number;
max: number;
step: number;
thumbHeight: string;
thumbWidth: string;
thumbColor: string;
trackHeight: string;
trackActiveColor: string;
trackColor: string;
onClick: () => void;
updateOutputValue: (value: number) => void;
}
function _styleTemplate(_class: string, props: SliderProps) {
return `.${_class}::-webkit-slider-thumb {
width: ${props.thumbWidth};
}`;
}
function setBorderStyle(style: React.CSSProperties, prefix: string, props: SliderProps) {
function setBorder(style: React.CSSProperties, group: string) {
const width = `${prefix}Border${group}Width`;
const color = `${prefix}Border${group}Color`;
const borderStyle = `${prefix}Border${group}Style`;
const w = props[width] || props[`${prefix}BorderWidth`];
if (w !== undefined) style[`border${group}Width`] = w;
const c = (style[color] = props[color] || props[`${prefix}BorderColor`]);
if (c !== undefined) style[`border${group}Color`] = c;
const s = props[borderStyle] || props[`${prefix}BorderStyle`];
if (s !== undefined) style[`border${group}Style`] = s;
}
setBorder(style, 'Top');
setBorder(style, 'Right');
setBorder(style, 'Bottom');
setBorder(style, 'Left');
}
function setBorderRadius(style: React.CSSProperties, prefix: string, props: SliderProps) {
const radius = props[`${prefix}BorderRadius`];
const tl = props[`${prefix}BorderTopLeftRadius`] || radius;
const tr = props[`${prefix}BorderTopRightRadius`] || radius;
const br = props[`${prefix}BorderBottomRightRadius`] || radius;
const bl = props[`${prefix}BorderBottomLeftRadius`] || radius;
style.borderRadius = `${tl} ${tr} ${br} ${bl}`;
}
function setShadow(style: React.CSSProperties, prefix: string, props: SliderProps) {
if (!props[`${prefix}BoxShadowEnabled`]) return;
const inset = props[`${prefix}BoxShadowInset`];
const x = props[`${prefix}BoxShadowOffsetX`];
const y = props[`${prefix}BoxShadowOffsetY`];
const blur = props[`${prefix}BoxShadowBlurRadius`];
const spread = props[`${prefix}BoxShadowSpreadRadius`];
const color = props[`${prefix}BoxShadowColor`];
style.boxShadow = `${inset ? 'inset ' : ''}${x} ${y} ${blur} ${spread} ${color}`;
}
function hasUnitPx(value: string) {
return value && value[value.length - 1] === 'x';
}
export function Slider(props: SliderProps) {
const [value, setValue] = useState(props.value);
useEffect(() => {
onValueChanged(props.value);
}, [props.value]);
function onValueChanged(value: number) {
setValue(value);
props.updateOutputValue(value);
}
const style: React.CSSProperties = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
const instanceClassId = 'ndl-controls-range-' + props._nodeId;
Utils.updateStylesForClass(instanceClassId, props, _styleTemplate);
const className = `ndl-controls-range2 ${instanceClassId} ${props.className ? props.className : ''} `;
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
id: props.id,
style: {
width: '100%',
opacity: 0
},
onClick: props.onClick,
min: props.min,
max: props.max
};
if (props.step) {
inputProps.step = props.step;
}
//make the input as tall as the tallest element, track or thumb, so the entire area becomes interactive
//Makes it possible to design sliders with thin tracks and tall thumbs
if (hasUnitPx(props.thumbHeight)) {
if (hasUnitPx(props.trackHeight)) {
const thumbHeight = Number(props.thumbHeight.slice(0, -2));
const trackHeight = Number(props.trackHeight.slice(0, -2));
inputProps.style.height = Math.max(thumbHeight, trackHeight) + 'px';
} else {
inputProps.style.height = props.thumbHeight;
}
} else {
inputProps.style.height = props.trackHeight;
}
const divStyle = {
display: 'flex',
alignItems: 'center',
...style
};
const valueFactor = (value - props.min) / (props.max - props.min);
const trackStyle: React.CSSProperties = {
position: 'absolute',
width: '100%',
height: props.trackHeight,
background: `linear-gradient(to right, ${props.trackActiveColor} 0%, ${props.trackActiveColor} ${
valueFactor * 100
}%, ${props.trackColor} ${valueFactor * 100}%, ${props.trackColor} 100%)`
};
setBorderStyle(trackStyle, 'track', props);
setBorderRadius(trackStyle, 'track', props);
setShadow(trackStyle, 'track', props);
const thumbStyle: React.CSSProperties = {
position: 'absolute',
left: 'calc((100% - ' + props.thumbWidth + ') * ' + valueFactor + ')',
width: props.thumbWidth,
height: props.thumbHeight,
backgroundColor: props.thumbColor
};
setBorderStyle(thumbStyle, 'thumb', props);
setBorderRadius(thumbStyle, 'thumb', props);
setShadow(thumbStyle, 'thumb', props);
return (
<div style={divStyle}>
<div style={trackStyle} />
<div style={thumbStyle} />
<input
className={className}
{...Utils.controlEvents(props)}
type="range"
{...inputProps}
value={value}
disabled={!props.enabled}
onChange={(e) => onValueChanged(Number(e.target.value))}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Slider';

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { TextInput } from './TextInput';
export default {
title: 'CATEGORY_HERE/TextInput',
component: TextInput,
argTypes: {}
} as ComponentMeta<typeof TextInput>;
const Template: ComponentStory<typeof TextInput> = (args) => <TextInput {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,264 @@
import React from 'react';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl, Slot } from '../../../types';
//this stops a text field from being unfocused by the clickHandler in the viewer that handles focus globally.
//The specific case is when a mouseDown is registered in the input, but the mouseUp is outside.
//It'll trigger a focus change that'll blur the input field, which is annyoing when you're selecting text
function preventGlobalFocusChange(e) {
e.stopPropagation();
window.removeEventListener('click', preventGlobalFocusChange, true);
}
export interface TextInputProps extends Noodl.ReactProps {
id: string;
type: 'text' | 'textArea' | 'email' | 'number' | 'password' | 'url';
textStyle: Noodl.TextStyle;
enabled: boolean;
placeholder: string;
maxLength: number;
startValue: string;
value: string;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
onTextChanged?: (value: string) => void;
onEnter?: () => void;
children: Slot;
}
// Based on (HTMLTextAreaElement | HTMLInputElement)
type InputRef = (HTMLTextAreaElement | HTMLInputElement) & {
noodlNode?: Noodl.ReactProps['noodlNode'];
};
type State = {
value: string;
};
export class TextInput extends React.Component<TextInputProps, State> {
ref: React.MutableRefObject<InputRef>;
constructor(props: TextInputProps) {
super(props);
this.state = {
value: props.startValue
} satisfies State;
this.ref = React.createRef();
}
setText(value: string) {
this.setState({ value });
this.props.onTextChanged && this.props.onTextChanged(value);
}
componentDidMount() {
//plumbing for the focused signals
this.ref.current.noodlNode = this.props.noodlNode;
this.setText(this.props.startValue);
}
render() {
const style: React.CSSProperties = { ...this.props.style };
Layout.size(style, this.props);
Layout.align(style, this.props);
if (style.opacity === 0) {
style.pointerEvents = 'none';
}
const { height, ...otherStylesTmp } = style;
// otherStylesTmp is not React.CSSProperties, reassigning it will correct the type.
const otherStyles: React.CSSProperties = otherStylesTmp;
const props = this.props;
const _renderIcon = () => {
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined)
return (
<img
alt=""
src={props.iconImageSource}
style={{
width: props.iconSize,
height: props.iconSize
}}
onClick={() => this.focus()}
/>
);
else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
const style: React.CSSProperties = {
userSelect: 'none',
fontSize: props.iconSize,
color: props.iconColor
};
if (props.iconPlacement === 'left' || props.iconPlacement === undefined) style.marginRight = props.iconSpacing;
else style.marginLeft = props.iconSpacing;
if (props.iconIconSource.codeAsClass === true) {
return (
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={style}></span>
);
} else {
return (
<span className={props.iconIconSource.class} style={style}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
};
let className = 'ndl-controls-textinput ' + props.id;
if (props.className) className = className + ' ' + props.className;
let inputContent;
const inputStyles: React.CSSProperties = {
...props.textStyle,
...props.styles.input,
width: '100%',
height: '100%'
};
inputStyles.color = props.noodlNode.context.styles.resolveColor(inputStyles.color);
const inputProps = {
id: props.id,
value: this.state.value,
...Utils.controlEvents(props),
disabled: !props.enabled,
style: inputStyles,
className,
placeholder: props.placeholder,
maxLength: props.maxLength,
onChange: (e) => this.onChange(e)
};
if (props.type !== 'textArea') {
inputContent = (
<input
ref={(ref) => (this.ref.current = ref)}
type={this.props.type}
{...inputProps}
onKeyDown={(e) => this.onKeyDown(e)}
onMouseDown={() => window.addEventListener('click', preventGlobalFocusChange, true)}
noodl-style-tag="input"
/>
);
} else {
inputProps.style.resize = 'none'; //disable user resizing
inputContent = (
<textarea
ref={(ref) => (this.ref.current = ref)}
{...inputProps}
onKeyDown={(e) => this.onKeyDown(e)}
noodl-style-tag="input"
/>
);
}
const inputWrapperStyle = {
display: 'flex',
alignItems: 'center',
...props.styles.inputWrapper
};
const heightInPercent = height && height[String(height).length - 1] === '%';
if (props.useLabel) {
if (heightInPercent) {
inputWrapperStyle.flexGrow = 1;
} else {
inputWrapperStyle.height = height;
}
} else {
Object.assign(inputWrapperStyle, otherStyles);
inputWrapperStyle.height = height;
}
if (props.type !== 'textArea') {
inputWrapperStyle.alignItems = 'center';
}
const inputWithWrapper = (
<div style={inputWrapperStyle} noodl-style-tag="inputWrapper">
{props.useIcon && props.iconPlacement === 'left' ? _renderIcon() : null}
{inputContent}
{props.useIcon && props.iconPlacement === 'right' ? _renderIcon() : null}
</div>
);
if (props.useLabel) {
otherStyles.display = 'flex';
otherStyles.flexDirection = 'column';
if (heightInPercent) otherStyles.height = height;
const labelStyle: React.CSSProperties = {
...props.labeltextStyle,
...props.styles.label,
marginBottom: props.labelSpacing
};
labelStyle.color = props.noodlNode.context.styles.resolveColor(labelStyle.color);
return (
<div style={otherStyles}>
<label htmlFor={props.id} style={labelStyle} noodl-style-tag="label">
{props.label}
</label>
{inputWithWrapper}
</div>
);
} else {
return inputWithWrapper;
}
}
onKeyDown(e) {
if (e.key === 'Enter' || e.which === 13) {
this.props.onEnter && this.props.onEnter();
}
}
onChange(event) {
const value = event.target.value;
this.setText(value);
}
focus() {
this.ref.current && this.ref.current.focus();
}
blur() {
this.ref.current && this.ref.current.blur();
}
hasFocus() {
return document.activeElement === this.ref.current;
}
}

View File

@@ -0,0 +1 @@
export * from './TextInput';

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Page } from './Page';
export default {
title: 'Navigation/Page',
component: Page,
argTypes: {},
} as ComponentMeta<typeof Page>;
const Template: ComponentStory<typeof Page> = (args) => <Page {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,158 @@
import React from 'react';
import type { TSFixme } from '../../../../typings/global';
import Layout from '../../../layout';
import { Noodl, Slot } from '../../../types';
type MetaTag = {
isProperty: boolean;
key: string;
displayName: string;
editorName?: string;
group: string;
type?: string;
popout?: TSFixme;
};
const ogPopout = {
group: 'seo-og',
label: 'Open Graph',
parentGroup: 'Experimental SEO'
};
const twitterPopout = {
group: 'seo-twitter',
label: 'Twitter',
parentGroup: 'Experimental SEO'
};
export const META_TAGS: MetaTag[] = [
{
isProperty: true,
key: 'description',
displayName: 'Description',
group: 'Experimental SEO'
},
{
isProperty: true,
key: 'robots',
displayName: 'Robots',
group: 'Experimental SEO'
},
{
isProperty: true,
key: 'og:title',
displayName: 'Title',
editorName: 'OG Title',
group: 'General',
popout: ogPopout
},
{
isProperty: true,
key: 'og:description',
displayName: 'Description',
editorName: 'OG Description',
group: 'General',
popout: ogPopout
},
{
isProperty: true,
key: 'og:url',
displayName: 'Url',
editorName: 'OG Url',
group: 'General',
popout: ogPopout
},
{
isProperty: true,
key: 'og:type',
displayName: 'Type',
editorName: 'OG Type',
group: 'General',
popout: ogPopout
},
{
isProperty: true,
key: 'og:image',
displayName: 'Image',
editorName: 'OG Image',
group: 'Image',
popout: ogPopout
},
{
isProperty: true,
key: 'og:image:width',
displayName: 'Image Width',
editorName: 'OG Image Width',
group: 'Image',
popout: ogPopout
},
{
isProperty: true,
key: 'og:image:height',
displayName: 'Image Height',
editorName: 'OG Image Height',
group: 'Image',
popout: ogPopout
},
{
isProperty: false,
key: 'twitter:card',
displayName: 'Card',
editorName: 'Twitter Card',
group: 'General',
popout: twitterPopout
},
{
isProperty: false,
key: 'twitter:title',
displayName: 'Title',
editorName: 'Twitter Title',
group: 'General',
popout: twitterPopout
},
{
isProperty: false,
key: 'twitter:description',
displayName: 'Description',
editorName: 'Twitter Description',
group: 'General',
popout: twitterPopout
},
{
isProperty: false,
key: 'twitter:image',
displayName: 'Image',
editorName: 'Twitter Image',
group: 'General',
popout: twitterPopout
}
];
type MetaTagKey = typeof META_TAGS[number]['key'];
export interface PageProps extends Noodl.ReactProps {
metatags?: Record<MetaTagKey, string>;
children: Slot;
}
export function Page(props: PageProps) {
const { style, children } = props;
Layout.size(style, props);
Layout.align(style, props);
// Allow changing the metatags from inputs
META_TAGS.forEach((item) => {
const value = props.metatags && props.metatags[item.key];
// @ts-expect-error Noodl is globally defined.
Noodl.SEO.setMeta(item.key, value);
});
return (
<div style={style} className={props.className}>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Page';

View File

@@ -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 = {};

View File

@@ -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>
);
}
}

View File

@@ -0,0 +1 @@
export * from './Circle';

View File

@@ -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 = {};

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export * from './Columns';

View File

@@ -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 = {};

View 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>
);
}
}

View File

@@ -0,0 +1 @@
export * from './Drag'

View File

@@ -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 = {};

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>
);
}
}

View File

@@ -0,0 +1 @@
export * from './Group';

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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 = {};

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export * from './Icon'

View File

@@ -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 = {};

View File

@@ -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} />;
}

View File

@@ -0,0 +1 @@
export * from './Image';

View File

@@ -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 = {};

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export * from './Text';

View File

@@ -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 = {};

View File

@@ -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);
}}
/>
);
}
}

View File

@@ -0,0 +1 @@
export * from './Video';