perf-web-components 0.1.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.
- package/README.md +88 -0
- package/dist/components/virtual-list/virtual-list.d.ts +46 -0
- package/dist/components/virtual-list/virtual-list.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.cjs +43 -0
- package/dist/index.umd.cjs.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# perf-web-components
|
|
2
|
+
|
|
3
|
+
High-performance, framework-agnostic web components built with native Custom Elements.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install perf-web-components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import 'perf-web-components';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Components
|
|
16
|
+
|
|
17
|
+
### `<virtual-list>`
|
|
18
|
+
|
|
19
|
+
A virtualized list/grid that only renders the DOM nodes needed to fill the viewport, regardless of how many items are in the dataset. Useful for rendering large collections (thousands+ of rows) without the performance cost of mounting every item.
|
|
20
|
+
|
|
21
|
+
#### Attributes / Properties
|
|
22
|
+
|
|
23
|
+
| Attribute | Property | Type | Default | Description |
|
|
24
|
+
|------------------------|----------------------|-----------------------------|--------------|---------------------------------------------------------------------------|
|
|
25
|
+
| `max-nodes` | `maxNodes` | `number` | `20` | Size of the reused DOM node pool (number of items rendered at once). |
|
|
26
|
+
| `estimated-item-size` | `estimatedItemSize` | `number` | `50` | Estimated height (vertical) or width (horizontal) of each item, in px. |
|
|
27
|
+
| `scroll-direction` | `scrollDirection` | `'vertical' \| 'horizontal'`| `'vertical'` | Scroll axis for the list. |
|
|
28
|
+
| — | `items` | `T[]` | `[]` | The data array to render. Must be set via the JS property, not an attribute. |
|
|
29
|
+
|
|
30
|
+
#### Item templates
|
|
31
|
+
|
|
32
|
+
Provide a `<template>` child to control markup per item. Use `{{ }}` interpolation to bind fields:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<virtual-list max-nodes="10" estimated-item-size="60">
|
|
36
|
+
<template>
|
|
37
|
+
<div class="row">{{ index }}: {{ name }}</div>
|
|
38
|
+
</template>
|
|
39
|
+
</virtual-list>
|
|
40
|
+
|
|
41
|
+
<script type="module">
|
|
42
|
+
const list = document.querySelector('virtual-list');
|
|
43
|
+
list.items = [{ name: 'Item 1' }, { name: 'Item 2' } /* ... */];
|
|
44
|
+
</script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If no template is provided, each item is rendered as `JSON.stringify(item)` (or `String(item)` for primitives).
|
|
48
|
+
|
|
49
|
+
#### Framework Bindings (Angular, React, Vue, Lit, Svelte)
|
|
50
|
+
|
|
51
|
+
Modern frameworks bind directly to the Web Component's JS property, not just its attributes. `items` must always be set as a property since arrays cannot be passed through HTML attributes.
|
|
52
|
+
|
|
53
|
+
**Angular**
|
|
54
|
+
```html
|
|
55
|
+
<virtual-list [items]="itemArray" max-nodes="5" estimated-item-size="60"></virtual-list>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Vue**
|
|
59
|
+
```html
|
|
60
|
+
<virtual-list :items="itemArray" max-nodes="5" estimated-item-size="60"></virtual-list>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**React**
|
|
64
|
+
```jsx
|
|
65
|
+
<virtual-list
|
|
66
|
+
ref={el => el && (el.items = itemArray)}
|
|
67
|
+
max-nodes="5"
|
|
68
|
+
estimated-item-size="60"
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Lit**
|
|
73
|
+
```html
|
|
74
|
+
<virtual-list .items=${itemArray} max-nodes="5" estimated-item-size="60"></virtual-list>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Svelte**
|
|
78
|
+
```svelte
|
|
79
|
+
<virtual-list this={el} max-nodes="5" estimated-item-size="60" use:setItems={itemArray}></virtual-list>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run dev # start vite dev server
|
|
86
|
+
npm run build # type-check and build the library
|
|
87
|
+
npm run preview # preview the production build
|
|
88
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type ScrollDirection = 'vertical' | 'horizontal';
|
|
2
|
+
export interface VirtualListOptions {
|
|
3
|
+
maxNodes?: number;
|
|
4
|
+
estimatedItemSize?: number;
|
|
5
|
+
scrollDirection?: ScrollDirection;
|
|
6
|
+
}
|
|
7
|
+
export declare class VirtualList<T = Record<string, unknown>> extends HTMLElement {
|
|
8
|
+
private _items;
|
|
9
|
+
private _maxNodes;
|
|
10
|
+
private _scrollDirection;
|
|
11
|
+
private _nodePool;
|
|
12
|
+
private _estimatedItemSize;
|
|
13
|
+
private _userTemplate;
|
|
14
|
+
private _pendingFrame;
|
|
15
|
+
private $viewport;
|
|
16
|
+
private $phantom;
|
|
17
|
+
private $content;
|
|
18
|
+
private $slot;
|
|
19
|
+
constructor();
|
|
20
|
+
static get observedAttributes(): string[];
|
|
21
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
22
|
+
connectedCallback(): void;
|
|
23
|
+
disconnectedCallback(): void;
|
|
24
|
+
private _onSlotChange;
|
|
25
|
+
private _getTemplate;
|
|
26
|
+
set items(data: T[]);
|
|
27
|
+
get items(): T[];
|
|
28
|
+
set maxNodes(val: number);
|
|
29
|
+
get maxNodes(): number;
|
|
30
|
+
set estimatedItemSize(val: number);
|
|
31
|
+
get estimatedItemSize(): number;
|
|
32
|
+
set scrollDirection(val: ScrollDirection);
|
|
33
|
+
get scrollDirection(): ScrollDirection;
|
|
34
|
+
private _buildNodePool;
|
|
35
|
+
private _updateLayout;
|
|
36
|
+
private _onScroll;
|
|
37
|
+
private _render;
|
|
38
|
+
private _interpolate;
|
|
39
|
+
private _escapeHtml;
|
|
40
|
+
}
|
|
41
|
+
declare global {
|
|
42
|
+
interface HTMLElementTagNameMap {
|
|
43
|
+
'virtual-list': VirtualList;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=virtual-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtual-list.d.ts","sourceRoot":"","sources":["../../../src/components/virtual-list/virtual-list.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,YAAY,CAAC;AAExD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,qBAAa,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,WAAW;IACvE,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,kBAAkB,CAAc;IACxC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,KAAK,CAAyB;;IA8DtC,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAED,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAiB9F,iBAAiB,IAAI,IAAI;IAYzB,oBAAoB,IAAI,IAAI;IAW5B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,YAAY;IAUpB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,EAclB;IAED,IAAI,KAAK,IAAI,CAAC,EAAE,CAEf;IAED,IAAI,QAAQ,CAAC,GAAG,EAAE,MAAM,EAEvB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAEhC;IAED,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,IAAI,eAAe,CAAC,GAAG,EAAE,eAAe,EAEvC;IAED,IAAI,eAAe,IAAI,eAAe,CAErC;IAED,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,OAAO;IA2Cf,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,WAAW;CAQpB;AAOD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,cAAc,EAAE,WAAW,CAAC;KAC7B;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AACxE,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/components/virtual-list/virtual-list.js
|
|
2
|
+
var e = class extends HTMLElement {
|
|
3
|
+
_items = [];
|
|
4
|
+
_maxNodes = 20;
|
|
5
|
+
_scrollDirection = "vertical";
|
|
6
|
+
_nodePool = [];
|
|
7
|
+
_estimatedItemSize = 50;
|
|
8
|
+
_userTemplate = null;
|
|
9
|
+
_pendingFrame = null;
|
|
10
|
+
$viewport;
|
|
11
|
+
$phantom;
|
|
12
|
+
$content;
|
|
13
|
+
$slot;
|
|
14
|
+
constructor() {
|
|
15
|
+
if (super(), this.attachShadow({ mode: "open" }), !this.shadowRoot) throw Error("Shadow root initialization failed");
|
|
16
|
+
this.shadowRoot.innerHTML = "\n <style>\n :host {\n display: block;\n position: relative;\n contain: strict;\n }\n .viewport {\n width: 100%;\n height: 100%;\n overflow: auto;\n position: relative;\n -webkit-overflow-scrolling: touch;\n }\n .phantom {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n }\n .content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n will-change: transform;\n }\n .content.horizontal {\n display: flex;\n flex-direction: row;\n width: max-content;\n height: 100%;\n }\n </style>\n\n <div class=\"viewport\" id=\"viewport\">\n <div class=\"phantom\" id=\"phantom\"></div>\n <div class=\"content\" id=\"content\"></div>\n <slot id=\"slot\" style=\"display: none;\"></slot>\n </div>\n ", this.$viewport = this.shadowRoot.getElementById("viewport"), this.$phantom = this.shadowRoot.getElementById("phantom"), this.$content = this.shadowRoot.getElementById("content"), this.$slot = this.shadowRoot.getElementById("slot"), this._onScroll = this._onScroll.bind(this), this._onSlotChange = this._onSlotChange.bind(this);
|
|
17
|
+
}
|
|
18
|
+
static get observedAttributes() {
|
|
19
|
+
return [
|
|
20
|
+
"max-nodes",
|
|
21
|
+
"scroll-direction",
|
|
22
|
+
"estimated-item-size"
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
attributeChangedCallback(e, t, n) {
|
|
26
|
+
if (t !== n) {
|
|
27
|
+
if (e === "max-nodes") {
|
|
28
|
+
let e = parseInt(n ?? "", 10);
|
|
29
|
+
this._maxNodes = Number.isNaN(e) ? 20 : e, this._buildNodePool();
|
|
30
|
+
} else if (e === "scroll-direction") this._scrollDirection = n === "horizontal" ? "horizontal" : "vertical";
|
|
31
|
+
else if (e === "estimated-item-size") {
|
|
32
|
+
let e = parseFloat(n ?? "");
|
|
33
|
+
this._estimatedItemSize = Number.isNaN(e) ? 50 : e;
|
|
34
|
+
}
|
|
35
|
+
this._updateLayout(), this._render();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
connectedCallback() {
|
|
39
|
+
this.$viewport.addEventListener("scroll", this._onScroll, { passive: !0 }), this.$slot && this.$slot.addEventListener("slotchange", this._onSlotChange), this._getTemplate(), this._buildNodePool(), this._updateLayout(), this._render();
|
|
40
|
+
}
|
|
41
|
+
disconnectedCallback() {
|
|
42
|
+
this.$viewport.removeEventListener("scroll", this._onScroll), this.$slot && this.$slot.removeEventListener("slotchange", this._onSlotChange), this._pendingFrame !== null && (cancelAnimationFrame(this._pendingFrame), this._pendingFrame = null);
|
|
43
|
+
}
|
|
44
|
+
_onSlotChange() {
|
|
45
|
+
this._userTemplate = null, this._getTemplate(), this._render();
|
|
46
|
+
}
|
|
47
|
+
_getTemplate() {
|
|
48
|
+
if (!this._userTemplate) {
|
|
49
|
+
let e = this.querySelector("template");
|
|
50
|
+
e && (this._userTemplate = e.innerHTML);
|
|
51
|
+
}
|
|
52
|
+
return this._userTemplate;
|
|
53
|
+
}
|
|
54
|
+
set items(e) {
|
|
55
|
+
let t = Array.isArray(e) ? e : [];
|
|
56
|
+
this._items = new Proxy(t, { set: (e, t, n, r) => {
|
|
57
|
+
let i = Reflect.set(e, t, n, r);
|
|
58
|
+
return this._updateLayout(), this._render(), i;
|
|
59
|
+
} }), this._updateLayout(), this._render();
|
|
60
|
+
}
|
|
61
|
+
get items() {
|
|
62
|
+
return this._items;
|
|
63
|
+
}
|
|
64
|
+
set maxNodes(e) {
|
|
65
|
+
this.setAttribute("max-nodes", String(e));
|
|
66
|
+
}
|
|
67
|
+
get maxNodes() {
|
|
68
|
+
return this._maxNodes;
|
|
69
|
+
}
|
|
70
|
+
set estimatedItemSize(e) {
|
|
71
|
+
this.setAttribute("estimated-item-size", String(e));
|
|
72
|
+
}
|
|
73
|
+
get estimatedItemSize() {
|
|
74
|
+
return this._estimatedItemSize;
|
|
75
|
+
}
|
|
76
|
+
set scrollDirection(e) {
|
|
77
|
+
this.setAttribute("scroll-direction", e);
|
|
78
|
+
}
|
|
79
|
+
get scrollDirection() {
|
|
80
|
+
return this._scrollDirection;
|
|
81
|
+
}
|
|
82
|
+
_buildNodePool() {
|
|
83
|
+
if (!this.$content) return;
|
|
84
|
+
this.$content.innerHTML = "", this._nodePool = [];
|
|
85
|
+
let e = document.createDocumentFragment();
|
|
86
|
+
for (let t = 0; t < this._maxNodes; t++) {
|
|
87
|
+
let t = document.createElement("div");
|
|
88
|
+
this._nodePool.push(t), e.appendChild(t);
|
|
89
|
+
}
|
|
90
|
+
this.$content.appendChild(e);
|
|
91
|
+
}
|
|
92
|
+
_updateLayout() {
|
|
93
|
+
if (!this.$phantom || !this.$content) return;
|
|
94
|
+
let e = this._scrollDirection === "horizontal", t = this._items.length * this._estimatedItemSize;
|
|
95
|
+
e ? (this.$phantom.style.width = `${t}px`, this.$phantom.style.height = "100%", this.$content.classList.add("horizontal")) : (this.$phantom.style.height = `${t}px`, this.$phantom.style.width = "100%", this.$content.classList.remove("horizontal"));
|
|
96
|
+
}
|
|
97
|
+
_onScroll() {
|
|
98
|
+
this._pendingFrame === null && (this._pendingFrame = requestAnimationFrame(() => {
|
|
99
|
+
this._pendingFrame = null, this._render();
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
_render() {
|
|
103
|
+
if (!this.$viewport || !this._nodePool.length) return;
|
|
104
|
+
if (!this._items.length) {
|
|
105
|
+
for (let e of this._nodePool) e.style.display = "none";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let e = this._scrollDirection === "horizontal", t = e ? this.$viewport.scrollLeft : this.$viewport.scrollTop, n = Math.floor(t / this._estimatedItemSize), r = Math.max(0, this._items.length - this._maxNodes);
|
|
109
|
+
n = Math.min(Math.max(0, n), r);
|
|
110
|
+
let i = n * this._estimatedItemSize;
|
|
111
|
+
this.$content.style.transform = e ? `translate3d(${i}px, 0, 0)` : `translate3d(0, ${i}px, 0)`;
|
|
112
|
+
let a = this._getTemplate();
|
|
113
|
+
for (let e = 0; e < this._maxNodes; e++) {
|
|
114
|
+
let t = n + e, r = this._nodePool[e];
|
|
115
|
+
if (t < this._items.length) {
|
|
116
|
+
r.style.display = "";
|
|
117
|
+
let e = this._items[t];
|
|
118
|
+
a ? r.innerHTML = this._interpolate(a, e, t) : r.textContent = typeof e == "object" ? JSON.stringify(e) : String(e);
|
|
119
|
+
} else r.style.display = "none";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
_interpolate(e, t, n) {
|
|
123
|
+
return e.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (e, r) => {
|
|
124
|
+
if (r === "index") return String(n);
|
|
125
|
+
if (r === "item") return this._escapeHtml(typeof t == "object" && t ? JSON.stringify(t) : String(t));
|
|
126
|
+
let i = (r.startsWith("item.") ? r.slice(5) : r).split(".").reduce((e, t) => e && e[t] !== void 0 ? e[t] : void 0, t);
|
|
127
|
+
return i === void 0 ? "" : this._escapeHtml(String(i));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
_escapeHtml(e) {
|
|
131
|
+
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
typeof window < "u" && !customElements.get("virtual-list") && customElements.define("virtual-list", e);
|
|
135
|
+
//#endregion
|
|
136
|
+
export { e as VirtualList };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/components/virtual-list/virtual-list.js"],"sourcesContent":["export class VirtualList extends HTMLElement {\n _items = [];\n _maxNodes = 20;\n _scrollDirection = 'vertical';\n _nodePool = [];\n _estimatedItemSize = 50;\n _userTemplate = null;\n _pendingFrame = null;\n $viewport;\n $phantom;\n $content;\n $slot;\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n if (!this.shadowRoot) {\n throw new Error('Shadow root initialization failed');\n }\n this.shadowRoot.innerHTML = `\n <style>\n :host {\n display: block;\n position: relative;\n contain: strict;\n }\n .viewport {\n width: 100%;\n height: 100%;\n overflow: auto;\n position: relative;\n -webkit-overflow-scrolling: touch;\n }\n .phantom {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n }\n .content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n will-change: transform;\n }\n .content.horizontal {\n display: flex;\n flex-direction: row;\n width: max-content;\n height: 100%;\n }\n </style>\n\n <div class=\"viewport\" id=\"viewport\">\n <div class=\"phantom\" id=\"phantom\"></div>\n <div class=\"content\" id=\"content\"></div>\n <slot id=\"slot\" style=\"display: none;\"></slot>\n </div>\n `;\n this.$viewport = this.shadowRoot.getElementById('viewport');\n this.$phantom = this.shadowRoot.getElementById('phantom');\n this.$content = this.shadowRoot.getElementById('content');\n this.$slot = this.shadowRoot.getElementById('slot');\n this._onScroll = this._onScroll.bind(this);\n this._onSlotChange = this._onSlotChange.bind(this);\n }\n static get observedAttributes() {\n return ['max-nodes', 'scroll-direction', 'estimated-item-size'];\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === 'max-nodes') {\n const parsed = parseInt(newValue ?? '', 10);\n this._maxNodes = Number.isNaN(parsed) ? 20 : parsed;\n this._buildNodePool();\n }\n else if (name === 'scroll-direction') {\n this._scrollDirection = newValue === 'horizontal' ? 'horizontal' : 'vertical';\n }\n else if (name === 'estimated-item-size') {\n const parsed = parseFloat(newValue ?? '');\n this._estimatedItemSize = Number.isNaN(parsed) ? 50 : parsed;\n }\n this._updateLayout();\n this._render();\n }\n connectedCallback() {\n this.$viewport.addEventListener('scroll', this._onScroll, { passive: true });\n if (this.$slot) {\n this.$slot.addEventListener('slotchange', this._onSlotChange);\n }\n this._getTemplate();\n this._buildNodePool();\n this._updateLayout();\n this._render();\n }\n disconnectedCallback() {\n this.$viewport.removeEventListener('scroll', this._onScroll);\n if (this.$slot) {\n this.$slot.removeEventListener('slotchange', this._onSlotChange);\n }\n if (this._pendingFrame !== null) {\n cancelAnimationFrame(this._pendingFrame);\n this._pendingFrame = null;\n }\n }\n _onSlotChange() {\n this._userTemplate = null;\n this._getTemplate();\n this._render();\n }\n _getTemplate() {\n if (!this._userTemplate) {\n const templateTag = this.querySelector('template');\n if (templateTag) {\n this._userTemplate = templateTag.innerHTML;\n }\n }\n return this._userTemplate;\n }\n set items(data) {\n const list = Array.isArray(data) ? data : [];\n this._items = new Proxy(list, {\n set: (target, property, value, receiver) => {\n const success = Reflect.set(target, property, value, receiver);\n this._updateLayout();\n this._render();\n return success;\n }\n });\n this._updateLayout();\n this._render();\n }\n get items() {\n return this._items;\n }\n set maxNodes(val) {\n this.setAttribute('max-nodes', String(val));\n }\n get maxNodes() {\n return this._maxNodes;\n }\n set estimatedItemSize(val) {\n this.setAttribute('estimated-item-size', String(val));\n }\n get estimatedItemSize() {\n return this._estimatedItemSize;\n }\n set scrollDirection(val) {\n this.setAttribute('scroll-direction', val);\n }\n get scrollDirection() {\n return this._scrollDirection;\n }\n _buildNodePool() {\n if (!this.$content)\n return;\n this.$content.innerHTML = '';\n this._nodePool = [];\n const fragment = document.createDocumentFragment();\n for (let i = 0; i < this._maxNodes; i++) {\n const nodeWrapper = document.createElement('div');\n this._nodePool.push(nodeWrapper);\n fragment.appendChild(nodeWrapper);\n }\n this.$content.appendChild(fragment);\n }\n _updateLayout() {\n if (!this.$phantom || !this.$content)\n return;\n const isHoriz = this._scrollDirection === 'horizontal';\n const totalSize = this._items.length * this._estimatedItemSize;\n if (isHoriz) {\n this.$phantom.style.width = `${totalSize}px`;\n this.$phantom.style.height = '100%';\n this.$content.classList.add('horizontal');\n }\n else {\n this.$phantom.style.height = `${totalSize}px`;\n this.$phantom.style.width = '100%';\n this.$content.classList.remove('horizontal');\n }\n }\n _onScroll() {\n if (this._pendingFrame !== null)\n return;\n this._pendingFrame = requestAnimationFrame(() => {\n this._pendingFrame = null;\n this._render();\n });\n }\n _render() {\n if (!this.$viewport || !this._nodePool.length)\n return;\n if (!this._items.length) {\n for (const node of this._nodePool) {\n node.style.display = 'none';\n }\n return;\n }\n const isHoriz = this._scrollDirection === 'horizontal';\n const scrollOffset = isHoriz ? this.$viewport.scrollLeft : this.$viewport.scrollTop;\n let startIndex = Math.floor(scrollOffset / this._estimatedItemSize);\n const maxStartIndex = Math.max(0, this._items.length - this._maxNodes);\n startIndex = Math.min(Math.max(0, startIndex), maxStartIndex);\n const offset = startIndex * this._estimatedItemSize;\n this.$content.style.transform = isHoriz\n ? `translate3d(${offset}px, 0, 0)`\n : `translate3d(0, ${offset}px, 0)`;\n const userTemplate = this._getTemplate();\n for (let i = 0; i < this._maxNodes; i++) {\n const dataIndex = startIndex + i;\n const node = this._nodePool[i];\n if (dataIndex < this._items.length) {\n node.style.display = '';\n const itemData = this._items[dataIndex];\n if (userTemplate) {\n node.innerHTML = this._interpolate(userTemplate, itemData, dataIndex);\n }\n else {\n node.textContent = typeof itemData === 'object' ? JSON.stringify(itemData) : String(itemData);\n }\n }\n else {\n node.style.display = 'none';\n }\n }\n }\n _interpolate(templateStr, item, index) {\n return templateStr.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_, key) => {\n if (key === 'index')\n return String(index);\n if (key === 'item') {\n return this._escapeHtml(typeof item === 'object' && item !== null ? JSON.stringify(item) : String(item));\n }\n const path = key.startsWith('item.') ? key.slice(5) : key;\n const val = path.split('.').reduce((obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined), item);\n return val !== undefined ? this._escapeHtml(String(val)) : '';\n });\n }\n _escapeHtml(value) {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n}\n// Auto-register custom element if window is available\nif (typeof window !== 'undefined' && !customElements.get('virtual-list')) {\n customElements.define('virtual-list', VirtualList);\n}\n//# sourceMappingURL=virtual-list.js.map"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.PerfWebComponents={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=class extends HTMLElement{_items=[];_maxNodes=20;_scrollDirection=`vertical`;_nodePool=[];_estimatedItemSize=50;_userTemplate=null;_pendingFrame=null;$viewport;$phantom;$content;$slot;constructor(){if(super(),this.attachShadow({mode:`open`}),!this.shadowRoot)throw Error(`Shadow root initialization failed`);this.shadowRoot.innerHTML=`
|
|
2
|
+
<style>
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
position: relative;
|
|
6
|
+
contain: strict;
|
|
7
|
+
}
|
|
8
|
+
.viewport {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
overflow: auto;
|
|
12
|
+
position: relative;
|
|
13
|
+
-webkit-overflow-scrolling: touch;
|
|
14
|
+
}
|
|
15
|
+
.phantom {
|
|
16
|
+
position: absolute;
|
|
17
|
+
top: 0;
|
|
18
|
+
left: 0;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
21
|
+
.content {
|
|
22
|
+
position: absolute;
|
|
23
|
+
top: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
will-change: transform;
|
|
28
|
+
}
|
|
29
|
+
.content.horizontal {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: row;
|
|
32
|
+
width: max-content;
|
|
33
|
+
height: 100%;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
|
|
37
|
+
<div class="viewport" id="viewport">
|
|
38
|
+
<div class="phantom" id="phantom"></div>
|
|
39
|
+
<div class="content" id="content"></div>
|
|
40
|
+
<slot id="slot" style="display: none;"></slot>
|
|
41
|
+
</div>
|
|
42
|
+
`,this.$viewport=this.shadowRoot.getElementById(`viewport`),this.$phantom=this.shadowRoot.getElementById(`phantom`),this.$content=this.shadowRoot.getElementById(`content`),this.$slot=this.shadowRoot.getElementById(`slot`),this._onScroll=this._onScroll.bind(this),this._onSlotChange=this._onSlotChange.bind(this)}static get observedAttributes(){return[`max-nodes`,`scroll-direction`,`estimated-item-size`]}attributeChangedCallback(e,t,n){if(t!==n){if(e===`max-nodes`){let e=parseInt(n??``,10);this._maxNodes=Number.isNaN(e)?20:e,this._buildNodePool()}else if(e===`scroll-direction`)this._scrollDirection=n===`horizontal`?`horizontal`:`vertical`;else if(e===`estimated-item-size`){let e=parseFloat(n??``);this._estimatedItemSize=Number.isNaN(e)?50:e}this._updateLayout(),this._render()}}connectedCallback(){this.$viewport.addEventListener(`scroll`,this._onScroll,{passive:!0}),this.$slot&&this.$slot.addEventListener(`slotchange`,this._onSlotChange),this._getTemplate(),this._buildNodePool(),this._updateLayout(),this._render()}disconnectedCallback(){this.$viewport.removeEventListener(`scroll`,this._onScroll),this.$slot&&this.$slot.removeEventListener(`slotchange`,this._onSlotChange),this._pendingFrame!==null&&(cancelAnimationFrame(this._pendingFrame),this._pendingFrame=null)}_onSlotChange(){this._userTemplate=null,this._getTemplate(),this._render()}_getTemplate(){if(!this._userTemplate){let e=this.querySelector(`template`);e&&(this._userTemplate=e.innerHTML)}return this._userTemplate}set items(e){let t=Array.isArray(e)?e:[];this._items=new Proxy(t,{set:(e,t,n,r)=>{let i=Reflect.set(e,t,n,r);return this._updateLayout(),this._render(),i}}),this._updateLayout(),this._render()}get items(){return this._items}set maxNodes(e){this.setAttribute(`max-nodes`,String(e))}get maxNodes(){return this._maxNodes}set estimatedItemSize(e){this.setAttribute(`estimated-item-size`,String(e))}get estimatedItemSize(){return this._estimatedItemSize}set scrollDirection(e){this.setAttribute(`scroll-direction`,e)}get scrollDirection(){return this._scrollDirection}_buildNodePool(){if(!this.$content)return;this.$content.innerHTML=``,this._nodePool=[];let e=document.createDocumentFragment();for(let t=0;t<this._maxNodes;t++){let t=document.createElement(`div`);this._nodePool.push(t),e.appendChild(t)}this.$content.appendChild(e)}_updateLayout(){if(!this.$phantom||!this.$content)return;let e=this._scrollDirection===`horizontal`,t=this._items.length*this._estimatedItemSize;e?(this.$phantom.style.width=`${t}px`,this.$phantom.style.height=`100%`,this.$content.classList.add(`horizontal`)):(this.$phantom.style.height=`${t}px`,this.$phantom.style.width=`100%`,this.$content.classList.remove(`horizontal`))}_onScroll(){this._pendingFrame===null&&(this._pendingFrame=requestAnimationFrame(()=>{this._pendingFrame=null,this._render()}))}_render(){if(!this.$viewport||!this._nodePool.length)return;if(!this._items.length){for(let e of this._nodePool)e.style.display=`none`;return}let e=this._scrollDirection===`horizontal`,t=e?this.$viewport.scrollLeft:this.$viewport.scrollTop,n=Math.floor(t/this._estimatedItemSize),r=Math.max(0,this._items.length-this._maxNodes);n=Math.min(Math.max(0,n),r);let i=n*this._estimatedItemSize;this.$content.style.transform=e?`translate3d(${i}px, 0, 0)`:`translate3d(0, ${i}px, 0)`;let a=this._getTemplate();for(let e=0;e<this._maxNodes;e++){let t=n+e,r=this._nodePool[e];if(t<this._items.length){r.style.display=``;let e=this._items[t];a?r.innerHTML=this._interpolate(a,e,t):r.textContent=typeof e==`object`?JSON.stringify(e):String(e)}else r.style.display=`none`}}_interpolate(e,t,n){return e.replace(/\{\{\s*([\w.]+)\s*\}\}/g,(e,r)=>{if(r===`index`)return String(n);if(r===`item`)return this._escapeHtml(typeof t==`object`&&t?JSON.stringify(t):String(t));let i=(r.startsWith(`item.`)?r.slice(5):r).split(`.`).reduce((e,t)=>e&&e[t]!==void 0?e[t]:void 0,t);return i===void 0?``:this._escapeHtml(String(i))})}_escapeHtml(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}};typeof window<`u`&&!customElements.get(`virtual-list`)&&customElements.define(`virtual-list`,t),e.VirtualList=t});
|
|
43
|
+
//# sourceMappingURL=index.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.umd.cjs","names":[],"sources":["../src/components/virtual-list/virtual-list.js"],"sourcesContent":["export class VirtualList extends HTMLElement {\n _items = [];\n _maxNodes = 20;\n _scrollDirection = 'vertical';\n _nodePool = [];\n _estimatedItemSize = 50;\n _userTemplate = null;\n _pendingFrame = null;\n $viewport;\n $phantom;\n $content;\n $slot;\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n if (!this.shadowRoot) {\n throw new Error('Shadow root initialization failed');\n }\n this.shadowRoot.innerHTML = `\n <style>\n :host {\n display: block;\n position: relative;\n contain: strict;\n }\n .viewport {\n width: 100%;\n height: 100%;\n overflow: auto;\n position: relative;\n -webkit-overflow-scrolling: touch;\n }\n .phantom {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n }\n .content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n will-change: transform;\n }\n .content.horizontal {\n display: flex;\n flex-direction: row;\n width: max-content;\n height: 100%;\n }\n </style>\n\n <div class=\"viewport\" id=\"viewport\">\n <div class=\"phantom\" id=\"phantom\"></div>\n <div class=\"content\" id=\"content\"></div>\n <slot id=\"slot\" style=\"display: none;\"></slot>\n </div>\n `;\n this.$viewport = this.shadowRoot.getElementById('viewport');\n this.$phantom = this.shadowRoot.getElementById('phantom');\n this.$content = this.shadowRoot.getElementById('content');\n this.$slot = this.shadowRoot.getElementById('slot');\n this._onScroll = this._onScroll.bind(this);\n this._onSlotChange = this._onSlotChange.bind(this);\n }\n static get observedAttributes() {\n return ['max-nodes', 'scroll-direction', 'estimated-item-size'];\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === 'max-nodes') {\n const parsed = parseInt(newValue ?? '', 10);\n this._maxNodes = Number.isNaN(parsed) ? 20 : parsed;\n this._buildNodePool();\n }\n else if (name === 'scroll-direction') {\n this._scrollDirection = newValue === 'horizontal' ? 'horizontal' : 'vertical';\n }\n else if (name === 'estimated-item-size') {\n const parsed = parseFloat(newValue ?? '');\n this._estimatedItemSize = Number.isNaN(parsed) ? 50 : parsed;\n }\n this._updateLayout();\n this._render();\n }\n connectedCallback() {\n this.$viewport.addEventListener('scroll', this._onScroll, { passive: true });\n if (this.$slot) {\n this.$slot.addEventListener('slotchange', this._onSlotChange);\n }\n this._getTemplate();\n this._buildNodePool();\n this._updateLayout();\n this._render();\n }\n disconnectedCallback() {\n this.$viewport.removeEventListener('scroll', this._onScroll);\n if (this.$slot) {\n this.$slot.removeEventListener('slotchange', this._onSlotChange);\n }\n if (this._pendingFrame !== null) {\n cancelAnimationFrame(this._pendingFrame);\n this._pendingFrame = null;\n }\n }\n _onSlotChange() {\n this._userTemplate = null;\n this._getTemplate();\n this._render();\n }\n _getTemplate() {\n if (!this._userTemplate) {\n const templateTag = this.querySelector('template');\n if (templateTag) {\n this._userTemplate = templateTag.innerHTML;\n }\n }\n return this._userTemplate;\n }\n set items(data) {\n const list = Array.isArray(data) ? data : [];\n this._items = new Proxy(list, {\n set: (target, property, value, receiver) => {\n const success = Reflect.set(target, property, value, receiver);\n this._updateLayout();\n this._render();\n return success;\n }\n });\n this._updateLayout();\n this._render();\n }\n get items() {\n return this._items;\n }\n set maxNodes(val) {\n this.setAttribute('max-nodes', String(val));\n }\n get maxNodes() {\n return this._maxNodes;\n }\n set estimatedItemSize(val) {\n this.setAttribute('estimated-item-size', String(val));\n }\n get estimatedItemSize() {\n return this._estimatedItemSize;\n }\n set scrollDirection(val) {\n this.setAttribute('scroll-direction', val);\n }\n get scrollDirection() {\n return this._scrollDirection;\n }\n _buildNodePool() {\n if (!this.$content)\n return;\n this.$content.innerHTML = '';\n this._nodePool = [];\n const fragment = document.createDocumentFragment();\n for (let i = 0; i < this._maxNodes; i++) {\n const nodeWrapper = document.createElement('div');\n this._nodePool.push(nodeWrapper);\n fragment.appendChild(nodeWrapper);\n }\n this.$content.appendChild(fragment);\n }\n _updateLayout() {\n if (!this.$phantom || !this.$content)\n return;\n const isHoriz = this._scrollDirection === 'horizontal';\n const totalSize = this._items.length * this._estimatedItemSize;\n if (isHoriz) {\n this.$phantom.style.width = `${totalSize}px`;\n this.$phantom.style.height = '100%';\n this.$content.classList.add('horizontal');\n }\n else {\n this.$phantom.style.height = `${totalSize}px`;\n this.$phantom.style.width = '100%';\n this.$content.classList.remove('horizontal');\n }\n }\n _onScroll() {\n if (this._pendingFrame !== null)\n return;\n this._pendingFrame = requestAnimationFrame(() => {\n this._pendingFrame = null;\n this._render();\n });\n }\n _render() {\n if (!this.$viewport || !this._nodePool.length)\n return;\n if (!this._items.length) {\n for (const node of this._nodePool) {\n node.style.display = 'none';\n }\n return;\n }\n const isHoriz = this._scrollDirection === 'horizontal';\n const scrollOffset = isHoriz ? this.$viewport.scrollLeft : this.$viewport.scrollTop;\n let startIndex = Math.floor(scrollOffset / this._estimatedItemSize);\n const maxStartIndex = Math.max(0, this._items.length - this._maxNodes);\n startIndex = Math.min(Math.max(0, startIndex), maxStartIndex);\n const offset = startIndex * this._estimatedItemSize;\n this.$content.style.transform = isHoriz\n ? `translate3d(${offset}px, 0, 0)`\n : `translate3d(0, ${offset}px, 0)`;\n const userTemplate = this._getTemplate();\n for (let i = 0; i < this._maxNodes; i++) {\n const dataIndex = startIndex + i;\n const node = this._nodePool[i];\n if (dataIndex < this._items.length) {\n node.style.display = '';\n const itemData = this._items[dataIndex];\n if (userTemplate) {\n node.innerHTML = this._interpolate(userTemplate, itemData, dataIndex);\n }\n else {\n node.textContent = typeof itemData === 'object' ? JSON.stringify(itemData) : String(itemData);\n }\n }\n else {\n node.style.display = 'none';\n }\n }\n }\n _interpolate(templateStr, item, index) {\n return templateStr.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_, key) => {\n if (key === 'index')\n return String(index);\n if (key === 'item') {\n return this._escapeHtml(typeof item === 'object' && item !== null ? JSON.stringify(item) : String(item));\n }\n const path = key.startsWith('item.') ? key.slice(5) : key;\n const val = path.split('.').reduce((obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined), item);\n return val !== undefined ? this._escapeHtml(String(val)) : '';\n });\n }\n _escapeHtml(value) {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n}\n// Auto-register custom element if window is available\nif (typeof window !== 'undefined' && !customElements.get('virtual-list')) {\n customElements.define('virtual-list', VirtualList);\n}\n//# sourceMappingURL=virtual-list.js.map"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "perf-web-components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance framework-agnostic web components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.umd.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.umd.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "vite",
|
|
21
|
+
"build": "tsc && vite build",
|
|
22
|
+
"preview": "vite preview"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.3.3",
|
|
26
|
+
"vite": "^8.3.0",
|
|
27
|
+
"vite-plugin-dts": "^3.7.2"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"web-components",
|
|
31
|
+
"custom-elements",
|
|
32
|
+
"virtual-scroll",
|
|
33
|
+
"virtualization",
|
|
34
|
+
"performance",
|
|
35
|
+
"typescript"
|
|
36
|
+
],
|
|
37
|
+
"author": "Santosh Kadam",
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|