Skip to content

Commit

Permalink
Merge pull request #31 from wix/technical/element-queries
Browse files Browse the repository at this point in the history
migrate from `css-element-queries` to `ResizeObserver` + custom logic
  • Loading branch information
BrooklynKing authored Mar 16, 2018
2 parents c3b1620 + 4142838 commit 5fdc42c
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@
},
"dependencies": {
"classnames": "^2.2.5",
"css-element-queries": "^1.0.1",
"dashjs": "^2.6.6",
"eventemitter3": "^2.0.3",
"hls.js": "^0.8.9",
"resize-observer-polyfill": "^1.5.0",
"tslib": "^1.9.0"
},
"devDependencies": {
Expand Down
15 changes: 12 additions & 3 deletions src/default-modules/root-container/root-container.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { ElementQueries } from 'css-element-queries';

import focusSource from '../../utils/focus-source';
import focusWithin from '../../utils/focus-within';

Expand All @@ -8,6 +6,7 @@ import playerAPI from '../../utils/player-api-decorator';
import { UI_EVENTS } from '../../constants/index';

import View from './root-container.view';
import ElementQueries from '../ui/core/element-queries';

const DEFAULT_CONFIG = {
fillAllSpace: false,
Expand All @@ -24,6 +23,7 @@ class RootContainer {

private _eventEmitter;
private _engine;
private _elementQueries: ElementQueries;
private _disengageFocusWithin;
private _disengageFocusSource;

Expand Down Expand Up @@ -121,7 +121,11 @@ class RootContainer {
this._enableFocusInterceptors();

node.appendChild(this.node);
ElementQueries.init();

if (!this._elementQueries) {
// NOTE: required for valid work of player "media queries"
this._elementQueries = new ElementQueries(this.node);
}
}

/**
Expand Down Expand Up @@ -205,6 +209,11 @@ class RootContainer {
this._unbindEvents();
this._disableFocusInterceptors();

if (this._elementQueries) {
this._elementQueries.destroy();
delete this._elementQueries;
}

this.view.destroy();
delete this.view;

Expand Down
57 changes: 57 additions & 0 deletions src/default-modules/ui/core/element-queries/element-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import ResizeObserver from 'resize-observer-polyfill';
import getQueriesForElement from './getQueriesForElement';

const DEFAULT_QUERY_PREFIX = 'data';

class ElementQueries {
private _element: Element;
private _queryPrefix: string;
private _queries: { mode: string; width: number }[];
private _observer: ResizeObserver;

constructor(element, { prefix = DEFAULT_QUERY_PREFIX } = {}) {
this._element = element;
this._queryPrefix = prefix;
this._queries = getQueriesForElement(element, prefix);

if (this._queries.length) {
this._observer = new ResizeObserver(([entry]) => {
this._onResized(entry.contentRect.width);
});

this._observer.observe(element);
}
}

private _getQueryAttributeValue(mode, elementWidth) {
return this._queries
.filter(query => query.mode === mode && query.width >= elementWidth)
.map(query => `${query.width}px`)
.join(' ');
}

private _setQueryAttribute(mode, elementWidth) {
const attributeName = `${this._queryPrefix}-${mode}-width`;
const attributeValue = this._getQueryAttributeValue(mode, elementWidth);

if (attributeValue) {
this._element.setAttribute(attributeName, attributeValue);
} else {
this._element.removeAttribute(attributeName);
}
}

private _onResized(width) {
this._setQueryAttribute('min', width);
this._setQueryAttribute('max', width);
}

destroy() {
if (this._observer) {
this._observer.unobserve(this._element);
this._observer = null;
}
}
}

export default ElementQueries;
110 changes: 110 additions & 0 deletions src/default-modules/ui/core/element-queries/getQueriesForElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { forEachMatch, reduce } from './utils';
import isElementMatchesSelector from '../isElementMatchesSelector';

// NOTE: "inspired" by https://github.com/marcj/css-element-queries/blob/1.0.2/src/ElementQueries.js#L340-L393

const CSS_SELECTOR_PATTERN = /,?[\s\t]*([^,\n]*?)((?:\[[\s\t]*?(?:[a-z-]+-)?(?:min|max)-width[\s\t]*?[~$\^]?=[\s\t]*?"[^"]*?"[\s\t]*?])+)([^,\n\s\{]*)/gim;
const QUERY_ATTR_PATTERN = /\[[\s\t]*?(?:([a-z-]+)-)?(min|max)-width[\s\t]*?[~$\^]?=[\s\t]*?"([^"]*?)"[\s\t]*?]/gim;

function getQueriesFromCssSelector(cssSelector: string) {
const results = [];

if (
cssSelector.indexOf('min-width') === -1 &&
cssSelector.indexOf('max-width') === -1
) {
return [];
}

cssSelector = cssSelector.replace(/'/g, '"');

forEachMatch(cssSelector, CSS_SELECTOR_PATTERN, match => {
const [selectorPart1, attribute, selectorPart2] = match.slice(1);
const selector = selectorPart1 + selectorPart2;

forEachMatch(attribute, QUERY_ATTR_PATTERN, match => {
const [prefix = '', mode, width] = match.slice(1);

results.push({
selector,
prefix,
mode,
width: parseInt(width, 10),
});
});
});

return results;
}

function getQueriesFromRules(rules: CSSRuleList) {
return reduce(
rules,
(results, rule) => {
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
// CSSRule.STYLE_RULE
if (rule.type === 1) {
const selector = rule.selectorText || rule.cssText;

return results.concat(getQueriesFromCssSelector(selector));
}

// NOTE: add other `CSSRule` types if required.
// Example - https://github.com/marcj/css-element-queries/blob/1.0.2/src/ElementQueries.js#L384-L390

return results;
},
[],
);
}

function getQueries() {
return reduce(
document.styleSheets,
(results, styleSheet) => {
const rules = styleSheet.cssRules || styleSheet.rules;

if (rules) {
return results.concat(getQueriesFromRules(rules));
}

if (styleSheet.cssText) {
return results.concat(getQueriesFromCssSelector(styleSheet.cssText));
}

return results;
},
[],
);
}

function getQueriesForElement(element, prefix = '') {
const matchedSelectors = new Map();
const queries = [];

getQueries().forEach(query => {
if (!matchedSelectors.has(query.selector)) {
matchedSelectors.set(query.selector, isElementMatchesSelector(element, query.selector));
}

if (!matchedSelectors.get(query.selector)) {
return;
}

if (
query.prefix === prefix &&
!queries.some(
_query => _query.mode === query.mode && _query.width === query.width,
)
) {
queries.push({
mode: query.mode,
width: query.width,
});
}
});

return queries.sort((a, b) => a.width - b.width);
}

export default getQueriesForElement;
1 change: 1 addition & 0 deletions src/default-modules/ui/core/element-queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './element-queries';
18 changes: 18 additions & 0 deletions src/default-modules/ui/core/element-queries/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function reduce(
arrayLike: { length: number },
callback: Function,
initialValue: any,
) {
return Array.prototype.reduce.call(arrayLike, callback, initialValue);
}

function forEachMatch(string: string, pattern: RegExp, callback: Function) {
let match = pattern.exec(string);

while (match !== null) {
callback(match);
match = pattern.exec(string);
}
}

export { reduce, forEachMatch };
26 changes: 26 additions & 0 deletions src/default-modules/ui/core/isElementMatchesSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const ALIASES = [
'matches',
'webkitMatchesSelector',
'mozMatchesSelector',
'msMatchesSelector',
];

let matchesSelectorFn;

for (let i = 0; i < ALIASES.length; i++) {
matchesSelectorFn = Element.prototype[ALIASES[i]];

if (matchesSelectorFn) {
break;
}
}

const isElementMatchesSelector = matchesSelectorFn
? (element, selector) => matchesSelectorFn.call(element, selector)
: (element, selector) =>
Array.prototype.indexOf.call(
document.querySelectorAll(selector),
element,
) !== -1;

export default isElementMatchesSelector;
9 changes: 1 addition & 8 deletions src/default-modules/ui/media-query-mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}

@mixin query-basic($query, $value) {
div[data-hook='player-container'][#{$query}~=#{'"' + $value + '"'}] & {
div[data-hook='player-container'][data-#{$query}~=#{'"' + $value + '"'}] & {
@content
}
}
Expand Down Expand Up @@ -34,13 +34,6 @@
}
}


@mixin max-width-280() {
@include query-basic(max-width, 280px) {
@content
}
}

@mixin max-width-280() {
@include query-basic(max-width, 280px) {
@content
Expand Down

0 comments on commit 5fdc42c

Please sign in to comment.