aberdeen 1.1.0 → 1.2.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.
@@ -1,4 +1,4 @@
1
- var H=Symbol("matchFailed"),G=Symbol("matchRest");class J{routes=[];addRoute(...q){let j=q.slice(0,-1),k=q[q.length-1];if(typeof k!=="function")throw new Error("Last argument should be a handler function");if(j.filter((C)=>C===G).length>1)throw new Error("Only one matchRest is allowed");this.routes.push({matchers:j,handler:k})}dispatch(q){for(let j of this.routes){let k=K(j,q);if(k)return j.handler(...k),!0}return!1}}function K(q,j){let k=[],B=0;for(let C of q.matchers){if(C===G){let z=j.length-(q.matchers.length-1);if(z<0)return;k.push(j.slice(B,B+z)),B+=z;continue}if(B>=j.length)return;let E=j[B];if(typeof C==="string"){if(E!==C)return}else if(typeof C==="function"){let z=C(E);if(z===H||typeof z==="number"&&isNaN(z))return;k.push(z)}B++}return k}export{G as matchRest,H as matchFailed,J as Dispatcher};
1
+ var H=Symbol("matchFailed"),G=Symbol("matchRest");class J{routes=[];addRoute(...q){let j=q.slice(0,-1),k=q[q.length-1];if(typeof k!=="function")throw Error("Last argument should be a handler function");if(j.filter((C)=>C===G).length>1)throw Error("Only one matchRest is allowed");this.routes.push({matchers:j,handler:k})}dispatch(q){for(let j of this.routes){let k=K(j,q);if(k)return j.handler(...k),!0}return!1}}function K(q,j){let k=[],B=0;for(let C of q.matchers){if(C===G){let z=j.length-(q.matchers.length-1);if(z<0)return;k.push(j.slice(B,B+z)),B+=z;continue}if(B>=j.length)return;let E=j[B];if(typeof C==="string"){if(E!==C)return}else if(typeof C==="function"){let z=C(E);if(z===H||typeof z==="number"&&isNaN(z))return;k.push(z)}B++}return k}export{G as matchRest,H as matchFailed,J as Dispatcher};
2
2
 
3
- //# debugId=80743EF466EF1C6E64756E2164756E21
3
+ //# debugId=DE8D81380A22066F64756E2164756E21
4
4
  //# sourceMappingURL=dispatcher.js.map
@@ -4,7 +4,7 @@
4
4
  "sourcesContent": [
5
5
  "/**\n * Symbol to return when a custom {@link Dispatcher.addRoute} matcher cannot match a segment.\n */\nexport const matchFailed: unique symbol = Symbol(\"matchFailed\");\n\n/**\n * Special {@link Dispatcher.addRoute} matcher that matches the rest of the segments as an array of strings.\n */\nexport const matchRest: unique symbol = Symbol(\"matchRest\");\n\ntype Matcher = string | ((segment: string) => any) | typeof matchRest;\n\ntype ExtractParamType<M> = M extends string\n? never : (\n M extends ((segment: string) => infer R)\n ? Exclude<R, typeof matchFailed>\n : (M extends typeof matchRest ? string[] : never)\n);\n\ntype ParamsFromMatchers<T extends Matcher[]> = T extends [infer M1, ...infer Rest]\n? (\n M1 extends Matcher\n ? (\n ExtractParamType<M1> extends never\n ? ParamsFromMatchers<Rest extends Matcher[] ? Rest : []>\n : [ExtractParamType<M1>, ...ParamsFromMatchers<Rest extends Matcher[] ? Rest : []>]\n ) : never\n) : [];\n\ninterface DispatcherRoute {\n matchers: Matcher[];\n handler: (...params: any[]) => void;\n}\n\n/**\n * Simple route matcher and dispatcher.\n * \n * Example usage:\n * \n * ```ts\n * const dispatcher = new Dispatcher();\n * \n * dispatcher.addRoute(\"user\", Number, \"stream\", String, (id, stream) => {\n * console.log(`User ${id}, stream ${stream}`);\n * });\n *\n * dispatcher.dispatch([\"user\", \"42\", \"stream\", \"music\"]);\n * // Logs: User 42, stream music\n * \n * dispatcher.addRoute(\"search\", matchRest, (terms: string[]) => {\n * console.log(\"Search terms:\", terms);\n * });\n * \n * dispatcher.dispatch([\"search\", \"classical\", \"piano\"]);\n * // Logs: Search terms: [ 'classical', 'piano' ]\n * ```\n */\nexport class Dispatcher {\n private routes: Array<DispatcherRoute> = [];\n \n /**\n * Add a route with matchers and a handler function.\n * @param args An array of matchers followed by a handler function. Each matcher can be:\n * - A string: matches exactly that string.\n * - A function: takes a string segment and returns a value (of any type) if it matches, or {@link matchFailed} if it doesn't match. The return value (if not `matchFailed` and not `NaN`) is passed as a parameter to the handler function. The built-in functions `Number` and `String` can be used to match numeric and string segments respectively.\n * - The special {@link matchRest} symbol: matches the rest of the segments as an array of strings. Only one `matchRest` is allowed, and it must be the last matcher.\n * @template T - Array of matcher types.\n * @template H - Handler function type, inferred from the matchers.\n */\n addRoute<T extends Matcher[], H extends (...args: ParamsFromMatchers<T>) => void>(...args: [...T, H]): void {\n const matchers = args.slice(0, -1) as Matcher[];\n const handler = args[args.length - 1] as (...args: any) => any;\n\n if (typeof handler !== \"function\") {\n throw new Error(\"Last argument should be a handler function\");\n }\n \n const restCount = matchers.filter(m => m === matchRest).length;\n if (restCount > 1) {\n throw new Error(\"Only one matchRest is allowed\");\n }\n\n this.routes.push({ matchers, handler });\n }\n \n /**\n * Dispatches the given segments to the first route handler that matches.\n * @param segments Array of string segments to match against the added routes. When using this class with the Aberdeen `route` module, one would typically pass `route.current.p`.\n * @returns True if a matching route was found and handled, false otherwise.\n */\n dispatch(segments: string[]): boolean {\n for (const route of this.routes) {\n const args = matchRoute(route, segments);\n if (args) {\n route.handler(...args);\n return true;\n }\n }\n return false;\n }\n}\n\nfunction matchRoute(route: DispatcherRoute, segments: string[]): any[] | undefined {\n const args: any[] = [];\n let segmentIndex = 0;\n\n for (const matcher of route.matchers) {\n if (matcher === matchRest) {\n const len = segments.length - (route.matchers.length - 1);\n if (len < 0) return;\n args.push(segments.slice(segmentIndex, segmentIndex + len));\n segmentIndex += len;\n continue;\n }\n\n if (segmentIndex >= segments.length) return;\n const segment = segments[segmentIndex];\n \n if (typeof matcher === \"string\") {\n if (segment !== matcher) return;\n } else if (typeof matcher === \"function\") {\n const result = matcher(segment);\n if (result === matchFailed || (typeof result === 'number' && isNaN(result))) return;\n args.push(result);\n }\n \n segmentIndex++;\n }\n return args; // success!\n}\n"
6
6
  ],
