Skip to content
Snippets Groups Projects
Commit 70cb57e1 authored by squidfunk's avatar squidfunk
Browse files

Refactored navigation blur component as observable

parent 9969cafd
Branches
No related tags found
No related merge requests found
......@@ -257,9 +257,9 @@ function initialize(config) { // eslint-disable-line func-style
"[data-md-component=header]")))
/* Component: link blurring for table of contents */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(window, "scroll",
new Material.Nav.Blur("[data-md-component=toc] [href]")))
// new Material.Event.MatchMedia("(min-width: 960px)",
// new Material.Event.Listener(window, "scroll",
// new Material.Nav.Blur("[data-md-component=toc] [href]")))
/* Component: collapsible elements for navigation */
const collapsibles =
......
/*
* Copyright (c) 2016-2018 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/Observable"
import "rxjs/add/observable/empty"
import "rxjs/add/observable/of"
import "rxjs/add/operator/scan"
import "rxjs/add/operator/switchMap"
import { merge } from "lodash"
import { Toggle } from "./toggle"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Component state
*/
export interface ComponentState {
active: boolean /* Whether the component is active */
}
/**
* Component lifecycle hooks
*/
export interface Component<
T extends ComponentState, /* Component state */
V /* Component value */
> {
initial: T /* Initial state */
create: (state: T) => T /* Creation hook */
destroy: (state: T) => T /* Destruction hook */
update: (state: T) => Observable<
Readonly<V>
> /* Update hook */
}
/* ----------------------------------------------------------------------------
* Lifecycle hooks
* ------------------------------------------------------------------------- */
/**
* No-op creation lifecycle hook
*
* @param state - Component state
*
* @return Altered state
*/
export function create<T extends ComponentState>(state: T) {
return state
}
/**
* No-op destruction lifecycle hook
*
* @param state - Component state
*
* @return Altered state
*/
export function destroy<T extends ComponentState>(state: T) {
return state
}
/**
* No-op update lifecycle hook
*
* @param state - Component state
*
* @return Altered state
*/
export function update<T extends ComponentState, V>(state: T) {
return Observable.empty<V>()
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Bind a component's lifecycle to a toggle
*
* @param component - Component
* @param toggle$ - Toggle observable
*
* @return Component observable
*/
export function bind<T extends ComponentState, V>(
component: Component<T, V>, toggle$?: Observable<Toggle>
) {
const { initial, create: c, destroy: d, update: u } = component
return (toggle$ || Observable.of({ active: true }))
/* Initialize component and handle state change */
.scan<Toggle, T>((state, { active }) => {
return (active ? c : d)(merge(state, { active }))
}, initial)
/* Return update component lifecycle hook */
.switchMap(state => {
return state.active ? u(state) : Observable.empty<V>()
})
}
/*
* Copyright (c) 2016-2018 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.
*/
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Toggle
*/
export interface Toggle {
active: boolean /* Whether the toggle is active */
}
/*
* Copyright (c) 2016-2018 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/Observable"
import "rxjs/add/observable/empty"
import "rxjs/add/observable/of"
import "rxjs/add/operator/switchMap"
import {
Component,
ComponentState,
create,
destroy
} from "../base/component"
import * as viewport from "../device/viewport"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Component state
*/
export interface NavBlurState extends ComponentState {
anchors: HTMLAnchorElement[] /* Anchor elements */
targets: HTMLElement[] /* Target elements */
index: number /* Current index */
offset: number /* Current offset */
direction: boolean /* Current scroll direction */
}
/**
* Component value
*/
export interface NavBlur {
anchor: HTMLAnchorElement /* Active anchor */
target: HTMLElement /* Active target */
}
/* ----------------------------------------------------------------------------
* Values
* ------------------------------------------------------------------------- */
/**
* Initial values for component state
*/
export const initial: NavBlurState = {
active: false, /* Whether the component is active */
anchors: [], /* Anchor elements */
targets: [], /* Target elements */
index: 0, /* Current index */
offset: 0, /* Current offset */
direction: false /* Current scroll direction */
}
/* ----------------------------------------------------------------------------
* Lifecycle hooks
* ------------------------------------------------------------------------- */
/**
* Blur links within the table of contents above current page y-offset
*
* @param state - Component state
*
* @return Altered state
*/
export function update(state: NavBlurState) {
const { anchors, targets } = state
return viewport.offset()
.switchMap(({ y }) => {
const direction = (state.offset - y < 0)
const index = state.index
/* Exit when there are no anchors/targets */
if (targets.length === 0)
return Observable.empty<NavBlur>()
/* Hack: reset index if direction changed to catch very fast scrolling,
because otherwise we would have to register a timer and that sucks */
if (state.direction !== direction)
state.index = direction
? state.index = 0
: state.index = anchors.length - 1
/* Scroll direction is down */
if (state.offset <= y) {
for (let i = state.index + 1; i < anchors.length; i++) {
if (targets[i].offsetTop - (56 + 24) <= y) { // TODO: Refactor value determination
if (i > 0)
anchors[i - 1].dataset.mdState = "blur"
state.index = i
} else {
break
}
}
/* Scroll direction is up */
} else {
for (let i = state.index; i >= 0; i--) {
if (targets[i].offsetTop - (56 + 24) > y) {
if (i > 0)
anchors[i - 1].dataset.mdState = ""
} else {
state.index = i
break
}
}
}
/* Remember current offset and direction for next iteration */
state.offset = y
state.direction = direction
/* Emit current anchor and target if index changed */
return state.index !== index
? Observable.of<NavBlur>({
anchor: anchors[state.index],
target: targets[state.index]
})
: Observable.empty<NavBlur>()
})
}
/**
* Component factory method
*
* @param els - Anchor elements
*
* @return Component
*/
export function factory(
els: ArrayLike<HTMLAnchorElement>
): Component<NavBlurState, NavBlur> {
const anchors = Array.from(els)
const targets: HTMLElement[] = anchors.reduce((result, anchor) => [
...result, document.getElementById(anchor.hash.substring(1)) || []
], [])
return {
initial: { ...initial, anchors, targets }, create, destroy, update
}
}
......@@ -25,6 +25,8 @@ import { Observable } from "rxjs/Observable"
import "rxjs/add/observable/fromEventPattern"
import { Toggle } from "../base/toggle"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
......@@ -35,10 +37,10 @@ import "rxjs/add/observable/fromEventPattern"
* @param media - Media query
*/
export function query(media: MediaQueryList) {
const media$ = new BehaviorSubject<boolean>(media.matches)
Observable.fromEventPattern<boolean>(next =>
const media$ = new BehaviorSubject<Toggle>({ active: media.matches })
Observable.fromEventPattern<Toggle>(next =>
media.addListener(
(mq: MediaQueryList) => next(mq.matches)
(mq: MediaQueryList) => next({ active: mq.matches })
))
.subscribe(media$)
return media$
......
......@@ -56,8 +56,8 @@ describe("device", () => {
/* Media query for testing purposes */
const media = window.matchMedia("(min-width: 480px)")
/* Test: it should return a behavior subject */
it("should return a behavior subject", () => {
/* Test: it should return a subject */
it("should return a subject", () => {
const media$ = query(media)
expect(media$).toEqual(jasmine.any(Subject))
})
......@@ -67,8 +67,8 @@ describe("device", () => {
const media$ = query(media)
media$
.take(1)
.subscribe(initial => {
expect(initial).toEqual(true)
.subscribe(({ active }) => {
expect(active).toEqual(true)
done()
}, done.fail)
})
......@@ -80,8 +80,8 @@ describe("device", () => {
.take(2)
.toArray()
.subscribe(([initial, changed]) => {
expect(initial).toEqual(true)
expect(changed).toEqual(false)
expect(initial.active).toEqual(true)
expect(changed.active).toEqual(false)
done()
}, done.fail)
requestAnimationFrame(() => {
......@@ -93,8 +93,8 @@ describe("device", () => {
/* Create a subject for a minimum media query */
describe(".from", () => {
/* Test: it should return a behavior subject */
it("should return a behavior subject", () => {
/* Test: it should return a subject */
it("should return a subject", () => {
const media$ = from(480)
expect(media$).toEqual(jasmine.any(Subject))
})
......@@ -104,8 +104,8 @@ describe("device", () => {
const media$ = from(480)
media$
.take(1)
.subscribe(initial => {
expect(initial).toEqual(true)
.subscribe(({ active }) => {
expect(active).toEqual(true)
done()
}, done.fail)
})
......@@ -117,8 +117,8 @@ describe("device", () => {
.take(2)
.toArray()
.subscribe(([initial, changed]) => {
expect(initial).toEqual(true)
expect(changed).toEqual(false)
expect(initial.active).toEqual(true)
expect(changed.active).toEqual(false)
done()
}, done.fail)
requestAnimationFrame(() => {
......@@ -130,8 +130,8 @@ describe("device", () => {
/* Create a subject for a maximum media query */
describe(".to", () => {
/* Test: it should return a behavior subject */
it("should return a behavior subject", () => {
/* Test: it should return a subject */
it("should return a subject", () => {
const media$ = to(479)
expect(media$).toEqual(jasmine.any(Subject))
})
......@@ -141,8 +141,8 @@ describe("device", () => {
const media$ = to(479)
media$
.take(1)
.subscribe(initial => {
expect(initial).toEqual(false)
.subscribe(({ active }) => {
expect(active).toEqual(false)
done()
}, done.fail)
})
......@@ -154,8 +154,8 @@ describe("device", () => {
.take(2)
.toArray()
.subscribe(([initial, changed]) => {
expect(initial).toEqual(false)
expect(changed).toEqual(true)
expect(initial.active).toEqual(false)
expect(changed.active).toEqual(true)
done()
}, done.fail)
requestAnimationFrame(() => {
......
......@@ -55,8 +55,8 @@ describe("device", () => {
/* Create a subject emitting changes in viewport offset */
describe(".offset", () => {
/* Test: it should return a behavior subject */
it("should return a behavior subject", () => {
/* Test: it should return a subject */
it("should return a subject", () => {
const offset$ = offset()
expect(offset$).toEqual(jasmine.any(Subject))
})
......@@ -99,8 +99,8 @@ describe("device", () => {
/* Create a subject emitting changes in viewport size */
describe(".size", () => {
/* Test: it should return a behavior subject */
it("should return a behavior subject", () => {
/* Test: it should return a subject */
it("should return a subject", () => {
const size$ = size()
expect(size$).toEqual(jasmine.any(Subject))
})
......
......@@ -25,6 +25,7 @@
"removeComments": true,
"sourceMap": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"stripInternal": true,
"target": "es5"
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment