@keenmate/svelte-spa-router 1.0.1

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/active.js ADDED
@@ -0,0 +1,119 @@
1
+ import {parse} from "regexparam"
2
+ import {BasePath, HashRoutingEnabled, loc} from "./Router.svelte"
3
+ import {get} from "svelte/store"
4
+
5
+ // List of nodes to update
6
+ const nodes = []
7
+
8
+ // Current location
9
+ let location
10
+
11
+ // Function that updates all nodes marking the active ones
12
+ function checkActive(el) {
13
+ const matchesLocation = el.pattern.test(location)
14
+ toggleClasses(el, el.className, matchesLocation)
15
+ toggleClasses(el, el.inactiveClassName, !matchesLocation)
16
+ }
17
+
18
+ function toggleClasses(el, className, shouldAdd) {
19
+ (className || "").split(" ").forEach((cls) => {
20
+ if (!cls) {
21
+ return
22
+ }
23
+ // Remove the class firsts
24
+ el.node.classList.remove(cls)
25
+
26
+ // If the pattern doesn't match, then set the class
27
+ if (shouldAdd) {
28
+ el.node.classList.add(cls)
29
+ }
30
+ })
31
+ }
32
+
33
+ // Listen to changes in the location
34
+ loc.subscribe((value) => {
35
+ // Update the location
36
+ location = value.location + (value.querystring ? "?" + value.querystring : "")
37
+
38
+ // Update all nodes
39
+ nodes.map(checkActive)
40
+ })
41
+
42
+ /**
43
+ * @typedef {Object} ActiveOptions
44
+ * @property {string|RegExp} [path] - Path expression that makes the link active when matched (must start with '/' or '*'); default is the link's href
45
+ * @property {string} [className] - CSS class to apply to the element when active; default value is "active"
46
+ */
47
+
48
+ /**
49
+ * Svelte Action for automatically adding the "active" class to elements (links, or any other DOM element) when the current location matches a certain path.
50
+ *
51
+ * @param {HTMLElement} node - The target node (automatically set by Svelte)
52
+ * @param {ActiveOptions|string|RegExp} [opts] - Can be an object of type ActiveOptions, or a string (or regular expressions) representing ActiveOptions.path.
53
+ * @returns {{destroy: function(): void}} Destroy function
54
+ */
55
+ export default function active(node, opts) {
56
+ const basePath = get(BasePath)
57
+
58
+ // Check options
59
+ if (opts && (typeof opts == "string" || (typeof opts == "object" && opts instanceof RegExp))) {
60
+ // Interpret strings and regular expressions as opts.path
61
+ opts = {
62
+ path: opts
63
+ }
64
+ } else {
65
+ // Ensure opts is a dictionary
66
+ opts = opts || {}
67
+ }
68
+
69
+ // Path defaults to link target
70
+ if (!opts.path && node.hasAttribute("href")) {
71
+ opts.path = node.getAttribute("href")
72
+
73
+ if (get(HashRoutingEnabled)) {
74
+ if (opts.path && opts.path.length > 1 && opts.path.charAt(0) == "#") {
75
+ opts.path = opts.path.substring(1)
76
+ }
77
+ } else {
78
+ if (opts.path.startsWith(basePath)) {
79
+ opts.path = opts.path.substring(basePath.length)
80
+ }
81
+ }
82
+ }
83
+
84
+ // Default class name
85
+ if (!opts.className) {
86
+ opts.className = "active"
87
+ }
88
+
89
+ // If path is a string, it must start with '/' or '*'
90
+ if (!opts.path ||
91
+ typeof opts.path == "string" && (opts.path.length < 1 || (opts.path.charAt(0) != "/" && opts.path.charAt(0) != "*"))
92
+ ) {
93
+ throw Error("Invalid value for \"path\" argument")
94
+ }
95
+
96
+ // If path is not a regular expression already, make it
97
+ const {pattern} = typeof opts.path == "string" ?
98
+ parse(opts.path) :
99
+ {pattern: opts.path}
100
+
101
+ // Add the node to the list
102
+ const el = {
103
+ node,
104
+ className: opts.className,
105
+ inactiveClassName: opts.inactiveClassName,
106
+ pattern
107
+ }
108
+ nodes.push(el)
109
+
110
+ // Trigger the action right away
111
+ checkActive(el)
112
+
113
+ return {
114
+ // When the element is destroyed, remove it from the list
115
+ destroy() {
116
+ nodes.splice(nodes.indexOf(el), 1)
117
+ }
118
+ }
119
+ }
package/constants.js ADDED
@@ -0,0 +1 @@
1
+ export const SvelteSPARouterNavigationEvent = "popstate"
@@ -0,0 +1,29 @@
1
+ import {BasePath} from "../Router.svelte"
2
+ import {get} from "svelte/store"
3
+
4
+ /**
5
+ * @typedef {Object} Location
6
+ * @property {string} location - Location (page/view), for example `/book`
7
+ * @property {string} [querystring] - Querystring from the hash, as a string not parsed
8
+ */
9
+ export function joinPaths(...paths) {
10
+ if (!paths || !paths.length) {
11
+ return get(BasePath)
12
+ }
13
+
14
+ return paths
15
+ .map(x => x.trim())
16
+ .filter(x => x)
17
+ .map((x, i, arr) => {
18
+ if (i === 0) {
19
+ return x.replace(/\/$/, "")
20
+ } else if (i < arr.length - 1) {
21
+ return x.replace(/(^\/|\/$)/, "")
22
+ } else {
23
+ return x.replace(/^\//, "")
24
+ }
25
+ })
26
+ .map((x) => x.trim())
27
+ .filter(x => x)
28
+ .join("/")
29
+ }
@@ -0,0 +1,52 @@
1
+ // Selenium configuration
2
+ const seleniumHost = process.env.SELENIUM_HOST || "127.0.0.1"
3
+ const seleniumPort = parseInt(process.env.SELENIUM_PORT || "4444", 10)
4
+
5
+ // Launch URL - where the server is
6
+ const launchUrl = process.env.LAUNCH_URL || "http://localhost:5050"
7
+
8
+ // Increase max listeners to avoid a warning
9
+ require("events").EventEmitter.defaultMaxListeners = 100
10
+
11
+ module.exports = {
12
+ src_folders: [
13
+ "test/cases/"
14
+ ],
15
+
16
+ output_folder: "result",
17
+
18
+ test_runner: {
19
+ type: "mocha",
20
+ options: {
21
+ ui: "bdd",
22
+ reporter: "list"
23
+ }
24
+ },
25
+
26
+ test_settings: {
27
+ default: {
28
+ launch_url: launchUrl
29
+ },
30
+ "selenium.chrome": {
31
+ selenium: {
32
+ start_process: false,
33
+ host: seleniumHost,
34
+ port: seleniumPort
35
+ },
36
+ webdriver: {
37
+ start_process: false
38
+ },
39
+ desiredCapabilities: {
40
+ browserName: "chrome",
41
+ chromeOptions: {
42
+ args: [
43
+ "--headless",
44
+ "--no-sandbox",
45
+ "--disable-gpu"
46
+ ]
47
+ },
48
+ acceptSslCerts: true
49
+ }
50
+ }
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@keenmate/svelte-spa-router",
3
+ "private": false,
4
+ "version": "1.0.1",
5
+ "description": "Router for SPAs using Svelte 4",
6
+ "main": "Router.svelte",
7
+ "svelte": "Router.svelte",
8
+ "types": "Router.d.ts",
9
+ "type": "module",
10
+ "exports": {
11
+ "./active": {
12
+ "types": "./active.d.ts",
13
+ "import": "./active.js"
14
+ },
15
+ "./wrap": {
16
+ "types": "./wrap.d.ts",
17
+ "import": "./wrap.js"
18
+ },
19
+ ".": {
20
+ "types": "./Router.d.ts",
21
+ "svelte": "./Router.svelte"
22
+ },
23
+ "./*": {
24
+ "import": "./*"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build-test-app": "(cd test/app && npx rollup -c)",
29
+ "start-test-app": "npx serve -n -l 5050 test/app/dist",
30
+ "eslint": "npx eslint -c .eslintrc.cjs --ext .js,.svelte,.html .",
31
+ "lint": "npm run eslint",
32
+ "nightwatch": "npx nightwatch -e selenium.chrome -c nightwatch.conf.cjs",
33
+ "test": "npm run nightwatch"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/keenmate/svelte-spa-router.git"
38
+ },
39
+ "keywords": [
40
+ "router",
41
+ "svelte",
42
+ "svelte3",
43
+ "svelte4",
44
+ "spa"
45
+ ],
46
+ "author": "Alessandro Segala (@ItalyPaleAle)",
47
+ "license": "MIT",
48
+ "funding": "https://github.com/sponsors/ItalyPaleAle",
49
+ "bugs": {
50
+ "url": "https://github.com/ItalyPaleAle/svelte-spa-router/issues"
51
+ },
52
+ "homepage": "https://github.com/ItalyPaleAle/svelte-spa-router#readme",
53
+ "dependencies": {
54
+ "regexparam": "2.0.2"
55
+ },
56
+ "devDependencies": {
57
+ "@rollup/plugin-commonjs": "^25.0.7",
58
+ "@rollup/plugin-node-resolve": "^15.2.3",
59
+ "chromedriver": "^119.0.1",
60
+ "eslint": "^8.55.0",
61
+ "eslint-plugin-html": "^7.1.0",
62
+ "eslint-plugin-svelte": "^2.35.1",
63
+ "nightwatch": "^2.6.23",
64
+ "rollup": "^4.7.0",
65
+ "rollup-plugin-css-only": "^4.5.2",
66
+ "rollup-plugin-svelte": "^7.1.6",
67
+ "serve": "^14.2.1",
68
+ "svelte": "^4.2.8"
69
+ }
70
+ }
package/wrap.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import {ComponentType} from "svelte"
2
+ import {AsyncSvelteComponent, RoutePrecondition, WrappedComponent} from "./Router"
3
+
4
+ /** Options object for the call to `wrap` */
5
+ export interface WrapOptions {
6
+ /** Svelte component to load (this is incompatible with `asyncComponent`) */
7
+ component?: ComponentType
8
+
9
+ /** Function that returns a Promise that fulfills with a Svelte component (e.g. `{asyncComponent: () => import('Foo.svelte')}`) */
10
+ asyncComponent?: AsyncSvelteComponent
11
+
12
+ /** Svelte component to be displayed while the async route is loading (as a placeholder); when unset or false-y, no component is shown while component */
13
+ loadingComponent?: ComponentType
14
+
15
+ /** Optional dictionary passed to the `loadingComponent` component as params (for an exported prop called `params`) */
16
+ loadingParams?: object
17
+
18
+ /** Optional object that will be passed to events such as `routeLoading`, `routeLoaded`, `conditionsFailed` */
19
+ userData?: object
20
+
21
+ /** Optional key-value dictionary of static props that will be passed to the component. The props are expanded with {...props}, so the key in the dictionary becomes the name of the prop. */
22
+ props?: object
23
+
24
+ /** Route pre-conditions to add, which will be executed in order */
25
+ conditions?: RoutePrecondition[] | RoutePrecondition
26
+ }
27
+
28
+ /**
29
+ * Wraps a component to enable multiple capabilities:
30
+ *
31
+ * 1. Using dynamically-imported component, with (e.g. `{asyncComponent: () => import('Foo.svelte')}`), which also allows bundlers to do code-splitting.
32
+ * 2. Adding route pre-conditions (e.g. `{conditions: [...]}`)
33
+ * 3. Adding static props that are passed to the component
34
+ * 4. Adding custom userData, which is passed to route events (e.g. route loaded events) or to route pre-conditions (e.g. `{userData: {foo: 'bar}}`)
35
+ *
36
+ * @param args Arguments object
37
+ * @returns Wrapped component
38
+ */
39
+ export function wrap(args: WrapOptions): WrappedComponent
40
+
41
+ export default wrap
package/wrap.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @typedef {Object} WrappedComponent Object returned by the `wrap` method
3
+ * @property {SvelteComponent} component - Component to load (this is always asynchronous)
4
+ * @property {RoutePrecondition[]} [conditions] - Route pre-conditions to validate
5
+ * @property {Object} [props] - Optional dictionary of static props
6
+ * @property {Object} [userData] - Optional user data dictionary
7
+ * @property {bool} _sveltesparouter - Internal flag; always set to true
8
+ */
9
+
10
+ /**
11
+ * @callback AsyncSvelteComponent
12
+ * @returns {Promise<SvelteComponent>} Returns a Promise that resolves with a Svelte component
13
+ */
14
+
15
+ /**
16
+ * @callback RoutePrecondition
17
+ * @param {RouteDetail} detail - Route detail object
18
+ * @returns {boolean|Promise<boolean>} If the callback returns a false-y value, it's interpreted as the precondition failed, so it aborts loading the component (and won't process other pre-condition callbacks)
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} WrapOptions Options object for the call to `wrap`
23
+ * @property {SvelteComponent} [component] - Svelte component to load (this is incompatible with `asyncComponent`)
24
+ * @property {AsyncSvelteComponent} [asyncComponent] - Function that returns a Promise that fulfills with a Svelte component (e.g. `{asyncComponent: () => import('Foo.svelte')}`)
25
+ * @property {SvelteComponent} [loadingComponent] - Svelte component to be displayed while the async route is loading (as a placeholder); when unset or false-y, no component is shown while component
26
+ * @property {object} [loadingParams] - Optional dictionary passed to the `loadingComponent` component as params (for an exported prop called `params`)
27
+ * @property {object} [userData] - Optional object that will be passed to events such as `routeLoading`, `routeLoaded`, `conditionsFailed`
28
+ * @property {object} [props] - Optional key-value dictionary of static props that will be passed to the component. The props are expanded with {...props}, so the key in the dictionary becomes the name of the prop.
29
+ * @property {RoutePrecondition[]|RoutePrecondition} [conditions] - Route pre-conditions to add, which will be executed in order
30
+ */
31
+
32
+ /**
33
+ * Wraps a component to enable multiple capabilities:
34
+ * 1. Using dynamically-imported component, with (e.g. `{asyncComponent: () => import('Foo.svelte')}`), which also allows bundlers to do code-splitting.
35
+ * 2. Adding route pre-conditions (e.g. `{conditions: [...]}`)
36
+ * 3. Adding static props that are passed to the component
37
+ * 4. Adding custom userData, which is passed to route events (e.g. route loaded events) or to route pre-conditions (e.g. `{userData: {foo: 'bar}}`)
38
+ *
39
+ * @param {WrapOptions} args - Arguments object
40
+ * @returns {WrappedComponent} Wrapped component
41
+ */
42
+ export function wrap(args) {
43
+ if (!args) {
44
+ throw Error("Parameter args is required")
45
+ }
46
+
47
+ // We need to have one and only one of component and asyncComponent
48
+ // This does a "XNOR"
49
+ if (!args.component == !args.asyncComponent) {
50
+ throw Error("One and only one of component and asyncComponent is required")
51
+ }
52
+
53
+ // If the component is not async, wrap it into a function returning a Promise
54
+ if (args.component) {
55
+ args.asyncComponent = () => Promise.resolve(args.component)
56
+ }
57
+
58
+ // Parameter asyncComponent and each item of conditions must be functions
59
+ if (typeof args.asyncComponent != "function") {
60
+ throw Error("Parameter asyncComponent must be a function")
61
+ }
62
+ if (args.conditions) {
63
+ // Ensure it's an array
64
+ if (!Array.isArray(args.conditions)) {
65
+ args.conditions = [args.conditions]
66
+ }
67
+ for (let i = 0; i < args.conditions.length; i++) {
68
+ if (!args.conditions[i] || typeof args.conditions[i] != "function") {
69
+ throw Error("Invalid parameter conditions[" + i + "]")
70
+ }
71
+ }
72
+ }
73
+
74
+ // Check if we have a placeholder component
75
+ if (args.loadingComponent) {
76
+ args.asyncComponent.loading = args.loadingComponent
77
+ args.asyncComponent.loadingParams = args.loadingParams || undefined
78
+ }
79
+
80
+ // Returns an object that contains all the functions to execute too
81
+ // The _sveltesparouter flag is to confirm the object was created by this router
82
+ const obj = {
83
+ component: args.asyncComponent,
84
+ userData: args.userData,
85
+ conditions: (args.conditions && args.conditions.length) ? args.conditions : undefined,
86
+ props: (args.props && Object.keys(args.props).length) ? args.props : {},
87
+ _sveltesparouter: true
88
+ }
89
+
90
+ return obj
91
+ }
92
+
93
+ export default wrap