@ng-native/fabric 0.13.0 → 0.15.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/css.d.ts +63 -6
- package/dist/css.js +268 -59
- package/dist/css.js.map +1 -1
- package/dist/defer-triggers.js +2 -8
- package/dist/defer-triggers.js.map +1 -1
- package/dist/engine.d.ts +201 -21
- package/dist/engine.js +915 -125
- package/dist/engine.js.map +1 -1
- package/dist/fabric.d.ts +1 -1
- package/dist/fabric.js +25 -0
- package/dist/fabric.js.map +1 -1
- package/dist/font-faces.d.ts +19 -0
- package/dist/font-faces.js +42 -0
- package/dist/font-faces.js.map +1 -1
- package/dist/forms-inputs.d.ts +1 -1
- package/dist/forms-inputs.js +2 -2
- package/dist/forms-inputs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.js +56 -7
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +4 -3
- package/dist/inline-token.js +41 -8
- package/dist/inline-token.js.map +1 -1
- package/dist/native-drive.d.ts +42 -1
- package/dist/native-drive.js +135 -9
- package/dist/native-drive.js.map +1 -1
- package/dist/node-global.d.ts +9 -0
- package/dist/node-global.js +41 -0
- package/dist/node-global.js.map +1 -0
- package/dist/relative-colour.d.ts +2 -2
- package/dist/relative-colour.js +4 -0
- package/dist/relative-colour.js.map +1 -1
- package/dist/scroll-animation.d.ts +12 -0
- package/dist/scroll-animation.js +10 -0
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +3 -2
- package/dist/transition.js +17 -8
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* because a persistent parent holds references to specific child handles. That bubble is
|
|
8
8
|
* inherent and is what React's own Fabric renderer does.
|
|
9
9
|
*/
|
|
10
|
-
import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, joinSubjects, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
11
11
|
import { applyAria } from "./aria-props.js";
|
|
12
12
|
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
13
13
|
import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
|
|
@@ -15,7 +15,7 @@ import { isFormsInput } from "./forms-inputs.js";
|
|
|
15
15
|
import { transformList } from "./inline-transform.js";
|
|
16
16
|
import { tokenFromValue } from "./inline-token.js";
|
|
17
17
|
import { NativeScrollDriver, scrollFeed, } from "./native-drive.js";
|
|
18
|
-
import { clockChannels, colourChannels, firstFrame, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
|
|
18
|
+
import { clockChannels, colourChannels, firstFrame, heldTransform, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
|
|
19
19
|
import { FontFaces } from "./font-faces.js";
|
|
20
20
|
import { premultipliedStops } from "./premultiplied-stops.js";
|
|
21
21
|
/**
|
|
@@ -189,6 +189,13 @@ const VIEW_EVENT_OPT_IN_PROPS = {
|
|
|
189
189
|
topError: 'shouldNotifyLoadEvents',
|
|
190
190
|
topProgress: 'shouldNotifyLoadEvents',
|
|
191
191
|
},
|
|
192
|
+
// `ReactTextInputManager` watches each of these only behind a prop of the handler's name,
|
|
193
|
+
// which `TextInput.js` sets by passing the handler through. iOS's field sends them unasked.
|
|
194
|
+
AndroidTextInput: {
|
|
195
|
+
topContentSizeChange: 'onContentSizeChange',
|
|
196
|
+
topScroll: 'onScroll',
|
|
197
|
+
topKeyPress: 'onKeyPress',
|
|
198
|
+
},
|
|
192
199
|
};
|
|
193
200
|
/**
|
|
194
201
|
* Props the engine itself writes or reads off a node, so never a misspelling: the style, the
|
|
@@ -461,10 +468,14 @@ const blockEdge = (props, side, logical) => [
|
|
|
461
468
|
/**
|
|
462
469
|
* A multiline field's `rows` as its height, where it was given none: that many lines of its
|
|
463
470
|
* line height at the system text size, inside its padding and border, as a browser sizes a `<textarea>`. A native field
|
|
464
|
-
* is as tall as its text, which is one line before anything is typed.
|
|
471
|
+
* is as tall as its text, which is one line before anything is typed. Taken off the props once
|
|
472
|
+
* read: neither platform's native field has a prop of that name.
|
|
465
473
|
*/
|
|
466
|
-
function rowsTall(props, fontScale) {
|
|
474
|
+
function rowsTall(viewName, props, fontScale) {
|
|
475
|
+
if (!TEXT_INPUTS.has(viewName))
|
|
476
|
+
return;
|
|
467
477
|
const rows = Number(props['rows']);
|
|
478
|
+
delete props['rows'];
|
|
468
479
|
const line = props['lineHeight'];
|
|
469
480
|
if (props['multiline'] !== true || !(rows > 0) || typeof line !== 'number')
|
|
470
481
|
return;
|
|
@@ -837,6 +848,12 @@ function placeholderFaded(props) {
|
|
|
837
848
|
else if (opacity <= 0)
|
|
838
849
|
props['placeholderTextColor'] = 'rgba(0, 0, 0, 0)';
|
|
839
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* What a removed prop travels as: null, which native resets the prop on, except `pointerEvents`,
|
|
853
|
+
* which react-native-svg's Android root reads as a string and stops the app on a null. `auto` is
|
|
854
|
+
* what no value means.
|
|
855
|
+
*/
|
|
856
|
+
const cleared = (key) => (key === 'pointerEvents' ? 'auto' : null);
|
|
840
857
|
/** The least alpha iOS sends a touch to a view at: under a hundredth it is passed over. */
|
|
841
858
|
const TOUCHED_ALPHA = 0.011;
|
|
842
859
|
/**
|
|
@@ -854,6 +871,133 @@ function stillTouched(props) {
|
|
|
854
871
|
props['opacity'] = TOUCHED_ALPHA;
|
|
855
872
|
return props;
|
|
856
873
|
}
|
|
874
|
+
/** Every prop `setOwned` can set, as a payload that changes them all. */
|
|
875
|
+
const RESET_OWNED = { opacity: undefined, transform: undefined };
|
|
876
|
+
/** As Animated does: a view native sets values on is kept, or there is no view to set them on. */
|
|
877
|
+
function keptForNative(node, props) {
|
|
878
|
+
if (node.owned && props['collapsable'] === undefined)
|
|
879
|
+
props['collapsable'] = false;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* How a paragraph is truncated, which native does by its props: one the engine made around a
|
|
883
|
+
* view's own words is truncated as the view says, as a browser truncates a block's own text.
|
|
884
|
+
*/
|
|
885
|
+
const TRUNCATION = ['numberOfLines', 'ellipsizeMode'];
|
|
886
|
+
/**
|
|
887
|
+
* The truncation each paragraph made around a view's words was merged with, as the view said it.
|
|
888
|
+
* Not what it was committed with, which has the paragraph's own defaults in it.
|
|
889
|
+
*/
|
|
890
|
+
const truncatedBy = new WeakMap();
|
|
891
|
+
/** What a view says of how its own words are truncated. */
|
|
892
|
+
const truncationOf = (parent) => TRUNCATION.map((key) => ownLayout(parent, key) ?? null);
|
|
893
|
+
/** Whether a paragraph made around a view's words is truncated other than the view now says. */
|
|
894
|
+
function truncationMoved(box, parent) {
|
|
895
|
+
const merged = truncatedBy.get(box);
|
|
896
|
+
if (!merged)
|
|
897
|
+
return false;
|
|
898
|
+
return truncationOf(parent).some((said, at) => said !== merged[at]);
|
|
899
|
+
}
|
|
900
|
+
function truncatedAs(box, props) {
|
|
901
|
+
if (!box.anonymous || !box.parent)
|
|
902
|
+
return;
|
|
903
|
+
const said = truncationOf(box.parent);
|
|
904
|
+
truncatedBy.set(box, said);
|
|
905
|
+
TRUNCATION.forEach((key, at) => {
|
|
906
|
+
if (said[at] !== null)
|
|
907
|
+
props[key] = said[at];
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Stretch the lines of a wrapping flex container that says nothing of `align-content`, as CSS
|
|
912
|
+
* starts them (`normal`, which is `stretch` in a flex container). Yoga starts them at
|
|
913
|
+
* `flex-start`, as tall as what is in them, where an item that stretches across its line has
|
|
914
|
+
* no size. A container that does not wrap has one line, which Yoga stretches already.
|
|
915
|
+
*/
|
|
916
|
+
function wrappedLinesStretch(props) {
|
|
917
|
+
const wraps = props['flexWrap'] === 'wrap' || props['flexWrap'] === 'wrap-reverse';
|
|
918
|
+
if (wraps && props['alignContent'] == null)
|
|
919
|
+
props['alignContent'] = 'stretch';
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* A flex item that stretches across its line, with its size across the line `auto`, as CSS has
|
|
923
|
+
* it: as wide as a column, or as tall as a row, where Yoga takes the ratio over the stretch. In a
|
|
924
|
+
* column, a height given leaves the ratio nothing to work out. In a row, the line is as tall as
|
|
925
|
+
* the ratio makes the item from its width, at least, which is then the item's least height.
|
|
926
|
+
*
|
|
927
|
+
* ponytail: a width in points that does not grow, in a row, with any least or most height in
|
|
928
|
+
* points. One that grows, or is a share, keeps the ratio, as Yoga has it. A plain view only: an image keeps its picture's proportions.
|
|
929
|
+
*/
|
|
930
|
+
function stretchedOverRatio(node, props) {
|
|
931
|
+
if (!isSet(props['aspectRatio']) || props['position'] === 'absolute')
|
|
932
|
+
return;
|
|
933
|
+
if (viewNameOf(node) !== DEFAULT_VIEW)
|
|
934
|
+
return;
|
|
935
|
+
const parent = layoutParent(node);
|
|
936
|
+
if (!parent)
|
|
937
|
+
return;
|
|
938
|
+
const container = containerOf(parent);
|
|
939
|
+
// Remembered on the container, so the item is merged again when its direction changes.
|
|
940
|
+
parent.fitContainer = container;
|
|
941
|
+
const [direction, align] = container.split(' ');
|
|
942
|
+
if (align !== 'stretch' || !stretchesAcross(props, direction === 'row'))
|
|
943
|
+
return;
|
|
944
|
+
if (direction === 'row')
|
|
945
|
+
ratioAsLeast(props);
|
|
946
|
+
else if (isSet(props['height']) && props['height'] !== 'auto')
|
|
947
|
+
delete props['aspectRatio'];
|
|
948
|
+
}
|
|
949
|
+
/** Whether an item stretches across its line: its own size and margins across it are `auto`. */
|
|
950
|
+
function stretchesAcross(props, row) {
|
|
951
|
+
if (!STRETCHES.has(props['alignSelf']))
|
|
952
|
+
return false;
|
|
953
|
+
const size = row ? 'height' : 'width';
|
|
954
|
+
if (isSet(props[size]) && props[size] !== 'auto')
|
|
955
|
+
return false;
|
|
956
|
+
const sides = row
|
|
957
|
+
? MARGINS_ACROSS_ROW
|
|
958
|
+
: props['direction'] === 'rtl'
|
|
959
|
+
? MARGINS_ACROSS_COLUMN_RTL
|
|
960
|
+
: MARGINS_ACROSS_COLUMN;
|
|
961
|
+
return sides.every((keys) => keys.map((key) => props[key]).find(isSet) !== 'auto');
|
|
962
|
+
}
|
|
963
|
+
/** The margins across a line, a side each, as Yoga reads them: the first set wins. */
|
|
964
|
+
const MARGINS_ACROSS_ROW = [
|
|
965
|
+
['marginTop', 'marginVertical', 'margin'],
|
|
966
|
+
['marginBottom', 'marginVertical', 'margin'],
|
|
967
|
+
];
|
|
968
|
+
const MARGINS_ACROSS_COLUMN = [
|
|
969
|
+
['marginStart', 'marginLeft', 'marginHorizontal', 'margin'],
|
|
970
|
+
['marginEnd', 'marginRight', 'marginHorizontal', 'margin'],
|
|
971
|
+
];
|
|
972
|
+
/** Right to left, where the start is the right side. */
|
|
973
|
+
const MARGINS_ACROSS_COLUMN_RTL = [
|
|
974
|
+
['marginEnd', 'marginLeft', 'marginHorizontal', 'margin'],
|
|
975
|
+
['marginStart', 'marginRight', 'marginHorizontal', 'margin'],
|
|
976
|
+
];
|
|
977
|
+
/** In a row, the height a ratio gives a width in points, as the item's least height. */
|
|
978
|
+
function ratioAsLeast(props) {
|
|
979
|
+
const [width, ratio] = [props['width'], props['aspectRatio']];
|
|
980
|
+
if (typeof width !== 'number' || typeof ratio !== 'number' || ratio <= 0)
|
|
981
|
+
return;
|
|
982
|
+
const [least, most] = [props['minHeight'], props['maxHeight']];
|
|
983
|
+
const inPoints = (bound) => !isSet(bound) || typeof bound === 'number';
|
|
984
|
+
if (props['boxSizing'] === 'content-box' || !inPoints(least) || !inPoints(most))
|
|
985
|
+
return;
|
|
986
|
+
if (sizedAlong(props))
|
|
987
|
+
return;
|
|
988
|
+
props['minHeight'] = heldTo(width / ratio, least, most);
|
|
989
|
+
delete props['aspectRatio'];
|
|
990
|
+
}
|
|
991
|
+
/** Whether an item's width is not what it is along its line: it has a basis, or it grows. */
|
|
992
|
+
function sizedAlong(props) {
|
|
993
|
+
const basis = props['flexBasis'];
|
|
994
|
+
return (isSet(basis) && basis !== 'auto') || Number(props['flexGrow'] ?? props['flex'] ?? 0) > 0;
|
|
995
|
+
}
|
|
996
|
+
/** A height held to the least and most given, which CSS keeps: the least wins over the most. */
|
|
997
|
+
function heldTo(height, least, most) {
|
|
998
|
+
const under = typeof most === 'number' ? Math.min(height, most) : height;
|
|
999
|
+
return typeof least === 'number' ? Math.max(under, least) : under;
|
|
1000
|
+
}
|
|
857
1001
|
/**
|
|
858
1002
|
* Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
|
|
859
1003
|
* for a size that is `auto`; Yoga takes it over one of two that are given, the height in a
|
|
@@ -879,6 +1023,20 @@ function colourless(props, side, flipped) {
|
|
|
879
1023
|
* Each side a border has: its width, its colour, and the logical colour React Native takes for
|
|
880
1024
|
* it too, read left to right and, `flipped`, right to left.
|
|
881
1025
|
*/
|
|
1026
|
+
const BORDER_DRAWN = /^border\w*(Width|Style)$/;
|
|
1027
|
+
const BORDER_COLOUR = /^border\w*Color$/;
|
|
1028
|
+
/**
|
|
1029
|
+
* Send a border's colours again with an update that gives it a width or a style: Android draws
|
|
1030
|
+
* the border black when that update does not carry its colour, though it was given one before.
|
|
1031
|
+
* A box made without a border and given one later, a file attachment that is done, has one.
|
|
1032
|
+
*/
|
|
1033
|
+
function borderColoursAgain(payload, props) {
|
|
1034
|
+
if (!Object.keys(payload).some((key) => BORDER_DRAWN.test(key)))
|
|
1035
|
+
return;
|
|
1036
|
+
for (const key in props)
|
|
1037
|
+
if (BORDER_COLOUR.test(key) && !(key in payload))
|
|
1038
|
+
payload[key] = props[key];
|
|
1039
|
+
}
|
|
882
1040
|
const BORDER_SIDES = [
|
|
883
1041
|
['Top', 'BlockStart', 'BlockStart'],
|
|
884
1042
|
['Right', 'End', 'Start'],
|
|
@@ -970,14 +1128,63 @@ function lineTall(props, run, fontScale) {
|
|
|
970
1128
|
}
|
|
971
1129
|
/** The points of padding and border above and below a view's content, where each is a length. */
|
|
972
1130
|
function aroundALine(props) {
|
|
973
|
-
|
|
1131
|
+
return around(props, 'Height');
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* The props each side's padding and border are read from, across a height and across a width,
|
|
1135
|
+
* the first set winning, as React Native and then Yoga read them: an alias of React Native's over
|
|
1136
|
+
* Yoga's own, a logical side over the physical one, and the start on the left unless the box is
|
|
1137
|
+
* right to left. A block's start and end are under the top and bottom.
|
|
1138
|
+
*/
|
|
1139
|
+
function sides(props, size) {
|
|
1140
|
+
if (size === 'Height') {
|
|
1141
|
+
const block = ['paddingBlock', 'paddingVertical', 'padding'];
|
|
1142
|
+
return [
|
|
1143
|
+
['paddingTop', 'paddingBlockStart', ...block],
|
|
1144
|
+
['borderTopWidth', 'borderWidth'],
|
|
1145
|
+
['paddingBottom', 'paddingBlockEnd', ...block],
|
|
1146
|
+
['borderBottomWidth', 'borderWidth'],
|
|
1147
|
+
];
|
|
1148
|
+
}
|
|
1149
|
+
const rtl = props['direction'] === 'rtl';
|
|
1150
|
+
const inline = ['paddingInline', 'paddingHorizontal', 'padding'];
|
|
1151
|
+
return ['Left', 'Right'].flatMap((physical, at) => {
|
|
1152
|
+
const logical = (at === 0) !== rtl ? 'Start' : 'End';
|
|
1153
|
+
return [
|
|
1154
|
+
[`paddingInline${logical}`, `padding${logical}`, `padding${physical}`, ...inline],
|
|
1155
|
+
[`border${logical}Width`, `border${physical}Width`, 'borderWidth'],
|
|
1156
|
+
];
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
/** The points of padding and border across a view's height or width, where each is a length. */
|
|
1160
|
+
function around(props, size) {
|
|
1161
|
+
let total = 0;
|
|
1162
|
+
for (const keys of sides(props, size)) {
|
|
974
1163
|
const found = keys.map((key) => props[key]).find((value) => value != null);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1164
|
+
if (typeof found === 'number')
|
|
1165
|
+
total += found;
|
|
1166
|
+
}
|
|
1167
|
+
return total;
|
|
1168
|
+
}
|
|
1169
|
+
/** The most and the least of each size, by name: asked of every node merged. */
|
|
1170
|
+
const MOST = { Height: 'maxHeight', Width: 'maxWidth' };
|
|
1171
|
+
const LEAST = { Height: 'minHeight', Width: 'minWidth' };
|
|
1172
|
+
/**
|
|
1173
|
+
* A most size under the least: under a `min-height`, or a border box's padding and border, which
|
|
1174
|
+
* CSS lets win (CSS 2.2, 10.7). React Native's Yoga holds the box to its most, and the box around
|
|
1175
|
+
* it to that, so the most is raised to the least. A most and a least in points: a share on either
|
|
1176
|
+
* side is left to Yoga.
|
|
1177
|
+
*/
|
|
1178
|
+
function leastWins(props, size) {
|
|
1179
|
+
const most = props[MOST[size]];
|
|
1180
|
+
if (typeof most !== 'number')
|
|
1181
|
+
return;
|
|
1182
|
+
const written = props[LEAST[size]];
|
|
1183
|
+
let least = typeof written === 'number' ? written : 0;
|
|
1184
|
+
if (props['boxSizing'] !== 'content-box')
|
|
1185
|
+
least = Math.max(least, around(props, size));
|
|
1186
|
+
if (most < least)
|
|
1187
|
+
props[MOST[size]] = least;
|
|
981
1188
|
}
|
|
982
1189
|
/**
|
|
983
1190
|
* Where a line may break: at a space, after a hyphen, and anywhere in a script written without
|
|
@@ -1096,8 +1303,18 @@ export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
|
1096
1303
|
LOOSE_TEXT[name] = options.looseText;
|
|
1097
1304
|
else
|
|
1098
1305
|
delete LOOSE_TEXT[name];
|
|
1306
|
+
if (options?.fontNames)
|
|
1307
|
+
FONT_NAMES[name] = options.fontNames;
|
|
1308
|
+
else
|
|
1309
|
+
delete FONT_NAMES[name];
|
|
1099
1310
|
}
|
|
1100
1311
|
}
|
|
1312
|
+
/** How each view that names fonts in its own props rewrites them. See `ViewNameOptions`. */
|
|
1313
|
+
const FONT_NAMES = {};
|
|
1314
|
+
/** Rewrites the fonts a view's own props name, for a view that says how. See `ViewNameOptions`. */
|
|
1315
|
+
function renameFonts(viewName, props, postScriptName) {
|
|
1316
|
+
FONT_NAMES[viewName]?.(props, postScriptName);
|
|
1317
|
+
}
|
|
1101
1318
|
/** The element loose text in a view is drawn by, for the views that say. See `ViewNameOptions`. */
|
|
1102
1319
|
const LOOSE_TEXT = {};
|
|
1103
1320
|
/** The registered names that give way to a component's own host. See `ViewNameOptions`. */
|
|
@@ -1245,6 +1462,10 @@ const SCROLL_VIEWS = new Set(['ScrollView', 'AndroidHorizontalScrollView']);
|
|
|
1245
1462
|
const HOISTS = {};
|
|
1246
1463
|
/** The `ancestorName`s, so a commit asks only those nodes what to hoist into them. */
|
|
1247
1464
|
const HOIST_TARGETS = new Set();
|
|
1465
|
+
/** The `ancestorName`s that commit only what is hoisted into them. See `HoistOptions.only`. */
|
|
1466
|
+
const HOIST_ONLY = new Set();
|
|
1467
|
+
/** Element name -> the ancestor names it never hoists past. See `HoistOptions.stopAt`. */
|
|
1468
|
+
const HOIST_STOPS = {};
|
|
1248
1469
|
/**
|
|
1249
1470
|
* Commit every `elementName` as a direct child of its nearest `ancestorName`, wherever the
|
|
1250
1471
|
* template put it, e.g. `registerHoist('native-header', 'screen')`.
|
|
@@ -1263,6 +1484,12 @@ export function registerHoist(elementName, ancestorName, options = {}) {
|
|
|
1263
1484
|
HOIST_TARGETS.add(ancestorName);
|
|
1264
1485
|
if (options.standIn)
|
|
1265
1486
|
STAND_INS[elementName] = options.standIn;
|
|
1487
|
+
if (options.only)
|
|
1488
|
+
HOIST_ONLY.add(ancestorName);
|
|
1489
|
+
if (options.stopAt)
|
|
1490
|
+
HOIST_STOPS[elementName] = new Set(options.stopAt);
|
|
1491
|
+
else
|
|
1492
|
+
delete HOIST_STOPS[elementName];
|
|
1266
1493
|
}
|
|
1267
1494
|
/** Element name -> the props it leaves behind in its ancestor. See `HoistOptions.standIn`. */
|
|
1268
1495
|
const STAND_INS = {};
|
|
@@ -1295,10 +1522,12 @@ function paragraphText(node) {
|
|
|
1295
1522
|
// Text written straight into a view is a paragraph of its own, and all of it.
|
|
1296
1523
|
if (root === null || !isTextElement(root))
|
|
1297
1524
|
return node.box ? trimEnds(node.text) : node.text;
|
|
1525
|
+
// A text element a view stands in for, which places what it holds: no paragraph runs past it.
|
|
1526
|
+
const inParagraph = (above) => isTextElement(above) && !aligningText(above);
|
|
1298
1527
|
// The commonest paragraph by far: one run and nothing else, so it is both the first and last.
|
|
1299
|
-
if (root.children.length === 1 && !
|
|
1528
|
+
if (root.children.length === 1 && !inParagraph(root.parent))
|
|
1300
1529
|
return trimEnds(node.text);
|
|
1301
|
-
while (
|
|
1530
|
+
while (inParagraph(root.parent))
|
|
1302
1531
|
root = root.parent;
|
|
1303
1532
|
const runs = [];
|
|
1304
1533
|
const collect = (from) => {
|
|
@@ -1326,12 +1555,12 @@ function paragraphText(node) {
|
|
|
1326
1555
|
function isTextElement(node) {
|
|
1327
1556
|
if (node === null || node.kind !== 'element')
|
|
1328
1557
|
return false;
|
|
1329
|
-
if (node.
|
|
1558
|
+
if (node.tag === 'text')
|
|
1330
1559
|
return true;
|
|
1331
1560
|
// One lookup settles nearly every other element, which is a view.
|
|
1332
|
-
if (registeredViewName(node.
|
|
1561
|
+
if (registeredViewName(node.tag) !== PARAGRAPH)
|
|
1333
1562
|
return false;
|
|
1334
|
-
return !TEXT_ELEMENTS.has(node.
|
|
1563
|
+
return !TEXT_ELEMENTS.has(node.tag) || holdsOnlyText(node);
|
|
1335
1564
|
}
|
|
1336
1565
|
/** Whether a text element holds text and nothing else. One that holds nothing holds no text. */
|
|
1337
1566
|
function holdsOnlyText(node) {
|
|
@@ -1351,25 +1580,48 @@ const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
|
1351
1580
|
* alignment moves, so such an element commits as a view, and its text as the paragraph any view's
|
|
1352
1581
|
* loose text is. Read from the node's own style on each reconcile, which a change to it brings
|
|
1353
1582
|
* about, so nothing is kept to go stale. The run can be a text element of its own, a label in a
|
|
1354
|
-
* span, which is then the item placed.
|
|
1355
|
-
*
|
|
1583
|
+
* span, which is then the item placed. So is a column of text elements, each an item under the
|
|
1584
|
+
* last. A `<text>` is a paragraph whatever its style, and any other element holding more than
|
|
1585
|
+
* the one run is left as it was.
|
|
1356
1586
|
*/
|
|
1357
1587
|
function aligningText(node) {
|
|
1358
|
-
if (node.kind !== 'element' || node.
|
|
1588
|
+
if (node.kind !== 'element' || node.tag === 'text' || !TEXT_ELEMENTS.has(node.tag)) {
|
|
1359
1589
|
return false;
|
|
1360
1590
|
}
|
|
1361
|
-
|
|
1591
|
+
const stacked = stacksRuns(node);
|
|
1592
|
+
if (!stacked && (node.children.length !== 1 || !oneRun(node.children[0])))
|
|
1362
1593
|
return false;
|
|
1363
1594
|
// Under a text element it is a span of that paragraph, unless that one is a view placing it.
|
|
1364
1595
|
if (isTextElement(node.parent) && !aligningText(node.parent))
|
|
1365
1596
|
return false;
|
|
1597
|
+
return placesByFlex(node, stacked);
|
|
1598
|
+
}
|
|
1599
|
+
/** Whether a flex container's own style places the run it holds, or stacks the runs it holds. */
|
|
1600
|
+
function placesByFlex(node, stacked) {
|
|
1366
1601
|
if (ownLayout(node, 'display') !== 'flex')
|
|
1367
1602
|
return false;
|
|
1603
|
+
// A column of them: each a line of its own, where one paragraph would run them on.
|
|
1604
|
+
if (stacked)
|
|
1605
|
+
return String(ownLayout(node, 'flexDirection') ?? 'column').startsWith('column');
|
|
1368
1606
|
return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
|
|
1369
1607
|
ALIGNS.has(ownLayout(node, 'justifyContent')));
|
|
1370
1608
|
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Whether a text element holds two or more text elements and nothing else but whitespace and
|
|
1611
|
+
* the anchors control flow leaves: a label around a name and a hint, which a flex column has one under the other.
|
|
1612
|
+
*/
|
|
1613
|
+
function stacksRuns(node) {
|
|
1614
|
+
let runs = 0;
|
|
1615
|
+
for (const child of node.children) {
|
|
1616
|
+
if (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag))
|
|
1617
|
+
runs++;
|
|
1618
|
+
else if (child.kind === 'text' ? trimEnds(child.text) : child.kind !== 'anchor')
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
return runs > 1;
|
|
1622
|
+
}
|
|
1371
1623
|
/** One run of text to place: text itself, or a text element around some, a label in a span. */
|
|
1372
|
-
const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.
|
|
1624
|
+
const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag));
|
|
1373
1625
|
/** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
|
|
1374
1626
|
const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
|
|
1375
1627
|
/**
|
|
@@ -1388,14 +1640,14 @@ export function viewNameOf(node) {
|
|
|
1388
1640
|
// Committing a nested `<text>` as another Paragraph gives a paragraph inside a paragraph,
|
|
1389
1641
|
// which lays out as a separate block instead of flowing inline.
|
|
1390
1642
|
// A component of the app's own under a name a view set registered: its host is a plain view.
|
|
1391
|
-
if (node.componentHost && !node.nativeView && YIELDING.has(node.
|
|
1643
|
+
if (node.componentHost && !node.nativeView && YIELDING.has(node.tag))
|
|
1392
1644
|
return DEFAULT_VIEW;
|
|
1393
|
-
const registered = registeredViewName(node.
|
|
1645
|
+
const registered = registeredViewName(node.tag);
|
|
1394
1646
|
// Nearly every element: a view of some kind, by its name alone.
|
|
1395
1647
|
if (registered !== PARAGRAPH)
|
|
1396
1648
|
return registered ?? DEFAULT_VIEW;
|
|
1397
1649
|
// One of HTML's text elements that holds something other than text: a view.
|
|
1398
|
-
if (node.
|
|
1650
|
+
if (node.tag !== 'text' && TEXT_ELEMENTS.has(node.tag) && !holdsOnlyText(node)) {
|
|
1399
1651
|
return DEFAULT_VIEW;
|
|
1400
1652
|
}
|
|
1401
1653
|
// The paragraph the engine makes for a view's loose text is under a view, whatever the name
|
|
@@ -1544,6 +1796,8 @@ const EASED_COLOURS = new Set([
|
|
|
1544
1796
|
'borderLeftColor',
|
|
1545
1797
|
]);
|
|
1546
1798
|
const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
|
|
1799
|
+
/** What a node that hands its background colour over has handed so far: nothing. */
|
|
1800
|
+
const UNREPORTED = Symbol('unreported');
|
|
1547
1801
|
/** The style keys that are a view's background. */
|
|
1548
1802
|
const PAINT_KEYS = [
|
|
1549
1803
|
'backgroundColor',
|
|
@@ -1552,6 +1806,15 @@ const PAINT_KEYS = [
|
|
|
1552
1806
|
'experimental_backgroundPosition',
|
|
1553
1807
|
'experimental_backgroundRepeat',
|
|
1554
1808
|
];
|
|
1809
|
+
/** Note a prop written on a node that keeps `writes`: see there. */
|
|
1810
|
+
function noteWrite(node, key, value) {
|
|
1811
|
+
if (!node.writes)
|
|
1812
|
+
return;
|
|
1813
|
+
if (value === undefined || value === null)
|
|
1814
|
+
node.writes.delete(key);
|
|
1815
|
+
else
|
|
1816
|
+
node.writes.add(key);
|
|
1817
|
+
}
|
|
1555
1818
|
/**
|
|
1556
1819
|
* A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
|
|
1557
1820
|
* attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
|
|
@@ -1718,6 +1981,17 @@ function reportHover(host) {
|
|
|
1718
1981
|
hoverReported = true;
|
|
1719
1982
|
console.warn('[angular-native] @defer (on hover) fires when a pointer enters the trigger, which takes a trackpad or a mouse (an iPad, or an Android device with one), or when the trigger takes focus. On a phone it waits for focus alone; add another trigger, such as on interaction or on viewport.');
|
|
1720
1983
|
}
|
|
1984
|
+
/** A bound value the cascade works out from tokens: a `var()`, a `color-mix()`, a relative colour. */
|
|
1985
|
+
const BOUND_TOKEN = /^[ \t\n\r\f]*(?:var|color-mix)\(|^[ \t\n\r\f]*[a-z]+\([ \t\n\r\f]*from[ \t\n\r\f]/i;
|
|
1986
|
+
/**
|
|
1987
|
+
* A bound value kept as a declaration the cascade settles: a `var()`, a colour made of tokens, or
|
|
1988
|
+
* a relative length.
|
|
1989
|
+
*/
|
|
1990
|
+
function boundFrom(prop, text, on) {
|
|
1991
|
+
return BOUND_TOKEN.test(text)
|
|
1992
|
+
? boundDeclaration(prop, tokenFromValue(text), on)
|
|
1993
|
+
: boundRelativeLength(prop, text, on);
|
|
1994
|
+
}
|
|
1721
1995
|
/** Whether a value is one of the engine's own nodes. */
|
|
1722
1996
|
export function isEngineNode(value) {
|
|
1723
1997
|
return value instanceof RetainedNode;
|
|
@@ -1777,17 +2051,18 @@ class RetainedNode {
|
|
|
1777
2051
|
transitions;
|
|
1778
2052
|
playing;
|
|
1779
2053
|
scrolled;
|
|
2054
|
+
owned;
|
|
1780
2055
|
kind;
|
|
1781
|
-
|
|
2056
|
+
tag;
|
|
1782
2057
|
/** See `HostNode.host`. */
|
|
1783
2058
|
host;
|
|
1784
|
-
constructor(kind,
|
|
2059
|
+
constructor(kind, tag, host) {
|
|
1785
2060
|
this.kind = kind;
|
|
1786
|
-
this.
|
|
2061
|
+
this.tag = tag;
|
|
1787
2062
|
this.host = host;
|
|
1788
2063
|
}
|
|
1789
2064
|
get tagName() {
|
|
1790
|
-
return this.
|
|
2065
|
+
return this.tag;
|
|
1791
2066
|
}
|
|
1792
2067
|
/** Angular's dev-mode assertion compares this against `Node.ELEMENT_NODE`. */
|
|
1793
2068
|
get nodeType() {
|
|
@@ -2102,6 +2377,26 @@ function applyIntrinsicSize(props, size) {
|
|
|
2102
2377
|
}
|
|
2103
2378
|
}
|
|
2104
2379
|
const now = () => globalThis.performance?.now?.() ?? Date.now();
|
|
2380
|
+
/** How many boxes `node` is under. */
|
|
2381
|
+
/**
|
|
2382
|
+
* How deep a node is, with the depth of every box on its way up kept in `known`: the boxes that
|
|
2383
|
+
* changed beneath are a node and its ancestors, so each is counted once, not once a box.
|
|
2384
|
+
*/
|
|
2385
|
+
function depthOf(node, known) {
|
|
2386
|
+
const path = [];
|
|
2387
|
+
let depth = -1;
|
|
2388
|
+
for (let up = node; up; up = up.parent) {
|
|
2389
|
+
const found = known.get(up);
|
|
2390
|
+
if (found !== undefined) {
|
|
2391
|
+
depth = found;
|
|
2392
|
+
break;
|
|
2393
|
+
}
|
|
2394
|
+
path.push(up);
|
|
2395
|
+
}
|
|
2396
|
+
for (let i = path.length - 1; i >= 0; i--)
|
|
2397
|
+
known.set(path[i], ++depth);
|
|
2398
|
+
return depth;
|
|
2399
|
+
}
|
|
2105
2400
|
function sameHandles(a, b) {
|
|
2106
2401
|
return a.length === b.length && a.every((handle, i) => handle === b[i]);
|
|
2107
2402
|
}
|
|
@@ -2124,6 +2419,11 @@ function couldBe(compound, node) {
|
|
|
2124
2419
|
return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
|
|
2125
2420
|
}
|
|
2126
2421
|
const FRAME_BUDGET_MS = 8;
|
|
2422
|
+
/** A solid brush with no colour in it, which stops react-native-svg. */
|
|
2423
|
+
const emptyBrush = (value) => typeof value === 'object' &&
|
|
2424
|
+
value !== null &&
|
|
2425
|
+
value.type === 0 &&
|
|
2426
|
+
value.payload == null;
|
|
2127
2427
|
export class Engine {
|
|
2128
2428
|
/** Scopes the CSS resolver's memo. Bumped once per commit. */
|
|
2129
2429
|
styleEpoch = 0;
|
|
@@ -2214,7 +2514,7 @@ export class Engine {
|
|
|
2214
2514
|
this.processColor = options.processColor ?? ((value) => value);
|
|
2215
2515
|
this.resolveAssetSource = options.resolveAssetSource ?? ((value) => value);
|
|
2216
2516
|
this.scrollDriver = options.nativeAnimated
|
|
2217
|
-
? new NativeScrollDriver(options.nativeAnimated)
|
|
2517
|
+
? new NativeScrollDriver(options.nativeAnimated, (view, tag, keys) => this.touchedNatively(view, tag, keys))
|
|
2218
2518
|
: null;
|
|
2219
2519
|
// Eagerly, not on the first listener. Fabric holds exactly one event handler and
|
|
2220
2520
|
// `ReactFabric` installs React's at module scope, so until we claim it every native event
|
|
@@ -2262,15 +2562,29 @@ export class Engine {
|
|
|
2262
2562
|
*/
|
|
2263
2563
|
setActiveChain(from, active) {
|
|
2264
2564
|
let changed = false;
|
|
2265
|
-
|
|
2565
|
+
const set = (node) => {
|
|
2266
2566
|
if (node.active === active)
|
|
2267
|
-
|
|
2567
|
+
return;
|
|
2268
2568
|
node.active = active;
|
|
2269
2569
|
this.markActive(node);
|
|
2270
2570
|
changed = true;
|
|
2571
|
+
};
|
|
2572
|
+
for (let node = from; node; node = node.parent) {
|
|
2573
|
+
set(node);
|
|
2574
|
+
if (active)
|
|
2575
|
+
this.activeNodes.add(node);
|
|
2576
|
+
}
|
|
2577
|
+
if (!active) {
|
|
2578
|
+
// Every node the press set, too: one the pressed node was moved or removed from mid-press
|
|
2579
|
+
// is no longer above it.
|
|
2580
|
+
for (const node of this.activeNodes)
|
|
2581
|
+
set(node);
|
|
2582
|
+
this.activeNodes.clear();
|
|
2271
2583
|
}
|
|
2272
2584
|
return changed;
|
|
2273
2585
|
}
|
|
2586
|
+
/** The nodes a press has set `:active` on, to clear whatever happens to the tree meanwhile. */
|
|
2587
|
+
activeNodes = new Set();
|
|
2274
2588
|
/** Whether any sheet styles a pressed element, and each compound that asks about one from another. */
|
|
2275
2589
|
activeUse = {
|
|
2276
2590
|
own: false,
|
|
@@ -2563,7 +2877,7 @@ export class Engine {
|
|
|
2563
2877
|
if (sheet === undefined || node.sheet === sheet)
|
|
2564
2878
|
return;
|
|
2565
2879
|
node.sheet = sheet;
|
|
2566
|
-
this.styles.noteSheet(sheet);
|
|
2880
|
+
this.styles.noteSheet(sheet, true);
|
|
2567
2881
|
this.watchStructure(sheet);
|
|
2568
2882
|
this.watchHas(sheet);
|
|
2569
2883
|
this.watchActive(sheet);
|
|
@@ -2599,7 +2913,7 @@ export class Engine {
|
|
|
2599
2913
|
createElement(name, sheet = null) {
|
|
2600
2914
|
const node = new RetainedNode('element', name, this);
|
|
2601
2915
|
node.sheet = sheet;
|
|
2602
|
-
this.styles.noteSheet(sheet);
|
|
2916
|
+
this.styles.noteSheet(sheet, true);
|
|
2603
2917
|
const html = HTML_ELEMENTS.get(name);
|
|
2604
2918
|
if (html !== undefined) {
|
|
2605
2919
|
if (html.role)
|
|
@@ -2629,8 +2943,10 @@ export class Engine {
|
|
|
2629
2943
|
* element matched again, and one named as what a rule's element is inside, `.animating .close`
|
|
2630
2944
|
* or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
|
|
2631
2945
|
* under any of those is styled again from the rules it matched, for what it inherits: a `dark`
|
|
2632
|
-
* class on the root finds no rule again for an element no `dark` rule is for.
|
|
2633
|
-
* has
|
|
2946
|
+
* class on the root finds no rule again for an element no `dark` rule is for. One named in
|
|
2947
|
+
* `:has()`, or answering a test of the class attribute there, has the boxes above the node
|
|
2948
|
+
* asked again, and one named only in `:has(> )` the box right above it. Any other class has the node and everything under it matched again, as any
|
|
2949
|
+
* other change to it does.
|
|
2634
2950
|
*
|
|
2635
2951
|
* A sheet that names a class later restyles what it is for as it arrives, and a node not yet
|
|
2636
2952
|
* committed is styled when it is, with the class as it is then.
|
|
@@ -2649,8 +2965,43 @@ export class Engine {
|
|
|
2649
2965
|
return;
|
|
2650
2966
|
if (reach.own)
|
|
2651
2967
|
node.ownDirty = true;
|
|
2652
|
-
if (this.
|
|
2968
|
+
if (this.hasSheets && reach.beneath)
|
|
2969
|
+
this.markBeneath(node.parent);
|
|
2970
|
+
// `:has(> .on)`: only the box right above the node can answer it.
|
|
2971
|
+
else if (this.hasSheets && reach.parentHas)
|
|
2972
|
+
this.markBeneath(node.parent, node.parent?.parent);
|
|
2973
|
+
if (this.styles.reachesUnder(reach)) {
|
|
2974
|
+
this.classesAbove += 1;
|
|
2653
2975
|
this.markUnder(node, reach);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* How many class changes have restyled what is under an element. `markUnder` walks the tree as
|
|
2980
|
+
* it is, so an element out of it then is not reached: one that comes back after any is matched
|
|
2981
|
+
* again, with all that is under it.
|
|
2982
|
+
*/
|
|
2983
|
+
classesAbove = 0;
|
|
2984
|
+
/**
|
|
2985
|
+
* The count when each element went out of the tree, for the ones still out. `detach` sets it
|
|
2986
|
+
* for a node that only moves too, so `markReturned` runs after it and clears that.
|
|
2987
|
+
*/
|
|
2988
|
+
outSince = new WeakMap();
|
|
2989
|
+
/** Have matched again a node coming back into the tree that missed a class change above it. */
|
|
2990
|
+
markReturned(child) {
|
|
2991
|
+
const since = this.outSince.get(child);
|
|
2992
|
+
if (since === undefined)
|
|
2993
|
+
return;
|
|
2994
|
+
this.outSince.delete(child);
|
|
2995
|
+
if (since !== this.classesAbove)
|
|
2996
|
+
this.markAllUnder(child);
|
|
2997
|
+
}
|
|
2998
|
+
markAllUnder(node) {
|
|
2999
|
+
if (node.kind !== 'element')
|
|
3000
|
+
return;
|
|
3001
|
+
node.ownDirty = true;
|
|
3002
|
+
this.markPath(node);
|
|
3003
|
+
for (const child of node.children)
|
|
3004
|
+
this.markAllUnder(child);
|
|
2654
3005
|
}
|
|
2655
3006
|
/**
|
|
2656
3007
|
* Have matched again each element under a node that one of `subjects`' rules could be for. One
|
|
@@ -2676,8 +3027,9 @@ export class Engine {
|
|
|
2676
3027
|
if (node.hostSheet === sheet)
|
|
2677
3028
|
return;
|
|
2678
3029
|
node.hostSheet = sheet;
|
|
2679
|
-
this.styles.noteSheet(sheet);
|
|
3030
|
+
this.styles.noteSheet(sheet, true);
|
|
2680
3031
|
this.watchStructure(sheet);
|
|
3032
|
+
this.watchHas(sheet);
|
|
2681
3033
|
this.watchActive(sheet);
|
|
2682
3034
|
this.markProps(node);
|
|
2683
3035
|
}
|
|
@@ -2687,6 +3039,7 @@ export class Engine {
|
|
|
2687
3039
|
* the web's mount point has, unless the app's `:host` sizes it otherwise.
|
|
2688
3040
|
*/
|
|
2689
3041
|
setDefaultStyle(node, style) {
|
|
3042
|
+
this.noteEasing(style);
|
|
2690
3043
|
node.defaultStyle = style;
|
|
2691
3044
|
// Under what the cascade comes to, and read by no selector: nothing is matched again.
|
|
2692
3045
|
this.markProps(node, false);
|
|
@@ -2696,7 +3049,8 @@ export class Engine {
|
|
|
2696
3049
|
* another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
|
|
2697
3050
|
*/
|
|
2698
3051
|
setClasses(node, value) {
|
|
2699
|
-
|
|
3052
|
+
// By HTML's spaces, which are CSS's: a vertical tab or a no-break space is part of a name.
|
|
3053
|
+
const names = value.split(CSS_SPACE).filter(Boolean);
|
|
2700
3054
|
const before = node.classes;
|
|
2701
3055
|
node.classes = names.length ? new Set(names) : null;
|
|
2702
3056
|
// Each class that came or went: one in both changes nothing.
|
|
@@ -2719,6 +3073,7 @@ export class Engine {
|
|
|
2719
3073
|
appendChild(parent, child) {
|
|
2720
3074
|
this.markAdopted(child, parent);
|
|
2721
3075
|
this.detach(child);
|
|
3076
|
+
this.markReturned(child);
|
|
2722
3077
|
child.parent = parent;
|
|
2723
3078
|
parent.children.push(child);
|
|
2724
3079
|
if (child.dormantHoists)
|
|
@@ -2729,6 +3084,7 @@ export class Engine {
|
|
|
2729
3084
|
insertBefore(parent, child, ref) {
|
|
2730
3085
|
this.markAdopted(child, parent);
|
|
2731
3086
|
this.detach(child);
|
|
3087
|
+
this.markReturned(child);
|
|
2732
3088
|
child.parent = parent;
|
|
2733
3089
|
const at = ref ? parent.children.indexOf(ref) : -1;
|
|
2734
3090
|
if (at < 0)
|
|
@@ -2759,6 +3115,7 @@ export class Engine {
|
|
|
2759
3115
|
return;
|
|
2760
3116
|
target.children.splice(at, 1);
|
|
2761
3117
|
child.parent = null;
|
|
3118
|
+
this.outSince.set(child, this.classesAbove);
|
|
2762
3119
|
this.removedSinceCommit.add(child);
|
|
2763
3120
|
this.markStructure(target, child, at);
|
|
2764
3121
|
this.markTextContent(target, child);
|
|
@@ -2846,7 +3203,26 @@ export class Engine {
|
|
|
2846
3203
|
}
|
|
2847
3204
|
}
|
|
2848
3205
|
if (this.focusedNode && this.topOf(this.focusedNode) !== this.root)
|
|
2849
|
-
this.
|
|
3206
|
+
this.dropFocus();
|
|
3207
|
+
this.dropDetachedPress();
|
|
3208
|
+
}
|
|
3209
|
+
/** A pressed node out of the tree gets no release: native has no view to end the touch on. */
|
|
3210
|
+
dropDetachedPress() {
|
|
3211
|
+
const pressed = this.currentResponder;
|
|
3212
|
+
if (!pressed || this.topOf(pressed) === this.root)
|
|
3213
|
+
return;
|
|
3214
|
+
this.setActiveChain(pressed, false);
|
|
3215
|
+
this.currentResponder = null;
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Forget the focused node, which has left the tree without a blur: off the node as well as the
|
|
3219
|
+
* engine, or a `:focus` rule styles it again when it is put back.
|
|
3220
|
+
*/
|
|
3221
|
+
dropFocus() {
|
|
3222
|
+
const node = this.focusedNode;
|
|
3223
|
+
node.focused = false;
|
|
3224
|
+
this.markProps(node);
|
|
3225
|
+
this.focusedNode = null;
|
|
2850
3226
|
}
|
|
2851
3227
|
/** Forget the committed views of each node removed since the last commit that is still out. */
|
|
2852
3228
|
forgetDetached() {
|
|
@@ -2893,7 +3269,7 @@ export class Engine {
|
|
|
2893
3269
|
node.kept = undefined;
|
|
2894
3270
|
// A field popped with the keyboard up never sends its blur.
|
|
2895
3271
|
if (this.focusedNode === node)
|
|
2896
|
-
this.
|
|
3272
|
+
this.dropFocus();
|
|
2897
3273
|
}
|
|
2898
3274
|
parentNode(node) {
|
|
2899
3275
|
return node.parent;
|
|
@@ -2904,7 +3280,46 @@ export class Engine {
|
|
|
2904
3280
|
return null;
|
|
2905
3281
|
return siblings[siblings.indexOf(node) + 1] ?? null;
|
|
2906
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Whether any declaration the engine was given transitions or animates: until one does, a
|
|
3285
|
+
* node's style is committed as it is, with nothing asked of what eases it. See `eased`.
|
|
3286
|
+
*/
|
|
3287
|
+
eases = false;
|
|
3288
|
+
noteEasing(declarations) {
|
|
3289
|
+
if (this.eases || !declarations)
|
|
3290
|
+
return;
|
|
3291
|
+
for (const key in declarations) {
|
|
3292
|
+
if (key.startsWith('$transition') || key.startsWith('$animation'))
|
|
3293
|
+
this.eases = true;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
/** A node's style as its transitions and animation have it: a declaration asks for one. */
|
|
3297
|
+
eased(node, style) {
|
|
3298
|
+
if (!this.eases)
|
|
3299
|
+
return style;
|
|
3300
|
+
return this.animated(node, this.transitioned(node, style));
|
|
3301
|
+
}
|
|
3302
|
+
/** Note a sheet any of whose rules transitions or animates: see `eases`. */
|
|
3303
|
+
noteSheetEasing(sheet) {
|
|
3304
|
+
for (const rule of this.eases ? [] : sheet.rules) {
|
|
3305
|
+
this.noteEasing(rule.declarations);
|
|
3306
|
+
this.noteEasing(rule.important);
|
|
3307
|
+
// One a token holds: `animation: var(--motion)`.
|
|
3308
|
+
for (const deferred of rule.deferred ?? []) {
|
|
3309
|
+
this.noteEasing(Object.fromEntries(deferred.props.map((key) => [key, true])));
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
/** Note a prop, or a style set as one, that transitions or animates: see `eases`. */
|
|
3314
|
+
noteEasingProp(key, value) {
|
|
3315
|
+
this.noteEasing({ [key]: value });
|
|
3316
|
+
if (key === 'style' || key === STYLE_OVERRIDE)
|
|
3317
|
+
this.noteEasing(flattenStyle(value, {}));
|
|
3318
|
+
}
|
|
2907
3319
|
setProp(node, key, value) {
|
|
3320
|
+
if (!this.eases)
|
|
3321
|
+
this.noteEasingProp(key, value);
|
|
3322
|
+
noteWrite(node, key, value);
|
|
2908
3323
|
// A prop written again is a binding's, and is committed as any other is.
|
|
2909
3324
|
if (node.attributeOnly?.delete(key))
|
|
2910
3325
|
this.markProps(node, false);
|
|
@@ -2930,7 +3345,9 @@ export class Engine {
|
|
|
2930
3345
|
// And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
|
|
2931
3346
|
// field's text on every key, match nothing they did not.
|
|
2932
3347
|
const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
|
|
2933
|
-
|
|
3348
|
+
// And restyles the elements after it only where a rule asks the prop of an element before
|
|
3349
|
+
// another, `[data-on] ~ .x`: a point of a chart set as a finger moves leaves those after it.
|
|
3350
|
+
this.markProps(node, read || heirs, this.styles.readsBeside(key));
|
|
2934
3351
|
if (SIZES_A_RUN.has(key))
|
|
2935
3352
|
this.markParagraph(node);
|
|
2936
3353
|
}
|
|
@@ -2956,6 +3373,40 @@ export class Engine {
|
|
|
2956
3373
|
this.markProps(node, false);
|
|
2957
3374
|
this.markProps(child, false);
|
|
2958
3375
|
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Hand a node's background colour to `listener` instead of painting it: what its stylesheets,
|
|
3378
|
+
* tokens and inline style resolve `background-color` to, or undefined for none. Called after
|
|
3379
|
+
* each commit that changes it, the first one included. What a component needs whose colour is
|
|
3380
|
+
* drawn by a native view the node is not, from a prop of that view's, as a bottom sheet's is.
|
|
3381
|
+
* The function returned stops it, and the node paints its own background again.
|
|
3382
|
+
*/
|
|
3383
|
+
takePaint(node, listener) {
|
|
3384
|
+
node.paintTaker = listener;
|
|
3385
|
+
node.paintTaken = UNREPORTED;
|
|
3386
|
+
this.markProps(node, false);
|
|
3387
|
+
return () => {
|
|
3388
|
+
if (node.paintTaker !== listener)
|
|
3389
|
+
return;
|
|
3390
|
+
node.paintTaker = undefined;
|
|
3391
|
+
this.paintReports.delete(node);
|
|
3392
|
+
this.markProps(node, false);
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
/** Nodes whose background colour is to be handed over once the commit running ends. */
|
|
3396
|
+
paintReports = new Set();
|
|
3397
|
+
reportPaint() {
|
|
3398
|
+
const nodes = [...this.paintReports];
|
|
3399
|
+
this.paintReports.clear();
|
|
3400
|
+
// One listener that throws stops neither the rest nor what the commit does after them.
|
|
3401
|
+
for (const node of nodes) {
|
|
3402
|
+
try {
|
|
3403
|
+
node.paintTaker?.(node.paintTaken);
|
|
3404
|
+
}
|
|
3405
|
+
catch (error) {
|
|
3406
|
+
this.reportEventError(error, 'paint');
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
2959
3410
|
/**
|
|
2960
3411
|
* Keep a prop the node already has as an attribute: on the node for a selector such as
|
|
2961
3412
|
* `:host([variant='primary'])` to match, and left out of what is committed to the native view.
|
|
@@ -2998,27 +3449,43 @@ export class Engine {
|
|
|
2998
3449
|
* has changed for the props diff to notice.
|
|
2999
3450
|
*/
|
|
3000
3451
|
styleChanged(node) {
|
|
3001
|
-
|
|
3452
|
+
if (!this.eases)
|
|
3453
|
+
this.noteEasingProp('style', node.props['style']);
|
|
3454
|
+
// As `setProp` restyles for a new style object: a child that takes one of its values, an
|
|
3455
|
+
// `inherit`, follows the one changed in place too.
|
|
3456
|
+
this.markProps(node, this.inlineReachesStyle(node) || node.styleCache?.heirs === true);
|
|
3002
3457
|
this.markParagraph(node);
|
|
3003
3458
|
}
|
|
3004
3459
|
/**
|
|
3005
|
-
* A bound declaration, `[style.color]`, whose value may be a `var()
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3460
|
+
* A bound declaration, `[style.color]`, whose value may be a `var()`, or a length in `em` or a
|
|
3461
|
+
* viewport unit. One that is, is kept as a declaration the cascade settles with the node's
|
|
3462
|
+
* tokens, font size and viewport, as a rule's is, and true is answered: the text is no value to
|
|
3463
|
+
* put in the node's style. A negative `line-height` is dropped, with a warning, and true is
|
|
3464
|
+
* answered too. Any other value forgets what was kept.
|
|
3008
3465
|
*/
|
|
3009
3466
|
setBoundStyle(node, prop, value) {
|
|
3010
|
-
const text = typeof value === 'string'
|
|
3011
|
-
const
|
|
3467
|
+
const text = typeof value === 'string' ? value : undefined;
|
|
3468
|
+
const refused = text !== undefined && this.refusedBound(node, prop, text);
|
|
3469
|
+
const bound = text === undefined || refused ? null : boundFrom(prop, text, node.tag);
|
|
3012
3470
|
if (!bound && !node.boundByProp?.has(prop))
|
|
3013
|
-
return
|
|
3471
|
+
return refused;
|
|
3014
3472
|
const byProp = (node.boundByProp ??= new Map());
|
|
3015
3473
|
if (bound)
|
|
3016
3474
|
byProp.set(prop, bound);
|
|
3017
3475
|
else
|
|
3018
3476
|
byProp.delete(prop);
|
|
3019
3477
|
node.boundStyle = byProp.size ? [...byProp.values()] : null;
|
|
3020
|
-
|
|
3021
|
-
|
|
3478
|
+
// Settled by the cascade as the node is styled, and asked about by no selector: nothing after
|
|
3479
|
+
// the node matches otherwise for it.
|
|
3480
|
+
this.markProps(node, true, false);
|
|
3481
|
+
return bound !== null || refused;
|
|
3482
|
+
}
|
|
3483
|
+
/** Whether a bound value is one CSS has none of, which is dropped with a warning. */
|
|
3484
|
+
refusedBound(node, prop, text) {
|
|
3485
|
+
if (!isNegativeLineHeight(prop, text))
|
|
3486
|
+
return false;
|
|
3487
|
+
this.reportDroppedDeclaration('line-height', text, node.tag, 'it is negative');
|
|
3488
|
+
return true;
|
|
3022
3489
|
}
|
|
3023
3490
|
/**
|
|
3024
3491
|
* Set a custom property on a node, `--tint` from `[style.--tint]`, or remove it with `null`.
|
|
@@ -3074,7 +3541,7 @@ export class Engine {
|
|
|
3074
3541
|
return () => set.delete(fn);
|
|
3075
3542
|
}
|
|
3076
3543
|
// --- dirty marking ----------------------------------------------------
|
|
3077
|
-
markProps(node, affectsStyle = true) {
|
|
3544
|
+
markProps(node, affectsStyle = true, beside = true) {
|
|
3078
3545
|
node.propsDirty = true;
|
|
3079
3546
|
// Classes, `nativeID` and anything else a selector can read live in props, so any of them
|
|
3080
3547
|
// changing can change what this node matches, and through it what its whole subtree does.
|
|
@@ -3084,7 +3551,8 @@ export class Engine {
|
|
|
3084
3551
|
node.styleDirty = true;
|
|
3085
3552
|
// `.peer:focus ~ .label`: what a later sibling matches can hang on this node's classes and
|
|
3086
3553
|
// state, though nothing about the sibling moved. Only sheets that ask about siblings pay.
|
|
3087
|
-
|
|
3554
|
+
// Not for a change `beside` says no rule asks of an element before another.
|
|
3555
|
+
if (this.structuralSheets && beside)
|
|
3088
3556
|
this.markLaterSiblings(node);
|
|
3089
3557
|
if (this.hasSheets)
|
|
3090
3558
|
this.markBeneath(node.parent);
|
|
@@ -3112,11 +3580,12 @@ export class Engine {
|
|
|
3112
3580
|
}
|
|
3113
3581
|
/**
|
|
3114
3582
|
* Something at or beneath `from` changed: every node from there up may match a `:has()` rule
|
|
3115
|
-
* it did not, or stop matching one. They are matched again rather than
|
|
3116
|
-
* which would restyle everything under them. Only sheets that use
|
|
3583
|
+
* it did not, or stop matching one, up to `upTo` and not it. They are matched again rather than
|
|
3584
|
+
* marked for restyling, which would restyle everything under them. Only sheets that use
|
|
3585
|
+
* `:has()` pay.
|
|
3117
3586
|
*/
|
|
3118
|
-
markBeneath(from) {
|
|
3119
|
-
for (let node = from; node; node = node.parent) {
|
|
3587
|
+
markBeneath(from, upTo = null) {
|
|
3588
|
+
for (let node = from; node && node !== upTo; node = node.parent) {
|
|
3120
3589
|
node.hasDirty = true;
|
|
3121
3590
|
if (this.hasReaders.size)
|
|
3122
3591
|
this.changedBeneath.add(node);
|
|
@@ -3133,20 +3602,49 @@ export class Engine {
|
|
|
3133
3602
|
* Have matched again what is under each box that changed beneath and that such a rule could
|
|
3134
3603
|
* be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
|
|
3135
3604
|
* there were beneath the box: a table gains its rows one at a time.
|
|
3605
|
+
*
|
|
3606
|
+
* A rule asked of a box above is walked under the highest box that asks it, which reaches what
|
|
3607
|
+
* is under the boxes beneath that one as well: a rule `:has()` asks of any box is asked by every
|
|
3608
|
+
* box from the change up to the root, and walked once, from the root. The rules a box asks are
|
|
3609
|
+
* walked together, the tree beneath it once.
|
|
3136
3610
|
*/
|
|
3137
3611
|
markUnderHas() {
|
|
3138
3612
|
if (!this.changedBeneath.size)
|
|
3139
3613
|
return;
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3614
|
+
const readers = [...this.hasReaders.values()].flat();
|
|
3615
|
+
const depths = new Map();
|
|
3616
|
+
for (const node of this.changedBeneath)
|
|
3617
|
+
depthOf(node, depths);
|
|
3618
|
+
const boxes = [...this.changedBeneath].sort((a, b) => depths.get(a) - depths.get(b));
|
|
3619
|
+
// By box walked, the rules walked under it and under every box above it.
|
|
3620
|
+
const walked = new Map();
|
|
3621
|
+
for (const node of boxes) {
|
|
3622
|
+
let above;
|
|
3623
|
+
for (let up = node.parent; up && !above; up = up.parent)
|
|
3624
|
+
above = walked.get(up);
|
|
3625
|
+
const all = this.markUnderBox(node, readers, above);
|
|
3626
|
+
if (all)
|
|
3627
|
+
walked.set(node, all);
|
|
3147
3628
|
}
|
|
3148
3629
|
this.changedBeneath.clear();
|
|
3149
3630
|
}
|
|
3631
|
+
/**
|
|
3632
|
+
* `markUnderHas` for one box: the rules it asks that no box above it was walked for, walked
|
|
3633
|
+
* together. What was walked under it and above it, which is `above` where it asks no more.
|
|
3634
|
+
*/
|
|
3635
|
+
markUnderBox(node, readers, above) {
|
|
3636
|
+
const asked = [];
|
|
3637
|
+
for (const reader of readers) {
|
|
3638
|
+
if (above?.has(reader) || !reader.classes.every((name) => node.classes?.has(name)))
|
|
3639
|
+
continue;
|
|
3640
|
+
asked.push(reader);
|
|
3641
|
+
}
|
|
3642
|
+
if (!asked.length)
|
|
3643
|
+
return above;
|
|
3644
|
+
const subjects = asked.map((reader) => reader.subjects);
|
|
3645
|
+
this.markUnder(node, subjects.length === 1 ? subjects[0] : joinSubjects(subjects));
|
|
3646
|
+
return new Set([...(above ?? []), ...asked]);
|
|
3647
|
+
}
|
|
3150
3648
|
/**
|
|
3151
3649
|
* What `node` matches changed, which the elements after it can read: as many of them as a rule
|
|
3152
3650
|
* steps along from one, every one for a `~`, and none where no sheet steps at all.
|
|
@@ -3332,6 +3830,7 @@ export class Engine {
|
|
|
3332
3830
|
this.stats.slowCommits++;
|
|
3333
3831
|
}
|
|
3334
3832
|
this.flushTransitionEvents();
|
|
3833
|
+
this.reportPaint();
|
|
3335
3834
|
this.playSized();
|
|
3336
3835
|
this.scrollDriver?.afterCommit();
|
|
3337
3836
|
// Read first: the commit that settles keyframes starts by clearing it.
|
|
@@ -3340,8 +3839,17 @@ export class Engine {
|
|
|
3340
3839
|
this.settleKeyframes();
|
|
3341
3840
|
if (facesAdded)
|
|
3342
3841
|
this.rematchFonts();
|
|
3842
|
+
this.blurHidden();
|
|
3343
3843
|
return true;
|
|
3344
3844
|
}
|
|
3845
|
+
/** A field went out of view focused, in the commit running: see `undisplayed`. */
|
|
3846
|
+
blurAfterCommit = false;
|
|
3847
|
+
blurHidden() {
|
|
3848
|
+
if (!this.blurAfterCommit)
|
|
3849
|
+
return;
|
|
3850
|
+
this.blurAfterCommit = false;
|
|
3851
|
+
this.setFocused(null);
|
|
3852
|
+
}
|
|
3345
3853
|
beforeCommit = null;
|
|
3346
3854
|
announceCommit() {
|
|
3347
3855
|
const run = this.beforeCommit;
|
|
@@ -3364,14 +3872,14 @@ export class Engine {
|
|
|
3364
3872
|
/** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
|
|
3365
3873
|
unseen = Infinity;
|
|
3366
3874
|
/**
|
|
3367
|
-
* A commit for what changed
|
|
3368
|
-
*
|
|
3875
|
+
* A commit for what changed after earlier commits, before a frame has drawn them. `since` is
|
|
3876
|
+
* how many commits there had been before the first of them (`stats.commits`).
|
|
3369
3877
|
*
|
|
3370
3878
|
* A view those commits created has not been seen in the style they gave it, so what this one
|
|
3371
|
-
* gives it is where it starts, and no transition runs toward it. That is what a browser
|
|
3372
|
-
*
|
|
3373
|
-
* is built on: Angular adds the enter class after the render pass that created
|
|
3374
|
-
* takes it off a frame later, which is the one change the transition is for.
|
|
3879
|
+
* gives it is where it starts, and no transition runs toward it. That is what a browser does
|
|
3880
|
+
* with a class added to an element before the frame after it was inserted, and what
|
|
3881
|
+
* `animate.enter` is built on: Angular adds the enter class after the render pass that created
|
|
3882
|
+
* the element, and takes it off a frame later, which is the one change the transition is for.
|
|
3375
3883
|
*/
|
|
3376
3884
|
commitUnseen(since) {
|
|
3377
3885
|
this.unseen = since;
|
|
@@ -3531,6 +4039,7 @@ export class Engine {
|
|
|
3531
4039
|
const resolved = props['pointerEvents'];
|
|
3532
4040
|
writeOwnProps(node.props, props, node.attributeOnly);
|
|
3533
4041
|
withTextContent(node, viewName, props);
|
|
4042
|
+
truncatedAs(node, props);
|
|
3534
4043
|
const cascaded = props['transform'];
|
|
3535
4044
|
const inline = inlineOf(node);
|
|
3536
4045
|
const style = boundTransform(node, Object.assign(props, inline), cascaded);
|
|
@@ -3547,6 +4056,8 @@ export class Engine {
|
|
|
3547
4056
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
3548
4057
|
this.fontFaces.apply(style);
|
|
3549
4058
|
holdLoadingFamily(node, style);
|
|
4059
|
+
// Once the face is matched: a family a view's own font names is the face its weight chose.
|
|
4060
|
+
renameFonts(viewName, style, this.postScriptName);
|
|
3550
4061
|
if (viewName === PARAGRAPH) {
|
|
3551
4062
|
alignText(style, this.directionOf(node, style));
|
|
3552
4063
|
this.wholeWords(node, style);
|
|
@@ -3555,8 +4066,8 @@ export class Engine {
|
|
|
3555
4066
|
if (this.fontsRefreshed)
|
|
3556
4067
|
this.capForFonts(node, style);
|
|
3557
4068
|
alignMultiline(viewName, style);
|
|
3558
|
-
rowsTall(style, this.fontScale);
|
|
3559
|
-
const merged = composeTransform(node, this.
|
|
4069
|
+
rowsTall(viewName, style, this.fontScale);
|
|
4070
|
+
const merged = composeTransform(node, this.eased(node, style));
|
|
3560
4071
|
// On iOS: Android's field says the baseline of the line it is sized and centred by.
|
|
3561
4072
|
// Still under the row that marked it: a row taken away is not committed again to unmark it.
|
|
3562
4073
|
const row = node.onBaseline;
|
|
@@ -3566,6 +4077,10 @@ export class Engine {
|
|
|
3566
4077
|
basisAsSize(node, merged);
|
|
3567
4078
|
// After the basis, which is a size given where it is committed as one.
|
|
3568
4079
|
ratioForAutoSize(merged);
|
|
4080
|
+
wrappedLinesStretch(merged);
|
|
4081
|
+
stretchedOverRatio(node, merged);
|
|
4082
|
+
leastWins(merged, 'Height');
|
|
4083
|
+
leastWins(merged, 'Width');
|
|
3569
4084
|
noOutline(merged);
|
|
3570
4085
|
this.borderInText(node, merged);
|
|
3571
4086
|
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
@@ -3577,6 +4092,7 @@ export class Engine {
|
|
|
3577
4092
|
this.movePaint(node, merged);
|
|
3578
4093
|
if (node.raised !== undefined)
|
|
3579
4094
|
merged['zIndex'] ??= node.raised;
|
|
4095
|
+
keptForNative(node, merged);
|
|
3580
4096
|
return merged;
|
|
3581
4097
|
}
|
|
3582
4098
|
/**
|
|
@@ -3677,6 +4193,11 @@ export class Engine {
|
|
|
3677
4193
|
* its parent's: the cascade's, and over it the parent's inline style.
|
|
3678
4194
|
*/
|
|
3679
4195
|
movePaint(node, merged) {
|
|
4196
|
+
if (node.paintTaker) {
|
|
4197
|
+
// Only the colour is handed over: a background image stays painted on the node.
|
|
4198
|
+
this.takenPaint(node, merged['backgroundColor']);
|
|
4199
|
+
delete merged['backgroundColor'];
|
|
4200
|
+
}
|
|
3680
4201
|
if (node.paintsOn)
|
|
3681
4202
|
for (const key of PAINT_KEYS)
|
|
3682
4203
|
delete merged[key];
|
|
@@ -3688,6 +4209,13 @@ export class Engine {
|
|
|
3688
4209
|
if (paint[key] !== undefined)
|
|
3689
4210
|
merged[key] = paint[key];
|
|
3690
4211
|
}
|
|
4212
|
+
/** Note the background colour a node that hands it over resolves to, where it changed. */
|
|
4213
|
+
takenPaint(node, color) {
|
|
4214
|
+
if (color === node.paintTaken)
|
|
4215
|
+
return;
|
|
4216
|
+
node.paintTaken = color;
|
|
4217
|
+
this.paintReports.add(node);
|
|
4218
|
+
}
|
|
3691
4219
|
/**
|
|
3692
4220
|
* A container whose direction or alignment changed since a child with a `fit-content` size
|
|
3693
4221
|
* read it, or whose height stopped or started being one a percentage is taken of since a child
|
|
@@ -3770,6 +4298,7 @@ export class Engine {
|
|
|
3770
4298
|
if (!sheet || this.knownSheets.has(sheet))
|
|
3771
4299
|
return;
|
|
3772
4300
|
this.knownSheets.add(sheet);
|
|
4301
|
+
this.noteSheetEasing(sheet);
|
|
3773
4302
|
this.styles.noteSheet(sheet);
|
|
3774
4303
|
this.sheetOrder.push(sheet);
|
|
3775
4304
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
@@ -3800,6 +4329,7 @@ export class Engine {
|
|
|
3800
4329
|
this.knownSheets.delete(sheet);
|
|
3801
4330
|
if (next && !this.knownSheets.has(next)) {
|
|
3802
4331
|
this.knownSheets.add(next);
|
|
4332
|
+
this.noteSheetEasing(next);
|
|
3803
4333
|
this.styles.noteSheet(next);
|
|
3804
4334
|
this.sheetOrder[at] = next;
|
|
3805
4335
|
}
|
|
@@ -3842,6 +4372,7 @@ export class Engine {
|
|
|
3842
4372
|
}
|
|
3843
4373
|
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
3844
4374
|
fontFaces = new FontFaces();
|
|
4375
|
+
postScriptName = (name) => this.fontFaces.postScriptName(name);
|
|
3845
4376
|
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
3846
4377
|
facesAdded = false;
|
|
3847
4378
|
/**
|
|
@@ -3865,7 +4396,10 @@ export class Engine {
|
|
|
3865
4396
|
named(child.box);
|
|
3866
4397
|
if (child.kind !== 'element')
|
|
3867
4398
|
continue;
|
|
3868
|
-
|
|
4399
|
+
if (child.committed && FONT_NAMES[child.committed.viewName])
|
|
4400
|
+
this.markProps(child, false);
|
|
4401
|
+
else
|
|
4402
|
+
named(child);
|
|
3869
4403
|
this.markFontText(child);
|
|
3870
4404
|
}
|
|
3871
4405
|
}
|
|
@@ -4081,8 +4615,9 @@ export class Engine {
|
|
|
4081
4615
|
}
|
|
4082
4616
|
/**
|
|
4083
4617
|
* Native reached the end of an animation it was playing. The frame it ends at, or the resting
|
|
4084
|
-
* style where it does not fill, is committed
|
|
4085
|
-
*
|
|
4618
|
+
* style where it does not fill, is committed, and set on native where the animation set the
|
|
4619
|
+
* prop (see `setOwned`). The view is let go first, which puts nothing back on Fabric: it is
|
|
4620
|
+
* drawn where the animation left it until then.
|
|
4086
4621
|
*/
|
|
4087
4622
|
endedNatively(node, running) {
|
|
4088
4623
|
if (node.playing !== running || !running.native)
|
|
@@ -4091,9 +4626,62 @@ export class Engine {
|
|
|
4091
4626
|
running.native = undefined;
|
|
4092
4627
|
this.playedNatively.delete(node);
|
|
4093
4628
|
const end = running.start + spec.duration * (spec.iterations ?? 1);
|
|
4629
|
+
native.stop();
|
|
4094
4630
|
this.advancePlayer(node, Math.max(this.now(), end));
|
|
4095
4631
|
this.commit();
|
|
4096
|
-
|
|
4632
|
+
// The commit sets only what it changed, and the style the view rests at can be the frame it
|
|
4633
|
+
// was committed with as it played: native has drawn it elsewhere since.
|
|
4634
|
+
const committed = node.committed;
|
|
4635
|
+
if (node.owned && committed)
|
|
4636
|
+
this.setOwned(node, RESET_OWNED, committed.props);
|
|
4637
|
+
}
|
|
4638
|
+
/** A graph of native's was connected to a view: see `Touched`. */
|
|
4639
|
+
touchedNatively(node, tag, keys) {
|
|
4640
|
+
const taken = keys.filter((key) => key === 'opacity' || key === 'transform');
|
|
4641
|
+
if (!taken.length)
|
|
4642
|
+
return;
|
|
4643
|
+
if (node.owned?.tag !== tag)
|
|
4644
|
+
node.owned = { tag, keys: new Set() };
|
|
4645
|
+
for (const key of taken)
|
|
4646
|
+
node.owned.keys.add(key);
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Set on native a change of a prop a graph of native's has set on the view before, while no
|
|
4650
|
+
* graph of its holds it: the commit cannot change it (see `Touched`). The payload is committed
|
|
4651
|
+
* as well, so the shadow node is as the view is drawn. A transform native cannot take as
|
|
4652
|
+
* numbers, a matrix, is left committed alone.
|
|
4653
|
+
*/
|
|
4654
|
+
setOwned(node, payload, props) {
|
|
4655
|
+
const owned = node.owned;
|
|
4656
|
+
const tag = node.committed?.tag;
|
|
4657
|
+
if (tag === undefined || owned.tag !== tag || !this.scrollDriver)
|
|
4658
|
+
return;
|
|
4659
|
+
const held = this.scrollDriver.held(node);
|
|
4660
|
+
const free = (key) => key in payload && owned.keys.has(key) && !held.has(key);
|
|
4661
|
+
const values = {};
|
|
4662
|
+
if (free('opacity')) {
|
|
4663
|
+
const opacity = props['opacity'];
|
|
4664
|
+
values.opacity = typeof opacity === 'number' ? opacity : 1;
|
|
4665
|
+
}
|
|
4666
|
+
if (free('transform')) {
|
|
4667
|
+
const transform = heldTransform(props['transform'], () => this.boxSize(node));
|
|
4668
|
+
if (transform)
|
|
4669
|
+
values.transform = transform;
|
|
4670
|
+
}
|
|
4671
|
+
if (values.opacity !== undefined || values.transform) {
|
|
4672
|
+
this.scrollDriver.set(node, tag, values);
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
/** The size a view is laid out at, where it has been. */
|
|
4676
|
+
boxSize(node) {
|
|
4677
|
+
let size;
|
|
4678
|
+
this.measure(node, (frame) => (size = frame));
|
|
4679
|
+
return size;
|
|
4680
|
+
}
|
|
4681
|
+
/** Forget the props native set on a node's view, which is gone or made again. */
|
|
4682
|
+
disown(node) {
|
|
4683
|
+
node.owned = undefined;
|
|
4684
|
+
this.scrollDriver?.forget(node);
|
|
4097
4685
|
}
|
|
4098
4686
|
/** Take an animation back from native, which stops it there. The caller plays it on, or not. */
|
|
4099
4687
|
stopNative(node, running) {
|
|
@@ -4369,8 +4957,9 @@ export class Engine {
|
|
|
4369
4957
|
this.takeBackEased(node, state, props, spec, now);
|
|
4370
4958
|
this.takeBackLaid(state, props, spec, now);
|
|
4371
4959
|
const started = [];
|
|
4960
|
+
const before = spec && this.paintedBefore(node);
|
|
4372
4961
|
for (const key of steppedKeys(props, spec, state)) {
|
|
4373
|
-
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
|
|
4962
|
+
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now, before)) {
|
|
4374
4963
|
this.running.add(node);
|
|
4375
4964
|
started.push(key);
|
|
4376
4965
|
this.emitTransition(node, 'topTransitionstart', key);
|
|
@@ -4381,6 +4970,29 @@ export class Engine {
|
|
|
4381
4970
|
node.transitions = undefined;
|
|
4382
4971
|
return props;
|
|
4383
4972
|
}
|
|
4973
|
+
/**
|
|
4974
|
+
* What a view was last painted with, by property, for a transition that arrives with the change
|
|
4975
|
+
* it eases: CSS takes the transition from the style after the change and eases from the value
|
|
4976
|
+
* before it. Nothing for a view nobody has seen yet, which starts where it is. The individual
|
|
4977
|
+
* transforms are read back from the parts the painted transform was put together from.
|
|
4978
|
+
*/
|
|
4979
|
+
paintedBefore(node) {
|
|
4980
|
+
const painted = node.committed?.props;
|
|
4981
|
+
// A view that paints its background on a child does not commit it as its own.
|
|
4982
|
+
if (!painted || node.paintsOn)
|
|
4983
|
+
return undefined;
|
|
4984
|
+
if (node.bornIn !== undefined && node.bornIn >= this.unseen)
|
|
4985
|
+
return undefined;
|
|
4986
|
+
const composed = node.composedTransform;
|
|
4987
|
+
if (!composed || painted['transform'] !== composed.value)
|
|
4988
|
+
return (key) => painted[key];
|
|
4989
|
+
return (key) => {
|
|
4990
|
+
if (key === 'transform')
|
|
4991
|
+
return composed.parts[3];
|
|
4992
|
+
const part = INDIVIDUAL_TRANSFORMS.indexOf(key);
|
|
4993
|
+
return part === -1 ? painted[key] : composed.parts[part];
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
4384
4996
|
/** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
|
|
4385
4997
|
unseenTransitions(node) {
|
|
4386
4998
|
// One with no commit it was born in has been seen, whatever was committed since.
|
|
@@ -4428,17 +5040,26 @@ export class Engine {
|
|
|
4428
5040
|
/**
|
|
4429
5041
|
* Commit where it ends each transition of a size or a place that just started, for native to
|
|
4430
5042
|
* move the view there: a layout animation, which the commit asks for once it is known that
|
|
4431
|
-
* nothing else in it moves. One still on its way is committed where it ends too.
|
|
5043
|
+
* nothing else in it moves. One still on its way is committed where it ends too. On a host
|
|
5044
|
+
* with no layout animation they are eased from JavaScript, but for one sent on again and again.
|
|
4432
5045
|
*
|
|
4433
5046
|
* A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
|
|
4434
5047
|
*/
|
|
4435
5048
|
layEased(node, state, started, props, now) {
|
|
4436
|
-
|
|
4437
|
-
return;
|
|
5049
|
+
const native = this.fabric.configureNextLayoutAnimation !== undefined;
|
|
4438
5050
|
for (const key of started) {
|
|
4439
5051
|
const eased = state.get(key);
|
|
4440
5052
|
if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
|
|
4441
5053
|
continue;
|
|
5054
|
+
if (this.sentOnAgain(node, key, eased, now)) {
|
|
5055
|
+
// Where it ends, at once: see `sentOnAgain`.
|
|
5056
|
+
Object.assign(eased, { current: eased.to, done: true });
|
|
5057
|
+
this.emitTransition(node, 'topTransitionend', key);
|
|
5058
|
+
props[key] = eased.to;
|
|
5059
|
+
continue;
|
|
5060
|
+
}
|
|
5061
|
+
if (!native)
|
|
5062
|
+
continue;
|
|
4442
5063
|
eased.laid = true;
|
|
4443
5064
|
this.laying.push({ node, key, eased });
|
|
4444
5065
|
}
|
|
@@ -4446,6 +5067,24 @@ export class Engine {
|
|
|
4446
5067
|
if (eased.laid && !eased.done)
|
|
4447
5068
|
props[key] = eased.to;
|
|
4448
5069
|
}
|
|
5070
|
+
/** When each size in transition was last started, and how many starts in a row came soon. */
|
|
5071
|
+
laidStarts = new WeakMap();
|
|
5072
|
+
/**
|
|
5073
|
+
* Whether a transition of a size is one sent on again and again, each before the last had
|
|
5074
|
+
* ended: a progress bar that steps every 50ms and eases each step over 150. Native moves the
|
|
5075
|
+
* view, and lays the screen out, every frame while a layout animation runs, and JavaScript
|
|
5076
|
+
* commits every frame while it eases one, so one started at every step is one that never
|
|
5077
|
+
* stops. The first, and one sent back before it ends, are played; from the second sent on in
|
|
5078
|
+
* a row, each is committed where it ends, and the steps are the motion. Played again once the
|
|
5079
|
+
* size has been left alone for as long as it lasts.
|
|
5080
|
+
*/
|
|
5081
|
+
sentOnAgain(node, key, eased, now) {
|
|
5082
|
+
const starts = this.laidStarts.get(node) ?? this.laidStarts.set(node, new Map()).get(node);
|
|
5083
|
+
const last = starts.get(key);
|
|
5084
|
+
const soon = last && now - last.at < eased.duration ? last.soon + 1 : 0;
|
|
5085
|
+
starts.set(key, { at: now, soon });
|
|
5086
|
+
return soon >= 2;
|
|
5087
|
+
}
|
|
4449
5088
|
/** The animations this commit lays out at their last frame, for native to move the views to. */
|
|
4450
5089
|
laidPlayed = [];
|
|
4451
5090
|
/**
|
|
@@ -4725,6 +5364,17 @@ export class Engine {
|
|
|
4725
5364
|
this.lettingGo.add(node);
|
|
4726
5365
|
return true;
|
|
4727
5366
|
}
|
|
5367
|
+
/** Whether a node, or an element it is in, is `display: none`. */
|
|
5368
|
+
boxless(node) {
|
|
5369
|
+
for (let at = node; at; at = at.parent) {
|
|
5370
|
+
if (at.kind !== 'element')
|
|
5371
|
+
continue;
|
|
5372
|
+
this.styles.resolve(at, this.styleEpoch);
|
|
5373
|
+
if (ownLayout(at, 'display') === 'none')
|
|
5374
|
+
return true;
|
|
5375
|
+
}
|
|
5376
|
+
return false;
|
|
5377
|
+
}
|
|
4728
5378
|
/** One `@keyframes` player, one frame on. */
|
|
4729
5379
|
advancePlayer(node, now) {
|
|
4730
5380
|
const running = node.playing;
|
|
@@ -4736,6 +5386,17 @@ export class Engine {
|
|
|
4736
5386
|
this.playing.delete(node);
|
|
4737
5387
|
return;
|
|
4738
5388
|
}
|
|
5389
|
+
// One with no box, or in a box with none, paints nothing, and an endless one would keep the
|
|
5390
|
+
// frame loop going for good. It is cancelled, as a browser cancels it, and starts again when
|
|
5391
|
+
// the node is shown, as it is made again.
|
|
5392
|
+
if (this.boxless(node)) {
|
|
5393
|
+
this.cancelPlaying(node);
|
|
5394
|
+
this.playing.delete(node);
|
|
5395
|
+
node.playing = undefined;
|
|
5396
|
+
// A commit, for its `animationcancel` to be sent after, and to drop its last frame.
|
|
5397
|
+
this.markProps(node, false);
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
4739
5400
|
// Frames that say nothing paint nothing: a keyframe's one declaration refused at build time
|
|
4740
5401
|
// leaves these. No frame of it is committed, and one that never ends has nothing left to do.
|
|
4741
5402
|
const idle = running.tracks.size === 0;
|
|
@@ -4815,15 +5476,21 @@ export class Engine {
|
|
|
4815
5476
|
}
|
|
4816
5477
|
return this.animating;
|
|
4817
5478
|
}
|
|
4818
|
-
/**
|
|
4819
|
-
|
|
5479
|
+
/**
|
|
5480
|
+
* The host's conversions, applied to a copy. A removed prop travels as `cleared` says, null for
|
|
5481
|
+
* nearly every one. On an update a colour dropped as unreadable is null too: JSI leaves out an
|
|
5482
|
+
* undefined key, and Fabric keeps what a clone leaves out.
|
|
5483
|
+
*/
|
|
5484
|
+
processed(props, update) {
|
|
4820
5485
|
const out = {};
|
|
4821
5486
|
for (const key of Object.keys(props)) {
|
|
4822
5487
|
const value = props[key];
|
|
4823
|
-
if (value === null
|
|
5488
|
+
if (value === null)
|
|
5489
|
+
out[key] = cleared(key);
|
|
5490
|
+
else if (value === undefined)
|
|
4824
5491
|
out[key] = value;
|
|
4825
5492
|
else if (isColorProp(key))
|
|
4826
|
-
out[key] = this.
|
|
5493
|
+
out[key] = this.colourProp(key, value, update);
|
|
4827
5494
|
// A list of sources has already been resolved by whoever built it, as RN's resolver
|
|
4828
5495
|
// also assumes: it passes any object through and only turns a number into one.
|
|
4829
5496
|
else if (ASSET_PROPS.has(key) && !Array.isArray(value))
|
|
@@ -4985,7 +5652,7 @@ export class Engine {
|
|
|
4985
5652
|
this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
|
|
4986
5653
|
let handle = previous.handle;
|
|
4987
5654
|
if (childrenChanged || propsPayload) {
|
|
4988
|
-
handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
|
|
5655
|
+
handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload, true), childrenChanged ? childHandles : null);
|
|
4989
5656
|
// A clone is the same view, so it keeps the tag it was created with, not the newest tag
|
|
4990
5657
|
// handed out, which is some other node's.
|
|
4991
5658
|
node.committed = { handle, tag: previous.tag, props, childHandles, viewName };
|
|
@@ -4996,6 +5663,10 @@ export class Engine {
|
|
|
4996
5663
|
/** What a clone sends: the props that changed, and a text input's font once it registered. */
|
|
4997
5664
|
propsPayload(node, previous, props) {
|
|
4998
5665
|
const payload = diffProps(previous, props);
|
|
5666
|
+
if (payload)
|
|
5667
|
+
borderColoursAgain(payload, props);
|
|
5668
|
+
if (payload && node.owned)
|
|
5669
|
+
this.setOwned(node, payload, props);
|
|
4999
5670
|
if (payload && this.fontsRefreshed && this.fontResends.delete(node)) {
|
|
5000
5671
|
payload['fontFamily'] = props['fontFamily'];
|
|
5001
5672
|
}
|
|
@@ -5024,10 +5695,17 @@ export class Engine {
|
|
|
5024
5695
|
*/
|
|
5025
5696
|
reconcileChildren(node, viewName, style) {
|
|
5026
5697
|
const context = style?.context;
|
|
5027
|
-
if (
|
|
5698
|
+
if (HOIST_ONLY.has(node.tag))
|
|
5699
|
+
return this.reconcileOnlyHoisted(node);
|
|
5700
|
+
if (HOIST_TARGETS.has(node.tag))
|
|
5028
5701
|
return this.reconcileHoistTarget(node, context);
|
|
5029
5702
|
const handles = [];
|
|
5030
5703
|
for (const child of node.children) {
|
|
5704
|
+
const clean = this.cleanUnder(node, child, context);
|
|
5705
|
+
if (clean) {
|
|
5706
|
+
handles.push(clean);
|
|
5707
|
+
continue;
|
|
5708
|
+
}
|
|
5031
5709
|
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
5032
5710
|
continue;
|
|
5033
5711
|
}
|
|
@@ -5071,7 +5749,7 @@ export class Engine {
|
|
|
5071
5749
|
return null;
|
|
5072
5750
|
let box = text.box;
|
|
5073
5751
|
// One made for another element, before the view was registered again, is done with.
|
|
5074
|
-
if (box && box.
|
|
5752
|
+
if (box && box.tag !== drawnBy) {
|
|
5075
5753
|
if (box.committed)
|
|
5076
5754
|
this.forgetCommitted(box);
|
|
5077
5755
|
box = undefined;
|
|
@@ -5083,6 +5761,9 @@ export class Engine {
|
|
|
5083
5761
|
text.box = box;
|
|
5084
5762
|
}
|
|
5085
5763
|
box.parent = parent;
|
|
5764
|
+
// Merged before the view is, so the view's truncation is read here: see `truncatedAs`.
|
|
5765
|
+
if (truncationMoved(box, parent))
|
|
5766
|
+
box.propsDirty = true;
|
|
5086
5767
|
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
5087
5768
|
if (!this.isClean(text)) {
|
|
5088
5769
|
box.subtreeDirty = true;
|
|
@@ -5143,10 +5824,12 @@ export class Engine {
|
|
|
5143
5824
|
return false;
|
|
5144
5825
|
if (child.committed) {
|
|
5145
5826
|
this.forgetCommitted(child);
|
|
5146
|
-
// A field in it has no view to send its blur from.
|
|
5827
|
+
// A field in it has no view to send its blur from. Blurred once this commit is done: a
|
|
5828
|
+
// `:focus` rule that hid it then no longer holds, and the commit that follows shows it.
|
|
5147
5829
|
if (isWithin(this.focusedNode, child))
|
|
5148
|
-
this.
|
|
5830
|
+
this.blurAfterCommit = true;
|
|
5149
5831
|
}
|
|
5832
|
+
settlePaths(child);
|
|
5150
5833
|
return true;
|
|
5151
5834
|
}
|
|
5152
5835
|
/**
|
|
@@ -5177,13 +5860,13 @@ export class Engine {
|
|
|
5177
5860
|
// which RNS keeps clear by never letting the header config be the first.
|
|
5178
5861
|
const direct = [];
|
|
5179
5862
|
for (const child of node.children) {
|
|
5180
|
-
|
|
5863
|
+
const taken = this.takenByTarget(node, child, context);
|
|
5864
|
+
if (taken === null)
|
|
5181
5865
|
continue;
|
|
5182
|
-
|
|
5183
|
-
if (this.hoisted.has(child))
|
|
5866
|
+
if (taken === 'direct')
|
|
5184
5867
|
direct.push(child);
|
|
5185
5868
|
else
|
|
5186
|
-
handles.push(
|
|
5869
|
+
handles.push(taken);
|
|
5187
5870
|
const moved = hoisted?.get(child);
|
|
5188
5871
|
for (const written of moved ?? []) {
|
|
5189
5872
|
if (!this.undisplayedUnder(node, written))
|
|
@@ -5196,6 +5879,33 @@ export class Engine {
|
|
|
5196
5879
|
this.standIn(node, landed, handles);
|
|
5197
5880
|
return handles;
|
|
5198
5881
|
}
|
|
5882
|
+
/**
|
|
5883
|
+
* `reconcileHoistTarget` for a node that commits only what is hoisted into it
|
|
5884
|
+
* (`HoistOptions.only`): those nodes, in the order they were written, and none of its own.
|
|
5885
|
+
*/
|
|
5886
|
+
reconcileOnlyHoisted(node) {
|
|
5887
|
+
const handles = [];
|
|
5888
|
+
const hoisted = this.hoistedInto(node);
|
|
5889
|
+
const landed = new Set();
|
|
5890
|
+
for (const child of node.children) {
|
|
5891
|
+
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
5892
|
+
continue;
|
|
5893
|
+
}
|
|
5894
|
+
if (this.hoisted.has(child)) {
|
|
5895
|
+
handles.push(this.land(node, child, landed));
|
|
5896
|
+
continue;
|
|
5897
|
+
}
|
|
5898
|
+
for (const written of hoisted?.get(child) ?? []) {
|
|
5899
|
+
if (!this.undisplayedUnder(node, written))
|
|
5900
|
+
handles.push(this.land(node, written, landed));
|
|
5901
|
+
}
|
|
5902
|
+
// Not committed, so the marks a change left on the way down to it are cleared here.
|
|
5903
|
+
settlePaths(child);
|
|
5904
|
+
}
|
|
5905
|
+
if (node.kept)
|
|
5906
|
+
this.standIn(node, landed, handles);
|
|
5907
|
+
return handles;
|
|
5908
|
+
}
|
|
5199
5909
|
/**
|
|
5200
5910
|
* A child's handle as a child of `parent`, created again when it was committed under another.
|
|
5201
5911
|
*
|
|
@@ -5228,8 +5938,19 @@ export class Engine {
|
|
|
5228
5938
|
forgetCommitted(node) {
|
|
5229
5939
|
node.committed = null;
|
|
5230
5940
|
node.committedUnder = null;
|
|
5941
|
+
// Another view, with another tag, which no graph has set anything on.
|
|
5942
|
+
if (node.owned)
|
|
5943
|
+
this.disown(node);
|
|
5231
5944
|
node.styleCommitted = null;
|
|
5232
5945
|
node.kept = undefined;
|
|
5946
|
+
// A scroll-driven animation is bound to native views by their tags: its own, and its scroll
|
|
5947
|
+
// view's. Made again, each has another, so what drives it starts again with the new ones.
|
|
5948
|
+
if (node.scrolled?.drive)
|
|
5949
|
+
this.stopScrolled(node);
|
|
5950
|
+
for (const follower of this.scrollTimelines.get(node) ?? []) {
|
|
5951
|
+
this.stopScrolled(follower);
|
|
5952
|
+
this.markProps(follower, false);
|
|
5953
|
+
}
|
|
5233
5954
|
if (node.box?.committed)
|
|
5234
5955
|
this.forgetCommitted(node.box);
|
|
5235
5956
|
for (const child of node.children) {
|
|
@@ -5246,9 +5967,9 @@ export class Engine {
|
|
|
5246
5967
|
* keeps for its name when it has one (`HoistOptions.standIn`), and keep the view it commits as.
|
|
5247
5968
|
*/
|
|
5248
5969
|
land(target, child, landed) {
|
|
5249
|
-
if (!STAND_INS[child.
|
|
5970
|
+
if (!STAND_INS[child.tag])
|
|
5250
5971
|
return this.reconcileUnder(target, child);
|
|
5251
|
-
const kept = target.kept?.get(child.
|
|
5972
|
+
const kept = target.kept?.get(child.tag);
|
|
5252
5973
|
if (kept && kept.node !== child && child.committed === null && !isWithin(kept.node, target)) {
|
|
5253
5974
|
// A node taken out without being destroyed would otherwise come back holding this tag too.
|
|
5254
5975
|
if (kept.node)
|
|
@@ -5258,8 +5979,8 @@ export class Engine {
|
|
|
5258
5979
|
}
|
|
5259
5980
|
const handle = this.reconcile(child);
|
|
5260
5981
|
child.committedUnder = target;
|
|
5261
|
-
(target.kept ??= new Map()).set(child.
|
|
5262
|
-
landed.add(child.
|
|
5982
|
+
(target.kept ??= new Map()).set(child.tag, { node: child, committed: child.committed });
|
|
5983
|
+
landed.add(child.tag);
|
|
5263
5984
|
return handle;
|
|
5264
5985
|
}
|
|
5265
5986
|
/**
|
|
@@ -5276,13 +5997,40 @@ export class Engine {
|
|
|
5276
5997
|
...STAND_INS[name],
|
|
5277
5998
|
};
|
|
5278
5999
|
const payload = diffProps(kept.committed.props, props);
|
|
5279
|
-
const handle = this.clone(kept.committed.handle, payload && this.processed(payload), []);
|
|
6000
|
+
const handle = this.clone(kept.committed.handle, payload && this.processed(payload, true), []);
|
|
5280
6001
|
kept.committed = { ...kept.committed, handle, props, childHandles: [] };
|
|
5281
6002
|
kept.node = null;
|
|
5282
6003
|
}
|
|
5283
6004
|
handles.push(kept.committed.handle);
|
|
5284
6005
|
}
|
|
5285
6006
|
}
|
|
6007
|
+
/**
|
|
6008
|
+
* What a hoist target takes of a child written in it: the child's handle, `direct` for one
|
|
6009
|
+
* hoisted into it, which goes after its siblings, or null for none.
|
|
6010
|
+
*/
|
|
6011
|
+
takenByTarget(node, child, context) {
|
|
6012
|
+
if (this.hoisted.has(child)) {
|
|
6013
|
+
if (this.committedElsewhere(child) || this.withheld(child))
|
|
6014
|
+
return null;
|
|
6015
|
+
return 'direct';
|
|
6016
|
+
}
|
|
6017
|
+
const clean = this.cleanUnder(node, child, context);
|
|
6018
|
+
if (clean)
|
|
6019
|
+
return clean;
|
|
6020
|
+
if (child.kind === 'anchor' || this.withheld(child))
|
|
6021
|
+
return null;
|
|
6022
|
+
return this.reconcileUnder(node, child, context);
|
|
6023
|
+
}
|
|
6024
|
+
/**
|
|
6025
|
+
* A clean element's handle under `parent`, before anything else is asked of it: one committed
|
|
6026
|
+
* there and unchanged since was displayed and in its place, and is still. The siblings of what
|
|
6027
|
+
* changed are most of what a commit steps over: a list one row of which ticks.
|
|
6028
|
+
*/
|
|
6029
|
+
cleanUnder(parent, child, context) {
|
|
6030
|
+
if (child.kind !== 'element' || child.committedUnder !== parent)
|
|
6031
|
+
return null;
|
|
6032
|
+
return this.stillCommitted(child, context);
|
|
6033
|
+
}
|
|
5286
6034
|
/** A clean child's committed handle, when its style still holds under its parent's. */
|
|
5287
6035
|
stillCommitted(child, context) {
|
|
5288
6036
|
const committed = child.committed;
|
|
@@ -5305,11 +6053,13 @@ export class Engine {
|
|
|
5305
6053
|
hoistedInto(node) {
|
|
5306
6054
|
let found = null;
|
|
5307
6055
|
for (const candidate of this.hoisted) {
|
|
5308
|
-
if (candidate.parent === node || HOISTS[candidate.
|
|
6056
|
+
if (candidate.parent === node || HOISTS[candidate.tag] !== node.tag)
|
|
5309
6057
|
continue;
|
|
5310
6058
|
let written = candidate;
|
|
5311
6059
|
let above = candidate.parent;
|
|
5312
|
-
while (above && above !== node && above.
|
|
6060
|
+
while (above && above !== node && above.tag !== node.tag) {
|
|
6061
|
+
if (HOIST_STOPS[candidate.tag]?.has(above.tag))
|
|
6062
|
+
break;
|
|
5313
6063
|
written = above;
|
|
5314
6064
|
above = above.parent;
|
|
5315
6065
|
}
|
|
@@ -5326,12 +6076,14 @@ export class Engine {
|
|
|
5326
6076
|
}
|
|
5327
6077
|
/** Whether a hoisted node commits further up than the parent it was written in. */
|
|
5328
6078
|
hoistsAbove(node) {
|
|
5329
|
-
const target = HOISTS[node.
|
|
5330
|
-
if (node.parent?.
|
|
6079
|
+
const target = HOISTS[node.tag];
|
|
6080
|
+
if (node.parent?.tag === target)
|
|
5331
6081
|
return false;
|
|
5332
|
-
for (let above = node.parent
|
|
5333
|
-
if (above.
|
|
6082
|
+
for (let above = node.parent; above; above = above.parent) {
|
|
6083
|
+
if (above.tag === target)
|
|
5334
6084
|
return true;
|
|
6085
|
+
if (HOIST_STOPS[node.tag]?.has(above.tag))
|
|
6086
|
+
return false;
|
|
5335
6087
|
}
|
|
5336
6088
|
return false;
|
|
5337
6089
|
}
|
|
@@ -5343,7 +6095,7 @@ export class Engine {
|
|
|
5343
6095
|
const tag = nextTag();
|
|
5344
6096
|
this.stats.createdNodes++;
|
|
5345
6097
|
// instanceHandle is the retained node itself: it is what Fabric hands back on events.
|
|
5346
|
-
const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props), node);
|
|
6098
|
+
const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props, false), node);
|
|
5347
6099
|
for (const child of childHandles)
|
|
5348
6100
|
this.fabric.appendChild(handle, child);
|
|
5349
6101
|
node.committed = { handle, tag, props, childHandles, viewName };
|
|
@@ -5360,13 +6112,13 @@ export class Engine {
|
|
|
5360
6112
|
// The paragraph the engine wraps loose text in is its own: no template wrote it.
|
|
5361
6113
|
if (node.kind !== 'element' || node.claimed || node.anonymous)
|
|
5362
6114
|
return;
|
|
5363
|
-
if (!PRIMITIVE_NAMES.has(node.
|
|
6115
|
+
if (!PRIMITIVE_NAMES.has(node.tag))
|
|
5364
6116
|
return;
|
|
5365
|
-
if (this.reported.has(node.
|
|
6117
|
+
if (this.reported.has(node.tag))
|
|
5366
6118
|
return;
|
|
5367
|
-
this.reported.add(node.
|
|
5368
|
-
console.error(`[angular-native] <${node.
|
|
5369
|
-
`${className(node.
|
|
6119
|
+
this.reported.add(node.tag);
|
|
6120
|
+
console.error(`[angular-native] <${node.tag}> is used in a template that does not import ` +
|
|
6121
|
+
`${className(node.tag)}. Add it to the component's \`imports\` from ` +
|
|
5370
6122
|
`'@ng-native/components'; without it the element has none of the component's inputs ` +
|
|
5371
6123
|
`or behavior, so a prop written on it stays the text it was written as.`);
|
|
5372
6124
|
}
|
|
@@ -5389,6 +6141,19 @@ export class Engine {
|
|
|
5389
6141
|
` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
|
|
5390
6142
|
` :root or an ancestor, or give the var() a fallback.`);
|
|
5391
6143
|
}
|
|
6144
|
+
droppedDeclarations = new Set();
|
|
6145
|
+
/**
|
|
6146
|
+
* A declaration in a style, written or bound, that is no value its property takes, so it is
|
|
6147
|
+
* dropped as a browser drops it. Said once per element name and declaration, in development.
|
|
6148
|
+
*/
|
|
6149
|
+
reportDroppedDeclaration(prop, value, on, why) {
|
|
6150
|
+
const report = `${on} ${prop}: ${value}`;
|
|
6151
|
+
if (!this.dev || this.droppedDeclarations.has(report))
|
|
6152
|
+
return;
|
|
6153
|
+
this.droppedDeclarations.add(report);
|
|
6154
|
+
console.warn(`[angular-native] ${prop}: ${value}, in a style on <${on}>, is no value ${prop} takes:` +
|
|
6155
|
+
` ${why}. The declaration is dropped, as a browser drops it.`);
|
|
6156
|
+
}
|
|
5392
6157
|
unreadDisplays = new Set();
|
|
5393
6158
|
/**
|
|
5394
6159
|
* Said once per token and value: the compiler refuses `display: grid` written out, but a token
|
|
@@ -5404,23 +6169,23 @@ export class Engine {
|
|
|
5404
6169
|
` and contents, and reads block, inline, inline-block, flow-root and inline-flex as flex.`);
|
|
5405
6170
|
}
|
|
5406
6171
|
checkProps(node) {
|
|
5407
|
-
if (!node.claimed || !PRIMITIVE_NAMES.has(node.
|
|
6172
|
+
if (!node.claimed || !PRIMITIVE_NAMES.has(node.tag))
|
|
5408
6173
|
return;
|
|
5409
|
-
const declared = DECLARED_PROPS.get(node.
|
|
6174
|
+
const declared = DECLARED_PROPS.get(node.tag);
|
|
5410
6175
|
if (!declared)
|
|
5411
6176
|
return;
|
|
5412
6177
|
for (const key of Object.keys(node.props)) {
|
|
5413
6178
|
if (this.isDeclared(declared, key, node.props[key]))
|
|
5414
6179
|
continue;
|
|
5415
|
-
const report = `${node.
|
|
6180
|
+
const report = `${node.tag} ${key}`;
|
|
5416
6181
|
if (this.reported.has(report))
|
|
5417
6182
|
continue;
|
|
5418
6183
|
this.reported.add(report);
|
|
5419
6184
|
const meant = [...declared].find((name) => name.toLowerCase() === key.toLowerCase());
|
|
5420
|
-
console.warn(`[angular-native] <${node.
|
|
6185
|
+
console.warn(`[angular-native] <${node.tag}> has no prop '${key}', so native ignores it.` +
|
|
5421
6186
|
(meant ? ` Did you mean '${meant}'?` : '') +
|
|
5422
|
-
` Check the spelling against the ${className(node.
|
|
5423
|
-
` declareNativeProps('${node.
|
|
6187
|
+
` Check the spelling against the ${className(node.tag)} page, or declare it with` +
|
|
6188
|
+
` declareNativeProps('${node.tag}', ['${key}']) if the native view does read it.`);
|
|
5424
6189
|
}
|
|
5425
6190
|
}
|
|
5426
6191
|
isDeclared(declared, key, value) {
|
|
@@ -5460,12 +6225,12 @@ export class Engine {
|
|
|
5460
6225
|
* the trade for not holding a registry per component, and the typo is the case that matters.
|
|
5461
6226
|
*/
|
|
5462
6227
|
checkKnown(node) {
|
|
5463
|
-
if (node.kind !== 'element' || node.componentHost || node.
|
|
6228
|
+
if (node.kind !== 'element' || node.componentHost || node.tag in VIEW_NAMES)
|
|
5464
6229
|
return;
|
|
5465
|
-
if (this.declaredElements.has(node.
|
|
6230
|
+
if (this.declaredElements.has(node.tag) || this.reported.has(node.tag))
|
|
5466
6231
|
return;
|
|
5467
|
-
this.reported.add(node.
|
|
5468
|
-
console.error(`[angular-native] <${node.
|
|
6232
|
+
this.reported.add(node.tag);
|
|
6233
|
+
console.error(`[angular-native] <${node.tag}> is not a known element: no component, directive or ` +
|
|
5469
6234
|
`registered native view claims it, so it renders as an empty view. Check the spelling, ` +
|
|
5470
6235
|
`or import the component that owns it.`);
|
|
5471
6236
|
}
|
|
@@ -5605,7 +6370,7 @@ export class Engine {
|
|
|
5605
6370
|
*/
|
|
5606
6371
|
get focused() {
|
|
5607
6372
|
if (this.focusedNode && !this.isAttached(this.focusedNode))
|
|
5608
|
-
this.
|
|
6373
|
+
this.dropFocus();
|
|
5609
6374
|
return this.focusedNode;
|
|
5610
6375
|
}
|
|
5611
6376
|
/** Whether a node is still part of the tree, rather than in a subtree that was removed. */
|
|
@@ -5622,17 +6387,29 @@ export class Engine {
|
|
|
5622
6387
|
resolveAsset(value) {
|
|
5623
6388
|
return this.resolveAssetSource(value);
|
|
5624
6389
|
}
|
|
6390
|
+
/** A colour prop as `processed` sends it: one dropped as unreadable is null on an update. */
|
|
6391
|
+
colourProp(key, value, update) {
|
|
6392
|
+
const painted = this.painted(key, value);
|
|
6393
|
+
return painted === undefined && update ? null : painted;
|
|
6394
|
+
}
|
|
5625
6395
|
/**
|
|
5626
6396
|
* A colour prop as native takes it. A shape's `fill` or `stroke` the cascade settled from a
|
|
5627
|
-
* token is the solid brush react-native-svg takes,
|
|
6397
|
+
* token is the solid brush react-native-svg takes, `none` no paint at all, and one the host
|
|
6398
|
+
* reads no colour in no paint of the shape's own.
|
|
5628
6399
|
*/
|
|
5629
6400
|
painted(key, value) {
|
|
5630
6401
|
if (!BRUSH_PROPS.has(key))
|
|
5631
6402
|
return this.color(value);
|
|
5632
|
-
// Already a brush, as the icon wrote it: only one the cascade settled is still text.
|
|
6403
|
+
// Already a brush, as the icon wrote it: only one the cascade settled is still text. An
|
|
6404
|
+
// icon's solid brush holds whatever the host read, which is nothing for an unreadable colour.
|
|
5633
6405
|
if (typeof value !== 'string')
|
|
5634
|
-
return value;
|
|
5635
|
-
|
|
6406
|
+
return emptyBrush(value) ? undefined : value;
|
|
6407
|
+
if (value === 'none')
|
|
6408
|
+
return null;
|
|
6409
|
+
// A colour the host cannot read is dropped, as a browser drops it: a solid brush with no
|
|
6410
|
+
// colour in it stops the app.
|
|
6411
|
+
const payload = this.color(value);
|
|
6412
|
+
return payload == null ? undefined : { type: 0, payload };
|
|
5636
6413
|
}
|
|
5637
6414
|
/**
|
|
5638
6415
|
* The host's `processColor`, and the one place a platform colour becomes one.
|
|
@@ -5973,7 +6750,7 @@ export class Engine {
|
|
|
5973
6750
|
for (let n = target; n; n = DIRECT_EVENTS.has(topLevelType) ? null : n.parent) {
|
|
5974
6751
|
matched += n.listeners?.get(topLevelType)?.size ?? 0;
|
|
5975
6752
|
}
|
|
5976
|
-
console.log('[event]', topLevelType, target?.
|
|
6753
|
+
console.log('[event]', topLevelType, target?.tag ?? '(no handle)', matched);
|
|
5977
6754
|
}
|
|
5978
6755
|
/**
|
|
5979
6756
|
* `:focus` follows the native events rather than being inferred from a tap, so it is right for a
|
|
@@ -6032,4 +6809,17 @@ export class Engine {
|
|
|
6032
6809
|
this.removeChild(child.parent, child);
|
|
6033
6810
|
}
|
|
6034
6811
|
}
|
|
6812
|
+
/**
|
|
6813
|
+
* Clear the marks that lead a commit down to a change, under a node no commit goes into: one with
|
|
6814
|
+
* no box. Left set, a later change under it stops climbing at them, as it stops at any mark already
|
|
6815
|
+
* there, and never reaches the root: nothing is committed, and an ancestor's `:has()` reading into
|
|
6816
|
+
* it is not asked again. What is under it has no views, so it is reconciled whole when it is shown.
|
|
6817
|
+
*/
|
|
6818
|
+
function settlePaths(node) {
|
|
6819
|
+
if (!node.subtreeDirty)
|
|
6820
|
+
return;
|
|
6821
|
+
node.subtreeDirty = false;
|
|
6822
|
+
for (const child of node.children)
|
|
6823
|
+
settlePaths(child);
|
|
6824
|
+
}
|
|
6035
6825
|
//# sourceMappingURL=engine.js.map
|