mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
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:
parent
c731c0e9f1
commit
e9defd60dc
13 changed files with 209 additions and 155 deletions
|
@ -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()
|
||||
|
|
|
@ -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"]')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"svg": "Save as SVG",
|
||||
"errorJson": "Error parsing the JSON"
|
||||
},
|
||||
"one-click-embedding": {
|
||||
"clickShield": {
|
||||
"previewHoverText": "Click to load content from {{target}}"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 |
|
@ -1,3 +0,0 @@
|
|||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue