neo.mjs 6.10.16 → 6.11.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.
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.10.16'
23
+ * @member {String} version='6.11.0'
24
24
  */
25
- version: '6.10.16'
25
+ version: '6.11.0'
26
26
  }
27
27
 
28
28
  /**
@@ -3,5 +3,5 @@
3
3
  "basePath" : "../../",
4
4
  "environment" : "development",
5
5
  "mainPath" : "./Main.mjs",
6
- "mainThreadAddons": ["DragDrop", "LocalStorage", "Stylesheet"]
6
+ "mainThreadAddons": ["DragDrop", "LocalStorage", "Navigator", "Stylesheet"]
7
7
  }
@@ -16,6 +16,10 @@ class SideNavList extends List {
16
16
  * @protected
17
17
  */
18
18
  baseCls: ['form-side-nav-list', 'neo-list'],
19
+ /**
20
+ * @member {Boolean} itemsFocusable=true
21
+ */
22
+ itemsFocusable: true,
19
23
  /**
20
24
  * @member {Boolean} useHeaders=true
21
25
  */
@@ -20,24 +20,26 @@ class Page6 extends FormPageContainer {
20
20
  * @member {Object} itemDefaults
21
21
  */
22
22
  itemDefaults: {
23
- module: TextArea
23
+ module : TextArea,
24
+ autoGrow: true
24
25
  },
25
26
  /**
26
27
  * @member {Object[]} items
27
28
  */
28
29
  items: [{
29
- height : 200,
30
30
  labelText: 'Page 6 Field 1',
31
+ minHeight: 150,
31
32
  name : 'field1',
32
33
  required : true,
33
34
  value : 'Lorem ipsum'
34
35
  }, {
35
- height : 300,
36
36
  labelText: 'Page 6 Field 2',
37
+ minHeight: 150,
37
38
  name : 'field2'
38
39
  }, {
39
40
  labelText: 'Page 6 Field 3',
40
- name : 'field3'
41
+ name : 'field3',
42
+ readOnly : true
41
43
  }]
42
44
  }
43
45
  }
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.10.16'
23
+ * @member {String} version='6.11.0'
24
24
  */
25
- version: '6.10.16'
25
+ version: '6.11.0'
26
26
  }
27
27
 
28
28
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "6.10.16",
3
+ "version": "6.11.0",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "inquirer": "^9.2.14",
56
56
  "neo-jsdoc": "1.0.1",
57
57
  "neo-jsdoc-x": "1.0.5",
58
- "postcss": "^8.4.33",
58
+ "postcss": "^8.4.35",
59
59
  "sass": "^1.70.0",
60
60
  "siesta-lite": "5.5.2",
61
61
  "showdown": "^2.1.0",
