mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
88
frontend/src/Components/Modal/ConfirmModal.js
Normal file
88
frontend/src/Components/Modal/ConfirmModal.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
function ConfirmModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
kind,
|
||||
size,
|
||||
title,
|
||||
message,
|
||||
confirmLabel,
|
||||
cancelLabel,
|
||||
hideCancelButton,
|
||||
isSpinning,
|
||||
onConfirm,
|
||||
onCancel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={size}
|
||||
onModalClose={onCancel}
|
||||
>
|
||||
<ModalContent onModalClose={onCancel}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{message}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
!hideCancelButton &&
|
||||
<Button
|
||||
kind={kinds.DEFAULT}
|
||||
onPress={onCancel}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<SpinnerButton
|
||||
data-autofocus={true}
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
onPress={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ConfirmModal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
title: PropTypes.string.isRequired,
|
||||
message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
confirmLabel: PropTypes.string,
|
||||
cancelLabel: PropTypes.string,
|
||||
hideCancelButton: PropTypes.bool,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ConfirmModal.defaultProps = {
|
||||
kind: kinds.PRIMARY,
|
||||
size: sizes.MEDIUM,
|
||||
confirmLabel: 'OK',
|
||||
cancelLabel: 'Cancel',
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default ConfirmModal;
|
92
frontend/src/Components/Modal/Modal.css
Normal file
92
frontend/src/Components/Modal/Modal.css
Normal file
@@ -0,0 +1,92 @@
|
||||
.modalContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modalBackdrop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $modalBackdropBackgroundColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
max-height: 90%;
|
||||
border-radius: 6px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modalOpen {
|
||||
/* Prevent the body from scrolling when the modal is open */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sizes
|
||||
*/
|
||||
|
||||
.small {
|
||||
composes: modal;
|
||||
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
composes: modal;
|
||||
|
||||
width: 720px;
|
||||
}
|
||||
|
||||
.large {
|
||||
composes: modal;
|
||||
|
||||
width: 1080px;
|
||||
}
|
||||
|
||||
.extraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
.modal.large {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.modal.small,
|
||||
.modal.medium {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalContainer {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.modal.small,
|
||||
.modal.medium,
|
||||
.modal.large,
|
||||
.modal.extraLarge {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
215
frontend/src/Components/Modal/Modal.js
Normal file
215
frontend/src/Components/Modal/Modal.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
import ModalError from './ModalError';
|
||||
import styles from './Modal.css';
|
||||
|
||||
const openModals = [];
|
||||
|
||||
function removeFromOpenModals(id) {
|
||||
const index = openModals.indexOf(id);
|
||||
|
||||
if (index >= 0) {
|
||||
openModals.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
class Modal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._node = document.getElementById('modal-root');
|
||||
this._backgroundRef = null;
|
||||
this._modalId = getUniqueElememtId();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.isOpen) {
|
||||
this._openModal();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isOpen
|
||||
} = this.props;
|
||||
|
||||
if (!prevProps.isOpen && isOpen) {
|
||||
this._openModal();
|
||||
} else if (prevProps.isOpen && !isOpen) {
|
||||
this._closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.isOpen) {
|
||||
this._closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_setBackgroundRef = (ref) => {
|
||||
this._backgroundRef = ref;
|
||||
}
|
||||
|
||||
_openModal() {
|
||||
openModals.push(this._modalId);
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
if (openModals.length === 1) {
|
||||
elementClass(document.body).add(styles.modalOpen);
|
||||
}
|
||||
}
|
||||
|
||||
_closeModal() {
|
||||
removeFromOpenModals(this._modalId);
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
|
||||
if (openModals.length === 0) {
|
||||
elementClass(document.body).remove(styles.modalOpen);
|
||||
}
|
||||
}
|
||||
|
||||
_isBackdropTarget(event) {
|
||||
const targetElement = this._findEventTarget(event);
|
||||
|
||||
if (targetElement) {
|
||||
const backgroundElement = ReactDOM.findDOMNode(this._backgroundRef);
|
||||
|
||||
return backgroundElement.isEqualNode(targetElement);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_findEventTarget(event) {
|
||||
const changedTouches = event.changedTouches;
|
||||
|
||||
if (!changedTouches) {
|
||||
return event.target;
|
||||
}
|
||||
|
||||
if (changedTouches.length === 1) {
|
||||
const touch = changedTouches[0];
|
||||
|
||||
return document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onBackdropBeginPress = (event) => {
|
||||
this._isBackdropPressed = this._isBackdropTarget(event);
|
||||
}
|
||||
|
||||
onBackdropEndPress = (event) => {
|
||||
const {
|
||||
closeOnBackgroundClick,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this._isBackdropPressed &&
|
||||
this._isBackdropTarget(event) &&
|
||||
closeOnBackgroundClick
|
||||
) {
|
||||
onModalClose();
|
||||
}
|
||||
|
||||
this._isBackdropPressed = false;
|
||||
}
|
||||
|
||||
onKeyDown = (event) => {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (keyCode === keyCodes.ESCAPE) {
|
||||
if (openModals.indexOf(this._modalId) === openModals.length - 1) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
backdropClassName,
|
||||
size,
|
||||
children,
|
||||
isOpen,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={styles.modalContainer}
|
||||
>
|
||||
<div
|
||||
ref={this._setBackgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={this.onBackdropBeginPress}
|
||||
onMouseUp={this.onBackdropEndPress}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
this._node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
backdropClassName: PropTypes.string,
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
closeOnBackgroundClick: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Modal.defaultProps = {
|
||||
className: styles.modal,
|
||||
backdropClassName: styles.modalBackdrop,
|
||||
size: sizes.LARGE,
|
||||
closeOnBackgroundClick: true
|
||||
};
|
||||
|
||||
export default Modal;
|
12
frontend/src/Components/Modal/ModalBody.css
Normal file
12
frontend/src/Components/Modal/ModalBody.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.modalBody {
|
||||
flex: 1 0 1px;
|
||||
padding: $modalBodyPadding;
|
||||
}
|
||||
|
||||
.modalScroller {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.innerModalBody {
|
||||
padding: $modalBodyPadding;
|
||||
}
|
59
frontend/src/Components/Modal/ModalBody.js
Normal file
59
frontend/src/Components/Modal/ModalBody.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import styles from './ModalBody.css';
|
||||
|
||||
class ModalBody extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
innerClassName,
|
||||
scrollDirection,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
let className = this.props.className;
|
||||
const hasScroller = scrollDirection !== scrollDirections.NONE;
|
||||
|
||||
if (!className) {
|
||||
className = hasScroller ? styles.modalScroller : styles.modalBody;
|
||||
}
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirection}
|
||||
scrollTop={0}
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
hasScroller ?
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
</div> :
|
||||
children
|
||||
}
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ModalBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL])
|
||||
};
|
||||
|
||||
ModalBody.defaultProps = {
|
||||
innerClassName: styles.innerModalBody,
|
||||
scrollDirection: scrollDirections.VERTICAL
|
||||
};
|
||||
|
||||
export default ModalBody;
|
23
frontend/src/Components/Modal/ModalContent.css
Normal file
23
frontend/src/Components/Modal/ModalContent.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.modalContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
background-color: $modalBackgroundColor;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
|
||||
&:hover {
|
||||
color: $modalCloseButtonHoverColor;
|
||||
}
|
||||
}
|
52
frontend/src/Components/Modal/ModalContent.js
Normal file
52
frontend/src/Components/Modal/ModalContent.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './ModalContent.css';
|
||||
|
||||
function ModalContent(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
showCloseButton,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
showCloseButton &&
|
||||
<Link
|
||||
className={styles.closeButton}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ModalContent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
showCloseButton: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ModalContent.defaultProps = {
|
||||
className: styles.modalContent,
|
||||
showCloseButton: true
|
||||
};
|
||||
|
||||
export default ModalContent;
|
15
frontend/src/Components/Modal/ModalError.css
Normal file
15
frontend/src/Components/Modal/ModalError.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.message {
|
||||
composes: message from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
font-weight: normal;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: details from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
}
|
46
frontend/src/Components/Modal/ModalError.js
Normal file
46
frontend/src/Components/Modal/ModalError.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './ModalError.css';
|
||||
|
||||
function ModalError(props) {
|
||||
const {
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Error
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ErrorBoundaryError
|
||||
messageClassName={styles.message}
|
||||
detailsClassName={styles.details}
|
||||
{...otherProps}
|
||||
message='There was an error loading this item'
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>);
|
||||
}
|
||||
|
||||
ModalError.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModalError;
|
23
frontend/src/Components/Modal/ModalFooter.css
Normal file
23
frontend/src/Components/Modal/ModalFooter.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
padding: 15px 30px;
|
||||
border-top: 1px solid $borderColor;
|
||||
|
||||
a,
|
||||
button {
|
||||
margin-left: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
32
frontend/src/Components/Modal/ModalFooter.js
Normal file
32
frontend/src/Components/Modal/ModalFooter.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './ModalFooter.css';
|
||||
|
||||
class ModalFooter extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.modalFooter}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ModalFooter.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default ModalFooter;
|
8
frontend/src/Components/Modal/ModalHeader.css
Normal file
8
frontend/src/Components/Modal/ModalHeader.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.modalHeader {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex-shrink: 0;
|
||||
padding: 15px 50px 15px 30px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
font-size: 18px;
|
||||
}
|
32
frontend/src/Components/Modal/ModalHeader.js
Normal file
32
frontend/src/Components/Modal/ModalHeader.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './ModalHeader.css';
|
||||
|
||||
class ModalHeader extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.modalHeader}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ModalHeader.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default ModalHeader;
|
Reference in New Issue
Block a user