mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
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:
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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;
|
@ -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];
|
||||
|
@ -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 = {};
|
||||
}
|
||||
|
Reference in New Issue
Block a user