# src / mixin / index.js
Copyright (c) 2017 Florian Klampfer <https://qwtel.com/>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

## Overview
This component is written in [RxJS] and reading its code requires some basic understanding
of how RxJS works. It may also serve as an example of how to use RxJS.

Other than RxJS, you should be familiar with [ES6 Mixin][esmixins],
which is a clever way of using the ES6 class syntax to achieve inheritance-based mixins.
The mixin in the main export of this file.

## Table of Contents
{:.no_toc}
* Table of Contents
{:toc}

## Imports
Including the patches for ES6+ functions, but
there is a -lite version of the component that comes without these.


```js
import 'core-js/fn/array/for-each';
import 'core-js/fn/array/from';
import 'core-js/fn/function/bind';
import 'core-js/fn/object/assign';
```

Importing the hy-compontent base libary,
which helps with making multiple versions of the component (Vanilla JS, WebComponent, etc...).


```js
import { componentMixin, COMPONENT_FEATURE_TESTS } from 'hy-component/src/component';
import { sSetup, sSetupDOM, sFire } from 'hy-component/src/symbols';

import { Observable, Subject } from 'rxjs';
```

Importing the subset of RxJS functions that we are going to use.


```js
import { defer } from 'rxjs/observable/defer';
import { from } from 'rxjs/observable/from';
import { fromEvent } from 'rxjs/observable/fromEvent';
import { merge } from 'rxjs/observable/merge';
import { never } from 'rxjs/observable/never';
import { of } from 'rxjs/observable/of';
import { timer } from 'rxjs/observable/timer';

import { ajax } from 'rxjs/observable/dom/ajax';

import {
  catchError,
  tap,
  concatMap,
  distinctUntilChanged,
  filter,
  map,
  mapTo,
  observeOn,
  partition,
  pairwise,
  share,
  startWith,
  switchMap,
  take,
  takeUntil,
  withLatestFrom,
  zip,
} from 'rxjs/operators';

import { animationFrame } from 'rxjs/scheduler/animationFrame';

import { array, bool, number, regex, string } from 'attr-types';
import { Set } from 'qd-set';
```

Partial polyfill of the URL class. Only provides the most basic funtionality of `URL`,
but sufficient for this compoennt.


```js
import { URL } from '../url';
```

Some of our own helper functions from [src / common.js](../common.md).


```js
import {
  fragmentFromString,
  getScrollTop,
  getScrollHeight,
  isExternal,
  isHash,
  matchesAncestors,
} from '../common';
```

## Constants
A set of [Modernizr] tests that are required to run this component.
These are the bare-minimum requirements, more ad-hoc features tests for optional behavior
is part of the code below.


```js
export const MIXIN_FEATURE_TESTS = new Set([
  ...COMPONENT_FEATURE_TESTS,
  'documentfragment',
  'eventlistener',
  'history',
  'promises',
  'queryselector',
  'requestanimationframe',
]);
```

We export the setup symbols,
so that mixin users don't have to import them from hy-compnent separately.


```js
export { sSetup, sSetupDOM };
```

These are some 'types' that we use throught the component.
Going with strings here instead of classes + instanceof / dynamic dispatch for simplicity.


```js
export const INIT = 'init';
export const HINT = 'hint';
export const PUSH = 'push';
export const POP = 'pop';
```

If Symbol isn't supported, just use underscore naming convention for private properties.
We don't need advanced features of Symbol.


```js
const Symbol = global.Symbol || (x => `_${x}`);
```

We use `Symbol`s for all internal variables, to avoid naming conflicts when using the mixin.


```js
const sAnimPromise = Symbol('animPromise');
const sReload$ = Symbol('reloadObservable');
```

For convenience....


```js
const assign = Object.assign.bind(Object);
```

Patching the document fragment's `getElementById` function, which is
not implemented in all browsers, even some modern ones.


```js
DocumentFragment.prototype.getElementById = DocumentFragment.prototype.getElementById ||
  function getElementById(id) { return this.querySelector(`#${id}`); };
