1
0
Fork 0
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:
Bill Thornton 2022-12-16 14:12:18 -05:00 committed by GitHub
commit 066a31e84c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 502 additions and 0 deletions

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

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

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

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

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