mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-12 01:50:52 +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 %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ 'assets/javascripts/custom.4eda089e.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/custom.a4bbca43.min.js' | url }}"></script>
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
{% 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 %}
|
||||
{{ script | script_tag }}
|
||||
{% endfor %}
|
||||
|
@ -38,7 +38,7 @@ export type Flag =
|
||||
| "header.autohide" /* Hide header */
|
||||
| "navigation.expand" /* Automatic expansion */
|
||||
| "navigation.indexes" /* Section pages */
|
||||
| "navigation.instant" /* Instant loading */
|
||||
| "navigation.instant" /* Instant navigation */
|
||||
| "navigation.sections" /* Section navigation */
|
||||
| "navigation.tabs" /* Tabs navigation */
|
||||
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
|
||||
|
@ -22,6 +22,9 @@
|
||||
|
||||
import { Subject } from "rxjs"
|
||||
|
||||
import { feature } from "~/_"
|
||||
import { h } from "~/utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -43,10 +46,31 @@ export function getLocation(): URL {
|
||||
/**
|
||||
* 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 {
|
||||
location.href = url.href
|
||||
export function setLocation(
|
||||
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 {
|
||||
SearchIndex,
|
||||
setupClipboardJS,
|
||||
setupInstantLoading,
|
||||
setupInstantNavigation,
|
||||
setupVersionSelector
|
||||
} from "./integrations"
|
||||
import {
|
||||
@ -143,9 +143,9 @@ const index$ = document.forms.namedItem("search")
|
||||
const alert$ = new Subject<string>()
|
||||
setupClipboardJS({ alert$ })
|
||||
|
||||
/* Set up instant loading, if enabled */
|
||||
/* Set up instant navigation, if enabled */
|
||||
if (feature("navigation.instant"))
|
||||
setupInstantLoading({ location$, viewport$ })
|
||||
setupInstantNavigation({ location$, viewport$ })
|
||||
.subscribe(document$)
|
||||
|
||||
/* Set up version selector */
|
||||
@ -175,10 +175,7 @@ keyboard$
|
||||
case ",":
|
||||
const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
|
||||
if (typeof prev !== "undefined")
|
||||
if (feature("navigation.instant"))
|
||||
location$.next(new URL(prev.href))
|
||||
else
|
||||
setLocation(prev)
|
||||
setLocation(prev)
|
||||
break
|
||||
|
||||
/* Go to next page */
|
||||
@ -186,10 +183,7 @@ keyboard$
|
||||
case ".":
|
||||
const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
|
||||
if (typeof next !== "undefined")
|
||||
if (feature("navigation.instant"))
|
||||
location$.next(new URL(next.href))
|
||||
else
|
||||
setLocation(next)
|
||||
setLocation(next)
|
||||
break
|
||||
|
||||
/* Expand navigation, see https://bit.ly/3ZjG5io */
|
||||
|
@ -82,7 +82,7 @@ export function mountAnnounce(
|
||||
if (!feature("announce.dismiss") || !el.childElementCount)
|
||||
return EMPTY
|
||||
|
||||
/* Support instant loading - see https://t.ly/3FTme */
|
||||
/* Support instant navigation - see https://t.ly/3FTme */
|
||||
if (!el.hidden) {
|
||||
const content = getElement(".md-typeset", el)
|
||||
if (__md_hash(content.innerHTML) === __md_get("__announce"))
|
||||
|
@ -117,7 +117,7 @@ export function watchSearchQuery(
|
||||
first(active => !active)
|
||||
)
|
||||
.subscribe(() => {
|
||||
const url = new URL(location.href)
|
||||
const url = getLocation()
|
||||
url.searchParams.delete("q")
|
||||
history.replaceState({}, "", `${url}`)
|
||||
})
|
||||
|
@ -70,6 +70,48 @@ interface SetupOptions {
|
||||
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
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -84,7 +126,7 @@ interface SetupOptions {
|
||||
*
|
||||
* @returns Document observable
|
||||
*/
|
||||
export function setupInstantLoading(
|
||||
export function setupInstantNavigation(
|
||||
{ location$, viewport$ }: SetupOptions
|
||||
): Observable<Document> {
|
||||
const config = configuration()
|
||||
@ -188,10 +230,10 @@ export function setupInstantLoading(
|
||||
history.pushState(null, "", url)
|
||||
})
|
||||
|
||||
// Emit URL that should be fetched via instant loading on location subject,
|
||||
// which was passed into this function. The idea is that instant loading can
|
||||
// be intercepted by other parts of the application, which can synchronously
|
||||
// back up or restore state before instant loading happens.
|
||||
// Emit URL that should be fetched via instant navigation on location subject,
|
||||
// which was passed into this function. Instant navigation can be intercepted
|
||||
// by other parts of the application, which can synchronously back up or
|
||||
// restore state before instant navigation happens.
|
||||
instant$.subscribe(location$)
|
||||
|
||||
// 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,
|
||||
// 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
|
||||
// trigger an instant loading event, which doesn't make sense.
|
||||
// trigger an instant navigation event, which doesn't make sense.
|
||||
const response$ = location$
|
||||
.pipe(
|
||||
startWith(getLocation()),
|
||||
@ -210,7 +252,7 @@ export function setupInstantLoading(
|
||||
switchMap(url => request(url)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
setLocation(url)
|
||||
setLocation(url, true)
|
||||
return EMPTY
|
||||
})
|
||||
)
|
||||
@ -218,24 +260,14 @@ export function setupInstantLoading(
|
||||
)
|
||||
|
||||
// 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 document$ = response$
|
||||
.pipe(
|
||||
switchMap(res => res.text()),
|
||||
switchMap(res => {
|
||||
const document = dom.parseFromString(res, "text/html")
|
||||
const next = dom.parseFromString(res, "text/html")
|
||||
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=container]",
|
||||
"[data-md-component=header-topic]",
|
||||
@ -247,7 +279,7 @@ export function setupInstantLoading(
|
||||
: []
|
||||
]) {
|
||||
const source = getOptionalElement(selector)
|
||||
const target = getOptionalElement(selector, document)
|
||||
const target = getOptionalElement(selector, next)
|
||||
if (
|
||||
typeof source !== "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
|
||||
const container = getComponentElement("container")
|
||||
return concat(getElements("script", container))
|
||||
.pipe(
|
||||
switchMap(el => {
|
||||
const script = document.createElement("script")
|
||||
const script = next.createElement("script")
|
||||
if (el.src) {
|
||||
for (const name of el.getAttributeNames())
|
||||
script.setAttribute(name, el.getAttribute(name)!)
|
||||
@ -281,7 +328,7 @@ export function setupInstantLoading(
|
||||
}
|
||||
}),
|
||||
ignoreElements(),
|
||||
endWith(document)
|
||||
endWith(next)
|
||||
)
|
||||
}),
|
||||
share()
|
||||
|
@ -50,7 +50,7 @@ export type Sitemap = string[]
|
||||
* Preprocess a list of URLs
|
||||
*
|
||||
* 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
|
||||
*
|
||||
|
@ -112,8 +112,8 @@ export function setupVersionSelector(
|
||||
// find the same page, as we might have different deployments
|
||||
// due to aliases. However, if we're outside the version
|
||||
// selector, we must abort here, because we might otherwise
|
||||
// interfere with instant loading. We need to refactor this
|
||||
// at some point together with instant loading.
|
||||
// interfere with instant navigation. We need to refactor this
|
||||
// at some point together with instant navigation.
|
||||
//
|
||||
// See https://github.com/squidfunk/mkdocs-material/issues/4012
|
||||
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 */
|
||||
combineLatest([versions$, current$])
|
||||
@ -152,7 +152,7 @@ export function setupVersionSelector(
|
||||
topic.appendChild(renderVersionSelector(versions, current))
|
||||
})
|
||||
|
||||
/* Integrate outdated version banner with instant loading */
|
||||
/* Integrate outdated version banner with instant navigation */
|
||||
document$.pipe(switchMap(() => current$))
|
||||
.subscribe(current => {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user