@playpilot/tpi 5.33.0 → 5.34.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/dist/link-injections.js +7 -7
- package/package.json +1 -1
- package/src/lib/api/api.ts +1 -1
- package/src/lib/color.ts +19 -0
- package/src/lib/enums/SplitTest.ts +5 -0
- package/src/lib/injection.ts +44 -3
- package/src/lib/modal.ts +4 -0
- package/src/lib/scss/global.scss +39 -0
- package/src/routes/+page.svelte +7 -3
- package/src/routes/components/Debugger.svelte +8 -0
- package/src/routes/components/Participant.svelte +0 -2
package/package.json
CHANGED
package/src/lib/api/api.ts
CHANGED
|
@@ -16,7 +16,7 @@ export async function api<T>(path: string, { headers = {}, method = 'GET', body
|
|
|
16
16
|
const useCache = !isEditorialModeEnabled() && method === 'GET'
|
|
17
17
|
if (useCache && (path in cache)) return cache[path]
|
|
18
18
|
|
|
19
|
-
const baseHeaders = { 'Content-Type': 'application/json' }
|
|
19
|
+
const baseHeaders: Record<string, string> = method === 'GET' ? {} : { 'Content-Type': 'application/json' }
|
|
20
20
|
|
|
21
21
|
const response = await fetch(apiBaseUrl + path, {
|
|
22
22
|
method,
|
package/src/lib/color.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Adjust the lightness of an RGB color. */
|
|
2
|
+
export function colorLuminance(input: string, luminosity: number): string {
|
|
3
|
+
const match = input
|
|
4
|
+
.replace(/\s+/g, '')
|
|
5
|
+
.match(/^rgb\((\d+),(\d+),(\d+)\)$/i)
|
|
6
|
+
|
|
7
|
+
if (!match) return ''
|
|
8
|
+
|
|
9
|
+
const [, r, g, b] = match
|
|
10
|
+
|
|
11
|
+
const adjust = (v: string) => {
|
|
12
|
+
const color = parseInt(v, 10)
|
|
13
|
+
return Math.round(Math.min(Math.max(0, color + color * luminosity), 255))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `rgb(${adjust(r)}, ${adjust(g)}, ${adjust(b)})`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
@@ -9,6 +9,11 @@ export const SplitTest = {
|
|
|
9
9
|
numberOfVariants: 2,
|
|
10
10
|
variantNames: ['Image', 'Label'] as string[],
|
|
11
11
|
},
|
|
12
|
+
InTextEngagement: {
|
|
13
|
+
key: 'in-text-engagement',
|
|
14
|
+
numberOfVariants: 4,
|
|
15
|
+
variantNames: ['Default', 'Clapper Icon', 'Play Icon', 'Animated Link'] as string[]
|
|
16
|
+
},
|
|
12
17
|
UserTimeSpent: {
|
|
13
18
|
key: 'user_time_spent',
|
|
14
19
|
numberOfVariants: 10,
|
package/src/lib/injection.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte'
|
|
2
1
|
import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getIndexOfPhraseInBoundary, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom, findSurroundingPhrases } from './text'
|
|
3
2
|
import type { LinkInjection, LinkInjectionTypes } from './types/injection'
|
|
4
3
|
import { getNumberOfOccurrencesInArray } from './array'
|
|
5
4
|
import { destroyAllModals, openModalForInjectedLink } from './modal'
|
|
6
|
-
import
|
|
5
|
+
import { getSplitTestVariantName } from './splitTest'
|
|
6
|
+
import { SplitTest } from './enums/SplitTest'
|
|
7
|
+
import { colorLuminance } from './color'
|
|
7
8
|
import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
|
|
8
9
|
import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
|
|
9
10
|
import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
|
|
@@ -11,6 +12,8 @@ import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
|
|
|
11
12
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
12
13
|
export const keySelector = `[${keyDataAttribute}]`
|
|
13
14
|
|
|
15
|
+
const linksIntersectionObserver = typeof window !== 'undefined' ? new IntersectionObserver(animateLink) : null
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Return a list of all valid text containing elements that may get injected into.
|
|
16
19
|
* This excludes duplicates, empty elements, links, buttons, and header tags.
|
|
@@ -298,6 +301,19 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
298
301
|
linkElement.target = '_blank'
|
|
299
302
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
300
303
|
|
|
304
|
+
// Part of a split test, insert an absolute positioned icon in the top right of links.
|
|
305
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' || getSplitTestVariantName(SplitTest.InTextEngagement) === 'Play Icon') {
|
|
306
|
+
const iconElement = document.createElement('span')
|
|
307
|
+
iconElement.classList.add('playpilot-injection-info-icon')
|
|
308
|
+
iconElement.dataset.playpilotElement = 'true'
|
|
309
|
+
|
|
310
|
+
const playIcon = '<svg width="8" height="8" viewBox="0 0 8 8" fill="none"><path d="M7.66011 4.53958C8.1133 4.31131 8.1133 3.68869 7.6601 3.46042L0.930122 0.0706345C0.507292 -0.142339 8.99396e-08 0.151949 8.42451e-08 0.610212L0 7.38979C-5.69452e-09 7.84805 0.507291 8.14234 0.930122 7.92937L7.66011 4.53958Z" fill="currentColor"/></svg>'
|
|
311
|
+
const clapperIcon = '<svg width="10" height="12" viewBox="0 0 13 16" fill="none"><rect x="0.950195" y="6.56641" width="12.0498" height="8.80563" rx="1" fill="currentColor" /><rect y="5.92969" width="12" height="2.4" rx="0.5" transform="rotate(-29.6116 0 5.92969)" fill="currentColor" /><path d="M8.82314 11.4484C9.02159 11.3443 9.02159 11.0602 8.82314 10.956L5.87605 9.40918C5.69089 9.312 5.46875 9.44629 5.46875 9.6554L5.46875 12.749C5.46875 12.9582 5.69089 13.0924 5.87605 12.9953L8.82314 11.4484Z" fill="white" /></svg>'
|
|
312
|
+
|
|
313
|
+
iconElement.innerHTML = getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' ? clapperIcon : playIcon
|
|
314
|
+
linkElement.insertAdjacentElement('beforeend', iconElement)
|
|
315
|
+
}
|
|
316
|
+
|
|
301
317
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
302
318
|
|
|
303
319
|
return { injectionElement, linkElement }
|
|
@@ -402,8 +418,10 @@ function addCSSVariablesToLinks(): void {
|
|
|
402
418
|
* Add event listeners to all injected links. These events are for both the popover and the modal.
|
|
403
419
|
*/
|
|
404
420
|
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
405
|
-
window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
|
|
406
421
|
window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
|
|
422
|
+
window.addEventListener('click', (event) => {
|
|
423
|
+
openModalForInjectedLink(event, injections)
|
|
424
|
+
})
|
|
407
425
|
|
|
408
426
|
const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
|
|
409
427
|
|
|
@@ -419,6 +437,26 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
419
437
|
})
|
|
420
438
|
|
|
421
439
|
injectionElement.addEventListener('mouseleave', clearCurrentlyHoveredInjection)
|
|
440
|
+
|
|
441
|
+
linksIntersectionObserver!.observe(injectionElement)
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function animateLink(entries: IntersectionObserverEntry[]): void {
|
|
446
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) !== 'Animated Link') return
|
|
447
|
+
|
|
448
|
+
entries.forEach(entry => {
|
|
449
|
+
if (!entry.isIntersecting) return
|
|
450
|
+
|
|
451
|
+
const linkElement = entry.target.querySelector('a')
|
|
452
|
+
if (!linkElement) return
|
|
453
|
+
|
|
454
|
+
const linkColor = window.getComputedStyle(linkElement).color
|
|
455
|
+
|
|
456
|
+
;(entry.target as HTMLElement).style = `--animation-color: ${colorLuminance(linkColor, -0.5)}`
|
|
457
|
+
|
|
458
|
+
entry.target.classList.remove('animate-injection')
|
|
459
|
+
setTimeout(() => entry.target.classList.add('animate-injection'))
|
|
422
460
|
})
|
|
423
461
|
}
|
|
424
462
|
|
|
@@ -443,6 +481,9 @@ export function clearLinkInjection(key: string): void {
|
|
|
443
481
|
const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
|
|
444
482
|
if (!element) return
|
|
445
483
|
|
|
484
|
+
const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
|
|
485
|
+
playpilotElements.forEach(element => element.remove())
|
|
486
|
+
|
|
446
487
|
const linkContent = element.querySelector('a')?.innerHTML
|
|
447
488
|
element.outerHTML = linkContent || ''
|
|
448
489
|
}
|
package/src/lib/modal.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { getPlayPilotWrapperElement, keyDataAttribute, keySelector } from "./inj
|
|
|
10
10
|
import { playFallbackViewTransition } from "./viewTransition"
|
|
11
11
|
import { destroyLinkPopover } from "./popover"
|
|
12
12
|
import { prefersReducedMotion } from "svelte/motion"
|
|
13
|
+
import { trackSplitTestAction } from "./splitTest"
|
|
14
|
+
import { SplitTest } from "./enums/SplitTest"
|
|
13
15
|
|
|
14
16
|
type ModalType = 'title' | 'participant'
|
|
15
17
|
|
|
@@ -129,6 +131,8 @@ export function openModalForInjectedLink(event: MouseEvent, injections: LinkInje
|
|
|
129
131
|
|
|
130
132
|
event.preventDefault()
|
|
131
133
|
|
|
134
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'click')
|
|
135
|
+
|
|
132
136
|
playFallbackViewTransition(() => {
|
|
133
137
|
destroyLinkPopover(false)
|
|
134
138
|
openModal({ event, injection, data: injection.title_details })
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -7,8 +7,47 @@
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
@keyframes animate-injection {
|
|
11
|
+
0% {
|
|
12
|
+
background-position: 0% 0%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
100% {
|
|
16
|
+
background-position: -200% 0%;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
[data-playpilot-injection-key] {
|
|
11
21
|
position: relative;
|
|
22
|
+
|
|
23
|
+
&.animate-injection a {
|
|
24
|
+
background: linear-gradient(to right, currentColor 50%, var(--animation-color), currentcolor);
|
|
25
|
+
background-clip: text;
|
|
26
|
+
-webkit-background-clip: text;
|
|
27
|
+
-webkit-text-fill-color: transparent;
|
|
28
|
+
background-size: 200% auto;
|
|
29
|
+
animation: animate-injection 650ms 500ms ease-in-out forwards;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.playpilot-injection-info-icon {
|
|
34
|
+
position: relative;
|
|
35
|
+
display: inline-block;
|
|
36
|
+
height: 1em;
|
|
37
|
+
|
|
38
|
+
svg {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
align-items: center;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -0.5em;
|
|
44
|
+
right: -0.25em;
|
|
45
|
+
font-size: 0.65em;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
49
|
+
display: none
|
|
50
|
+
}
|
|
12
51
|
}
|
|
13
52
|
|
|
14
53
|
.playpilot-injection-highlight {
|
package/src/routes/+page.svelte
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { fetchAds } from '$lib/api/ads'
|
|
10
10
|
import { fetchConfig } from '$lib/api/config'
|
|
11
11
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
|
|
12
|
-
import { getSplitTestVariantName } from '$lib/splitTest'
|
|
12
|
+
import { trackSplitTestView, getSplitTestVariantName } from '$lib/splitTest'
|
|
13
13
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
14
14
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
15
15
|
import Editor from './components/Editorial/Editor.svelte'
|
|
@@ -57,7 +57,11 @@
|
|
|
57
57
|
fireQueuedTrackingEvents()
|
|
58
58
|
track(TrackingEvent.ArticlePageView)
|
|
59
59
|
|
|
60
|
-
if (aiInjections.length
|
|
60
|
+
if (!aiInjections.length && !manualInjections.length) return
|
|
61
|
+
|
|
62
|
+
window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
63
|
+
|
|
64
|
+
trackSplitTestView(SplitTest.InTextEngagement)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
async function initialize(): Promise<void> {
|
|
@@ -234,7 +238,7 @@
|
|
|
234
238
|
</svelte:boundary>
|
|
235
239
|
{/if}
|
|
236
240
|
|
|
237
|
-
<Debugger />
|
|
241
|
+
<Debugger onrerender={rerender} />
|
|
238
242
|
</div>
|
|
239
243
|
|
|
240
244
|
{#if response?.pixels?.length}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import { getFullUrlPath } from '$lib/url'
|
|
4
4
|
import { onDestroy } from 'svelte'
|
|
5
5
|
|
|
6
|
+
interface Props {
|
|
7
|
+
onrerender: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { onrerender }: Props = $props()
|
|
11
|
+
|
|
6
12
|
const secrets = ['tpidebug', 'debugtpi']
|
|
7
13
|
const lastInputs: string[] = []
|
|
8
14
|
const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
|
|
@@ -151,6 +157,8 @@
|
|
|
151
157
|
|
|
152
158
|
<hr />
|
|
153
159
|
|
|
160
|
+
<button onclick={onrerender}>Re-inject</button>
|
|
161
|
+
|
|
154
162
|
{#if isUsingBetaScript}
|
|
155
163
|
<small>You are using the beta version of TPI</small>
|
|
156
164
|
{:else}
|
|
@@ -82,8 +82,6 @@
|
|
|
82
82
|
<style lang="scss">
|
|
83
83
|
.header {
|
|
84
84
|
padding: margin(4) margin(1) margin(2);
|
|
85
|
-
background: linear-gradient(to bottom, theme(detail-background-light, lighter), transparent);
|
|
86
|
-
border-radius: theme(detail-border-radius, margin(1) margin(1) 0 0);
|
|
87
85
|
font-family: theme(detail-font-family, font-family);
|
|
88
86
|
font-weight: theme(detail-font-weight, normal);
|
|
89
87
|
font-size: theme(detail-font-size, font-size-base);
|