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 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
@@ -35,82 +35,106 @@ class Bindings {
35
35
  bindElements.forEach(element => {
36
36
  const bindValue = element.getAttribute('data-wm-bind');
37
37
 
38
- // Parse action and expression
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 = bindValue;
63
+ let expression = part;
41
64
 
42
65
  // Check if it starts with an action keyword
43
- if (bindValue.startsWith('@')) {
44
- const spaceIndex = bindValue.indexOf(' ');
66
+ if (part.startsWith('@')) {
67
+ const spaceIndex = part.indexOf(' ');
45
68
  if (spaceIndex > -1) {
46
- action = bindValue.slice(0, spaceIndex);
47
- expression = bindValue.slice(spaceIndex + 1);
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 = bindValue;
73
+ action = part;
51
74
  expression = '';
52
75
  }
53
76
  }
54
77
 
55
- // Execute the action
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-manager",
3
- "version": "4.0.19",
3
+ "version": "4.0.21",
4
4
  "description": "Easily access important variables such as the query string, current domain, and current page in a single object.",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.js",