컨테이너 쿼리: 또 다른 Polyfill

저는 컨테이너 쿼리를 좋아합니다. 몇 년 동안 기다려 왔습니다.

그러나 아쉽게도 모든 브라우저가 이를 구현하기 전까지는 폴리필에 의존해야 작동합니다.

다른 polyfill은 잘 작동하지만 postCSS나 특정 구문이 필요하지 않고 현재 작업 중인 프로젝트에 더 적합한 것이 필요했습니다.

그래서 저는 저만의 polyfill을 만들기로 결정했고 결국 502바이트 gzip으로 압축된 스크립트로 끝났습니다.

if(!("CSSContainerRule"in window)){const e=(e,s)=>e.reduce((e,t,c)=>s(t)?c:e,-1),s=new ResizeObserver(s=>{for(let t of s){const s=t.target,c=s.__cq,n=e(c.bp,e=>e<=t.contentRect.width);n!==s.index?(s.style.cssText=c.css.filter((e,s)=>s<=n).join(""),c.index=n):-1===n&&s.removeAttribute("style")}});[...document.styleSheets].map(e=>{fetch(e.href).then(e=>e.text()).then(e=>{let t,c=new Set;const n=/@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm;for(;t=n.exec(e);)[...document.querySelectorAll(t.groups.selector)].forEach(e=>{e.__cq=e.__cq||{bp:[],css:[],index:-1};const s=t.groups.breakpoint-0,n=t.groups.css,o=e.__cq.bp.findIndex(e=>e===s);o<0?(e.__cq.bp.push(s),e.__cq.css.push(n)):e.__cq.css[o]=e.__cq.css[o].concat(n),c.add(e)});for(let e of c)s.observe(e)})})}


알겠습니다. 완전히 읽을 수 없으므로 스크립트를 보기 전에 HTML 및 CSS로 스테이지를 설정해 보겠습니다.


무대 설정



HTML에서 다음을 새 문서에 추가하십시오.

<main>
  <div class="cqw"><div class="cq cq1"></div></div>
  <div class="cqw"><div class="cq cq2"></div></div>
  <div class="cqw"><div class="cq cq3"></div></div>
  <div class="cqw"><div class="cq cq4"></div></div>
</main>

<head> -섹션에서 스타일시트에 대한 링크를 추가합니다.

<link href="cq.css" rel="stylesheet">


이제 cq.css -시트를 생성합니다.

body {
  margin: unset;
}
main { 
  display: flex;
  flex-wrap: wrap;
}
.cq {
  aspect-ratio: var(--asr, 1);
  background-color: var(--bgc, silver);
  width: var(--w, 25vw);
}
.cqw {
  contain: layout inline-size;
}
.cq1 { --bgc: tomato }
.cq2 { --bgc: orange }
.cq3 { --bgc: skyblue }
.cq4 { --bgc: tan; }

