Spaces:
Running
Running
import PropTypes from 'prop-types'; | |
import React, {Fragment} from 'react'; | |
import classNames from 'classnames'; | |
import {FormattedMessage} from 'react-intl'; | |
import Draggable from 'react-draggable'; | |
import styles from './card.css'; | |
import shrinkIcon from './icon--shrink.svg'; | |
import expandIcon from './icon--expand.svg'; | |
import rightArrow from './icon--next.svg'; | |
import leftArrow from './icon--prev.svg'; | |
import helpIcon from '../../lib/assets/icon--tutorials.svg'; | |
import closeIcon from './icon--close.svg'; | |
import {translateVideo} from '../../lib/libraries/decks/translate-video.js'; | |
import {translateImage} from '../../lib/libraries/decks/translate-image.js'; | |
const CardHeader = ({onCloseCards, onShrinkExpandCards, onShowAll, totalSteps, step, expanded}) => ( | |
<div className={expanded ? styles.headerButtons : classNames(styles.headerButtons, styles.headerButtonsHidden)}> | |
<div | |
className={styles.allButton} | |
onClick={onShowAll} | |
> | |
<img | |
className={styles.helpIcon} | |
src={helpIcon} | |
/> | |
<FormattedMessage | |
defaultMessage="Tutorials" | |
description="Title for button to return to tutorials library" | |
id="gui.cards.all-tutorials" | |
/> | |
</div> | |
{totalSteps > 1 ? ( | |
<div className={styles.stepsList}> | |
{Array(totalSteps).fill(0) | |
.map((_, i) => ( | |
<div | |
className={i === step ? styles.activeStepPip : styles.inactiveStepPip} | |
key={`pip-step-${i}`} | |
/> | |
))} | |
</div> | |
) : null} | |
<div className={styles.headerButtonsRight}> | |
<div | |
className={styles.shrinkExpandButton} | |
onClick={onShrinkExpandCards} | |
> | |
<img | |
draggable={false} | |
src={expanded ? shrinkIcon : expandIcon} | |
/> | |
{expanded ? | |
<FormattedMessage | |
defaultMessage="Shrink" | |
description="Title for button to shrink how-to card" | |
id="gui.cards.shrink" | |
/> : | |
<FormattedMessage | |
defaultMessage="Expand" | |
description="Title for button to expand how-to card" | |
id="gui.cards.expand" | |
/> | |
} | |
</div> | |
<div | |
className={styles.removeButton} | |
onClick={onCloseCards} | |
> | |
<img | |
className={styles.closeIcon} | |
src={closeIcon} | |
/> | |
<FormattedMessage | |
defaultMessage="Close" | |
description="Title for button to close how-to card" | |
id="gui.cards.close" | |
/> | |
</div> | |
</div> | |
</div> | |
); | |
class VideoStep extends React.Component { | |
componentDidMount () { | |
const script = document.createElement('script'); | |
script.src = `https://fast.wistia.com/embed/medias/${this.props.video}.jsonp`; | |
script.async = true; | |
script.setAttribute('id', 'wistia-video-content'); | |
document.body.appendChild(script); | |
const script2 = document.createElement('script'); | |
script2.src = 'https://fast.wistia.com/assets/external/E-v1.js'; | |
script2.async = true; | |
script2.setAttribute('id', 'wistia-video-api'); | |
document.body.appendChild(script2); | |
} | |
// We use the Wistia API here to update or pause the video dynamically: | |
// https://wistia.com/support/developers/player-api | |
componentDidUpdate (prevProps) { | |
// Ensure the wistia API is loaded and available | |
if (!(window.Wistia && window.Wistia.api)) return; | |
// Get a handle on the currently loaded video | |
const video = window.Wistia.api(prevProps.video); | |
// Reset the video source if a new video has been chosen from the library | |
if (prevProps.video !== this.props.video) { | |
video.replaceWith(this.props.video); | |
} | |
// Pause the video if the modal is being shrunken | |
if (!this.props.expanded) { | |
video.pause(); | |
} | |
} | |
componentWillUnmount () { | |
const script = document.getElementById('wistia-video-content'); | |
script.parentNode.removeChild(script); | |
const script2 = document.getElementById('wistia-video-api'); | |
script2.parentNode.removeChild(script2); | |
} | |
render () { | |
return ( | |
<div className={styles.stepVideo}> | |
<div | |
className={`wistia_embed wistia_async_${this.props.video}`} | |
id="video-div" | |
style={{height: `257px`, width: `466px`}} | |
> | |
| |
</div> | |
</div> | |
); | |
} | |
} | |
VideoStep.propTypes = { | |
expanded: PropTypes.bool.isRequired, | |
video: PropTypes.string.isRequired | |
}; | |
const ImageStep = ({title, image}) => ( | |
<Fragment> | |
<div className={styles.stepTitle}> | |
{title} | |
</div> | |
<div className={styles.stepImageContainer}> | |
<img | |
className={styles.stepImage} | |
draggable={false} | |
key={image} /* Use src as key to prevent hanging around on slow connections */ | |
src={image} | |
/> | |
</div> | |
</Fragment> | |
); | |
ImageStep.propTypes = { | |
image: PropTypes.string.isRequired, | |
title: PropTypes.node.isRequired | |
}; | |
const NextPrevButtons = ({isRtl, onNextStep, onPrevStep, expanded}) => ( | |
<Fragment> | |
{onNextStep ? ( | |
<div> | |
<div className={expanded ? (isRtl ? styles.leftCard : styles.rightCard) : styles.hidden} /> | |
<div | |
className={expanded ? (isRtl ? styles.leftButton : styles.rightButton) : styles.hidden} | |
onClick={onNextStep} | |
> | |
<img | |
draggable={false} | |
src={isRtl ? leftArrow : rightArrow} | |
/> | |
</div> | |
</div> | |
) : null} | |
{onPrevStep ? ( | |
<div> | |
<div className={expanded ? (isRtl ? styles.rightCard : styles.leftCard) : styles.hidden} /> | |
<div | |
className={expanded ? (isRtl ? styles.rightButton : styles.leftButton) : styles.hidden} | |
onClick={onPrevStep} | |
> | |
<img | |
draggable={false} | |
src={isRtl ? rightArrow : leftArrow} | |
/> | |
</div> | |
</div> | |
) : null} | |
</Fragment> | |
); | |
NextPrevButtons.propTypes = { | |
expanded: PropTypes.bool.isRequired, | |
isRtl: PropTypes.bool, | |
onNextStep: PropTypes.func, | |
onPrevStep: PropTypes.func | |
}; | |
CardHeader.propTypes = { | |
expanded: PropTypes.bool.isRequired, | |
onCloseCards: PropTypes.func.isRequired, | |
onShowAll: PropTypes.func.isRequired, | |
onShrinkExpandCards: PropTypes.func.isRequired, | |
step: PropTypes.number, | |
totalSteps: PropTypes.number | |
}; | |
const PreviewsStep = ({deckIds, content, onActivateDeckFactory, onShowAll}) => ( | |
<Fragment> | |
<div className={styles.stepTitle}> | |
<FormattedMessage | |
defaultMessage="More things to try!" | |
description="Title card with more things to try" | |
id="gui.cards.more-things-to-try" | |
/> | |
</div> | |
<div className={styles.decks}> | |
{deckIds.slice(0, 2).map(id => ( | |
<div | |
className={styles.deck} | |
key={`deck-preview-${id}`} | |
onClick={onActivateDeckFactory(id)} | |
> | |
<img | |
className={styles.deckImage} | |
draggable={false} | |
src={content[id].img} | |
/> | |
<div className={styles.deckName}>{content[id].name}</div> | |
</div> | |
))} | |
</div> | |
<div className={styles.seeAll}> | |
<div | |
className={styles.seeAllButton} | |
onClick={onShowAll} | |
> | |
<FormattedMessage | |
defaultMessage="See more" | |
description="Title for button to see more in how-to library" | |
id="gui.cards.see-more" | |
/> | |
</div> | |
</div> | |
</Fragment> | |
); | |
PreviewsStep.propTypes = { | |
content: PropTypes.shape({ | |
id: PropTypes.shape({ | |
name: PropTypes.node.isRequired, | |
img: PropTypes.string.isRequired, | |
steps: PropTypes.arrayOf(PropTypes.shape({ | |
title: PropTypes.node, | |
image: PropTypes.string, | |
video: PropTypes.string, | |
deckIds: PropTypes.arrayOf(PropTypes.string) | |
})) | |
}) | |
}).isRequired, | |
deckIds: PropTypes.arrayOf(PropTypes.string).isRequired, | |
onActivateDeckFactory: PropTypes.func.isRequired, | |
onShowAll: PropTypes.func.isRequired | |
}; | |
const Cards = props => { | |
const { | |
activeDeckId, | |
content, | |
dragging, | |
isRtl, | |
locale, | |
onActivateDeckFactory, | |
onCloseCards, | |
onShrinkExpandCards, | |
onDrag, | |
onStartDrag, | |
onEndDrag, | |
onShowAll, | |
onNextStep, | |
onPrevStep, | |
showVideos, | |
step, | |
expanded, | |
...posProps | |
} = props; | |
let {x, y} = posProps; | |
if (activeDeckId === null) return; | |
// Tutorial cards need to calculate their own dragging bounds | |
// to allow for dragging the cards off the left, right and bottom | |
// edges of the workspace. | |
const cardHorizontalDragOffset = 400; // ~80% of card width | |
const cardVerticalDragOffset = expanded ? 257 : 0; // ~80% of card height, if expanded | |
const menuBarHeight = 48; // TODO: get pre-calculated from elsewhere? | |
const wideCardWidth = 500; | |
if (x === 0 && y === 0) { | |
// initialize positions | |
x = isRtl ? (-190 - wideCardWidth - cardHorizontalDragOffset) : 292; | |
x += cardHorizontalDragOffset; | |
// The tallest cards are about 320px high, and the default position is pinned | |
// to near the bottom of the blocks palette to allow room to work above. | |
const tallCardHeight = 320; | |
const bottomMargin = 60; // To avoid overlapping the backpack region | |
y = window.innerHeight - tallCardHeight - bottomMargin - menuBarHeight; | |
} | |
const steps = content[activeDeckId].steps; | |
return ( | |
// Custom overlay to act as the bounding parent for the draggable, using values from above | |
<div | |
className={styles.cardContainerOverlay} | |
style={{ | |
width: `${window.innerWidth + (2 * cardHorizontalDragOffset)}px`, | |
height: `${window.innerHeight - menuBarHeight + cardVerticalDragOffset}px`, | |
top: `${menuBarHeight}px`, | |
left: `${-cardHorizontalDragOffset}px` | |
}} | |
> | |
<Draggable | |
bounds="parent" | |
cancel="#video-div" // disable dragging on video div | |
position={{x: x, y: y}} | |
onDrag={onDrag} | |
onStart={onStartDrag} | |
onStop={onEndDrag} | |
> | |
<div className={styles.cardContainer}> | |
<div className={styles.card}> | |
<CardHeader | |
expanded={expanded} | |
step={step} | |
totalSteps={steps.length} | |
onCloseCards={onCloseCards} | |
onShowAll={onShowAll} | |
onShrinkExpandCards={onShrinkExpandCards} | |
/> | |
<div className={expanded ? styles.stepBody : styles.hidden}> | |
{steps[step].deckIds ? ( | |
<PreviewsStep | |
content={content} | |
deckIds={steps[step].deckIds} | |
onActivateDeckFactory={onActivateDeckFactory} | |
onShowAll={onShowAll} | |
/> | |
) : ( | |
steps[step].video ? ( | |
showVideos ? ( | |
<VideoStep | |
dragging={dragging} | |
expanded={expanded} | |
video={translateVideo(steps[step].video, locale)} | |
/> | |
) : ( // Else show the deck image and title | |
<ImageStep | |
image={content[activeDeckId].img} | |
title={content[activeDeckId].name} | |
/> | |
) | |
) : ( | |
<ImageStep | |
image={translateImage(steps[step].image, locale)} | |
title={steps[step].title} | |
/> | |
) | |
)} | |
{steps[step].trackingPixel && steps[step].trackingPixel} | |
</div> | |
<NextPrevButtons | |
expanded={expanded} | |
isRtl={isRtl} | |
onNextStep={step < steps.length - 1 ? onNextStep : null} | |
onPrevStep={step > 0 ? onPrevStep : null} | |
/> | |
</div> | |
</div> | |
</Draggable> | |
</div> | |
); | |
}; | |
Cards.propTypes = { | |
activeDeckId: PropTypes.string.isRequired, | |
content: PropTypes.shape({ | |
id: PropTypes.shape({ | |
name: PropTypes.node.isRequired, | |
img: PropTypes.string.isRequired, | |
steps: PropTypes.arrayOf(PropTypes.shape({ | |
title: PropTypes.node, | |
image: PropTypes.string, | |
video: PropTypes.string, | |
deckIds: PropTypes.arrayOf(PropTypes.string) | |
})) | |
}) | |
}), | |
dragging: PropTypes.bool.isRequired, | |
expanded: PropTypes.bool.isRequired, | |
isRtl: PropTypes.bool.isRequired, | |
locale: PropTypes.string.isRequired, | |
onActivateDeckFactory: PropTypes.func.isRequired, | |
onCloseCards: PropTypes.func.isRequired, | |
onDrag: PropTypes.func, | |
onEndDrag: PropTypes.func, | |
onNextStep: PropTypes.func.isRequired, | |
onPrevStep: PropTypes.func.isRequired, | |
onShowAll: PropTypes.func, | |
onShrinkExpandCards: PropTypes.func.isRequired, | |
onStartDrag: PropTypes.func, | |
showVideos: PropTypes.bool, | |
step: PropTypes.number.isRequired, | |
x: PropTypes.number, | |
y: PropTypes.number | |
}; | |
Cards.defaultProps = { | |
showVideos: true | |
}; | |
export { | |
Cards as default, | |
// Others exported for testability | |
ImageStep, | |
VideoStep | |
}; | |