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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/collection.d.cts +22 -0
  4. package/dist/collection.d.ts +22 -0
  5. package/dist/handlers/query/helpers.d.cts +45 -0
  6. package/dist/handlers/query/helpers.d.ts +45 -0
  7. package/dist/handlers/query/index.d.cts +31 -0
  8. package/dist/handlers/query/index.d.ts +31 -0
  9. package/dist/handlers/query/transform-helpers.d.cts +123 -0
  10. package/dist/handlers/query/transform-helpers.d.ts +123 -0
  11. package/dist/handlers/query/types.d.cts +103 -0
  12. package/dist/handlers/query/types.d.ts +103 -0
  13. package/dist/handlers.d.cts +15 -0
  14. package/dist/handlers.d.ts +15 -0
  15. package/dist/index.cjs +455 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +12 -0
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.js +413 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/injectionSymbols.d.cts +7 -0
  22. package/dist/injectionSymbols.d.ts +7 -0
  23. package/dist/symbols.d.cts +4 -0
  24. package/dist/symbols.d.ts +4 -0
  25. package/package.json +77 -0
  26. package/src/collection.ts +71 -0
  27. package/src/components/ContextStorageActivator.vue +20 -0
  28. package/src/components/ContextStorageCollection.vue +91 -0
  29. package/src/components/ContextStorageProvider.vue +38 -0
  30. package/src/components/ContextStorageRoot.vue +23 -0
  31. package/src/components.ts +5 -0
  32. package/src/handlers/query/helpers.ts +134 -0
  33. package/src/handlers/query/index.ts +355 -0
  34. package/src/handlers/query/transform-helpers.ts +309 -0
  35. package/src/handlers/query/types.ts +125 -0
  36. package/src/handlers.ts +18 -0
  37. package/src/index.ts +42 -0
  38. package/src/injectionSymbols.ts +15 -0
  39. package/src/plugin.ts +16 -0
  40. package/src/shims-vue.d.ts +5 -0
  41. 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
+ }