mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-23 06:13:38 -04:00
Merge of Insiders features tied to 'Black Pearl' funding goal
This commit is contained in:
@@ -35,6 +35,7 @@ export type Flag =
|
||||
| "navigation.instant" /* Instant loading */
|
||||
| "navigation.sections" /* Sections navigation */
|
||||
| "navigation.tabs" /* Tabs navigation */
|
||||
| "navigation.top" /* Back-to-top button */
|
||||
| "toc.integrate" /* Integrated table of contents */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
@@ -28,3 +28,4 @@ export * from "./search"
|
||||
export * from "./sidebar"
|
||||
export * from "./source"
|
||||
export * from "./tabs"
|
||||
export * from "./top"
|
||||
|
||||
48
src/assets/javascripts/actions/top/index.ts
Normal file
48
src/assets/javascripts/actions/top/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set back-to-top state
|
||||
*
|
||||
* @param el - Back-to-top element
|
||||
* @param state - Back-to-top state
|
||||
*/
|
||||
export function setBackToTopState(
|
||||
el: HTMLElement, state: "hidden"
|
||||
): void {
|
||||
el.setAttribute("data-md-state", state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset back-to-top state
|
||||
*
|
||||
* @param el - Back-to-top element
|
||||
*/
|
||||
export function resetBackToTopState(
|
||||
el: HTMLElement
|
||||
): void {
|
||||
el.removeAttribute("data-md-state")
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
export function request(
|
||||
url: URL | string, options: RequestInit = { credentials: "same-origin" }
|
||||
): Observable<Response> {
|
||||
return from(fetch(url.toString(), options))
|
||||
return from(fetch(`${url}`, options))
|
||||
.pipe(
|
||||
filter(res => res.status === 200),
|
||||
)
|
||||
|
||||
@@ -48,10 +48,12 @@ import {
|
||||
import {
|
||||
getComponentElement,
|
||||
getComponentElements,
|
||||
mountBackToTop,
|
||||
mountContent,
|
||||
mountDialog,
|
||||
mountHeader,
|
||||
mountHeaderTitle,
|
||||
mountPalette,
|
||||
mountSearch,
|
||||
mountSidebar,
|
||||
mountSource,
|
||||
@@ -173,17 +175,17 @@ const control$ = merge(
|
||||
...getComponentElements("header")
|
||||
.map(el => mountHeader(el, { viewport$, header$, main$ })),
|
||||
|
||||
/* Color palette */
|
||||
...getComponentElements("palette")
|
||||
.map(el => mountPalette(el)),
|
||||
|
||||
/* Search */
|
||||
...getComponentElements("search")
|
||||
.map(el => mountSearch(el, { index$, keyboard$ })),
|
||||
|
||||
/* Repository information */
|
||||
...getComponentElements("source")
|
||||
.map(el => mountSource(el)),
|
||||
|
||||
/* Navigation tabs */
|
||||
...getComponentElements("tabs")
|
||||
.map(el => mountTabs(el, { viewport$, header$ })),
|
||||
.map(el => mountSource(el))
|
||||
)
|
||||
|
||||
/* Set up content component observables */
|
||||
@@ -204,9 +206,17 @@ const content$ = defer(() => merge(
|
||||
: at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))
|
||||
),
|
||||
|
||||
/* Navigation tabs */
|
||||
...getComponentElements("tabs")
|
||||
.map(el => mountTabs(el, { viewport$, header$ })),
|
||||
|
||||
/* Table of contents */
|
||||
...getComponentElements("toc")
|
||||
.map(el => mountTableOfContents(el, { viewport$, header$ })),
|
||||
|
||||
/* Back-to-top button */
|
||||
...getComponentElements("top")
|
||||
.map(el => mountBackToTop(el, { viewport$, main$ }))
|
||||
))
|
||||
|
||||
/* Set up component observables */
|
||||
|
||||
@@ -38,6 +38,7 @@ export type ComponentType =
|
||||
| "header-title" /* Header title */
|
||||
| "header-topic" /* Header topic */
|
||||
| "main" /* Main area */
|
||||
| "palette" /* Color palette */
|
||||
| "search" /* Search */
|
||||
| "search-query" /* Search input */
|
||||
| "search-result" /* Search results */
|
||||
@@ -46,6 +47,7 @@ export type ComponentType =
|
||||
| "source" /* Repository information */
|
||||
| "tabs" /* Navigation tabs */
|
||||
| "toc" /* Table of contents */
|
||||
| "top" /* Back-to-top button */
|
||||
|
||||
/**
|
||||
* A component
|
||||
@@ -77,6 +79,7 @@ interface ComponentTypeMap {
|
||||
"header-title": HTMLElement /* Header title */
|
||||
"header-topic": HTMLElement /* Header topic */
|
||||
"main": HTMLElement /* Main area */
|
||||
"palette": HTMLElement /* Color palette */
|
||||
"search": HTMLElement /* Search */
|
||||
"search-query": HTMLInputElement /* Search input */
|
||||
"search-result": HTMLElement /* Search results */
|
||||
@@ -85,6 +88,7 @@ interface ComponentTypeMap {
|
||||
"source": HTMLAnchorElement /* Repository information */
|
||||
"tabs": HTMLElement /* Navigation tabs */
|
||||
"toc": HTMLElement /* Table of contents */
|
||||
"top": HTMLAnchorElement /* Back-to-top button */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
|
||||
export * from "./_"
|
||||
export * from "./code"
|
||||
export * from "./details"
|
||||
export * from "./table"
|
||||
|
||||
@@ -120,7 +120,7 @@ function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Compute threshold for autohiding */
|
||||
/* Compute threshold for hiding */
|
||||
const search$ = watchToggle("search")
|
||||
return combineLatest([viewport$, search$])
|
||||
.pipe(
|
||||
|
||||
@@ -25,8 +25,10 @@ export * from "./content"
|
||||
export * from "./dialog"
|
||||
export * from "./header"
|
||||
export * from "./main"
|
||||
export * from "./palette"
|
||||
export * from "./search"
|
||||
export * from "./sidebar"
|
||||
export * from "./source"
|
||||
export * from "./tabs"
|
||||
export * from "./toc"
|
||||
export * from "./top"
|
||||
|
||||
147
src/assets/javascripts/components/palette/index.ts
Normal file
147
src/assets/javascripts/components/palette/index.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
fromEvent,
|
||||
of
|
||||
} from "rxjs"
|
||||
import {
|
||||
finalize,
|
||||
map,
|
||||
mapTo,
|
||||
mergeMap,
|
||||
shareReplay,
|
||||
startWith,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { getElements } from "~/browser"
|
||||
|
||||
import { Component } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Palette colors
|
||||
*/
|
||||
export interface PaletteColor {
|
||||
scheme?: string /* Color scheme */
|
||||
primary?: string /* Primary color */
|
||||
accent?: string /* Accent color */
|
||||
}
|
||||
|
||||
/**
|
||||
* Palette
|
||||
*/
|
||||
export interface Palette {
|
||||
index: number /* Palette index */
|
||||
color: PaletteColor /* Palette colors */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch color palette
|
||||
*
|
||||
* @param inputs - Color palette element
|
||||
*
|
||||
* @returns Color palette observable
|
||||
*/
|
||||
export function watchPalette(
|
||||
inputs: HTMLInputElement[]
|
||||
): Observable<Palette> {
|
||||
const data = localStorage.getItem(__prefix("__palette"))!
|
||||
const current = JSON.parse(data) || {
|
||||
index: inputs.findIndex(input => (
|
||||
matchMedia(input.getAttribute("data-md-color-media")!).matches
|
||||
))
|
||||
}
|
||||
|
||||
/* Emit changes in color palette */
|
||||
const palette$ = of(...inputs)
|
||||
.pipe(
|
||||
mergeMap(input => fromEvent(input, "change")
|
||||
.pipe(
|
||||
mapTo(input)
|
||||
)
|
||||
),
|
||||
startWith(inputs[Math.max(0, current.index)]),
|
||||
map(input => ({
|
||||
index: inputs.indexOf(input),
|
||||
color: {
|
||||
scheme: input.getAttribute("data-md-color-scheme"),
|
||||
primary: input.getAttribute("data-md-color-primary"),
|
||||
accent: input.getAttribute("data-md-color-accent")
|
||||
}
|
||||
} as Palette)),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Persist preference in local storage */
|
||||
palette$.subscribe(palette => {
|
||||
localStorage.setItem(__prefix("__palette"), JSON.stringify(palette))
|
||||
})
|
||||
|
||||
/* Return palette */
|
||||
return palette$
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount color palette
|
||||
*
|
||||
* @param el - Color palette element
|
||||
*
|
||||
* @returns Color palette component observable
|
||||
*/
|
||||
export function mountPalette(
|
||||
el: HTMLElement
|
||||
): Observable<Component<Palette>> {
|
||||
const internal$ = new Subject<Palette>()
|
||||
|
||||
/* Set color palette */
|
||||
internal$.subscribe(palette => {
|
||||
for (const [key, value] of Object.entries(palette.color))
|
||||
if (typeof value === "string")
|
||||
document.body.setAttribute(`data-md-color-${key}`, value)
|
||||
|
||||
/* Toggle visibility */
|
||||
for (let index = 0; index < inputs.length; index++) {
|
||||
const label = inputs[index].nextElementSibling as HTMLElement
|
||||
label.hidden = palette.index !== index
|
||||
}
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
const inputs = getElements<HTMLInputElement>("input", el)
|
||||
return watchPalette(inputs)
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
|
||||
import { setSourceFacts, setSourceState } from "~/actions"
|
||||
import { renderSourceFacts } from "~/templates"
|
||||
import { digest } from "~/utilities"
|
||||
|
||||
import { Component } from "../../_"
|
||||
import { SourceFacts, fetchSourceFacts } from "../facts"
|
||||
@@ -75,14 +74,14 @@ export function watchSource(
|
||||
el: HTMLAnchorElement
|
||||
): Observable<Source> {
|
||||
return fetch$ ||= defer(() => {
|
||||
const data = sessionStorage.getItem(digest("__repo"))
|
||||
const data = sessionStorage.getItem(__prefix("__source"))
|
||||
if (data) {
|
||||
return of<SourceFacts>(JSON.parse(data))
|
||||
} else {
|
||||
const value$ = fetchSourceFacts(el.href)
|
||||
value$.subscribe(value => {
|
||||
try {
|
||||
sessionStorage.setItem(digest("__repo"), JSON.stringify(value))
|
||||
sessionStorage.setItem(__prefix("__source"), JSON.stringify(value))
|
||||
} catch (err) {
|
||||
/* Uncritical, just swallow */
|
||||
}
|
||||
@@ -94,7 +93,7 @@ export function watchSource(
|
||||
})
|
||||
.pipe(
|
||||
catchError(() => NEVER),
|
||||
filter(facts => facts.length > 0),
|
||||
filter(facts => Object.keys(facts).length > 0),
|
||||
map(facts => ({ facts })),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
@@ -29,10 +29,30 @@ import { fetchSourceFactsFromGitLab } from "../gitlab"
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Repository facts for repositories
|
||||
*/
|
||||
export interface RepositoryFacts {
|
||||
stars?: number /* Number of stars */
|
||||
forks?: number /* Number of forks */
|
||||
version?: string /* Latest version */
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository facts for organizations
|
||||
*/
|
||||
export interface OrganizationFacts {
|
||||
repositories?: number /* Number of repositories */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Repository facts
|
||||
*/
|
||||
export type SourceFacts = string[]
|
||||
export type SourceFacts =
|
||||
| RepositoryFacts
|
||||
| OrganizationFacts
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
|
||||
@@ -21,14 +21,24 @@
|
||||
*/
|
||||
|
||||
import { Repo, User } from "github-types"
|
||||
import { Observable } from "rxjs"
|
||||
import { Observable, zip } from "rxjs"
|
||||
import { defaultIfEmpty, map } from "rxjs/operators"
|
||||
|
||||
import { requestJSON } from "~/browser"
|
||||
import { round } from "~/utilities"
|
||||
|
||||
import { SourceFacts } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* GitHub release (partial)
|
||||
*/
|
||||
interface Release {
|
||||
tag_name: string /* Tag name */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -44,29 +54,42 @@ import { SourceFacts } from "../_"
|
||||
export function fetchSourceFactsFromGitHub(
|
||||
user: string, repo?: string
|
||||
): Observable<SourceFacts> {
|
||||
const url = typeof repo !== "undefined"
|
||||
? `https://api.github.com/repos/${user}/${repo}`
|
||||
: `https://api.github.com/users/${user}`
|
||||
return requestJSON<Repo & User>(url)
|
||||
.pipe(
|
||||
map(data => {
|
||||
if (typeof repo !== "undefined") {
|
||||
const url = `https://api.github.com/repos/${user}/${repo}`
|
||||
return zip(
|
||||
|
||||
/* GitHub repository */
|
||||
if (typeof repo !== "undefined") {
|
||||
const { stargazers_count, forks_count }: Repo = data
|
||||
return [
|
||||
`${round(stargazers_count!)} Stars`,
|
||||
`${round(forks_count!)} Forks`
|
||||
]
|
||||
/* Fetch version */
|
||||
requestJSON<Release>(`${url}/releases/latest`)
|
||||
.pipe(
|
||||
map(release => ({
|
||||
version: release.tag_name
|
||||
})),
|
||||
defaultIfEmpty({})
|
||||
),
|
||||
|
||||
/* GitHub user/organization */
|
||||
} else {
|
||||
const { public_repos }: User = data
|
||||
return [
|
||||
`${round(public_repos!)} Repositories`
|
||||
]
|
||||
}
|
||||
}),
|
||||
defaultIfEmpty([])
|
||||
/* Fetch stars and forks */
|
||||
requestJSON<Repo>(url)
|
||||
.pipe(
|
||||
map(info => ({
|
||||
stars: info.stargazers_count,
|
||||
forks: info.forks_count
|
||||
})),
|
||||
defaultIfEmpty({})
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
map(([release, info]) => ({ ...release, ...info }))
|
||||
)
|
||||
|
||||
/* User or organization */
|
||||
} else {
|
||||
const url = `https://api.github.com/repos/${user}`
|
||||
return requestJSON<User>(url)
|
||||
.pipe(
|
||||
map(info => ({
|
||||
repositories: info.public_repos
|
||||
})),
|
||||
defaultIfEmpty({})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Observable } from "rxjs"
|
||||
import { defaultIfEmpty, map } from "rxjs/operators"
|
||||
|
||||
import { requestJSON } from "~/browser"
|
||||
import { round } from "~/utilities"
|
||||
|
||||
import { SourceFacts } from "../_"
|
||||
|
||||
@@ -47,10 +46,10 @@ export function fetchSourceFactsFromGitLab(
|
||||
const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`
|
||||
return requestJSON<ProjectSchema>(url)
|
||||
.pipe(
|
||||
map(({ star_count, forks_count }) => ([
|
||||
`${round(star_count)} Stars`,
|
||||
`${round(forks_count)} Forks`
|
||||
])),
|
||||
defaultIfEmpty([])
|
||||
map(({ star_count, forks_count }) => ({
|
||||
stars: star_count,
|
||||
forks: forks_count
|
||||
})),
|
||||
defaultIfEmpty({})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,16 @@ import {
|
||||
finalize,
|
||||
map,
|
||||
observeOn,
|
||||
switchMap,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { resetTabsState, setTabsState } from "~/actions"
|
||||
import { Viewport, watchViewportAt } from "~/browser"
|
||||
import {
|
||||
Viewport,
|
||||
watchElementSize,
|
||||
watchViewportAt
|
||||
} from "~/browser"
|
||||
|
||||
import { Component } from "../_"
|
||||
import { Header } from "../header"
|
||||
@@ -81,8 +86,9 @@ interface MountOptions {
|
||||
export function watchTabs(
|
||||
el: HTMLElement, { viewport$, header$ }: WatchOptions
|
||||
): Observable<Tabs> {
|
||||
return watchViewportAt(el, { header$, viewport$ })
|
||||
return watchElementSize(document.body)
|
||||
.pipe(
|
||||
switchMap(() => watchViewportAt(el, { header$, viewport$ })),
|
||||
map(({ offset: { y } }) => {
|
||||
return {
|
||||
hidden: y >= 10
|
||||
|
||||
160
src/assets/javascripts/components/top/index.ts
Normal file
160
src/assets/javascripts/components/top/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
animationFrameScheduler,
|
||||
combineLatest
|
||||
} from "rxjs"
|
||||
import {
|
||||
bufferCount,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
finalize,
|
||||
map,
|
||||
observeOn,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { resetBackToTopState, setBackToTopState } from "~/actions"
|
||||
import { Viewport } from "~/browser"
|
||||
|
||||
import { Component } from "../_"
|
||||
import { Main } from "../main"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Back-to-top button
|
||||
*/
|
||||
export interface BackToTop {
|
||||
hidden: boolean /* User scrolled up */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
main$: Observable<Main> /* Main area observable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
main$: Observable<Main> /* Main area observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch back-to-top
|
||||
*
|
||||
* @param _el - Back-to-top element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Back-to-top observable
|
||||
*/
|
||||
export function watchBackToTop(
|
||||
_el: HTMLElement, { viewport$, main$ }: WatchOptions
|
||||
): Observable<BackToTop> {
|
||||
|
||||
/* Compute direction */
|
||||
const direction$ = viewport$
|
||||
.pipe(
|
||||
map(({ offset: { y } }) => y),
|
||||
bufferCount(2, 1),
|
||||
map(([a, b]) => a > b),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Compute whether button should be hidden */
|
||||
const hidden$ = main$
|
||||
.pipe(
|
||||
distinctUntilKeyChanged("active")
|
||||
)
|
||||
|
||||
/* Compute threshold for hiding */
|
||||
return combineLatest([hidden$, direction$])
|
||||
.pipe(
|
||||
map(([{ active }, direction]) => ({
|
||||
hidden: !(active && direction)
|
||||
})),
|
||||
distinctUntilChanged((a, b) => (
|
||||
a.hidden === b.hidden
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount back-to-top
|
||||
*
|
||||
* @param el - Back-to-top element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Back-to-top component observable
|
||||
*/
|
||||
export function mountBackToTop(
|
||||
el: HTMLElement, options: MountOptions
|
||||
): Observable<Component<BackToTop>> {
|
||||
const internal$ = new Subject<BackToTop>()
|
||||
internal$
|
||||
.pipe(
|
||||
observeOn(animationFrameScheduler)
|
||||
)
|
||||
.subscribe({
|
||||
|
||||
/* Update state */
|
||||
next({ hidden }) {
|
||||
if (hidden)
|
||||
setBackToTopState(el, "hidden")
|
||||
else
|
||||
resetBackToTopState(el)
|
||||
},
|
||||
|
||||
/* Reset on complete */
|
||||
complete() {
|
||||
resetBackToTopState(el)
|
||||
}
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
return watchBackToTop(el, options)
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export function setupInstantLoading(
|
||||
sample(response$)
|
||||
)
|
||||
.subscribe(({ url }) => {
|
||||
history.pushState({}, "", url.toString())
|
||||
history.pushState({}, "", `${url}`)
|
||||
})
|
||||
|
||||
/* Parse and emit fetched document */
|
||||
@@ -274,14 +274,14 @@ export function setupInstantLoading(
|
||||
|
||||
/* Meta tags */
|
||||
"title",
|
||||
"link[rel='canonical']",
|
||||
"meta[name='author']",
|
||||
"meta[name='description']",
|
||||
"link[rel=canonical]",
|
||||
"meta[name=author]",
|
||||
"meta[name=description]",
|
||||
|
||||
/* Components */
|
||||
"[data-md-component=announce]",
|
||||
"[data-md-component=header-topic]",
|
||||
"[data-md-component=container]",
|
||||
"[data-md-component=header-topic]",
|
||||
"[data-md-component=logo], .md-logo", // compat
|
||||
"[data-md-component=skip]"
|
||||
]) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { SourceFacts } from "~/components"
|
||||
import { h } from "~/utilities"
|
||||
import { h, round } from "~/utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -37,8 +37,10 @@ import { h } from "~/utilities"
|
||||
export function renderSourceFacts(facts: SourceFacts): HTMLElement {
|
||||
return (
|
||||
<ul class="md-source__facts">
|
||||
{facts.map(fact => (
|
||||
<li class="md-source__fact">{fact}</li>
|
||||
{Object.entries(facts).map(([key, value]) => (
|
||||
<li class={`md-source__fact md-source__fact--${key}`}>
|
||||
{typeof value === "number" ? round(value) : value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { configuration } from "~/_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -90,18 +88,3 @@ export function hash(value: string): number {
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a digest to a value to ensure uniqueness
|
||||
*
|
||||
* When a single account hosts multiple sites on the same GitHub Pages domain,
|
||||
* entries would collide, because session and local storage are not unique.
|
||||
*
|
||||
* @param value - Value
|
||||
*
|
||||
* @returns Value with digest
|
||||
*/
|
||||
export function digest(value: string): string {
|
||||
const config = configuration()
|
||||
return `${value}[${hash(config.base)}]`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user