Skip to content

Commit

Permalink
#921@minor: Improves support for media queries.
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed May 17, 2023
1 parent 221e7b0 commit 42a8ce1
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 85 deletions.
5 changes: 5 additions & 0 deletions packages/happy-dom/src/match-media/IMediaQueryRange.ts
@@ -0,0 +1,5 @@
export default interface IMediaQueryRange {
before: { value: string; operator: string };
type: string;
after: { value: string; operator: string };
}
4 changes: 4 additions & 0 deletions packages/happy-dom/src/match-media/IMediaQueryRule.ts
@@ -0,0 +1,4 @@
export default interface IMediaQueryRule {
name: string;
value: string | null;
}
7 changes: 0 additions & 7 deletions packages/happy-dom/src/match-media/MediaQueryDeviceEnum.ts

This file was deleted.

147 changes: 117 additions & 30 deletions packages/happy-dom/src/match-media/MediaQueryItem.ts
@@ -1,44 +1,59 @@
import IWindow from '../window/IWindow';
import MediaQueryDeviceEnum from './MediaQueryDeviceEnum';
import IMediaQueryRange from './IMediaQueryRange';
import IMediaQueryRule from './IMediaQueryRule';
import MediaQueryTypeEnum from './MediaQueryTypeEnum';

/**
* Media query this.
*/
export default class MediaQueryItem {
public devices: MediaQueryDeviceEnum[];
public mediaTypes: MediaQueryTypeEnum[];
public not: boolean;
public rules: Array<{ name: string; value: string | null }>;
public rules: IMediaQueryRule[];
public ranges: IMediaQueryRange[];
private ownerWindow: IWindow;

/**
* Constructor.
*
* @param ownerWindow Window.
* @param [options] Options.
* @param [options.devices] Devices.
* @param [options.mediaTypes] Media types.
* @param [options.not] Not.
* @param [options.rules] Rules.
* @param options.ranges
*/
constructor(
ownerWindow: IWindow,
options?: {
devices?: MediaQueryDeviceEnum[];
mediaTypes?: MediaQueryTypeEnum[];
not?: boolean;
rules?: Array<{ name: string; value: string | null }>;
rules?: IMediaQueryRule[];
ranges?: IMediaQueryRange[];
}
) {
this.ownerWindow = ownerWindow;
this.devices = (options && options.devices) || [];
this.mediaTypes = (options && options.mediaTypes) || [];
this.not = (options && options.not) || false;
this.rules = (options && options.rules) || [];
this.ranges = (options && options.ranges) || [];
}

/**
* Returns media string.
*/
public toString(): string {
return `${this.not ? 'not ' : ''}${this.devices.join(', ')}${
(this.not || this.devices.length > 0) && !!this.rules.length ? ' and ' : ''
return `${this.not ? 'not ' : ''}${this.mediaTypes.join(', ')}${
(this.not || this.mediaTypes.length > 0) && !!this.ranges.length ? ' and ' : ''
}${this.ranges
.map(
(range) =>
`(${range.before ? `${range.before.value} ${range.before.operator} ` : ''}${range.type}${
range.after ? ` ${range.after.operator} ${range.after.value}` : ''
})`
)
.join(' and ')}${
(this.not || this.mediaTypes.length > 0) && !!this.rules.length ? ' and ' : ''
}${this.rules
.map((rule) => (rule.value ? `(${rule.name}: ${rule.value})` : `(${rule.name})`))
.join(' and ')}`;
Expand All @@ -57,16 +72,16 @@ export default class MediaQueryItem {
* @returns "true" if all matches.
*/
private matchesAll(): boolean {
if (!!this.devices.length) {
let isDeviceMatch = false;
for (const device of this.devices) {
if (this.matchesDevice(device)) {
isDeviceMatch = true;
if (!!this.mediaTypes.length) {
let isMediaTypeMatch = false;
for (const mediaType of this.mediaTypes) {
if (this.matchesMediaType(mediaType)) {
isMediaTypeMatch = true;
break;
}
}

if (!isDeviceMatch) {
if (!isMediaTypeMatch) {
return false;
}
}
Expand All @@ -77,35 +92,107 @@ export default class MediaQueryItem {
}
}

for (const range of this.ranges) {
if (!this.matchesRange(range)) {
return false;
}
}

return true;
}

/**
* Returns "true" if the device matches.
* Returns "true" if the mediaType matches.
*
* @param device Device.
* @returns "true" if the device matches.
* @param mediaType Media type.
* @returns "true" if the mediaType matches.
*/
private matchesDevice(device: MediaQueryDeviceEnum): boolean {
switch (device) {
case MediaQueryDeviceEnum.all:
return true;
case MediaQueryDeviceEnum.screen:
return true;
case MediaQueryDeviceEnum.print:
return false;
private matchesMediaType(mediaType: MediaQueryTypeEnum): boolean {
if (mediaType === MediaQueryTypeEnum.all) {
return true;
}
return (
mediaType ===
<MediaQueryTypeEnum>(<unknown>this.ownerWindow.happyDOM.settings.device.mediaType)
);
}

/**
* Returns "true" if the range matches.
*
* @param range Range.
* @returns "true" if the range matches.
*/
private matchesRange(range: IMediaQueryRange): boolean {
const size =
range.type === 'width' ? this.ownerWindow.innerWidth : this.ownerWindow.innerHeight;

if (range.before) {
const beforeValue = parseInt(range.before.value, 10);
if (!isNaN(beforeValue)) {
switch (range.before.operator) {
case '<':
if (beforeValue >= size) {
return false;
}
break;
case '<=':
if (beforeValue > size) {
return false;
}
break;
case '>':
if (beforeValue <= size) {
return false;
}
break;
case '>=':
if (beforeValue < size) {
return false;
}
break;
}
}
}

if (range.after) {
const afterValue = parseInt(range.after.value, 10);
if (!isNaN(afterValue)) {
switch (range.after.operator) {
case '<':
if (size >= afterValue) {
return false;
}
break;
case '<=':
if (size > afterValue) {
return false;
}
break;
case '>':
if (size <= afterValue) {
return false;
}
break;
case '>=':
if (size < afterValue) {
return false;
}
break;
}
}
}

return true;
}

/**
* Returns "true" if the rule matches.
*
* @param rule Rule.
* @param rule.name Rule name.
* @param rule.value Rule value.
* @returns "true" if the rule matches.
*/
private matchesRule(rule: { name: string; value: string | null }): boolean {
private matchesRule(rule: IMediaQueryRule): boolean {
if (!rule.value) {
switch (rule.name) {
case 'min-width':
Expand Down Expand Up @@ -145,7 +232,7 @@ export default class MediaQueryItem {
? this.ownerWindow.innerWidth > this.ownerWindow.innerHeight
: this.ownerWindow.innerWidth < this.ownerWindow.innerHeight;
case 'prefers-color-scheme':
return rule.value === this.ownerWindow.happyDOM.settings.colorScheme;
return rule.value === this.ownerWindow.happyDOM.settings.device.prefersColorScheme;
case 'any-hover':
case 'hover':
if (rule.value === 'none') {
Expand Down
66 changes: 25 additions & 41 deletions packages/happy-dom/src/match-media/MediaQueryParser.ts
@@ -1,5 +1,5 @@
import MediaQueryItem from './MediaQueryItem';
import MediaQueryDeviceEnum from './MediaQueryDeviceEnum';
import MediaQueryTypeEnum from './MediaQueryTypeEnum';
import IWindow from '../window/IWindow';

/**
Expand All @@ -21,16 +21,14 @@ const IS_RESOLUTION_REGEXP = /[<>]/;
/**
* Resolution RegExp.
*
* Group 1: First resolution number.
* Group 2: First resolution entity.
* Group 3: First resolution operator.
* Group 4: Resolution type.
* Group 5: Second resolution operator.
* Group 6: Second resolution number.
* Group 7: Second resolution entity.
* Group 1: First resolution value.
* Group 2: First resolution operator.
* Group 3: Resolution type.
* Group 4: Second resolution operator.
* Group 5: Second resolution value.
*/
const RESOLUTION_REGEXP =
/(?:([0-9]+)([a-z]+) *(<|<=|>|=>)){0,1} *(width|height) *(?:(<|<=|>|=>) *([0-9]+)([a-z]+)){0,1}/;
/(?:([0-9]+[a-z]+) *(<|<=|>|=>)){0,1} *(width|height) *(?:(<|<=|>|=>) *([0-9]+[a-z]+)){0,1}/;

/**
* Utility for parsing a query string.
Expand All @@ -54,49 +52,35 @@ export default class MediaQueryParser {
currentMediaQueryItem = new MediaQueryItem(ownerWindow);
mediaQueryItems.push(currentMediaQueryItem);
} else if (match[1] === 'all' || match[1] === 'screen' || match[1] === 'print') {
currentMediaQueryItem.devices.push(<MediaQueryDeviceEnum>match[1]);
currentMediaQueryItem.mediaTypes.push(<MediaQueryTypeEnum>match[1]);
} else if (match[1] === 'not') {
currentMediaQueryItem.not = true;
} else if (match[2]) {
const resolutionMatch = IS_RESOLUTION_REGEXP.test(match[2])
? match[2].match(RESOLUTION_REGEXP)
: null;
if (resolutionMatch && (resolutionMatch[1] || resolutionMatch[6])) {
if (resolutionMatch[1] && resolutionMatch[2] && resolutionMatch[3]) {
const value = parseInt(resolutionMatch[1], 10);
const parsedValue =
resolutionMatch[1] === '<'
? value - 1
: resolutionMatch[1] === '>'
? value + 1
: value;
currentMediaQueryItem.rules.push({
name: `${resolutionMatch[3] === '<' || resolutionMatch[3] === '<=' ? 'max' : 'min'}-${
resolutionMatch[3]
}`,
value: `${parsedValue}${resolutionMatch[2]}`
});
} else if (resolutionMatch[4] && resolutionMatch[5] && resolutionMatch[6]) {
const value = parseInt(resolutionMatch[1], 10);
const parsedValue =
resolutionMatch[6] === '<'
? value + 1
: resolutionMatch[6] === '>'
? value - 1
: value;
currentMediaQueryItem.rules.push({
name: `${resolutionMatch[6] === '<' || resolutionMatch[6] === '<=' ? 'min' : 'max'}-${
resolutionMatch[5]
}`,
value: `${parsedValue}${resolutionMatch[5]}`
});
}
if (resolutionMatch && (resolutionMatch[1] || resolutionMatch[5])) {
currentMediaQueryItem.ranges.push({
before: resolutionMatch[1]
? {
value: resolutionMatch[1],
operator: resolutionMatch[2]
}
: null,
type: resolutionMatch[3],
after: resolutionMatch[5]
? {
value: resolutionMatch[5],
operator: resolutionMatch[4]
}
: null
});
} else {
const [name, value] = match[2].split(':');
const trimmedValue = value ? value.trim() : null;
if (!trimmedValue && !match[3]) {
return [
new MediaQueryItem(ownerWindow, { not: true, devices: [MediaQueryDeviceEnum.all] })
new MediaQueryItem(ownerWindow, { not: true, mediaTypes: [MediaQueryTypeEnum.all] })
];
}
currentMediaQueryItem.rules.push({
Expand Down
7 changes: 7 additions & 0 deletions packages/happy-dom/src/match-media/MediaQueryTypeEnum.ts
@@ -0,0 +1,7 @@
enum MediaQueryTypeEnum {
all = 'all',
print = 'print',
screen = 'screen'
}

export default MediaQueryTypeEnum;
@@ -0,0 +1,6 @@
enum HappyDOMSettingsMediaTypeEnum {
screen = 'screen',
print = 'print'
}

export default HappyDOMSettingsMediaTypeEnum;
@@ -0,0 +1,6 @@
enum HappyDOMSettingsPrefersColorSchemeEnum {
light = 'light',
dark = 'dark'
}

export default HappyDOMSettingsPrefersColorSchemeEnum;
7 changes: 7 additions & 0 deletions packages/happy-dom/src/window/IHappyDOMOptions.ts
@@ -1,3 +1,6 @@
import HappyDOMSettingsMediaTypeEnum from './HappyDOMSettingsMediaTypeEnum';
import HappyDOMSettingsPrefersColorSchemeEnum from './HappyDOMSettingsPrefersColorSchemeEnum';

/**
* Happy DOM options.
*/
Expand All @@ -11,5 +14,9 @@ export default interface IHappyDOMOptions {
disableCSSFileLoading?: boolean;
disableIframePageLoading?: boolean;
enableFileSystemHttpRequests?: boolean;
device?: {
prefersColorScheme?: HappyDOMSettingsPrefersColorSchemeEnum;
mediaType?: HappyDOMSettingsMediaTypeEnum;
};
};
}
8 changes: 7 additions & 1 deletion packages/happy-dom/src/window/IHappyDOMSettings.ts
@@ -1,3 +1,6 @@
import HappyDOMSettingsMediaTypeEnum from './HappyDOMSettingsMediaTypeEnum';
import HappyDOMSettingsPrefersColorSchemeEnum from './HappyDOMSettingsPrefersColorSchemeEnum';

/**
* Happy DOM settings.
*/
Expand All @@ -7,5 +10,8 @@ export default interface IHappyDOMSettings {
disableCSSFileLoading: boolean;
disableIframePageLoading: boolean;
enableFileSystemHttpRequests: boolean;
colorScheme: string;
device: {
prefersColorScheme: HappyDOMSettingsPrefersColorSchemeEnum;
mediaType: HappyDOMSettingsMediaTypeEnum;
};
}

0 comments on commit 42a8ce1

Please sign in to comment.