@uistate/core 3.0.0 → 3.1.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/package.json +1 -1
- package/src/cssState.js +132 -51
- package/src/eventState.js +4 -4
- package/src/index.js +61 -61
- package/src/stateInspector.js +14 -14
- package/src/stateSerializer.js +93 -30
package/package.json
CHANGED
package/src/cssState.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* UIstate - CSS-based state management module with integrated serialization
|
|
3
3
|
* Part of the UIstate declarative state management system
|
|
4
4
|
* Uses CSS custom properties and data attributes for state representation
|
|
5
|
-
*
|
|
5
|
+
* Features modular extension capabilities for DOM binding and events
|
|
6
6
|
*/
|
|
7
7
|
import StateSerializer from './stateSerializer.js';
|
|
8
8
|
|
|
@@ -11,74 +11,60 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
11
11
|
_sheet: null,
|
|
12
12
|
_observers: new Map(),
|
|
13
13
|
_serializer: serializer,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
_specialHandlers: {},
|
|
15
|
+
_eventHandlers: new Map(), // Store custom event binding handlers
|
|
16
|
+
|
|
17
|
+
init(serializerConfig) {
|
|
16
18
|
if (!this._sheet) {
|
|
17
19
|
const style = document.createElement('style');
|
|
18
20
|
document.head.appendChild(style);
|
|
19
21
|
this._sheet = style.sheet;
|
|
20
22
|
this._addRule(':root {}');
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
// Configure serializer if options provided
|
|
26
|
+
if (serializerConfig && typeof serializerConfig === 'object') {
|
|
27
|
+
this._serializer.configure(serializerConfig);
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
// Initialize with any provided state
|
|
24
31
|
if (initialState && typeof initialState === 'object') {
|
|
25
32
|
Object.entries(initialState).forEach(([key, value]) => {
|
|
26
33
|
this.setState(key, value);
|
|
27
34
|
});
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
return this;
|
|
31
38
|
},
|
|
32
|
-
|
|
39
|
+
|
|
33
40
|
setState(key, value) {
|
|
34
41
|
// Use serializer for CSS variables
|
|
35
42
|
const cssValue = this._serializer.serialize(key, value);
|
|
36
43
|
document.documentElement.style.setProperty(`--${key}`, cssValue);
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
document.documentElement.setAttribute(`data-${key}`, this._serializer.serialize(key, value));
|
|
43
|
-
|
|
44
|
-
// For objects, also set each property as a separate data attribute
|
|
45
|
-
if (!Array.isArray(value)) {
|
|
46
|
-
Object.entries(value).forEach(([propKey, propValue]) => {
|
|
47
|
-
const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
|
|
48
|
-
if (propValue !== null && propValue !== undefined) {
|
|
49
|
-
if (typeof propValue === 'object') {
|
|
50
|
-
document.documentElement.setAttribute(
|
|
51
|
-
attributeKey,
|
|
52
|
-
this._serializer.serialize(`${key}.${propKey}`, propValue)
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
document.documentElement.setAttribute(attributeKey, propValue);
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
document.documentElement.removeAttribute(attributeKey);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
// For primitives, set directly
|
|
64
|
-
document.documentElement.setAttribute(`data-${key}`, value);
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
document.documentElement.removeAttribute(`data-${key}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
44
|
+
|
|
45
|
+
// Use serializer to handle all attribute application consistently
|
|
46
|
+
this._serializer.applyToAttributes(key, value);
|
|
47
|
+
|
|
48
|
+
// Notify any registered observers of the state change
|
|
70
49
|
this._notifyObservers(key, value);
|
|
71
50
|
return value;
|
|
72
51
|
},
|
|
73
|
-
|
|
52
|
+
|
|
53
|
+
setStates(stateObject) {
|
|
54
|
+
Object.entries(stateObject).forEach(([key, value]) => {
|
|
55
|
+
this.setState(key, value);
|
|
56
|
+
});
|
|
57
|
+
return this;
|
|
58
|
+
},
|
|
59
|
+
|
|
74
60
|
getState(key) {
|
|
75
61
|
const value = getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim();
|
|
76
62
|
if (!value) return '';
|
|
77
|
-
|
|
63
|
+
|
|
78
64
|
// Use serializer for deserialization
|
|
79
65
|
return this._serializer.deserialize(key, value);
|
|
80
66
|
},
|
|
81
|
-
|
|
67
|
+
|
|
82
68
|
observe(key, callback) {
|
|
83
69
|
if (!this._observers.has(key)) {
|
|
84
70
|
this._observers.set(key, new Set());
|
|
@@ -91,20 +77,115 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
91
77
|
}
|
|
92
78
|
};
|
|
93
79
|
},
|
|
94
|
-
|
|
80
|
+
|
|
95
81
|
_notifyObservers(key, value) {
|
|
96
82
|
const observers = this._observers.get(key);
|
|
97
83
|
if (observers) {
|
|
98
84
|
observers.forEach(cb => cb(value));
|
|
99
85
|
}
|
|
100
86
|
},
|
|
101
|
-
|
|
87
|
+
|
|
88
|
+
registerSpecialHandler(stateKey, handlerFn) {
|
|
89
|
+
this._specialHandlers[stateKey] = handlerFn;
|
|
90
|
+
return this;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// New method for registering event bindings
|
|
94
|
+
registerEventBinding(eventType, handler) {
|
|
95
|
+
this._eventHandlers.set(eventType, handler);
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
setupObservers(container = document) {
|
|
100
|
+
container.querySelectorAll('[data-observe]:not([data-observing])').forEach(el => {
|
|
101
|
+
const stateKey = el.dataset.observe;
|
|
102
|
+
|
|
103
|
+
this.observe(stateKey, (value) => {
|
|
104
|
+
// Special handlers should run first to set data-state
|
|
105
|
+
if (this._specialHandlers[stateKey]?.observe) {
|
|
106
|
+
this._specialHandlers[stateKey].observe(value, el);
|
|
107
|
+
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
108
|
+
// Only update data-state for elements that already have this attribute
|
|
109
|
+
el.dataset.state = value;
|
|
110
|
+
} else {
|
|
111
|
+
// For normal state observers like theme, counter, etc.
|
|
112
|
+
el.textContent = value;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Trigger initial state
|
|
117
|
+
const initialValue = this.getState(stateKey);
|
|
118
|
+
if (this._specialHandlers[stateKey]?.observe) {
|
|
119
|
+
this._specialHandlers[stateKey].observe(initialValue, el);
|
|
120
|
+
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
121
|
+
// Only set data-state for elements that should have this attribute
|
|
122
|
+
el.dataset.state = initialValue;
|
|
123
|
+
} else {
|
|
124
|
+
// For normal elements
|
|
125
|
+
el.textContent = initialValue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
el.dataset.observing = 'true';
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return this;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Default event handlers available for implementations to use
|
|
135
|
+
defaultClickHandler(e) {
|
|
136
|
+
const target = e.target.closest('[data-state-action]');
|
|
137
|
+
if (!target) return;
|
|
138
|
+
|
|
139
|
+
const stateAction = target.dataset.stateAction;
|
|
140
|
+
if (!stateAction) return;
|
|
141
|
+
|
|
142
|
+
// Special handlers get first priority
|
|
143
|
+
if (this._specialHandlers[stateAction]?.action) {
|
|
144
|
+
this._specialHandlers[stateAction].action(target);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle direct value setting via data-state-value
|
|
149
|
+
if (target.dataset.stateValue !== undefined) {
|
|
150
|
+
const valueToSet = target.dataset.stateValue;
|
|
151
|
+
this.setState(stateAction, valueToSet);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
defaultInputHandler(e) {
|
|
156
|
+
const target = e.target;
|
|
157
|
+
const stateAction = target.dataset.stateAction;
|
|
158
|
+
|
|
159
|
+
if (!stateAction) return;
|
|
160
|
+
|
|
161
|
+
// Special handlers should access any needed data directly from the target
|
|
162
|
+
if (this._specialHandlers[stateAction]?.action) {
|
|
163
|
+
this._specialHandlers[stateAction].action(target);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// Updated setupStateActions to use registered event handlers
|
|
168
|
+
setupStateActions(container = document) {
|
|
169
|
+
// Only bind the registered event types
|
|
170
|
+
this._eventHandlers.forEach((handler, eventType) => {
|
|
171
|
+
container.addEventListener(eventType, handler);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// If no event handlers registered, register the default ones
|
|
175
|
+
if (this._eventHandlers.size === 0) {
|
|
176
|
+
container.addEventListener('click', (e) => this.defaultClickHandler(e));
|
|
177
|
+
container.addEventListener('input', (e) => this.defaultInputHandler(e));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return this;
|
|
181
|
+
},
|
|
182
|
+
|
|
102
183
|
_addRule(rule) {
|
|
103
184
|
if (this._sheet) {
|
|
104
185
|
this._sheet.insertRule(rule, this._sheet.cssRules.length);
|
|
105
186
|
}
|
|
106
187
|
},
|
|
107
|
-
|
|
188
|
+
|
|
108
189
|
// Add serializer configuration method
|
|
109
190
|
configureSerializer(config) {
|
|
110
191
|
if (this._serializer.configure) {
|
|
@@ -112,7 +193,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
112
193
|
}
|
|
113
194
|
return this;
|
|
114
195
|
},
|
|
115
|
-
|
|
196
|
+
|
|
116
197
|
// Clean up resources
|
|
117
198
|
destroy() {
|
|
118
199
|
this._observers.clear();
|
|
@@ -120,12 +201,12 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
120
201
|
// as removing it would affect the UI state
|
|
121
202
|
}
|
|
122
203
|
};
|
|
123
|
-
|
|
204
|
+
|
|
124
205
|
return state.init();
|
|
125
206
|
};
|
|
126
207
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
208
|
+
// Create a singleton instance for easy usage
|
|
209
|
+
const UIstate = createCssState();
|
|
129
210
|
|
|
130
|
-
export
|
|
131
|
-
export
|
|
211
|
+
export { createCssState };
|
|
212
|
+
export default UIstate;
|
package/src/eventState.js
CHANGED
|
@@ -23,12 +23,12 @@ const createEventState = (initial = {}) => {
|
|
|
23
23
|
return path
|
|
24
24
|
.split(".")
|
|
25
25
|
.reduce(
|
|
26
|
-
(obj, prop) =>
|
|
26
|
+
(obj, prop) =>
|
|
27
27
|
obj && obj[prop] !== undefined ? obj[prop] : undefined,
|
|
28
28
|
store
|
|
29
29
|
);
|
|
30
30
|
},
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
// set a value in the store by path
|
|
33
33
|
set: (path, value) => {
|
|
34
34
|
if(!path) return;
|
|
@@ -84,10 +84,10 @@ const createEventState = (initial = {}) => {
|
|
|
84
84
|
|
|
85
85
|
return () => bus.removeEventListener(path, handler);
|
|
86
86
|
},
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// Optional method to clean up resources
|
|
89
89
|
destroy: () => {
|
|
90
|
-
if (bus.parentNode) {
|
|
90
|
+
if (bus.parentNode) {
|
|
91
91
|
bus.parentNode.removeChild(bus);
|
|
92
92
|
}
|
|
93
93
|
},
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UIstate - Declarative state management for the web
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* A unified system that combines CSS and Event-based state management with templating
|
|
5
5
|
* Integrates CSS variables, event-based state, and template management for optimal performance
|
|
6
6
|
* Provides a simple, declarative API for state management
|
|
@@ -24,10 +24,10 @@ function convertPathToCssPath(path) {
|
|
|
24
24
|
const createUnifiedState = (initialState = {}) => {
|
|
25
25
|
// Create the CSS state manager with integrated serialization
|
|
26
26
|
const cssState = createCssState(initialState);
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
// Create the event state manager with the same initial state
|
|
29
29
|
const eventState = createEventState(initialState);
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
// Plugin system
|
|
32
32
|
const plugins = new Map();
|
|
33
33
|
const middlewares = [];
|
|
@@ -37,36 +37,36 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
37
37
|
onInit: [],
|
|
38
38
|
onDestroy: []
|
|
39
39
|
};
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
// Create a unified API
|
|
42
42
|
const unifiedState = {
|
|
43
43
|
_isNotifying: false,
|
|
44
44
|
_cssState: cssState, // Reference to cssState for direct access to advanced features
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
// Get state with hierarchical path support
|
|
47
47
|
getState(path) {
|
|
48
48
|
// Always use eventState as the source of truth
|
|
49
49
|
// This ensures consistency with the DOM state
|
|
50
50
|
const value = eventState.get(path);
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
// If path is undefined or value not found in eventState
|
|
53
53
|
if (path && value === undefined) {
|
|
54
54
|
// Fall back to CSS variable (for values set outside this API)
|
|
55
55
|
const cssPath = convertPathToCssPath(path);
|
|
56
56
|
return cssState.getState(cssPath);
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
return value;
|
|
60
60
|
},
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
// Set state with hierarchical path support
|
|
63
63
|
setState(path, value) {
|
|
64
64
|
if (!path) return this;
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
// Run before state change middlewares
|
|
67
67
|
let finalValue = value;
|
|
68
68
|
let shouldContinue = true;
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
// Apply middlewares
|
|
71
71
|
for (const middleware of middlewares) {
|
|
72
72
|
const result = middleware(path, finalValue, this.getState.bind(this));
|
|
@@ -77,49 +77,49 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
77
77
|
finalValue = result;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
// Run lifecycle hooks
|
|
82
82
|
for (const hook of lifecycleHooks.beforeStateChange) {
|
|
83
83
|
hook(path, finalValue, this.getState.bind(this));
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
if (!shouldContinue) return this;
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// Update event state (for JS access and pub/sub)
|
|
89
89
|
eventState.set(path, finalValue);
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
// Update CSS state (for styling and DOM representation)
|
|
92
92
|
const cssPath = convertPathToCssPath(path);
|
|
93
93
|
cssState.setState(cssPath, finalValue);
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
// Run after state change hooks
|
|
96
96
|
for (const hook of lifecycleHooks.afterStateChange) {
|
|
97
97
|
hook(path, finalValue, this.getState.bind(this));
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
return this;
|
|
101
101
|
},
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
// Plugin system methods
|
|
104
104
|
use(pluginName, plugin) {
|
|
105
105
|
if (plugins.has(pluginName)) {
|
|
106
106
|
console.warn(`Plugin '${pluginName}' is already registered. It will be replaced.`);
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
// Register the plugin
|
|
110
110
|
plugins.set(pluginName, plugin);
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
// Initialize the plugin
|
|
113
113
|
if (typeof plugin.init === 'function') {
|
|
114
114
|
// Pass the API to the plugin
|
|
115
115
|
plugin.init(this);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Register middlewares
|
|
119
119
|
if (Array.isArray(plugin.middlewares)) {
|
|
120
120
|
middlewares.push(...plugin.middlewares);
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
// Register lifecycle hooks
|
|
124
124
|
if (plugin.hooks) {
|
|
125
125
|
Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
|
|
@@ -128,7 +128,7 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
// Add plugin methods to the API
|
|
133
133
|
if (plugin.methods) {
|
|
134
134
|
Object.entries(plugin.methods).forEach(([methodName, method]) => {
|
|
@@ -137,34 +137,34 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
return this;
|
|
142
142
|
},
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
getPlugin(pluginName) {
|
|
145
145
|
return plugins.get(pluginName);
|
|
146
146
|
},
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
hasPlugin(pluginName) {
|
|
149
149
|
return plugins.has(pluginName);
|
|
150
150
|
},
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
removePlugin(pluginName) {
|
|
153
153
|
const plugin = plugins.get(pluginName);
|
|
154
154
|
if (!plugin) return false;
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
// Run cleanup if available
|
|
157
157
|
if (typeof plugin.destroy === 'function') {
|
|
158
158
|
plugin.destroy();
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
// Remove plugin methods
|
|
162
162
|
if (plugin.methods) {
|
|
163
163
|
Object.keys(plugin.methods).forEach(methodName => {
|
|
164
164
|
delete this[methodName];
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
// Remove middlewares
|
|
169
169
|
if (Array.isArray(plugin.middlewares)) {
|
|
170
170
|
plugin.middlewares.forEach(middleware => {
|
|
@@ -174,7 +174,7 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
// Remove lifecycle hooks
|
|
179
179
|
if (plugin.hooks) {
|
|
180
180
|
Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
|
|
@@ -186,12 +186,12 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
// Remove the plugin
|
|
191
191
|
plugins.delete(pluginName);
|
|
192
192
|
return true;
|
|
193
193
|
},
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
// Add middleware
|
|
196
196
|
addMiddleware(middleware) {
|
|
197
197
|
if (typeof middleware === 'function') {
|
|
@@ -200,7 +200,7 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
200
200
|
}
|
|
201
201
|
return false;
|
|
202
202
|
},
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
// Add lifecycle hook
|
|
205
205
|
addHook(hookName, hookFn) {
|
|
206
206
|
if (typeof hookFn === 'function' && Array.isArray(lifecycleHooks[hookName])) {
|
|
@@ -209,60 +209,60 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
209
209
|
}
|
|
210
210
|
return false;
|
|
211
211
|
},
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
// Subscribe to state changes with support for wildcards
|
|
214
214
|
subscribe(path, callback) {
|
|
215
215
|
return eventState.subscribe(path, callback);
|
|
216
216
|
},
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
// Observe CSS state changes (simpler API, no wildcards)
|
|
219
219
|
observe(key, callback) {
|
|
220
220
|
return cssState.observe(key, callback);
|
|
221
221
|
},
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
// Configure serialization options
|
|
224
224
|
configureSerializer(config) {
|
|
225
225
|
cssState.configureSerializer(config);
|
|
226
226
|
return this;
|
|
227
227
|
},
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
// Get current serializer configuration
|
|
230
230
|
getSerializerConfig() {
|
|
231
231
|
return cssState._serializer?.config || null;
|
|
232
232
|
},
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
// Set serialization mode ('hybrid', 'json', or 'escape')
|
|
235
235
|
setSerializationMode(mode) {
|
|
236
236
|
return this.configureSerializer({ mode });
|
|
237
237
|
},
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
// Clean up resources
|
|
240
240
|
destroy() {
|
|
241
241
|
// Run onDestroy hooks
|
|
242
242
|
for (const hook of lifecycleHooks.onDestroy) {
|
|
243
243
|
hook();
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
// Destroy all plugins
|
|
247
247
|
for (const [pluginName, plugin] of plugins.entries()) {
|
|
248
248
|
if (typeof plugin.destroy === 'function') {
|
|
249
249
|
plugin.destroy();
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
// Clear plugins and middlewares
|
|
254
254
|
plugins.clear();
|
|
255
255
|
middlewares.length = 0;
|
|
256
256
|
Object.keys(lifecycleHooks).forEach(key => {
|
|
257
257
|
lifecycleHooks[key].length = 0;
|
|
258
258
|
});
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
// Destroy core state management
|
|
261
261
|
cssState.destroy();
|
|
262
262
|
eventState.destroy();
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
return unifiedState;
|
|
267
267
|
};
|
|
268
268
|
|
|
@@ -273,47 +273,47 @@ const UnifiedState = createUnifiedState();
|
|
|
273
273
|
const templateManagerPlugin = {
|
|
274
274
|
name: 'templateManager',
|
|
275
275
|
instance: null,
|
|
276
|
-
|
|
276
|
+
|
|
277
277
|
init(api) {
|
|
278
278
|
this.instance = createTemplateManager(api);
|
|
279
279
|
return this;
|
|
280
280
|
},
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
destroy() {
|
|
283
283
|
// Any cleanup needed for the template manager
|
|
284
284
|
},
|
|
285
|
-
|
|
285
|
+
|
|
286
286
|
// Plugin methods that will be added to the UIstate API
|
|
287
287
|
methods: {
|
|
288
288
|
onAction(action, handler) {
|
|
289
289
|
return this.instance.onAction(action, handler);
|
|
290
290
|
},
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
registerActions(actionsMap) {
|
|
293
293
|
return this.instance.registerActions(actionsMap);
|
|
294
294
|
},
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
attachDelegation(root = document.body) {
|
|
297
297
|
return this.instance.attachDelegation(root);
|
|
298
298
|
},
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
mount(componentName, container) {
|
|
301
301
|
return this.instance.mount(componentName, container);
|
|
302
302
|
},
|
|
303
|
-
|
|
303
|
+
|
|
304
304
|
renderTemplateFromCss(templateName, data) {
|
|
305
305
|
return this.instance.renderTemplateFromCss(templateName, data);
|
|
306
306
|
},
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
createComponent(name, renderFn, stateKeys) {
|
|
309
309
|
return this.instance.createComponent(name, renderFn, stateKeys);
|
|
310
310
|
},
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
applyClassesFromState(element, stateKey, options) {
|
|
313
313
|
return this.instance.applyClassesFromState(element, stateKey, options);
|
|
314
314
|
}
|
|
315
315
|
},
|
|
316
|
-
|
|
316
|
+
|
|
317
317
|
// Expose handlers property
|
|
318
318
|
get handlers() {
|
|
319
319
|
return this.instance.handlers;
|
|
@@ -323,30 +323,30 @@ const templateManagerPlugin = {
|
|
|
323
323
|
// Create a combined API with plugin support
|
|
324
324
|
const UIstate = {
|
|
325
325
|
...UnifiedState,
|
|
326
|
-
|
|
326
|
+
|
|
327
327
|
// Initialize the system with plugins
|
|
328
328
|
init() {
|
|
329
329
|
// Register the template manager plugin
|
|
330
330
|
this.use('templateManager', templateManagerPlugin);
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
// Run onInit hooks
|
|
333
333
|
for (const hook of this.getPlugin('templateManager').instance.lifecycleHooks?.onInit || []) {
|
|
334
334
|
// for (const hook of lifecycleHooks.onInit) {
|
|
335
335
|
hook();
|
|
336
336
|
}
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
// Attach event delegation to document body
|
|
339
339
|
this.attachDelegation(document.body);
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
return this;
|
|
342
342
|
}
|
|
343
343
|
};
|
|
344
344
|
|
|
345
345
|
export default UIstate;
|
|
346
|
-
export {
|
|
347
|
-
createUnifiedState,
|
|
348
|
-
createCssState,
|
|
349
|
-
createEventState,
|
|
346
|
+
export {
|
|
347
|
+
createUnifiedState,
|
|
348
|
+
createCssState,
|
|
349
|
+
createEventState,
|
|
350
350
|
createTemplateManager,
|
|
351
351
|
convertPathToCssPath,
|
|
352
352
|
templateManagerPlugin
|
package/src/stateInspector.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StateInspector - Real-time state inspection panel for UIstate
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This module provides a configurable UI panel that displays and allows manipulation
|
|
5
5
|
* of CSS variables and state values in UIstate applications.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Toggle visibility with a dedicated button
|
|
9
9
|
* - Real-time updates of CSS variable values
|
|
@@ -28,10 +28,10 @@ function createStateInspector(config = {}) {
|
|
|
28
28
|
stateManager: null, // Optional reference to UIstate or CssState instance
|
|
29
29
|
theme: 'light'
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
// Merge provided config with defaults
|
|
33
33
|
const options = { ...defaultConfig, ...config };
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Private state
|
|
36
36
|
let isVisible = options.initiallyVisible;
|
|
37
37
|
let panel = null;
|
|
@@ -39,12 +39,12 @@ function createStateInspector(config = {}) {
|
|
|
39
39
|
let filterInput = null;
|
|
40
40
|
let stateContainer = null;
|
|
41
41
|
let currentFilter = '';
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
// StateInspector instance
|
|
44
44
|
const inspector = {
|
|
45
45
|
// Current configuration
|
|
46
46
|
config: options,
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
/**
|
|
49
49
|
* Update configuration
|
|
50
50
|
* @param {Object} newConfig - New configuration options
|
|
@@ -53,7 +53,7 @@ function createStateInspector(config = {}) {
|
|
|
53
53
|
Object.assign(this.config, newConfig);
|
|
54
54
|
this.refresh();
|
|
55
55
|
},
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
/**
|
|
58
58
|
* Attach the inspector panel to the target element
|
|
59
59
|
* @param {Element} element - Target element to attach to (defaults to config.target)
|
|
@@ -65,7 +65,7 @@ function createStateInspector(config = {}) {
|
|
|
65
65
|
console.log('StateInspector: Panel would be attached to', element);
|
|
66
66
|
return this;
|
|
67
67
|
},
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
/**
|
|
70
70
|
* Show the inspector panel
|
|
71
71
|
* @returns {Object} - The inspector instance for chaining
|
|
@@ -75,7 +75,7 @@ function createStateInspector(config = {}) {
|
|
|
75
75
|
if (panel) panel.style.display = 'block';
|
|
76
76
|
return this;
|
|
77
77
|
},
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
/**
|
|
80
80
|
* Hide the inspector panel
|
|
81
81
|
* @returns {Object} - The inspector instance for chaining
|
|
@@ -85,7 +85,7 @@ function createStateInspector(config = {}) {
|
|
|
85
85
|
if (panel) panel.style.display = 'none';
|
|
86
86
|
return this;
|
|
87
87
|
},
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
/**
|
|
90
90
|
* Toggle the visibility of the inspector panel
|
|
91
91
|
* @returns {Object} - The inspector instance for chaining
|
|
@@ -93,7 +93,7 @@ function createStateInspector(config = {}) {
|
|
|
93
93
|
toggle() {
|
|
94
94
|
return isVisible ? this.hide() : this.show();
|
|
95
95
|
},
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
/**
|
|
98
98
|
* Refresh the state display
|
|
99
99
|
* @returns {Object} - The inspector instance for chaining
|
|
@@ -104,7 +104,7 @@ function createStateInspector(config = {}) {
|
|
|
104
104
|
console.log('StateInspector: State display would be refreshed');
|
|
105
105
|
return this;
|
|
106
106
|
},
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
/**
|
|
109
109
|
* Filter the displayed state variables
|
|
110
110
|
* @param {string} filterText - Text to filter by
|
|
@@ -115,7 +115,7 @@ function createStateInspector(config = {}) {
|
|
|
115
115
|
// Implementation will filter the displayed variables
|
|
116
116
|
return this.refresh();
|
|
117
117
|
},
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
/**
|
|
120
120
|
* Clean up resources used by the inspector
|
|
121
121
|
*/
|
|
@@ -129,7 +129,7 @@ function createStateInspector(config = {}) {
|
|
|
129
129
|
stateContainer = null;
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
return inspector;
|
|
134
134
|
}
|
|
135
135
|
|
package/src/stateSerializer.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StateSerializer - Configurable serialization module for UIstate
|
|
3
3
|
* Handles transformation between JavaScript values and CSS-compatible string values
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Supports multiple serialization strategies:
|
|
6
6
|
* - 'escape': Uses custom escaping for all values (original UIstate approach)
|
|
7
7
|
* - 'json': Uses JSON.stringify for complex objects, direct values for primitives
|
|
8
8
|
* - 'hybrid': Automatically selects the best strategy based on value type
|
|
9
|
+
*
|
|
10
|
+
* Also handles serialization of values for data attributes and CSS variables
|
|
11
|
+
* with consistent rules and unified serialization behavior
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
// Utility functions for CSS value escaping/unescaping
|
|
@@ -21,7 +24,7 @@ function unescapeCssValue(value) {
|
|
|
21
24
|
if (typeof value !== 'string') return value;
|
|
22
25
|
// Only perform unescaping if there are escape sequences
|
|
23
26
|
if (!value.includes('\\')) return value;
|
|
24
|
-
|
|
27
|
+
|
|
25
28
|
return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
|
|
26
29
|
return String.fromCharCode(parseInt(hex, 16));
|
|
27
30
|
});
|
|
@@ -40,15 +43,15 @@ function createSerializer(config = {}) {
|
|
|
40
43
|
complexThreshold: 3, // Object properties threshold for hybrid mode
|
|
41
44
|
preserveTypes: true // Preserve type information in serialization
|
|
42
45
|
};
|
|
43
|
-
|
|
46
|
+
|
|
44
47
|
// Merge provided config with defaults
|
|
45
48
|
const options = { ...defaultConfig, ...config };
|
|
46
|
-
|
|
49
|
+
|
|
47
50
|
// Serializer instance
|
|
48
51
|
const serializer = {
|
|
49
52
|
// Current configuration
|
|
50
53
|
config: options,
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
/**
|
|
53
56
|
* Update configuration
|
|
54
57
|
* @param {Object} newConfig - New configuration options
|
|
@@ -59,7 +62,7 @@ function createSerializer(config = {}) {
|
|
|
59
62
|
console.log('StateSerializer config updated:', this.config);
|
|
60
63
|
}
|
|
61
64
|
},
|
|
62
|
-
|
|
65
|
+
|
|
63
66
|
/**
|
|
64
67
|
* Serialize a value for storage in CSS variables
|
|
65
68
|
* @param {string} key - The state key (for context-aware serialization)
|
|
@@ -71,14 +74,14 @@ function createSerializer(config = {}) {
|
|
|
71
74
|
if (value === null || value === undefined) {
|
|
72
75
|
return '';
|
|
73
76
|
}
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
const valueType = typeof value;
|
|
76
|
-
const isComplex = valueType === 'object' &&
|
|
77
|
-
(Array.isArray(value) ||
|
|
79
|
+
const isComplex = valueType === 'object' &&
|
|
80
|
+
(Array.isArray(value) ||
|
|
78
81
|
(Object.keys(value).length >= this.config.complexThreshold));
|
|
79
|
-
|
|
82
|
+
|
|
80
83
|
// Select serialization strategy based on configuration and value type
|
|
81
|
-
if (this.config.mode === 'escape' ||
|
|
84
|
+
if (this.config.mode === 'escape' ||
|
|
82
85
|
(this.config.mode === 'hybrid' && !isComplex)) {
|
|
83
86
|
// Use escape strategy for primitives or when escape mode is forced
|
|
84
87
|
if (valueType === 'string') {
|
|
@@ -96,7 +99,7 @@ function createSerializer(config = {}) {
|
|
|
96
99
|
return JSON.stringify(value);
|
|
97
100
|
}
|
|
98
101
|
},
|
|
99
|
-
|
|
102
|
+
|
|
100
103
|
/**
|
|
101
104
|
* Deserialize a value from CSS variable storage
|
|
102
105
|
* @param {string} key - The state key (for context-aware deserialization)
|
|
@@ -106,10 +109,10 @@ function createSerializer(config = {}) {
|
|
|
106
109
|
deserialize(key, value) {
|
|
107
110
|
// Handle empty values
|
|
108
111
|
if (!value) return '';
|
|
109
|
-
|
|
112
|
+
|
|
110
113
|
// Try JSON parse first for values that look like JSON
|
|
111
|
-
if (this.config.mode !== 'escape' &&
|
|
112
|
-
((value.startsWith('{') && value.endsWith('}')) ||
|
|
114
|
+
if (this.config.mode !== 'escape' &&
|
|
115
|
+
((value.startsWith('{') && value.endsWith('}')) ||
|
|
113
116
|
(value.startsWith('[') && value.endsWith(']')))) {
|
|
114
117
|
try {
|
|
115
118
|
return JSON.parse(value);
|
|
@@ -120,13 +123,13 @@ function createSerializer(config = {}) {
|
|
|
120
123
|
// Fall through to unescaping if JSON parse fails
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
|
-
|
|
126
|
+
|
|
124
127
|
// For non-JSON or escape mode, try unescaping
|
|
125
128
|
const unescaped = unescapeCssValue(value);
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
// If unescaped looks like JSON (might have been double-escaped), try parsing it
|
|
128
131
|
if (this.config.mode !== 'escape' &&
|
|
129
|
-
((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
|
|
132
|
+
((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
|
|
130
133
|
(unescaped.startsWith('[') && unescaped.endsWith(']')))) {
|
|
131
134
|
try {
|
|
132
135
|
return JSON.parse(unescaped);
|
|
@@ -134,10 +137,70 @@ function createSerializer(config = {}) {
|
|
|
134
137
|
// Not valid JSON, return unescaped string
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
|
-
|
|
140
|
+
|
|
138
141
|
return unescaped;
|
|
139
142
|
},
|
|
140
|
-
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Serialize a value for data-* attribute
|
|
146
|
+
* @param {string} key - The state key
|
|
147
|
+
* @param {any} value - The value to serialize for attribute
|
|
148
|
+
* @returns {string} - Serialized attribute value
|
|
149
|
+
*/
|
|
150
|
+
serializeForAttribute(key, value) {
|
|
151
|
+
if (value === null || value === undefined) return null;
|
|
152
|
+
|
|
153
|
+
// For objects, use the standard serializer
|
|
154
|
+
if (typeof value === 'object') {
|
|
155
|
+
return this.serialize(key, value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For primitive values, use direct string conversion
|
|
159
|
+
return String(value);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Apply serialized state to HTML element attributes and properties
|
|
164
|
+
* @param {string} key - State key
|
|
165
|
+
* @param {any} value - Value to apply
|
|
166
|
+
* @param {HTMLElement} element - Target element (defaults to documentElement)
|
|
167
|
+
*/
|
|
168
|
+
applyToAttributes(key, value, element = document.documentElement) {
|
|
169
|
+
// Skip null/undefined values
|
|
170
|
+
if (value === null || value === undefined) {
|
|
171
|
+
element.removeAttribute(`data-${key}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle objects specially
|
|
176
|
+
if (typeof value === 'object') {
|
|
177
|
+
// Set the main attribute with serialized value
|
|
178
|
+
element.setAttribute(`data-${key}`, this.serialize(key, value));
|
|
179
|
+
|
|
180
|
+
// For non-array objects, set individual property attributes
|
|
181
|
+
if (!Array.isArray(value)) {
|
|
182
|
+
Object.entries(value).forEach(([propKey, propValue]) => {
|
|
183
|
+
const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
|
|
184
|
+
if (propValue !== null && propValue !== undefined) {
|
|
185
|
+
if (typeof propValue === 'object') {
|
|
186
|
+
element.setAttribute(
|
|
187
|
+
attributeKey,
|
|
188
|
+
this.serialize(`${key}.${propKey}`, propValue)
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
element.setAttribute(attributeKey, propValue);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
element.removeAttribute(attributeKey);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// For primitives, set directly
|
|
200
|
+
element.setAttribute(`data-${key}`, value);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
141
204
|
/**
|
|
142
205
|
* Utility method to determine if a value needs complex serialization
|
|
143
206
|
* @param {any} value - Value to check
|
|
@@ -146,7 +209,7 @@ function createSerializer(config = {}) {
|
|
|
146
209
|
needsComplexSerialization(value) {
|
|
147
210
|
return typeof value === 'object' && value !== null;
|
|
148
211
|
},
|
|
149
|
-
|
|
212
|
+
|
|
150
213
|
/**
|
|
151
214
|
* Set state with proper serialization for CSS variables
|
|
152
215
|
* @param {Object} uistate - UIstate instance
|
|
@@ -157,23 +220,23 @@ function createSerializer(config = {}) {
|
|
|
157
220
|
setStateWithCss(uistate, path, value) {
|
|
158
221
|
// Update UIstate
|
|
159
222
|
uistate.setState(path, value);
|
|
160
|
-
|
|
223
|
+
|
|
161
224
|
// Update CSS variable with properly serialized value
|
|
162
225
|
const cssPath = path.replace(/\./g, '-');
|
|
163
226
|
const serialized = this.serialize(path, value);
|
|
164
227
|
document.documentElement.style.setProperty(`--${cssPath}`, serialized);
|
|
165
|
-
|
|
228
|
+
|
|
166
229
|
// Update data attribute for root level state
|
|
167
230
|
const segments = path.split('.');
|
|
168
231
|
if (segments.length === 1) {
|
|
169
|
-
document.documentElement.dataset[path] = typeof value === 'object'
|
|
170
|
-
? JSON.stringify(value)
|
|
232
|
+
document.documentElement.dataset[path] = typeof value === 'object'
|
|
233
|
+
? JSON.stringify(value)
|
|
171
234
|
: value;
|
|
172
235
|
}
|
|
173
|
-
|
|
236
|
+
|
|
174
237
|
return value;
|
|
175
238
|
},
|
|
176
|
-
|
|
239
|
+
|
|
177
240
|
/**
|
|
178
241
|
* Get state with fallback to CSS variables
|
|
179
242
|
* @param {Object} uistate - UIstate instance
|
|
@@ -184,16 +247,16 @@ function createSerializer(config = {}) {
|
|
|
184
247
|
// First try UIstate
|
|
185
248
|
const value = uistate.getState(path);
|
|
186
249
|
if (value !== undefined) return value;
|
|
187
|
-
|
|
250
|
+
|
|
188
251
|
// If not found, try CSS variable
|
|
189
252
|
const cssPath = path.replace(/\./g, '-');
|
|
190
253
|
const cssValue = getComputedStyle(document.documentElement)
|
|
191
254
|
.getPropertyValue(`--${cssPath}`).trim();
|
|
192
|
-
|
|
255
|
+
|
|
193
256
|
return cssValue ? this.deserialize(path, cssValue) : undefined;
|
|
194
257
|
}
|
|
195
258
|
};
|
|
196
|
-
|
|
259
|
+
|
|
197
260
|
return serializer;
|
|
198
261
|
}
|
|
199
262
|
|