@playpilot/tpi 8.28.0-beta.1 → 8.28.1
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/.prettierrc +1 -1
- package/dist/editorial.mount.js +8 -8
- package/dist/link-injections.js +2 -2
- package/dist/mount.js +7 -7
- package/eslint.config.js +0 -1
- package/package.json +1 -1
- package/src/lib/explore.ts +2 -1
- package/src/lib/injection.ts +14 -1
- package/src/lib/types/config.d.ts +2 -0
- package/src/lib/types/position.d.ts +2 -0
- package/src/lib/types/script.d.ts +4 -2
- package/src/routes/components/Debugger.svelte +1 -1
- package/src/routes/components/Explore/Routes/ExploreModal.svelte +70 -19
- package/src/routes/components/Modals/Modal.svelte +2 -1
- package/src/routes/explore/+page.svelte +1 -2
- package/src/tests/lib/explore.test.js +4 -2
- package/src/tests/lib/injection.test.js +21 -1
- package/src/tests/lib/retargeting.test.js +2 -0
- package/src/tests/routes/components/Playlinks/Playlink.test.js +2 -0
package/eslint.config.js
CHANGED
package/package.json
CHANGED
package/src/lib/explore.ts
CHANGED
|
@@ -44,7 +44,8 @@ export function insertExploreIntoNavigation(): boolean {
|
|
|
44
44
|
if (!config) return false
|
|
45
45
|
|
|
46
46
|
const { explore_navigation_selector: selector, explore_navigation_label: label, explore_navigation_path: path, explore_navigation_insert_position: insertPosition } = config
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
if (!selector || !path) return false
|
|
48
49
|
|
|
49
50
|
// Make sure to remove an element we created if it already exists. Should not be possible under normal circumstances.
|
|
50
51
|
document.querySelector('[data-playpilot-explore-navigation-element]')?.remove()
|
package/src/lib/injection.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './after
|
|
|
7
7
|
import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
|
|
8
8
|
import { exploreTitleUrl, titleUrl } from './routes'
|
|
9
9
|
import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
|
|
10
|
+
import { decodeHtmlEntities, encodeHtmlEntities } from './html'
|
|
10
11
|
|
|
11
12
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
12
13
|
export const keySelector = `[${keyDataAttribute}]`
|
|
@@ -84,9 +85,21 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
84
85
|
// We insert the html here separately from below, where it's done with replacementIndex because we need to capture all html
|
|
85
86
|
// that may have been inside of the match.
|
|
86
87
|
if (replacementIndex === -1) {
|
|
87
|
-
|
|
88
|
+
let match = findShortestMatchBetweenPhrases(element.innerHTML, injection.title, phrase_before || '', phrase_after || '')
|
|
88
89
|
|
|
89
90
|
if (match) {
|
|
91
|
+
// This is a crude way of checking if a match contains HTML elements
|
|
92
|
+
// If the title is directly inside of an element we discard the rest.
|
|
93
|
+
// This happens sometimes in lists that consists of nothing but titles and may contain special characters
|
|
94
|
+
if (match.match.includes(">") && match.match.includes("<")) {
|
|
95
|
+
const encodedTitle = encodeHtmlEntities(injection.title)
|
|
96
|
+
const matchInsideHTMLIndex = match.match.search(new RegExp(`>\s*${encodedTitle}\s*<`)) + 1
|
|
97
|
+
|
|
98
|
+
if (matchInsideHTMLIndex > 0) {
|
|
99
|
+
match = { match: match.match.slice(matchInsideHTMLIndex, matchInsideHTMLIndex + encodedTitle.length), index: matchInsideHTMLIndex }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
const { leadingSpaces, trailingSpaces } = getNumberOfLeadingAndTrailingSpaces(match.match)
|
|
91
104
|
|
|
92
105
|
linkElement.innerHTML = match.match.trim()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Campaign } from './campaign'
|
|
2
|
+
import type { ConfigResponse } from './config'
|
|
2
3
|
import type { ConsentOptions } from './consent'
|
|
3
4
|
import type { LinkInjection, LinkInjectionResponse } from './injection'
|
|
5
|
+
import type { InsertPosition } from './position'
|
|
4
6
|
import type { TitleData } from './title'
|
|
5
7
|
|
|
6
8
|
export type ScriptConfig = {
|
|
@@ -17,7 +19,7 @@ export type ScriptConfig = {
|
|
|
17
19
|
// Selector for the after article playlinks. Will be placed _after_ the given selector.
|
|
18
20
|
after_article_selector?: string
|
|
19
21
|
// Selector to change the insert position of the after article playlinks. Should only be used if no viable selector can be given with `after_article_selector`.
|
|
20
|
-
after_article_insert_position?: InsertPosition
|
|
22
|
+
after_article_insert_position?: InsertPosition
|
|
21
23
|
// The language of the page will be inferred from the html `lang` attribute. Can be set manually using this option in the config object.
|
|
22
24
|
language?: string | null
|
|
23
25
|
// Set to the last success external-pages fetch.
|
|
@@ -25,7 +27,7 @@ export type ScriptConfig = {
|
|
|
25
27
|
// Lists all tracked events through the `track()` function.
|
|
26
28
|
tracked_events?: { event: string, payload: Record<string, any> }[]
|
|
27
29
|
// Queued tracking events that were fired before consent was given. These might be fired later if the user consents.
|
|
28
|
-
queued_tracking_events?: { event: string, title: TitleData | null, payload: Record<
|
|
30
|
+
queued_tracking_events?: { event: string, title: TitleData | null, payload: Record<string, any> }[]
|
|
29
31
|
// Lists all split test identifiers as created by each running split test. Identifiers are only created when a split test is fired.
|
|
30
32
|
split_test_identifiers?: Record<string, number>
|
|
31
33
|
// All link injections as returned from external-pages, with AI and manual injections merged.
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
{#if !useExploreRouter()}
|
|
182
182
|
<button onclick={(() => {
|
|
183
183
|
destroyExplore()
|
|
184
|
-
window.PlayPilotLinkInjections.config
|
|
184
|
+
window.PlayPilotLinkInjections.config!.explore_use_router = !useExploreRouter()
|
|
185
185
|
insertExplore()
|
|
186
186
|
})}>Toggle new explore</button>
|
|
187
187
|
{/if}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
|
|
3
3
|
import { mobileBreakpoint } from '$lib/constants'
|
|
4
4
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
5
|
+
import { t } from '$lib/localization'
|
|
5
6
|
import { openModal } from '$lib/modal'
|
|
6
7
|
import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
|
|
7
8
|
import type { TitleData } from '$lib/types/title'
|
|
9
|
+
import Modal from '../../Modals/Modal.svelte'
|
|
8
10
|
|
|
9
11
|
interface Props {
|
|
10
12
|
navigate?: (key: string, pushState?: boolean) => void
|
|
@@ -12,8 +14,6 @@
|
|
|
12
14
|
|
|
13
15
|
const { navigate = () => null }: Props = $props()
|
|
14
16
|
|
|
15
|
-
openModalViaRoute()
|
|
16
|
-
|
|
17
17
|
async function openModalViaRoute(): Promise<void> {
|
|
18
18
|
const currentUrl = new URL(document.location.toString())
|
|
19
19
|
const sid = currentUrl.searchParams.get('sid')
|
|
@@ -26,22 +26,73 @@
|
|
|
26
26
|
|
|
27
27
|
if (isMobile) trackSplitTestView(SplitTest.Reels)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
43
|
-
} catch {
|
|
44
|
-
navigate('home')
|
|
45
|
-
}
|
|
29
|
+
const [title, railTitles] = (await Promise.allSettled([
|
|
30
|
+
fetchTitleBySid(sid),
|
|
31
|
+
fetchSimilarTitles({ sid } as unknown as TitleData)])
|
|
32
|
+
).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
|
|
33
|
+
|
|
34
|
+
openModal({
|
|
35
|
+
type,
|
|
36
|
+
data: [(title as TitleData), ...(railTitles as TitleData[])],
|
|
37
|
+
props: {
|
|
38
|
+
onclose: () => navigate('home'),
|
|
39
|
+
pushState: false,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
46
42
|
}
|
|
47
43
|
</script>
|
|
44
|
+
|
|
45
|
+
{#await openModalViaRoute()}
|
|
46
|
+
<Modal blur pushState={false}>
|
|
47
|
+
{#snippet dialog()}
|
|
48
|
+
<div class="loading">
|
|
49
|
+
<div class="bar"></div>
|
|
50
|
+
<div class="bar"></div>
|
|
51
|
+
<div class="bar"></div>
|
|
52
|
+
</div>
|
|
53
|
+
{/snippet}
|
|
54
|
+
</Modal>
|
|
55
|
+
{:catch}
|
|
56
|
+
<Modal blur onclose={() => navigate('home')} pushState={false}>
|
|
57
|
+
<div class="error">
|
|
58
|
+
{t('An Error Occurred')}
|
|
59
|
+
</div>
|
|
60
|
+
</Modal>
|
|
61
|
+
{/await}
|
|
62
|
+
|
|
63
|
+
<style lang="scss">
|
|
64
|
+
.loading {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
height: 100%;
|
|
69
|
+
gap: margin(0.5);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@keyframes animate-bar {
|
|
73
|
+
to {
|
|
74
|
+
height: margin(3);
|
|
75
|
+
opacity: 0.5;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.bar {
|
|
80
|
+
height: margin(4);
|
|
81
|
+
width: margin(1);
|
|
82
|
+
border-radius: margin(0.25);
|
|
83
|
+
background: white;
|
|
84
|
+
animation: animate-bar 750ms infinite;
|
|
85
|
+
|
|
86
|
+
@for $i from 0 through 2 {
|
|
87
|
+
&:nth-child(#{$i}) {
|
|
88
|
+
animation-delay: $i * 250ms;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.error {
|
|
94
|
+
padding: margin(2);
|
|
95
|
+
font-size: 1.25em;
|
|
96
|
+
color: theme(text-color);
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
onMount(() => {
|
|
61
61
|
const scrollElement = getScrollRoot()
|
|
62
|
+
|
|
62
63
|
scrollElement.classList.add('playpilot-modal-open')
|
|
63
64
|
|
|
64
65
|
hasPreviousModal = !!getPreviousModal()
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
requestAnimationFrame(setInitialScrollPosition)
|
|
71
72
|
|
|
72
73
|
return () => {
|
|
73
|
-
scrollElement.classList.remove('playpilot-modal-open')
|
|
74
|
+
if (!document.querySelector('.modal')) scrollElement.classList.remove('playpilot-modal-open')
|
|
74
75
|
|
|
75
76
|
// Remove the modal entry from above from the browser history. This is done only once for the entire stack.
|
|
76
77
|
// We check for getPreviousModal not because we really care about the previous modal, but to check if all
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<button onclick={(() => {
|
|
37
37
|
destroyExplore()
|
|
38
|
-
window.PlayPilotLinkInjections.config
|
|
38
|
+
window.PlayPilotLinkInjections.config!.explore_use_router = !useExploreRouter()
|
|
39
39
|
insertExplore()
|
|
40
40
|
})}>Toggle new explore</button>
|
|
41
41
|
|
|
@@ -73,4 +73,3 @@
|
|
|
73
73
|
background: theme(light);
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
|
-
|
|
@@ -120,7 +120,7 @@ describe('explore.js', () => {
|
|
|
120
120
|
|
|
121
121
|
it('Should not insert if config object is present but explore_navigation_selector is not given', () => {
|
|
122
122
|
window.PlayPilotLinkInjections.config = {
|
|
123
|
-
explore_navigation_selector:
|
|
123
|
+
explore_navigation_selector: '',
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
expect(insertExploreIntoNavigation()).toBe(false)
|
|
@@ -136,11 +136,12 @@ describe('explore.js', () => {
|
|
|
136
136
|
expect(insertExploreIntoNavigation()).toBe(false)
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
it('Should insert if explore_navigation_selector is given and selector
|
|
139
|
+
it('Should insert if explore_navigation_selector is given and selector and path are valid', () => {
|
|
140
140
|
document.body.innerHTML = '<nav><a></a></nav>'
|
|
141
141
|
|
|
142
142
|
window.PlayPilotLinkInjections.config = {
|
|
143
143
|
explore_navigation_selector: 'a',
|
|
144
|
+
explore_navigation_path: '/explore',
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
expect(insertExploreIntoNavigation()).toBe(true)
|
|
@@ -165,6 +166,7 @@ describe('explore.js', () => {
|
|
|
165
166
|
window.PlayPilotLinkInjections.config = {
|
|
166
167
|
explore_navigation_selector: 'a',
|
|
167
168
|
explore_navigation_label: 'Some label',
|
|
169
|
+
explore_navigation_path: '/explore',
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
insertExploreIntoNavigation()
|
|
@@ -867,6 +867,26 @@ describe('injection.ts', () => {
|
|
|
867
867
|
expect(document.querySelectorAll('a')).toHaveLength(4)
|
|
868
868
|
})
|
|
869
869
|
|
|
870
|
+
it('Should properly inject into lists of titles that contain special characters', () => {
|
|
871
|
+
document.body.innerHTML = `<section>
|
|
872
|
+
<ul>
|
|
873
|
+
<li>Daisy Jones & The Six</li>
|
|
874
|
+
<li>Love & Death</li>
|
|
875
|
+
</ul>
|
|
876
|
+
</section>
|
|
877
|
+
`
|
|
878
|
+
|
|
879
|
+
const elements = getLinkInjectionElements(/** @type {HTMLElement} */ (document.querySelector('section')))
|
|
880
|
+
const injections = [
|
|
881
|
+
generateInjection('Daisy Jones & The Six', 'Daisy Jones & The Six'),
|
|
882
|
+
generateInjection('Love & Death', 'Love & Death'),
|
|
883
|
+
]
|
|
884
|
+
|
|
885
|
+
injectLinksInDocument(elements, { aiInjections: [], manualInjections: injections })
|
|
886
|
+
|
|
887
|
+
expect(document.querySelectorAll('a')).toHaveLength(2)
|
|
888
|
+
})
|
|
889
|
+
|
|
870
890
|
it('Should properly inject into titles that are split up by multiple elements when phrase_before and phrase_after are given', () => {
|
|
871
891
|
document.body.innerHTML = '<section>Some text with a <strong>phra</strong><strong>se</strong> in it</section>'
|
|
872
892
|
|
|
@@ -984,7 +1004,7 @@ describe('injection.ts', () => {
|
|
|
984
1004
|
injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
|
|
985
1005
|
|
|
986
1006
|
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
987
|
-
expect(link.href).toBe(window.PlayPilotLinkInjections.config
|
|
1007
|
+
expect(link.href).toBe(window.PlayPilotLinkInjections.config?.explore_navigation_path + `?route=modal&sid=${injection.title_details?.sid}`)
|
|
988
1008
|
expect(link.target).not.toBeTruthy()
|
|
989
1009
|
})
|
|
990
1010
|
|
|
@@ -26,6 +26,7 @@ describe('retargeting.ts', () => {
|
|
|
26
26
|
describe('isPixelAllow', () => {
|
|
27
27
|
it('Should return false if no config object is set', () => {
|
|
28
28
|
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
29
|
+
// @ts-ignore
|
|
29
30
|
window.PlayPilotLinkInjections.config = null
|
|
30
31
|
|
|
31
32
|
expect(isRetargetingAllowed()).toBe(false)
|
|
@@ -33,6 +34,7 @@ describe('retargeting.ts', () => {
|
|
|
33
34
|
|
|
34
35
|
it('Should return false if config object is without allow_retargeting_pixels', () => {
|
|
35
36
|
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
37
|
+
// @ts-ignore
|
|
36
38
|
window.PlayPilotLinkInjections.config = { something: true }
|
|
37
39
|
|
|
38
40
|
expect(isRetargetingAllowed()).toBe(false)
|
|
@@ -118,6 +118,7 @@ describe('Playlink.svelte', () => {
|
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
it('Should render as link when user has not consented to affiliate but require_affiliate_consent is false', () => {
|
|
121
|
+
// @ts-ignore
|
|
121
122
|
window.PlayPilotLinkInjections.config.require_affiliate_consent = false
|
|
122
123
|
|
|
123
124
|
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
@@ -129,6 +130,7 @@ describe('Playlink.svelte', () => {
|
|
|
129
130
|
})
|
|
130
131
|
|
|
131
132
|
it('Should render as link when user has not consented to affiliate and require_affiliate_consent is true', () => {
|
|
133
|
+
// @ts-ignore
|
|
132
134
|
window.PlayPilotLinkInjections.config.require_affiliate_consent = true
|
|
133
135
|
|
|
134
136
|
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|