Skip to content

Instantly share code, notes, and snippets.

@th3hunt
Created November 18, 2020 15:57
Show Gist options
  • Save th3hunt/2601a6dbfaf5ca38f243184a584f0731 to your computer and use it in GitHub Desktop.
Save th3hunt/2601a6dbfaf5ca38f243184a584f0731 to your computer and use it in GitHub Desktop.
Nested History
/**
* NestedHistory
* -------------
*
* TODO: doc
*
*/
import _ from 'underscore';
const NESTED_HISTORY_HEAD = 'head';
const NESTED_HISTORY_TAIL = 'tail';
const headState = {nested_history: NESTED_HISTORY_HEAD};
const tailState = {nested_history: NESTED_HISTORY_TAIL};
const sortByPosition = (entry1, entry2) => _.compareNumbers(entry1.position, entry2.position);
const isBrowserHistoryAtNestHead = () => (global.history.state || {}).nested_history === NESTED_HISTORY_HEAD;
const isBrowserHistoryAtNestTail = () => (global.history.state || {}).nested_history === NESTED_HISTORY_TAIL;
class Entry {
constructor({id, state, title, url, position, onPop, onRemove}) {
this.id = id;
this.state = state;
this.title = title;
this.url = url;
this.popCallback = onPop;
this.removeCallback = onRemove;
this.position = position;
}
onPop() {
if (this.popCallback) {
this.popCallback();
}
}
onRemove() {
if (this.removeCallback) {
this.removeCallback();
}
}
}
class NestedHistory {
constructor() {
this.entries = new Map();
}
bindToBrowserHistory() {
global.addEventListener('popstate', e => this.onPopState(e.state));
this.isBoundToBrowserHistory = true;
}
onPopState() {
if (!isBrowserHistoryAtNestTail()) {
return;
}
if (this.isEmpty) {
return;
}
const previousHead = this.head();
if (!previousHead) {
return;
}
this.remove(previousHead);
if (this.isEmpty) {
return;
}
this.pushToBrowserHistory();
const poppedUpHead = this.head();
if (poppedUpHead) {
poppedUpHead.onPop();
}
}
jumpBefore(entry, {silent} = {}) {
if (!this.hasEntry(entry)) {
return;
}
entry = this.getEntry(entry);
for (const entryToRemove of this.getEntries().reverse()) {
this.remove(entryToRemove, {silent});
if (entryToRemove === entry) {
break;
}
}
if (this.isEmpty && isBrowserHistoryAtNestHead()) {
setTimeout(() => {
if (this.isEmpty && isBrowserHistoryAtNestHead()) {
global.history.go(-1);
}
}, 20);
} else {
this.pushToBrowserHistory();
}
}
push(entryProps = {}) {
if (!this.isBoundToBrowserHistory) {
this.bindToBrowserHistory();
}
const entry = new Entry({
...entryProps,
id: entryProps.id || _.uniqueId('nh:'),
position: this.entries.size
});
this.entries.set(entry.id, entry);
this.pushToBrowserHistory();
return entry;
}
remove(entry, {silent = false} = {}) {
if (!entry) {
return;
}
this.entries.delete(entry.id);
if (!silent) {
entry.onRemove();
}
}
getEntry(entry) {
return this.entries.get(entry.id || entry);
}
hasEntry(entry) {
return this.entries.has(entry.id || entry);
}
getEntries() {
return Array.from(this.entries.values()).sort(sortByPosition);
}
getHistoryFromEntries() {
return this.getEntries().map(entry => entry.id);
}
clear() {
this.entries.clear();
}
head() {
return _.last(this.getEntries());
}
pushToBrowserHistory() {
if (isBrowserHistoryAtNestHead() || this.isEmpty) {
return;
}
const title = document.title;
if (isBrowserHistoryAtNestTail()) {
global.history.pushState(headState, title);
} else {
global.history.replaceState({...global.history.state, ...tailState}, title);
global.history.pushState(headState, title);
}
}
get isEmpty() {
return this.entries.size === 0;
}
}
export default new NestedHistory();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment