hy-push-state
Version:
Turn static web sites into dynamic web apps
139 lines (124 loc) • 6.26 kB
JavaScript
// # src / mixin / event-listeners.js
// Copyright (c) 2018 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/>.
import { createXObservable } from "hy-component/src/rxjs";
import { matches, matchesAncestors } from "../common";
export const eventListenersMixin = C =>
class extends C {
setupEventListeners() {
// #### 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.
if ("MutationObserver" in window && "WeakSet" in window) {
// A `Set` of `Element`s.
// We use this to keep track of which links already have their event listeners registered.
const links = new WeakSet();
// Binding `next` functions to their `Subject`s,
// so that we can pass them as callbacks directly. This is just for convenience.
const pushNext = this.pushSubject.next.bind(this.pushSubject);
const hintNext = this.hintSubject.next.bind(this.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:
const addLink = 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 });
// When fetching resources from an external domain, rewrite the link's href,
// so that shift-click, etc works as expected.
// if (isExternal(this)) {
// link.href = new URL(link.getAttribute("href"), this.href).href;
// }
}
};
const addListeners = addedNode => {
if (addedNode instanceof Element) {
if (matches.call(addedNode, this.linkSelector)) {
addLink(addedNode);
} else {
Array.from(addedNode.querySelectorAll(this.linkSelector)).forEach(addLink);
}
}
};
// 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.
const removeLink = 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 });
};
const removeListeners = removedNode => {
if (removedNode instanceof Element) {
if (matches.call(removedNode, this.linkSelector)) {
removeLink(removedNode);
} else {
Array.from(removedNode.querySelectorAll(this.linkSelector)).forEach(removeLink);
}
}
};
// An observable wrapper around the mutation observer.
// We're only interested in nodes entering and leaving the entire subtree of this component,
// but not attribute changes.
const mutation$ = createXObservable(MutationObserver)(
this.el,
{},
{
childList: true,
subtree: true,
}
);
// For every mutation, we remove the event listeners of elements that go out of the component
// (if any), and add event listeners to all elements that make it into the compnent (if any).
mutation$.subscribe(({ addedNodes, removedNodes }) => {
Array.from(removedNodes).forEach(removeListeners.bind(this));
Array.from(addedNodes).forEach(addListeners.bind(this));
});
// TODO
this.subjects.linkSelector.subscribe(() => {
// TODO
Array.from(links).forEach(removeLink);
// The mutation observer does not pick up the links that are already on the page,
// so we add them manually here, once.
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.
} 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);
}
});
}
}
};