-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #31 from wix/technical/element-queries
migrate from `css-element-queries` to `ResizeObserver` + custom logic
- Loading branch information
Showing
8 changed files
with
226 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
src/default-modules/ui/core/element-queries/element-queries.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
110
src/default-modules/ui/core/element-queries/getQueriesForElement.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './element-queries'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters