Skip to content

Commit d4aa4fd

Browse files
authored
Merge pull request #598 from docat-org/fix/render-loop
Fix/docs-page-url-handling
2 parents d05fb53 + d68d806 commit d4aa4fd

6 files changed

Lines changed: 234 additions & 178 deletions

File tree

web/src/components/DocumentControlButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function DocumentControlButtons (props: Props): JSX.Element {
3131
<Select
3232
className={styles['version-select']}
3333
onChange={(e) => { props.onVersionChange(e.target.value) }}
34-
value={props.versions.length > 0 ? props.version : ''}
34+
value={props.versions.find(v => v.name === props.version) !== undefined ? props.version : ''}
3535
>
3636
{props.versions
3737
.filter((v) => !v.hidden || v.name === props.version)

web/src/components/IFrame.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React, { useRef } from 'react'
2+
import { uniqueId } from 'lodash'
3+
4+
import styles from '../style/components/IFrame.module.css'
5+
interface Props {
6+
src: string
7+
onPageChanged: (page: string, hash: string) => void
8+
onHashChanged: (hash: string) => void
9+
}
10+
11+
export default function IFrame(props: Props): JSX.Element {
12+
const iFrameRef = useRef<HTMLIFrameElement>(null)
13+
14+
const onIframeLoad = (): void => {
15+
if (iFrameRef.current == null) {
16+
console.error('iFrameRef is null')
17+
return
18+
}
19+
20+
// remove the hashchange event listener to prevent memory leaks
21+
iFrameRef.current.contentWindow?.removeEventListener('hashchange', hashChangeEventListener)
22+
23+
const url = iFrameRef.current?.contentDocument?.location.href
24+
if (url == null) {
25+
console.warn('IFrame onload event triggered, but url is null')
26+
return
27+
}
28+
29+
// make all external links in iframe open in new tab
30+
// and make internal links replace the iframe url so that change
31+
// doesn't show up in the page history (we'd need to click back twice)
32+
iFrameRef.current.contentDocument
33+
?.querySelectorAll('a')
34+
.forEach((a: HTMLAnchorElement) => {
35+
if (!a.href.startsWith(window.location.origin)) {
36+
a.setAttribute('target', '_blank')
37+
return
38+
}
39+
40+
if (a.href.trim() === '') {
41+
// ignore empty links, may be handled with js internally,
42+
// so we'll ignore it. Will inevitably cause the user to have to click back
43+
// multiple times to get back to the previous page.
44+
return
45+
}
46+
47+
// From here: https://www.ozzu.com/questions/358584/how-do-you-ignore-iframes-javascript-history
48+
a.onclick = () => {
49+
iFrameRef.current?.contentWindow?.location.replace(a.href)
50+
return false
51+
}
52+
})
53+
54+
// Add the event listener again
55+
iFrameRef.current.contentWindow?.addEventListener('hashchange', hashChangeEventListener)
56+
57+
const parts = url.split('/doc/').slice(1).join('/doc/').split('/')
58+
const urlPageAndHash = parts.slice(2).join('/')
59+
const hashIndex = urlPageAndHash.includes('#') ? urlPageAndHash.indexOf('#') : urlPageAndHash.length
60+
const urlPage = urlPageAndHash.slice(0, hashIndex)
61+
const urlHash = urlPageAndHash.slice(hashIndex)
62+
63+
props.onPageChanged(urlPage, urlHash)
64+
}
65+
66+
const hashChangeEventListener = (): void => {
67+
if (iFrameRef.current == null) {
68+
console.error('hashChangeEvent from iframe but iFrameRef is null')
69+
return
70+
}
71+
72+
const url = iFrameRef.current?.contentDocument?.location.href
73+
if (url == null) {
74+
return
75+
}
76+
77+
let hash = url.split('#')[1]
78+
if (hash !== null) {
79+
hash = `#${hash}`
80+
} else {
81+
hash = ''
82+
}
83+
84+
props.onHashChanged(hash)
85+
}
86+
87+
return (<iframe
88+
ref={iFrameRef}
89+
key={uniqueId()}
90+
className={styles['docs-iframe']}
91+
src={props.src}
92+
title="docs"
93+
onLoad={onIframeLoad}
94+
/>)
95+
}

web/src/data-providers/MessageBannerProvider.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ export interface Message {
1515

1616
interface MessageBannerState {
1717
showMessage: (message: Message) => void
18+
clearMessages: () => void
1819
}
1920

2021
export const Context = React.createContext<MessageBannerState>({
2122
showMessage: (): void => {
2223
console.warn('MessageBannerProvider not initialized')
24+
},
25+
clearMessages: (): void => {
26+
console.warn('MessageBannerProvider not initialized')
2327
}
2428
})
2529

@@ -59,8 +63,20 @@ export function MessageBannerProvider ({ children }: any): JSX.Element {
5963
setLastTimeout(newTimeout)
6064
}, [])
6165

66+
const clearMessages = useCallback(() => {
67+
if (lastTimeout !== undefined) {
68+
clearTimeout(lastTimeout)
69+
}
70+
71+
setMessage({
72+
content: undefined,
73+
type: 'success',
74+
showMs: 6000
75+
})
76+
}, [])
77+
6278
return (
63-
<Context.Provider value={{ showMessage }}>
79+
<Context.Provider value={{ showMessage, clearMessages }}>
6480
<Banner message={message} />
6581
{children}
6682
</Context.Provider>

0 commit comments

Comments
 (0)