diff --git a/plugins/css-blank-pseudo/.gitignore b/plugins/css-blank-pseudo/.gitignore index 03fe35af5..7172b04f1 100644 --- a/plugins/css-blank-pseudo/.gitignore +++ b/plugins/css-blank-pseudo/.gitignore @@ -1,15 +1,6 @@ node_modules -dist package-lock.json yarn.lock -browser.js -!src/browser.js -browser-legacy.js -*.log* *.result.css *.result.css.map -!.editorconfig -!.gitignore -!.rollup.js -!.tape.js -!.travis.yml +dist/* diff --git a/plugins/css-blank-pseudo/.tape.mjs b/plugins/css-blank-pseudo/.tape.mjs index 1c0aa6706..20a53c7d2 100644 --- a/plugins/css-blank-pseudo/.tape.mjs +++ b/plugins/css-blank-pseudo/.tape.mjs @@ -17,11 +17,35 @@ postcssTape(plugin)({ preserve: false } }, - 'generated-selector-cases': { - message: 'correctly handles generated cases', + 'basic:wrong-replacewith': { + message: 'correctly warns when replace with is invalid', warnings: 1, + options: { + replaceWith: '#css-blank' + } + }, + 'examples/example': { + message: 'minimal example', + }, + 'examples/example:preserve-false': { + message: 'minimal example', options: { preserve: false } }, + 'examples/example:replacewith': { + message: 'minimal example', + options: { + replaceWith: '.css-blank' + } + }, + 'browser': { + message: 'css for browser tests', + }, + 'browser:replacewith': { + message: 'css for browser tests', + options: { + replaceWith: '.css-blank' + } + }, }); diff --git a/plugins/css-blank-pseudo/CHANGELOG.md b/plugins/css-blank-pseudo/CHANGELOG.md index faab22d18..7701a1bfb 100644 --- a/plugins/css-blank-pseudo/CHANGELOG.md +++ b/plugins/css-blank-pseudo/CHANGELOG.md @@ -1,5 +1,28 @@ # Changes to CSS Blank Pseudo +### Unreleased (major) + +- Updated: The polyfill now only attaches a single listener to the body so it's +more efficient and also does less work at the MutationObserver handler. +- Breaking: removed old CDN urls +- Breaking: removed old CLI + +#### How to migrate: + +- If you use a CDN url, please update it. +- Re-build your CSS with the new version of the library. + +```diff +- +- ++ +``` + +```diff +- cssBlankPseudo(document) ++ cssBlankPseudoInit() +``` + ### 3.0.3 (February 5, 2022) - Rebuild of browser polyfills diff --git a/plugins/css-blank-pseudo/INSTALL.md b/plugins/css-blank-pseudo/INSTALL.md index 79d87e3c5..1cc2e5d88 100644 --- a/plugins/css-blank-pseudo/INSTALL.md +++ b/plugins/css-blank-pseudo/INSTALL.md @@ -1,13 +1,13 @@ -# Installing CSS Blank Pseudo +# Installing PostCSS Blank Pseudo -[CSS Blank Pseudo] runs in all Node environments, with special instructions for: +[PostCSS Blank Pseudo] runs in all Node environments, with special instructions for: | [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | | --- | --- | --- | --- | --- | --- | ## Node -Add [CSS Blank Pseudo] to your project: +Add [PostCSS Blank Pseudo] to your project: ```bash npm install postcss css-blank-pseudo --save-dev @@ -32,7 +32,7 @@ Add [PostCSS CLI] to your project: npm install postcss-cli css-blank-pseudo --save-dev ``` -Use [CSS Blank Pseudo] in your `postcss.config.js` configuration file: +Use [PostCSS Blank Pseudo] in your `postcss.config.js` configuration file: ```js const postcssBlankPseudo = require('css-blank-pseudo'); @@ -54,7 +54,7 @@ Add [PostCSS Loader] to your project: npm install postcss-loader css-blank-pseudo --save-dev ``` -Use [CSS Blank Pseudo] in your Webpack configuration: +Use [PostCSS Blank Pseudo] in your Webpack configuration: ```js module.exports = { @@ -98,7 +98,7 @@ Add [React App Rewired] and [React App Rewire PostCSS] to your project: npm install react-app-rewired react-app-rewire-postcss css-blank-pseudo --save-dev ``` -Use [React App Rewire PostCSS] and [CSS Blank Pseudo] in your +Use [React App Rewire PostCSS] and [PostCSS Blank Pseudo] in your `config-overrides.js` file: ```js @@ -120,7 +120,7 @@ Add [Gulp PostCSS] to your project: npm install gulp-postcss css-blank-pseudo --save-dev ``` -Use [CSS Blank Pseudo] in your Gulpfile: +Use [PostCSS Blank Pseudo] in your Gulpfile: ```js const postcss = require('gulp-postcss'); @@ -145,7 +145,7 @@ Add [Grunt PostCSS] to your project: npm install grunt-postcss css-blank-pseudo --save-dev ``` -Use [CSS Blank Pseudo] in your Gruntfile: +Use [PostCSS Blank Pseudo] in your Gruntfile: ```js const postcssBlankPseudo = require('css-blank-pseudo'); @@ -171,6 +171,6 @@ grunt.initConfig({ [PostCSS]: https://github.com/postcss/postcss [PostCSS CLI]: https://github.com/postcss/postcss-cli [PostCSS Loader]: https://github.com/postcss/postcss-loader -[CSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo +[PostCSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo [React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss [React App Rewired]: https://github.com/timarney/react-app-rewired diff --git a/plugins/css-blank-pseudo/README-BROWSER.md b/plugins/css-blank-pseudo/README-BROWSER.md deleted file mode 100644 index 5bec8b999..000000000 --- a/plugins/css-blank-pseudo/README-BROWSER.md +++ /dev/null @@ -1,80 +0,0 @@ -# CSS Blank Pseudo for Browsers [][CSS Blank Pseudo] - -[![NPM Version][npm-img]][npm-url] -[Discord][discord] - -[CSS Blank Pseudo] lets you style form elements when they are empty, following -the [Selectors Level 4] specification. - -```css -input { - /* style an input */ -} - -input[blank] { - /* style an input without a value */ -} -``` - -## Usage - -Add [CSS Blank Pseudo] to your build tool: - -```bash -npm install css-blank-pseudo -``` - -Then include and initialize it on your document: - -```js -const cssBlankPseudo = require('css-blank-pseudo/browser'); - -cssBlankPseudo(document); -``` - -## Options - -[CSS Blank Pseudo] accepts a secondary paramater to configure the attribute or -class name added to elements matching focused elements or containing focused -elements. - -```js -cssBlankPseudo(document, { - attr: false, - className: '.blank' -}); -``` - -Falsey values on either `attr` or `className` will disable setting the -attribute or class name on elements matching `:blank`. - -[CSS Blank Pseudo] also accepts a secondary paramater to configure whether the -polyfill is loaded regardless of support. If `force` is given a truthy value, -then the polyfill will always execute. - -```js -cssBlankPseudo(document, { - force: true -}); -``` - -## Dependencies - -Web API's: - -- [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) - -ECMA Script: - -- `Object.prototype.toString` -- `Object.getOwnPropertyDescriptor` -- `Object.defineProperty` -- `Array.prototype.forEach` - - -[discord]: https://discord.gg/bUadyRwkJS -[npm-img]: https://img.shields.io/npm/v/css-blank-pseudo.svg -[npm-url]: https://www.npmjs.com/package/css-blank-pseudo - -[CSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo -[Selectors Level 4]: https://drafts.csswg.org/selectors-4/#blank diff --git a/plugins/css-blank-pseudo/README-POSTCSS.md b/plugins/css-blank-pseudo/README-POSTCSS.md deleted file mode 100644 index 18884b4eb..000000000 --- a/plugins/css-blank-pseudo/README-POSTCSS.md +++ /dev/null @@ -1,116 +0,0 @@ -# CSS Blank Pseudo for PostCSS [][CSS Blank Pseudo] - -[![NPM Version][npm-img]][npm-url] -[Discord][discord] - -[CSS Blank Pseudo] lets you style form elements when they are empty, following -the [Selectors Level 4] specification. - -```css -input:blank { - background-color: yellow; -} - -/* becomes */ - -.field[blank] label { - background-color: yellow; -} - -.field:blank label { - background-color: yellow; -} -``` - -[CSS Blank Pseudo] duplicates rules using the `:blank` pseudo-class with a -`[blank]` attribute selector. This replacement selector can be changed -using the `replaceWith` option. Also, the preservation of the original -`:blank` rule can be disabled using the `preserve` option. - -## Usage - -Add [CSS Blank Pseudo] to your project: - -```bash -npm install css-blank-pseudo --save-dev -``` - -Use [CSS Blank Pseudo] to process your CSS: - -```js -const postcssBlankPseudo = require('css-blank-pseudo'); - -postcssBlankPseudo.process(YOUR_CSS /*, processOptions, pluginOptions */); -``` - -Or use it as a [PostCSS] plugin: - -```js -const postcss = require('postcss'); -const postcssBlankPseudo = require('css-blank-pseudo'); - -postcss([ - postcssBlankPseudo(/* pluginOptions */) -]).process(YOUR_CSS /*, processOptions */); -``` - -[CSS Blank Pseudo] runs in all Node environments, with special -instructions for: - -| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | -| --- | --- | --- | --- | --- | --- | - -## Options - -### preserve - -The `preserve` option defines whether the original selector should remain. By -default, the original selector is preserved. - -```js -cssBlankPseudo({ preserve: false }); -``` - -```css -input:blank { - background-color: yellow; -} - -/* becomes */ - -.field[blank] label { - background-color: yellow; -} -``` - -### replaceWith - -The `replaceWith` option defines the selector to replace `:blank`. By -default, the replacement selector is `[blank]`. - -```js -cssBlankPseudo({ replaceWith: '.blank' }); -``` - -```css -input:blank { - background-color: yellow; -} - -/* becomes */ - -.field.blank label { - background-color: yellow; -} - -.field:blank label { - background-color: yellow; -} -``` - -[discord]: https://discord.gg/bUadyRwkJS -[npm-img]: https://img.shields.io/npm/v/css-blank-pseudo.svg -[npm-url]: https://www.npmjs.com/package/css-blank-pseudo - -[CSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo -[Selectors Level 4]: https://drafts.csswg.org/selectors-4/#blank diff --git a/plugins/css-blank-pseudo/README.md b/plugins/css-blank-pseudo/README.md index b1cf7fd52..4179bd424 100644 --- a/plugins/css-blank-pseudo/README.md +++ b/plugins/css-blank-pseudo/README.md @@ -1,104 +1,163 @@ -# CSS Blank Pseudo [][CSS Blank Pseudo] +# PostCSS Blank Pseudo [PostCSS Logo][postcss] -[![NPM Version][npm-img]][npm-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] -[CSS Blank Pseudo] lets you style form elements when they are empty, following +[PostCSS Blank Pseudo] lets you style form elements when they are empty, following the [Selectors Level 4] specification. -```css -input { - /* style an input */ +```pcss +input:blank { + background-color: yellow; } +/* becomes */ + +input[blank] { + background-color: yellow; +} input:blank { - /* style an input without a value */ + background-color: yellow; } ``` ## Usage -From the command line, transform CSS files that use `:blank` selectors: +Add [PostCSS Blank Pseudo] to your project: ```bash -npx css-blank-pseudo SOURCE.css --output TRANSFORMED.css +npm install postcss css-blank-pseudo --save-dev ``` -Next, use your transformed CSS with this script: +Use it as a [PostCSS] plugin: -```html - - - +```js +const postcss = require('postcss'); +const postcssBlankPseudo = require('css-blank-pseudo'); + +postcss([ + postcssBlankPseudo(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); ``` -⚠️ Please use a versioned url, like this : `https://unpkg.com/css-blank-pseudo@3.0.0/dist/browser-global.js` -Without the version, you might unexpectedly get a new major version of the library with breaking changes. +[PostCSS Blank Pseudo] runs in all Node environments, with special +instructions for: + +| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | +| --- | --- | --- | --- | --- | --- | -⚠️ If you were using an older version via a CDN, please update the entire url. -The old URL will no longer work in a future release. +## Options -That’s it. The script works in most browsers. +### preserve -## How it works +The `preserve` option determines whether the original notation +is preserved. By default, it is preserved. -The [PostCSS plugin](README-POSTCSS.md) clones rules containing `:blank`, -replacing them with an alternative `[blank]` selector. +```js +postcssBlankPseudo({ preserve: false }) +``` -```css +```pcss input:blank { - background-color: yellow; + background-color: yellow; } /* becomes */ input[blank] { - background-color: yellow; + background-color: yellow; } +``` + +### replaceWith + +The `replaceWith` option determines the selector to use when replacing +the `:blank` pseudo. By default is `[blank]` +```js +postcssBlankPseudo({ replaceWith: '.css-blank' }) +``` + +```pcss input:blank { - background-color: yellow; + background-color: yellow; +} + +/* becomes */ + +.foo { + color: blue; + color: red; +} + +.baz { + color: green; } ``` -Next, the [JavaScript library](README-BROWSER.md) adds a `blank` attribute to -elements otherwise matching `:blank` natively. +Note that changing this option implies that it needs to be passed to the +browser polyfill as well. + +## Browser + +```js +import cssBlankPseudoInit from 'css-blank-pseudo/browser'; + +cssBlankPseudoInit(); +``` + +or ```html - - + + + ``` -## ⚠️ `:not(:blank)` +[PostCSS Blank Pseudo] works in all major browsers, including Safari 6+ and +Internet Explorer 9+ without any additional polyfills. -with option : `preserve` `true` +This plugin conditionally uses `MutationObserver` to ensure recently inserted +inputs get correct styling upon insertion. If you intend to rely on that +behaviour for browsers that do not support `MutationObserver`, you have two +options: -```css -input:not(:blank) { - background-color: yellow; -} +1. Polyfill `MutationObserver`. As long as it runs before `cssBlankPseudoInit`, +the polyfill will work. +2. If you don't want to polyfill `MutationObserver` you can also manually fire +a `change` event upon insertion so they're automatically inspected by the +polyfill. -/* becomes */ +### Browser Usage -input:not([blank]) { - background-color: yellow; -} +#### force -input:not(:blank) { - background-color: yellow; -} +The `force` option determines whether the library runs even if the browser +supports the selector or not. By default, it won't run if the browser does +support the selector. + +```js +cssBlankPseudoInit({ force: true }); ``` -When you do not include the JS polyfill one will always match in browsers that support `:blank` natively. -In browsers that do not support `:blank` natively the rule will be invalid. +#### replaceWith -_currently no browsers support `:blank`_ +Similar to the option for the PostCSS Plugin, `replaceWith` determines the +attribute or class to apply to an element when it's considered to be `:blank`. + +```js +cssBlankPseudoInit({ replaceWith: '.css-blank' }); +``` +This option should be used if it was changed at PostCSS configuration level. +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[css-url]: https://cssdb.org/#blank-pseudo-class [discord]: https://discord.gg/bUadyRwkJS -[npm-img]: https://img.shields.io/npm/v/css-blank-pseudo.svg [npm-url]: https://www.npmjs.com/package/css-blank-pseudo -[CSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo -[PostCSS Preset Env]: https://preset-env.cssdb.org/ -[Selectors Level 4]: https://drafts.csswg.org/selectors-4/#blank +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Blank Pseudo]: https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo +[Selectors Level 4]: https://www.w3.org/TR/selectors-4/#blank diff --git a/plugins/css-blank-pseudo/docs/README.md b/plugins/css-blank-pseudo/docs/README.md new file mode 100644 index 000000000..fbd96f626 --- /dev/null +++ b/plugins/css-blank-pseudo/docs/README.md @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + +
+ +[] lets you style form elements when they are empty, following +the [Selectors Level 4] specification. + +```pcss + + +/* becomes */ + + +``` + + + + + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is preserved. + +```js +({ preserve: false }) +``` + +```pcss + + +/* becomes */ + + +``` + +### replaceWith + +The `replaceWith` option determines the selector to use when replacing +the `:blank` pseudo. By default is `[blank]` + +```js +({ replaceWith: '.css-blank' }) +``` + +```pcss + + +/* becomes */ + + +``` + +Note that changing this option implies that it needs to be passed to the +browser polyfill as well. + +## Browser + +```js +import cssBlankPseudoInit from 'css-blank-pseudo/browser'; + +cssBlankPseudoInit(); +``` + +or + +```html + + + +``` + +[] works in all major browsers, including Safari 6+ and +Internet Explorer 9+ without any additional polyfills. + +This plugin conditionally uses `MutationObserver` to ensure recently inserted +inputs get correct styling upon insertion. If you intend to rely on that +behaviour for browsers that do not support `MutationObserver`, you have two +options: + +1. Polyfill `MutationObserver`. As long as it runs before `cssBlankPseudoInit`, +the polyfill will work. +2. If you don't want to polyfill `MutationObserver` you can also manually fire +a `change` event upon insertion so they're automatically inspected by the +polyfill. + +### Browser Usage + +#### force + +The `force` option determines whether the library runs even if the browser +supports the selector or not. By default, it won't run if the browser does +support the selector. + +```js +cssBlankPseudoInit({ force: true }); +``` + +#### replaceWith + +Similar to the option for the PostCSS Plugin, `replaceWith` determines the +attribute or class to apply to an element when it's considered to be `:blank`. + +```js +cssBlankPseudoInit({ replaceWith: '.css-blank' }); +``` + +This option should be used if it was changed at PostCSS configuration level. + + +[Selectors Level 4]: diff --git a/plugins/css-blank-pseudo/package.json b/plugins/css-blank-pseudo/package.json index 7215e4447..b62161050 100644 --- a/plugins/css-blank-pseudo/package.json +++ b/plugins/css-blank-pseudo/package.json @@ -2,7 +2,21 @@ "name": "css-blank-pseudo", "description": "Style form elements when they are empty", "version": "3.0.3", - "author": "Jonathan Neal ", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + }, + { + "name": "Jonathan Neal", + "email": "jonathantneal@hotmail.com" + } + ], "license": "CC0-1.0", "funding": { "type": "opencollective", @@ -13,9 +27,7 @@ }, "main": "dist/index.cjs", "module": "dist/index.mjs", - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, + "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.mjs", @@ -35,7 +47,6 @@ "CHANGELOG.md", "LICENSE.md", "README.md", - "browser.js", "dist" ], "dependencies": { @@ -45,17 +56,17 @@ "postcss": "^8.2" }, "scripts": { - "build": "rollup -c ../../rollup/default.js && npm run copy-browser-scripts-to-old-location", + "build": "rollup -c ../../rollup/default.js", "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", - "cli": "css-blank-pseudo", - "copy-browser-scripts-to-old-location": "node -e \"fs.copyFileSync('./dist/browser-global.js', './browser.js'); fs.copyFileSync('./dist/browser-global.js', './browser-legacy.js')\"", - "docs": "node ../../.github/bin/generate-docs/install.mjs", + "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs", "lint": "npm run lint:eslint && npm run lint:package-json", "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", "lint:package-json": "node ../../.github/bin/format-package-json.mjs", "prepublishOnly": "npm run clean && npm run build && npm run test", - "test": "node .tape.mjs && npm run test:exports", + "test": "node .tape.mjs && npm run test:exports && npm run test:invalid-replacement", + "test:browser": "node ./test/_browser.mjs", "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", + "test:invalid-replacement": "node ./test/_valid-replacements.mjs", "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" }, "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/css-blank-pseudo#readme", @@ -83,8 +94,10 @@ "textarea" ], "csstools": { + "cssdbId": "blank-pseudo-class", "exportName": "postcssBlankPseudo", - "humanReadableName": "CSS Blank Pseudo" + "humanReadableName": "PostCSS Blank Pseudo", + "specUrl": "https://www.w3.org/TR/selectors-4/#blank" }, "volta": { "extends": "../../package.json" diff --git a/plugins/css-blank-pseudo/src/browser-global.js b/plugins/css-blank-pseudo/src/browser-global.js index f6f2114e9..e21c7bf19 100644 --- a/plugins/css-blank-pseudo/src/browser-global.js +++ b/plugins/css-blank-pseudo/src/browser-global.js @@ -1,3 +1,3 @@ /* global self */ -import { default as cssBlankPseudo } from './browser'; -self.cssBlankPseudo = cssBlankPseudo; +import { default as cssBlankPseudoInit } from './browser'; +self.cssBlankPseudoInit = cssBlankPseudoInit; diff --git a/plugins/css-blank-pseudo/src/browser.js b/plugins/css-blank-pseudo/src/browser.js index af5cc5e9f..5706d8713 100644 --- a/plugins/css-blank-pseudo/src/browser.js +++ b/plugins/css-blank-pseudo/src/browser.js @@ -1,126 +1,146 @@ -/* global MutationObserver */ -export default function cssBlankPseudo(document, opts) { - // configuration - const className = Object(opts).className; - const attr = Object(opts).attr || 'blank'; - const force = Object(opts).force; +/* global document,MutationObserver */ +import isValidReplacement from './is-valid-replacement.mjs'; - try { - document.querySelector(':blank'); +// form control elements selector +const BLANK_CANDIDATES = 'input,select,textarea'; + +function createNewEvent(eventName) { + let event; + + if (typeof(Event) === 'function') { + event = new Event(eventName, { bubbles: true }); + } else { + event = document.createEvent('Event'); + event.initEvent(eventName, true, false); + } - if (!force) { + return event; +} + +function generateHandler(replaceWith) { + let selector; + let remove; + let add; + const matches = typeof document.body.matches === 'function' + ? 'matches' : 'msMatchesSelector'; + + if (replaceWith[0] === '.') { + selector = replaceWith.slice(1); + remove = (el) => el.classList.remove(selector); + add = (el) => el.classList.add(selector); + } else { + // A bit naive + selector = replaceWith.slice(1, -1); + remove = (el) => el.removeAttribute(selector, ''); + add = (el) => el.setAttribute(selector, ''); + } + + return function handleInputOrChangeEvent(event) { + const element = event.target; + if (!element[matches](BLANK_CANDIDATES)) { return; } - } catch (ignoredError) { /* do nothing and continue */ } - // observe value changes on , + + + + + diff --git a/plugins/css-blank-pseudo/test/_browser.mjs b/plugins/css-blank-pseudo/test/_browser.mjs new file mode 100644 index 000000000..b8fc7dad4 --- /dev/null +++ b/plugins/css-blank-pseudo/test/_browser.mjs @@ -0,0 +1,228 @@ +/* global window,requestAnimationFrame */ +import puppeteer from 'puppeteer'; +import http from 'http'; +import { promises as fsp } from 'fs'; + +(async () => { + const requestListener = async function (req, res) { + + const parsedUrl = new URL(req.url, 'http://localhost:8080'); + const pathname = parsedUrl.pathname; + + switch (pathname) { + case '': + case '/': + res.setHeader('Content-type', 'text/html'); + res.writeHead(200); + res.end(await fsp.readFile('test/_browser.html', 'utf8')); + break; + case '/replace-with': + res.setHeader('Content-type', 'text/html'); + res.writeHead(200); + res.end(await fsp.readFile('test/_browser_replace.html', 'utf8')); + break; + case '/test/browser.expect.css': + res.setHeader('Content-type', 'text/css'); + res.writeHead(200); + res.end(await fsp.readFile('test/browser.expect.css', 'utf8')); + break; + case '/test/browser.replacewith.expect.css': + res.setHeader('Content-type', 'text/css'); + res.writeHead(200); + res.end(await fsp.readFile('test/browser.replacewith.expect.css', 'utf8')); + break; + case '/dist/browser-global.js': + res.setHeader('Content-type', 'text/javascript'); + res.writeHead(200); + res.end(await fsp.readFile('dist/browser-global.js', 'utf8')); + break; + default: + res.setHeader('Content-type', 'text/plain' ); + res.writeHead(404); + res.end('Not found'); + break; + } + }; + + // Use different servers for HTML/CSS/JS to trigger CORS + const server = http.createServer(requestListener); + server.listen(8080); + + if (!process.env.DEBUG) { + const browser = await puppeteer.launch({ + headless: true, + }); + + const page = await browser.newPage(); + page.on('pageerror', (msg) => { + throw msg; + }); + + const clearInput = async (page, selector ) => { + const input = await page.$(selector); + await input.click({ clickCount: 3 }); + await page.keyboard.press('Backspace'); + }; + + // Default + { + await page.goto('http://localhost:8080'); + const result = await page.evaluate(async () => { + // eslint-disable-next-line no-undef + return await window.runTest(); + }); + + if (!result) { + throw new Error('Test failed, expected "window.runTest()" to return true'); + } + } + + // Changing values + { + await page.goto('http://localhost:8080'); + + await page.evaluate(async () => window.runTest()); + + await page.type('#tel-input', '1234'); + await page.type('#text-input', '1234'); + await page.type('#number-input', '1234'); + await page.type('#password-input', '1234'); + await page.type('#textarea', '1234'); + await page.select('#select', 'non-empty'); + + const fillingResults = await Promise.all([ + page.evaluate(async () => window.checkElement('user typing', 'tel', false)), + page.evaluate(async () => window.checkElement('user typing', 'text', false)), + page.evaluate(async () => window.checkElement('user typing', 'number', false)), + page.evaluate(async () => window.checkElement('user typing', 'password', false)), + page.evaluate(async () => window.checkElement('user typing', 'textarea', false)), + page.evaluate(async () => window.checkElement('user typing', 'select', false)), + ]); + + // Reverting now, should revert + await clearInput(page, '#tel-input'); + await clearInput(page, '#text-input'); + await clearInput(page, '#number-input'); + await clearInput(page, '#password-input'); + await clearInput(page, '#textarea'); + await page.select('#select', ''); + + const unfillingResults = await Promise.all([ + page.evaluate(async () => window.checkElement('user typing', 'tel', true)), + page.evaluate(async () => window.checkElement('user typing', 'text', true)), + page.evaluate(async () => window.checkElement('user typing', 'number', true)), + page.evaluate(async () => window.checkElement('user typing', 'password', true)), + page.evaluate(async () => window.checkElement('user typing', 'textarea', true)), + page.evaluate(async () => window.checkElement('user typing', 'select', true)), + ]); + + const result = [ + ...fillingResults, + ...unfillingResults, + ].every(test => !!test); + + if (!result) { + throw new Error('Test failed'); + } + } + + // Changing values via JS + { + await page.goto('http://localhost:8080'); + + await page.evaluate(async () => window.runTest()); + + await page.evaluate(async () => window.document.getElementById('tel-input').value = '1234'); + await page.evaluate(async () => window.document.getElementById('text-input').value = '1234'); + await page.evaluate(async () => window.document.getElementById('number-input').value = '1234'); + await page.evaluate(async () => window.document.getElementById('password-input').value = '1234'); + await page.evaluate(async () => window.document.getElementById('textarea').value = '1234'); + await page.evaluate(async () => window.document.getElementById('select').value = 'non-empty'); + + + const fillingResults = await Promise.all([ + page.evaluate(async () => window.checkElement('js value change', 'tel', false)), + page.evaluate(async () => window.checkElement('js value change', 'text', false)), + page.evaluate(async () => window.checkElement('js value change', 'number', false)), + page.evaluate(async () => window.checkElement('js value change', 'password', false)), + page.evaluate(async () => window.checkElement('js value change', 'textarea', false)), + page.evaluate(async () => window.checkElement('js value change', 'select', false)), + ]); + + // Reverting + await page.evaluate(async () => window.document.getElementById('tel-input').value = ''); + await page.evaluate(async () => window.document.getElementById('text-input').value = ''); + await page.evaluate(async () => window.document.getElementById('number-input').value = ''); + await page.evaluate(async () => window.document.getElementById('password-input').value = ''); + await page.evaluate(async () => window.document.getElementById('textarea').value = ''); + await page.evaluate(async () => window.document.getElementById('select').value = ''); + + const unfillingResults = await Promise.all([ + page.evaluate(async () => window.checkElement('js value change', 'tel', true)), + page.evaluate(async () => window.checkElement('js value change', 'text', true)), + page.evaluate(async () => window.checkElement('js value change', 'number', true)), + page.evaluate(async () => window.checkElement('js value change', 'password', true)), + page.evaluate(async () => window.checkElement('js value change', 'textarea', true)), + page.evaluate(async () => window.checkElement('js value change', 'select', true)), + ]); + + await page.evaluate(async () => { + window.document.getElementById('select').options[1].selected = true; + }); + await page.evaluate(async () => window.checkElement('js value change', 'select', false)); + + const result = [ + ...fillingResults, + ...unfillingResults, + ].every(test => !!test); + + if (!result) { + throw new Error('Test failed'); + } + } + + // Dynamic element + { + await page.goto('http://localhost:8080'); + await page.evaluate(async () => { + // eslint-disable-next-line no-undef + return await window.runTest(); + }); + + await page.evaluate(async () => { + const filledInput = window.document.createElement('input'); + const unfilledInput = window.document.createElement('input'); + filledInput.value = 'foo'; + + window.document.body.append(filledInput); + window.document.body.append(unfilledInput); + + requestAnimationFrame(() => { + // Not blank + window.checkElement('dynamic input', filledInput, false); + // Blank + window.checkElement('dynamic input', unfilledInput, true); + }); + }); + } + + // Replace with + { + await page.goto('http://localhost:8080/replace-with'); + const result = await page.evaluate(async () => { + // eslint-disable-next-line no-undef + return await window.runTest(); + }); + + if (!result) { + throw new Error('Test failed, expected "window.runTest()" to return true'); + } + } + + await browser.close(); + + await server.close(); + } else { + console.log('visit : http://localhost:8080'); + } +})(); diff --git a/plugins/css-blank-pseudo/test/_browser_replace.html b/plugins/css-blank-pseudo/test/_browser_replace.html new file mode 100644 index 000000000..07d8fb633 --- /dev/null +++ b/plugins/css-blank-pseudo/test/_browser_replace.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/css-blank-pseudo/test/_valid-replacements.mjs b/plugins/css-blank-pseudo/test/_valid-replacements.mjs new file mode 100644 index 000000000..bcc4d294d --- /dev/null +++ b/plugins/css-blank-pseudo/test/_valid-replacements.mjs @@ -0,0 +1,24 @@ +import assert from 'assert'; +import isValidReplacement from '../src/is-valid-replacement.mjs'; + +const valid = [ + '[some-attribute]', + '[data-blank]', + '.class', +]; + +const invalid = [ + '@media', + '#id', + '.some:not(.complex)', + '.some:is(.complex)', + '.some:where(.complex)', + '::before', + '.some .nested', + '.some > .nested', + '.some ~ .sibling', + '.some + .adjacent', +]; + +valid.forEach(selector => assert.ok(isValidReplacement(selector))); +invalid.forEach(selector => assert.ok(!isValidReplacement(selector))); diff --git a/plugins/css-blank-pseudo/test/basic.wrong-replacewith.expect.css b/plugins/css-blank-pseudo/test/basic.wrong-replacewith.expect.css new file mode 100644 index 000000000..bd8a562c9 --- /dev/null +++ b/plugins/css-blank-pseudo/test/basic.wrong-replacewith.expect.css @@ -0,0 +1,35 @@ +:blank { + order: 1; +} + +:blank, +:blank test, +test :blank, +test test:blank, +test :blank test, +test test:blank test, +test :blank :blank test, +test :matches(:blank) test, +test :matches(:blank test) test, +test :matches(test :blank) test, +test :matches(test test:blank) test, +test :matches(test :blank test) test, +test :matches(test test:blank test) test, +test :matches(test :blank :blank test) test { + order: 2; +} + +:ignore-blank, +:blank-ignore, +:ignoreblank, +:blankignore { + order: 3; +} + +:blank(ignore) { + order: 4; +} + +test:not(:blank) { + order: 5; +} diff --git a/plugins/css-blank-pseudo/test/browser.css b/plugins/css-blank-pseudo/test/browser.css new file mode 100644 index 000000000..e10a52399 --- /dev/null +++ b/plugins/css-blank-pseudo/test/browser.css @@ -0,0 +1,9 @@ +input, +select, +textarea { + background-color: rgb(255, 192, 203); +} + +:blank { + background-color: rgb(128, 0, 128); +} diff --git a/plugins/css-blank-pseudo/test/browser.expect.css b/plugins/css-blank-pseudo/test/browser.expect.css new file mode 100644 index 000000000..ade83fb55 --- /dev/null +++ b/plugins/css-blank-pseudo/test/browser.expect.css @@ -0,0 +1,13 @@ +input, +select, +textarea { + background-color: rgb(255, 192, 203); +} + +[blank] { + background-color: rgb(128, 0, 128); +} + +:blank { + background-color: rgb(128, 0, 128); +} diff --git a/plugins/css-blank-pseudo/test/browser.replacewith.expect.css b/plugins/css-blank-pseudo/test/browser.replacewith.expect.css new file mode 100644 index 000000000..9ea7c84de --- /dev/null +++ b/plugins/css-blank-pseudo/test/browser.replacewith.expect.css @@ -0,0 +1,13 @@ +input, +select, +textarea { + background-color: rgb(255, 192, 203); +} + +.css-blank { + background-color: rgb(128, 0, 128); +} + +:blank { + background-color: rgb(128, 0, 128); +} diff --git a/plugins/css-blank-pseudo/test/examples/example.css b/plugins/css-blank-pseudo/test/examples/example.css new file mode 100644 index 000000000..5c3a3113a --- /dev/null +++ b/plugins/css-blank-pseudo/test/examples/example.css @@ -0,0 +1,3 @@ +input:blank { + background-color: yellow; +} diff --git a/plugins/css-blank-pseudo/test/examples/example.expect.css b/plugins/css-blank-pseudo/test/examples/example.expect.css new file mode 100644 index 000000000..df6c00a20 --- /dev/null +++ b/plugins/css-blank-pseudo/test/examples/example.expect.css @@ -0,0 +1,6 @@ +input[blank] { + background-color: yellow; +} +input:blank { + background-color: yellow; +} diff --git a/plugins/css-blank-pseudo/test/examples/example.preserve-false.expect.css b/plugins/css-blank-pseudo/test/examples/example.preserve-false.expect.css new file mode 100644 index 000000000..a3a7b9d30 --- /dev/null +++ b/plugins/css-blank-pseudo/test/examples/example.preserve-false.expect.css @@ -0,0 +1,3 @@ +input[blank] { + background-color: yellow; +} diff --git a/plugins/css-blank-pseudo/test/examples/example.preserve-true.expect.css b/plugins/css-blank-pseudo/test/examples/example.preserve-true.expect.css new file mode 100644 index 000000000..8b020470a --- /dev/null +++ b/plugins/css-blank-pseudo/test/examples/example.preserve-true.expect.css @@ -0,0 +1,8 @@ +.foo { + color: blue; + color: red; +} + +.baz { + color: green; +} diff --git a/plugins/css-blank-pseudo/test/examples/example.replacewith.expect.css b/plugins/css-blank-pseudo/test/examples/example.replacewith.expect.css new file mode 100644 index 000000000..24f2fc6d1 --- /dev/null +++ b/plugins/css-blank-pseudo/test/examples/example.replacewith.expect.css @@ -0,0 +1,6 @@ +input.css-blank { + background-color: yellow; +} +input:blank { + background-color: yellow; +} diff --git a/plugins/css-blank-pseudo/tsconfig.json b/plugins/css-blank-pseudo/tsconfig.json new file mode 100644 index 000000000..e0d06239c --- /dev/null +++ b/plugins/css-blank-pseudo/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +}