@ng-native/fabric 0.2.0 → 0.3.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/dist/color-mix.d.ts +12 -2
- package/dist/color-mix.js +16 -5
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +87 -4
- package/dist/css.js +507 -66
- package/dist/css.js.map +1 -1
- package/dist/defer-triggers.js +3 -1
- package/dist/defer-triggers.js.map +1 -1
- package/dist/engine.d.ts +142 -14
- package/dist/engine.js +451 -60
- package/dist/engine.js.map +1 -1
- package/dist/host.d.ts +7 -0
- package/dist/host.js +7 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.d.ts +3 -12
- package/dist/inline-derived-token.js +241 -6
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +5 -4
- package/dist/inline-token.js +28 -3
- package/dist/inline-token.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.js +21 -3
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* inherent and is what React's own Fabric renderer does.
|
|
9
9
|
*/
|
|
10
10
|
import { StyleResolver, } from "./css.js";
|
|
11
|
-
import { animationEvent, bezier, interpolate, sample, step, tracksOf, } from "./transition.js";
|
|
11
|
+
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
12
12
|
import { isFormsInput } from "./forms-inputs.js";
|
|
13
13
|
import { transformList } from "./inline-transform.js";
|
|
14
14
|
import { tokenFromValue } from "./inline-token.js";
|
|
@@ -22,14 +22,41 @@ import { FontFaces } from "./font-faces.js";
|
|
|
22
22
|
* and this runs for every node that commits.
|
|
23
23
|
*/
|
|
24
24
|
function animationOf(props) {
|
|
25
|
-
|
|
25
|
+
let spec = props['$animation'];
|
|
26
26
|
if (spec !== undefined)
|
|
27
27
|
delete props['$animation'];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
for (const [key, field] of Object.entries(ANIMATION_PARTS)) {
|
|
29
|
+
if (!(key in props))
|
|
30
|
+
continue;
|
|
31
|
+
const value = props[key];
|
|
32
|
+
delete props[key];
|
|
33
|
+
if (spec && value !== null)
|
|
34
|
+
spec = { ...spec, ...animationPart(field, value) };
|
|
35
|
+
}
|
|
36
|
+
return spec;
|
|
37
|
+
}
|
|
38
|
+
const ANIMATION_PARTS = {
|
|
39
|
+
$animationDuration: 'duration',
|
|
40
|
+
$animationDelay: 'delay',
|
|
41
|
+
$animationEasing: 'easing',
|
|
42
|
+
$animationIterations: 'iterations',
|
|
43
|
+
$animationDirection: 'direction',
|
|
44
|
+
$animationFill: 'fill',
|
|
45
|
+
$playState: 'paused',
|
|
46
|
+
};
|
|
47
|
+
function animationPart(field, value) {
|
|
48
|
+
switch (field) {
|
|
49
|
+
case 'iterations':
|
|
50
|
+
return { iterations: value === 'infinite' ? null : value };
|
|
51
|
+
case 'direction':
|
|
52
|
+
return {
|
|
53
|
+
direction: value === 'normal' ? undefined : value,
|
|
54
|
+
};
|
|
55
|
+
case 'paused':
|
|
56
|
+
return { paused: value === 'paused' ? true : undefined };
|
|
57
|
+
default:
|
|
58
|
+
return { [field]: value };
|
|
59
|
+
}
|
|
33
60
|
}
|
|
34
61
|
/**
|
|
35
62
|
* Whether two specs ask for the same animation. By what they say rather than which object they
|
|
@@ -53,23 +80,24 @@ const SPEC_FIELDS = [
|
|
|
53
80
|
'direction',
|
|
54
81
|
'timeline',
|
|
55
82
|
];
|
|
83
|
+
/**
|
|
84
|
+
* The scroll events that mean a finger is dragging the list rather than tapping something in it.
|
|
85
|
+
*
|
|
86
|
+
* `topScrollBeginDrag` is the precise one; `topScroll` is here because a scroll view configured
|
|
87
|
+
* without drag events still reports movement, and a press surviving that is the bug either way.
|
|
88
|
+
*/
|
|
89
|
+
const SCROLL_CANCELS_PRESS = new Set(['topScroll', 'topScrollBeginDrag']);
|
|
56
90
|
/**
|
|
57
91
|
* RN's `directEventTypes`: delivered to the target only, never up the tree. Everything else
|
|
58
92
|
* bubbles, which matches RN's `bubblingEventTypes` for the touch and input events that matter.
|
|
93
|
+
* The web host's engine reads this set too, so both hosts deliver an event the same way.
|
|
59
94
|
*
|
|
60
95
|
* ponytail: a hand-kept set rather than real ViewConfigs. It is wrong only for a nested
|
|
61
96
|
* scroll view, where an ancestor listening for the same direct event would not fire (correct)
|
|
62
97
|
* but a third-party component's custom direct event would bubble (harmless in practice).
|
|
63
98
|
* Swap it for per-component ViewConfig metadata if that ever matters.
|
|
64
99
|
*/
|
|
65
|
-
|
|
66
|
-
* The scroll events that mean a finger is dragging the list rather than tapping something in it.
|
|
67
|
-
*
|
|
68
|
-
* `topScrollBeginDrag` is the precise one; `topScroll` is here because a scroll view configured
|
|
69
|
-
* without drag events still reports movement, and a press surviving that is the bug either way.
|
|
70
|
-
*/
|
|
71
|
-
const SCROLL_CANCELS_PRESS = new Set(['topScroll', 'topScrollBeginDrag']);
|
|
72
|
-
const DIRECT_EVENTS = new Set([
|
|
100
|
+
export const DIRECT_EVENTS = new Set([
|
|
73
101
|
'topScroll',
|
|
74
102
|
'topScrollBeginDrag',
|
|
75
103
|
'topScrollEndDrag',
|
|
@@ -108,9 +136,11 @@ const DIRECT_EVENTS = new Set([
|
|
|
108
136
|
*/
|
|
109
137
|
export class SyntheticEvent {
|
|
110
138
|
nativeEvent;
|
|
139
|
+
target;
|
|
111
140
|
stopped = false;
|
|
112
|
-
constructor(nativeEvent) {
|
|
141
|
+
constructor(nativeEvent, target = null) {
|
|
113
142
|
this.nativeEvent = nativeEvent;
|
|
143
|
+
this.target = target;
|
|
114
144
|
}
|
|
115
145
|
stopPropagation() {
|
|
116
146
|
this.stopped = true;
|
|
@@ -256,6 +286,11 @@ export const ANDROID_VIEW_NAMES = {
|
|
|
256
286
|
'safe-area-view': 'View',
|
|
257
287
|
'input-accessory-view': 'View',
|
|
258
288
|
};
|
|
289
|
+
/**
|
|
290
|
+
* iOS's names for the elements Android names differently, so reporting iOS after Android puts
|
|
291
|
+
* them back.
|
|
292
|
+
*/
|
|
293
|
+
const IOS_VIEW_NAMES = Object.fromEntries(Object.keys(ANDROID_VIEW_NAMES).map((element) => [element, VIEW_NAMES[element]]));
|
|
259
294
|
/**
|
|
260
295
|
* The platform the host reported at startup. Defaults to iOS, which is what the view-name table
|
|
261
296
|
* above assumes too.
|
|
@@ -274,11 +309,9 @@ export function nativePlatform() {
|
|
|
274
309
|
*/
|
|
275
310
|
export function registerPlatformComponents(platform) {
|
|
276
311
|
platformOS = platform;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for (const [element, viewName] of Object.entries(ANDROID_VIEW_NAMES)) {
|
|
312
|
+
const names = platform === 'android' ? ANDROID_VIEW_NAMES : IOS_VIEW_NAMES;
|
|
313
|
+
for (const [element, viewName] of Object.entries(names))
|
|
280
314
|
registerViewName(element, viewName);
|
|
281
|
-
}
|
|
282
315
|
}
|
|
283
316
|
/**
|
|
284
317
|
* Props RN's JS wrappers apply before the caller's, per native view name.
|
|
@@ -318,6 +351,88 @@ const DEFAULT_VIEW = 'View';
|
|
|
318
351
|
/** A top-level text, the one view that aligns its lines. A nested text is a `VirtualText`. */
|
|
319
352
|
const PARAGRAPH = 'Paragraph';
|
|
320
353
|
const textDirection = (value) => value === 'ltr' || value === 'rtl' ? value : undefined;
|
|
354
|
+
/**
|
|
355
|
+
* What iOS scales a text's `lineHeight` by: the system text size, capped by the text's own
|
|
356
|
+
* `maxFontSizeMultiplier` where it is at least 1, and 1 without font scaling
|
|
357
|
+
* (`RCTEffectiveFontSizeMultiplierFromTextAttributes`).
|
|
358
|
+
*/
|
|
359
|
+
function textScale(props, fontScale = 1) {
|
|
360
|
+
if (props['allowFontScaling'] === false)
|
|
361
|
+
return 1;
|
|
362
|
+
const cap = props['maxFontSizeMultiplier'];
|
|
363
|
+
return typeof cap === 'number' && cap >= 1 ? Math.min(fontScale, cap) : fontScale;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Centre the text of a single-line text field that has a line height, as Chrome centres an
|
|
367
|
+
* input's, keeping the height the line height gives it.
|
|
368
|
+
*
|
|
369
|
+
* Where a `height` in points sizes the field, the line height has nothing left to do, and is left
|
|
370
|
+
* out on both platforms: on Android, `EditText` centres a line box taller than the font's own
|
|
371
|
+
* about 1.7pt high of centre in a 44pt field, and centres the font's own exactly. A percentage
|
|
372
|
+
* height sizes the field only where its parent's height is definite, which is known at layout
|
|
373
|
+
* alone, so such a field is left as it was. Elsewhere Android sizes the field by the line height
|
|
374
|
+
* and centres the text in it itself, and keeps it.
|
|
375
|
+
*
|
|
376
|
+
* On iOS, React Native's field sets `lineHeight` as the paragraph's minimum and maximum line
|
|
377
|
+
* height and, unlike a paragraph (`RCTApplyBaselineOffset`), never offsets the baseline, so the
|
|
378
|
+
* glyphs sit at the bottom of a line box taller than the font. One line has nothing to space, so
|
|
379
|
+
* the line height is left out. What it does in Chrome, and on Android, is set the field's height:
|
|
380
|
+
* line height, padding and border. That is kept as a `minHeight`, the larger of it and the field's
|
|
381
|
+
* own, within its `maxHeight`: the whole sum for a border-box field, Yoga's default, and the line
|
|
382
|
+
* height alone for a content-box one. A null `height` is no height. A value that is not a number
|
|
383
|
+
* cannot be added up, and leaves the field as it was.
|
|
384
|
+
*/
|
|
385
|
+
function centreSingleLine(viewName, props, fontScale) {
|
|
386
|
+
const lineHeight = props['lineHeight'];
|
|
387
|
+
if (!TEXT_INPUTS.has(viewName) || typeof lineHeight !== 'number')
|
|
388
|
+
return;
|
|
389
|
+
if (props['multiline'] === true)
|
|
390
|
+
return;
|
|
391
|
+
const height = props['height'];
|
|
392
|
+
if (typeof height === 'number') {
|
|
393
|
+
delete props['lineHeight'];
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// A percentage is a fixed height only where the parent's is definite, known at layout alone.
|
|
397
|
+
if (isSet(height) && height !== 'auto')
|
|
398
|
+
return;
|
|
399
|
+
// Android sizes the field by its line height, and centres the text in it, itself.
|
|
400
|
+
if (viewName !== 'TextInput')
|
|
401
|
+
return;
|
|
402
|
+
const edge = (side, logical) => [
|
|
403
|
+
props[`padding${side}`] ??
|
|
404
|
+
props[`paddingBlock${logical}`] ??
|
|
405
|
+
props['paddingBlock'] ??
|
|
406
|
+
props['paddingVertical'] ??
|
|
407
|
+
props['padding'] ??
|
|
408
|
+
0,
|
|
409
|
+
props[`border${side}Width`] ?? props['borderWidth'] ?? 0,
|
|
410
|
+
];
|
|
411
|
+
const line = lineHeight * textScale(props, fontScale);
|
|
412
|
+
const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
|
|
413
|
+
const own = props['minHeight'] ?? 0;
|
|
414
|
+
const max = props['maxHeight'] ?? Infinity;
|
|
415
|
+
if (![...box, own, max].every((part) => typeof part === 'number'))
|
|
416
|
+
return;
|
|
417
|
+
delete props['lineHeight'];
|
|
418
|
+
// A content-box field's minHeight is its content's: Yoga adds the padding and border itself.
|
|
419
|
+
const content = props['boxSizing'] === 'content-box'
|
|
420
|
+
? line
|
|
421
|
+
: box.reduce((sum, part) => sum + part, 0);
|
|
422
|
+
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Start a multiline Android text field's text at the top, as iOS and a `<textarea>` do.
|
|
426
|
+
*
|
|
427
|
+
* React Native maps an unset `textAlignVertical` to `Gravity.NO_GRAVITY`, which `ReactEditText`
|
|
428
|
+
* reads as the `EditText`'s own gravity, `center_vertical`. One the app set, through the input or
|
|
429
|
+
* a CSS `vertical-align`, stays.
|
|
430
|
+
*/
|
|
431
|
+
function alignMultiline(viewName, props) {
|
|
432
|
+
if (viewName !== 'AndroidTextInput' || props['multiline'] !== true)
|
|
433
|
+
return;
|
|
434
|
+
props['textAlignVertical'] ??= 'top';
|
|
435
|
+
}
|
|
321
436
|
/**
|
|
322
437
|
* Resolve a paragraph's `text-align` against its direction, as CSS does.
|
|
323
438
|
*
|
|
@@ -361,13 +476,45 @@ const MODAL_HOST = 'ModalHostView';
|
|
|
361
476
|
* and skipping it is not cosmetic: `RNSScreenStackHeaderConfig` without its `position: absolute`
|
|
362
477
|
* joins the screen's flex layout and pushes every page down by the height of the header.
|
|
363
478
|
*/
|
|
364
|
-
export function registerViewName(elementName, viewName, defaultProps) {
|
|
479
|
+
export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
365
480
|
VIEW_NAMES[elementName] = viewName;
|
|
366
|
-
if (!defaultProps)
|
|
367
|
-
return;
|
|
368
481
|
for (const name of typeof viewName === 'string' ? [viewName] : [viewName.ios, viewName.android]) {
|
|
369
|
-
|
|
482
|
+
if (defaultProps)
|
|
483
|
+
DEFAULT_PROPS[name] = defaultProps;
|
|
484
|
+
if (options?.textContent)
|
|
485
|
+
TEXT_CONTENT_PROPS[name] = options.textContent;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** The prop each view that takes its text content as a prop reads it from. See `ViewNameOptions`. */
|
|
489
|
+
const TEXT_CONTENT_PROPS = {};
|
|
490
|
+
/** Whether `node` takes its text content as a prop rather than as text children. */
|
|
491
|
+
function takesTextAsProp(node) {
|
|
492
|
+
return node.kind === 'element' && TEXT_CONTENT_PROPS[viewNameOf(node)] !== undefined;
|
|
493
|
+
}
|
|
494
|
+
/** Sets a view's text prop from its content, unless the prop is set explicitly. */
|
|
495
|
+
function withTextContent(node, viewName, props) {
|
|
496
|
+
const prop = TEXT_CONTENT_PROPS[viewName];
|
|
497
|
+
if (!prop || node.props[prop] !== undefined)
|
|
498
|
+
return;
|
|
499
|
+
const text = textContent(node);
|
|
500
|
+
if (text)
|
|
501
|
+
props[prop] = text;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The text written inside a view that takes it as a prop: every run joined, with the whitespace at
|
|
505
|
+
* the two ends dropped as a paragraph drops it, and kept inside. A nested view is drawn after the
|
|
506
|
+
* text, as SwiftUI's `Text` through `@expo/ui` draws a child `Text`, so the space before it stays.
|
|
507
|
+
*/
|
|
508
|
+
function textContent(node) {
|
|
509
|
+
let text = '';
|
|
510
|
+
let nested = false;
|
|
511
|
+
for (const child of node.children) {
|
|
512
|
+
if (child.kind === 'text')
|
|
513
|
+
text += child.text;
|
|
514
|
+
else if (child.kind === 'element')
|
|
515
|
+
nested = true;
|
|
370
516
|
}
|
|
517
|
+
return nested ? text.trimStart() : text.trim();
|
|
371
518
|
}
|
|
372
519
|
/** An element's registered view name, for the platform the host reported. */
|
|
373
520
|
function registeredViewName(elementName) {
|
|
@@ -949,15 +1096,19 @@ export class Engine {
|
|
|
949
1096
|
* for them. Only those in the tree: see `releaseDetached`.
|
|
950
1097
|
*/
|
|
951
1098
|
hoisted = new Set();
|
|
952
|
-
/**
|
|
953
|
-
removedSinceCommit =
|
|
1099
|
+
/** The nodes taken out of a child list since the last commit. See `releaseDetached`. */
|
|
1100
|
+
removedSinceCommit = new Set();
|
|
954
1101
|
/**
|
|
955
|
-
* `@keyframes` by name, across every sheet
|
|
1102
|
+
* `@keyframes` by name, across every sheet registered. One registry rather than one per sheet,
|
|
956
1103
|
* because that is the scope CSS gives them: a name defined in a global stylesheet is usable
|
|
957
1104
|
* from a component's. The cost is that two components defining the same name collide, last one
|
|
958
1105
|
* in wins, exactly as two stylesheets in a document would.
|
|
959
1106
|
*/
|
|
960
1107
|
keyframes = new Map();
|
|
1108
|
+
/** Every sheet registered, in the order a document would hold them. See `sheetReplaced`. */
|
|
1109
|
+
sheetOrder = [];
|
|
1110
|
+
/** The `@keyframes` each playing animation was started from. See `startPlaying`. */
|
|
1111
|
+
playedFrames = new WeakMap();
|
|
961
1112
|
/** Element names already reported as unclaimed, so a list of a thousand rows reports once. */
|
|
962
1113
|
reported = new Set();
|
|
963
1114
|
processColor;
|
|
@@ -973,12 +1124,15 @@ export class Engine {
|
|
|
973
1124
|
const conditions = options.conditions ?? { width: 0, height: 0, colorScheme: 'light' };
|
|
974
1125
|
this.styles = new StyleResolver(options.globalStyles ?? null, conditions);
|
|
975
1126
|
this.viewportSize = { width: conditions.width, height: conditions.height };
|
|
1127
|
+
this.fontScale = conditions.fontScale;
|
|
976
1128
|
if (options.tokens)
|
|
977
1129
|
this.styles.setRootTokens(options.tokens);
|
|
978
1130
|
this.structuralSheets = options.globalStyles?.structural === true;
|
|
979
1131
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
980
|
-
if (this.dev)
|
|
1132
|
+
if (this.dev) {
|
|
981
1133
|
this.styles.onUndefinedToken = (name, props) => this.reportUndefinedToken(name, props);
|
|
1134
|
+
this.styles.onUnreadDisplay = (name, value) => this.reportUnreadDisplay(name, value);
|
|
1135
|
+
}
|
|
982
1136
|
this.now = options.now ?? (() => globalThis.performance?.now?.() ?? Date.now());
|
|
983
1137
|
this.onDirty = options.onDirty;
|
|
984
1138
|
this.onError = options.onError;
|
|
@@ -1050,6 +1204,35 @@ export class Engine {
|
|
|
1050
1204
|
* a caller owns the properties it names and nothing else. Each owner writes all of its own at
|
|
1051
1205
|
* once - four insets together, never one - so nothing is left over from a previous rotation.
|
|
1052
1206
|
*/
|
|
1207
|
+
/**
|
|
1208
|
+
* Match `sheet` against every node from now on, after the global sheet the engine was made
|
|
1209
|
+
* with, and restyle everything; false, and nothing done, when it already is. How a platform
|
|
1210
|
+
* applies a component whose CSS is not scoped, such as Angular's `ViewEncapsulation.None`.
|
|
1211
|
+
* `replacing` is a sheet added before that this one takes the place of, after a hot swap.
|
|
1212
|
+
*/
|
|
1213
|
+
addGlobalSheet(sheet, replacing) {
|
|
1214
|
+
if (!this.styles.addGlobalSheet(sheet, replacing))
|
|
1215
|
+
return false;
|
|
1216
|
+
if (replacing)
|
|
1217
|
+
this.sheetReplaced(replacing, sheet);
|
|
1218
|
+
else
|
|
1219
|
+
this.registerSheet(sheet);
|
|
1220
|
+
if (sheet.structural)
|
|
1221
|
+
this.structuralSheets = true;
|
|
1222
|
+
this.markPath(this.root);
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Stop matching a sheet `addGlobalSheet` added, and restyle everything; false, and nothing
|
|
1227
|
+
* done, when it is not one. A hot swap that leaves a None component no rules, or makes it scoped.
|
|
1228
|
+
*/
|
|
1229
|
+
removeGlobalSheet(sheet) {
|
|
1230
|
+
if (!this.styles.removeGlobalSheet(sheet))
|
|
1231
|
+
return false;
|
|
1232
|
+
this.sheetReplaced(sheet, null);
|
|
1233
|
+
this.markPath(this.root);
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1053
1236
|
updateTokens(next) {
|
|
1054
1237
|
this.styles.setRootTokens({ ...this.styles.rootTokens, ...next });
|
|
1055
1238
|
this.root.subtreeDirty = true;
|
|
@@ -1189,13 +1372,33 @@ export class Engine {
|
|
|
1189
1372
|
this.markProps(node, false);
|
|
1190
1373
|
this.commit();
|
|
1191
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* A node's component rules are its creating sheet's, so this matches it against `like`'s, or
|
|
1377
|
+
* against the one it was created with once `like` is null.
|
|
1378
|
+
*/
|
|
1379
|
+
adoptScope(node, like) {
|
|
1380
|
+
if (like && !this.ownSheets.has(node))
|
|
1381
|
+
this.ownSheets.set(node, node.sheet);
|
|
1382
|
+
const sheet = like ? like.sheet : this.ownSheets.get(node);
|
|
1383
|
+
if (sheet === undefined || node.sheet === sheet)
|
|
1384
|
+
return;
|
|
1385
|
+
node.sheet = sheet;
|
|
1386
|
+
if (sheet?.structural)
|
|
1387
|
+
this.structuralSheets = true;
|
|
1388
|
+
this.markProps(node);
|
|
1389
|
+
}
|
|
1390
|
+
/** The sheet each node given another's scope was created with. */
|
|
1391
|
+
ownSheets = new WeakMap();
|
|
1192
1392
|
/** The window's size, from the conditions media queries use. Zero until the platform says. */
|
|
1193
1393
|
get viewport() {
|
|
1194
1394
|
return this.viewportSize;
|
|
1195
1395
|
}
|
|
1196
1396
|
viewportSize;
|
|
1397
|
+
/** The system text size, from the conditions. See `Conditions.fontScale`. */
|
|
1398
|
+
fontScale;
|
|
1197
1399
|
updateConditions(next) {
|
|
1198
1400
|
this.viewportSize = { width: next.width, height: next.height };
|
|
1401
|
+
this.fontScale = next.fontScale;
|
|
1199
1402
|
this.styles.setConditions(next);
|
|
1200
1403
|
this.root.subtreeDirty = true;
|
|
1201
1404
|
// Committed here rather than left to the next change-detection pass, because there may not
|
|
@@ -1220,6 +1423,26 @@ export class Engine {
|
|
|
1220
1423
|
node.classes?.delete(name);
|
|
1221
1424
|
this.markProps(node);
|
|
1222
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* The sheet of the component `node` hosts, whose `:host` rules it matches. Set again when a hot
|
|
1428
|
+
* swap replaces the component's sheet, which recreates the views inside but not the host, so the
|
|
1429
|
+
* host is styled afresh here.
|
|
1430
|
+
*/
|
|
1431
|
+
setHostSheet(node, sheet) {
|
|
1432
|
+
if (node.hostSheet === sheet)
|
|
1433
|
+
return;
|
|
1434
|
+
node.hostSheet = sheet;
|
|
1435
|
+
this.markProps(node);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Style for `node` that any matched rule or inline style overrides, as a browser's own default
|
|
1439
|
+
* for an element is. `mount` gives the root component's host the surface's height this way, as
|
|
1440
|
+
* the web's mount point has, unless the app's `:host` sizes it otherwise.
|
|
1441
|
+
*/
|
|
1442
|
+
setDefaultStyle(node, style) {
|
|
1443
|
+
node.defaultStyle = style;
|
|
1444
|
+
this.markProps(node);
|
|
1445
|
+
}
|
|
1223
1446
|
/** `class="a b"` from a template. Replaces the set rather than adding to it. */
|
|
1224
1447
|
setClasses(node, value) {
|
|
1225
1448
|
const names = value.split(/\s+/).filter(Boolean);
|
|
@@ -1242,6 +1465,7 @@ export class Engine {
|
|
|
1242
1465
|
if (child.dormantHoists)
|
|
1243
1466
|
this.wakeHoists(child);
|
|
1244
1467
|
this.markStructure(parent);
|
|
1468
|
+
this.markTextContent(parent, child);
|
|
1245
1469
|
}
|
|
1246
1470
|
insertBefore(parent, child, ref) {
|
|
1247
1471
|
this.detach(child);
|
|
@@ -1254,6 +1478,7 @@ export class Engine {
|
|
|
1254
1478
|
if (child.dormantHoists)
|
|
1255
1479
|
this.wakeHoists(child);
|
|
1256
1480
|
this.markStructure(parent);
|
|
1481
|
+
this.markTextContent(parent, child);
|
|
1257
1482
|
}
|
|
1258
1483
|
removeChild(parent, child) {
|
|
1259
1484
|
const target = parent ?? child.parent;
|
|
@@ -1264,8 +1489,17 @@ export class Engine {
|
|
|
1264
1489
|
return;
|
|
1265
1490
|
target.children.splice(at, 1);
|
|
1266
1491
|
child.parent = null;
|
|
1267
|
-
this.removedSinceCommit
|
|
1492
|
+
this.removedSinceCommit.add(child);
|
|
1268
1493
|
this.markStructure(target);
|
|
1494
|
+
this.markTextContent(target, child);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* A child came or went under a view that takes its text as a prop: that prop changed. A span
|
|
1498
|
+
* changes it too, by keeping or dropping the space before it.
|
|
1499
|
+
*/
|
|
1500
|
+
markTextContent(parent, child) {
|
|
1501
|
+
if (child.kind !== 'anchor' && takesTextAsProp(parent))
|
|
1502
|
+
this.markProps(parent);
|
|
1269
1503
|
}
|
|
1270
1504
|
/** A subtree that went out of the tree is back in: its hoisted nodes commit again. */
|
|
1271
1505
|
wakeHoists(node) {
|
|
@@ -1288,12 +1522,17 @@ export class Engine {
|
|
|
1288
1522
|
* whole. So a hoisted node moves to the root of the subtree it left with, which is garbage along
|
|
1289
1523
|
* with it if the subtree never returns, and is handed back when it does. An animation on a node
|
|
1290
1524
|
* out of the tree stops rather than running a frame loop for something nobody can see, and
|
|
1291
|
-
* starts again if the node returns.
|
|
1525
|
+
* starts again if the node returns. And a node out of the tree when the commit runs forgets its
|
|
1526
|
+
* committed views, as a withheld modal does: the commit unmounts them, Fabric never re-enables an
|
|
1527
|
+
* unmounted view's events (`EventEmitter::setEnabled`), and committed again they would ignore
|
|
1528
|
+
* every touch. It comes back created afresh, as React creates an element it mounts again. One
|
|
1529
|
+
* put back before the commit never left, and keeps its views.
|
|
1292
1530
|
*
|
|
1293
|
-
* Once per commit, and only after a removal: the walks are the depth of a handful of nodes
|
|
1531
|
+
* Once per commit, and only after a removal: the walks are the depth of a handful of nodes, and
|
|
1532
|
+
* forgetting walks each subtree that left once.
|
|
1294
1533
|
*/
|
|
1295
1534
|
releaseDetached() {
|
|
1296
|
-
this.
|
|
1535
|
+
this.forgetDetached();
|
|
1297
1536
|
for (const node of this.hoisted) {
|
|
1298
1537
|
const top = this.topOf(node);
|
|
1299
1538
|
if (top === this.root)
|
|
@@ -1319,6 +1558,14 @@ export class Engine {
|
|
|
1319
1558
|
if (this.focusedNode && this.topOf(this.focusedNode) !== this.root)
|
|
1320
1559
|
this.focusedNode = null;
|
|
1321
1560
|
}
|
|
1561
|
+
/** Forget the committed views of each node removed since the last commit that is still out. */
|
|
1562
|
+
forgetDetached() {
|
|
1563
|
+
for (const node of this.removedSinceCommit) {
|
|
1564
|
+
if (node.committed && this.topOf(node) !== this.root)
|
|
1565
|
+
this.forgetCommitted(node);
|
|
1566
|
+
}
|
|
1567
|
+
this.removedSinceCommit.clear();
|
|
1568
|
+
}
|
|
1322
1569
|
/** The root of the tree a node is in: the engine's root while it is attached. */
|
|
1323
1570
|
topOf(node) {
|
|
1324
1571
|
let top = node;
|
|
@@ -1421,6 +1668,8 @@ export class Engine {
|
|
|
1421
1668
|
setText(node, value) {
|
|
1422
1669
|
node.text = value;
|
|
1423
1670
|
this.markProps(node);
|
|
1671
|
+
if (node.parent)
|
|
1672
|
+
this.markTextContent(node.parent, node);
|
|
1424
1673
|
}
|
|
1425
1674
|
setEventListener(node, topLevelType, fn) {
|
|
1426
1675
|
this.registerEventHandler();
|
|
@@ -1494,7 +1743,7 @@ export class Engine {
|
|
|
1494
1743
|
* optimisation.
|
|
1495
1744
|
*/
|
|
1496
1745
|
commit() {
|
|
1497
|
-
if (this.removedSinceCommit)
|
|
1746
|
+
if (this.removedSinceCommit.size)
|
|
1498
1747
|
this.releaseDetached();
|
|
1499
1748
|
// The root is never reconciled itself, so it has no `committed` record and `isClean` would
|
|
1500
1749
|
// always say dirty. Ask the flags directly, or every change-detection pass ends in a
|
|
@@ -1511,6 +1760,8 @@ export class Engine {
|
|
|
1511
1760
|
this.facesAdded = false;
|
|
1512
1761
|
const set = this.fabric.createChildSet(this.rootTag);
|
|
1513
1762
|
for (const child of this.visibleChildren(this.root)) {
|
|
1763
|
+
if (this.withheld(child))
|
|
1764
|
+
continue;
|
|
1514
1765
|
this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
|
|
1515
1766
|
}
|
|
1516
1767
|
this.clearFlags(this.root);
|
|
@@ -1599,12 +1850,14 @@ export class Engine {
|
|
|
1599
1850
|
}
|
|
1600
1851
|
/** Anchors take part in sibling ordering but never reach Fabric. */
|
|
1601
1852
|
visibleChildren(node) {
|
|
1602
|
-
|
|
1853
|
+
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
1854
|
+
const textAsProp = takesTextAsProp(node);
|
|
1855
|
+
return node.children.filter((child) => child.kind !== 'anchor' && !(textAsProp && child.kind === 'text'));
|
|
1603
1856
|
}
|
|
1604
1857
|
/**
|
|
1605
|
-
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults,
|
|
1606
|
-
* matched CSS, explicit props, inline style, a component's
|
|
1607
|
-
* on the web.
|
|
1858
|
+
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults, the
|
|
1859
|
+
* node's `defaultStyle`, matched CSS, explicit props, inline style, a component's
|
|
1860
|
+
* `styleOverride`. Inline wins over CSS for the same reason it does on the web.
|
|
1608
1861
|
*
|
|
1609
1862
|
* Raw, because this is what the next commit diffs against. Colours and asset ids are converted
|
|
1610
1863
|
* on the way out instead (`processed`), and only for the keys that changed: the converters
|
|
@@ -1618,7 +1871,7 @@ export class Engine {
|
|
|
1618
1871
|
this.checkProps(node);
|
|
1619
1872
|
this.registerSheet(node.sheet);
|
|
1620
1873
|
this.registerSheet(node.hostSheet);
|
|
1621
|
-
const props = { ...DEFAULT_PROPS[viewName] };
|
|
1874
|
+
const props = { ...DEFAULT_PROPS[viewName], ...node.defaultStyle };
|
|
1622
1875
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
1623
1876
|
for (const key of Object.keys(node.props)) {
|
|
1624
1877
|
// No native prop has a hyphen. `data-*` and `aria-*` attributes stay on the node for
|
|
@@ -1630,6 +1883,7 @@ export class Engine {
|
|
|
1630
1883
|
props[key] = node.props[key];
|
|
1631
1884
|
}
|
|
1632
1885
|
}
|
|
1886
|
+
withTextContent(node, viewName, props);
|
|
1633
1887
|
const style = flattenStyle(node.props['style'], props);
|
|
1634
1888
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
1635
1889
|
if (intrinsic)
|
|
@@ -1640,7 +1894,10 @@ export class Engine {
|
|
|
1640
1894
|
alignText(style, this.directionOf(node, style));
|
|
1641
1895
|
if (this.fontsRefreshed)
|
|
1642
1896
|
this.capForFonts(node, style);
|
|
1643
|
-
|
|
1897
|
+
alignMultiline(viewName, style);
|
|
1898
|
+
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
1899
|
+
centreSingleLine(viewName, merged, this.fontScale);
|
|
1900
|
+
return merged;
|
|
1644
1901
|
}
|
|
1645
1902
|
/**
|
|
1646
1903
|
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
@@ -1698,12 +1955,59 @@ export class Engine {
|
|
|
1698
1955
|
if (!sheet || this.knownSheets.has(sheet))
|
|
1699
1956
|
return;
|
|
1700
1957
|
this.knownSheets.add(sheet);
|
|
1958
|
+
this.sheetOrder.push(sheet);
|
|
1701
1959
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
1702
1960
|
this.keyframes.set(name, sheet.keyframes[name]);
|
|
1703
1961
|
}
|
|
1704
1962
|
if (sheet.fonts && this.fontFaces.add(sheet.fonts))
|
|
1705
1963
|
this.facesAdded = true;
|
|
1706
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* A hot swap edited a component's sheet into `next`, or took it away with null: `sheet`'s
|
|
1967
|
+
* `@keyframes` go, and `next`'s take its place among the others, as an edited stylesheet keeps
|
|
1968
|
+
* its place in a document. A name another sheet also defines falls back to that one. Every
|
|
1969
|
+
* animation re-resolves the name it plays, so one whose keyframes went stops, as in a browser.
|
|
1970
|
+
* A `sheet` already gone, such as a global sheet removed, leaves `next` registered last.
|
|
1971
|
+
*/
|
|
1972
|
+
sheetReplaced(sheet, next) {
|
|
1973
|
+
if (sheet === next && this.knownSheets.has(sheet))
|
|
1974
|
+
return;
|
|
1975
|
+
const at = this.sheetOrder.indexOf(sheet);
|
|
1976
|
+
if (at === -1) {
|
|
1977
|
+
this.registerSheet(next);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
this.knownSheets.delete(sheet);
|
|
1981
|
+
if (next && !this.knownSheets.has(next)) {
|
|
1982
|
+
this.knownSheets.add(next);
|
|
1983
|
+
this.sheetOrder[at] = next;
|
|
1984
|
+
if (next.fonts && this.fontFaces.add(next.fonts))
|
|
1985
|
+
this.facesAdded = true;
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
this.sheetOrder.splice(at, 1);
|
|
1989
|
+
}
|
|
1990
|
+
if (sheet.keyframes || next?.keyframes)
|
|
1991
|
+
this.reindexKeyframes();
|
|
1992
|
+
}
|
|
1993
|
+
/** Every sheet's `@keyframes` again, in order, and every animation told to look them up. */
|
|
1994
|
+
reindexKeyframes() {
|
|
1995
|
+
this.keyframes.clear();
|
|
1996
|
+
for (const sheet of this.sheetOrder) {
|
|
1997
|
+
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
1998
|
+
this.keyframes.set(name, sheet.keyframes[name]);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
this.markAnimated(this.root);
|
|
2002
|
+
}
|
|
2003
|
+
/** Mark every node under `node` with an animation, so it looks its keyframes up again. */
|
|
2004
|
+
markAnimated(node) {
|
|
2005
|
+
for (const child of node.children) {
|
|
2006
|
+
if (child.playing || child.scrolled)
|
|
2007
|
+
this.markProps(child, false);
|
|
2008
|
+
this.markAnimated(child);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
1707
2011
|
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
1708
2012
|
fontFaces = new FontFaces();
|
|
1709
2013
|
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
@@ -1747,6 +2051,10 @@ export class Engine {
|
|
|
1747
2051
|
}
|
|
1748
2052
|
const frames = this.keyframes.get(spec.name);
|
|
1749
2053
|
if (!frames) {
|
|
2054
|
+
// Keyframes a hot swap deleted, from under an animation that was playing them.
|
|
2055
|
+
node.playing = undefined;
|
|
2056
|
+
this.playing.delete(node);
|
|
2057
|
+
this.stopScrolled(node);
|
|
1750
2058
|
if (this.dev)
|
|
1751
2059
|
this.reportMissingKeyframes(spec.name);
|
|
1752
2060
|
return props;
|
|
@@ -1759,10 +2067,12 @@ export class Engine {
|
|
|
1759
2067
|
/** Start the clock on an animation, unless the node is already playing this one. */
|
|
1760
2068
|
startPlaying(node, spec, frames, props) {
|
|
1761
2069
|
const current = node.playing;
|
|
2070
|
+
const inherited = this.inheritedColour(node, frames);
|
|
1762
2071
|
if (!current || !sameAnimation(current.spec, spec)) {
|
|
1763
2072
|
const started = {
|
|
1764
2073
|
spec,
|
|
1765
|
-
tracks: tracksOf(frames, props),
|
|
2074
|
+
tracks: tracksOf(frames, props, inherited),
|
|
2075
|
+
inherited,
|
|
1766
2076
|
start: this.now(),
|
|
1767
2077
|
values: {},
|
|
1768
2078
|
done: false,
|
|
@@ -1775,6 +2085,7 @@ export class Engine {
|
|
|
1775
2085
|
// resting style: `animation-fill-mode: backwards` behaviour, always. It is what an entrance
|
|
1776
2086
|
// wants, and the alternative is the flash again.
|
|
1777
2087
|
started.values = sample(started, this.now()).values;
|
|
2088
|
+
this.playedFrames.set(started, frames);
|
|
1778
2089
|
node.playing = started;
|
|
1779
2090
|
if (spec.paused)
|
|
1780
2091
|
started.pausedAt = this.now();
|
|
@@ -1783,7 +2094,34 @@ export class Engine {
|
|
|
1783
2094
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
1784
2095
|
return;
|
|
1785
2096
|
}
|
|
1786
|
-
this.
|
|
2097
|
+
if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
|
|
2098
|
+
this.reframe(node, current, frames, props, inherited);
|
|
2099
|
+
}
|
|
2100
|
+
this.playOrPause(node, node.playing, spec);
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
|
|
2104
|
+
* wherever the animation has got to, as in CSS. Undefined for frames that do not.
|
|
2105
|
+
*/
|
|
2106
|
+
inheritedColour(node, frames) {
|
|
2107
|
+
if (!readsInheritedColour(frames))
|
|
2108
|
+
return undefined;
|
|
2109
|
+
const parent = node.parent && this.styles.resolve(node.parent, this.styleEpoch);
|
|
2110
|
+
return parent?.inherited['color'] ?? 'black';
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* The name an animation plays now has other frames: a hot swap edited them, or took away the
|
|
2114
|
+
* sheet whose copy won. Or the colour its `color: currentColor` stands for has changed. It
|
|
2115
|
+
* carries on along the new tracks, on its own clock, as a browser does, paused or not.
|
|
2116
|
+
*/
|
|
2117
|
+
reframe(node, current, frames, props, inherited) {
|
|
2118
|
+
const tracks = tracksOf(frames, props, inherited);
|
|
2119
|
+
const reframed = { ...current, tracks, inherited };
|
|
2120
|
+
const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
|
|
2121
|
+
const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
|
|
2122
|
+
reframed.values = finished && !holds ? {} : values;
|
|
2123
|
+
this.playedFrames.set(reframed, frames);
|
|
2124
|
+
node.playing = reframed;
|
|
1787
2125
|
}
|
|
1788
2126
|
/**
|
|
1789
2127
|
* `animation-play-state` changed on an animation already playing: pausing holds the frame it
|
|
@@ -1817,15 +2155,16 @@ export class Engine {
|
|
|
1817
2155
|
*/
|
|
1818
2156
|
scrollAnimated(node, spec, frames, props) {
|
|
1819
2157
|
const current = node.scrolled;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2158
|
+
const inherited = this.inheritedColour(node, frames);
|
|
2159
|
+
if (current?.frames === frames && sameAnimation(current.spec, spec)) {
|
|
2160
|
+
return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
|
|
2161
|
+
}
|
|
2162
|
+
this.stopScrolled(node);
|
|
1824
2163
|
if (node.playing) {
|
|
1825
2164
|
node.playing = undefined;
|
|
1826
2165
|
this.playing.delete(node);
|
|
1827
2166
|
}
|
|
1828
|
-
const tracks = tracksOf(frames, props);
|
|
2167
|
+
const tracks = tracksOf(frames, props, inherited);
|
|
1829
2168
|
const source = this.scrollSourceOf(node);
|
|
1830
2169
|
const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
|
|
1831
2170
|
const range = rangeOf(spec, extent);
|
|
@@ -1838,9 +2177,31 @@ export class Engine {
|
|
|
1838
2177
|
// view for the animation to move. Committed with the first frame, so it stays put.
|
|
1839
2178
|
if (drive)
|
|
1840
2179
|
first['collapsable'] = false;
|
|
1841
|
-
node.scrolled = { spec, tracks, resting, first, source, drive };
|
|
2180
|
+
node.scrolled = { spec, frames, tracks, resting, first, source, drive, inherited };
|
|
1842
2181
|
return Object.assign(props, first);
|
|
1843
2182
|
}
|
|
2183
|
+
/**
|
|
2184
|
+
* The first frame of a scroll-driven animation already playing. When the colour its
|
|
2185
|
+
* `color: currentColor` stands for has changed, that frame's colour is taken from tracks built
|
|
2186
|
+
* again. Native drives opacity and transforms only, so a colour holds its first frame, and
|
|
2187
|
+
* nothing native animates is touched.
|
|
2188
|
+
*/
|
|
2189
|
+
rescrolled(node, current, frames, props, inherited) {
|
|
2190
|
+
if (inherited === current.inherited)
|
|
2191
|
+
return current.first;
|
|
2192
|
+
// The element's own colour, which a track with no first frame starts at, may be the one it
|
|
2193
|
+
// inherits, so it is read again too.
|
|
2194
|
+
const resting = { ...current.resting, color: props['color'] };
|
|
2195
|
+
const tracks = tracksOf(frames, resting, inherited);
|
|
2196
|
+
const extent = current.source ? this.scrollExtents.get(current.source) : undefined;
|
|
2197
|
+
const range = rangeOf(current.spec, extent?.[current.spec.timeline] ?? null);
|
|
2198
|
+
const first = { ...current.first };
|
|
2199
|
+
const colour = firstFrame(tracks, current.spec, range)['color'];
|
|
2200
|
+
if ('color' in first)
|
|
2201
|
+
first['color'] = colour;
|
|
2202
|
+
node.scrolled = { ...current, tracks, resting, first, inherited };
|
|
2203
|
+
return first;
|
|
2204
|
+
}
|
|
1844
2205
|
/** Hand a scroll-driven animation's channels to native, fed by `source`'s scroll events. */
|
|
1845
2206
|
driveScrolled(node, source, spec, tracks, resting, range) {
|
|
1846
2207
|
if (!this.scrollDriver)
|
|
@@ -2114,9 +2475,9 @@ export class Engine {
|
|
|
2114
2475
|
if (this.isClean(node) && !styleChanged)
|
|
2115
2476
|
return node.committed.handle;
|
|
2116
2477
|
node.styleCommitted = style;
|
|
2117
|
-
const childHandles = this.reconcileChildren(node, style);
|
|
2118
|
-
const previous = node.committed;
|
|
2119
2478
|
const viewName = viewNameOf(node);
|
|
2479
|
+
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
2480
|
+
const previous = node.committed;
|
|
2120
2481
|
this.notePresented(node, viewName);
|
|
2121
2482
|
if (!previous)
|
|
2122
2483
|
return this.create(node, viewName, this.mergeProps(node, viewName), childHandles);
|
|
@@ -2168,19 +2529,30 @@ export class Engine {
|
|
|
2168
2529
|
* handle as it stands, so it is answered here without the call, the resolve and the walk -
|
|
2169
2530
|
* which, per child, was most of what a one-row update cost.
|
|
2170
2531
|
*/
|
|
2171
|
-
reconcileChildren(node, style) {
|
|
2532
|
+
reconcileChildren(node, viewName, style) {
|
|
2172
2533
|
const context = style?.context;
|
|
2173
2534
|
if (HOIST_TARGETS.has(node.name))
|
|
2174
2535
|
return this.reconcileHoistTarget(node, context);
|
|
2536
|
+
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
2537
|
+
const textAsProp = TEXT_CONTENT_PROPS[viewName] !== undefined;
|
|
2175
2538
|
const handles = [];
|
|
2176
2539
|
for (const child of node.children) {
|
|
2177
2540
|
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
2178
2541
|
continue;
|
|
2179
2542
|
}
|
|
2543
|
+
if (textAsProp && child.kind === 'text')
|
|
2544
|
+
continue;
|
|
2180
2545
|
handles.push(this.reconcileUnder(node, child, context));
|
|
2181
2546
|
}
|
|
2182
2547
|
return handles;
|
|
2183
2548
|
}
|
|
2549
|
+
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|
|
2550
|
+
notePresented(node, viewName) {
|
|
2551
|
+
if (viewName !== MODAL_HOST || node.props['visible'] === false)
|
|
2552
|
+
return;
|
|
2553
|
+
if (platformOS === 'ios')
|
|
2554
|
+
node.presented = true;
|
|
2555
|
+
}
|
|
2184
2556
|
/**
|
|
2185
2557
|
* Whether a child is left out of its parent's commit: a modal host whose `visible` is false.
|
|
2186
2558
|
*
|
|
@@ -2190,18 +2562,19 @@ export class Engine {
|
|
|
2190
2562
|
* committed, with `visible` false, until native reports `topDismiss`: Modal.js keeps it that
|
|
2191
2563
|
* long so the dismissal animates and `(dismiss)` fires. Android drops it at once, as Modal.js
|
|
2192
2564
|
* does there.
|
|
2565
|
+
*
|
|
2566
|
+
* A withheld host is unmounted, and Fabric drops an unmounted view's event target for good
|
|
2567
|
+
* (`EventEmitter::setEnabled`), so its committed handles are forgotten and it is created afresh
|
|
2568
|
+
* when it is shown again, as Modal.js creates it afresh. Committed again, it ignores every touch.
|
|
2193
2569
|
*/
|
|
2194
|
-
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|
|
2195
|
-
notePresented(node, viewName) {
|
|
2196
|
-
if (viewName !== MODAL_HOST || node.props['visible'] === false)
|
|
2197
|
-
return;
|
|
2198
|
-
if (platformOS === 'ios')
|
|
2199
|
-
node.presented = true;
|
|
2200
|
-
}
|
|
2201
2570
|
withheld(child) {
|
|
2202
2571
|
if (child.props['visible'] !== false || child.presented)
|
|
2203
2572
|
return false;
|
|
2204
|
-
|
|
2573
|
+
if (viewNameOf(child) !== MODAL_HOST)
|
|
2574
|
+
return false;
|
|
2575
|
+
if (child.committed)
|
|
2576
|
+
this.forgetCommitted(child);
|
|
2577
|
+
return true;
|
|
2205
2578
|
}
|
|
2206
2579
|
/**
|
|
2207
2580
|
* `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
|
|
@@ -2252,11 +2625,15 @@ export class Engine {
|
|
|
2252
2625
|
child.committedUnder = parent;
|
|
2253
2626
|
return handle;
|
|
2254
2627
|
}
|
|
2255
|
-
/**
|
|
2628
|
+
/**
|
|
2629
|
+
* Let a node and everything under it be created again at the next commit. The views it keeps
|
|
2630
|
+
* for hoisted names go too: they are children of the native view it is losing.
|
|
2631
|
+
*/
|
|
2256
2632
|
forgetCommitted(node) {
|
|
2257
2633
|
node.committed = null;
|
|
2258
2634
|
node.committedUnder = null;
|
|
2259
2635
|
node.styleCommitted = null;
|
|
2636
|
+
node.kept = undefined;
|
|
2260
2637
|
for (const child of node.children)
|
|
2261
2638
|
if (child.committed)
|
|
2262
2639
|
this.forgetCommitted(child);
|
|
@@ -2406,6 +2783,20 @@ export class Engine {
|
|
|
2406
2783
|
` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
|
|
2407
2784
|
` :root or an ancestor, or give the var() a fallback.`);
|
|
2408
2785
|
}
|
|
2786
|
+
unreadDisplays = new Set();
|
|
2787
|
+
/**
|
|
2788
|
+
* Said once per token and value: the compiler refuses `display: grid` written out, but a token
|
|
2789
|
+
* is only known here, and without this the view lays out as flex with nothing said.
|
|
2790
|
+
*/
|
|
2791
|
+
reportUnreadDisplay(name, value) {
|
|
2792
|
+
const report = `${name} ${value}`;
|
|
2793
|
+
if (this.unreadDisplays.has(report))
|
|
2794
|
+
return;
|
|
2795
|
+
this.unreadDisplays.add(report);
|
|
2796
|
+
console.warn(`[angular-native] display: var(${name}) is ${value ?? 'no keyword'}, which is no display` +
|
|
2797
|
+
` native has, so display is unset and the view lays out as flex. Native has flex, none` +
|
|
2798
|
+
` and contents, and reads block, inline, inline-block, flow-root and inline-flex as flex.`);
|
|
2799
|
+
}
|
|
2409
2800
|
checkProps(node) {
|
|
2410
2801
|
if (!node.claimed || !PRIMITIVE_NAMES.has(node.name))
|
|
2411
2802
|
return;
|
|
@@ -2767,7 +3158,7 @@ export class Engine {
|
|
|
2767
3158
|
// Listeners get RN's documented shape, `{nativeEvent}`. Fabric hands us the payload bare;
|
|
2768
3159
|
// React wraps it in a synthetic event and every RN API is written against `event.nativeEvent`,
|
|
2769
3160
|
// so handlers ported from RN would silently read undefined otherwise.
|
|
2770
|
-
const event = new SyntheticEvent(nativeEvent);
|
|
3161
|
+
const event = new SyntheticEvent(nativeEvent, target);
|
|
2771
3162
|
try {
|
|
2772
3163
|
if (topLevelType === 'topDismiss' && target)
|
|
2773
3164
|
this.dismissed(target);
|