@playpilot/tpi 3.3.3 → 3.4.0-beta.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.
Files changed (35) hide show
  1. package/dist/link-injections.js +8 -8
  2. package/package.json +1 -1
  3. package/src/lib/api.ts +6 -9
  4. package/src/lib/hash.ts +4 -0
  5. package/src/lib/linkInjection.ts +15 -40
  6. package/src/lib/localization.ts +7 -7
  7. package/src/lib/scss/_mixins.scss +0 -13
  8. package/src/lib/scss/global.scss +0 -5
  9. package/src/lib/scss/variables.scss +1 -1
  10. package/src/lib/session.ts +78 -0
  11. package/src/lib/tracking.ts +1 -2
  12. package/src/lib/types/injection.d.ts +2 -0
  13. package/src/lib/types/session.d.ts +14 -0
  14. package/src/routes/+page.svelte +37 -7
  15. package/src/routes/components/AfterArticlePlaylinks.svelte +6 -5
  16. package/src/routes/components/Editorial/Editor.svelte +36 -26
  17. package/src/routes/components/Editorial/Session.svelte +97 -0
  18. package/src/routes/components/Modal.svelte +1 -3
  19. package/src/routes/components/Playlinks.svelte +4 -5
  20. package/src/routes/components/Popover.svelte +1 -3
  21. package/src/routes/components/Title.svelte +0 -5
  22. package/src/tests/lib/api.test.js +14 -14
  23. package/src/tests/lib/linkInjection.test.js +56 -51
  24. package/src/tests/lib/localization.test.js +0 -7
  25. package/src/tests/lib/session.test.js +95 -0
  26. package/src/tests/lib/tracking.test.js +0 -16
  27. package/src/tests/routes/+page.test.js +14 -4
  28. package/src/tests/routes/components/Editorial/Editor.test.js +17 -1
  29. package/src/tests/routes/components/Editorial/EditorItem.test.js +7 -7
  30. package/src/tests/routes/components/Editorial/Session.test.js +80 -0
  31. package/src/tests/setup.js +23 -5
  32. package/src/lib/event.ts +0 -6
  33. package/src/lib/viewTransition.ts +0 -25
  34. package/src/tests/lib/event.test.js +0 -22
  35. package/src/tests/lib/viewTransition.test.js +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.3.3",
3
+ "version": "3.4.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/api.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
2
2
  import { apiBaseUrl } from './constants'
3
- import { stringToHash } from './hash'
3
+ import { generateRandomHash, stringToHash } from './hash'
4
4
  import { getLanguage } from './localization'
5
5
  import { getPageMetaData } from './meta'
6
6
  import type { ConfigResponse } from './types/config'
@@ -11,13 +11,13 @@ let pollTimeout: ReturnType<typeof setTimeout> | null = null
11
11
 
12
12
  /**
13
13
  * Fetch link injections for a URL.
14
- * @param url URL of the given article
15
14
  * @param html HTML to be crawled
16
15
  * @param options
16
+ * @param [options.url] URL of the given article
17
17
  * @param [options.hash] unique key to identify the HTML
18
18
  * @param [options.params] Any rest params to include in the request body
19
19
  */
