-
Notifications
You must be signed in to change notification settings - Fork 15
/
react.js
479 lines (275 loc) · 16.7 KB
/
react.js
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// "instance" is whatever you want. They're also called "host components" in this documentation.
import { camelCase, difference, has, intersection, isUndefined, kebabCase, lowerFirst, partition, pick, upperFirst } from 'lodash';
import ReactFiberReconcilier from 'react-dom/lib/ReactFiberReconciler';
import ReactPortal from 'react-dom/lib/ReactPortal';
import { Point, StyleManager } from '../core';
import * as TermElements from './elements';
import { KeySequence } from './misc/KeySequence';
const EVENT_SYMBOL = Symbol();
const SHORTCUT_SYMBOL = Symbol();
const MANAGED_PROPS = new Map([
[ `caret`, (element, caret) => {
if (caret !== null) {
element.caret = new Point({ x: caret.x, y: caret.y });
} else {
element.caret = null;
}
} ],
[ `scroll`, (element, scroll) => {
if (element.scrollRect.x !== scroll.x)
element.scrollLeft = scroll.x;
if (element.scrollRect.y !== scroll.y) {
element.scrollTop = scroll.y;
}
} ],
[ `value`, (element, value) => {
element.value = value;
} ]
]);
function toEventName(key) {
return key.replace(/^on|Capture$/g, ``).toLowerCase();
}
function doesUseCapture(key) {
return key.endsWith(`Capture`);
}
function wrapShortcutListener(descriptor, fn) {
let sequence = new KeySequence(descriptor);
return { original: fn, wrapper: e => {
if (!e.key)
return;
if (!sequence.add(e.key))
return;
TermRenderer.batchedUpdates(() => {
fn(Object.assign(Object.create(e), {
setDefault: fn => {
e.setDefault(() => {
TermRenderer.batchedUpdates(() => {
fn.call(e);
});
});
}
}));
});
} };
}
function wrapEventListener(fn) {
return { original: fn, wrapper: (... args) => {
TermRenderer.batchedUpdates(() => {
fn(... args);
});
} };
}
function findListeners(props) {
let events = new Map();
let shortcuts = new Map();
let renderers = new Map();
for (let [ prop, value ] of Object.entries(props)) {
if (!value)
continue;
if (prop === `elementRenderer` || prop === `contentRenderer`) {
renderers.set(prop, value);
} else if (prop === `onShortcuts`) {
for (let [ shortcut, listener ] of Object.entries(value)) {
shortcuts.set(shortcut, wrapShortcutListener(shortcut, listener));
}
} else if (/^on[A-Z]/.test(prop)) {
events.set(prop, wrapEventListener(value));
}
}
return { events, shortcuts, renderers };
}
function setupEventListeners(instance, newListeners) {
let oldListeners = Reflect.get(instance, EVENT_SYMBOL);
let oldEvents = Array.from(oldListeners.keys());
let newEvents = Array.from(newListeners.keys());
let removedEvents = difference(oldEvents, newEvents);
let addedEvents = difference(newEvents, oldEvents);
let replacedEvents = intersection(newEvents, oldEvents).filter(prop => oldListeners.get(prop).original !== newListeners.get(prop).original);
for (let prop of [ ... removedEvents, ... replacedEvents ])
instance.removeEventListener(toEventName(prop), oldListeners.get(prop).wrapper, { capture: doesUseCapture(prop) });
for (let prop of [ ... replacedEvents, ... addedEvents ])
instance.addEventListener(toEventName(prop), newListeners.get(prop).wrapper, { capture: doesUseCapture(prop) });
Reflect.set(instance, EVENT_SYMBOL, newListeners);
}
function setupShortcutListeners(instance, newListeners) {
let oldListeners = Reflect.get(instance, SHORTCUT_SYMBOL);
let oldShortcuts = Array.from(oldListeners.keys());
let newShortcuts = Array.from(newListeners.keys());
let removedShortcuts = difference(oldShortcuts, newShortcuts);
let addedShortcuts = difference(newShortcuts, oldShortcuts);
let replacedShortcuts = intersection(newShortcuts, oldShortcuts).filter(prop => oldListeners.get(prop).original !== newListeners.get(prop).original);
for (let prop of [ ... removedShortcuts, ... replacedShortcuts ])
instance.removeEventListener(`keypress`, oldListeners.get(prop).wrapper, { capture: true });
for (let prop of [ ... replacedShortcuts, ... addedShortcuts ])
instance.addEventListener(`keypress`, newListeners.get(prop).wrapper, { capture: true });
Reflect.set(instance, SHORTCUT_SYMBOL, newListeners);
}
function setupRenderers(instance, renderers) {
let oldElementListener = has(instance, `renderElement`) ? instance.renderElement.original : null;
let newElementListener = renderers.get(`elementRenderer`) || null;
let oldContentListener = has(instance, `renderContent`) ? instance.renderContent.original : null;
let newContentListener = renderers.get(`contentRenderer`) || null;
if (newElementListener !== oldElementListener) {
if (newElementListener) {
instance.renderElement = (... args) => newElementListener(... args, instance);
instance.renderElement.original = newElementListener;
} else {
delete instance.renderElement;
}
instance.queueDirtyRect(instance.elementClipRect);
}
if (newContentListener !== oldContentListener) {
if (newContentListener) {
instance.renderContent = (... args) => newContentListener(... args, instance);
instance.renderContent.original = newContentListener;
} else {
delete instance.renderContent;
}
instance.queueDirtyRect(instance.contentClipRect);
}
}
function createInstance(type, props) {
let elementName = type !== `div` ? `Term${upperFirst(camelCase(type))}` : `TermElement`;
let ElementClass = TermElements[elementName];
if (!ElementClass)
throw new Error(`Invalid element type "${type}" (${elementName} is not amongst ${Object.keys(TermElements).join(`, `)})`);
let instance = new ElementClass(props);
Reflect.set(instance, EVENT_SYMBOL, new Map());
Reflect.set(instance, SHORTCUT_SYMBOL, new Map());
return instance;
}
let TermRenderer = ReactFiberReconcilier(new class {
useSyncScheduling = true;
getRootHostContext() {
// TODO: ???
return {};
}
getChildHostContext() {
// TODO: ???
return {};
}
createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
// Note that `type` will always be a string (because React itself will handle the React components). You will probably want to make some kind of switch (or use a type->host map, maybe?) to convert this string into the right host component.
let propNames = Reflect.ownKeys(props).filter(prop => prop !== `ref` && prop !== `key`);
let [ managed, unmanaged ] = partition(propNames, prop => MANAGED_PROPS.has(prop));
let instance = createInstance(type, pick(props, unmanaged));
let listeners = findListeners(props);
setupEventListeners(instance, listeners.events);
setupShortcutListeners(instance, listeners.shortcuts);
setupRenderers(instance, listeners.renderers);
for (let prop of managed)
if (!isUndefined(props[prop]))
MANAGED_PROPS.get(prop)(instance, props[prop]);
return instance;
}
createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
// Create a text instance. It will get called for each text node in your React tree, except if their parent is returning a truthy value when invoked through the `shouldSetTextContent` hook.
return new (TermElements.TermText)({ textContent: text });
}
getPublicInstance(instance) {
// UNCONFIRMED: Return the public instance to the components (ie. the one which can be accessed via refs).
return instance;
}
appendInitialChild(parentInstance, child) {
// This function will only be called before the node is injected into the dom. In every other case, you will want to look at appendChild. That being said, multiple children might get appended before the element is inserted into the dom, so you can't just replace all the elements and be done with it.
// The distinction between appendInitialChild and appendChild might be interesting for various optimizations. For example, in the Noop renderer, the `appendChild` method needs to check if the child node is already somewhere in the tree before appending it. Because `appendInitialChild` is only called before the node is mounted, we know for sure that the node we will insert will never have been already registered in our parent, so we can skip this check.
// Don't forget to check for text nodes. For example, if "child" is a text node, you might want to return immediately if it should be stored into a property of "parent" instead of as a child node. That being said, you should probably avoid this pattern, since using a property means that you won't be able to have multiple text nodes (the last one will overwrite the others). Still, keep it in mind :)
parentInstance.appendChild(child);
}
finalizeInitialChildren(instance, type, props) {
// This function can be used to assign a value to an instance *after* both the elements and its child have been generated and linked together, but *before* they get mounted. It's useful in some particular cases, such as `<select value={...}>`: in order to support this use case, you need to check the child values and get the index of the child that match the specified value, which is only possible if the children have been generated (hence why you can't do this in `createInstance`).
// The return value is used to inform React that a custom effect will need to be applied during commit (once host components have been mounted). For example, a DOM implementation might want to return true in order to support auto-focus. The auto-focus itself will be implemented into the `commitMount` method.
return props.autofocus ? true : false;
}
appendChild(parentInstance, child) {
// This function will be called every time we need to append a child node inside its new parent, but only after the parent has been mounted into the tree (otherwise the function called will be `appendInitialChild`).
// Note that you will probably need to make sure that the child node is not already present in your registered children, and remove it if it is (so that you can move it at the end of the list). Some renderers don't need to do this because the underlying APIs (such as DOM) already check for this, but be careful.
parentInstance.appendChild(child);
}
insertBefore(parentInstance, child, beforeChild) {
// Nothing too complicated - just add a child inside a parent, but make sure to add it before another child. Just like `appendChild`, make sure that the child isn't already present in its parent before adding it, otherwise you could end up with duplicates.
parentInstance.insertBefore(child, beforeChild);
}
removeChild(parentInstance, child) {
// Just remove the child from its parent.
parentInstance.removeChild(child);
}
prepareForCommit() {
// This hook is used to save some values before proceeding to commit our new instances into the tree. For example, a DOM renderer would probably want to use this opportunity to save the current selection range, since adding new DOM nodes would probably destroy this selection that you will then need to restore.
// The return value is a user-defined set of data that will be forwarded to `resetAfterCommit` (this is needed because `prepareForCommit` might be reentrant).
return {};
}
resetAfterCommit(data) {
// The data parameter is the value returned from `prepareForCommit`. To continue with the example of the DOM renderer, you would use this function to restore the selection range previously saved and returned in the state object.
}
commitMount(instance, type, newProps) {
// This function will be called after the elements have been inserted into the host tree via `appendInitialChild` & co. That's where you need to trigger the actions that require the host components to be inserted in the tree, but also need to be only called once. For example, a DOM renderer would add support for autofocus through this hook.
if (newProps.autofocus) {
if (newProps.autofocus !== `steal` && newProps.autofocus !== `initial`) {
throw new Error(`Invalid autofocus directive; expected "steal" or "initial"`);
} else if (newProps.autofocus === `steal` || instance.rootNode.activeElement === null) {
instance.focus();
}
}
}
prepareUpdate(instance, type, oldProps, newProps, hostContext) {
// This function needs to check if an update is actually required, and return true in such a case. If you don't, the commitUpdate hook will not get called.
return true;
}
commitUpdate(instance, payload, type, oldProps, newProps) {
// This function is called everytime the host component needs to be synced with the React props.
let listeners = findListeners(newProps);
setupEventListeners(instance, listeners.events);
setupShortcutListeners(instance, listeners.shortcuts);
setupRenderers(instance, listeners.renderers);
let { style = {}, classList = [], ... rest } = newProps;
for (let name of Reflect.ownKeys(style))
Reflect.set(instance.style, name, style[name]);
for (let name of Reflect.ownKeys(rest)) {
let manager = MANAGED_PROPS.get(name);
let value = newProps[name];
if (!manager) {
Reflect.set(instance, name, value);
} else if (!isUndefined(value)) {
manager(instance, value);
}
}
instance.classList.assign(classList);
}
commitTextUpdate(textInstance, oldText, newText) {
// This function is called whenever
textInstance.textContent = newText;
}
shouldSetTextContent(props) {
// If this function return true, then `createTextInstance` and `commitTextUpdate` will not get called if the guest component contains some text. Instead, it is expected that you will set this text content yourself, both in `createInstance` and `commitUpdate`, and that you will correctly reset it when the `resetTextContent` hook will be called.
// You will usually only check for "props.children is a string" and "props.children is a number", but it might check for additional props depending on your needs. For example, the DOM renderer also check for any `dangerouslySetInnerHTML` prop.
return false;
}
resetTextContent(instance) {
// This function is called when parent that returned true on `shouldSetTextContent` loses its text content.
// TODO: Why isn't this reset inside `commitUpdate`? Just so that people don't forget to implement it?
instance.textContent = ``;
}
scheduleAnimationCallback(callback) {
// Register a function to be called just before the next screen redraw. In a browser environment, it would probably be implemented using `requestAnimationFrame`.
// TODO: What are the tasks scheduled through this function?
setTimeout(callback, 1000 / 60);
}
scheduleDeferredCallback(callback) {
// Register a function to be called whenever we've got the time. In a browser environment, that would be something like `requestIdleCallback`.
// TODO: What are the tasks scheduled through this function?
setImmediate(callback);
}
});
export function render(element, root, callback) {
let container = TermRenderer.createContainer(root);
TermRenderer.unbatchedUpdates(() => {
TermRenderer.updateContainer(element, container, null, callback);
});
}
export function createPortal(children, containerTag, key) {
return ReactPortal.createPortal(children, containerTag, null, key);
}
export function getTermNode(component) {
return TermRenderer.getHostInstance(component);
}