@@ -1,9 +1,14 @@
1
1
  .neo-tree-list {
2
- border : none;
3
- color : var(--tree-list-color);
4
- display : flex;
5
- overflow: hidden;
6
- position: relative;
2
+ border : none;
3
+ color : var(--tree-list-color);
4
+ display : flex;
5
+ flex-direction : column;
6
+ overflow : hidden;
7
+ position : relative;
8
+
9
+ // Allow item's scrollIntoView upwards to avoid
10
+ // being hidden below a sticky item stuck at the top.
11
+ scroll-padding-block-start : 3em;
7
12
 
8
13
  .neo-list {
9
14
  overflow : visible;
@@ -236,12 +236,12 @@ const DefaultConfig = {
236
236
  useVdomWorker: true,
237
237
  /**
238
238
  * buildScripts/injectPackageVersion.mjs will update this value
239
- * @default '6.10.16'
239
+ * @default '6.11.0'
240
240
  * @memberOf! module:Neo
241
241
  * @name config.version
242
242
  * @type String
243
243
  */
244
- version: '6.10.16'
244
+ version: '6.11.0'
245
245
  };
246
246
 
247
247
  Object.assign(DefaultConfig, {
package/src/Neo.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import DefaultConfig from './DefaultConfig.mjs';
2
2
 
3
- const configSymbol = Symbol.for('configSymbol'),
4
- getSetCache = Symbol('getSetCache'),
5
- typeDetector = {
3
+ const
4
+ camelRegex = /-./g,
5
+ configSymbol = Symbol.for('configSymbol'),
6
+ getSetCache = Symbol('getSetCache'),
7
+ typeDetector = {
6
8
  function: (item) => {
7
9
  if (item.prototype?.constructor.isClass) {
8
10
  return 'NeoClass'
@@ -60,17 +62,19 @@ Neo = globalThis.Neo = Object.assign({
60
62
  * @tutorial 02_ClassSystem
61
63
  */
62
64
  applyClassConfig(cls) {
63
- let baseCfg = null,
64
- ntypeMap = Neo.ntypeMap,
65
- proto = cls.prototype || cls,
66
- protos = [],
65
+ let baseCfg = null,
66
+ ntypeChain = [],
67
+ ntypeMap = Neo.ntypeMap,
68
+ proto = cls.prototype || cls,
69
+ protos = [],
67
70
  cfg, config, ctor, ntype;
68
71
 
69
72
  while (proto.__proto__) {
70
73
  ctor = proto.constructor;
71
74
 
72
75
  if (Object.hasOwn(ctor, 'classConfigApplied')) {
73
- baseCfg = Neo.clone(ctor.config, true);
76
+ baseCfg = Neo.clone(ctor.config, true);
77
+ ntypeChain = [...ctor.ntypeChain];
74
78
  break
75
79
  }
76
80
 
@@ -113,6 +117,8 @@ Neo = globalThis.Neo = Object.assign({
113
117
  if (Object.hasOwn(cfg, 'ntype')) {
114
118
  ntype = cfg.ntype;
115
119
 
120
+ ntypeChain.unshift(ntype);
121
+
116
122
  // Running the docs app inside a workspace can pull in the same classes from different roots,
117
123
  // so we want to check for different class names as well
118
124
  if (Object.hasOwn(ntypeMap, ntype) && cfg.className !== ntypeMap[ntype]) {
@@ -136,7 +142,7 @@ Neo = globalThis.Neo = Object.assign({
136
142
  applyMixins(ctor, mixins);
137
143
 
138
144
  if (Neo.ns('Neo.core.Observable', false, ctor.prototype.mixins)) {
139
- ctor.observable = true;
145
+ ctor.observable = true
140
146
  }
141
147
  }
142
148
 
@@ -148,7 +154,8 @@ Neo = globalThis.Neo = Object.assign({
148
154
  Object.assign(ctor, {
149
155
  classConfigApplied: true,
150
156
  config : Neo.clone(config, true),
151
- isClass : true
157
+ isClass : true,
158
+ ntypeChain
152
159
  });
153
160
 
154
161
  !config.singleton && this.applyToGlobalNs(cls)
@@ -156,6 +163,10 @@ Neo = globalThis.Neo = Object.assign({
156
163
 
157
164
  proto = cls.prototype || cls;
158
165
 
166
+ ntypeChain.forEach(ntype => {
167
+ proto[`is${Neo.capitalize(Neo.camel(ntype))}`] = true
168
+ });
169
+
159
170
  if (proto.singleton) {
160
171
  cls = Neo.create(cls);
161
172
  Neo.applyToGlobalNs(cls)
@@ -234,6 +245,26 @@ Neo = globalThis.Neo = Object.assign({
234
245
  return target
235
246
  },
236
247
 
248
+ /**
249
+ * Converts kebab-case strings into camel-case
250
+ * @memberOf module:Neo
251
+ * @param {String} value The target object
252
+ * @returns {String}
253
+ */
254
+ camel(value) {
255
+ return value.replace(camelRegex, match => match[1].toUpperCase())
256
+ },
257
+
258
+ /**
259
+ * Makes the first character of a string uppercase
260
+ * @memberOf module:Neo
261
+ * @param {String} value
262
+ * @returns {Boolean|String} Returns false for non string inputs
263
+ */
264
+ capitalize(value) {
265
+ return value[0].toUpperCase() + value.slice(1)
266
+ },
267
+
237
268
  /**
238
269
  * @memberOf module:Neo
239
270
  * @param {Object|Array|*} obj
package/src/core/Base.mjs CHANGED
@@ -327,6 +327,15 @@ class Base {
327
327
  return this.constructor[key]
328
328
  }
329
329
 
330
+ /**
331
+ * Check if a given ntype exists inside the proto chain, including the top level class
332
+ * @param {String} ntype
333
+ * @returns {Boolean}
334
+ */
335
+ hasNtype(ntype) {
336
+ return this.constructor.ntypeChain.includes(ntype)
337
+ }
338
+
330
339
  /**
331
340
  * Gets triggered after onConstructed() is done
332
341
  * @see {@link Neo.core.Base#onConstructed onConstructed}
package/src/core/Util.mjs CHANGED
@@ -36,15 +36,6 @@ class Util extends Base {
36
36
  });
37
37
  }
38
38
 
39
- /**
40
- * Makes the first character of a string uppercase
41
- * @param {String} value
42
- * @returns {Boolean|String} Returns false for non string inputs
43
- */
44
- static capitalize(value) {
45
- return value[0].toUpperCase() + value.slice(1)
46
- }
47
-
48
39
  /**
49
40
  * Transforms a styles string into a styles object using camelcase syntax
50
41
  * @param {String} string The styles string to parse
@@ -225,7 +216,6 @@ Neo.applyFromNs(Neo, Util, {
225
216
  bindMethods : 'bindMethods',
226
217
  createStyleObject: 'createStyleObject',
227
218
  createStyles : 'createStyles',
228
- capitalize : 'capitalize',
229
219
  decamel : 'decamel',
230
220
  isArray : 'isArray',
231
221
  isBoolean : 'isBoolean',
@@ -79,12 +79,20 @@ class TextArea extends Text {
79
79
  wrap_: null
80
80
  }
81
81
 
82
- afterSetAutoGrow(autoGrow) {
83
- autoGrow && this.syncAutoGrowMonitor();
82
+ /**
83
+ * Triggered after the autoGrow config got changed
84
+ * @param {Boolean} value
85
+ * @param {Boolean} oldValue
86
+ * @protected
87
+ */
88
+ afterSetAutoGrow(value, oldValue) {
89
+ let me = this;
90
+
91
+ value && me.syncAutoGrowMonitor();
84
92
 
85
93
  // Restore any configured height if autoGrow turned off
86
- if (!autoGrow) {
87
- this.afterSetHeight(this._height);
94
+ if (!value) {
95
+ me.afterSetHeight(me._height);
88
96
  }
89
97
  }
90
98
 
@@ -150,13 +158,24 @@ class TextArea extends Text {
150
158
  * @protected
151
159
  */
152
160
  afterSetValue(value, oldValue) {
153
- let inputEl = this.getInputEl();
161
+ let me = this,
162
+ inputEl = me.getInputEl();
154
163
 
155
164
  if (inputEl) {
156
165
  inputEl.html = StringUtil.escapeHtml(value);
157
166
  }
158
167
 
159
168
  super.afterSetValue(value, oldValue);
169
+
170
+ if (me.autoGrow && me.mounted && me.readOnly) {
171
+ setTimeout(() => {
172
+ Neo.main.DomAccess.monitorAutoGrowHandler({
173
+ appName : me.appName,
174
+ id : inputEl.id,
175
+ windowId: me.windowId
176
+ })
177
+ }, 50)
178
+ }
160
179
  }
161
180
 
162
181
  /**
@@ -180,14 +199,19 @@ class TextArea extends Text {
180
199
  return this.beforeSetEnumValue(value, oldValue, 'wrap', 'wrapValues');
181
200
  }
182
201
 
202
+ /**
203
+ *
204
+ */
183
205
  async syncAutoGrowMonitor() {
184
- if (this.mounted && this.autoGrow) {
185
- // Delegate monitoring of sizes to the VDOM thread.
206
+ let me = this;
207
+
208
+ if (me.mounted && me.autoGrow) {
209
+ // Delegate monitoring of sizes to the main thread.
186
210
  Neo.main.DomAccess.monitorAutoGrow({
187
- appName : this.appName,
188
- id : this.getInputElId(),
189
- autoGrow : this.autoGrow
190
- });
211
+ appName : me.appName,
212
+ id : me.getInputElId(),
213
+ autoGrow : me.autoGrow
214
+ })
191
215
  }
192
216
  }
193
217
  }
package/src/list/Base.mjs CHANGED
@@ -411,14 +411,13 @@ class Base extends Component {
411
411
  itemContent = me.createItemContent(record, index),
412
412
  itemId = me.getItemId(record[me.getKeyProperty()]),
413
413
  selectionModel = me.selectionModel,
414
+ isSelected = !me.disableSelection && selectionModel?.isSelected(itemId),
414
415
  item;
415
416
 
416
417
  isHeader && cls.push('neo-list-header');
417
418
 
418
- if (!me.disableSelection && selectionModel) {
419
- if (selectionModel.isSelected(itemId)) {
420
- cls.push(selectionModel.selectedCls)
421
- }
419
+ if (isSelected){
420
+ cls.push(selectionModel.selectedCls)
422
421
  }
423
422
 
424
423
  if (record.cls) {
@@ -432,7 +431,7 @@ class Base extends Component {
432
431
  item = {
433
432
  id : itemId,
434
433
  tag : isHeader ? 'dt' : me.itemTagName,
435
- 'aria-selected' : false,
434
+ 'aria-selected' : isSelected,
436
435
  cls
437
436
  };
438
437
 
@@ -88,6 +88,7 @@ class DomAccess extends Base {
88
88
  'getScrollingDimensions',
89
89
  'measure',
90
90
  'monitorAutoGrow',
91
+ 'monitorAutoGrowHandler',
91
92
  'navigate',
92
93
  'navigateTo',
93
94
  'scrollBy',
@@ -637,8 +638,15 @@ class DomAccess extends Base {
637
638
  })
638
639
  }
639
640
 
640
- monitorAutoGrowHandler({ target }) {
641
+ /**
642
+ *
643
+ * @param {Event|Object} data
644
+ * @param {String} [data.id]
645
+ * @param {HTMLElement} [data.target]
646
+ */
647
+ monitorAutoGrowHandler(data) {
641
648
  const
649
+ target = data.target || this.getElement(data.id),
642
650
  { style } = target,
643
651
  { style : inputStyle } = target.closest('.neo-textarea');
644
652
 
@@ -348,13 +348,19 @@ class Navigator extends Base {
348
348
  return;
349
349
  }
350
350
 
351
+ // Scroll the target into view smoothly before we focus it without triggering a scroll
352
+ newActiveElement.scrollIntoView({
353
+ block : 'nearest',
354
+ behavior : 'smooth'
355
+ });
356
+
351
357
  // Find a focusable element which may be the item, or inside the item to draw focus to.
352
358
  // For example a Chip list in which .neo-list-items contain focusable Chips.
353
359
  const focusTarget = DomUtils.query(newActiveElement, DomUtils.isFocusable);
354
360
 
355
361
  // If the item contains a focusable, we focus it and then react in navigateFocusInHandler
356
362
  if (focusTarget) {
357
- focusTarget.focus();
363
+ focusTarget.focus({ preventScroll : true });
358
364
  }
359
365
  // If not, we programatically navigate there
360
366
  else {
@@ -96,7 +96,7 @@ class Model extends Base {
96
96
  deselect(item, silent, itemCollection=this.items, selectedCls) {
97
97
  // We hold vdom ids for now, so all incoming selections must be converted.
98
98
  item = item.isRecord ? view.getItemId(item) : Neo.isObject(item) ? item.id : item;
99
-
99
+
100
100
  if (itemCollection.includes(item)) {
101
101
  let me = this,
102
102
  view = me.view,
@@ -231,7 +231,7 @@ class Model extends Base {
231
231
 
232
232
  items.forEach((node, i) => {
233
233
  node = view.getVdomChild(node);
234
-
234
+
235
235
  if (node) {
236
236
  node.cls = NeoArray.add(node.cls || [], selectedCls || me.selectedCls);
237
237
  node['aria-selected'] = true;
@@ -257,10 +257,12 @@ class Model extends Base {
257
257
  * @param {Object} item
258
258
  */
259
259
  toggleSelection(item) {
260
- if (this.isSelected(item)) {
261
- this.deselect(item);
260
+ let me = this;
261
+
262
+ if (me.isSelected(item)) {
263
+ me.deselect(item)
262
264
  } else {
263
- this.select(item);
265
+ me.select(item)
264
266
  }
265
267
  }
266
268
 
@@ -83,7 +83,7 @@ class AccordionTree extends TreeList {
83
83
  */
84
84
  _vdom:
85
85
  {cn: [
86
- {tag: 'ul', cls: ['neo-list-container', 'neo-list', 'neo-accordion-style'], tabIndex: -1, cn: []}
86
+ {tag: 'ul', cls: ['neo-list-container', 'neo-list', 'neo-accordion-style'], cn: []}
87
87
  ]}
88
88
  }
89
89
 
@@ -260,7 +260,8 @@ class AccordionTree extends TreeList {
260
260
  id = me.getItemId(item.id);
261
261
 
262
262
  tmpRoot.cn.push({
263
- tag: 'li',
263
+ tabIndex : -1,
264
+ tag : 'li',
264
265
  cls,
265
266
  id,
266
267
  cn : [{
package/src/tree/List.mjs CHANGED
@@ -60,11 +60,9 @@ class Tree extends Base {
60
60
  * @member {Object} _vdom
61
61
  */
62
62
  _vdom:
63
- {
64
- cn: [
65
- {tag: 'ul', cls: ['neo-list-container', 'neo-list'], tabIndex: -1, cn: []}
66
- ]
67
- }
63
+ {cn: [
64
+ {tag: 'ul', cls: ['neo-list-container', 'neo-list'], tabIndex: -1, cn: []}
65
+ ]}
68
66
  }
69
67
 
70
68
  /**
@@ -223,10 +221,11 @@ class Tree extends Base {
223
221
  }
224
222
 
225
223
  tmpRoot.cn.push({
226
- tag : 'li',
224
+ tag : 'li',
225
+ tabIndex : -1,
227
226
  cls,
228
- id : me.getItemId(item.id),
229
- cn : [{
227
+ id : me.getItemId(item.id),
228
+ cn : [{
230
229
  tag : 'span',
231
230
  cls : [itemCls + '-content', item.iconCls],
232
231
  innerHTML: item.name,
@@ -234,7 +233,7 @@ class Tree extends Base {
234
233
  pointerEvents: 'none'
235
234
  }
236
235
  }],
237
- style: {
236
+ style : {
238
237
  padding : '10px',
239
238
  position: item.isLeaf ? null : 'sticky',
240
239
  top : item.isLeaf ? null : (level * 38) + 'px',