vue-context-storage 0.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/collection.d.cts +22 -0
- package/dist/collection.d.ts +22 -0
- package/dist/handlers/query/helpers.d.cts +45 -0
- package/dist/handlers/query/helpers.d.ts +45 -0
- package/dist/handlers/query/index.d.cts +31 -0
- package/dist/handlers/query/index.d.ts +31 -0
- package/dist/handlers/query/transform-helpers.d.cts +123 -0
- package/dist/handlers/query/transform-helpers.d.ts +123 -0
- package/dist/handlers/query/types.d.cts +103 -0
- package/dist/handlers/query/types.d.ts +103 -0
- package/dist/handlers.d.cts +15 -0
- package/dist/handlers.d.ts +15 -0
- package/dist/index.cjs +455 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +413 -0
- package/dist/index.js.map +1 -0
- package/dist/injectionSymbols.d.cts +7 -0
- package/dist/injectionSymbols.d.ts +7 -0
- package/dist/symbols.d.cts +4 -0
- package/dist/symbols.d.ts +4 -0
- package/package.json +77 -0
- package/src/collection.ts +71 -0
- package/src/components/ContextStorageActivator.vue +20 -0
- package/src/components/ContextStorageCollection.vue +91 -0
- package/src/components/ContextStorageProvider.vue +38 -0
- package/src/components/ContextStorageRoot.vue +23 -0
- package/src/components.ts +5 -0
- package/src/handlers/query/helpers.ts +134 -0
- package/src/handlers/query/index.ts +355 -0
- package/src/handlers/query/transform-helpers.ts +309 -0
- package/src/handlers/query/types.ts +125 -0
- package/src/handlers.ts +18 -0
- package/src/index.ts +42 -0
- package/src/injectionSymbols.ts +15 -0
- package/src/plugin.ts +16 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/symbols.ts +4 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ContextStorageHandler, ContextStorageHandlerConstructor } from './handlers'
|
|
2
|
+
|
|
3
|
+
export type ContextStorageCollectionItem = {
|
|
4
|
+
key: string
|
|
5
|
+
handlers: ContextStorageHandler[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ItemOptions {
|
|
9
|
+
key: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ContextStorageCollection {
|
|
13
|
+
public active?: ContextStorageCollectionItem = undefined
|
|
14
|
+
private collection: ContextStorageCollectionItem[] = []
|
|
15
|
+
private onActiveChangeCallbacks: ((item: ContextStorageCollectionItem) => void)[] = []
|
|
16
|
+
|
|
17
|
+
constructor(private handlerConstructors: ContextStorageHandlerConstructor[]) {}
|
|
18
|
+
|
|
19
|
+
onActiveChange(callback: (item: ContextStorageCollectionItem) => void): void {
|
|
20
|
+
this.onActiveChangeCallbacks.push(callback)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
first(): ContextStorageCollectionItem | undefined {
|
|
24
|
+
return this.collection[0]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
findItemByKey(key: string): ContextStorageCollectionItem | undefined {
|
|
28
|
+
return this.collection.find((item) => item.key === key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
add(options: ItemOptions): ContextStorageCollectionItem {
|
|
32
|
+
const handlers = this.handlerConstructors.map((constructor) => new constructor())
|
|
33
|
+
|
|
34
|
+
const item: ContextStorageCollectionItem = { handlers, key: options.key }
|
|
35
|
+
|
|
36
|
+
this.collection.push(item)
|
|
37
|
+
|
|
38
|
+
return item
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
remove(removeItem: ContextStorageCollectionItem): void {
|
|
42
|
+
if (this.collection.indexOf(removeItem) === -1) {
|
|
43
|
+
throw new Error('[ContextStorage] Item not found in collection')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.collection = this.collection.filter((item) => item !== removeItem)
|
|
47
|
+
|
|
48
|
+
if (this.active === removeItem && this.collection.length > 0) {
|
|
49
|
+
this.setActive(this.collection[this.collection.length - 1])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setActive(activeItem: ContextStorageCollectionItem): void {
|
|
54
|
+
if (this.active === activeItem) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hasActiveBefore = this.active !== undefined
|
|
59
|
+
this.active = activeItem
|
|
60
|
+
|
|
61
|
+
this.collection.forEach((item) => {
|
|
62
|
+
Object.values(item.handlers).forEach((handler) => {
|
|
63
|
+
if (handler.setEnabled) {
|
|
64
|
+
handler.setEnabled(item === activeItem, !hasActiveBefore)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
this.onActiveChangeCallbacks.forEach((callback) => callback(activeItem))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { defineComponent, h, inject } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
contextStorageCollectionInjectKey,
|
|
5
|
+
contextStorageCollectionItemInjectKey,
|
|
6
|
+
} from '../injectionSymbols'
|
|
7
|
+
|
|
8
|
+
export default defineComponent({
|
|
9
|
+
setup(_, { slots }) {
|
|
10
|
+
const collection = inject(contextStorageCollectionInjectKey)!
|
|
11
|
+
const item = inject(contextStorageCollectionItemInjectKey)!
|
|
12
|
+
|
|
13
|
+
const onActivate = () => {
|
|
14
|
+
collection.setActive(item)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return () => h('div', { onMousedown: onActivate }, slots.default?.())
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ContextStorageCollection, ContextStorageCollectionItem } from '../collection'
|
|
3
|
+
import { ContextStorageHandlerConstructor } from '../handlers'
|
|
4
|
+
import { contextStorageCollectionInjectKey } from '../injectionSymbols'
|
|
5
|
+
import { computed, defineComponent, PropType, provide } from 'vue'
|
|
6
|
+
import { useRouter } from 'vue-router'
|
|
7
|
+
|
|
8
|
+
export default defineComponent({
|
|
9
|
+
props: {
|
|
10
|
+
handlers: {
|
|
11
|
+
type: Object as PropType<ContextStorageHandlerConstructor[]>,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
setup({ handlers }, { slots }) {
|
|
16
|
+
const lastActive = computed({
|
|
17
|
+
get: () => localStorage.getItem('context-storage-last-active') || 'main',
|
|
18
|
+
set: (value) => localStorage.setItem('context-storage-last-active', value),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const router = useRouter()
|
|
22
|
+
|
|
23
|
+
const initialNavigatorState = new Map<
|
|
24
|
+
ContextStorageHandlerConstructor,
|
|
25
|
+
Record<string, unknown>
|
|
26
|
+
>()
|
|
27
|
+
const initialNavigatorStateResolvers = new Map<
|
|
28
|
+
ContextStorageHandlerConstructor,
|
|
29
|
+
() => Record<string, unknown>
|
|
30
|
+
>()
|
|
31
|
+
|
|
32
|
+
handlers.forEach((handler) => {
|
|
33
|
+
if (!handler.getInitialStateResolver) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
initialNavigatorStateResolvers.set(handler, handler.getInitialStateResolver())
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
router.isReady().then(() => {
|
|
41
|
+
initialNavigatorStateResolvers.forEach((resolver, handler) => {
|
|
42
|
+
initialNavigatorState.set(handler, resolver())
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
activateLastActiveItem()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const collection = new ContextStorageCollection(handlers)
|
|
49
|
+
collection.onActiveChange((item) => {
|
|
50
|
+
lastActive.value = item.key
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
provide(contextStorageCollectionInjectKey, collection)
|
|
54
|
+
|
|
55
|
+
const activateInitialItem = (item: ContextStorageCollectionItem) => {
|
|
56
|
+
item.handlers.forEach((handler) => {
|
|
57
|
+
const state = initialNavigatorState.get(
|
|
58
|
+
handler.constructor as ContextStorageHandlerConstructor,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!state) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handler.setInitialState?.(state)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
collection.setActive(item)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const activateLastActiveItem = () => {
|
|
72
|
+
const lastActiveItem = collection.findItemByKey(lastActive.value)
|
|
73
|
+
if (lastActiveItem) {
|
|
74
|
+
activateInitialItem(lastActiveItem)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const firstItem = collection.first()
|
|
79
|
+
if (!firstItem) {
|
|
80
|
+
throw new Error('[ContextStorage] Cannot find first item in collection')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
activateInitialItem(firstItem)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
return slots.default?.()
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
contextStorageCollectionInjectKey,
|
|
4
|
+
contextStorageCollectionItemInjectKey,
|
|
5
|
+
contextStorageHandlersInjectKey,
|
|
6
|
+
} from '../injectionSymbols'
|
|
7
|
+
import { defineComponent, inject, onUnmounted, provide } from 'vue'
|
|
8
|
+
|
|
9
|
+
export default defineComponent({
|
|
10
|
+
props: {
|
|
11
|
+
itemKey: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
setup(props, { slots }) {
|
|
17
|
+
const collection = inject(contextStorageCollectionInjectKey)
|
|
18
|
+
if (!collection) throw new Error('[ContextStorage] Context storage collection not found')
|
|
19
|
+
|
|
20
|
+
const item = collection.add({
|
|
21
|
+
key: props.itemKey,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
provide(contextStorageCollectionItemInjectKey, item)
|
|
25
|
+
provide(contextStorageHandlersInjectKey, item.handlers)
|
|
26
|
+
|
|
27
|
+
item.handlers.forEach((handler) => {
|
|
28
|
+
provide(handler.getInjectionKey(), handler)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
onUnmounted(() => {
|
|
32
|
+
collection.remove(item)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return () => slots.default?.()
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ContextStorageCollection :handlers="handlers">
|
|
3
|
+
<ContextStorageProvider item-key="main">
|
|
4
|
+
<ContextStorageActivator>
|
|
5
|
+
<slot />
|
|
6
|
+
</ContextStorageActivator>
|
|
7
|
+
</ContextStorageProvider>
|
|
8
|
+
</ContextStorageCollection>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import ContextStorageActivator from './ContextStorageActivator.vue'
|
|
13
|
+
import ContextStorageCollection from './ContextStorageCollection.vue'
|
|
14
|
+
import ContextStorageProvider from './ContextStorageProvider.vue'
|
|
15
|
+
import { ContextStorageHandlerConstructor } from '../handlers'
|
|
16
|
+
import { defaultHandlers } from '../index'
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
handlers: ContextStorageHandlerConstructor[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { handlers = defaultHandlers } = defineProps<Props>()
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Vue components - import directly from source
|
|
2
|
+
export { default as ContextStorageActivator } from './components/ContextStorageActivator.vue'
|
|
3
|
+
export { default as ContextStorageCollection } from './components/ContextStorageCollection.vue'
|
|
4
|
+
export { default as ContextStorageProvider } from './components/ContextStorageProvider.vue'
|
|
5
|
+
export { default as ContextStorageRoot } from './components/ContextStorageRoot.vue'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { LocationQuery } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
export interface SerializeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Custom prefix for serialized keys.
|
|
6
|
+
* @example
|
|
7
|
+
* - prefix: 'filters' => 'filters[key]'
|
|
8
|
+
* - prefix: 'search' => 'search[key]'
|
|
9
|
+
* - prefix: '' => 'key' (no prefix)
|
|
10
|
+
*/
|
|
11
|
+
prefix?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serializes filter parameters into a URL-friendly format.
|
|
16
|
+
*
|
|
17
|
+
* @param params - Raw parameters object to serialize
|
|
18
|
+
* @param options - Serialization options
|
|
19
|
+
* @returns Serialized parameters with prefixed keys
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // With default prefix 'filters'
|
|
23
|
+
* serializeFiltersParams({ status: 'active', tags: ['a', 'b'] })
|
|
24
|
+
* // => { 'filters[status]': 'active', 'filters[tags]': 'a,b' }
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // With custom prefix
|
|
28
|
+
* serializeFiltersParams({ name: 'John', all: true }, { prefix: 'search' })
|
|
29
|
+
* // => { 'search[name]': 'John', 'search[all]': '1' }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Without prefix
|
|
33
|
+
* serializeFiltersParams({ page: 1, all: false }, { prefix: '' })
|
|
34
|
+
* // => { 'page': '1', 'all': '0' }
|
|
35
|
+
*/
|
|
36
|
+
export function serializeParams(
|
|
37
|
+
params: Record<string, unknown>,
|
|
38
|
+
options: SerializeOptions = {},
|
|
39
|
+
): LocationQuery {
|
|
40
|
+
const { prefix = '' } = options
|
|
41
|
+
|
|
42
|
+
const result: LocationQuery = {}
|
|
43
|
+
|
|
44
|
+
Object.keys(params).forEach((key) => {
|
|
45
|
+
const value = params[key]
|
|
46
|
+
|
|
47
|
+
// Skip empty values, null, and empty arrays
|
|
48
|
+
if (value === '') {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (value === null) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Format the key with prefix (or without if prefix is empty)
|
|
61
|
+
const formattedKey = prefix ? `${prefix}[${key}]` : key
|
|
62
|
+
|
|
63
|
+
if (typeof value === 'object') {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
// Serialize arrays directly: a=1&a=2&a=3
|
|
66
|
+
result[formattedKey] = value.map(String)
|
|
67
|
+
} else {
|
|
68
|
+
Object.assign(
|
|
69
|
+
result,
|
|
70
|
+
serializeParams(value as Record<string, unknown>, {
|
|
71
|
+
...options,
|
|
72
|
+
prefix: formattedKey,
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
} else if (typeof value === 'boolean') {
|
|
77
|
+
result[formattedKey] = value ? '1' : '0'
|
|
78
|
+
} else {
|
|
79
|
+
result[formattedKey] = String(value)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deserializes query parameters from a URL-friendly format back to an object.
|
|
88
|
+
*
|
|
89
|
+
* @param params - Serialized parameters object
|
|
90
|
+
* @returns Deserialized parameters object
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* deserializeParams({ 'filters[status]': 'active', search: 'test' })
|
|
94
|
+
* // => { filters: {status: 'active'}, search: 'test' }
|
|
95
|
+
*/
|
|
96
|
+
export function deserializeParams(params: Record<string, any>): Record<string, any> {
|
|
97
|
+
return Object.keys(params).reduce<Record<string, any>>((acc, key) => {
|
|
98
|
+
const value = params[key]
|
|
99
|
+
|
|
100
|
+
// Parse nested structure: 'filters[status]' -> { filters: { status: value } }
|
|
101
|
+
const bracketMatch = key.match(/^([^[]+)\[(.+)]$/)
|
|
102
|
+
|
|
103
|
+
if (bracketMatch) {
|
|
104
|
+
const [, rootKey, nestedPath] = bracketMatch
|
|
105
|
+
|
|
106
|
+
// Initialize root object if needed
|
|
107
|
+
if (!acc[rootKey]) {
|
|
108
|
+
acc[rootKey] = {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse nested path: 'created_at][from' -> ['created_at', 'from']
|
|
112
|
+
const pathParts = nestedPath.split('][')
|
|
113
|
+
|
|
114
|
+
// Navigate/create nested structure
|
|
115
|
+
let current = acc[rootKey]
|
|
116
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
117
|
+
const part = pathParts[i]
|
|
118
|
+
if (!current[part]) {
|
|
119
|
+
current[part] = {}
|
|
120
|
+
}
|
|
121
|
+
current = current[part]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Set the final value
|
|
125
|
+
const finalKey = pathParts[pathParts.length - 1]
|
|
126
|
+
current[finalKey] = value
|
|
127
|
+
} else {
|
|
128
|
+
// No brackets - simple key
|
|
129
|
+
acc[key] = value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return acc
|
|
133
|
+
}, {})
|
|
134
|
+
}
|