-
-
Notifications
You must be signed in to change notification settings - Fork 11
/
nanochoo.js
135 lines (114 loc) · 3.64 KB
/
nanochoo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* Modified from nanochoo, a fork of choo at about half the size, specifically for use with Feather Wiki
* https://github.com/nanaian/nanochoo
*
* The `nanochoo` fork of `choo@6` removed the `navigate` events and `nanohref` package
* that Feather Wiki needs to prevent links from changing pages, so that has been
* added back in along with a `go` event that combines the removed `navigate`,
* `pushState`, and `replaceState` events into one. I also removed the `toString`
* method entirely because Feather Wiki is only focused on browser use.
*
* `nanochoo`'s primary changes to `choo@6` are recorded in its README on GitHub.
*
* @licence MIT
*/
var nanobus = require('nanobus') // Handles the emitter
var nanohref = require('nanohref') // Prevents browser navigation within wiki
var nanomorph = require('nanomorph') // Efficiently diffs DOM elements for render
var nanoraf = require('nanoraf') // Prevents too many renders
var params = () => {
const p = {};
(new URLSearchParams(window.location.search)).forEach((v, k, s) => {
v = s.getAll(k);
p[k] = v.length > 1 ? v : v[0];
});
return p;
};
var HISTORY = {};
export default function Choo () {
if (!(this instanceof Choo)) return new Choo()
this.ready = (f) => {
if (document.readyState === 'complete' || document.readyState === 'interactive') f()
else document.addEventListener('DOMContentLoaded', f)
}
// define events used by choo
this._events = {
ONLOAD: 'DOMContentLoaded',
TITLE: 'DOMTitleChange',
RENDER: 'render',
GO: 'go',
}
// properties for internal use only
this._loaded = false
this._tree = null
this._view = () => {};
// properties that are part of the API
this.emitter = nanobus('choo.emit')
this.emit = this.emitter.emit.bind(this.emitter)
this.state = {
events: this._events,
title: document.title,
query: params(),
}
this.emitter.prependListener(this._events.TITLE, (title) => {
this.state.title = document.title = title
})
}
Choo.prototype.start = function () {
const hashScroll = () => {
const el = document.getElementById(location.hash.substring(1));
if (!el) return false;
el?.scrollIntoView();
return true;
}
this.emitter.prependListener(this._events.GO, (to = null, action = 'push') => {
if (to) {
history[action + 'State'](HISTORY, this.state.title, to)
}
this.state.query = params()
if (this._loaded) {
this.emit(this._events.RENDER, () => {
// Scroll to top of page if no location hash is set
hashScroll() || window.scroll(0, 0);
})
}
})
window.onpopstate = () => {
this.emit(this._events.GO)
}
nanohref(({ href }) => {
var currHref = window.location.href
if (href === currHref) return
this.emit(this._events.GO, href)
})
var render = () => this._view(this.state, this.emit)
this._tree = render()
this._rq = [] // render queue
this._rd = null // render debounce
this.emitter.prependListener(this._events.RENDER, cb => {
if (typeof cb === 'function') this._rq.push(cb)
if (this._rd !== null) clearTimeout(this._rd)
this._rd = setTimeout(nanoraf(() => {
var newTree = render()
nanomorph(this._tree, newTree)
while(this._rq.length > 0) (this._rq.shift())()
}), 9)
})
this.ready(() => {
this.emit(this._events.ONLOAD)
this._loaded = true
hashScroll();
})
return this._tree
}
Choo.prototype.mount = function (selector) {
this.ready(() => {
var newTree = this.start()
if (typeof selector === 'string') {
this._tree = document.querySelector(selector)
} else {
this._tree = selector
}
nanomorph(this._tree, newTree)
})
}