```

## Functions
What you will notice about the following helper functions is that many make reference to `this`.
This is because they are invoked with the `::` operator, binding `this` to the component,
effectively turning them into (private) methods. Since the final export is a mixin,
we want to avoid potentially conflicting names as much as possible.

### Observable extensions
#### Unsubscribe when
This operator unsubscribes from the source observable when `pauser$` emits a truthy value,
and re-subscribes when it emits a falsy value.


```js
const unsubscribeWhen = pauser$ => (source) => {
  if (process.env.DEBUG && !pauser$) throw Error();
  return pauser$.pipe(switchMap(paused => (paused ? never() : source)));
};
```

#### Custom subscribe
A custom subscribe function that will `recover` from an error and log it to the console.
This is a line of last defense to make sure the entire pipeline/page doesn't crash.
TODO: maybe just let it crash s.t. the page reloads on the next click on a link!?


```js
/*
function subscribe(ne, er, co) {
  let res = this;
  if (process.env.DEBUG) res = this.pipe(tap({ error: e => console.error(e) }));
  return res
    .pipe(catchError((e, c) => c))
    .subscribe(ne, er, co);
}
*/
```

### Event filters


```js
function shouldLoadAnchor(anchor, hrefRegex) {
  return anchor && anchor.target === ''
    && (!hrefRegex || anchor.href.search(hrefRegex) !== -1);
}

function isPushEvent({ metaKey, ctrlKey, currentTarget }) {
  return !metaKey && !ctrlKey
    && shouldLoadAnchor(currentTarget, this._hrefRegex)
    && !isExternal(currentTarget);
}

function isHintEvent({ currentTarget }) {
  return shouldLoadAnchor(currentTarget, this._hrefRegex)
    && !isExternal(currentTarget)
    && !isHash(currentTarget);
}
```

### Managing scroll positions
The following functions deal with managing the scroll position of the site.

Returns an identifier to mark frames on the history stack.


```js
function histId() {
  return this.el.id || this.constructor.componentName;
}
```

Given a hash, find the element of the same id on the page, and scroll it into view.
If no hash is provided, scroll to the top instead.


```js
function scrollHashIntoView(hash) {
  if (hash) {
    const el = document.getElementById(hash.substr(1));
    if (el) el.scrollIntoView();
    else if (process.env.DEBUG) console.warn(`Can't find element with id ${hash}`);
  } else window.scroll(window.pageXOffset, 0);
}
```

Takes the current history state, and restores the scroll position.


```js
function restoreScrollPostion() {
  const id = histId.call(this); // TODO
  const state = (window.history.state && window.history.state[id]) || {};

  if (state.scrollTop != null) {
    document.body.style.minHeight = state.scrollHeight;
    window.scroll(window.pageXOffset, state.scrollTop);
    /* document.body.style.minHeight = ''; */
  } else if (state.hash) {
    scrollHashIntoView(window.location.hash);
  }
}
```

TODO


```js
function manageScrollPostion({ type, url: { hash } }) {
  if (type === PUSH) {
    scrollHashIntoView(hash);
  }

  if (type === POP && this.scrollRestoration) {
    restoreScrollPostion.call(this);
  }
}

function saveScrollPosition(state) {
  const id = histId.call(this);
  return assign(state, {
    [id]: assign(state[id] || {}, {
      scrollTop: getScrollTop(),
      scrollHeight: getScrollHeight(),
    }),
  });
}

function updateHistoryState({ type, replace, url: { href, hash } }) {
  if (type === PUSH || type === INIT) {
    const id = histId.call(this);
    const method = replace || href === window.location.href ? 'replaceState' : 'pushState';
    window.history[method]({ [id]: { hash: !!hash } }, '', href);
  }
}

function updateHistoryStateHash({ type, url }) {
  const { hash, href } = url;

  if (type === PUSH) {
    const id = histId.call(this);
    const currState = assign(window.history.state, {
      [id]: assign(window.history.state[id], { hash: true }),
    });
    const nextState = {
      [id]: { hash: true },
    };
    window.history.replaceState(currState, document.title, window.location.href);
    window.history.pushState(nextState, document.title, href);
  }

  scrollHashIntoView(hash);
}