@container (min-width: 300px) { .cq { --asr: 2/1; } }
@container (min-width: 300px) { .cq1 { --bgc: indianred; } }
@container (min-width: 300px) { .cq2 { --bgc: darkorange; } }
@container (min-width: 300px) { .cq3 { --bgc: steelblue; } }
@container (min-width: 300px) { .cq4 { --bgc: lavender; } }
@media (min-width: 600px) { .cq { --w: 50vw; } }
@media (min-width: 900px) { .cq { --w: 25vw } }`


이제 페이지가 다음과 같이 표시됩니다.




스크립트



먼저 스크립트가 필요한지 여부를 확인해야 합니다.

if (!('CSSContainerRule' in window))


다음으로 페이지의 스타일시트를 반복하고 (다시 말하지만 캐시되어 있음) fetch() 로 결과를 변환하고 .text() 규칙을 문자열로 반환합니다.

[...document.styleSheets].map(sheet => {
  fetch(sheet.href)
    .then(css => css.text())
    .then(rules => { ... }

regEx를 사용하여 해당 문자열에서 필요한 것을 찾습니다.

const re = /@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm


NOTE: A good place to play around with RegEx, is regex101.com



이 식은 breakpoint , selectorcss 라는 제목의 일치 그룹을 반환합니다.

이제 일치 항목을 반복해 보겠습니다. 일치할 때마다 querySelectorAll를 사용하여 DOM와 일치하는 selector의 요소를 찾습니다.

각 요소에서 중단점 배열, 각 중단점에 대한 css 및 인덱스를 포함하는 객체__cq를 생성합니다. 반복할 때마다 객체가 이미 존재하는지 확인합니다.

let match;
let observe = new Set();
while (match = re.exec(rules)) {
  [...document.querySelectorAll(match.groups.selector)].forEach(elm => {
    elm.__cq = elm.__cq || { bp: [], css: [], index: -1 }
    const bp = match.groups.breakpoint-0;
    const css = match.groups.css;
    const index = elm.__cq.bp.findIndex(item => item === bp);
    if (index < 0) {
      elm.__cq.bp.push(bp);
      elm.__cq.css.push(css);
    }
    else {
      elm.__cq.css[index] = elm.__cq.css[index].concat(css);
    }
    observe.add(elm);
  })
}

Set()라는 observe는 (고유한) 요소 집합을 유지하는 데 사용되며 다음을 관찰해야 합니다.

for (let item of observe) RO.observe(item);

ROResizeObserver이다.

const RO = new ResizeObserver(entries => {
  for (let entry of entries) {
    const elm = entry.target;
    const cq = elm.__cq;
    const lastIndex = findLastIndex(cq.bp, item => item <= entry.contentRect.width);
    if (lastIndex !== elm.index) {
      elm.style.cssText = cq.css.filter((item, index) => index <= lastIndex).join('');
      cq.index = lastIndex;
    }
    else if (lastIndex === -1) elm.removeAttribute('style');
  }
});


findLastIndex라는 작은 방법을 사용하고 있습니다.

const findLastIndex = (items, callback) => items.reduce((acc, curr, index) => callback(curr) ? index : acc, -1);


... 그리고 그것을 사용하여 현재 필요한 breakpoint (bp)를 결정한 다음 요소의 style> 속성을 css 개체의 __cq로 설정합니다.

다음은 전체 스크립트입니다. 이 스크립트 또는 위의 축소된 버전을 데모 페이지의 <script> -태그에 추가하세요.

if (!('CSSContainerRule' in window)) {
  const findLastIndex = (items, callback) => items.reduce((acc, curr, index) => callback(curr) ? index : acc, -1);
  const RO = new ResizeObserver(entries => {
    for (let entry of entries) {
      const elm = entry.target;
      const cq = elm.__cq;
      const lastIndex = findLastIndex(cq.bp, item => item <= entry.contentRect.width);
      if (lastIndex !== elm.index) {
        elm.style.cssText = cq.css.filter((item, index) => index <= lastIndex).join('');
        cq.index = lastIndex;
      }
      else if (lastIndex === -1) elm.removeAttribute('style');
    }
  });

  [...document.styleSheets].map(sheet => {
    fetch(sheet.href)
      .then(css => css.text())
      .then(rules => {
        let match;
        let observe = new Set();
        const re = /@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm
        while (match = re.exec(rules)) {
          [...document.querySelectorAll(match.groups.selector)].forEach(elm => {
            elm.__cq = elm.__cq || { bp: [], css: [], index: -1 }
            const bp = match.groups.breakpoint-0;
            const css = match.groups.css;
            const index = elm.__cq.bp.findIndex(item => item === bp);
            if (index < 0) {
              elm.__cq.bp.push(bp);
              elm.__cq.css.push(css);
            }
            else {
              elm.__cq.css[index] = elm.__cq.css[index].concat(css);
            }
            observe.add(elm);
          })
        }
        for (let item of observe) RO.observe(item);
      }
    )
  })
}



이제 페이지 크기를 조정하면 상자가 aspect-ratiobackground-color로 변경됩니다.


900px에서 레이아웃은 초기 값으로 돌아간 다음 1200px에서 업데이트된 값으로 돌아갑니다.

NOTE: It's mobile first, thus only min-width will work, and only with pixels as it's value, since the contentRect.width of the ResizeObserver returns a value in pixels.



저는 최적화하거나 변경/추가할 수 있는 많은 것들이 있다고 확신합니다(예를 들어 오류 처리!). 결국 이것은 제가 3-4시간 안에 요리한 것입니다!

아래의 Codepen은 Codepen에서 열거나 편집하고 브라우저 크기를 조정하는 경우 가장 잘 작동합니다.



읽어 주셔서 감사합니다!


Pexels에서 제공하는 Pixabay의 표지 이미지

좋은 웹페이지 즐겨찾기