mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-26 11:17:04 +08:00
feat(core): css media query support (#10530)
This commit is contained in:

committed by
GitHub

parent
6dd441d6ba
commit
9fd361c2e6
@ -1,7 +1,7 @@
|
||||
import * as TKUnit from '../../tk-unit';
|
||||
import { StyleScope } from '@nativescript/core/ui/styling/style-scope';
|
||||
import * as keyframeAnimation from '@nativescript/core/ui/animation/keyframe-animation';
|
||||
import { CoreTypes } from '@nativescript/core';
|
||||
import { Application, CoreTypes, Screen } from '@nativescript/core';
|
||||
import * as helper from '../../ui-helper';
|
||||
import * as stackModule from '@nativescript/core/ui/layouts/stack-layout';
|
||||
import * as labelModule from '@nativescript/core/ui/label';
|
||||
@ -18,7 +18,6 @@ function createAnimationFromCSS(css: string, name: string): keyframeAnimation.Ke
|
||||
const selector = findSelectorInScope(scope, name);
|
||||
if (selector) {
|
||||
const animation = scope.getAnimations(selector.ruleset)[0];
|
||||
|
||||
return animation;
|
||||
}
|
||||
}
|
||||
@ -40,7 +39,7 @@ export function test_ReadAnimationProperties() {
|
||||
animation-direction: reverse;
|
||||
animation-fill-mode: forwards;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.name, 'first');
|
||||
TKUnit.assertEqual(animation.duration, 4000);
|
||||
@ -56,7 +55,7 @@ export function test_ReadTheAnimationProperty() {
|
||||
`.test {
|
||||
animation: second 0.2s ease-out 1s 2;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.name, 'second');
|
||||
TKUnit.assertEqual(animation.duration, 200);
|
||||
@ -70,42 +69,42 @@ export function test_ReadAnimationCurve() {
|
||||
`.test {
|
||||
animation-timing-function: ease-in;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeIn);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: ease-out;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeOut);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: linear;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.linear);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: ease-in-out;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeInOut);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: spring;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.spring);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: cubic-bezier(0.1, 1.0, 0.5, 0.5);
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
let curve = animation.curve;
|
||||
TKUnit.assert(curve.x1 === 0.1 && curve.y1 === 1.0 && curve.x2 === 0.5 && curve.y2 === 0.5);
|
||||
@ -116,14 +115,14 @@ export function test_ReadIterations() {
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.iterations, 5);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-iteration-count: infinite;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.iterations, Number.POSITIVE_INFINITY);
|
||||
}
|
||||
@ -133,21 +132,21 @@ export function test_ReadFillMode() {
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertFalse(animation.isForwards);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-fill-mode: forwards;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertTrue(animation.isForwards);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-fill-mode: backwards;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertFalse(animation.isForwards);
|
||||
}
|
||||
@ -157,21 +156,21 @@ export function test_ReadDirection() {
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertFalse(animation.isReverse);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-direction: reverse;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertTrue(animation.isReverse);
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-direction: normal;
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertFalse(animation.isReverse);
|
||||
}
|
||||
@ -183,7 +182,7 @@ export function test_ReadKeyframe() {
|
||||
from { background-color: red; }
|
||||
to { background-color: blue; }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assert(animation !== undefined, 'CSS selector was not created!');
|
||||
TKUnit.assertEqual(animation.name, 'test', 'Wrong animation name!');
|
||||
@ -200,7 +199,7 @@ export function test_ReadTransformAllSet() {
|
||||
@keyframes test {
|
||||
to { transform: rotate(10) scaleX(5) translate(100, 200); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
@ -219,7 +218,7 @@ export function test_ReadTransformNone() {
|
||||
@keyframes test {
|
||||
to { transform: none; }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
@ -238,7 +237,7 @@ export function test_ReadScale() {
|
||||
@keyframes test {
|
||||
to { transform: scale(-5, 12.3pt); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -253,7 +252,7 @@ export function test_ReadScaleSingle() {
|
||||
@keyframes test {
|
||||
to { transform: scale(2); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -268,7 +267,7 @@ export function test_ReadScaleXY() {
|
||||
@keyframes test {
|
||||
to { transform: scaleX(5) scaleY(10); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -283,7 +282,7 @@ export function test_ReadScaleX() {
|
||||
@keyframes test {
|
||||
to { transform: scaleX(12.5); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -299,7 +298,7 @@ export function test_ReadScaleY() {
|
||||
@keyframes test {
|
||||
to { transform: scaleY(10); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -315,7 +314,7 @@ export function test_ReadScale3d() {
|
||||
@keyframes test {
|
||||
to { transform: scale3d(10, 20, 30); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -330,7 +329,7 @@ export function test_ReadTranslate() {
|
||||
@keyframes test {
|
||||
to { transform: translate(100, 20); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -345,7 +344,7 @@ export function test_ReadTranslateSingle() {
|
||||
@keyframes test {
|
||||
to { transform: translate(30); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -360,7 +359,7 @@ export function test_ReadTranslateXY() {
|
||||
@keyframes test {
|
||||
to { transform: translateX(5) translateY(10); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -375,7 +374,7 @@ export function test_ReadTranslateX() {
|
||||
@keyframes test {
|
||||
to { transform: translateX(12.5); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -391,7 +390,7 @@ export function test_ReadTranslateY() {
|
||||
@keyframes test {
|
||||
to { transform: translateY(10); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -407,7 +406,7 @@ export function test_ReadTranslate3d() {
|
||||
@keyframes test {
|
||||
to { transform: translate3d(10, 20, 30); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -422,7 +421,7 @@ export function test_ReadRotate() {
|
||||
@keyframes test {
|
||||
to { transform: rotate(5); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -436,7 +435,7 @@ export function test_ReadRotateDeg() {
|
||||
@keyframes test {
|
||||
to { transform: rotate(45deg); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -450,7 +449,7 @@ export function test_ReadRotateRad() {
|
||||
@keyframes test {
|
||||
to { transform: rotate(0.7853981634rad); }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
@ -467,7 +466,7 @@ export function test_ReadAnimationWithUnsortedKeyframes() {
|
||||
40%, 80% { opacity: 0.3; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 6);
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 0);
|
||||
@ -502,7 +501,7 @@ export function test_LoadTwoAnimationsWithTheSameName() {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.5; } /* this should override the previous one */
|
||||
}`,
|
||||
'a'
|
||||
'a',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 0.5);
|
||||
@ -520,7 +519,7 @@ export function test_LoadTwoAnimationsWithTheSameName() {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'a'
|
||||
'a',
|
||||
);
|
||||
|
||||
TKUnit.assertEqual(animation2.keyframes.length, 2);
|
||||
@ -542,6 +541,68 @@ export function test_LoadAnimationProgrammatically() {
|
||||
});
|
||||
}
|
||||
|
||||
export function test_LoadMatchingMediaQueryKeyframeAnimation() {
|
||||
const animation = createAnimationFromCSS(
|
||||
`.a { animation-name: mq1; }
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs}) {
|
||||
@keyframes mq1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
}`,
|
||||
'a',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 1);
|
||||
}
|
||||
|
||||
export function test_IgnoreNonMatchingMediaQueryKeyframe() {
|
||||
const animation = createAnimationFromCSS(
|
||||
`.a { animation-name: mq1; }
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs - 1}) {
|
||||
@keyframes mq1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
}`,
|
||||
'a',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes, null);
|
||||
}
|
||||
|
||||
export function test_LoadMatchingNestedMediaQueryKeyframeAnimation() {
|
||||
const animation = createAnimationFromCSS(
|
||||
`.a { animation-name: mq1; }
|
||||
@media only screen and (orientation: ${Application.orientation()}) {
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs}) {
|
||||
@keyframes mq1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'a',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 1);
|
||||
}
|
||||
|
||||
export function test_IgnoreNonMatchingNestedMediaQueryKeyframe() {
|
||||
const animation = createAnimationFromCSS(
|
||||
`.a { animation-name: mq1; }
|
||||
@media only screen and (orientation: ${Application.orientation()}) {
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs - 1}) {
|
||||
@keyframes mq1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'a',
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes, null);
|
||||
}
|
||||
|
||||
export function test_ExecuteCSSAnimation() {
|
||||
let mainPage = helper.getCurrentPage();
|
||||
mainPage.css = null;
|
||||
@ -615,7 +676,7 @@ export function test_AnimationCurveInKeyframes() {
|
||||
50% { background-color: green; }
|
||||
to { background-color: black; }
|
||||
}`,
|
||||
'test'
|
||||
'test',
|
||||
);
|
||||
|
||||
TKUnit.assertEqual(animation.keyframes[0].curve, CoreTypes.AnimationCurve.linear);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as TKUnit from '../../tk-unit';
|
||||
import { Application, Button, Label, Page, StackLayout, WrapLayout, TabView, TabViewItem, View, Utils, Color, resolveFileNameFromUrl, removeTaggedAdditionalCSS, addTaggedAdditionalCSS, unsetValue, knownFolders } from '@nativescript/core';
|
||||
import { Application, Button, Label, Page, StackLayout, WrapLayout, TabView, TabViewItem, View, Utils, Color, resolveFileNameFromUrl, removeTaggedAdditionalCSS, addTaggedAdditionalCSS, unsetValue, knownFolders, Screen } from '@nativescript/core';
|
||||
import * as helper from '../../ui-helper';
|
||||
import { _evaluateCssCalcExpression } from '@nativescript/core/ui/core/properties';
|
||||
|
||||
@ -464,6 +464,132 @@ export function test_id_and_state_selector() {
|
||||
testButtonPressedStateIsRed(btn);
|
||||
}
|
||||
|
||||
export function test_matching_media_query_selector() {
|
||||
let page = helper.getClearCurrentPage();
|
||||
page.style.color = unsetValue;
|
||||
let btnWithId: Button;
|
||||
let btnWithNoId: Button;
|
||||
|
||||
// >> article-using-matching-media-query-selector
|
||||
page.css = `@media only screen and (max-width: ${Screen.mainScreen.widthDIPs}) {
|
||||
Button#myButton {
|
||||
color: red;
|
||||
}
|
||||
}`;
|
||||
|
||||
//// Will be styled
|
||||
btnWithId = new Button();
|
||||
btnWithId.id = 'myButton';
|
||||
|
||||
//// Won't be styled
|
||||
btnWithNoId = new Button();
|
||||
// << article-using-matching-media-query-selector
|
||||
|
||||
const stack = new StackLayout();
|
||||
page.content = stack;
|
||||
stack.addChild(btnWithId);
|
||||
stack.addChild(btnWithNoId);
|
||||
|
||||
helper.assertViewColor(btnWithId, '#FF0000');
|
||||
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
|
||||
}
|
||||
|
||||
export function test_non_matching_media_query_selector() {
|
||||
let page = helper.getClearCurrentPage();
|
||||
page.style.color = unsetValue;
|
||||
let btnWithId: Button;
|
||||
let btnWithNoId: Button;
|
||||
|
||||
// >> article-using-non-matching-media-query-selector
|
||||
page.css = `@media only screen and (max-width: ${Screen.mainScreen.widthDIPs - 1}) {
|
||||
Button#myButton {
|
||||
color: red;
|
||||
}
|
||||
}`;
|
||||
|
||||
//// Will be styled
|
||||
btnWithId = new Button();
|
||||
btnWithId.id = 'myButton';
|
||||
|
||||
//// Won't be styled
|
||||
btnWithNoId = new Button();
|
||||
// << article-using-non-matching-media-query-selector
|
||||
|
||||
const stack = new StackLayout();
|
||||
page.content = stack;
|
||||
stack.addChild(btnWithId);
|
||||
stack.addChild(btnWithNoId);
|
||||
|
||||
TKUnit.assert(btnWithId.style.color === undefined, 'Color should not have a value');
|
||||
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
|
||||
}
|
||||
|
||||
export function test_matching_nested_media_query_selector() {
|
||||
let page = helper.getClearCurrentPage();
|
||||
page.style.color = unsetValue;
|
||||
let btnWithId: Button;
|
||||
let btnWithNoId: Button;
|
||||
|
||||
// >> article-using-matching-nested-media-query-selector
|
||||
page.css = `
|
||||
@media only screen and (orientation: ${Application.orientation()}) {
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs}) {
|
||||
Button#myButton {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
//// Will be styled
|
||||
btnWithId = new Button();
|
||||
btnWithId.id = 'myButton';
|
||||
|
||||
//// Won't be styled
|
||||
btnWithNoId = new Button();
|
||||
// << article-using-matching-nested-media-query-selector
|
||||
|
||||
const stack = new StackLayout();
|
||||
page.content = stack;
|
||||
stack.addChild(btnWithId);
|
||||
stack.addChild(btnWithNoId);
|
||||
|
||||
helper.assertViewColor(btnWithId, '#FF0000');
|
||||
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
|
||||
}
|
||||
|
||||
export function test_non_matching_nested_media_query_selector() {
|
||||
let page = helper.getClearCurrentPage();
|
||||
page.style.color = unsetValue;
|
||||
let btnWithId: Button;
|
||||
let btnWithNoId: Button;
|
||||
|
||||
// >> article-using-non-matching-nested-media-query-selector
|
||||
page.css = `
|
||||
@media only screen and (orientation: ${Application.orientation()}) {
|
||||
@media only screen and (max-width: ${Screen.mainScreen.widthDIPs - 1}) {
|
||||
Button#myButton {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
//// Will be styled
|
||||
btnWithId = new Button();
|
||||
btnWithId.id = 'myButton';
|
||||
|
||||
//// Won't be styled
|
||||
btnWithNoId = new Button();
|
||||
// << article-using-non-matching-nested-media-query-selector
|
||||
|
||||
const stack = new StackLayout();
|
||||
page.content = stack;
|
||||
stack.addChild(btnWithId);
|
||||
stack.addChild(btnWithNoId);
|
||||
|
||||
TKUnit.assert(btnWithId.style.color === undefined, 'Color should not have a value');
|
||||
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
|
||||
}
|
||||
|
||||
export function test_restore_original_values_when_state_is_changed() {
|
||||
let page = helper.getClearCurrentPage();
|
||||
page.style.color = unsetValue;
|
||||
|
@ -38,6 +38,7 @@ export function pageLoaded(args: EventData) {
|
||||
examples.set('repeater', 'repeater/main-page');
|
||||
examples.set('date-picker', 'date-picker/date-picker-page');
|
||||
examples.set('nested-frames', 'nested-frames/main-page');
|
||||
examples.set('media-queries', 'media-queries/main-page');
|
||||
examples.set('screen-qualifiers', 'screen-qualifiers/main-page');
|
||||
page.bindingContext = new MainPageViewModel(wrapLayout, examples);
|
||||
|
||||
|
19
apps/ui/src/media-queries/main-page.css
Normal file
19
apps/ui/src/media-queries/main-page.css
Normal file
@ -0,0 +1,19 @@
|
||||
.orientation-btn {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.orientation-btn {
|
||||
background-color: pink;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-mode-btn {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.theme-mode-btn {
|
||||
background-color: pink;
|
||||
}
|
||||
}
|
34
apps/ui/src/media-queries/main-page.ts
Normal file
34
apps/ui/src/media-queries/main-page.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button } from '@nativescript/core';
|
||||
import { EventData } from '@nativescript/core/data/observable';
|
||||
import { Observable } from '@nativescript/core/data/observable';
|
||||
import { Page } from '@nativescript/core/ui/page';
|
||||
|
||||
export function onNavigatedTo(args: EventData) {
|
||||
const page = <Page>args.object;
|
||||
|
||||
const orientationButton: Button = page.getViewById('orientation-match-media-btn');
|
||||
if (orientationButton) {
|
||||
const mq = matchMedia('(orientation: portrait)');
|
||||
let mode = mq.matches ? 'portrait' : 'landscape';
|
||||
|
||||
orientationButton.text = `I' m in ${mode} mode!`;
|
||||
|
||||
mq.addEventListener('change', (event: MediaQueryListEvent) => {
|
||||
mode = event.matches ? 'portrait' : 'landscape';
|
||||
orientationButton.text = `I' m in ${mode} mode!`;
|
||||
});
|
||||
}
|
||||
|
||||
const themeModeButton: Button = page.getViewById('theme-match-media-btn');
|
||||
if (themeModeButton) {
|
||||
const mq = matchMedia('(prefers-color-scheme: light)');
|
||||
let mode = mq.matches ? 'light' : 'dark';
|
||||
|
||||
themeModeButton.text = `I' m in ${mode} mode!`;
|
||||
|
||||
mq.addEventListener('change', (event: MediaQueryListEvent) => {
|
||||
mode = event.matches ? 'light' : 'dark';
|
||||
themeModeButton.text = `I' m in ${mode} mode!`;
|
||||
});
|
||||
}
|
||||
}
|
25
apps/ui/src/media-queries/main-page.xml
Normal file
25
apps/ui/src/media-queries/main-page.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatedTo="onNavigatedTo">
|
||||
<ActionBar>
|
||||
<Label text="Media Queries"></Label>
|
||||
</ActionBar>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout padding="20">
|
||||
<Label text="Orientation change" fontSize="16" fontWeight="bold" marginBottom="12"/>
|
||||
<StackLayout>
|
||||
<Label text="CSS media query - color change" fontSize="14"/>
|
||||
<Button class="orientation-btn" text="My color is different after orientation!"/>
|
||||
<Label text="The matchMedia API - text change" fontSize="14"/>
|
||||
<Button id="orientation-match-media-btn"/>
|
||||
</StackLayout>
|
||||
|
||||
<Label text="Theme mode change" fontSize="16" fontWeight="bold" marginTop="12" marginBottom="12"/>
|
||||
<StackLayout>
|
||||
<Label text="CSS media query - color change" fontSize="14"/>
|
||||
<Button class="theme-mode-btn" text="My color is different after theme change!"/>
|
||||
<Label text="The matchMedia API - text change" fontSize="14"/>
|
||||
<Button id="theme-match-media-btn"/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</Page>
|
@ -2,7 +2,7 @@ import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-h
|
||||
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
||||
import { CoreTypes } from '../core-types';
|
||||
import { CSSUtils } from '../css/system-classes';
|
||||
import { Device } from '../platform';
|
||||
import { Device, Screen } from '../platform';
|
||||
import { profile } from '../profiling';
|
||||
import { Trace } from '../trace';
|
||||
import { Builder } from '../ui/builder';
|
||||
@ -220,13 +220,17 @@ export class ApplicationCommon {
|
||||
* @param rootView
|
||||
* @param cssClasses
|
||||
* @param newCssClass
|
||||
* @param skipCssUpdate
|
||||
*/
|
||||
applyCssClass(rootView: View, cssClasses: string[], newCssClass: string): void {
|
||||
applyCssClass(rootView: View, cssClasses: string[], newCssClass: string, skipCssUpdate: boolean = false): void {
|
||||
if (!rootView.cssClasses.has(newCssClass)) {
|
||||
cssClasses.forEach((cssClass) => this.removeCssClass(rootView, cssClass));
|
||||
this.addCssClass(rootView, newCssClass);
|
||||
this.increaseStyleScopeApplicationCssSelectorVersion(rootView);
|
||||
|
||||
if (!skipCssUpdate) {
|
||||
rootView._onCssStateChange();
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
const rootCssClasses = Array.from(rootView.cssClasses);
|
||||
@ -457,7 +461,13 @@ export class ApplicationCommon {
|
||||
if (this._orientation === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._orientation = value;
|
||||
|
||||
// Update metrics early enough regardless of the existence of root view
|
||||
// Also, CSS will use the correct size values during update trigger
|
||||
Screen.mainScreen._updateMetrics();
|
||||
|
||||
this.orientationChanged(this.getRootView(), value);
|
||||
this.notify(<OrientationChangedEventData>{
|
||||
eventName: this.orientationChangedEvent,
|
||||
@ -478,12 +488,18 @@ export class ApplicationCommon {
|
||||
}
|
||||
|
||||
const newOrientationCssClass = `${CSSUtils.CLASS_PREFIX}${newOrientation}`;
|
||||
this.applyCssClass(rootView, ORIENTATION_CSS_CLASSES, newOrientationCssClass);
|
||||
this.applyCssClass(rootView, ORIENTATION_CSS_CLASSES, newOrientationCssClass, true);
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => {
|
||||
this.applyCssClass(rootModalView, ORIENTATION_CSS_CLASSES, newOrientationCssClass);
|
||||
this.applyCssClass(rootModalView, ORIENTATION_CSS_CLASSES, newOrientationCssClass, true);
|
||||
|
||||
// Trigger state change for root modal view classes and media queries
|
||||
rootModalView._onCssStateChange();
|
||||
});
|
||||
|
||||
// Trigger state change for root view classes and media queries
|
||||
rootView._onCssStateChange();
|
||||
}
|
||||
|
||||
getNativeApplication(): any {
|
||||
@ -545,12 +561,18 @@ export class ApplicationCommon {
|
||||
}
|
||||
|
||||
const newSystemAppearanceCssClass = `${CSSUtils.CLASS_PREFIX}${newSystemAppearance}`;
|
||||
this.applyCssClass(rootView, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass);
|
||||
this.applyCssClass(rootView, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass, true);
|
||||
|
||||
const rootModalViews = rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => {
|
||||
this.applyCssClass(rootModalView as View, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass);
|
||||
this.applyCssClass(rootModalView as View, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass, true);
|
||||
|
||||
// Trigger state change for root modal view classes and media queries
|
||||
rootModalView._onCssStateChange();
|
||||
});
|
||||
|
||||
// Trigger state change for root view classes and media queries
|
||||
rootView._onCssStateChange();
|
||||
}
|
||||
|
||||
private _inBackground: boolean = false;
|
||||
|
@ -231,7 +231,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
}
|
||||
|
||||
get rootController() {
|
||||
return this.window.rootViewController;
|
||||
return this.window?.rootViewController;
|
||||
}
|
||||
|
||||
get nativeApp() {
|
||||
|
27
packages/core/css-mediaquery/LICENSE
Normal file
27
packages/core/css-mediaquery/LICENSE
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright 2014 Yahoo! Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the Yahoo! Inc. nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
118
packages/core/css-mediaquery/index.spec.ts
Normal file
118
packages/core/css-mediaquery/index.spec.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { MediaQueryType, matchQuery, parseQuery } from '.';
|
||||
|
||||
describe('css-mediaquery', () => {
|
||||
describe('parseQuery', () => {
|
||||
it('should parse media queries without expressions', () => {
|
||||
expect(parseQuery('screen')).toEqual([
|
||||
{
|
||||
inverse: false,
|
||||
type: MediaQueryType.screen,
|
||||
features: [],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(parseQuery('not screen')).toEqual([
|
||||
{
|
||||
inverse: true,
|
||||
type: MediaQueryType.screen,
|
||||
features: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw a SyntaxError when a media query is invalid', () => {
|
||||
expect(() => parseQuery('some crap')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('48em')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('screen and crap')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('screen and (48em)')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('screen and (foo:)')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('()')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('(foo) (bar)')).toThrow(SyntaxError);
|
||||
expect(() => parseQuery('(foo:) and (bar)')).toThrow(SyntaxError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchQuery', () => {
|
||||
describe('Equality check', () => {
|
||||
it('orientation: should return true for a correct match (===)', () => {
|
||||
expect(matchQuery('(orientation: portrait)', { orientation: 'portrait' })).toBe(true);
|
||||
});
|
||||
|
||||
it('orientation: should return false for an incorrect match (===)', () => {
|
||||
expect(matchQuery('(orientation: landscape)', { orientation: 'portrait' })).toBe(false);
|
||||
});
|
||||
|
||||
it('prefers-color-scheme: should return true for a correct match (===)', () => {
|
||||
expect(matchQuery('(prefers-color-scheme: dark)', { 'prefers-color-scheme': 'dark' })).toBe(true);
|
||||
});
|
||||
|
||||
it('prefers-color-scheme: should return false for an incorrect match (===)', () => {
|
||||
expect(matchQuery('(prefers-color-scheme: light)', { 'prefers-color-scheme': 'dark' })).toBe(false);
|
||||
});
|
||||
|
||||
it('width: should return true for a correct match', () => {
|
||||
expect(matchQuery('(width: 800px)', { width: 800 })).toBe(true);
|
||||
});
|
||||
|
||||
it('width: should return false for an incorrect match', () => {
|
||||
expect(matchQuery('(width: 800px)', { width: 900 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type', () => {
|
||||
it('should return true for a correct match', () => {
|
||||
expect(matchQuery('screen', { type: MediaQueryType.screen })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an incorrect match', () => {
|
||||
expect(
|
||||
matchQuery('screen and (orientation: portrait)', {
|
||||
type: MediaQueryType.print,
|
||||
orientation: 'portrait',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a media query without a type when type is specified in the value object', () => {
|
||||
expect(matchQuery('(min-width: 500px)', { type: MediaQueryType.screen })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for a media query without a type when type is not specified in the value object', () => {
|
||||
expect(matchQuery('(min-width: 500px)', { width: 700 })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Not', () => {
|
||||
it('should return false when theres a match on a `not` query', () => {
|
||||
expect(
|
||||
matchQuery('not screen and (orientation: portrait)', {
|
||||
type: MediaQueryType.screen,
|
||||
orientation: 'landscape',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should not disrupt an OR query', () => {
|
||||
expect(
|
||||
matchQuery('not screen and (color), screen and (min-height: 48em)', {
|
||||
type: MediaQueryType.screen,
|
||||
height: 1000,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for when type === all', () => {
|
||||
expect(
|
||||
matchQuery('not all and (min-width: 48em)', {
|
||||
type: MediaQueryType.all,
|
||||
width: 1000,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for inverted value', () => {
|
||||
expect(matchQuery('not screen and (min-width: 48px)', { width: 24 })).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
176
packages/core/css-mediaquery/index.ts
Normal file
176
packages/core/css-mediaquery/index.ts
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
|
||||
Copyrights licensed under the New BSD License.
|
||||
See the accompanying LICENSE file for terms.
|
||||
*/
|
||||
|
||||
// https://github.com/ericf/css-mediaquery
|
||||
|
||||
import { Trace } from '../trace';
|
||||
import { Length } from '../ui/styling/style-properties';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const RE_MEDIA_QUERY = /^(?:(only|not)?\s*([_a-z][_a-z0-9-]*)|(\([^\)]+\)))(?:\s*and\s*(.*))?$/i,
|
||||
RE_MQ_EXPRESSION = /^\(\s*([_a-z-][_a-z0-9-]*)\s*(?:\:\s*([^\)]+))?\s*\)$/,
|
||||
RE_MQ_FEATURE = /^(?:(min|max)-)?(.+)/,
|
||||
RE_LENGTH_UNIT = /(em|rem|px|cm|mm|in|pt|pc)?\s*$/,
|
||||
RE_RESOLUTION_UNIT = /(dpi|dpcm|dppx)?\s*$/;
|
||||
|
||||
export enum MediaQueryType {
|
||||
all = 'all',
|
||||
print = 'print',
|
||||
screen = 'screen',
|
||||
}
|
||||
|
||||
export type MediaQueryProperties = 'width' | 'height' | 'device-width' | 'device-height' | 'orientation' | 'prefers-color-scheme';
|
||||
|
||||
export interface MediaQueryEnvironmentParams {
|
||||
type?: MediaQueryType;
|
||||
width?: number;
|
||||
height?: number;
|
||||
'device-width'?: number;
|
||||
'device-height'?: number;
|
||||
orientation?: string;
|
||||
'prefers-color-scheme'?: string;
|
||||
}
|
||||
|
||||
export interface MediaQueryExpression {
|
||||
inverse: boolean;
|
||||
type: MediaQueryType;
|
||||
features: MediaQueryFeature[];
|
||||
}
|
||||
|
||||
export interface MediaQueryFeature {
|
||||
modifier: string;
|
||||
property: MediaQueryProperties | string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function matchQuery(mediaQuery: string, values: MediaQueryEnvironmentParams): boolean {
|
||||
const expressions = parseQuery(mediaQuery);
|
||||
|
||||
return expressions.some((query) => {
|
||||
const { type, inverse, features } = query;
|
||||
|
||||
// Either the parsed or specified `type` is "all", or the types must be
|
||||
// equal for a match.
|
||||
const typeMatch = query.type === 'all' || values.type === query.type;
|
||||
|
||||
// Quit early when `type` doesn't match, but take "not" into account
|
||||
if ((typeMatch && inverse) || !(typeMatch || inverse)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expressionsMatch = features.every((feature) => {
|
||||
const value: any = values[feature.property];
|
||||
|
||||
// Missing or falsy values don't match
|
||||
if (!value && value !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (feature.property) {
|
||||
case 'orientation':
|
||||
case 'prefers-color-scheme':
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.toLowerCase() === feature.value.toLowerCase();
|
||||
default: {
|
||||
// Numeric properties
|
||||
let numVal: number;
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
Trace.write(`Unknown CSS media query feature property: '${feature.property}' on '${query}'`, Trace.categories.MediaQuery, Trace.messageType.warn);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (feature.property) {
|
||||
case 'width':
|
||||
case 'height':
|
||||
case 'device-width':
|
||||
case 'device-height': {
|
||||
numVal = Length.toDevicePixels(Length.parse(feature.value), 0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Trace.write(`Unknown CSS media query feature property: '${feature.property}' on '${query}'`, Trace.categories.MediaQuery, Trace.messageType.warn);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (feature.modifier) {
|
||||
case 'min':
|
||||
return value >= numVal;
|
||||
case 'max':
|
||||
return value <= numVal;
|
||||
default:
|
||||
return value === numVal;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (expressionsMatch && !inverse) || (!expressionsMatch && inverse);
|
||||
});
|
||||
}
|
||||
|
||||
export function parseQuery(mediaQuery: string): MediaQueryExpression[] {
|
||||
const mediaQueryStrings = mediaQuery.split(',');
|
||||
|
||||
return mediaQueryStrings.map((query) => {
|
||||
query = query.trim();
|
||||
|
||||
const captures = query.match(RE_MEDIA_QUERY);
|
||||
|
||||
// Media query must be valid
|
||||
if (!captures) {
|
||||
throw new SyntaxError(`Invalid CSS media query: '${query}'`);
|
||||
}
|
||||
|
||||
const modifier = captures[1];
|
||||
const type = captures[2];
|
||||
const featureString = ((captures[3] || '') + (captures[4] || '')).trim();
|
||||
|
||||
const expression: MediaQueryExpression = {
|
||||
inverse: !!modifier && modifier.toLowerCase() === 'not',
|
||||
type: MediaQueryType[type ? type.toLowerCase() : 'all'] ?? 'all',
|
||||
features: [],
|
||||
};
|
||||
|
||||
// Check for media query features
|
||||
if (!featureString) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
// Split features string into a list
|
||||
const features = featureString.match(/\([^\)]+\)/g);
|
||||
|
||||
// Media query must be valid
|
||||
if (!features) {
|
||||
throw new SyntaxError(`Invalid CSS media query features: '${featureString}' on '${query}'`);
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
const captures = feature.match(RE_MQ_EXPRESSION);
|
||||
|
||||
// Media query must be valid
|
||||
if (!captures) {
|
||||
throw new SyntaxError(`Invalid CSS media query feature: '${feature}' on '${query}'`);
|
||||
}
|
||||
|
||||
const featureData = captures[1].toLowerCase().match(RE_MQ_FEATURE);
|
||||
|
||||
expression.features.push({
|
||||
modifier: featureData[1],
|
||||
property: featureData[2],
|
||||
value: captures[2],
|
||||
});
|
||||
}
|
||||
|
||||
return expression;
|
||||
});
|
||||
}
|
16
packages/core/css/reworkcss.d.ts
vendored
16
packages/core/css/reworkcss.d.ts
vendored
@ -4,7 +4,7 @@ export interface Position {
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
type: 'rule' | 'keyframes' | 'declaration' | 'import';
|
||||
type: 'rule' | 'keyframes' | 'declaration' | 'import' | 'media';
|
||||
position: Position;
|
||||
}
|
||||
|
||||
@ -18,8 +18,22 @@ export interface Rule extends Node {
|
||||
declarations: Declaration[];
|
||||
}
|
||||
|
||||
export type AtRule = KeyFrames | Media;
|
||||
|
||||
export interface Keyframes extends Rule {
|
||||
name: string;
|
||||
vendor?: string;
|
||||
keyframes?: Array<KeyFrame>;
|
||||
}
|
||||
|
||||
export interface KeyFrame extends Node {
|
||||
values: string[];
|
||||
declarations: Array<Declaration>;
|
||||
}
|
||||
|
||||
export interface Media extends Node {
|
||||
media: string;
|
||||
rules: Array<Rule | AtRule>;
|
||||
}
|
||||
|
||||
export interface StyleSheet {
|
||||
|
@ -307,6 +307,9 @@ export function initGlobal() {
|
||||
global.registerModule('animation', () => require('../animation-frame'));
|
||||
installPolyfills('animation', ['requestAnimationFrame', 'cancelAnimationFrame']);
|
||||
|
||||
global.registerModule('media-query-list', () => require('../media-query-list'));
|
||||
installPolyfills('media-query-list', ['matchMedia', 'MediaQueryList']);
|
||||
|
||||
global.registerModule('ui-dialogs', () => require('../ui/dialogs'));
|
||||
installPolyfills('ui-dialogs', ['alert', 'confirm', 'prompt', 'login', 'action']);
|
||||
|
||||
|
@ -48,9 +48,11 @@ global.interop = {
|
||||
bool: {},
|
||||
},
|
||||
};
|
||||
// global.UIApplication = {
|
||||
|
||||
// }
|
||||
global.UIApplication = {
|
||||
sharedApplication: {
|
||||
statusBarOrientation: 1, // Portrait by default
|
||||
},
|
||||
};
|
||||
global.UIDevice = {
|
||||
currentDevice: {
|
||||
systemVersion: '13.0',
|
||||
@ -59,6 +61,16 @@ global.UIDevice = {
|
||||
global.UIScreen = {
|
||||
mainScreen: {
|
||||
scale: 1,
|
||||
bounds: {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: {
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const cgColors = { CGColor: 1 };
|
||||
|
74
packages/core/media-query-list/index.spec.ts
Normal file
74
packages/core/media-query-list/index.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { matchMedia, checkIfMediaQueryMatches, MediaQueryList } from '.';
|
||||
import { Screen } from '../platform';
|
||||
|
||||
describe('media-query-list', () => {
|
||||
const { widthDIPs } = Screen.mainScreen;
|
||||
|
||||
describe('checkIfMediaQueryMatches', () => {
|
||||
it('should return true for a correct match', () => {
|
||||
expect(checkIfMediaQueryMatches(`only screen and (max-width: ${widthDIPs})`)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an incorrect match', () => {
|
||||
expect(checkIfMediaQueryMatches(`only screen and (max-width: ${widthDIPs - 1})`)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchMedia', () => {
|
||||
it('should return a MediaQueryList that matches the css media query', () => {
|
||||
const matchMediaWrapper = () => matchMedia(`only screen and (max-width: ${widthDIPs})`);
|
||||
|
||||
expect(matchMediaWrapper).not.toThrow();
|
||||
|
||||
const mql = matchMediaWrapper();
|
||||
|
||||
expect(mql).toBeInstanceOf(MediaQueryList);
|
||||
expect(() => mql.matches).not.toThrow();
|
||||
expect(mql.matches).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a MediaQueryList that does not match the css media query', () => {
|
||||
const matchMediaWrapper = () => matchMedia(`only screen and (max-width: ${widthDIPs - 1})`);
|
||||
|
||||
expect(matchMediaWrapper).not.toThrow();
|
||||
|
||||
const mql = matchMediaWrapper();
|
||||
|
||||
expect(mql).toBeInstanceOf(MediaQueryList);
|
||||
expect(() => mql.matches).not.toThrow();
|
||||
expect(mql.matches).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MediaQueryList', () => {
|
||||
it('should throw when calling constructor', () => {
|
||||
expect(() => new MediaQueryList()).toThrow(new TypeError('Illegal constructor'));
|
||||
});
|
||||
|
||||
it('should throw when accessing matches and media getters', () => {
|
||||
const error = new TypeError('Illegal invocation');
|
||||
|
||||
expect(() => MediaQueryList.prototype.matches).toThrow(error);
|
||||
expect(() => MediaQueryList.prototype.media).toThrow(error);
|
||||
});
|
||||
|
||||
it('should throw when accessing or modifying onchange event', () => {
|
||||
const error = new TypeError('Illegal invocation');
|
||||
|
||||
expect(() => MediaQueryList.prototype.onchange).toThrow(error);
|
||||
expect(() => {
|
||||
MediaQueryList.prototype.onchange = null;
|
||||
}).toThrow(error);
|
||||
});
|
||||
|
||||
it('should throw when adding or removing event listeners', () => {
|
||||
const eventCallback = (data) => {};
|
||||
const error = new TypeError('Illegal invocation');
|
||||
|
||||
expect(() => MediaQueryList.prototype.addEventListener('change', eventCallback)).toThrow(error);
|
||||
expect(() => MediaQueryList.prototype.removeEventListener('change', eventCallback)).toThrow(error);
|
||||
expect(() => MediaQueryList.prototype.addListener(eventCallback)).toThrow(error);
|
||||
expect(() => MediaQueryList.prototype.removeListener(eventCallback)).toThrow(error);
|
||||
});
|
||||
});
|
||||
});
|
214
packages/core/media-query-list/index.ts
Normal file
214
packages/core/media-query-list/index.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { EventData, Observable } from '../data/observable';
|
||||
import { Screen } from '../platform';
|
||||
import { Application, ApplicationEventData } from '../application';
|
||||
import { matchQuery, MediaQueryType } from '../css-mediaquery';
|
||||
import { Trace } from '../trace';
|
||||
|
||||
const mediaQueryLists: MediaQueryListImpl[] = [];
|
||||
const applicationEvents: string[] = [Application.orientationChangedEvent, Application.systemAppearanceChangedEvent];
|
||||
|
||||
// In browser, developers cannot create MediaQueryList instances without calling matchMedia
|
||||
let isMediaInitializationEnabled: boolean = false;
|
||||
|
||||
function toggleApplicationEventListeners(toAdd: boolean) {
|
||||
for (const eventName of applicationEvents) {
|
||||
if (toAdd) {
|
||||
Application.on(eventName, onDeviceChange);
|
||||
} else {
|
||||
Application.off(eventName, onDeviceChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDeviceChange(args: ApplicationEventData) {
|
||||
for (const mql of mediaQueryLists) {
|
||||
const matches = checkIfMediaQueryMatches(mql.media);
|
||||
if (mql.matches !== matches) {
|
||||
mql._matches = matches;
|
||||
|
||||
mql.notify({
|
||||
eventName: MediaQueryListImpl.changeEvent,
|
||||
object: mql,
|
||||
matches: mql.matches,
|
||||
media: mql.media,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfMediaQueryMatches(mediaQueryString: string): boolean {
|
||||
const { widthPixels, heightPixels } = Screen.mainScreen;
|
||||
|
||||
let matches: boolean;
|
||||
|
||||
try {
|
||||
matches = matchQuery(mediaQueryString, {
|
||||
type: MediaQueryType.screen,
|
||||
width: widthPixels,
|
||||
height: heightPixels,
|
||||
'device-width': widthPixels,
|
||||
'device-height': heightPixels,
|
||||
orientation: Application.orientation(),
|
||||
'prefers-color-scheme': Application.systemAppearance(),
|
||||
});
|
||||
} catch (err) {
|
||||
matches = false;
|
||||
Trace.write(err, Trace.categories.MediaQuery, Trace.messageType.error);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function matchMedia(mediaQueryString: string): MediaQueryListImpl {
|
||||
isMediaInitializationEnabled = true;
|
||||
const mediaQueryList = new MediaQueryListImpl();
|
||||
isMediaInitializationEnabled = false;
|
||||
|
||||
mediaQueryList._media = mediaQueryString;
|
||||
mediaQueryList._matches = checkIfMediaQueryMatches(mediaQueryString);
|
||||
return mediaQueryList;
|
||||
}
|
||||
|
||||
class MediaQueryListImpl extends Observable implements MediaQueryList {
|
||||
public static readonly changeEvent = 'change';
|
||||
|
||||
public _media: string;
|
||||
public _matches: boolean;
|
||||
|
||||
private _onChange: (this: MediaQueryList, ev: MediaQueryListEvent) => any;
|
||||
private mediaQueryChangeListeners: Map<(this: MediaQueryList, ev: MediaQueryListEvent) => any, (data: EventData) => void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!isMediaInitializationEnabled) {
|
||||
throw new TypeError('Illegal constructor');
|
||||
}
|
||||
|
||||
Object.defineProperties(this, {
|
||||
_media: {
|
||||
writable: true,
|
||||
},
|
||||
_matches: {
|
||||
writable: true,
|
||||
},
|
||||
_onChange: {
|
||||
writable: true,
|
||||
value: null,
|
||||
},
|
||||
mediaQueryChangeListeners: {
|
||||
value: new Map<(this: MediaQueryList, ev: MediaQueryListEvent) => any, (data: EventData) => void>(),
|
||||
},
|
||||
_throwInvocationError: {
|
||||
value: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get media(): string {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
return this._media;
|
||||
}
|
||||
|
||||
get matches(): boolean {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
return this._matches;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
const hasChangeListeners = this.hasListeners(MediaQueryListImpl.changeEvent);
|
||||
|
||||
// Call super method first since it throws in the case of bad parameters
|
||||
super.addEventListener(eventName, callback, thisArg);
|
||||
|
||||
if (eventName === MediaQueryListImpl.changeEvent && !hasChangeListeners) {
|
||||
mediaQueryLists.push(this);
|
||||
|
||||
if (mediaQueryLists.length === 1) {
|
||||
toggleApplicationEventListeners(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
public removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
// Call super method first since it throws in the case of bad parameters
|
||||
super.removeEventListener(eventName, callback, thisArg);
|
||||
|
||||
if (eventName === MediaQueryListImpl.changeEvent) {
|
||||
const hasChangeListeners = this.hasListeners(MediaQueryListImpl.changeEvent);
|
||||
|
||||
if (!hasChangeListeners) {
|
||||
const index = mediaQueryLists.indexOf(this);
|
||||
if (index >= 0) {
|
||||
mediaQueryLists.splice(index, 1);
|
||||
|
||||
if (!mediaQueryLists.length) {
|
||||
toggleApplicationEventListeners(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addListener(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
// This kind of implementation helps maintain listener registration order
|
||||
// regardless of using the deprecated methods or property onchange
|
||||
const wrapperCallback = (data) => {
|
||||
callback.call(this, <MediaQueryListEvent>{
|
||||
matches: this.matches,
|
||||
media: this.media,
|
||||
});
|
||||
};
|
||||
|
||||
// Call this method first since it throws in the case of bad parameters
|
||||
this.addEventListener(MediaQueryListImpl.changeEvent, wrapperCallback);
|
||||
this.mediaQueryChangeListeners.set(callback, wrapperCallback);
|
||||
}
|
||||
|
||||
removeListener(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
if (this.mediaQueryChangeListeners.has(callback)) {
|
||||
// Call this method first since it throws in the case of bad parameters
|
||||
this.removeEventListener(MediaQueryListImpl.changeEvent, this.mediaQueryChangeListeners.get(callback));
|
||||
this.mediaQueryChangeListeners.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
public get onchange(): (this: MediaQueryList, ev: MediaQueryListEvent) => any {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
return this._onChange;
|
||||
}
|
||||
|
||||
public set onchange(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any) {
|
||||
this._throwInvocationError?.();
|
||||
|
||||
// Remove old listener if any
|
||||
if (this._onChange) {
|
||||
this.removeListener(this._onChange);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
this.addListener(callback);
|
||||
}
|
||||
|
||||
this._onChange = callback;
|
||||
}
|
||||
|
||||
private _throwInvocationError() {
|
||||
throw new TypeError('Illegal invocation');
|
||||
}
|
||||
}
|
||||
|
||||
export { matchMedia, MediaQueryListImpl as MediaQueryList, checkIfMediaQueryMatches };
|
@ -1,64 +1,10 @@
|
||||
/* tslint:disable:class-name */
|
||||
import { Application } from '../application';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
import { platformNames } from './common';
|
||||
import { Application } from '../../application';
|
||||
import { SDK_VERSION } from '../../utils/constants';
|
||||
import { platformNames } from '../common';
|
||||
import { Screen } from '../screen';
|
||||
|
||||
const MIN_TABLET_PIXELS = 600;
|
||||
|
||||
export * from './common';
|
||||
|
||||
class MainScreen {
|
||||
private _metrics: android.util.DisplayMetrics;
|
||||
|
||||
private reinitMetrics(): void {
|
||||
if (!this._metrics) {
|
||||
this._metrics = new android.util.DisplayMetrics();
|
||||
}
|
||||
this.initMetrics();
|
||||
}
|
||||
|
||||
private initMetrics(): void {
|
||||
const nativeApp = Application.android.getNativeApplication();
|
||||
nativeApp.getSystemService(android.content.Context.WINDOW_SERVICE).getDefaultDisplay().getRealMetrics(this._metrics);
|
||||
}
|
||||
|
||||
private get metrics(): android.util.DisplayMetrics {
|
||||
if (!this._metrics) {
|
||||
// NOTE: This will be memory leak but we MainScreen is singleton
|
||||
Application.on('cssChanged', this.reinitMetrics, this);
|
||||
Application.on(Application.orientationChangedEvent, this.reinitMetrics, this);
|
||||
|
||||
this._metrics = new android.util.DisplayMetrics();
|
||||
this.initMetrics();
|
||||
}
|
||||
|
||||
return this._metrics;
|
||||
}
|
||||
|
||||
get widthPixels(): number {
|
||||
return this.metrics.widthPixels;
|
||||
}
|
||||
get heightPixels(): number {
|
||||
return this.metrics.heightPixels;
|
||||
}
|
||||
get scale(): number {
|
||||
return this.metrics.density;
|
||||
}
|
||||
get widthDIPs(): number {
|
||||
return this.metrics.widthPixels / this.metrics.density;
|
||||
}
|
||||
get heightDIPs(): number {
|
||||
return this.metrics.heightPixels / this.metrics.density;
|
||||
}
|
||||
}
|
||||
|
||||
export class Screen {
|
||||
static mainScreen = new MainScreen();
|
||||
}
|
||||
|
||||
// This retains compatibility with NS6
|
||||
export const screen = Screen;
|
||||
|
||||
class DeviceRef {
|
||||
private _manufacturer: string;
|
||||
private _model: string;
|
70
packages/core/platform/device/index.d.ts
vendored
Normal file
70
packages/core/platform/device/index.d.ts
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* An object containing device specific information.
|
||||
*/
|
||||
export interface IDevice {
|
||||
/**
|
||||
* Gets the manufacturer of the device.
|
||||
* For example: "Apple" or "HTC" or "Samsung".
|
||||
*/
|
||||
manufacturer: string;
|
||||
|
||||
/**
|
||||
* Gets the model of the device.
|
||||
* For example: "Nexus 5" or "iPhone".
|
||||
*/
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* Gets the OS of the device.
|
||||
* For example: "Android" or "iOS".
|
||||
*/
|
||||
os: string;
|
||||
|
||||
/**
|
||||
* Gets the OS version.
|
||||
* For example: 4.4.4(android), 8.1(ios)
|
||||
*/
|
||||
osVersion: string;
|
||||
|
||||
/**
|
||||
* Gets the SDK version.
|
||||
* For example: 19(android), 8.1(ios).
|
||||
*/
|
||||
sdkVersion: string;
|
||||
|
||||
/**
|
||||
* Gets the type of the current device.
|
||||
* Available values: "Phone", "Tablet".
|
||||
*/
|
||||
deviceType: 'Phone' | 'Tablet';
|
||||
|
||||
/**
|
||||
* Gets the uuid.
|
||||
* On iOS this will return a new uuid if the application is re-installed on the device.
|
||||
* If you need to receive the same uuid even after the application has been re-installed on the device,
|
||||
* use this plugin: https://www.npmjs.com/package/nativescript-ios-uuid
|
||||
*/
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* Gets the preferred language. For example "en" or "en-US".
|
||||
*/
|
||||
language: string;
|
||||
|
||||
/**
|
||||
* Gets the preferred region. For example "US".
|
||||
*/
|
||||
region: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current device information.
|
||||
*/
|
||||
export const Device: IDevice;
|
||||
|
||||
/**
|
||||
* Gets the current device information.
|
||||
*
|
||||
* This retains compatibility with NS6
|
||||
*/
|
||||
export const device: IDevice;
|
@ -1,8 +1,4 @@
|
||||
/* tslint:disable:class-name */
|
||||
|
||||
import { platformNames } from './common';
|
||||
import { ios } from '../utils';
|
||||
export * from './common';
|
||||
import { platformNames } from '../common';
|
||||
|
||||
type DeviceType = 'Phone' | 'Tablet' | 'Vision';
|
||||
|
||||
@ -87,44 +83,7 @@ class DeviceRef {
|
||||
}
|
||||
}
|
||||
|
||||
class MainScreen {
|
||||
private _screen: UIScreen;
|
||||
|
||||
private get screen(): UIScreen {
|
||||
if (!this._screen) {
|
||||
// NOTE: may not want to cache this value with SwiftUI app lifecycle based apps (using NativeScriptViewFactory) given the potential of multiple scenes
|
||||
const window = ios.getWindow();
|
||||
this._screen = window ? window.screen : UIScreen.mainScreen;
|
||||
}
|
||||
|
||||
return this._screen;
|
||||
}
|
||||
|
||||
get widthPixels(): number {
|
||||
return this.widthDIPs * this.scale;
|
||||
}
|
||||
get heightPixels(): number {
|
||||
return this.heightDIPs * this.scale;
|
||||
}
|
||||
get scale(): number {
|
||||
return this.screen.scale;
|
||||
}
|
||||
get widthDIPs(): number {
|
||||
return this.screen.bounds.size.width;
|
||||
}
|
||||
get heightDIPs(): number {
|
||||
return this.screen.bounds.size.height;
|
||||
}
|
||||
}
|
||||
|
||||
export const Device = new DeviceRef();
|
||||
|
||||
// This retains compatibility with NS6
|
||||
export const device = Device;
|
||||
|
||||
export class Screen {
|
||||
static mainScreen = new MainScreen();
|
||||
}
|
||||
|
||||
// This retains compatibility with NS6
|
||||
export const screen = Screen;
|
120
packages/core/platform/index.d.ts
vendored
120
packages/core/platform/index.d.ts
vendored
@ -25,121 +25,5 @@ export const isApple: boolean;
|
||||
export const isVisionOS: boolean;
|
||||
|
||||
export * from './common';
|
||||
|
||||
/*
|
||||
* An object containing device specific information.
|
||||
*/
|
||||
export interface IDevice {
|
||||
/**
|
||||
* Gets the manufacturer of the device.
|
||||
* For example: "Apple" or "HTC" or "Samsung".
|
||||
*/
|
||||
manufacturer: string;
|
||||
|
||||
/**
|
||||
* Gets the model of the device.
|
||||
* For example: "Nexus 5" or "iPhone".
|
||||
*/
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* Gets the OS of the device.
|
||||
* For example: "Android" or "iOS".
|
||||
*/
|
||||
os: string;
|
||||
|
||||
/**
|
||||
* Gets the OS version.
|
||||
* For example: 4.4.4(android), 8.1(ios)
|
||||
*/
|
||||
osVersion: string;
|
||||
|
||||
/**
|
||||
* Gets the SDK version.
|
||||
* For example: 19(android), 8.1(ios).
|
||||
*/
|
||||
sdkVersion: string;
|
||||
|
||||
/**
|
||||
* Gets the type of the current device.
|
||||
* Available values: "Phone", "Tablet".
|
||||
*/
|
||||
deviceType: 'Phone' | 'Tablet';
|
||||
|
||||
/**
|
||||
* Gets the uuid.
|
||||
* On iOS this will return a new uuid if the application is re-installed on the device.
|
||||
* If you need to receive the same uuid even after the application has been re-installed on the device,
|
||||
* use this plugin: https://www.npmjs.com/package/nativescript-ios-uuid
|
||||
*/
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* Gets the preferred language. For example "en" or "en-US".
|
||||
*/
|
||||
language: string;
|
||||
|
||||
/**
|
||||
* Gets the preferred region. For example "US".
|
||||
*/
|
||||
region: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing screen information.
|
||||
*/
|
||||
export interface ScreenMetrics {
|
||||
/**
|
||||
* Gets the absolute width of the screen in pixels.
|
||||
*/
|
||||
widthPixels: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute height of the screen in pixels.
|
||||
*/
|
||||
heightPixels: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute width of the screen in density independent pixels.
|
||||
*/
|
||||
widthDIPs: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute height of the screen in density independent pixels.
|
||||
*/
|
||||
heightDIPs: number;
|
||||
|
||||
/**
|
||||
* The logical density of the display. This is a scaling factor for the Density Independent Pixel unit.
|
||||
*/
|
||||
scale: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing general information about a display.
|
||||
*/
|
||||
export class Screen {
|
||||
/**
|
||||
* Gets information about the main screen of the current device.
|
||||
*/
|
||||
static mainScreen: ScreenMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing general information about a display.
|
||||
*
|
||||
* This retains compatibility with NS6
|
||||
*/
|
||||
export const screen: Screen;
|
||||
|
||||
/**
|
||||
* Gets the current device information.
|
||||
*/
|
||||
export const Device: IDevice;
|
||||
|
||||
/**
|
||||
* Gets the current device information.
|
||||
*
|
||||
* This retains compatibility with NS6
|
||||
*/
|
||||
export const device: IDevice;
|
||||
export * from './device';
|
||||
export * from './screen';
|
||||
|
3
packages/core/platform/index.ts
Normal file
3
packages/core/platform/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './device';
|
||||
export * from './screen';
|
49
packages/core/platform/screen/index.android.ts
Normal file
49
packages/core/platform/screen/index.android.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Application } from '../../application';
|
||||
|
||||
class MainScreen {
|
||||
private _metrics: android.util.DisplayMetrics;
|
||||
|
||||
private initMetrics(): void {
|
||||
const nativeApp = Application.android.getNativeApplication();
|
||||
nativeApp.getSystemService(android.content.Context.WINDOW_SERVICE).getDefaultDisplay().getRealMetrics(this._metrics);
|
||||
}
|
||||
|
||||
private get metrics(): android.util.DisplayMetrics {
|
||||
if (!this._metrics) {
|
||||
this._metrics = new android.util.DisplayMetrics();
|
||||
this.initMetrics();
|
||||
}
|
||||
|
||||
return this._metrics;
|
||||
}
|
||||
|
||||
get widthPixels(): number {
|
||||
return this.metrics.widthPixels;
|
||||
}
|
||||
get heightPixels(): number {
|
||||
return this.metrics.heightPixels;
|
||||
}
|
||||
get scale(): number {
|
||||
return this.metrics.density;
|
||||
}
|
||||
get widthDIPs(): number {
|
||||
return this.metrics.widthPixels / this.metrics.density;
|
||||
}
|
||||
get heightDIPs(): number {
|
||||
return this.metrics.heightPixels / this.metrics.density;
|
||||
}
|
||||
|
||||
public _updateMetrics(): void {
|
||||
if (!this._metrics) {
|
||||
this._metrics = new android.util.DisplayMetrics();
|
||||
}
|
||||
this.initMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
export class Screen {
|
||||
static mainScreen = new MainScreen();
|
||||
}
|
||||
|
||||
// This retains compatibility with NS6
|
||||
export const screen = Screen;
|
48
packages/core/platform/screen/index.d.ts
vendored
Normal file
48
packages/core/platform/screen/index.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* An object containing screen information.
|
||||
*/
|
||||
export interface ScreenMetrics {
|
||||
/**
|
||||
* Gets the absolute width of the screen in pixels.
|
||||
*/
|
||||
widthPixels: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute height of the screen in pixels.
|
||||
*/
|
||||
heightPixels: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute width of the screen in density independent pixels.
|
||||
*/
|
||||
widthDIPs: number;
|
||||
|
||||
/**
|
||||
* Gets the absolute height of the screen in density independent pixels.
|
||||
*/
|
||||
heightDIPs: number;
|
||||
|
||||
/**
|
||||
* The logical density of the display. This is a scaling factor for the Density Independent Pixel unit.
|
||||
*/
|
||||
scale: number;
|
||||
|
||||
_updateMetrics(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing general information about a display.
|
||||
*/
|
||||
export class Screen {
|
||||
/**
|
||||
* Gets information about the main screen of the current device.
|
||||
*/
|
||||
static mainScreen: ScreenMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing general information about a display.
|
||||
*
|
||||
* This retains compatibility with NS6
|
||||
*/
|
||||
export const screen: Screen;
|
42
packages/core/platform/screen/index.ios.ts
Normal file
42
packages/core/platform/screen/index.ios.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { ios } from '../../utils';
|
||||
|
||||
class MainScreen {
|
||||
private _screen: UIScreen;
|
||||
|
||||
private get screen(): UIScreen {
|
||||
if (!this._screen) {
|
||||
// NOTE: may not want to cache this value with SwiftUI app lifecycle based apps (using NativeScriptViewFactory) given the potential of multiple scenes
|
||||
const window = ios.getWindow();
|
||||
this._screen = window ? window.screen : UIScreen.mainScreen;
|
||||
}
|
||||
|
||||
return this._screen;
|
||||
}
|
||||
|
||||
get widthPixels(): number {
|
||||
return this.widthDIPs * this.scale;
|
||||
}
|
||||
get heightPixels(): number {
|
||||
return this.heightDIPs * this.scale;
|
||||
}
|
||||
get scale(): number {
|
||||
return this.screen.scale;
|
||||
}
|
||||
get widthDIPs(): number {
|
||||
return this.screen.bounds.size.width;
|
||||
}
|
||||
get heightDIPs(): number {
|
||||
return this.screen.bounds.size.height;
|
||||
}
|
||||
|
||||
public _updateMetrics(): void {
|
||||
// UIScreen handles the update on iOS
|
||||
}
|
||||
}
|
||||
|
||||
export class Screen {
|
||||
static mainScreen = new MainScreen();
|
||||
}
|
||||
|
||||
// This retains compatibility with NS6
|
||||
export const screen = Screen;
|
1
packages/core/trace/index.d.ts
vendored
1
packages/core/trace/index.d.ts
vendored
@ -119,6 +119,7 @@ export namespace Trace {
|
||||
export const Transition = 'Transition';
|
||||
export const Livesync = 'Livesync';
|
||||
export const ModuleNameResolver = 'ModuleNameResolver';
|
||||
export const MediaQuery = 'MediaQuery';
|
||||
|
||||
export const separator = ',';
|
||||
export const All: string;
|
||||
|
@ -201,9 +201,10 @@ export namespace Trace {
|
||||
export const Transition = 'Transition';
|
||||
export const Livesync = 'Livesync';
|
||||
export const ModuleNameResolver = 'ModuleNameResolver';
|
||||
export const MediaQuery = 'MediaQuery';
|
||||
|
||||
export const separator = ',';
|
||||
export const All: string = [VisualTreeEvents, Layout, Style, ViewHierarchy, NativeLifecycle, Debug, Navigation, Test, Binding, Error, Animation, Transition, Livesync, ModuleNameResolver].join(separator);
|
||||
export const All: string = [VisualTreeEvents, Layout, Style, ViewHierarchy, NativeLifecycle, Debug, Navigation, Test, Binding, Error, Animation, Transition, Livesync, ModuleNameResolver, MediaQuery].join(separator);
|
||||
|
||||
export function concat(...args: any): string {
|
||||
let result: string;
|
||||
|
@ -21,6 +21,9 @@ interface Keyframe {
|
||||
export interface Keyframes {
|
||||
name: string;
|
||||
keyframes: Array<UnparsedKeyframe>;
|
||||
tag?: string | number;
|
||||
scopedTag?: string;
|
||||
mediaQueryString?: string;
|
||||
}
|
||||
|
||||
export interface UnparsedKeyframe {
|
||||
|
@ -10,9 +10,12 @@ import { unsetValue } from '../core/properties';
|
||||
import { Animation } from '.';
|
||||
import { backgroundColorProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, rotateProperty, opacityProperty, rotateXProperty, rotateYProperty, widthProperty, heightProperty } from '../styling/style-properties';
|
||||
|
||||
export class Keyframes {
|
||||
export interface Keyframes {
|
||||
name: string;
|
||||
keyframes: Array<UnparsedKeyframe>;
|
||||
tag?: string | number;
|
||||
scopedTag?: string;
|
||||
mediaQueryString?: string;
|
||||
}
|
||||
|
||||
export class UnparsedKeyframe {
|
||||
@ -68,6 +71,11 @@ export class KeyframeAnimation {
|
||||
private _target: View;
|
||||
|
||||
public static keyframeAnimationFromInfo(info: KeyframeAnimationInfo): KeyframeAnimation {
|
||||
if (!info?.keyframes?.length) {
|
||||
Trace.write(`No keyframes found for animation '${info.name}'.`, Trace.categories.Animation, Trace.messageType.warn);
|
||||
return null;
|
||||
}
|
||||
|
||||
const length = info.keyframes.length;
|
||||
const animations = new Array<Keyframe>();
|
||||
let startDuration = 0;
|
||||
@ -244,7 +252,7 @@ export class KeyframeAnimation {
|
||||
},
|
||||
(error: any) => {
|
||||
Trace.write(typeof error === 'string' ? error : error.message, Trace.categories.Animation, Trace.messageType.warn);
|
||||
}
|
||||
},
|
||||
)
|
||||
.catch((error: any) => {
|
||||
Trace.write(typeof error === 'string' ? error : error.message, Trace.categories.Animation, Trace.messageType.warn);
|
||||
|
@ -16,9 +16,8 @@ import { CoreTypes } from '../../../core-types';
|
||||
import { Background, BackgroundClearFlags, refreshBorderDrawable } from '../../styling/background';
|
||||
import { profile } from '../../../profiling';
|
||||
import { topmost } from '../../frame/frame-stack';
|
||||
import { Screen } from '../../../platform';
|
||||
import { Device, Screen } from '../../../platform';
|
||||
import { AndroidActivityBackPressedEventData, Application } from '../../../application';
|
||||
import { Device } from '../../../platform';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
|
||||
import * as Utils from '../../../utils';
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { parse } from '../../css/reworkcss';
|
||||
import { createSelector, RuleSet, SelectorsMap, fromAstNodes, Node, Changes } from './css-selector';
|
||||
import { Screen } from '../../platform';
|
||||
import { createSelector, RuleSet, StyleSheetSelectorScope, fromAstNode, Node, Changes } from './css-selector';
|
||||
import { _populateRules } from './style-scope';
|
||||
|
||||
describe('css-selector', () => {
|
||||
it('button[attr]', () => {
|
||||
@ -17,20 +19,23 @@ describe('css-selector', () => {
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
function create(css: string, source = 'css-selectors.ts@test'): { rules: RuleSet[]; map: SelectorsMap<any> } {
|
||||
function create(css: string, source = 'css-selectors.ts@test'): { rulesets: RuleSet[]; selectorScope: StyleSheetSelectorScope<any> } {
|
||||
const parsed = parse(css, { source });
|
||||
const rulesAst = parsed.stylesheet.rules.filter((n) => n.type === 'rule');
|
||||
const rules = fromAstNodes(rulesAst);
|
||||
const map = new SelectorsMap(rules);
|
||||
const rulesAst = parsed.stylesheet.rules;
|
||||
const rulesets = [];
|
||||
|
||||
return { rules, map };
|
||||
_populateRules(rulesAst, rulesets, []);
|
||||
|
||||
const selectorScope = new StyleSheetSelectorScope(rulesets);
|
||||
|
||||
return { rulesets, selectorScope };
|
||||
}
|
||||
|
||||
function createOne(css: string, source = 'css-selectors.ts@test'): RuleSet {
|
||||
const { rules } = create(css, source);
|
||||
expect(rules.length).toBe(1);
|
||||
const { rulesets } = create(css, source);
|
||||
expect(rulesets.length).toBe(1);
|
||||
|
||||
return rules[0];
|
||||
return rulesets[0];
|
||||
}
|
||||
|
||||
it('single selector', () => {
|
||||
@ -48,21 +53,21 @@ describe('css-selector', () => {
|
||||
});
|
||||
|
||||
it('narrow selection', () => {
|
||||
const { map } = create(`
|
||||
const { selectorScope } = create(`
|
||||
.login { color: blue; }
|
||||
button { color: red; }
|
||||
image { color: green; }
|
||||
`);
|
||||
|
||||
const buttonQuerry = map.query({ cssType: 'button' }).selectors;
|
||||
expect(buttonQuerry.length).toBe(1);
|
||||
expect(buttonQuerry[0].ruleset.declarations).toEqual([{ property: 'color', value: 'red' }]);
|
||||
const buttonQuery = selectorScope.query({ cssType: 'button' }).selectors;
|
||||
expect(buttonQuery.length).toBe(1);
|
||||
expect(buttonQuery[0].ruleset.declarations).toEqual([{ property: 'color', value: 'red' }]);
|
||||
|
||||
const imageQuerry = map.query({ cssType: 'image', cssClasses: new Set(['login']) }).selectors;
|
||||
expect(imageQuerry.length).toBe(2);
|
||||
const imageQuery = selectorScope.query({ cssType: 'image', cssClasses: new Set(['login']) }).selectors;
|
||||
expect(imageQuery.length).toBe(2);
|
||||
// Note class before type
|
||||
expect(imageQuerry[0].ruleset.declarations).toEqual([{ property: 'color', value: 'blue' }]);
|
||||
expect(imageQuerry[1].ruleset.declarations).toEqual([{ property: 'color', value: 'green' }]);
|
||||
expect(imageQuery[0].ruleset.declarations).toEqual([{ property: 'color', value: 'blue' }]);
|
||||
expect(imageQuery[1].ruleset.declarations).toEqual([{ property: 'color', value: 'green' }]);
|
||||
});
|
||||
|
||||
const positiveMatches = {
|
||||
@ -271,10 +276,93 @@ describe('css-selector', () => {
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
// TODO: Re-add this when decorators actually work with ts-jest
|
||||
// TODO: Re-add this when decorators actually work properly on ts-jest
|
||||
//expect(rule.selectors[0].specificity).toEqual(0);
|
||||
});
|
||||
|
||||
describe('media queries', () => {
|
||||
const { widthDIPs } = Screen.mainScreen;
|
||||
|
||||
it('should apply css rules of matching media query', () => {
|
||||
const { selectorScope } = create(`
|
||||
@media only screen and (max-width: ${widthDIPs}) {
|
||||
.login { color: blue; }
|
||||
button { color: red; }
|
||||
image { color: green; }
|
||||
}
|
||||
`);
|
||||
|
||||
const { selectors: buttonSelectors } = selectorScope.query({ cssType: 'button' });
|
||||
expect(buttonSelectors.length).toBe(1);
|
||||
expect(buttonSelectors[0].ruleset.declarations).toEqual([{ property: 'color', value: 'red' }]);
|
||||
|
||||
const { selectors: imageSelectors } = selectorScope.query({ cssType: 'image', cssClasses: new Set(['login']) });
|
||||
expect(imageSelectors.length).toBe(2);
|
||||
// Note class before type
|
||||
expect(imageSelectors[0].ruleset.declarations).toEqual([{ property: 'color', value: 'blue' }]);
|
||||
expect(imageSelectors[1].ruleset.declarations).toEqual([{ property: 'color', value: 'green' }]);
|
||||
});
|
||||
|
||||
it('should not apply css rules of non-matching media query', () => {
|
||||
const { selectorScope } = create(`
|
||||
@media only screen and (max-width: ${widthDIPs - 1}) {
|
||||
.login { color: blue; }
|
||||
button { color: red; }
|
||||
image { color: green; }
|
||||
}
|
||||
`);
|
||||
|
||||
const { selectors: buttonSelectors } = selectorScope.query({ cssType: 'button' });
|
||||
expect(buttonSelectors.length).toBe(0);
|
||||
|
||||
const { selectors: imageSelectors } = selectorScope.query({ cssType: 'image', cssClasses: new Set(['login']) });
|
||||
expect(imageSelectors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply css rules of matching media and nested media queries', () => {
|
||||
const { selectorScope } = create(`
|
||||
@media only screen and (max-width: ${widthDIPs}) {
|
||||
.login { color: blue; }
|
||||
button { color: red; }
|
||||
@media only screen and (orientation: portrait) {
|
||||
image { color: green; }
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const { selectors: buttonSelectors } = selectorScope.query({ cssType: 'button' });
|
||||
expect(buttonSelectors.length).toBe(1);
|
||||
expect(buttonSelectors[0].ruleset.declarations).toEqual([{ property: 'color', value: 'red' }]);
|
||||
|
||||
const { selectors: imageSelectors } = selectorScope.query({ cssType: 'image', cssClasses: new Set(['login']) });
|
||||
expect(imageSelectors.length).toBe(2);
|
||||
// Note class before type
|
||||
expect(imageSelectors[0].ruleset.declarations).toEqual([{ property: 'color', value: 'blue' }]);
|
||||
expect(imageSelectors[1].ruleset.declarations).toEqual([{ property: 'color', value: 'green' }]);
|
||||
});
|
||||
|
||||
it('should apply css rules of matching media queries but not non-matching nested media queries', () => {
|
||||
const { selectorScope } = create(`
|
||||
@media only screen and (max-width: ${widthDIPs}) {
|
||||
.login { color: blue; }
|
||||
|
||||
@media only screen and (orientation: none) {
|
||||
button { color: red; }
|
||||
image { color: green; }
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const { selectors: buttonSelectors } = selectorScope.query({ cssType: 'button' });
|
||||
expect(buttonSelectors.length).toBe(0);
|
||||
|
||||
const { selectors: imageSelectors } = selectorScope.query({ cssType: 'image', cssClasses: new Set(['login']) });
|
||||
expect(imageSelectors.length).toBe(1);
|
||||
|
||||
expect(imageSelectors[0].ruleset.declarations).toEqual([{ property: 'color', value: 'blue' }]);
|
||||
});
|
||||
});
|
||||
|
||||
function toString() {
|
||||
return this.cssType;
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ import { isNullOrUndefined } from '../../utils/types';
|
||||
|
||||
import * as ReworkCSS from '../../css';
|
||||
import { CSSUtils } from '../../css/system-classes';
|
||||
import { checkIfMediaQueryMatches } from '../../media-query-list';
|
||||
|
||||
export const MEDIA_QUERY_SEPARATOR = '&&';
|
||||
|
||||
/**
|
||||
* An interface describing the shape of a type on which the selectors may apply.
|
||||
@ -779,29 +782,34 @@ export namespace Selector {
|
||||
}
|
||||
|
||||
export class RuleSet {
|
||||
tag: string | number;
|
||||
scopedTag: string;
|
||||
constructor(
|
||||
public selectors: SelectorCore[],
|
||||
public declarations: Declaration[],
|
||||
) {
|
||||
public selectors: SelectorCore[];
|
||||
public declarations: Declaration[];
|
||||
public mediaQueryString: string;
|
||||
public tag?: string | number;
|
||||
public scopedTag?: string;
|
||||
|
||||
constructor(selectors: SelectorCore[], declarations: Declaration[]) {
|
||||
this.selectors = selectors;
|
||||
this.declarations = declarations;
|
||||
this.selectors.forEach((sel) => (sel.ruleset = this));
|
||||
}
|
||||
public toString(): string {
|
||||
return `${this.selectors.join(', ')} {${this.declarations.map((d, i) => `${i === 0 ? ' ' : ''}${d.property}: ${d.value}`).join('; ')} }`;
|
||||
let desc = `${this.selectors.join(', ')} {${this.declarations.map((d, i) => `${i === 0 ? ' ' : ''}${d.property}: ${d.value}`).join('; ')} }`;
|
||||
if (this.mediaQueryString) {
|
||||
desc = `@media ${this.mediaQueryString} { ${desc} }`;
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
public lookupSort(sorter: LookupSorter): void {
|
||||
this.selectors.forEach((sel) => sel.lookupSort(sorter));
|
||||
}
|
||||
}
|
||||
|
||||
export function fromAstNodes(astRules: ReworkCSS.Node[]): RuleSet[] {
|
||||
return (<ReworkCSS.Rule[]>astRules.filter(isRule)).map((rule) => {
|
||||
const declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
|
||||
const selectors = rule.selectors.map(createSelector);
|
||||
export function fromAstNode(astRule: ReworkCSS.Rule): RuleSet {
|
||||
const declarations = astRule.declarations.filter(isDeclaration).map(createDeclaration);
|
||||
const selectors = astRule.selectors.map(createSelector);
|
||||
|
||||
return new RuleSet(selectors, declarations);
|
||||
});
|
||||
}
|
||||
|
||||
function createDeclaration(decl: ReworkCSS.Declaration): any {
|
||||
@ -939,30 +947,43 @@ export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequ
|
||||
}
|
||||
}
|
||||
|
||||
function isRule(node: ReworkCSS.Node): node is ReworkCSS.Rule {
|
||||
return node.type === 'rule';
|
||||
}
|
||||
function isDeclaration(node: ReworkCSS.Node): node is ReworkCSS.Declaration {
|
||||
return node.type === 'declaration';
|
||||
}
|
||||
|
||||
export function matchMediaQueryString(mediaQueryString: string, cachedQueries: string[]): boolean {
|
||||
// It can be a single or multiple queries in case of nested media queries
|
||||
const mediaQueryStrings = mediaQueryString.split(MEDIA_QUERY_SEPARATOR);
|
||||
|
||||
return mediaQueryStrings.every((mq) => {
|
||||
let isMatching: boolean;
|
||||
|
||||
// Query has already been validated
|
||||
if (cachedQueries.includes(mq)) {
|
||||
isMatching = true;
|
||||
} else {
|
||||
isMatching = checkIfMediaQueryMatches(mq);
|
||||
if (isMatching) {
|
||||
cachedQueries.push(mq);
|
||||
}
|
||||
}
|
||||
return isMatching;
|
||||
});
|
||||
}
|
||||
|
||||
interface SelectorMap {
|
||||
[key: string]: SelectorCore[];
|
||||
}
|
||||
export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||
|
||||
export abstract class SelectorScope<T extends Node> implements LookupSorter {
|
||||
private id: SelectorMap = {};
|
||||
private class: SelectorMap = {};
|
||||
private type: SelectorMap = {};
|
||||
private universal: SelectorCore[] = [];
|
||||
|
||||
private position = 0;
|
||||
public position: number = 0;
|
||||
|
||||
constructor(rulesets: RuleSet[]) {
|
||||
rulesets.forEach((rule) => rule.lookupSort(this));
|
||||
}
|
||||
|
||||
query(node: T): SelectorsMatch<T> {
|
||||
const selectorsMatch = new SelectorsMatch<T>();
|
||||
getSelectorCandidates(node: T) {
|
||||
const { cssClasses, id, cssType } = node;
|
||||
const selectorClasses = [this.universal, this.id[id], this.type[cssType]];
|
||||
|
||||
@ -970,11 +991,7 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||
cssClasses.forEach((c) => selectorClasses.push(this.class[c]));
|
||||
}
|
||||
|
||||
const selectors = selectorClasses.reduce((cur, next) => cur.concat(next || []), []);
|
||||
|
||||
selectorsMatch.selectors = selectors.filter((sel) => sel.accumulateChanges(node, selectorsMatch)).sort((a, b) => a.specificity - b.specificity || a.pos - b.pos);
|
||||
|
||||
return selectorsMatch;
|
||||
return selectorClasses.reduce((cur, next) => cur.concat(next || []), []);
|
||||
}
|
||||
|
||||
sortById(id: string, sel: SelectorCore): void {
|
||||
@ -1005,6 +1022,99 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaQuerySelectorScope<T extends Node> extends SelectorScope<T> {
|
||||
private _mediaQueryString: string;
|
||||
|
||||
constructor(mediaQueryString: string) {
|
||||
super();
|
||||
|
||||
this._mediaQueryString = mediaQueryString;
|
||||
}
|
||||
|
||||
get mediaQueryString(): string {
|
||||
return this._mediaQueryString;
|
||||
}
|
||||
}
|
||||
|
||||
export class StyleSheetSelectorScope<T extends Node> extends SelectorScope<T> {
|
||||
private mediaQuerySelectorScopes: MediaQuerySelectorScope<T>[];
|
||||
|
||||
constructor(rulesets: RuleSet[]) {
|
||||
super();
|
||||
|
||||
this.lookupRulesets(rulesets);
|
||||
}
|
||||
|
||||
private createMediaQuerySelectorScope(mediaQueryString: string): MediaQuerySelectorScope<T> {
|
||||
const selectorScope = new MediaQuerySelectorScope(mediaQueryString);
|
||||
selectorScope.position = this.position;
|
||||
|
||||
if (this.mediaQuerySelectorScopes) {
|
||||
this.mediaQuerySelectorScopes.push(selectorScope);
|
||||
} else {
|
||||
this.mediaQuerySelectorScopes = [selectorScope];
|
||||
}
|
||||
|
||||
return selectorScope;
|
||||
}
|
||||
|
||||
private lookupRulesets(rulesets: RuleSet[]) {
|
||||
let lastMediaSelectorScope: MediaQuerySelectorScope<T>;
|
||||
|
||||
for (let i = 0, length = rulesets.length; i < length; i++) {
|
||||
const ruleset = rulesets[i];
|
||||
|
||||
if (lastMediaSelectorScope && lastMediaSelectorScope.mediaQueryString !== ruleset.mediaQueryString) {
|
||||
// Once done with current media query scope, update stylesheet scope position
|
||||
this.position = lastMediaSelectorScope.position;
|
||||
lastMediaSelectorScope = null;
|
||||
}
|
||||
|
||||
if (ruleset.mediaQueryString) {
|
||||
// Create media query selector scope and register selector lookups there
|
||||
if (!lastMediaSelectorScope) {
|
||||
lastMediaSelectorScope = this.createMediaQuerySelectorScope(ruleset.mediaQueryString);
|
||||
}
|
||||
|
||||
ruleset.lookupSort(lastMediaSelectorScope);
|
||||
} else {
|
||||
ruleset.lookupSort(this);
|
||||
}
|
||||
}
|
||||
|
||||
// If reference of last media selector scope is still kept, update stylesheet scope position
|
||||
if (lastMediaSelectorScope) {
|
||||
this.position = lastMediaSelectorScope.position;
|
||||
lastMediaSelectorScope = null;
|
||||
}
|
||||
}
|
||||
|
||||
query(node: T): SelectorsMatch<T> {
|
||||
const selectorsMatch = new SelectorsMatch<T>();
|
||||
const selectors = this.getSelectorCandidates(node);
|
||||
|
||||
// Validate media queries and include their selectors if needed
|
||||
if (this.mediaQuerySelectorScopes) {
|
||||
// Cache media query results to avoid validations of other identical queries
|
||||
const validatedMediaQueries: string[] = [];
|
||||
|
||||
for (let i = 0, length = this.mediaQuerySelectorScopes.length; i < length; i++) {
|
||||
const selectorScope = this.mediaQuerySelectorScopes[i];
|
||||
const isMatchingAllQueries = matchMediaQueryString(selectorScope.mediaQueryString, validatedMediaQueries);
|
||||
|
||||
if (isMatchingAllQueries) {
|
||||
const mediaQuerySelectors = selectorScope.getSelectorCandidates(node);
|
||||
selectors.push(...mediaQuerySelectors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectorsMatch.selectors = selectors.filter((sel) => sel.accumulateChanges(node, selectorsMatch)).sort((a, b) => a.specificity - b.specificity || a.pos - b.pos);
|
||||
|
||||
return selectorsMatch;
|
||||
}
|
||||
}
|
||||
|
||||
interface ChangeAccumulator {
|
||||
addAttribute(node: Node, attribute: string): void;
|
||||
addPseudoClass(node: Node, pseudoClass: string): void;
|
||||
@ -1056,7 +1166,9 @@ export const CSSHelper = {
|
||||
SimpleSelectorSequence,
|
||||
Selector,
|
||||
RuleSet,
|
||||
SelectorsMap,
|
||||
fromAstNodes,
|
||||
SelectorScope,
|
||||
MediaQuerySelectorScope,
|
||||
StyleSheetSelectorScope,
|
||||
fromAstNode,
|
||||
SelectorsMatch,
|
||||
};
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Keyframes } from '../animation/keyframe-animation';
|
||||
import { ViewBase } from '../core/view-base';
|
||||
import { View } from '../core/view';
|
||||
import { unsetValue, _evaluateCssVariableExpression, _evaluateCssCalcExpression, isCssVariable, isCssVariableExpression, isCssCalcExpression } from '../core/properties';
|
||||
import { SyntaxTree, Keyframes as KeyframesDefinition, Node as CssNode } from '../../css';
|
||||
import * as ReworkCSS from '../../css';
|
||||
|
||||
import { RuleSet, SelectorsMap, SelectorCore, SelectorsMatch, ChangeMap, fromAstNodes, Node } from './css-selector';
|
||||
import { RuleSet, StyleSheetSelectorScope, SelectorCore, SelectorsMatch, ChangeMap, fromAstNode, Node, MEDIA_QUERY_SEPARATOR, matchMediaQueryString } from './css-selector';
|
||||
import { Trace } from '../../trace';
|
||||
import { File, knownFolders, path } from '../../file-system';
|
||||
import { Application, CssChangedEventData, LoadAppCSSEventData } from '../../application';
|
||||
@ -45,6 +44,25 @@ try {
|
||||
//
|
||||
}
|
||||
|
||||
type KeyframesMap = Map<string, kam.Keyframes[]>;
|
||||
|
||||
let mergedApplicationCssSelectors: RuleSet[] = [];
|
||||
let applicationCssSelectors: RuleSet[] = [];
|
||||
const applicationAdditionalSelectors: RuleSet[] = [];
|
||||
|
||||
let mergedApplicationCssKeyframes: kam.Keyframes[] = [];
|
||||
let applicationCssKeyframes: kam.Keyframes[] = [];
|
||||
const applicationAdditionalKeyframes: kam.Keyframes[] = [];
|
||||
|
||||
let applicationCssSelectorVersion = 0;
|
||||
|
||||
const tagToScopeTag: Map<string | number, string> = new Map();
|
||||
let currentScopeTag: string = null;
|
||||
|
||||
const animationsSymbol = Symbol('animations');
|
||||
const kebabCasePattern = /-([a-z])/g;
|
||||
const pattern = /('|")(.*?)\1/;
|
||||
|
||||
/**
|
||||
* Evaluate css-variable and css-calc expressions
|
||||
*/
|
||||
@ -68,39 +86,32 @@ function evaluateCssExpressions(view: ViewBase, property: string, value: string)
|
||||
}
|
||||
|
||||
export function mergeCssSelectors(): void {
|
||||
applicationCssSelectors = applicationSelectors.slice();
|
||||
applicationCssSelectors.push(...applicationAdditionalSelectors);
|
||||
applicationCssSelectorVersion++;
|
||||
mergedApplicationCssSelectors = applicationCssSelectors.slice();
|
||||
mergedApplicationCssSelectors.push(...applicationAdditionalSelectors);
|
||||
}
|
||||
|
||||
let applicationCssSelectors: RuleSet[] = [];
|
||||
let applicationCssSelectorVersion = 0;
|
||||
let applicationSelectors: RuleSet[] = [];
|
||||
const tagToScopeTag: Map<string | number, string> = new Map();
|
||||
let currentScopeTag: string = null;
|
||||
const applicationAdditionalSelectors: RuleSet[] = [];
|
||||
const applicationKeyframes: any = {};
|
||||
const animationsSymbol = Symbol('animations');
|
||||
const kebabCasePattern = /-([a-z])/g;
|
||||
const pattern = /('|")(.*?)\1/;
|
||||
export function mergeCssKeyframes(): void {
|
||||
mergedApplicationCssKeyframes = applicationCssKeyframes.slice();
|
||||
mergedApplicationCssKeyframes.push(...applicationAdditionalKeyframes);
|
||||
}
|
||||
|
||||
class CSSSource {
|
||||
private _selectors: RuleSet[] = [];
|
||||
private _keyframes: kam.Keyframes[] = [];
|
||||
|
||||
private constructor(
|
||||
private _ast: SyntaxTree,
|
||||
private _ast: ReworkCSS.SyntaxTree,
|
||||
private _url: string,
|
||||
private _file: string,
|
||||
private _keyframes: KeyframesMap,
|
||||
private _source: string,
|
||||
) {
|
||||
this.parse();
|
||||
}
|
||||
|
||||
public static fromDetect(cssOrAst: any, keyframes: KeyframesMap, fileName?: string): CSSSource {
|
||||
public static fromDetect(cssOrAst: any, fileName?: string): CSSSource {
|
||||
if (typeof cssOrAst === 'string') {
|
||||
// raw-loader
|
||||
return CSSSource.fromSource(cssOrAst, keyframes, fileName);
|
||||
return CSSSource.fromSource(cssOrAst, fileName);
|
||||
} else if (typeof cssOrAst === 'object') {
|
||||
if (cssOrAst.default) {
|
||||
cssOrAst = cssOrAst.default;
|
||||
@ -108,15 +119,15 @@ class CSSSource {
|
||||
|
||||
if (cssOrAst.type === 'stylesheet' && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
|
||||
// css-loader
|
||||
return CSSSource.fromAST(cssOrAst, keyframes, fileName);
|
||||
return CSSSource.fromAST(cssOrAst, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
// css2json-loader
|
||||
return CSSSource.fromSource(cssOrAst.toString(), keyframes, fileName);
|
||||
return CSSSource.fromSource(cssOrAst.toString(), fileName);
|
||||
}
|
||||
|
||||
public static fromURI(uri: string, keyframes: KeyframesMap): CSSSource {
|
||||
public static fromURI(uri: string): CSSSource {
|
||||
// webpack modules require all file paths to be relative to /app folder
|
||||
const appRelativeUri = CSSSource.pathRelativeToApp(uri);
|
||||
const sanitizedModuleName = sanitizeModuleName(appRelativeUri);
|
||||
@ -125,7 +136,7 @@ class CSSSource {
|
||||
try {
|
||||
const cssOrAst = global.loadModule(resolvedModuleName, true);
|
||||
if (cssOrAst) {
|
||||
return CSSSource.fromDetect(cssOrAst, keyframes, resolvedModuleName);
|
||||
return CSSSource.fromDetect(cssOrAst, resolvedModuleName);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Trace.isEnabled()) {
|
||||
@ -133,7 +144,7 @@ class CSSSource {
|
||||
}
|
||||
}
|
||||
|
||||
return CSSSource.fromFile(appRelativeUri, keyframes);
|
||||
return CSSSource.fromFile(appRelativeUri);
|
||||
}
|
||||
|
||||
private static pathRelativeToApp(uri: string): string {
|
||||
@ -148,30 +159,30 @@ class CSSSource {
|
||||
return uri;
|
||||
}
|
||||
|
||||
const relativeUri = `.${uri.substr(appPath.length)}`;
|
||||
const relativeUri = `.${uri.substring(appPath.length)}`;
|
||||
|
||||
return relativeUri;
|
||||
}
|
||||
|
||||
public static fromFile(url: string, keyframes: KeyframesMap): CSSSource {
|
||||
public static fromFile(url: string): CSSSource {
|
||||
// .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first.
|
||||
const cssFileUrl = url.replace(/\..\w+$/, '.css');
|
||||
if (cssFileUrl !== url) {
|
||||
const cssFile = CSSSource.resolveCSSPathFromURL(cssFileUrl);
|
||||
if (cssFile) {
|
||||
return new CSSSource(undefined, url, cssFile, keyframes, undefined);
|
||||
return new CSSSource(undefined, url, cssFile, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const file = CSSSource.resolveCSSPathFromURL(url);
|
||||
|
||||
return new CSSSource(undefined, url, file, keyframes, undefined);
|
||||
return new CSSSource(undefined, url, file, undefined);
|
||||
}
|
||||
|
||||
public static fromFileImport(url: string, keyframes: KeyframesMap, importSource: string): CSSSource {
|
||||
public static fromFileImport(url: string, importSource: string): CSSSource {
|
||||
const file = CSSSource.resolveCSSPathFromURL(url, importSource);
|
||||
|
||||
return new CSSSource(undefined, url, file, keyframes, undefined);
|
||||
return new CSSSource(undefined, url, file, undefined);
|
||||
}
|
||||
|
||||
@profile
|
||||
@ -182,17 +193,22 @@ class CSSSource {
|
||||
return file;
|
||||
}
|
||||
|
||||
public static fromSource(source: string, keyframes: KeyframesMap, url?: string): CSSSource {
|
||||
return new CSSSource(undefined, url, undefined, keyframes, source);
|
||||
public static fromSource(source: string, url?: string): CSSSource {
|
||||
return new CSSSource(undefined, url, undefined, source);
|
||||
}
|
||||
|
||||
public static fromAST(ast: SyntaxTree, keyframes: KeyframesMap, url?: string): CSSSource {
|
||||
return new CSSSource(ast, url, undefined, keyframes, undefined);
|
||||
public static fromAST(ast: ReworkCSS.SyntaxTree, url?: string): CSSSource {
|
||||
return new CSSSource(ast, url, undefined, undefined);
|
||||
}
|
||||
|
||||
get selectors(): RuleSet[] {
|
||||
return this._selectors;
|
||||
}
|
||||
|
||||
get keyframes(): kam.Keyframes[] {
|
||||
return this._keyframes;
|
||||
}
|
||||
|
||||
get source(): string {
|
||||
return this._source;
|
||||
}
|
||||
@ -216,7 +232,7 @@ class CSSSource {
|
||||
}
|
||||
}
|
||||
if (this._ast) {
|
||||
this.createSelectors();
|
||||
this.createSelectorsAndKeyframes();
|
||||
} else {
|
||||
this._selectors = [];
|
||||
}
|
||||
@ -249,14 +265,40 @@ class CSSSource {
|
||||
}
|
||||
|
||||
@profile
|
||||
private createSelectors() {
|
||||
private createSelectorsAndKeyframes() {
|
||||
if (this._ast) {
|
||||
this._selectors = [...this.createSelectorsFromImports(), ...this.createSelectorsFromSyntaxTree()];
|
||||
const nodes = this._ast.stylesheet.rules;
|
||||
|
||||
const rulesets: RuleSet[] = [];
|
||||
const keyframes: kam.Keyframes[] = [];
|
||||
|
||||
// When css2json-loader is enabled, imports are handled there and removed from AST rules
|
||||
populateRulesFromImports(nodes, rulesets, keyframes);
|
||||
_populateRules(nodes, rulesets, keyframes);
|
||||
|
||||
if (rulesets && rulesets.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
|
||||
rulesets.forEach((rule) => {
|
||||
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations);
|
||||
});
|
||||
}
|
||||
|
||||
this._selectors = rulesets;
|
||||
this._keyframes = keyframes;
|
||||
}
|
||||
}
|
||||
|
||||
private createSelectorsFromImports(): RuleSet[] {
|
||||
const imports = this._ast['stylesheet']['rules'].filter((r) => r.type === 'import');
|
||||
toString(): string {
|
||||
return this._file || this._url || '(in-memory)';
|
||||
}
|
||||
}
|
||||
|
||||
function populateRulesFromImports(nodes: ReworkCSS.Node[], rulesets: RuleSet[], keyframes: kam.Keyframes[]): void {
|
||||
const imports = nodes.filter((r) => r.type === 'import');
|
||||
if (!imports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlFromImportObject = (importObject) => {
|
||||
const importItem = importObject['import'] as string;
|
||||
@ -272,83 +314,151 @@ class CSSSource {
|
||||
source: sourceFromImportObject(importObject),
|
||||
});
|
||||
|
||||
const getCssFile = ({ url, source }) => (source ? CSSSource.fromFileImport(url, this._keyframes, source) : CSSSource.fromURI(url, this._keyframes));
|
||||
const getCssFile = ({ url, source }) => (source ? CSSSource.fromFileImport(url, source) : CSSSource.fromURI(url));
|
||||
|
||||
const cssFiles = imports
|
||||
.map(toUrlSourcePair)
|
||||
.filter(({ url }) => !!url)
|
||||
.map(getCssFile);
|
||||
|
||||
const selectors = cssFiles.map((file) => (file && file.selectors) || []);
|
||||
|
||||
return selectors.reduce((acc, val) => acc.concat(val), []);
|
||||
for (const cssFile of cssFiles) {
|
||||
if (cssFile) {
|
||||
rulesets.push(...cssFile.selectors);
|
||||
keyframes.push(...cssFile.keyframes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSelectorsFromSyntaxTree(): RuleSet[] {
|
||||
const nodes = this._ast.stylesheet.rules;
|
||||
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach((node) => (this._keyframes[node.name] = node));
|
||||
export function _populateRules(nodes: ReworkCSS.Node[], rulesets: RuleSet[], keyframes: kam.Keyframes[], mediaQueryString?: string): void {
|
||||
for (const node of nodes) {
|
||||
if (isKeyframe(node)) {
|
||||
const keyframeRule: kam.Keyframes = {
|
||||
name: node.name,
|
||||
keyframes: node.keyframes,
|
||||
mediaQueryString: mediaQueryString,
|
||||
};
|
||||
|
||||
const rulesets = fromAstNodes(nodes);
|
||||
if (rulesets && rulesets.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
keyframes.push(keyframeRule);
|
||||
} else if (isMedia(node)) {
|
||||
// Media query is composite in the case of nested media queries
|
||||
const compositeMediaQuery = mediaQueryString ? mediaQueryString + MEDIA_QUERY_SEPARATOR + node.media : node.media;
|
||||
|
||||
rulesets.forEach((rule) => {
|
||||
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations);
|
||||
});
|
||||
_populateRules(node.rules, rulesets, keyframes, compositeMediaQuery);
|
||||
} else if (isRule(node)) {
|
||||
const ruleset = fromAstNode(node);
|
||||
ruleset.mediaQueryString = mediaQueryString;
|
||||
|
||||
rulesets.push(ruleset);
|
||||
}
|
||||
|
||||
return rulesets;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this._file || this._url || '(in-memory)';
|
||||
}
|
||||
}
|
||||
|
||||
export function removeTaggedAdditionalCSS(tag: string | number): boolean {
|
||||
let changed = false;
|
||||
let selectorsChanged = false;
|
||||
let keyframesChanged = false;
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < applicationAdditionalSelectors.length; i++) {
|
||||
if (applicationAdditionalSelectors[i].tag === tag) {
|
||||
applicationAdditionalSelectors.splice(i, 1);
|
||||
i--;
|
||||
changed = true;
|
||||
selectorsChanged = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
mergeCssSelectors();
|
||||
}
|
||||
|
||||
return changed;
|
||||
for (let i = 0; i < applicationAdditionalKeyframes.length; i++) {
|
||||
if (applicationAdditionalKeyframes[i].tag === tag) {
|
||||
applicationAdditionalKeyframes.splice(i, 1);
|
||||
i--;
|
||||
keyframesChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectorsChanged) {
|
||||
mergeCssSelectors();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (keyframesChanged) {
|
||||
mergeCssKeyframes();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function addTaggedAdditionalCSS(cssText: string, tag?: string | number): boolean {
|
||||
const parsed: RuleSet[] = CSSSource.fromDetect(cssText, applicationKeyframes, undefined).selectors;
|
||||
const { selectors, keyframes } = CSSSource.fromDetect(cssText, undefined);
|
||||
const tagScope = currentScopeTag || (tag && tagToScopeTag.has(tag) && tagToScopeTag.get(tag)) || null;
|
||||
|
||||
if (tagScope && tag) {
|
||||
tagToScopeTag.set(tag, tagScope);
|
||||
}
|
||||
let changed = false;
|
||||
if (parsed && parsed.length) {
|
||||
changed = true;
|
||||
|
||||
let selectorsChanged = false;
|
||||
let keyframesChanged = false;
|
||||
let updated = false;
|
||||
|
||||
if (selectors && selectors.length) {
|
||||
selectorsChanged = true;
|
||||
|
||||
if (tag != null || tagScope != null) {
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
parsed[i].tag = tag;
|
||||
parsed[i].scopedTag = tagScope;
|
||||
for (let i = 0, length = selectors.length; i < length; i++) {
|
||||
selectors[i].tag = tag;
|
||||
selectors[i].scopedTag = tagScope;
|
||||
}
|
||||
}
|
||||
applicationAdditionalSelectors.push(...parsed);
|
||||
mergeCssSelectors();
|
||||
}
|
||||
|
||||
return changed;
|
||||
applicationAdditionalSelectors.push(...selectors);
|
||||
mergeCssSelectors();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (keyframes && keyframes.length) {
|
||||
keyframesChanged = true;
|
||||
|
||||
if (tag != null || tagScope != null) {
|
||||
for (let i = 0, length = keyframes.length; i < length; i++) {
|
||||
keyframes[i].tag = tag;
|
||||
keyframes[i].scopedTag = tagScope;
|
||||
}
|
||||
}
|
||||
|
||||
applicationAdditionalKeyframes.push(...keyframes);
|
||||
mergeCssKeyframes();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
const onCssChanged = profile('"style-scope".onCssChanged', (args: CssChangedEventData) => {
|
||||
if (args.cssText) {
|
||||
const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors;
|
||||
if (parsed) {
|
||||
applicationAdditionalSelectors.push(...parsed);
|
||||
const { selectors, keyframes } = CSSSource.fromSource(args.cssText, args.cssFile);
|
||||
let updated = false;
|
||||
|
||||
if (selectors) {
|
||||
applicationAdditionalSelectors.push(...selectors);
|
||||
mergeCssSelectors();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (keyframes) {
|
||||
applicationAdditionalKeyframes.push(...keyframes);
|
||||
mergeCssKeyframes();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
} else if (args.cssFile) {
|
||||
loadCss(args.cssFile, null, null);
|
||||
@ -359,20 +469,35 @@ function onLiveSync(args: CssChangedEventData): void {
|
||||
loadCss(Application.getCssFileName(), null, null);
|
||||
}
|
||||
|
||||
const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => {
|
||||
const loadCss = profile(`"style-scope".loadCss`, (cssModule: string): void => {
|
||||
if (!cssModule) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// safely remove "./" as global CSS should be resolved relative to app folder
|
||||
if (cssModule.startsWith('./')) {
|
||||
cssModule = cssModule.substr(2);
|
||||
cssModule = cssModule.substring(2);
|
||||
}
|
||||
|
||||
const result = CSSSource.fromURI(cssModule, applicationKeyframes).selectors;
|
||||
if (result.length > 0) {
|
||||
applicationSelectors = result;
|
||||
const { selectors, keyframes } = CSSSource.fromURI(cssModule);
|
||||
let updated = false;
|
||||
|
||||
// Check for existing application css selectors too in case the app is undergoing a live-sync
|
||||
if (selectors.length > 0 || applicationCssSelectors.length > 0) {
|
||||
applicationCssSelectors = selectors;
|
||||
mergeCssSelectors();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Check for existing application css keyframes too in case the app is undergoing a live-sync
|
||||
if (keyframes.length > 0 || applicationCssKeyframes.length > 0) {
|
||||
applicationCssKeyframes = keyframes;
|
||||
mergeCssKeyframes();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
applicationCssSelectorVersion++;
|
||||
}
|
||||
});
|
||||
|
||||
@ -475,7 +600,7 @@ export class CssState {
|
||||
private updateMatch() {
|
||||
const view = this.viewRef.get();
|
||||
if (view && view._styleScope) {
|
||||
this._match = view._styleScope.matchSelectors(view);
|
||||
this._match = view._styleScope.matchSelectors(view) ?? CssState.emptyMatch;
|
||||
this._appliedSelectorsVersion = view._styleScope.getSelectorsVersion();
|
||||
} else {
|
||||
this._match = CssState.emptyMatch;
|
||||
@ -494,12 +619,14 @@ export class CssState {
|
||||
}
|
||||
|
||||
const matchingSelectors = this._match.selectors.filter((sel) => (sel.dynamic ? sel.match(view) : true));
|
||||
if (!matchingSelectors || matchingSelectors.length === 0) {
|
||||
|
||||
// Ideally we should return here if there are no matching selectors, however
|
||||
// if there are property removals, returning here would not remove them
|
||||
// this is seen in STYLE test in automated.
|
||||
// if (!matchingSelectors || matchingSelectors.length === 0) {
|
||||
// return;
|
||||
}
|
||||
// }
|
||||
|
||||
view._batchUpdate(() => {
|
||||
this.stopKeyframeAnimations();
|
||||
this.setPropertyValues(matchingSelectors);
|
||||
@ -511,7 +638,7 @@ export class CssState {
|
||||
const animations: kam.KeyframeAnimation[] = [];
|
||||
|
||||
matchingSelectors.forEach((selector) => {
|
||||
const ruleAnimations: kam.KeyframeAnimationInfo[] = selector.ruleset[animationsSymbol];
|
||||
const ruleAnimations: kam.KeyframeAnimationInfo[] = selector.ruleset?.[animationsSymbol];
|
||||
if (ruleAnimations) {
|
||||
ensureKeyframeAnimationModule();
|
||||
for (const animationInfo of ruleAnimations) {
|
||||
@ -716,14 +843,18 @@ CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
|
||||
CssState.prototype._matchInvalid = true;
|
||||
|
||||
export class StyleScope {
|
||||
private _selectors: SelectorsMap<any>;
|
||||
private _selectorScope: StyleSheetSelectorScope<any>;
|
||||
private _css = '';
|
||||
|
||||
private _mergedCssSelectors: RuleSet[];
|
||||
private _mergedCssKeyframes: kam.Keyframes[];
|
||||
|
||||
private _localCssSelectors: RuleSet[] = [];
|
||||
private _localCssKeyframes: kam.Keyframes[] = [];
|
||||
private _localCssSelectorVersion = 0;
|
||||
|
||||
private _localCssSelectorsAppliedVersion = 0;
|
||||
private _applicationCssSelectorsAppliedVersion = 0;
|
||||
private _keyframes = new Map<string, Keyframes>();
|
||||
private _cssFiles: string[] = [];
|
||||
|
||||
get css(): string {
|
||||
@ -749,10 +880,12 @@ export class StyleScope {
|
||||
this._cssFiles.push(cssFileName);
|
||||
currentScopeTag = cssFileName;
|
||||
|
||||
const cssSelectors = CSSSource.fromURI(cssFileName, this._keyframes);
|
||||
const cssFile = CSSSource.fromURI(cssFileName);
|
||||
|
||||
currentScopeTag = null;
|
||||
this._css = cssSelectors.source;
|
||||
this._localCssSelectors = cssSelectors.selectors;
|
||||
this._css = cssFile.source;
|
||||
this._localCssSelectors = cssFile.selectors;
|
||||
this._localCssKeyframes = cssFile.keyframes;
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
@ -761,8 +894,9 @@ export class StyleScope {
|
||||
private setCss(cssString: string, cssFileName?): void {
|
||||
this._css = cssString;
|
||||
|
||||
const cssFile = CSSSource.fromSource(cssString, this._keyframes, cssFileName);
|
||||
const cssFile = CSSSource.fromSource(cssString, cssFileName);
|
||||
this._localCssSelectors = cssFile.selectors;
|
||||
this._localCssKeyframes = cssFile.keyframes;
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
@ -777,24 +911,27 @@ export class StyleScope {
|
||||
currentScopeTag = cssFileName;
|
||||
}
|
||||
|
||||
const parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromURI(cssFileName, this._keyframes);
|
||||
const cssFile = cssString ? CSSSource.fromSource(cssString, cssFileName) : CSSSource.fromURI(cssFileName);
|
||||
|
||||
currentScopeTag = null;
|
||||
this._css = this._css + parsedCssSelectors.source;
|
||||
this._localCssSelectors.push(...parsedCssSelectors.selectors);
|
||||
this._css = this._css + cssFile.source;
|
||||
this._localCssSelectors.push(...cssFile.selectors);
|
||||
this._localCssKeyframes.push(...cssFile.keyframes);
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
|
||||
public getKeyframeAnimationWithName(animationName: string): kam.KeyframeAnimationInfo {
|
||||
const cssKeyframes = this._keyframes[animationName];
|
||||
if (!cssKeyframes) {
|
||||
return;
|
||||
if (!this._mergedCssKeyframes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyframeRule = this.findKeyframeRule(animationName);
|
||||
|
||||
ensureKeyframeAnimationModule();
|
||||
const animation = new keyframeAnimationModule.KeyframeAnimationInfo();
|
||||
ensureCssAnimationParserModule();
|
||||
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(cssKeyframes.keyframes);
|
||||
animation.keyframes = keyframeRule ? cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
|
||||
|
||||
return animation;
|
||||
}
|
||||
@ -821,36 +958,54 @@ export class StyleScope {
|
||||
|
||||
@profile
|
||||
private _createSelectors() {
|
||||
const toMerge: RuleSet[][] = [];
|
||||
toMerge.push(applicationCssSelectors.filter((v) => !v.scopedTag || this._cssFiles.indexOf(v.scopedTag) >= 0));
|
||||
const toMerge: RuleSet[] = [];
|
||||
const toMergeKeyframes: kam.Keyframes[] = [];
|
||||
|
||||
toMerge.push(...mergedApplicationCssSelectors.filter((v) => !v.scopedTag || this._cssFiles.indexOf(v.scopedTag) >= 0));
|
||||
toMergeKeyframes.push(...mergedApplicationCssKeyframes.filter((v) => !v.scopedTag || this._cssFiles.indexOf(v.scopedTag) >= 0));
|
||||
this._applicationCssSelectorsAppliedVersion = applicationCssSelectorVersion;
|
||||
toMerge.push(this._localCssSelectors);
|
||||
|
||||
toMerge.push(...this._localCssSelectors);
|
||||
toMergeKeyframes.push(...this._localCssKeyframes);
|
||||
this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion;
|
||||
for (const keyframe in applicationKeyframes) {
|
||||
this._keyframes[keyframe] = applicationKeyframes[keyframe];
|
||||
}
|
||||
|
||||
if (toMerge.length > 0) {
|
||||
this._mergedCssSelectors = toMerge.reduce((merged, next) => merged.concat(next || []), []);
|
||||
this._applyKeyframesOnSelectors();
|
||||
this._selectors = new SelectorsMap(this._mergedCssSelectors);
|
||||
this._mergedCssSelectors = toMerge;
|
||||
this._selectorScope = new StyleSheetSelectorScope(this._mergedCssSelectors);
|
||||
} else {
|
||||
this._mergedCssSelectors = null;
|
||||
this._selectorScope = null;
|
||||
}
|
||||
|
||||
this._mergedCssKeyframes = toMergeKeyframes.length > 0 ? toMergeKeyframes : null;
|
||||
}
|
||||
|
||||
// HACK: This @profile decorator creates a circular dependency
|
||||
// HACK: because the function parameter type is evaluated with 'typeof'
|
||||
@profile
|
||||
public matchSelectors(view): SelectorsMatch<ViewBase> {
|
||||
let match: SelectorsMatch<ViewBase>;
|
||||
|
||||
// should be (view: ViewBase): SelectorsMatch<ViewBase>
|
||||
this.ensureSelectors();
|
||||
|
||||
return this._selectors.query(view);
|
||||
if (this._selectorScope) {
|
||||
match = this._selectorScope.query(view);
|
||||
|
||||
// Make sure to re-apply keyframes to matching selectors as a media query keyframe might be applicable at this point
|
||||
this._applyKeyframesToSelectors(match.selectors);
|
||||
} else {
|
||||
match = null;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
public query(node: Node): SelectorCore[] {
|
||||
this.ensureSelectors();
|
||||
|
||||
return this._selectors.query(node).selectors;
|
||||
const match = this.matchSelectors(node);
|
||||
return match ? match.selectors : [];
|
||||
}
|
||||
|
||||
getSelectorsVersion() {
|
||||
@ -859,17 +1014,21 @@ export class StyleScope {
|
||||
return 100000 * this._applicationCssSelectorsAppliedVersion + this._localCssSelectorsAppliedVersion;
|
||||
}
|
||||
|
||||
private _applyKeyframesOnSelectors() {
|
||||
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
|
||||
const ruleset = this._mergedCssSelectors[i];
|
||||
const animations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
|
||||
if (animations !== undefined && animations.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
for (const animation of animations) {
|
||||
const cssKeyframe = this._keyframes[animation.name];
|
||||
if (cssKeyframe !== undefined) {
|
||||
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(cssKeyframe.keyframes);
|
||||
private _applyKeyframesToSelectors(selectors: SelectorCore[]) {
|
||||
if (!selectors?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = selectors.length - 1; i >= 0; i--) {
|
||||
const ruleset = selectors[i].ruleset;
|
||||
const animations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
|
||||
|
||||
if (animations != null && animations.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
|
||||
for (const animation of animations) {
|
||||
const keyframeRule = this.findKeyframeRule(animation.name);
|
||||
animation.keyframes = keyframeRule ? cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -878,9 +1037,39 @@ export class StyleScope {
|
||||
public getAnimations(ruleset: RuleSet): kam.KeyframeAnimationInfo[] {
|
||||
return ruleset[animationsSymbol];
|
||||
}
|
||||
|
||||
private findKeyframeRule(animationName: string): kam.Keyframes {
|
||||
if (!this._mergedCssKeyframes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
type KeyframesMap = Map<string, Keyframes>;
|
||||
// Cache media query results to avoid validations of other identical queries
|
||||
let validatedMediaQueries: string[];
|
||||
|
||||
// Iterate in reverse order as the last usable keyframe rule matters the most
|
||||
for (let i = this._mergedCssKeyframes.length - 1; i >= 0; i--) {
|
||||
const rule = this._mergedCssKeyframes[i];
|
||||
if (rule.name !== animationName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!rule.mediaQueryString) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
if (!validatedMediaQueries) {
|
||||
validatedMediaQueries = [];
|
||||
}
|
||||
|
||||
const isMatchingAllQueries = matchMediaQueryString(rule.mediaQueryString, validatedMediaQueries);
|
||||
if (isMatchingAllQueries) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean, importSource?: string): string {
|
||||
let fileName: string = typeof url === 'string' ? url.trim() : '';
|
||||
@ -896,7 +1085,7 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx
|
||||
|
||||
if (!isAbsolutePath) {
|
||||
if (fileName[0] === '~' && fileName[1] !== '/' && fileName[1] !== '"') {
|
||||
fileName = fileName.substr(1);
|
||||
fileName = fileName.substring(1);
|
||||
}
|
||||
|
||||
if (importSource) {
|
||||
@ -932,7 +1121,7 @@ function resolveFilePathFromImport(importSource: string, fileName: string): stri
|
||||
|
||||
export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
const localStyle = `local { ${styleStr} }`;
|
||||
const inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
|
||||
const inlineRuleSet = CSSSource.fromSource(localStyle).selectors;
|
||||
|
||||
// Reset unscoped css-variables
|
||||
view.style.resetUnscopedCssVariables();
|
||||
@ -978,6 +1167,14 @@ function isParentDirectory(uriPart: string): boolean {
|
||||
return uriPart === '..';
|
||||
}
|
||||
|
||||
function isKeyframe(node: CssNode): node is KeyframesDefinition {
|
||||
function isMedia(node: ReworkCSS.Node): node is ReworkCSS.Media {
|
||||
return node.type === 'media';
|
||||
}
|
||||
|
||||
function isKeyframe(node: ReworkCSS.Node): node is ReworkCSS.Keyframes {
|
||||
return node.type === 'keyframes';
|
||||
}
|
||||
|
||||
function isRule(node: ReworkCSS.Node): node is ReworkCSS.Rule {
|
||||
return node.type === 'rule';
|
||||
}
|
||||
|
Reference in New Issue
Block a user