function saveScrollHistoryState() {
  const state = saveScrollPosition.call(this, window.history.state || {});
  window.history.replaceState(state, document.title, window.location);
}
```

### Fetching


```js
function hrefToAjax({ url }) {
  return {
    method: 'GET',
    responseType: 'text',
    url,
  };
}
```

The `ajax` method will throw when it encoutners an a 400+ status code,
however these are still valid responses from the server, that can be shown using this component.
This assumes error pages have the same HTML strcuture as the other pages though.


```js
function recoverIfResponse(context, error) {
  const { status, xhr } = error;
```

If we have a response, recover with it and continue with the pipeline.


```js
  if (xhr && xhr.response && status > 400) {
    return of(assign(context, { response: xhr.response }));
  }
```

If we don't have a response, this is an acutal error that should be dealt with.


```js
  return of(assign(context, { error }));
}
```

This function returns the request that matches a given URL.
The way the pipeline is set up, it is either the current `prefetch` value,
or the next value on the prefetch observable.
TODO: rename


```js
function getFetch$({ url: { href } }, latestPrefetch, prefetch$) {
  return href === latestPrefetch.url.href && latestPrefetch.error == null ?
    of(latestPrefetch) :
    prefetch$.pipe(take(1));
}
```

TODO: rename


```js
function getResponse(prefetch$, [context, latestPrefetch]) {
  return getFetch$(context, latestPrefetch, prefetch$).pipe(
    map(fetch => assign(fetch, context)),
    zip(this[sAnimPromise], x => x),
  );
}
```

### Experimental script feature
TODO

This function removes all script tags (as query'ed by `_scriptSelector`) from the response.


```js
function tempRemoveScriptTags(replaceEls) {
  const scripts = [];

  replaceEls.forEach(docfrag =>
    Array.from(docfrag.querySelectorAll(this._scriptSelector)).forEach((script) => {
      const pair = [script, script.previousSibling];
      script.parentNode.removeChild(script);
      scripts.push(pair);
    }));

  return scripts;
}
```

Attempts to (synchronously) insert a `script` tag into the DOM, *before* a given `ref` element.


```js
function insertScript([script, ref]) {
```

Temporarily overwrite `document.wirte` to simulate its behavior during the initial load.
This only works because scripts are inserted one-at-a-time (via `concatMap`).


```js
  const originalWrite = document.write;

  document.write = (...args) => {
    const temp = document.createElement('div');
    temp.innerHTML = args.join();
    Array.from(temp.childNodes).forEach((node) => {
      ref.parentNode.insertBefore(node, ref.nextSibling);
    });
  };
```

If the script tag needs to fetch its source code, we insert it into the DOM,
but we return an observable that only completes once the script has fired its `load` event.


```js
  return script.src !== '' ?
    Observable.create((observer) => {
      script.addEventListener('load', (x) => {
        document.write = originalWrite;
        observer.complete(x);
      });

      script.addEventListener('error', (x) => {
        document.write = originalWrite;
        observer.error(x);
      });

      ref.parentNode.insertBefore(script, ref.nextSibling);
    }) :
```

Otherwise we insert it into the DOM and reset the `document.write` function.


```js
    of({}).pipe(tap(() => {
      ref.parentNode.insertBefore(script, ref.nextSibling);
      document.write = originalWrite;
    }));
}
```

Takes a list of `script`--`ref` pairs, and inserts them into the DOM one-by-one.


```js
function reinsertScriptTags(context) {
  const { scripts } = context;

  return from(scripts).pipe(
    concatMap(insertScript),
    catchError((error) => { throw assign(context, { error }); }),
  )
    .toPromise()
    .then(() => context);
}
```

### Content replacement
TODO


```js
function getTitle(fragment) {
  return (fragment.querySelector('title') || {}).textContent;
}

function getReplaceElements(fragment) {
  if (this.replaceIds.length > 0) {
    return this.replaceIds.map(id => fragment.getElementById(id));
  } else {
    let replaceEl;
    if (this.el.id) {
      replaceEl = fragment.getElementById(this.el.id);
    } else {
      const index = Array.from(document.getElementsByTagName(this.el.tagName)).indexOf(this.el);
      replaceEl = fragment.querySelectorAll(this.el.tagName)[index];
    }
    return [replaceEl];
  }
}

function responseToContent(context) {
  const { response } = context;

  const fragment = fragmentFromString(response);
  const title = getTitle.call(this, fragment);
  const replaceEls = getReplaceElements.call(this, fragment);

  if (replaceEls.some(x => x == null)) {
    throw assign(context, { relaceElMissing: true });
  }

  const scripts = this._scriptSelector ? tempRemoveScriptTags.call(this, replaceEls) : [];

  return assign(context, { title, replaceEls, scripts });
}