20
- export async function fetchLinkInjections(url: string, html: string, { hash = stringToHash(html), params = {} }: { hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
20
+ export async function fetchLinkInjections(html: string, { url = getFullUrlPath(), hash = stringToHash(html), params = {} }: { url?: string, hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
21
21
  const headers = new Headers({ 'Content-Type': 'application/json' })
22
22
  const apiToken = getApiToken()
23
23
  const isEditorialMode = isEditorialModeEnabled() ? await authorize() : false
@@ -48,16 +48,13 @@ export async function fetchLinkInjections(url: string, html: string, { hash = st
48
48
  /**
49
49
  * Link injections take a while to be ready. During this time we poll the endpoint until it returns the result we want.
50
50
  * The results return `injections_ready=false` while the injections are not yet ready.
51
- * @param url URL of the given article
52
51
  * @param html HTML to be crawled
53
52
  */
54
53
  export async function pollLinkInjections(
55
- url: string,
56
54
  html: string,
57
55
  { requireCompletedResult = false, pollInterval = 3000, maxTries = 600, onpoll = () => null }:
58
56
  { requireCompletedResult?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {}
59
57
  ): Promise<LinkInjectionResponse> {
60
- let hash = stringToHash(html)
61
58
  let currentTry = 0
62
59
 
63
60
  // Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
@@ -72,7 +69,7 @@ export async function pollLinkInjections(
72
69
  const poll = async (resolve: Function, reject: Function): Promise<void> => {
73
70
  let response
74
71
  try {
75
- response = await fetchLinkInjections(url, html, { hash })
72
+ response = await fetchLinkInjections(html)
76
73
 
77
74
  if (requireCompletedResult && (response.automation_enabled && response.ai_running)) throw new Error
78
75
 
@@ -117,7 +114,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
117
114
  removed: !!linkInjection.removed
118
115
  }))
119
116
 
120
- const response = await fetchLinkInjections(getFullUrlPath(), html, {
117
+ const response = await fetchLinkInjections(html, {
121
118
  params: {
122
119
  private_token: getAuthToken(),
123
120
  link_injections: newLinkInjections,
@@ -166,7 +163,7 @@ function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
166
163
  * @returns Random string preprending with title sid.
167
164
  */
168
165
  export function generateInjectionKey(sid: string): string {
169
- return sid + '-' + (Math.random() + 1).toString(36).substring(7)
166
+ return sid + '-' + generateRandomHash()
170
167
  }
171
168
 
172
169
  export function getApiToken(): string | undefined {
package/src/lib/hash.ts CHANGED
@@ -12,3 +12,7 @@ export function stringToHash(string: string): string {
12
12
 
13
13
  return hash.toString(16)
14
14
  }
15
+
16
+ export function generateRandomHash() {
17
+ return (Math.random() + 1).toString(36).substring(7)
18
+ }
@@ -1,12 +1,9 @@
1
1
  import { mount, unmount } from 'svelte'
2
- import TitleModal from '../routes/components/TitleModal.svelte'
3
2
  import TitlePopover from '../routes/components/TitlePopover.svelte'
4
3
  import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
5
4
  import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
6
5
  import { getLargestValueInArray } from './array'
7
6
  import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
8
- import { isHoldingSpecialKey } from './event'
9
- import { playFallbackViewTransition } from './viewTransition'
10
7
 
11
8
  const keyDataAttribute = 'data-playpilot-injection-key'
12
9
  const keySelector = `[${keyDataAttribute}]`
@@ -15,7 +12,6 @@ const activePopovers: Record<string, { injection: LinkInjection; component: obje
15
12
 
16
13
  let currentlyHoveredInjection: EventTarget | null = null
17
14
  let afterArticlePlaylinkInsertedComponent: object | null = null
18
- let activeModalInsertedComponent: object | null = null
19
15
 
20
16
  /**
21
17
  * Return a list of all valid text containing elements that may get injected into.
@@ -32,7 +28,7 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
32
28
  if (validElements.includes(element)) continue
33
29
 
34
30
  // Ignore links, buttons, and headers
35
- if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|H[1-6])$/.test(element.tagName)) continue
31
+ if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|H[1-6])$/.test(element.tagName)) continue
36
32
 
37
33
  // Check if this element has a direct text node
38
34
  const hasTextNode = Array.from(element.childNodes).some(
@@ -94,10 +90,9 @@ export function getLinkInjectionsParentElement(): HTMLElement {
94
90
  * Replace all found injections within all given elements on the page
95
91
  * @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
96
92
  */
97
- export function injectLinksInDocument(elements: HTMLElement[], injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
98
- clearLinkInjections()
99
-
93
+ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInjection: LinkInjection) => void, injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
100
94
  const mergedInjections = mergeInjectionTypes(injections)
95
+
101
96
  if (!mergedInjections) return []
102
97
 
103
98
  // Find injection in text content of all elements together, ignore potential HTML elements.
@@ -167,11 +162,11 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
167
162
  }
168
163
  }
169
164
 
170
- addLinkInjectionEventListeners(validInjections)
165
+ addLinkInjectionEventListeners(validInjections, onclick)
171
166
  addCSSVariablesToLinks()
172
167
 
173
168
  const afterArticleInjections = filterInvalidAfterArticleInjections(mergedInjections)
174
- if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections)
169
+ if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections, onclick)
175
170
 
176
171
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
177
172
  // Favour manual injections over AI injections
@@ -224,11 +219,9 @@ function addCSSVariablesToLinks(): void {
224
219
  /**
225
220
  * Add event listeners to all injected links. These events are for both the popover and the modal.
226
221
  */
227
- function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
222
+ function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (injection: LinkInjection) => void): void {
228
223
  // Open modal on click
229
224
  window.addEventListener('click', (event) => {
230
- if (isHoldingSpecialKey(event)) return
231
-
232
225
  const target = event.target as HTMLElement | null
233
226
  if (!target?.parentElement) return
234
227
 
@@ -238,12 +231,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
238
231
  const injection = injections.find(injection => key === injection.key)
239
232
  if (!injection) return
240
233
 
241
- event.preventDefault()
242
-
243
- playFallbackViewTransition(() => {
244
- destroyLinkPopover(injection, false)
245
- openLinkModal(event, injection)
246
- }, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
234
+ openLinkModal(event, injection, onclick)
247
235
  })
248
236
 
249
237
  const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
@@ -262,26 +250,16 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
262
250
  }
263
251
 
264
252
  /**
265
- * Open modal for the corresponding injection by mounting the component and saving it to a variable.
266
- * Ignore clicks that used modifier keys or that were not left click.
253
+ * Prevent default click and run onclick from parent. Ignore clicks that used modifier keys or that were not left click.
254
+ * The event is not fired when the click happens from inside a popover.
267
255
  */
268
- function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
269
- if (isHoldingSpecialKey(event)) return
270
- if (activeModalInsertedComponent) return
256
+ function openLinkModal(event: MouseEvent, injection: LinkInjection, onclick: (injection: LinkInjection) => void): void {
257
+ if (event.ctrlKey || event.metaKey || event.button !== 0) return
271
258
 
272
259
  event.preventDefault()
273
260
 
274
- activeModalInsertedComponent = mount(TitleModal, { target: document.body, props: { title: injection.title_details!, onclose: destroyLinkModal } })
275
- }
276
-
277
- /**
278
- * Unmount the modal, removing it from the dom
279
- */
280
- function destroyLinkModal(outro: boolean = true): void {
281
- if (!activeModalInsertedComponent) return
282
-
283
- unmount(activeModalInsertedComponent, { outro })
284
- activeModalInsertedComponent = null
261
+ onclick(injection)
262
+ destroyLinkPopover(injection)
285
263
  }
286
264
 
287
265
  /**
@@ -325,8 +303,7 @@ function destroyLinkPopover(injection: LinkInjection, outro: boolean = true) {
325
303
  * The config object contains a selector option as well as a position. This way a selector can be given and you can
326
304
  * choose to insert the after article before or after the given element.
327
305
  */
328
- export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[]): void {
329
- if (afterArticlePlaylinkInsertedComponent) return
306
+ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[], onclickmodal: (linkInjection: LinkInjection) => void) {
330
307
  if (!injections.length) return
331
308
 
332
309
  const target = document.createElement('div')
@@ -337,7 +314,7 @@ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections:
337
314
  target.dataset.playpilotAfterArticlePlaylinks = 'true'
338
315
  insertElement.insertAdjacentElement(insertPosition, target)
339
316
 
340
- afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal: (event, injection) => openLinkModal(event, injection) } })
317
+ afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal } })
341
318
  }
342
319
 
343
320
  function clearAfterArticlePlaylinks(): void {
@@ -360,7 +337,6 @@ export function clearLinkInjections(): void {
360
337
  Object.values(activePopovers).forEach(({ injection }) => destroyLinkPopover(injection, false))
361
338
 
362
339
  clearAfterArticlePlaylinks()
363
- destroyLinkModal()
364
340
  }
365
341
 
366
342
  /**
@@ -446,4 +422,3 @@ export function isAvailableAsManualInjection(injection: LinkInjection, injection
446
422
  export function isEquivalentInjection(injection1: LinkInjection, injection2: LinkInjection): boolean {
447
423
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
448
424
  }
449
-
@@ -10,7 +10,11 @@ export function t(key: string, language: LanguageCode = getLanguage()): string {
10
10
  return translations[key]?.[language] || key
11
11
  }
12
12
 
13
+ /**
14
+ * @returns {LanguageCode}
15
+ */
13
16
  export function getLanguage(): LanguageCode {
17
+ // @ts-ignore
14
18
  const configLanguage = window.PlayPilotLinkInjections?.language as LanguageCode | undefined
15
19
  const languageCodes = Object.values(Language) as LanguageCode[]
16
20
 
@@ -20,14 +24,10 @@ export function getLanguage(): LanguageCode {
20
24
  console.warn(`PlayPilot Link Injections: ${configLanguage} is not an accepted language`)
21
25
  }
22
26
 
23
- const documentLanguage = document.querySelector('html')?.getAttribute('lang')
24
-
25
- if (documentLanguage) {
26
- if (languageCodes.includes(documentLanguage as LanguageCode)) return documentLanguage as LanguageCode
27
+ const documentLanguage = document.querySelector('html')?.getAttribute('lang') as LanguageCode | undefined
27
28
 
28
- // Match against shorthand language codes like da -> da-DK
29
- const matched = languageCodes.find(code => code.toLowerCase().startsWith(documentLanguage + '-'))
30
- if (matched) return matched
29
+ if (documentLanguage && languageCodes.includes(documentLanguage)) {
30
+ return documentLanguage
31
31
  }
32
32
 
33
33
  return Language.English
@@ -6,16 +6,3 @@
6
6
  fill: none;
7
7
  }
8
8
  }
9
-
10
- @mixin global-outlines() {
11
- :global(a),
12
- :global(button),
13
- :global(input) {
14
- transition: outline-offset 100ms;
15
-
16
- &:focus-visible {
17
- outline: 2px solid white;
18
- outline-offset: 2px;
19
- }
20
- }
21
- }
@@ -73,8 +73,3 @@
73
73
  background-color: var(--playpilot-injection-background-color-hover);
74
74
  }
75
75
  }
76
-
77
- ::view-transition-old(playpilot-title-content),
78
- ::view-transition-new(playpilot-title-content) {
79
- height: 100%;
80
- }
@@ -8,7 +8,7 @@
8
8
  --playpilot-content: #354367;
9
9
  --playpilot-content-light: #4b5b82;
10
10
  --playpilot-green: #53bca0;
11
- --playpilot-font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
11
+ --playpilot-font-family: "Poppins", sans-serif;
12
12
  --playpilot-text-color: #fff;
13
13
  --playpilot-text-color-alt: #c8d4de;
14
14
  --playpilot-shadow:
@@ -0,0 +1,78 @@
1
+ import { fetchLinkInjections } from "./api"
2
+ import { generateRandomHash } from "./hash"
3
+ import type { SessionResponse } from "./types/session"
4
+
5
+ export const sessionKey = 'PlayPilotEditorialSessionId'
6
+ export const sessionPollPeriodMilliseconds = 5000
7
+ export const sessionPeriodMilliseconds = sessionPollPeriodMilliseconds * 2
8
+
9
+ /**
10
+ * Fetch link injections and infer if the current session is allowed to edit. If they are, we make an additional request
11
+ * where we save the session.
12
+ * Along with that, we also return automation_enabled and injections_enabled as they are used to update the state of
13
+ * the current editing session.
14
+ * @param html Despite not being used, the request still requires a valid html string
15
+ */
16
+ export async function fetchAsSession(html: string): Promise<SessionResponse> {
17
+ const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(html)
18
+
19
+ const isCurrentlyAllowToEdit = isAllowedToEdit(session_id || null, session_last_ping || null)
20
+
21
+ const response = {
22
+ automation_enabled,
23
+ injections_enabled,
24
+ session_id,
25
+ session_last_ping,
26
+ }
27
+
28
+ if (!isCurrentlyAllowToEdit) return response
29
+
30
+ return await saveCurrentSession(html)
31
+ }
32
+
33
+ /**
34
+ * Save the current users session id as the currently active session. This is always completed regardless of if the user
35
+ * is the current owner of the session, so check that first if necessary!
36
+ * @param html Despite not being used, the request still requires a valid html string
37
+ */
38
+ export async function saveCurrentSession(html: string): Promise<SessionResponse> {
39
+ const sessionId = getSessionId()
40
+ const now = new Date(Date.now()).toISOString()
41
+
42
+ const params = {
43
+ session_id: sessionId,
44
+ session_last_ping: now.toString(),
45
+ }
46
+
47
+ const { automation_enabled, injections_enabled } = await fetchLinkInjections(html, { params })
48
+
49
+ return {
50
+ automation_enabled,
51
+ injections_enabled,
52
+ ...params,
53
+ }
54
+ }
55
+
56
+ export function isAllowedToEdit(responseSessionId: string | null, responseSessionLastPing: string | null): boolean {
57
+ if (!responseSessionId || !responseSessionLastPing) return true
58
+
59
+ const now = Date.now()
60
+ const responseSessionLastPingDate = new Date(responseSessionLastPing)
61
+ const currentSessionId = getSessionId()
62
+
63
+ // Last session ping was older than given period, so the previous session has likely ended
64
+ if (now - responseSessionLastPingDate.getTime() > sessionPeriodMilliseconds) return true
65
+
66
+ return currentSessionId === responseSessionId
67
+ }
68
+
69
+ export function getSessionId(): string {
70
+ return sessionStorage.getItem(sessionKey) || setSessionId()
71
+ }
72
+
73
+ export function setSessionId(): string {
74
+ const id = sessionStorage.getItem(sessionKey) || generateRandomHash()
75
+ sessionStorage.setItem(sessionKey, id)
76
+
77
+ return id
78
+ }
@@ -1,7 +1,6 @@
1
1
  import { get } from "svelte/store"
2
2
  import { currentDomainSid, currentOrganizationSid } from "./stores/organization"
3
3
  import type { TitleData } from "./types/title"
4
- import { getFullUrlPath } from "./url"
5
4
 
6
5
  const baseUrl = 'https://insights.playpilot.net'
7
6
 
@@ -23,7 +22,7 @@ export async function track(event: string, title: TitleData | null = null, paylo
23
22
  }
24
23
  }
25
24
 
26
- payload.url = getFullUrlPath()
25
+ payload.url = window.location.href
27
26
  payload.organization_sid = get(currentOrganizationSid)
28
27
  payload.domain_sid = get(currentDomainSid)
29
28
 
@@ -32,6 +32,8 @@ export type LinkInjectionResponse = {
32
32
  link_injections: LinkInjection[] | null
33
33
  organization_sid?: string
34
34
  domain_sid?: string
35
+ session_id?: string | null
36
+ session_last_ping?: string | null
35
37
  }
36
38
 
37
39
  export type LinkInjectionTypes = {
@@ -0,0 +1,14 @@
1
+ import type { LinkInjectionResponse } from "./injection"
2
+
3
+ export type SessionResponse = Omit<
4
+ LinkInjectionResponse,
5
+ 'injections_ready' |
6
+ 'page_updated' |
7
+ 'ai_running' |
8
+ 'ai_progress_message' |
9
+ 'ai_progress_percentage' |
10
+ 'ai_injections' |
11
+ 'organization_sid' |
12
+ 'domain_sid' |
13
+ 'link_injections'
14
+ >
@@ -7,6 +7,7 @@
7
7
  import { isCrawler } from '$lib/crawler'
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
9
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
10
+ import TitleModal from './components/TitleModal.svelte'
10
11
  import Editor from './components/Editorial/Editor.svelte'
11
12
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
12
13
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
@@ -16,12 +17,12 @@
16
17
  const htmlString = elements.map(p => p.outerHTML).join('')
17
18
 
18
19
  let response: LinkInjectionResponse | null = $state(null)
20
+ let activeInjection: LinkInjection | null = $state(null)
19
21
  let isEditorialMode = $state(isEditorialModeEnabled())
20
22
  let hasAuthToken = $state(!!getAuthToken())
21
23
  let authorized = $state(false)
22
24
  let loading = $state(true)
23
25
  let linkInjections: LinkInjection[] = $state([])
24
- let editor = $state()
25
26
 
26
27
  // @ts-ignore It's ok if the response is empty
27
28
  const { ai_injections: aiInjections = [], link_injections: manualInjections = [] } = $derived(response || {})
@@ -47,10 +48,9 @@
47
48
 
48
49
  if (isEditorialMode) authorized = await authorize()
49
50
 
50
- const url = getFullUrlPath()
51
-
52
51
  try {
53
52
  const config = await fetchConfig()
53
+ const url = getFullUrlPath()
54
54
 
55
55
  // URL was marked as being excluded, we stop injections here unless we're in editorial mode.
56
56
  if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
@@ -64,7 +64,7 @@
64
64
 
65
65
  // Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
66
66
  // by users who are not in the editorial view.
67
- response = await pollLinkInjections(url, htmlString, { maxTries: 1 })
67
+ response = await pollLinkInjections(htmlString, { maxTries: 1 })
68
68
 
69
69
  loading = false
70
70
 
@@ -84,18 +84,28 @@
84
84
  // so as not to suddenly insert new links while a user is reading the article.
85
85
  if (!isEditorialMode) return
86
86
 
87
- response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
87
+ response = await pollLinkInjections(htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
88
88
  inject({ aiInjections, manualInjections })
89
89
  }
90
90
 
91
91
  function rerender(): void {
92
+ clearLinkInjections()
92
93
  inject(separateLinkInjectionTypes(linkInjections))
93
94
  }
94
95
 
96
+ function reinitializeEditor(): void {
97
+ isEditorialMode = false
98
+
99
+ requestAnimationFrame(() => {
100
+ isEditorialMode = true
101
+ initialize()
102
+ })
103
+ }
104
+
95
105
  function inject(injections: LinkInjectionTypes = { aiInjections, manualInjections }): void {
96
106
  // Get filtered injections as they are shown on the page.
97
107
  // Only update state if it they are different from current injections.
98
- const filteredInjections = injectLinksInDocument(elements, injections)
108
+ const filteredInjections = injectLinksInDocument(elements, setTarget, injections)
99
109
  if (JSON.stringify(filteredInjections) !== JSON.stringify(linkInjections)) linkInjections = filteredInjections
100
110
 
101
111
  const successfulInjections = filteredInjections.filter(i => !i.failed)
@@ -109,6 +119,10 @@
109
119
  })
110
120
  }
111
121
 
122
+ function setTarget(injection: LinkInjection): void {
123
+ activeInjection = injection
124
+ }
125
+
112
126
  function openEditorialMode() {
113
127
  isEditorialMode = true
114
128
  setEditorialParamInUrl()
@@ -127,9 +141,9 @@
127
141
  {#if isEditorialMode && authorized}
128
142
  <Editor
129
143
  bind:linkInjections
130
- bind:this={editor}
131
144
  {htmlString}
132
145
  {loading}
146
+ onreinitialize={reinitializeEditor}
133
147
  injectionsEnabled={response?.injections_enabled}
134
148
  aiStatus={{
135
149
  automationEnabled: response?.automation_enabled,
@@ -138,9 +152,14 @@
138
152
  percentage: response?.ai_progress_percentage,
139
153
  }} />
140
154
  {/if}
155
+
156
+ {#if activeInjection && activeInjection.title_details}
157
+ <TitleModal title={activeInjection.title_details} onclose={() => activeInjection = null} />
158
+ {/if}
141
159
  </div>
142
160
 
143
161
  <style lang="scss">
162
+ @import url('https://fonts.googleapis.com/css?family=Poppins:400,600,700');
144
163
  @import url('$lib/scss/variables.scss');
145
164
  @import url('$lib/scss/global.scss');
146
165
 
@@ -148,5 +167,16 @@
148
167
  :global(*) {
149
168
  box-sizing: border-box;
150
169
  }
170
+
171
+ :global(.playpilot-link-injections button),
172
+ :global(.playpilot-link-injections input) {
173
+ transition: outline-offset 100ms;
174
+
175
+ &:focus-visible,
176
+ &:focus-visible {
177
+ outline: 2px solid white;
178
+ outline-offset: 2px;
179
+ }
180
+ }
151
181
  }
152
182
  </style>
@@ -8,10 +8,11 @@
8
8
  interface Props {
9
9
  linkInjections: LinkInjection[],
10
10
  // eslint-disable-next-line no-unused-vars
11
- onclickmodal?: (event: MouseEvent, linkInjection: LinkInjection) => void
11
+ onclickmodal?: (linkInjection: LinkInjection) => void
12
12
  }
13
13
 
14
- const { linkInjections, onclickmodal = () => null }: Props = $props()
14
+ // eslint-disable-next-line no-unused-vars
15
+ const { linkInjections, onclickmodal = (linkInjection) => null }: Props = $props()
15
16
 
16
17
  function onclick(title: TitleData, playlink: string): void {
17
18
  track(TrackingEvent.AfterArticlePlaylinkClick, title, { playlink })
@@ -20,9 +21,9 @@
20
21
  /**
21
22
  * Open a modal for the given injection and track the click
22
23
  */
23
- function openModal(event: MouseEvent, title: TitleData, linkInjection: LinkInjection): void {
24
+ function openModal(title: TitleData, linkInjection: LinkInjection): void {
24
25
  track(TrackingEvent.AfterArticleModalButtonClick, title)
25
- onclickmodal(event, linkInjection)
26
+ onclickmodal(linkInjection)
26
27
  }
27
28
  </script>
28
29
 
@@ -38,7 +39,7 @@
38
39
  "{title}" {t('Is Available To Stream')}
39
40
 
40
41
  <span>
41
- <button onclick={(event) => openModal(event, title_details as TitleData, linkInjection)}>
42
+ <button onclick={() => openModal(title_details as TitleData, linkInjection)}>
42
43
  {t('View Streaming Options')}
43
44
  </button>
44
45
  </span>