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

View file

@ -37,10 +37,10 @@ describe('Short code gets replaced or rendered: ', () => {
}) })
describe('youtube', () => { describe('youtube', () => {
it('renders one-click-embedding', () => { it('renders click-shield', () => {
cy.setCodemirrorContent(`{%youtube YE7VzlLtp-4 %}`) cy.setCodemirrorContent(`{%youtube YE7VzlLtp-4 %}`)
cy.getMarkdownBody() 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", "svg": "Save as SVG",
"errorJson": "Error parsing the JSON" "errorJson": "Error parsing the JSON"
}, },
"one-click-embedding": { "clickShield": {
"previewHoverText": "Click to load content from {{target}}" "previewHoverText": "Click to load content from {{target}}"
} }
}, },

View file

@ -5,22 +5,29 @@
*/ */
import React from 'react' 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' 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 }) => { export const AsciinemaFrame: React.FC<IdProps> = ({ id }) => {
return ( return (
<OneClickEmbedding <ClickShield
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
hoverIcon={'play'} hoverIcon={'play'}
targetDescription={'asciinema'} targetDescription={'asciinema'}
loadingImageUrl={`https://asciinema.org/a/${id}.png`}> fallbackPreviewImageUrl={`https://asciinema.org/a/${id}.png`}
<iframe fallbackBackgroundColor={'#d40000'}
className='embed-responsive-item' data-cypress-id={'click-shield-asciinema'}>
title={`asciinema cast ${id}`} <span className={'embed-responsive embed-responsive-16by9'}>
src={`https://asciinema.org/a/${id}/embed?autoplay=1`} <iframe
/> className='embed-responsive-item'
</OneClickEmbedding> title={`asciinema cast ${id}`}
src={`https://asciinema.org/a/${id}/embed?autoplay=1`}
/>
</span>
</ClickShield>
) )
} }

View file

@ -4,33 +4,44 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
.one-click-embedding { .click-shield {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
.one-click-embedding-icon { .preview-hover {
display: inline; display: inline;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
color: white; color: white;
opacity: 0.4; opacity: 0.5;
transition: opacity 0.2s; transition: opacity 0.2s;
text-shadow: #000000 0 0 5px; text-shadow: #000000 0 0 5px;
} }
&:hover > .one-click-embedding-icon { .preview-hover-text {
opacity: 0.8; opacity: 0;
text-shadow: #000000 0 0 10px;
} }
.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; background: none;
height: 100%; 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
.gist-frame {
.one-click-embedding-preview {
filter: blur(3px);
}
}
.gist-resizer-row { .gist-resizer-row {
display: flex; display: flex;
justify-content: center; justify-content: center;

View file

@ -8,8 +8,7 @@ import React, { useCallback } from 'react'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import './gist-frame.scss' import './gist-frame.scss'
import { useResizeGistFrame } from './use-resize-gist-frame' import { useResizeGistFrame } from './use-resize-gist-frame'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding' import { ClickShield } from '../click-shield/click-shield'
import preview from './gist-preview.png'
import type { IdProps } from '../custom-tag-with-id-component-replacer' import type { IdProps } from '../custom-tag-with-id-component-replacer'
/** /**
@ -28,11 +27,11 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
) )
return ( return (
<OneClickEmbedding <ClickShield
previewContainerClassName={'gist-frame'} fallbackBackgroundColor={'#161b22'}
loadingImageUrl={preview}
hoverIcon={'github'} hoverIcon={'github'}
targetDescription={'GitHub Gist'}> targetDescription={'GitHub Gist'}
data-cypress-id={'click-shield-gist'}>
<iframe <iframe
sandbox='' sandbox=''
{...cypressId('gh-gist')} {...cypressId('gh-gist')}
@ -45,6 +44,6 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
<span className={'gist-resizer-row'}> <span className={'gist-resizer-row'}>
<span className={'gist-resizer'} onMouseDown={onStart} onTouchStart={onStart} /> <span className={'gist-resizer'} onMouseDown={onStart} onTouchStart={onStart} />
</span> </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 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' import type { IdProps } from '../custom-tag-with-id-component-replacer'
interface VimeoApiResponse { interface VimeoApiResponse {
@ -14,6 +14,11 @@ interface VimeoApiResponse {
thumbnail_large?: string 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 }) => { export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
const getPreviewImageLink = useCallback(async () => { const getPreviewImageLink = useCallback(async () => {
const response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`, { const response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`, {
@ -33,19 +38,20 @@ export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
}, [id]) }, [id])
return ( return (
<OneClickEmbedding <ClickShield
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
loadingImageUrl={'https://i.vimeocdn.com/video/'}
hoverIcon={'vimeo-square'} hoverIcon={'vimeo-square'}
targetDescription={'Vimeo'} targetDescription={'Vimeo'}
onImageFetch={getPreviewImageLink}> onImageFetch={getPreviewImageLink}
<iframe fallbackBackgroundColor={'#00adef'}
className='embed-responsive-item' data-cypress-id={'click-shield-vimeo'}>
title={`vimeo video of ${id}`} <span className={'embed-responsive embed-responsive-16by9'}>
src={`https://player.vimeo.com/video/${id}?autoplay=1`} <iframe
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' className='embed-responsive-item'
/> title={`vimeo video of ${id}`}
</OneClickEmbedding> src={`https://player.vimeo.com/video/${id}?autoplay=1`}
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
/>
</span>
</ClickShield>
) )
} }

View file

@ -5,23 +5,30 @@
*/ */
import React from 'react' 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' 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 }) => { export const YouTubeFrame: React.FC<IdProps> = ({ id }) => {
return ( return (
<OneClickEmbedding <ClickShield
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
hoverIcon={'youtube-play'} hoverIcon={'youtube-play'}
targetDescription={'YouTube'} targetDescription={'YouTube'}
loadingImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}> fallbackPreviewImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}
<iframe fallbackBackgroundColor={'#ff0000'}
className='embed-responsive-item' data-cypress-id={'click-shield-youtube'}>
title={`youtube video of ${id}`} <span className={'embed-responsive embed-responsive-16by9'}>
src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`} <iframe
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' className='embed-responsive-item'
/> title={`youtube video of ${id}`}
</OneClickEmbedding> src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`}
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
/>
</span>
</ClickShield>
) )
} }