@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/eslint.config.js CHANGED
@@ -69,7 +69,6 @@ export default [
69
69
  },
70
70
  {
71
71
  files: ['**/*.ts'],
72
- ignores: ['**/*.d.ts'],
73
72
  languageOptions: {
74
73
  parser: tsParser,
75
74
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.28.0-beta.1",
3
+ "version": "8.28.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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
- if (!selector) return false
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()
@@ -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
- const match = findShortestMatchBetweenPhrases(element.innerHTML, injection.title, phrase_before || '', phrase_after || '')
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,3 +1,5 @@
1
+ import type { InsertPosition } from './position'
2
+
1
3
  export type ConfigResponse = {
2
4
  /**
3
5
  * Used to exclude URL matching a regex pattern. If a url matches, nothing is executed beyond fetching the config.
@@ -4,3 +4,5 @@ export type Position = {
4
4
  }
5
5
 
6
6
  export type Alignment = 'bottom' | 'center'
7
+
8
+ export type InsertPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | ''
@@ -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<strong, any> }[]
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.explore_use_router = !useExploreRouter()
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
- try {
30
- const [title, railTitles] = (await Promise.allSettled([
31
- fetchTitleBySid(sid),
32
- fetchSimilarTitles({ sid } as unknown as TitleData)])
33
- ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
34
-
35
- openModal({
36
- type,
37
- data: [(title as TitleData), ...(railTitles as TitleData[])],
38
- props: {
39
- onclose: () => navigate('home'),
40
- pushState: false,
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.explore_use_router = !useExploreRouter()
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: null,
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 is valid', () => {
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.explore_navigation_path + `?route=modal&sid=${injection.title_details?.sid}`)
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)