cavalion-vcl 1.1.90 → 1.1.91

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/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ### `2026/01/18` 1.1.91 — UI/UX utility upgrades: visibility, filtering, sorting, and hints
2
+
3
+ #### List sorting/selection fixes + date/numeric heuristics
4
+
5
+ - Defers row selection updates to the next tick after click dispatch to avoid selection timing issues.
6
+ - Broadens date detection to treat epoch-millis ranges as dates (in addition to `Date` instances and ISO `...Z` strings).
7
+ - Improves date formatting to accept numeric-string timestamps by coercing before constructing a `Date`.
8
+ - Adds automatic numeric-column detection (samples first 100 rows) when the caller doesn’t specify `numeric`.
9
+ - Coerces finite numeric cell values to floats during sort comparisons for consistent numeric ordering.
10
+ - Avoids a generated CSS class name collision by renaming attribute `"fa"` to `"fa_"` for list columns.
11
+
12
+
13
+ #### Changes
14
+
15
+ - Adds `Component.debounce(name, ms, fn)` as a shorthand over named timeouts.
16
+ - Extends component lookup to accept a predicate function and walk owners until it matches.
17
+ - Enhances `Control.whenVisible(...)` to accept an optional callback, reuse cached promises, and optionally reject on destroy (`rejectOnDestroy`); default destroy now resolves with `null`.
18
+ - Adds `data/Array` support for filtering out `Source.Pending` items, plus a new `filterPending` property and `setFilterPending(...)`.
19
+ - Reduces toast spam by silencing clipboard debug prints while keeping user feedback toasts.
20
+ - Improves the developer console helpers: adds `tap`, paste helper, and safer/clearer HTML rendering for selection metadata.
21
+ - Adds `Element.hint` (sets DOM `title`) and ensures hints are applied when attributes update.
22
+ - Adjusts `List` click handling to avoid selection timing issues by deferring selection to the next tick.
23
+ - Broadens date detection/formatting in `List` to handle epoch-millis (including numeric strings) in addition to `Date` and ISO Z strings.
24
+ - Improves `List` sorting by auto-detecting numeric columns and coercing finite numeric values before comparing.
25
+ - Adds `List.isNumericColumn(...)` heuristic (samples up to 100 rows) to drive numeric sorting defaults.
26
+ - Hardens `ListColumn.getAttributeClassName()` to avoid CSS-class collisions for attribute `"fa"` by renaming to `"fa_"`.
27
+ - Minor whitespace cleanup in `Application.js` (no functional change).
28
+
29
+
30
+
1
31
  ### `2025/10/27` 1.1.90 — List cell formatting & selection handling
2
32
 
3
33
  * Adds `_formatNumbers: true` and `_renderCellTitles: true` defaults in `List` prototype.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  * [Declarative Component Format.md](docs/:)
2
2
  * [Naming Conventions.md](docs/:)
3
3
  * ...
4
+ * [Application Events.md](docs/:)
4
5
 
5
6
  # cavalion-vcl
