@html-next/vertical-collection 5.0.0 → 5.0.1

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,2 @@
1
+ export { D as DynamicRadar, S as ScrollHandler, a as StaticRadar, V as ViewportContainer, b as addScrollHandler, c as closestElement, k as keyForItem, o as objectAt, r as removeScrollHandler } from '../static-radar-D0EvnYLd.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,513 @@
1
+ import "./occluded-content.css"
2
+ import { DEBUG } from '@glimmer/env';
3
+ import { assert } from '@ember/debug';
4
+ import { readOnly, empty } from '@ember/object/computed';
5
+ import Component, { setComponentTemplate } from '@ember/component';
6
+ import { get, computed } from '@ember/object';
7
+ import { run } from '@ember/runloop';
8
+ import { Token, scheduler } from 'ember-raf-scheduler';
9
+ import { a as StaticRadar, D as DynamicRadar, V as ViewportContainer, o as objectAt, k as keyForItem } from './static-radar-D0EvnYLd.js';
10
+ import { precompileTemplate } from '@ember/template-compilation';
11
+
12
+ ;
13
+
14
+ function isNonZero(value) {
15
+ let int = parseInt(value, 10);
16
+ let float = parseFloat(value);
17
+ return !isNaN(int) && (int !== 0 || float !== 0);
18
+ }
19
+ function hasStyleValue(styles, key, value) {
20
+ return styles[key] === value;
21
+ }
22
+ function hasStyleWithNonZeroValue(styles, key) {
23
+ return isNonZero(styles[key]);
24
+ }
25
+ function styleIsOneOf(styles, key, values) {
26
+ return styles[key] && values.indexOf(styles[key]) !== -1;
27
+ }
28
+ function applyVerticalStyles(element, geography) {
29
+ element.style.height = `${geography.height}px`;
30
+ element.style.top = `${geography.top}px`;
31
+ }
32
+ class Visualization {
33
+ constructor(radar) {
34
+ this.radar = radar;
35
+ this.satellites = [];
36
+ this.cache = [];
37
+ this.wrapper = document.createElement('div');
38
+ this.wrapper.className = 'vertical-collection-visual-debugger';
39
+ this.container = document.createElement('div');
40
+ this.container.className = 'vc_visualization-container';
41
+ this.wrapper.appendChild(this.container);
42
+ this.itemContainer = document.createElement('div');
43
+ this.itemContainer.className = 'vc_visualization-item-container';
44
+ this.container.appendChild(this.itemContainer);
45
+ this.scrollContainer = document.createElement('div');
46
+ this.scrollContainer.className = 'vc_visualization-scroll-container';
47
+ this.container.appendChild(this.scrollContainer);
48
+ this.screen = document.createElement('div');
49
+ this.screen.className = 'vc_visualization-screen';
50
+ this.container.appendChild(this.screen);
51
+ document.body.appendChild(this.wrapper);
52
+ }
53
+ render() {
54
+ this.styleViewport();
55
+ this.updateSatellites();
56
+ }
57
+ styleViewport() {
58
+ const {
59
+ _scrollContainer
60
+ } = this.radar;
61
+ this.container.style.height = `${_scrollContainer.getBoundingClientRect().height}px`;
62
+ applyVerticalStyles(this.scrollContainer, _scrollContainer.getBoundingClientRect());
63
+ applyVerticalStyles(this.screen, ViewportContainer.getBoundingClientRect());
64
+ }
65
+ makeSatellite() {
66
+ let satellite;
67
+ if (this.cache.length) {
68
+ satellite = this.cache.pop();
69
+ } else {
70
+ satellite = document.createElement('div');
71
+ satellite.className = 'vc_visualization-virtual-component';
72
+ }
73
+ this.satellites.push(satellite);
74
+ this.itemContainer.append(satellite);
75
+ }
76
+ updateSatellites() {
77
+ const {
78
+ satellites: sats
79
+ } = this;
80
+ let {
81
+ firstItemIndex,
82
+ lastItemIndex,
83
+ totalItems,
84
+ totalBefore,
85
+ totalAfter,
86
+ skipList,
87
+ _calculatedEstimateHeight
88
+ } = this.radar;
89
+ const isDynamic = !!skipList;
90
+ const itemHeights = isDynamic && skipList.values;
91
+ const firstVisualizedIndex = Math.max(firstItemIndex - 10, 0);
92
+ const lastVisualizedIndex = Math.min(lastItemIndex + 10, totalItems - 1);
93
+ const lengthWithBuffer = lastVisualizedIndex - firstVisualizedIndex + 1;
94
+ const isShrinking = sats.length > lengthWithBuffer;
95
+ while (sats.length !== lengthWithBuffer) {
96
+ if (isShrinking) {
97
+ const satellite = sats.pop();
98
+ satellite.parentNode.removeChild(satellite);
99
+ this.cache.push(satellite);
100
+ } else {
101
+ this.makeSatellite();
102
+ }
103
+ }
104
+ for (let itemIndex = firstVisualizedIndex, i = 0; itemIndex <= lastVisualizedIndex; itemIndex++, i++) {
105
+ const element = sats[i];
106
+ const itemHeight = isDynamic ? itemHeights[itemIndex] : _calculatedEstimateHeight;
107
+ element.style.height = `${itemHeight}px`;
108
+ element.setAttribute('index', String(itemIndex));
109
+ element.innerText = String(itemIndex);
110
+ if (itemIndex < firstItemIndex) {
111
+ element.classList.add('culled');
112
+ totalBefore -= itemHeight;
113
+ } else if (itemIndex > lastItemIndex) {
114
+ element.classList.add('culled');
115
+ totalAfter -= itemHeight;
116
+ } else {
117
+ element.classList.remove('culled');
118
+ }
119
+ }
120
+ this.itemContainer.style.paddingTop = `${totalBefore}px`;
121
+ this.itemContainer.style.paddingBottom = `${totalAfter}px`;
122
+ }
123
+ destroy() {
124
+ this.wrapper.parentNode.removeChild(this.wrapper);
125
+ this.wrapper = null;
126
+ this.radar = null;
127
+ this.component = null;
128
+ this.satellites.forEach(satellite => {
129
+ if (satellite.parentNode) {
130
+ satellite.parentNode.removeChild(satellite);
131
+ }
132
+ });
133
+ this.satellites = null;
134
+ this.cache = null;
135
+ }
136
+ }
137
+ /*
138
+ * END DEBUG HELPERS
139
+ */
140
+ class VerticalCollection extends Component.extend({
141
+ tagName: '',
142
+ /**
143
+ * Property name used for storing references to each item in items. Accessing this attribute for each item
144
+ * should yield a unique result for every item in the list.
145
+ *
146
+ * @property key
147
+ * @type String
148
+ * @default '@identity'
149
+ */
150
+ key: '@identity',
151
+ // –––––––––––––– Required Settings
152
+ /**
153
+ * Estimated height of an item to be rendered. Use best guess as this will be used to determine how many items
154
+ * are displayed virtually, before and after the vertical-collection viewport.
155
+ *
156
+ * @property estimateHeight
157
+ * @type Number
158
+ * @required
159
+ */
160
+ estimateHeight: null,
161
+ /**
162
+ * List of objects to svelte-render.
163
+ * Can be called like `<VerticalCollection @items={{itemsArray}} />`.
164
+ *
165
+ * @property items
166
+ * @type Array
167
+ * @required
168
+ */
169
+ items: null,
170
+ // –––––––––––––– Optional Settings
171
+ /**
172
+ * Indicates if the occluded items' heights will change or not.
173
+ * If true, the vertical-collection will assume that items' heights are always equal to estimateHeight;
174
+ * this is more performant, but less flexible.
175
+ *
176
+ * @property staticHeight
177
+ * @type Boolean
178
+ */
179
+ staticHeight: false,
180
+ /**
181
+ * Indicates whether or not list items in the Radar should be reused on update of virtual components (e.g. scroll).
182
+ * This yields performance benefits because it is not necessary to repopulate the component pool of the radar.
183
+ * Set to false when recycling a component instance has undesirable ramifications including:
184
+ * - When using `unbound` in a component or sub-component
185
+ * - When using init for instance state that differs between instances of a component or sub-component
186
+ * (can move to didInitAttrs to fix this)
187
+ * - When templates for individual items vary widely or are based on conditionals that are likely to change
188
+ * (i.e. would defeat any benefits of DOM recycling anyway)
189
+ *
190
+ * @property shouldRecycle
191
+ * @type Boolean
192
+ */
193
+ shouldRecycle: true,
194
+ /*
195
+ * A selector string that will select the element from
196
+ * which to calculate the viewable height and needed offsets.
197
+ *
198
+ * This element will also have the `scroll` event handler added to it.
199
+ *
200
+ * Usually this element will be the component's immediate parent element,
201
+ * if so, you can leave this null.
202
+ *
203
+ * Set this to "body" to scroll the entire web page.
204
+ */
205
+ containerSelector: '*',
206
+ // –––––––––––––– Performance Tuning
207
+ /**
208
+ * The amount of extra items to keep visible on either side of the viewport -- must be greater than 0.
209
+ * Increasing this value is useful when doing infinite scrolling and loading data from a remote service,
210
+ * with the desire to allow records to show as the user scrolls and the backend API takes time to respond.
211
+ *
212
+ * @property bufferSize
213
+ * @type Number
214
+ * @default 1
215
+ */
216
+ bufferSize: 1,
217
+ // –––––––––––––– Initial Scroll State
218
+ /**
219
+ * If set, upon initialization the scroll
220
+ * position will be set such that the item
221
+ * with the provided id is at the top left
222
+ * on screen.
223
+ *
224
+ * If the item cannot be found, scrollTop
225
+ * is set to 0.
226
+ * @property idForFirstItem
227
+ */
228
+ idForFirstItem: null,
229
+ /**
230
+ * If set, if scrollPosition is empty
231
+ * at initialization, the component will
232
+ * render starting at the bottom.
233
+ * @property renderFromLast
234
+ * @type Boolean
235
+ * @default false
236
+ */
237
+ renderFromLast: false,
238
+ /**
239
+ * If set to true, the collection will render all of the items passed into the component.
240
+ * This counteracts the performance benefits of using vertical collection, but has several potential applications,
241
+ * including but not limited to:
242
+ *
243
+ * - It allows for improved accessibility since all elements are rendered and can be picked up by a screen reader.
244
+ * - Can be applied in SEO solutions (i.e. fastboot) where rendering every item is desirable.
245
+ * - Can be used to respond to the keyboard input for Find (i.e. ctrl+F/cmd+F) to show all elements, which then
246
+ * allows the list items to be searchable
247
+ *
248
+ * @property renderAll
249
+ * @type Boolean
250
+ * @default false
251
+ */
252
+ renderAll: false,
253
+ /**
254
+ * The tag name used in DOM elements before and after the rendered list. By default, it is set to
255
+ * 'occluded-content' to avoid any confusion with user's CSS settings. However, it could be
256
+ * overriden to provide custom behavior (for example, in table user wants to set it to 'tr' to
257
+ * comply with table semantics).
258
+ */
259
+ occlusionTagName: 'occluded-content',
260
+ isEmpty: empty('items'),
261
+ shouldYieldToInverse: readOnly('isEmpty'),
262
+ virtualComponents: computed('items.[]', 'renderAll', 'estimateHeight', 'bufferSize', function () {
263
+ const {
264
+ _radar
265
+ } = this;
266
+ const items = this.items;
267
+ _radar.items = items === null || items === undefined ? [] : items;
268
+ _radar.estimateHeight = this.estimateHeight;
269
+ _radar.renderAll = this.renderAll;
270
+ _radar.bufferSize = this.bufferSize;
271
+ _radar.scheduleUpdate(true);
272
+ this._clearScheduledActions();
273
+ return _radar.virtualComponents;
274
+ }),
275
+ schedule(queueName, job) {
276
+ return scheduler.schedule(queueName, job, this.token);
277
+ },
278
+ _clearScheduledActions() {
279
+ clearTimeout(this._nextSendActions);
280
+ this._nextSendActions = null;
281
+ this._scheduledActions.length = 0;
282
+ },
283
+ _scheduleSendAction(action, index) {
284
+ this._scheduledActions.push([action, index]);
285
+ if (this._nextSendActions === null) {
286
+ this._nextSendActions = setTimeout(() => {
287
+ this._nextSendActions = null;
288
+ run(() => {
289
+ const items = this.items;
290
+ const keyPath = this.key;
291
+ this._scheduledActions.forEach(([action, index]) => {
292
+ const item = objectAt(items, index);
293
+ const key = keyForItem(item, keyPath, index);
294
+ // this.sendAction will be deprecated in ember 4.0
295
+ const _action = get(this, action);
296
+ if (typeof _action == 'function') {
297
+ _action(item, index, key);
298
+ } else if (typeof _action === 'string') {
299
+ this.sendAction(action, item, index, key);
300
+ }
301
+ });
302
+ this._scheduledActions.length = 0;
303
+ });
304
+ });
305
+ }
306
+ },
307
+ /* Public API Methods
308
+ @index => number
309
+ This will return offset height of the indexed item.
310
+ */
311
+ scrollToItem(index) {
312
+ const {
313
+ _radar
314
+ } = this;
315
+ // Getting the offset height from Radar
316
+ let scrollTop = _radar.getOffsetForIndex(index);
317
+ _radar._scrollContainer.scrollTop = scrollTop;
318
+ // To scroll exactly to specified index, we are changing the prevIndex values to specified index
319
+ _radar._prevFirstVisibleIndex = _radar._prevFirstItemIndex = index;
320
+ // Components will be rendered after schedule 'measure' inside 'update' method.
321
+ // In our case, we need to focus the element after component is rendered. So passing the promise.
322
+ return new Promise(resolve => {
323
+ _radar.scheduleUpdate(false, resolve);
324
+ });
325
+ },
326
+ // –––––––––––––– Setup/Teardown
327
+ didInsertElement() {
328
+ this._super();
329
+ this.schedule('sync', () => {
330
+ this._radar.start();
331
+ });
332
+ },
333
+ willDestroy() {
334
+ this.token.cancel();
335
+ this._radar.destroy();
336
+ let registerAPI = this.registerAPI;
337
+ if (registerAPI) {
338
+ registerAPI(null);
339
+ }
340
+ clearTimeout(this._nextSendActions);
341
+ if (DEBUG) {
342
+ if (this.__visualization) {
343
+ console.info('destroying visualization');
344
+ this.__visualization.destroy();
345
+ this.__visualization = null;
346
+ }
347
+ }
348
+ this._super();
349
+ },
350
+ init() {
351
+ this._super();
352
+ this.token = new Token();
353
+ const RadarClass = this.staticHeight ? StaticRadar : DynamicRadar;
354
+ const items = this.items || [];
355
+ const {
356
+ bufferSize,
357
+ containerSelector,
358
+ estimateHeight,
359
+ initialRenderCount,
360
+ renderAll,
361
+ renderFromLast,
362
+ shouldRecycle,
363
+ occlusionTagName,
364
+ idForFirstItem,
365
+ key
366
+ } = this;
367
+ const startingIndex = calculateStartingIndex(items, idForFirstItem, key, renderFromLast);
368
+ this._radar = new RadarClass(this.token, {
369
+ bufferSize,
370
+ containerSelector,
371
+ estimateHeight,
372
+ initialRenderCount,
373
+ items,
374
+ key,
375
+ renderAll,
376
+ renderFromLast,
377
+ shouldRecycle,
378
+ startingIndex,
379
+ occlusionTagName
380
+ });
381
+ this._prevItemsLength = 0;
382
+ this._prevFirstKey = null;
383
+ this._prevLastKey = null;
384
+ this._hasAction = null;
385
+ this._scheduledActions = [];
386
+ this._nextSendActions = null;
387
+ let a = !!this.lastReached;
388
+ let b = !!this.firstReached;
389
+ let c = !!this.lastVisibleChanged;
390
+ let d = !!this.firstVisibleChanged;
391
+ let any = a || b || c || d;
392
+ if (any) {
393
+ this._hasAction = {
394
+ lastReached: a,
395
+ firstReached: b,
396
+ lastVisibleChanged: c,
397
+ firstVisibleChanged: d
398
+ };
399
+ this._radar.sendAction = (action, index) => {
400
+ if (this._hasAction[action]) {
401
+ this._scheduleSendAction(action, index);
402
+ }
403
+ };
404
+ }
405
+ /* Public methods to Expose to parent
406
+ Usage:
407
+ Template:
408
+ <VerticalCollection @registerAPI={{this.registerAPI}} />
409
+ Component:
410
+ export default class extends Component {
411
+ @action
412
+ registerAPI(api) {
413
+ this.collectionAPI = api;
414
+ }
415
+ @action
416
+ scrollToItem(index) {
417
+ this.collectionAPI.scrollToItem(index);
418
+ }
419
+ }
420
+ Need to pass this property in the vertical-collection template
421
+ Listen in the component actions and do your custom logic
422
+ This API will have below methods.
423
+ 1. scrollToItem
424
+ */
425
+ let registerAPI = get(this, 'registerAPI');
426
+ if (registerAPI) {
427
+ /* List of methods to be exposed to public should be added here */let publicAPI = {
428
+ scrollToItem: this.scrollToItem.bind(this)
429
+ };
430
+ registerAPI(publicAPI);
431
+ }
432
+ if (DEBUG) {
433
+ this.__visualization = null;
434
+ this._radar._debugDidUpdate = () => {
435
+ // Update visualization
436
+ //
437
+ // This debugging mode can be controlled via the argument
438
+ // `@debugVis={{true}}` at component invocation.
439
+ //
440
+ if (this.debugVis !== true) {
441
+ if (this.__visualization !== null) {
442
+ console.info('tearing down existing visualization');
443
+ this.__visualization.destroy();
444
+ this.__visualization = null;
445
+ }
446
+ return;
447
+ }
448
+ if (this.__visualization === null) {
449
+ this.__visualization = new Visualization(this._radar);
450
+ }
451
+ this.__visualization.render();
452
+ // Detect issues with CSS
453
+ //
454
+ // This debugging mode can be controlled via the argument
455
+ // `@debugCSS={{true}}` at component invocation.
456
+ //
457
+ if (this.debugCSS !== true) {
458
+ return;
459
+ }
460
+ let radar = this._radar;
461
+ let styles;
462
+ // check telescope
463
+ if (radar.scrollContainer !== ViewportContainer) {
464
+ styles = window.getComputedStyle(radar.scrollContainer);
465
+ } else {
466
+ styles = window.getComputedStyle(document.body);
467
+ }
468
+ assert(`scrollContainer cannot be inline.`, styleIsOneOf(styles, 'display', ['block', 'inline-block', 'flex', 'inline-flex']));
469
+ assert(`scrollContainer must define position`, styleIsOneOf(styles, 'position', ['static', 'relative', 'absolute']));
470
+ assert(`scrollContainer must define height or max-height`, hasStyleWithNonZeroValue(styles, 'height') || hasStyleWithNonZeroValue(styles, 'max-height'));
471
+ // conditional perf check for non-body scrolling
472
+ if (radar.scrollContainer !== ViewportContainer) {
473
+ assert(`scrollContainer must define overflow-y`, hasStyleValue(styles, 'overflow-y', 'scroll') || hasStyleValue(styles, 'overflow', 'scroll'));
474
+ }
475
+ // check itemContainer
476
+ styles = window.getComputedStyle(radar.itemContainer);
477
+ assert(`itemContainer cannot be inline.`, styleIsOneOf(styles, 'display', ['block', 'inline-block', 'flex', 'inline-flex']));
478
+ assert(`itemContainer must define position`, styleIsOneOf(styles, 'position', ['static', 'relative', 'absolute']));
479
+ // check item defaults
480
+ assert(`You must supply at least one item to the collection to debug it's CSS.`, this.items.length);
481
+ let element = radar._itemContainer.firstElementChild;
482
+ styles = window.getComputedStyle(element);
483
+ assert(`Item cannot be inline.`, styleIsOneOf(styles, 'display', ['block', 'inline-block', 'flex', 'inline-flex']));
484
+ assert(`Item must define position`, styleIsOneOf(styles, 'position', ['static', 'relative', 'absolute']));
485
+ };
486
+ }
487
+ }
488
+ }) {
489
+ static {
490
+ setComponentTemplate(precompileTemplate("{{#each this.virtualComponents key=\"id\" as |virtualComponent|~}}\n {{~unbound virtualComponent.upperBound~}}\n {{~#if virtualComponent.isOccludedContent~}}\n {{{unbound virtualComponent.element}}}\n {{~else~}}\n {{~yield virtualComponent.content virtualComponent.index~}}\n {{~/if~}}\n {{~unbound virtualComponent.lowerBound~}}\n{{~/each}}\n\n{{#if this.shouldYieldToInverse}}\n {{yield to=\"inverse\"}}\n{{/if}}", {
491
+ strictMode: true
492
+ }), this);
493
+ }
494
+ }
495
+ function calculateStartingIndex(items, idForFirstItem, key, renderFromLast) {
496
+ const totalItems = get(items, 'length');
497
+ let startingIndex = 0;
498
+ if (idForFirstItem !== undefined && idForFirstItem !== null) {
499
+ for (let i = 0; i < totalItems; i++) {
500
+ if (keyForItem(objectAt(items, i), key, i) === idForFirstItem) {
501
+ startingIndex = i;
502
+ break;
503
+ }
504
+ }
505
+ } else if (renderFromLast === true) {
506
+ // If no id was set and `renderFromLast` is true, start from the bottom
507
+ startingIndex = totalItems - 1;
508
+ }
509
+ return startingIndex;
510
+ }
511
+
512
+ export { VerticalCollection };
513
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ .occluded-content {
2
+ display: block;
3
+ position: relative;
4
+ width: 100%;
5
+
6
+ /* prevents margin overflow on item container */
7
+ min-height: 0.01px;
8
+
9
+ /* hides text visually while still being readable by screen readers */
10
+ color: rgb(0 0 0 / 0%);
11
+
12
+ /*
13
+ * Prevents the debug text ("And X items before/after") from affecting
14
+ * the element's layout height. Without this, inherited line-height values
15
+ * can cause the text to create a line box that inflates the element's
16
+ * actual height above its inline style height, especially when used
17
+ * inside tables with display: table-row.
18
+ */
19
+ font-size: 0;
20
+ line-height: 0;
21
+ }
22
+
23
+ table .occluded-content,
24
+ tbody .occluded-content,
25
+ thead .occluded-content,
26
+ tfoot .occluded-content {
27
+ display: table-row;
28
+ position: relative;
29
+ width: 100%;
30
+ }
31
+
32
+ ul .occluded-content,
33
+ ol .occluded-content {
34
+ display: list-item;
35
+ position: relative;
36
+ width: 100%;
37
+ list-style-type: none;
38
+ height: 0;
39
+ }