7
- "mappings": "AAGO,IAAM,EAA6B,OAAO,aAAa,EAKjD,EAA2B,OAAO,WAAW,EAiDnD,MAAM,CAAW,CACZ,OAAiC,CAAC,EAW1C,QAAiF,IAAI,EAAuB,CACxG,IAAM,EAAW,EAAK,MAAM,EAAG,EAAE,EAC3B,EAAU,EAAK,EAAK,OAAS,GAEnC,GAAI,OAAO,IAAY,WACnB,MAAM,IAAI,MAAM,4CAA4C,EAIhE,GADkB,EAAS,OAAO,KAAK,IAAM,CAAS,EAAE,OACxC,EACZ,MAAM,IAAI,MAAM,+BAA+B,EAGnD,KAAK,OAAO,KAAK,CAAE,WAAU,SAAQ,CAAC,EAQ1C,QAAQ,CAAC,EAA6B,CAClC,QAAW,KAAS,KAAK,OAAQ,CAC7B,IAAM,EAAO,EAAW,EAAO,CAAQ,EACvC,GAAI,EAEA,OADA,EAAM,QAAQ,GAAG,CAAI,EACd,GAGf,MAAO,GAEf,CAEA,SAAS,CAAU,CAAC,EAAwB,EAAuC,CAC/E,IAAM,EAAc,CAAC,EACjB,EAAe,EAEnB,QAAW,KAAW,EAAM,SAAU,CAClC,GAAI,IAAY,EAAW,CACvB,IAAM,EAAM,EAAS,QAAU,EAAM,SAAS,OAAS,GACvD,GAAI,EAAM,EAAG,OACb,EAAK,KAAK,EAAS,MAAM,EAAc,EAAe,CAAG,CAAC,EAC1D,GAAgB,EAChB,SAGJ,GAAI,GAAgB,EAAS,OAAQ,OACrC,IAAM,EAAU,EAAS,GAEzB,GAAI,OAAO,IAAY,UACnB,GAAI,IAAY,EAAS,OACtB,QAAI,OAAO,IAAY,WAAY,CACtC,IAAM,EAAS,EAAQ,CAAO,EAC9B,GAAI,IAAW,GAAgB,OAAO,IAAW,UAAY,MAAM,CAAM,EAAI,OAC7E,EAAK,KAAK,CAAM,EAGpB,IAEJ,OAAO",
8
- "debugId": "80743EF466EF1C6E64756E2164756E21",
7
+ "mappings": "AAGO,IAAM,EAA6B,OAAO,aAAa,EAKjD,EAA2B,OAAO,WAAW,EAiDnD,MAAM,CAAW,CACZ,OAAiC,CAAC,EAW1C,QAAiF,IAAI,EAAuB,CACxG,IAAM,EAAW,EAAK,MAAM,EAAG,EAAE,EAC3B,EAAU,EAAK,EAAK,OAAS,GAEnC,GAAI,OAAO,IAAY,WACnB,MAAU,MAAM,4CAA4C,EAIhE,GADkB,EAAS,OAAO,KAAK,IAAM,CAAS,EAAE,OACxC,EACZ,MAAU,MAAM,+BAA+B,EAGnD,KAAK,OAAO,KAAK,CAAE,WAAU,SAAQ,CAAC,EAQ1C,QAAQ,CAAC,EAA6B,CAClC,QAAW,KAAS,KAAK,OAAQ,CAC7B,IAAM,EAAO,EAAW,EAAO,CAAQ,EACvC,GAAI,EAEA,OADA,EAAM,QAAQ,GAAG,CAAI,EACd,GAGf,MAAO,GAEf,CAEA,SAAS,CAAU,CAAC,EAAwB,EAAuC,CAC/E,IAAM,EAAc,CAAC,EACjB,EAAe,EAEnB,QAAW,KAAW,EAAM,SAAU,CAClC,GAAI,IAAY,EAAW,CACvB,IAAM,EAAM,EAAS,QAAU,EAAM,SAAS,OAAS,GACvD,GAAI,EAAM,EAAG,OACb,EAAK,KAAK,EAAS,MAAM,EAAc,EAAe,CAAG,CAAC,EAC1D,GAAgB,EAChB,SAGJ,GAAI,GAAgB,EAAS,OAAQ,OACrC,IAAM,EAAU,EAAS,GAEzB,GAAI,OAAO,IAAY,UACnB,GAAI,IAAY,EAAS,OACtB,QAAI,OAAO,IAAY,WAAY,CACtC,IAAM,EAAS,EAAQ,CAAO,EAC9B,GAAI,IAAW,GAAgB,OAAO,IAAW,UAAY,MAAM,CAAM,EAAI,OAC7E,EAAK,KAAK,CAAM,EAGpB,IAEJ,OAAO",
8
+ "debugId": "DE8D81380A22066F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
- import{defaultEmitHandler as W,withEmitHandler as X}from"./aberdeen.js";function S(G){let C=new Map;return X((B,z,I,J)=>{U(C,B,z,I,J)},G),C}function U(G,C,B,z,I){let J=G.get(C);if(!J)J=new Map,G.set(C,J);let K=J.get(B),L=K?K[1]:I;if(z===L)J.delete(B);else J.set(B,[z,L])}function V(G){for(let[C,B]of G)for(let[z,[I,J]]of B)W(C,z,I,J)}function Q(G,C,B=!1){for(let[z,I]of C)for(let[J,[K,L]]of I)U(G,z,J,B?L:K,B?K:L)}function R(G,C=!1){for(let[B,z]of G)for(let[I,[J,K]]of z){let L=B[I];if(L!==K)if(C)setTimeout(()=>{throw new Error(`Applying invalid patch: data ${L} is unequal to expected old data ${K} for index ${I}`)},0);else return!1}for(let[B,z]of G)for(let[I,[J,K]]of z)B[I]=J;return!0}var N=[];function Z(G){let C=S(G);return N.push(C),V(C),C}function _(G,C=[]){let B=new Map;for(let z of N)Q(B,z,!0);R(B,!0);for(let z of C){let I=N.indexOf(z);if(I>=0)N.splice(I,1)}if(G)Q(B,S(G));for(let z=0;z<N.length;z++)if(R(N[z]))Q(B,N[z]);else N.splice(z,1),z--;V(B)}export{Z as applyPrediction,_ as applyCanon};
1
+ import{defaultEmitHandler as W,withEmitHandler as X}from"./aberdeen.js";function S(G){let C=new Map;return X((B,z,I,J)=>{U(C,B,z,I,J)},G),C}function U(G,C,B,z,I){let J=G.get(C);if(!J)J=new Map,G.set(C,J);let K=J.get(B),L=K?K[1]:I;if(z===L)J.delete(B);else J.set(B,[z,L])}function V(G){for(let[C,B]of G)for(let[z,[I,J]]of B)W(C,z,I,J)}function Q(G,C,B=!1){for(let[z,I]of C)for(let[J,[K,L]]of I)U(G,z,J,B?L:K,B?K:L)}function R(G,C=!1){for(let[B,z]of G)for(let[I,[J,K]]of z){let L=B[I];if(L!==K)if(C)setTimeout(()=>{throw Error(`Applying invalid patch: data ${L} is unequal to expected old data ${K} for index ${I}`)},0);else return!1}for(let[B,z]of G)for(let[I,[J,K]]of z)B[I]=J;return!0}var N=[];function Z(G){let C=S(G);return N.push(C),V(C),C}function _(G,C=[]){let B=new Map;for(let z of N)Q(B,z,!0);R(B,!0);for(let z of C){let I=N.indexOf(z);if(I>=0)N.splice(I,1)}if(G)Q(B,S(G));for(let z=0;z<N.length;z++)if(R(N[z]))Q(B,N[z]);else N.splice(z,1),z--;V(B)}export{Z as applyPrediction,_ as applyCanon};
2
2
 
3
- //# debugId=277071596D78B55D64756E2164756E21
3
+ //# debugId=4067069B259E531864756E2164756E21
4
4
  //# sourceMappingURL=prediction.js.map
@@ -4,7 +4,7 @@
4
4
  "sourcesContent": [
5
5
  "import { defaultEmitHandler, withEmitHandler } from \"./aberdeen.js\";\nimport type { TargetType } from \"./aberdeen.js\";\n\n/**\n * Represents a set of changes that can be applied to proxied objects.\n * This is an opaque type - its internal structure is not part of the public API.\n * @private\n */\nexport type Patch = Map<TargetType, Map<any, [any, any]>>;\n\nfunction recordPatch(func: () => void): Patch {\n\tconst recordingPatch = new Map();\n\twithEmitHandler((target, index, newData, oldData) => {\n\t\taddToPatch(recordingPatch, target, index, newData, oldData);\n\t}, func);\n\treturn recordingPatch;\n}\n\nfunction addToPatch(\n\tpatch: Patch,\n\tcollection: TargetType,\n\tindex: any,\n\tnewData: any,\n\toldData: any,\n) {\n\tlet collectionMap = patch.get(collection);\n\tif (!collectionMap) {\n\t\tcollectionMap = new Map();\n\t\tpatch.set(collection, collectionMap);\n\t}\n\tconst prev = collectionMap.get(index);\n\tconst oldData0 = prev ? prev[1] : oldData;\n\tif (newData === oldData0) collectionMap.delete(index);\n\telse collectionMap.set(index, [newData, oldData0]);\n}\n\nfunction emitPatch(patch: Patch) {\n\tfor (const [collection, collectionMap] of patch) {\n\t\tfor (const [index, [newData, oldData]] of collectionMap) {\n\t\t\tdefaultEmitHandler(collection, index, newData, oldData);\n\t\t}\n\t}\n}\n\nfunction mergePatch(target: Patch, source: Patch, reverse = false) {\n\tfor (const [collection, collectionMap] of source) {\n\t\tfor (const [index, [newData, oldData]] of collectionMap) {\n\t\t\taddToPatch(\n\t\t\t\ttarget,\n\t\t\t\tcollection,\n\t\t\t\tindex,\n\t\t\t\treverse ? oldData : newData,\n\t\t\t\treverse ? newData : oldData,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction silentlyApplyPatch(patch: Patch, force = false): boolean {\n\tfor (const [collection, collectionMap] of patch) {\n\t\tfor (const [index, [newData, oldData]] of collectionMap) {\n\t\t\tconst actualData = (collection as any)[index];\n\t\t\tif (actualData !== oldData) {\n\t\t\t\tif (force)\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Applying invalid patch: data ${actualData} is unequal to expected old data ${oldData} for index ${index}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}, 0);\n\t\t\t\telse return false;\n\t\t\t}\n\t\t}\n\t}\n\tfor (const [collection, collectionMap] of patch) {\n\t\tfor (const [index, [newData, oldData]] of collectionMap) {\n\t\t\t(collection as any)[index] = newData;\n\t\t}\n\t}\n\treturn true;\n}\n\nconst appliedPredictions: Array<Patch> = [];\n\n/**\n * Run the provided function, while treating all changes to Observables as predictions,\n * meaning they will be reverted when changes come back from the server (or some other\n * async source).\n * @param predictFunc The function to run. It will generally modify some Observables\n * \tto immediately reflect state (as closely as possible) that we expect the server\n * to communicate back to us later on.\n * @returns A `Patch` object. Don't modify it. This is only meant to be passed to `applyCanon`.\n */\nexport function applyPrediction(predictFunc: () => void): Patch {\n\tconst patch = recordPatch(predictFunc);\n\tappliedPredictions.push(patch);\n\temitPatch(patch);\n\treturn patch;\n}\n\n/**\n * Temporarily revert all outstanding predictions, optionally run the provided function\n * (which will generally make authoritative changes to the data based on a server response),\n * and then attempt to reapply the predictions on top of the new canonical state, dropping\n * any predictions that can no longer be applied cleanly (the data has been modified) or\n * that were specified in `dropPredictions`.\n *\n * All of this is done such that redraws are only triggered if the overall effect is an\n * actual change to an `Observable`.\n * @param canonFunc The function to run without any predictions applied. This will typically\n * make authoritative changes to the data, based on a server response.\n * @param dropPredictions An optional list of predictions (as returned by `applyPrediction`)\n * to undo. Typically, when a server response for a certain request is being handled,\n * you'd want to drop the prediction that was done for that request.\n */\nexport function applyCanon(\n\tcanonFunc?: () => void,\n\tdropPredictions: Array<Patch> = [],\n) {\n\tconst resultPatch = new Map();\n\tfor (const prediction of appliedPredictions)\n\t\tmergePatch(resultPatch, prediction, true);\n\tsilentlyApplyPatch(resultPatch, true);\n\n\tfor (const prediction of dropPredictions) {\n\t\tconst pos = appliedPredictions.indexOf(prediction);\n\t\tif (pos >= 0) appliedPredictions.splice(pos, 1);\n\t}\n\tif (canonFunc) mergePatch(resultPatch, recordPatch(canonFunc));\n\n\tfor (let idx = 0; idx < appliedPredictions.length; idx++) {\n\t\tif (silentlyApplyPatch(appliedPredictions[idx])) {\n\t\t\tmergePatch(resultPatch, appliedPredictions[idx]);\n\t\t} else {\n\t\t\tappliedPredictions.splice(idx, 1);\n\t\t\tidx--;\n\t\t}\n\t}\n\n\temitPatch(resultPatch);\n}\n"
6
6
  ],