function replaceContentByIds(elements) {
  this.replaceIds
    .map(id => document.getElementById(id))
    .forEach((oldElement, i) => {
      oldElement.parentNode.replaceChild(elements[i], oldElement);
    });
}

function replaceContentWholesale([el]) {
  this.el.innerHTML = el.innerHTML;
}

function replaceContent(replaceEls) {
  if (this.replaceIds.length > 0) {
    replaceContentByIds.call(this, replaceEls);
  } else {
    replaceContentWholesale.call(this, replaceEls);
  }
}

function updateDOM(context) {
  try {
    const { title, replaceEls, type } = context;

    document.title = title;

    if (type === PUSH) {
      window.history.replaceState(window.history.state, title, window.location);
    }

    replaceContent.call(this, replaceEls);
  } catch (error) {
    throw assign(context, { error });
  }
}
```

### Event functions
These functions are called at various points along the observable pipeline to fire events,
and cause other side effects.

#### On start


```js
function onStart(context) {
```

By default, hy-push-state will wait at least `duration` ms before replacing the content,
so that animations have enough time to finish.
The behavior is encoded with a promise that resolves after `duration` ms.


```js
  this[sAnimPromise] = timer(this.duration);
```

The `waitUntil` function lets users of this component override the animation promise.
This allows for event-based code execution, rather than timing-based, which prevents hiccups
and glitches when, for example, painting takes longer than expected.


```js
  const waitUntil = (promise) => {
    if (process.env.DEBUG && !(promise instanceof Promise || promise instanceof Observable)) {
      console.warn('waitUntil expects a Promise as first argument.');
    }
    this[sAnimPromise] = promise;
  };

  this[sFire]('start', { detail: assign(context, { waitUntil }) });
}
```

Example usage of `waitUntil`:

```js
hyPushStateEl.addEventListener('hy-push-state-start', ({ detail }) => {
  const animPromise = new Promise((resolve) => {
    const anim = myContent.animate(...);
    anim.addEventListener('finish', resolve);
  });
  detail.waitUntil(animPromise);
});
```
{:style="font-style:italic"}

#### Error callbacks
This function handles errors while trying to insert the new content into the document.
If the retrieved documened doesn't contain the ids we are looking for
we can't insert the content dynamically, so we tell the browser to open the link directly.


```js
function onDOMError(context) {
  const { relaceElMissing, url } = context;
```

Ideally you should prevent this situation by adding the
`no-push-state` CSS class
on links to documents that don't match the expected document layout.
This only serves as a fallback.


```js
  if (relaceElMissing) {
    if (process.env.DEBUG) {
      const ids = this.replaceIds.concat(this.el.id || []).map(x => `#${x}`).join(', ');
      console.warn(`Couldn't find one or more ids of '${ids}' in the document at '${window.location}'. Opening the link directly.`);
    }
```

To open the link directly, we first pop one entry off the browser history.
We have to do this because (some) browsers won't handle the back button correctly otherwise.
We then wait for a short time and change the document's location.
TODO: If we didn't call `pushState` optimistically we wouldn't have to do this.


```js
    window.history.back();
    setTimeout(() => { document.location.href = url; }, 100);
```

If it's a different error, throw the generic `error` event.


```js
  } else {
    if (process.env.DEBUG) console.error(context);
    this[sFire]('error', { detail: context });
  }
}
```

If there is a network error during (pre-) fetching, fire `networkerror` event.


```js
function onNetworkError(context) {
  if (process.env.DEBUG) console.error(context);
  this[sFire]('networkerror', { detail: context });
}
```

When using the experimental script feature,
fire `scripterror` event if something goes wrong during script insertion.


```js
function onError(context) {
  if (process.env.DEBUG) console.error(context);
  this[sFire]('error', { detail: context });
}
```

#### Others
These event callbacks simply fire an event and pass the context as `detail`.


```js
function onReady(context) {
  this[sFire]('ready', { detail: context });
}

function onAfter(context) {
  this[sFire]('after', { detail: context });
}

function onProgress(context) {
  this[sFire]('progress', { detail: context });
}

function onLoad(context) {
  this[sFire]('load', { detail: context });
}
```

A compare function for contexts, used in combination with `distinctUntilChanged`.
We use `cacheNr` as it is a convenient (hacky) way of circumventing
`distinctUntilChanged` when retrying requests.


```js
let cacheNr = 1;