6
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cavalion-vcl",
3
- "version": "1.1.90",
3
+ "version": "1.1.91",
4
4
  "description": "Visual Component Library for vcl-comps",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,8 +8,6 @@ define(function(require) {
8
8
  var CssRules = require("./CssRules");
9
9
 
10
10
  var instances = [];
11
-
12
-
13
11
 
14
12
  return (Application = Application(require, {
15
13
  inherits: Component,
package/src/Component.js CHANGED
@@ -269,6 +269,9 @@ define(function (require) {
269
269
  }
270
270
  return this.setTimeout(name, f, 0, args);
271
271
  },
272
+ debounce(name, ms, f) {
273
+ return this.setTimeout(name, f, ms);
274
+ },
272
275
  setTimeout: function(name, f, ms, args) {
273
276
  /**
274
277
  * @param name {String} [optional] Used to identify the timeout. Successive calls will cancel a previous timeout with the same name.
@@ -820,6 +823,18 @@ define(function (require) {
820
823
  }
821
824
  return selector;
822
825
  }
826
+
827
+
828
+ if(typeof selector === "function") {
829
+ let c = this.getOwner();
830
+ while(c !== null) {
831
+ if(selector(c) == true) {
832
+ break;
833
+ }
834
+ c = c.getOwner();
835
+ }
836
+ return c;
837
+ }
823
838
 
824
839
  /*- Queries all components for the given selector and filters out
825
840
  those matches which are an owner of the calling component. The
package/src/Control.js CHANGED
@@ -1112,22 +1112,29 @@ define(function(require) {
1112
1112
  return r === true ? r : r === "always";
1113
1113
  },
1114
1114
  whenVisible: function(options) {
1115
+ let cb;
1116
+ if (typeof options === "function") {
1117
+ cb = options;
1118
+ options = {};
1119
+ }
1115
1120
  options = options || {};
1116
1121
 
1122
+ const rejectOnDestroy = options.rejectOnDestroy === true;
1123
+
1117
1124
  // Already visible → reuse a cached resolved promise (no new allocs).
1118
1125
  if (this.isShowing() === true) {
1119
- return this._wv_resolved || (this._wv_resolved = Promise.resolve(this));
1126
+ const p = this._wv_resolved || (this._wv_resolved = Promise.resolve(this));
1127
+ return cb ? p.then(() => cb(this), () => undefined) : p;
1120
1128
  }
1121
1129
 
1122
1130
  // Already waiting → return the same pending promise.
1123
- if (this._wv_pending) return this._wv_pending;
1131
+ if (this._wv_pending) {
1132
+ return cb ? this._wv_pending.then(v => (v && cb(v), v)) : this._wv_pending;
1133
+ }
1124
1134
 
1125
- // If a parent must become visible first, start our listeners now, but don't
1126
- // allocate a *second* promise for us — we still reuse _wv_pending below.
1127
1135
  if (this._parent && this._parent !== this &&
1128
- typeof this._parent.whenVisible === "function" &&
1129
- this._parent.isShowing() === false) {
1130
- // Fire and forget: when parent shows, we’ll re-check via resolveVisible().
1136
+ typeof this._parent.whenVisible === "function" &&
1137
+ this._parent.isShowing() === false) {
1131
1138
  this._parent.whenVisible(options).then(() => { this.update(); });
1132
1139
  }
1133
1140
 
@@ -1137,30 +1144,33 @@ define(function(require) {
1137
1144
  if (destroyLis !== undefined) { this.un(destroyLis); destroyLis = undefined; }
1138
1145
  };
1139
1146
 
1140
- // Create exactly one pending promise and cache it.
1141
1147
  this._wv_pending = new Promise((resolve, reject) => {
1142
1148
  const settle = (fn, v) => { cleanup(); this._wv_pending = null; return fn(v); };
1143
1149
 
1144
1150
  const resolveVisible = () => {
1145
1151
  if (this.isShowing() === true) {
1146
- // Cache a resolved promise for next time.
1147
1152
  this._wv_resolved = this._wv_resolved || Promise.resolve(this);
1148
1153
  return settle(resolve, this);
1149
1154
  }
1150
1155
  };
1151
1156
 
1152
- const rejectVisible = (err) => settle(reject, err);
1153
-
1154
1157
  showLis = this.on("show", resolveVisible);
1155
- destroyLis = this.on("destroy",
1156
- () => rejectVisible(new Error("Control destroyed before becoming visible")));
1157
1158
 
1158
- // Nudge layout/visibility and do a synchronous re-check.
1159
+ destroyLis = this.on("destroy", () => {
1160
+ if (rejectOnDestroy) {
1161
+ return settle(reject, new Error("Control destroyed before becoming visible"));
1162
+ }
1163
+ // Cancellation/no-op by default: resolves to null/undefined.
1164
+ return settle(resolve, null);
1165
+ });
1166
+
1159
1167
  this.update();
1160
1168
  resolveVisible();
1161
1169
  });
1162
1170
 
1163
- return this._wv_pending;
1171
+ return cb
1172
+ ? this._wv_pending.then(v => (v && cb(v), v))
1173
+ : this._wv_pending;
1164
1174
  },
1165
1175
  isControlVisible: function(control) {
1166
1176
  return this.hasState(ControlState.acceptChildNodes) && this.isVisible();
package/src/data/Array.js CHANGED
@@ -20,6 +20,8 @@ define(function(require) {
20
20
  _onFilterObject: null,
21
21
  _notifications: null,
22
22
 
23
+ _filterPending: false,
24
+
23
25
  _onUpdate: null,
24
26
  _onChange: null,
25
27
  _onBusyChanged: null,
@@ -121,6 +123,10 @@ define(function(require) {
121
123
  /** @overrides ../data/Source.prototype.getAttributeValue */
122
124
  this.assertArray(index);
123
125
  var obj = this.getObject(index || 0);
126
+ if(obj === null || typeof obj !== "object") {
127
+ return obj;
128
+ }
129
+
124
130
  if(name === ".") {
125
131
  return obj;
126
132
  }
@@ -189,7 +195,10 @@ define(function(require) {
189
195
  this._arr = this._array;
190
196
  for(var i = 0; i < this._array.length; ++i) {
191
197
  var obj = this._array[i];
192
- if(obj === Source.Pending || this.fire("onFilterObject", [obj, i, context]) !== true) {
198
+ if(
199
+ (obj === Source.Pending && !this._filterPending) ||
200
+ this.fire("onFilterObject", [obj, i, context]) !== true
201
+ ) {
193
202
  arr.push(obj);
194
203
  }
195
204
  }
@@ -250,6 +259,12 @@ define(function(require) {
250
259
  this.notify(SourceEvent.layoutChanged);
251
260
  }
252
261
  },
262
+ setFilterPending: function(value) {
263
+ if(this._filterPending !== value) {
264
+ this._filterPending = value;
265
+ this.updateFilter();
266
+ }
267
+ },
253
268
  arrayChanged: function() {
254
269
  this.updateFilter(false);
255
270
  //this.notify(SourceEvent.layoutChanged);
@@ -357,6 +372,10 @@ define(function(require) {
357
372
  "array": {
358
373
  type: Type.ARRAY,
359
374
  set: Function
375
+ },
376
+ "filterPending": {
377
+ type: Type.BOOLEAN,
378
+ set: Function
360
379
  },
361
380
  "onActiveChanged": {
362
381
  type: Type.EVENT
@@ -16,10 +16,10 @@ const Clipboard = req("util/Clipboard");
16
16
  };
17
17
 
18
18
  Clipboard.onPaste.addListener(e => {
19
- this.print("onPaste", e);
19
+ // this.print("onPaste", e);
20
20
  this.toast(js.sf("Pasted %d bytes...", e.length ))});
21
21
  Clipboard.onCopy.addListener(e => {
22
- this.print("onCopy", e);
22
+ // this.print("onCopy", e);
23
23
  if(typeof e === "string" && e.length > 150) {
24
24
  this.toast(js.sf("Copied %d bytes", e.length ));
25
25
  } else {
@@ -30,14 +30,6 @@ const default_zoom = Browser.win ? "zoom-109" : "zoom-112";
30
30
 
31
31
  return this.inherited(arguments);
32
32
  },
33
- onDispatchChildEvent: function(component, name, evt, f, args) {
34
- if(name === "touchstart") {
35
- if(!Fullscreen.hasRequested()) {
36
- Fullscreen.request(document.documentElement);
37
- }
38
- }
39
- return this.inherited(arguments);
40
- },
41
33
  onGetState: function() {
42
34
  var scope = this.getScope();
43
35
  var form = scope.client._form;
@@ -64,6 +56,8 @@ const default_zoom = Browser.win ? "zoom-109" : "zoom-112";
64
56
  'font-size': font_size,
65
57
  'letter-spacing': letter_spacing,
66
58
 
59
+ '.right': "float: right;",
60
+
67
61
  '.{Button}': {
68
62
  'font-size': font_size,
69
63
  'font-family': font_family,
@@ -36,14 +36,13 @@ const deselect = () => {
36
36
  const H = (uri, vars, opts) => B.i(["Hover<>", { vars: js.mi({ uri: uri }, vars)}], opts);
37
37
  H.i = (obj) => H("devtools/Alphaview.csv", { sel: [obj] });
38
38
 
39
+ const tap = fn => x => (fn(x), x);
39
40
  const cc = (text) => Clipboard.copy(text);
41
+ const cp = (cb) => Clipboard.paste(cb);
40
42
  const cl = console.log;
41
43
  const facts = (comp) => Component.getFactories(comp);
42
44
 
43
- window.B = B; window.H = H;
44
- window.facts = facts;
45
- window.cc = cc;
46
- window.cl = cl;
45
+ js.mi(window, { B, H, facts, cc, cp, cl, tap });
47
46
 
48
47
  [["ui/Form"], {
49
48
  activeControl: "console",
@@ -79,16 +78,18 @@ window.cl = cl;
79
78
 
80
79
  if (value !== null) {
81
80
  // `#CVLN-20200904-3`
82
- content.push(js.sf("%s%s%s",
81
+ content.push(js.sf("%H%H%H",
83
82
  value.isRootComponent() ? ":root" : "",
84
83
  value.isSelected && value.isSelected() ? ":selected" : "",
85
84
  value.isEnabled && value.isEnabled() ? "" : ":disabled"));
86
85
 
87
86
  if(value['@factory']) {
88
- content.push(js.n(value['@factory']).split("#").slice(0, -1).join("!"));
87
+ content.push(js.sf("<small>%H</small>", js.n(value['@factory']).split("#").slice(0, -1).join("!")));
88
+ } else {
89
+ content.push("<i>no-factory</i>");
89
90
  }
90
91
 
91
- content.push(js.sf("[%s]", value));
92
+ content.push(js.sf("<b>[%H]</b>", value));
92
93
 
93
94
  var props = [], hashAndNameOrUri = (c) => [c.hashCode(), c._name ? "#" + c._name : " " + c._uri].filter(s => s !== "").join("");
94
95
  if(value.up()) {
@@ -115,10 +116,14 @@ window.cl = cl;
115
116
  consoles.forEach(c => c.getNode("input").value = js.sf("#%d", value.hashCode()));
116
117
  consoles.forEach(c => c.focus());
117
118
  }
119
+
120
+ content.splice(0, 0, js.sf("<b>%H#%s</b>", value._name, value.hashCode()));
118
121
  }
119
- scope.sizer_selection.setContent(String.format("%H", content.join(" ")));
120
-
122
+
123
+ scope.sizer_selection.setContent(content.join(" "));
124
+
121
125
  if(value !== null) {
126
+
122
127
  if(!content[0]) content.shift();
123
128
  // content.pop();
124
129
  app.toast({ title: js.sf("%n", value), content: " " || js.sf("<ul style='padding:0;padding-left:8px;'><li>%s</li></ul>", content.join("</li><li>")), classes: "glassy fade"});
package/src/ui/Element.js CHANGED
@@ -93,6 +93,9 @@ define(function(require) {
93
93
  for(var k in value) {
94
94
  this._node.setAttribute(k, value[k]);
95
95
  }
96
+ if(this._hint) {
97
+ this._node.title = this._hint;
98
+ }
96
99
  return this.inherited(arguments);
97
100
  },
98
101
 
@@ -106,6 +109,15 @@ define(function(require) {
106
109
  this.recreateNode();
107
110
  }
108
111
  },
112
+ setHint: function(value) {
113
+ if(this._hint !== value) {
114
+ this._hint = value;
115
+
116
+ if(this._node) {
117
+ this._node.title = value;
118
+ }
119
+ }
120
+ },
109
121
  sourceNotifyEvent: function(event, data) {
110
122
  switch(event) {
111
123
 
@@ -149,6 +161,10 @@ define(function(require) {
149
161
  stored: false,
150
162
  type: Type.OBJECT,
151
163
  set: Function
164
+ },
165
+ "hint": {
166
+ type: Type.STRING,
167
+ set: Function
152
168
  }
153
169
  },
154
170
  statics: {
package/src/ui/List.js CHANGED
@@ -349,8 +349,8 @@ workaroundColumnAlignment(this);
349
349
  selection = [component._rowIndex];
350
350
  evt.preventDefault();
351
351
  }
352
- this.setSelection(selection);
353
352
  this.dispatch("click", evt);
353
+ this.nextTick(() => this.setSelection(selection));
354
354
  }
355
355
  }
356
356
  return this.inherited(arguments);
@@ -531,11 +531,18 @@ workaroundColumnAlignment(this);
531
531
  workaroundColumnAlignment(this);
532
532
  },
533
533
  isDate: function(value) {
534
- return (value instanceof Date) || (typeof value === "string" &&
535
- value.length === 24 && value.endsWith("Z"));
534
+ return (value instanceof Date) ||
535
+ (value > 946706400000 && value < 1893477600000) ||
536
+ (typeof value === "string" && value.length === 24 && value.endsWith("Z"));
536
537
  },
537
538
  formatDate: function(value, opts) {
538
- if(!(value instanceof Date)) value = new Date(value);
539
+ if(!(value instanceof Date)) {
540
+ if(typeof value === "string" && value.match(/^\d+$/)) {
541
+ value = parseInt(value, 10);
542
+ }
543
+ value = new Date(value);
544
+ }
545
+
539
546
  if(opts && opts.utc) {
540
547
  return js.sf("%d/%02d/%02d %02d:%02d", value.getUTCFullYear(), value.getUTCMonth() + 1,
541
548
  value.getUTCDate(), value.getUTCHours(), value.getUTCMinutes());
@@ -955,6 +962,10 @@ workaroundColumnAlignment(this);
955
962
  } else if(typeof column === "number") {
956
963
  column = this.getColumn(column);
957
964
  }
965
+
966
+ if(numeric === undefined) {
967
+ numeric = this.isNumericColumn(column);
968
+ }
958
969
 
959
970
  const sv = column.get("onSortValues") || Array.sortValues;
960
971
  this._source.sort((i1, i2) => {
@@ -964,6 +975,11 @@ workaroundColumnAlignment(this);
964
975
  i1 = this.valueByColumnAndRow(column, row1);
965
976
  i2 = this.valueByColumnAndRow(column, row2);
966
977
 
978
+ if(numeric) {
979
+ i1 = isFinite(i1) ? parseFloat(i1) : i1;
980
+ i2 = isFinite(i2) ? parseFloat(i2) : i2;
981
+ }
982
+
967
983
  return dir * sv(i1, i2);
968
984
  });
969
985
  // (i1, i2) => {
@@ -994,6 +1010,13 @@ workaroundColumnAlignment(this);
994
1010
  // });
995
1011
  },
996
1012
 
1013
+ isNumericColumn(column) {
1014
+ return this._source._array
1015
+ .slice(0, 100)
1016
+ .every((o, i) =>
1017
+ isFinite(this.valueByColumnAndRow(column, i)));
1018
+ },
1019
+
997
1020
  hasSelection: function() {
998
1021
  return this._selection.length > 0;
999
1022
  },
@@ -281,7 +281,13 @@ define(function(require) {
281
281
  }
282
282
  },
283
283
  getAttributeClassName: function() {
284
- return Array.from(this._attribute).map(char => /[\w-]/.test(char) ? char : '_').join('');
284
+ let r = Array.from(this._attribute).map(char => /[\w-]/.test(char) ? char : '_').join('');
285
+
286
+ if(r === "fa") {
287
+ r += "_";
288
+ }
289
+
290
+ return r;
285
291
 
286
292
  // return this._attribute.
287
293
  // replace(/\#/g, "-").