7
- "mappings": "AAAA,6BAAS,qBAAoB,sBAU7B,SAAS,CAAW,CAAC,EAAyB,CAC7C,IAAM,EAAiB,IAAI,IAI3B,OAHA,EAAgB,CAAC,EAAQ,EAAO,EAAS,IAAY,CACpD,EAAW,EAAgB,EAAQ,EAAO,EAAS,CAAO,GACxD,CAAI,EACA,EAGR,SAAS,CAAU,CAClB,EACA,EACA,EACA,EACA,EACC,CACD,IAAI,EAAgB,EAAM,IAAI,CAAU,EACxC,IAAK,EACJ,EAAgB,IAAI,IACpB,EAAM,IAAI,EAAY,CAAa,EAEpC,IAAM,EAAO,EAAc,IAAI,CAAK,EAC9B,EAAW,EAAO,EAAK,GAAK,EAClC,GAAI,IAAY,EAAU,EAAc,OAAO,CAAK,EAC/C,OAAc,IAAI,EAAO,CAAC,EAAS,CAAQ,CAAC,EAGlD,SAAS,CAAS,CAAC,EAAc,CAChC,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACzC,EAAmB,EAAY,EAAO,EAAS,CAAO,EAKzD,SAAS,CAAU,CAAC,EAAe,EAAe,EAAU,GAAO,CAClE,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACzC,EACC,EACA,EACA,EACA,EAAU,EAAU,EACpB,EAAU,EAAU,CACrB,EAKH,SAAS,CAAkB,CAAC,EAAc,EAAQ,GAAgB,CACjE,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EAAe,CACxD,IAAM,EAAc,EAAmB,GACvC,GAAI,IAAe,EAClB,GAAI,EACH,WAAW,IAAM,CAChB,MAAM,IAAI,MACT,gCAAgC,qCAA8C,eAAqB,GACpG,GACE,CAAC,EACA,WAAO,GAIf,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACxC,EAAmB,GAAS,EAG/B,MAAO,GAGR,IAAM,EAAmC,CAAC,EAWnC,SAAS,CAAe,CAAC,EAAgC,CAC/D,IAAM,EAAQ,EAAY,CAAW,EAGrC,OAFA,EAAmB,KAAK,CAAK,EAC7B,EAAU,CAAK,EACR,EAkBD,SAAS,CAAU,CACzB,EACA,EAAgC,CAAC,EAChC,CACD,IAAM,EAAc,IAAI,IACxB,QAAW,KAAc,EACxB,EAAW,EAAa,EAAY,EAAI,EACzC,EAAmB,EAAa,EAAI,EAEpC,QAAW,KAAc,EAAiB,CACzC,IAAM,EAAM,EAAmB,QAAQ,CAAU,EACjD,GAAI,GAAO,EAAG,EAAmB,OAAO,EAAK,CAAC,EAE/C,GAAI,EAAW,EAAW,EAAa,EAAY,CAAS,CAAC,EAE7D,QAAS,EAAM,EAAG,EAAM,EAAmB,OAAQ,IAClD,GAAI,EAAmB,EAAmB,EAAI,EAC7C,EAAW,EAAa,EAAmB,EAAI,EAE/C,OAAmB,OAAO,EAAK,CAAC,EAChC,IAIF,EAAU,CAAW",
8
- "debugId": "277071596D78B55D64756E2164756E21",
7
+ "mappings": "AAAA,6BAAS,qBAAoB,sBAU7B,SAAS,CAAW,CAAC,EAAyB,CAC7C,IAAM,EAAiB,IAAI,IAI3B,OAHA,EAAgB,CAAC,EAAQ,EAAO,EAAS,IAAY,CACpD,EAAW,EAAgB,EAAQ,EAAO,EAAS,CAAO,GACxD,CAAI,EACA,EAGR,SAAS,CAAU,CAClB,EACA,EACA,EACA,EACA,EACC,CACD,IAAI,EAAgB,EAAM,IAAI,CAAU,EACxC,GAAI,CAAC,EACJ,EAAgB,IAAI,IACpB,EAAM,IAAI,EAAY,CAAa,EAEpC,IAAM,EAAO,EAAc,IAAI,CAAK,EAC9B,EAAW,EAAO,EAAK,GAAK,EAClC,GAAI,IAAY,EAAU,EAAc,OAAO,CAAK,EAC/C,OAAc,IAAI,EAAO,CAAC,EAAS,CAAQ,CAAC,EAGlD,SAAS,CAAS,CAAC,EAAc,CAChC,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACzC,EAAmB,EAAY,EAAO,EAAS,CAAO,EAKzD,SAAS,CAAU,CAAC,EAAe,EAAe,EAAU,GAAO,CAClE,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACzC,EACC,EACA,EACA,EACA,EAAU,EAAU,EACpB,EAAU,EAAU,CACrB,EAKH,SAAS,CAAkB,CAAC,EAAc,EAAQ,GAAgB,CACjE,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EAAe,CACxD,IAAM,EAAc,EAAmB,GACvC,GAAI,IAAe,EAClB,GAAI,EACH,WAAW,IAAM,CAChB,MAAU,MACT,gCAAgC,qCAA8C,eAAqB,GACpG,GACE,CAAC,EACA,WAAO,GAIf,QAAY,EAAY,KAAkB,EACzC,QAAY,GAAQ,EAAS,MAAa,EACxC,EAAmB,GAAS,EAG/B,MAAO,GAGR,IAAM,EAAmC,CAAC,EAWnC,SAAS,CAAe,CAAC,EAAgC,CAC/D,IAAM,EAAQ,EAAY,CAAW,EAGrC,OAFA,EAAmB,KAAK,CAAK,EAC7B,EAAU,CAAK,EACR,EAkBD,SAAS,CAAU,CACzB,EACA,EAAgC,CAAC,EAChC,CACD,IAAM,EAAc,IAAI,IACxB,QAAW,KAAc,EACxB,EAAW,EAAa,EAAY,EAAI,EACzC,EAAmB,EAAa,EAAI,EAEpC,QAAW,KAAc,EAAiB,CACzC,IAAM,EAAM,EAAmB,QAAQ,CAAU,EACjD,GAAI,GAAO,EAAG,EAAmB,OAAO,EAAK,CAAC,EAE/C,GAAI,EAAW,EAAW,EAAa,EAAY,CAAS,CAAC,EAE7D,QAAS,EAAM,EAAG,EAAM,EAAmB,OAAQ,IAClD,GAAI,EAAmB,EAAmB,EAAI,EAC7C,EAAW,EAAa,EAAmB,EAAI,EAE/C,OAAmB,OAAO,EAAK,CAAC,EAChC,IAIF,EAAU,CAAW",
8
+ "debugId": "4067069B259E531864756E2164756E21",
9
9
  "names": []
10
10
  }
