@playpilot/tpi 5.19.2 → 5.19.3
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 +10 -10
- package/package.json +1 -1
- package/src/lib/injection.ts +8 -1
- package/src/routes/components/Icons/IconArrow.svelte +6 -1
- package/src/routes/components/Icons/IconIMDb.svelte +2 -1
- package/src/routes/components/Modal.svelte +12 -5
- package/src/routes/components/Rails/Rail.svelte +6 -2
- package/src/routes/components/Title.svelte +1 -4
- package/src/tests/lib/injections.test.js +16 -0
- package/src/tests/routes/components/Modal.test.js +16 -10
package/package.json
CHANGED
package/src/lib/injection.ts
CHANGED
|
@@ -209,6 +209,13 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
209
209
|
const valueIndex = element.innerHTML.indexOf(nodeContainingText.nodeValue)
|
|
210
210
|
const sentenceIndex = element.innerHTML.indexOf(injection.sentence)
|
|
211
211
|
|
|
212
|
+
// Sentences are often broken up by HTML elements. When that happens the index of the sentence can't be found in the HTML.
|
|
213
|
+
// In that case we can attempt to search for part of the sentence instead, and hope for the best. We grab the first 20 characters
|
|
214
|
+
// of the sentence and look for that instead. This will not work if sentence is broken up within those first characters,
|
|
215
|
+
// if the injections is right at the start of the sentence, or the element contains multiple matches for that same slice.
|
|
216
|
+
// But it's something to fall back on regardless.
|
|
217
|
+
const startOfSentenceIndex = sentenceIndex === -1 ? element.innerHTML.indexOf(injection.sentence.slice(0, 20)) : -1
|
|
218
|
+
|
|
212
219
|
// Starting from occurence happens when a sentence might include multiple occurences of the same phrase.
|
|
213
220
|
// We might still prefer the valueIndex if that is higher than this value.
|
|
214
221
|
|
|
@@ -217,7 +224,7 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
217
224
|
const indexOfOccurrence = getNumberOfOccurrencesInArray<LinkInjection>(foundInjections.slice(0, injectionIndex + 1), injection, ['title', 'sentence']) - 1
|
|
218
225
|
const indexInSentence = getIndexOfPhraseInElement(nodeContainingText.nodeValue, element, indexOfOccurrence)
|
|
219
226
|
|
|
220
|
-
replacementIndex = Math.max(valueIndex, indexInSentence, sentenceIndex, 0)
|
|
227
|
+
replacementIndex = Math.max(valueIndex, startOfSentenceIndex, indexInSentence, sentenceIndex, 0)
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
if (replacementIndex === -1) continue
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
interface Props {
|
|
3
3
|
direction?: 'left' | 'right'
|
|
4
|
+
title?: string
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
const { direction = 'right' }: Props = $props()
|
|
7
|
+
const { direction = 'right', title = '' }: Props = $props()
|
|
7
8
|
</script>
|
|
8
9
|
|
|
9
10
|
<svg class="{direction}" height="16px" viewBox="0 -960 960 960" width="16px">
|
|
11
|
+
{#if title}
|
|
12
|
+
<title>{title}</title>
|
|
13
|
+
{/if}
|
|
14
|
+
|
|
10
15
|
<path d="m288-96-68-68 316-316-316-316 68-68 384 384L288-96Z" fill="currentColor" />
|
|
11
16
|
</svg>
|
|
12
17
|
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<svg width={size} height={size} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
10
|
-
<
|
|
10
|
+
<title>IMDb rating</title>
|
|
11
|
+
<path d="M7 0.351929L9.163 4.72803L14 5.43408L10.5 8.8385L11.326 13.648L7 11.3761L2.674 13.648L3.5 8.8385L0 5.43408L4.837 4.72803L7 0.351929Z" fill="#F6C045"/>
|
|
11
12
|
</svg>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
closeButtonStyle?: 'shadow' | 'flat'
|
|
20
20
|
initialScrollPosition?: number
|
|
21
21
|
onscroll?: () => void
|
|
22
|
+
onclose?: () => void
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const {
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
closeButtonStyle = 'shadow',
|
|
30
31
|
initialScrollPosition = 0,
|
|
31
32
|
onscroll = () => null,
|
|
33
|
+
onclose = () => null,
|
|
32
34
|
}: Props = $props()
|
|
33
35
|
|
|
34
36
|
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
@@ -80,11 +82,16 @@
|
|
|
80
82
|
if (isMobile) return fly(node, { duration: 250, ...options })
|
|
81
83
|
return scale(node, { duration: 150, start: 0.85 })
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
function close() {
|
|
87
|
+
onclose()
|
|
88
|
+
destroyAllModals()
|
|
89
|
+
}
|
|
83
90
|
</script>
|
|
84
91
|
|
|
85
92
|
<svelte:window
|
|
86
|
-
onkeydown={({ key }) => { if (key === 'Escape')
|
|
87
|
-
onhashchange={
|
|
93
|
+
onkeydown={({ key }) => { if (key === 'Escape') close() }}
|
|
94
|
+
onhashchange={close}
|
|
88
95
|
bind:innerWidth={windowWidth} />
|
|
89
96
|
|
|
90
97
|
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
|
|
@@ -102,7 +109,7 @@
|
|
|
102
109
|
|
|
103
110
|
{#if isMobile}
|
|
104
111
|
<div class="swipe-handle" transition:scaleOrFly|global>
|
|
105
|
-
<SwipeHandle target={dialogElement!} onpassed={
|
|
112
|
+
<SwipeHandle target={dialogElement!} onpassed={close} />
|
|
106
113
|
</div>
|
|
107
114
|
{/if}
|
|
108
115
|
|
|
@@ -116,7 +123,7 @@
|
|
|
116
123
|
{/if}
|
|
117
124
|
|
|
118
125
|
<div class="close {closeButtonStyle}">
|
|
119
|
-
<RoundButton onclick={
|
|
126
|
+
<RoundButton onclick={close} aria-label="Close">
|
|
120
127
|
<IconClose />
|
|
121
128
|
</RoundButton>
|
|
122
129
|
</div>
|
|
@@ -126,7 +133,7 @@
|
|
|
126
133
|
|
|
127
134
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
128
135
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
129
|
-
<div class="backdrop" onclick={
|
|
136
|
+
<div class="backdrop" onclick={close}></div>
|
|
130
137
|
</div>
|
|
131
138
|
|
|
132
139
|
<style lang="scss">
|
|
@@ -22,11 +22,15 @@
|
|
|
22
22
|
|
|
23
23
|
{#snippet controls({ setIndex, currentIndex, reachedEnd })}
|
|
24
24
|
{#if currentIndex > 0}
|
|
25
|
-
<button class="arrow left" onclick={() => setIndex(currentIndex - 1)}
|
|
25
|
+
<button class="arrow left" onclick={() => setIndex(currentIndex - 1)} aria-label="Previous" aria-live="polite">
|
|
26
|
+
<IconArrow direction="left" title="Previous" />
|
|
27
|
+
</button>
|
|
26
28
|
{/if}
|
|
27
29
|
|
|
28
30
|
{#if !reachedEnd}
|
|
29
|
-
<button class="arrow right" onclick={() => setIndex(currentIndex + 1)}
|
|
31
|
+
<button class="arrow right" onclick={() => setIndex(currentIndex + 1)} aria-label="Next" aria-live="polite">
|
|
32
|
+
<IconArrow title="Next" />
|
|
33
|
+
</button>
|
|
30
34
|
{/if}
|
|
31
35
|
{/snippet}
|
|
32
36
|
</TinySlider>
|
|
@@ -57,10 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- Temporarily not available on production as there is not yet an API endpoint for either -->
|
|
59
59
|
{#if process.env.NODE_ENV !== 'production'}
|
|
60
|
-
|
|
61
|
-
<ParticipantsRail />
|
|
62
|
-
{/if}
|
|
63
|
-
|
|
60
|
+
<ParticipantsRail />
|
|
64
61
|
<SimilarRail {title} />
|
|
65
62
|
{/if}
|
|
66
63
|
</div>
|
|
@@ -296,6 +296,22 @@ describe('linkInjection.js', () => {
|
|
|
296
296
|
expect(document.querySelector('[data-playpilot-injection-key]')).toBeTruthy()
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
+
it('Should inject correctly when sentence is broken up by html and element contains multiple matches', () => {
|
|
300
|
+
document.body.innerHTML = '<p>This is some match. Some other sentence with a match. But this is yet another <em>sentence</em> with a match.</p>'
|
|
301
|
+
|
|
302
|
+
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
303
|
+
|
|
304
|
+
const injections = [
|
|
305
|
+
generateInjection('This is some match.', 'some match'),
|
|
306
|
+
generateInjection('But this is yet another sentence with a match.', 'a match'),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
injectLinksInDocument(elements, { aiInjections: injections, manualInjections: [] })
|
|
310
|
+
|
|
311
|
+
expect(document.body.innerHTML).toContain('Some other sentence with a match')
|
|
312
|
+
expect(document.querySelectorAll('[data-playpilot-injection-key]')).toHaveLength(2)
|
|
313
|
+
})
|
|
314
|
+
|
|
299
315
|
it('Should leave the text intact if no injections were found', () => {
|
|
300
316
|
document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
|
|
301
317
|
|
|
@@ -16,47 +16,53 @@ describe('Modal.svelte', () => {
|
|
|
16
16
|
vi.resetAllMocks()
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
+
const onclose = vi.fn()
|
|
19
20
|
const children = createRawSnippet(() => ({ render: () => '<div></div>' }))
|
|
20
21
|
|
|
21
|
-
it('Should fire given destroyAllModals
|
|
22
|
-
const { container } = render(Modal, { children })
|
|
22
|
+
it('Should fire given onclose and destroyAllModals functions when backdrop is clicked', async () => {
|
|
23
|
+
const { container } = render(Modal, { children, onclose })
|
|
23
24
|
|
|
24
25
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('.backdrop')))
|
|
25
26
|
|
|
27
|
+
expect(onclose).toHaveBeenCalled()
|
|
26
28
|
expect(destroyAllModals).toHaveBeenCalled()
|
|
27
29
|
})
|
|
28
30
|
|
|
29
|
-
it('Should fire destroyAllModals
|
|
30
|
-
const { container } = render(Modal, { children })
|
|
31
|
+
it('Should fire given onclose and destroyAllModals functions when close button is clicked', async () => {
|
|
32
|
+
const { container } = render(Modal, { children, onclose })
|
|
31
33
|
|
|
32
34
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('button')))
|
|
33
35
|
|
|
36
|
+
expect(onclose).toHaveBeenCalled()
|
|
34
37
|
expect(destroyAllModals).toHaveBeenCalled()
|
|
35
38
|
})
|
|
36
39
|
|
|
37
|
-
it('Should not fire given onclose
|
|
38
|
-
const { container } = render(Modal, { children })
|
|
40
|
+
it('Should not fire given onclose or destroyAllModals functions when dialog is clicked', async () => {
|
|
41
|
+
const { container } = render(Modal, { children, onclose })
|
|
39
42
|
|
|
40
43
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('.dialog')))
|
|
41
44
|
|
|
45
|
+
expect(onclose).not.toHaveBeenCalled()
|
|
42
46
|
expect(destroyAllModals).not.toHaveBeenCalled()
|
|
43
47
|
})
|
|
44
48
|
|
|
45
|
-
it('Should fire given destroyAllModals
|
|
46
|
-
render(Modal, { children })
|
|
49
|
+
it('Should fire given onclose and destroyAllModals functions when escape key is pressed', async () => {
|
|
50
|
+
render(Modal, { children, onclose })
|
|
47
51
|
|
|
48
52
|
fireEvent.keyDown(window, { key: 'Escape' })
|
|
49
53
|
|
|
54
|
+
expect(onclose).toHaveBeenCalled()
|
|
50
55
|
expect(destroyAllModals).toHaveBeenCalled()
|
|
51
56
|
})
|
|
52
57
|
|
|
53
|
-
it('Should not fire given onclose function when key other than escape key is pressed', async () => {
|
|
54
|
-
render(Modal, { children })
|
|
58
|
+
it('Should not fire given onclose or destroyAllModals function when key other than escape key is pressed', async () => {
|
|
59
|
+
render(Modal, { children, onclose })
|
|
55
60
|
|
|
56
61
|
fireEvent.keyDown(window, { key: 'a' })
|
|
57
62
|
fireEvent.keyDown(window, { key: 'Enter' })
|
|
58
63
|
fireEvent.keyDown(window, { key: 'Space' })
|
|
59
64
|
|
|
65
|
+
expect(onclose).not.toHaveBeenCalled()
|
|
60
66
|
expect(destroyAllModals).not.toHaveBeenCalled()
|
|
61
67
|
})
|
|
62
68
|
|