3 |
4 | # CSS Has Pseudo [][CSS Has Pseudo]
5 |
6 | [![NPM Version][npm-img]][npm-url]
7 | [![Build Status][cli-img]][cli-url]
8 | [![Support Chat][git-img]][git-url]
9 |
10 | ----------------
11 |
12 | [CSS Has Pseudo] lets you style elements relative to other elements in CSS,
13 | following the [Selectors Level 4] specification.
14 |
15 | [](https://caniuse.com/#feat=css-has)
16 |
17 | ```css
18 | a:has(> img) {
19 | /* style links that contain an image */
20 | }
21 |
22 | h1:has(+ p) {
23 | /* style level 1 headings that are followed by a paragraph */
24 | }
25 |
26 | section:not(:has(h1, h2, h3, h4, h5, h6)) {
27 | /* style sections that don’t contain any heading elements */
28 | }
29 |
30 | body:has(:focus) {
31 | /* style the body if it contains a focused element */
32 | }
33 | ```
34 |
35 | ## Usage
36 |
37 | From the command line, transform CSS files that use `:has` selectors:
38 |
39 | ```bash
40 | npx css-has-pseudo SOURCE.css TRANSFORMED.css
41 | ```
42 |
43 | Next, use your transformed CSS with this script:
44 |
45 | ```html
46 |
47 |
48 |
49 | ```
50 |
51 | That’s it. The script is 765 bytes and works in all browsers, including
52 | Internet Explorer 11. With a [Mutation Observer polyfill], the script will work
53 | down to Internet Explorer 9.
54 |
55 | ## How it works
56 |
57 | The [PostCSS plugin](README-POSTCSS.md) clones rules containing `:has`,
58 | replacing them with an alternative `[:has]` selector.
59 |
60 | ```css
61 | body:has(:focus) {
62 | background-color: yellow;
63 | }
64 |
65 | section:not(:has(h1, h2, h3, h4, h5, h6)) {
66 | background-color: gray;
67 | }
68 |
69 | /* becomes */
70 |
71 | body[\:has\(\:focus\)] {
72 | background-color: yellow;
73 | }
74 |
75 | body:has(:focus) {
76 | background-color: yellow;
77 | }
78 |
79 | section[\:not-has\(h1\,\%20h2\,\%20h3\,\%20h4\,\%20h5\,\%20h6\)] {
80 | background-color: gray;
81 | }
82 |
83 | section:not(:has(h1, h2, h3, h4, h5, h6)) {
84 | background-color: gray;
85 | }
86 | ```
87 |
88 | Next, the [JavaScript library](README-BROWSER.md) adds a `[:has]` attribute to
89 | elements otherwise matching `:has` natively.
90 |
91 | ```html
92 |
93 |
94 |
95 | ```
96 |
97 | [cli-img]: https://img.shields.io/travis/csstools/css-has-pseudo/master.svg
98 | [cli-url]: https://travis-ci.org/csstools/css-has-pseudo
99 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg
100 | [git-url]: https://gitter.im/postcss/postcss
101 | [npm-img]: https://img.shields.io/npm/v/css-has-pseudo.svg
102 | [npm-url]: https://www.npmjs.com/package/css-has-pseudo
103 |
104 | [CSS Has Pseudo]: https://github.com/csstools/css-has-pseudo
105 | [Mutation Observer polyfill]: https://github.com/webmodules/mutation-observer
106 | [Selectors Level 4]: https://drafts.csswg.org/selectors-4/#has-pseudo
107 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: 0 */
2 |
3 | import fs from 'fs';
4 | import plugin from './postcss';
5 |
6 | // get process and plugin options from the command line
7 | const fileRegExp = /^[\w/.]+$/;
8 | const argRegExp = /^--(\w+)=("|')?(.+)\2$/;
9 | const relaxedJsonPropRegExp = /(['"])?([a-z0-9A-Z_]+)(['"])?:/g;
10 | const relaxedJsonValueRegExp = /("[a-z0-9A-Z_]+":\s*)(?!true|false|null|\d+)'?([A-z0-9]+)'?([,}])/g;
11 | const argo = process.argv.slice(2).reduce(
12 | (object, arg) => {
13 | const argMatch = arg.match(argRegExp);
14 | const fileMatch = arg.match(fileRegExp);
15 |
16 | if (argMatch) {
17 | object[argMatch[1]] = argMatch[3];
18 | } else if (fileMatch) {
19 | if (object.from === '') {
20 | object.from = arg;
21 | } else if (object.to === '') {
22 | object.to = arg;
23 | }
24 | }
25 |
26 | return object;
27 | },
28 | { from: '', to: '', opts: 'null' }
29 | );
30 |
31 | // get css from command line arguments or stdin
32 | (argo.from === '' ? getStdin() : readFile(argo.from))
33 | .then(css => {
34 | if (argo.from === '' && !css) {
35 | console.log([
36 | 'CSS Has Pseudo\n',
37 | ' Transforms CSS with :has {}\n',
38 | 'Usage:\n',
39 | ' css-has-pseudo source.css transformed.css',
40 | ' css-has-pseudo --from=source.css --to=transformed.css --opts={}',
41 | ' echo "body:has(:focus) {}" | css-has-pseudo\n'
42 | ].join('\n'));
43 |
44 | process.exit(0);
45 | }
46 |
47 | const pluginOpts = JSON.parse(
48 | argo.opts
49 | .replace(relaxedJsonPropRegExp, '"$2": ')
50 | .replace(relaxedJsonValueRegExp, '$1"$2"$3')
51 | );
52 | const processOptions = Object.assign({ from: argo.from, to: argo.to || argo.from }, argo.map ? { map: JSON.parse(argo.map) } : {});
53 |
54 | const result = plugin.process(css, processOptions, pluginOpts);
55 |
56 | if (argo.to === '') {
57 | return result.css;
58 | } else {
59 | return writeFile(argo.to, result.css).then(
60 | () => `CSS was written to "${argo.to}"`
61 | )
62 | }
63 | }).catch(
64 | error => {
65 | if (Object(error).name === 'CssSyntaxError') {
66 | throw new Error(`PostCSS had trouble reading the file (${error.reason} on line ${error.line}, column ${error.column}).`);
67 | }
68 |
69 | if (Object(error).errno === -2) {
70 | throw new Error(`Sorry, "${error.path}" could not be read.`);
71 | }
72 |
73 | throw error;
74 | }
75 | ).then(
76 | result => {
77 | console.log(result);
78 |
79 | process.exit(0);
80 | },
81 | error => {
82 | console.error(Object(error).message || 'Something bad happened and we don’t even know what it was.');
83 |
84 | process.exit(1);
85 | }
86 | );
87 |
88 | function readFile (pathname) {
89 | return new Promise((resolve, reject) => {
90 | fs.readFile(pathname, 'utf8', (error, data) => {
91 | if (error) {
92 | reject(error);
93 | } else {
94 | resolve(data);
95 | }
96 | });
97 | });
98 | }
99 |
100 | function writeFile (pathname, data) {
101 | return new Promise((resolve, reject) => {
102 | fs.writeFile(pathname, data, (error, content) => {
103 | if (error) {
104 | reject(error);
105 | } else {
106 | resolve(content);
107 | }
108 | });
109 | });
110 | }
111 |
112 | function getStdin () {
113 | return new Promise(resolve => {
114 | let data = '';
115 |
116 | if (process.stdin.isTTY) {
117 | resolve(data);
118 | } else {
119 | process.stdin.setEncoding('utf8');
120 |
121 | process.stdin.on('readable', () => {
122 | let chunk;
123 |
124 | while ((chunk = process.stdin.read())) {
125 | data += chunk;
126 | }
127 | });
128 |
129 | process.stdin.on('end', () => {
130 | resolve(data);
131 | });
132 | }
133 | });
134 | }
135 |
--------------------------------------------------------------------------------
/INSTALL-POSTCSS.md:
--------------------------------------------------------------------------------
1 | # Installing PostCSS
2 |
3 | [CSS Has Pseudo] runs in all Node environments, with special instructions for:
4 |
5 | | [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) |
6 | | --- | --- | --- | --- | --- | --- |
7 |
8 | ## Node
9 |
10 | Add [CSS Has Pseudo] to your project:
11 |
12 | ```bash
13 | npm install css-has-pseudo --save-dev
14 | ```
15 |
16 | Use [CSS Has Pseudo] to process your CSS:
17 |
18 | ```js
19 | const postcssHasPseudo = require('css-has-pseudo/postcss');
20 |
21 | postcssHasPseudo.process(YOUR_CSS /*, processOptions, pluginOptions */);
22 | ```
23 |
24 | Or use it as a [PostCSS] plugin:
25 |
26 | ```js
27 | const postcss = require('postcss');
28 | const postcssHasPseudo = require('css-has-pseudo/postcss');
29 |
30 | postcss([
31 | postcssHasPseudo(/* pluginOptions */)
32 | ]).process(YOUR_CSS /*, processOptions */);
33 | ```
34 |
35 | ## PostCSS CLI
36 |
37 | Add [PostCSS CLI] to your project:
38 |
39 | ```bash
40 | npm install postcss-cli --save-dev
41 | ```
42 |
43 | Use [CSS Has Pseudo] in your `postcss.config.js` configuration file:
44 |
45 | ```js
46 | const postcssHasPseudo = require('css-has-pseudo/postcss');
47 |
48 | module.exports = {
49 | plugins: [
50 | postcssHasPseudo(/* pluginOptions */)
51 | ]
52 | }
53 | ```
54 |
55 | ## Webpack
56 |
57 | Add [PostCSS Loader] to your project:
58 |
59 | ```bash
60 | npm install postcss-loader --save-dev
61 | ```
62 |
63 | Use [CSS Has Pseudo] in your Webpack configuration:
64 |
65 | ```js
66 | const postcssHasPseudo = require('css-has-pseudo/postcss');
67 |
68 | module.exports = {
69 | module: {
70 | rules: [
71 | {
72 | test: /\.css$/,
73 | use: [
74 | 'style-loader',
75 | { loader: 'css-loader', options: { importLoaders: 1 } },
76 | { loader: 'postcss-loader', options: {
77 | ident: 'postcss',
78 | plugins: () => [
79 | postcssHasPseudo(/* pluginOptions */)
80 | ]
81 | } }
82 | ]
83 | }
84 | ]
85 | }
86 | }
87 | ```
88 |
89 | ## Create React App
90 |
91 | Add [React App Rewired] and [React App Rewire PostCSS] to your project:
92 |
93 | ```bash
94 | npm install react-app-rewired react-app-rewire-postcss --save-dev
95 | ```
96 |
97 | Use [React App Rewire PostCSS] and [CSS Has Pseudo] in your
98 | `config-overrides.js`
99 | file:
100 |
101 | ```js
102 | const reactAppRewirePostcss = require('react-app-rewire-postcss');
103 | const postcssHasPseudo = require('css-has-pseudo/postcss');
104 |
105 | module.exports = config => reactAppRewirePostcss(config, {
106 | plugins: () => [
107 | postcssHasPseudo(/* pluginOptions */)
108 | ]
109 | });
110 | ```
111 |
112 | ## Gulp
113 |
114 | Add [Gulp PostCSS] to your project:
115 |
116 | ```bash
117 | npm install gulp-postcss --save-dev
118 | ```
119 |
120 | Use [CSS Has Pseudo] in your Gulpfile:
121 |
122 | ```js
123 | const postcss = require('gulp-postcss');
124 | const postcssHasPseudo = require('css-has-pseudo/postcss');
125 |
126 | gulp.task('css', () => gulp.src('./src/*.css').pipe(
127 | postcss([
128 | postcssHasPseudo(/* pluginOptions */)
129 | ])
130 | ).pipe(
131 | gulp.dest('.')
132 | ));
133 | ```
134 |
135 | ## Grunt
136 |
137 | Add [Grunt PostCSS] to your project:
138 |
139 | ```bash
140 | npm install grunt-postcss --save-dev
141 | ```
142 |
143 | Use [CSS Has Pseudo] in your Gruntfile:
144 |
145 | ```js
146 | const postcssHasPseudo = require('css-has-pseudo/postcss');
147 |
148 | grunt.loadNpmTasks('grunt-postcss');
149 |
150 | grunt.initConfig({
151 | postcss: {
152 | options: {
153 | use: [
154 | postcssHasPseudo(/* pluginOptions */)
155 | ]
156 | },
157 | dist: {
158 | src: '*.css'
159 | }
160 | }
161 | });
162 | ```
163 |
164 | [CSS Has Pseudo]: https://github.com/csstools/css-has-pseudo
165 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss
166 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss
167 | [PostCSS]: https://github.com/postcss/postcss
168 | [PostCSS CLI]: https://github.com/postcss/postcss-cli
169 | [PostCSS Loader]: https://github.com/postcss/postcss-loader
170 | [React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss
171 | [React App Rewired]: https://github.com/timarney/react-app-rewired
172 |
--------------------------------------------------------------------------------
/src/browser.js:
--------------------------------------------------------------------------------
1 | export default function cssHasPseudo (document) {
2 | const observedItems = [];
3 |
4 | // document.createAttribute() doesn't support `:` in the name. innerHTML does
5 | const attributeElement = document.createElement('x');
6 |
7 | // walk all stylesheets to collect observed css rules
8 | [].forEach.call(document.styleSheets, walkStyleSheet);
9 | transformObservedItems();
10 |
11 | // observe DOM modifications that affect selectors
12 | const mutationObserver = new MutationObserver(mutationsList => {
13 | mutationsList.forEach(mutation => {
14 | [].forEach.call(mutation.addedNodes || [], node => {
15 | // walk stylesheets to collect observed css rules
16 | if (node.nodeType === 1 && node.sheet) {
17 | walkStyleSheet(node.sheet);
18 | }
19 | });
20 |
21 | // transform observed css rules
22 | cleanupObservedCssRules();
23 | transformObservedItems();
24 | });
25 | });
26 |
27 | mutationObserver.observe(document, { childList: true, subtree: true });
28 |
29 | // observe DOM events that affect pseudo-selectors
30 | document.addEventListener('focus', transformObservedItems, true);
31 | document.addEventListener('blur', transformObservedItems, true);
32 | document.addEventListener('input', transformObservedItems);
33 |
34 | // transform observed css rules
35 | function transformObservedItems () {
36 | requestAnimationFrame(() => {
37 | observedItems.forEach(
38 | item => {
39 | const nodes = [];
40 |
41 | [].forEach.call(
42 | document.querySelectorAll(item.scopeSelector),
43 | element => {
44 | const nthChild = [].indexOf.call(element.parentNode.children, element) + 1;
45 | const relativeSelectors = item.relativeSelectors.map(
46 | relativeSelector => item.scopeSelector + ':nth-child(' + nthChild + ') ' + relativeSelector
47 | ).join();
48 |
49 | // find any relative :has element from the :scope element
50 | const relativeElement = element.parentNode.querySelector(relativeSelectors);
51 |
52 | const shouldElementMatch = item.isNot ? !relativeElement : relativeElement;
53 |
54 | if (shouldElementMatch) {
55 | // memorize the node
56 | nodes.push(element);
57 |
58 | // set an attribute with an irregular attribute name
59 | // document.createAttribute() doesn't support special characters
60 | attributeElement.innerHTML = '';
61 |
62 | element.setAttributeNode(attributeElement.children[0].attributes[0].cloneNode());
63 |
64 | // trigger a style refresh in IE and Edge
65 | document.documentElement.style.zoom = 1; document.documentElement.style.zoom = null;
66 | }
67 | }
68 | );
69 |
70 | // remove the encoded attribute from all nodes that no longer match them
71 | item.nodes.forEach(node => {
72 | if (nodes.indexOf(node) === -1) {
73 | node.removeAttribute(item.attributeName);
74 |
75 | // trigger a style refresh in IE and Edge
76 | document.documentElement.style.zoom = 1; document.documentElement.style.zoom = null;
77 | }
78 | });
79 |
80 | // update the
81 | item.nodes = nodes;
82 | }
83 | );
84 | });
85 | }
86 |
87 | // remove any observed cssrules that no longer apply
88 | function cleanupObservedCssRules () {
89 | [].push.apply(
90 | observedItems,
91 | observedItems.splice(0).filter(
92 | item => item.rule.parentStyleSheet &&
93 | item.rule.parentStyleSheet.ownerNode &&
94 | document.documentElement.contains(item.rule.parentStyleSheet.ownerNode)
95 | )
96 | );
97 | }
98 |
99 | // walk a stylesheet to collect observed css rules
100 | function walkStyleSheet (styleSheet) {
101 | try {
102 | // walk a css rule to collect observed css rules
103 | [].forEach.call(styleSheet.cssRules || [], rule => {
104 | if (rule.selectorText) {
105 | // decode the selector text in all browsers to:
106 | // [1] = :scope, [2] = :not(:has), [3] = :has relative, [4] = :scope relative
107 | const selectors = decodeURIComponent(rule.selectorText.replace(/\\(.)/g, '$1')).match(/^(.*?)\[:(not-)?has\((.+?)\)\](.*?)$/);
108 |
109 | if (selectors) {
110 | const attributeName = ':' + (selectors[2] ? 'not-' : '') + 'has(' +
111 | // encode a :has() pseudo selector as an attribute name
112 | encodeURIComponent(selectors[3]).replace(/%3A/g, ':').replace(/%5B/g, '[').replace(/%5D/g, ']').replace(/%2C/g, ',') +
113 | ')';
114 |
115 | observedItems.push({
116 | rule,
117 | scopeSelector: selectors[1],
118 | isNot: selectors[2],
119 | relativeSelectors: selectors[3].split(/\s*,\s*/),
120 | attributeName,
121 | nodes: []
122 | });
123 | }
124 | } else {
125 | walkStyleSheet(rule);
126 | }
127 | });
128 | } catch (error) {
129 | /* do nothing and continue */
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # CC0 1.0 Universal
2 |
3 | ## Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an “owner”) of an original work of
8 | authorship and/or a database (each, a “Work”).
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific works
12 | (“Commons”) that the public can reliably and without fear of later claims of
13 | infringement build upon, modify, incorporate in other works, reuse and
14 | redistribute as freely as possible in any form whatsoever and for any purposes,
15 | including without limitation commercial purposes. These owners may contribute
16 | to the Commons to promote the ideal of a free culture and the further
17 | production of creative, cultural and scientific works, or to gain reputation or
18 | greater distribution for their Work in part through the use and efforts of
19 | others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation of
22 | additional consideration or compensation, the person associating CC0 with a
23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
25 | publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights (“Copyright and
31 | Related Rights”). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate,
34 | and translate a Work;
35 | 2. moral rights retained by the original author(s) and/or performer(s);
36 | 3. publicity and privacy rights pertaining to a person’s image or likeness
37 | depicted in a Work;
38 | 4. rights protecting against unfair competition in regards to a Work,
39 | subject to the limitations in paragraph 4(i), below;
40 | 5. rights protecting the extraction, dissemination, use and reuse of data in
41 | a Work;
42 | 6. database rights (such as those arising under Directive 96/9/EC of the
43 | European Parliament and of the Council of 11 March 1996 on the legal
44 | protection of databases, and under any national implementation thereof,
45 | including any amended or successor version of such directive); and
46 | 7. other similar, equivalent or corresponding rights throughout the world
47 | based on applicable law or treaty, and any national implementations
48 | thereof.
49 |
50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
53 | and Related Rights and associated claims and causes of action, whether now
54 | known or unknown (including existing as well as future claims and causes of
55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
56 | duration provided by applicable law or treaty (including future time
57 | extensions), (iii) in any current or future medium and for any number of
58 | copies, and (iv) for any purpose whatsoever, including without limitation
59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer
60 | makes the Waiver for the benefit of each member of the public at large and
61 | to the detriment of Affirmer’s heirs and successors, fully intending that
62 | such Waiver shall not be subject to revocation, rescission, cancellation,
63 | termination, or any other legal or equitable action to disrupt the quiet
64 | enjoyment of the Work by the public as contemplated by Affirmer’s express
65 | Statement of Purpose.
66 |
67 | 3. Public License Fallback. Should any part of the Waiver for any reason be
68 | judged legally invalid or ineffective under applicable law, then the Waiver
69 | shall be preserved to the maximum extent permitted taking into account
70 | Affirmer’s express Statement of Purpose. In addition, to the extent the
71 | Waiver is so judged Affirmer hereby grants to each affected person a
72 | royalty-free, non transferable, non sublicensable, non exclusive,
73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and
74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
75 | maximum duration provided by applicable law or treaty (including future time
76 | extensions), (iii) in any current or future medium and for any number of
77 | copies, and (iv) for any purpose whatsoever, including without limitation
78 | commercial, advertising or promotional purposes (the “License”). The License
79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the
80 | Work. Should any part of the License for any reason be judged legally
81 | invalid or ineffective under applicable law, such partial invalidity or
82 | ineffectiveness shall not invalidate the remainder of the License, and in
83 | such case Affirmer hereby affirms that he or she will not (i) exercise any
84 | of his or her remaining Copyright and Related Rights in the Work or (ii)
85 | assert any associated claims and causes of action with respect to the Work,
86 | in either case contrary to Affirmer’s express Statement of Purpose.
87 |
88 | 4. Limitations and Disclaimers.
89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned,
90 | surrendered, licensed or otherwise affected by this document.
91 | 2. Affirmer offers the Work as-is and makes no representations or warranties
92 | of any kind concerning the Work, express, implied, statutory or
93 | otherwise, including without limitation warranties of title,
94 | merchantability, fitness for a particular purpose, non infringement, or
95 | the absence of latent or other defects, accuracy, or the present or
96 | absence of errors, whether or not discoverable, all to the greatest
97 | extent permissible under applicable law.
98 | 3. Affirmer disclaims responsibility for clearing rights of other persons
99 | that may apply to the Work or any use thereof, including without
100 | limitation any person’s Copyright and Related Rights in the Work.
101 | Further, Affirmer disclaims responsibility for obtaining any necessary
102 | consents, permissions or other rights required for any use of the Work.
103 | 4. Affirmer understands and acknowledges that Creative Commons is not a
104 | party to this document and has no duty or obligation with respect to this
105 | CC0 or use of the Work.
106 |
107 | For more information, please see
108 | http://creativecommons.org/publicdomain/zero/1.0/.
109 |
--------------------------------------------------------------------------------