[CSS + Javascript] Firefoxで:has()に自動対応できるライブラリを作ってみた。

2023年11月5日

CSS Javascript

eyecatch CSSコーダーが最も使いたいけど使えない機能に:has()があります。 この機能、Firefoxブラウザで対応していないために、公開Webサイトなどで使えなくて枕を涙で濡らしたエンジニアも多いと思います。 もう、今日から枕の濡れ具合が少なくなりますよ。 cssの:has()記述を自動で読み取り、firefoxでも:has()機能が使えるようになるライブラリを作ってみました。

コード

sample.html

<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"/> <title>has check</title> <link rel="stylesheet" href="style.css"/> <script type='module' src='main.js'></script> </head> <body> <div class="has-test"> <input type='checkbox' name='test'/> <ul></ul> </div> <div class='no-support'> has()使えないよ </div> </body> </html>

sample.css

.has-test:has(ul:empty){ border:2px dashed #aaa; } .has-test:has(input[type='checkbox']:checked){ border:2px dashed red; } @supports selector(:has(+ *)) { .no-support { display: none; } }

main.js

import { Has } from './has.js' class Main{ constructor(){ new Has() } } switch(document.readyState){ case 'complete': case 'interactive': new Main() break default: window.addEventListener('DOMContentLoaded',(()=>new Main())) break }

has.js

import { Css } from './css.js' export class Has{ link_files = [] link_value = "" constructor(){ if(this.is_has()){return} this.get_style_tag() this.get_link_tag().then(() => { const selector_datas = this.get_has(this.link_value) this.css_set_arr(selector_datas) }) } is_has(){ try{ return document.querySelector(`html:has(body)`) ? true : false } catch(err){ return false } } get_has(string){ if(!string.trim()){return} string = string.trim().replace(/(\/\*.*?\*\/)/g , '') // コメントを削除 string = string.replace(/(\r|\n)/g , '') // 改行を削除 const reg = /(.+?)\{(.+?)\}{1,2}/ig const arr = [] let res while ((res = reg.exec(string)) !== null){ // @クエリ処理 if(res[1].trim().match(/^@/)){ const media = res[0].trim().match(/@media\s+?(.+?)\{(.*?)\}\}/i) if(media){ const queries = this.get_has(`${media[2]}}`) if(queries && queries.length){ for(const query of queries){ query.media_name = '@media' query.media_selector = media[1] arr.push(query) } } } } else{ // :has()分解処理 const res2 = res[1].match(/(.+?):has+\((.+?)\)(.*?)/i) if(!res2){continue} arr.push({ selector_before : res2[1].trim(), selector_after : res2[3].trim(), condition : res2[2].trim(), property : res[2].trim(), res1 : res, res2 : res2, }) } } return arr } get_style_tag(){ const styles = document.querySelectorAll(`style`) for(const style of styles){ this.link_value += `${style.textContent}` } } get_link_tag(){ let flg = 0 return new Promise(resolve => { const links = document.querySelectorAll(`link[rel='stylesheet']`) for(const link of links){ if(!link.href){continue} if(!this.is_same_domain(link.href)){continue} this.load_css_file(link.href, resolve) flg++ } if(!flg){ resolve(true) } }) } is_same_domain(url){ const sp = url.split('/') return sp[2] === location.host ? true : false } load_css_file(file, resolve){ this.link_files.push(file) const xhr = new XMLHttpRequest() xhr.open('get' , file , true) xhr.setRequestHeader('Content-Type', 'text/css'); xhr.onload = (e => { const url = e.target.responseURL const index = this.link_files.findIndex(e => e === url) if(index >= 0){ this.link_files.splice(index,1) } this.link_value += `${e.target.response}` // @importを読み込む const imports = this.get_imports(url, e.target.response) if(imports){ for(const path of imports){ this.load_css_file(path, resolve) } } if(this.link_files && !this.link_files.length){ resolve(true) } }) xhr.send() } get_imports(base_url, string){ if(!string){return} const reg = RegExp("@import\\s+(url\\()*[\'\"\\s]+(.+?)[\'\"\\s]+(\\))*(.*?);(\n)?" , 'ig') const arr = [] let res while ((res = reg.exec(string)) !== null){ const url = res[2] if(!url){continue} arr.push(this.conv_url(base_url, url)) this.link_value = this.link_value.replace(res[0] , '') } return arr } conv_url(base_url , target_url){ if(target_url.match(/^http(s):\/\//)){ return target_url } const sp = base_url.split('/') sp[sp.length-1] = target_url return sp.join('/') } get_style_value(){ const styles = document.querySelectorAll('style') console.log(styles) let style_value = "" if(styles){ for(const style of styles){ style_value += `${style.textContent}\n` } } return style_value || null } // :hasが機能しないブラウザのために、新たにselectorを登録する。 css_set_arr(selector_datas){ if(!selector_datas || !selector_datas.length){return} this.monitoring_setting = [] this.observer = new MutationObserver((mutations,e) => { }) window.addEventListener('click' , this.monitoring.bind(this)) window.addEventListener('resize' , this.monitoring.bind(this)) // for(let i=0; i<selector_datas.length; i++){ for(let i=selector_datas.length-1; i>=0; i--){ this.css_set(selector_datas[i] , i) } this.monitoring() } css_set(selector_data , num){ const attribute_key = `data-has-${num}` const selector = `${selector_data.selector_before}[${attribute_key}='true'] ${selector_data.selector_after}` const reg = RegExp("^(.+?):(.+?)\;?$" , 'i') let res = null const stylesheet = Css.get_last_stylesheet() || Css.create_stylesheet() if(selector_data.media_name){ stylesheet.insertRule(`@media ${selector_data.media_selector}{${selector}{${selector_data.property}}}` , 0) } else{ stylesheet.insertRule(`${selector}{${selector_data.property}}` , 0) } // ターゲット const selector_parent = `${selector_data.selector_before} ${selector_data.selector_after}` const elm = { parent : document.querySelector(selector_parent), target : null, } if(elm.parent){ elm.target = elm.parent.querySelector(`${selector_data.condition}`) } this.observe_init(elm.parent) this.monitoring_setting.push({ parent : elm.parent, target_selector : selector_data.condition, attribute_key : attribute_key, }) } monitoring(){ for(const data of this.monitoring_setting){ this.monitoring_check(data.parent , data.target_selector , data.attribute_key) } } monitoring_check(parent, selector_target, attribute_key){ if(!parent){return} const target = parent.querySelector(selector_target) parent.setAttribute(attribute_key , target ? true : false) } observe_init(parent){ if(!parent){return} // 監視を開始 this.observer.observe(parent, { // attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する // characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する attributes: true, // 属性変化の監視 characterData: true, // テキストノードの変化を監視 childList: true, // 子ノードの変化を監視 subtree: true, // 子孫ノードも監視対象に含める }) } }

css.js

export class Css{ static set_css(selector , property , value){ const rule = Css.get_last_rule(selector) if(rule){ rule.style.setProperty(property , value , '') } else{ Css.create_rule(selector , property , value) } } static get_last_rule(selector){ const rules = Css.get_rules(selector) return rules && rules.length ? rules[rules.length-1] : null } static create_rule(selector , property , value){ const stylesheet = Css.get_last_stylesheet() || Css.create_stylesheet() stylesheet.insertRule(`${selector}{${property}:${value}}` , 0) } static get_last_stylesheet(){ const stylesheets = Css.get_stylesheets() return stylesheets && stylesheets.length ? stylesheets[stylesheets.length-1] : null } static create_stylesheet(){ const style = document.createElement('style') document.querySelector('head').appendChild(style) return Css.get_last_stylesheet() } static get_ss(selector , property){ const styleSheets = Array.from(document.styleSheets).filter((styleSheet) => !styleSheet.href || styleSheet.href.startsWith(window.location.origin)) let value = null for(const ss of styleSheets){ if(!ss.cssRules){continue} for(const cssRule of ss.cssRules){ if(!cssRule.styleSheet || !cssRule.styleSheet.cssRules){continue} for(const rule of cssRule.styleSheet.cssRules){ if(rule.selectorText !== selector){continue} value = rule.style[property] } } } return value; } static get_css(selector , property){ const rules = Css.get_rules(selector) let value = null for(const rule of rules){ value = rule.style.getPropertyValue(property) || value } return value } static get_stylesheets(){ return Array.from(document.styleSheets).filter((styleSheet) => !styleSheet.href || styleSheet.href.startsWith(window.location.origin)) } static get_rules(selector){ const styleSheets = Css.get_stylesheets() let arr = [] for(const ss of styleSheets){ if(!ss.cssRules){continue} const res = this.get_rule(ss.cssRules , selector) if(!res || !res.length){continue} arr = arr.concat(res) } return arr; } static get_rule(rules , selector){ if(!rules){return} let arr = [] for(const rule of rules){ if(rule.selectorText === selector){ arr.push(rule) } if(rule.styleSheet && rule.styleSheet.cssRules){ const res = Css.get_rule(rule.styleSheet.cssRules , selector) if(!res || !res.length){continue} arr = arr.concat(res) } } return arr; } static get_animation_range(selector){ const value = Css.get_css(selector, '--animation-range') } static get_rule_properties(property){ const styleSheets = Css.get_stylesheets() const arr = [] for(const ss of styleSheets){ // tags,files if(!ss.cssRules){continue} // selectors for(const cssRule of ss.cssRules){ if(!cssRule.selectorText){continue} // styles for(const style of cssRule.style){ if(style === property){ arr.push(cssRule) } } } } return arr; } static get_keyframes(animation_name){ const keyframes = Css.get_keyframes2(animation_name) const arr = [] for(const keyframe of keyframes.cssRules){ keyframe.rate = Number(keyframe.keyText.replace('%','')) arr.push(keyframe) } return arr } static get_keyframes2(animation_name){ const styleSheets = Css.get_stylesheets() const arr = [] for(const ss of styleSheets){ // tags,files if(!ss.cssRules){continue} // selectors for(const cssRule of ss.cssRules){ if(cssRule.name !== animation_name){continue} return cssRule } } } }

デモ

    has()が使えないブラウザです。

    あとがき

    枠の中のCheckboxをクリックすると、枠の色が変わります。 :has()で、中のチェックボックスがクリックされているかどうかで判定しているのが確認できますよ。 ちゃんとFirefoxでも色が切り替わる事を確認してもらうと、もうあなたは、このJavascriptを自分の作るWebページに毎回セットしたくなるでしょう。 技術的な解説なんて野暮な事はあまりしないでおこうと思ったんですが、実はこのツールまだ完璧ではなく、クリックと、画面サイズ変更のイベント時にしか反応しません。 MutationObserver()処理をやってみたんですが、無限ループになったり、処理が重くなったりして、かなりメモリ消費が大きいことがわかったので、一番使い勝手が良いコスパの良さげな段階でとどめています。 今後バージョンアップしていくのが早いか、Firefoxが:has()対応をするのが早いか、おもしろがって見守ってくだされ!

    人気の投稿

    このブログを検索

    ごあいさつ

    このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

    ブログ アーカイブ