web-manager 4.0.19 → 4.0.21
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/README.md +65 -0
- package/dist/modules/bindings.js +89 -65
- package/firebase-debug.log +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -314,6 +314,71 @@ const context = Manager.bindings().getContext();
|
|
|
314
314
|
Manager.bindings().clear();
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
+
#### Attribute Actions
|
|
318
|
+
```html
|
|
319
|
+
<!-- Set an attribute value -->
|
|
320
|
+
<img data-wm-bind="@attr src auth.user.photoURL" />
|
|
321
|
+
<a data-wm-bind="@attr href settings.profileUrl">Profile</a>
|
|
322
|
+
|
|
323
|
+
<!-- Multiple attributes on same element -->
|
|
324
|
+
<img data-wm-bind="@attr src auth.user.photoURL, @attr alt auth.user.displayName" />
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Multiple Actions
|
|
328
|
+
You can combine multiple actions on a single element by separating them with commas:
|
|
329
|
+
|
|
330
|
+
```html
|
|
331
|
+
<!-- Set text AND show/hide -->
|
|
332
|
+
<div data-wm-bind="@show auth.user, @text auth.user.displayName"></div>
|
|
333
|
+
|
|
334
|
+
<!-- Set text AND multiple attributes -->
|
|
335
|
+
<img data-wm-bind="@text auth.user.displayName, @attr src auth.user.photoURL, @attr title auth.user.email" />
|
|
336
|
+
|
|
337
|
+
<!-- Multiple attributes -->
|
|
338
|
+
<a data-wm-bind="@attr href settings.url, @attr target settings.target, @text settings.linkText"></a>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### Skeleton Loaders
|
|
342
|
+
Add the `wm-binding-skeleton` class to show a loading skeleton while data is being bound:
|
|
343
|
+
|
|
344
|
+
```html
|
|
345
|
+
<!-- Shows a shimmer loading effect until data is bound -->
|
|
346
|
+
<span data-wm-bind="auth.user.displayName" class="wm-binding-skeleton"></span>
|
|
347
|
+
|
|
348
|
+
<!-- Profile card with multiple skeleton loaders -->
|
|
349
|
+
<div class="profile-card">
|
|
350
|
+
<img data-wm-bind="@attr src auth.user.photoURL" class="wm-binding-skeleton">
|
|
351
|
+
<h3 data-wm-bind="auth.user.displayName" class="wm-binding-skeleton"></h3>
|
|
352
|
+
<p data-wm-bind="auth.user.email" class="wm-binding-skeleton"></p>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<!-- Elements without the class won't show skeleton loaders -->
|
|
356
|
+
<span data-wm-bind="settings.theme"></span>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The skeleton loader automatically:
|
|
360
|
+
- Displays a shimmer animation while the element is unbound
|
|
361
|
+
- Hides the text content during loading
|
|
362
|
+
- Prevents interaction until data is loaded
|
|
363
|
+
- Fades in smoothly when data arrives
|
|
364
|
+
- Adapts to dark themes
|
|
365
|
+
- Respects `prefers-reduced-motion` accessibility settings
|
|
366
|
+
|
|
367
|
+
#### Visual Feedback
|
|
368
|
+
When an element is bound, Web Manager automatically adds the `wm-bound` class to it. You can use this class for styling or debugging:
|
|
369
|
+
|
|
370
|
+
```css
|
|
371
|
+
/* Add a subtle indicator for bound elements in development */
|
|
372
|
+
.wm-bound {
|
|
373
|
+
outline: 1px dashed rgba(0, 123, 255, 0.3);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Custom styling after binding */
|
|
377
|
+
.wm-bound {
|
|
378
|
+
/* Element has been successfully bound */
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
317
382
|
#### Supported Actions
|
|
318
383
|
- **`@text`** (default): Sets the text content of the element
|
|
319
384
|
- **`@show`**: Shows the element when condition is true
|
package/dist/modules/bindings.js
CHANGED
|
@@ -35,82 +35,106 @@ class Bindings {
|
|
|
35
35
|
bindElements.forEach(element => {
|
|
36
36
|
const bindValue = element.getAttribute('data-wm-bind');
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Split by comma to support multiple actions
|
|
39
|
+
const bindings = this._parseBindings(bindValue);
|
|
40
|
+
|
|
41
|
+
// Execute each action
|
|
42
|
+
bindings.forEach(({ action, expression }) => {
|
|
43
|
+
this._executeAction(element, action, expression, context);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Add bound class to indicate element has been processed
|
|
47
|
+
if (!element.classList.contains('wm-bound')) {
|
|
48
|
+
element.classList.add('wm-bound');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse binding string into separate actions
|
|
54
|
+
_parseBindings(bindValue) {
|
|
55
|
+
const bindings = [];
|
|
56
|
+
|
|
57
|
+
// Split by comma, but be smart about it
|
|
58
|
+
// We need to handle cases where commas might be inside expressions
|
|
59
|
+
const parts = bindValue.split(',').map(p => p.trim());
|
|
60
|
+
|
|
61
|
+
parts.forEach(part => {
|
|
39
62
|
let action = '@text'; // Default action
|
|
40
|
-
let expression =
|
|
63
|
+
let expression = part;
|
|
41
64
|
|
|
42
65
|
// Check if it starts with an action keyword
|
|
43
|
-
if (
|
|
44
|
-
const spaceIndex =
|
|
66
|
+
if (part.startsWith('@')) {
|
|
67
|
+
const spaceIndex = part.indexOf(' ');
|
|
45
68
|
if (spaceIndex > -1) {
|
|
46
|
-
action =
|
|
47
|
-
expression =
|
|
69
|
+
action = part.slice(0, spaceIndex);
|
|
70
|
+
expression = part.slice(spaceIndex + 1).trim();
|
|
48
71
|
} else {
|
|
49
72
|
// No space means it's just an action with no expression (like @hide)
|
|
50
|
-
action =
|
|
73
|
+
action = part;
|
|
51
74
|
expression = '';
|
|
52
75
|
}
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
switch (action) {
|
|
57
|
-
case '@show':
|
|
58
|
-
// Show element if condition is true (or always if no condition)
|
|
59
|
-
const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
|
|
60
|
-
if (shouldShow) {
|
|
61
|
-
element.removeAttribute('hidden');
|
|
62
|
-
} else {
|
|
63
|
-
element.setAttribute('hidden', '');
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
|
|
67
|
-
case '@hide':
|
|
68
|
-
// Hide element if condition is true (or always if no condition)
|
|
69
|
-
const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
|
|
70
|
-
if (shouldHide) {
|
|
71
|
-
element.setAttribute('hidden', '');
|
|
72
|
-
} else {
|
|
73
|
-
element.removeAttribute('hidden');
|
|
74
|
-
}
|
|
75
|
-
break;
|
|
76
|
-
|
|
77
|
-
case '@attr':
|
|
78
|
-
// Set attribute value
|
|
79
|
-
// Format: @attr attributeName expression
|
|
80
|
-
const attrParts = expression.split(' ');
|
|
81
|
-
const attrName = attrParts[0];
|
|
82
|
-
const attrExpression = attrParts.slice(1).join(' ');
|
|
83
|
-
const attrValue = this._resolvePath(context, attrExpression) || '';
|
|
84
|
-
|
|
85
|
-
if (attrValue) {
|
|
86
|
-
element.setAttribute(attrName, attrValue);
|
|
87
|
-
} else {
|
|
88
|
-
element.removeAttribute(attrName);
|
|
89
|
-
}
|
|
90
|
-
break;
|
|
91
|
-
|
|
92
|
-
case '@text':
|
|
93
|
-
default:
|
|
94
|
-
// Set text content (default behavior)
|
|
95
|
-
const value = this._resolvePath(context, expression) || '';
|
|
96
|
-
|
|
97
|
-
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
98
|
-
element.value = value;
|
|
99
|
-
} else {
|
|
100
|
-
element.textContent = value;
|
|
101
|
-
}
|
|
102
|
-
break;
|
|
103
|
-
|
|
104
|
-
// Future actions can be added here:
|
|
105
|
-
// case '@class':
|
|
106
|
-
// case '@style':
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Add bound class to indicate element has been processed
|
|
110
|
-
if (!element.classList.contains('wm-bound')) {
|
|
111
|
-
element.classList.add('wm-bound');
|
|
112
|
-
}
|
|
78
|
+
bindings.push({ action, expression });
|
|
113
79
|
});
|
|
80
|
+
|
|
81
|
+
return bindings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute a single action on an element
|
|
85
|
+
_executeAction(element, action, expression, context) {
|
|
86
|
+
switch (action) {
|
|
87
|
+
case '@show':
|
|
88
|
+
// Show element if condition is true (or always if no condition)
|
|
89
|
+
const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
|
|
90
|
+
if (shouldShow) {
|
|
91
|
+
element.removeAttribute('hidden');
|
|
92
|
+
} else {
|
|
93
|
+
element.setAttribute('hidden', '');
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case '@hide':
|
|
98
|
+
// Hide element if condition is true (or always if no condition)
|
|
99
|
+
const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
|
|
100
|
+
if (shouldHide) {
|
|
101
|
+
element.setAttribute('hidden', '');
|
|
102
|
+
} else {
|
|
103
|
+
element.removeAttribute('hidden');
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case '@attr':
|
|
108
|
+
// Set attribute value
|
|
109
|
+
// Format: @attr attributeName expression
|
|
110
|
+
const attrParts = expression.split(' ');
|
|
111
|
+
const attrName = attrParts[0];
|
|
112
|
+
const attrExpression = attrParts.slice(1).join(' ');
|
|
113
|
+
const attrValue = this._resolvePath(context, attrExpression) || '';
|
|
114
|
+
|
|
115
|
+
if (attrValue) {
|
|
116
|
+
element.setAttribute(attrName, attrValue);
|
|
117
|
+
} else {
|
|
118
|
+
element.removeAttribute(attrName);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case '@text':
|
|
123
|
+
default:
|
|
124
|
+
// Set text content (default behavior)
|
|
125
|
+
const value = this._resolvePath(context, expression) || '';
|
|
126
|
+
|
|
127
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
128
|
+
element.value = value;
|
|
129
|
+
} else {
|
|
130
|
+
element.textContent = value;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
// Future actions can be added here:
|
|
135
|
+
// case '@class':
|
|
136
|
+
// case '@style':
|
|
137
|
+
}
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
// Resolve nested object path
|
package/firebase-debug.log
CHANGED
|
@@ -6,3 +6,11 @@
|
|
|
6
6
|
[debug] [2025-10-20T10:38:46.422Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
7
7
|
[debug] [2025-10-20T10:38:46.422Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
8
8
|
[debug] [2025-10-20T10:38:46.422Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
9
|
+
[debug] [2025-10-20T11:33:46.238Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
10
|
+
[debug] [2025-10-20T11:33:46.237Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
11
|
+
[debug] [2025-10-20T11:33:46.239Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
12
|
+
[debug] [2025-10-20T11:33:46.240Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
13
|
+
[debug] [2025-10-20T11:33:46.240Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
14
|
+
[debug] [2025-10-20T11:33:46.239Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
15
|
+
[debug] [2025-10-20T11:33:46.240Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
16
|
+
[debug] [2025-10-20T11:33:46.240Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
package/package.json
CHANGED