@playpilot/tpi 8.12.2 → 8.13.0-beta.2

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/events.md CHANGED
@@ -100,6 +100,11 @@ Event | Action | Info | Payload
100
100
  `ali_display_ad_playlink_click` | _Fires any time the playlink linked to the display ad is clicked_ | | `campaign_name`
101
101
  `ali_ads_fetch_failed` | _Fires whenever ads tried to but failed to fetch_ | | `status` (Response status code)
102
102
 
103
+ ## Widgets
104
+ Event | Action | Info | Payload
105
+ --- | --- | --- | ---
106
+ `ali_widget_article_rail_title_click` | _Fires when a title in an tpi rail widget is clicked_ | | `Title`
107
+
103
108
  ### Various
104
109
  Event | Action | Info | Payload
105
110
  --- | --- | --- | ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.12.2",
3
+ "version": "8.13.0-beta.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -36,7 +36,7 @@
36
36
  "svelte": "5.44.1",
37
37
  "svelte-check": "^4.0.0",
38
38
  "svelte-preprocess": "^6.0.3",
39
- "svelte-tiny-slider": "^2.7.1",
39
+ "svelte-tiny-slider": "^2.7.2",
40
40
  "typescript": "^5.9.3",
41
41
  "typescript-eslint": "^8.59.2",
42
42
  "vite": "^5.4.21",
@@ -281,6 +281,11 @@ export const translations = {
281
281
  [Language.Swedish]: 'Utforska',
282
282
  [Language.Danish]: 'Udforsk',
283
283
  },
284
+ 'Mentioned In This Article': {
285
+ [Language.English]: 'Mentioned in this article',
286
+ [Language.Swedish]: 'Nämnda i den här artikeln',
287
+ [Language.Danish]: 'Nævnt i denne artikel',
288
+ },
284
289
 
285
290
  // List titles
