@playpilot/tpi 2.0.4 → 3.0.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 (63) hide show
  1. package/dist/link-injections.js +5 -5
  2. package/eslint.config.js +20 -0
  3. package/package.json +5 -2
  4. package/src/lib/{api.js → api.ts} +29 -39
  5. package/src/lib/{array.js → array.ts} +1 -3
  6. package/src/lib/{auth.js → auth.ts} +8 -10
  7. package/src/lib/{fakeData.js → fakeData.ts} +7 -6
  8. package/src/lib/{hash.js → hash.ts} +2 -3
  9. package/src/lib/{html.js → html.ts} +2 -4
  10. package/src/lib/{linkInjection.js → linkInjection.ts} +41 -85
  11. package/src/lib/{localization.js → localization.ts} +10 -12
  12. package/src/lib/{meta.js → meta.ts} +8 -18
  13. package/src/lib/{playlink.js → playlink.ts} +4 -5
  14. package/src/lib/{search.js → search.ts} +2 -3
  15. package/src/lib/{text.js → text.ts} +7 -19
  16. package/src/lib/{tracking.js → tracking.ts} +6 -4
  17. package/src/lib/types/global.d.ts +13 -0
  18. package/src/lib/types/injection.d.ts +41 -0
  19. package/src/lib/types/language.d.ts +1 -0
  20. package/src/lib/types/participant.d.ts +13 -0
  21. package/src/lib/types/playlink.d.ts +11 -0
  22. package/src/lib/types/position.d.ts +6 -0
  23. package/src/lib/types/title.d.ts +23 -0
  24. package/src/lib/types/user.d.ts +7 -0
  25. package/src/lib/{url.js → url.ts} +1 -2
  26. package/src/routes/+layout.svelte +3 -3
  27. package/src/routes/+page.svelte +12 -17
  28. package/src/routes/components/AfterArticlePlaylinks.svelte +14 -13
  29. package/src/routes/components/ContextMenu.svelte +11 -7
  30. package/src/routes/components/Description.svelte +9 -4
  31. package/src/routes/components/Editorial/AIIndicator.svelte +10 -7
  32. package/src/routes/components/Editorial/Alert.svelte +8 -3
  33. package/src/routes/components/Editorial/DragHandle.svelte +18 -26
  34. package/src/routes/components/Editorial/Editor.svelte +24 -23
  35. package/src/routes/components/Editorial/EditorItem.svelte +20 -18
  36. package/src/routes/components/Editorial/ManualInjection.svelte +16 -17
  37. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +8 -4
  38. package/src/routes/components/Editorial/Search/TitleSearch.svelte +13 -17
  39. package/src/routes/components/Editorial/Switch.svelte +14 -4
  40. package/src/routes/components/Editorial/TextInput.svelte +10 -3
  41. package/src/routes/components/Genres.svelte +8 -5
  42. package/src/routes/components/Icons/IconAlign.svelte +8 -3
  43. package/src/routes/components/Icons/IconChevron.svelte +6 -3
  44. package/src/routes/components/Icons/IconEnlarge.svelte +6 -3
  45. package/src/routes/components/Modal.svelte +11 -10
  46. package/src/routes/components/Participants.svelte +8 -3
  47. package/src/routes/components/Playlinks.svelte +11 -7
  48. package/src/routes/components/Popover.svelte +11 -10
  49. package/src/routes/components/RoundButton.svelte +11 -9
  50. package/src/routes/components/SkeletonText.svelte +8 -3
  51. package/src/routes/components/Title.svelte +9 -3
  52. package/src/routes/components/TitleModal.svelte +9 -4
  53. package/src/routes/components/TitlePopover.svelte +7 -3
  54. package/src/tests/lib/api.test.js +22 -0
  55. package/src/tests/routes/components/Genres.test.js +1 -1
  56. package/src/lib/index.js +0 -1
  57. package/src/typedefs.js +0 -95
  58. /package/src/lib/{constants.js → constants.ts} +0 -0
  59. /package/src/lib/{genres.json → data/genres.json} +0 -0
  60. /package/src/lib/data/{translations.js → translations.ts} +0 -0
  61. /package/src/lib/enums/{Language.js → Language.ts} +0 -0
  62. /package/src/lib/enums/{TrackingEvent.js → TrackingEvent.ts} +0 -0
  63. /package/{jsconfig.json → tsconfig.json} +0 -0
package/eslint.config.js CHANGED
@@ -2,6 +2,9 @@ import prettier from 'eslint-config-prettier'
2
2
  import js from '@eslint/js'