function compareContext(p, q) {
  return p.url.href === q.url.href
    && p.error === q.error
    && p.cacheNr === q.cacheNr;
}
```

Determines if a pair of context's constitutes a hash change (vs. a page chagne)
We take as a hash change when the pathname of the URLs is the same,
and the `hash` isn't empty.


```js
function isHashChange([
  { url: { pathname: prevPathname } },
  { url: { pathname, hash }, type },
]) {
  return pathname === prevPathname
    && (type === POP || (type === PUSH && hash !== ''));
}
```

### Setup observable
This functions sets up the core observable pipeline of this component.


```js
function setupObservables() {
```

For now, we take for granted that we have a stream of all `PUSH` events (loading a new page by
clicking on a link) and `HINT` events (probable click on a link) which are `pushSubject` and
`hintSubject` respectively.


```js
  const pushSubject = new Subject();
  const hintSubject = new Subject();
```

Emits a value each time the `reload` method is called on this component.


```js
  this[sReload$] = new Subject();
```

This is used to reference deferred observaables.


```js
  const ref = {};
```

TODO


```js
  const push$ = pushSubject.pipe(
    filter(isPushEvent.bind(this)),
    map(event => ({
      type: PUSH,
      url: new URL(event.currentTarget.href),
      anchor: event.currentTarget,
      event,
      cacheNr,
    })),
    tap(({ event }) => {
      event.preventDefault();
      saveScrollHistoryState.call(this);
    }),
  );
```

In additon to `HINT` and `PUSH` events, there's also `POP` events, which are caused by
modifying the browser history, e.g. clicking the back button, etc.


```js
  const pop$ = fromEvent(window, 'popstate').pipe(
    filter(() => window.history.state && window.history.state[histId.call(this)]),
    map(event => ({
      type: POP,
      url: new URL(window.location),
      event,
      cacheNr,
    })),
  );
```

TODO


```js
  const [hash$, page$] = merge(push$, pop$, this[sReload$]).pipe(
    startWith({ url: new URL(window.location) }),
    pairwise(),
    share(),
    partition(isHashChange),
  )
    .map(obs$ => obs$.pipe(map(([, x]) => x), share()));
```

We don't want to prefetch (i.e. use bandwidth) for a _probabilistic_ page load,
while a _definitive_ page load is going on => `pauser$` stream.
Needs to be deferred b/c of "cyclical" dependency.


```js
  const pauser$ = defer(() =>
```

A page change event means we want to pause prefetching, while
a response event means we want to resume prefetching.


```js
    merge(page$.pipe(mapTo(true)), ref.fetch$.pipe(mapTo(false)))).pipe(
```

Start with `false`, i.e. we want to prefetch


```js
    startWith(false),
    share(),
  );
```

TODO


```js
  const hint$ = hintSubject.pipe(
    unsubscribeWhen(pauser$),
    filter(isHintEvent.bind(this)),
    map(event => ({
      type: HINT,
      url: new URL(event.currentTarget.href),
      anchor: event.currentTarget,
      event,
      cacheNr,
    })),
  );
```

The stream of (pre-)fetch events.
Includes definitive page change events do deal with unexpected page changes.


```js
  const prefetch$ = merge(hint$, page$).pipe(
```

Don't abort a request if the user "jiggles" over a link


```js
    distinctUntilChanged(compareContext),
    switchMap(context =>
      ajax(hrefToAjax(context)).pipe(
        map(({ response }) => assign(context, { response })),
        catchError(error => recoverIfResponse.call(this, context, error)),
      )),
```

Start with some value so `withLatestFrom` below doesn't "block"


```js
    startWith({ url: {} }),
    share(),
  );
```

TODO


```js
  ref.fetch$ = page$.pipe(
    tap(updateHistoryState.bind(this)),
    tap(onStart.bind(this)),
    withLatestFrom(prefetch$),
    switchMap(getResponse.bind(this, prefetch$)),
    share(),
  );
```

TODO


```js
  const [fetchOk$, fetchError$] = ref.fetch$.pipe(partition(({ error }) => !error));
```

TODO


```js
  let main$ = fetchOk$.pipe(
    map(responseToContent.bind(this)),
    observeOn(animationFrame),
    tap(onReady.bind(this)),
    tap(updateDOM.bind(this)),
    tap(onAfter.bind(this)),
    tap(manageScrollPostion.bind(this)),
    tap({ error: e => onDOMError.call(this, e) }),
    catchError((e, c) => c),
  );
```

If the experimental script feature is enabled,
scripts tags have been stripped from the content,
and this is where we insert them again.


```js
  if (this._scriptSelector) {
    main$ = main$.pipe(
      switchMap(reinsertScriptTags.bind(this)),
      tap({ error: e => onError.call(this, e) }),
      catchError((e, c) => c),
    );
  }
```

#### Subscriptions
Subscribe to main and hash observables.


```js
  main$.subscribe(onLoad.bind(this));
  hash$.subscribe(updateHistoryStateHash.bind(this));
```

Subscribe to the fetch error branch.


```js
  fetchError$.subscribe(onNetworkError.bind(this));
```

Fire `progress` event when fetching takes longer than expected.


```js
  page$.pipe(switchMap(context =>
    defer(() => this[sAnimPromise]).pipe(
      takeUntil(ref.fetch$),
      mapTo(context),
    )))
    .subscribe(onProgress.bind(this));
```

#### Keeping track of links
We use a `MutationObserver` to keep track of all the links inside the component,
and put events on the `pushSubject` and `hintSubject` observables,
but first we need to check if `MutationObserver` is available.


```js
  if ('MutationObserver' in window && 'Set' in window) {
```

An observable of mutations. The `MutationObserver` will put mutations onto it.


```js
    const mutation$ = new Subject();
```

A `Set` of `Element`s.
We use this to keep track of which links already have their event listeners registered.
TODO: can we guarantee that we won't find the same link twice?


```js
    const links = new Set();
```

Binding `next` functions to their `Subject`s,
so that we can pass them as callbacks directly. This is just for convenience.


```js
    const mutationNext = mutation$.next.bind(mutation$);
    const pushNext = pushSubject.next.bind(pushSubject);
    const hintNext = hintSubject.next.bind(hintSubject);
```

We don't use `Observable.fromEvent` here to avoid creating too many observables.
Registering an unknown number of event listeners is somewhat debatable,
but we certainly don't want to make it wrose.
(The number could be brought down by using an `IntersectionObserver` in the future.
Also note that typically there will be an animation playing while this is happening,
so the effects are not easily noticed).

In any case, `MutationObserver` and `Set` help us keep track of which links are children
of this component, so that we can reliably add and remove the event listeners.
The function to be called for every added node:


```js
    const addListeners = (addedNode) => {
      if (addedNode instanceof Element) {
        Array.from(addedNode.querySelectorAll(this.linkSelector)).forEach((link) => {
          if (!links.has(link)) {
            links.add(link);
            link.addEventListener('click', pushNext);
            link.addEventListener('mouseenter', hintNext, { passive: true });
            link.addEventListener('touchstart', hintNext, { passive: true });
            link.addEventListener('focus', hintNext, { passive: true });
          }
        });
      }
    };
```

Next, The function to be called for every removed node.
Usually the elments will be removed from the document altogher
when they are removed from this component,
but since we can't be sure, we remove the event listeners anyway.


```js
    const removeListeners = (removedNode) => {
      if (removedNode instanceof Element) {
        Array.from(removedNode.querySelectorAll(this.linkSelector)).forEach((link) => {
          links.delete(link);
          link.removeEventListener('click', pushNext);
          link.removeEventListener('mouseenter', hintNext, { passive: true });
          link.removeEventListener('touchstart', hintNext, { passive: true });
          link.removeEventListener('focus', hintNext, { passive: true });
        });
      }
    };
```

The mutation observer callback simply puts all mutations on the `mutation$` observable.


```js
    const observer = new MutationObserver(mutations => Array.from(mutations).forEach(mutationNext));
```

For every mutation, we remove the event listeners of elements that go out of the component
(if any), and add event listeners for all elements that make it into the compnent (if any).


```js
    mutation$.pipe(unsubscribeWhen(pauser$))
      .subscribe(({ addedNodes, removedNodes }) => {
        Array.from(removedNodes).forEach(removeListeners.bind(this));
        Array.from(addedNodes).forEach(addListeners.bind(this));
      });
```

We're interested in nodes entering and leaving the entire subtree of this component,
but not attribute changes:


```js
    observer.observe(this.el, { childList: true, subtree: true });
```

The mutation observer does not pick up the links that are already on the page,
so we add them manually here, once.


```js
    addListeners.call(this, this.el);
```

If we don't have `MutationObserver` and `Set`, we just register a `click` event listener
on the entire component, and check if a click occurred on one of our links.
Note that we can't reliably generate hints this way, so we don't.


```js
  } else {
    this.el.addEventListener('click', (event) => {
      const anchor = matchesAncestors.call(event.target, this.linkSelector);
      if (anchor && anchor.href) {
        event.currentTarget = anchor; // eslint-disable-line no-param-reassign
        pushSubject.next(event);
      }
    });
  }
}
```

## Push state mixin


```js
export function pushStateMixin(C) {
```

TODO: see ES6 mixins...


```js
  return class extends componentMixin(C) {
```

The name of the component (required by hy-component)


```js
    static get componentName() { return 'hy-push-state'; }
```

### Setup
Overriding the setup function.


```js
    [sSetup](el, props) {
      super[sSetup](el, props);
```

Setting up scroll restoration


```js
      if ('scrollRestoration' in window.history && this.scrollRestoration) {
        window.history.scrollRestoration = 'manual';
      }
```

If restore the last scroll position, if any.


```js
      restoreScrollPostion.call(this);
```

Remember the current scroll position (for F5/reloads).


```js
      window.addEventListener('beforeunload', saveScrollHistoryState.bind(this));
```

Finally, calling the [setup observables function](#setup-observables) function.


```js
      setupObservables.call(this);
```

Setting the initial `history.state`.


```js
      const url = new URL(window.location);
      updateHistoryState.call(this, { type: INIT, replace: true, url });
```

After all this is done, we can fire the one-time `init` event...


```js
      this[sFire]('init');
```

...and our custom `load` event, which gets fired on every page change.
We provide similar data as subsequent `load` events,
however we can't provide an `anchor` or `event`,
since this `load` event wasn't caused by a user interaction.


```js
      onLoad.call(this, {
        type: INIT,
        title: getTitle.call(this, document),
        replaceEls: getReplaceElements.call(this, document),
        url,
        cacheNr,
      });
```

Allow function chaining.


```js
      return this;
    }

    [sSetupDOM](el) { return el; }
```

### Options
The default values (and types) of the configuration options (required by hy-component)
See [Options](../../options.md) for usage information.


```js
    static get defaults() {
      return {
        replaceIds: [],
        linkSelector: 'a[href]:not(.no-push-state)',
        scrollRestoration: false,
        duration: 0,
        _hrefRegex: null,
        _scriptSelector: null,
        /* prefetch: true, */
        /* repeatDelay: 500, */
      };
    }

    static get types() {
      return {
        replaceIds: array,
        linkSelector: string,
        scrollRestoration: bool,
        duration: number,
        _hrefRegex: regex,
        _scriptSelector: string,
        /* prefetch: bool, */
        /* repeatDelay: number, */
      };
    }
```

Modifying options of this component doesn't have side effects (so far).


```js
    static get sideEffects() {
      return {};
    }
```

### Methods
Public methods of this component. See [Methods](../../methods.md) for more.


```js
    assign(url) {
      this[sReload$].next({
        type: PUSH,
        url: new URL(url, window.location),
        cacheNr: ++cacheNr, // eslint-disable-line no-plusplus
      });
    }

    reload() {
      this[sReload$].next({
        type: PUSH,
        url: new URL(window.location.href),
        cacheNr: ++cacheNr, // eslint-disable-line no-plusplus
        replace: true,
      });
    }

    replace(url) {
      this[sReload$].next({
        type: PUSH,
        url: new URL(url, window.location),
        cacheNr: ++cacheNr, // eslint-disable-line no-plusplus
        replace: true,
      });
    }
  };
}
```

This concludes the implementation of push-state mixin.
You can now check out

* [vanilla / index.js](../vanilla/index.md)
* [jquery / index.js](../jquery/index.md)
* [webcomponent / index.js](../webcomponent/index.md)

to see how it is used.

[rxjs]: https://github.com/ReactiveX/rxjs
[esmixins]: http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
[modernizr]: https://modernizr.com/


