Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/common/utils/dom.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createElement } from '@common/test/utils'
import {
getElementById,
getElementsByName,
observeElement,
querySelector,
querySelectorAll,
querySelectorAllIn,
querySelectorIn,
} from '@common/utils/dom'
import { logger } from '@common/utils/logger'
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'

describe(getElementById, () => {
beforeEach(() => {
Expand Down Expand Up @@ -207,3 +209,41 @@ describe(querySelectorAllIn, () => {
expect(logger.warn).not.toHaveBeenCalled()
})
})

describe(observeElement, () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="container">
<div class="content">Initial content</div>
</div>
`
})

it('calls callback when element children change', async () => {
const container = document.getElementById('container')!
const callback = vi.fn<() => void>()

observeElement(container, callback)

container.appendChild(createElement('New content'))

await vi.waitFor(() => {
expect(callback).toHaveBeenCalledTimes(1)
})
})

it('calls callback when subtree changes', async () => {
const container = document.getElementById('container')!
const callback = vi.fn<() => void>()

observeElement(container, callback)

// Modify a nested element
const content = container.querySelector('.content')!
content.textContent = 'Modified content'

await vi.waitFor(() => {
expect(callback).toHaveBeenCalledTimes(1)
})
})
})
16 changes: 16 additions & 0 deletions src/common/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,19 @@ export const querySelectorAllIn = <E extends Element = Element>(

return elements
}

/**
* Observe an element for DOM changes and run a callback when mutations occur.
*
* @param element - The element to observe for changes
* @param callback - Function to execute when the element's children change
*/
export const observeElement = (element: Element, callback: () => void) => {
const observer = new MutationObserver(() => {
observer.disconnect()
callback()
observer.observe(element, { childList: true, subtree: true })
})

observer.observe(element, { childList: true, subtree: true })
}
36 changes: 35 additions & 1 deletion src/modules/week-number/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { isPage, pages } from '@common/utils/pages'
import weekNumber from '@modules/week-number'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

vi.mock(import('@common/utils/pages'), async () => {
const originalModule = await vi.importActual('@common/utils/pages')

return {
...originalModule,
isPage: vi.fn<typeof isPage>(),
}
})

describe('week-number module', () => {
it.each([
Expand All @@ -19,6 +29,30 @@ describe('week-number module', () => {
expect(span?.textContent).toBe(expected)
})

it.each([
{ desc: 'own team', page: pages.playerDetailOwnTeam },
{ desc: 'other team', page: pages.playerDetailOtherTeam },
])('observes player tabs and adds week numbers on content change', async ({ page }) => {
vi.mocked(isPage).mockImplementation((p) => p === page)

document.body.innerHTML = `
<div id="mainBody">
<div id="ctl00_ctl00_CPContent_CPMain_updPlayerTabs"></div>
</div>
`
weekNumber.run()

// Simulate content update
const playerTabs = document.getElementById('ctl00_ctl00_CPContent_CPMain_updPlayerTabs')!
playerTabs.innerHTML = '<div class="date">15.05.2025</div>'

await vi.waitFor(() => {
const newWeekNumber = playerTabs.querySelector<HTMLSpanElement>('span.hte-week-number')

expect(newWeekNumber?.textContent).toBe(' (W3)')
})
})

it("doesn't add week number for invalid date", () => {
document.body.innerHTML = `
<div id="mainBody">
Expand Down
49 changes: 36 additions & 13 deletions src/modules/week-number/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import '@modules/week-number/index.css'

import type { Module } from '@common/types/module'
import { querySelectorAll } from '@common/utils/dom'
import { pages } from '@common/utils/pages'
import { getElementById, observeElement, querySelectorAllIn } from '@common/utils/dom'
import { isPage, pages } from '@common/utils/pages'
import { calcWeekNumber, parseDate } from '@modules/week-number/utils'

/**
* Add week numbers to all date elements within the given root element.
*
* @param root - The parent element to search for date elements within
*/
const addWeekNumbers = (root: Element) => {
const elements = querySelectorAllIn(root, '.date', false)

elements.forEach((element) => {
const date = parseDate(element)
if (!date) return

const weekNumber = calcWeekNumber(date)

const span = document.createElement('span')
span.className = 'hte-week-number shy'
span.textContent = ` (W${weekNumber})`

element.appendChild(span)
})
}

/**
* Display week numbers next to dates throughout Hattrick.
*/
Expand All @@ -13,20 +35,21 @@ const weekNumber: Module = {
pages: [pages.all],
excludePages: [pages.forum],
run: () => {
const elements = querySelectorAll('#mainBody .date', false)

elements.forEach((element) => {
const date = parseDate(element)
if (!date) return
const mainBody = getElementById('mainBody')
if (!mainBody) return

const weekNumber = calcWeekNumber(date)
addWeekNumbers(mainBody)

const span = document.createElement('span')
span.className = 'hte-week-number shy'
span.textContent = ` (W${weekNumber})`
// Watch for tab changes on the player detail page. Tab content is loaded asynchronously when clicked, so we need to
// re-apply week numbers after each update.
if (isPage(pages.playerDetailOwnTeam) || isPage(pages.playerDetailOtherTeam)) {
const playerTabs = getElementById('ctl00_ctl00_CPContent_CPMain_updPlayerTabs')
if (!playerTabs) return

element.appendChild(span)
})
observeElement(playerTabs, () => {
addWeekNumbers(playerTabs)
})
}
},
}

Expand Down