-
Notifications
You must be signed in to change notification settings - Fork 51
/
html.ts
196 lines (179 loc) · 6.86 KB
/
html.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { document } from './polyfills';
import { TemplateInstance, ChildNodePart, AttrPart, Part } from 'media-chrome/dist/media-theme-element.js';
// NOTE: These are either direct ports or significantly based off of github's jtml template part processing logic. For more, see: https://github.com/github/jtml
const eventListeners = new WeakMap<Element, Map<string, EventHandler>>();
class EventHandler {
handleEvent!: EventListener;
constructor(
private element: Element,
private type: string
) {
this.element.addEventListener(this.type, this);
const elementMap = eventListeners.get(this.element);
if (elementMap) {
elementMap.set(this.type, this);
}
}
set(listener: EventListener) {
if (typeof listener == 'function') {
this.handleEvent = listener.bind(this.element);
} else if (typeof listener === 'object' && typeof (listener as EventHandler).handleEvent === 'function') {
this.handleEvent = (listener as EventHandler).handleEvent.bind(listener);
} else {
this.element.removeEventListener(this.type, this);
const elementMap = eventListeners.get(this.element);
if (elementMap) {
elementMap.delete(this.type);
}
}
}
static for(part: AttrPart): EventHandler {
if (!eventListeners.has(part.element)) eventListeners.set(part.element, new Map());
const type = part.attributeName.slice(2);
const elementListeners = eventListeners.get(part.element);
if (elementListeners && elementListeners.has(type)) return elementListeners.get(type) as EventHandler;
return new EventHandler(part.element, type);
}
}
export function processEvent(part: Part, value: unknown): boolean {
if (part instanceof AttrPart && part.attributeName.startsWith('on')) {
EventHandler.for(part).set(value as unknown as EventListener);
part.element.removeAttributeNS(part.attributeNamespace, part.attributeName);
return true;
}
return false;
}
function processSubTemplate(part: Part, value: unknown): boolean {
if (value instanceof TemplateResult && part instanceof ChildNodePart) {
value.renderInto(part);
return true;
}
return false;
}
function processDocumentFragment(part: Part, value: unknown): boolean {
if (value instanceof DocumentFragment && part instanceof ChildNodePart) {
if (value.childNodes.length) part.replace(...value.childNodes);
return true;
}
return false;
}
export function processPropertyIdentity(part: Part, value: unknown): boolean {
if (part instanceof AttrPart) {
const ns = part.attributeNamespace;
const oldValue = part.element.getAttributeNS(ns, part.attributeName);
if (String(value) !== oldValue) {
part.value = String(value);
}
return true;
}
part.value = String(value);
return true;
}
export function processElementAttribute(part: Part, value: unknown): boolean {
// This allows us to set the media-theme template property directly.
if (part instanceof AttrPart && value instanceof Element) {
const element = part.element as any;
if (element[part.attributeName] !== value) {
part.element.removeAttributeNS(part.attributeNamespace, part.attributeName);
element[part.attributeName] = value;
}
return true;
}
return false;
}
export function processBooleanAttribute(part: Part, value: unknown): boolean {
if (
typeof value === 'boolean' &&
part instanceof AttrPart
// can't use this because on custom elements the props are always undefined
// typeof part.element[part.attributeName as keyof Element] === 'boolean'
) {
const ns = part.attributeNamespace;
const oldValue = part.element.hasAttributeNS(ns, part.attributeName);
if (value !== oldValue) {
part.booleanValue = value;
}
return true;
}
return false;
}
export function processBooleanNode(part: Part, value: unknown): boolean {
if (value === false && part instanceof ChildNodePart) {
part.replace('');
return true;
}
return false;
}
export function processPart(part: Part, value: unknown): void {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
processElementAttribute(part, value) ||
processBooleanAttribute(part, value) ||
processEvent(part, value) ||
processBooleanNode(part, value) ||
processSubTemplate(part, value) ||
processDocumentFragment(part, value) ||
processPropertyIdentity(part, value);
}
// The Map's data will not be garbage collected however the number of created
// templates will not exceed the html`` defined templates. This is fine.
const templates = new Map<string, HTMLTemplateElement>();
const renderedTemplates = new WeakMap<Node | ChildNodePart, HTMLTemplateElement>();
const renderedTemplateInstances = new WeakMap<Node | ChildNodePart, TemplateInstance>();
export class TemplateResult {
public readonly stringsKey: string;
constructor(
public readonly strings: TemplateStringsArray,
public readonly values: unknown[],
public readonly processor: any
) {
// Use a control character to join the expression boundaries. It should be
// a character that is not used in the static strings so the key is unique
// if the expressions are in a different place even tough the static strings
// are identical.
this.stringsKey = this.strings.join('\x01');
}
get template(): HTMLTemplateElement {
if (templates.has(this.stringsKey)) {
return templates.get(this.stringsKey) as HTMLTemplateElement;
} else {
const template = document.createElement('template');
const end = this.strings.length - 1;
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '');
templates.set(this.stringsKey, template);
return template;
}
}
renderInto(element: Node | ChildNodePart): void {
const template = this.template;
if (renderedTemplates.get(element) !== template) {
renderedTemplates.set(element, template);
const instance = new TemplateInstance(template, this.values, this.processor);
renderedTemplateInstances.set(element, instance);
if (element instanceof ChildNodePart) {
element.replace(...instance.children);
} else {
element.appendChild(instance);
}
return;
}
const templateInstance = renderedTemplateInstances.get(element);
templateInstance?.update?.(this.values as unknown as Record<string, unknown>);
}
}
const defaultProcessor = {
processCallback(_instance: any, parts: any, state: any) {
if (!state) return;
for (const [expression, part] of parts) {
if (expression in state) {
const value = state[expression] ?? '';
processPart(part, value);
}
}
},
};
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult {
return new TemplateResult(strings, values, defaultProcessor);
}
export function render(result: TemplateResult, element: Node | ChildNodePart): void {
result.renderInto(element);
}