UNPKG

11.2 kBJavaScriptView Raw
1import shiftyRouterModule from 'shifty-router'
2import hrefModule from 'shifty-router/href'
3import historyModule from 'shifty-router/history'
4import createLocation from 'shifty-router/create-location'
5import bel from 'nanohtml'
6import update from 'nanomorph'
7import axios from 'axios'
8import cssInject from 'csjs-inject'
9import merge from 'deepmerge'
10import marked from 'marked'
11import htmlEntities from 'html-entities'
12import eventEmitter from './eventEmitter'
13import qs from 'qs'
14import toSource from 'tosource'
15
16const {AllHtmlEntities} = htmlEntities
17
18let componentRegistry
19let entities = new AllHtmlEntities()
20let cssTag = cssInject
21let componentCSSString = ''
22let routesArray = []
23let externalRoutes = []
24let state = {}
25let router
26let rootEl
27let components
28let dataInitial
29let el
30
31marked.setOptions({
32 breaks: true
33})
34
35function b64DecodeUnicode (str) {
36 // Going backwards: from bytestream, to percent-encoding, to original string.
37 return decodeURIComponent(atob(str).split('').map(function (c) {
38 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
39 }).join(''))
40}
41
42if (typeof window !== 'undefined') {
43 componentRegistry = new Map()
44 dataInitial = document.querySelector('[data-initial]')
45 if (!!dataInitial) {
46 state = (dataInitial && dataInitial.dataset.initial) && Object.assign({}, JSON.parse(b64DecodeUnicode(dataInitial.dataset.initial)))
47
48 if (!state.router) {
49 state.router = {}
50 }
51
52 if (!state.router.pathname) {
53 Object.assign(state.router, {
54 pathname: window.location.pathname,
55 hash: window.location.hash,
56 query: qs.parse(window.location.search)
57 })
58 }
59 }
60} else {
61
62 cssTag = (cssStrings, ...values) => {
63 let output = cssInject(cssStrings, ...values)
64 componentCSSString += componentCSSString.indexOf(output[' css ']) === -1 ? output[' css '] : ''
65 return output
66 }
67}
68
69let geb = new eventEmitter({state})
70
71let html = (strings, ...values) => {
72 // fix for allowing csjs to coexist with nanohtml SSR
73 values = values.map(value => {
74 if (value && value.hasOwnProperty('toString')) {
75 return value.toString()
76 }
77 return value
78 })
79
80 return bel(strings, ...values)
81}
82
83function ssr (rootComponent) {
84 let componentsString = `${rootComponent}`
85 return {componentsString, stylesString: componentCSSString}
86}
87
88function defineRoute (routeObject) {
89 if (routeObject.external) {
90 return externalRoutes.push(routeObject.path)
91 }
92 routesArray.push(routeObject)
93}
94
95function formField (ob, prop) {
96 return e => {
97 ob[prop] = e.currentTarget.type === 'checkbox' || e.currentTarget.type === 'radio' ? e.currentTarget.checked : e.currentTarget.value
98 let validOb
99 let touchedOb
100 if (!ob.valid) {
101 if (Object.getOwnPropertySymbols(ob).length > 0) {
102 Object.getOwnPropertySymbols(ob).forEach(symb => {
103 if (symb.toString().indexOf('Symbol(valid)') === 0 && ob[symb]) {
104 validOb = symb
105 }
106 })
107 } else {
108 ob.valid = {}
109 validOb = 'valid'
110 }
111
112 } else {
113 validOb = 'valid'
114 }
115
116 Object.getOwnPropertySymbols(ob).forEach(symb => {
117 if (symb.toString().indexOf('Symbol(touched)') === 0 && ob[symb]) {
118 touchedOb = symb
119 }
120 })
121
122 if (touchedOb) {
123 if (!ob[touchedOb][prop]) {
124 ob[touchedOb][prop] = true
125 stateUpdated()
126 }
127 }
128
129 ob[validOb][prop] = e.currentTarget.validity.valid
130 console.log('---formField update---')
131 console.log(prop, ob)
132 console.log(`Valid? ${ob[validOb][prop]}`)
133 }
134}
135
136function formIsValid (holidingPen) {
137 let validProp = holidingPen.valid && 'valid'
138 if (!validProp) {
139 Object.getOwnPropertySymbols(holidingPen).forEach(symb => {
140 if (symb.toString().indexOf('Symbol(valid)') === 0 && holidingPen[symb]) {
141 validProp = symb
142 }
143 })
144
145 if (!validProp) {
146 return false
147 }
148 }
149
150 let validOb = Object.keys(holidingPen[validProp])
151
152 for (let i = 0; i < validOb.length; i++) {
153 if (holidingPen[validProp][validOb[i]] !== true) {
154 return false
155 }
156 }
157
158 return true
159}
160
161function fieldIsTouched (holidingPen, property) {
162 let touchedProp
163 Object.getOwnPropertySymbols(holidingPen).forEach(symb => {
164 if (symb.toString().indexOf('Symbol(touched)') === 0 && holidingPen[symb]) {
165 touchedProp = symb
166 }
167 })
168
169 if (!touchedProp) {
170 return false
171 }
172
173 return !!holidingPen[touchedProp][property]
174}
175
176function resetTouched (holidingPen) {
177 let touchedProp
178 Object.getOwnPropertySymbols(holidingPen).forEach(symb => {
179 if (symb.toString() === 'Symbol(touched)') {
180 touchedProp = symb
181 }
182 })
183
184 if (!touchedProp) {
185 return
186 }
187
188 for (let prop in holidingPen[touchedProp]) {
189 holidingPen[touchedProp][prop] = false
190 }
191 stateUpdated()
192}
193
194let waitingAlready = false
195
196function debounce (func) {
197 if (!waitingAlready) {
198 waitingAlready = true
199 nextTick(() => {
200 func()
201 waitingAlready = false
202 })
203 }
204}
205
206function nextTick (func) {
207 if (typeof window !== 'undefined' && window.requestAnimationFrame) {
208 window.requestAnimationFrame(func)
209 } else {
210 setTimeout(func, 17)
211 }
212}
213
214function stateUpdated () {
215 rootEl && update(rootEl, components(state))
216}
217
218function updateState (updateObject, options) {
219 if (updateObject) {
220 if (options && options.deepMerge === false) {
221 Object.assign(state, updateObject)
222 } else {
223 let deepMergeOptions = {clone: false}
224 if (options && options.arrayMerge === false) {
225 deepMergeOptions.arrayMerge = (destinationArray, sourceArray, options) => {
226 //don't merge arrays, just return the new one
227 return sourceArray
228 }
229 }
230 Object.assign(state, merge(state, updateObject, deepMergeOptions))
231 }
232 }
233
234 if (options && options.rerender === false) {
235 return
236 }
237
238 debounce(stateUpdated)
239
240 if (process.env.NODE_ENV !== 'production') {
241 console.log('------STATE UPDATE------')
242 console.log(updateObject)
243 console.log(' ')
244 console.log('------NEW STATE------')
245 console.log(state)
246 console.log(' ')
247 }
248}
249
250function emptySSRVideos (c) {
251 //SSR videos with source tags don't like morphing and you get double audio,
252 // so remove src from the new one so it never starts
253 let autoplayTrue = c.querySelectorAll('video[autoplay="true"]')
254 let autoplayAutoplay = c.querySelectorAll('video[autoplay="autoplay"]')
255 let autoplayOn = c.querySelectorAll('video[autoplay="on"]')
256 let selectors = [autoplayTrue, autoplayAutoplay, autoplayOn]
257 selectors.forEach(selector => {
258 Array.from(selector).forEach(video => {
259 video.pause()
260 Array.from(video.childNodes).forEach(source => {
261 source.src && (source.src = '')
262 })
263 })
264 })
265}
266
267function injectHTML (htmlString, options) {
268 if (options && options.wrapper === false) {
269 return html([htmlString])
270 }
271 return html([`<div>${htmlString}</div>`]) // using html as a regular function instead of a tag function, and prevent double encoding of ampersands while we're at it
272}
273
274function injectMarkdown (mdString, options) {
275 return injectHTML(entities.decode(marked(mdString)), options) //using html as a regular function instead of a tag function, and prevent double encoding of ampersands while we're at it
276}
277
278function cache (c, args) {
279 if (typeof window === 'undefined') {
280 return c(args)
281 }
282
283 let key = c.toString() + toSource(args)
284
285 if (!componentRegistry.has(key)) {
286
287 //not already in the registry, add it
288 let el = c(args)
289 componentRegistry.set(key, el)
290 return el
291 } else {
292 return componentRegistry.get(key)
293 }
294
295}
296
297function gotoRoute (route) {
298 let {pathname, hash, search, href} = createLocation({}, route)
299 //if pathname doesn't begin with a /, add one
300 if (pathname && pathname.indexOf('/') !== 0) {
301 pathname = `/${pathname}`
302 }
303 let component = router(route, {pathname, hash, search, href})
304 updateState({
305 router: {
306 component
307 }
308 })
309}
310
311function getRouteComponent (pathname) {
312 let foundRoute = routesArray.find(route => route.key === pathname || route.path === pathname)
313 return foundRoute && foundRoute.component
314}
315
316function getSymbol (ob, symbolName) {
317 let symbols = Object.getOwnPropertySymbols(ob)
318 if (symbols.length) {
319 return symbols.find(symb => symb.toString()
320 .includes(`Symbol(${symbolName})`))
321 }
322}
323
324function addToHoldingPen (holdingPen, addition) {
325 let currentValid = holdingPen[getSymbol(holdingPen, 'valid')]
326 let currentTouched = holdingPen[getSymbol(holdingPen, 'touched')]
327 let additionValid = addition[getSymbol(addition, 'valid')]
328 let additionTouched = addition[getSymbol(addition, 'touched')]
329 let additionWithoutSymbols = {}
330 Object.keys(addition).forEach(ad => {
331 additionWithoutSymbols[ad] = addition[ad]
332 })
333 Object.assign(currentValid, additionValid)
334 Object.assign(currentTouched, additionTouched)
335 Object.assign(holdingPen, additionWithoutSymbols)
336}
337
338function removeFromHoldingPen (holdingPen, removal) {
339 let currentValid = holdingPen[getSymbol(holdingPen, 'valid')]
340 let currentTouched = holdingPen[getSymbol(holdingPen, 'touched')]
341 removal.forEach(key => {
342 delete currentValid[key]
343 delete currentTouched[key]
344 delete holdingPen[key]
345 })
346}
347
348export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, history = historyModule} = {}) => {
349 //this default function is used for setting up client side and is not run on
350 // the server
351 ({components, el} = config)
352
353 return new Promise((resolve, reject) => {
354 let routesFormatted = routesArray.map(r => [
355 r.path,
356 (params, parts) => {
357
358 r.callback && r.callback(Object.assign({}, parts, {params}))
359 if (parts && window.location.pathname !== parts.pathname) {
360 window.history.pushState({href: parts.href}, r.title, parts.href)
361 }
362
363 updateState({
364 router: {
365 pathname: parts.pathname,
366 hash: parts.hash,
367 query: qs.parse(parts.search),
368 params,
369 key: r.key || r.path,
370 href: location.href
371 }
372 }, {
373 deepMerge: false
374 })
375
376 document.title = r.title || ''
377
378 return r.component
379
380 }
381
382 ])
383
384 router = shiftyRouter({default: '/404'}, routesFormatted)
385
386 href(location => {
387 if (externalRoutes.includes(location.pathname)) {
388 window.location = location.pathname
389 return
390 }
391
392 gotoRoute(location.href)
393 })
394
395 history(location => {
396 gotoRoute(location.href)
397 })
398
399 let c = components(state)//root element generated by components
400 if (el) {
401
402 emptySSRVideos(c)
403
404 let r = document.querySelector(el)
405 rootEl = update(r, c)
406 return resolve({rootEl, state})
407 }
408 rootEl = c
409 resolve({rootEl, state})//if no root element provided, just return the root
410 // component and the state
411 })
412}
413
414export {
415 getRouteComponent,
416 cache,
417 stateUpdated as rerender,
418 formIsValid,
419 ssr,
420 injectHTML,
421 injectMarkdown,
422 geb,
423 eventEmitter,
424 html,
425 defineRoute,
426 updateState,
427 formField,
428 gotoRoute,
429 cssTag as css,
430 axios as http,
431 fieldIsTouched,
432 resetTouched,
433 nextTick,
434 addToHoldingPen,
435 removeFromHoldingPen
436}
\No newline at end of file