package/dist-min/route.js CHANGED
@@ -1,4 +1,4 @@
1
- import{clean as B,getParentElement as F,$ as _,proxy as Q,runQueue as f,unproxy as N,copy as O,merge as U,clone as X,leakScope as $}from"./aberdeen.js";var J=()=>{};function d(z){if(z===!0)J=console.log.bind(console,"aberdeen router");else if(z===!1)J=()=>{};else J=z}function j(){return W({path:location.pathname,hash:location.hash,search:Object.fromEntries(new URLSearchParams(location.search)),state:history.state?.state||{}},"load",(history.state?.stack?.length||0)+1)}function V(z,A,D){if(z===A)return!0;if(typeof z!=="object"||!z||typeof A!=="object"||!A)return!1;if(z.constructor!==A.constructor)return!1;if(A instanceof Array){if(z.length!==A.length)return!1;for(let G=0;G<A.length;G++)if(!V(z[G],A[G],D))return!1}else{for(let G of Object.keys(A))if(!V(z[G],A[G],D))return!1;if(!D){for(let G of Object.keys(z))if(!A.hasOwnProperty(G))return!1}}return!0}function E(z){let A=new URLSearchParams(z.search).toString();return(A?`${z.path}?${A}`:z.path)+z.hash}function W(z,A,D){let G=z.path||(z.p||[]).join("/")||"/";if(G=(""+G).replace(/\/+$/,""),!G.startsWith("/"))G=`/${G}`;return{path:G,hash:z.hash&&z.hash!=="#"?z.hash.startsWith("#")?z.hash:"#"+z.hash:"",p:G.length>1?G.slice(1).replace(/\/+$/,"").split("/"):[],nav:A,search:typeof z.search==="object"&&z.search?X(z.search):{},state:typeof z.state==="object"&&z.state?X(z.state):{},depth:D}}function Y(z){if(typeof z==="string")z={path:z};else if(z instanceof Array)z={p:z};if(z.p)z.p=z.p.map(String);if(z.search)for(let A of Object.keys(z.search))z.search[A]=String(z.search[A]);return z}function I(z){L=(history.state?.stack||[]).concat(JSON.stringify(N(H)));let D=W(Y(z),"go",L.length+1);O(H,D),J("go",D),history.pushState({state:D.state,stack:L},"",E(D)),f()}function C(z){let A=X(N(H));U(A,Y(z)),I(A)}function P(z={}){let A=Y(z),D=history.state?.stack||[];for(let K=D.length-1;K>=0;K--){let M=JSON.parse(D[K]);if(V(M,A,!0)){let Z=K-D.length;J("back",Z,M),history.go(Z);return}}let G=W(A,"back",D.length+1);J("back not found, replacing",A),O(H,G)}function S(z=1){let A=N(H).p,D=history.state?.stack||[];for(let K=D.length-1;K>=0;K--){let M=JSON.parse(D[K]);if(M.p.length<A.length&&V(M.p,A.slice(0,M.p.length),!1)){J(`up to ${K+1} / ${D.length}`,M),history.go(K-D.length);return}}let G=W({p:A.slice(0,A.length-z)},"back",D.length+1);J("up not found, replacing",G),O(H,G)}function x(z="main"){let A=F();A.addEventListener("scroll",G),B(()=>A.removeEventListener("scroll",G));let D=N(H).state.scroll?.[z];if(D)J("restoring scroll",z,D),Object.assign(A,D);function G(){(H.state.scroll||={})[z]={scrollTop:A.scrollTop,scrollLeft:A.scrollLeft}}}var L,H=Q({});function T(){L=history.state?.stack||[];let z=j();J("initial",z),O(N(H),z)}T();window.addEventListener("popstate",function(z){let A=j(),D=history.state?.stack||[];if(D.length!==L.length){let G=Math.min(L.length,D.length)-1;if(G<0||D[G]===L[G])A.nav=D.length<L.length?"back":"forward"}L=D,J("popstate",A),O(H,A),f()});$(()=>{_(()=>{H.path="/"+Array.from(H.p).join("/")}),_(()=>{let z=history.state?.stack||[],A=W(H,N(H).nav,z.length+1);O(H,A);let D={state:A.state,stack:z},G=E(A);if(G!==location.pathname+location.search+location.hash||!V(history.state,D,!1))J("replaceState",A,D,G),history.replaceState(D,"",G)})});export{S as up,d as setLog,T as reset,C as push,x as persistScroll,I as go,H as current,P as back};
1
+ import{clean as B,getParentElement as F,$ as _,proxy as Q,runQueue as f,unproxy as N,copy as O,merge as U,clone as X,leakScope as $}from"./aberdeen.js";var J=()=>{};function d(z){if(z===!0)J=console.log.bind(console,"aberdeen router");else if(z===!1)J=()=>{};else J=z}function j(){return W({path:location.pathname,hash:location.hash,search:Object.fromEntries(new URLSearchParams(location.search)),state:history.state?.state||{}},"load",(history.state?.stack?.length||0)+1)}function V(z,A,G){if(z===A)return!0;if(typeof z!=="object"||!z||typeof A!=="object"||!A)return!1;if(z.constructor!==A.constructor)return!1;if(A instanceof Array){if(z.length!==A.length)return!1;for(let D=0;D<A.length;D++)if(!V(z[D],A[D],G))return!1}else{for(let D of Object.keys(A))if(!V(z[D],A[D],G))return!1;if(!G){for(let D of Object.keys(z))if(!A.hasOwnProperty(D))return!1}}return!0}function E(z){let A=new URLSearchParams(z.search).toString();return(A?`${z.path}?${A}`:z.path)+z.hash}function W(z,A,G){let D=z.path||(z.p||[]).join("/")||"/";if(D=(""+D).replace(/\/+$/,""),!D.startsWith("/"))D=`/${D}`;return{path:D,hash:z.hash&&z.hash!=="#"?z.hash.startsWith("#")?z.hash:"#"+z.hash:"",p:D.length>1?D.slice(1).replace(/\/+$/,"").split("/"):[],nav:A,search:typeof z.search==="object"&&z.search?X(z.search):{},state:typeof z.state==="object"&&z.state?X(z.state):{},depth:G}}function Y(z){if(typeof z==="string")z={path:z};else if(z instanceof Array)z={p:z};if(z.p)z.p=z.p.map(String);if(z.search)for(let A of Object.keys(z.search))z.search[A]=String(z.search[A]);return z}function I(z,A="go"){L=(history.state?.stack||[]).concat(JSON.stringify(N(H)));let D=W(Y(z),A,L.length+1);O(H,D),J(A,D),history.pushState({state:D.state,stack:L},"",E(D)),f()}function C(z){let A=X(N(H));U(A,Y(z)),I(A)}function P(z={}){let A=Y(z),G=history.state?.stack||[];for(let K=G.length-1;K>=0;K--){let M=JSON.parse(G[K]);if(V(M,A,!0)){let Z=K-G.length;J("back",Z,M),history.go(Z);return}}let D=W(A,"back",G.length+1);J("back not found, replacing",A),O(H,D)}function S(z=1){let A=N(H).p,G=history.state?.stack||[];for(let K=G.length-1;K>=0;K--){let M=JSON.parse(G[K]);if(M.p.length<A.length&&V(M.p,A.slice(0,M.p.length),!1)){J(`up to ${K+1} / ${G.length}`,M),history.go(K-G.length);return}}let D=W({p:A.slice(0,A.length-z)},"back",G.length+1);J("up not found, replacing",D),O(H,D)}function x(z="main"){let A=F();A.addEventListener("scroll",D),B(()=>A.removeEventListener("scroll",D));let G=N(H).state.scroll?.[z];if(G)J("restoring scroll",z,G),Object.assign(A,G);function D(){(H.state.scroll||={})[z]={scrollTop:A.scrollTop,scrollLeft:A.scrollLeft}}}var L,H=Q({});function T(){L=history.state?.stack||[];let z=j();J("initial",z),O(N(H),z)}T();window.addEventListener("popstate",function(z){let A=j(),G=history.state?.stack||[];if(G.length!==L.length){let D=Math.min(L.length,G.length)-1;if(D<0||G[D]===L[D])A.nav=G.length<L.length?"back":"forward"}L=G,J("popstate",A),O(H,A),f()});$(()=>{_(()=>{H.path="/"+Array.from(H.p).join("/")}),_(()=>{let z=history.state?.stack||[],A=W(H,N(H).nav,z.length+1);O(H,A);let G={state:A.state,stack:z},D=E(A);if(D!==location.pathname+location.search+location.hash||!V(history.state,G,!1))J("replaceState",A,G,D),history.replaceState(G,"",D)})});export{S as up,d as setLog,T as reset,C as push,x as persistScroll,I as go,H as current,P as back};
2
2
 
3
- //# debugId=B6ADEE6973F8780664756E2164756E21
3
+ //# debugId=4AFCE83BFB8282B064756E2164756E21
4
4
  //# sourceMappingURL=route.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/route.ts"],
