Refactored Clipboard.js integration and table patching

This commit is contained in:
squidfunk 2020-02-14 14:47:44 +01:00
parent 8dcbbf2e84
commit 864f612005
14 changed files with 202 additions and 98 deletions

View File

@ -26,7 +26,6 @@
import "../stylesheets/app.scss"
import "../stylesheets/app-palette.scss"
import * as Clipboard from "clipboard"
import { identity, values } from "ramda"
import {
EMPTY,
@ -44,7 +43,6 @@ import {
import {
mountHero,
mountTableOfContents,
mountTabs,
} from "./components"
import {
@ -61,8 +59,7 @@ import {
} from "./observables"
import { setupSearchWorker } from "./workers"
import { renderSource } from "templates"
import { renderClipboard } from "templates/clipboard"
import { fetchGitHubStats } from "modules/source/github"
import { fetchGitHubStats } from "integrations/source/github"
import { renderTable } from "templates/table"
import { setToggle } from "actions"
import {
@ -71,9 +68,12 @@ import {
mountMain,
mountNavigation,
mountSearch,
mountTableOfContents,
useComponent,
watchComponentMap
} from "components2"
import { mountClipboard } from "./integrations/clipboard"
import { patchTables } from "patches/table"
/* ----------------------------------------------------------------------------
* Types
@ -212,10 +212,10 @@ export function initialize(config: unknown) {
// pass config here!?
const document$ = watchDocument()
const hash$ = watchLocationHash()
const keyboard$ = watchKeyboard()
const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)")
const keyboard$ = watchKeyboard()
/* ----------------------------------------------------------------------- */
@ -285,16 +285,16 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */
// patches... table, details, pre, ...
/* Open details before printing */
merge(
watchMedia("print").pipe(filter(identity)), // Webkit
fromEvent(window, "beforeprint") // IE, FF
fromEvent(window, "beforeprint") // IE, FF
)
.subscribe(() => {
const details = getElements("details")
Array.prototype.forEach.call(details, detail => {
for (const detail of getElements("details"))
detail.setAttribute("open", "")
})
})
// Close drawer and search on hash change
@ -311,25 +311,10 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */
/* Clipboard.js integration */
if (Clipboard.isSupported()) {
const blocks = getElements("pre > code")
for (const [index, block] of blocks.entries()) {
const parent = block.parentElement!
parent.id = `__code_${index}`
parent.insertBefore(renderClipboard(parent.id), block)
}
// watchClipboard
/* Initialize Clipboard listener */
const copy = new Clipboard(".md-clipboard") // create observable...
/* Success handler */
// copy.on("success", action => {
// alert("Copied to clipboard") // TODO: integrate snackbar
// // TODO: add a snackbar/notification
// })
}
mountClipboard({ document$ })
.subscribe(console.log)
// TODO: WIP repo rendering
repository().subscribe(facts => {
@ -344,15 +329,18 @@ export function initialize(config: unknown) {
}
})
/* Wrap all data tables for better overflow scrolling */
const tables = getElements<HTMLTableElement>("table:not([class])")
const placeholder = document.createElement("table")
tables.forEach(table => {
table.replaceWith(placeholder)
placeholder.replaceWith(renderTable(table))
})
// TODO: this is just a re-rendering patch...
// patchTables({ document$ })
// .subscribe(console.log)
// search lock
// /* Wrap all data tables for better overflow scrolling */
// const placeholder = document.createElement("table")
// for (const table of getElements<HTMLTableElement>("table:not([class])")) {
// table.replaceWith(placeholder)
// placeholder.replaceWith(renderTable(table))
// }
// accidentally triggers on resize
let lastOffset = 0
tablet$.pipe(
switchMap(active => {

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2016-2020 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 * as ClipboardJS from "clipboard"
import { NEVER, Observable, fromEventPattern } from "rxjs"
import { shareReplay, switchMap, tap } from "rxjs/operators"
import { getElements } from "observables"
import { renderClipboard } from "templates"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
document$: Observable<Document> /* Document observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount clipboard
*
* This function implements the Clipboard.js integration and injects a button
* into all code blocks when the document changes.
*
* @param options - Options
*
* @return Clipboard observable
*/
export function mountClipboard(
{ document$ }: MountOptions
): Observable<ClipboardJS.Event> {
if (ClipboardJS.isSupported()) {
return document$
.pipe(
/* Inject 'copy-to-clipboard' buttons */
tap(() => {
const blocks = getElements("pre > code")
for (const [index, block] of blocks.entries()) {
const parent = block.parentElement!
parent.id = `__code_${index}`
parent.insertBefore(renderClipboard(parent.id), block)
}
}),
/* Initialize and mount clipboard */
switchMap(() => {
return fromEventPattern<ClipboardJS.Event>(next => {
const clipboard = new ClipboardJS(".md-clipboard")
clipboard.on("success", next)
})
}),
shareReplay(1)
)
} else {
return NEVER
}
}

View File

@ -20,8 +20,6 @@
* IN THE SOFTWARE.
*/
import * as lunr from "expose-loader?lunr!lunr"
import {
ArticleDocument,
SearchDocumentMap,

View File

@ -23,15 +23,6 @@
import { Observable, fromEvent } from "rxjs"
import { mapTo, shareReplay } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for document load events
*/
const load$ = fromEvent(document, "DOMContentLoaded")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -42,7 +33,7 @@ const load$ = fromEvent(document, "DOMContentLoaded")
* @return Document observable
*/
export function watchDocument(): Observable<Document> {
return load$
return fromEvent(document, "DOMContentLoaded")
.pipe(
mapTo(document),
shareReplay(1)

View File

@ -35,15 +35,6 @@ export interface Key {
claim(): void /* Key claim */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window keyboard events
*/
const keydown$ = fromEvent<KeyboardEvent>(window, "keydown")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -78,7 +69,7 @@ export function mayReceiveKeyboardEvents(el: HTMLElement) {
* @return Keyboard observable
*/
export function watchKeyboard(): Observable<Key> {
return keydown$
return fromEvent<KeyboardEvent>(window, "keydown")
.pipe(
filter(ev => !(ev.shiftKey || ev.metaKey || ev.ctrlKey)),
map(ev => ({

View File

@ -23,20 +23,6 @@
import { Observable, Subject, fromEvent } from "rxjs"
import { filter, map, share } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window hash change events
*/
const hashchange$ = fromEvent<HashChangeEvent>(window, "hashchange")
/**
* Observable for window pop state events
*/
const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -48,7 +34,7 @@ const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
*/
export function watchLocation(): Subject<string> {
const location$ = new Subject<string>()
popstate$
fromEvent<PopStateEvent>(window, "popstate")
.pipe(
map(() => location.href),
share()
@ -65,7 +51,7 @@ export function watchLocation(): Subject<string> {
* @return Location hash observable
*/
export function watchLocationHash(): Observable<string> {
return hashchange$
return fromEvent<HashChangeEvent>(window, "hashchange")
.pipe(
map(() => location.hash),
filter(hash => hash.length > 0),

View File

@ -51,20 +51,6 @@ export interface Viewport {
size: ViewportSize /* Viewport size */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Observable for window scroll events
*/
const scroll$ = fromEvent<UIEvent>(window, "scroll")
/**
* Observable for window resize events
*/
const resize$ = fromEvent<UIEvent>(window, "resize")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -101,7 +87,10 @@ export function getViewportSize(): ViewportSize {
* @return Viewport offset observable
*/
export function watchViewportOffset(): Observable<ViewportOffset> {
return merge(scroll$, resize$)
return merge(
fromEvent<UIEvent>(window, "scroll"),
fromEvent<UIEvent>(window, "resize")
)
.pipe(
map(getViewportOffset),
startWith(getViewportOffset())
@ -114,7 +103,7 @@ export function watchViewportOffset(): Observable<ViewportOffset> {
* @return Viewport size observable
*/
export function watchViewportSize(): Observable<ViewportSize> {
return resize$
return fromEvent<UIEvent>(window, "resize")
.pipe(
map(getViewportSize),
startWith(getViewportSize())

View File

@ -20,4 +20,4 @@
* IN THE SOFTWARE.
*/
export * from "./search"
export * from "./table"

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2016-2020 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 } from "rxjs"
import { map, shareReplay, tap } from "rxjs/operators"
import { getElements } from "observables"
import { renderTable } from "templates"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
document$: Observable<Document> /* Document observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Patch all `table` elements
*
* @param options - Options
*
* @return Table elements observable
*/
export function patchTables(
{ document$ }: MountOptions
): Observable<HTMLTableElement[]> {
return document$
.pipe(
map(() => getElements<HTMLTableElement>("table:not([class])")),
tap(els => {
const placeholder = document.createElement("table")
for (const el of els) {
el.replaceWith(placeholder)
placeholder.replaceWith(renderTable(el))
}
}),
shareReplay(1)
)
}

View File

@ -24,7 +24,7 @@ import { Subject } from "rxjs"
import { ajax } from "rxjs/ajax"
import { map, pluck } from "rxjs/operators"
import { SearchIndexOptions } from "integrations"
import { SearchIndexOptions } from "integrations/search"
import { WorkerHandler, watchWorker } from "observables"
import {

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { SearchIndex, SearchIndexConfig } from "integrations"
import { SearchIndex, SearchIndexConfig } from "integrations/search"
import { SearchMessage, SearchMessageType } from "../message"

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { SearchIndexOptions, SearchResult } from "integrations"
import { SearchIndexOptions, SearchResult } from "integrations/search"
/* ----------------------------------------------------------------------------
* Types

7
typings/lunr.d.ts vendored
View File

@ -20,7 +20,8 @@
* IN THE SOFTWARE.
*/
declare module "expose-loader?lunr!lunr" {
import * as lunr from "lunr"
export = lunr
import * as lunr from "lunr"
declare global {
const lunr: typeof lunr
}

View File

@ -27,7 +27,7 @@ import { minify as minhtml } from "html-minifier"
import * as path from "path"
import { toPairs } from "ramda"
import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin"
import { Configuration } from "webpack"
import { Configuration, ProvidePlugin } from "webpack"
import * as AssetsManifestPlugin from "webpack-assets-manifest"
/* ----------------------------------------------------------------------------
@ -280,7 +280,17 @@ export default (_env: never, args: Configuration): Configuration[] => {
filename: `[name]${hash}.js`,
hashDigestLength: 8,
libraryTarget: "var"
}
},
/* Plugins */
plugins: [
...base.plugins,
/* Inject globals */
new ProvidePlugin({
lunr: "lunr"
})
]
},
/* Packer worker */