mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4173 from grafixeyehero/Convert-emby-scroller-emby-button
Convert emby-button and emby-scroller to react
This commit is contained in:
commit
066a31e84c
5 changed files with 502 additions and 0 deletions
53
src/elements/emby-button/Button.tsx
Normal file
53
src/elements/emby-button/Button.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import './emby-button.scss';
|
||||||
|
|
||||||
|
enum IconPosition {
|
||||||
|
RIGHT = 'RIGHT',
|
||||||
|
LEFT = 'LEFT',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
HTMLButtonElement
|
||||||
|
> {
|
||||||
|
icon?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
iconPos?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
iconClassName,
|
||||||
|
iconPos,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const btnClass = classNames(
|
||||||
|
'emby-button',
|
||||||
|
className,
|
||||||
|
{ 'show-focus': layoutManager.tv }
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconClass = classNames(
|
||||||
|
'material-icons',
|
||||||
|
iconClassName,
|
||||||
|
icon
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={btnClass}
|
||||||
|
onClick={onClick}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{icon && iconPos === IconPosition.LEFT && <span className={iconClass} aria-hidden='true' />}
|
||||||
|
<span>{title}</span>
|
||||||
|
{icon && iconPos === IconPosition.RIGHT && <span className={iconClass} aria-hidden='true' />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
47
src/elements/emby-button/IconButton.tsx
Normal file
47
src/elements/emby-button/IconButton.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import React, { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import './emby-button.scss';
|
||||||
|
|
||||||
|
interface IconButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
HTMLButtonElement
|
||||||
|
> {
|
||||||
|
icon?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconButton: React.FC<IconButtonProps> = ({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
iconClassName,
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const btnClass = classNames(
|
||||||
|
'paper-icon-button-light',
|
||||||
|
className,
|
||||||
|
{ 'show-focus': layoutManager.tv }
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconClass = classNames(
|
||||||
|
'material-icons',
|
||||||
|
iconClassName,
|
||||||
|
icon
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={btnClass}
|
||||||
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span className={iconClass} aria-hidden='true' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconButton;
|
66
src/elements/emby-button/LinkButton.tsx
Normal file
66
src/elements/emby-button/LinkButton.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import React, { AnchorHTMLAttributes, DetailedHTMLProps, MouseEvent } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import shell from '../../scripts/shell';
|
||||||
|
import { appRouter } from '../../components/appRouter';
|
||||||
|
import { appHost } from '../../components/apphost';
|
||||||
|
import './emby-button.scss';
|
||||||
|
|
||||||
|
interface LinkButtonProps extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
> {
|
||||||
|
className?: string;
|
||||||
|
isAutoHideEnabled?: boolean;
|
||||||
|
href?: string;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkButton: React.FC<LinkButtonProps> = ({
|
||||||
|
className,
|
||||||
|
isAutoHideEnabled,
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const onAnchorClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
const url = href || '';
|
||||||
|
if (url !== '#') {
|
||||||
|
if (target) {
|
||||||
|
if (!appHost.supports('targetblank')) {
|
||||||
|
e.preventDefault();
|
||||||
|
shell.openUrl(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
appRouter.show(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAutoHideEnabled === true && !appHost.supports('externallinks')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssClass = classNames(
|
||||||
|
'emby-button',
|
||||||
|
className,
|
||||||
|
{ 'show-focus': layoutManager.tv }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={cssClass}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
onClick={onAnchorClick}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkButton;
|
83
src/elements/emby-scrollbuttons/ScrollButtons.tsx
Normal file
83
src/elements/emby-scrollbuttons/ScrollButtons.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import scrollerFactory from '../../libraries/scroller';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import IconButton from '../emby-button/IconButton';
|
||||||
|
import './emby-scrollbuttons.scss';
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
RIGHT,
|
||||||
|
LEFT,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollButtonsProps {
|
||||||
|
scrollRef?: React.MutableRefObject<HTMLElement | null>;
|
||||||
|
scrollerFactoryRef: React.MutableRefObject<scrollerFactory | null>;
|
||||||
|
scrollState: {
|
||||||
|
scrollSize: number;
|
||||||
|
scrollPos: number;
|
||||||
|
scrollWidth: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollButtons: FC<ScrollButtonsProps> = ({ scrollerFactoryRef, scrollState }) => {
|
||||||
|
const [localeScrollPos, setLocaleScrollPos] = useState<number>(0);
|
||||||
|
const scrollButtonsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToPosition = useCallback((pos: number, immediate: boolean) => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
scrollerFactoryRef.current.slideTo(pos, immediate, undefined );
|
||||||
|
}
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
const onScrollButtonClick = (direction: Direction) => {
|
||||||
|
let newPos;
|
||||||
|
if (direction === Direction.LEFT) {
|
||||||
|
newPos = Math.max(0, scrollState.scrollPos - scrollState.scrollSize);
|
||||||
|
} else {
|
||||||
|
newPos = scrollState.scrollPos + scrollState.scrollSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalize.getIsRTL() && direction === Direction.LEFT) {
|
||||||
|
newPos = scrollState.scrollPos + scrollState.scrollSize;
|
||||||
|
} else if (globalize.getIsRTL()) {
|
||||||
|
newPos = Math.min(0, scrollState.scrollPos - scrollState.scrollSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToPosition(newPos, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parent = scrollButtonsRef.current?.parentNode as HTMLDivElement;
|
||||||
|
parent.classList.add('emby-scroller-container');
|
||||||
|
|
||||||
|
let localeAwarePos = scrollState.scrollPos;
|
||||||
|
if (globalize.getIsElementRTL(scrollButtonsRef.current)) {
|
||||||
|
localeAwarePos *= -1;
|
||||||
|
}
|
||||||
|
setLocaleScrollPos(localeAwarePos);
|
||||||
|
}, [scrollState.scrollPos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={scrollButtonsRef} className='emby-scrollbuttons padded-right'>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
type='button'
|
||||||
|
className='emby-scrollbuttons-button btnPrev'
|
||||||
|
onClick={() => onScrollButtonClick(Direction.LEFT)}
|
||||||
|
icon='chevron_left'
|
||||||
|
disabled={localeScrollPos > 0 ? false : true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
type='button'
|
||||||
|
className='emby-scrollbuttons-button btnNext'
|
||||||
|
onClick={() => onScrollButtonClick(Direction.RIGHT)}
|
||||||
|
icon='chevron_right'
|
||||||
|
disabled={scrollState.scrollWidth > 0 && localeScrollPos + scrollState.scrollSize >= scrollState.scrollWidth ? true : false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollButtons;
|
||||||
|
|
253
src/elements/emby-scroller/Scroller.tsx
Normal file
253
src/elements/emby-scroller/Scroller.tsx
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import browser from '../../scripts/browser';
|
||||||
|
import focusManager from '../../components/focusManager';
|
||||||
|
import scrollerFactory from '../../libraries/scroller';
|
||||||
|
import ScrollButtons from '../emby-scrollbuttons/ScrollButtons';
|
||||||
|
import './emby-scroller.scss';
|
||||||
|
|
||||||
|
interface ScrollerProps {
|
||||||
|
className?: string;
|
||||||
|
isHorizontalEnabled?: boolean;
|
||||||
|
isMouseWheelEnabled?: boolean;
|
||||||
|
isCenterFocusEnabled?: boolean;
|
||||||
|
isScrollButtonsEnabled?: boolean;
|
||||||
|
isSkipFocusWhenVisibleEnabled?: boolean;
|
||||||
|
isScrollEventEnabled?: boolean;
|
||||||
|
isHideScrollbarEnabled?: boolean;
|
||||||
|
isAllowNativeSmoothScrollEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Scroller: FC<ScrollerProps> = ({
|
||||||
|
className,
|
||||||
|
isHorizontalEnabled,
|
||||||
|
isMouseWheelEnabled,
|
||||||
|
isCenterFocusEnabled,
|
||||||
|
isScrollButtonsEnabled,
|
||||||
|
isSkipFocusWhenVisibleEnabled,
|
||||||
|
isScrollEventEnabled,
|
||||||
|
isHideScrollbarEnabled,
|
||||||
|
isAllowNativeSmoothScrollEnabled,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const [showControls, setShowControls] = useState(false);
|
||||||
|
const [scrollState, setScrollState] = useState({
|
||||||
|
scrollSize: 0,
|
||||||
|
scrollPos: 0,
|
||||||
|
scrollWidth: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollerFactoryRef = useRef<scrollerFactory | null>(null);
|
||||||
|
|
||||||
|
const getScrollSlider = useCallback(() => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
return scrollerFactoryRef.current.getScrollSlider();
|
||||||
|
}
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
const getScrollPosition = useCallback(() => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
return scrollerFactoryRef.current.getScrollPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
const getScrollWidth = useCallback(() => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
return scrollerFactoryRef.current.getScrollSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
const getStyleValue = useCallback((style: CSSStyleDeclaration, name: string) => {
|
||||||
|
let value = style.getPropertyValue(name);
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.replace('px', '');
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(parseInt(value))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getScrollSize = useCallback(() => {
|
||||||
|
const scroller = scrollRef?.current as HTMLDivElement;
|
||||||
|
let scrollSize = scroller.offsetWidth;
|
||||||
|
let style = window.getComputedStyle(scroller, null);
|
||||||
|
|
||||||
|
let paddingLeft = getStyleValue(style, 'padding-left');
|
||||||
|
if (paddingLeft) {
|
||||||
|
scrollSize -= paddingLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paddingRight = getStyleValue(style, 'padding-right');
|
||||||
|
if (paddingRight) {
|
||||||
|
scrollSize -= paddingRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slider = getScrollSlider();
|
||||||
|
style = window.getComputedStyle(slider, null);
|
||||||
|
|
||||||
|
paddingLeft = getStyleValue(style, 'padding-left');
|
||||||
|
if (paddingLeft) {
|
||||||
|
scrollSize -= paddingLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingRight = getStyleValue(style, 'padding-right');
|
||||||
|
if (paddingRight) {
|
||||||
|
scrollSize -= paddingRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scrollSize;
|
||||||
|
}, [getScrollSlider, getStyleValue, scrollRef]);
|
||||||
|
|
||||||
|
const onScroll = useCallback(() => {
|
||||||
|
const scrollSize = getScrollSize();
|
||||||
|
const scrollPos = getScrollPosition();
|
||||||
|
const scrollWidth = getScrollWidth();
|
||||||
|
|
||||||
|
setScrollState({
|
||||||
|
scrollSize: scrollSize,
|
||||||
|
scrollPos: scrollPos,
|
||||||
|
scrollWidth: scrollWidth
|
||||||
|
});
|
||||||
|
}, [getScrollPosition, getScrollSize, getScrollWidth]);
|
||||||
|
|
||||||
|
const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => {
|
||||||
|
dom.addEventListener(elem, 'focus', function (e: { target: any; }) {
|
||||||
|
const focused = focusManager.focusableParent(e.target);
|
||||||
|
if (focused) {
|
||||||
|
scrollerInstance.toCenter(focused, false);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
capture: true,
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addScrollEventListener = useCallback((fn, options) => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
dom.addEventListener(scrollerFactoryRef.current.getScrollFrame(), scrollerFactoryRef.current.getScrollEventName(), fn, options);
|
||||||
|
}
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
const removeScrollEventListener = useCallback((fn, options) => {
|
||||||
|
if (scrollerFactoryRef.current) {
|
||||||
|
dom.removeEventListener(scrollerFactoryRef.current.getScrollFrame(), scrollerFactoryRef.current.getScrollEventName(), fn, options);
|
||||||
|
}
|
||||||
|
}, [scrollerFactoryRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollerElement = scrollRef.current as HTMLDivElement;
|
||||||
|
|
||||||
|
const horizontal = isHorizontalEnabled !== false;
|
||||||
|
const scrollbuttons = isScrollButtonsEnabled !== false;
|
||||||
|
const mousewheel = isMouseWheelEnabled !== false;
|
||||||
|
|
||||||
|
const slider = scrollerElement.querySelector('.scrollSlider');
|
||||||
|
|
||||||
|
const scrollFrame = scrollerElement;
|
||||||
|
const enableScrollButtons = layoutManager.desktop && horizontal && scrollbuttons;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
horizontal: horizontal,
|
||||||
|
mouseDragging: 1,
|
||||||
|
mouseWheel: mousewheel,
|
||||||
|
touchDragging: 1,
|
||||||
|
slidee: slider,
|
||||||
|
scrollBy: 200,
|
||||||
|
speed: horizontal ? 270 : 240,
|
||||||
|
elasticBounds: 1,
|
||||||
|
dragHandle: 1,
|
||||||
|
autoImmediate: true,
|
||||||
|
skipSlideToWhenVisible: isSkipFocusWhenVisibleEnabled === true,
|
||||||
|
dispatchScrollEvent: enableScrollButtons || isScrollEventEnabled === true,
|
||||||
|
hideScrollbar: enableScrollButtons || isHideScrollbarEnabled === true,
|
||||||
|
allowNativeSmoothScroll: isAllowNativeSmoothScrollEnabled === true && !enableScrollButtons,
|
||||||
|
allowNativeScroll: !enableScrollButtons,
|
||||||
|
forceHideScrollbars: enableScrollButtons,
|
||||||
|
// In edge, with the native scroll, the content jumps around when hovering over the buttons
|
||||||
|
requireAnimation: enableScrollButtons && browser.edge
|
||||||
|
};
|
||||||
|
|
||||||
|
// If just inserted it might not have any height yet - yes this is a hack
|
||||||
|
scrollerFactoryRef.current = new scrollerFactory(scrollFrame, options);
|
||||||
|
scrollerFactoryRef.current.init();
|
||||||
|
scrollerFactoryRef.current.reload();
|
||||||
|
|
||||||
|
if (layoutManager.tv && isCenterFocusEnabled) {
|
||||||
|
initCenterFocus(scrollerElement, scrollerFactoryRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableScrollButtons) {
|
||||||
|
addScrollEventListener(onScroll, {
|
||||||
|
capture: false,
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
setShowControls(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const scrollerInstance = scrollerFactoryRef.current;
|
||||||
|
if (scrollerInstance) {
|
||||||
|
scrollerInstance.destroy();
|
||||||
|
scrollerFactoryRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeScrollEventListener(onScroll, {
|
||||||
|
capture: false,
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
addScrollEventListener,
|
||||||
|
initCenterFocus,
|
||||||
|
isAllowNativeSmoothScrollEnabled,
|
||||||
|
isCenterFocusEnabled,
|
||||||
|
isHideScrollbarEnabled,
|
||||||
|
isHorizontalEnabled,
|
||||||
|
isMouseWheelEnabled,
|
||||||
|
isScrollButtonsEnabled,
|
||||||
|
isScrollEventEnabled,
|
||||||
|
isSkipFocusWhenVisibleEnabled,
|
||||||
|
onScroll,
|
||||||
|
removeScrollEventListener
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
showControls && scrollState.scrollWidth > scrollState.scrollSize + 20 &&
|
||||||
|
<ScrollButtons
|
||||||
|
scrollRef={scrollRef}
|
||||||
|
scrollerFactoryRef={scrollerFactoryRef}
|
||||||
|
scrollState={scrollState}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className={classNames('emby-scroller', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Scroller;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue