mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-13 18:40:54 +01:00
Added support for auto-replacement of meta tags when using instant loading
This commit is contained in:
parent
15538b0a39
commit
639dbacc63
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -23,5 +23,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="{{ 'assets/javascripts/custom.4eda089e.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/custom.a4bbca43.min.js' | url }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
29
material/templates/assets/javascripts/bundle.726fbb30.min.js
vendored
Normal file
29
material/templates/assets/javascripts/bundle.726fbb30.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -250,7 +250,7 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ 'assets/javascripts/bundle.5827baa9.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/bundle.726fbb30.min.js' | url }}"></script>
|
||||||
{% for script in config.extra_javascript %}
|
{% for script in config.extra_javascript %}
|
||||||
{{ script | script_tag }}
|
{{ script | script_tag }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -38,7 +38,7 @@ export type Flag =
|
|||||||
| "header.autohide" /* Hide header */
|
| "header.autohide" /* Hide header */
|
||||||
| "navigation.expand" /* Automatic expansion */
|
| "navigation.expand" /* Automatic expansion */
|
||||||
| "navigation.indexes" /* Section pages */
|
| "navigation.indexes" /* Section pages */
|
||||||
| "navigation.instant" /* Instant loading */
|
| "navigation.instant" /* Instant navigation */
|
||||||
| "navigation.sections" /* Section navigation */
|
| "navigation.sections" /* Section navigation */
|
||||||
| "navigation.tabs" /* Tabs navigation */
|
| "navigation.tabs" /* Tabs navigation */
|
||||||
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
|
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
|
||||||
|
@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
import { Subject } from "rxjs"
|
import { Subject } from "rxjs"
|
||||||
|
|
||||||
|
import { feature } from "~/_"
|
||||||
|
import { h } from "~/utilities"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@ -43,10 +46,31 @@ export function getLocation(): URL {
|
|||||||
/**
|
/**
|
||||||
* Set location
|
* Set location
|
||||||
*
|
*
|
||||||
* @param url - URL to change to
|
* If instant navigation is enabled, this function creates a temporary anchor
|
||||||
|
* element, sets the `href` attribute, appends it to the body, clicks it, and
|
||||||
|
* then removes it again. The event will bubble up the DOM and trigger be
|
||||||
|
* intercepted by the instant loading business logic.
|
||||||
|
*
|
||||||
|
* Note that we must append and remove the anchor element, or the event will
|
||||||
|
* not bubble up the DOM, making it impossible to intercept it.
|
||||||
|
*
|
||||||
|
* @param url - URL to navigate to
|
||||||
|
* @param navigate - Force navigation
|
||||||
*/
|
*/
|
||||||
export function setLocation(url: URL | HTMLLinkElement): void {
|
export function setLocation(
|
||||||
location.href = url.href
|
url: URL | HTMLLinkElement, navigate = false
|
||||||
|
): void {
|
||||||
|
if (feature("navigation.instant") && !navigate) {
|
||||||
|
const el = h("a", { href: url.href })
|
||||||
|
document.body.appendChild(el)
|
||||||
|
el.click()
|
||||||
|
el.remove()
|
||||||
|
|
||||||
|
// If we're not using instant navigation, and the page should not be reloaded
|
||||||
|
// just instruct the browser to navigate to the given URL
|
||||||
|
} else {
|
||||||
|
location.href = url.href
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
@ -77,7 +77,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
SearchIndex,
|
SearchIndex,
|
||||||
setupClipboardJS,
|
setupClipboardJS,
|
||||||
setupInstantLoading,
|
setupInstantNavigation,
|
||||||
setupVersionSelector
|
setupVersionSelector
|
||||||
} from "./integrations"
|
} from "./integrations"
|
||||||
import {
|
import {
|
||||||
@ -143,9 +143,9 @@ const index$ = document.forms.namedItem("search")
|
|||||||
const alert$ = new Subject<string>()
|
const alert$ = new Subject<string>()
|
||||||
setupClipboardJS({ alert$ })
|
setupClipboardJS({ alert$ })
|
||||||
|
|
||||||
/* Set up instant loading, if enabled */
|
/* Set up instant navigation, if enabled */
|
||||||
if (feature("navigation.instant"))
|
if (feature("navigation.instant"))
|
||||||
setupInstantLoading({ location$, viewport$ })
|
setupInstantNavigation({ location$, viewport$ })
|
||||||
.subscribe(document$)
|
.subscribe(document$)
|
||||||
|
|
||||||
/* Set up version selector */
|
/* Set up version selector */
|
||||||
@ -175,10 +175,7 @@ keyboard$
|
|||||||
case ",":
|
case ",":
|
||||||
const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
|
const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
|
||||||
if (typeof prev !== "undefined")
|
if (typeof prev !== "undefined")
|
||||||
if (feature("navigation.instant"))
|
setLocation(prev)
|
||||||
location$.next(new URL(prev.href))
|
|
||||||
else
|
|
||||||
setLocation(prev)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
/* Go to next page */
|
/* Go to next page */
|
||||||
@ -186,10 +183,7 @@ keyboard$
|
|||||||
case ".":
|
case ".":
|
||||||
const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
|
const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
|
||||||
if (typeof next !== "undefined")
|
if (typeof next !== "undefined")
|
||||||
if (feature("navigation.instant"))
|
setLocation(next)
|
||||||
location$.next(new URL(next.href))
|
|
||||||
else
|
|
||||||
setLocation(next)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
/* Expand navigation, see https://bit.ly/3ZjG5io */
|
/* Expand navigation, see https://bit.ly/3ZjG5io */
|
||||||
|
@ -82,7 +82,7 @@ export function mountAnnounce(
|
|||||||
if (!feature("announce.dismiss") || !el.childElementCount)
|
if (!feature("announce.dismiss") || !el.childElementCount)
|
||||||
return EMPTY
|
return EMPTY
|
||||||
|
|
||||||
/* Support instant loading - see https://t.ly/3FTme */
|
/* Support instant navigation - see https://t.ly/3FTme */
|
||||||
if (!el.hidden) {
|
if (!el.hidden) {
|
||||||
const content = getElement(".md-typeset", el)
|
const content = getElement(".md-typeset", el)
|
||||||
if (__md_hash(content.innerHTML) === __md_get("__announce"))
|
if (__md_hash(content.innerHTML) === __md_get("__announce"))
|
||||||
|
@ -117,7 +117,7 @@ export function watchSearchQuery(
|
|||||||
first(active => !active)
|
first(active => !active)
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
const url = new URL(location.href)
|
const url = getLocation()
|
||||||
url.searchParams.delete("q")
|
url.searchParams.delete("q")
|
||||||
history.replaceState({}, "", `${url}`)
|
history.replaceState({}, "", `${url}`)
|
||||||
})
|
})
|
||||||
|
@ -70,6 +70,48 @@ interface SetupOptions {
|
|||||||
viewport$: Observable<Viewport> /* Viewport observable */
|
viewport$: Observable<Viewport> /* Viewport observable */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Helper functions
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a map of head elements for lookup and replacement
|
||||||
|
*
|
||||||
|
* @param head - Document head
|
||||||
|
*
|
||||||
|
* @returns Element map
|
||||||
|
*/
|
||||||
|
function lookup(head: HTMLHeadElement): Map<string, HTMLElement> {
|
||||||
|
const tags = new Map<string, HTMLElement>()
|
||||||
|
for (const el of getElements(":scope > *", head)) {
|
||||||
|
let html = el.outerHTML
|
||||||
|
|
||||||
|
// If the current element is a style sheet or script, we must resolve the
|
||||||
|
// URL relative to the current location and make it absolute, so it's easy
|
||||||
|
// to deduplicate it later on by comparing the outer HTML of tags. We must
|
||||||
|
// keep identical style sheets and scripts without replacing them.
|
||||||
|
for (const key of ["href", "src"]) {
|
||||||
|
const value = el.getAttribute(key)!
|
||||||
|
if (value === null)
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Resolve URL relative to current location
|
||||||
|
const url = new URL(value, getLocation())
|
||||||
|
const ref = el.cloneNode() as HTMLElement
|
||||||
|
|
||||||
|
// Set resolved URL and retrieve HTML for deduplication
|
||||||
|
ref.setAttribute(key, `${url}`)
|
||||||
|
html = ref.outerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index element in tag map
|
||||||
|
tags.set(html, el)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return tag map
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@ -84,7 +126,7 @@ interface SetupOptions {
|
|||||||
*
|
*
|
||||||
* @returns Document observable
|
* @returns Document observable
|
||||||
*/
|
*/
|
||||||
export function setupInstantLoading(
|
export function setupInstantNavigation(
|
||||||
{ location$, viewport$ }: SetupOptions
|
{ location$, viewport$ }: SetupOptions
|
||||||
): Observable<Document> {
|
): Observable<Document> {
|
||||||
const config = configuration()
|
const config = configuration()
|
||||||
@ -188,10 +230,10 @@ export function setupInstantLoading(
|
|||||||
history.pushState(null, "", url)
|
history.pushState(null, "", url)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Emit URL that should be fetched via instant loading on location subject,
|
// Emit URL that should be fetched via instant navigation on location subject,
|
||||||
// which was passed into this function. The idea is that instant loading can
|
// which was passed into this function. Instant navigation can be intercepted
|
||||||
// be intercepted by other parts of the application, which can synchronously
|
// by other parts of the application, which can synchronously back up or
|
||||||
// back up or restore state before instant loading happens.
|
// restore state before instant navigation happens.
|
||||||
instant$.subscribe(location$)
|
instant$.subscribe(location$)
|
||||||
|
|
||||||
// Fetch document - when fetching, we could use `responseType: document`, but
|
// Fetch document - when fetching, we could use `responseType: document`, but
|
||||||
@ -201,7 +243,7 @@ export function setupInstantLoading(
|
|||||||
// reason, we fall back to regular navigation and set the location explicitly,
|
// reason, we fall back to regular navigation and set the location explicitly,
|
||||||
// which will force-load the page. Furthermore, we must pre-warm the buffer
|
// which will force-load the page. Furthermore, we must pre-warm the buffer
|
||||||
// for the duplicate check, or the first click on an anchor link will also
|
// for the duplicate check, or the first click on an anchor link will also
|
||||||
// trigger an instant loading event, which doesn't make sense.
|
// trigger an instant navigation event, which doesn't make sense.
|
||||||
const response$ = location$
|
const response$ = location$
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(getLocation()),
|
startWith(getLocation()),
|
||||||
@ -210,7 +252,7 @@ export function setupInstantLoading(
|
|||||||
switchMap(url => request(url)
|
switchMap(url => request(url)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
setLocation(url)
|
setLocation(url, true)
|
||||||
return EMPTY
|
return EMPTY
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -218,24 +260,14 @@ export function setupInstantLoading(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Initialize the DOM parser, parse the returned HTML, and replace selected
|
// Initialize the DOM parser, parse the returned HTML, and replace selected
|
||||||
// meta tags and components before handing control down to the application
|
// components before handing control down to the application
|
||||||
const dom = new DOMParser()
|
const dom = new DOMParser()
|
||||||
const document$ = response$
|
const document$ = response$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(res => res.text()),
|
switchMap(res => res.text()),
|
||||||
switchMap(res => {
|
switchMap(res => {
|
||||||
const document = dom.parseFromString(res, "text/html")
|
const next = dom.parseFromString(res, "text/html")
|
||||||
for (const selector of [
|
for (const selector of [
|
||||||
|
|
||||||
// Meta tags
|
|
||||||
"title",
|
|
||||||
"link[rel=prev]",
|
|
||||||
"link[rel=next]",
|
|
||||||
"link[rel=canonical]",
|
|
||||||
"meta[name=author]",
|
|
||||||
"meta[name=description]",
|
|
||||||
|
|
||||||
// Components
|
|
||||||
"[data-md-component=announce]",
|
"[data-md-component=announce]",
|
||||||
"[data-md-component=container]",
|
"[data-md-component=container]",
|
||||||
"[data-md-component=header-topic]",
|
"[data-md-component=header-topic]",
|
||||||
@ -247,7 +279,7 @@ export function setupInstantLoading(
|
|||||||
: []
|
: []
|
||||||
]) {
|
]) {
|
||||||
const source = getOptionalElement(selector)
|
const source = getOptionalElement(selector)
|
||||||
const target = getOptionalElement(selector, document)
|
const target = getOptionalElement(selector, next)
|
||||||
if (
|
if (
|
||||||
typeof source !== "undefined" &&
|
typeof source !== "undefined" &&
|
||||||
typeof target !== "undefined"
|
typeof target !== "undefined"
|
||||||
@ -256,13 +288,28 @@ export function setupInstantLoading(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// After meta tags and components were replaced, re-evaluate scripts
|
// Update meta tags
|
||||||
|
const source = lookup(document.head)
|
||||||
|
const target = lookup(next.head)
|
||||||
|
for (const [html, el] of target) {
|
||||||
|
if (source.has(html)) {
|
||||||
|
source.delete(html)
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove meta tags that are not present in the new document
|
||||||
|
for (const el of source.values())
|
||||||
|
el.remove()
|
||||||
|
|
||||||
|
// After components and meta tags were replaced, re-evaluate scripts
|
||||||
// that were provided by the author as part of Markdown files
|
// that were provided by the author as part of Markdown files
|
||||||
const container = getComponentElement("container")
|
const container = getComponentElement("container")
|
||||||
return concat(getElements("script", container))
|
return concat(getElements("script", container))
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(el => {
|
switchMap(el => {
|
||||||
const script = document.createElement("script")
|
const script = next.createElement("script")
|
||||||
if (el.src) {
|
if (el.src) {
|
||||||
for (const name of el.getAttributeNames())
|
for (const name of el.getAttributeNames())
|
||||||
script.setAttribute(name, el.getAttribute(name)!)
|
script.setAttribute(name, el.getAttribute(name)!)
|
||||||
@ -281,7 +328,7 @@ export function setupInstantLoading(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ignoreElements(),
|
ignoreElements(),
|
||||||
endWith(document)
|
endWith(next)
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
share()
|
share()
|
||||||
|
@ -50,7 +50,7 @@ export type Sitemap = string[]
|
|||||||
* Preprocess a list of URLs
|
* Preprocess a list of URLs
|
||||||
*
|
*
|
||||||
* This function replaces the `site_url` in the sitemap with the actual base
|
* This function replaces the `site_url` in the sitemap with the actual base
|
||||||
* URL, to allow instant loading to work in occasions like Netlify previews.
|
* URL, to allow instant navigation to work in occasions like Netlify previews.
|
||||||
*
|
*
|
||||||
* @param urls - URLs
|
* @param urls - URLs
|
||||||
*
|
*
|
||||||
|
@ -112,8 +112,8 @@ export function setupVersionSelector(
|
|||||||
// find the same page, as we might have different deployments
|
// find the same page, as we might have different deployments
|
||||||
// due to aliases. However, if we're outside the version
|
// due to aliases. However, if we're outside the version
|
||||||
// selector, we must abort here, because we might otherwise
|
// selector, we must abort here, because we might otherwise
|
||||||
// interfere with instant loading. We need to refactor this
|
// interfere with instant navigation. We need to refactor this
|
||||||
// at some point together with instant loading.
|
// at some point together with instant navigation.
|
||||||
//
|
//
|
||||||
// See https://github.com/squidfunk/mkdocs-material/issues/4012
|
// See https://github.com/squidfunk/mkdocs-material/issues/4012
|
||||||
if (!ev.target.closest(".md-version")) {
|
if (!ev.target.closest(".md-version")) {
|
||||||
@ -143,7 +143,7 @@ export function setupVersionSelector(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe(url => setLocation(url))
|
.subscribe(url => setLocation(url, true))
|
||||||
|
|
||||||
/* Render version selector and warning */
|
/* Render version selector and warning */
|
||||||
combineLatest([versions$, current$])
|
combineLatest([versions$, current$])
|
||||||
@ -152,7 +152,7 @@ export function setupVersionSelector(
|
|||||||
topic.appendChild(renderVersionSelector(versions, current))
|
topic.appendChild(renderVersionSelector(versions, current))
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Integrate outdated version banner with instant loading */
|
/* Integrate outdated version banner with instant navigation */
|
||||||
document$.pipe(switchMap(() => current$))
|
document$.pipe(switchMap(() => current$))
|
||||||
.subscribe(current => {
|
.subscribe(current => {
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user