Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created horizontal-collection #269

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# vertical-collection
virtual-collection

[![Greenkeeper badge](https://badges.greenkeeper.io/html-next/vertical-collection.svg)](https://greenkeeper.io/)

[![Build Status](https://travis-ci.org/html-next/vertical-collection.svg)](https://travis-ci.org/html-next/vertical-collection)

Infinite Scroll and Occlusion at > 60FPS

`vertical-collection` is an `ember-addon` that is part of the `smoke-and-mirrors` framework. It
`virtual-collection` is an `ember-addon` that is part of the `smoke-and-mirrors` framework. It
focuses on improving initial and re-render performance in high-stress situations by providing a
component for performant lists and `svelte renders` to match a core belief:
**Don't render the universe, render the scene.**
Expand All @@ -18,17 +18,19 @@ out-of-scene content, your application should smartly cull the content it doesn'
excess content lets the browser perform both initial renders and re-renders at far higher frame-rates, as the only
content it needs to focus on for layout is the content the user can see.

`vertical-collection` augments your existing app, it doesn't ask you to rewrite layouts or logic in order to use it.
`virtual-collection` augments your existing app, it doesn't ask you to rewrite layouts or logic in order to use it.
It will try its best to allow you to keep the conventions, structures, and layouts you want.

## Install

```bash
ember install @html-next/vertical-collection
ember install @html-next/virtual-collection
```

## Usage

There are two components you can use, `vertical-collection` for vertical scrollables:

```htmlbars
{{#vertical-collection
items
Expand All @@ -50,6 +52,29 @@ ember install @html-next/vertical-collection
{{/vertical-collection}}
```

... and `horizontal-collection` for horizontal scrollables:

```htmlbars
{{#horizontal-collection
items
tagName='div'
estimateWidth=50
staticWidth=false
bufferSize=1
renderAll=false
renderFromLast=false
idForFirstItem=idForFirstItem
firstReached=firstReached
lastReached=lastReached
firstVisibleChanged=firstVisibleChanged
lastVisibleChanged=lastVisibleChanged
as |item i|}}
<div>
{{item.number}} {{i}}
</div>
{{/vertical-collection}}
```

### Actions

`firstReached` - Triggered when scroll reaches the first element in the collection
Expand Down
19 changes: 17 additions & 2 deletions addon/-debug/edge-visualization/debug-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,26 @@ export default Mixin.create({

assert(`scrollContainer cannot be inline.`, styleIsOneOf(styles, 'display', ['block', 'inline-block', 'flex', 'inline-flex']));
assert(`scrollContainer must define position`, styleIsOneOf(styles, 'position', ['static', 'relative', 'absolute']));
assert(`scrollContainer must define height or max-height`, hasStyleWithNonZeroValue(styles, 'height') || hasStyleWithNonZeroValue(styles, 'max-height'));

if( this.get( 'orientation' ) === 'horizontal' )
{
assert(`scrollContainer must define width or max-width`, hasStyleWithNonZeroValue(styles, 'width') || hasStyleWithNonZeroValue(styles, 'max-width'));
}
else
{
assert(`scrollContainer must define height or max-height`, hasStyleWithNonZeroValue(styles, 'height') || hasStyleWithNonZeroValue(styles, 'max-height'));
}

// conditional perf check for non-body scrolling
if (radar.scrollContainer !== ViewportContainer) {
assert(`scrollContainer must define overflow-y`, hasStyleValue(styles, 'overflow-y', 'scroll') || hasStyleValue(styles, 'overflow', 'scroll'));
if( this.get( 'orientation' ) === 'horizontal' )
{
assert(`scrollContainer must define overflow-x`, hasStyleValue(styles, 'overflow-x', 'scroll') || hasStyleValue(styles, 'overflow', 'scroll'));
}
else
{
assert(`scrollContainer must define overflow-y`, hasStyleValue(styles, 'overflow-y', 'scroll') || hasStyleValue(styles, 'overflow', 'scroll'));
}
}

// check itemContainer
Expand Down
30 changes: 19 additions & 11 deletions addon/-debug/edge-visualization/visualization.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { ViewportContainer } from '../../-private';

function applyVerticalStyles(element, geography) {
element.style.height = `${geography.height}px`;
element.style.top = `${geography.top}px`;
function applyVerticalStyles(element, geography, orientation) {
if( orientation === 'horizontal' )
{
element.style.width = `${geography.width}px`;
element.style.left = `${geography.left}px`;
}
else
{
element.style.height = `${geography.height}px`;
element.style.top = `${geography.top}px`;
}
}

export default class Visualization {
Expand All @@ -12,7 +20,7 @@ export default class Visualization {
this.cache = [];

this.wrapper = document.createElement('div');
this.wrapper.className = 'vertical-collection-visual-debugger';
this.wrapper.className = `virtual-collection-visual-debugger${this.get('orientation') === 'horizontal' ? ' horizontal' : ''}`;

this.container = document.createElement('div');
this.container.className = 'vc_visualization-container';
Expand Down Expand Up @@ -40,7 +48,7 @@ export default class Visualization {

styleViewport() {
const { _scrollContainer } = this.radar;
this.container.style.height = `${_scrollContainer.getBoundingClientRect().height}px`;
this.container.style[this.get( 'orientation' ) === 'horizontal' ? 'width' : 'height'] = `${_scrollContainer.getBoundingClientRect()[this.get( 'orientation' ) === 'horizontal' ? 'width' : 'height']}px`;

applyVerticalStyles(this.scrollContainer, _scrollContainer.getBoundingClientRect());
applyVerticalStyles(this.screen, ViewportContainer.getBoundingClientRect());
Expand Down Expand Up @@ -71,11 +79,11 @@ export default class Visualization {
totalBefore,
totalAfter,
skipList,
_calculatedEstimateHeight
_calculatedEstimateSize
} = this.radar;

const isDynamic = !!skipList;
const itemHeights = isDynamic && skipList.values;
const itemSizes = isDynamic && skipList.values;

const firstVisualizedIndex = Math.max(firstItemIndex - 10, 0);
const lastVisualizedIndex = Math.min(lastItemIndex + 10, totalItems - 1);
Expand All @@ -97,9 +105,9 @@ export default class Visualization {
for (let itemIndex = firstVisualizedIndex, i = 0; itemIndex <= lastVisualizedIndex; itemIndex++, i++) {
const element = sats[i];

const itemHeight = isDynamic ? itemHeights[itemIndex] : _calculatedEstimateHeight;
const itemSize = isDynamic ? itemSizes[itemIndex] : _calculatedEstimateSize;

element.style.height = `${itemHeight}px`;
element.style[this.get( 'orientation' ) === 'horizontal' ? 'width' : 'height'] = `${itemSize}px`;
element.setAttribute('index', String(itemIndex));
element.innerText = String(itemIndex);

Expand All @@ -114,8 +122,8 @@ export default class Visualization {
}
}

this.itemContainer.style.paddingTop = `${totalBefore}px`;
this.itemContainer.style.paddingBottom = `${totalAfter}px`;
this.itemContainer.style[this.get( 'orientation' ) === 'horizontal' ? 'paddingLeft' : 'paddingTop'] = `${totalBefore}px`;
this.itemContainer.style[this.get( 'orientation' ) === 'horizontal' ? 'paddingRight' : 'paddingBottom'] = `${totalAfter}px`;
}

destroy() {
Expand Down
6 changes: 4 additions & 2 deletions addon/-debug/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import DebugMixin from './edge-visualization/debug-mixin';
import Collection from '../components/vertical-collection/component';
import VerticalCollection from '../components/vertical-collection/component';
import HorizontalCollection from '../components/horizontal-collection/component';

Collection.reopen(DebugMixin);
VerticalCollection.reopen(DebugMixin);
HorizontalCollection.reopen(DebugMixin);
6 changes: 4 additions & 2 deletions addon/-private/data-view/elements/occluded-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import document from '../../utils/document-shim';
let OC_IDENTITY = 0;

export default class OccludedContent {
constructor(tagName) {
constructor(tagName, orientation = 'vertical') {
this.id = `OC-${OC_IDENTITY++}`;
this.isOccludedContent = true;

this.orientation = orientation

// We check to see if the document exists in Fastboot. Since RAF won't run in
// Fastboot, we'll never have to use these text nodes for measurements, so they
// can be empty
if (document !== undefined) {
this.element = document.createElement(tagName);
this.element.className += 'occluded-content';
this.element.className += `occluded-content ${this.orientation}`;

this.upperBound = document.createTextNode('');
this.lowerBound = document.createTextNode('');
Expand Down
21 changes: 17 additions & 4 deletions addon/-private/data-view/elements/virtual-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import document from '../../utils/document-shim';
let VC_IDENTITY = 0;

export default class VirtualComponent {
constructor(content = null, index = null) {
constructor(content = null, index = null, orientation = 'vertical') {
this.id = `VC-${VC_IDENTITY++}`;

this.content = content;
this.index = index;
this.orientation = orientation;

// We check to see if the document exists in Fastboot. Since RAF won't run in
// Fastboot, we'll never have to use these text nodes for measurements, so they
Expand Down Expand Up @@ -46,13 +47,17 @@ export default class VirtualComponent {

let top = Infinity;
let bottom = -Infinity;
let left = Infinity;
let right = -Infinity;

while (upperBound !== lowerBound) {
upperBound = upperBound.nextSibling;

if (upperBound instanceof Element) {
top = Math.min(top, upperBound.getBoundingClientRect().top);
bottom = Math.max(bottom, upperBound.getBoundingClientRect().bottom);
left = Math.min(left, upperBound.getBoundingClientRect().left);
right = Math.max(right, upperBound.getBoundingClientRect().right);
}

if (DEBUG) {
Expand All @@ -62,15 +67,23 @@ export default class VirtualComponent {

const text = upperBound.textContent;

assert(`All content inside of vertical-collection must be wrapped in an element. Detected a text node with content: ${text}`, text === '' || text.match(/^\s+$/));
assert(`All content inside of ${this.orientation}-collection must be wrapped in an element. Detected a text node with content: ${text}`, text === '' || text.match(/^\s+$/));
}
}

assert('Items in a vertical collection require atleast one element in them', top !== Infinity && bottom !== -Infinity);
if( this.orientation === 'horizontal' )
{
assert(`Items in a ${this.orientation} collection require atleast one element in them`, left !== Infinity && right !== -Infinity);
}
else
{
assert(`Items in a ${this.orientation} collection require atleast one element in them`, top !== Infinity && bottom !== -Infinity);
}

const height = bottom - top;
const width = right - left;

return { top, bottom, height };
return { top, bottom, height, left, right, width };
}

recycle(newContent, newIndex) {
Expand Down
46 changes: 24 additions & 22 deletions addon/-private/data-view/radar/dynamic-radar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class DynamicRadar extends Radar {
this._totalBefore = 0;
this._totalAfter = 0;

this._minHeight = Infinity;
this._minSize = Infinity;

this._nextIncrementalRender = null;

Expand Down Expand Up @@ -60,24 +60,24 @@ export default class DynamicRadar extends Radar {
_updateConstants() {
super._updateConstants();

if (this._calculatedEstimateHeight < this._minHeight) {
this._minHeight = this._calculatedEstimateHeight;
if (this._calculatedEstimateSize < this._minSize) {
this._minSize = this._calculatedEstimateSize;
}

// Create the SkipList only after the estimateHeight has been calculated the first time
// Create the SkipList only after the estimateSize has been calculated the first time
if (this.skipList === null) {
this.skipList = new SkipList(this.totalItems, this._calculatedEstimateHeight);
this.skipList = new SkipList(this.totalItems, this._calculatedEstimateSize);
} else {
this.skipList.defaultValue = this._calculatedEstimateHeight;
this.skipList.defaultValue = this._calculatedEstimateSize;
}
}

_updateIndexes() {
const {
bufferSize,
skipList,
visibleTop,
visibleBottom,
visibleStart,
visibleEnd,
totalItems,

_didReset
Expand All @@ -101,8 +101,8 @@ export default class DynamicRadar extends Radar {

const { values } = skipList;

let { totalBefore, index: firstVisibleIndex } = this.skipList.find(visibleTop);
let { totalAfter, index: lastVisibleIndex } = this.skipList.find(visibleBottom);
let { totalBefore, index: firstVisibleIndex } = this.skipList.find(visibleStart);
let { totalAfter, index: lastVisibleIndex } = this.skipList.find(visibleEnd);

const maxIndex = totalItems - 1;

Expand Down Expand Up @@ -188,22 +188,24 @@ export default class DynamicRadar extends Radar {

const {
top: currentItemTop,
height: currentItemHeight
left: currentItemLeft,
height: currentItemHeight,
width: currentItemWidth
} = getScaledClientRect(currentItem, _transformScale);

let margin;

if (previousItem !== undefined) {
margin = currentItemTop - getScaledClientRect(previousItem, _transformScale).bottom;
margin = ( this.orientation === 'horizontal' ? currentItemLeft : currentItemTop ) - getScaledClientRect(previousItem, _transformScale)[ this.orientation === 'horizontal' ? 'right' : 'bottom'];
} else {
margin = currentItemTop - getScaledClientRect(_occludedContentBefore, _transformScale).bottom;
margin = ( this.orientation === 'horizontal' ? currentItemLeft : currentItemTop ) - getScaledClientRect(_occludedContentBefore, _transformScale)[ this.orientation === 'horizontal' ? 'right' : 'bottom'];
}

const newHeight = roundTo(currentItemHeight + margin);
const itemDelta = skipList.set(itemIndex, newHeight);
const newSize = roundTo( (this.orientation === 'horizontal' ? currentItemWidth : currentItemHeight ) + margin);
const itemDelta = skipList.set(itemIndex, newSize);

if (newHeight < this._minHeight) {
this._minHeight = newHeight;
if (newSize < this._minSize) {
this._minSize = newSize;
}

if (itemDelta !== 0) {
Expand All @@ -215,7 +217,7 @@ export default class DynamicRadar extends Radar {
}

_didEarthquake(scrollDiff) {
return scrollDiff > (this._minHeight / 2);
return scrollDiff > (this._minSize / 2);
}

get total() {
Expand All @@ -240,21 +242,21 @@ export default class DynamicRadar extends Radar {

get firstVisibleIndex() {
const {
visibleTop
visibleStart
} = this;

const { index } = this.skipList.find(visibleTop);
const { index } = this.skipList.find(visibleStart);

return index;
}

get lastVisibleIndex() {
const {
visibleBottom,
visibleEnd,
totalItems
} = this;

const { index } = this.skipList.find(visibleBottom);
const { index } = this.skipList.find(visibleEnd);

return Math.min(index, totalItems - 1);
}
Expand Down
Loading