@matthewp/zebra 0.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.
- package/package.json +10 -0
- package/src/list.ts +146 -0
- package/src/server.ts +5 -0
- package/src/view.ts +34 -0
package/package.json
ADDED
package/src/list.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { View } from './view.ts';
|
|
2
|
+
|
|
3
|
+
type ViewConstructor = new () => View;
|
|
4
|
+
|
|
5
|
+
export class List<T = Record<string, unknown>> {
|
|
6
|
+
private ViewClass: ViewConstructor;
|
|
7
|
+
private keyFn: (item: T) => unknown;
|
|
8
|
+
private views: (View | null)[] = [];
|
|
9
|
+
private keys: unknown[] = [];
|
|
10
|
+
private container: HTMLElement | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(ViewClass: ViewConstructor, keyFn: (item: T) => unknown) {
|
|
13
|
+
this.ViewClass = ViewClass;
|
|
14
|
+
this.keyFn = keyFn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
template(items?: T[]): string {
|
|
18
|
+
if (!items || items.length === 0) return '';
|
|
19
|
+
return items.map(item => {
|
|
20
|
+
let view = new this.ViewClass();
|
|
21
|
+
return view.template(item as unknown as Record<string, unknown>);
|
|
22
|
+
}).join('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
mount(container: HTMLElement, items?: T[]) {
|
|
26
|
+
this.container = container;
|
|
27
|
+
let children = Array.from(container.children) as HTMLElement[];
|
|
28
|
+
if (children.length > 0 && items) {
|
|
29
|
+
for (let i = 0; i < children.length && i < items.length; i++) {
|
|
30
|
+
let view = new this.ViewClass();
|
|
31
|
+
view.mount(children[i]);
|
|
32
|
+
view.update(items[i] as unknown as Record<string, unknown>);
|
|
33
|
+
this.views.push(view);
|
|
34
|
+
this.keys.push(this.keyFn(items[i]));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private updateView(view: View, item: T) {
|
|
40
|
+
view.update(item as unknown as Record<string, unknown>);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
update(items: T[]) {
|
|
44
|
+
let container = this.container!;
|
|
45
|
+
let oldViews = this.views;
|
|
46
|
+
let oldKeys = this.keys;
|
|
47
|
+
let newKeys = items.map(this.keyFn);
|
|
48
|
+
let newViews: (View | null)[] = new Array(items.length).fill(null);
|
|
49
|
+
|
|
50
|
+
let oldHead = 0;
|
|
51
|
+
let oldTail = oldViews.length - 1;
|
|
52
|
+
let newHead = 0;
|
|
53
|
+
let newTail = items.length - 1;
|
|
54
|
+
|
|
55
|
+
let oldKeyToIndex: Map<unknown, number> | undefined;
|
|
56
|
+
|
|
57
|
+
while (oldHead <= oldTail && newHead <= newTail) {
|
|
58
|
+
if (oldViews[oldHead] === null) {
|
|
59
|
+
oldHead++;
|
|
60
|
+
} else if (oldViews[oldTail] === null) {
|
|
61
|
+
oldTail--;
|
|
62
|
+
} else if (oldKeys[oldHead] === newKeys[newHead]) {
|
|
63
|
+
// Head-Head match
|
|
64
|
+
newViews[newHead] = oldViews[oldHead];
|
|
65
|
+
this.updateView(oldViews[oldHead]!, items[newHead]);
|
|
66
|
+
oldHead++;
|
|
67
|
+
newHead++;
|
|
68
|
+
} else if (oldKeys[oldTail] === newKeys[newTail]) {
|
|
69
|
+
// Tail-Tail match
|
|
70
|
+
newViews[newTail] = oldViews[oldTail];
|
|
71
|
+
this.updateView(oldViews[oldTail]!, items[newTail]);
|
|
72
|
+
oldTail--;
|
|
73
|
+
newTail--;
|
|
74
|
+
} else if (oldKeys[oldHead] === newKeys[newTail]) {
|
|
75
|
+
// Head-Tail match: move old head to after old tail
|
|
76
|
+
newViews[newTail] = oldViews[oldHead];
|
|
77
|
+
this.updateView(oldViews[oldHead]!, items[newTail]);
|
|
78
|
+
oldViews[oldTail]!.el.after(oldViews[oldHead]!.el);
|
|
79
|
+
oldHead++;
|
|
80
|
+
newTail--;
|
|
81
|
+
} else if (oldKeys[oldTail] === newKeys[newHead]) {
|
|
82
|
+
// Tail-Head match: move old tail to before old head
|
|
83
|
+
newViews[newHead] = oldViews[oldTail];
|
|
84
|
+
this.updateView(oldViews[oldTail]!, items[newHead]);
|
|
85
|
+
oldViews[oldHead]!.el.before(oldViews[oldTail]!.el);
|
|
86
|
+
oldTail--;
|
|
87
|
+
newHead++;
|
|
88
|
+
} else {
|
|
89
|
+
// Build map lazily
|
|
90
|
+
if (!oldKeyToIndex) {
|
|
91
|
+
oldKeyToIndex = new Map();
|
|
92
|
+
for (let i = oldHead; i <= oldTail; i++) {
|
|
93
|
+
if (oldViews[i] !== null) {
|
|
94
|
+
oldKeyToIndex.set(oldKeys[i], i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let oldIndex = oldKeyToIndex.get(newKeys[newHead]);
|
|
100
|
+
if (oldIndex === undefined) {
|
|
101
|
+
// New item
|
|
102
|
+
let view = new this.ViewClass();
|
|
103
|
+
view.createAndMount();
|
|
104
|
+
newViews[newHead] = view;
|
|
105
|
+
oldViews[oldHead]!.el.before(view.el);
|
|
106
|
+
this.updateView(view, items[newHead]);
|
|
107
|
+
} else {
|
|
108
|
+
// Move existing
|
|
109
|
+
let view = oldViews[oldIndex]!;
|
|
110
|
+
this.updateView(view, items[newHead]);
|
|
111
|
+
newViews[newHead] = view;
|
|
112
|
+
oldViews[oldHead]!.el.before(view.el);
|
|
113
|
+
oldViews[oldIndex] = null;
|
|
114
|
+
}
|
|
115
|
+
newHead++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove remaining old items
|
|
120
|
+
while (oldHead <= oldTail) {
|
|
121
|
+
if (oldViews[oldHead] !== null) {
|
|
122
|
+
oldViews[oldHead]!.el.remove();
|
|
123
|
+
}
|
|
124
|
+
oldHead++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add remaining new items
|
|
128
|
+
while (newHead <= newTail) {
|
|
129
|
+
let view = new this.ViewClass();
|
|
130
|
+
view.createAndMount();
|
|
131
|
+
newViews[newHead] = view;
|
|
132
|
+
let el = view.el;
|
|
133
|
+
let ref = newViews[newTail + 1]?.el;
|
|
134
|
+
if (ref) {
|
|
135
|
+
ref.before(el);
|
|
136
|
+
} else {
|
|
137
|
+
container.append(el);
|
|
138
|
+
}
|
|
139
|
+
this.updateView(view, items[newHead]);
|
|
140
|
+
newHead++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.views = newViews;
|
|
144
|
+
this.keys = newKeys;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/server.ts
ADDED
package/src/view.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class View {
|
|
2
|
+
el!: HTMLElement;
|
|
3
|
+
|
|
4
|
+
createElement(): HTMLElement {
|
|
5
|
+
let tpl = document.createElement('template');
|
|
6
|
+
tpl.innerHTML = this.template();
|
|
7
|
+
this.el = document.importNode(tpl.content, true).firstElementChild as HTMLElement;
|
|
8
|
+
return this.el;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
template(_props?: any): string {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createAndMount(): void {
|
|
16
|
+
this.mount(this.createElement());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
mount(el: HTMLElement): void {
|
|
20
|
+
this.el = el;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(_data?: Record<string, unknown>): HTMLElement {
|
|
24
|
+
return this.el;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Slottable {
|
|
29
|
+
template(props?: any): string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function slot<T extends Slottable>(target: T, ...args: Parameters<T['template']>): string {
|
|
33
|
+
return target.template(...args);
|
|
34
|
+
}
|