286
291
  'List: Trending': {
@@ -55,6 +55,9 @@ export const TrackingEvent = {
55
55
  SplitTestView: 'ali_split_test_view',
56
56
  SplitTestAction: 'ali_split_test_action',
57
57
 
58
+ // Widgets
59
+ WidgetArticleRailTitleClick: 'ali_widget_article_rail_title_click',
60
+
58
61
  // Various
59
62
  ShareTitle: 'ali_share_title',
60
63
  SaveTitle: 'ali_save_title',
@@ -49,7 +49,7 @@ export const linkInjections: LinkInjection[] = [{
49
49
  sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
50
50
  playpilot_url: 'https://playpilot.com/movie/example/',
51
51
  key: 'some-key-1',
52
- title_details: title,
52
+ title_details: { ...title, sid: '1' },
53
53
  }, {
54
54
  sid: '2',
55
55
  title: 'The Long Kiss Goodnight',
@@ -57,7 +57,7 @@ export const linkInjections: LinkInjection[] = [{
57
57
  playpilot_url: 'https://playpilot.com/movie/example-2/',
58
58
  key: 'some-key-2',
59
59
  after_article: false,
60
- title_details: title,
60
+ title_details: { ...title, sid: '2' },
61
61
  }, {
62
62
  sid: '3',
63
63
  title: 'Nobody',
@@ -65,7 +65,7 @@ export const linkInjections: LinkInjection[] = [{
65
65
  playpilot_url: 'https://playpilot.com/movie/example-3/',
66
66
  key: 'some-key-3',
67
67
  after_article: true,
68
- title_details: title,
68
+ title_details: { ...title, sid: '3' },
69
69
  manual: false,
70
70
  }, {
71
71
  sid: '4',
@@ -0,0 +1,43 @@
1
+ import { mount, unmount } from 'svelte'
2
+ import type { LinkInjection } from './types/injection'
3
+ import InjectionsWidgetRail from '../routes/components/Widgets/InjectionsWidgetRail.svelte'
4
+
5
+ const widgets: Record<string, any> = {
6
+ 'tpi-rail': InjectionsWidgetRail,
7
+ }
8
+
9
+ export const inTextWidgetSelector = '[data-playpilot-widget]'
10
+
11
+ export let inTextWidgetInsertedComponents: any[] = []
12
+
13
+ export function insertInTextWidgets(linkInjections: LinkInjection[]): void {
14
+ clearInTextWidgets()
15
+
16
+ if (!linkInjections.length) return
17
+
18
+ const targets = document.querySelectorAll<HTMLElement>(inTextWidgetSelector)
19
+
20
+ targets.forEach(target => {
21
+ const widget = target.dataset.playpilotWidget || ''
22
+
23
+ const component = widgets[widget]
24
+
25
+ if (!component) return
26
+
27
+ const insertedComponent = mount(component, {
28
+ target,
29
+ props: {
30
+ linkInjections,
31
+ },
32
+ })
33
+
34
+ inTextWidgetInsertedComponents.push(insertedComponent)
35
+ })
36
+ }
37
+
38
+ export function clearInTextWidgets(): void {
39
+ inTextWidgetInsertedComponents.forEach(component => unmount(component))
40
+ document.querySelectorAll('[data-playpilot-widget]').forEach(element => element.innerHTML = '')
41
+
42
+ inTextWidgetInsertedComponents = []
43
+ }
@@ -5,6 +5,7 @@ import { destroyAllModals, openModalForInjectedLink } from './modal'
5
5
  import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
6
6
  import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
7
7
  import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
8
+ import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
8
9
 
9
10
  export const keyDataAttribute = 'data-playpilot-injection-key'
10
11
  export const keySelector = `[${keyDataAttribute}]`
@@ -168,6 +169,8 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
168
169
  // The function itself will decide whether or not it should actually insert the component based on the config.
169
170
  if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
170
171
 
172
+ insertInTextWidgets(foundInjections)
173
+
171
174
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
172
175
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
173
176
  const duplicate = injection.duplicate ?? hasManualEquivalent
@@ -333,6 +336,7 @@ export function clearLinkInjections(): void {
333
336
 
334
337
  clearAfterArticlePlaylinks()
335
338
  clearInTextDisclaimer()
339
+ clearInTextWidgets()
336
340
  destroyAllModals(false)
337
341
  destroyLinkPopover(false)
338
342
  }
@@ -70,6 +70,8 @@
70
70
  <p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
71
71
  <p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
72
72
 
73
+ <div data-playpilot-widget="tpi-rail"></div>
74
+
73
75
  <h2>A matching link is already present</h2>
74
76
  <p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
75
77
 
@@ -58,15 +58,15 @@
58
58
  .rail {
59
59
  --gap: var(--rail-gap, #{margin(0.5)});
60
60
  position: relative;
61
- width: calc(100% + margin(2));
62
- margin: 0 margin(-1);
61
+ width: calc(100% + var(--rail-margin, margin(1)) * 2);
62
+ margin: 0 calc(var(--rail-margin, margin(1)) * -1);
63
63
 
64
64
  :global(.slider) {
65
- padding: 0 margin(1);
65
+ padding: 0 var(--rail-margin, margin(1));
66
66
  }
67
67
 
68
68
  :global(.slider-content > :last-child) {
69
- margin-right: margin(2);
69
+ margin-right: calc(var(--rail-margin, margin(1)) * 2);
70
70
  }
71
71
  }
72
72
 
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { t } from '$lib/localization'
4
+ import { track } from '$lib/tracking'
5
+ import type { LinkInjection } from '$lib/types/injection'
6
+ import type { TitleData } from '$lib/types/title'
7
+ import TitlesRail from '../Rails/TitlesRail.svelte'
8
+
9
+ interface Props {
10
+ linkInjections: LinkInjection[]
11
+ }
12
+
13
+ const { linkInjections }: Props = $props()
14
+
15
+ let element: HTMLElement | null = $state(null)
16
+
17
+ // @ts-ignore
18
+ const heading = $derived(element?.parentNode?.dataset.heading || t('Mentioned In This Article'))
19
+
20
+ const titles: TitleData[] = $derived.by(() => {
21
+ const uniqueTitles: TitleData[] = []
22
+
23
+ linkInjections.forEach(injection => {
24
+ const title = injection.title_details!
25
+
26
+ if (!title) return
27
+ if (uniqueTitles.some(t => t.sid === title.sid)) return
28
+
29
+ uniqueTitles.push(title)
30
+ })
31
+
32
+ return uniqueTitles
33
+ })
34
+ </script>
35
+
36
+ {#if titles.length}
37
+ <div class="widget" bind:this={element} data-testid="widget">
38
+ <TitlesRail {titles} {heading} onclick={(title) => track(TrackingEvent.WidgetArticleRailTitleClick, title)} />
39
+ </div>
40
+ {/if}
41
+
42
+ <style lang="scss">
43
+ .widget {
44
+ --rail-margin: 0;
45
+ --playpilot-rails-arrow-background: black;
46
+ --playpilot-detail-background-light: black;
47
+ --playpilot-rail-title-text-color: currentColor;
48
+ --playpilot-rail-text-color: currentColor;
49
+ margin: margin(1) 0;
50
+ }
51
+ </style>
@@ -0,0 +1,160 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { mount, unmount } from 'svelte'
3
+ import { generateInjection } from '../helpers'
4
+ import { insertInTextWidgets, clearInTextWidgets, inTextWidgetInsertedComponents } from '$lib/inTextWidgets'
5
+
6
+ vi.mock('svelte', () => ({
7
+ mount: vi.fn(),
8
+ unmount: vi.fn(),
9
+ }))
10
+
11
+ describe('inTextWidgets.ts', () => {
12
+ beforeEach(() => {
13
+ vi.resetAllMocks()
14
+ document.body.innerHTML = ''
15
+
16
+ clearInTextWidgets()
17
+ })
18
+
19
+ describe('insertInTextWidgets', () => {
20
+ it('Should not mount any component if no linkInjections are given', () => {
21
+ document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
22
+
23
+ insertInTextWidgets([])
24
+
25
+ expect(mount).not.toHaveBeenCalled()
26
+ })
27
+
28
+ it('Should not mount any component if no widget targets exist', () => {
29
+ document.body.innerHTML = '<div>No widgets here</div>'
30
+
31
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
32
+
33
+ insertInTextWidgets([injection])
34
+
35
+ expect(mount).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('Should mount a component for a known widget type', () => {
39
+ document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
40
+
41
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
42
+
43
+ insertInTextWidgets([injection])
44
+
45
+ expect(mount).toHaveBeenCalledOnce()
46
+ expect(mount).toHaveBeenCalledWith(
47
+ expect.anything(),
48
+ expect.objectContaining({
49
+ target: document.querySelector('[data-playpilot-widget="tpi-rail"]'),
50
+ props: { linkInjections: [injection] },
51
+ }),
52
+ )
53
+ })
54
+
55
+ it('Should not mount a component for an unknown widget type', () => {
56
+ document.body.innerHTML = '<div data-playpilot-widget="unknown-widget"></div>'
57
+
58
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
59
+
60
+ insertInTextWidgets([injection])
61
+
62
+ expect(mount).not.toHaveBeenCalled()
63
+ })
64
+
65
+ it('Should mount components for multiple widget targets on the same page', () => {
66
+ document.body.innerHTML = `
67
+ <div data-playpilot-widget="tpi-rail"></div>
68
+ <div data-playpilot-widget="tpi-rail"></div>
69
+ `
70
+
71
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
72
+
73
+ insertInTextWidgets([injection])
74
+
75
+ expect(mount).toHaveBeenCalledTimes(2)
76
+ })
77
+
78
+ it('Should skip unknown widget targets and mount known ones', () => {
79
+ document.body.innerHTML = `
80
+ <div data-playpilot-widget="tpi-rail"></div>
81
+ <div data-playpilot-widget="unknown-widget"></div>
82
+ `
83
+
84
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
85
+
86
+ insertInTextWidgets([injection])
87
+
88
+ expect(mount).toHaveBeenCalledOnce()
89
+ })
90
+
91
+ it('Should track inserted components', () => {
92
+ document.body.innerHTML = `
93
+ <div data-playpilot-widget="tpi-rail"></div>
94
+ <div data-playpilot-widget="tpi-rail"></div>
95
+ `
96
+
97
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
98
+
99
+ insertInTextWidgets([injection])
100
+
101
+ expect(inTextWidgetInsertedComponents).toHaveLength(2)
102
+ })
103
+
104
+ it('Should clear previously inserted widgets before inserting new ones', () => {
105
+ document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
106
+
107
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
108
+
109
+ insertInTextWidgets([injection])
110
+ insertInTextWidgets([injection])
111
+
112
+ expect(unmount).toHaveBeenCalled()
113
+ })
114
+ })
115
+
116
+ describe('clearInTextWidgets', () => {
117
+ it('Should unmount all inserted components', () => {
118
+ document.body.innerHTML = `
119
+ <div data-playpilot-widget="tpi-rail"></div>
120
+ <div data-playpilot-widget="tpi-rail"></div>
121
+ `
122
+
123
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
124
+ insertInTextWidgets([injection])
125
+
126
+ clearInTextWidgets()
127
+
128
+ expect(unmount).toHaveBeenCalled()
129
+ })
130
+
131
+ it('Should clear innerHTML of all widget elements', () => {
132
+ document.body.innerHTML = `
133
+ <div data-playpilot-widget="tpi-rail"><span>leftover content</span></div>
134
+ `
135
+
136
+ clearInTextWidgets()
137
+
138
+ expect(/** @type {HTMLElement} */ (document.querySelector('[data-playpilot-widget]')).innerHTML).toBe('')
139
+ })
140
+
141
+ it('Should reset inTextWidgetInsertedComponents to empty array', () => {
142
+ document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
143
+
144
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
145
+ insertInTextWidgets([injection])
146
+
147
+ expect(inTextWidgetInsertedComponents.length).toBe(1)
148
+
149
+ clearInTextWidgets()
150
+
151
+ expect(inTextWidgetInsertedComponents.length).toBe(0)
152
+ })
153
+
154
+ it('Should do nothing if no components are present', () => {
155
+ clearInTextWidgets()
156
+
157
+ expect(unmount).not.toHaveBeenCalled()
158
+ })
159
+ })
160
+ })
@@ -0,0 +1,28 @@
1
+ import { render } from '@testing-library/svelte'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import InjectionsWidgetRail from '../../../../routes/components/Widgets/InjectionsWidgetRail.svelte'
5
+ import { linkInjections } from '$lib/fakeData'
6
+
7
+ describe('InjectionsWidgetRail.svelte', () => {
8
+ it('Should render unique titles for given injections', () => {
9
+ const injections = [linkInjections[0], linkInjections[0], linkInjections[1], linkInjections[2], linkInjections[2]]
10
+
11
+ const { getAllByTestId } = render(InjectionsWidgetRail, { linkInjections: injections })
12
+
13
+ expect(getAllByTestId('title')).toHaveLength(3)
14
+ })
15
+
16
+ it('Should not render if no injections were given', () => {
17
+ const { queryByTestId } = render(InjectionsWidgetRail, { linkInjections: [] })
18
+
19
+ expect(queryByTestId('widget')).not.toBeTruthy()
20
+ })
21
+
22
+ it('Should not render if injections contained no valid titles', () => {
23
+ // @ts-ignore
24
+ const { queryByTestId } = render(InjectionsWidgetRail, { linkInjections: [{ ...linkInjections, title_details: null }] })
25
+
26
+ expect(queryByTestId('widget')).not.toBeTruthy()
27
+ })
28
+ })