1 | import shiftyRouterModule from 'shifty-router'
|
2 | import hrefModule from 'shifty-router/href'
|
3 | import historyModule from 'shifty-router/history'
|
4 | import createLocation from 'shifty-router/create-location'
|
5 | import bel from 'nanohtml'
|
6 | import update from 'nanomorph'
|
7 | import axios from 'axios'
|
8 | import cssInject from 'csjs-inject'
|
9 | import merge from 'deepmerge'
|
10 | import marked from 'marked'
|
11 | import htmlEntities from 'html-entities'
|
12 | import eventEmitter from './eventEmitter'
|
13 | import qs from 'qs'
|
14 | import toSource from 'tosource'
|
15 |
|
16 | const {AllHtmlEntities} = htmlEntities
|
17 |
|
18 | let componentRegistry
|
19 | let entities = new AllHtmlEntities()
|
20 | let cssTag = cssInject
|
21 | let componentCSSString = ''
|
22 | let routesArray = []
|
23 | let externalRoutes = []
|
24 | let state = {}
|
25 | let router
|
26 | let rootEl
|
27 | let components
|
28 | let dataInitial
|
29 | let el
|
30 |
|
31 | marked.setOptions({
|
32 | breaks: true
|
33 | })
|
34 |
|
35 | function b64DecodeUnicode (str) {
|
36 |
|
37 | return decodeURIComponent(atob(str).split('').map(function (c) {
|
38 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
39 | }).join(''))
|
40 | }
|
41 |
|
42 | if (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 |
|
69 | let geb = new eventEmitter({state})
|
70 |
|
71 | let html = (strings, ...values) => {
|
72 |
|
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 |
|
83 | function ssr (rootComponent) {
|
84 | let componentsString = `${rootComponent}`
|
85 | return {componentsString, stylesString: componentCSSString}
|
86 | }
|
87 |
|
88 | function defineRoute (routeObject) {
|
89 | if (routeObject.external) {
|
90 | return externalRoutes.push(routeObject.path)
|
91 | }
|
92 | routesArray.push(routeObject)
|
93 | }
|
94 |
|
95 | function 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 |
|
136 | function 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 |
|
161 | function 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 |
|
176 | function 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 |
|
194 | let waitingAlready = false
|
195 |
|
196 | function debounce (func) {
|
197 | if (!waitingAlready) {
|
198 | waitingAlready = true
|
199 | nextTick(() => {
|
200 | func()
|
201 | waitingAlready = false
|
202 | })
|
203 | }
|
204 | }
|
205 |
|
206 | function nextTick (func) {
|
207 | if (typeof window !== 'undefined' && window.requestAnimationFrame) {
|
208 | window.requestAnimationFrame(func)
|
209 | } else {
|
210 | setTimeout(func, 17)
|
211 | }
|
212 | }
|
213 |
|
214 | function stateUpdated () {
|
215 | rootEl && update(rootEl, components(state))
|
216 | }
|
217 |
|
218 | function 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 |
|
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 |
|
250 | function emptySSRVideos (c) {
|
251 |
|
252 |
|
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 |
|
267 | function injectHTML (htmlString, options) {
|
268 | if (options && options.wrapper === false) {
|
269 | return html([htmlString])
|
270 | }
|
271 | return html([`<div>${htmlString}</div>`])
|
272 | }
|
273 |
|
274 | function injectMarkdown (mdString, options) {
|
275 | return injectHTML(entities.decode(marked(mdString)), options)
|
276 | }
|
277 |
|
278 | function 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 |
|
288 | let el = c(args)
|
289 | componentRegistry.set(key, el)
|
290 | return el
|
291 | } else {
|
292 | return componentRegistry.get(key)
|
293 | }
|
294 |
|
295 | }
|
296 |
|
297 | function gotoRoute (route) {
|
298 | let {pathname, hash, search, href} = createLocation({}, route)
|
299 |
|
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 |
|
311 | function getRouteComponent (pathname) {
|
312 | let foundRoute = routesArray.find(route => route.key === pathname || route.path === pathname)
|
313 | return foundRoute && foundRoute.component
|
314 | }
|
315 |
|
316 | function 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 |
|
324 | function 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 |
|
338 | function 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 |
|
348 | export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, history = historyModule} = {}) => {
|
349 |
|
350 |
|
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)
|
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})
|
410 |
|
411 | })
|
412 | }
|
413 |
|
414 | export {
|
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 |