Skip to content

Commit

Permalink
#344@trivial: Continue on CSSStyleDeclaration.
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Sep 28, 2022
1 parent d1a0bac commit 36aceeb
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 24 deletions.
Expand Up @@ -184,4 +184,18 @@ export default abstract class AbstractCSSStyleDeclaration {
}
return this._style.get(name)?.value || '';
}

/**
* Returns a property.
*
* @param name Property name in kebab case.
* @returns "important" if set to be important.
*/
public getPropertyPriority(name: string): string {
if (this._ownerElement) {
const style = CSSStyleDeclarationElement.getElementStyle(this._ownerElement, this._computed);
return style.get(name)?.important ? 'important' : '';
}
return this._style.get(name)?.important ? 'important' : '';
}
}
Expand Up @@ -98,7 +98,9 @@ export default class CSSStyleDeclarationElement {
);
for (const name of Object.keys(properties)) {
if (CSSStyleDeclarationElementInheritedProperties.includes(name)) {
inheritedProperties[name] = properties[name];
if (!inheritedProperties[name]?.important || properties[name].important) {
inheritedProperties[name] = properties[name];
}
}
}
}
Expand All @@ -107,14 +109,21 @@ export default class CSSStyleDeclarationElement {
targetElement.cssText + (targetElement.element['_attributes']['style']?.value || '')
);

targetPropertyManager.properties = Object.assign(
const targetProperties = Object.assign(
{},
CSSStyleDeclarationElementDefaultProperties.default,
CSSStyleDeclarationElementDefaultProperties[targetElement.element.tagName],
inheritedProperties,
targetPropertyManager.properties
inheritedProperties
);

for (const name of Object.keys(targetPropertyManager.properties)) {
if (!targetProperties[name]?.important || targetPropertyManager.properties[name].important) {
targetProperties[name] = targetPropertyManager.properties[name];
}
}

targetPropertyManager.properties = targetProperties;

return targetPropertyManager;
}

