🪄

メディアクエリ全部捕捉す

2024/12/18に公開

というわけで 年末なので メディアクエリ全部捕捉します。

対応範囲

で、出来上がったものがこれとなります。

できあがったもの

メディア種別 や メディア特性 は 値の取り方で概ね値の取り方に二つの方法があり、それにより表示方法を変えています。

値が決まっているもの

メディア種別 や color-gamutany-hover など、 値が決まっているものです。
これは全てメディアクエリで検知して表示すればこと足ります。

<div class="select">
  any-hover:
  <span id="any-hover"><span class="none">none</span><span class="hover">hover</span></span>
</div>
:where(#any-hover) {
  .none {
    display: none;
    @media (any-hover: none) {
      display: initial;
    }
  }
  .hover {
    display: none;
    @media (any-hover: hover) {
      display: initial;
    }
  }
  @media (any-hover: none) or (any-hover: hover) {
    --not-work: none;
  }
}
.select {
  --not-work: "not work";
  [id] {
    &::before {
      color: var(--not-work-color);
      background: var(--not-work-bg-color);
      content: var(--not-work);
      display: inline;
      padding-inline: 0.5em;
    }
  }
}

--not-work は 未対応対策です hit したら 表示を解除します

勿論 grid や monochrome 等の 0 か 1 しかとらないものも含みます。

値がある程度の範囲を持つもの

width や height 及び color 等がそれです。
この場合は元ネタとなる数値を js で取得した上で メディアクエリを発行して変更されたらまた取得して設定みたいなことをする必要がある為、次の様になります。

<div class="value">resolution: <span id="resolution"></span></div>
.value {
  --not-work: none;
  [id] {
    &:empty {
      --not-work: "not work";
    }
    &::before {
      color: var(--not-work-color);
      background: var(--not-work-bg-color);
      content: var(--not-work);
      display: inline;
      padding-inline: 0.5em;
    }
  }
}

// #region resolution
checkStart({
  name: "resolution",
  get: () => `${globalThis.devicePixelRatio}dppx`,
  set: (value) => {
    const resolution = document.getElementById("resolution");
    resolution.textContent = value;
  },
  toQuery: (value) => `(resolution: ${value})`,
});
// #endregion

/**
 * メディアクエリを取得してその更新を待機する
 * @param {{
 *   name: string;
 *   get: () => string;
 *   set: (t:string) => void;
 *   toQuery: (t:string) => string;
 * }}
 */
async function checkStart({ name, get, set, toQuery }) {
  if (!get) throw new Error("required options.get parameter");
  if (!set) throw new Error("required options.set parameter");
  if (!toQuery) throw new Error("required options.set parameter");
  /** @type {string} */
  let value;
  const change = () => {
    const once = true;
    const { promise, resolve } = Promise.withResolvers();
    const query = toQuery(value);
    const media = globalThis.matchMedia(query);
    media.addEventListener("change", resolve, { once });
    return promise.then(() => true);
  };
  try {
    do {
      value = get();
      set(value);
    } while (await change());
  } catch (e) {
    console.error("%o %o", name, e);
  }
}

メディアクエリ と 実際の値を司るプロパティ

css メディアクエリ名 js プロパティ
resolution globalThis.devicePixelRatio (dppx)
width globalThis.innerWidth
height globalThis.innerHeight
color globalThis.screen.colorDepth
color-index なし

そう……問題点は…… color-index メディアクエリに対応した js 側のプロパティが無いということです。

ただ、 メディアクエリで 現在の値がそうかどうかは確認できます。
現在の値から超えているか 、現在の値よりも少ないかはわかります。

というわけで color-index は 二分探索法で 探すことにしました。

color-index は二分探索法で実際の値を測定

イテレータは next() メソッドに引数を渡せるのをご存じでしょうか?
それを使えば 範囲を対話で絞り込むだけのイテレータを作成することができます。

二分探索法のやり方については他の文書を読めばわかると重いので省略します。

// #region color-index
{
  const maxColorIndex = 100000000000;
  checkStart({
    name: "color-index",
    get,
    set,
    toQuery,
  });
  function get() {
    const min = 0;
    const max = maxColorIndex;
    if (match(min)) return `${min}`;
    if (match(max)) return `${max}`;
    const ite = find(min, max);
    /** @type {number|undefined} */
    let comp;
    try {
      do {
        const { value: index, done } = ite.next(comp);
        if (done) break;
        comp = compare(index);
        if (comp === 0) return `${index}`;
      } while (comp !== 0);
      throw new Error(`not found color index ${min}${max}`);
    } finally {
      if (ite.return) ite.return(undefined);
    }
    /**
     * @param {number} index
     * @returns {1|0|-1}
     */
    function compare(index) {
      if (match(index)) return 0;
      if (globalthis.matchMedia(`(color-index < ${index})`).matches) return -1;
      return 1;
    }
    /**
     * @param {param} index
     * @returns {boolean}
     */
    function match(index) {
      return globalThis.matchMedia(toQuery(index)).matches;
    }
  }
  function toQuery(value) {
    return `(color-index: ${value})`;
  }
  function set(value) {
    const colorIndex = document.getElementById("color-index");
    colorIndex.textContent = value;
  }
  /**
   * 二分探索法で値を探すイテレータ
   * @param {number} min
   * @param {number} max
   * @returns {Iterator<number, undefined, number | undefined>}
   */
  function* find(min, max) {
    let half;
    while (min < max) {
      half = Math.ceil((max + min) / 2);
      const result = yield half;
      if (result === 0) return;
      if (result > 0) {
        if (min === half) return;
        min = half;
        continue;
      }
      if (result < 0) {
        if (max === half) return;
        max = half;
        continue;
      }
      throw new Error("unkown result value");
    }
    return;
  }
}
// #endregion

以上。

Discussion