neo.mjs 8.22.0 → 8.24.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.
@@ -0,0 +1,494 @@
1
+ import Component from '../component/Base.mjs';
2
+
3
+ /**
4
+ * Deeply inspired by https://github.com/yangshun 's video on LinkedIn
5
+ * as well as Apple's Keynote Magic Move effect
6
+ * @class Neo.component.MagicMoveText
7
+ * @extends Neo.component.Base
8
+ */
9
+ class MagicMoveText extends Component {
10
+ static config = {
11
+ /**
12
+ * @member {String} className='Neo.component.MagicMoveText'
13
+ * @protected
14
+ */
15
+ className: 'Neo.component.MagicMoveText',
16
+ /**
17
+ * @member {String} ntype='magic-move-text'
18
+ * @protected
19
+ */
20
+ ntype: 'magic-move-text',
21
+ /**
22
+ * @member {Boolean} autoCycle_=true
23
+ */
24
+ autoCycle_: true,
25
+ /**
26
+ * @member {Number} autoCycleInterval_=2000
27
+ */
28
+ autoCycleInterval_: 2000,
29
+ /**
30
+ * @member {String[]} baseCls=['neo-magic-move-text']
31
+ * @protected
32
+ */
33
+ baseCls: ['neo-magic-move-text'],
34
+ /**
35
+ * @member {String|null} colorMove=null
36
+ */
37
+ colorMove: null,
38
+ /**
39
+ * @member {String|null} colorFadeIn=null
40
+ */
41
+ colorFadeIn: null,
42
+ /**
43
+ * @member {String|null} colorFadeOut=null
44
+ */
45
+ colorFadeOut: null,
46
+ /**
47
+ * @member {String[]|null} cycleTexts=null
48
+ */
49
+ cycleTexts: null,
50
+ /**
51
+ * @member {String} fontFamily_='Helvetica Neue'
52
+ */
53
+ fontFamily_: 'Helvetica Neue',
54
+ /**
55
+ * @member {String} text_=null
56
+ */
57
+ text_: null,
58
+ /**
59
+ * Time in ms for the fadeIn, fadeOut and move character OPs
60
+ * @member {Number} transitionTime_=500
61
+ */
62
+ transitionTime_: 500,
63
+ /**
64
+ * @member {Object} _vdom
65
+ */
66
+ _vdom:
67
+ {style: {}, cn: [
68
+ {cls: ['neo-content'], cn: []},
69
+ {cls: ['neo-measure-element-wrapper'], removeDom: true, cn: [
70
+ {cls: ['neo-measure-element'], cn:[]}
71
+ ]}
72
+ ]}
73
+ }
74
+
75
+ /**
76
+ * @member {Object[]} chars=[]
77
+ * @protected
78
+ */
79
+ chars = []
80
+ /**
81
+ * @member {Object[]} charsVdom=[]
82
+ * @protected
83
+ */
84
+ charsVdom = []
85
+ /**
86
+ * @member {Number} contentHeight=0
87
+ * @protected
88
+ */
89
+ contentHeight = 0
90
+ /**
91
+ * @member {Number} contentWidth=0
92
+ * @protected
93
+ */
94
+ contentWidth = 0
95
+ /**
96
+ * @member {Number} currentIndex=0
97
+ * @protected
98
+ */
99
+ currentIndex = 0
100
+ /**
101
+ * We do not need the first event to trigger logic, since afterSetMounted() handles this
102
+ * @member {Boolean} initialResizeEvent=true
103
+ * @protected
104
+ */
105
+ initialResizeEvent = true
106
+ /**
107
+ * @member {Number|null} intervalId=null
108
+ * @protected
109
+ */
110
+ intervalId = null
111
+ /**
112
+ * Internal flag which gets set to true while the animated char transitions are running
113
+ * @member {Boolean} isTransitioning=false
114
+ * @protected
115
+ */
116
+ isTransitioning = false
117
+ /**
118
+ * @member {Object} measureCache={}
119
+ * @protected
120
+ */
121
+ measureCache = {}
122
+ /**
123
+ * @member {Object[]} previousChars=[]
124
+ * @protected
125
+ */
126
+ previousChars = []
127
+ /**
128
+ * @member {Object} measureElement
129
+ * @protected
130
+ */
131
+ get measureElement() {
132
+ return this.measureWrapper.cn[0]
133
+ }
134
+ /**
135
+ * @member {Object} measureElement
136
+ * @protected
137
+ */
138
+ get measureWrapper() {
139
+ return this.vdom.cn[1]
140
+ }
141
+
142
+ /**
143
+ * @param {Object} config
144
+ */
145
+ construct(config) {
146
+ super.construct(config);
147
+
148
+ let me = this;
149
+
150
+ me.addDomListeners({
151
+ resize: me.onResize,
152
+ scope : me
153
+ })
154
+ }
155
+
156
+ /**
157
+ * @param {Boolean} mounted
158
+ * @protected
159
+ */
160
+ async addResizeObserver(mounted) {
161
+ let {id, windowId} = this,
162
+ ResizeObserver = await Neo.currentWorker.getAddon('ResizeObserver', windowId);
163
+
164
+ ResizeObserver[mounted ? 'register' : 'unregister']({id, windowId});
165
+
166
+ if (mounted) {
167
+ this.initialResizeEvent = true
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Triggered after the autoCycle config got changed
173
+ * @param {Boolean} value
174
+ * @param {Boolean} oldValue
175
+ * @protected
176
+ */
177
+ afterSetAutoCycle(value, oldValue) {
178
+ this.mounted && this.startAutoCycle(value)
179
+ }
180
+
181
+ /**
182
+ * Triggered after the autoCycleInterval config got changed
183
+ * @param {Number} value
184
+ * @param {Number} oldValue
185
+ * @protected
186
+ */
187
+ afterSetAutoCycleInterval(value, oldValue) {
188
+ let me = this;
189
+
190
+ if (oldValue && me.mounted) {
191
+ me.startAutoCycle(false);
192
+ me.startAutoCycle()
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Triggered after the fontFamily config got changed
198
+ * @param {String} value
199
+ * @param {String} oldValue
200
+ * @protected
201
+ */
202
+ afterSetFontFamily(value, oldValue) {
203
+ let me = this;
204
+
205
+ me.measureCache = {};
206
+
207
+ me.vdom.style.fontFamily = value;
208
+ me.update()
209
+ }
210
+
211
+ /**
212
+ * Triggered after the mounted config got changed
213
+ * @param {Boolean} value
214
+ * @param {Boolean} oldValue
215
+ * @protected
216
+ */
217
+ afterSetMounted(value, oldValue) {
218
+ super.afterSetMounted(value, oldValue);
219
+
220
+ let me = this;
221
+
222
+ value && me.getDomRect().then(rect => {
223
+ me.contentHeight = rect.height;
224
+ me.contentWidth = rect.width;
225
+
226
+ me.autoCycle && me.startAutoCycle(value)
227
+ });
228
+
229
+ oldValue !== undefined && me.addResizeObserver(value)
230
+ }
231
+
232
+ /**
233
+ * Triggered after the text config got changed
234
+ * @param {String} value
235
+ * @param {String} oldValue
236
+ * @returns {Promise<void>}
237
+ * @protected
238
+ */
239
+ async afterSetText(value, oldValue) {
240
+ let me = this,
241
+ {measureElement} = me;
242
+
243
+ if (oldValue) {
244
+ me.previousChars = me.chars
245
+ }
246
+
247
+ if (value) {
248
+ me.chars = [];
249
+ measureElement.cn = [];
250
+
251
+ value?.split('').forEach(char => {
252
+ me.chars.push({name: char});
253
+
254
+ if (char === ' ') {
255
+ char = '&#32;'
256
+ }
257
+
258
+ measureElement.cn.push({tag: 'span', html: char})
259
+ });
260
+
261
+ if (me.mounted) {
262
+ await me.measureChars()
263
+ }
264
+
265
+ await me.updateChars()
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Triggered after the transitionTime config got changed
271
+ * @param {Number} value
272
+ * @param {Number} oldValue
273
+ * @protected
274
+ */
275
+ afterSetTransitionTime(value, oldValue) {
276
+ this.vdom.style['--neo-transition-time'] = value + 'ms';
277
+ this.update()
278
+ }
279
+
280
+ /**
281
+ * @param {String[]} letters
282
+ * @returns {Object[]}
283
+ * @protected
284
+ */
285
+ createCharsVdom(letters) {
286
+ let me = this,
287
+ {chars} = me,
288
+ charsContainer = [],
289
+ char;
290
+
291
+ letters.forEach((letter, index) => {
292
+ if (letter !== null) {
293
+ char = chars[index];
294
+
295
+ charsContainer.push({
296
+ cls : ['neo-char'],
297
+ html : char.name,
298
+ style: {color: me.colorFadeIn, left: char.left, opacity: 0, top: char.top}
299
+ })
300
+ }
301
+ });
302
+
303
+ return charsContainer
304
+ }
305
+
306
+ /**
307
+ * @protected
308
+ */
309
+ cycleText() {
310
+ let me = this;
311
+
312
+ me.text = me.cycleTexts[me.currentIndex];
313
+ me.currentIndex = (me.currentIndex + 1) % me.cycleTexts.length
314
+ }
315
+
316
+ /**
317
+ * @returns {Promise<void>}
318
+ * @protected
319
+ */
320
+ async measureChars() {
321
+ let me = this,
322
+ {measureCache, measureElement, measureWrapper, text} = me,
323
+ parentRect, rects;
324
+
325
+ if (measureCache[text]) {
326
+ rects = [...measureCache[text]];
327
+ parentRect = rects.shift()
328
+ } else {
329
+ measureWrapper.style = {
330
+ height: me.contentHeight + 'px',
331
+ width : me.contentWidth + 'px'
332
+ };
333
+
334
+ delete measureWrapper.removeDom;
335
+
336
+ await me.promiseUpdate();
337
+ await me.timeout(20);
338
+
339
+ rects = await me.getDomRect([measureWrapper.id, ...measureElement.cn.map(node => node.id)]);
340
+ parentRect = rects.shift();
341
+
342
+ measureCache[text] = [parentRect, ...rects]
343
+ }
344
+
345
+ rects.forEach((rect, index) => {
346
+ me.chars[index].left = `${rect.left - parentRect.left}px`;
347
+ me.chars[index].top = `${rect.top - parentRect.top }px`;
348
+ });
349
+
350
+ measureWrapper.removeDom = true;
351
+ await me.promiseUpdate()
352
+ }
353
+
354
+ /**
355
+ * @param {Object} data
356
+ * @returns {Promise<void>}
357
+ * @protected
358
+ */
359
+ async onResize({rect}) {
360
+ let me = this;
361
+
362
+ me.contentHeight = rect.height;
363
+ me.contentWidth = rect.width;
364
+
365
+ me.measureCache = {};
366
+
367
+
368
+ if (!me.initialResizeEvent) {
369
+ if (!me.isTransitioning) {
370
+ await me.measureChars();
371
+
372
+ me.charsVdom = me.createCharsVdom(me.chars.map(char => char.name))
373
+ }
374
+ } else {
375
+ me.initialResizeEvent = false
376
+ }
377
+ }
378
+
379
+ /**
380
+ * @param {Object} a
381
+ * @param {Object} b
382
+ * @returns {Number}
383
+ * @protected
384
+ */
385
+ sortCharacters(a, b) {
386
+ let deltaTop = parseFloat(a.style.top) - parseFloat(b.style.top);
387
+
388
+ if (deltaTop !== 0) {
389
+ return deltaTop
390
+ }
391
+
392
+ return parseFloat(a.style.left) - parseFloat(b.style.left)
393
+ }
394
+
395
+ /**
396
+ * @param {Boolean} start=true
397
+ * @protected
398
+ */
399
+ startAutoCycle(start=true) {
400
+ let me = this;
401
+
402
+ if (start) {
403
+ me.intervalId = setInterval(me.cycleText.bind(me), me.autoCycleInterval);
404
+
405
+ me.timeout(20).then(() => {me.cycleText()});
406
+ } else {
407
+ clearInterval(me.intervalId)
408
+ }
409
+ }
410
+
411
+ /**
412
+ * @returns {Promise<void>}
413
+ * @protected
414
+ */
415
+ async updateChars() {
416
+ let me = this,
417
+ {chars, previousChars} = me,
418
+ charsContainer = me.vdom.cn[0],
419
+ letters = chars.map(char => char.name),
420
+ charNode, index;
421
+
422
+ me.isTransitioning = true;
423
+
424
+ if (me.charsVdom.length > 1) {
425
+ charsContainer.cn = me.charsVdom;
426
+ await me.promiseUpdate()
427
+ }
428
+
429
+ previousChars.forEach((previousChar, previousIndex) => {
430
+ index = letters.indexOf(previousChar.name);
431
+
432
+ if (index > -1) {
433
+ charNode = charsContainer.cn[previousIndex];
434
+
435
+ Object.assign(charNode.style, {
436
+ color: me.colorMove,
437
+ left : chars[index].left,
438
+ top : chars[index].top
439
+ });
440
+
441
+ letters[index] = null
442
+ } else {
443
+ charNode = charsContainer.cn[previousIndex];
444
+
445
+ charNode.flag = 'remove'
446
+ }
447
+ });
448
+
449
+ charsContainer.cn.push(...me.createCharsVdom(letters));
450
+
451
+ await me.promiseUpdate();
452
+
453
+ charsContainer.cn.forEach(charNode => {
454
+ if (charNode.flag === 'remove') {
455
+ charNode.style.color = me.colorFadeOut;
456
+ charNode.style.opacity = 0
457
+ } else {
458
+ delete charNode.style.opacity
459
+ }
460
+ });
461
+
462
+ await me.promiseUpdate();
463
+ await me.timeout(me.transitionTime);
464
+
465
+ charsContainer.cn.sort(me.sortCharacters);
466
+
467
+ index = charsContainer.cn.length - 1;
468
+
469
+ for (; index >= 0; index--) {
470
+ charNode = charsContainer.cn[index];
471
+
472
+ delete charNode.flag;
473
+ delete charNode.style.color;
474
+
475
+ if (charNode.style.opacity === 0) {
476
+ charsContainer.cn.splice(index, 1)
477
+ }
478
+ }
479
+
480
+ await me.promiseUpdate();
481
+ await me.timeout(200);
482
+
483
+ me.charsVdom = [...charsContainer.cn];
484
+
485
+ charsContainer.cn.length = 0;
486
+
487
+ charsContainer.cn.push({html: me.text});
488
+ await me.promiseUpdate();
489
+
490
+ me.isTransitioning = false
491
+ }
492
+ }
493
+
494
+ export default Neo.setupClass(MagicMoveText);
@@ -330,6 +330,18 @@ class Store extends Base {
330
330
  return this.keyProperty || this.model.keyProperty
331
331
  }
332
332
 
333
+ /**
334
+ * Convenience shortcut to check for int based keyProperties
335
+ * @returns {String|null} lowercase value of the model field type
336
+ */
337
+ getKeyType() {
338
+ let me = this,
339
+ {model} = me,
340
+ keyField = model?.getField(me.getKeyProperty());
341
+
342
+ return keyField?.type?.toLowerCase() || null
343
+ }
344
+
333
345
  /**
334
346
  * @param {Object} opts={}
335
347
  * @param {Object} opts.data
@@ -197,6 +197,25 @@ class GridContainer extends BaseContainer {
197
197
  })
198
198
  }
199
199
 
200
+ /**
201
+ * @param {Boolean} mounted
202
+ * @protected
203
+ */
204
+ async addResizeObserver(mounted) {
205
+ let me = this,
206
+ {windowId} = me,
207
+ ResizeObserver = await Neo.currentWorker.getAddon('ResizeObserver', windowId),
208
+ resizeParams = {id: me.id, windowId};
209
+
210
+ if (mounted) {
211
+ ResizeObserver.register(resizeParams);
212
+ await me.passSizeToView()
213
+ } else {
214
+ me.initialResizeEvent = true;
215
+ ResizeObserver.unregister(resizeParams)
216
+ }
217
+ }
218
+
200
219
  /**
201
220
  * Triggered after the cellEditing config got changed
202
221
  * @param {Boolean} value
@@ -245,25 +264,6 @@ class GridContainer extends BaseContainer {
245
264
  }
246
265
  }
247
266
 
248
- /**
249
- * @param {Boolean} mounted
250
- * @protected
251
- */
252
- async addResizeObserver(mounted) {
253
- let me = this,
254
- {windowId} = me,
255
- ResizeObserver = await Neo.currentWorker.getAddon('ResizeObserver', windowId),
256
- resizeParams = {id: me.id, windowId};
257
-
258
- if (mounted) {
259
- ResizeObserver.register(resizeParams);
260
- await me.passSizeToView()
261
- } else {
262
- me.initialResizeEvent = true;
263
- ResizeObserver.unregister(resizeParams)
264
- }
265
- }
266
-
267
267
  /**
268
268
  * Triggered after the mounted config got changed
269
269
  * @param {Boolean} value
@@ -522,7 +522,7 @@ class GridContainer extends BaseContainer {
522
522
  if (!me.initialResizeEvent) {
523
523
  await me.passSizeToView(true);
524
524
 
525
- me.view.updateVisibleColumns();
525
+ me.view.updateMountedAndVisibleColumns();
526
526
 
527
527
  await me.headerToolbar.passSizeToView()
528
528
  } else {
@@ -537,16 +537,17 @@ class GridContainer extends BaseContainer {
537
537
  * @param {Object} data.touches
538
538
  */
539
539
  onScroll({scrollLeft, target, touches}) {
540
- let me = this,
540
+ let me = this,
541
+ {view} = me,
541
542
  deltaY, lastTouchY;
542
543
 
543
544
  // We must ignore events for grid-scrollbar
544
545
  if (target.id.includes('grid-container')) {
545
546
  me.headerToolbar.scrollLeft = scrollLeft;
546
- me.view.scrollPosition = {x: scrollLeft, y: me.view.scrollPosition.y};
547
+ view.scrollPosition = {x: scrollLeft, y: view.scrollPosition.y};
547
548
 
548
549
  if (touches) {
549
- if (!me.view.isTouchMoveOwner) {
550
+ if (!view.isTouchMoveOwner) {
550
551
  me.isTouchMoveOwner = true
551
552
  }
552
553
 
@@ -556,8 +557,8 @@ class GridContainer extends BaseContainer {
556
557
 
557
558
  deltaY !== 0 && Neo.main.DomAccess.scrollTo({
558
559
  direction: 'top',
559
- id : me.view.vdom.id,
560
- value : me.view.scrollPosition.y + deltaY
560
+ id : view.vdom.id,
561
+ value : view.scrollPosition.y + deltaY
561
562
  })
562
563
 
563
564
  me.lastTouchY = lastTouchY
@@ -657,6 +658,58 @@ class GridContainer extends BaseContainer {
657
658
  }
658
659
  })
659
660
  }
661
+
662
+ /**
663
+ * Used for keyboard navigation (selection models)
664
+ * @param {Number} index
665
+ * @param {Number} step
666
+ */
667
+ scrollByColumns(index, step) {
668
+ let me = this,
669
+ {view} = me,
670
+ {columnPositions, containerWidth, mountedColumns, visibleColumns} = view,
671
+ countColumns = columnPositions.getCount(),
672
+ newIndex = index + step,
673
+ column, mounted, scrollPosition, visible;
674
+
675
+ if (newIndex >= countColumns) {
676
+ newIndex %= countColumns;
677
+ step = newIndex - index
678
+ }
679
+
680
+ while (newIndex < 0) {
681
+ newIndex += countColumns;
682
+ step += countColumns
683
+ }
684
+
685
+ mounted = newIndex >= mountedColumns[0] && newIndex <= mountedColumns[1];
686
+
687
+ // Not using >= or <=, since the first / last column might not be fully visible
688
+ visible = newIndex > visibleColumns[0] && newIndex < visibleColumns[1];
689
+
690
+ if (!visible) {
691
+ // Leaving the mounted area will re-calculate the visibleColumns for us
692
+ if (mounted) {
693
+ visibleColumns[0] += step;
694
+ visibleColumns[1] += step
695
+ }
696
+
697
+ column = columnPositions.getAt(newIndex);
698
+
699
+ if (step < 0) {
700
+ scrollPosition = column.x
701
+ } else {
702
+ scrollPosition = column.x - containerWidth + column.width
703
+ }
704
+
705
+ Neo.main.DomAccess.scrollTo({
706
+ direction: 'left',
707
+ id : me.id,
708
+ value : scrollPosition,
709
+ windowId : me.windowId
710
+ })
711
+ }
712
+ }
660
713
  }
661
714
 
662
715
  export default Neo.setupClass(GridContainer);