Expand Down
Expand Up @@ -31,7 +31,10 @@ export default class CSSStyleDeclarationPropertyManager {
const important = trimmedValue.endsWith(' !important');
const valueWithoutImportant = trimmedValue.replace(' !important', '');

if (valueWithoutImportant) {
if (
valueWithoutImportant &&
(important || !this.properties[trimmedName]?.important)
) {
this.set(trimmedName, valueWithoutImportant, important);
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/happy-dom/src/event/events/IMediaQueryListInit.ts
@@ -0,0 +1,6 @@
import IEventInit from '../IEventInit';

export default interface IMediaQueryListInit extends IEventInit {
matches?: boolean;
media?: string;
}
25 changes: 25 additions & 0 deletions packages/happy-dom/src/event/events/MediaQueryListEvent.ts
@@ -0,0 +1,25 @@
import Event from '../Event';
import IMediaQueryListInit from './IMediaQueryListInit';

/**
*
*/
export default class MediaQueryListEvent extends Event {
public readonly matches: boolean = false;
public readonly media: string = '';

/**
* Constructor.
*
* @param type Event type.
* @param [eventInit] Event init.
*/
constructor(type: string, eventInit: IMediaQueryListInit = null) {
super(type, eventInit);

if (eventInit) {
this.matches = eventInit.matches || false;
this.media = eventInit.media || '';
}
}
}
73 changes: 63 additions & 10 deletions packages/happy-dom/src/match-media/MediaQueryList.ts
@@ -1,5 +1,11 @@
import EventTarget from '../event/EventTarget';
import Event from '../event/Event';
import IWindow from '../window/IWindow';
import IEventListener from '../event/IEventListener';
import MediaQueryListEvent from '../event/events/MediaQueryListEvent';

const MEDIA_REGEXP =
/min-width: *([0-9]+) *px|max-width: *([0-9]+) *px|min-height: *([0-9]+) *px|max-height: *([0-9]+) *px/;

/**
* Media Query List.
Expand All @@ -8,26 +14,41 @@ import Event from '../event/Event';
* https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList.
*/
export default class MediaQueryList extends EventTarget {
public _matches = false;
public _media = '';
public readonly media: string = '';
public onchange: (event: Event) => void = null;
private _ownerWindow: IWindow;

/**
* Returns "true" if the document matches.
* Constructor.
*
* @returns Matches.
* @param ownerWindow Window.
* @param media Media.
*/
public get matches(): boolean {
return this._matches;
constructor(ownerWindow: IWindow, media: string) {
super();
this._ownerWindow = ownerWindow;
this.media = media;
}

/**
* Returns the serialized media query.
* Returns "true" if the document matches.
*
* @returns Serialized media query.
* @returns Matches.
*/
public get media(): string {
return this._media;
public get matches(): boolean {
const match = MEDIA_REGEXP.exec(this.media);
if (match) {
if (match[1]) {
return this._ownerWindow.innerWidth >= parseInt(match[1]);
} else if (match[2]) {
return this._ownerWindow.innerWidth <= parseInt(match[2]);
} else if (match[3]) {
return this._ownerWindow.innerHeight >= parseInt(match[3]);
} else if (match[4]) {
return this._ownerWindow.innerHeight <= parseInt(match[4]);
}
}
return false;
}

/**
Expand All @@ -49,4 +70,36 @@ export default class MediaQueryList extends EventTarget {
public removeListener(callback: (event: Event) => void): void {
this.removeEventListener('change', callback);
}

/**
* @override
*/
public addEventListener(type: string, listener: IEventListener | ((event: Event) => void)): void {
super.addEventListener(type, listener);
if (type === 'change') {
let matchesState = false;
const resizeListener = (): void => {
const matches = this.matches;
if (matches !== matchesState) {
matchesState = matches;
this.dispatchEvent(new MediaQueryListEvent('change', { matches, media: this.media }));
}
};
listener['_windowResizeListener'] = resizeListener;
this._ownerWindow.addEventListener('resize', resizeListener);
}
}

/**
* @override
*/
public removeEventListener(
type: string,
listener: IEventListener | ((event: Event) => void)
): void {
super.removeEventListener(type, listener);
if (type === 'change' && listener['_windowResizeListener']) {
this._ownerWindow.removeEventListener('resize', listener['_windowResizeListener']);
}
}
}
4 changes: 4 additions & 0 deletions packages/happy-dom/src/window/IWindow.ts
Expand Up @@ -35,6 +35,7 @@ import CustomEvent from '../event/events/CustomEvent';
import AnimationEvent from '../event/events/AnimationEvent';
import KeyboardEvent from '../event/events/KeyboardEvent';
import ProgressEvent from '../event/events/ProgressEvent';
import MediaQueryListEvent from '../event/events/MediaQueryListEvent';
import EventTarget from '../event/EventTarget';
import URL from '../location/URL';
import Location from '../location/Location';
Expand Down Expand Up @@ -97,6 +98,8 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
whenAsyncComplete: () => Promise<void>;
cancelAsync: () => void;
asyncTaskManager: AsyncTaskManager;
setInnerWidth: (width: number) => void;
setInnerHeight: (height: number) => void;
};

// Global classes
Expand Down Expand Up @@ -147,6 +150,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
readonly ErrorEvent: typeof ErrorEvent;
readonly StorageEvent: typeof StorageEvent;
readonly ProgressEvent: typeof ProgressEvent;
readonly MediaQueryListEvent: typeof MediaQueryListEvent;
readonly EventTarget: typeof EventTarget;
readonly DataTransfer: typeof DataTransfer;
readonly DataTransferItem: typeof DataTransferItem;
Expand Down
39 changes: 31 additions & 8 deletions packages/happy-dom/src/window/Window.ts
Expand Up @@ -35,6 +35,7 @@ import CustomEvent from '../event/events/CustomEvent';
import AnimationEvent from '../event/events/AnimationEvent';
import KeyboardEvent from '../event/events/KeyboardEvent';
import ProgressEvent from '../event/events/ProgressEvent';
import MediaQueryListEvent from '../event/events/MediaQueryListEvent';
import EventTarget from '../event/EventTarget';
import URL from '../location/URL';
import Location from '../location/Location';
Expand Down Expand Up @@ -119,7 +120,19 @@ export default class Window extends EventTarget implements IWindow {
cancelAsync: (): void => {
this.happyDOM.asyncTaskManager.cancelAll();
},
asyncTaskManager: new AsyncTaskManager()
asyncTaskManager: new AsyncTaskManager(),
setInnerWidth: (width: number): void => {
if (this.innerWidth !== width) {
(<number>this.innerWidth) = width;
this.dispatchEvent(new Event('resize'));
}
},
setInnerHeight: (height: number): void => {
if (this.innerHeight !== height) {
(<number>this.innerHeight) = height;
this.dispatchEvent(new Event('resize'));
}
}
};

// Global classes
Expand Down Expand Up @@ -168,6 +181,7 @@ export default class Window extends EventTarget implements IWindow {
public readonly ErrorEvent = ErrorEvent;
public readonly StorageEvent = StorageEvent;
public readonly ProgressEvent = ProgressEvent;
public readonly MediaQueryListEvent = MediaQueryListEvent;
public readonly EventTarget = EventTarget;
public readonly DataTransfer = DataTransfer;
public readonly DataTransferItem = DataTransferItem;
Expand All @@ -188,7 +202,6 @@ export default class Window extends EventTarget implements IWindow {
public readonly URLSearchParams = URLSearchParams;
public readonly HTMLCollection = HTMLCollection;
public readonly NodeList = NodeList;
public readonly MediaQueryList = MediaQueryList;
public readonly CSSUnitValue = CSSUnitValue;
public readonly Selection = Selection;
public readonly Navigator = Navigator;
Expand Down Expand Up @@ -226,12 +239,12 @@ export default class Window extends EventTarget implements IWindow {
public readonly window = this;
public readonly globalThis = this;
public readonly screen = new Screen();
public readonly innerWidth = 1024;
public readonly innerHeight = 768;
public readonly devicePixelRatio = 1;
public readonly sessionStorage = new Storage();
public readonly localStorage = new Storage();
public readonly performance = PerfHooks.performance;
public readonly innerWidth: number;
public readonly innerHeight: number;

// Node.js Globals
public ArrayBuffer;
Expand Down Expand Up @@ -304,10 +317,22 @@ export default class Window extends EventTarget implements IWindow {

/**
* Constructor.
*
* @param [options] Options.
* @param [options.innerWidth] Inner width.
* @param [options.innerHeight] Inner height.
* @param [options.url] URL.
*/
constructor() {
constructor(options?: { innerWidth?: number; innerHeight?: number; url?: string }) {
super();

this.innerWidth = options?.innerWidth ? options.innerWidth : 0;
this.innerHeight = options?.innerHeight ? options.innerHeight : 0;

if (options?.url) {
this.location.href = options.url;
}

this._setTimeout = ORIGINAL_SET_TIMEOUT;
this._clearTimeout = ORIGINAL_CLEAR_TIMEOUT;
this._setInterval = ORIGINAL_SET_INTERVAL;
Expand Down Expand Up @@ -485,9 +510,7 @@ export default class Window extends EventTarget implements IWindow {
* @returns A new MediaQueryList.
*/
public matchMedia(mediaQueryString: string): MediaQueryList {
const mediaQueryList = new MediaQueryList();
mediaQueryList._media = mediaQueryString;
return mediaQueryList;
return new MediaQueryList(this, mediaQueryString);
}

/**
Expand Down
Expand Up @@ -2183,5 +2183,31 @@ describe('CSSStyleDeclaration', () => {
expect(declaration.getPropertyValue('font-size')).toBe('12px');
expect(declaration.getPropertyValue('background')).toBe('');
});

it('Does not override important values when defined multiple times.', () => {
const declaration = new CSSStyleDeclaration(element);

element.setAttribute(
'style',
`text-transform: uppercase !important; text-transform: capitalize;`
);

expect(declaration.getPropertyValue('text-transform')).toBe('uppercase');
expect(declaration.getPropertyPriority('text-transform')).toBe('important');
});
});

describe('getPropertyPriority()', () => {
it('Returns property priority.', () => {
const declaration = new CSSStyleDeclaration(element);

element.setAttribute('style', `text-transform: uppercase`);

expect(declaration.getPropertyPriority('text-transform')).toBe('');

element.setAttribute('style', `text-transform: uppercase !important`);

expect(declaration.getPropertyPriority('text-transform')).toBe('important');
});
});
});

0 comments on commit 36aceeb

Please sign in to comment.