4
4
  "sourcesContent": [
5
- "import {clean, getParentElement, $, proxy, runQueue, unproxy, copy, merge, clone, leakScope} from \"./aberdeen.js\";\n\ntype NavType = \"load\" | \"back\" | \"forward\" | \"go\";\n\n/**\n* The class for the global `route` object.\n*/\nexport interface Route {\n\t/** The current path of the URL as a string. For instance `\"/\"` or `\"/users/123/feed\"`. Paths are normalized to always start with a `/` and never end with a `/` (unless it's the root path). */\n\tpath: string;\n\t/** An convenience array containing path segments, mapping to `path`. For instance `[]` (for `\"/\"`) or `['users', '123', 'feed']` (for `\"/users/123/feed\"`). */\n\tp: string[];\n\t/** The hash fragment including the leading `#`, or an empty string. For instance `\"#my_section\"` or `\"\"`. */\n\thash: string;\n\t/** The query string interpreted as search parameters. So `\"a=x&b=y\"` becomes `{a: \"x\", b: \"y\"}`. */\n\tsearch: Record<string, string>;\n\t/** An object to be used for any additional data you want to associate with the current page. Data should be JSON-compatible. */\n\tstate: Record<string, any>;\n\t/** The navigation depth of the current session. Starts at 1. Writing to this property has no effect. */\n\tdepth: number;\n\t/** The navigation action that got us to this page. Writing to this property has no effect.\n\t- `\"load\"`: An initial page load.\n\t- `\"back\"` or `\"forward\"`: When we navigated backwards or forwards in the stack.\n\t- `\"go\"`: When we added a new page on top of the stack.\n\tMostly useful for page transition animations. Writing to this property has no effect.\n\t*/\n\tnav: NavType;\n}\n\nlet log: (...args: any) => void = () => {};\n\n/**\n * Configure logging on route changes.\n * @param value `true` to enable logging to console, `false` to disable logging, or a custom logging function. Defaults to `false`.\n */\nexport function setLog(value: boolean | ((...args: any[]) => void)) {\n\tif (value === true) {\n\t\tlog = console.log.bind(console, 'aberdeen router');\n\t} else if (value === false) {\n\t\tlog = () => {};\n\t} else {\n\t\tlog = value;\n\t}\n}\n\nfunction getRouteFromBrowser(): Route {\n\treturn toCanonRoute({\n\t\tpath: location.pathname,\n\t\thash: location.hash,\n\t\tsearch: Object.fromEntries(new URLSearchParams(location.search)),\n\t\tstate: history.state?.state || {},\n\t}, \"load\", (history.state?.stack?.length || 0) + 1);\n}\n\n/**\n* Deep compare `a` and `b`. If `partial` is true, objects contained in `b` may be a subset\n* of their counterparts in `a` and still be considered equal.\n*/\nfunction equal(a: any, b: any, partial: boolean): boolean {\n\tif (a===b) return true;\n\tif (typeof a !== \"object\" || !a || typeof b !== \"object\" || !b) return false; // otherwise they would have been equal\n\tif (a.constructor !== b.constructor) return false;\n\tif (b instanceof Array) {\n\t\tif (a.length !== b.length) return false;\n\t\tfor(let i = 0; i < b.length; i++) {\n\t\t\tif (!equal(a[i], b[i], partial)) return false;\n\t\t}\n\t} else {\n\t\tfor(const k of Object.keys(b)) {\n\t\t\tif (!equal(a[k], b[k], partial)) return false;\n\t\t}\n\t\tif (!partial) {\n\t\t\tfor(const k of Object.keys(a)) {\n\t\t\t\tif (!b.hasOwnProperty(k)) return false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction getUrl(target: Route) {\n\tconst search = new URLSearchParams(target.search).toString();\n\treturn (search ? `${target.path}?${search}` : target.path) + target.hash;\n}\n\nfunction toCanonRoute(target: Partial<Route>, nav: NavType, depth: number): Route {\n\tlet path = target.path || (target.p || []).join(\"/\") || \"/\";\n\tpath = (\"\"+path).replace(/\\/+$/, \"\");\n\tif (!path.startsWith(\"/\")) path = `/${path}`;\n\t\n\treturn {\n\t\tpath,\n\t\thash: target.hash && target.hash !==\"#\" ? (target.hash.startsWith(\"#\") ? target.hash : \"#\" + target.hash) : \"\",\n\t\tp: path.length > 1 ? path.slice(1).replace(/\\/+$/, \"\").split(\"/\") : [],\n\t\tnav,\n\t\tsearch: typeof target.search === 'object' && target.search ? clone(target.search) : {},\n\t\tstate: typeof target.state === 'object' && target.state ? clone(target.state) : {},\n\t\tdepth,\n\t};\n}\n\n\ntype RouteTarget = string | (string|number)[] | Partial<Omit<Omit<Route,\"p\">,\"search\"> & {\n\t/** An convenience array containing path segments, mapping to `path`. For instance `[]` (for `\"/\"`) or `['users', 123, 'feed']` (for `\"/users/123/feed\"`). Values may be integers but will be converted to strings.*/\n\tp: (string|number)[],\n\t/** The query string interpreted as search parameters. So `\"a=x&b=y\"` becomes `{a: \"x\", b: \"y\", c: 42}`. Values may be integers but will be converted to strings. */\n\tsearch: Record<string,string|number>,\n}>;\n\nfunction targetToPartial(target: RouteTarget) {\n\t// Convert shortcut values to objects\n\tif (typeof target === 'string') {\n\t\ttarget = {path: target};\n\t} else if (target instanceof Array) {\n\t\ttarget = {p: target};\n\t}\n\t// Convert numbers in p and search to strings\n\tif (target.p) {\n\t\ttarget.p = target.p.map(String);\n\t}\n\tif (target.search) {\n\t\tfor(const key of Object.keys(target.search)) {\n\t\t\ttarget.search[key] = String(target.search[key]);\n\t\t}\n\t}\n\treturn target as Partial<Route>;\n}\n\n\n/**\n* Navigate to a new URL by pushing a new history entry.\n* \n* Note that this happens synchronously, immediately updating `route` and processing any reactive updates based on that.\n* \n* @param target A subset of the {@link Route} properties to navigate to. If neither `p` nor `path` is given, the current path is used. For other properties, an empty/default value is assumed if not given. For convenience:\n* - You may pass a string instead of an object, which is interpreted as the `path`.\n* - You may pass an array instead of an object, which is interpreted as the `p` array.\n* - If you pass `p`, it may contain numbers, which will be converted to strings.\n* - If you pass `search`, its values may be numbers, which will be converted to strings.\n* \n* Examples:\n* ```js\n* // Navigate to /users/123\n* route.go(\"/users/123\");\n* \n* // Navigate to /users/123?tab=feed#top\n* route.go({p: [\"users\", 123], search: {tab: \"feed\"}, hash: \"top\"});\n* ```\n*/\nexport function go(target: RouteTarget): void {\n\tconst stack: string[] = history.state?.stack || [];\n\n\tprevStack = stack.concat(JSON.stringify(unproxy(current)));\n\t\n\tconst newRoute: Route = toCanonRoute(targetToPartial(target), \"go\", prevStack.length + 1);\n\tcopy(current, newRoute);\n\t\n\tlog('go', newRoute);\n\thistory.pushState({state: newRoute.state, stack: prevStack}, \"\", getUrl(newRoute));\n\t\n\trunQueue();\n}\n\n/**\n * Modify the current route by merging `target` into it (using {@link merge}), pushing a new history entry.\n * \n * This is useful for things like opening modals or side panels, where you want a browser back action to return to the previous state.\n * \n * @param target Same as for {@link go}, but merged into the current route instead deleting all state.\n */\nexport function push(target: RouteTarget): void {\n\tlet copy = clone(unproxy(current));\n\tmerge(copy, targetToPartial(target));\n\tgo(copy);\n}\n\n/**\n * Try to go back in history to the first entry that matches the given target. If none is found, the given state will replace the current page. This is useful for \"cancel\" or \"close\" actions that should return to the previous page if possible, but create a new page if not (for instance when arriving at the current page through a direct link).\n * \n * Consider using {@link up} to go up in the path hierarchy.\n * \n * @param target The target route to go back to. May be a subset of {@link Route}, or a string (for `path`), or an array of strings (for `p`).\n */\nexport function back(target: RouteTarget = {}): void {\n\tconst partial = targetToPartial(target);\n\tconst stack: string[] = history.state?.stack || [];\n\tfor(let i = stack.length - 1; i >= 0; i--) {\n\t\tconst histRoute: Route = JSON.parse(stack[i]);\n\t\tif (equal(histRoute, partial, true)) {\n\t\t\tconst pages = i - stack.length;\n\t\t\tlog(`back`, pages, histRoute);\n\t\t\thistory.go(pages);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst newRoute = toCanonRoute(partial, \"back\", stack.length + 1);\n\tlog(`back not found, replacing`, partial);\n\tcopy(current, newRoute);\n}\n\n/**\n* Navigate up in the path hierarchy, by going back to the first history entry\n* that has a shorter path than the current one. If there's none, we just shorten\n* the current path.\n* \n* Note that going back in browser history happens asynchronously, so `route` will not be updated immediately.\n*/\nexport function up(stripCount: number = 1): void {\n\tconst currentP = unproxy(current).p;\n\tconst stack: string[] = history.state?.stack || [];\n\tfor(let i = stack.length - 1; i >= 0; i--) {\n\t\tconst histRoute: Route = JSON.parse(stack[i]);\n\t\tif (histRoute.p.length < currentP.length && equal(histRoute.p, currentP.slice(0, histRoute.p.length), false)) {\n\t\t\t// This route is shorter and matches the start of the current path\n\t\t\tlog(`up to ${i+1} / ${stack.length}`, histRoute);\n\t\t\thistory.go(i - stack.length);\n\t\t\treturn;\n\t\t}\n\t}\n\t// Replace current route with /\n\tconst newRoute = toCanonRoute({p: currentP.slice(0, currentP.length - stripCount)}, \"back\", stack.length + 1);\n\tlog(`up not found, replacing`, newRoute);\n\tcopy(current, newRoute);\n}\n\n/**\n* Restore and store the vertical and horizontal scroll position for\n* the parent element to the page state.\n*\n* @param {string} name - A unique (within this page) name for this\n* scrollable element. Defaults to 'main'.\n*\n* The scroll position will be persisted in `route.aux.scroll.<name>`.\n*/\nexport function persistScroll(name = \"main\") {\n\tconst el = getParentElement();\n\tel.addEventListener(\"scroll\", onScroll);\n\tclean(() => el.removeEventListener(\"scroll\", onScroll));\n\t\n\tconst restore = unproxy(current).state.scroll?.[name];\n\tif (restore) {\n\t\tlog(\"restoring scroll\", name, restore);\n\t\tObject.assign(el, restore);\n\t}\n\t\n\tfunction onScroll() {\n\t\t(current.state.scroll ||= {})[name] = {\n\t\t\tscrollTop: el.scrollTop,\n\t\t\tscrollLeft: el.scrollLeft,\n\t\t};\n\t}\n}\n\nlet prevStack: string[];\n\n/**\n* The global {@link Route} object reflecting the current URL and browser history state. Changes you make to this affect the current browser history item (modifying the URL if needed).\n*/\nexport const current: Route = proxy({}) as Route;\n\n/**\n * Reset the router to its initial state, based on the current browser state. Intended for testing purposes only.\n * @internal\n * */\nexport function reset() {\n\tprevStack = history.state?.stack || [];\n\tconst initRoute = getRouteFromBrowser();\n\tlog('initial', initRoute);\n\tcopy(unproxy(current), initRoute);\n}\nreset();\n\n// Handle browser history back and forward\nwindow.addEventListener(\"popstate\", function(event: PopStateEvent) {\n\tconst newRoute = getRouteFromBrowser();\n\t\n\t// If the stack length changes, and at least the top-most shared entry is the same,\n\t// we'll interpret this as a \"back\" or \"forward\" navigation.\n\tconst stack: string[] = history.state?.stack || [];\n\tif (stack.length !== prevStack.length) {\n\t\tconst maxIndex = Math.min(prevStack.length, stack.length) - 1;\n\t\tif (maxIndex < 0 || stack[maxIndex] === prevStack[maxIndex]) {\n\t\t\tnewRoute.nav = stack.length < prevStack.length ? \"back\" : \"forward\";\n\t\t}\n\t}\n\t// else nav will be \"load\"\n\t\n\tprevStack = stack;\n\tlog('popstate', newRoute);\n\tcopy(current, newRoute);\n\t\n\trunQueue();\n});\n\n// Make sure these observers are never cleaned up, not even by `unmountAll`.\nleakScope(() => {\n\t// Sync `p` to `path`. We need to do this in a separate, higher-priority observer,\n\t// so that setting `route.p` will not be immediately overruled by the pre-existing `route.path`.\n\t$(() => {\n\t\tcurrent.path = \"/\" + Array.from(current.p).join(\"/\");\n\t});\n\n\t// Do a replaceState based on changes to proxy\n\t$(() => {\n\n\t\t// First normalize `route`\n\t\tconst stack = history.state?.stack || [];\n\t\tconst newRoute = toCanonRoute(current, unproxy(current).nav, stack.length + 1);\n\t\tcopy(current, newRoute);\n\t\t\n\t\t// Then replace the current browser state if something actually changed\n\t\tconst state = {state: newRoute.state, stack};\n\t\tconst url = getUrl(newRoute);\n\t\tif (url !== location.pathname + location.search + location.hash || !equal(history.state, state, false)) {\n\t\t\tlog('replaceState', newRoute, state, url);\n\t\t\thistory.replaceState(state, \"\", url);\n\t\t}\n\t});\n});\n"
5
+ "import {clean, getParentElement, $, proxy, runQueue, unproxy, copy, merge, clone, leakScope} from \"./aberdeen.js\";\n\ntype NavType = \"load\" | \"back\" | \"forward\" | \"go\" | \"push\";\n\n/**\n* The class for the global `route` object.\n*/\nexport interface Route {\n\t/** The current path of the URL as a string. For instance `\"/\"` or `\"/users/123/feed\"`. Paths are normalized to always start with a `/` and never end with a `/` (unless it's the root path). */\n\tpath: string;\n\t/** An convenience array containing path segments, mapping to `path`. For instance `[]` (for `\"/\"`) or `['users', '123', 'feed']` (for `\"/users/123/feed\"`). */\n\tp: string[];\n\t/** The hash fragment including the leading `#`, or an empty string. For instance `\"#my_section\"` or `\"\"`. */\n\thash: string;\n\t/** The query string interpreted as search parameters. So `\"a=x&b=y\"` becomes `{a: \"x\", b: \"y\"}`. */\n\tsearch: Record<string, string>;\n\t/** An object to be used for any additional data you want to associate with the current page. Data should be JSON-compatible. */\n\tstate: Record<string, any>;\n\t/** The navigation depth of the current session. Starts at 1. Writing to this property has no effect. */\n\tdepth: number;\n\t/** The navigation action that got us to this page. Writing to this property has no effect.\n\t- `\"load\"`: An initial page load.\n\t- `\"back\"` or `\"forward\"`: When we navigated backwards or forwards in the stack.\n\t- `\"go\"`: When we added a new page on top of the stack.\n\t- `\"push\"`: When we added a new page on top of the stack, merging with the current page.\n\tMostly useful for page transition animations. Writing to this property has no effect.\n\t*/\n\tnav: NavType;\n}\n\nlet log: (...args: any) => void = () => {};\n\n/**\n * Configure logging on route changes.\n * @param value `true` to enable logging to console, `false` to disable logging, or a custom logging function. Defaults to `false`.\n */\nexport function setLog(value: boolean | ((...args: any[]) => void)) {\n\tif (value === true) {\n\t\tlog = console.log.bind(console, 'aberdeen router');\n\t} else if (value === false) {\n\t\tlog = () => {};\n\t} else {\n\t\tlog = value;\n\t}\n}\n\nfunction getRouteFromBrowser(): Route {\n\treturn toCanonRoute({\n\t\tpath: location.pathname,\n\t\thash: location.hash,\n\t\tsearch: Object.fromEntries(new URLSearchParams(location.search)),\n\t\tstate: history.state?.state || {},\n\t}, \"load\", (history.state?.stack?.length || 0) + 1);\n}\n\n/**\n* Deep compare `a` and `b`. If `partial` is true, objects contained in `b` may be a subset\n* of their counterparts in `a` and still be considered equal.\n*/\nfunction equal(a: any, b: any, partial: boolean): boolean {\n\tif (a===b) return true;\n\tif (typeof a !== \"object\" || !a || typeof b !== \"object\" || !b) return false; // otherwise they would have been equal\n\tif (a.constructor !== b.constructor) return false;\n\tif (b instanceof Array) {\n\t\tif (a.length !== b.length) return false;\n\t\tfor(let i = 0; i < b.length; i++) {\n\t\t\tif (!equal(a[i], b[i], partial)) return false;\n\t\t}\n\t} else {\n\t\tfor(const k of Object.keys(b)) {\n\t\t\tif (!equal(a[k], b[k], partial)) return false;\n\t\t}\n\t\tif (!partial) {\n\t\t\tfor(const k of Object.keys(a)) {\n\t\t\t\tif (!b.hasOwnProperty(k)) return false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction getUrl(target: Route) {\n\tconst search = new URLSearchParams(target.search).toString();\n\treturn (search ? `${target.path}?${search}` : target.path) + target.hash;\n}\n\nfunction toCanonRoute(target: Partial<Route>, nav: NavType, depth: number): Route {\n\tlet path = target.path || (target.p || []).join(\"/\") || \"/\";\n\tpath = (\"\"+path).replace(/\\/+$/, \"\");\n\tif (!path.startsWith(\"/\")) path = `/${path}`;\n\t\n\treturn {\n\t\tpath,\n\t\thash: target.hash && target.hash !==\"#\" ? (target.hash.startsWith(\"#\") ? target.hash : \"#\" + target.hash) : \"\",\n\t\tp: path.length > 1 ? path.slice(1).replace(/\\/+$/, \"\").split(\"/\") : [],\n\t\tnav,\n\t\tsearch: typeof target.search === 'object' && target.search ? clone(target.search) : {},\n\t\tstate: typeof target.state === 'object' && target.state ? clone(target.state) : {},\n\t\tdepth,\n\t};\n}\n\n\ntype RouteTarget = string | (string|number)[] | Partial<Omit<Omit<Route,\"p\">,\"search\"> & {\n\t/** An convenience array containing path segments, mapping to `path`. For instance `[]` (for `\"/\"`) or `['users', 123, 'feed']` (for `\"/users/123/feed\"`). Values may be integers but will be converted to strings.*/\n\tp: (string|number)[],\n\t/** The query string interpreted as search parameters. So `\"a=x&b=y\"` becomes `{a: \"x\", b: \"y\", c: 42}`. Values may be integers but will be converted to strings. */\n\tsearch: Record<string,string|number>,\n}>;\n\nfunction targetToPartial(target: RouteTarget) {\n\t// Convert shortcut values to objects\n\tif (typeof target === 'string') {\n\t\ttarget = {path: target};\n\t} else if (target instanceof Array) {\n\t\ttarget = {p: target};\n\t}\n\t// Convert numbers in p and search to strings\n\tif (target.p) {\n\t\ttarget.p = target.p.map(String);\n\t}\n\tif (target.search) {\n\t\tfor(const key of Object.keys(target.search)) {\n\t\t\ttarget.search[key] = String(target.search[key]);\n\t\t}\n\t}\n\treturn target as Partial<Route>;\n}\n\n\n/**\n* Navigate to a new URL by pushing a new history entry.\n* \n* Note that this happens synchronously, immediately updating `route` and processing any reactive updates based on that.\n* \n* @param target A subset of the {@link Route} properties to navigate to. If neither `p` nor `path` is given, the current path is used. For other properties, an empty/default value is assumed if not given. For convenience:\n* - You may pass a string instead of an object, which is interpreted as the `path`.\n* - You may pass an array instead of an object, which is interpreted as the `p` array.\n* - If you pass `p`, it may contain numbers, which will be converted to strings.\n* - If you pass `search`, its values may be numbers, which will be converted to strings.\n* \n* Examples:\n* ```js\n* // Navigate to /users/123\n* route.go(\"/users/123\");\n* \n* // Navigate to /users/123?tab=feed#top\n* route.go({p: [\"users\", 123], search: {tab: \"feed\"}, hash: \"top\"});\n* ```\n*/\nexport function go(target: RouteTarget, nav: NavType = \"go\"): void {\n\tconst stack: string[] = history.state?.stack || [];\n\n\tprevStack = stack.concat(JSON.stringify(unproxy(current)));\n\t\n\tconst newRoute: Route = toCanonRoute(targetToPartial(target), nav, prevStack.length + 1);\n\tcopy(current, newRoute);\n\t\n\tlog(nav, newRoute);\n\thistory.pushState({state: newRoute.state, stack: prevStack}, \"\", getUrl(newRoute));\n\t\n\trunQueue();\n}\n\n/**\n * Modify the current route by merging `target` into it (using {@link merge}), pushing a new history entry.\n * \n * This is useful for things like opening modals or side panels, where you want a browser back action to return to the previous state.\n * \n * @param target Same as for {@link go}, but merged into the current route instead deleting all state.\n */\nexport function push(target: RouteTarget): void {\n\tlet copy = clone(unproxy(current));\n\tmerge(copy, targetToPartial(target));\n\tgo(copy);\n}\n\n/**\n * Try to go back in history to the first entry that matches the given target. If none is found, the given state will replace the current page. This is useful for \"cancel\" or \"close\" actions that should return to the previous page if possible, but create a new page if not (for instance when arriving at the current page through a direct link).\n * \n * Consider using {@link up} to go up in the path hierarchy.\n * \n * @param target The target route to go back to. May be a subset of {@link Route}, or a string (for `path`), or an array of strings (for `p`).\n */\nexport function back(target: RouteTarget = {}): void {\n\tconst partial = targetToPartial(target);\n\tconst stack: string[] = history.state?.stack || [];\n\tfor(let i = stack.length - 1; i >= 0; i--) {\n\t\tconst histRoute: Route = JSON.parse(stack[i]);\n\t\tif (equal(histRoute, partial, true)) {\n\t\t\tconst pages = i - stack.length;\n\t\t\tlog(`back`, pages, histRoute);\n\t\t\thistory.go(pages);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst newRoute = toCanonRoute(partial, \"back\", stack.length + 1);\n\tlog(`back not found, replacing`, partial);\n\tcopy(current, newRoute);\n}\n\n/**\n* Navigate up in the path hierarchy, by going back to the first history entry\n* that has a shorter path than the current one. If there's none, we just shorten\n* the current path.\n* \n* Note that going back in browser history happens asynchronously, so `route` will not be updated immediately.\n*/\nexport function up(stripCount: number = 1): void {\n\tconst currentP = unproxy(current).p;\n\tconst stack: string[] = history.state?.stack || [];\n\tfor(let i = stack.length - 1; i >= 0; i--) {\n\t\tconst histRoute: Route = JSON.parse(stack[i]);\n\t\tif (histRoute.p.length < currentP.length && equal(histRoute.p, currentP.slice(0, histRoute.p.length), false)) {\n\t\t\t// This route is shorter and matches the start of the current path\n\t\t\tlog(`up to ${i+1} / ${stack.length}`, histRoute);\n\t\t\thistory.go(i - stack.length);\n\t\t\treturn;\n\t\t}\n\t}\n\t// Replace current route with /\n\tconst newRoute = toCanonRoute({p: currentP.slice(0, currentP.length - stripCount)}, \"back\", stack.length + 1);\n\tlog(`up not found, replacing`, newRoute);\n\tcopy(current, newRoute);\n}\n\n/**\n* Restore and store the vertical and horizontal scroll position for\n* the parent element to the page state.\n*\n* @param {string} name - A unique (within this page) name for this\n* scrollable element. Defaults to 'main'.\n*\n* The scroll position will be persisted in `route.aux.scroll.<name>`.\n*/\nexport function persistScroll(name = \"main\") {\n\tconst el = getParentElement();\n\tel.addEventListener(\"scroll\", onScroll);\n\tclean(() => el.removeEventListener(\"scroll\", onScroll));\n\t\n\tconst restore = unproxy(current).state.scroll?.[name];\n\tif (restore) {\n\t\tlog(\"restoring scroll\", name, restore);\n\t\tObject.assign(el, restore);\n\t}\n\t\n\tfunction onScroll() {\n\t\t(current.state.scroll ||= {})[name] = {\n\t\t\tscrollTop: el.scrollTop,\n\t\t\tscrollLeft: el.scrollLeft,\n\t\t};\n\t}\n}\n\nlet prevStack: string[];\n\n/**\n* The global {@link Route} object reflecting the current URL and browser history state. Changes you make to this affect the current browser history item (modifying the URL if needed).\n*/\nexport const current: Route = proxy({}) as Route;\n\n/**\n * Reset the router to its initial state, based on the current browser state. Intended for testing purposes only.\n * @internal\n * */\nexport function reset() {\n\tprevStack = history.state?.stack || [];\n\tconst initRoute = getRouteFromBrowser();\n\tlog('initial', initRoute);\n\tcopy(unproxy(current), initRoute);\n}\nreset();\n\n// Handle browser history back and forward\nwindow.addEventListener(\"popstate\", function(event: PopStateEvent) {\n\tconst newRoute = getRouteFromBrowser();\n\t\n\t// If the stack length changes, and at least the top-most shared entry is the same,\n\t// we'll interpret this as a \"back\" or \"forward\" navigation.\n\tconst stack: string[] = history.state?.stack || [];\n\tif (stack.length !== prevStack.length) {\n\t\tconst maxIndex = Math.min(prevStack.length, stack.length) - 1;\n\t\tif (maxIndex < 0 || stack[maxIndex] === prevStack[maxIndex]) {\n\t\t\tnewRoute.nav = stack.length < prevStack.length ? \"back\" : \"forward\";\n\t\t}\n\t}\n\t// else nav will be \"load\"\n\t\n\tprevStack = stack;\n\tlog('popstate', newRoute);\n\tcopy(current, newRoute);\n\t\n\trunQueue();\n});\n\n// Make sure these observers are never cleaned up, not even by `unmountAll`.\nleakScope(() => {\n\t// Sync `p` to `path`. We need to do this in a separate, higher-priority observer,\n\t// so that setting `route.p` will not be immediately overruled by the pre-existing `route.path`.\n\t$(() => {\n\t\tcurrent.path = \"/\" + Array.from(current.p).join(\"/\");\n\t});\n\n\t// Do a replaceState based on changes to proxy\n\t$(() => {\n\n\t\t// First normalize `route`\n\t\tconst stack = history.state?.stack || [];\n\t\tconst newRoute = toCanonRoute(current, unproxy(current).nav, stack.length + 1);\n\t\tcopy(current, newRoute);\n\t\t\n\t\t// Then replace the current browser state if something actually changed\n\t\tconst state = {state: newRoute.state, stack};\n\t\tconst url = getUrl(newRoute);\n\t\tif (url !== location.pathname + location.search + location.hash || !equal(history.state, state, false)) {\n\t\t\tlog('replaceState', newRoute, state, url);\n\t\t\thistory.replaceState(state, \"\", url);\n\t\t}\n\t});\n});\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "B6ADEE6973F8780664756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "4AFCE83BFB8282B064756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aberdeen",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "author": "Frank van Viegen",
5
5
  "main": "dist-min/aberdeen.js",
6
6
  "devDependencies": {
@@ -20,10 +20,10 @@
20
20
  "development": "./dist/route.js",
21
21
  "types": "./dist/route.d.ts"
22
22
  },
23
- "./transition": {
24
- "default": "./dist-min/transition.js",
25
- "development": "./dist/transition.js",
26
- "types": "./dist/transition.d.ts"
23
+ "./transitions": {
24
+ "default": "./dist-min/transitions.js",
25
+ "development": "./dist/transitions.js",
26
+ "types": "./dist/transitions.d.ts"
27
27
  },
28
28
  "./prediction": {
29
29
  "default": "./dist-min/prediction.js",
package/src/aberdeen.ts CHANGED
@@ -1056,7 +1056,7 @@ const objectHandler: ProxyHandler<any> = {
1056
1056
  return true;
1057
1057
  },
1058
1058
  deleteProperty(target: any, prop: any) {
1059
- const old = target.hasOwnProperty(prop) ? target[prop] : undefined;
1059
+ const old = target.hasOwnProperty(prop) ? target[prop] : EMPTY;
1060
1060
  delete target[prop];
1061
1061
  emit(target, prop, EMPTY, old);
1062
1062
  return true;
@@ -1289,6 +1289,13 @@ function optProxy(value: any): any {
1289
1289
  return proxied;
1290
1290
  }
1291
1291
 
1292
+ interface PromiseProxy<T> {
1293
+ busy: boolean;
1294
+ error?: any;
1295
+ value?: T;
1296
+ }
1297
+
1298
+ export function proxy<T extends any>(target: Promise<T>): PromiseProxy<T>;
1292
1299
  export function proxy<T extends any>(target: Array<T>): Array<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T >;
1293
1300
  export function proxy<T extends object>(target: T): T;
1294
1301
  export function proxy<T extends any>(target: T): ValueRef<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T>;
@@ -1304,6 +1311,10 @@ export function proxy<T extends any>(target: T): ValueRef<T extends number ? num
1304
1311
  * property access and mutations, but otherwise works like the underlying data.
1305
1312
  * - Primitives (string, number, boolean, null, undefined) are wrapped in an object
1306
1313
  * `{ value: T }` which is then proxied. Access the primitive via the `.value` property.
1314
+ * - Promises are represented by proxied objects `{ busy: boolean, value?: T, error?: any }`.
1315
+ * Initially, `busy` is `true`. When the promise resolves, `value` is set and `busy`
1316
+ * is set to `false`. If the promise is rejected, `error` is set and `busy` is also
1317
+ * set to `false`.
1307
1318
  *
1308
1319
  * Use {@link unproxy} to get the original underlying data back.
1309
1320
  *
@@ -1347,6 +1358,21 @@ export function proxy<T extends any>(target: T): ValueRef<T extends number ? num
1347
1358
  * ```
1348
1359
  */
1349
1360
  export function proxy(target: TargetType): TargetType {
1361
+ if (target instanceof Promise) {
1362
+ const result: PromiseProxy<any> = optProxy({
1363
+ busy: true,
1364
+ });
1365
+ target
1366
+ .then((value) => {
1367
+ result.value = value;
1368
+ result.busy = false;
1369
+ })
1370
+ .catch((err) => {
1371
+ result.error = err;
1372
+ result.busy = false;
1373
+ });
1374
+ return result;
1375
+ }
1350
1376
  return optProxy(
1351
1377
  typeof target === "object" && target !== null ? target : { value: target },
1352
1378
  );
@@ -1439,14 +1465,14 @@ export function copy<T extends object>(dst: T, src: T): boolean;
1439
1465
  export function copy<T extends object>(dst: T, dstKey: keyof T, src: T[typeof dstKey]): boolean;
1440
1466
  export function copy(a: any, b: any, c?: any): boolean {
1441
1467
  if (arguments.length > 2) return copySet(a, b, c, 0);
1442
- return copyRecursive(a, b, 0);
1468
+ return copyImpl(a, b, 0);
1443
1469
  }
1444
1470
 
1445
1471
  function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
1446
1472
  let dstVal = peek(dst, dstKey);
1447
1473
  if (src === dstVal) return false;
1448
1474
  if (typeof dstVal === "object" && dstVal && typeof src === "object" && src && dstVal.constructor === src.constructor) {
1449
- return copyRecursive(dstVal, src, flags);
1475
+ return copyImpl(dstVal, src, flags);
1450
1476
  }
1451
1477
  src = clone(src);
1452
1478
  if (dst instanceof Map) dst.set(dstKey, src);
@@ -1483,26 +1509,31 @@ export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
1483
1509
  export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
1484
1510
  export function merge(a: any, b: any, c?: any) {
1485
1511
  if (arguments.length > 2) return copySet(a, b, c, MERGE);
1486
- return copyRecursive(a, b, MERGE);
1512
+ return copyImpl(a, b, MERGE);
1487
1513
  }
1488
1514
 
1489
- // The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
1490
- function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
1515
+ function copyImpl(dst: any, src: any, flags: number): boolean {
1491
1516
  // We never want to subscribe to reads we do to the target (to find changes). So we'll
1492
1517
  // take the unproxied version and `emit` updates ourselve.
1493
- let unproxied = (dst as any)[TARGET_SYMBOL] as T;
1518
+ let unproxied = (dst as any)[TARGET_SYMBOL];
1494
1519
  if (unproxied) {
1495
1520
  dst = unproxied;
1496
1521
  flags |= COPY_EMIT;
1497
1522
  }
1498
1523
  // For performance, we'll work on the unproxied `src` and manually subscribe to changes.
1499
- unproxied = (src as any)[TARGET_SYMBOL] as T;
1524
+ unproxied = (src as any)[TARGET_SYMBOL];
1500
1525
  if (unproxied) {
1501
1526
  src = unproxied;
1502
1527
  // If we're not in peek mode, we'll manually subscribe to all source reads.
1503
1528
  if (currentScope !== ROOT_SCOPE && !peeking) flags |= COPY_SUBSCRIBE;
1504
1529
  }
1505
1530
 
1531
+ return copyRecursive(dst, src, flags);
1532
+ }
1533
+
1534
+ // The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
1535
+ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
1536
+
1506
1537
  if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
1507
1538
  let changed = false;
1508
1539
 
@@ -1642,7 +1673,7 @@ export function clone<T extends object>(src: T): T {
1642
1673
  const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
1643
1674
  // Copy all properties to it. This doesn't need to emit anything, and because
1644
1675
  // the destination is an empty object, we can just MERGE, which is a bit faster.
1645
- copyRecursive(copied, src, MERGE);
1676
+ copyImpl(copied, src, MERGE);
1646
1677
  return copied;
1647
1678
  }
1648
1679
 
@@ -1787,7 +1818,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1787
1818
  },
1788
1819
  text: (value: any) => {
1789
1820
  addNode(document.createTextNode(value));
1790
- }
1821
+ },
1791
1822
  };
1792
1823
 
1793
1824
  /**
@@ -1798,11 +1829,13 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1798
1829
  * @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
1799
1830
  *
1800
1831
  * - `string`: Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
1801
- * The format of a string is: **tag**? (`.` **class**)* (':' **text**)?
1802
- * meaning it consists of...
1803
- * - An optional HTML **tag**, something like `h1`. If present, a DOM element of that tag is created, and that element will be the *current* element for the rest of this `$` function execution.
1804
- * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element.
1805
- * - Optional content **text** prefixed by a `:` character, ranging til the end of the string. This will be added as a TextNode to the *current* element.
1832
+ * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* (':' **text** | **key**=)?
1833
+ * So there can be:
1834
+ * - Any number of **tag** element, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
1835
+ * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
1836
+ * - Any number of key/value pairs with string values, like `placeholder="Your name"` or `data-id=123`. These will be handled according to the rules specified for `object`, below, but with the caveat that values can only be strings. The quotes around string values are optional, unless the value contains spaces. It's not possible to escape quotes within the value. If you want to do that, or if you have user-provided values, use the `object` syntax (see below) or end your string with `key=` followed by the data as a separate argument (see below).
1837
+ * - The string may end in a ':' followed by text, which will be added as a TextNode to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
1838
+ * - Alternatively, the string may end in a key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the rules specified for `object` below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`.
1806
1839
  * - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
1807
1840
  * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
1808
1841
  * - `{<attrName>: any}`: The common case is setting the value as an HTML attribute named key. So `{placeholder: "Your name"}` would add `placeholder="Your name"` to the current HTML element.
@@ -1836,6 +1869,14 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1836
1869
  * $color: 'red'
1837
1870
  * });
1838
1871
  * ```
1872
+ *
1873
+ * Which can also be written as:
1874
+ * ```typescript
1875
+ * $('button.secondary.outline text=Submit $color=red disabled=', false, 'click=', () => console.log('Clicked!'));
1876
+ * ```
1877
+ *
1878
+ * We want to set `disabled` as a property instead of an attribute, so we must use the `key=` syntax in order to provide
1879
+ * `false` as a boolean instead of a string.
1839
1880
  *
1840
1881
  * @example Create Nested Elements
1841
1882
  * ```typescript
@@ -1889,24 +1930,85 @@ export function $(
1889
1930
  let savedCurrentScope: undefined | ContentScope;
1890
1931
  let err: undefined | string;
1891
1932
  let result: undefined | Element;
1933
+ let nextArgIsProp: undefined | string;
1892
1934
 
1893
1935
  for (let arg of args) {
1894
- if (arg == null || arg === false) continue;
1895
- if (typeof arg === "string") {
1896
- let text: undefined | string;
1897
- let classes: undefined | string;
1898
- const textPos = arg.indexOf(":");
1899
- if (textPos >= 0) {
1900
- text = arg.substring(textPos + 1);
1901
- arg = arg.substring(0, textPos);
1902
- }
1903
- const classPos = arg.indexOf(".");
1904
- if (classPos >= 0) {
1905
- classes = arg.substring(classPos + 1);
1906
- arg = arg.substring(0, classPos);
1907
- }
1908
- if (arg === "") {
1909
- // Add text or classes to parent
1936
+ if (nextArgIsProp) {
1937
+ applyArg(nextArgIsProp, arg);
1938
+ nextArgIsProp = undefined;
1939
+ } else if (arg == null || arg === false) {
1940
+ // Ignore
1941
+ } else if (typeof arg === "string") {
1942
+ let pos = 0;
1943
+ let argLen = arg.length;
1944
+ while(pos < argLen) {
1945
+ let nextSpace = arg.indexOf(" ", pos);
1946
+ if (nextSpace < 0) nextSpace = arg.length;
1947
+ let part = arg.substring(pos, nextSpace);
1948
+ const oldPos = pos;
1949
+ pos = nextSpace + 1;
1950
+
1951
+ const firstIs = part.indexOf('=');
1952
+ const firstColon = part.indexOf(':');
1953
+ if (firstIs >= 0 && (firstColon < 0 || firstIs < firstColon)) {
1954
+ const prop = part.substring(0, firstIs);
1955
+ if (firstIs < part.length - 1) {
1956
+ let value = part.substring(firstIs + 1);
1957
+ if (value[0] === '"') {
1958
+ const closeIndex = arg.indexOf('"', firstIs+2+oldPos);
1959
+ if (closeIndex < 0) throw new Error(`Unterminated string for '${prop}'`);
1960
+ value = arg.substring(firstIs+2+oldPos, closeIndex);
1961
+ pos = closeIndex + 1;
1962
+ if (arg[pos] === ' ') pos++;
1963
+ }
1964
+ applyArg(prop, value);
1965
+ continue;
1966
+ } else {
1967
+ if (pos < argLen) throw new Error(`No value given for '${part}'`);
1968
+ nextArgIsProp = prop;
1969
+ break
1970
+ }
1971
+ }
1972
+
1973
+ let text;
1974
+ if (firstColon >= 0) {
1975
+ // Read to the end of the arg, ignoring any spaces
1976
+ text = arg.substring(firstColon + 1 + oldPos);
1977
+ part = part.substring(0, firstColon);
1978
+ if (!text) {
1979
+ if (pos < argLen) throw new Error(`No value given for '${part}'`);
1980
+ nextArgIsProp = 'text';
1981
+ break;
1982
+ }
1983
+ pos = argLen;
1984
+ }
1985
+
1986
+ let classes: undefined | string;
1987
+ const classPos = part.indexOf(".");
1988
+ if (classPos >= 0) {
1989
+ classes = part.substring(classPos + 1);
1990
+ part = part.substring(0, classPos);
1991
+ }
1992
+
1993
+ if (part) { // Add an element
1994
+ // Determine which namespace to use for element creation
1995
+ const svg = currentScope.inSvgNamespace || part === 'svg';
1996
+ if (svg) {
1997
+ result = document.createElementNS('http://www.w3.org/2000/svg', part);
1998
+ } else {
1999
+ result = document.createElement(part);
2000
+ }
2001
+ addNode(result);
2002
+ if (!savedCurrentScope) savedCurrentScope = currentScope;
2003
+ const newScope = new ChainedScope(result, true);
2004
+
2005
+ // SVG namespace should be inherited by children
2006
+ if (svg) newScope.inSvgNamespace = true;
2007
+
2008
+ if (topRedrawScope === currentScope) topRedrawScope = newScope;
2009
+ currentScope = newScope;
2010
+ }
2011
+
1910
2012
  if (text) addNode(document.createTextNode(text));
1911
2013
  if (classes) {
1912
2014
  const el = currentScope.parentElement;
@@ -1915,34 +2017,6 @@ export function $(
1915
2017
  clean(() => el.classList.remove(...classes.split(".")));
1916
2018
  }
1917
2019
  }
1918
- } else if (arg.indexOf(" ") >= 0) {
1919
- err = `Tag '${arg}' cannot contain space`;
1920
- break;
1921
- } else {
1922
- // Determine which namespace to use for element creation
1923
- const useNamespace = currentScope.inSvgNamespace || arg === 'svg';
1924
- if (useNamespace) {
1925
- result = document.createElementNS('http://www.w3.org/2000/svg', arg);
1926
- } else {
1927
- result = document.createElement(arg);
1928
- }
1929
-
1930
- if (classes) result.className = classes.replaceAll(".", " ");
1931
- if (text) result.textContent = text;
1932
- addNode(result);
1933
- if (!savedCurrentScope) {
1934
- savedCurrentScope = currentScope;
1935
- }
1936
- const newScope = new ChainedScope(result, true);
1937
-
1938
- // If we're creating an SVG element, set the SVG namespace flag for child scopes
1939
- if (arg === 'svg') {
1940
- newScope.inSvgNamespace = true;
1941
- }
1942
-
1943
- newScope.lastChild = result.lastChild || undefined;
1944
- if (topRedrawScope === currentScope) topRedrawScope = newScope;
1945
- currentScope = newScope;
1946
2020
  }
1947
2021
  } else if (typeof arg === "object") {
1948
2022
  if (arg.constructor !== Object) {
@@ -1971,9 +2045,8 @@ export function $(
1971
2045
  break;
1972
2046
  }
1973
2047
  }
1974
- if (savedCurrentScope) {
1975
- currentScope = savedCurrentScope;
1976
- }
2048
+ if (nextArgIsProp !== undefined) throw new Error(`No value given for '${nextArgIsProp}='`);
2049
+ if (savedCurrentScope) currentScope = savedCurrentScope;
1977
2050
  if (err) throw new Error(err);
1978
2051
  return result;
1979
2052
  }
package/src/route.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {clean, getParentElement, $, proxy, runQueue, unproxy, copy, merge, clone, leakScope} from "./aberdeen.js";
2
2
 
3
- type NavType = "load" | "back" | "forward" | "go";
3
+ type NavType = "load" | "back" | "forward" | "go" | "push";
4
4
 
5
5
  /**
6
6
  * The class for the global `route` object.
@@ -22,6 +22,7 @@ export interface Route {
22
22
  - `"load"`: An initial page load.
23
23
  - `"back"` or `"forward"`: When we navigated backwards or forwards in the stack.
24
24
  - `"go"`: When we added a new page on top of the stack.
25
+ - `"push"`: When we added a new page on top of the stack, merging with the current page.
25
26
  Mostly useful for page transition animations. Writing to this property has no effect.
26
27
  */
27
28
  nav: NavType;
@@ -147,15 +148,15 @@ function targetToPartial(target: RouteTarget) {
147
148
  * route.go({p: ["users", 123], search: {tab: "feed"}, hash: "top"});
148
149
  * ```
149
150
  */
150
- export function go(target: RouteTarget): void {
151
+ export function go(target: RouteTarget, nav: NavType = "go"): void {
151
152
  const stack: string[] = history.state?.stack || [];
152
153
 
153
154
  prevStack = stack.concat(JSON.stringify(unproxy(current)));
154
155
 
155
- const newRoute: Route = toCanonRoute(targetToPartial(target), "go", prevStack.length + 1);
156
+ const newRoute: Route = toCanonRoute(targetToPartial(target), nav, prevStack.length + 1);
156
157
  copy(current, newRoute);
157
158
 
158
- log('go', newRoute);
159
+ log(nav, newRoute);
159
160
  history.pushState({state: newRoute.state, stack: prevStack}, "", getUrl(newRoute));
160
161
 
161
162
  runQueue();