@srfnstack/fntags 1.2.8 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/fnroute.d.mts +14 -2
- package/src/fnroute.d.mts.map +1 -1
- package/src/fnroute.mjs +79 -18
- package/src/fntags.d.mts +2 -1
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +27 -2
package/package.json
CHANGED
package/src/fnroute.d.mts
CHANGED
|
@@ -40,13 +40,25 @@ export function routeSwitch(...children: (any | Node)[]): Node | (() => Node);
|
|
|
40
40
|
*/
|
|
41
41
|
export function fnlink(...children: (any | Node)[]): HTMLAnchorElement;
|
|
42
42
|
/**
|
|
43
|
-
* A function to navigate to the specified route
|
|
43
|
+
* A function to navigate to the specified route.
|
|
44
|
+
*
|
|
45
|
+
* Navigating to the current path with only a different hash or query (for example from /docs to /docs#faq)
|
|
46
|
+
* and no new context is not a route change: the history entry is updated and the hash target is scrolled
|
|
47
|
+
* into view, but the route is not re-rendered and no route events are emitted.
|
|
44
48
|
* @param {string} route The route to navigate to
|
|
45
|
-
* @param {any} context Data related to the route change
|
|
49
|
+
* @param {any} [context] Data related to the route change
|
|
46
50
|
* @param {boolean} replace Whether to replace the state or push it. pushState is used by default.
|
|
47
51
|
* @param {boolean} silent Prevent route change events from being emitted for this route change
|
|
48
52
|
*/
|
|
49
53
|
export function goTo(route: string, context?: any, replace?: boolean, silent?: boolean): void;
|
|
54
|
+
/**
|
|
55
|
+
* Scroll the element whose id matches the hash into view. If the element does not exist yet, for example because
|
|
56
|
+
* the route's content is a promise that has not resolved, wait for it to be added to the document and scroll then.
|
|
57
|
+
* With no hash, scroll to the top of the page.
|
|
58
|
+
* @param {string|null|undefined} hash The hash to scroll to, with or without the leading #
|
|
59
|
+
* @param {number} [timeout] How long to wait for the element to appear, in milliseconds
|
|
60
|
+
*/
|
|
61
|
+
export function scrollToHash(hash: string | null | undefined, timeout?: number): void;
|
|
50
62
|
/**
|
|
51
63
|
* Listen for routing events
|
|
52
64
|
* @param {string} event a string event to listen for
|
package/src/fnroute.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fnroute.d.mts","sourceRoot":"","sources":["fnroute.mjs"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,mCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,cAAc,CAoB1B;AAED;;;;;GAKG;AACH,yCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,IAAI,GAAC,CAAC,MAAI,IAAI,CAAC,CAyB3B;AAoBD;;;;GAIG;AACH,oCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,iBAAiB,CAuB7B;AAED
|
|
1
|
+
{"version":3,"file":"fnroute.d.mts","sourceRoot":"","sources":["fnroute.mjs"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,mCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,cAAc,CAoB1B;AAED;;;;;GAKG;AACH,yCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,IAAI,GAAC,CAAC,MAAI,IAAI,CAAC,CAyB3B;AAoBD;;;;GAIG;AACH,oCAHW,CAAC,MAAO,IAAI,CAAC,EAAE,GACb,iBAAiB,CAuB7B;AAED;;;;;;;;;;GAUG;AACH,4BALW,MAAM,YACN,GAAG,YACH,OAAO,WACP,OAAO,QA+CjB;AAID;;;;;;GAMG;AACH,mCAHW,MAAM,GAAC,IAAI,GAAC,SAAS,YACrB,MAAM,QAiChB;AAiED;;;;;;GAMG;AACH,iCALW,MAAM,iBAGL,MAAI,IAAI,CAanB;AAED;;;GAGG;AACH,sCAFW,MAAM,QAOhB;AAxFD;;;GAGG;AAEH;;;GAGG;AACH,6BAFU,OAAO,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CAEf;AAEzC;;;GAGG;AAEH;;;GAGG;AACH,wBAFU,OAAO,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,CAO/C;AAMJ;;GAEG;AACH;;;GAGG;AACH,gCAFU,UAAU,CAEgC;AACpD;;;GAGG;AACH,+BAFU,UAAU,CAE8B;AAClD;;;GAGG;AACH,kCAFU,UAAU,CAEoC;;;;;;;;wBAnC3C;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAC;yBAmBrD,MAAM"}
|
package/src/fnroute.mjs
CHANGED
|
@@ -106,7 +106,7 @@ function updatePathParameters () {
|
|
|
106
106
|
* @returns {HTMLAnchorElement} An anchor element that will navigate to the specified route when clicked
|
|
107
107
|
*/
|
|
108
108
|
export function fnlink (...children) {
|
|
109
|
-
let context
|
|
109
|
+
let context
|
|
110
110
|
if (children[0] && children[0].context) {
|
|
111
111
|
context = children[0].context
|
|
112
112
|
}
|
|
@@ -129,21 +129,36 @@ export function fnlink (...children) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* A function to navigate to the specified route
|
|
132
|
+
* A function to navigate to the specified route.
|
|
133
|
+
*
|
|
134
|
+
* Navigating to the current path with only a different hash or query (for example from /docs to /docs#faq)
|
|
135
|
+
* and no new context is not a route change: the history entry is updated and the hash target is scrolled
|
|
136
|
+
* into view, but the route is not re-rendered and no route events are emitted.
|
|
133
137
|
* @param {string} route The route to navigate to
|
|
134
|
-
* @param {any} context Data related to the route change
|
|
138
|
+
* @param {any} [context] Data related to the route change
|
|
135
139
|
* @param {boolean} replace Whether to replace the state or push it. pushState is used by default.
|
|
136
140
|
* @param {boolean} silent Prevent route change events from being emitted for this route change
|
|
137
141
|
*/
|
|
138
|
-
export function goTo (route, context
|
|
142
|
+
export function goTo (route, context, replace = false, silent = false) {
|
|
139
143
|
const newPath = window.location.origin + makePath(route)
|
|
144
|
+
const hash = newPath.indexOf('#') > -1 ? newPath.split('#')[1] : null
|
|
145
|
+
const oldPathState = pathState()
|
|
146
|
+
const currentPath = ensureOnlyLeadingSlash(route.split(/[#?]/)[0])
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
if (currentPath === oldPathState.currentPath && context === undefined) {
|
|
149
|
+
if (replace) {
|
|
150
|
+
history.replaceState({}, route, newPath)
|
|
151
|
+
} else {
|
|
152
|
+
history.pushState({}, route, newPath)
|
|
153
|
+
}
|
|
154
|
+
scrollToHash(hash)
|
|
155
|
+
return
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
const
|
|
158
|
+
const patch = {
|
|
159
|
+
currentPath,
|
|
160
|
+
context: context === undefined ? {} : context
|
|
161
|
+
}
|
|
147
162
|
const newPathState = Object.assign({}, oldPathState, patch)
|
|
148
163
|
if (!silent) {
|
|
149
164
|
try {
|
|
@@ -160,25 +175,59 @@ export function goTo (route, context = {}, replace = false, silent = false) {
|
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
setTimeout(() => {
|
|
163
|
-
pathState.assign(
|
|
164
|
-
currentPath: route.split(/[#?]/)[0],
|
|
165
|
-
context
|
|
166
|
-
})
|
|
178
|
+
pathState.assign(patch)
|
|
167
179
|
if (!silent) {
|
|
168
180
|
emit(afterRouteChange, newPathState, oldPathState)
|
|
169
181
|
}
|
|
170
|
-
|
|
171
|
-
const el = document.getElementById(decodeURIComponent(newPath.split('#')[1]))
|
|
172
|
-
el && el.scrollIntoView()
|
|
173
|
-
} else {
|
|
174
|
-
window.scrollTo(0, 0)
|
|
175
|
-
}
|
|
182
|
+
scrollToHash(hash)
|
|
176
183
|
if (!silent) {
|
|
177
184
|
emit(routeChangeComplete, newPathState, oldPathState)
|
|
178
185
|
}
|
|
179
186
|
})
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
let cancelPendingScroll = null
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Scroll the element whose id matches the hash into view. If the element does not exist yet, for example because
|
|
193
|
+
* the route's content is a promise that has not resolved, wait for it to be added to the document and scroll then.
|
|
194
|
+
* With no hash, scroll to the top of the page.
|
|
195
|
+
* @param {string|null|undefined} hash The hash to scroll to, with or without the leading #
|
|
196
|
+
* @param {number} [timeout] How long to wait for the element to appear, in milliseconds
|
|
197
|
+
*/
|
|
198
|
+
export function scrollToHash (hash, timeout = 5000) {
|
|
199
|
+
if (cancelPendingScroll) {
|
|
200
|
+
cancelPendingScroll()
|
|
201
|
+
}
|
|
202
|
+
const id = hash ? decodeURIComponent(hash.startsWith('#') ? hash.slice(1) : hash) : ''
|
|
203
|
+
if (!id) {
|
|
204
|
+
window.scrollTo(0, 0)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const scrollIfPresent = () => {
|
|
208
|
+
const el = document.getElementById(id)
|
|
209
|
+
if (el) {
|
|
210
|
+
el.scrollIntoView()
|
|
211
|
+
}
|
|
212
|
+
return !!el
|
|
213
|
+
}
|
|
214
|
+
if (scrollIfPresent() || typeof MutationObserver === 'undefined') {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const observer = new MutationObserver(() => {
|
|
218
|
+
if (scrollIfPresent()) {
|
|
219
|
+
cancelPendingScroll()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
const timer = setTimeout(() => cancelPendingScroll(), timeout)
|
|
223
|
+
cancelPendingScroll = () => {
|
|
224
|
+
observer.disconnect()
|
|
225
|
+
clearTimeout(timer)
|
|
226
|
+
cancelPendingScroll = null
|
|
227
|
+
}
|
|
228
|
+
observer.observe(document.documentElement, { childList: true, subtree: true })
|
|
229
|
+
}
|
|
230
|
+
|
|
182
231
|
const ensureOnlyLeadingSlash = (part) => removeTrailingSlash(part.startsWith('/') ? part : '/' + part)
|
|
183
232
|
|
|
184
233
|
const removeTrailingSlash = part => part.endsWith('/') && part.length > 1 ? part.slice(0, -1) : part
|
|
@@ -280,6 +329,12 @@ window.addEventListener(
|
|
|
280
329
|
const patch = {
|
|
281
330
|
currentPath: ensureOnlyLeadingSlash(window.location.pathname.replace(new RegExp('^' + pathState().rootPath), '')) || '/'
|
|
282
331
|
}
|
|
332
|
+
// Clicking <a href="#section"> and moving back/forward between hash entries fire popstate without
|
|
333
|
+
// changing the path. That is not a route change, so leave the rendered route alone and let the
|
|
334
|
+
// browser handle scrolling to the fragment.
|
|
335
|
+
if (patch.currentPath === oldPathState.currentPath) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
283
338
|
const newPathState = Object.assign({}, oldPathState, patch)
|
|
284
339
|
try {
|
|
285
340
|
emit(beforeRouteChange, newPathState, oldPathState)
|
|
@@ -294,6 +349,12 @@ window.addEventListener(
|
|
|
294
349
|
}
|
|
295
350
|
)
|
|
296
351
|
|
|
352
|
+
// On initial load the browser only scrolls to the hash if the target already exists. When the target is inside
|
|
353
|
+
// a route whose content has not rendered yet, wait for it the same way goTo does.
|
|
354
|
+
if (window.location.hash && !document.getElementById(decodeURIComponent(window.location.hash.slice(1)))) {
|
|
355
|
+
scrollToHash(window.location.hash)
|
|
356
|
+
}
|
|
357
|
+
|
|
297
358
|
const makePath = path => (pathState().rootPath === '/' ? '' : pathState().rootPath) + ensureOnlyLeadingSlash(path)
|
|
298
359
|
|
|
299
360
|
const makePathPattern = (path, absolute) => {
|
package/src/fntags.d.mts
CHANGED
|
@@ -72,7 +72,8 @@ export function h<T extends HTMLElement | SVGElement>(tag: string, ...children:
|
|
|
72
72
|
*/
|
|
73
73
|
export function fnstate<T>(initialValue: T, mapKey?: (T: any) => any): FnState<T>;
|
|
74
74
|
/**
|
|
75
|
-
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
|
|
75
|
+
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes.
|
|
76
|
+
* Arrays are flattened at any depth into a DocumentFragment.
|
|
76
77
|
* @param {any} node The node to render
|
|
77
78
|
* @returns {Node} The rendered node
|
|
78
79
|
*/
|
package/src/fntags.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,+BAFW,OAAO,QAIjB;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,+BAFW,OAAO,QAIjB;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OAsC1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cAqIlB;AAwlBD;;;;;GAKG;AACH,iCAHW,GAAG,GACD,IAAI,CAgChB;AAqFD;;;;GAIG;AACH,6BAHW,GAAG,GACD,OAAO,CAInB;AAED;;;;GAIG;AACH,mCAHW,GAAG,GACF,MAAM,CAIjB;AAED;;;;;;;;;;GAUG;AACH,kEALW,MAAM,GAAC,MAAM,OACb,MAAM,YACN,MAAM,EAAE,GAAC,IAAI,EAAE,KAezB;AAOD;;;;;;;;GAQG;AACH,wCAJW,MAAM,0BAiBhB;AAED;;;;;;;;;GASG;AACH,oCALW,MAAM,gBACN,GAAG,eACE,GAAG,KAAK,MAAM,OAc7B;AAED;;;;;;;;GAQG;AACH,mCAJW,WAAW,SACX,CAAC,MAAI,IAAI,CAAC,GAAC,IAAI,GACb,WAAW,CAWvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;;kCAzjCmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAG/C,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;;sCAEI,CAAC,YAAY,CAAC,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;mCAEvC,CAAC,YAAY,CAAC,KAAG,MAAM,KAAK,MAAM;;;;yCAC7B,GAAG,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;+CACrB,GAAG,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7C,GAAG,KAAG,IAAI;;;;cAGhB,MAAK,GAAG;;;;;qBACC,CAAC,KAAG,IAAI;;;;;;oBAEV,MAAM,KAAG,GAAG;;;;oBAGZ,MAAM,SAAS,GAAG,mBAAmB,OAAO,KAAG,IAAI;;;;uCAClC,CAAC,YAAY,CAAC,KAAG,IAAI,KAAK,IAAI;;;;eACtD,OAAO;;;;;sDAKqB,CAAC,KAAG,CAAC"}
|
package/src/fntags.mjs
CHANGED
|
@@ -81,6 +81,8 @@ export function h (tag, ...children) {
|
|
|
81
81
|
for (let i = firstChildIdx; i < children.length; i++) {
|
|
82
82
|
const child = children[i]
|
|
83
83
|
if (Array.isArray(child)) {
|
|
84
|
+
// Append array items directly to skip the DocumentFragment renderNode would build.
|
|
85
|
+
// Nested arrays inside still go through renderNode, which flattens them.
|
|
84
86
|
for (const c of child) {
|
|
85
87
|
element.append(renderNode(c))
|
|
86
88
|
}
|
|
@@ -500,6 +502,15 @@ function doBindChildren (parent, element) {
|
|
|
500
502
|
return parent
|
|
501
503
|
}
|
|
502
504
|
|
|
505
|
+
// A bound function must produce exactly one node. A DocumentFragment empties
|
|
506
|
+
// itself when inserted, so there would be nothing left to replaceWith on the
|
|
507
|
+
// next state change and the binding would silently stop updating.
|
|
508
|
+
function assertSingleNode (evaluated, name) {
|
|
509
|
+
if (Array.isArray(evaluated)) {
|
|
510
|
+
throw new Error(name + ' functions must return a single node, not an array. Wrap the array in an element, e.g. div(...items).')
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
503
514
|
const doBind = function (ctx, element, handleReplace) {
|
|
504
515
|
if (typeof element !== 'function') {
|
|
505
516
|
throw new Error('You must pass a function to bind with')
|
|
@@ -512,6 +523,7 @@ const doBind = function (ctx, element, handleReplace) {
|
|
|
512
523
|
const thisRenderCleanups = []
|
|
513
524
|
activeRenderCleanups = thisRenderCleanups
|
|
514
525
|
const evaluated = evaluateElement(element, ctx.currentValue)
|
|
526
|
+
assertSingleNode(evaluated, 'bindAs')
|
|
515
527
|
const childElCtx = (typeof evaluated === 'function' && evaluated.isBindGetter)
|
|
516
528
|
? evaluated.elCtx
|
|
517
529
|
: null
|
|
@@ -611,6 +623,7 @@ const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
|
611
623
|
const thisRenderCleanups = []
|
|
612
624
|
activeRenderCleanups = thisRenderCleanups
|
|
613
625
|
const evaluated = evaluateElement(element, ctx.currentValue, oldValue)
|
|
626
|
+
assertSingleNode(evaluated, 'bindAs')
|
|
614
627
|
const childElCtx = (typeof evaluated === 'function' && evaluated.isBindGetter)
|
|
615
628
|
? evaluated.elCtx
|
|
616
629
|
: null
|
|
@@ -790,7 +803,9 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
790
803
|
const thisElementCleanups = []
|
|
791
804
|
activeRenderCleanups = thisElementCleanups
|
|
792
805
|
// Pass the valueState (the fnstate wrapper) to the user's element builder
|
|
793
|
-
|
|
806
|
+
const evaluated = evaluateElement(bindContext.element, valueState)
|
|
807
|
+
assertSingleNode(evaluated, 'bindChildren')
|
|
808
|
+
current = bindContext.boundElementByKey[key] = renderNode(evaluated)
|
|
794
809
|
activeRenderCleanups = prevCleanups
|
|
795
810
|
bindContext.elementCleanups[key] = thisElementCleanups
|
|
796
811
|
current.key = key
|
|
@@ -859,7 +874,8 @@ const evaluateElement = (element, value, oldValue) => {
|
|
|
859
874
|
}
|
|
860
875
|
|
|
861
876
|
/**
|
|
862
|
-
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes
|
|
877
|
+
* Convert non objects (objects are assumed to be nodes) to text nodes and allow promises to resolve to nodes.
|
|
878
|
+
* Arrays are flattened at any depth into a DocumentFragment.
|
|
863
879
|
* @param {any} node The node to render
|
|
864
880
|
* @returns {Node} The rendered node
|
|
865
881
|
*/
|
|
@@ -868,6 +884,15 @@ export function renderNode (node) {
|
|
|
868
884
|
const element = h('div')
|
|
869
885
|
node(element, 'node')
|
|
870
886
|
return element
|
|
887
|
+
} else if (Array.isArray(node)) {
|
|
888
|
+
// Arrays (including nested arrays and arrays resolved from promises) are
|
|
889
|
+
// flattened into a DocumentFragment so each item lands in the DOM as its
|
|
890
|
+
// own node. Without this, append/replaceWith would stringify the array.
|
|
891
|
+
const frag = document.createDocumentFragment()
|
|
892
|
+
for (const child of node) {
|
|
893
|
+
frag.append(renderNode(child))
|
|
894
|
+
}
|
|
895
|
+
return frag
|
|
871
896
|
} else if (node && typeof node === 'object') {
|
|
872
897
|
if (typeof node.then === 'function') {
|
|
873
898
|
let temp = h('div', { style: 'display:none', class: 'fntags-promise-marker' })
|