3
3
  import svelte from 'eslint-plugin-svelte'
4
4
  import globals from 'globals'
5
+ import tseslint from '@typescript-eslint/eslint-plugin'
6
+ import tsParser from '@typescript-eslint/parser'
7
+ import ts from 'typescript-eslint'
5
8
 
6
9
  /** @type {import('eslint').Linter.Config[]} */
7
10
  export default [
@@ -16,12 +19,29 @@ export default [
16
19
  ...globals.node,
17
20
  PlayPilotLinkInjections: 'writable',
18
21
  },
22
+ parserOptions: {
23
+ parser: tsParser,
24
+ ecmaVersion: 2021,
25
+ sourceType: 'module',
26
+ },
27
+ },
28
+ },
29
+ {
30
+ files: ['**/*.svelte'],
31
+
32
+ languageOptions: {
33
+ parserOptions: {
34
+ parser: ts.parser,
35
+ },
19
36
  },
20
37
  },
21
38
  {
22
39
  ignores: ['build/', '.svelte-kit/', 'dist/'],
23
40
  },
24
41
  {
42
+ plugins: {
43
+ '@typescript-eslint': tseslint,
44
+ },
25
45
  rules: {
26
46
  semi: ['error', 'never'],
27
47
  quotes: ['error', 'single'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "2.0.4",
3
+ "version": "3.0.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -18,7 +18,9 @@
18
18
  "@sveltejs/kit": "^2.0.0",
19
19
  "@sveltejs/vite-plugin-svelte": "^4.0.0",
20
20
  "@testing-library/svelte": "^5.2.6",
21
- "eslint": "^9.7.0",
21
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
22
+ "@typescript-eslint/parser": "^8.32.1",
23
+ "eslint": "^9.27.0",
22
24
  "eslint-config-prettier": "^9.1.0",
23
25
  "eslint-plugin-svelte": "^2.36.0",
24
26
  "globals": "^15.0.0",
@@ -30,6 +32,7 @@
30
32
  "svelte-check": "^4.0.0",
31
33
  "svelte-preprocess": "^6.0.3",
32
34
  "typescript": "^5.0.0",
35
+ "typescript-eslint": "^8.32.1",
33
36
  "vite": "^5.0.3",
34
37
  "vite-plugin-css-injected-by-js": "^3.5.2",
35
38
  "vitest": "^2.1.8"
@@ -1,31 +1,32 @@
1
1
  import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
2
2
  import { apiBaseUrl } from './constants'
3
3
  import { stringToHash } from './hash'
4
+ import { getLanguage } from './localization'
4
5
  import { getPageMetaData } from './meta'
6
+ import type { LinkInjectionResponse, LinkInjection } from './types/injection'
5
7
  import { getFullUrlPath } from './url'
6
8
 
7
- /** @type {NodeJS.Timeout | number | null} */
8
- let pollTimeout = null
9
+ let pollTimeout: ReturnType<typeof setTimeout> | null = null
9
10
 
10
11
  /**
11
12
  * Fetch link injections for a URL.
12
- * @param {string} url URL of the given article
13
- * @param {string} html HTML to be crawled
14
- * @param {object} options
15
- * @param {boolean} [options.automation] Enable automation, disable when inserting into editorial
16
- * @param {string} [options.hash] unique key to identify the HTML
17
- * @param {object} [options.params] Any rest params to include in the request body
18
- * @returns {Promise<LinkInjectionResponse>}
13
+ * @param url URL of the given article
14
+ * @param html HTML to be crawled
15
+ * @param options
16
+ * @param [options.automation] Enable automation, disable when inserting into editorial
17
+ * @param [options.hash] unique key to identify the HTML
18
+ * @param [options.params] Any rest params to include in the request body
19
19
  */
20
- export async function fetchLinkInjections(url, html, { hash = stringToHash(html), params = {} } = {}) {
20
+ export async function fetchLinkInjections(url: string, html: string, { hash = stringToHash(html), params = {} }: { automation?: boolean; 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
24
+ const language = getLanguage()
24
25
  const metadata = getPageMetaData()
25
26
 
26
27
  if (!apiToken) throw new Error('No token was provided')
27
28
 
28
- const response = await fetch(apiBaseUrl + `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}`, {
29
+ const response = await fetch(apiBaseUrl + `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`, {
29
30
  headers,
30
31
  method: 'POST',
31
32
  body: JSON.stringify(({
@@ -47,11 +48,10 @@ export async function fetchLinkInjections(url, html, { hash = stringToHash(html)
47
48
  /**
48
49
  * Link injections take a while to be ready. During this time we poll the endpoint until it returns the result we want.
49
50
  * The results return `injections_ready=false` while the injections are not yet ready.
50
- * @param {string} url URL of the given article
51
- * @param {string} html HTML to be crawled
52
- * @returns {Promise<LinkInjectionResponse>}
51
+ * @param url URL of the given article
52
+ * @param html HTML to be crawled
53
53
  */
54
- export async function pollLinkInjections(url, html, { requireCompletedResult = false, pollInterval = 3000, maxTries = 600 } = {}) {
54
+ export async function pollLinkInjections(url: string, html: string, { requireCompletedResult = false, pollInterval = 3000, maxTries = 600 } = {}): Promise<LinkInjectionResponse> {
55
55
  let hash = stringToHash(html)
56
56
  let currentTry = 0
57
57
 
@@ -61,11 +61,10 @@ export async function pollLinkInjections(url, html, { requireCompletedResult = f
61
61
 
62
62
  /**
63
63
  * Polls the endpoint recursively until it is ready.
64
- * @param {Function} resolve Injections are ready and returned as expected
65
- * @param {Function} reject Injections are not yet ready
66
- * @returns {Promise<void>}
64
+ * @param resolve Injections are ready and returned as expected
65
+ * @param reject Injections are not yet ready
67
66
  */
68
- const poll = async (resolve, reject) => {
67
+ const poll = async (resolve: Function, reject: Function): Promise<void> => {
69
68
  try {
70
69
  const response = await fetchLinkInjections(url, html, { hash })
71
70
 
@@ -92,19 +91,13 @@ export async function pollLinkInjections(url, html, { requireCompletedResult = f
92
91
  return new Promise(poll)
93
92
  }
94
93
 
95
- /**
96
- * @param {LinkInjection[]} linkInjections
97
- * @param {string} html HTML to be crawled
98
- * @returns {Promise<LinkInjection[]>}
99
- */
100
- export async function saveLinkInjections(linkInjections, html) {
101
- // @ts-ignore
94
+ export async function saveLinkInjections(linkInjections: LinkInjection[], html: string): Promise<LinkInjection[]> {
102
95
  const selector = window.PlayPilotLinkInjections?.selector
103
96
 
104
97
  // Only save manual injections, AI injections should be left intact.
105
- const filteredLinkInjections = linkInjections.filter(i => i.manual)
98
+ const filteredLinkInjections = linkInjections.filter((i: LinkInjection) => i.manual)
106
99
 
107
- const newLinkInjections = filteredLinkInjections.map((/** @type {any} */linkInjection) => ({
100
+ const newLinkInjections = filteredLinkInjections.map((linkInjection: LinkInjection) => ({
108
101
  sid: linkInjection.sid,
109
102
  title: linkInjection.title,
110
103
  sentence: linkInjection.sentence,
@@ -113,7 +106,7 @@ export async function saveLinkInjections(linkInjections, html) {
113
106
  after_article_style: linkInjection.after_article_style || null,
114
107
  in_text: linkInjection.in_text ?? true,
115
108
  inactive: !!linkInjection.inactive,
116
- removed: !!linkInjection.removed,
109
+ removed: !!linkInjection.removed
117
110
  }))
118
111
 
119
112
  const response = await fetchLinkInjections(getFullUrlPath(), html, {
@@ -133,26 +126,23 @@ export async function saveLinkInjections(linkInjections, html) {
133
126
  /**
134
127
  * Insert random keys into link injections. These are used to identify the links on the page.
135
128
  * We can't just use SIDs, as a page might include multiple links of the same title
136
- * @param {LinkInjection[]} linkInjections
137
- * @returns {LinkInjection[]}
138
129
  */
139
- function insertRandomKeys(linkInjections) {
140
- return linkInjections.map(linkInjection => ({
130
+ function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
131
+ return linkInjections.map((linkInjection: LinkInjection) => ({
141
132
  ...linkInjection,
142
- key: generateInjectionKey(linkInjection.sid),
143
- }))
133
+ key: generateInjectionKey(linkInjection.sid)
134
+ }));
144
135
  }
145
136
 
146
137
  /**
147
138
  * Generate a key for a linkInjection. This is used to match an injection to it's element on the page.
148
- * @param {string} sid Sid of the title linked to the injection
149
- * @returns {string} Random string preprending with title sid.
139
+ * @param sid Sid of the title linked to the injection
140
+ * @returns Random string preprending with title sid.
150
141
  */
151
- export function generateInjectionKey(sid) {
142
+ export function generateInjectionKey(sid: string): string {
152
143
  return sid + '-' + (Math.random() + 1).toString(36).substring(7)
153
144
  }
154
145
 
155
146
  export function getApiToken() {
156
- // @ts-ignore
157
147
  return window.PlayPilotLinkInjections?.token
158
148
  }
@@ -1,10 +1,8 @@
1
1
 
2
2
  /**
3
3
  * Returns the largest number in an array of numbers. Returns 0 if the array has no entries.
4
- * @param {number[]} array
5
- * @return {number}
6
4
  */
7
- export function getLargestValueInArray(array) {
5
+ export function getLargestValueInArray(array: number[]): number {
8
6
  let largest = 0
9
7
 
10
8
  for (let i = 0; i < array.length; i++) {
@@ -6,10 +6,10 @@ const urlParam = 'articleReplacementEditToken'
6
6
 
7
7
  /**
8
8
  * Authorize the user
9
- * @param {string} href The current window.location.href
10
- * @returns {Promise<boolean>} Whether the user is authorized or not
9
+ * @param href The current window.location.href
10
+ * @returns Whether the user is authorized or not
11
11
  */
12
- export async function authorize(href = window.location.href) {
12
+ export async function authorize(href: string = window.location.href): Promise<boolean> {
13
13
  const headers = new Headers({ 'Content-Type': 'application/json' })
14
14
 
15
15
  try {
@@ -41,10 +41,10 @@ export async function authorize(href = window.location.href) {
41
41
 
42
42
  /**
43
43
  * Get the auth token from the URL, a stored cookie, or from the window object
44
- * @param {string} [href] URL that the param is extracted from
45
- * @returns {string} Auth token
44
+ * @param [href] URL that the param is extracted from
45
+ * @returns Auth token
46
46
  */
47
- export function getAuthToken(href = '') {
47
+ export function getAuthToken(href: string = ''): string {
48
48
  // @ts-ignore
49
49
  const configToken = window?.PlayPilotLinkInjections?.editorial_token
50
50
  if (configToken) return configToken
@@ -62,7 +62,7 @@ export function getAuthToken(href = '') {
62
62
  * Set auth cookie equal to given value
63
63
  * @param {string} value The auth token value
64
64
  */
65
- function setAuthCookie(value) {
65
+ function setAuthCookie(value: string) {
66
66
  const time = new Date()
67
67
  const days = 30
68
68
 
@@ -75,10 +75,8 @@ function setAuthCookie(value) {
75
75
  /**
76
76
  * Returns whether or not the user has requested editorial mode to be enabled.
77
77
  * This won't enable editorial mode by itself, as that also requires authentication.
78
- * @returns {boolean}
79
78
  */
80
- export function isEditorialModeEnabled() {
81
- // @ts-ignore
79
+ export function isEditorialModeEnabled(): boolean {
82
80
  const windowToken = window?.PlayPilotLinkInjections?.editorial_token
83
81
  return new URLSearchParams(window.location.search).get('playpilot-editorial-mode') === 'true' || !!windowToken
84
82
  }
@@ -1,5 +1,8 @@
1
- /** @type {Participant[]} */
2
- export const participants = [
1
+ import type { LinkInjection } from "./types/injection"
2
+ import type { Participant } from "./types/participant"
3
+ import type { TitleData } from "./types/title"
4
+
5
+ export const participants: Participant[] = [
3
6
  {
4
7
  sid: 'pr5C5W',
5
8
  name: 'James Franco',
@@ -68,8 +71,7 @@ export const participants = [
68
71
  },
69
72
  ]
70
73
 
71
- /** @type {TitleData} */
72
- export const title = {
74
+ export const title: TitleData = {
73
75
  sid: 'tig9r9F',
74
76
  slug: 'dune-prophecy-2024-series',
75
77
  poster_uuid: '61feb4b0a58f11efb0b50a58a9feac02',
@@ -107,8 +109,7 @@ export const title = {
107
109
  participants,
108
110
  }
109
111
 
110
- /** @type {LinkInjection[]} */
111
- export const linkInjections = [{
112
+ export const linkInjections: LinkInjection[] = [{
112
113
  sid: '1',
113
114
  title: 'Quan',
114
115
  sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Turns any string into a very short hash. This is super basic and not super reliable, but it's good enough for our purpose.
3
- * @param {string} string
4
- * @returns {string}
3
+ * @param string
5
4
  */
6
- export function stringToHash(string) {
5
+ export function stringToHash(string: string): string {
7
6
  let hash = 0
8
7
 
9
8
  for (let i = 0; i < string.length; i++) {
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Returns a string with decoded html characters. For instance & -> &amp;
3
- * @param {string} string
4
3
  */
5
- export function encodeHtmlEntities(string) {
4
+ export function encodeHtmlEntities(string: string): string {
6
5
  const tempElement = document.createElement('div')
7
6
  tempElement.textContent = string
8
7
 
@@ -11,9 +10,8 @@ export function encodeHtmlEntities(string) {
11
10
 
12
11
  /**
13
12
  * Returns a string with encoded html characters. For instance &amp; -> &
14
- * @param {string} string
15
13
  */
16
- export function decodeHtmlEntities(string) {
14
+ export function decodeHtmlEntities(string: string): string {
17
15
  const tempElement = document.createElement('div')
18
16
  tempElement.innerHTML = string
19
17
 
@@ -3,28 +3,28 @@ import TitlePopover from '../routes/components/TitlePopover.svelte'
3
3
  import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
4
4
  import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
5
5
  import { getLargestValueInArray } from './array'
6
+ import { decodeHtmlEntities } from './html'
7
+ import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
6
8
 
7
9
  const keyDataAttribute = 'data-playpilot-injection-key'
8
10
  const keySelector = `[${keyDataAttribute}]`
9
11
 
10
- /** @type {Record<string, { injection: LinkInjection, component: object }>} */
11
- const activePopovers = {}
12
- /** @type {object | null} */
13
- let afterArticlePlaylinkInsertedComponent = null
12
+ const activePopovers: Record<string, { injection: LinkInjection; component: object }> = {}
13
+
14
+ let afterArticlePlaylinkInsertedComponent: object | null = null
14
15
 
15
16
  /**
16
17
  * Return a list of all valid text containing elements that may get injected into.
17
18
  * This excludes duplicates, empty elements, links, buttons, and header tags.
18
- * @param {HTMLElement} parentElement
19
- * @returns {HTMLElement[]} A list of all HTMLElements that contain text, without repeating the same text in
19
+ * @returns A list of all HTMLElements that contain text, without repeating the same text in
20
20
  */
21
- export function getLinkInjectionElements(parentElement) {
21
+ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElement[] {
22
22
  let validElements = []
23
23
 
24
24
  let remainingChildren = [parentElement]
25
25
 
26
26
  while (remainingChildren.length > 0) {
27
- const element = /** @type {HTMLElement} */ (remainingChildren.pop())
27
+ const element = remainingChildren.pop() as HTMLElement
28
28
 
29
29
  // Ignore links, buttons, and headers
30
30
  if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|H[1-6])$/.test(element.tagName)) continue
@@ -41,9 +41,9 @@ export function getLinkInjectionElements(parentElement) {
41
41
  continue
42
42
  }
43
43
 
44
- const children = /** @type {HTMLElement[]} */ (Array.from(element.children))
44
+ const children = Array.from(element.children) as HTMLElement[]
45
45
  for (let i = children.length - 1; i >= 0; i--) {
46
- remainingChildren.push(children[i])
46
+ remainingChildren.push(children[i] as HTMLElement)
47
47
  }
48
48
  }
49
49
 
@@ -54,9 +54,8 @@ export function getLinkInjectionElements(parentElement) {
54
54
  * Get the parent selector that will be used to find the link injections in.
55
55
  * This selector is passed when the script is initialized.
56
56
  * If no selector is passed a default is returned instead.
57
- * @returns {HTMLElement}
58
57
  */
59
- export function getLinkInjectionsParentElement() {
58
+ export function getLinkInjectionsParentElement(): HTMLElement {
60
59
  // @ts-ignore
61
60
  const selector = window.PlayPilotLinkInjections?.selector
62
61
 
@@ -65,7 +64,7 @@ export function getLinkInjectionsParentElement() {
65
64
  const escaped = selector.replace(/(:)/g, '\\$1')
66
65
  const element = document.querySelector(escaped)
67
66
 
68
- if (element) return element
67
+ if (element) return element as HTMLElement
69
68
  }
70
69
 
71
70
  return document.querySelector('article') || document.querySelector('main') || document.body
@@ -73,12 +72,9 @@ export function getLinkInjectionsParentElement() {
73
72
 
74
73
  /**
75
74
  * Replace all found injections within all given elements on the page
76
- * @param {HTMLElement[]} elements
77
- * @param {(LinkInjection: LinkInjection) => void} onclick
78
- * @param {LinkInjectionTypes} injections
79
- * @returns {LinkInjection[]} Returns an array of injections with injections that failed to be inserted marked as `failed`.
75
+ * @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
80
76
  */
81
- export function injectLinksInDocument(elements, onclick, injections = { aiInjections: [], manualInjections: [] }) {
77
+ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInjection: LinkInjection) => void, injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
82
78
  const mergedInjections = mergeInjectionTypes(injections)
83
79
 
84
80
  // Find injection in text content of all elements together, ignore potential HTML elements.
@@ -90,8 +86,7 @@ export function injectLinksInDocument(elements, onclick, injections = { aiInject
90
86
  return cleanPhrase(fullText).includes(cleanPhrase(i.sentence))
91
87
  })
92
88
 
93
- /** @type {LinkInjectionRanges} */
94
- const ranges = {}
89
+ const ranges: LinkInjectionRanges = {}
95
90
 
96
91
  for (const injection of foundInjections) {
97
92
  const elementIndex = elements.findIndex(element => cleanPhrase(element.innerText).includes(cleanPhrase(injection.sentence)))
@@ -157,10 +152,9 @@ export function injectLinksInDocument(elements, onclick, injections = { aiInject
157
152
  * Add all used CSS variables to a data attribute. This data attribute is then used for selectors that for each
158
153
  * individual CSS variable. This is done this way so that CSS variables are only set when they are used.
159
154
  * Using the variables straight up or with a fallback value would not allow them to use their default page styling.
160
- * @returns {void}
161
155
  */
162
- function addCSSVariablesToLinks() {
163
- const createdLinkElements = /** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll(`${keySelector} a`)))
156
+ function addCSSVariablesToLinks(): void {
157
+ const createdLinkElements = Array.from(document.querySelectorAll(`${keySelector} a`)) as HTMLElement[]
164
158
 
165
159
  const variables = [
166
160
  '--playpilot-injection-text-color',
@@ -183,14 +177,11 @@ function addCSSVariablesToLinks() {
183
177
 
184
178
  /**
185
179
  * Add event listeners to all injected links. These events are for both the popover and the modal.
186
- * @param {LinkInjection[]} injections
187
- * @param {function} onclick
188
- * @returns {void}
189
180
  */
190
- function addLinkInjectionEventListeners(injections, onclick) {
181
+ function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (injection: LinkInjection) => void): void {
191
182
  // Open modal on click
192
183
  window.addEventListener('click', (event) => {
193
- const target = /** @type {HTMLElement | null} */ (event.target)
184
+ const target = event.target as HTMLElement | null
194
185
  if (!target?.parentElement) return
195
186
 
196
187
  const key = target.parentElement.getAttribute(keyDataAttribute)
@@ -202,11 +193,10 @@ function addLinkInjectionEventListeners(injections, onclick) {
202
193
  openLinkModal(event, injection, onclick)
203
194
  })
204
195
 
205
- const createdInjectionElements = document.querySelectorAll(keySelector)
196
+ const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
206
197
 
207
198
  // Open and close popover on mouseenter/mouseleave
208
199
  createdInjectionElements.forEach((injectionElement) => {
209
- // @ts-ignore
210
200
  const key = injectionElement.dataset.playpilotInjectionKey
211
201
  const injection = injections.find(injection => key === injection.key)
212
202
 
@@ -221,12 +211,8 @@ function addLinkInjectionEventListeners(injections, onclick) {
221
211
  /**
222
212
  * Prevent default click and run onclick from parent. Ignore clicks that used modifier keys or that were not left click.
223
213
  * The event is not fired when the click happens from inside a popover.
224
- * @param {MouseEvent} event
225
- * @param {LinkInjection} injection
226
- * @param {function} onclick
227
- * @returns {void}
228
214
  */
229
- function openLinkModal(event, injection, onclick) {
215
+ function openLinkModal(event: MouseEvent, injection: LinkInjection, onclick: (injection: LinkInjection) => void): void {
230
216
  if (event.ctrlKey || event.metaKey || event.button !== 0) return
231
217
 
232
218
  event.preventDefault()
@@ -238,28 +224,24 @@ function openLinkModal(event, injection, onclick) {
238
224
  /**
239
225
  * When a link is hovered, it is shown as a popover. The component is mounted when a mouse enters the link,
240
226
  * and removed when clicked or on mouseleave.
241
- * @param {MouseEvent} event
242
- * @param {LinkInjection} injection
243
227
  */
244
- function openLinkPopover(event, injection) {
228
+ function openLinkPopover(event: MouseEvent, injection: LinkInjection) {
245
229
  // Popover for this link was already open and was called again... for some reason
246
230
  if (activePopovers[injection.key]) return
247
231
 
248
232
  // Skip touch devices
249
233
  if (window.matchMedia('(pointer: coarse)').matches) return
250
234
 
251
- const target = /** @type {Element} */ (event.currentTarget)
252
- const popover = mount(TitlePopover, { target, props: { title: injection.title_details } })
235
+ const target = event.currentTarget as Element
236
+ const popover = mount(TitlePopover, { target, props: { title: injection.title_details! } })
253
237
 
254
238
  activePopovers[injection.key] = { injection, component: popover }
255
239
  }
256
240
 
257
241
  /**
258
242
  * Unmount the popover, removing it from the dom
259
- * @param {LinkInjection} injection
260
- * @param {boolean} outro
261
243
  */
262
- function destroyLinkPopover(injection, outro = true) {
244
+ function destroyLinkPopover(injection: LinkInjection, outro: boolean = true) {
263
245
  const popover = activePopovers[injection.key]
264
246
 
265
247
  if (!popover) return
@@ -270,11 +252,8 @@ function destroyLinkPopover(injection, outro = true) {
270
252
 
271
253
  /**
272
254
  * Insert AfterArticlePlaylinks after the last valid element.
273
- * @param {HTMLElement[]} elements
274
- * @param {LinkInjection[]} injections
275
- * @param {(linkInjection: LinkInjection) => void} onclickmodal
276
255
  */
277
- export function insertAfterArticlePlaylinks(elements, injections, onclickmodal) {
256
+ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[], onclickmodal: (linkInjection: LinkInjection) => void) {
278
257
  if (!injections.length) return
279
258
 
280
259
  const target = document.createElement('div')
@@ -284,7 +263,7 @@ export function insertAfterArticlePlaylinks(elements, injections, onclickmodal)
284
263
  afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal } })
285
264
  }
286
265
 
287
- function clearAfterArticlePlaylinks() {
266
+ function clearAfterArticlePlaylinks(): void {
288
267
  if (!afterArticlePlaylinkInsertedComponent) return
289
268
 
290
269
  unmount(afterArticlePlaylinkInsertedComponent)
@@ -295,11 +274,11 @@ function clearAfterArticlePlaylinks() {
295
274
  /**
296
275
  * Clear link injections from the page
297
276
  */
298
- export function clearLinkInjections() {
277
+ export function clearLinkInjections(): void {
299
278
  Object.values(activePopovers).forEach(popover => destroyLinkPopover(popover.injection))
300
279
 
301
280
  const elements = document.querySelectorAll(keySelector)
302
- elements.forEach((element /** @type {HTMLAnchorElement} */) => element.outerHTML = element.textContent || '')
281
+ elements.forEach((element) => element.outerHTML = element.textContent || '')
303
282
 
304
283
  Object.values(activePopovers).forEach(({ injection }) => destroyLinkPopover(injection, false))
305
284
 
@@ -308,22 +287,18 @@ export function clearLinkInjections() {
308
287
 
309
288
  /**
310
289
  * Clear specific link injection from the page
311
- * @param {string} key Given of the injection to be removed from the page
290
+ * @param key Given of the injection to be removed from the page
312
291
  */
313
- export function clearLinkInjection(key) {
314
- /** @type {HTMLAnchorElement | null} */
315
- const element = document.querySelector(`[${keyDataAttribute}="${key}"]`)
292
+ export function clearLinkInjection(key: string): void {
293
+ const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
316
294
  if (element) element.outerHTML = element.textContent || ''
317
295
  }
318
296
 
319
297
  /**
320
298
  * Sort injections by where they were inserted. First by their element index, second by where in the element the
321
299
  * injection was injected. Injections without range (after article injections or failed injection) go last.
322
- * @param {LinkInjection[]} injections
323
- * @param {LinkInjectionRanges} ranges
324
- * @returns {LinkInjection[]}
325
300
  */
326
- export function sortLinkInjectionsByRange(injections, ranges) {
301
+ export function sortLinkInjectionsByRange(injections: LinkInjection[], ranges: LinkInjectionRanges): LinkInjection[] {
327
302
  return injections.sort((a, b) => {
328
303
  const rangeA = ranges[a.key]
329
304
  const rangeB = ranges[b.key]
@@ -342,19 +317,15 @@ export function sortLinkInjectionsByRange(injections, ranges) {
342
317
 
343
318
  /**
344
319
  * Merge different injection types
345
- * @param {LinkInjectionTypes} injections
346
- * @returns {LinkInjection[]}
347
320
  */
348
- export function mergeInjectionTypes({ aiInjections, manualInjections }) {
321
+ export function mergeInjectionTypes({ aiInjections, manualInjections }: LinkInjectionTypes): LinkInjection[] {
349
322
  return [...aiInjections, ...manualInjections.map(i => ({ ...i, manual: true }))]
350
323
  }
351
324
 
352
325
  /**
353
326
  * Separate an array of flat injections into ai and manual arrays.
354
- * @param {LinkInjection[]} injections
355
- * @returns {LinkInjectionTypes}
356
327
  */
357
- export function separateLinkInjectionTypes(injections) {
328
+ export function separateLinkInjectionTypes(injections: LinkInjection[]): LinkInjectionTypes {
358
329
  return {
359
330
  aiInjections: injections.filter(i => !i.manual),
360
331
  manualInjections: injections.filter(i => i.manual),
@@ -363,37 +334,29 @@ export function separateLinkInjectionTypes(injections) {
363
334
 
364
335
  /**
365
336
  * Returns whether or not an injection would be valid for any sort of injection, text or after_article
366
- * @param {LinkInjection} injection
367
- * @returns {boolean}
368
337
  */
369
- export function isValidInjection(injection) {
338
+ export function isValidInjection(injection: LinkInjection): boolean {
370
339
  return !injection.inactive && !injection.removed && !injection.duplicate && !!injection.title_details
371
340
  }
372
341
 
373
342
  /**
374
343
  * Filter links for in-text injections, removing after article, inactive, removed, duplicate, and items without title_details
375
- * @param {LinkInjection[]} injections
376
- * @returns {LinkInjection[]}
377
344
  */
378
- export function filterInvalidInTextInjections(injections) {
345
+ export function filterInvalidInTextInjections(injections: LinkInjection[]): LinkInjection[] {
379
346
  return filterRemovedInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
380
347
  }
381
348
 
382
349
  /**
383
350
  * Filter links for after article injections, removing in-text only, inactive, removed, duplicate, and items without title_details
384
- * @param {LinkInjection[]} injections
385
- * @returns {LinkInjection[]}
386
351
  */
387
- export function filterInvalidAfterArticleInjections(injections) {
352
+ export function filterInvalidAfterArticleInjections(injections: LinkInjection[]): LinkInjection[] {
388
353
  return filterRemovedInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
389
354
  }
390
355
 
391
356
  /**
392
357
  * Filter injections that were marked as removed or have an equivalent removed manual injections, soley based on the same sentence and title.
393
- * @param {LinkInjection[]} injections
394
- * @returns {LinkInjection[]}
395
358
  */
396
- export function filterRemovedInjections(injections) {
359
+ export function filterRemovedInjections(injections: LinkInjection[]): LinkInjection[] {
397
360
  return injections.filter(injection => {
398
361
  if (injection.removed) return false
399
362
  if (injection.manual && !injection.removed) return true
@@ -403,12 +366,8 @@ export function filterRemovedInjections(injections) {
403
366
 
404
367
  /**
405
368
  * Return whether or not an injection is also available as manual injection
406
- * @param {LinkInjection} injection
407
- * @param {number} injectionIndex
408
- * @param {LinkInjection[]} injections
409
- * @returns {boolean}
410
369
  */
411
- export function isAvailableAsManualInjection(injection, injectionIndex, injections) {
370
+ export function isAvailableAsManualInjection(injection: LinkInjection, injectionIndex: number, injections: LinkInjection[]): boolean {
412
371
  return injections.some((i, index) => {
413
372
  return injectionIndex !== index && i.manual && isEquivalentInjection(i, injection)
414
373
  })
@@ -416,10 +375,7 @@ export function isAvailableAsManualInjection(injection, injectionIndex, injectio
416
375
 
417
376
  /**
418
377
  * Returns whether or not 2 injections match in title and sentence
419
- * @param {LinkInjection} injection1
420
- * @param {LinkInjection} injection2
421
- * @returns {boolean}
422
378
  */
423
- export function isEquivalentInjection(injection1, injection2) {
379
+ export function isEquivalentInjection(injection1: LinkInjection, injection2: LinkInjection): boolean {
424
380
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
425
381
  }