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