element-adapter
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -1,1 +0,1 @@ | ||
const e=e=>e.length>0?Array.from(new Set(e)):e,t=(e,t)=>r=>e*r[t],r=e=>()=>e,s=(e,t)=>{const r=typeof e,s=typeof t;if(r===s)return!0;throw new Error(`type mismatch: a(${e}) is ${r} and b(${t}) is ${s}`)};var n={">":e=>(t,r)=>{const n=e(t);return s(r,n)&&r>n},">=":e=>(t,r)=>{const n=e(t);return s(r,n)&&r>=n},"<":e=>(t,r)=>{const n=e(t);return s(r,n)&&r<n},"<=":e=>(t,r)=>{const n=e(t);return s(r,n)&&r<=n},"==":e=>(t,r)=>{const n=e(t);return s(r,n)&&r===e(t)}};const i={width:"w%",height:"h%"},o=Object.entries(i).reduce((e,[t,r])=>({...e,[r]:t}),{}),a=(e,t,r)=>{if(0===t.length)return;const s=document.createElement("b");s.style.position="absolute",e.appendChild(s);const n=t.reduce((e,t)=>({...e,[t]:r(s,t)}),{});return e.removeChild(s),n},c=(e,t)=>a(e,t,(e,t)=>(e.style.width="1"+t,e.getBoundingClientRect().width)),p=(e,t)=>a(e.parentNode,t,(e,t)=>{const r=o[t];e.style[r]="1%";const{[r]:s}=e.getBoundingClientRect();return s}),u=e=>["width","height"].includes(e),h=(e,t)=>e>t?"landscape":e<t?"portrait":"square",d=(e,t)=>0===e&&0===t?1:e/t,l=["width","height","aspect-ratio","orientation","children","characters"],m=["%","cap","ch","em","ex","ic","lh","rem","rlh","vb","vh","vi","vw","vmin","vmax","mm","Q","cm","in","pt","pc"],w=new RegExp(`^(\\d+(\\.\\d+)?)(${m.join("|")})$`),g=/^\d+(\.\d+)?(px)?$/,f=e=>{const s=e.trim().toLowerCase().split(/\s*,\s*/),o=[],a=[],c=[],p=[];for(const e of s){const s=e.split(/\s*&&\s*/),u=[];for(const e of s){const[s,o,h]=e.split(/\s+/),[,d,,l]=h.match(w)||[],m="%"===l?i[s]:l,f=m&&parseFloat(d);let b;if(p.push(s),m)("%"===l?c:a).push(m),b=t(f,m);else{const e=h.match(g)?parseFloat(h):h;b=r(e)}u.push({[s]:n[o](b)})}o.push(u)}return{query:o,units:a,percentUnits:c,watchedProperties:p}},b=({query:e,unitsMeasurements:t,props:r})=>e.some(e=>e.every(e=>{const[[s,n]]=Object.entries(e);return n(t,r[s])})),v=["button","submit","image","checkbox","radio","hidden","range","reset"],y=e=>"INPUT"===e.tagName&&!v.includes(e.getAttribute("type"))||"TEXTAREA"===e.tagName,E=e=>y(e)?e.value.trim().length:e.isContentEditable?e.textContent.trim().length:0,C=e=>e.isContentEditable,q=(e,t)=>e.find(e=>e===t)||(t.parentNode?q(e,t.parentNode):void 0),U=(e,t,r=!1)=>{e.observe(t,{childList:!0,characterData:r,subtree:r})},O=e=>{const t=["width","height","orientation","aspect-ratio"];return e.some(e=>t.includes(e))},P=e=>e.includes("characters"),$=(e,t)=>P(e)&&t.isContentEditable,j=e=>e.includes("children"),A=(e,t)=>!y(t)&&($(e,t)||j(e)),x=(e,t)=>{const r={};if(O(t)){const{clientWidth:t,clientHeight:s}=e,{paddingTop:n,paddingRight:i,paddingBottom:o,paddingLeft:a}=window.getComputedStyle(e),c=t-(parseInt(a,10)+parseInt(i,10)),p=s-(parseInt(n,10)+parseInt(o,10));Object.assign(r,{width:c,height:p,orientation:h(c,p),"aspect-ratio":d(c,p)})}return P(t)&&(e.isContentEditable||y(e))&&(r.characters=E(e)),!j(t)||e.isContentEditable||y(e)||(r.children=e.childElementCount),r},L=({elt:e,props:t,queries:r,units:s,percentUnits:n})=>{(({elt:e,props:t,queries:r,unitsMeasurements:s})=>{const n={...t,characters:t.characters||0,children:t.children||0};for(const[t,i]of Object.entries(r))e.classList.toggle(t,b({query:i,unitsMeasurements:s,props:n}));for(const[r,s]of Object.entries(t))e.style.setProperty("--ea-"+r,u(r)?s+"px":s)})({elt:e,props:t,queries:r,unitsMeasurements:{...c(e,s),...p(e,n)}})},Q={},R=`((width|height)\\s+(((>|<)=?)|==)\\s+\\d+(\\.\\d+)?(${m.join("|")}|px)|(characters|children)\\s+(((>|<)=?)|==)\\s+\\d+|aspect-ratio\\s+(((>|<)=?)|==)\\s+\\d+(\\.\\d+)?|orientation\\s+==\\s+(landscape|portrait|square))`,M=`${R}(\\s+&&\\s+${R})*`,N=new RegExp(`^\\s*${M}(\\s*,\\s*${M})*\\s*$`);export default function({target:t,queries:r={},...s}={}){(e=>{for(const t of Object.values(e))if(!t||!t.match(N))throw new Error(`invalid query "${t}"`)})(r),(({watchedProperties:e})=>{if(e){if(!Array.isArray(e))throw new Error("watchedProperties must be an array");if(e.length<1||!e.every(e=>l.includes(e)))throw new Error("watchedProperties must be an array with at least one of "+l.join(", "))}})(s);const n=(e=>{if(!e)throw new Error("target must be provided");const t="length"in Object(e)?Array.isArray(e)?e:Array.from(e):[e];if(t.length<1)throw new Error("at least one Element must be provided as target");if(t.some(e=>!(e instanceof window.Element)))throw new Error(`target must be an Element or a list of Elements. Actual:\n[${t.map(e=>String(e)).join(", ")}]`);return t})(t),{compiledQueries:i,units:o,percentUnits:a,watchedProperties:c}=(t=>{const r={},s=[],n=[],i=[];for(const[e,o]of Object.entries(t)){const t=f(o);r[e]=t.query,s.push(...t.units),n.push(...t.percentUnits),i.push(...t.watchedProperties)}return{compiledQueries:r,units:e(s),percentUnits:e(n),watchedProperties:e(i)}})(r);if(c.length<1&&!s.watchedProperties)throw new Error("at least one query or one watched properties must be provided");const{unobserve:p,applyStyle:u}=(e=>{const{ResizeObserver:t,MutationObserver:r}=window,s=new WeakMap,n={...e,propsCache:s},{elements:i,compiledQueries:o,units:a,percentUnits:c,watchedProperties:p}=e,u=O(p)&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:s})=>n=>{for(const{contentRect:{width:i,height:o},target:a}of n){const n={...e.get(a),width:i,height:o,orientation:h(i,o),"aspect-ratio":d(i,o)};e.set(a,n),window.requestAnimationFrame(()=>L({elt:a,props:n,queries:t,units:r,percentUnits:s}))}})(n),m=u&&new t(u),w=P(p)&&i.some(y)&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:s})=>({target:n})=>{const i=n.value.trim().length,o=e.get(n);if(i!==o.characters){const i={...o,characters:E(n)};e.set(n,i),L({elt:n,props:i,queries:t,units:r,percentUnits:s})}})(n),g=(j(p)&&!i.every(y)||P(p)&&i.some(C))&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:s,elements:n,watchedProperties:i})=>(o,a)=>{a.disconnect();for(const{type:a,target:c}of o)if(["childList","characterData"].includes(a)){const o=q(n,c);if(o){let n,a;const c={...e.get(o)};j(i)&&!o.isContentEditable&&(c.children=o.childElementCount,n=!0),$(i,o)&&(c.characters=E(o),a=!0),(n||a)&&(e.set(o,c),L({elt:o,props:c,queries:t,units:r,percentUnits:s}))}}for(const e of n)A(i,e)&&U(a,e,$(i,e))})(n),f=g&&new r(g);for(const e of i){const t=x(e,p);s.set(e,t),m&&m.observe(e),w&&y(e)&&e.addEventListener("input",w),f&&A(p,e)&&f&&U(f,e,$(p,e)),L({elt:e,props:t,queries:o,units:a,percentUnits:c})}return{unobserve:()=>{s.set(Q,!0),(({elements:e,mutationObserver:t,resizeObserver:r,inputListener:s,behaviourCssClasses:n})=>{t&&t.disconnect();for(const t of e)r&&r.unobserve(t),s&&y(t)&&t.removeEventListener("input",s),t.classList.remove(...n),l.forEach(e=>t.style.removeProperty("--ea-"+e))})({elements:i,mutationObserver:f,resizeObserver:m,inputListener:w,behaviourCssClasses:Object.keys(o)})},applyStyle:()=>{s.get(Q)||i.forEach(e=>L({elt:e,props:s.get(e),queries:o,units:a,percentUnits:c}))}}})({elements:n,compiledQueries:i,units:o,percentUnits:a,watchedProperties:e([...c,...s.watchedProperties||[]])});return{removeAdaptiveBehaviour:p,applyAdaptiveBehaviour:u}} | ||
const e=e=>e.length>0?Array.from(new Set(e)):e,t=(e,t)=>r=>e*r[t],r=e=>()=>e,n=(e,t)=>{const r=typeof e,n=typeof t;if(r===n)return!0;throw new Error(`type mismatch: a(${e}) is ${r} and b(${t}) is ${n}`)};var s={">":e=>(t,r)=>{const s=e(t);return n(r,s)&&r>s},">=":e=>(t,r)=>{const s=e(t);return n(r,s)&&r>=s},"<":e=>(t,r)=>{const s=e(t);return n(r,s)&&r<s},"<=":e=>(t,r)=>{const s=e(t);return n(r,s)&&r<=s},"==":e=>(t,r)=>{const s=e(t);return n(r,s)&&r===e(t)}};const i={width:"w%",height:"h%"},o=Object.entries(i).reduce((e,[t,r])=>({...e,[r]:t}),{}),a=(e,t,r)=>{if(0===t.length)return;const n=document.createElement("b");n.style.position="absolute",e.appendChild(n);const s=t.reduce((e,t)=>({...e,[t]:r(n,t)}),{});return e.removeChild(n),s},c=(e,t)=>a(e,t,(e,t)=>(e.style.width="1"+t,e.getBoundingClientRect().width)),p=(e,t)=>a(e.parentNode,t,(e,t)=>{const r=o[t];e.style[r]="1%";const{[r]:n}=e.getBoundingClientRect();return n}),u=e=>["width","height"].includes(e),d=(e,t)=>e>t?"landscape":e<t?"portrait":"square",h=(e,t)=>0===e&&0===t?1:e/t,l=["width","height","aspect-ratio","orientation","children","characters"],m=["%","cap","ch","em","ex","ic","lh","rem","rlh","vb","vh","vi","vw","vmin","vmax","mm","Q","cm","in","pt","pc"],w=new RegExp(`^(\\d+(\\.\\d+)?)(${m.join("|")})$`),f=/^\d+(\.\d+)?(px)?$/,g=e=>{const n=e.trim().toLowerCase().split(/\s*,\s*/),o=[],a=[],c=[],p=[];for(const e of n){const n=e.split(/\s*&&\s*/),u=[];for(const e of n){const[n,o,d]=e.split(/\s+/),[,h,,l]=d.match(w)||[],m="%"===l?i[n]:l,g=m&&parseFloat(h);let v;if(p.push(n),m)("%"===l?c:a).push(m),v=t(g,m);else{const e=d.match(f)?parseFloat(d):d;v=r(e)}u.push({[n]:s[o](v)})}o.push(u)}return{query:o,units:a,percentUnits:c,watchedProperties:p}},v=({query:e,unitsMeasurements:t,props:r})=>e.some(e=>e.every(e=>{const[[n,s]]=Object.entries(e);return s(t,r[n])})),b=["button","submit","image","checkbox","radio","hidden","range","reset"],y=e=>"INPUT"===e.tagName&&!b.includes(e.getAttribute("type"))||"TEXTAREA"===e.tagName,E=e=>y(e)?e.value.trim().length:e.isContentEditable?e.textContent.trim().length:0,C=e=>e.isContentEditable,q=(e,t)=>e.find(e=>e===t)||(t.parentNode?q(e,t.parentNode):void 0),U=(e,t,r=!1)=>{e.observe(t,{childList:!0,characterData:r,subtree:r})},$=e=>{const t=["width","height","orientation","aspect-ratio"];return e.some(e=>t.includes(e))},A=e=>e.includes("characters"),P=(e,t)=>A(e)&&t.isContentEditable,O=e=>e.includes("children"),j=(e,t)=>!y(t)&&(P(e,t)||O(e)),x=(e,t)=>{const r={};if($(t)){const{clientWidth:t,clientHeight:n}=e,{paddingTop:s,paddingRight:i,paddingBottom:o,paddingLeft:a}=window.getComputedStyle(e),c=t-(parseInt(a,10)+parseInt(i,10)),p=n-(parseInt(s,10)+parseInt(o,10));Object.assign(r,{width:c,height:p,orientation:d(c,p),"aspect-ratio":h(c,p)})}return A(t)&&(e.isContentEditable||y(e))&&(r.characters=E(e)),!O(t)||e.isContentEditable||y(e)||(r.children=e.childElementCount),r},L=new WeakMap,M=({elt:e,props:t,queries:r,units:n,percentUnits:s})=>{(({elt:e,props:t,queries:r,unitsMeasurements:n})=>{const s={...t,characters:t.characters||0,children:t.children||0};for(const[t,o]of r.entries()){const r=v({query:o,unitsMeasurements:n,props:s});var i;"string"==typeof t?e.classList.toggle(t,r):r!==Boolean(null==(i=L.get(e))?void 0:i.get(o))&&(t(e,s),L.get(e)||L.set(e,new WeakMap),L.get(e).set(o,r))}for(const[r,n]of Object.entries(t))e.style.setProperty("--ea-"+r,u(r)?n+"px":n)})({elt:e,props:t,queries:r,unitsMeasurements:{...c(e,n),...p(e,s)}})},B={},Q=`((width|height)\\s+(((>|<)=?)|==)\\s+\\d+(\\.\\d+)?(${m.join("|")}|px)|(characters|children)\\s+(((>|<)=?)|==)\\s+\\d+|aspect-ratio\\s+(((>|<)=?)|==)\\s+\\d+(\\.\\d+)?|orientation\\s+==\\s+(landscape|portrait|square))`,R=`${Q}(\\s+&&\\s+${Q})*`,N=new RegExp(`^\\s*${R}(\\s*,\\s*${R})*\\s*$`),k=["string","function"];export default function({target:t,queries:r={},...n}={}){(e=>{for(const[t,r]of Object.entries(e)){if(!t||!t.match(N))throw new Error(`invalid query "${t}"`);if(!k.includes(typeof r))throw new Error(`invalid behaviour "${r}"`)}})(r),(({watchedProperties:e})=>{if(e){if(!Array.isArray(e))throw new Error("watchedProperties must be an array");if(e.length<1||!e.every(e=>l.includes(e)))throw new Error("watchedProperties must be an array with at least one of "+l.join(", "))}})(n);const s=(e=>{if(!e)throw new Error("target must be provided");const t="length"in Object(e)?Array.isArray(e)?e:Array.from(e):[e];if(t.length<1)throw new Error("at least one Element must be provided as target");if(t.some(e=>!(e instanceof window.Element)))throw new Error(`target must be an Element or a list of Elements. Actual:\n[${t.map(e=>String(e)).join(", ")}]`);return t})(t),{compiledQueries:i,units:o,percentUnits:a,watchedProperties:c}=(t=>{const r=new Map,n=[],s=[],i=[];for(const[e,o]of Object.entries(t)){const t=g(e);r.set(o,t.query),n.push(...t.units),s.push(...t.percentUnits),i.push(...t.watchedProperties)}return{compiledQueries:r,units:e(n),percentUnits:e(s),watchedProperties:e(i)}})(r);if(c.length<1&&!n.watchedProperties)throw new Error("at least one query or one watched properties must be provided");const{unobserve:p,applyAdaptiveBehaviour:u}=(e=>{const{ResizeObserver:t,MutationObserver:r}=window,n=new WeakMap,s={...e,propsCache:n},{elements:i,compiledQueries:o,units:a,percentUnits:c,watchedProperties:p}=e,u=$(p)&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:n})=>s=>{for(const{contentRect:{width:i,height:o},target:a}of s){const s={...e.get(a),width:i,height:o,orientation:d(i,o),"aspect-ratio":h(i,o)};e.set(a,s),window.requestAnimationFrame(()=>M({elt:a,props:s,queries:t,units:r,percentUnits:n}))}})(s),m=u&&new t(u),w=A(p)&&i.some(y)&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:n})=>({target:s})=>{const i=s.value.trim().length,o=e.get(s);if(i!==o.characters){const i={...o,characters:E(s)};e.set(s,i),M({elt:s,props:i,queries:t,units:r,percentUnits:n})}})(s),f=(O(p)&&!i.every(y)||A(p)&&i.some(C))&&(({propsCache:e,compiledQueries:t,units:r,percentUnits:n,elements:s,watchedProperties:i})=>(o,a)=>{a.disconnect();for(const{type:a,target:c}of o)if(["childList","characterData"].includes(a)){const o=q(s,c);if(o){let s,a;const c={...e.get(o)};O(i)&&!o.isContentEditable&&(c.children=o.childElementCount,s=!0),P(i,o)&&(c.characters=E(o),a=!0),(s||a)&&(e.set(o,c),M({elt:o,props:c,queries:t,units:r,percentUnits:n}))}}for(const e of s)j(i,e)&&U(a,e,P(i,e))})(s),g=f&&new r(f);for(const e of i){const t=x(e,p);n.set(e,t),m&&m.observe(e),w&&y(e)&&e.addEventListener("input",w),g&&j(p,e)&&g&&U(g,e,P(p,e)),M({elt:e,props:t,queries:o,units:a,percentUnits:c})}return{unobserve:()=>{n.set(B,!0),(({elements:e,mutationObserver:t,resizeObserver:r,inputListener:n,behaviours:s})=>{t&&t.disconnect();const i=s.filter(e=>"string"==typeof e);for(const t of e)r&&r.unobserve(t),n&&y(t)&&t.removeEventListener("input",n),t.classList.remove(...i),l.forEach(e=>t.style.removeProperty("--ea-"+e))})({elements:i,mutationObserver:g,resizeObserver:m,inputListener:w,behaviours:[...o.keys()]}),function(e){for(const t of e)L.delete(t)}(i)},applyAdaptiveBehaviour:()=>{n.get(B)||i.forEach(e=>M({elt:e,props:n.get(e),queries:o,units:a,percentUnits:c}))}}})({elements:s,compiledQueries:i,units:o,percentUnits:a,watchedProperties:e([...c,...n.watchedProperties||[]])});return{removeAdaptiveBehaviour:p,applyAdaptiveBehaviour:u}} |
@@ -10,8 +10,22 @@ import addAdaptiveBehaviour from '../dist/element-adapter.js' | ||
queries: { | ||
classA: `width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`, | ||
classB: 'orientation == landscape', | ||
classC: 'width > 75%', | ||
classD: 'characters > 10', | ||
classE: 'children >= 2 && children < 5', | ||
classF: 'characters == 0' | ||
[`width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`]: | ||
'classA', | ||
'orientation == landscape': | ||
'classB', | ||
'orientation == portrait' (element, props) { | ||
console.debug(element, 'changed:\n', props) | ||
}, | ||
'width > 75%': | ||
'classC', | ||
'characters > 10': | ||
'classD', | ||
'children >= 2 && children < 5': | ||
'classE', | ||
'characters == 0': 'classF' | ||
} | ||
@@ -18,0 +32,0 @@ |
{ | ||
"name": "element-adapter", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"description": "Provides utilities to render dom element adatptive (responsive) to their own properties – dimensions for instance. Can be used with shadow dom", | ||
@@ -60,3 +60,6 @@ "keywords": [ | ||
"standard": "^14.3.3" | ||
}, | ||
"volta": { | ||
"node": "16.15.0" | ||
} | ||
} |
@@ -33,2 +33,6 @@ # element-adapter | ||
function logOrientationChangesToPortrait (element, props) { | ||
console.debug(element, 'changed:\n', props) | ||
} | ||
addAdaptiveBehaviour({ | ||
@@ -38,7 +42,19 @@ target: document.querySelectorAll('.component'), | ||
queries: { | ||
classA: `width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`, | ||
classB: 'orientation == landscape', | ||
classC: 'width > 75%', | ||
classD: 'characters == 0, characters > 10', | ||
classE: 'children >= 2 && children < 5' | ||
[`width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`]: | ||
'classA', | ||
'orientation == landscape': | ||
'classB', | ||
'orientation == portrait': | ||
logOrientationChangesToPortrait, | ||
'width > 75%': | ||
'classC', | ||
'characters == 0, characters > 10': | ||
'classD', | ||
'children >= 2 && children < 5': | ||
'classE' | ||
} | ||
@@ -51,3 +67,4 @@ }) | ||
```javascript | ||
// classA: `width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px` | ||
// [`width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`]: | ||
// 'classA', | ||
@@ -67,7 +84,18 @@ ADD 'classA' WHEN | ||
``` | ||
**5th query:** | ||
**3rd query:** | ||
```javascript | ||
// classE: 'children >= 2 && children < 5' | ||
// 'orientation == portrait': | ||
// (element, props) => { | ||
// console.debug(element, 'changed:\n', props) | ||
// }, | ||
EXECUTE FUNCTION logOrientationChangesToPortrait WHEN orientation === 'portrait' | ||
``` | ||
**6th query:** | ||
```javascript | ||
// 'children >= 2 && children < 5': | ||
// 'classE' | ||
ADD 'classE' WHEN | ||
@@ -114,7 +142,17 @@ children >= 2 | ||
queries: { | ||
cssClassA: <query a>, | ||
cssClassB: <query b>, | ||
'query a': 'cssClassA', | ||
'query b': 'cssClassB', | ||
'query c': function onQueryMatch (element, props) { | ||
// Do something when 'query c' matches element props | ||
}, | ||
... | ||
} | ||
``` | ||
> ### Note | ||
> Function behaviour parameters: | ||
> - `element` is a reference to the DOM element which props the query is run against | ||
> - `props` are the subset of props *({ key: value } pairs)* which the query is run against | ||
#### Formal query syntax | ||
@@ -203,3 +241,6 @@ | ||
> ### Note | ||
> Function behaviours – *as opposed to classes behaviours* – don't get re-run when the element state hasn't changed | ||
# Browsers compatibility | ||
Tested on **Safari**, **Chrome** and **Firefox** |
@@ -25,3 +25,3 @@ import { dedup } from './utils/array' | ||
unobserve: removeAdaptiveBehaviour, | ||
applyStyle: applyAdaptiveBehaviour | ||
applyAdaptiveBehaviour | ||
} = observe({ | ||
@@ -28,0 +28,0 @@ elements, |
@@ -71,3 +71,5 @@ import { | ||
const applyStyle = ({ elt, props, queries, unitsMeasurements }) => { | ||
const functionBehaviourApplyCache = new WeakMap() | ||
const applyAdaptiveBehaviour = ({ elt, props, queries, unitsMeasurements }) => { | ||
const queryProps = { | ||
@@ -79,12 +81,26 @@ ...props, | ||
for (const [cls, query] of Object.entries(queries)) { | ||
elt.classList.toggle( | ||
cls, | ||
for (const [behaviour, query] of queries.entries()) { | ||
const isQueryMatched = runQuery({ | ||
query, | ||
unitsMeasurements, | ||
props: queryProps | ||
}) | ||
runQuery({ | ||
query, | ||
unitsMeasurements, | ||
props: queryProps | ||
}) | ||
) | ||
if (typeof behaviour === 'string') { | ||
elt.classList.toggle(behaviour, isQueryMatched) | ||
} else { | ||
const previousMatch = Boolean(functionBehaviourApplyCache.get(elt)?.get(query)) | ||
if (isQueryMatched !== previousMatch) { | ||
if (isQueryMatched) { | ||
behaviour(elt, queryProps) | ||
} | ||
if (!functionBehaviourApplyCache.get(elt)) { | ||
functionBehaviourApplyCache.set(elt, new WeakMap()) | ||
} | ||
functionBehaviourApplyCache.get(elt).set(query, isQueryMatched) | ||
} | ||
} | ||
} | ||
@@ -101,3 +117,3 @@ | ||
export const adapt = ({ elt, props, queries, units, percentUnits }) => { | ||
applyStyle({ | ||
applyAdaptiveBehaviour({ | ||
elt, | ||
@@ -113,1 +129,7 @@ props, | ||
} | ||
export function clearFunctionBehaviourApplyCache (elements) { | ||
for (const elt of elements) { | ||
functionBehaviourApplyCache.delete(elt) | ||
} | ||
} |
@@ -22,3 +22,4 @@ import { WATCHABLE_PROPERTIES } from './constants' | ||
computeInitialProps, | ||
adapt | ||
adapt, | ||
clearFunctionBehaviourApplyCache | ||
} from './adapters' | ||
@@ -33,6 +34,8 @@ | ||
inputListener, | ||
behaviourCssClasses | ||
behaviours | ||
}) => { | ||
mutationObserver && mutationObserver.disconnect() | ||
const behaviourCssClasses = behaviours.filter(behaviour => typeof behaviour === 'string') | ||
for (const e of elements) { | ||
@@ -114,7 +117,9 @@ resizeObserver && resizeObserver.unobserve(e) | ||
inputListener, | ||
behaviourCssClasses: Object.keys(compiledQueries) | ||
behaviours: [...compiledQueries.keys()] | ||
}) | ||
clearFunctionBehaviourApplyCache(elements) | ||
}, | ||
applyStyle: () => { | ||
applyAdaptiveBehaviour: () => { | ||
if (!propsCache.get(INVALID)) { | ||
@@ -121,0 +126,0 @@ elements.forEach(elt => adapt({ |
@@ -40,7 +40,13 @@ import { WATCHABLE_PROPERTIES, CSS_UNITS_BUT_PX } from './constants' | ||
const allowedBehaviourTypes = ['string', 'function'] | ||
export const validateQueries = queries => { | ||
for (const query of Object.values(queries)) { | ||
for (const [query, behaviour] of Object.entries(queries)) { | ||
if (!query || !query.match(QUERY_VALIDATOR_PATTERN)) { | ||
throw new Error(`invalid query "${query}"`) | ||
} | ||
if (!allowedBehaviourTypes.includes(typeof behaviour)) { | ||
throw new Error(`invalid behaviour "${behaviour}"`) | ||
} | ||
} | ||
@@ -47,0 +53,0 @@ } |
@@ -64,3 +64,3 @@ import { length, constant } from './calculators' | ||
export const compileQueryList = queries => { | ||
const compiledQueries = {} | ||
const compiledQueries = new Map() | ||
const units = [] | ||
@@ -70,6 +70,6 @@ const percentUnits = [] | ||
for (const [cssClass, query] of Object.entries(queries)) { | ||
for (const [query, behaviour] of Object.entries(queries)) { | ||
const compilationOut = compileQuery(query) | ||
compiledQueries[cssClass] = compilationOut.query | ||
compiledQueries.set(behaviour, compilationOut.query) | ||
units.push(...compilationOut.units) | ||
@@ -76,0 +76,0 @@ percentUnits.push(...compilationOut.percentUnits) |
@@ -37,3 +37,3 @@ /* eslint-env jest */ | ||
unobserve: removeAdaptiveBehaviour, | ||
applyStyle: applyAdaptiveBehaviour | ||
applyAdaptiveBehaviour | ||
})) | ||
@@ -50,3 +50,3 @@ | ||
removeAdaptiveBehaviour: cleanUp, | ||
applyAdaptiveBehaviour: applyStyle | ||
applyAdaptiveBehaviour | ||
} = addAdaptiveBehaviour({ target, queries, anyOptions }) | ||
@@ -65,3 +65,3 @@ | ||
expect(cleanUp).toBe(removeAdaptiveBehaviour) | ||
expect(applyStyle).toBe(applyAdaptiveBehaviour) | ||
expect(applyAdaptiveBehaviour).toBe(applyAdaptiveBehaviour) | ||
}) | ||
@@ -68,0 +68,0 @@ |
/* eslint-env jest */ | ||
import { | ||
computeInitialProps, | ||
adapt | ||
} from '../../src/lib/adapters' | ||
import { runQuery } from '../../src/lib/query_processor' | ||
@@ -37,8 +32,14 @@ import { measureNonPercentUnits, measurePercentUnits } from '../../src/utils/dimensions' | ||
const queries = { | ||
classA: function widthGreaterThanConstant100 () { return true }, | ||
classB: function charactersLesserThanConstant10 () { return true }, | ||
classC: function childrenGreaterThanConstant3 () { return false } | ||
} | ||
const notifySquareOrientation = jest.fn().mockName('notifySquareOrientation') | ||
const notifyPortraitOrientation = jest.fn().mockName('notifyPortraitOrientation') | ||
const orientationEqualPortrait = jest.fn(() => true).mockName('notifyPortraitOrientation') | ||
const queries = new Map([ | ||
['classA', function widthGreaterThanConstant100 () { return true }], | ||
['classB', function charactersLesserThanConstant10 () { return true }], | ||
['classC', function childrenGreaterThanConstant3 () { return false }], | ||
[notifySquareOrientation, function orientationEqualSquare () { return true }], | ||
[notifyPortraitOrientation, orientationEqualPortrait] | ||
]) | ||
runQuery.mockImplementation(({ query }) => query()) | ||
@@ -71,8 +72,20 @@ | ||
let computeInitialProps | ||
let adapt | ||
let clearFunctionBehaviourApplyCache | ||
describe('lib/adapters', () => { | ||
beforeEach(() => jest.isolateModules(() => ({ | ||
computeInitialProps, | ||
adapt, | ||
clearFunctionBehaviourApplyCache | ||
} = require('../../src/lib/adapters')))) | ||
afterEach(() => { | ||
jest.clearAllMocks() | ||
const behaviourCssClasses = [...queries.keys()].filter(behaviour => typeof behaviour === 'string') | ||
elements.forEach(e => { | ||
e.classList.remove(...Object.keys(queries)) | ||
e.classList.remove(...behaviourCssClasses) | ||
WATCHABLE_PROPERTIES.forEach(prop => e.style.removeProperty(`--ea-${prop}`)) | ||
@@ -83,3 +96,3 @@ }) | ||
describe('#adapt', () => { | ||
it('should apply behaviour classes according to queries results', () => { | ||
it('should apply behaviours according to queries results', () => { | ||
const props = { | ||
@@ -93,6 +106,10 @@ width: 160, | ||
const compiledQueries = Object.values(queries) | ||
orientationEqualPortrait.mockReturnValueOnce(false) | ||
expect(runQuery).toHaveBeenCalledTimes(compiledQueries.length) | ||
adapt({ elt: div, props, queries, units, percentUnits }) | ||
const compiledQueries = [...queries.values()] | ||
expect(runQuery).toHaveBeenCalledTimes(compiledQueries.length * 2) | ||
compiledQueries.forEach(query => expect(runQuery).toHaveBeenCalledWith({ | ||
@@ -105,2 +122,5 @@ query, | ||
expect(Array.from(div.classList)).toEqual(['classA', 'classB']) | ||
expect(notifySquareOrientation).toHaveBeenCalledTimes(1) | ||
expect(notifySquareOrientation).toHaveBeenCalledWith(div, props) | ||
expect(notifyPortraitOrientation).toHaveBeenCalledTimes(1) | ||
}) | ||
@@ -134,5 +154,5 @@ | ||
adapt({ elt: div, props, queries, units, percentUnits }) | ||
adapt({ elt: div, props, queries, units, percentUnits }); | ||
Object.values(queries).forEach(query => expect(runQuery).toHaveBeenCalledWith({ | ||
[...queries.values()].forEach(query => expect(runQuery).toHaveBeenCalledWith({ | ||
query, | ||
@@ -150,2 +170,24 @@ unitsMeasurements, | ||
describe('#clearFunctionBehaviourApplyCache', () => { | ||
it('should actually clear cache', () => { | ||
const props = { | ||
width: 160, | ||
characters: 8, | ||
children: 1 | ||
} | ||
const span = document.createElement('span') | ||
adapt({ elt: div, props, queries, units, percentUnits }) | ||
adapt({ elt: span, props, queries, units, percentUnits }) | ||
clearFunctionBehaviourApplyCache([div]) | ||
adapt({ elt: div, props, queries, units, percentUnits }) | ||
adapt({ elt: span, props, queries, units, percentUnits }) | ||
expect(notifySquareOrientation).toHaveBeenCalledTimes(3) | ||
}) | ||
}) | ||
describe('#computeInitialProps', () => { | ||
@@ -152,0 +194,0 @@ it.each([ |
@@ -14,3 +14,4 @@ /* eslint-env jest */ | ||
computeInitialProps, | ||
adapt | ||
adapt, | ||
clearFunctionBehaviourApplyCache | ||
} from '../../src/lib/adapters' | ||
@@ -30,3 +31,4 @@ | ||
computeInitialProps: jest.fn().mockName('computeInitialProps'), | ||
adapt: jest.fn().mockName('adapt') | ||
adapt: jest.fn().mockName('adapt'), | ||
clearFunctionBehaviourApplyCache: jest.fn().mockName('clearFunctionBehaviourApplyCache') | ||
})) | ||
@@ -77,3 +79,3 @@ | ||
elements, | ||
compiledQueries: { classA: '{ width: greaterThan(constant(300)) }' }, | ||
compiledQueries: new Map([[['classA'], ['{ width: greaterThan(constant(300)) }']]]), | ||
units: ['em'], | ||
@@ -137,3 +139,3 @@ percentUnits: ['w%'] | ||
elements, | ||
compiledQueries: { classA: '{ characters: greaterThan(constant(300)) }' }, | ||
compiledQueries: new Map([[['classA'], ['{ characters: greaterThan(constant(300)) }']]]), | ||
units: [], | ||
@@ -235,3 +237,3 @@ percentUnits: [] | ||
elements, | ||
compiledQueries: { classA: '{ children: greaterThan(constant(3)) }' }, | ||
compiledQueries: new Map([[['classA'], ['{ children: greaterThan(constant(3)) }']]]), | ||
units: [], | ||
@@ -412,3 +414,3 @@ percentUnits: [] | ||
elements, | ||
compiledQueries: { classA: '{ width: greaterThan(constant(300)) }' }, | ||
compiledQueries: new Map([[['classA'], ['{ width: greaterThan(constant(300)) }']]]), | ||
units: ['em'], | ||
@@ -440,3 +442,3 @@ percentUnits: ['w%'], | ||
elements, | ||
compiledQueries: { classA: '{ width: greaterThan(constant(300)) }' }, | ||
compiledQueries: new Map([[['classA'], ['{ width: greaterThan(constant(300)) }']]]), | ||
units: ['em'], | ||
@@ -462,3 +464,3 @@ percentUnits: ['w%'], | ||
const [div, span] = elements | ||
const compiledQueries = { classA: '{ width: greaterThan(constant(300)) }' } | ||
const compiledQueries = new Map([[['classA'], ['{ width: greaterThan(constant(300)) }']]]) | ||
const units = ['em'] | ||
@@ -512,7 +514,8 @@ const percentUnits = ['w%'] | ||
compiledQueries: { | ||
classA: '{ width: greaterThan(constant(300)) }', | ||
classB: '{ characters: greaterThan(constant(300)) }', | ||
classC: '{ children: greaterThan(constant(3)) }' | ||
}, | ||
compiledQueries: new Map([ | ||
['classA', '{ width: greaterThan(constant(300)) }'], | ||
['classB', '{ characters: greaterThan(constant(300)) }'], | ||
['classC', '{ children: greaterThan(constant(3)) }'], | ||
[function notifySquareScreen () {}, 'equal(constant(square))'] | ||
]), | ||
@@ -543,2 +546,3 @@ units: ['em'], | ||
expect(textarea.removeEventListener).toHaveBeenCalledWith('input', inputListener) | ||
expect(clearFunctionBehaviourApplyCache).toHaveBeenCalledWith(elements) | ||
}) | ||
@@ -564,5 +568,7 @@ | ||
const behaviourCssClasses = [...params.compiledQueries.keys()].filter(key => typeof key === 'string') | ||
elements.forEach(e => { | ||
expect(e.classList.remove) | ||
.toHaveBeenCalledWith(...Object.keys(params.compiledQueries)) | ||
.toHaveBeenCalledWith(...behaviourCssClasses) | ||
@@ -585,7 +591,7 @@ expect(e.style.removeProperty).toHaveBeenCalledTimes(WATCHABLE_PROPERTIES.length) | ||
const compiledQueries = { | ||
classA: '{ width: greaterThan(constant(300)) }', | ||
classB: '{ characters: greaterThan(constant(300)) }', | ||
classC: '{ children: greaterThan(constant(3)) }' | ||
} | ||
const compiledQueries = new Map([ | ||
['classA', '{ width: greaterThan(constant(300)) }'], | ||
['classB', '{ characters: greaterThan(constant(300)) }'], | ||
['classC', '{ children: greaterThan(constant(3)) }'] | ||
]) | ||
@@ -618,7 +624,7 @@ const units = ['em'] | ||
const { applyStyle } = observeAll(params) | ||
const { applyAdaptiveBehaviour } = observeAll(params) | ||
adapt.mockClear() | ||
applyStyle() | ||
applyAdaptiveBehaviour() | ||
@@ -638,3 +644,3 @@ expect(adapt).toHaveBeenCalledTimes(elements.length) | ||
unobserve: cleanUp, | ||
applyStyle | ||
applyAdaptiveBehaviour | ||
} = observeAll(params) | ||
@@ -645,3 +651,3 @@ | ||
cleanUp() | ||
applyStyle() | ||
applyAdaptiveBehaviour() | ||
@@ -648,0 +654,0 @@ expect(adapt).not.toHaveBeenCalled() |
@@ -105,11 +105,27 @@ /* eslint-env jest */ | ||
expect(() => validateQueries({ | ||
classA: `width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`, | ||
classB: ' orientation == landscape ', | ||
classC: ' width > 75%', | ||
classD: 'characters > 10', | ||
classE: 'children >= 2 && children < 5 ', | ||
classF: 'characters == 0', | ||
classG: 'width >= 75em,height >= 80%', | ||
classH: 'orientation == portrait', | ||
classI: 'orientation == square' | ||
[`width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`]: | ||
'classA', | ||
' orientation == landscape ': | ||
'classB', | ||
' width > 75%': | ||
'classC', | ||
'characters > 10': | ||
'classD', | ||
'children >= 2 && children < 5 ': | ||
'classE', | ||
'characters == 0': | ||
'classF', | ||
'width >= 75em,height >= 80%': | ||
'classG', | ||
'orientation == portrait': | ||
'classH', | ||
'orientation == square' () {} | ||
})).not.toThrow() | ||
@@ -147,7 +163,16 @@ }) | ||
expect(() => validateQueries({ | ||
classA: query, | ||
classB: 'width > 75%' | ||
[query]: 'classA', | ||
'width > 75%': 'classB' | ||
})).toThrow(`invalid query "${query}"`) | ||
}) | ||
it('should reject query having invalid behaviour', () => { | ||
const wrongBehaviour = ['wrong behaviour'] | ||
expect(() => validateQueries({ | ||
'orientation == square': wrongBehaviour, | ||
'width > 75%': 'classB' | ||
})).toThrow(`invalid behaviour "${wrongBehaviour}"`) | ||
}) | ||
}) | ||
}) |
@@ -93,13 +93,29 @@ /* eslint-env jest */ | ||
it('should compile query list', () => { | ||
function notifySquareScreen () {} | ||
const compiled = compileQueryList({ | ||
classA: `width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`, | ||
classB: 'orientation == landscape && height < 10.325em', | ||
classC: 'width > 75%', | ||
classD: 'characters > 10 && height <= 13.56%', | ||
classE: 'children >= 2 && children < 5', | ||
classF: 'characters == 0' | ||
[`width >= 6.25em && height < 50%, aspect-ratio <= ${16 / 9}, width >= 680px`]: | ||
'classA', | ||
'orientation == landscape && height < 10.325em': | ||
'classB', | ||
'width > 75%': | ||
'classC', | ||
'characters > 10 && height <= 13.56%': | ||
'classD', | ||
'children >= 2 && children < 5': | ||
'classE', | ||
'characters == 0': | ||
'classF', | ||
'orientation == square': | ||
notifySquareScreen | ||
}) | ||
const compiledQueries = { | ||
classA: [ | ||
const compiledQueries = new Map([ | ||
['classA', [ | ||
[ | ||
@@ -112,23 +128,24 @@ { width: 'greaterThanOrEqual(length(6.25, em))' }, | ||
[{ width: 'greaterThanOrEqual(constant(680))' }] | ||
], | ||
]], | ||
classB: [[ | ||
['classB', [[ | ||
{ orientation: 'equal(constant(landscape))' }, | ||
{ height: 'lesserThan(length(10.325, em))' } | ||
]], | ||
]]], | ||
classC: [[{ width: 'greaterThan(length(75, w%))' }]], | ||
['classC', [[{ width: 'greaterThan(length(75, w%))' }]]], | ||
classD: [[ | ||
['classD', [[ | ||
{ characters: 'greaterThan(constant(10))' }, | ||
{ height: 'lesserThanOrEqual(length(13.56, h%))' } | ||
]], | ||
]]], | ||
classE: [[ | ||
['classE', [[ | ||
{ children: 'greaterThanOrEqual(constant(2))' }, | ||
{ children: 'lesserThan(constant(5))' } | ||
]], | ||
]]], | ||
classF: [[{ characters: 'equal(constant(0))' }]] | ||
} | ||
['classF', [[{ characters: 'equal(constant(0))' }]]], | ||
[notifySquareScreen, [[{ orientation: 'equal(constant(square))' }]]] | ||
]) | ||
@@ -135,0 +152,0 @@ const units = ['em'] |
103832
2344
241