refactor(components): update to use shadow DOM and work with css variables

- updates components to use shadow DOM or scoped if they require css variables
- moves global styles to an external stylesheet that needs to be imported
- adds support for additional colors and removes the Sass loops to generate colors for each component
- several property renames, bug fixes, and test updates

Co-authored-by: Manu Mtz.-Almeida <manu.mtza@gmail.com>
Co-authored-by: Adam Bradley <adambradley25@gmail.com>
Co-authored-by: Cam Wiegert <cam@camwiegert.com>
This commit is contained in:
Brandy Carney
2018-07-09 12:57:21 -04:00
parent a4659f03b4
commit a7f1f4daa7
710 changed files with 20999 additions and 20853 deletions

View File

@ -6,33 +6,32 @@
<title>Ionic Item Sliding</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Ionic CDN demo</ion-title>
<ion-buttons slot="primary">
<ion-button onclick="addItems()">Add Items</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<p>
<ion-header>
<ion-toolbar>
<ion-title>Ionic CDN demo</ion-title>
<ion-buttons slot="primary">
<ion-button onclick="addItems()">Add Items</ion-button>
</p>
<ion-virtual-scroll id="virtual"></ion-virtual-scroll>
<ion-infinite-scroll threshold="100px" id="infinite-scroll">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<p>
<ion-button onclick="addItems()">Add Items</ion-button>
</p>
<ion-virtual-scroll id="virtual"></ion-virtual-scroll>
<ion-infinite-scroll threshold="100px" id="infinite-scroll">
<ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
@ -40,11 +39,11 @@
<script>
const virtual = document.getElementById('virtual');
const items = Array.from({length: 100}, (x, i) => i);
const items = Array.from({ length: 100 }, (x, i) => i);
function addItems(append) {
if(!append) {
append = Array.from({length: 10}, (x, i) => "append" + i);
if (!append) {
append = Array.from({ length: 10 }, (x, i) => "append" + i);
}
items.push(...append);
virtual.markDirtyTail(append.length)
@ -91,7 +90,7 @@
const infiniteScroll = document.getElementById('infinite-scroll');
infiniteScroll.addEventListener('ionInfinite', async function() {
infiniteScroll.addEventListener('ionInfinite', async function () {
console.log('Loading data...');
const data = await getAsyncData();
infiniteScroll.complete();
@ -103,11 +102,12 @@
function getAsyncData() {
return new Promise(resolve => {
setTimeout(() => {
const data = Array.from({length: 10}, (x, i) => "append" + i);
const data = Array.from({ length: 10 }, (x, i) => "append" + i);
resolve(data);
}, 500);
});
}
</script>
</body>
</html>

View File

@ -6,21 +6,22 @@
<title>Ionic Item Sliding</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Ionic CDN demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-title>Ionic CDN demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-virtual-scroll id="virtual"></ion-virtual-scroll>
</ion-content>
<ion-content>
<ion-virtual-scroll id="virtual"></ion-virtual-scroll>
</ion-content>
@ -71,4 +72,5 @@
</script>
</body>
</html>

View File

@ -1,4 +1,6 @@
import { CellType, HeaderFn, ItemHeightFn, Range, VirtualNode, calcCells, calcHeightIndex, getRange, getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom } from '../virtual-scroll-utils';
import { HeaderFn, ItemHeightFn, VirtualNode } from '../../../interface';
import { CellType } from '../virtual-scroll-interface';
import { Range, calcCells, calcHeightIndex, getRange, getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom } from '../virtual-scroll-utils';
describe('getViewport', () => {
@ -111,7 +113,7 @@ describe('getRange', () => {
describe('resizeBuffer', () => {
it('should allocate a buffer', () => {
const buf = resizeBuffer(null, 10);
const buf = resizeBuffer(undefined, 10);
expect(buf.length).toEqual(10);
});
@ -148,7 +150,7 @@ describe('resizeBuffer', () => {
describe('calcCells', () => {
it('should calculate cells without headers and itemHeight', () => {
const items = ['0', 2, 'hola', {data: 'hello'}];
const cells = calcCells(items, null, null, null, 10, 20, 30, 0, 0, items.length);
const cells = calcCells(items, undefined, undefined, undefined, 10, 20, 30, 0, 0, items.length);
expect(cells).toEqual([
{
type: CellType.Item,
@ -192,12 +194,12 @@ describe('calcCells', () => {
it('should calculate cells with itemHeight', () => {
const items = [10, 9, 8];
let called = 0;
const itemHeight: ItemHeightFn = (item: any, index?: number) => {
const itemHeight: ItemHeightFn = (item: any, index: number) => {
expect(item).toEqual(items[index]);
called++;
return index * 20 + 20;
};
const cells = calcCells(items, itemHeight, null, null, 10, 20, 30, 0, 0, items.length);
const cells = calcCells(items, itemHeight, undefined, undefined, 10, 20, 30, 0, 0, items.length);
expect(called).toEqual(3);
expect(cells).toEqual([
@ -248,7 +250,7 @@ describe('calcCells', () => {
footerCalled++;
return (index === 2) ? 'my footer' : null;
};
const itemHeight: ItemHeightFn = (item: any, index?: number) => {
const itemHeight: ItemHeightFn = (item: any, index: number) => {
expect(item).toEqual(items[index]);
called++;
return index * 20 + 20;
@ -317,8 +319,8 @@ describe('calcHeightIndex', () => {
const footerFn: HeaderFn = (_, index) => {
return (index === 2) ? 'my footer' : null;
};
const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50, 0, 0, items.length);
const buf = resizeBuffer(null, cells.length);
const cells = calcCells(items, undefined, headerFn, footerFn, 10, 20, 50, 0, 0, items.length);
const buf = resizeBuffer(undefined, cells.length);
const totalHeight = calcHeightIndex(buf, cells, 0);
expect(buf.length).toEqual(7);
expect(buf[0]).toEqual(0);
@ -502,12 +504,12 @@ describe('updateVDom', () => {
function mockVirtualScroll(
items: any[],
itemHeight: ItemHeightFn = null,
headerFn: HeaderFn = null,
footerFn: HeaderFn = null
itemHeight?: ItemHeightFn,
headerFn?: HeaderFn,
footerFn?: HeaderFn
) {
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 10, 30, 0, 0, items.length);
const heightIndex = resizeBuffer(null, cells.length);
const heightIndex = resizeBuffer(undefined, cells.length);
calcHeightIndex(heightIndex, cells, 0);
return { items, heightIndex, cells };
}

View File

@ -0,0 +1,35 @@
export const enum CellType {
Item,
Header,
Footer
}
export const enum NodeChange {
NoChange,
Position,
Cell,
}
export interface Cell {
i: number;
index: number;
value: any;
type: CellType;
height: number;
reads: number;
visible: boolean;
}
export interface VirtualNode {
cell: Cell;
top: number;
change: NodeChange;
d: boolean;
visible: boolean;
}
export type HeaderFn = (item: any, index: number, items: any[]) => string | null;
export type ItemHeightFn = (item: any, index: number) => number;
export type ItemRenderFn = (el: HTMLElement|null, cell: Cell, domIndex: number) => HTMLElement;
export type DomRenderFn = (dom: VirtualNode[]) => void;

View File

@ -1,3 +1,6 @@
import { Cell, HeaderFn, ItemHeightFn, ItemRenderFn, VirtualNode } from '../../interface';
import { CellType, NodeChange } from './virtual-scroll-interface';
export interface Viewport {
top: number;
bottom: number;
@ -8,43 +11,8 @@ export interface Range {
length: number;
}
export const enum CellType {
Item,
Header,
Footer
}
export const enum NodeChange {
NoChange,
Position,
Cell,
}
export interface Cell {
i: number;
index: number;
value: any;
type: CellType;
height: number;
reads: number;
visible: boolean;
}
export interface VirtualNode {
cell: Cell;
top: number;
change: NodeChange;
d: boolean;
visible: boolean;
}
const MIN_READS = 2;
export type HeaderFn = (item: any, index: number, items: any[]) => string | null;
export type ItemHeightFn = (item: any, index?: number) => number;
export type ItemRenderFn = (el: HTMLElement|null, cell: Cell, domIndex?: number) => HTMLElement;
export type DomRenderFn = (dom: VirtualNode[]) => void;
export function updateVDom(dom: VirtualNode[], heightIndex: Uint32Array, cells: Cell[], range: Range) {
// reset dom
for (const node of dom) {
@ -227,7 +195,7 @@ export function calcCells(
offset: number,
len: number
): Cell[] {
const cells = [];
const cells: Cell[] = [];
const end = len + offset;
for (let i = offset; i < end; i++) {
const item = items[i];

View File

@ -1,8 +1,10 @@
import { Component, Element, EventListenerEnable, Listen, Method, Prop, Watch } from '@stencil/core';
import { QueueController } from '../../interface';
import { Cell, CellType, DomRenderFn, HeaderFn, ItemHeightFn,
ItemRenderFn, Range,
VirtualNode, calcCells, calcHeightIndex, doRender,
import { Component, Element, EventListenerEnable, Listen, Method, Prop, QueueApi, State, Watch } from '@stencil/core';
import { Cell, DomRenderFn, HeaderFn, ItemHeightFn, ItemRenderFn, VirtualNode } from '../../interface';
import { CellType } from './virtual-scroll-interface';
import {
Range,
calcCells, calcHeightIndex, doRender,
findCellIndex, getRange, getShouldUpdate, getViewport,
inplaceUpdate, positionForIndex, resizeBuffer, updateVDom } from './virtual-scroll-utils';
@ -24,13 +26,13 @@ export class VirtualScroll {
private viewportOffset = 0;
private currentScrollTop = 0;
private indexDirty = 0;
private totalHeight = 0;
private heightChanged = false;
private lastItemLen = 0;
@Element() el!: HTMLStencilElement;
@Prop({ context: 'queue' }) queue!: QueueController;
@State() totalHeight = 0;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'enableListener' }) enableListener!: EventListenerEnable;
@Prop({ context: 'window' }) win!: Window;
@ -116,13 +118,13 @@ export class VirtualScroll {
}
componentDidLoad() {
const scrollEl = this.el.closest('ion-scroll');
if (!scrollEl) {
const contentEl = this.el.closest('ion-content');
if (!contentEl) {
console.error('virtual-scroll must be used inside ion-scroll/ion-content');
return;
}
this.scrollEl = scrollEl;
scrollEl.componentOnReady().then(() => {
contentEl.componentOnReady().then(() => {
this.scrollEl = contentEl.getScrollElement();
this.calcDimensions();
this.calcCells();
this.updateState();
@ -204,29 +206,33 @@ export class VirtualScroll {
}
private updateVirtualScroll() {
// do nothing if there is a scheduled update
// do nothing if virtual-scroll is disabled
if (!this.isEnabled || !this.scrollEl) {
return;
}
// unschedule future updates
if (this.timerUpdate) {
clearTimeout(this.timerUpdate);
this.timerUpdate = null;
}
// schedule DOM operations into the stencil queue
this.queue.read(this.readVS.bind(this));
this.queue.write(this.writeVS.bind(this));
}
private readVS() {
const { scrollEl, el } = this;
let topOffset = 0;
let node: HTMLElement | null = this.el;
while (node && node !== this.scrollEl) {
let node: HTMLElement | null = el;
while (node && node !== scrollEl) {
topOffset += node.offsetTop;
node = node.parentElement;
}
this.viewportOffset = topOffset;
if (this.scrollEl) {
this.currentScrollTop = this.scrollEl.scrollTop;
if (scrollEl) {
this.currentScrollTop = scrollEl.scrollTop;
}
}
@ -258,7 +264,8 @@ export class VirtualScroll {
range
);
// write DOM
// Write DOM
// Different code paths taken depending of the render API used
if (this.nodeRender) {
doRender(this.el, this.nodeRender, this.virtualDom, this.updateCellHeight.bind(this));
} else if (this.domRender) {
@ -266,13 +273,9 @@ export class VirtualScroll {
} else if (this.renderItem) {
this.el.forceUpdate();
}
if (this.heightChanged) {
this.el.style.height = this.totalHeight + 'px';
this.heightChanged = false;
}
}
private updateCellHeight(cell: Cell, node: HTMLStencilElement | HTMLElement) {
private updateCellHeight(cell: Cell, node: HTMLElement) {
const update = () => {
if ((node as any)['$ionCell'] === cell) {
const style = this.win.getComputedStyle(node);
@ -351,12 +354,8 @@ export class VirtualScroll {
private calcHeightIndex(index = 0) {
// TODO: optimize, we don't need to calculate all the cells
this.heightIndex = resizeBuffer(this.heightIndex, this.cells.length);
const totalHeight = calcHeightIndex(this.heightIndex, this.cells, index);
if (totalHeight !== this.totalHeight) {
console.debug(`[virtual] total height changed: ${this.totalHeight}px -> ${totalHeight}px`);
this.totalHeight = totalHeight;
this.heightChanged = true;
}
this.totalHeight = calcHeightIndex(this.heightIndex, this.cells, index);
console.debug('[virtual] height index recalculated', this.heightIndex.length - index);
this.indexDirty = Infinity;
}
@ -374,15 +373,23 @@ export class VirtualScroll {
}
}
renderVirtualNode(node: VirtualNode) {
const cell = node.cell;
switch (cell.type) {
case CellType.Item: return this.renderItem!(cell.value, cell.index);
case CellType.Header: return this.renderHeader!(cell.value, cell.index);
case CellType.Footer: return this.renderFooter!(cell.value, cell.index);
private renderVirtualNode(node: VirtualNode) {
const { type, value, index } = node.cell;
switch (type) {
case CellType.Item: return this.renderItem!(value, index);
case CellType.Header: return this.renderHeader!(value, index);
case CellType.Footer: return this.renderFooter!(value, index);
}
}
hostData() {
return {
style: {
height: `${this.totalHeight}px`
}
};
}
render() {
const renderItem = this.renderItem;
if (renderItem) {
@ -392,11 +399,10 @@ export class VirtualScroll {
if (!item.vattrs) {
item.vattrs = {};
}
item.vattrs.class += ' virtual-item';
if (!node.visible) {
classes.push('virtual-loading');
}
item.vattrs.class += ' ' + classes.join(' ');
item.vattrs.class += classes.join(' ');
if (!item.vattrs.style) {
item.vattrs.style = {};
}