Restructure click shield (#1611)

* Rename one-click-embedding to click shield
* Removes the static fallback image from the vimeo frame. The set URL is broken.
* Adds the ability to define background colors for the click shield background


Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-11-14 18:44:55 +01:00 committed by GitHub
parent c731c0e9f1
commit e9defd60dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 209 additions and 155 deletions

View file

@ -14,7 +14,8 @@ describe('Link gets replaced with embedding: ', () => {
it('GitHub Gist', () => {
cy.setCodemirrorContent('https://gist.github.com/schacon/1')
cy.getMarkdownBody()
.find('.one-click-embedding.gist-frame')
.find('[data-cypress-id="click-shield-gist"] .preview-background')
.parent()
.click()
cy.getMarkdownBody()
.find('iframe[data-cypress-id=gh-gist]')
@ -24,7 +25,7 @@ describe('Link gets replaced with embedding: ', () => {
it('YouTube', () => {
cy.setCodemirrorContent('https://www.youtube.com/watch?v=YE7VzlLtp-4')
cy.getMarkdownBody()
.find('.one-click-embedding-preview')
.find('[data-cypress-id="click-shield-youtube"] .preview-background')
.should('have.attr', 'src', 'https://i.ytimg.com/vi/YE7VzlLtp-4/maxresdefault.jpg')
.parent()
.click()
@ -46,7 +47,7 @@ describe('Link gets replaced with embedding: ', () => {
})
cy.setCodemirrorContent('https://vimeo.com/23237102')
cy.getMarkdownBody()
.find('.one-click-embedding-preview')
.find('[data-cypress-id="click-shield-vimeo"] .preview-background')
.should('have.attr', 'src', 'https://i.vimeocdn.com/video/503631401_640.jpg')
.parent()
.click()
@ -58,7 +59,7 @@ describe('Link gets replaced with embedding: ', () => {
it('Asciinema', () => {
cy.setCodemirrorContent('https://asciinema.org/a/117928')
cy.getMarkdownBody()
.find('.one-click-embedding-preview')
.find('[data-cypress-id="click-shield-asciinema"] .preview-background')
.should('have.attr', 'src', 'https://asciinema.org/a/117928.png')
.parent()
.click()

View file

@ -37,10 +37,10 @@ describe('Short code gets replaced or rendered: ', () => {
})
describe('youtube', () => {
it('renders one-click-embedding', () => {
it('renders click-shield', () => {
cy.setCodemirrorContent(`{%youtube YE7VzlLtp-4 %}`)
cy.getMarkdownBody()
.find('.one-click-embedding.embed-responsive-item')
.find('[data-cypress-id="click-shield-youtube"]')
})
})
})

View file

@ -29,7 +29,7 @@
"svg": "Save as SVG",
"errorJson": "Error parsing the JSON"
},
"one-click-embedding": {
"clickShield": {
"previewHoverText": "Click to load content from {{target}}"
}
},

View file

@ -5,22 +5,29 @@
*/
import React from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { ClickShield } from '../click-shield/click-shield'
import type { IdProps } from '../custom-tag-with-id-component-replacer'
/**
* Renders an embedding for https://asciinema.org
*
* @param id The id from the asciinema url
*/
export const AsciinemaFrame: React.FC<IdProps> = ({ id }) => {
return (
<OneClickEmbedding
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
<ClickShield
hoverIcon={'play'}
targetDescription={'asciinema'}
loadingImageUrl={`https://asciinema.org/a/${id}.png`}>
fallbackPreviewImageUrl={`https://asciinema.org/a/${id}.png`}
fallbackBackgroundColor={'#d40000'}
data-cypress-id={'click-shield-asciinema'}>
<span className={'embed-responsive embed-responsive-16by9'}>
<iframe
className='embed-responsive-item'
title={`asciinema cast ${id}`}
src={`https://asciinema.org/a/${id}/embed?autoplay=1`}
/>
</OneClickEmbedding>
</span>
</ClickShield>
)
}

View file

@ -4,33 +4,44 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
.one-click-embedding {
.click-shield {
position: relative;
cursor: pointer;
width: 100%;
overflow: hidden;
.one-click-embedding-icon {
.preview-hover {
display: inline;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
opacity: 0.4;
opacity: 0.5;
transition: opacity 0.2s;
text-shadow: #000000 0 0 5px;
}
&:hover > .one-click-embedding-icon {
opacity: 0.8;
text-shadow: #000000 0 0 10px;
.preview-hover-text {
opacity: 0;
}
.one-click-embedding-preview {
&:hover {
.preview-hover-text {
opacity: 1;
}
.preview-hover {
opacity: 1;
text-shadow: #000000 0 0 5px, #000000 0 0 10px;
}
}
.preview-background {
background: none;
height: 100%;
object-fit: contain;
object-fit: cover;
min-height: 300px;
width: 100%;
}
}

View file

@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import type { IconName } from '../../../common/fork-awesome/types'
import { ShowIf } from '../../../common/show-if/show-if'
import './click-shield.scss'
import { ProxyImageFrame } from '../image/proxy-image-frame'
import { Logger } from '../../../../utils/logger'
import type { Property } from 'csstype'
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
const log = new Logger('OneClickEmbedding')
interface ClickShieldProps extends PropsWithDataCypressId {
onImageFetch?: () => Promise<string>
fallbackPreviewImageUrl?: string
hoverIcon?: IconName
hoverTextI18nKey?: string
targetDescription?: string
containerClassName?: string
fallbackBackgroundColor?: Property.BackgroundColor
}
/**
* Prevents loading of the children elements until the user unlocks the content by e.g. clicking.
*
* @param containerClassName Additional CSS classes for the complete component
* @param onImageFetch A callback that is used to get an URL for the preview image
* @param fallbackPreviewImageUrl The URL for an image that should be shown. If onImageFetch is defined then this image will be shown until onImageFetch provides another URL.
* @param targetDescription The name of the target service
* @param hoverIcon The name of an icon that should be shown in the preview
* @param fallbackBackgroundColor A color that should be used if no background image was provided or could be loaded.
* @param children The children element that should be shielded.
*/
export const ClickShield: React.FC<ClickShieldProps> = ({
containerClassName,
onImageFetch,
fallbackPreviewImageUrl,
children,
targetDescription,
hoverIcon,
fallbackBackgroundColor,
...props
}) => {
const [showChildren, setShowChildren] = useState(false)
const [previewImageUrl, setPreviewImageUrl] = useState(fallbackPreviewImageUrl)
const { t } = useTranslation()
const doShowChildren = useCallback(() => {
setShowChildren(true)
}, [])
useEffect(() => {
if (!onImageFetch) {
return
}
onImageFetch()
.then((imageLink) => {
setPreviewImageUrl(imageLink)
})
.catch((message) => {
log.error(message)
})
}, [onImageFetch])
const fallbackBackgroundStyle = useMemo<React.CSSProperties>(
() =>
!fallbackBackgroundColor
? {}
: {
backgroundColor: fallbackBackgroundColor
},
[fallbackBackgroundColor]
)
const previewHoverText = useMemo(() => {
return targetDescription ? t('renderer.clickShield.previewHoverText', { target: targetDescription }) : ''
}, [t, targetDescription])
const previewBackground = useMemo(() => {
return previewImageUrl === undefined ? (
<span className={'preview-background embed-responsive-item'} style={fallbackBackgroundStyle} />
) : (
<ProxyImageFrame
className={'preview-background embed-responsive-item'}
style={fallbackBackgroundStyle}
src={previewImageUrl}
alt={previewHoverText}
title={previewHoverText}
/>
)
}, [fallbackBackgroundStyle, previewHoverText, previewImageUrl])
return (
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
<ShowIf condition={showChildren}>{children}</ShowIf>
<ShowIf condition={!showChildren}>
<span className={`click-shield embed-responsive embed-responsive-16by9`} onClick={doShowChildren}>
{previewBackground}
<ShowIf condition={!!hoverIcon}>
<span className={`preview-hover text-center`}>
<span className={'preview-hover-text'}>
<Trans i18nKey={'renderer.clickShield.previewHoverText'} tOptions={{ target: targetDescription }} />
</span>
<br />
<ForkAwesomeIcon icon={hoverIcon as IconName} size={'5x'} className={'mb-2'} />
</span>
</ShowIf>
</span>
</ShowIf>
</span>
)
}

View file

@ -4,12 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
.gist-frame {
.one-click-embedding-preview {
filter: blur(3px);
}
}
.gist-resizer-row {
display: flex;
justify-content: center;

View file

@ -8,8 +8,7 @@ import React, { useCallback } from 'react'
import { cypressId } from '../../../../utils/cypress-attribute'
import './gist-frame.scss'
import { useResizeGistFrame } from './use-resize-gist-frame'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import preview from './gist-preview.png'
import { ClickShield } from '../click-shield/click-shield'
import type { IdProps } from '../custom-tag-with-id-component-replacer'
/**
@ -28,11 +27,11 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
)
return (
<OneClickEmbedding
previewContainerClassName={'gist-frame'}
loadingImageUrl={preview}
<ClickShield
fallbackBackgroundColor={'#161b22'}
hoverIcon={'github'}
targetDescription={'GitHub Gist'}>
targetDescription={'GitHub Gist'}
data-cypress-id={'click-shield-gist'}>
<iframe
sandbox=''
{...cypressId('gh-gist')}
@ -45,6 +44,6 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
<span className={'gist-resizer-row'}>
<span className={'gist-resizer'} onMouseDown={onStart} onTouchStart={onStart} />
</span>
</OneClickEmbedding>
</ClickShield>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -1,88 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { IconName } from '../../../common/fork-awesome/types'
import { ShowIf } from '../../../common/show-if/show-if'
import './one-click-embedding.scss'
import { ProxyImageFrame } from '../image/proxy-image-frame'
import { Logger } from '../../../../utils/logger'
const log = new Logger('OneClickEmbedding')
interface OneClickFrameProps {
onImageFetch?: () => Promise<string>
loadingImageUrl?: string
hoverIcon?: IconName
hoverTextI18nKey?: string
targetDescription?: string
containerClassName?: string
previewContainerClassName?: string
onActivate?: () => void
}
export const OneClickEmbedding: React.FC<OneClickFrameProps> = ({
previewContainerClassName,
containerClassName,
onImageFetch,
loadingImageUrl,
children,
targetDescription,
hoverIcon,
onActivate
}) => {
const [showFrame, setShowFrame] = useState(false)
const [previewImageUrl, setPreviewImageUrl] = useState(loadingImageUrl)
const { t } = useTranslation()
const showChildren = () => {
setShowFrame(true)
if (onActivate) {
onActivate()
}
}
useEffect(() => {
if (!onImageFetch) {
return
}
onImageFetch()
.then((imageLink) => {
setPreviewImageUrl(imageLink)
})
.catch((message) => {
log.error(message)
})
}, [onImageFetch])
const previewHoverText = useMemo(() => {
return targetDescription ? t('renderer.one-click-embedding.previewHoverText', { target: targetDescription }) : ''
}, [t, targetDescription])
return (
<span className={containerClassName}>
<ShowIf condition={showFrame}>{children}</ShowIf>
<ShowIf condition={!showFrame}>
<span className={`one-click-embedding ${previewContainerClassName || ''}`} onClick={showChildren}>
<ShowIf condition={!!previewImageUrl}>
<ProxyImageFrame
className={'one-click-embedding-preview'}
src={previewImageUrl}
alt={previewHoverText}
title={previewHoverText}
/>
</ShowIf>
<ShowIf condition={!!hoverIcon}>
<span className='one-click-embedding-icon text-center'>
<i className={`fa fa-${hoverIcon as string} fa-5x mb-2`} />
</span>
</ShowIf>
</span>
</ShowIf>
</span>
)
}

View file

@ -5,7 +5,7 @@
*/
import React, { useCallback } from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { ClickShield } from '../click-shield/click-shield'
import type { IdProps } from '../custom-tag-with-id-component-replacer'
interface VimeoApiResponse {
@ -14,6 +14,11 @@ interface VimeoApiResponse {
thumbnail_large?: string
}
/**
* Renders a video player embedding for https://vimeo.com
*
* @param id The id from the vimeo video url
*/
export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
const getPreviewImageLink = useCallback(async () => {
const response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`, {
@ -33,19 +38,20 @@ export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
}, [id])
return (
<OneClickEmbedding
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
loadingImageUrl={'https://i.vimeocdn.com/video/'}
<ClickShield
hoverIcon={'vimeo-square'}
targetDescription={'Vimeo'}
onImageFetch={getPreviewImageLink}>
onImageFetch={getPreviewImageLink}
fallbackBackgroundColor={'#00adef'}
data-cypress-id={'click-shield-vimeo'}>
<span className={'embed-responsive embed-responsive-16by9'}>
<iframe
className='embed-responsive-item'
title={`vimeo video of ${id}`}
src={`https://player.vimeo.com/video/${id}?autoplay=1`}
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
/>
</OneClickEmbedding>
</span>
</ClickShield>
)
}

View file

@ -5,23 +5,30 @@
*/
import React from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { ClickShield } from '../click-shield/click-shield'
import type { IdProps } from '../custom-tag-with-id-component-replacer'
/**
* Renders a video player embedding for https://youtube.com
*
* @param id The id from the youtube video url
*/
export const YouTubeFrame: React.FC<IdProps> = ({ id }) => {
return (
<OneClickEmbedding
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
<ClickShield
hoverIcon={'youtube-play'}
targetDescription={'YouTube'}
loadingImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}>
fallbackPreviewImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}
fallbackBackgroundColor={'#ff0000'}
data-cypress-id={'click-shield-youtube'}>
<span className={'embed-responsive embed-responsive-16by9'}>
<iframe
className='embed-responsive-item'
title={`youtube video of ${id}`}
src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`}
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
/>
</OneClickEmbedding>
</span>
</ClickShield>
)
}