@financial-times/cp-content-pipeline-ui 6.4.6 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.js +3 -1
- package/lib/client.js.map +1 -1
- package/lib/components/Clip/client/index.d.ts +1 -0
- package/lib/components/Clip/client/index.js +77 -67
- package/lib/components/Clip/client/index.js.map +1 -1
- package/lib/components/Clip/client/progressBar.d.ts +0 -1
- package/lib/components/Clip/client/progressBar.js +10 -8
- package/lib/components/Clip/client/progressBar.js.map +1 -1
- package/lib/components/Clip/template/component.js +1 -1
- package/lib/components/Clip/template/component.js.map +1 -1
- package/lib/components/Clip/test/snapshot.spec.js +2 -0
- package/lib/components/Clip/test/snapshot.spec.js.map +1 -1
- package/lib/components/Flourish/client/index.d.ts +2 -0
- package/lib/components/Flourish/client/index.js +16 -0
- package/lib/components/Flourish/client/index.js.map +1 -0
- package/lib/components/Flourish/index.d.ts +16 -3
- package/lib/components/Flourish/index.js +21 -4
- package/lib/components/Flourish/index.js.map +1 -1
- package/lib/components/Flourish/test/snapshot.spec.d.ts +1 -0
- package/lib/components/Flourish/test/snapshot.spec.js +57 -0
- package/lib/components/Flourish/test/snapshot.spec.js.map +1 -0
- package/lib/components/Layout/index.d.ts +9 -3
- package/lib/components/Layout/index.js +7 -1
- package/lib/components/Layout/index.js.map +1 -1
- package/lib/components/Topper/index.d.ts +1 -2
- package/lib/components/Topper/index.js +6 -0
- package/lib/components/Topper/index.js.map +1 -1
- package/lib/stories/Topper.stories.d.ts +9 -0
- package/lib/stories/Topper.stories.js +23 -0
- package/lib/stories/Topper.stories.js.map +1 -0
- package/lib/stories/content.d.ts +65 -0
- package/lib/stories/content.js +82 -0
- package/lib/stories/content.js.map +1 -0
- package/package.json +6 -5
- package/src/client.ts +1 -0
- package/src/components/Clip/client/index.ts +86 -70
- package/src/components/Clip/client/progressBar.ts +12 -8
- package/src/components/Clip/template/component.tsx +1 -1
- package/src/components/Clip/test/snapshot.spec.tsx +2 -0
- package/src/components/Flourish/client/index.ts +17 -0
- package/src/components/Flourish/client/main.scss +38 -0
- package/src/components/Flourish/index.tsx +55 -6
- package/src/components/Flourish/test/__snapshots__/snapshot.spec.tsx.snap +310 -0
- package/src/components/Flourish/test/snapshot.spec.tsx +115 -0
- package/src/components/Layout/index.tsx +17 -4
- package/src/components/Topper/client/main.scss +5 -0
- package/src/components/Topper/index.tsx +21 -2
- package/src/stories/Topper.stories.tsx +22 -0
- package/src/stories/content.tsx +78 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -38,6 +38,7 @@ interface TrackingData {
|
|
|
38
38
|
category: string
|
|
39
39
|
contentId?: string
|
|
40
40
|
action: string
|
|
41
|
+
url: string
|
|
41
42
|
progress?: number
|
|
42
43
|
amount?: number
|
|
43
44
|
amountPercentage?: number
|
|
@@ -194,91 +195,105 @@ class Clip extends ClipInterface {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
async init() {
|
|
197
|
-
|
|
198
|
+
try {
|
|
199
|
+
this.performSourceErrorCheck()
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
!this.opts.noDescription ||
|
|
203
|
+
!this.opts.noInfoBox ||
|
|
204
|
+
!this.opts.noCaption
|
|
205
|
+
) {
|
|
206
|
+
await loadExpander()
|
|
207
|
+
}
|
|
198
208
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await loadExpander()
|
|
205
|
-
}
|
|
209
|
+
this.videoEl.addEventListener('playing', this.markPlayStart.bind(this))
|
|
210
|
+
this.videoEl.addEventListener(
|
|
211
|
+
'pause',
|
|
212
|
+
this.updateAmountWatched.bind(this)
|
|
213
|
+
)
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
])
|
|
220
|
-
|
|
221
|
-
/* When a video starts to autoplay the controls will be visible for about a second.
|
|
215
|
+
this.addEvents([
|
|
216
|
+
'playing',
|
|
217
|
+
'pause',
|
|
218
|
+
'ended',
|
|
219
|
+
'seeked',
|
|
220
|
+
'error',
|
|
221
|
+
'stalled',
|
|
222
|
+
'waiting',
|
|
223
|
+
'timeupdate',
|
|
224
|
+
])
|
|
225
|
+
|
|
226
|
+
/* When a video starts to autoplay the controls will be visible for about a second.
|
|
222
227
|
We want to remove the controls for the initial play, but also add them back in when the user
|
|
223
228
|
wants to see the controls (mouseover focus, and press on a touchscreen).
|
|
224
229
|
*/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
if (this.opts.autoplay && !this.useCustomPlayer) {
|
|
231
|
+
this.videoEl.addEventListener('mouseenter', () => {
|
|
232
|
+
this.videoEl.setAttribute('controls', '')
|
|
233
|
+
})
|
|
229
234
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
this.videoEl.addEventListener('touchenter', () => {
|
|
236
|
+
this.videoEl.setAttribute('controls', '')
|
|
237
|
+
})
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
this.videoEl.addEventListener('focus', () => {
|
|
240
|
+
this.videoEl.setAttribute('controls', '')
|
|
241
|
+
})
|
|
242
|
+
}
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
// send 'watched' event on page unload,
|
|
245
|
+
window.addEventListener(unloadEventName, this.fireWatchedEvent, {
|
|
246
|
+
capture: true,
|
|
247
|
+
})
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
this.fireEvent('mount')
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
const expander = Expander?.init(this.containerEl)
|
|
252
|
+
this.expander = (Array.isArray(expander) ? expander[0] : expander) ?? null
|
|
248
253
|
|
|
249
|
-
|
|
254
|
+
const oExpanderElement = this.expander?.oExpanderElement
|
|
250
255
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
if (oExpanderElement) {
|
|
257
|
+
const buttonElement = oExpanderElement.querySelector('button')
|
|
258
|
+
if (buttonElement) {
|
|
259
|
+
buttonElement.setAttribute('tabindex', '-1')
|
|
260
|
+
}
|
|
255
261
|
}
|
|
256
|
-
}
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
263
|
+
// Listen to offline/online events
|
|
264
|
+
window.addEventListener('offline', this.onOfflineEvent)
|
|
265
|
+
window.addEventListener('online', this.onOnlineEvent)
|
|
266
|
+
// Listen to stalled event to show offline message
|
|
267
|
+
this.videoEl.addEventListener('stalled', this.onOfflineEvent)
|
|
268
|
+
// If the user play a buffered video offline, we need to hide the offline message.
|
|
269
|
+
this.videoEl.addEventListener('playing', this.onOfflineEvent)
|
|
270
|
+
|
|
271
|
+
// Mobile App, differently from DotCom, can render article offline, so we need to check if we are already offline.
|
|
272
|
+
if (!window.navigator.onLine && this.expander) {
|
|
273
|
+
this.showOffLineMessage()
|
|
274
|
+
}
|
|
270
275
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
if (this.expander) {
|
|
277
|
+
this.descriptionToggle()
|
|
278
|
+
}
|
|
279
|
+
//Add custom player
|
|
280
|
+
if (this.useCustomPlayer) {
|
|
281
|
+
this.createCustomPlayer()
|
|
282
|
+
//set default value for mute
|
|
283
|
+
this.muted =
|
|
284
|
+
!this.muteIcon ||
|
|
285
|
+
// TODO: fix this type properly
|
|
286
|
+
(this.opts.autoplay as boolean)
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const customEvent = new CustomEvent(
|
|
290
|
+
'cpContentPipeline.clipComponent.initFailure',
|
|
291
|
+
{
|
|
292
|
+
detail: { clipInstance: this, error },
|
|
293
|
+
bubbles: true,
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
this.containerEl.dispatchEvent(customEvent)
|
|
282
297
|
}
|
|
283
298
|
}
|
|
284
299
|
|
|
@@ -332,6 +347,7 @@ class Clip extends ClipInterface {
|
|
|
332
347
|
//contentId: this.opts.id,
|
|
333
348
|
action: eventType,
|
|
334
349
|
progress: this.getRelevantProgress(),
|
|
350
|
+
url: window.location.href,
|
|
335
351
|
video: {
|
|
336
352
|
duration: this.getDuration(),
|
|
337
353
|
source_url: this.opts.id,
|
|
@@ -26,13 +26,13 @@ class ProgressBar {
|
|
|
26
26
|
private onWindowMouseMoveListener: (e: MouseEvent) => void
|
|
27
27
|
private onWindowMouseUpListener: (e: MouseEvent) => void
|
|
28
28
|
private opts: Required<Opts>
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
constructor(videoEl: HTMLVideoElement, opts: Opts) {
|
|
31
31
|
this.videoEl = videoEl
|
|
32
32
|
this.opts = { ...defaultOpts, ...opts }
|
|
33
33
|
this.onWindowMouseMoveListener = this.windowMouseMoveListener.bind(this)
|
|
34
34
|
this.onWindowMouseUpListener = this.windowMouseUpListener.bind(this)
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
const container = document.createElement('div')
|
|
37
37
|
container.classList.add('cp-clip__video-progress-bar')
|
|
38
38
|
const mainBar = document.createElement('div')
|
|
@@ -88,17 +88,21 @@ class ProgressBar {
|
|
|
88
88
|
|
|
89
89
|
//When mousedown for progressBarTimeTriggerTap time we dispatch the tap event
|
|
90
90
|
container.addEventListener('mousedown', () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
if (!isMobile()) {
|
|
92
|
+
this.mousePressed = true
|
|
93
|
+
window.addEventListener('mouseup', this.onWindowMouseUpListener)
|
|
94
|
+
window.addEventListener('mousemove', this.onWindowMouseMoveListener)
|
|
95
|
+
}
|
|
94
96
|
})
|
|
95
97
|
|
|
96
98
|
container.addEventListener('mouseover', () => {
|
|
97
|
-
|
|
99
|
+
if (!isMobile()) {
|
|
100
|
+
mainBar.classList.add('cp-clip__progress-enlarged')
|
|
101
|
+
}
|
|
98
102
|
})
|
|
99
103
|
|
|
100
104
|
container.addEventListener('mouseleave', () => {
|
|
101
|
-
if (!this.mousePressed) {
|
|
105
|
+
if (!this.mousePressed && !isMobile()) {
|
|
102
106
|
mainBar.classList.remove('cp-clip__progress-enlarged')
|
|
103
107
|
}
|
|
104
108
|
})
|
|
@@ -235,7 +239,7 @@ class ProgressBar {
|
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
windowMouseUpListener(e: MouseEvent) {
|
|
238
|
-
if (this.mousePressed
|
|
242
|
+
if (this.mousePressed) {
|
|
239
243
|
this.scrub(e.clientX)
|
|
240
244
|
this.mousePressed = false
|
|
241
245
|
if (!e.target || !this?.container?.contains(e.target as Node)) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
import React from 'react'
|
|
4
|
+
// TODO: React test renderer is depracated, we should use @testing-library/react
|
|
5
|
+
// https://react.dev/warnings/react-test-renderer
|
|
4
6
|
import renderer from 'react-test-renderer'
|
|
5
7
|
import prettier from 'prettier'
|
|
6
8
|
import { TextEncoder } from 'util'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function hideIFrame(): void {
|
|
2
|
+
const flourishComponent = document.querySelectorAll(
|
|
3
|
+
'[data-component="flourish"]'
|
|
4
|
+
)
|
|
5
|
+
flourishComponent.forEach((component) => {
|
|
6
|
+
component.classList.add('disable-iframe')
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function init(featureFlag: boolean = true): void {
|
|
11
|
+
// turning the feature flag off will hide the iframe and display the fallback image
|
|
12
|
+
if (!featureFlag) {
|
|
13
|
+
hideIFrame()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default init
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.flourish {
|
|
2
|
+
&.flourish--iFrame {
|
|
3
|
+
@include oGridRespondTo(XL) {
|
|
4
|
+
max-width: oGridColspan(12);
|
|
5
|
+
}
|
|
6
|
+
margin-bottom: 0;
|
|
7
|
+
margin-top: oSpacingByName("s4")
|
|
8
|
+
}
|
|
9
|
+
// We need to force the iframe to be hidden if we turn the feature flag off
|
|
10
|
+
&.disable-iframe {
|
|
11
|
+
.flourish__i-frame {
|
|
12
|
+
display: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.flourish__figure {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Without JS the i-frame will not work, so we hide it
|
|
21
|
+
.no-js {
|
|
22
|
+
.flourish__i-frame {
|
|
23
|
+
display: none;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
.js {
|
|
27
|
+
.flourish__i-frame {
|
|
28
|
+
display: block;
|
|
29
|
+
border: 0;
|
|
30
|
+
@media screen and (max-width: 739px) {
|
|
31
|
+
// We need to enforce this otherwise it will use the inline styling
|
|
32
|
+
aspect-ratio: 3/4 !important;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
.flourish__figure {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import classnames from 'classnames'
|
|
3
|
-
import { ContentTree } from '@financial-times/content-tree'
|
|
4
|
-
import { FlourishFragment } from '@financial-times/cp-content-pipeline-client'
|
|
5
3
|
|
|
6
4
|
type disclaimerProps = {
|
|
7
5
|
id: string
|
|
8
6
|
}
|
|
9
7
|
|
|
8
|
+
type FlourishProps = {
|
|
9
|
+
id: string
|
|
10
|
+
type?: string
|
|
11
|
+
flourishType?: string
|
|
12
|
+
description?: string
|
|
13
|
+
layoutWidth?: string
|
|
14
|
+
fallbackImage: {
|
|
15
|
+
url?: string | null | undefined
|
|
16
|
+
width?: number | null | undefined
|
|
17
|
+
height?: number | null | undefined
|
|
18
|
+
}
|
|
19
|
+
iFrame?: boolean
|
|
20
|
+
inArticleBody?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
const DisclaimerNotice = ({ id }: disclaimerProps) => (
|
|
11
24
|
<div
|
|
12
25
|
id={id}
|
|
@@ -30,20 +43,50 @@ export default function Flourish({
|
|
|
30
43
|
description,
|
|
31
44
|
layoutWidth,
|
|
32
45
|
fallbackImage,
|
|
33
|
-
|
|
46
|
+
iFrame = false,
|
|
47
|
+
inArticleBody = true,
|
|
48
|
+
}: FlourishProps) {
|
|
34
49
|
const anchorHref = `#${id}`
|
|
35
50
|
const fullGrid = layoutWidth === 'full-grid' || layoutWidth === 'grid'
|
|
36
51
|
const figureClassnames = classnames({
|
|
37
52
|
'n-content-picture': true,
|
|
38
53
|
'n-content-layout__container': true,
|
|
39
54
|
'n-content-picture--wide': fullGrid,
|
|
55
|
+
flourish__figure: iFrame,
|
|
40
56
|
})
|
|
41
57
|
|
|
58
|
+
const iframeAspectRatio =
|
|
59
|
+
iFrame && fallbackImage?.width && fallbackImage?.height
|
|
60
|
+
? `${fallbackImage.width}/${fallbackImage.height}`
|
|
61
|
+
: '16/9'
|
|
62
|
+
|
|
63
|
+
const imageAspectRatio =
|
|
64
|
+
!inArticleBody && fallbackImage?.width && fallbackImage?.height
|
|
65
|
+
? {
|
|
66
|
+
width: '100%',
|
|
67
|
+
aspectRatio: `${fallbackImage.width}/${fallbackImage.height}`,
|
|
68
|
+
}
|
|
69
|
+
: {}
|
|
70
|
+
|
|
71
|
+
if (!id) return null
|
|
72
|
+
|
|
42
73
|
return (
|
|
43
74
|
<div
|
|
44
|
-
className=
|
|
75
|
+
className={classnames({
|
|
76
|
+
'n-content-layout': inArticleBody,
|
|
77
|
+
flourish: iFrame,
|
|
78
|
+
'flourish--iFrame': iFrame,
|
|
79
|
+
})}
|
|
45
80
|
data-layout-width={fullGrid ? 'full-grid' : null}
|
|
81
|
+
data-component="flourish"
|
|
46
82
|
>
|
|
83
|
+
{iFrame && (
|
|
84
|
+
<iframe
|
|
85
|
+
src={`https://flo.uri.sh/visualisation/${id}/embed?hideTitle=${!inArticleBody}`} // hide the title of if in topper
|
|
86
|
+
style={{ width: '100%', aspectRatio: iframeAspectRatio }}
|
|
87
|
+
className="flourish__i-frame"
|
|
88
|
+
></iframe>
|
|
89
|
+
)}
|
|
47
90
|
<figure
|
|
48
91
|
className={figureClassnames}
|
|
49
92
|
data-original-image-width={fullGrid ? fallbackImage?.width : null}
|
|
@@ -51,12 +94,18 @@ export default function Flourish({
|
|
|
51
94
|
>
|
|
52
95
|
<a href={anchorHref}>
|
|
53
96
|
<picture
|
|
54
|
-
|
|
97
|
+
// `flourish-embed` loads an iframe containing the JS version of a flourish chart
|
|
98
|
+
// `flourish` will be instead targeted by the flourish JS, that will replace the picture with the flourish JS version contained in an iframe
|
|
99
|
+
data-asset-type={iFrame ? 'flourish-embed' : 'flourish'}
|
|
55
100
|
data-flourish-id={id}
|
|
56
101
|
data-flourish-type={flourishType}
|
|
57
102
|
>
|
|
58
103
|
<DisclaimerNotice id={id} />
|
|
59
|
-
<img
|
|
104
|
+
<img
|
|
105
|
+
src={fallbackImage?.url || ''}
|
|
106
|
+
alt={description}
|
|
107
|
+
style={imageAspectRatio}
|
|
108
|
+
/>
|
|
60
109
|
</picture>
|
|
61
110
|
</a>
|
|
62
111
|
</figure>
|