@ng-native/fabric 0.3.0 → 0.4.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/aria-props.d.ts +16 -0
- package/dist/aria-props.js +78 -0
- package/dist/aria-props.js.map +1 -0
- package/dist/css.d.ts +73 -1
- package/dist/css.js +345 -31
- package/dist/css.js.map +1 -1
- package/dist/element-styles.d.ts +15 -0
- package/dist/element-styles.js +103 -0
- package/dist/element-styles.js.map +1 -0
- package/dist/engine.d.ts +147 -6
- package/dist/engine.js +602 -68
- package/dist/engine.js.map +1 -1
- package/dist/font-faces.d.ts +5 -2
- package/dist/font-faces.js +13 -2
- package/dist/font-faces.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.js +11 -10
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +7 -0
- package/dist/inline-token.js +50 -4
- package/dist/inline-token.js.map +1 -1
- package/dist/inline-transform.d.ts +5 -1
- package/dist/inline-transform.js +34 -6
- package/dist/inline-transform.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
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 { StyleResolver, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, inlineInherited, setsInherited, StyleResolver, } from "./css.js";
|
|
11
|
+
import { applyAria } from "./aria-props.js";
|
|
12
|
+
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
11
13
|
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
12
14
|
import { isFormsInput } from "./forms-inputs.js";
|
|
13
15
|
import { transformList } from "./inline-transform.js";
|
|
@@ -240,6 +242,34 @@ const VIEW_NAMES = {
|
|
|
240
242
|
* `@ng-native/expo`, and warning about it would be noise.
|
|
241
243
|
*/
|
|
242
244
|
const PRIMITIVE_NAMES = new Set(Object.keys(VIEW_NAMES));
|
|
245
|
+
/**
|
|
246
|
+
* HTML's text elements, which a template can write as it would for a browser.
|
|
247
|
+
*
|
|
248
|
+
* Each is text when it holds only text and other text elements, and a view when it holds anything
|
|
249
|
+
* else: a box inside a paragraph has no layout, so `<span>Inbox<view class="dot" /></span>` is a
|
|
250
|
+
* view with its text in a paragraph of its own. Decided as the element is committed, by
|
|
251
|
+
* `viewNameOf`. They have no component to import, so they are not `PRIMITIVE_NAMES`.
|
|
252
|
+
*/
|
|
253
|
+
const TEXT_ELEMENTS = new Set([
|
|
254
|
+
...['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'strong', 'b', 'em', 'i'],
|
|
255
|
+
...['u', 's', 'small', 'code', 'mark', 'abbr', 'cite', 'time'],
|
|
256
|
+
]);
|
|
257
|
+
/** HTML's layout elements, each a plain view, as a browser's are boxes with a name. */
|
|
258
|
+
const LAYOUT_ELEMENTS = [
|
|
259
|
+
...['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'ul', 'ol', 'li'],
|
|
260
|
+
];
|
|
261
|
+
for (const name of TEXT_ELEMENTS)
|
|
262
|
+
VIEW_NAMES[name] = 'Paragraph';
|
|
263
|
+
for (const name of LAYOUT_ELEMENTS)
|
|
264
|
+
VIEW_NAMES[name] = 'View';
|
|
265
|
+
/**
|
|
266
|
+
* What an HTML element has for its name alone, given to the node as it is made: a heading's role,
|
|
267
|
+
* which a binding can replace as it can any prop, and whether the engine has styles for it.
|
|
268
|
+
*/
|
|
269
|
+
const HTML_ELEMENTS = new Map([
|
|
270
|
+
...['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].map((name) => [name, { role: 'header' }]),
|
|
271
|
+
...[...STYLED_ELEMENTS].map((name) => [name, { styled: true }]),
|
|
272
|
+
]);
|
|
243
273
|
/**
|
|
244
274
|
* The props each host primitive's native view is sent, by element name, as the components
|
|
245
275
|
* declare them: every input the element's component has, and the props it writes that no input
|
|
@@ -274,6 +304,14 @@ export function claimHost(node) {
|
|
|
274
304
|
export function markComponentHost(node) {
|
|
275
305
|
node.componentHost = true;
|
|
276
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Say that a component's host is the native view its element name is registered as: a typed
|
|
309
|
+
* wrapper for the view, where a component of the app's own with the same selector is a plain view.
|
|
310
|
+
* Called from the component's constructor. See `ViewNameOptions.yieldsToComponents`.
|
|
311
|
+
*/
|
|
312
|
+
export function keepNativeView(node) {
|
|
313
|
+
node.nativeView = true;
|
|
314
|
+
}
|
|
277
315
|
/** `text-input` -> `TextInput`: how the components package names the class for an element. */
|
|
278
316
|
const className = (element) => element.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
279
317
|
/** Names Android registers differently. */
|
|
@@ -478,6 +516,12 @@ const MODAL_HOST = 'ModalHostView';
|
|
|
478
516
|
*/
|
|
479
517
|
export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
480
518
|
VIEW_NAMES[elementName] = viewName;
|
|
519
|
+
if (options?.yieldsToComponents)
|
|
520
|
+
YIELDING.add(elementName);
|
|
521
|
+
else
|
|
522
|
+
YIELDING.delete(elementName);
|
|
523
|
+
// A name the app registers is the app's: no longer decided by what the element holds.
|
|
524
|
+
TEXT_ELEMENTS.delete(elementName);
|
|
481
525
|
for (const name of typeof viewName === 'string' ? [viewName] : [viewName.ios, viewName.android]) {
|
|
482
526
|
if (defaultProps)
|
|
483
527
|
DEFAULT_PROPS[name] = defaultProps;
|
|
@@ -485,6 +529,8 @@ export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
|
485
529
|
TEXT_CONTENT_PROPS[name] = options.textContent;
|
|
486
530
|
}
|
|
487
531
|
}
|
|
532
|
+
/** The registered names that give way to a component's own host. See `ViewNameOptions`. */
|
|
533
|
+
const YIELDING = new Set();
|
|
488
534
|
/** The prop each view that takes its text content as a prop reads it from. See `ViewNameOptions`. */
|
|
489
535
|
const TEXT_CONTENT_PROPS = {};
|
|
490
536
|
/** Whether `node` takes its text content as a prop rather than as text children. */
|
|
@@ -500,6 +546,14 @@ function withTextContent(node, viewName, props) {
|
|
|
500
546
|
if (text)
|
|
501
547
|
props[prop] = text;
|
|
502
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* The whitespace a paragraph drops at its ends is what CSS collapses: spaces, tabs and line
|
|
551
|
+
* breaks. A no-break space is text, as in a browser, so ` ` is how a value keeps a space at
|
|
552
|
+
* its end; `String.prototype.trim` would take that too.
|
|
553
|
+
*/
|
|
554
|
+
const trimStart = (text) => text.replace(/^[ \t\n\r\f]+/, '');
|
|
555
|
+
const trimEnd = (text) => text.replace(/[ \t\n\r\f]+$/, '');
|
|
556
|
+
const trimEnds = (text) => trimEnd(trimStart(text));
|
|
503
557
|
/**
|
|
504
558
|
* The text written inside a view that takes it as a prop: every run joined, with the whitespace at
|
|
505
559
|
* the two ends dropped as a paragraph drops it, and kept inside. A nested view is drawn after the
|
|
@@ -514,7 +568,7 @@ function textContent(node) {
|
|
|
514
568
|
else if (child.kind === 'element')
|
|
515
569
|
nested = true;
|
|
516
570
|
}
|
|
517
|
-
return nested ?
|
|
571
|
+
return nested ? trimStart(text) : trimEnd(trimStart(text));
|
|
518
572
|
}
|
|
519
573
|
/** An element's registered view name, for the platform the host reported. */
|
|
520
574
|
function registeredViewName(elementName) {
|
|
@@ -528,6 +582,17 @@ function registeredViewName(elementName) {
|
|
|
528
582
|
* a paragraph, and a text input on either platform. See `Engine.remeasureText`.
|
|
529
583
|
*/
|
|
530
584
|
const MEASURED_VIEWS = new Set(['Paragraph', 'TextInput', 'AndroidTextInput']);
|
|
585
|
+
/** Leave a family that is still loading out of `style`, remembering it. See `loadingFamilies`. */
|
|
586
|
+
function holdLoadingFamily(node, style) {
|
|
587
|
+
const family = style['fontFamily'];
|
|
588
|
+
if (typeof family === 'string' && loadingFamilies.has(family)) {
|
|
589
|
+
delete style['fontFamily'];
|
|
590
|
+
node.heldFamily = family;
|
|
591
|
+
}
|
|
592
|
+
else if (node.heldFamily !== undefined) {
|
|
593
|
+
node.heldFamily = undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
531
596
|
/** What each mounted app does when faces register. See `fontsRegistered`. */
|
|
532
597
|
const fontListeners = new Set();
|
|
533
598
|
/** Hear about faces registering with the platform, until the returned function is called. */
|
|
@@ -547,6 +612,45 @@ export function fontsRegistered(families) {
|
|
|
547
612
|
for (const listener of [...fontListeners])
|
|
548
613
|
listener(names);
|
|
549
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Families a load has asked the platform for and not yet settled, by how many loads asked. Text
|
|
617
|
+
* naming one is laid out in the fallback without the name until it settles: native caches a
|
|
618
|
+
* text's measurement by its family name, so a fallback measurement under the face's name would
|
|
619
|
+
* outlast the face arriving.
|
|
620
|
+
*/
|
|
621
|
+
const loadingFamilies = new Map();
|
|
622
|
+
/** Faces a load is about to ask the platform for. See `loadingFamilies`. */
|
|
623
|
+
export function fontsLoading(families) {
|
|
624
|
+
for (const family of families)
|
|
625
|
+
loadingFamilies.set(family, (loadingFamilies.get(family) ?? 0) + 1);
|
|
626
|
+
}
|
|
627
|
+
/** What each mounted app does when a load settles. See `fontsSettled`. */
|
|
628
|
+
const settleListeners = new Set();
|
|
629
|
+
/** Hear about loads settling, until the returned function is called. */
|
|
630
|
+
export function onFontsSettled(listener) {
|
|
631
|
+
settleListeners.add(listener);
|
|
632
|
+
return () => settleListeners.delete(listener);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Faces a load asked for and has finished with, registered or not, before `fontsRegistered`.
|
|
636
|
+
* Text held back for one that no load still asks for is laid out with the name again: see
|
|
637
|
+
* `Engine.fontsSettled`.
|
|
638
|
+
*/
|
|
639
|
+
export function fontsSettled(families) {
|
|
640
|
+
const settled = new Set();
|
|
641
|
+
for (const family of families) {
|
|
642
|
+
const left = (loadingFamilies.get(family) ?? 1) - 1;
|
|
643
|
+
if (left > 0)
|
|
644
|
+
loadingFamilies.set(family, left);
|
|
645
|
+
else {
|
|
646
|
+
loadingFamilies.delete(family);
|
|
647
|
+
settled.add(family);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (settled.size)
|
|
651
|
+
for (const listener of [...settleListeners])
|
|
652
|
+
listener(settled);
|
|
653
|
+
}
|
|
550
654
|
/** Whether a paragraph, or a span in it, was committed asking for one of these families. */
|
|
551
655
|
function namesFamily(node, families) {
|
|
552
656
|
const family = node.committed?.props['fontFamily'];
|
|
@@ -617,12 +721,13 @@ const STAND_INS = {};
|
|
|
617
721
|
*/
|
|
618
722
|
function paragraphText(node) {
|
|
619
723
|
let root = node.parent;
|
|
620
|
-
|
|
621
|
-
|
|
724
|
+
// Text written straight into a view is a paragraph of its own, and all of it.
|
|
725
|
+
if (root === null || !isTextElement(root))
|
|
726
|
+
return node.box ? trimEnds(node.text) : node.text;
|
|
622
727
|
// The commonest paragraph by far: one run and nothing else, so it is both the first and last.
|
|
623
|
-
if (root.children.length === 1 && root.parent
|
|
624
|
-
return node.text
|
|
625
|
-
while (root.parent
|
|
728
|
+
if (root.children.length === 1 && !isTextElement(root.parent))
|
|
729
|
+
return trimEnds(node.text);
|
|
730
|
+
while (isTextElement(root.parent))
|
|
626
731
|
root = root.parent;
|
|
627
732
|
const runs = [];
|
|
628
733
|
const collect = (from) => {
|
|
@@ -631,18 +736,39 @@ function paragraphText(node) {
|
|
|
631
736
|
if (child.text)
|
|
632
737
|
runs.push(child);
|
|
633
738
|
}
|
|
634
|
-
else if (child
|
|
739
|
+
else if (isTextElement(child))
|
|
635
740
|
collect(child);
|
|
636
741
|
}
|
|
637
742
|
};
|
|
638
743
|
collect(root);
|
|
639
744
|
let text = node.text;
|
|
640
745
|
if (runs[0] === node)
|
|
641
|
-
text = text
|
|
746
|
+
text = trimStart(text);
|
|
642
747
|
if (runs[runs.length - 1] === node)
|
|
643
|
-
text = text
|
|
748
|
+
text = trimEnd(text);
|
|
644
749
|
return text;
|
|
645
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Whether an element is text: a `<text>`, or one of HTML's text elements that holds only text and
|
|
753
|
+
* other such elements. Worked out each time, from the tree as it is, so nothing can go stale.
|
|
754
|
+
*/
|
|
755
|
+
function isTextElement(node) {
|
|
756
|
+
if (node === null || node.kind !== 'element')
|
|
757
|
+
return false;
|
|
758
|
+
if (node.name === 'text')
|
|
759
|
+
return true;
|
|
760
|
+
// One lookup settles nearly every other element, which is a view.
|
|
761
|
+
if (registeredViewName(node.name) !== PARAGRAPH)
|
|
762
|
+
return false;
|
|
763
|
+
return !TEXT_ELEMENTS.has(node.name) || holdsOnlyText(node);
|
|
764
|
+
}
|
|
765
|
+
function holdsOnlyText(node) {
|
|
766
|
+
for (const child of node.children ?? []) {
|
|
767
|
+
if (child.kind === 'element' && !isTextElement(child))
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
646
772
|
/**
|
|
647
773
|
* The native view a node commits as: `Paragraph` for a `<text>`, `VirtualText` for one nested in
|
|
648
774
|
* another, `RawText` for a run of text, and `View` for anything unregistered, which is what an
|
|
@@ -658,9 +784,169 @@ export function viewNameOf(node) {
|
|
|
658
784
|
// RN models nested text as a span inside a paragraph: `Paragraph > [RawText, Text > RawText]`.
|
|
659
785
|
// Committing a nested `<text>` as another Paragraph gives a paragraph inside a paragraph,
|
|
660
786
|
// which lays out as a separate block instead of flowing inline.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
787
|
+
// A component of the app's own under a name a view set registered: its host is a plain view.
|
|
788
|
+
if (node.componentHost && !node.nativeView && YIELDING.has(node.name))
|
|
789
|
+
return DEFAULT_VIEW;
|
|
790
|
+
const registered = registeredViewName(node.name);
|
|
791
|
+
// Nearly every element: a view of some kind, by its name alone.
|
|
792
|
+
if (registered !== PARAGRAPH)
|
|
793
|
+
return registered ?? DEFAULT_VIEW;
|
|
794
|
+
// One of HTML's text elements that holds something other than text: a view.
|
|
795
|
+
if (node.name !== 'text' && TEXT_ELEMENTS.has(node.name) && !holdsOnlyText(node)) {
|
|
796
|
+
return DEFAULT_VIEW;
|
|
797
|
+
}
|
|
798
|
+
return isTextElement(node.parent) ? VIRTUAL_TEXT : PARAGRAPH;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The props every native view reads as a boolean: the `bool` fields of React Native's
|
|
802
|
+
* `BaseViewProps`, `AccessibilityProps` and `HostPlatformViewProps`, by the names JavaScript sends.
|
|
803
|
+
*
|
|
804
|
+
* An attribute arrives as text. A component that declares the prop as an input turns it into a
|
|
805
|
+
* boolean itself, but an element nothing claims, such as a component's own host, holds what it
|
|
806
|
+
* was given, and Android refuses a string where it reads a boolean: `focusable="false"` is
|
|
807
|
+
* `java.lang.String cannot be cast to java.lang.Boolean` at the first commit. iOS ignores it.
|
|
808
|
+
*
|
|
809
|
+
* So the text is made a boolean as it is committed, `"false"` false and anything else true, as an
|
|
810
|
+
* attribute reads in HTML. The node keeps the text, which is what `[focusable="false"]` in a
|
|
811
|
+
* stylesheet matches against.
|
|
812
|
+
*/
|
|
813
|
+
const BOOLEAN_VIEW_PROPS = new Set([
|
|
814
|
+
'accessibilityElementsHidden',
|
|
815
|
+
'accessibilityIgnoresInvertColors',
|
|
816
|
+
'accessibilityRespondsToUserInteraction',
|
|
817
|
+
'accessibilityShowsLargeContentViewer',
|
|
818
|
+
'accessibilityViewIsModal',
|
|
819
|
+
'accessible',
|
|
820
|
+
'collapsable',
|
|
821
|
+
'collapsableChildren',
|
|
822
|
+
'focusable',
|
|
823
|
+
'hasTVPreferredFocus',
|
|
824
|
+
'needsOffscreenAlphaCompositing',
|
|
825
|
+
// The flags native reads to know an event has a listener, which are booleans as well.
|
|
826
|
+
'onAccessibilityAction',
|
|
827
|
+
'onAccessibilityEscape',
|
|
828
|
+
'onAccessibilityMagicTap',
|
|
829
|
+
'onAccessibilityTap',
|
|
830
|
+
'onLayout',
|
|
831
|
+
'removeClippedSubviews',
|
|
832
|
+
'renderToHardwareTextureAndroid',
|
|
833
|
+
'screenReaderFocusable',
|
|
834
|
+
'shouldRasterizeIOS',
|
|
835
|
+
]);
|
|
836
|
+
/**
|
|
837
|
+
* Whether `node` holds nothing `:empty` sees besides `moved`: so it was empty before `moved` came,
|
|
838
|
+
* or is now that it has gone. An anchor is a comment on the web, which `:empty` does not see.
|
|
839
|
+
*/
|
|
840
|
+
function wasOrIsEmptyWithout(node, moved) {
|
|
841
|
+
if (moved.kind === 'anchor')
|
|
842
|
+
return false;
|
|
843
|
+
for (const child of node.children) {
|
|
844
|
+
if (child !== moved && child.kind !== 'anchor')
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
|
|
851
|
+
* attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
|
|
852
|
+
* native reads.
|
|
853
|
+
*/
|
|
854
|
+
function writeOwnProps(own, props) {
|
|
855
|
+
let aria = false;
|
|
856
|
+
for (const key of Object.keys(own)) {
|
|
857
|
+
if (key.includes('-'))
|
|
858
|
+
aria ||= key.startsWith('aria-');
|
|
859
|
+
else if (key !== 'style' && key !== INTRINSIC_SIZE && key !== STYLE_OVERRIDE) {
|
|
860
|
+
props[key] = committedProp(key, own[key]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (aria)
|
|
864
|
+
applyAria(own, props);
|
|
865
|
+
}
|
|
866
|
+
/** A prop as it is committed: a boolean view prop held as text is the boolean the text says. */
|
|
867
|
+
function committedProp(key, value) {
|
|
868
|
+
return typeof value === 'string' && BOOLEAN_VIEW_PROPS.has(key) ? value !== 'false' : value;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The `pointer-events` an element ends up with, in the order its props are merged: its inline
|
|
872
|
+
* style's, then its `pointerEvents` prop, then what the sheets resolve. The inline one is read for
|
|
873
|
+
* itself because an element no rule matches resolves to what it inherits, with its own inline
|
|
874
|
+
* value only in what it hands down.
|
|
875
|
+
*/
|
|
876
|
+
function pointerEventsOf(node, resolved) {
|
|
877
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
878
|
+
const own = inline?.['pointerEvents'];
|
|
879
|
+
// Merged over the prop, so its `inherit` is what the sheets and the parent settled, not the prop.
|
|
880
|
+
if (own === 'inherit')
|
|
881
|
+
return resolved['pointerEvents'];
|
|
882
|
+
return own ?? node.props['pointerEvents'] ?? resolved['pointerEvents'];
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* A node's `pointer-events` as native takes it, once its props and styles are merged.
|
|
886
|
+
*
|
|
887
|
+
* In CSS `none` is the element alone, and a descendant's `auto` takes touches again, which native
|
|
888
|
+
* calls `box-none`: what a `none` with something inside to open it is committed as. Native's own
|
|
889
|
+
* `none` is the whole subtree. It is what a `none` with nothing inside to open it stays, because
|
|
890
|
+
* Android's text and images take no `pointerEvents` of their own and only an ancestor's `none`
|
|
891
|
+
* keeps touches off them. It is also what the `pointerEvents` prop means, so a `none` that is the
|
|
892
|
+
* prop's is left as it is.
|
|
893
|
+
*
|
|
894
|
+
* An inline `inherit` is what the element has without it, which is `resolved`: what the sheets
|
|
895
|
+
* and its parent settled.
|
|
896
|
+
*/
|
|
897
|
+
function nativePointerEvents(node, style, resolved) {
|
|
898
|
+
let value = style['pointerEvents'];
|
|
899
|
+
if (value === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (value === 'inherit') {
|
|
902
|
+
value = resolved;
|
|
903
|
+
if (value === undefined)
|
|
904
|
+
delete style['pointerEvents'];
|
|
905
|
+
else
|
|
906
|
+
style['pointerEvents'] = value;
|
|
907
|
+
}
|
|
908
|
+
if (value === 'none' && node.touchWithin && node.props['pointerEvents'] !== 'none') {
|
|
909
|
+
style['pointerEvents'] = 'box-none';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/** Whether `node`, whose `pointer-events` is `value`, takes touches or holds something that does. */
|
|
913
|
+
function takesTouches(node, value, prop) {
|
|
914
|
+
if (value === undefined)
|
|
915
|
+
return false;
|
|
916
|
+
// The prop's `none` is native's: the whole subtree, whatever is inside it.
|
|
917
|
+
if (value === 'none' && prop === 'none')
|
|
918
|
+
return false;
|
|
919
|
+
// CSS's `none` and native's `box-none` take no touches themselves: their children answer.
|
|
920
|
+
if (value === 'none' || value === 'box-none') {
|
|
921
|
+
return node.children.some((child) => child.touchWithin);
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Records on `node` whether it, or something inside it, takes touches where it would inherit
|
|
927
|
+
* `none`. When that changes a `none` element's own answer, its props are marked to be committed
|
|
928
|
+
* again.
|
|
929
|
+
*
|
|
930
|
+
* Called as a node is reconciled, after its children, so theirs are already current: a child that
|
|
931
|
+
* was skipped as clean has nothing beneath it that changed, and a change beneath marks every
|
|
932
|
+
* ancestor to come back through here.
|
|
933
|
+
*/
|
|
934
|
+
function noteTouches(node, style) {
|
|
935
|
+
if (!style)
|
|
936
|
+
return;
|
|
937
|
+
// Nearly every node: no `pointer-events` in any sheet, style or prop, and nothing recorded.
|
|
938
|
+
const resolved = style.style['pointerEvents'];
|
|
939
|
+
const prop = node.props['pointerEvents'];
|
|
940
|
+
if (resolved === undefined && prop === undefined && !node.inlineInherits && !node.touchWithin) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const value = pointerEventsOf(node, style.style);
|
|
944
|
+
const within = takesTouches(node, value, prop);
|
|
945
|
+
if (within === !!node.touchWithin)
|
|
946
|
+
return;
|
|
947
|
+
node.touchWithin = within;
|
|
948
|
+
if (value === 'none')
|
|
949
|
+
node.propsDirty = true;
|
|
664
950
|
}
|
|
665
951
|
/** Whether `node` is `ancestor` or somewhere under it. */
|
|
666
952
|
function isWithin(node, ancestor) {
|
|
@@ -763,6 +1049,10 @@ class RetainedNode {
|
|
|
763
1049
|
propsDirty = false;
|
|
764
1050
|
structureDirty = false;
|
|
765
1051
|
subtreeDirty = false;
|
|
1052
|
+
touchWithin = undefined;
|
|
1053
|
+
box = undefined;
|
|
1054
|
+
anonymous = undefined;
|
|
1055
|
+
styled = undefined;
|
|
766
1056
|
transitions;
|
|
767
1057
|
playing;
|
|
768
1058
|
scrolled;
|
|
@@ -896,8 +1186,9 @@ const IS_COLOR_PROP = /color(android)?$/i;
|
|
|
896
1186
|
const colorProps = new Map();
|
|
897
1187
|
function isColorProp(key) {
|
|
898
1188
|
let answer = colorProps.get(key);
|
|
899
|
-
if (answer === undefined)
|
|
900
|
-
colorProps.set(key, (answer = IS_COLOR_PROP.test(key)));
|
|
1189
|
+
if (answer === undefined) {
|
|
1190
|
+
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || BRUSH_PROPS.has(key)));
|
|
1191
|
+
}
|
|
901
1192
|
return answer;
|
|
902
1193
|
}
|
|
903
1194
|
const ASSET_PROPS = new Set(['source', 'defaultSource', 'loadingIndicatorSource']);
|
|
@@ -910,7 +1201,30 @@ const ASSET_PROPS = new Set(['source', 'defaultSource', 'loadingIndicatorSource'
|
|
|
910
1201
|
* approximated, it is dropped.
|
|
911
1202
|
*/
|
|
912
1203
|
const NESTED_COLOR_LIST_PROPS = new Set(['boxShadow']);
|
|
1204
|
+
/** The props of an SVG shape that take a brush, not a colour. */
|
|
1205
|
+
const BRUSH_PROPS = new Set(['fill', 'stroke']);
|
|
913
1206
|
/** Styles arrive as objects, arrays, nested arrays and nulls. Reduce to one object. */
|
|
1207
|
+
/**
|
|
1208
|
+
* A bound transform is still the CSS string: see `inline-transform.ts`. One CSS cannot read
|
|
1209
|
+
* leaves the transform the rules set, as a browser drops it.
|
|
1210
|
+
*/
|
|
1211
|
+
function boundTransform(node, style, cascaded) {
|
|
1212
|
+
const text = style['transform'];
|
|
1213
|
+
if (typeof text !== 'string')
|
|
1214
|
+
return style;
|
|
1215
|
+
// The same list for the same string, commit after commit: a transition compares the two ends
|
|
1216
|
+
// it is given, and a list made afresh each time reads as a new place to go.
|
|
1217
|
+
if (node.boundTransform?.text !== text)
|
|
1218
|
+
node.boundTransform = { text, list: transformList(text) };
|
|
1219
|
+
const list = node.boundTransform.list ?? cascaded;
|
|
1220
|
+
if (list === undefined)
|
|
1221
|
+
delete style['transform'];
|
|
1222
|
+
else
|
|
1223
|
+
style['transform'] = list;
|
|
1224
|
+
return style;
|
|
1225
|
+
}
|
|
1226
|
+
/** A transform string as the list native reads, and none for one CSS cannot read. */
|
|
1227
|
+
const transformOf = (value) => transformList(value) ?? [];
|
|
914
1228
|
function flattenStyle(value, into) {
|
|
915
1229
|
if (!value)
|
|
916
1230
|
return into;
|
|
@@ -986,7 +1300,7 @@ function composeTransform(node, props) {
|
|
|
986
1300
|
}
|
|
987
1301
|
else {
|
|
988
1302
|
// A bound style's transform is still the CSS string: see `inline-transform.ts`.
|
|
989
|
-
const own = typeof transform === 'string' ?
|
|
1303
|
+
const own = typeof transform === 'string' ? transformOf(transform) : transform;
|
|
990
1304
|
value = [translate, rotate, scale, own].flatMap((part) => (Array.isArray(part) ? part : []));
|
|
991
1305
|
node.composedTransform = { parts, value };
|
|
992
1306
|
}
|
|
@@ -1130,7 +1444,7 @@ export class Engine {
|
|
|
1130
1444
|
this.structuralSheets = options.globalStyles?.structural === true;
|
|
1131
1445
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
1132
1446
|
if (this.dev) {
|
|
1133
|
-
this.styles.onUndefinedToken = (name, props) => this.reportUndefinedToken(name, props);
|
|
1447
|
+
this.styles.onUndefinedToken = (name, props, on) => this.reportUndefinedToken(name, props, on);
|
|
1134
1448
|
this.styles.onUnreadDisplay = (name, value) => this.reportUnreadDisplay(name, value);
|
|
1135
1449
|
}
|
|
1136
1450
|
this.now = options.now ?? (() => globalThis.performance?.now?.() ?? Date.now());
|
|
@@ -1285,6 +1599,25 @@ export class Engine {
|
|
|
1285
1599
|
* text attributes change, which the same cap does. Android sets it only when `fontFamily` is in
|
|
1286
1600
|
* the props it is sent, and the engine sends only what changed, so it is sent again once.
|
|
1287
1601
|
*/
|
|
1602
|
+
/**
|
|
1603
|
+
* Loads have settled for these families: every node held back for one, a span inside a
|
|
1604
|
+
* paragraph included, is laid out with the name again, registered or not. See `loadingFamilies`.
|
|
1605
|
+
*/
|
|
1606
|
+
fontsSettled(families) {
|
|
1607
|
+
let held = false;
|
|
1608
|
+
const visit = (node) => {
|
|
1609
|
+
for (const child of node.children) {
|
|
1610
|
+
if (child.heldFamily !== undefined && families.has(child.heldFamily)) {
|
|
1611
|
+
this.markProps(child, false);
|
|
1612
|
+
held = true;
|
|
1613
|
+
}
|
|
1614
|
+
visit(child);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
visit(this.root);
|
|
1618
|
+
if (held)
|
|
1619
|
+
this.commit();
|
|
1620
|
+
}
|
|
1288
1621
|
fontsRegistered(families) {
|
|
1289
1622
|
const visit = (node) => {
|
|
1290
1623
|
for (const child of node.children) {
|
|
@@ -1396,6 +1729,12 @@ export class Engine {
|
|
|
1396
1729
|
viewportSize;
|
|
1397
1730
|
/** The system text size, from the conditions. See `Conditions.fontScale`. */
|
|
1398
1731
|
fontScale;
|
|
1732
|
+
viewportWatchers = new Set();
|
|
1733
|
+
/** Calls `listener` after each change of conditions, which is when `viewport` can change. */
|
|
1734
|
+
watchViewport(listener) {
|
|
1735
|
+
this.viewportWatchers.add(listener);
|
|
1736
|
+
return () => this.viewportWatchers.delete(listener);
|
|
1737
|
+
}
|
|
1399
1738
|
updateConditions(next) {
|
|
1400
1739
|
this.viewportSize = { width: next.width, height: next.height };
|
|
1401
1740
|
this.fontScale = next.fontScale;
|
|
@@ -1404,11 +1743,19 @@ export class Engine {
|
|
|
1404
1743
|
// Committed here rather than left to the next change-detection pass, because there may not
|
|
1405
1744
|
// be one: no view is dirty, so a tick does no work and never reaches the renderer at all.
|
|
1406
1745
|
this.commit();
|
|
1746
|
+
for (const watcher of this.viewportWatchers)
|
|
1747
|
+
watcher();
|
|
1407
1748
|
}
|
|
1408
1749
|
// --- mutation API -----------------------------------------------------
|
|
1409
1750
|
createElement(name, sheet = null) {
|
|
1410
1751
|
const node = new RetainedNode('element', name, this);
|
|
1411
1752
|
node.sheet = sheet;
|
|
1753
|
+
const html = HTML_ELEMENTS.get(name);
|
|
1754
|
+
if (html !== undefined) {
|
|
1755
|
+
if (html.role)
|
|
1756
|
+
node.props['accessibilityRole'] = html.role;
|
|
1757
|
+
node.styled = html.styled;
|
|
1758
|
+
}
|
|
1412
1759
|
if (sheet?.structural)
|
|
1413
1760
|
this.structuralSheets = true;
|
|
1414
1761
|
if (HOISTS[name])
|
|
@@ -1464,7 +1811,7 @@ export class Engine {
|
|
|
1464
1811
|
parent.children.push(child);
|
|
1465
1812
|
if (child.dormantHoists)
|
|
1466
1813
|
this.wakeHoists(child);
|
|
1467
|
-
this.markStructure(parent);
|
|
1814
|
+
this.markStructure(parent, child);
|
|
1468
1815
|
this.markTextContent(parent, child);
|
|
1469
1816
|
}
|
|
1470
1817
|
insertBefore(parent, child, ref) {
|
|
@@ -1477,7 +1824,7 @@ export class Engine {
|
|
|
1477
1824
|
parent.children.splice(at, 0, child);
|
|
1478
1825
|
if (child.dormantHoists)
|
|
1479
1826
|
this.wakeHoists(child);
|
|
1480
|
-
this.markStructure(parent);
|
|
1827
|
+
this.markStructure(parent, child);
|
|
1481
1828
|
this.markTextContent(parent, child);
|
|
1482
1829
|
}
|
|
1483
1830
|
removeChild(parent, child) {
|
|
@@ -1490,7 +1837,7 @@ export class Engine {
|
|
|
1490
1837
|
target.children.splice(at, 1);
|
|
1491
1838
|
child.parent = null;
|
|
1492
1839
|
this.removedSinceCommit.add(child);
|
|
1493
|
-
this.markStructure(target);
|
|
1840
|
+
this.markStructure(target, child);
|
|
1494
1841
|
this.markTextContent(target, child);
|
|
1495
1842
|
}
|
|
1496
1843
|
/**
|
|
@@ -1620,7 +1967,17 @@ export class Engine {
|
|
|
1620
1967
|
// Inline style is applied after the cascade and inherited by nothing, so it cannot change
|
|
1621
1968
|
// what any node matches or inherits. Every other prop can: `[disabled]` is a selector. An
|
|
1622
1969
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
1623
|
-
this.markProps(node, key !== 'style' || this.
|
|
1970
|
+
this.markProps(node, key !== 'style' || this.inlineReachesStyle(node));
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Whether a node's inline style can change what it or anything below it resolves to: it sets,
|
|
1974
|
+
* or until now set, a property its children inherit.
|
|
1975
|
+
*/
|
|
1976
|
+
inlineReachesStyle(node) {
|
|
1977
|
+
const inherits = setsInherited(node.props['style']);
|
|
1978
|
+
const inherited = node.inlineInherits === true;
|
|
1979
|
+
node.inlineInherits = inherits;
|
|
1980
|
+
return this.noteInlineDirection(node) || inherits || inherited;
|
|
1624
1981
|
}
|
|
1625
1982
|
/** Whether a node's inline style sets `direction`, remembering that one has. */
|
|
1626
1983
|
noteInlineDirection(node) {
|
|
@@ -1642,7 +1999,26 @@ export class Engine {
|
|
|
1642
1999
|
* has changed for the props diff to notice.
|
|
1643
2000
|
*/
|
|
1644
2001
|
styleChanged(node) {
|
|
1645
|
-
this.markProps(node, this.
|
|
2002
|
+
this.markProps(node, this.inlineReachesStyle(node));
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* A bound declaration, `[style.color]`, whose value may be a `var()`. One that is, is kept as a
|
|
2006
|
+
* declaration the cascade settles with the node's tokens, as a rule's is, and true is answered:
|
|
2007
|
+
* the text is no value to put in the node's style. Any other value forgets what was kept.
|
|
2008
|
+
*/
|
|
2009
|
+
setBoundStyle(node, prop, value) {
|
|
2010
|
+
const text = typeof value === 'string' && /^\s*var\(/i.test(value) ? value : undefined;
|
|
2011
|
+
const bound = text === undefined ? null : boundDeclaration(prop, tokenFromValue(text), node.name);
|
|
2012
|
+
if (!bound && !node.boundByProp?.has(prop))
|
|
2013
|
+
return false;
|
|
2014
|
+
const byProp = (node.boundByProp ??= new Map());
|
|
2015
|
+
if (bound)
|
|
2016
|
+
byProp.set(prop, bound);
|
|
2017
|
+
else
|
|
2018
|
+
byProp.delete(prop);
|
|
2019
|
+
node.boundStyle = byProp.size ? [...byProp.values()] : null;
|
|
2020
|
+
this.markProps(node);
|
|
2021
|
+
return bound !== null;
|
|
1646
2022
|
}
|
|
1647
2023
|
/**
|
|
1648
2024
|
* Set a custom property on a node, `--tint` from `[style.--tint]`, or remove it with `null`.
|
|
@@ -1709,7 +2085,8 @@ export class Engine {
|
|
|
1709
2085
|
siblings[i].styleDirty = true;
|
|
1710
2086
|
}
|
|
1711
2087
|
}
|
|
1712
|
-
|
|
2088
|
+
/** `node`'s child list changed: `moved` came into it or went out of it. */
|
|
2089
|
+
markStructure(node, moved) {
|
|
1713
2090
|
node.structureDirty = true;
|
|
1714
2091
|
// A child list that moved changes what its members match, though nothing about them did:
|
|
1715
2092
|
// the old last row is no longer the last. Only sheets that ask about position pay for this.
|
|
@@ -1717,6 +2094,10 @@ export class Engine {
|
|
|
1717
2094
|
node.styleDirty = true;
|
|
1718
2095
|
for (const child of node.children)
|
|
1719
2096
|
child.styleDirty = true;
|
|
2097
|
+
// `.box:empty + .spacer`: what a later sibling matches can hang on whether this node is
|
|
2098
|
+
// empty, which only its first child arriving or its last one leaving changes.
|
|
2099
|
+
if (wasOrIsEmptyWithout(node, moved))
|
|
2100
|
+
this.markLaterSiblings(node);
|
|
1720
2101
|
}
|
|
1721
2102
|
this.markPath(node.parent);
|
|
1722
2103
|
}
|
|
@@ -1780,10 +2161,59 @@ export class Engine {
|
|
|
1780
2161
|
}
|
|
1781
2162
|
this.flushTransitionEvents();
|
|
1782
2163
|
this.scrollDriver?.afterCommit();
|
|
1783
|
-
|
|
2164
|
+
// Read first: the commit that settles keyframes starts by clearing it.
|
|
2165
|
+
const facesAdded = this.facesAdded;
|
|
2166
|
+
if (this.awaitingKeyframes.size)
|
|
2167
|
+
this.settleKeyframes();
|
|
2168
|
+
if (facesAdded)
|
|
1784
2169
|
this.rematchFonts();
|
|
1785
2170
|
return true;
|
|
1786
2171
|
}
|
|
2172
|
+
/** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
|
|
2173
|
+
unseen = Infinity;
|
|
2174
|
+
/**
|
|
2175
|
+
* A commit for what changed in the same turn as earlier commits, before a frame was drawn.
|
|
2176
|
+
* `since` is how many commits there had been before the first of them (`stats.commits`).
|
|
2177
|
+
*
|
|
2178
|
+
* A view those commits created has not been seen in the style they gave it, so what this one
|
|
2179
|
+
* gives it is where it starts, and no transition runs toward it. That is what a browser
|
|
2180
|
+
* does with a class added to an element in the turn that inserted it, and what `animate.enter`
|
|
2181
|
+
* is built on: Angular adds the enter class after the render pass that created the element, and
|
|
2182
|
+
* takes it off a frame later, which is the one change the transition is for.
|
|
2183
|
+
*/
|
|
2184
|
+
commitUnseen(since) {
|
|
2185
|
+
this.unseen = since;
|
|
2186
|
+
try {
|
|
2187
|
+
return this.commit();
|
|
2188
|
+
}
|
|
2189
|
+
finally {
|
|
2190
|
+
this.unseen = Infinity;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
/** Nodes that asked for `@keyframes` this commit had not met, by the name they asked for. */
|
|
2194
|
+
awaitingKeyframes = new Map();
|
|
2195
|
+
/**
|
|
2196
|
+
* A sheet is registered when the first node it styles is committed, so a node committed before
|
|
2197
|
+
* it can name keyframes that arrive later in the same commit. Those nodes play them from another
|
|
2198
|
+
* commit straight away, as a browser applies keyframes however the sheets arrived; a name still
|
|
2199
|
+
* unknown once the commit is over is reported.
|
|
2200
|
+
*/
|
|
2201
|
+
settleKeyframes() {
|
|
2202
|
+
const waiting = [...this.awaitingKeyframes];
|
|
2203
|
+
this.awaitingKeyframes.clear();
|
|
2204
|
+
let found = false;
|
|
2205
|
+
for (const [node, name] of waiting) {
|
|
2206
|
+
if (this.keyframes.has(name)) {
|
|
2207
|
+
this.markProps(node, false);
|
|
2208
|
+
found = true;
|
|
2209
|
+
}
|
|
2210
|
+
else if (this.dev) {
|
|
2211
|
+
this.reportMissingKeyframes(name);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (found)
|
|
2215
|
+
this.commit();
|
|
2216
|
+
}
|
|
1787
2217
|
scrollDriver;
|
|
1788
2218
|
/** Whether native can move a view by a scroll with no JavaScript in between. */
|
|
1789
2219
|
get drivesScroll() {
|
|
@@ -1873,23 +2303,18 @@ export class Engine {
|
|
|
1873
2303
|
this.registerSheet(node.hostSheet);
|
|
1874
2304
|
const props = { ...DEFAULT_PROPS[viewName], ...node.defaultStyle };
|
|
1875
2305
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
// selectors to match, and the components package maps `aria-*` to what native reads.
|
|
1879
|
-
if (key !== 'style' &&
|
|
1880
|
-
key !== INTRINSIC_SIZE &&
|
|
1881
|
-
key !== STYLE_OVERRIDE &&
|
|
1882
|
-
!key.includes('-')) {
|
|
1883
|
-
props[key] = node.props[key];
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
2306
|
+
const resolved = props['pointerEvents'];
|
|
2307
|
+
writeOwnProps(node.props, props);
|
|
1886
2308
|
withTextContent(node, viewName, props);
|
|
1887
|
-
const
|
|
2309
|
+
const cascaded = props['transform'];
|
|
2310
|
+
const style = boundTransform(node, flattenStyle(node.props['style'], props), cascaded);
|
|
2311
|
+
nativePointerEvents(node, style, resolved);
|
|
1888
2312
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
1889
2313
|
if (intrinsic)
|
|
1890
2314
|
applyIntrinsicSize(style, intrinsic);
|
|
1891
2315
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
1892
2316
|
this.fontFaces.apply(style);
|
|
2317
|
+
holdLoadingFamily(node, style);
|
|
1893
2318
|
if (viewName === PARAGRAPH)
|
|
1894
2319
|
alignText(style, this.directionOf(node, style));
|
|
1895
2320
|
if (this.fontsRefreshed)
|
|
@@ -1981,14 +2406,25 @@ export class Engine {
|
|
|
1981
2406
|
if (next && !this.knownSheets.has(next)) {
|
|
1982
2407
|
this.knownSheets.add(next);
|
|
1983
2408
|
this.sheetOrder[at] = next;
|
|
1984
|
-
if (next.fonts && this.fontFaces.add(next.fonts))
|
|
1985
|
-
this.facesAdded = true;
|
|
1986
2409
|
}
|
|
1987
2410
|
else {
|
|
1988
2411
|
this.sheetOrder.splice(at, 1);
|
|
1989
2412
|
}
|
|
1990
2413
|
if (sheet.keyframes || next?.keyframes)
|
|
1991
2414
|
this.reindexKeyframes();
|
|
2415
|
+
if (sheet.fonts || next?.fonts)
|
|
2416
|
+
this.reindexFonts();
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Every sheet's `@font-face` rules again, in order, and every text that names a family matched
|
|
2420
|
+
* again: a face whose rule went is no longer matched, though the platform keeps it registered.
|
|
2421
|
+
*/
|
|
2422
|
+
reindexFonts() {
|
|
2423
|
+
this.fontFaces.clear();
|
|
2424
|
+
for (const sheet of this.sheetOrder)
|
|
2425
|
+
if (sheet.fonts)
|
|
2426
|
+
this.fontFaces.add(sheet.fonts);
|
|
2427
|
+
this.markFontText(this.root);
|
|
1992
2428
|
}
|
|
1993
2429
|
/** Every sheet's `@keyframes` again, in order, and every animation told to look them up. */
|
|
1994
2430
|
reindexKeyframes() {
|
|
@@ -2018,18 +2454,19 @@ export class Engine {
|
|
|
2018
2454
|
* Once per sheet that declares faces, so walking the tree costs nothing that matters.
|
|
2019
2455
|
*/
|
|
2020
2456
|
rematchFonts() {
|
|
2021
|
-
|
|
2022
|
-
for (const child of node.children) {
|
|
2023
|
-
if (child.kind !== 'element')
|
|
2024
|
-
continue;
|
|
2025
|
-
if (typeof child.committed?.props['fontFamily'] === 'string')
|
|
2026
|
-
this.markProps(child, false);
|
|
2027
|
-
visit(child);
|
|
2028
|
-
}
|
|
2029
|
-
};
|
|
2030
|
-
visit(this.root);
|
|
2457
|
+
this.markFontText(this.root);
|
|
2031
2458
|
this.commit();
|
|
2032
2459
|
}
|
|
2460
|
+
/** Mark every element under `node` committed with a family, so it is matched again. */
|
|
2461
|
+
markFontText(node) {
|
|
2462
|
+
for (const child of node.children) {
|
|
2463
|
+
if (child.kind !== 'element')
|
|
2464
|
+
continue;
|
|
2465
|
+
if (typeof child.committed?.props['fontFamily'] === 'string')
|
|
2466
|
+
this.markProps(child, false);
|
|
2467
|
+
this.markFontText(child);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2033
2470
|
knownSheets = new WeakSet();
|
|
2034
2471
|
/**
|
|
2035
2472
|
* Start, advance or stop the `@keyframes` animation a node's style asks for.
|
|
@@ -2044,6 +2481,7 @@ export class Engine {
|
|
|
2044
2481
|
this.stopScrolled(node);
|
|
2045
2482
|
if (!spec) {
|
|
2046
2483
|
if (node.playing) {
|
|
2484
|
+
this.cancelPlaying(node);
|
|
2047
2485
|
node.playing = undefined;
|
|
2048
2486
|
this.playing.delete(node);
|
|
2049
2487
|
}
|
|
@@ -2051,12 +2489,13 @@ export class Engine {
|
|
|
2051
2489
|
}
|
|
2052
2490
|
const frames = this.keyframes.get(spec.name);
|
|
2053
2491
|
if (!frames) {
|
|
2054
|
-
// Keyframes a hot swap deleted, from under an animation that was playing them
|
|
2492
|
+
// Keyframes a hot swap deleted, from under an animation that was playing them, or ones a
|
|
2493
|
+
// sheet later in this commit has: see `settleKeyframes`.
|
|
2494
|
+
this.cancelPlaying(node);
|
|
2055
2495
|
node.playing = undefined;
|
|
2056
2496
|
this.playing.delete(node);
|
|
2057
2497
|
this.stopScrolled(node);
|
|
2058
|
-
|
|
2059
|
-
this.reportMissingKeyframes(spec.name);
|
|
2498
|
+
this.awaitingKeyframes.set(node, spec.name);
|
|
2060
2499
|
return props;
|
|
2061
2500
|
}
|
|
2062
2501
|
if (spec.timeline)
|
|
@@ -2064,11 +2503,34 @@ export class Engine {
|
|
|
2064
2503
|
this.startPlaying(node, spec, frames, props);
|
|
2065
2504
|
return Object.assign(props, node.playing?.values ?? {});
|
|
2066
2505
|
}
|
|
2506
|
+
/**
|
|
2507
|
+
* A clock animation the scroll plays instead: the same animation where only the timeline moved,
|
|
2508
|
+
* and one cancelled where the name changed.
|
|
2509
|
+
*/
|
|
2510
|
+
handOverToScroll(node, spec) {
|
|
2511
|
+
if (!node.playing)
|
|
2512
|
+
return;
|
|
2513
|
+
if (node.playing.spec.name !== spec.name)
|
|
2514
|
+
this.cancelPlaying(node);
|
|
2515
|
+
node.playing = undefined;
|
|
2516
|
+
this.playing.delete(node);
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* `animationcancel` for an animation stopped before it ended: the element no longer asks for it,
|
|
2520
|
+
* asks for another by name, or its keyframes went, as a browser fires it.
|
|
2521
|
+
*/
|
|
2522
|
+
cancelPlaying(node) {
|
|
2523
|
+
const running = node.playing;
|
|
2524
|
+
if (running && !running.done)
|
|
2525
|
+
this.emitTransition(node, 'topAnimationcancel', running.spec.name);
|
|
2526
|
+
}
|
|
2067
2527
|
/** Start the clock on an animation, unless the node is already playing this one. */
|
|
2068
2528
|
startPlaying(node, spec, frames, props) {
|
|
2069
2529
|
const current = node.playing;
|
|
2070
2530
|
const inherited = this.inheritedColour(node, frames);
|
|
2071
2531
|
if (!current || !sameAnimation(current.spec, spec)) {
|
|
2532
|
+
if (current && current.spec.name !== spec.name)
|
|
2533
|
+
this.cancelPlaying(node);
|
|
2072
2534
|
const started = {
|
|
2073
2535
|
spec,
|
|
2074
2536
|
tracks: tracksOf(frames, props, inherited),
|
|
@@ -2160,10 +2622,7 @@ export class Engine {
|
|
|
2160
2622
|
return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
|
|
2161
2623
|
}
|
|
2162
2624
|
this.stopScrolled(node);
|
|
2163
|
-
|
|
2164
|
-
node.playing = undefined;
|
|
2165
|
-
this.playing.delete(node);
|
|
2166
|
-
}
|
|
2625
|
+
this.handOverToScroll(node, spec);
|
|
2167
2626
|
const tracks = tracksOf(frames, props, inherited);
|
|
2168
2627
|
const source = this.scrollSourceOf(node);
|
|
2169
2628
|
const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
|
|
@@ -2280,6 +2739,9 @@ export class Engine {
|
|
|
2280
2739
|
`has loaded, so the block has to exist somewhere by the time the animation starts.`);
|
|
2281
2740
|
}
|
|
2282
2741
|
transitioned(node, props) {
|
|
2742
|
+
// Nobody saw what the last commit gave a view it created, so this is where the view starts.
|
|
2743
|
+
if (node.bornIn >= this.unseen)
|
|
2744
|
+
node.transitions = undefined;
|
|
2283
2745
|
const spec = transitionSpec(props);
|
|
2284
2746
|
if (!spec && !node.transitions)
|
|
2285
2747
|
return props;
|
|
@@ -2391,7 +2853,7 @@ export class Engine {
|
|
|
2391
2853
|
if (value === null || value === undefined)
|
|
2392
2854
|
out[key] = value;
|
|
2393
2855
|
else if (isColorProp(key))
|
|
2394
|
-
out[key] = this.
|
|
2856
|
+
out[key] = this.painted(key, value);
|
|
2395
2857
|
// A list of sources has already been resolved by whoever built it, as RN's resolver
|
|
2396
2858
|
// also assumes: it passes any object through and only turns a number into one.
|
|
2397
2859
|
else if (ASSET_PROPS.has(key) && !Array.isArray(value))
|
|
@@ -2404,7 +2866,7 @@ export class Engine {
|
|
|
2404
2866
|
out[key] = this.processFilters(value);
|
|
2405
2867
|
// A bound style's transform is still the CSS string: see `inline-transform.ts`.
|
|
2406
2868
|
else if (key === 'transform' && typeof value === 'string')
|
|
2407
|
-
out[key] =
|
|
2869
|
+
out[key] = transformOf(value);
|
|
2408
2870
|
else
|
|
2409
2871
|
out[key] = value;
|
|
2410
2872
|
}
|
|
@@ -2476,7 +2938,9 @@ export class Engine {
|
|
|
2476
2938
|
return node.committed.handle;
|
|
2477
2939
|
node.styleCommitted = style;
|
|
2478
2940
|
const viewName = viewNameOf(node);
|
|
2941
|
+
this.forgetRenamed(node, viewName);
|
|
2479
2942
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
2943
|
+
noteTouches(node, style);
|
|
2480
2944
|
const previous = node.committed;
|
|
2481
2945
|
this.notePresented(node, viewName);
|
|
2482
2946
|
if (!previous)
|
|
@@ -2495,7 +2959,7 @@ export class Engine {
|
|
|
2495
2959
|
handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
|
|
2496
2960
|
// A clone is the same view, so it keeps the tag it was created with, not the newest tag
|
|
2497
2961
|
// handed out, which is some other node's.
|
|
2498
|
-
node.committed = { handle, tag: previous.tag, props, childHandles };
|
|
2962
|
+
node.committed = { handle, tag: previous.tag, props, childHandles, viewName };
|
|
2499
2963
|
}
|
|
2500
2964
|
this.clearFlags(node);
|
|
2501
2965
|
return handle;
|
|
@@ -2533,19 +2997,62 @@ export class Engine {
|
|
|
2533
2997
|
const context = style?.context;
|
|
2534
2998
|
if (HOIST_TARGETS.has(node.name))
|
|
2535
2999
|
return this.reconcileHoistTarget(node, context);
|
|
2536
|
-
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
2537
|
-
const textAsProp = TEXT_CONTENT_PROPS[viewName] !== undefined;
|
|
2538
3000
|
const handles = [];
|
|
2539
3001
|
for (const child of node.children) {
|
|
2540
3002
|
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
2541
3003
|
continue;
|
|
2542
3004
|
}
|
|
2543
|
-
if (
|
|
3005
|
+
if (child.kind === 'text') {
|
|
3006
|
+
const handle = this.reconcileText(node, child, viewName, context);
|
|
3007
|
+
if (handle)
|
|
3008
|
+
handles.push(handle);
|
|
2544
3009
|
continue;
|
|
3010
|
+
}
|
|
2545
3011
|
handles.push(this.reconcileUnder(node, child, context));
|
|
2546
3012
|
}
|
|
2547
3013
|
return handles;
|
|
2548
3014
|
}
|
|
3015
|
+
/**
|
|
3016
|
+
* The paragraph a run of text written straight into a view is drawn in, or nothing for a run of
|
|
3017
|
+
* whitespace, which a browser draws nothing for between the items of a flex box either.
|
|
3018
|
+
*
|
|
3019
|
+
* It is the text's own, kept on the text node, and no part of the tree: its parent is the view,
|
|
3020
|
+
* so it inherits what the view's text would, and no rule matches it.
|
|
3021
|
+
*/
|
|
3022
|
+
/**
|
|
3023
|
+
* A run of text under `parent`, which commits as `viewName`: the text itself inside a text, a
|
|
3024
|
+
* paragraph of its own inside any other view, and nothing where the view takes its text as a
|
|
3025
|
+
* prop (see `ViewNameOptions`) or the run is whitespace between a view's children.
|
|
3026
|
+
*/
|
|
3027
|
+
reconcileText(parent, text, viewName, context) {
|
|
3028
|
+
if (TEXT_CONTENT_PROPS[viewName] !== undefined)
|
|
3029
|
+
return null;
|
|
3030
|
+
if (viewName === PARAGRAPH || viewName === VIRTUAL_TEXT) {
|
|
3031
|
+
// Under a text element, the paragraph it had in a view is done with. Under that paragraph
|
|
3032
|
+
// itself, which is where a loose run is committed, it is the one to keep.
|
|
3033
|
+
if (text.box && text.box !== parent)
|
|
3034
|
+
text.box = undefined;
|
|
3035
|
+
return this.reconcileUnder(parent, text, context);
|
|
3036
|
+
}
|
|
3037
|
+
const box = this.looseText(parent, text);
|
|
3038
|
+
return box ? this.reconcileUnder(parent, box, context) : null;
|
|
3039
|
+
}
|
|
3040
|
+
looseText(parent, text) {
|
|
3041
|
+
if (!trimEnds(text.text))
|
|
3042
|
+
return null;
|
|
3043
|
+
let box = text.box;
|
|
3044
|
+
if (!box) {
|
|
3045
|
+
box = new RetainedNode('element', 'text', this);
|
|
3046
|
+
box.anonymous = true;
|
|
3047
|
+
box.children = [text];
|
|
3048
|
+
text.box = box;
|
|
3049
|
+
}
|
|
3050
|
+
box.parent = parent;
|
|
3051
|
+
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
3052
|
+
if (!this.isClean(text))
|
|
3053
|
+
box.subtreeDirty = true;
|
|
3054
|
+
return box;
|
|
3055
|
+
}
|
|
2549
3056
|
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|
|
2550
3057
|
notePresented(node, viewName) {
|
|
2551
3058
|
if (viewName !== MODAL_HOST || node.props['visible'] === false)
|
|
@@ -2625,6 +3132,14 @@ export class Engine {
|
|
|
2625
3132
|
child.committedUnder = parent;
|
|
2626
3133
|
return handle;
|
|
2627
3134
|
}
|
|
3135
|
+
/**
|
|
3136
|
+
* A node whose native view is no longer the one it was created as is created again: a text
|
|
3137
|
+
* element that came to hold a view, or stopped. A clone keeps the view it was made as.
|
|
3138
|
+
*/
|
|
3139
|
+
forgetRenamed(node, viewName) {
|
|
3140
|
+
if (node.committed && node.committed.viewName !== viewName)
|
|
3141
|
+
this.forgetCommitted(node);
|
|
3142
|
+
}
|
|
2628
3143
|
/**
|
|
2629
3144
|
* Let a node and everything under it be created again at the next commit. The views it keeps
|
|
2630
3145
|
* for hoisted names go too: they are children of the native view it is losing.
|
|
@@ -2634,9 +3149,12 @@ export class Engine {
|
|
|
2634
3149
|
node.committedUnder = null;
|
|
2635
3150
|
node.styleCommitted = null;
|
|
2636
3151
|
node.kept = undefined;
|
|
2637
|
-
|
|
2638
|
-
|
|
3152
|
+
if (node.box?.committed)
|
|
3153
|
+
this.forgetCommitted(node.box);
|
|
3154
|
+
for (const child of node.children) {
|
|
3155
|
+
if (child.committed || child.box?.committed)
|
|
2639
3156
|
this.forgetCommitted(child);
|
|
3157
|
+
}
|
|
2640
3158
|
}
|
|
2641
3159
|
/** A hoisted node, which commits into an ancestor further up than the parent it is in. */
|
|
2642
3160
|
committedElsewhere(child) {
|
|
@@ -2678,7 +3196,7 @@ export class Engine {
|
|
|
2678
3196
|
};
|
|
2679
3197
|
const payload = diffProps(kept.committed.props, props);
|
|
2680
3198
|
const handle = this.clone(kept.committed.handle, payload && this.processed(payload), []);
|
|
2681
|
-
kept.committed = {
|
|
3199
|
+
kept.committed = { ...kept.committed, handle, props, childHandles: [] };
|
|
2682
3200
|
kept.node = null;
|
|
2683
3201
|
}
|
|
2684
3202
|
handles.push(kept.committed.handle);
|
|
@@ -2747,13 +3265,14 @@ export class Engine {
|
|
|
2747
3265
|
const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props), node);
|
|
2748
3266
|
for (const child of childHandles)
|
|
2749
3267
|
this.fabric.appendChild(handle, child);
|
|
2750
|
-
node.committed = { handle, tag, props, childHandles };
|
|
3268
|
+
node.committed = { handle, tag, props, childHandles, viewName };
|
|
3269
|
+
node.bornIn = this.stats.commits;
|
|
2751
3270
|
this.clearFlags(node);
|
|
2752
3271
|
return handle;
|
|
2753
3272
|
}
|
|
2754
3273
|
/**
|
|
2755
3274
|
* Angular's own unknown-element check does not run in this pipeline, so a `<text>` in a
|
|
2756
|
-
* template that never imported `Text` compiles,
|
|
3275
|
+
* template that never imported `Text` compiles, draws without the component, and looks like a
|
|
2757
3276
|
* layout bug. On the first commit of any known element with no component behind it, say so.
|
|
2758
3277
|
*/
|
|
2759
3278
|
checkClaimed(node) {
|
|
@@ -2764,7 +3283,8 @@ export class Engine {
|
|
|
2764
3283
|
this.reported.add(node.name);
|
|
2765
3284
|
console.error(`[angular-native] <${node.name}> is used in a template that does not import ` +
|
|
2766
3285
|
`${className(node.name)}. Add it to the component's \`imports\` from ` +
|
|
2767
|
-
`'@ng-native/components'; without it the element
|
|
3286
|
+
`'@ng-native/components'; without it the element has none of the component's inputs ` +
|
|
3287
|
+
`or behavior, so a prop written on it stays the text it was written as.`);
|
|
2768
3288
|
}
|
|
2769
3289
|
/**
|
|
2770
3290
|
* The misspelt-prop check. Angular's own "Can't bind to" check needs a DOM to run, so a
|
|
@@ -2775,11 +3295,13 @@ export class Engine {
|
|
|
2775
3295
|
*/
|
|
2776
3296
|
undefinedTokens = new Set();
|
|
2777
3297
|
/** Said once per name: a phone has no inspector to show the declaration struck out. */
|
|
2778
|
-
reportUndefinedToken(name, props) {
|
|
3298
|
+
reportUndefinedToken(name, props, on) {
|
|
2779
3299
|
if (this.undefinedTokens.has(name))
|
|
2780
3300
|
return;
|
|
2781
3301
|
this.undefinedTokens.add(name);
|
|
2782
|
-
|
|
3302
|
+
// A bound declaration has no rule to be found by, so it says which element it is on.
|
|
3303
|
+
const where = on === undefined ? props.join(', ') : `${props.join(', ')}, bound on <${on}>,`;
|
|
3304
|
+
console.warn(`[angular-native] var(${name}) in ${where} names a custom property nothing in` +
|
|
2783
3305
|
` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
|
|
2784
3306
|
` :root or an ancestor, or give the var() a fallback.`);
|
|
2785
3307
|
}
|
|
@@ -3008,6 +3530,18 @@ export class Engine {
|
|
|
3008
3530
|
resolveAsset(value) {
|
|
3009
3531
|
return this.resolveAssetSource(value);
|
|
3010
3532
|
}
|
|
3533
|
+
/**
|
|
3534
|
+
* A colour prop as native takes it. A shape's `fill` or `stroke` the cascade settled from a
|
|
3535
|
+
* token is the solid brush react-native-svg takes, and `none` no paint at all.
|
|
3536
|
+
*/
|
|
3537
|
+
painted(key, value) {
|
|
3538
|
+
if (!BRUSH_PROPS.has(key))
|
|
3539
|
+
return this.color(value);
|
|
3540
|
+
// Already a brush, as the icon wrote it: only one the cascade settled is still text.
|
|
3541
|
+
if (typeof value !== 'string')
|
|
3542
|
+
return value;
|
|
3543
|
+
return value === 'none' ? null : { type: 0, payload: this.color(value) };
|
|
3544
|
+
}
|
|
3011
3545
|
/**
|
|
3012
3546
|
* The host's `processColor`, and the one place a platform colour becomes one.
|
|
3013
3547
|
*
|