@ng-native/fabric 0.12.0 → 0.14.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 +16 -1
- package/dist/css.js +49 -8
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +143 -8
- package/dist/engine.js +701 -82
- package/dist/engine.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 +2 -2
- package/dist/index.js +1 -1
- package/dist/index.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/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, 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
|
/**
|
|
@@ -854,6 +854,133 @@ function stillTouched(props) {
|
|
|
854
854
|
props['opacity'] = TOUCHED_ALPHA;
|
|
855
855
|
return props;
|
|
856
856
|
}
|
|
857
|
+
/** Every prop `setOwned` can set, as a payload that changes them all. */
|
|
858
|
+
const RESET_OWNED = { opacity: undefined, transform: undefined };
|
|
859
|
+
/** As Animated does: a view native sets values on is kept, or there is no view to set them on. */
|
|
860
|
+
function keptForNative(node, props) {
|
|
861
|
+
if (node.owned && props['collapsable'] === undefined)
|
|
862
|
+
props['collapsable'] = false;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* How a paragraph is truncated, which native does by its props: one the engine made around a
|
|
866
|
+
* view's own words is truncated as the view says, as a browser truncates a block's own text.
|
|
867
|
+
*/
|
|
868
|
+
const TRUNCATION = ['numberOfLines', 'ellipsizeMode'];
|
|
869
|
+
/**
|
|
870
|
+
* The truncation each paragraph made around a view's words was merged with, as the view said it.
|
|
871
|
+
* Not what it was committed with, which has the paragraph's own defaults in it.
|
|
872
|
+
*/
|
|
873
|
+
const truncatedBy = new WeakMap();
|
|
874
|
+
/** What a view says of how its own words are truncated. */
|
|
875
|
+
const truncationOf = (parent) => TRUNCATION.map((key) => ownLayout(parent, key) ?? null);
|
|
876
|
+
/** Whether a paragraph made around a view's words is truncated other than the view now says. */
|
|
877
|
+
function truncationMoved(box, parent) {
|
|
878
|
+
const merged = truncatedBy.get(box);
|
|
879
|
+
if (!merged)
|
|
880
|
+
return false;
|
|
881
|
+
return truncationOf(parent).some((said, at) => said !== merged[at]);
|
|
882
|
+
}
|
|
883
|
+
function truncatedAs(box, props) {
|
|
884
|
+
if (!box.anonymous || !box.parent)
|
|
885
|
+
return;
|
|
886
|
+
const said = truncationOf(box.parent);
|
|
887
|
+
truncatedBy.set(box, said);
|
|
888
|
+
TRUNCATION.forEach((key, at) => {
|
|
889
|
+
if (said[at] !== null)
|
|
890
|
+
props[key] = said[at];
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Stretch the lines of a wrapping flex container that says nothing of `align-content`, as CSS
|
|
895
|
+
* starts them (`normal`, which is `stretch` in a flex container). Yoga starts them at
|
|
896
|
+
* `flex-start`, as tall as what is in them, where an item that stretches across its line has
|
|
897
|
+
* no size. A container that does not wrap has one line, which Yoga stretches already.
|
|
898
|
+
*/
|
|
899
|
+
function wrappedLinesStretch(props) {
|
|
900
|
+
const wraps = props['flexWrap'] === 'wrap' || props['flexWrap'] === 'wrap-reverse';
|
|
901
|
+
if (wraps && props['alignContent'] == null)
|
|
902
|
+
props['alignContent'] = 'stretch';
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* A flex item that stretches across its line, with its size across the line `auto`, as CSS has
|
|
906
|
+
* it: as wide as a column, or as tall as a row, where Yoga takes the ratio over the stretch. In a
|
|
907
|
+
* column, a height given leaves the ratio nothing to work out. In a row, the line is as tall as
|
|
908
|
+
* the ratio makes the item from its width, at least, which is then the item's least height.
|
|
909
|
+
*
|
|
910
|
+
* ponytail: a width in points that does not grow, in a row, with any least or most height in
|
|
911
|
+
* 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.
|
|
912
|
+
*/
|
|
913
|
+
function stretchedOverRatio(node, props) {
|
|
914
|
+
if (!isSet(props['aspectRatio']) || props['position'] === 'absolute')
|
|
915
|
+
return;
|
|
916
|
+
if (viewNameOf(node) !== DEFAULT_VIEW)
|
|
917
|
+
return;
|
|
918
|
+
const parent = layoutParent(node);
|
|
919
|
+
if (!parent)
|
|
920
|
+
return;
|
|
921
|
+
const container = containerOf(parent);
|
|
922
|
+
// Remembered on the container, so the item is merged again when its direction changes.
|
|
923
|
+
parent.fitContainer = container;
|
|
924
|
+
const [direction, align] = container.split(' ');
|
|
925
|
+
if (align !== 'stretch' || !stretchesAcross(props, direction === 'row'))
|
|
926
|
+
return;
|
|
927
|
+
if (direction === 'row')
|
|
928
|
+
ratioAsLeast(props);
|
|
929
|
+
else if (isSet(props['height']) && props['height'] !== 'auto')
|
|
930
|
+
delete props['aspectRatio'];
|
|
931
|
+
}
|
|
932
|
+
/** Whether an item stretches across its line: its own size and margins across it are `auto`. */
|
|
933
|
+
function stretchesAcross(props, row) {
|
|
934
|
+
if (!STRETCHES.has(props['alignSelf']))
|
|
935
|
+
return false;
|
|
936
|
+
const size = row ? 'height' : 'width';
|
|
937
|
+
if (isSet(props[size]) && props[size] !== 'auto')
|
|
938
|
+
return false;
|
|
939
|
+
const sides = row
|
|
940
|
+
? MARGINS_ACROSS_ROW
|
|
941
|
+
: props['direction'] === 'rtl'
|
|
942
|
+
? MARGINS_ACROSS_COLUMN_RTL
|
|
943
|
+
: MARGINS_ACROSS_COLUMN;
|
|
944
|
+
return sides.every((keys) => keys.map((key) => props[key]).find(isSet) !== 'auto');
|
|
945
|
+
}
|
|
946
|
+
/** The margins across a line, a side each, as Yoga reads them: the first set wins. */
|
|
947
|
+
const MARGINS_ACROSS_ROW = [
|
|
948
|
+
['marginTop', 'marginVertical', 'margin'],
|
|
949
|
+
['marginBottom', 'marginVertical', 'margin'],
|
|
950
|
+
];
|
|
951
|
+
const MARGINS_ACROSS_COLUMN = [
|
|
952
|
+
['marginStart', 'marginLeft', 'marginHorizontal', 'margin'],
|
|
953
|
+
['marginEnd', 'marginRight', 'marginHorizontal', 'margin'],
|
|
954
|
+
];
|
|
955
|
+
/** Right to left, where the start is the right side. */
|
|
956
|
+
const MARGINS_ACROSS_COLUMN_RTL = [
|
|
957
|
+
['marginEnd', 'marginLeft', 'marginHorizontal', 'margin'],
|
|
958
|
+
['marginStart', 'marginRight', 'marginHorizontal', 'margin'],
|
|
959
|
+
];
|
|
960
|
+
/** In a row, the height a ratio gives a width in points, as the item's least height. */
|
|
961
|
+
function ratioAsLeast(props) {
|
|
962
|
+
const [width, ratio] = [props['width'], props['aspectRatio']];
|
|
963
|
+
if (typeof width !== 'number' || typeof ratio !== 'number' || ratio <= 0)
|
|
964
|
+
return;
|
|
965
|
+
const [least, most] = [props['minHeight'], props['maxHeight']];
|
|
966
|
+
const inPoints = (bound) => !isSet(bound) || typeof bound === 'number';
|
|
967
|
+
if (props['boxSizing'] === 'content-box' || !inPoints(least) || !inPoints(most))
|
|
968
|
+
return;
|
|
969
|
+
if (sizedAlong(props))
|
|
970
|
+
return;
|
|
971
|
+
props['minHeight'] = heldTo(width / ratio, least, most);
|
|
972
|
+
delete props['aspectRatio'];
|
|
973
|
+
}
|
|
974
|
+
/** Whether an item's width is not what it is along its line: it has a basis, or it grows. */
|
|
975
|
+
function sizedAlong(props) {
|
|
976
|
+
const basis = props['flexBasis'];
|
|
977
|
+
return (isSet(basis) && basis !== 'auto') || Number(props['flexGrow'] ?? props['flex'] ?? 0) > 0;
|
|
978
|
+
}
|
|
979
|
+
/** A height held to the least and most given, which CSS keeps: the least wins over the most. */
|
|
980
|
+
function heldTo(height, least, most) {
|
|
981
|
+
const under = typeof most === 'number' ? Math.min(height, most) : height;
|
|
982
|
+
return typeof least === 'number' ? Math.max(under, least) : under;
|
|
983
|
+
}
|
|
857
984
|
/**
|
|
858
985
|
* Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
|
|
859
986
|
* for a size that is `auto`; Yoga takes it over one of two that are given, the height in a
|
|
@@ -879,6 +1006,20 @@ function colourless(props, side, flipped) {
|
|
|
879
1006
|
* Each side a border has: its width, its colour, and the logical colour React Native takes for
|
|
880
1007
|
* it too, read left to right and, `flipped`, right to left.
|
|
881
1008
|
*/
|
|
1009
|
+
const BORDER_DRAWN = /^border\w*(Width|Style)$/;
|
|
1010
|
+
const BORDER_COLOUR = /^border\w*Color$/;
|
|
1011
|
+
/**
|
|
1012
|
+
* Send a border's colours again with an update that gives it a width or a style: Android draws
|
|
1013
|
+
* the border black when that update does not carry its colour, though it was given one before.
|
|
1014
|
+
* A box made without a border and given one later, a file attachment that is done, has one.
|
|
1015
|
+
*/
|
|
1016
|
+
function borderColoursAgain(payload, props) {
|
|
1017
|
+
if (!Object.keys(payload).some((key) => BORDER_DRAWN.test(key)))
|
|
1018
|
+
return;
|
|
1019
|
+
for (const key in props)
|
|
1020
|
+
if (BORDER_COLOUR.test(key) && !(key in payload))
|
|
1021
|
+
payload[key] = props[key];
|
|
1022
|
+
}
|
|
882
1023
|
const BORDER_SIDES = [
|
|
883
1024
|
['Top', 'BlockStart', 'BlockStart'],
|
|
884
1025
|
['Right', 'End', 'Start'],
|
|
@@ -970,14 +1111,63 @@ function lineTall(props, run, fontScale) {
|
|
|
970
1111
|
}
|
|
971
1112
|
/** The points of padding and border above and below a view's content, where each is a length. */
|
|
972
1113
|
function aroundALine(props) {
|
|
973
|
-
|
|
1114
|
+
return around(props, 'Height');
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* The props each side's padding and border are read from, across a height and across a width,
|
|
1118
|
+
* the first set winning, as React Native and then Yoga read them: an alias of React Native's over
|
|
1119
|
+
* Yoga's own, a logical side over the physical one, and the start on the left unless the box is
|
|
1120
|
+
* right to left. A block's start and end are under the top and bottom.
|
|
1121
|
+
*/
|
|
1122
|
+
function sides(props, size) {
|
|
1123
|
+
if (size === 'Height') {
|
|
1124
|
+
const block = ['paddingBlock', 'paddingVertical', 'padding'];
|
|
1125
|
+
return [
|
|
1126
|
+
['paddingTop', 'paddingBlockStart', ...block],
|
|
1127
|
+
['borderTopWidth', 'borderWidth'],
|
|
1128
|
+
['paddingBottom', 'paddingBlockEnd', ...block],
|
|
1129
|
+
['borderBottomWidth', 'borderWidth'],
|
|
1130
|
+
];
|
|
1131
|
+
}
|
|
1132
|
+
const rtl = props['direction'] === 'rtl';
|
|
1133
|
+
const inline = ['paddingInline', 'paddingHorizontal', 'padding'];
|
|
1134
|
+
return ['Left', 'Right'].flatMap((physical, at) => {
|
|
1135
|
+
const logical = (at === 0) !== rtl ? 'Start' : 'End';
|
|
1136
|
+
return [
|
|
1137
|
+
[`paddingInline${logical}`, `padding${logical}`, `padding${physical}`, ...inline],
|
|
1138
|
+
[`border${logical}Width`, `border${physical}Width`, 'borderWidth'],
|
|
1139
|
+
];
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
/** The points of padding and border across a view's height or width, where each is a length. */
|
|
1143
|
+
function around(props, size) {
|
|
1144
|
+
let total = 0;
|
|
1145
|
+
for (const keys of sides(props, size)) {
|
|
974
1146
|
const found = keys.map((key) => props[key]).find((value) => value != null);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1147
|
+
if (typeof found === 'number')
|
|
1148
|
+
total += found;
|
|
1149
|
+
}
|
|
1150
|
+
return total;
|
|
1151
|
+
}
|
|
1152
|
+
/** The most and the least of each size, by name: asked of every node merged. */
|
|
1153
|
+
const MOST = { Height: 'maxHeight', Width: 'maxWidth' };
|
|
1154
|
+
const LEAST = { Height: 'minHeight', Width: 'minWidth' };
|
|
1155
|
+
/**
|
|
1156
|
+
* A most size under the least: under a `min-height`, or a border box's padding and border, which
|
|
1157
|
+
* CSS lets win (CSS 2.2, 10.7). React Native's Yoga holds the box to its most, and the box around
|
|
1158
|
+
* it to that, so the most is raised to the least. A most and a least in points: a share on either
|
|
1159
|
+
* side is left to Yoga.
|
|
1160
|
+
*/
|
|
1161
|
+
function leastWins(props, size) {
|
|
1162
|
+
const most = props[MOST[size]];
|
|
1163
|
+
if (typeof most !== 'number')
|
|
1164
|
+
return;
|
|
1165
|
+
const written = props[LEAST[size]];
|
|
1166
|
+
let least = typeof written === 'number' ? written : 0;
|
|
1167
|
+
if (props['boxSizing'] !== 'content-box')
|
|
1168
|
+
least = Math.max(least, around(props, size));
|
|
1169
|
+
if (most < least)
|
|
1170
|
+
props[MOST[size]] = least;
|
|
981
1171
|
}
|
|
982
1172
|
/**
|
|
983
1173
|
* Where a line may break: at a space, after a hyphen, and anywhere in a script written without
|
|
@@ -1096,8 +1286,18 @@ export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
|
1096
1286
|
LOOSE_TEXT[name] = options.looseText;
|
|
1097
1287
|
else
|
|
1098
1288
|
delete LOOSE_TEXT[name];
|
|
1289
|
+
if (options?.fontNames)
|
|
1290
|
+
FONT_NAMES[name] = options.fontNames;
|
|
1291
|
+
else
|
|
1292
|
+
delete FONT_NAMES[name];
|
|
1099
1293
|
}
|
|
1100
1294
|
}
|
|
1295
|
+
/** How each view that names fonts in its own props rewrites them. See `ViewNameOptions`. */
|
|
1296
|
+
const FONT_NAMES = {};
|
|
1297
|
+
/** Rewrites the fonts a view's own props name, for a view that says how. See `ViewNameOptions`. */
|
|
1298
|
+
function renameFonts(viewName, props, postScriptName) {
|
|
1299
|
+
FONT_NAMES[viewName]?.(props, postScriptName);
|
|
1300
|
+
}
|
|
1101
1301
|
/** The element loose text in a view is drawn by, for the views that say. See `ViewNameOptions`. */
|
|
1102
1302
|
const LOOSE_TEXT = {};
|
|
1103
1303
|
/** The registered names that give way to a component's own host. See `ViewNameOptions`. */
|
|
@@ -1245,6 +1445,10 @@ const SCROLL_VIEWS = new Set(['ScrollView', 'AndroidHorizontalScrollView']);
|
|
|
1245
1445
|
const HOISTS = {};
|
|
1246
1446
|
/** The `ancestorName`s, so a commit asks only those nodes what to hoist into them. */
|
|
1247
1447
|
const HOIST_TARGETS = new Set();
|
|
1448
|
+
/** The `ancestorName`s that commit only what is hoisted into them. See `HoistOptions.only`. */
|
|
1449
|
+
const HOIST_ONLY = new Set();
|
|
1450
|
+
/** Element name -> the ancestor names it never hoists past. See `HoistOptions.stopAt`. */
|
|
1451
|
+
const HOIST_STOPS = {};
|
|
1248
1452
|
/**
|
|
1249
1453
|
* Commit every `elementName` as a direct child of its nearest `ancestorName`, wherever the
|
|
1250
1454
|
* template put it, e.g. `registerHoist('native-header', 'screen')`.
|
|
@@ -1263,6 +1467,12 @@ export function registerHoist(elementName, ancestorName, options = {}) {
|
|
|
1263
1467
|
HOIST_TARGETS.add(ancestorName);
|
|
1264
1468
|
if (options.standIn)
|
|
1265
1469
|
STAND_INS[elementName] = options.standIn;
|
|
1470
|
+
if (options.only)
|
|
1471
|
+
HOIST_ONLY.add(ancestorName);
|
|
1472
|
+
if (options.stopAt)
|
|
1473
|
+
HOIST_STOPS[elementName] = new Set(options.stopAt);
|
|
1474
|
+
else
|
|
1475
|
+
delete HOIST_STOPS[elementName];
|
|
1266
1476
|
}
|
|
1267
1477
|
/** Element name -> the props it leaves behind in its ancestor. See `HoistOptions.standIn`. */
|
|
1268
1478
|
const STAND_INS = {};
|
|
@@ -1295,10 +1505,12 @@ function paragraphText(node) {
|
|
|
1295
1505
|
// Text written straight into a view is a paragraph of its own, and all of it.
|
|
1296
1506
|
if (root === null || !isTextElement(root))
|
|
1297
1507
|
return node.box ? trimEnds(node.text) : node.text;
|
|
1508
|
+
// A text element a view stands in for, which places what it holds: no paragraph runs past it.
|
|
1509
|
+
const inParagraph = (above) => isTextElement(above) && !aligningText(above);
|
|
1298
1510
|
// The commonest paragraph by far: one run and nothing else, so it is both the first and last.
|
|
1299
|
-
if (root.children.length === 1 && !
|
|
1511
|
+
if (root.children.length === 1 && !inParagraph(root.parent))
|
|
1300
1512
|
return trimEnds(node.text);
|
|
1301
|
-
while (
|
|
1513
|
+
while (inParagraph(root.parent))
|
|
1302
1514
|
root = root.parent;
|
|
1303
1515
|
const runs = [];
|
|
1304
1516
|
const collect = (from) => {
|
|
@@ -1326,12 +1538,12 @@ function paragraphText(node) {
|
|
|
1326
1538
|
function isTextElement(node) {
|
|
1327
1539
|
if (node === null || node.kind !== 'element')
|
|
1328
1540
|
return false;
|
|
1329
|
-
if (node.
|
|
1541
|
+
if (node.tag === 'text')
|
|
1330
1542
|
return true;
|
|
1331
1543
|
// One lookup settles nearly every other element, which is a view.
|
|
1332
|
-
if (registeredViewName(node.
|
|
1544
|
+
if (registeredViewName(node.tag) !== PARAGRAPH)
|
|
1333
1545
|
return false;
|
|
1334
|
-
return !TEXT_ELEMENTS.has(node.
|
|
1546
|
+
return !TEXT_ELEMENTS.has(node.tag) || holdsOnlyText(node);
|
|
1335
1547
|
}
|
|
1336
1548
|
/** Whether a text element holds text and nothing else. One that holds nothing holds no text. */
|
|
1337
1549
|
function holdsOnlyText(node) {
|
|
@@ -1351,25 +1563,48 @@ const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
|
1351
1563
|
* alignment moves, so such an element commits as a view, and its text as the paragraph any view's
|
|
1352
1564
|
* loose text is. Read from the node's own style on each reconcile, which a change to it brings
|
|
1353
1565
|
* 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
|
-
*
|
|
1566
|
+
* span, which is then the item placed. So is a column of text elements, each an item under the
|
|
1567
|
+
* last. A `<text>` is a paragraph whatever its style, and any other element holding more than
|
|
1568
|
+
* the one run is left as it was.
|
|
1356
1569
|
*/
|
|
1357
1570
|
function aligningText(node) {
|
|
1358
|
-
if (node.kind !== 'element' || node.
|
|
1571
|
+
if (node.kind !== 'element' || node.tag === 'text' || !TEXT_ELEMENTS.has(node.tag)) {
|
|
1359
1572
|
return false;
|
|
1360
1573
|
}
|
|
1361
|
-
|
|
1574
|
+
const stacked = stacksRuns(node);
|
|
1575
|
+
if (!stacked && (node.children.length !== 1 || !oneRun(node.children[0])))
|
|
1362
1576
|
return false;
|
|
1363
1577
|
// Under a text element it is a span of that paragraph, unless that one is a view placing it.
|
|
1364
1578
|
if (isTextElement(node.parent) && !aligningText(node.parent))
|
|
1365
1579
|
return false;
|
|
1580
|
+
return placesByFlex(node, stacked);
|
|
1581
|
+
}
|
|
1582
|
+
/** Whether a flex container's own style places the run it holds, or stacks the runs it holds. */
|
|
1583
|
+
function placesByFlex(node, stacked) {
|
|
1366
1584
|
if (ownLayout(node, 'display') !== 'flex')
|
|
1367
1585
|
return false;
|
|
1586
|
+
// A column of them: each a line of its own, where one paragraph would run them on.
|
|
1587
|
+
if (stacked)
|
|
1588
|
+
return String(ownLayout(node, 'flexDirection') ?? 'column').startsWith('column');
|
|
1368
1589
|
return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
|
|
1369
1590
|
ALIGNS.has(ownLayout(node, 'justifyContent')));
|
|
1370
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Whether a text element holds two or more text elements and nothing else but whitespace and
|
|
1594
|
+
* the anchors control flow leaves: a label around a name and a hint, which a flex column has one under the other.
|
|
1595
|
+
*/
|
|
1596
|
+
function stacksRuns(node) {
|
|
1597
|
+
let runs = 0;
|
|
1598
|
+
for (const child of node.children) {
|
|
1599
|
+
if (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag))
|
|
1600
|
+
runs++;
|
|
1601
|
+
else if (child.kind === 'text' ? trimEnds(child.text) : child.kind !== 'anchor')
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
return runs > 1;
|
|
1605
|
+
}
|
|
1371
1606
|
/** 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.
|
|
1607
|
+
const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag));
|
|
1373
1608
|
/** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
|
|
1374
1609
|
const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
|
|
1375
1610
|
/**
|
|
@@ -1388,14 +1623,14 @@ export function viewNameOf(node) {
|
|
|
1388
1623
|
// Committing a nested `<text>` as another Paragraph gives a paragraph inside a paragraph,
|
|
1389
1624
|
// which lays out as a separate block instead of flowing inline.
|
|
1390
1625
|
// 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.
|
|
1626
|
+
if (node.componentHost && !node.nativeView && YIELDING.has(node.tag))
|
|
1392
1627
|
return DEFAULT_VIEW;
|
|
1393
|
-
const registered = registeredViewName(node.
|
|
1628
|
+
const registered = registeredViewName(node.tag);
|
|
1394
1629
|
// Nearly every element: a view of some kind, by its name alone.
|
|
1395
1630
|
if (registered !== PARAGRAPH)
|
|
1396
1631
|
return registered ?? DEFAULT_VIEW;
|
|
1397
1632
|
// One of HTML's text elements that holds something other than text: a view.
|
|
1398
|
-
if (node.
|
|
1633
|
+
if (node.tag !== 'text' && TEXT_ELEMENTS.has(node.tag) && !holdsOnlyText(node)) {
|
|
1399
1634
|
return DEFAULT_VIEW;
|
|
1400
1635
|
}
|
|
1401
1636
|
// The paragraph the engine makes for a view's loose text is under a view, whatever the name
|
|
@@ -1718,6 +1953,12 @@ function reportHover(host) {
|
|
|
1718
1953
|
hoverReported = true;
|
|
1719
1954
|
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
1955
|
}
|
|
1956
|
+
/** A bound value kept as a declaration the cascade settles: a `var()` or a relative length. */
|
|
1957
|
+
function boundFrom(prop, text, on) {
|
|
1958
|
+
return /^\s*var\(/i.test(text)
|
|
1959
|
+
? boundDeclaration(prop, tokenFromValue(text), on)
|
|
1960
|
+
: boundRelativeLength(prop, text, on);
|
|
1961
|
+
}
|
|
1721
1962
|
/** Whether a value is one of the engine's own nodes. */
|
|
1722
1963
|
export function isEngineNode(value) {
|
|
1723
1964
|
return value instanceof RetainedNode;
|
|
@@ -1777,17 +2018,18 @@ class RetainedNode {
|
|
|
1777
2018
|
transitions;
|
|
1778
2019
|
playing;
|
|
1779
2020
|
scrolled;
|
|
2021
|
+
owned;
|
|
1780
2022
|
kind;
|
|
1781
|
-
|
|
2023
|
+
tag;
|
|
1782
2024
|
/** See `HostNode.host`. */
|
|
1783
2025
|
host;
|
|
1784
|
-
constructor(kind,
|
|
2026
|
+
constructor(kind, tag, host) {
|
|
1785
2027
|
this.kind = kind;
|
|
1786
|
-
this.
|
|
2028
|
+
this.tag = tag;
|
|
1787
2029
|
this.host = host;
|
|
1788
2030
|
}
|
|
1789
2031
|
get tagName() {
|
|
1790
|
-
return this.
|
|
2032
|
+
return this.tag;
|
|
1791
2033
|
}
|
|
1792
2034
|
/** Angular's dev-mode assertion compares this against `Node.ELEMENT_NODE`. */
|
|
1793
2035
|
get nodeType() {
|
|
@@ -2214,7 +2456,7 @@ export class Engine {
|
|
|
2214
2456
|
this.processColor = options.processColor ?? ((value) => value);
|
|
2215
2457
|
this.resolveAssetSource = options.resolveAssetSource ?? ((value) => value);
|
|
2216
2458
|
this.scrollDriver = options.nativeAnimated
|
|
2217
|
-
? new NativeScrollDriver(options.nativeAnimated)
|
|
2459
|
+
? new NativeScrollDriver(options.nativeAnimated, (view, tag, keys) => this.touchedNatively(view, tag, keys))
|
|
2218
2460
|
: null;
|
|
2219
2461
|
// Eagerly, not on the first listener. Fabric holds exactly one event handler and
|
|
2220
2462
|
// `ReactFabric` installs React's at module scope, so until we claim it every native event
|
|
@@ -2262,15 +2504,29 @@ export class Engine {
|
|
|
2262
2504
|
*/
|
|
2263
2505
|
setActiveChain(from, active) {
|
|
2264
2506
|
let changed = false;
|
|
2265
|
-
|
|
2507
|
+
const set = (node) => {
|
|
2266
2508
|
if (node.active === active)
|
|
2267
|
-
|
|
2509
|
+
return;
|
|
2268
2510
|
node.active = active;
|
|
2269
2511
|
this.markActive(node);
|
|
2270
2512
|
changed = true;
|
|
2513
|
+
};
|
|
2514
|
+
for (let node = from; node; node = node.parent) {
|
|
2515
|
+
set(node);
|
|
2516
|
+
if (active)
|
|
2517
|
+
this.activeNodes.add(node);
|
|
2518
|
+
}
|
|
2519
|
+
if (!active) {
|
|
2520
|
+
// Every node the press set, too: one the pressed node was moved or removed from mid-press
|
|
2521
|
+
// is no longer above it.
|
|
2522
|
+
for (const node of this.activeNodes)
|
|
2523
|
+
set(node);
|
|
2524
|
+
this.activeNodes.clear();
|
|
2271
2525
|
}
|
|
2272
2526
|
return changed;
|
|
2273
2527
|
}
|
|
2528
|
+
/** The nodes a press has set `:active` on, to clear whatever happens to the tree meanwhile. */
|
|
2529
|
+
activeNodes = new Set();
|
|
2274
2530
|
/** Whether any sheet styles a pressed element, and each compound that asks about one from another. */
|
|
2275
2531
|
activeUse = {
|
|
2276
2532
|
own: false,
|
|
@@ -2649,8 +2905,38 @@ export class Engine {
|
|
|
2649
2905
|
return;
|
|
2650
2906
|
if (reach.own)
|
|
2651
2907
|
node.ownDirty = true;
|
|
2652
|
-
if (this.styles.reachesUnder(reach))
|
|
2908
|
+
if (this.styles.reachesUnder(reach)) {
|
|
2909
|
+
this.classesAbove += 1;
|
|
2653
2910
|
this.markUnder(node, reach);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* How many class changes have restyled what is under an element. `markUnder` walks the tree as
|
|
2915
|
+
* it is, so an element out of it then is not reached: one that comes back after any is matched
|
|
2916
|
+
* again, with all that is under it.
|
|
2917
|
+
*/
|
|
2918
|
+
classesAbove = 0;
|
|
2919
|
+
/**
|
|
2920
|
+
* The count when each element went out of the tree, for the ones still out. `detach` sets it
|
|
2921
|
+
* for a node that only moves too, so `markReturned` runs after it and clears that.
|
|
2922
|
+
*/
|
|
2923
|
+
outSince = new WeakMap();
|
|
2924
|
+
/** Have matched again a node coming back into the tree that missed a class change above it. */
|
|
2925
|
+
markReturned(child) {
|
|
2926
|
+
const since = this.outSince.get(child);
|
|
2927
|
+
if (since === undefined)
|
|
2928
|
+
return;
|
|
2929
|
+
this.outSince.delete(child);
|
|
2930
|
+
if (since !== this.classesAbove)
|
|
2931
|
+
this.markAllUnder(child);
|
|
2932
|
+
}
|
|
2933
|
+
markAllUnder(node) {
|
|
2934
|
+
if (node.kind !== 'element')
|
|
2935
|
+
return;
|
|
2936
|
+
node.ownDirty = true;
|
|
2937
|
+
this.markPath(node);
|
|
2938
|
+
for (const child of node.children)
|
|
2939
|
+
this.markAllUnder(child);
|
|
2654
2940
|
}
|
|
2655
2941
|
/**
|
|
2656
2942
|
* Have matched again each element under a node that one of `subjects`' rules could be for. One
|
|
@@ -2687,6 +2973,7 @@ export class Engine {
|
|
|
2687
2973
|
* the web's mount point has, unless the app's `:host` sizes it otherwise.
|
|
2688
2974
|
*/
|
|
2689
2975
|
setDefaultStyle(node, style) {
|
|
2976
|
+
this.noteEasing(style);
|
|
2690
2977
|
node.defaultStyle = style;
|
|
2691
2978
|
// Under what the cascade comes to, and read by no selector: nothing is matched again.
|
|
2692
2979
|
this.markProps(node, false);
|
|
@@ -2696,7 +2983,8 @@ export class Engine {
|
|
|
2696
2983
|
* another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
|
|
2697
2984
|
*/
|
|
2698
2985
|
setClasses(node, value) {
|
|
2699
|
-
|
|
2986
|
+
// By HTML's spaces, which are CSS's: a vertical tab or a no-break space is part of a name.
|
|
2987
|
+
const names = value.split(CSS_SPACE).filter(Boolean);
|
|
2700
2988
|
const before = node.classes;
|
|
2701
2989
|
node.classes = names.length ? new Set(names) : null;
|
|
2702
2990
|
// Each class that came or went: one in both changes nothing.
|
|
@@ -2719,6 +3007,7 @@ export class Engine {
|
|
|
2719
3007
|
appendChild(parent, child) {
|
|
2720
3008
|
this.markAdopted(child, parent);
|
|
2721
3009
|
this.detach(child);
|
|
3010
|
+
this.markReturned(child);
|
|
2722
3011
|
child.parent = parent;
|
|
2723
3012
|
parent.children.push(child);
|
|
2724
3013
|
if (child.dormantHoists)
|
|
@@ -2729,6 +3018,7 @@ export class Engine {
|
|
|
2729
3018
|
insertBefore(parent, child, ref) {
|
|
2730
3019
|
this.markAdopted(child, parent);
|
|
2731
3020
|
this.detach(child);
|
|
3021
|
+
this.markReturned(child);
|
|
2732
3022
|
child.parent = parent;
|
|
2733
3023
|
const at = ref ? parent.children.indexOf(ref) : -1;
|
|
2734
3024
|
if (at < 0)
|
|
@@ -2759,6 +3049,7 @@ export class Engine {
|
|
|
2759
3049
|
return;
|
|
2760
3050
|
target.children.splice(at, 1);
|
|
2761
3051
|
child.parent = null;
|
|
3052
|
+
this.outSince.set(child, this.classesAbove);
|
|
2762
3053
|
this.removedSinceCommit.add(child);
|
|
2763
3054
|
this.markStructure(target, child, at);
|
|
2764
3055
|
this.markTextContent(target, child);
|
|
@@ -2846,7 +3137,26 @@ export class Engine {
|
|
|
2846
3137
|
}
|
|
2847
3138
|
}
|
|
2848
3139
|
if (this.focusedNode && this.topOf(this.focusedNode) !== this.root)
|
|
2849
|
-
this.
|
|
3140
|
+
this.dropFocus();
|
|
3141
|
+
this.dropDetachedPress();
|
|
3142
|
+
}
|
|
3143
|
+
/** A pressed node out of the tree gets no release: native has no view to end the touch on. */
|
|
3144
|
+
dropDetachedPress() {
|
|
3145
|
+
const pressed = this.currentResponder;
|
|
3146
|
+
if (!pressed || this.topOf(pressed) === this.root)
|
|
3147
|
+
return;
|
|
3148
|
+
this.setActiveChain(pressed, false);
|
|
3149
|
+
this.currentResponder = null;
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Forget the focused node, which has left the tree without a blur: off the node as well as the
|
|
3153
|
+
* engine, or a `:focus` rule styles it again when it is put back.
|
|
3154
|
+
*/
|
|
3155
|
+
dropFocus() {
|
|
3156
|
+
const node = this.focusedNode;
|
|
3157
|
+
node.focused = false;
|
|
3158
|
+
this.markProps(node);
|
|
3159
|
+
this.focusedNode = null;
|
|
2850
3160
|
}
|
|
2851
3161
|
/** Forget the committed views of each node removed since the last commit that is still out. */
|
|
2852
3162
|
forgetDetached() {
|
|
@@ -2893,7 +3203,7 @@ export class Engine {
|
|
|
2893
3203
|
node.kept = undefined;
|
|
2894
3204
|
// A field popped with the keyboard up never sends its blur.
|
|
2895
3205
|
if (this.focusedNode === node)
|
|
2896
|
-
this.
|
|
3206
|
+
this.dropFocus();
|
|
2897
3207
|
}
|
|
2898
3208
|
parentNode(node) {
|
|
2899
3209
|
return node.parent;
|
|
@@ -2904,7 +3214,45 @@ export class Engine {
|
|
|
2904
3214
|
return null;
|
|
2905
3215
|
return siblings[siblings.indexOf(node) + 1] ?? null;
|
|
2906
3216
|
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Whether any declaration the engine was given transitions or animates: until one does, a
|
|
3219
|
+
* node's style is committed as it is, with nothing asked of what eases it. See `eased`.
|
|
3220
|
+
*/
|
|
3221
|
+
eases = false;
|
|
3222
|
+
noteEasing(declarations) {
|
|
3223
|
+
if (this.eases || !declarations)
|
|
3224
|
+
return;
|
|
3225
|
+
for (const key in declarations) {
|
|
3226
|
+
if (key.startsWith('$transition') || key.startsWith('$animation'))
|
|
3227
|
+
this.eases = true;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
/** A node's style as its transitions and animation have it: a declaration asks for one. */
|
|
3231
|
+
eased(node, style) {
|
|
3232
|
+
if (!this.eases)
|
|
3233
|
+
return style;
|
|
3234
|
+
return this.animated(node, this.transitioned(node, style));
|
|
3235
|
+
}
|
|
3236
|
+
/** Note a sheet any of whose rules transitions or animates: see `eases`. */
|
|
3237
|
+
noteSheetEasing(sheet) {
|
|
3238
|
+
for (const rule of this.eases ? [] : sheet.rules) {
|
|
3239
|
+
this.noteEasing(rule.declarations);
|
|
3240
|
+
this.noteEasing(rule.important);
|
|
3241
|
+
// One a token holds: `animation: var(--motion)`.
|
|
3242
|
+
for (const deferred of rule.deferred ?? []) {
|
|
3243
|
+
this.noteEasing(Object.fromEntries(deferred.props.map((key) => [key, true])));
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/** Note a prop, or a style set as one, that transitions or animates: see `eases`. */
|
|
3248
|
+
noteEasingProp(key, value) {
|
|
3249
|
+
this.noteEasing({ [key]: value });
|
|
3250
|
+
if (key === 'style' || key === STYLE_OVERRIDE)
|
|
3251
|
+
this.noteEasing(flattenStyle(value, {}));
|
|
3252
|
+
}
|
|
2907
3253
|
setProp(node, key, value) {
|
|
3254
|
+
if (!this.eases)
|
|
3255
|
+
this.noteEasingProp(key, value);
|
|
2908
3256
|
// A prop written again is a binding's, and is committed as any other is.
|
|
2909
3257
|
if (node.attributeOnly?.delete(key))
|
|
2910
3258
|
this.markProps(node, false);
|
|
@@ -2998,19 +3346,26 @@ export class Engine {
|
|
|
2998
3346
|
* has changed for the props diff to notice.
|
|
2999
3347
|
*/
|
|
3000
3348
|
styleChanged(node) {
|
|
3001
|
-
|
|
3349
|
+
if (!this.eases)
|
|
3350
|
+
this.noteEasingProp('style', node.props['style']);
|
|
3351
|
+
// As `setProp` restyles for a new style object: a child that takes one of its values, an
|
|
3352
|
+
// `inherit`, follows the one changed in place too.
|
|
3353
|
+
this.markProps(node, this.inlineReachesStyle(node) || node.styleCache?.heirs === true);
|
|
3002
3354
|
this.markParagraph(node);
|
|
3003
3355
|
}
|
|
3004
3356
|
/**
|
|
3005
|
-
* A bound declaration, `[style.color]`, whose value may be a `var()
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3357
|
+
* A bound declaration, `[style.color]`, whose value may be a `var()`, or a length in `em` or a
|
|
3358
|
+
* viewport unit. One that is, is kept as a declaration the cascade settles with the node's
|
|
3359
|
+
* tokens, font size and viewport, as a rule's is, and true is answered: the text is no value to
|
|
3360
|
+
* put in the node's style. A negative `line-height` is dropped, with a warning, and true is
|
|
3361
|
+
* answered too. Any other value forgets what was kept.
|
|
3008
3362
|
*/
|
|
3009
3363
|
setBoundStyle(node, prop, value) {
|
|
3010
|
-
const text = typeof value === 'string'
|
|
3011
|
-
const
|
|
3364
|
+
const text = typeof value === 'string' ? value : undefined;
|
|
3365
|
+
const refused = text !== undefined && this.refusedBound(node, prop, text);
|
|
3366
|
+
const bound = text === undefined || refused ? null : boundFrom(prop, text, node.tag);
|
|
3012
3367
|
if (!bound && !node.boundByProp?.has(prop))
|
|
3013
|
-
return
|
|
3368
|
+
return refused;
|
|
3014
3369
|
const byProp = (node.boundByProp ??= new Map());
|
|
3015
3370
|
if (bound)
|
|
3016
3371
|
byProp.set(prop, bound);
|
|
@@ -3018,7 +3373,14 @@ export class Engine {
|
|
|
3018
3373
|
byProp.delete(prop);
|
|
3019
3374
|
node.boundStyle = byProp.size ? [...byProp.values()] : null;
|
|
3020
3375
|
this.markProps(node);
|
|
3021
|
-
return bound !== null;
|
|
3376
|
+
return bound !== null || refused;
|
|
3377
|
+
}
|
|
3378
|
+
/** Whether a bound value is one CSS has none of, which is dropped with a warning. */
|
|
3379
|
+
refusedBound(node, prop, text) {
|
|
3380
|
+
if (!isNegativeLineHeight(prop, text))
|
|
3381
|
+
return false;
|
|
3382
|
+
this.reportDroppedDeclaration('line-height', text, node.tag, 'it is negative');
|
|
3383
|
+
return true;
|
|
3022
3384
|
}
|
|
3023
3385
|
/**
|
|
3024
3386
|
* Set a custom property on a node, `--tint` from `[style.--tint]`, or remove it with `null`.
|
|
@@ -3340,8 +3702,17 @@ export class Engine {
|
|
|
3340
3702
|
this.settleKeyframes();
|
|
3341
3703
|
if (facesAdded)
|
|
3342
3704
|
this.rematchFonts();
|
|
3705
|
+
this.blurHidden();
|
|
3343
3706
|
return true;
|
|
3344
3707
|
}
|
|
3708
|
+
/** A field went out of view focused, in the commit running: see `undisplayed`. */
|
|
3709
|
+
blurAfterCommit = false;
|
|
3710
|
+
blurHidden() {
|
|
3711
|
+
if (!this.blurAfterCommit)
|
|
3712
|
+
return;
|
|
3713
|
+
this.blurAfterCommit = false;
|
|
3714
|
+
this.setFocused(null);
|
|
3715
|
+
}
|
|
3345
3716
|
beforeCommit = null;
|
|
3346
3717
|
announceCommit() {
|
|
3347
3718
|
const run = this.beforeCommit;
|
|
@@ -3531,6 +3902,8 @@ export class Engine {
|
|
|
3531
3902
|
const resolved = props['pointerEvents'];
|
|
3532
3903
|
writeOwnProps(node.props, props, node.attributeOnly);
|
|
3533
3904
|
withTextContent(node, viewName, props);
|
|
3905
|
+
renameFonts(viewName, props, this.postScriptName);
|
|
3906
|
+
truncatedAs(node, props);
|
|
3534
3907
|
const cascaded = props['transform'];
|
|
3535
3908
|
const inline = inlineOf(node);
|
|
3536
3909
|
const style = boundTransform(node, Object.assign(props, inline), cascaded);
|
|
@@ -3556,7 +3929,7 @@ export class Engine {
|
|
|
3556
3929
|
this.capForFonts(node, style);
|
|
3557
3930
|
alignMultiline(viewName, style);
|
|
3558
3931
|
rowsTall(style, this.fontScale);
|
|
3559
|
-
const merged = composeTransform(node, this.
|
|
3932
|
+
const merged = composeTransform(node, this.eased(node, style));
|
|
3560
3933
|
// On iOS: Android's field says the baseline of the line it is sized and centred by.
|
|
3561
3934
|
// Still under the row that marked it: a row taken away is not committed again to unmark it.
|
|
3562
3935
|
const row = node.onBaseline;
|
|
@@ -3566,6 +3939,10 @@ export class Engine {
|
|
|
3566
3939
|
basisAsSize(node, merged);
|
|
3567
3940
|
// After the basis, which is a size given where it is committed as one.
|
|
3568
3941
|
ratioForAutoSize(merged);
|
|
3942
|
+
wrappedLinesStretch(merged);
|
|
3943
|
+
stretchedOverRatio(node, merged);
|
|
3944
|
+
leastWins(merged, 'Height');
|
|
3945
|
+
leastWins(merged, 'Width');
|
|
3569
3946
|
noOutline(merged);
|
|
3570
3947
|
this.borderInText(node, merged);
|
|
3571
3948
|
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
@@ -3577,6 +3954,7 @@ export class Engine {
|
|
|
3577
3954
|
this.movePaint(node, merged);
|
|
3578
3955
|
if (node.raised !== undefined)
|
|
3579
3956
|
merged['zIndex'] ??= node.raised;
|
|
3957
|
+
keptForNative(node, merged);
|
|
3580
3958
|
return merged;
|
|
3581
3959
|
}
|
|
3582
3960
|
/**
|
|
@@ -3770,6 +4148,7 @@ export class Engine {
|
|
|
3770
4148
|
if (!sheet || this.knownSheets.has(sheet))
|
|
3771
4149
|
return;
|
|
3772
4150
|
this.knownSheets.add(sheet);
|
|
4151
|
+
this.noteSheetEasing(sheet);
|
|
3773
4152
|
this.styles.noteSheet(sheet);
|
|
3774
4153
|
this.sheetOrder.push(sheet);
|
|
3775
4154
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
@@ -3800,6 +4179,7 @@ export class Engine {
|
|
|
3800
4179
|
this.knownSheets.delete(sheet);
|
|
3801
4180
|
if (next && !this.knownSheets.has(next)) {
|
|
3802
4181
|
this.knownSheets.add(next);
|
|
4182
|
+
this.noteSheetEasing(next);
|
|
3803
4183
|
this.styles.noteSheet(next);
|
|
3804
4184
|
this.sheetOrder[at] = next;
|
|
3805
4185
|
}
|
|
@@ -3842,6 +4222,7 @@ export class Engine {
|
|
|
3842
4222
|
}
|
|
3843
4223
|
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
3844
4224
|
fontFaces = new FontFaces();
|
|
4225
|
+
postScriptName = (name) => this.fontFaces.postScriptName(name);
|
|
3845
4226
|
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
3846
4227
|
facesAdded = false;
|
|
3847
4228
|
/**
|
|
@@ -3865,7 +4246,10 @@ export class Engine {
|
|
|
3865
4246
|
named(child.box);
|
|
3866
4247
|
if (child.kind !== 'element')
|
|
3867
4248
|
continue;
|
|
3868
|
-
|
|
4249
|
+
if (child.committed && FONT_NAMES[child.committed.viewName])
|
|
4250
|
+
this.markProps(child, false);
|
|
4251
|
+
else
|
|
4252
|
+
named(child);
|
|
3869
4253
|
this.markFontText(child);
|
|
3870
4254
|
}
|
|
3871
4255
|
}
|
|
@@ -4081,8 +4465,9 @@ export class Engine {
|
|
|
4081
4465
|
}
|
|
4082
4466
|
/**
|
|
4083
4467
|
* 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
|
-
*
|
|
4468
|
+
* style where it does not fill, is committed, and set on native where the animation set the
|
|
4469
|
+
* prop (see `setOwned`). The view is let go first, which puts nothing back on Fabric: it is
|
|
4470
|
+
* drawn where the animation left it until then.
|
|
4086
4471
|
*/
|
|
4087
4472
|
endedNatively(node, running) {
|
|
4088
4473
|
if (node.playing !== running || !running.native)
|
|
@@ -4091,9 +4476,62 @@ export class Engine {
|
|
|
4091
4476
|
running.native = undefined;
|
|
4092
4477
|
this.playedNatively.delete(node);
|
|
4093
4478
|
const end = running.start + spec.duration * (spec.iterations ?? 1);
|
|
4479
|
+
native.stop();
|
|
4094
4480
|
this.advancePlayer(node, Math.max(this.now(), end));
|
|
4095
4481
|
this.commit();
|
|
4096
|
-
|
|
4482
|
+
// The commit sets only what it changed, and the style the view rests at can be the frame it
|
|
4483
|
+
// was committed with as it played: native has drawn it elsewhere since.
|
|
4484
|
+
const committed = node.committed;
|
|
4485
|
+
if (node.owned && committed)
|
|
4486
|
+
this.setOwned(node, RESET_OWNED, committed.props);
|
|
4487
|
+
}
|
|
4488
|
+
/** A graph of native's was connected to a view: see `Touched`. */
|
|
4489
|
+
touchedNatively(node, tag, keys) {
|
|
4490
|
+
const taken = keys.filter((key) => key === 'opacity' || key === 'transform');
|
|
4491
|
+
if (!taken.length)
|
|
4492
|
+
return;
|
|
4493
|
+
if (node.owned?.tag !== tag)
|
|
4494
|
+
node.owned = { tag, keys: new Set() };
|
|
4495
|
+
for (const key of taken)
|
|
4496
|
+
node.owned.keys.add(key);
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* Set on native a change of a prop a graph of native's has set on the view before, while no
|
|
4500
|
+
* graph of its holds it: the commit cannot change it (see `Touched`). The payload is committed
|
|
4501
|
+
* as well, so the shadow node is as the view is drawn. A transform native cannot take as
|
|
4502
|
+
* numbers, a matrix, is left committed alone.
|
|
4503
|
+
*/
|
|
4504
|
+
setOwned(node, payload, props) {
|
|
4505
|
+
const owned = node.owned;
|
|
4506
|
+
const tag = node.committed?.tag;
|
|
4507
|
+
if (tag === undefined || owned.tag !== tag || !this.scrollDriver)
|
|
4508
|
+
return;
|
|
4509
|
+
const held = this.scrollDriver.held(node);
|
|
4510
|
+
const free = (key) => key in payload && owned.keys.has(key) && !held.has(key);
|
|
4511
|
+
const values = {};
|
|
4512
|
+
if (free('opacity')) {
|
|
4513
|
+
const opacity = props['opacity'];
|
|
4514
|
+
values.opacity = typeof opacity === 'number' ? opacity : 1;
|
|
4515
|
+
}
|
|
4516
|
+
if (free('transform')) {
|
|
4517
|
+
const transform = heldTransform(props['transform'], () => this.boxSize(node));
|
|
4518
|
+
if (transform)
|
|
4519
|
+
values.transform = transform;
|
|
4520
|
+
}
|
|
4521
|
+
if (values.opacity !== undefined || values.transform) {
|
|
4522
|
+
this.scrollDriver.set(node, tag, values);
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
/** The size a view is laid out at, where it has been. */
|
|
4526
|
+
boxSize(node) {
|
|
4527
|
+
let size;
|
|
4528
|
+
this.measure(node, (frame) => (size = frame));
|
|
4529
|
+
return size;
|
|
4530
|
+
}
|
|
4531
|
+
/** Forget the props native set on a node's view, which is gone or made again. */
|
|
4532
|
+
disown(node) {
|
|
4533
|
+
node.owned = undefined;
|
|
4534
|
+
this.scrollDriver?.forget(node);
|
|
4097
4535
|
}
|
|
4098
4536
|
/** Take an animation back from native, which stops it there. The caller plays it on, or not. */
|
|
4099
4537
|
stopNative(node, running) {
|
|
@@ -4369,8 +4807,9 @@ export class Engine {
|
|
|
4369
4807
|
this.takeBackEased(node, state, props, spec, now);
|
|
4370
4808
|
this.takeBackLaid(state, props, spec, now);
|
|
4371
4809
|
const started = [];
|
|
4810
|
+
const before = spec && this.paintedBefore(node);
|
|
4372
4811
|
for (const key of steppedKeys(props, spec, state)) {
|
|
4373
|
-
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
|
|
4812
|
+
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now, before)) {
|
|
4374
4813
|
this.running.add(node);
|
|
4375
4814
|
started.push(key);
|
|
4376
4815
|
this.emitTransition(node, 'topTransitionstart', key);
|
|
@@ -4381,6 +4820,29 @@ export class Engine {
|
|
|
4381
4820
|
node.transitions = undefined;
|
|
4382
4821
|
return props;
|
|
4383
4822
|
}
|
|
4823
|
+
/**
|
|
4824
|
+
* What a view was last painted with, by property, for a transition that arrives with the change
|
|
4825
|
+
* it eases: CSS takes the transition from the style after the change and eases from the value
|
|
4826
|
+
* before it. Nothing for a view nobody has seen yet, which starts where it is. The individual
|
|
4827
|
+
* transforms are read back from the parts the painted transform was put together from.
|
|
4828
|
+
*/
|
|
4829
|
+
paintedBefore(node) {
|
|
4830
|
+
const painted = node.committed?.props;
|
|
4831
|
+
// A view that paints its background on a child does not commit it as its own.
|
|
4832
|
+
if (!painted || node.paintsOn)
|
|
4833
|
+
return undefined;
|
|
4834
|
+
if (node.bornIn !== undefined && node.bornIn >= this.unseen)
|
|
4835
|
+
return undefined;
|
|
4836
|
+
const composed = node.composedTransform;
|
|
4837
|
+
if (!composed || painted['transform'] !== composed.value)
|
|
4838
|
+
return (key) => painted[key];
|
|
4839
|
+
return (key) => {
|
|
4840
|
+
if (key === 'transform')
|
|
4841
|
+
return composed.parts[3];
|
|
4842
|
+
const part = INDIVIDUAL_TRANSFORMS.indexOf(key);
|
|
4843
|
+
return part === -1 ? painted[key] : composed.parts[part];
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4384
4846
|
/** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
|
|
4385
4847
|
unseenTransitions(node) {
|
|
4386
4848
|
// One with no commit it was born in has been seen, whatever was committed since.
|
|
@@ -4439,6 +4901,13 @@ export class Engine {
|
|
|
4439
4901
|
const eased = state.get(key);
|
|
4440
4902
|
if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
|
|
4441
4903
|
continue;
|
|
4904
|
+
if (this.sentOnAgain(node, key, eased, now)) {
|
|
4905
|
+
// Where it ends, at once: see `sentOnAgain`.
|
|
4906
|
+
Object.assign(eased, { current: eased.to, done: true });
|
|
4907
|
+
this.emitTransition(node, 'topTransitionend', key);
|
|
4908
|
+
props[key] = eased.to;
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4442
4911
|
eased.laid = true;
|
|
4443
4912
|
this.laying.push({ node, key, eased });
|
|
4444
4913
|
}
|
|
@@ -4446,6 +4915,23 @@ export class Engine {
|
|
|
4446
4915
|
if (eased.laid && !eased.done)
|
|
4447
4916
|
props[key] = eased.to;
|
|
4448
4917
|
}
|
|
4918
|
+
/** When each size in transition was last started, and how many starts in a row came soon. */
|
|
4919
|
+
laidStarts = new WeakMap();
|
|
4920
|
+
/**
|
|
4921
|
+
* Whether a transition of a size is one sent on again and again, each before the last had
|
|
4922
|
+
* ended: a progress bar that steps every 50ms and eases each step over 150. Native moves the
|
|
4923
|
+
* view, and lays the screen out, every frame while a layout animation runs, so one started at
|
|
4924
|
+
* every step is one that never stops. The first, and one sent back before it ends, are
|
|
4925
|
+
* played; from the second sent on in a row, each is committed where it ends, and the steps
|
|
4926
|
+
* are the motion. Played again once the size has been left alone for as long as it lasts.
|
|
4927
|
+
*/
|
|
4928
|
+
sentOnAgain(node, key, eased, now) {
|
|
4929
|
+
const starts = this.laidStarts.get(node) ?? this.laidStarts.set(node, new Map()).get(node);
|
|
4930
|
+
const last = starts.get(key);
|
|
4931
|
+
const soon = last && now - last.at < eased.duration ? last.soon + 1 : 0;
|
|
4932
|
+
starts.set(key, { at: now, soon });
|
|
4933
|
+
return soon >= 2;
|
|
4934
|
+
}
|
|
4449
4935
|
/** The animations this commit lays out at their last frame, for native to move the views to. */
|
|
4450
4936
|
laidPlayed = [];
|
|
4451
4937
|
/**
|
|
@@ -4725,6 +5211,17 @@ export class Engine {
|
|
|
4725
5211
|
this.lettingGo.add(node);
|
|
4726
5212
|
return true;
|
|
4727
5213
|
}
|
|
5214
|
+
/** Whether a node, or an element it is in, is `display: none`. */
|
|
5215
|
+
boxless(node) {
|
|
5216
|
+
for (let at = node; at; at = at.parent) {
|
|
5217
|
+
if (at.kind !== 'element')
|
|
5218
|
+
continue;
|
|
5219
|
+
this.styles.resolve(at, this.styleEpoch);
|
|
5220
|
+
if (ownLayout(at, 'display') === 'none')
|
|
5221
|
+
return true;
|
|
5222
|
+
}
|
|
5223
|
+
return false;
|
|
5224
|
+
}
|
|
4728
5225
|
/** One `@keyframes` player, one frame on. */
|
|
4729
5226
|
advancePlayer(node, now) {
|
|
4730
5227
|
const running = node.playing;
|
|
@@ -4736,6 +5233,17 @@ export class Engine {
|
|
|
4736
5233
|
this.playing.delete(node);
|
|
4737
5234
|
return;
|
|
4738
5235
|
}
|
|
5236
|
+
// One with no box, or in a box with none, paints nothing, and an endless one would keep the
|
|
5237
|
+
// frame loop going for good. It is cancelled, as a browser cancels it, and starts again when
|
|
5238
|
+
// the node is shown, as it is made again.
|
|
5239
|
+
if (this.boxless(node)) {
|
|
5240
|
+
this.cancelPlaying(node);
|
|
5241
|
+
this.playing.delete(node);
|
|
5242
|
+
node.playing = undefined;
|
|
5243
|
+
// A commit, for its `animationcancel` to be sent after, and to drop its last frame.
|
|
5244
|
+
this.markProps(node, false);
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
4739
5247
|
// Frames that say nothing paint nothing: a keyframe's one declaration refused at build time
|
|
4740
5248
|
// leaves these. No frame of it is committed, and one that never ends has nothing left to do.
|
|
4741
5249
|
const idle = running.tracks.size === 0;
|
|
@@ -4996,6 +5504,10 @@ export class Engine {
|
|
|
4996
5504
|
/** What a clone sends: the props that changed, and a text input's font once it registered. */
|
|
4997
5505
|
propsPayload(node, previous, props) {
|
|
4998
5506
|
const payload = diffProps(previous, props);
|
|
5507
|
+
if (payload)
|
|
5508
|
+
borderColoursAgain(payload, props);
|
|
5509
|
+
if (payload && node.owned)
|
|
5510
|
+
this.setOwned(node, payload, props);
|
|
4999
5511
|
if (payload && this.fontsRefreshed && this.fontResends.delete(node)) {
|
|
5000
5512
|
payload['fontFamily'] = props['fontFamily'];
|
|
5001
5513
|
}
|
|
@@ -5024,10 +5536,17 @@ export class Engine {
|
|
|
5024
5536
|
*/
|
|
5025
5537
|
reconcileChildren(node, viewName, style) {
|
|
5026
5538
|
const context = style?.context;
|
|
5027
|
-
if (
|
|
5539
|
+
if (HOIST_ONLY.has(node.tag))
|
|
5540
|
+
return this.reconcileOnlyHoisted(node);
|
|
5541
|
+
if (HOIST_TARGETS.has(node.tag))
|
|
5028
5542
|
return this.reconcileHoistTarget(node, context);
|
|
5029
5543
|
const handles = [];
|
|
5030
5544
|
for (const child of node.children) {
|
|
5545
|
+
const clean = this.cleanUnder(node, child, context);
|
|
5546
|
+
if (clean) {
|
|
5547
|
+
handles.push(clean);
|
|
5548
|
+
continue;
|
|
5549
|
+
}
|
|
5031
5550
|
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
5032
5551
|
continue;
|
|
5033
5552
|
}
|
|
@@ -5071,7 +5590,7 @@ export class Engine {
|
|
|
5071
5590
|
return null;
|
|
5072
5591
|
let box = text.box;
|
|
5073
5592
|
// One made for another element, before the view was registered again, is done with.
|
|
5074
|
-
if (box && box.
|
|
5593
|
+
if (box && box.tag !== drawnBy) {
|
|
5075
5594
|
if (box.committed)
|
|
5076
5595
|
this.forgetCommitted(box);
|
|
5077
5596
|
box = undefined;
|
|
@@ -5083,6 +5602,9 @@ export class Engine {
|
|
|
5083
5602
|
text.box = box;
|
|
5084
5603
|
}
|
|
5085
5604
|
box.parent = parent;
|
|
5605
|
+
// Merged before the view is, so the view's truncation is read here: see `truncatedAs`.
|
|
5606
|
+
if (truncationMoved(box, parent))
|
|
5607
|
+
box.propsDirty = true;
|
|
5086
5608
|
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
5087
5609
|
if (!this.isClean(text)) {
|
|
5088
5610
|
box.subtreeDirty = true;
|
|
@@ -5143,10 +5665,12 @@ export class Engine {
|
|
|
5143
5665
|
return false;
|
|
5144
5666
|
if (child.committed) {
|
|
5145
5667
|
this.forgetCommitted(child);
|
|
5146
|
-
// A field in it has no view to send its blur from.
|
|
5668
|
+
// A field in it has no view to send its blur from. Blurred once this commit is done: a
|
|
5669
|
+
// `:focus` rule that hid it then no longer holds, and the commit that follows shows it.
|
|
5147
5670
|
if (isWithin(this.focusedNode, child))
|
|
5148
|
-
this.
|
|
5671
|
+
this.blurAfterCommit = true;
|
|
5149
5672
|
}
|
|
5673
|
+
settlePaths(child);
|
|
5150
5674
|
return true;
|
|
5151
5675
|
}
|
|
5152
5676
|
/**
|
|
@@ -5177,13 +5701,13 @@ export class Engine {
|
|
|
5177
5701
|
// which RNS keeps clear by never letting the header config be the first.
|
|
5178
5702
|
const direct = [];
|
|
5179
5703
|
for (const child of node.children) {
|
|
5180
|
-
|
|
5704
|
+
const taken = this.takenByTarget(node, child, context);
|
|
5705
|
+
if (taken === null)
|
|
5181
5706
|
continue;
|
|
5182
|
-
|
|
5183
|
-
if (this.hoisted.has(child))
|
|
5707
|
+
if (taken === 'direct')
|
|
5184
5708
|
direct.push(child);
|
|
5185
5709
|
else
|
|
5186
|
-
handles.push(
|
|
5710
|
+
handles.push(taken);
|
|
5187
5711
|
const moved = hoisted?.get(child);
|
|
5188
5712
|
for (const written of moved ?? []) {
|
|
5189
5713
|
if (!this.undisplayedUnder(node, written))
|
|
@@ -5196,6 +5720,33 @@ export class Engine {
|
|
|
5196
5720
|
this.standIn(node, landed, handles);
|
|
5197
5721
|
return handles;
|
|
5198
5722
|
}
|
|
5723
|
+
/**
|
|
5724
|
+
* `reconcileHoistTarget` for a node that commits only what is hoisted into it
|
|
5725
|
+
* (`HoistOptions.only`): those nodes, in the order they were written, and none of its own.
|
|
5726
|
+
*/
|
|
5727
|
+
reconcileOnlyHoisted(node) {
|
|
5728
|
+
const handles = [];
|
|
5729
|
+
const hoisted = this.hoistedInto(node);
|
|
5730
|
+
const landed = new Set();
|
|
5731
|
+
for (const child of node.children) {
|
|
5732
|
+
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
5733
|
+
continue;
|
|
5734
|
+
}
|
|
5735
|
+
if (this.hoisted.has(child)) {
|
|
5736
|
+
handles.push(this.land(node, child, landed));
|
|
5737
|
+
continue;
|
|
5738
|
+
}
|
|
5739
|
+
for (const written of hoisted?.get(child) ?? []) {
|
|
5740
|
+
if (!this.undisplayedUnder(node, written))
|
|
5741
|
+
handles.push(this.land(node, written, landed));
|
|
5742
|
+
}
|
|
5743
|
+
// Not committed, so the marks a change left on the way down to it are cleared here.
|
|
5744
|
+
settlePaths(child);
|
|
5745
|
+
}
|
|
5746
|
+
if (node.kept)
|
|
5747
|
+
this.standIn(node, landed, handles);
|
|
5748
|
+
return handles;
|
|
5749
|
+
}
|
|
5199
5750
|
/**
|
|
5200
5751
|
* A child's handle as a child of `parent`, created again when it was committed under another.
|
|
5201
5752
|
*
|
|
@@ -5228,8 +5779,19 @@ export class Engine {
|
|
|
5228
5779
|
forgetCommitted(node) {
|
|
5229
5780
|
node.committed = null;
|
|
5230
5781
|
node.committedUnder = null;
|
|
5782
|
+
// Another view, with another tag, which no graph has set anything on.
|
|
5783
|
+
if (node.owned)
|
|
5784
|
+
this.disown(node);
|
|
5231
5785
|
node.styleCommitted = null;
|
|
5232
5786
|
node.kept = undefined;
|
|
5787
|
+
// A scroll-driven animation is bound to native views by their tags: its own, and its scroll
|
|
5788
|
+
// view's. Made again, each has another, so what drives it starts again with the new ones.
|
|
5789
|
+
if (node.scrolled?.drive)
|
|
5790
|
+
this.stopScrolled(node);
|
|
5791
|
+
for (const follower of this.scrollTimelines.get(node) ?? []) {
|
|
5792
|
+
this.stopScrolled(follower);
|
|
5793
|
+
this.markProps(follower, false);
|
|
5794
|
+
}
|
|
5233
5795
|
if (node.box?.committed)
|
|
5234
5796
|
this.forgetCommitted(node.box);
|
|
5235
5797
|
for (const child of node.children) {
|
|
@@ -5246,9 +5808,9 @@ export class Engine {
|
|
|
5246
5808
|
* keeps for its name when it has one (`HoistOptions.standIn`), and keep the view it commits as.
|
|
5247
5809
|
*/
|
|
5248
5810
|
land(target, child, landed) {
|
|
5249
|
-
if (!STAND_INS[child.
|
|
5811
|
+
if (!STAND_INS[child.tag])
|
|
5250
5812
|
return this.reconcileUnder(target, child);
|
|
5251
|
-
const kept = target.kept?.get(child.
|
|
5813
|
+
const kept = target.kept?.get(child.tag);
|
|
5252
5814
|
if (kept && kept.node !== child && child.committed === null && !isWithin(kept.node, target)) {
|
|
5253
5815
|
// A node taken out without being destroyed would otherwise come back holding this tag too.
|
|
5254
5816
|
if (kept.node)
|
|
@@ -5258,8 +5820,8 @@ export class Engine {
|
|
|
5258
5820
|
}
|
|
5259
5821
|
const handle = this.reconcile(child);
|
|
5260
5822
|
child.committedUnder = target;
|
|
5261
|
-
(target.kept ??= new Map()).set(child.
|
|
5262
|
-
landed.add(child.
|
|
5823
|
+
(target.kept ??= new Map()).set(child.tag, { node: child, committed: child.committed });
|
|
5824
|
+
landed.add(child.tag);
|
|
5263
5825
|
return handle;
|
|
5264
5826
|
}
|
|
5265
5827
|
/**
|
|
@@ -5283,6 +5845,33 @@ export class Engine {
|
|
|
5283
5845
|
handles.push(kept.committed.handle);
|
|
5284
5846
|
}
|
|
5285
5847
|
}
|
|
5848
|
+
/**
|
|
5849
|
+
* What a hoist target takes of a child written in it: the child's handle, `direct` for one
|
|
5850
|
+
* hoisted into it, which goes after its siblings, or null for none.
|
|
5851
|
+
*/
|
|
5852
|
+
takenByTarget(node, child, context) {
|
|
5853
|
+
if (this.hoisted.has(child)) {
|
|
5854
|
+
if (this.committedElsewhere(child) || this.withheld(child))
|
|
5855
|
+
return null;
|
|
5856
|
+
return 'direct';
|
|
5857
|
+
}
|
|
5858
|
+
const clean = this.cleanUnder(node, child, context);
|
|
5859
|
+
if (clean)
|
|
5860
|
+
return clean;
|
|
5861
|
+
if (child.kind === 'anchor' || this.withheld(child))
|
|
5862
|
+
return null;
|
|
5863
|
+
return this.reconcileUnder(node, child, context);
|
|
5864
|
+
}
|
|
5865
|
+
/**
|
|
5866
|
+
* A clean element's handle under `parent`, before anything else is asked of it: one committed
|
|
5867
|
+
* there and unchanged since was displayed and in its place, and is still. The siblings of what
|
|
5868
|
+
* changed are most of what a commit steps over: a list one row of which ticks.
|
|
5869
|
+
*/
|
|
5870
|
+
cleanUnder(parent, child, context) {
|
|
5871
|
+
if (child.kind !== 'element' || child.committedUnder !== parent)
|
|
5872
|
+
return null;
|
|
5873
|
+
return this.stillCommitted(child, context);
|
|
5874
|
+
}
|
|
5286
5875
|
/** A clean child's committed handle, when its style still holds under its parent's. */
|
|
5287
5876
|
stillCommitted(child, context) {
|
|
5288
5877
|
const committed = child.committed;
|
|
@@ -5305,11 +5894,13 @@ export class Engine {
|
|
|
5305
5894
|
hoistedInto(node) {
|
|
5306
5895
|
let found = null;
|
|
5307
5896
|
for (const candidate of this.hoisted) {
|
|
5308
|
-
if (candidate.parent === node || HOISTS[candidate.
|
|
5897
|
+
if (candidate.parent === node || HOISTS[candidate.tag] !== node.tag)
|
|
5309
5898
|
continue;
|
|
5310
5899
|
let written = candidate;
|
|
5311
5900
|
let above = candidate.parent;
|
|
5312
|
-
while (above && above !== node && above.
|
|
5901
|
+
while (above && above !== node && above.tag !== node.tag) {
|
|
5902
|
+
if (HOIST_STOPS[candidate.tag]?.has(above.tag))
|
|
5903
|
+
break;
|
|
5313
5904
|
written = above;
|
|
5314
5905
|
above = above.parent;
|
|
5315
5906
|
}
|
|
@@ -5326,12 +5917,14 @@ export class Engine {
|
|
|
5326
5917
|
}
|
|
5327
5918
|
/** Whether a hoisted node commits further up than the parent it was written in. */
|
|
5328
5919
|
hoistsAbove(node) {
|
|
5329
|
-
const target = HOISTS[node.
|
|
5330
|
-
if (node.parent?.
|
|
5920
|
+
const target = HOISTS[node.tag];
|
|
5921
|
+
if (node.parent?.tag === target)
|
|
5331
5922
|
return false;
|
|
5332
|
-
for (let above = node.parent
|
|
5333
|
-
if (above.
|
|
5923
|
+
for (let above = node.parent; above; above = above.parent) {
|
|
5924
|
+
if (above.tag === target)
|
|
5334
5925
|
return true;
|
|
5926
|
+
if (HOIST_STOPS[node.tag]?.has(above.tag))
|
|
5927
|
+
return false;
|
|
5335
5928
|
}
|
|
5336
5929
|
return false;
|
|
5337
5930
|
}
|
|
@@ -5360,13 +5953,13 @@ export class Engine {
|
|
|
5360
5953
|
// The paragraph the engine wraps loose text in is its own: no template wrote it.
|
|
5361
5954
|
if (node.kind !== 'element' || node.claimed || node.anonymous)
|
|
5362
5955
|
return;
|
|
5363
|
-
if (!PRIMITIVE_NAMES.has(node.
|
|
5956
|
+
if (!PRIMITIVE_NAMES.has(node.tag))
|
|
5364
5957
|
return;
|
|
5365
|
-
if (this.reported.has(node.
|
|
5958
|
+
if (this.reported.has(node.tag))
|
|
5366
5959
|
return;
|
|
5367
|
-
this.reported.add(node.
|
|
5368
|
-
console.error(`[angular-native] <${node.
|
|
5369
|
-
`${className(node.
|
|
5960
|
+
this.reported.add(node.tag);
|
|
5961
|
+
console.error(`[angular-native] <${node.tag}> is used in a template that does not import ` +
|
|
5962
|
+
`${className(node.tag)}. Add it to the component's \`imports\` from ` +
|
|
5370
5963
|
`'@ng-native/components'; without it the element has none of the component's inputs ` +
|
|
5371
5964
|
`or behavior, so a prop written on it stays the text it was written as.`);
|
|
5372
5965
|
}
|
|
@@ -5389,6 +5982,19 @@ export class Engine {
|
|
|
5389
5982
|
` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
|
|
5390
5983
|
` :root or an ancestor, or give the var() a fallback.`);
|
|
5391
5984
|
}
|
|
5985
|
+
droppedDeclarations = new Set();
|
|
5986
|
+
/**
|
|
5987
|
+
* A declaration in a style, written or bound, that is no value its property takes, so it is
|
|
5988
|
+
* dropped as a browser drops it. Said once per element name and declaration, in development.
|
|
5989
|
+
*/
|
|
5990
|
+
reportDroppedDeclaration(prop, value, on, why) {
|
|
5991
|
+
const report = `${on} ${prop}: ${value}`;
|
|
5992
|
+
if (!this.dev || this.droppedDeclarations.has(report))
|
|
5993
|
+
return;
|
|
5994
|
+
this.droppedDeclarations.add(report);
|
|
5995
|
+
console.warn(`[angular-native] ${prop}: ${value}, in a style on <${on}>, is no value ${prop} takes:` +
|
|
5996
|
+
` ${why}. The declaration is dropped, as a browser drops it.`);
|
|
5997
|
+
}
|
|
5392
5998
|
unreadDisplays = new Set();
|
|
5393
5999
|
/**
|
|
5394
6000
|
* Said once per token and value: the compiler refuses `display: grid` written out, but a token
|
|
@@ -5404,23 +6010,23 @@ export class Engine {
|
|
|
5404
6010
|
` and contents, and reads block, inline, inline-block, flow-root and inline-flex as flex.`);
|
|
5405
6011
|
}
|
|
5406
6012
|
checkProps(node) {
|
|
5407
|
-
if (!node.claimed || !PRIMITIVE_NAMES.has(node.
|
|
6013
|
+
if (!node.claimed || !PRIMITIVE_NAMES.has(node.tag))
|
|
5408
6014
|
return;
|
|
5409
|
-
const declared = DECLARED_PROPS.get(node.
|
|
6015
|
+
const declared = DECLARED_PROPS.get(node.tag);
|
|
5410
6016
|
if (!declared)
|
|
5411
6017
|
return;
|
|
5412
6018
|
for (const key of Object.keys(node.props)) {
|
|
5413
6019
|
if (this.isDeclared(declared, key, node.props[key]))
|
|
5414
6020
|
continue;
|
|
5415
|
-
const report = `${node.
|
|
6021
|
+
const report = `${node.tag} ${key}`;
|
|
5416
6022
|
if (this.reported.has(report))
|
|
5417
6023
|
continue;
|
|
5418
6024
|
this.reported.add(report);
|
|
5419
6025
|
const meant = [...declared].find((name) => name.toLowerCase() === key.toLowerCase());
|
|
5420
|
-
console.warn(`[angular-native] <${node.
|
|
6026
|
+
console.warn(`[angular-native] <${node.tag}> has no prop '${key}', so native ignores it.` +
|
|
5421
6027
|
(meant ? ` Did you mean '${meant}'?` : '') +
|
|
5422
|
-
` Check the spelling against the ${className(node.
|
|
5423
|
-
` declareNativeProps('${node.
|
|
6028
|
+
` Check the spelling against the ${className(node.tag)} page, or declare it with` +
|
|
6029
|
+
` declareNativeProps('${node.tag}', ['${key}']) if the native view does read it.`);
|
|
5424
6030
|
}
|
|
5425
6031
|
}
|
|
5426
6032
|
isDeclared(declared, key, value) {
|
|
@@ -5460,12 +6066,12 @@ export class Engine {
|
|
|
5460
6066
|
* the trade for not holding a registry per component, and the typo is the case that matters.
|
|
5461
6067
|
*/
|
|
5462
6068
|
checkKnown(node) {
|
|
5463
|
-
if (node.kind !== 'element' || node.componentHost || node.
|
|
6069
|
+
if (node.kind !== 'element' || node.componentHost || node.tag in VIEW_NAMES)
|
|
5464
6070
|
return;
|
|
5465
|
-
if (this.declaredElements.has(node.
|
|
6071
|
+
if (this.declaredElements.has(node.tag) || this.reported.has(node.tag))
|
|
5466
6072
|
return;
|
|
5467
|
-
this.reported.add(node.
|
|
5468
|
-
console.error(`[angular-native] <${node.
|
|
6073
|
+
this.reported.add(node.tag);
|
|
6074
|
+
console.error(`[angular-native] <${node.tag}> is not a known element: no component, directive or ` +
|
|
5469
6075
|
`registered native view claims it, so it renders as an empty view. Check the spelling, ` +
|
|
5470
6076
|
`or import the component that owns it.`);
|
|
5471
6077
|
}
|
|
@@ -5605,7 +6211,7 @@ export class Engine {
|
|
|
5605
6211
|
*/
|
|
5606
6212
|
get focused() {
|
|
5607
6213
|
if (this.focusedNode && !this.isAttached(this.focusedNode))
|
|
5608
|
-
this.
|
|
6214
|
+
this.dropFocus();
|
|
5609
6215
|
return this.focusedNode;
|
|
5610
6216
|
}
|
|
5611
6217
|
/** Whether a node is still part of the tree, rather than in a subtree that was removed. */
|
|
@@ -5973,7 +6579,7 @@ export class Engine {
|
|
|
5973
6579
|
for (let n = target; n; n = DIRECT_EVENTS.has(topLevelType) ? null : n.parent) {
|
|
5974
6580
|
matched += n.listeners?.get(topLevelType)?.size ?? 0;
|
|
5975
6581
|
}
|
|
5976
|
-
console.log('[event]', topLevelType, target?.
|
|
6582
|
+
console.log('[event]', topLevelType, target?.tag ?? '(no handle)', matched);
|
|
5977
6583
|
}
|
|
5978
6584
|
/**
|
|
5979
6585
|
* `:focus` follows the native events rather than being inferred from a tap, so it is right for a
|
|
@@ -6032,4 +6638,17 @@ export class Engine {
|
|
|
6032
6638
|
this.removeChild(child.parent, child);
|
|
6033
6639
|
}
|
|
6034
6640
|
}
|
|
6641
|
+
/**
|
|
6642
|
+
* Clear the marks that lead a commit down to a change, under a node no commit goes into: one with
|
|
6643
|
+
* no box. Left set, a later change under it stops climbing at them, as it stops at any mark already
|
|
6644
|
+
* there, and never reaches the root: nothing is committed, and an ancestor's `:has()` reading into
|
|
6645
|
+
* it is not asked again. What is under it has no views, so it is reconciled whole when it is shown.
|
|
6646
|
+
*/
|
|
6647
|
+
function settlePaths(node) {
|
|
6648
|
+
if (!node.subtreeDirty)
|
|
6649
|
+
return;
|
|
6650
|
+
node.subtreeDirty = false;
|
|
6651
|
+
for (const child of node.children)
|
|
6652
|
+
settlePaths(child);
|
|
6653
|
+
}
|
|
6035
6654
|
//# sourceMappingURL=engine.js.map
|