Merge pull request #1496 from spotify/mob/techdocs-anchor-scrolling
Scroll to anchor link
This commit is contained in:
@@ -25,12 +25,12 @@ import { Header, Content, ItemCard } from '@backstage/core';
|
||||
import transformer, {
|
||||
addBaseUrl,
|
||||
rewriteDocLinks,
|
||||
addEventListener,
|
||||
addLinkClickListener,
|
||||
removeMkdocsHeader,
|
||||
modifyCss,
|
||||
} from '../transformers';
|
||||
import { docStorageURL } from '../../config';
|
||||
import URLParser from '../urlParser';
|
||||
import URLFormatter from '../urlFormatter';
|
||||
|
||||
const useFetch = (url: string) => {
|
||||
const state = useAsync(async () => {
|
||||
@@ -45,7 +45,7 @@ const useFetch = (url: string) => {
|
||||
const useEnforcedTrailingSlash = (): void => {
|
||||
React.useEffect(() => {
|
||||
const actualUrl = window.location.href;
|
||||
const expectedUrl = new URLParser(window.location.href, '.').parse();
|
||||
const expectedUrl = new URLFormatter(window.location.href).formatBaseURL();
|
||||
|
||||
if (actualUrl !== expectedUrl) {
|
||||
window.history.replaceState({}, document.title, expectedUrl);
|
||||
@@ -58,10 +58,9 @@ export const Reader = () => {
|
||||
const { componentId, '*': path } = useParams();
|
||||
const shadowDomRef = useShadowDom();
|
||||
const navigate = useNavigate();
|
||||
const normalizedUrl = new URLParser(
|
||||
const normalizedUrl = new URLFormatter(
|
||||
`${docStorageURL}${location.pathname.replace('/docs', '')}`,
|
||||
'.',
|
||||
).parse();
|
||||
).formatBaseURL();
|
||||
const state = useFetch(`${normalizedUrl}index.html`);
|
||||
|
||||
useEnforcedTrailingSlash();
|
||||
@@ -89,11 +88,28 @@ export const Reader = () => {
|
||||
]);
|
||||
|
||||
divElement.shadowRoot.innerHTML = '';
|
||||
|
||||
if (transformedElement) {
|
||||
divElement.shadowRoot.appendChild(transformedElement);
|
||||
transformer(divElement.shadowRoot.children[0], [
|
||||
addEventListener({
|
||||
onClick: navigate,
|
||||
dom => {
|
||||
setTimeout(() => {
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash.slice(1);
|
||||
divElement.shadowRoot?.getElementById(hash)?.scrollIntoView();
|
||||
}
|
||||
}, 200);
|
||||
return dom;
|
||||
},
|
||||
addLinkClickListener({
|
||||
onClick: (_: MouseEvent, url: string) => {
|
||||
const parsedUrl = new URL(url);
|
||||
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
|
||||
|
||||
divElement.shadowRoot
|
||||
?.querySelector(parsedUrl.hash)
|
||||
?.scrollIntoView();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('addBaseUrl', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes path option', () => {
|
||||
it('includes path option without slash', () => {
|
||||
const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
|
||||
transformers: [
|
||||
addBaseUrl({
|
||||
@@ -86,4 +86,29 @@ describe('addBaseUrl', () => {
|
||||
'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes path option with slash', () => {
|
||||
const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
|
||||
transformers: [
|
||||
addBaseUrl({
|
||||
docStorageURL: DOC_STORAGE_URL,
|
||||
componentId: 'example-docs',
|
||||
path: 'examplepath/',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(getSample(shadowDom, 'img', 'src')).toEqual([
|
||||
'https://example-host.storage.googleapis.com/example-docs/examplepath/img/win-py-install.png',
|
||||
'https://example-host.storage.googleapis.com/example-docs/examplepath/img/initial-layout.png',
|
||||
]);
|
||||
expect(getSample(shadowDom, 'link', 'href')).toEqual([
|
||||
'https://www.mkdocs.org/',
|
||||
'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/images/favicon.png',
|
||||
]);
|
||||
expect(getSample(shadowDom, 'script', 'src')).toEqual([
|
||||
'https://www.google-analytics.com/analytics.js',
|
||||
'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import URLParser from '../urlParser';
|
||||
import URLFormatter from '../urlFormatter';
|
||||
import type { Transformer } from './index';
|
||||
|
||||
type AddBaseUrlOptions = {
|
||||
@@ -36,11 +36,16 @@ export const addBaseUrl = ({
|
||||
Array.from(list)
|
||||
.filter(elem => !!elem.getAttribute(attributeName))
|
||||
.forEach((elem: T) => {
|
||||
const newUrl = new URLParser(
|
||||
`${docStorageURL}/${componentId}/${path}`,
|
||||
elem.getAttribute(attributeName)!,
|
||||
).parse();
|
||||
elem.setAttribute(attributeName, newUrl);
|
||||
const urlFormatter = new URLFormatter(
|
||||
path.length < 1 || path.endsWith('/')
|
||||
? `${docStorageURL}/${componentId}/${path}`
|
||||
: `${docStorageURL}/${componentId}/${path}/`,
|
||||
);
|
||||
|
||||
elem.setAttribute(
|
||||
attributeName,
|
||||
urlFormatter.formatURL(elem.getAttribute(attributeName)!),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
import { createTestShadowDom, FIXTURES } from '../../test-utils';
|
||||
import { addEventListener } from '../transformers';
|
||||
import { addLinkClickListener } from '.';
|
||||
|
||||
describe('addEventListener', () => {
|
||||
describe('addLinkClickListener', () => {
|
||||
it('calls onClick when a link has been clicked', () => {
|
||||
const fn = jest.fn();
|
||||
const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
|
||||
transformers: [
|
||||
addEventListener({
|
||||
addLinkClickListener({
|
||||
onClick: fn,
|
||||
}),
|
||||
],
|
||||
+5
-7
@@ -16,22 +16,20 @@
|
||||
|
||||
import type { Transformer } from './index';
|
||||
|
||||
type AddEventListenerOptions = {
|
||||
onClick: (newUrl: string) => void;
|
||||
type AddLinkClickListenerOptions = {
|
||||
onClick: (e: MouseEvent, newUrl: string) => void;
|
||||
};
|
||||
|
||||
export const addEventListener = ({
|
||||
export const addLinkClickListener = ({
|
||||
onClick,
|
||||
}: AddEventListenerOptions): Transformer => {
|
||||
}: AddLinkClickListenerOptions): Transformer => {
|
||||
return dom => {
|
||||
Array.from(dom.getElementsByTagName('a')).forEach(elem => {
|
||||
elem.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const target = e.target as HTMLAnchorElement;
|
||||
if (target?.getAttribute('href')) {
|
||||
onClick(
|
||||
target.getAttribute('href')!.replace(window.location.origin, ''),
|
||||
);
|
||||
onClick(e, target.getAttribute('href')!);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
export * from './addBaseUrl';
|
||||
export * from './rewriteDocLinks';
|
||||
export * from './addEventListener';
|
||||
export * from './addLinkClickListener';
|
||||
export * from './removeMkdocsHeader';
|
||||
export * from './modifyCss';
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('rewriteDocLinks', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transform a href with licalhost as baseUrl', () => {
|
||||
it('should transform a href with localhost as baseUrl', () => {
|
||||
const shadowDom = createTestShadowDom(
|
||||
`
|
||||
<a href="http://example.org/">Test</a>
|
||||
@@ -49,9 +49,9 @@ describe('rewriteDocLinks', () => {
|
||||
|
||||
expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([
|
||||
'http://example.org/',
|
||||
'http://localhost/example',
|
||||
'http://localhost/example-docs',
|
||||
'http://localhost/example-docs/example-page',
|
||||
'http://localhost/example/',
|
||||
'http://localhost/example-docs/',
|
||||
'http://localhost/example-docs/example-page/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import URLParser from '../urlParser';
|
||||
import URLFormatter from '../urlFormatter';
|
||||
import type { Transformer } from './index';
|
||||
|
||||
export const rewriteDocLinks = (): Transformer => {
|
||||
@@ -26,12 +26,10 @@ export const rewriteDocLinks = (): Transformer => {
|
||||
Array.from(list)
|
||||
.filter(elem => elem.hasAttribute(attributeName))
|
||||
.forEach((elem: T) => {
|
||||
const urlFormatter = new URLFormatter(window.location.href);
|
||||
elem.setAttribute(
|
||||
attributeName,
|
||||
new URLParser(
|
||||
window.location.href,
|
||||
elem.getAttribute(attributeName)!,
|
||||
).parse(),
|
||||
urlFormatter.formatURL(elem.getAttribute(attributeName)!),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import URLFormatter from './urlFormatter';
|
||||
|
||||
describe('URLFormatter', () => {
|
||||
describe('formatURL', () => {
|
||||
it('should not change an absolute url', () => {
|
||||
const formatter = new URLFormatter('https://www.google.com/');
|
||||
expect(formatter.formatURL('https://www.mkdocs.org/')).toEqual(
|
||||
'https://www.mkdocs.org/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert a relative url to an absolute url', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
expect(formatter.formatURL('../../support/installing/')).toEqual(
|
||||
'https://www.mkdocs.org/support/installing/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a trailing slash', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started',
|
||||
);
|
||||
expect(formatter.formatURL('./getting-started')).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
expect(formatter.formatURL('.')).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add multiple hashes', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/#hash1',
|
||||
);
|
||||
expect(formatter.formatURL('./#hash2')).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/#hash2',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBaseURL', () => {
|
||||
it('should keep query params in URL', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world',
|
||||
);
|
||||
expect(formatter.formatBaseURL()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world',
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep hash in URL', () => {
|
||||
const formatter = new URLFormatter(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/#hash',
|
||||
);
|
||||
expect(formatter.formatBaseURL()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/#hash',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-10
@@ -14,18 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const normalizeBaseURL = (baseURL: string): string => {
|
||||
const url = new URL(baseURL);
|
||||
url.pathname = url.pathname.replace(/([^/])$/, '$1/');
|
||||
return url.toString();
|
||||
};
|
||||
export default class URLFormatter {
|
||||
constructor(public baseURL: string) {}
|
||||
|
||||
export default class URLParser {
|
||||
constructor(public baseURL: string, public pathname: string) {
|
||||
this.baseURL = normalizeBaseURL(baseURL);
|
||||
formatBaseURL(): string {
|
||||
return this.normalizeURL(this.baseURL);
|
||||
}
|
||||
|
||||
parse(): string {
|
||||
return new URL(this.pathname, this.baseURL).toString();
|
||||
formatURL(pathname: string): string {
|
||||
return this.normalizeURL(new URL(pathname, this.baseURL).toString());
|
||||
}
|
||||
|
||||
private normalizeURL(urlString: string): string {
|
||||
const url = new URL(urlString);
|
||||
const filename: string = url.pathname.split('/').pop() ?? url.pathname;
|
||||
const isDir: boolean = filename.includes('.') === false;
|
||||
|
||||
if (isDir) {
|
||||
url.pathname = url.pathname.replace(/([^/])$/, '$1/');
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import URLParser from './urlParser';
|
||||
|
||||
describe('URLParser', () => {
|
||||
it('should not change an absolute url', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.google.com/',
|
||||
'https://www.mkdocs.org/',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual('https://www.mkdocs.org/');
|
||||
});
|
||||
|
||||
it('should convert a relative url to an absolute url', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
'../../support/installing/',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/support/installing/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a trailing slash', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started',
|
||||
'.',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
'.',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user