├── test-files ├── test.txt ├── cors.css └── cors-with-cq.css ├── .github └── FUNDING.yml ├── .gitignore ├── .tern-project ├── .editorconfig ├── docs ├── index.md ├── browserify.md ├── cors.md ├── installation.md ├── config.md ├── how-it-works.md ├── postcss.md ├── api.md └── usage.md ├── .eslintrc ├── .gitattributes ├── browserstack.json ├── postcss-plugin.js ├── LICENSE ├── mixins.scss ├── package.json ├── README.md ├── .travis.yml ├── postcss-tests.js ├── Makefile ├── tests-functional.js ├── cq-prolyfill.js └── tests.js /test-files/test.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /test-files/cors.css: -------------------------------------------------------------------------------- 1 | .cors-test { 2 | color: red; 3 | width: 10%; 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ausi] 4 | -------------------------------------------------------------------------------- /test-files/cors-with-cq.css: -------------------------------------------------------------------------------- 1 | .cors-test:container(width >= 0px) { 2 | color: blue; 3 | width: 20%; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cq-prolyfill.min.js 2 | /cq-prolyfill.min.js.gz 3 | /cq-prolyfill.tmp.min.js 4 | /node_modules/ 5 | /tests/ 6 | /coverage/ 7 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": [ 3 | "browser" 4 | ], 5 | "plugins": { 6 | "complete_strings": { 7 | "maxLength": 30 8 | }, 9 | "doc_comment": { 10 | "fullDocs": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | tab_width = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * [Installation](installation.md) 4 | * [Usage](usage.md) 5 | * [JavaScript API](api.md) 6 | * [Configuration](config.md) 7 | * [PostCSS plugin](postcss.md) 8 | * [browserify and webpack](browserify.md) 9 | * [Cross origin stylesheets](cors.md) 10 | * [How it works](how-it-works.md) 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "quotes": [2, "single"], 7 | "comma-dangle": [2, "always-multiline"], 8 | "strict": [2, "function"], 9 | "no-console": 1, 10 | "no-use-before-define": [2, "nofunc"], 11 | "no-underscore-dangle": 0, 12 | "no-loop-func": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto whitespace=indent-with-non-tab,tabwidth=4 2 | 3 | *.js text 4 | *.json text 5 | *.md text 6 | *.css text 7 | *.txt text 8 | 9 | Makefile text 10 | 11 | .editorconfig text 12 | .eslintrc text 13 | .gitattributes text 14 | .gitignore text 15 | .tern-project text 16 | .travis.yml text whitespace=-indent-with-non-tab,tab-in-indent 17 | -------------------------------------------------------------------------------- /docs/browserify.md: -------------------------------------------------------------------------------- 1 | # browserify and webpack 2 | 3 | If you want to use the prolyfill with [browserify](http://browserify.org/) or [webpack](https://webpack.github.io/) you can do so by `require`ing the module as usual. [The configuration](config.md) can be passed into the required function and [the API](api.md) gets returned: 4 | 5 | ```js 6 | var cq = require('cq-prolyfill')({ /* configuration */ }); 7 | cq.reevaluate(false, function() { 8 | // Do something after all elements were updated 9 | }); 10 | ``` 11 | -------------------------------------------------------------------------------- /browserstack.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_framework" : "qunit", 3 | "test_path": ["tests/coverage.html", "tests/functional.html"], 4 | "browsers": [ 5 | "chrome_latest", 6 | "firefox_36", 7 | "firefox_latest", 8 | "safari_7_1", 9 | "safari_8", 10 | "safari_latest", 11 | "ie_9", 12 | "ie_10", 13 | "ie_11", 14 | "edge_14", 15 | "edge_latest", 16 | "opera_30", 17 | "opera_latest", 18 | { 19 | "os": "ios", 20 | "os_version": "8.3" 21 | }, 22 | { 23 | "os": "ios", 24 | "os_version": "9.3" 25 | }, 26 | { 27 | "os":"winphone", 28 | "os_version":"8.1" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/cors.md: -------------------------------------------------------------------------------- 1 | # Cross origin stylesheets 2 | 3 | In order to work properly the prolyfill needs access to the stylesheets on the page. If a stylesheet is served from a different domain it needs to have a CORS header, e.g. `Access-Control-Allow-Origin: *`. You can find more information about how to enable CORS on your server at [enable-cors.org](http://enable-cors.org/server.html). 4 | 5 | For better performance it’s also recommended to use the attribute `crossorigin="anonymous"` for cross origin `` tags: 6 | 7 | ```html 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /postcss-plugin.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | var postcss = require('postcss'); 3 | 4 | module.exports = postcss.plugin('cq-prolyfill', function () { 5 | 'use strict'; 6 | return function (css) { 7 | css.walkRules(/:container\(/i, function (rule) { 8 | rule.selectors = rule.selectors.map(function(selector) { 9 | return selector.replace(/:container\((?:[^()]+|\([^()]*\))+\)/gi, function(match) { 10 | return '.' + match 11 | .replace(/\s+/g, '') 12 | .replace(/^:container\("((?:[^()]+|\([^()]*\))+)"\)$/i, ':container($1)') 13 | .replace(/[[\]!"#$%&'()*+,./:;<=>?@^`{|}~]/g, '\\$&') 14 | .toLowerCase(); 15 | }); 16 | }); 17 | }); 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | You can install the prolyfill by [downloading it from GitHub](#download-from-github) or [via npm](#install-via-npm). After installing you can use the container queries as described in [Usage](usage.md). 4 | 5 | ## Download from GitHub 6 | 7 | Download the file *cq-prolyfill.min.js* from the [latest release on GitHub](https://github.com/ausi/cq-prolyfill/releases/latest). 8 | 9 | ## Install via npm 10 | 11 | To install it via [npm](https://www.npmjs.com/package/cq-prolyfill) execute the following command in your project directory: 12 | 13 | ```bash 14 | npm install --save cq-prolyfill 15 | ``` 16 | 17 | After the installation completes you can find the prolyfill at *node_modules/cq-prolyfill/cq-prolyfill.min.js* or you load it via [browserify or webpack](browserify.md). 18 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | * `preprocess` should be set to `true` if the [PostCSS plugin](postcss.md) is not used and the CSS code has to be preprocessed client side. 4 | * `skipObserving` set this to `true` if the prolyfill shouldn’t listen to browser events and DOM modifications and you want to manage it yourself via the [API methods](api.md). 5 | 6 | ## Normal script 7 | 8 | If you installed the prolyfill as a normal script, the configuration can be set via `window.cqConfig`: 9 | 10 | ```html 11 | 17 | 18 | ``` 19 | 20 | ## Module loader 21 | 22 | If you’re using a module loader and [browserify or webpack](browserify.md), pass the configuration as the first parameter to the module function. 23 | 24 | ```js 25 | // Pass the configuration as a parameter 26 | var cq = require('cq-prolyfill')({ 27 | preprocess: true 28 | }); 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2015 Martin Auswöger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/how-it-works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | It basically runs in three steps: 4 | 5 | ## Step 1 6 | 7 | If the `preprocess` configuration is set, it looks for stylesheets that contain container queries and escapes them to be readable by the browser. 8 | 9 | E.g. this: 10 | 11 | ```css 12 | .element:container(width >= 10px) { 13 | color: red; 14 | } 15 | ``` 16 | 17 | gets converted to this: 18 | 19 | ```css 20 | .element.\:container\(width\>\=10px\) { 21 | color: red; 22 | } 23 | ``` 24 | 25 | This step should be done by the [PostCSS plugin](postcss.md) on the server side to speed up the script. Client side processing is meant for demonstration purposes. 26 | 27 | ## Step 2 28 | 29 | Parses all (pre)processed container query rules and stores them indexed by the preceding selector to be used in step 3. 30 | 31 | ## Step 3 32 | 33 | Loops through all stored queries and adds or removes the CSS classes of the matching elements. The added CSS classes look the same as the container query itself to improve the readability in the developer tools of the browser. E.g.: 34 | 35 | ```html 36 |
37 | ``` 38 | -------------------------------------------------------------------------------- /mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin cq-prolyfill($query) { 2 | &#{cq-prolyfill($query)} { 3 | @content; 4 | } 5 | } 6 | 7 | @function cq-prolyfill($query) { 8 | @return unquote(".\\:container\\(" + cq-prolyfill-escape(cq-prolyfill-strip-spaces(to-lower-case($query))) + "\\)"); 9 | } 10 | 11 | @function cq-prolyfill-add-backslash($string, $search) { 12 | $index: str-index($string, $search); 13 | @while $index { 14 | $string: str-insert($string, '\\', $index); 15 | $newIndex: if( 16 | str-length($string) < $index + 2, 17 | null, 18 | str-index(str-slice($string, $index + 2), $search) 19 | ); 20 | $index: if($newIndex, $index + 1 + $newIndex, null); 21 | } 22 | @return $string; 23 | } 24 | 25 | @function cq-prolyfill-remove($string, $search) { 26 | $index: str-index($string, $search); 27 | @while $index { 28 | $string: str-slice($string, 1, $index - 1) + str-slice($string, $index + 1); 29 | $index: str-index($string, $search); 30 | } 31 | @return $string; 32 | } 33 | 34 | @function cq-prolyfill-escape($string) { 35 | @each $char in '[' ']' '!' '"' '#' '$' '%' '&' "'" '(' ')' '*' '+' ',' '.' '/' ':' ';' '<' '=' '>' '?' '@' '^' '`' '{' '|' '}' '~' { 36 | $string: cq-prolyfill-add-backslash($string, $char); 37 | } 38 | @return $string; 39 | } 40 | 41 | @function cq-prolyfill-strip-spaces($string) { 42 | // tab, line feed, carriage return and space 43 | $chars: "\9\a\d\20"; 44 | @for $i from 1 through str-length($chars) { 45 | $string: cq-prolyfill-remove( 46 | $string, 47 | str-slice($chars, $i, $i) 48 | ); 49 | } 50 | @return $string; 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cq-prolyfill", 3 | "version": "0.4.0", 4 | "description": "Prolyfill for CSS Container Queries (aka Element Queries)", 5 | "license": "MIT", 6 | "keywords": [ 7 | "CSS", 8 | "css", 9 | "RWD", 10 | "rwd", 11 | "Container Queries", 12 | "container-queries", 13 | "Container Query", 14 | "container-query", 15 | "Element Queries", 16 | "element-queries", 17 | "Element Query", 18 | "element-query", 19 | "Prolyfill", 20 | "prolyfill", 21 | "Prollyfill", 22 | "prollyfill", 23 | "Polyfill", 24 | "polyfill", 25 | "Browser", 26 | "browser", 27 | "postcss", 28 | "postcss-plugin" 29 | ], 30 | "scripts": { 31 | "test": "make test" 32 | }, 33 | "main": "cq-prolyfill.js", 34 | "browser": "cq-prolyfill.js", 35 | "files": [ 36 | "cq-prolyfill.js", 37 | "cq-prolyfill.min.js", 38 | "cq-prolyfill.min.js.gz", 39 | "postcss-plugin.js", 40 | "docs" 41 | ], 42 | "homepage": "https://github.com/ausi/cq-prolyfill", 43 | "repository": "ausi/cq-prolyfill", 44 | "bugs": { 45 | "url": "https://github.com/ausi/cq-prolyfill/issues" 46 | }, 47 | "dependencies": { 48 | "postcss": "^5.0.2" 49 | }, 50 | "devDependencies": { 51 | "browserstack-runner": "^0.5.0", 52 | "connect": "^3.4.0", 53 | "coveralls": "^2.11.4", 54 | "eslint": "^1.10.3", 55 | "istanbul": "^0.4.1", 56 | "node-sass": "^3.4.2", 57 | "node-zopfli": "^1.4.0", 58 | "qunit-phantomjs-runner": "^2.1.0", 59 | "qunitjs": "^1.18.0", 60 | "serve-static": "^1.10.0", 61 | "slimerjs": "^0.906.2", 62 | "uglify-js": "^2.4.24" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/postcss.md: -------------------------------------------------------------------------------- 1 | # PostCSS plugin 2 | 3 | To improve the performance of the prolyfill, you should use [PostCSS](https://github.com/postcss/postcss) to prepare the stylesheet on the server side: 4 | 5 | ```js 6 | var fs = require('fs'); 7 | var cqPostcss = require('cq-prolyfill/postcss-plugin'); 8 | 9 | fs.writeFileSync( 10 | 'dist.css', 11 | cqPostcss.process(fs.readFileSync('source.css', 'utf-8')).css 12 | ); 13 | ``` 14 | 15 | This converts container queries like: 16 | 17 | ```css 18 | .element:container(width >= 100px) { /* ... */ } 19 | ``` 20 | 21 | Into valid CSS selectors: 22 | 23 | ```css 24 | .element.\:container\(width\>\=100px\) { /* ... */ } 25 | ``` 26 | 27 | If you don’t use the PostCSS plugin, you can use the supplied [Sass mixin](usage.md#sass-less-and-other-preprocessors) instead or activate the `preprocess` option in the [configuration](config.md). 28 | 29 | Don’t forget to [enable CORS](cors.md) if the stylesheet is loaded from a different domain. 30 | 31 | ## Build systems 32 | 33 | If you’re using a build system like grunt or gulp you can integrate the PostCSS plugin in this process. 34 | 35 | ### Grunt 36 | 37 | ```js 38 | grunt.loadNpmTasks('grunt-postcss'); 39 | grunt.initConfig({ 40 | postcss: { 41 | options: { 42 | processors: [ 43 | require('cq-prolyfill/postcss-plugin')() 44 | ] 45 | }, 46 | dist: { 47 | src: 'css/*.css' 48 | } 49 | } 50 | }); 51 | ``` 52 | 53 | ### Gulp 54 | 55 | ```js 56 | var postcss = require('gulp-postcss'); 57 | gulp.task('css', function () { 58 | return gulp.src('./src/*.css') 59 | .pipe(postcss([ 60 | require('cq-prolyfill/postcss-plugin')() 61 | ])) 62 | .pipe(gulp.dest('./dest')); 63 | }); 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # JavaScript API 2 | 3 | The script triggers itself on load, on DOM ready, when the browser window resizes and if new elements are added to the DOM. If you want to trigger it manually you can call `reprocess` (step 1), `reparse` (step 2) or `reevaluate` (step 3) on the `window.cqApi` object. Most of the time `reevaluate` should do the job if you didn’t add, remove or change stylesheets. E.g. 4 | 5 | ```js 6 | document.querySelector('.element').addEventListener('click', function() { 7 | // Do something that changes the size of container elements 8 | // ... 9 | window.cqApi.reevaluate(false, function() { 10 | // Do something after all elements were updated 11 | }); 12 | }); 13 | ``` 14 | 15 | If you installed the prolyfill as a normal script the API is available at `window.cqApi`. If you’re using a module loader the API gets returned from the module function. 16 | 17 | ## `reprocess(fn callback)` 18 | 19 | Reprocess all stylesheets on the page. Call this method if you added a stylesheet via JavaScript. The `callback` gets called after all stylesheets are processed, parsed and evaluated. 20 | 21 | ## `reparse(fn callback)` 22 | 23 | Reparse all stylesheets on the page and look for new container queries. Call this method if you added a stylesheet via JavaScript which doesn’t contain a container query. The `callback` gets called after all stylesheets are parsed and evaluated. 24 | 25 | ## `reevaluate(bool clearCache, fn callback, array contexts)` 26 | 27 | Reevaluate all container queries. Call this method if you added new elements or changed styles that affect a container query. The boolean parameter `clearCache` specifies if the container cache should be cleared before the evaluation. The `callback` gets called after all container queries are evaluated. You can optionally pass an array of DOM elements as `contexts` if you only want to reevaluate some parts of the page. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Container Queries Prolyfill 2 | 3 | [![Build Status](https://img.shields.io/travis/ausi/cq-prolyfill/master.svg?style=flat-square)](https://travis-ci.org/ausi/cq-prolyfill/branches) [![Coverage](https://img.shields.io/coveralls/ausi/cq-prolyfill/master.svg?style=flat-square)](https://coveralls.io/github/ausi/cq-prolyfill?branch=master) [![npm version](https://img.shields.io/npm/v/cq-prolyfill.svg?style=flat-square) ![npm downloads](https://img.shields.io/npm/dt/cq-prolyfill.svg?style=flat-square)](https://www.npmjs.com/package/cq-prolyfill) ![MIT](https://img.shields.io/npm/l/cq-prolyfill.svg?style=flat-square) 4 | 5 | This is a [prolyfill](https://au.si/what-is-a-prolyfill) for a special version of [container queries](https://github.com/ResponsiveImagesCG/container-queries) (aka element queries). You can read more about the idea and how they work internally in [this article](https://au.si/css-container-element-queries). 6 | 7 | ## Demo 8 | 9 | A quick demo of the container queries in action can be found here: 10 | 11 | 12 | ## Usage 13 | 14 | With this prolyfill you can use container queries in your CSS in the following form: 15 | 16 | ```css 17 | .element:container(min-width: 100px) { 18 | /* Styles for .element if its container is at least 100px wide */ 19 | } 20 | .element[data-cq~="min-width:100px"] { 21 | /* Alternative syntax, same as the container query above */ 22 | } 23 | .element:container(text-align = right) { 24 | /* Styles for .element if its container has a right text-align */ 25 | } 26 | ``` 27 | 28 | For more information take a look at the [usage documentation](docs/usage.md). 29 | 30 | ## Documentation 31 | 32 | [Read the documentation](docs/index.md) to see how you can install and use this script on your next project. 33 | 34 | ## Browser Support 35 | 36 | * Firefox 36+ 37 | * Opera 12.16+ 38 | * Chrome 40+ 39 | * Internet Explorer 9+ 40 | * Edge 41 | * Safari 7+ 42 | * Yandex 14+ 43 | * iOS 7+ 44 | * Android 4+ 45 | * Windows Phone 8.1+ 46 | 47 | Thanks to [BrowserStack](https://www.browserstack.com/automate) for sponsoring automated cross browser testing for this project. 48 | 49 | ## Contribute 50 | 51 | * Create a [new issue on GitHub](https://github.com/ausi/cq-prolyfill/issues/new) if you have a question, a suggestion or found a bug. 52 | * Talk about it on IRC: Join `#cq-prolyfill` on Freenode or [connect with the browser](https://webchat.freenode.net?randomnick=1&channels=%23cq-prolyfill&prompt=1). 53 | * Spread the word about this project. 54 | * [Support this project on Patreon](https://www.patreon.com/ausi). 55 | 56 | ## Sponsors 57 | 58 | Thanks to all sponsors that help to bring this project forward. You can [become a sponsor now](https://www.patreon.com/ausi) too. 59 | 60 | * [Webflow](https://webflow.com/) 61 | * [BrowserStack](https://www.browserstack.com/) 62 | 63 | ## License 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "node" 3 | addons: 4 | apt: 5 | sources: 6 | - ubuntu-toolchain-r-test 7 | packages: 8 | - g++-4.8 9 | before_script: 10 | - "export DISPLAY=:99.0" 11 | - "sh -e /etc/init.d/xvfb start" 12 | script: 13 | - make test && ( [ "$TRAVIS_SECURE_ENV_VARS" != "true" ] || ( make browserstack && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls ) ) 14 | env: 15 | global: 16 | - CXX=g++-4.8 17 | - secure: bUyIN5cHVHRnihJw4z84nKhpp8LRy1WwqdI1n134HHZ1UnS52t0XfNRiO0iwmJmyT5TeHXqNEKN6IpvWUAdM8his2bvoAurNdoH3BELFXWz66esw/dNpl9pUy/mEgTsLnzCIIUcvIdVz6iSeH4aiBzImbuKS9JT2zzLZaSCpoKeNitRTHpuYd1wSaNmaNkdB6u5oy+YV1snzJuFQ4JetezmfZy4pEYrPYdxwh7YTigqQhRchoBvhHVRtyIxRjp9GNl5cITtYOejfMa2zuj6ISoDg04QslS3XQVX6MEUmTzlQBwZYBWFIbfOzkgdxk3wVXwi1QsgQEKN4781VJ3PrYpgxzCSRRpHfftCoWnhlunS953KGjqcf85MhcF/jZpKpM6LT5j2b52c2VJvSR2H2G/5NTGCBEdM+NUlQblHzqfGl/Iz0fYAhHuEDq3KiOPt7WCCfcu4YeLcih2MGsllnRJx4lrTisVtMTk1EZMaqnH0ChHbPX/Yo34VWoD0ByfGBhEFtgR5QbOYOTXldjq2BZKKEi1uA/bKA0Ey4Q9Q0xCfPc1VfD/8cAQooHTA5ta8C3w1/sxr7jNRX72c9C/JBL532BI0Gdng8PAl9Qvgj6xSmmPcXK5KWfmtAo/BIVwUtswk6WsmnHI7kfvAQv5CnpH9Azup9fO0jlCLCcjbhIH8= 18 | - secure: smI7rQrcoYErPOTruGINXzo8BHzlcevTY9GXdq7cbvfSkXmGYVqAQ/Vy2KjAe36iIlifsOKuvMlFQNQgghGHaZkO2Ol4hSfVu8UMKmQ8mEi+E5LSSwIY4NEPDrwVADXuzZjow329riutvuauEifJn+WNLDey/bFbBdKKIW4hYK9kiXvMwOlDmRwDMgZzw/IJgPCrmT2XKqDGofSnP75NMRj71HrctsxH2INHKFbGRvwIcCEUH6T3ifhBXdQaRPkcxfgvrvsUjxW8PiUlQI+TswgbTVsT/1UtejHcr1y5DYOj8e6BEbSOC+/KPrsj/YuCSgRiV4Qy+Ab2pM32kCKYpJJMPRUhzNnd9uauV2NXqLSRyPmCn41VFZnQlgSNc6DhWxDOuEg+cokcS5p0i1PpMxiaRZVtEYjgKUi0ETKotr+c+WJWSFQN3SWySGjQ0NiUvTWhrChEBbWQNVgRhF9jMNQQoEYWSlppo4ek5S7VyLPRRl0cz88zPdcKKKpmuktZxY7CQ/Bk3zcEz6EhaDaWE1YBuLeZAKtdlgEZ9zFhMFU6YZso5x1FhpX1xbwWuulG/HycZD44KhB2kDDCgTJ8BGDxkneOsTLMUAsXZbs8iYMLwRfwuSlmrQNKrlrfcSIpEVqDd2y8j3PSfXk+pjaLtJ2wfvT542FpHqxcvVwF8Ig= 19 | before_deploy: 20 | - make cq-prolyfill.min.js 21 | - make cq-prolyfill.min.js.gz 22 | deploy: 23 | - 24 | provider: releases 25 | api_key: 26 | secure: j0eUc5q6S3/P/iL36xhw6MYFrRP9986dv9WGwF1o9HlmmtkTR0ts7zhvY8dQK5Pp2LAaYCT1HokNCfvu9FDoNTIpn5o9mFLXbe7pBbFX2wg2QKc5iN5x19TOiMPLfQhatgaJ1x7Cbp/kANbR8ZtCjfrEQJZDI0IR68DdD6FH1Uq87a3F3AgkzqD5YCZu4G17ZpLophZIPFKk2SUcE6Kmdk9JW0lo+D/ubP4OwXL131CrS7M1KU1yChTHmquplwQ0lTe7n6+mOU/Xhi8NihXj0h2TzKrw9DRBEHi5e4RZbwJ2/jv86LcjTRO23jkX0i8XKQgqzHCbFfwfK6ExGh5ihqJpVTF8o1wdQZpXX6Tf7ALnqkK5o3V2j474B26qvHNOClB4lThfDaWJtp+/8y5vjE5okSHO95ZlCPt7FzugsgigfyC+M/b514BbfXL2MhZUpKsEjdLuPkAR2YJVs1Rbaw1GOuLlSI+jKWiU+uUDH+e1uuTc68gfzM/UJ+8tcB6Z38c3f4a7yKaF6SjJDPJgHPkpoCV6DXFUPgkn+UypWGkN/Vs9k/3+y1qaPIF3AN5v+2Rcn0u/FW6wa0SRqXF0g53mGNZRcIAKhz1/XQpYWyE6p9Y5eRSIVD73iLkHCr7uR5Oxs9blSqAsCn1e4XLmienUhiXQcy804raP15ZWQxk= 27 | file: 28 | - cq-prolyfill.js 29 | - cq-prolyfill.min.js 30 | - cq-prolyfill.min.js.gz 31 | skip_cleanup: true 32 | on: 33 | tags: true 34 | - 35 | provider: npm 36 | email: martin@auswoeger.com 37 | api_key: 38 | secure: ljq7zGzA0NJx5ykcDtLf1g+KaCeMmTzFvcWbKRdYw51JzSIxKdJv/C45ovVnUUPBrnI1oNLW5qk5IO3wh7obW+2OsWafHU/okG/ZWmword48IomnvZmIAjMF6GwJ5ZT2EU44wCSC7zZrMMFszPqyoKi45DVKTxthw2IztU0klRvzXlP4AZ1vGR8zYCGOG3rR6iLe4HY4ZNNpRFBj+ONC7VBZERQ5DsZZDcMXcZpnDTEvbn6HCycpR7N5NJgcCfTLm1KWW8yQZNVAaEtuaCpw0Z1I5E3TSZFfl2xV3ES5Kg6qSi18M/EkZ9aXPoU3JrAMiTGTH0R+6PSe7E0GdB+PBLZ8smk6/nAbHdd+BlsvEj/B70l8vWlwo+0giaZSQ0wb8g9KbQClP9txaCCjMF2Z9vFPlXtokIjwhQfGQ46xg7DjEkURoVVtlPxhPdiZ4D1MnkasMhuJGLfidVGkgcMLQxc2CG4xdQm9nIH3izWTYOvPVK0SPSt+pRSLIHnDeswAg2EwMEhneYdyOo0Yjs7I82fppFdJmZ9jbyeskLdn9u5dQh15CN7TK4e6JJNBPADs8bJiWSuKOHXRnfRqXz0mEFbHQ+YZcojINyVDXmhqxI/xvRu9vCeAp9jvm+l8CsPvs/735v+HswFZkiqo0acLjOVqNsH9/e3wXETLcAWgZYE= 39 | skip_cleanup: true 40 | on: 41 | tags: true 42 | -------------------------------------------------------------------------------- /postcss-tests.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | /*eslint-disable no-process-exit,strict*/ 3 | 4 | var postcssPlugin = require('./postcss-plugin'); 5 | var sass = require('node-sass'); 6 | 7 | var data = { 8 | ':container( MIN-WIDTH : 100.00px )': '.\\:container\\(min-width\\:100\\.00px\\)', 9 | ':container( 10% < background-color-lightness < 90% )': '.\\:container\\(10\\%\\ 100px).after': '.before.\\:container\\(height\\>100px\\).after', 11 | '.combined-selector:container(width > 100px):container(height > 100px)': '.combined-selector.\\:container\\(width\\>100px\\).\\:container\\(height\\>100px\\)', 12 | ':container( " width <= 100.00px")': '.\\:container\\(width\\<\\=100\\.00px\\)', 13 | ':container(color: rgba(255, 0, 0, 1))': '.\\:container\\(color\\:rgba\\(255\\,0\\,0\\,1\\)\\)', 14 | }; 15 | 16 | var dataScss = { 17 | '.foo#{cq-prolyfill("width >= 100.00px")} { color: red }': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 18 | '.foo { @include cq-prolyfill("width >= 100.00px") { color: red } }': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 19 | '@function container($query) { @return cq-prolyfill($query) } .foo#{container("width >= 100.00px")} { color: red }': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 20 | '@mixin container($query) { @include cq-prolyfill($query) { @content } } .foo { @include container("width >= 100.00px") { color: red } }': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 21 | }; 22 | 23 | var dataSass = { 24 | '.foo#{cq-prolyfill("width >= 100.00px")}\n\tcolor: red': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 25 | '.foo\n\t+cq-prolyfill("width >= 100.00px")\n\t\tcolor: red': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 26 | '@function container($query)\n\t@return cq-prolyfill($query)\n.foo#{container("width >= 100.00px")}\n\tcolor: red': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 27 | '=container($query)\n\t+cq-prolyfill($query)\n\t\t@content\n.foo\n\t+container("width >= 100.00px")\n\t\tcolor: red': '.foo.\\:container\\(width\\>\\=100\\.00px\\){color:red}', 28 | }; 29 | 30 | var failed = []; 31 | 32 | Object.keys(data).forEach(function(selector) { 33 | 34 | var expected = data[selector] + '{}'; 35 | var processed = postcssPlugin.process(selector + '{}').css; 36 | if (processed !== expected) { 37 | failed.push('Failed that "' + processed + '" equals "' + expected + '"'); 38 | } 39 | 40 | expected = data[selector] + '{color:red}'; 41 | selector = '@import "mixins.scss"; ' + selector.replace(/:container\(\s*"*((?:[^()]+?|\([^()]*\))+?)"*\s*\)/g, '#{cq-prolyfill("$1")}') + '{color:red}'; 42 | processed = (sass.renderSync({ 43 | data: selector, 44 | outputStyle: 'compressed', 45 | }).css + '').trim(); 46 | if (processed !== expected) { 47 | failed.push('Failed that "' + processed + '" equals "' + expected + '"'); 48 | } 49 | 50 | }); 51 | 52 | Object.keys(dataScss).forEach(function(css) { 53 | 54 | var expected = dataScss[css]; 55 | css = '@import "mixins.scss"; ' + css; 56 | processed = (sass.renderSync({ 57 | data: css, 58 | outputStyle: 'compressed', 59 | }).css + '').trim(); 60 | if (processed !== expected) { 61 | failed.push('Failed that "' + processed + '" equals "' + expected + '"'); 62 | } 63 | 64 | }); 65 | 66 | Object.keys(dataSass).forEach(function(css) { 67 | 68 | var expected = dataSass[css]; 69 | css = '@import "mixins.scss"\n' + css; 70 | processed = (sass.renderSync({ 71 | data: css, 72 | indentedSyntax: true, 73 | outputStyle: 'compressed', 74 | }).css + '').trim(); 75 | if (processed !== expected) { 76 | failed.push('Failed that "' + processed + '" equals "' + expected + '"'); 77 | } 78 | 79 | }); 80 | 81 | if (failed.length) { 82 | /*eslint-disable no-console*/ 83 | console.log(failed.join('\n')); 84 | console.log('PostCSS tests failed'); 85 | /*eslint-enable no-console*/ 86 | process.exit(1); 87 | } 88 | else { 89 | /*eslint-disable no-console*/ 90 | console.log('PostCSS tests: ' + Object.keys(data).length + ' passed'); 91 | /*eslint-enable no-console*/ 92 | } 93 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | One goal of this prolyfill is to be ease to use. Elements with container queries don’t have to be modified in the markup, you don’t have to predefine “element break-points” or the like, everything is done via CSS. All you need is to load the script on your page, enable the [PostCSS plugin](postcss.md) or [Sass mixin](#sass-less-and-other-preprocessors) and you are ready to use container queries in your CSS. You can load the script in any way you like, I would recommend to load it asynchronously in the head: 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | Now you can use container queries in the following form: 10 | 11 | ```css 12 | .element:container(min-width: 100px) { 13 | /* Styles for .element if its container is at least 100px wide */ 14 | } 15 | .element:container(100px < height < 200px) { 16 | /* Styles for .element if its container is between 100px and 200px high */ 17 | } 18 | .element:container(text-align = right) { 19 | /* Styles for .element if its container has a right text-align */ 20 | } 21 | ``` 22 | 23 | If you don’t want to use a preprocessor you can use attribute selectors instead: 24 | 25 | ```css 26 | .element[data-cq~="min-width:100px"] { 27 | /* Styles for .element if its container is at least 100px wide */ 28 | } 29 | ``` 30 | 31 | ## Syntax 32 | 33 | A container query begins with `:container(` and ends with `)`. It contains a single query against a CSS property and can be suffixed by an [optional filter](#colors). The syntax of the query follows the [Media Queries Level 4](https://www.w3.org/TR/2017/WD-mediaqueries-4-20170519/#mq-features) syntax for media features including the range form. The container query is attached to the element you want to style. So instead of writing `.parent:media(min-with: 100px) .child` like in other element query scripts, you append the query to the child itself `.child:container(width > 100px)`. 34 | 35 | The alternative attribute selector syntax begins with `[data-cq~="` and ends with `"]`. It does not allow space characters in the query part, so `[data-cq~="width > 100px"]` is invalid while `[data-cq~="width>100px"]` works fine. 36 | 37 | ### Sass, Less and other preprocessors 38 | 39 | If your CSS preprocessor has problems with the container query syntax, you can put quotes around the comparison like so: 40 | 41 | ```css 42 | .element:container("width >= 100px") { 43 | /* Styles for .element if its container is at least 100px wide */ 44 | } 45 | ``` 46 | 47 | For Sass and SCSS this library includes a [mixins.scss](../mixins.scss) file that can be used as an alternative to the [PostCSS plugin](postcss.md): 48 | 49 | ```scss 50 | @import "node_modules/cq-prolyfill/mixins.scss"; 51 | .element { 52 | @include cq-prolyfill("width >= 100px") { 53 | /* Styles for .element if its container is at least 100px wide */ 54 | } 55 | } 56 | ``` 57 | 58 | ## Supported CSS properties 59 | 60 | Technically all CSS properties are supported, but that doesn’t mean you should use them all. The following properties are most useful and tested: `width`, `height`, `background-color`, `color`, `text-align`, `font-size` and [custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables). 61 | 62 | ## Comparison operators 63 | 64 | Available comparison operators are: 65 | 66 | * `<` less than 67 | * `>` greater than 68 | * `<=` less than or equal 69 | * `>=` greater than or equal 70 | * `=` equal 71 | 72 | For more details about the comparison syntax take a look at the [Media Queries Level 4 Specification](https://www.w3.org/TR/2017/WD-mediaqueries-4-20170519/#mq-features). 73 | 74 | ## Colors 75 | 76 | If you want to query for a specific color you can use the `rgb()` or `rgba()` notation. 77 | 78 | ```css 79 | .element:container(color: rgb(0, 0, 0)) { 80 | /* Styles for .element if its containers color is black */ 81 | } 82 | .element:container(background-color = rgba(255, 255, 255, 0.5)) { 83 | /* Styles for .element if its containers background-color is 50% transparent white */ 84 | } 85 | ``` 86 | 87 | It’s also possible to query color properties, for this purpose the color filters `hue`, `saturation`, `lightness` and `alpha` are available. 88 | 89 | ```css 90 | .element:container(background-color-lightness > 20%) { 91 | /* Styles for .element if its containers background-color is brighter than 20% */ 92 | } 93 | .element:container(60deg < background-color-hue < 180deg) { 94 | /* Styles for .element if its containers background-color is greenish */ 95 | } 96 | .element:container(max-background-color-alpha: 10%) { 97 | /* Styles for .element if its containers background-color is nearly transparent */ 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULES = node_modules 2 | BIN = $(MODULES)/.bin 3 | UGLIFY = $(BIN)/uglifyjs 4 | UGLIFY_OPTS = --compress=unsafe,pure_getters --mangle --mangle-props --mangle-regex="/^_/" --screw-ie8 5 | ESLINT = $(BIN)/eslint 6 | ISTANBUL = $(BIN)/istanbul 7 | ZOPFLI = $(BIN)/zopfli 8 | SOURCE = cq-prolyfill.js 9 | TARGET = $(SOURCE:%.js=%.min.js) 10 | TARGET_GZ = $(SOURCE:%.js=%.min.js.gz) 11 | TARGET_TMP = $(SOURCE:%.js=%.tmp.min.js) 12 | TESTS = tests.js 13 | TESTS_FUNCTIONAL = tests-functional.js 14 | QUNIT = $(MODULES)/qunitjs/qunit 15 | QUNIT_JS = $(QUNIT)/qunit.js 16 | QUNIT_CSS = $(QUNIT)/qunit.css 17 | TEST_HTML_ALL = tests/all.html 18 | TEST_HTML_COVERAGE = tests/coverage.html 19 | TEST_HTML_FUNCTIONAL = tests/functional.html 20 | TEST_POSTCSS = postcss-tests.js 21 | SLIMERJS = $(BIN)/slimerjs 22 | PHANTOMJS_RUNNER = $(MODULES)/qunit-phantomjs-runner/runner.js 23 | TEST_RUNNER = tests/slimerjs-runner.js 24 | BROWSERSTACK_RUNNER = $(BIN)/browserstack-runner 25 | 26 | all: $(TARGET) $(TARGET_GZ) 27 | 28 | $(TARGET): $(TARGET_TMP) 29 | make test 30 | cat $< > $@ 31 | 32 | $(TARGET_GZ): $(TARGET) $(ZOPFLI) 33 | rm -f $@ 34 | $(ZOPFLI) $< 35 | 36 | $(TARGET_TMP): $(SOURCE) $(UGLIFY) $(TESTS) $(TESTS_FUNCTIONAL) 37 | $(UGLIFY) $(UGLIFY_OPTS) $< > $@ 38 | 39 | $(MODULES): package.json 40 | npm install && touch $@ 41 | 42 | $(UGLIFY): $(MODULES) 43 | touch $@ 44 | 45 | $(ESLINT): $(MODULES) 46 | touch $@ 47 | 48 | $(ISTANBUL): $(MODULES) 49 | touch $@ 50 | 51 | $(ZOPFLI): $(MODULES) 52 | touch $@ 53 | 54 | $(QUNIT_JS): $(MODULES) 55 | touch $@ 56 | 57 | $(QUNIT_CSS): $(MODULES) 58 | touch $@ 59 | 60 | $(SLIMERJS): $(MODULES) 61 | touch $@ 62 | 63 | $(PHANTOMJS_RUNNER): $(MODULES) 64 | touch $@ 65 | 66 | $(BROWSERSTACK_RUNNER): $(MODULES) 67 | touch $@ 68 | 69 | .PHONY: test 70 | test: $(ESLINT) $(SOURCE) $(TEST_POSTCSS) $(TEST_RUNNER) $(SLIMERJS) $(TEST_HTML_ALL) $(TEST_HTML_COVERAGE) $(TEST_HTML_FUNCTIONAL) $(MODULES) 71 | $(ESLINT) $(SOURCE) 72 | node $(TEST_POSTCSS) 73 | node -e "require('connect')().use(require('serve-static')(__dirname)).listen(8888)" & echo "$$!" > server.pid 74 | node -e "require('connect')().use('/cors', require('serve-static')(__dirname, {setHeaders: corsHeaders})).use('/time', function(req, res){corsHeaders(res);res.end((new Date()).getTime()+'')}).listen(8889);function corsHeaders(res) {res.setHeader('Access-Control-Allow-Origin', '*')}" & echo "$$!" > server2.pid 75 | $(SLIMERJS) $(TEST_RUNNER) http://localhost:8888/$(TEST_HTML_ALL) 20 | tee tests/slimerjs.log 76 | kill `cat server.pid` && rm server.pid 77 | kill `cat server2.pid` && rm server2.pid 78 | @ grep ' passed, 0 failed.' tests/slimerjs.log > /dev/null 79 | @ rm tests/slimerjs.log 80 | node -e "require('connect')().use(require('serve-static')(__dirname)).listen(8888)" & echo "$$!" > server.pid 81 | $(SLIMERJS) $(TEST_RUNNER) http://localhost:8888/$(TEST_HTML_FUNCTIONAL) 20 | tee tests/slimerjs.log 82 | kill `cat server.pid` && rm server.pid 83 | @ grep ' passed, 0 failed.' tests/slimerjs.log > /dev/null 84 | @ rm tests/slimerjs.log 85 | 86 | .PHONY: watch 87 | watch: 88 | while true; do (make || make -t) | grep -v "Nothing to be done"; sleep 1; done 89 | 90 | $(TEST_HTML_ALL): $(TESTS) $(TESTS_FUNCTIONAL) $(SOURCE) $(QUNIT_JS) $(QUNIT_CSS) 91 | mkdir -p tests 92 | echo '' > $@ 93 | echo '' >> $@ 94 | echo '' >> $@ 95 | echo '' >> $@ 96 | echo '' >> $@ 97 | echo '
' >> $@ 98 | echo '
' >> $@ 99 | echo '' >> $@ 100 | echo '' >> $@ 107 | echo '' >> $@ 108 | rm -rf tests/test-files 109 | cp -r test-files tests/ 110 | 111 | $(TEST_HTML_COVERAGE): $(TESTS) $(TESTS_FUNCTIONAL) $(SOURCE) $(QUNIT_JS) $(QUNIT_CSS) $(ISTANBUL) 112 | mkdir -p tests 113 | echo '' > $@ 114 | echo '' >> $@ 115 | echo '' >> $@ 116 | echo '' >> $@ 117 | echo '' >> $@ 118 | echo '
' >> $@ 119 | echo '
' >> $@ 120 | echo '' >> $@ 121 | echo '' >> $@ 128 | echo '' >> $@ 129 | rm -rf tests/test-files 130 | cp -r test-files tests/ 131 | 132 | $(TEST_HTML_FUNCTIONAL): $(TESTS_FUNCTIONAL) $(TARGET_TMP) $(QUNIT_JS) $(QUNIT_CSS) 133 | mkdir -p tests 134 | echo '' > $@ 135 | echo '' >> $@ 136 | echo '' >> $@ 137 | echo '' >> $@ 138 | echo '' >> $@ 139 | echo '
' >> $@ 140 | echo '
' >> $@ 141 | echo '' >> $@ 142 | echo '' >> $@ 145 | echo '' >> $@ 146 | echo '' >> $@ 147 | echo '' >> $@ 148 | rm -rf tests/test-files 149 | cp -r test-files tests/ 150 | 151 | $(TEST_RUNNER): $(PHANTOMJS_RUNNER) 152 | mkdir -p tests 153 | cat $< | replace 'exit(failed ? 1 : 0);' 'setTimeout(function(){exit(failed ? 1 : 0);}, 500);' > $@ 154 | 155 | .PHONY: clean 156 | clean: 157 | rm -f $(TARGET_TMP) 158 | rm -f $(TARGET_GZ) 159 | rm -f $(TARGET) 160 | rm -fr tests 161 | rm -fr $(MODULES) 162 | 163 | .PHONY: browserstack 164 | browserstack: $(BROWSERSTACK_RUNNER) $(ISTANBUL) $(TEST_HTML_COVERAGE) $(TEST_HTML_FUNCTIONAL) 165 | node -e "require('connect')().use('/cors', require('serve-static')(__dirname, {setHeaders: corsHeaders})).use('/time', function(req, res){corsHeaders(res);res.end((new Date()).getTime()+'')}).listen(8889);function corsHeaders(res) {res.setHeader('Access-Control-Allow-Origin', '*')}" & echo "$$!" > server.pid 166 | $(BROWSERSTACK_RUNNER) | tee tests/browserstack.log | grep -v 'coverage: {' 167 | kill `cat server.pid` && rm server.pid 168 | @ grep 'All tests done, failures: 0.' tests/browserstack.log > /dev/null 169 | rm -f tests/coverage-* 170 | cat tests/browserstack.log | grep 'coverage: {' | node -e 'console.log(require("fs").readFileSync("/dev/stdin", "utf8").split("\n").filter(Boolean).map(line => line.split("coverage: ")[1]).join("\n"));' | split -l 1 - tests/coverage- 171 | $(ISTANBUL) report --include 'tests/coverage-*' text-summary html lcovonly 172 | -------------------------------------------------------------------------------- /tests-functional.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | var fixture = document.getElementById('qunit-fixture'); 6 | 7 | QUnit.test('Simple width and height Query', function(assert) { 8 | 9 | var style = document.createElement('style'); 10 | style.type = 'text/css'; 11 | style.innerHTML = '@font-face { font-family: invalid-query; src: local("Times New Roman"), local("Droid Serif") }' 12 | + '@font-face { font-family: no-query; src: local("Times New Roman"), local("Droid Serif") }' 13 | + '@font-face { font-family: min-width-100; src: local("Times New Roman"), local("Droid Serif") }' 14 | + '@font-face { font-family: min-width-200; src: local("Times New Roman"), local("Droid Serif") }' 15 | + '@font-face { font-family: min-height-100; src: local("Times New Roman"), local("Droid Serif") }' 16 | + '@font-face { font-family: min-height-200; src: local("Times New Roman"), local("Droid Serif") }' 17 | + '@font-face { font-family: max-width-200; src: local("Times New Roman"), local("Droid Serif") }' 18 | + '@font-face { font-family: max-width-100; src: local("Times New Roman"), local("Droid Serif") }' 19 | + '@font-face { font-family: max-height-200; src: local("Times New Roman"), local("Droid Serif") }' 20 | + '@font-face { font-family: max-height-100; src: local("Times New Roman"), local("Droid Serif") }' 21 | + 'html:container( width >= 1px ) { font-family: invalid-query }' 22 | + '.minW, .maxW, .minH, .maxH { font-family: no-query }' 23 | + '.minW:container( width >= 100px ) { font-family: min-width-100 }' 24 | + '.minW:container( 200px <= width ) { font-family: min-width-200 }' 25 | + '.minH:container( height >= 100px ) { font-family: min-height-100 }' 26 | + '.minH:container( min-height: 200px ) { font-family: min-height-200 }' 27 | + '.maxW:container( " WIDTH <= 200px") { font-family: max-width-200 }' 28 | + '.maxW:container( " MAX-WIDTH : 100px") { font-family: max-width-100 }' 29 | + '.maxH:container( height <= 200px ) { font-family: max-height-200 }' 30 | + '.maxH[data-cq~="max-height:100px"] { font-family: max-height-100 }'; 31 | fixture.appendChild(style); 32 | 33 | var element = document.createElement('div'); 34 | element.innerHTML = '
' 35 | + '
' 36 | + '
' 37 | + '
' 38 | + ''; 39 | fixture.appendChild(element); 40 | var minW = element.querySelector('.minW'); 41 | var maxW = element.querySelector('.maxW'); 42 | var minH = element.querySelector('.minH'); 43 | var maxH = element.querySelector('.maxH'); 44 | 45 | var reevaluate = window.cqApi.reevaluate; 46 | 47 | var done = assert.async(); 48 | window.cqApi.reprocess(function () { 49 | 50 | var font = function(node) { 51 | return window.getComputedStyle(node).fontFamily; 52 | }; 53 | 54 | reevaluate(); 55 | assert.notEqual(font(document.documentElement), 'invalid-query', 'Invalid HTML container query'); 56 | 57 | element.style.cssText = 'width: 109px; height: 109px'; 58 | reevaluate(true); 59 | assert.equal(font(minW), 'no-query', 'min-width at 99px'); 60 | assert.equal(font(minH), 'no-query', 'min-height at 99px'); 61 | 62 | element.style.cssText = 'width: 110px; height: 110px'; 63 | reevaluate(); 64 | assert.equal(font(minW), 'min-width-100', 'min-width at 100px'); 65 | assert.equal(font(minH), 'min-height-100', 'min-height at 100px'); 66 | assert.equal(font(maxW), 'max-width-100', 'max-width at 100px'); 67 | assert.equal(font(maxH), 'max-height-100', 'max-height at 100px'); 68 | 69 | element.style.cssText = 'width: 111px; height: 111px'; 70 | reevaluate(); 71 | assert.equal(font(maxW), 'max-width-200', 'max-width at 101px'); 72 | assert.equal(font(maxH), 'max-height-200', 'max-height at 101px'); 73 | 74 | element.style.cssText = 'width: 209px; height: 209px'; 75 | reevaluate(); 76 | assert.equal(font(minW), 'min-width-100', 'min-width at 199px'); 77 | assert.equal(font(minH), 'min-height-100', 'min-height at 199px'); 78 | 79 | element.style.cssText = 'width: 210px; height: 210px'; 80 | reevaluate(); 81 | assert.equal(font(maxW), 'max-width-200', 'max-width at 200px'); 82 | assert.equal(font(maxH), 'max-height-200', 'max-height at 200px'); 83 | assert.equal(font(minW), 'min-width-200', 'min-width at 200px'); 84 | assert.equal(font(minH), 'min-height-200', 'min-height at 200px'); 85 | 86 | element.style.cssText = 'width: 211px; height: 211px'; 87 | reevaluate(); 88 | assert.equal(font(maxW), 'no-query', 'max-width at 201px'); 89 | assert.equal(font(maxH), 'no-query', 'max-height at 201px'); 90 | 91 | element.style.cssText = 'width: 400px; height: 400px'; 92 | element.firstChild.firstChild.firstChild.style.cssText = 'float: left; width: 51.29%; width: calc(100% / 2 + 5px); height: 51.29%; height: calc(100% / 2 + 5px)'; 93 | reevaluate(true); 94 | assert.equal(font(maxW), 'max-width-200', 'max-width at 200px'); 95 | assert.equal(font(maxH), 'max-height-200', 'max-height at 200px'); 96 | assert.equal(font(minW), 'min-width-200', 'min-width at 200px'); 97 | assert.equal(font(minH), 'min-height-200', 'min-height at 200px'); 98 | 99 | done(); 100 | 101 | }); 102 | }); 103 | 104 | QUnit.test('Combined Queries', function(assert) { 105 | 106 | var style = document.createElement('style'); 107 | style.type = 'text/css'; 108 | style.innerHTML = '@font-face { font-family: query; src: local("Times New Roman"), local("Droid Serif") }' 109 | + '.test:container(width > 100px):container(width < 200px):container(height > 100px):container(height < 200px) { font-family: query }'; 110 | fixture.appendChild(style); 111 | 112 | var element = document.createElement('div'); 113 | element.innerHTML = '
'; 114 | fixture.appendChild(element); 115 | var test = element.firstChild; 116 | 117 | var reevaluate = window.cqApi.reevaluate; 118 | 119 | var done = assert.async(); 120 | window.cqApi.reprocess(function () { 121 | 122 | var font = function(node) { 123 | return window.getComputedStyle(node).fontFamily; 124 | }; 125 | 126 | element.style.cssText = 'width: 100px; height: 100px'; 127 | reevaluate(true); 128 | assert.notEqual(font(test), 'query', 'width 100, height 100'); 129 | 130 | element.style.cssText = 'width: 101px; height: 100px'; 131 | reevaluate(); 132 | assert.notEqual(font(test), 'query', 'width 101, height 100'); 133 | 134 | element.style.cssText = 'width: 100px; height: 101px'; 135 | reevaluate(); 136 | assert.notEqual(font(test), 'query', 'width 100, height 101'); 137 | 138 | element.style.cssText = 'width: 101px; height: 101px'; 139 | reevaluate(); 140 | assert.equal(font(test), 'query', 'width 101, height 101'); 141 | 142 | element.style.cssText = 'width: 199px; height: 199px'; 143 | reevaluate(); 144 | assert.equal(font(test), 'query', 'width 199, height 199'); 145 | 146 | element.style.cssText = 'width: 200px; height: 199px'; 147 | reevaluate(); 148 | assert.notEqual(font(test), 'query', 'width 200, height 199'); 149 | 150 | element.style.cssText = 'width: 199px; height: 200px'; 151 | reevaluate(); 152 | assert.notEqual(font(test), 'query', 'width 199, height 200'); 153 | 154 | element.style.cssText = 'width: 200px; height: 200px'; 155 | reevaluate(); 156 | assert.notEqual(font(test), 'query', 'width 200, height 200'); 157 | 158 | done(); 159 | 160 | }); 161 | }); 162 | 163 | QUnit.test('Double comparison Query', function(assert) { 164 | 165 | var style = document.createElement('style'); 166 | style.type = 'text/css'; 167 | style.innerHTML = '@font-face { font-family: query; src: local("Times New Roman"), local("Droid Serif") }' 168 | + '.test:container(200px > width > 100px) { font-family: query }'; 169 | fixture.appendChild(style); 170 | 171 | var element = document.createElement('div'); 172 | element.innerHTML = '
'; 173 | fixture.appendChild(element); 174 | var test = element.firstChild; 175 | 176 | var reevaluate = window.cqApi.reevaluate; 177 | 178 | var done = assert.async(); 179 | window.cqApi.reprocess(function () { 180 | 181 | var font = function(node) { 182 | return window.getComputedStyle(node).fontFamily; 183 | }; 184 | 185 | element.style.cssText = 'width: 100px'; 186 | reevaluate(); 187 | assert.notEqual(font(test), 'query', 'width 100'); 188 | 189 | element.style.cssText = 'width: 101px'; 190 | reevaluate(); 191 | assert.equal(font(test), 'query', 'width 101'); 192 | 193 | element.style.cssText = 'width: 199px'; 194 | reevaluate(); 195 | assert.equal(font(test), 'query', 'width 199'); 196 | 197 | element.style.cssText = 'width: 200px'; 198 | reevaluate(); 199 | assert.notEqual(font(test), 'query', 'width 200'); 200 | 201 | done(); 202 | 203 | }); 204 | }); 205 | 206 | QUnit.test('Visibility Query', function(assert) { 207 | 208 | var style = document.createElement('style'); 209 | style.type = 'text/css'; 210 | style.innerHTML = '@font-face { font-family: visible; src: local("Times New Roman"), local("Droid Serif") }' 211 | + '@font-face { font-family: hidden; src: local("Times New Roman"), local("Droid Serif") }' 212 | + '.test:container(visibility = visible) { font-family: visible }' 213 | + '.test:container(visibility = hidden) { font-family: hidden }'; 214 | fixture.appendChild(style); 215 | 216 | var element = document.createElement('div'); 217 | element.innerHTML = '
'; 218 | fixture.appendChild(element); 219 | var test = element.firstChild; 220 | 221 | var reevaluate = window.cqApi.reevaluate; 222 | 223 | var done = assert.async(); 224 | window.cqApi.reprocess(function () { 225 | 226 | var font = function(node) { 227 | return window.getComputedStyle(node).fontFamily; 228 | }; 229 | 230 | assert.equal(font(test), 'visible', 'Default style visible'); 231 | 232 | element.style.cssText = 'visibility: visible'; 233 | reevaluate(); 234 | assert.equal(font(test), 'visible', 'Style visible'); 235 | 236 | element.style.cssText = 'visibility: hidden'; 237 | reevaluate(); 238 | assert.equal(font(test), 'hidden', 'Style hidden'); 239 | 240 | element.style.cssText = 'visibility: invalid'; 241 | reevaluate(); 242 | assert.equal(font(test), 'visible', 'Style invalid'); 243 | 244 | done(); 245 | 246 | }); 247 | 248 | }); 249 | 250 | QUnit.test('Background Color Query', function(assert) { 251 | 252 | var style = document.createElement('style'); 253 | style.type = 'text/css'; 254 | style.innerHTML = '@font-face { font-family: light; src: local("Times New Roman"), local("Droid Serif") }' 255 | + '@font-face { font-family: dark; src: local("Times New Roman"), local("Droid Serif") }' 256 | + '@font-face { font-family: green; src: local("Times New Roman"), local("Droid Serif") }' 257 | + '@font-face { font-family: transparent; src: local("Times New Roman"), local("Droid Serif") }' 258 | + '@font-face { font-family: semi-orange; src: local("Times New Roman"), local("Droid Serif") }' 259 | + '.test:container(background-color-lightness > 80%) { font-family: light }' 260 | + '.test:container(background-color-lightness < 20%) { font-family: dark }' 261 | + '.test:container(background-color-hue > 80deg < 160deg) { font-family: green }' 262 | + '.test:container(background-color-alpha < 10%) { font-family: transparent }' 263 | + '.test:container(background-color: rgba(255, 99, 66, 0.5)) { font-family: semi-orange }'; 264 | fixture.appendChild(style); 265 | 266 | var element = document.createElement('div'); 267 | element.innerHTML = '
'; 268 | fixture.appendChild(element); 269 | var test = element.firstChild.firstChild; 270 | 271 | var reevaluate = window.cqApi.reevaluate; 272 | 273 | var done = assert.async(); 274 | window.cqApi.reprocess(function () { 275 | 276 | var font = function(node) { 277 | return window.getComputedStyle(node).fontFamily; 278 | }; 279 | 280 | element.style.cssText = 'background: white'; 281 | reevaluate(true); 282 | assert.equal(font(test), 'light', 'White background'); 283 | 284 | element.style.cssText = 'background: black'; 285 | reevaluate(); 286 | assert.equal(font(test), 'dark', 'Black background'); 287 | 288 | element.style.cssText = 'background: green'; 289 | reevaluate(); 290 | assert.equal(font(test), 'green', 'Green background'); 291 | 292 | element.style.cssText = 'background: #40bf93'; 293 | reevaluate(); 294 | assert.equal(font(test), 'green', 'Green HEX background'); 295 | 296 | element.style.cssText = 'background: #af1'; 297 | reevaluate(); 298 | assert.equal(font(test), 'green', 'Green short HEX background'); 299 | 300 | element.style.cssText = 'background: rgba(64, 191, 147, 0.5)'; 301 | reevaluate(); 302 | assert.equal(font(test), 'green', 'Semi transparent blue green background'); 303 | 304 | element.style.cssText = 'background: hsla(159, 50%, 50%, 0.5)'; 305 | reevaluate(); 306 | assert.equal(font(test), 'green', 'Semi transparent blue green HSLA background'); 307 | 308 | element.style.cssText = 'color: green; background: currentColor'; 309 | reevaluate(); 310 | assert.equal(font(test), 'green', 'Green currentColor'); 311 | 312 | element.style.cssText = 'background: transparent'; 313 | reevaluate(); 314 | assert.equal(font(test), 'transparent', 'Transparent background'); 315 | 316 | if (window.CSS && CSS.supports && CSS.supports('--foo', 0)) { 317 | element.style.cssText = '--my-color: green; background: var(--my-color)'; 318 | reevaluate(); 319 | assert.equal(font(test), 'green', 'Green via CSS variable'); 320 | } 321 | 322 | element.style.cssText = 'background: rgba(255, 99, 66, 0.5)'; 323 | reevaluate(); 324 | assert.equal(font(test), 'semi-orange', 'Semi-orange background'); 325 | 326 | done(); 327 | 328 | }); 329 | 330 | }); 331 | 332 | QUnit[window.CSS && CSS.supports && CSS.supports('--foo', 0) 333 | ? 'test' 334 | : 'skip' 335 | ]('CSS variable Query (only for supported browsers)', function(assert) { 336 | 337 | var style = document.createElement('style'); 338 | style.type = 'text/css'; 339 | style.innerHTML = '@font-face { font-family: equal; src: local("Times New Roman"), local("Droid Serif") }' 340 | + '@font-face { font-family: less-than; src: local("Times New Roman"), local("Droid Serif") }' 341 | + '@font-face { font-family: greater-than; src: local("Times New Roman"), local("Droid Serif") }' 342 | + '.test:container(--foo = bar) { font-family: equal }' 343 | + '.test:container(--foo < 10em) { font-family: less-than }' 344 | + '.test:container(--foo > 10em) { font-family: greater-than }'; 345 | fixture.appendChild(style); 346 | 347 | var element = document.createElement('div'); 348 | element.innerHTML = '
'; 349 | fixture.appendChild(element); 350 | var test = element.firstChild; 351 | 352 | var reevaluate = window.cqApi.reevaluate; 353 | 354 | var done = assert.async(); 355 | window.cqApi.reprocess(function () { 356 | 357 | var font = function(node) { 358 | return window.getComputedStyle(node).fontFamily; 359 | }; 360 | 361 | element.style.cssText = '--foo: bar'; 362 | reevaluate(); 363 | assert.equal(font(test), 'equal', 'Equal'); 364 | 365 | element.style.cssText = '--foo: 9.9em'; 366 | reevaluate(); 367 | assert.equal(font(test), 'less-than', 'Less than'); 368 | 369 | element.style.cssText = '--foo: 101px; font-size: 10px'; 370 | reevaluate(); 371 | assert.equal(font(test), 'greater-than', 'Greater than'); 372 | 373 | done(); 374 | 375 | }); 376 | 377 | }); 378 | 379 | QUnit.test('Opacity Query', function(assert) { 380 | 381 | var style = document.createElement('style'); 382 | style.type = 'text/css'; 383 | style.innerHTML = '@font-face { font-family: opaque; src: local("Times New Roman"), local("Droid Serif") }' 384 | + '@font-face { font-family: semi-transparent; src: local("Times New Roman"), local("Droid Serif") }' 385 | + '@font-face { font-family: transparent; src: local("Times New Roman"), local("Droid Serif") }' 386 | + '.test:container(opacity = 1) { font-family: opaque }' 387 | + '.test:container(0 < opacity < 1) { font-family: semi-transparent }' 388 | + '.test:container(opacity = 0) { font-family: transparent }'; 389 | fixture.appendChild(style); 390 | 391 | var element = document.createElement('div'); 392 | element.innerHTML = '
'; 393 | fixture.appendChild(element); 394 | var test = element.firstChild; 395 | 396 | var reevaluate = window.cqApi.reevaluate; 397 | 398 | var done = assert.async(); 399 | window.cqApi.reprocess(function () { 400 | 401 | var font = function(node) { 402 | return window.getComputedStyle(node).fontFamily; 403 | }; 404 | 405 | assert.equal(font(test), 'opaque', 'Default opacity'); 406 | 407 | element.style.cssText = 'opacity: 1'; 408 | reevaluate(); 409 | assert.equal(font(test), 'opaque', 'Opacity 1'); 410 | 411 | element.style.cssText = 'opacity: 0.9'; 412 | reevaluate(); 413 | assert.equal(font(test), 'semi-transparent', 'Opacity 0.9'); 414 | 415 | element.style.cssText = 'opacity: 0.1'; 416 | reevaluate(); 417 | assert.equal(font(test), 'semi-transparent', 'Opacity 0.1'); 418 | 419 | element.style.cssText = 'opacity: 0'; 420 | reevaluate(); 421 | assert.equal(font(test), 'transparent', 'Opacity 0'); 422 | 423 | done(); 424 | 425 | }); 426 | 427 | }); 428 | 429 | QUnit.test('PostCSS skip step 1', function(assert) { 430 | 431 | var style = document.createElement('style'); 432 | style.type = 'text/css'; 433 | style.innerHTML = '@font-face { font-family: no-query; src: local("Times New Roman"), local("Droid Serif") }' 434 | + '@font-face { font-family: query; src: local("Times New Roman"), local("Droid Serif") }' 435 | + '.test { font-family: no-query }' 436 | + '.test.\\:container\\(width\\>100px\\) { font-family: query }'; 437 | fixture.appendChild(style); 438 | 439 | var element = document.createElement('div'); 440 | element.innerHTML = '
'; 441 | fixture.appendChild(element); 442 | var test = element.firstChild; 443 | 444 | var reevaluate = window.cqApi.reevaluate; 445 | 446 | window.cqApi.config.preprocess = undefined; 447 | 448 | var done = false; 449 | window.cqApi.reprocess(function () { 450 | 451 | var font = function(node) { 452 | return window.getComputedStyle(node).fontFamily; 453 | }; 454 | 455 | element.style.cssText = 'width: 100px'; 456 | reevaluate(true); 457 | assert.equal(font(test), 'no-query', 'Width 100px'); 458 | 459 | element.style.cssText = 'width: 101px'; 460 | reevaluate(); 461 | assert.equal(font(test), 'query', 'Width 101px'); 462 | 463 | done = true; 464 | 465 | }); 466 | 467 | assert.ok(done, 'Reprocess callback synchronous'); 468 | 469 | window.cqApi.config.preprocess = true; 470 | 471 | }); 472 | 473 | QUnit.test('Performance of many elements on the same level', function(assert) { 474 | 475 | var style = document.createElement('style'); 476 | style.type = 'text/css'; 477 | style.innerHTML = '@font-face { font-family: no-query; src: local("Times New Roman"), local("Droid Serif") }' 478 | + '@font-face { font-family: query; src: local("Times New Roman"), local("Droid Serif") }' 479 | + '.test { font-family: no-query }' 480 | + '.test:container(width > 100px) { font-family: query }'; 481 | fixture.appendChild(style); 482 | 483 | var element = document.createElement('div'); 484 | element.innerHTML = new Array(1001).join('
'); 485 | element.style.cssText = 'width: 0'; 486 | fixture.appendChild(element); 487 | 488 | var reevaluate = window.cqApi.reevaluate; 489 | 490 | var done = assert.async(); 491 | window.cqApi.reprocess(function () { 492 | 493 | var font = function(node) { 494 | return window.getComputedStyle(node).fontFamily; 495 | }; 496 | 497 | element.style.cssText = 'width: 100px'; 498 | element.getBoundingClientRect(); // force reflow 499 | /*eslint-disable no-console*/ 500 | if (window.console && console.timeStamp) { 501 | console.timeStamp('No reflow should occur from here'); 502 | console.profile('No reflow should occur'); 503 | console.time('Performance of many elements on the same level'); 504 | } 505 | reevaluate(); 506 | if (window.console && console.timeStamp) { 507 | console.timeEnd('Performance of many elements on the same level'); 508 | console.timeStamp('No reflow should occur until here'); 509 | console.profileEnd('No reflow should occur'); 510 | } 511 | /*eslint-enable no-console*/ 512 | assert.equal(font(element.firstChild), 'no-query', 'Width 100px first'); 513 | assert.equal(font(element.lastChild), 'no-query', 'Width 100px last'); 514 | 515 | element.style.cssText = 'width: 101px'; 516 | reevaluate(); 517 | assert.equal(font(element.firstChild), 'query', 'Width 101px first'); 518 | assert.equal(font(element.lastChild), 'query', 'Width 101px last'); 519 | 520 | done(); 521 | 522 | }); 523 | 524 | }); 525 | 526 | QUnit.test('Performance of many nested elements', function(assert) { 527 | 528 | var style = document.createElement('style'); 529 | style.type = 'text/css'; 530 | style.innerHTML = '@font-face { font-family: no-query; src: local("Times New Roman"), local("Droid Serif") }' 531 | + '@font-face { font-family: query; src: local("Times New Roman"), local("Droid Serif") }' 532 | + '.test { font-family: no-query }' 533 | + '.test:container(width > 100px) { padding: 1px; font-family: query }'; 534 | fixture.appendChild(style); 535 | 536 | var element = document.createElement('div'); 537 | element.style.cssText = 'width: 0'; 538 | element.innerHTML = new Array(101).join( 539 | new Array(11).join('
') 540 | + new Array(11).join('
') 541 | ); 542 | fixture.appendChild(element); 543 | var first = element.firstChild; 544 | var nestedLast = element.lastChild.querySelector('.test:empty'); 545 | 546 | var reevaluate = window.cqApi.reevaluate; 547 | 548 | var done = assert.async(); 549 | window.cqApi.reprocess(function () { 550 | 551 | var font = function(node) { 552 | return window.getComputedStyle(node).fontFamily; 553 | }; 554 | 555 | element.style.cssText = 'width: 100px'; 556 | element.getBoundingClientRect(); // force reflow 557 | /*eslint-disable no-console*/ 558 | if (window.console && console.timeStamp) { 559 | console.timeStamp('No reflow should occur from here'); 560 | console.profile('No reflow should occur'); 561 | console.time('Performance of many nested elements - no reflow'); 562 | } 563 | reevaluate(); 564 | if (window.console && console.timeStamp) { 565 | console.timeEnd('Performance of many nested elements - no reflow'); 566 | console.timeStamp('No reflow should occur until here'); 567 | console.profileEnd('No reflow should occur'); 568 | } 569 | /*eslint-enable no-console*/ 570 | assert.equal(font(first), 'no-query', 'Width 100px first'); 571 | assert.equal(font(nestedLast), 'no-query', 'Width 100px nested last'); 572 | 573 | element.style.cssText = 'width: 118px'; 574 | element.getBoundingClientRect(); // force reflow 575 | /*eslint-disable no-console*/ 576 | if (window.console && console.timeStamp) { 577 | console.timeStamp('Not more than 10 reflows should occur from here'); 578 | console.profile('Not more than 10 reflows should occur'); 579 | console.time('Performance of many nested elements - not more than 10 reflows'); 580 | } 581 | reevaluate(); 582 | if (window.console && console.timeStamp) { 583 | console.timeEnd('Performance of many nested elements - not more than 10 reflows'); 584 | console.timeStamp('Not more than 10 reflows should occur until here'); 585 | console.profileEnd('Not more than 10 reflows should occur'); 586 | } 587 | /*eslint-enable no-console*/ 588 | assert.equal(font(first), 'query', 'Width 118px first'); 589 | assert.equal(font(nestedLast.parentNode), 'query', 'Width 118px nested last parent'); 590 | assert.equal(font(nestedLast), 'no-query', 'Width 118px nested last'); 591 | 592 | done(); 593 | 594 | }); 595 | 596 | }); 597 | 598 | })(); 599 | -------------------------------------------------------------------------------- /cq-prolyfill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Martin Auswöger 3 | * 4 | * For the full copyright and license information, please view the LICENSE 5 | * file that was distributed with this source code. 6 | */ 7 | 8 | (function( 9 | window, 10 | document, 11 | /*eslint-disable no-shadow-restricted-names*/ 12 | undefined 13 | /*eslint-enable no-shadow-restricted-names*/ 14 | ) { 15 | 'use strict'; 16 | 17 | (function (factory) { 18 | /*global define*/ 19 | /* istanbul ignore next: don’t cover module definition */ 20 | if (typeof define === 'function' && define.amd) { 21 | define([], function() { 22 | return factory; 23 | }); 24 | } 25 | /*global module*/ 26 | else if (typeof module === 'object' && module.exports) { 27 | module.exports = factory; 28 | } 29 | else { 30 | /*eslint-disable dot-notation*/ 31 | window['cqApi'] = factory(window['cqConfig']); 32 | /*eslint-enable dot-notation*/ 33 | } 34 | }(function(config) { 35 | 36 | config = config || {}; 37 | 38 | // Public API 39 | /*eslint-disable dot-notation*/ 40 | var api = { 41 | 'reprocess': reprocess, 42 | 'reparse': reparse, 43 | 'reevaluate': reevaluate, 44 | 'config': config, 45 | }; 46 | /*eslint-enable dot-notation*/ 47 | 48 | var REGEXP_ESCAPE_REGEXP = /[.?*+^$[\]\\(){}|-]/g; 49 | var SELECTOR_REGEXP = /\.?:container\((?:[^()]+|\([^()]*\))+\)/gi; 50 | var SELECTOR_ESCAPED_REGEXP = /\.\\:container\\\(((?:[^()]+?|\\\([^()]*\\\))+?)\\\)|\[data-cq~=["']((?:\\.|[^"'])+)["']\]/gi; 51 | var QUERY_REGEXP = /^(?:(.+?)([<>]=?|=))??(?:(min|max)-)?([a-z-]+?)(?:-(hue|saturation|lightness|alpha))?(?:([<>]=?|=|:)(.+))?$/; 52 | var ESCAPE_REGEXP = /[.:()<>=%,]/g; 53 | var SPACE_REGEXP = / /g; 54 | var LENGTH_REGEXP = /^(-?(?:\d*\.)?\d+)(em|ex|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pt|pc)$/i; 55 | var NUMBER_REGEXP = /^-?(?:\d*\.)?\d+$/i; 56 | var COLOR_REGEXP = /^rgba?\s*\([0-9.,\s]*\)$/i; 57 | var URL_VALUE_REGEXP = /url\(\s*(?:(["'])(.*?)\1|([^)\s]*))\s*\)/gi; 58 | var ATTR_REGEXP = /\[.+?\]/g; 59 | var PSEUDO_NOT_REGEXP = /:not\(/g; 60 | var ID_REGEXP = /#[^\s\[\\#+,.:>~]+/g; 61 | var CLASS_REGEXP = /\.[^\s\[\\#+,.:>~]+/g; 62 | var PSEUDO_ELEMENT_REGEXP = /::[^\s\[\\#+,.:>~]+/g; 63 | var PSEUDO_CLASS_REGEXP = /:[^\s\[\\#+,.:>~]+/g; 64 | var ELEMENT_REGEXP = /[a-z-]+/gi; 65 | var FIXED_UNIT_MAP = { 66 | 'px': 1, 67 | 'pt': 16 / 12, 68 | 'pc': 16, 69 | 'in': 96, 70 | 'cm': 96 / 2.54, 71 | 'mm': 96 / 25.4, 72 | }; 73 | 74 | var queries; 75 | var containerCache; 76 | var styleCache; 77 | var processedSheets = createCacheMap(); 78 | var requestCache = {}; 79 | var domMutations = []; 80 | var processed = false; 81 | var parsed = false; 82 | var documentElement = document.documentElement; 83 | var styleSheets = document.styleSheets; 84 | var createElement = document.createElement.bind(document); 85 | var requestAnimationFrame = window.requestAnimationFrame || setTimeout; 86 | var observer; 87 | var scheduledCall; 88 | 89 | /** 90 | * @param {function()} callback 91 | */ 92 | function reprocess(callback) { 93 | preprocess(function() { 94 | processed = true; 95 | reparse(callback); 96 | }); 97 | } 98 | 99 | /** 100 | * @param {function()} callback 101 | */ 102 | function reparse(callback) { 103 | if (!processed) { 104 | return reprocess(callback); 105 | } 106 | parseRules(); 107 | buildStyleCache(); 108 | parsed = true; 109 | reevaluate(true, callback); 110 | } 111 | 112 | /** 113 | * @param {boolean} clearContainerCache 114 | * @param {function()} callback 115 | * @param {Array.} contexts 116 | */ 117 | function reevaluate(clearContainerCache, callback, contexts) { 118 | if (!parsed) { 119 | return reparse(callback); 120 | } 121 | updateClasses(clearContainerCache, contexts); 122 | if (callback) { 123 | callback(); 124 | } 125 | } 126 | 127 | /** 128 | * Schedule the execution of step 1, 2 or 3 for the next animation frame 129 | * 130 | * @param {number} step 1: reprocess, 2: reparse, 3: reevaluate 131 | * @param {boolean} clearContainerCache 132 | * @param {Array.} contexts 133 | */ 134 | function scheduleExecution(step, clearContainerCache, contexts) { 135 | 136 | if (!scheduledCall) { 137 | scheduledCall = { 138 | _step: step, 139 | _clearContainerCache: clearContainerCache, 140 | _contexts: contexts, 141 | }; 142 | requestAnimationFrame(executeScheduledCall); 143 | return; 144 | } 145 | 146 | scheduledCall._step = Math.min(scheduledCall._step, step); 147 | 148 | // Merge parameters for reevaluate 149 | if (scheduledCall._step === 3) { 150 | scheduledCall._clearContainerCache = scheduledCall._clearContainerCache || clearContainerCache; 151 | if (scheduledCall._contexts && contexts) { 152 | scheduledCall._contexts = scheduledCall._contexts.concat(contexts); 153 | } 154 | } 155 | 156 | } 157 | 158 | /** 159 | * Executes the scheduled call from scheduleExecution() in an animation frame 160 | */ 161 | function executeScheduledCall() { 162 | 163 | var call = scheduledCall; 164 | scheduledCall = undefined; 165 | 166 | if (call._step === 1) { 167 | reprocess(); 168 | } 169 | else if (call._step === 2) { 170 | reparse(); 171 | } 172 | else { 173 | reevaluate(call._clearContainerCache, undefined, call._contexts); 174 | } 175 | 176 | } 177 | 178 | /** 179 | * Starts observing DOM events and mutations 180 | */ 181 | function startObserving() { 182 | 183 | if (config.skipObserving) { 184 | return; 185 | } 186 | 187 | // Reprocess now 188 | scheduleExecution(1); 189 | 190 | window.addEventListener('DOMContentLoaded', scheduleExecution.bind(undefined, 1, undefined, undefined)); 191 | window.addEventListener('load', scheduleExecution.bind(undefined, 1, undefined, undefined)); 192 | window.addEventListener('resize', scheduleExecution.bind(undefined, 3, true, undefined)); 193 | 194 | var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 195 | if (MutationObserver) { 196 | observer = new MutationObserver(checkMutations); 197 | observer.observe(document.documentElement, { 198 | childList: true, 199 | subtree: true, 200 | }); 201 | } 202 | else { 203 | window.addEventListener('DOMNodeInserted', onDomMutate); 204 | window.addEventListener('DOMNodeRemoved', onDomMutate); 205 | } 206 | 207 | } 208 | 209 | /** 210 | * Check DOM mutations and reprocess or reevaluate 211 | * 212 | * @param {Array.} mutations 213 | */ 214 | function checkMutations(mutations) { 215 | 216 | // Skip iterating the nodes, if a run is already scheduled, to improve performance 217 | if (scheduledCall && (scheduledCall._level < 3 || !scheduledCall._contexts)) { 218 | return; 219 | } 220 | 221 | var addedNodes = []; 222 | var stylesChanged = false; 223 | 224 | var replacedSheets = []; 225 | processedSheets.forEach(function(newNode) { 226 | replacedSheets.push(newNode); 227 | }); 228 | 229 | arrayFrom(mutations).forEach(function(mutation) { 230 | 231 | addedNodes.push.apply(addedNodes, arrayFrom(mutation.addedNodes).filter(function(node) { 232 | return node.nodeType === 1; 233 | })); 234 | 235 | arrayFrom(mutation.removedNodes).forEach(function(node) { 236 | var index = addedNodes.indexOf(node); 237 | if (index !== -1) { 238 | addedNodes.splice(index, 1); 239 | } 240 | else if ( 241 | (node.tagName === 'LINK' || node.tagName === 'STYLE') 242 | && replacedSheets.indexOf(node) === -1 243 | ) { 244 | stylesChanged = true; 245 | } 246 | }); 247 | 248 | }); 249 | 250 | addedNodes.forEach(function(node) { 251 | if (node.sheet && replacedSheets.indexOf(node) === -1) { 252 | stylesChanged = true; 253 | } 254 | }); 255 | 256 | if (stylesChanged) { 257 | scheduleExecution(1); 258 | } 259 | else if (addedNodes.length) { 260 | scheduleExecution(3, false, addedNodes); 261 | } 262 | 263 | } 264 | 265 | /** 266 | * Event handler for DOMNodeInserted and DOMNodeRemoved 267 | * 268 | * @param {MutationEvent} event 269 | */ 270 | function onDomMutate(event) { 271 | 272 | var mutation = { 273 | addedNodes: [], 274 | removedNodes: [], 275 | }; 276 | mutation[ 277 | (event.type === 'DOMNodeInserted' ? 'added' : 'removed') + 'Nodes' 278 | ] = [event.target]; 279 | 280 | domMutations.push(mutation); 281 | 282 | // Delay the call to checkMutations() 283 | setTimeout(function() { 284 | checkMutations(domMutations); 285 | domMutations = []; 286 | }); 287 | 288 | } 289 | 290 | /** 291 | * Step 1: Preprocess all active stylesheets in the document 292 | * 293 | * Look for stylesheets that contain container queries and escape them to be 294 | * readable by the browser, e.g. convert `:container(width >= 10px)` to 295 | * `\:container\(width\>\=10px\)` 296 | * 297 | * @param {function()} callback 298 | */ 299 | function preprocess(callback) { 300 | 301 | var sheets = arrayFrom(styleSheets); 302 | 303 | // Check removed stylesheets 304 | processedSheets.forEach(function(newNode, node) { 305 | // Original stylesheet has been deleted 306 | if (sheets.indexOf(node.sheet) === -1) { 307 | if (newNode && sheets.indexOf(newNode.sheet) !== -1 && newNode.parentNode) { 308 | sheets.splice(sheets.indexOf(newNode.sheet), 1); 309 | newNode.parentNode.removeChild(newNode); 310 | } 311 | processedSheets.delete(node); 312 | } 313 | }); 314 | 315 | var done = -1; 316 | function step() { 317 | done++; 318 | if (done === sheets.length) { 319 | callback(); 320 | } 321 | } 322 | sheets.forEach(function(sheet) { 323 | preprocessSheet(sheet, step); 324 | }); 325 | step(); 326 | 327 | } 328 | 329 | /** 330 | * @param {CSSStyleSheet} sheet 331 | * @param {function()} callback 332 | */ 333 | function preprocessSheet(sheet, callback) { 334 | if (sheet.disabled) { 335 | callback(); 336 | return; 337 | } 338 | if (!config.preprocess) { 339 | var rulesLength = -1; 340 | try { 341 | rulesLength = sheet.cssRules.length; 342 | } 343 | catch(e) { 344 | // Do nothing 345 | } 346 | // Check if cssRules is accessible 347 | if (rulesLength !== -1 || !sheet.ownerNode.getAttribute('crossorigin')) { 348 | callback(); 349 | return; 350 | } 351 | } 352 | var ownerNode = sheet.ownerNode; 353 | var tag = ownerNode && ownerNode.tagName; 354 | if (tag === 'LINK' && !processedSheets.has(ownerNode)) { 355 | loadExternal(ownerNode.href, function(cssText) { 356 | // Check again because loadExternal is async 357 | if (sheet.disabled || !cssText) { 358 | callback(); 359 | return; 360 | } 361 | preprocessStyle(ownerNode, fixRelativeUrls(cssText, ownerNode.href)); 362 | callback(); 363 | }); 364 | } 365 | else if (tag === 'STYLE') { 366 | preprocessStyle(ownerNode, ownerNode.innerHTML); 367 | callback(); 368 | } 369 | else { 370 | callback(); 371 | } 372 | } 373 | 374 | /** 375 | * Load external file via AJAX 376 | * 377 | * @param {string} href 378 | * @param {function(string)} callback Gets called with the response text on 379 | * success or empty string on failure 380 | */ 381 | function loadExternal(href, callback) { 382 | var cacheEntryType = typeof requestCache[href]; 383 | if (cacheEntryType === 'string') { 384 | callback(requestCache[href]); 385 | return; 386 | } 387 | else if (cacheEntryType === 'object') { 388 | requestCache[href].push(callback); 389 | return; 390 | } 391 | requestCache[href] = [callback] 392 | var isDone = false; 393 | var done = function(response) { 394 | if (!isDone) { 395 | response = response || ''; 396 | requestCache[href].forEach(function(cachedCallback) { 397 | setTimeout(function() { 398 | cachedCallback(response); 399 | }); 400 | }); 401 | requestCache[href] = response; 402 | } 403 | isDone = true; 404 | }; 405 | var xhr = new XMLHttpRequest(); 406 | xhr.onreadystatechange = function() { 407 | if (xhr.readyState !== 4) { 408 | return; 409 | } 410 | done(xhr.status === 200 && xhr.responseText); 411 | }; 412 | try { 413 | xhr.open('GET', href); 414 | xhr.send(); 415 | } 416 | catch(e) { 417 | if (window.XDomainRequest) { 418 | xhr = new XDomainRequest(); 419 | xhr.onprogress = 420 | /* istanbul ignore next: fix for a rare IE9 bug */ 421 | function() {}; 422 | xhr.onload = xhr.onerror = xhr.ontimeout = function() { 423 | done(xhr.responseText); 424 | }; 425 | try { 426 | xhr.open('GET', href); 427 | xhr.send(); 428 | } 429 | catch(e2) { 430 | done(); 431 | } 432 | } 433 | else { 434 | done(); 435 | } 436 | } 437 | } 438 | 439 | /** 440 | * Replace relative CSS URLs with their absolute counterpart 441 | * 442 | * @param {string} cssText 443 | * @param {string} href URL of the stylesheet 444 | * @return {string} 445 | */ 446 | function fixRelativeUrls(cssText, href) { 447 | var base = resolveRelativeUrl(href, document.baseURI); 448 | return cssText.replace(URL_VALUE_REGEXP, function(match, quote, url1, url2) { 449 | var url = url1 || url2; 450 | if (!url) { 451 | return match; 452 | } 453 | return 'url(' + (quote || '"') + resolveRelativeUrl(url, base) + (quote || '"') + ')'; 454 | }); 455 | } 456 | 457 | /** 458 | * @param {string} url 459 | * @param {string} base 460 | * @return {string} 461 | */ 462 | function resolveRelativeUrl(url, base) { 463 | var absoluteUrl; 464 | try { 465 | absoluteUrl = new URL(url, base).href; 466 | } 467 | catch(e) { 468 | absoluteUrl = false; 469 | } 470 | if (!absoluteUrl) { 471 | var baseElement = createElement('base'); 472 | baseElement.href = base; 473 | document.head.insertBefore(baseElement, document.head.firstChild); 474 | var link = createElement('a'); 475 | link.href = url; 476 | absoluteUrl = link.href; 477 | // Catch error in iOS 7.0 478 | try { 479 | // Fix for a bug in Opera 12 480 | delete baseElement.href; 481 | } 482 | catch(e) { 483 | // Do nothing 484 | } 485 | document.head.removeChild(baseElement); 486 | } 487 | return absoluteUrl; 488 | } 489 | 490 | /** 491 | * @param {Node} node Stylesheet ownerNode 492 | * @param {string} cssText 493 | */ 494 | function preprocessStyle(node, cssText) { 495 | processedSheets.set(node, false); 496 | var escapedText = escapeSelectors(cssText); 497 | var rulesLength = -1; 498 | if (escapedText === cssText) { 499 | try { 500 | rulesLength = node.sheet.cssRules.length; 501 | } 502 | catch(e) { 503 | rulesLength = -1; 504 | } 505 | // Check if cssRules is accessible 506 | if (rulesLength !== -1) { 507 | return; 508 | } 509 | } 510 | var style = createElement('style'); 511 | style.textContent = escapedText; 512 | style.media = node.media || 'all'; 513 | node.parentNode.insertBefore(style, node); 514 | node.sheet.disabled = true; 515 | processedSheets.set(node, style); 516 | } 517 | 518 | /** 519 | * @param {string} cssText 520 | * @return {string} 521 | */ 522 | function escapeSelectors(cssText) { 523 | return cssText.replace(SELECTOR_REGEXP, function(selector) { 524 | return '.' + selector.substr(selector[0] === '.' ? 1 : 0) 525 | .replace(SPACE_REGEXP, '') 526 | .replace(/"/g, '') 527 | .replace(ESCAPE_REGEXP, '\\$&') 528 | .toLowerCase(); 529 | }); 530 | } 531 | 532 | /** 533 | * Step 2: Parse all processed container query rules and store them in `queries` 534 | * indexed by the preceding selector 535 | */ 536 | function parseRules() { 537 | queries = {}; 538 | var rules; 539 | for (var i = 0; i < styleSheets.length; i++) { 540 | if (styleSheets[i].disabled) { 541 | continue; 542 | } 543 | try { 544 | rules = styleSheets[i].cssRules; 545 | if (!rules || !rules.length) { 546 | continue; 547 | } 548 | } 549 | catch(e) { 550 | continue; 551 | } 552 | for (var j = 0; j < rules.length; j++) { 553 | parseRule(rules[j]); 554 | } 555 | } 556 | } 557 | 558 | /** 559 | * @param {CSSRule} rule 560 | */ 561 | function parseRule(rule) { 562 | if (rule.cssRules) { 563 | for (var i = 0; i < rule.cssRules.length; i++) { 564 | parseRule(rule.cssRules[i]); 565 | } 566 | return; 567 | } 568 | if (rule.type !== 1) { 569 | return; 570 | } 571 | splitSelectors(rule.selectorText).forEach(function(selector) { 572 | selector = escapeSelectors(selector); 573 | selector.replace(SELECTOR_ESCAPED_REGEXP, function(match, query, queryUnescaped, offset) { 574 | var precedingSelector = 575 | ( 576 | selector.substr(0, offset) 577 | + selector.substr(offset + match.length).replace(/^((?:\([^)]*\)|[^\s>+~])*).*$/, '$1') 578 | ) 579 | .replace(SELECTOR_ESCAPED_REGEXP, '') 580 | .replace(PSEUDO_ELEMENT_REGEXP, '') 581 | .replace(/:(?:active|hover|focus|checked|before|after)/gi, ''); 582 | if (!precedingSelector.substr(-1).trim()) { 583 | precedingSelector += '*'; 584 | } 585 | var query = parseQuery(queryUnescaped || unescape(query)); 586 | if (query) { 587 | query._selector = precedingSelector; 588 | query._className = queryUnescaped ? undefined : unescape(match.substr(1)); 589 | query._attribute = queryUnescaped; 590 | queries[precedingSelector + match] = query; 591 | } 592 | }); 593 | }); 594 | } 595 | 596 | /** 597 | * @param {string} query 598 | * @return {Array.<{_prop: string, _filter: string, _types: array, _values: array, _valueType: string}>} 599 | */ 600 | function parseQuery(query) { 601 | 602 | if (!(query = QUERY_REGEXP.exec(query))) { 603 | return; 604 | } 605 | 606 | var values = [query[7], query[1]].filter(Boolean); 607 | var valueType = 608 | (query[5] || values[0].match(NUMBER_REGEXP)) ? 'n' : 609 | values[0].match(LENGTH_REGEXP) ? 'l' : 610 | values[0].match(COLOR_REGEXP) ? 'c' : 611 | 's'; 612 | if (valueType === 'n') { 613 | values = values.map(parseFloat); 614 | } 615 | else if (valueType === 'c') { 616 | values = values.map(function(value) { 617 | return parseColor(value).join(','); 618 | }); 619 | } 620 | 621 | return { 622 | _prop: query[4], 623 | _filter: query[5], 624 | _types: [ 625 | query[6] !== ':' ? query[6] 626 | : query[3] === 'min' ? '>=' 627 | : query[3] ? '<=' 628 | : '=', 629 | query[2] && query[2].replace(/[<>]/, function(match) { 630 | // Invert the left side comparison operator 631 | return match === '<' ? '>' : '<'; 632 | }), 633 | ].filter(Boolean), 634 | _values: values, 635 | _valueType: valueType, 636 | }; 637 | 638 | } 639 | 640 | /** 641 | * Unescape backslash escaped string 642 | * 643 | * @param {string} string 644 | * @return {string} 645 | */ 646 | function unescape(string) { 647 | return string && string.replace(/\\(.)/g, '$1'); 648 | } 649 | 650 | /** 651 | * Split multiple selectors by `,` 652 | * 653 | * @param {string} selectors 654 | * @return {Array.} 655 | */ 656 | function splitSelectors(selectors) { 657 | return (selectors.match(/(?:\\.|"(?:\\.|[^"])*"|\([^)]*\)|[^,])+/g) || []) 658 | .map(function(selector) { 659 | return selector.trim(); 660 | }); 661 | } 662 | 663 | /** 664 | * Builds the styleCache needed by getOriginalStyle 665 | */ 666 | function buildStyleCache() { 667 | styleCache = { 668 | width: {}, 669 | height: {}, 670 | }; 671 | var rules; 672 | for (var i = 0; i < styleSheets.length; i++) { 673 | if (styleSheets[i].disabled) { 674 | continue; 675 | } 676 | try { 677 | rules = styleSheets[i].cssRules; 678 | if (!rules || !rules.length) { 679 | continue; 680 | } 681 | } 682 | catch(e) { 683 | continue; 684 | } 685 | buildStyleCacheFromRules(rules); 686 | } 687 | } 688 | 689 | /** 690 | * @param {CSSRuleList} rules 691 | */ 692 | function buildStyleCacheFromRules(rules) { 693 | for (var i = 0; i < rules.length; i++) { 694 | if (rules[i].type === 1) { // Style rule 695 | if ( 696 | rules[i].style.getPropertyValue('width') 697 | || rules[i].style.getPropertyValue('height') 698 | ) { 699 | splitSelectors(escapeSelectors(rules[i].selectorText)).forEach(function(selector) { 700 | var rule = { 701 | _selector: selector, 702 | _rule: rules[i], 703 | _specificity: getSpecificity(selector), 704 | }; 705 | var rightMostSelector = selector 706 | .replace(/:[a-z-]+\([^)]*\)/i, '') 707 | .replace(/^.*[^\\][\s>+~]\s*/, ''); 708 | if ( 709 | rightMostSelector.match(PSEUDO_ELEMENT_REGEXP) 710 | || rightMostSelector.match(/:(?:before|after)/i) 711 | ) { 712 | return; 713 | } 714 | rightMostSelector = rightMostSelector 715 | .replace(PSEUDO_CLASS_REGEXP, '') 716 | .trim(); 717 | ['width', 'height'].forEach(function(prop) { 718 | if (!rules[i].style.getPropertyValue(prop)) { 719 | return; 720 | } 721 | var match = rightMostSelector.match(ID_REGEXP); 722 | if (!match) { 723 | match = rightMostSelector.match(CLASS_REGEXP); 724 | } 725 | if (!match) { 726 | match = rightMostSelector.match(ELEMENT_REGEXP); 727 | if (match) { 728 | match = [match[0].toLowerCase()]; 729 | } 730 | } 731 | if (!match) { 732 | match = '*'; 733 | } 734 | if (!styleCache[prop][match[0]]) { 735 | styleCache[prop][match[0]] = []; 736 | } 737 | styleCache[prop][match[0]].push(rule); 738 | }); 739 | }); 740 | } 741 | } 742 | else if (rules[i].cssRules) { 743 | buildStyleCacheFromRules(rules[i].cssRules); 744 | } 745 | } 746 | } 747 | 748 | /** 749 | * Step 3: Loop through the `queries` and add or remove the CSS classes of all 750 | * matching elements 751 | * 752 | * @param {boolean} clearContainerCache 753 | * @param {Array.} contexts 754 | */ 755 | function updateClasses(clearContainerCache, contexts) { 756 | 757 | if (clearContainerCache || !containerCache) { 758 | containerCache = createCacheMap(); 759 | } 760 | 761 | if (!Object.keys(queries).length) { 762 | return; 763 | } 764 | 765 | var elementsTree = buildElementsTree(contexts); 766 | 767 | while(updateClassesRead(elementsTree)) { 768 | updateClassesWrite(elementsTree); 769 | } 770 | updateClassesWrite(elementsTree); 771 | 772 | } 773 | 774 | /** 775 | * Update classes read step 776 | * 777 | * @param {Array.<{_element: Element, _children: array, _queries: array, _changes: array, _done: boolean}>} treeNodes 778 | * @param {boolean} dontMarkAsDone 779 | * @return {boolean} True if changes were found 780 | */ 781 | function updateClassesRead(treeNodes, dontMarkAsDone) { 782 | var hasChanges = false; 783 | var i, node, j, query; 784 | for (i = 0; i < treeNodes.length; i++) { 785 | node = treeNodes[i]; 786 | if (!node._done) { 787 | for (j = 0; j < node._queries.length; j++) { 788 | query = node._queries[j]; 789 | var queryMatches = evaluateQuery(node._element.parentNode, query); 790 | if (queryMatches !== hasQuery(node._element, query)) { 791 | node._changes.push([queryMatches, query]); 792 | } 793 | } 794 | node._done = !dontMarkAsDone; 795 | } 796 | hasChanges = updateClassesRead(node._children, dontMarkAsDone || node._changes.length) 797 | || node._changes.length 798 | || hasChanges; 799 | } 800 | return hasChanges; 801 | } 802 | 803 | /** 804 | * Update classes write step 805 | * 806 | * @param {Array.<{_element: Element, _children: array, _queries: array, _changes: array, _done: boolean}>} treeNodes 807 | */ 808 | function updateClassesWrite(treeNodes) { 809 | var node, j; 810 | for (var i = 0; i < treeNodes.length; i++) { 811 | node = treeNodes[i]; 812 | for (j = 0; j < node._changes.length; j++) { 813 | (node._changes[j][0] ? addQuery : removeQuery)(node._element, node._changes[j][1]); 814 | } 815 | node._changes = []; 816 | updateClassesWrite(node._children); 817 | } 818 | } 819 | 820 | /** 821 | * Build tree of all query elements 822 | * 823 | * @param {Array.} contexts 824 | * @return {Array.<{_element: Element, _children: array, _queries: array, _changes: array, _done: boolean}>} 825 | */ 826 | function buildElementsTree(contexts) { 827 | 828 | contexts = contexts || [document]; 829 | 830 | var queriesArray = Object.keys(queries).map(function(key) { 831 | return queries[key]; 832 | }); 833 | 834 | var selector = queriesArray.map(function(query) { 835 | return query._selector; 836 | }).join(','); 837 | 838 | var elements = []; 839 | contexts.forEach(function(context) { 840 | for (var node = context.parentNode; node; node = node.parentNode) { 841 | // Skip nested contexts 842 | if (contexts.indexOf(node) !== -1) { 843 | return; 844 | } 845 | } 846 | if (context !== document && elementMatchesSelector(context, selector)) { 847 | elements.push(context); 848 | } 849 | elements.push.apply(elements, arrayFrom(context.querySelectorAll(selector))); 850 | }); 851 | 852 | var tree = []; 853 | var treeCache = createCacheMap(); 854 | 855 | elements.forEach(function(element) { 856 | 857 | if (element === documentElement) { 858 | return; 859 | } 860 | 861 | var treeNode = { 862 | _element: element, 863 | _children: [], 864 | _queries: [], 865 | _changes: [], 866 | _done: false, 867 | }; 868 | 869 | var children = tree; 870 | for (var node = element.parentNode; node; node = node.parentNode) { 871 | if (treeCache.get(node)) { 872 | children = treeCache.get(node)._children; 873 | break; 874 | } 875 | } 876 | 877 | treeCache.set(element, treeNode); 878 | 879 | children.push(treeNode); 880 | 881 | queriesArray.forEach(function(query) { 882 | if (elementMatchesSelector(element, query._selector)) { 883 | treeNode._queries.push(query); 884 | } 885 | }); 886 | 887 | }); 888 | 889 | return tree; 890 | 891 | } 892 | 893 | /** 894 | * True if the query matches otherwise false 895 | * 896 | * @param {Element} parent 897 | * @param {object} query 898 | * @return {boolean} 899 | */ 900 | function evaluateQuery(parent, query) { 901 | 902 | var container = getContainer(parent, query._prop); 903 | var qValues = query._values.slice(0); 904 | var i; 905 | 906 | var cValue; 907 | if (query._prop === 'width' || query._prop === 'height') { 908 | cValue = getSize(container, query._prop); 909 | } 910 | else { 911 | cValue = getComputedStyle(container).getPropertyValue(query._prop); 912 | } 913 | 914 | if (query._filter) { 915 | var color = parseColor(cValue); 916 | if (query._filter[0] === 'h') { 917 | cValue = color[0]; 918 | } 919 | else if (query._filter[0] === 's') { 920 | cValue = color[1]; 921 | } 922 | else if (query._filter[0] === 'l') { 923 | cValue = color[2]; 924 | } 925 | else if (query._filter[0] === 'a') { 926 | cValue = color[3]; 927 | } 928 | else { 929 | return false; 930 | } 931 | } 932 | else if (query._valueType === 'c') { 933 | cValue = parseColor(cValue).join(','); 934 | } 935 | else if (query._valueType === 'l') { 936 | for (i = 0; i < qValues.length; i++) { 937 | qValues[i] = getComputedLength(qValues[i], parent); 938 | } 939 | if (typeof cValue === 'string') { 940 | cValue = getComputedLength(cValue, parent); 941 | } 942 | } 943 | else if (query._valueType === 'n') { 944 | cValue = parseFloat(cValue); 945 | } 946 | else if (typeof cValue === 'string') { 947 | cValue = cValue.trim(); 948 | } 949 | 950 | if (( 951 | query._types[0][0] === '>' 952 | || query._types[0][0] === '<' 953 | ) && ( 954 | typeof cValue !== 'number' 955 | || typeof qValues[0] !== 'number' 956 | )) { 957 | return false; 958 | } 959 | 960 | for (i = 0; i < qValues.length; i++) { 961 | if (!( 962 | (query._types[i] === '>=' && cValue >= qValues[i]) 963 | || (query._types[i] === '<=' && cValue <= qValues[i]) 964 | || (query._types[i] === '>' && cValue > qValues[i]) 965 | || (query._types[i] === '<' && cValue < qValues[i]) 966 | || (query._types[i] === '=' && cValue === qValues[i]) 967 | )) { 968 | return false; 969 | } 970 | } 971 | 972 | return true; 973 | 974 | } 975 | 976 | /** 977 | * Get the nearest qualified container element starting by the element itself 978 | * 979 | * @param {Element} element 980 | * @param {string} prop CSS property 981 | * @return {Element} 982 | */ 983 | function getContainer(element, prop) { 984 | 985 | var cache; 986 | if (containerCache.has(element)) { 987 | cache = containerCache.get(element); 988 | if (cache[prop]) { 989 | return cache[prop]; 990 | } 991 | } 992 | else { 993 | cache = {}; 994 | containerCache.set(element, cache); 995 | } 996 | 997 | if (element === documentElement) { 998 | cache[prop] = element; 999 | } 1000 | 1001 | else if (prop !== 'width' && prop !== 'height') { 1002 | // Skip transparent background colors 1003 | if (prop === 'background-color' && !parseColor(getComputedStyle(element).getPropertyValue(prop))[3]) { 1004 | cache[prop] = getContainer(element.parentNode, prop); 1005 | } 1006 | else { 1007 | cache[prop] = element; 1008 | } 1009 | } 1010 | 1011 | // Skip inline elements 1012 | else if (getComputedStyle(element).display === 'inline') { 1013 | cache[prop] = getContainer(element.parentNode, prop); 1014 | } 1015 | 1016 | else if (isFixedSize(element, prop)) { 1017 | cache[prop] = element; 1018 | } 1019 | 1020 | else { 1021 | 1022 | var parentNode = element.parentNode; 1023 | 1024 | // Skip to next positioned ancestor for absolute positioned elements 1025 | if (getComputedStyle(element).position === 'absolute') { 1026 | while ( 1027 | parentNode.parentNode 1028 | && parentNode.parentNode.nodeType === 1 1029 | && ['relative', 'absolute'].indexOf(getComputedStyle(parentNode).position) === -1 1030 | ) { 1031 | parentNode = parentNode.parentNode; 1032 | } 1033 | } 1034 | 1035 | // Skip to next ancestor with a transform applied for fixed positioned elements 1036 | if (getComputedStyle(element).position === 'fixed') { 1037 | while ( 1038 | parentNode.parentNode 1039 | && parentNode.parentNode.nodeType === 1 1040 | && [undefined, 'none'].indexOf( 1041 | getComputedStyle(parentNode).transform 1042 | || getComputedStyle(parentNode).MsTransform 1043 | || getComputedStyle(parentNode).WebkitTransform 1044 | ) !== -1 1045 | ) { 1046 | parentNode = parentNode.parentNode; 1047 | } 1048 | } 1049 | 1050 | var parentContainer = getContainer(parentNode, prop); 1051 | while (getComputedStyle(parentNode).display === 'inline') { 1052 | parentNode = parentNode.parentNode; 1053 | } 1054 | if (parentNode === parentContainer && !isIntrinsicSize(element, prop)) { 1055 | cache[prop] = element; 1056 | } 1057 | else { 1058 | cache[prop] = parentContainer; 1059 | } 1060 | } 1061 | 1062 | return cache[prop]; 1063 | 1064 | } 1065 | 1066 | /** 1067 | * Is the size of the element a fixed length e.g. `1px`? 1068 | * 1069 | * @param {Element} element 1070 | * @param {string} prop `width` or `height` 1071 | * @return {boolean} 1072 | */ 1073 | function isFixedSize(element, prop) { 1074 | var originalStyle = getOriginalStyle(element, prop); 1075 | if (originalStyle && ( 1076 | originalStyle.match(LENGTH_REGEXP) 1077 | || originalStyle.match(/^calc\([^%]*\)$/i) 1078 | )) { 1079 | return true; 1080 | } 1081 | return false; 1082 | } 1083 | 1084 | /** 1085 | * Is the size of the element depending on its descendants? 1086 | * 1087 | * @param {Element} element 1088 | * @param {string} prop `width` or `height` 1089 | * @return {boolean} 1090 | */ 1091 | function isIntrinsicSize(element, prop) { 1092 | 1093 | var computedStyle = getComputedStyle(element); 1094 | 1095 | if (computedStyle.display === 'none') { 1096 | return false; 1097 | } 1098 | 1099 | if (computedStyle.display === 'inline') { 1100 | return true; 1101 | } 1102 | 1103 | // Non-floating non-absolute block elements (only width) 1104 | if ( 1105 | prop === 'width' 1106 | && ['block', 'list-item', 'flex', 'grid'].indexOf(computedStyle.display) !== -1 1107 | && computedStyle.cssFloat === 'none' 1108 | && computedStyle.position !== 'absolute' 1109 | && computedStyle.position !== 'fixed' 1110 | ) { 1111 | return false; 1112 | } 1113 | 1114 | var originalStyle = getOriginalStyle(element, prop); 1115 | 1116 | // Fixed size 1117 | if (originalStyle && originalStyle.match(LENGTH_REGEXP)) { 1118 | return false; 1119 | } 1120 | 1121 | // Percentage size 1122 | if (originalStyle && originalStyle.substr(-1) === '%') { 1123 | return false; 1124 | } 1125 | 1126 | // Calc expression 1127 | if (originalStyle && originalStyle.substr(0, 5) === 'calc(') { 1128 | return false; 1129 | } 1130 | 1131 | // Elements without a defined size 1132 | return true; 1133 | 1134 | } 1135 | 1136 | /** 1137 | * Get the computed content-box size 1138 | * 1139 | * @param {Element} element 1140 | * @param {string} prop `width` or `height` 1141 | * @return {number} 1142 | */ 1143 | function getSize(element, prop) { 1144 | var style = getComputedStyle(element); 1145 | if (prop === 'width') { 1146 | return element.offsetWidth 1147 | - parseFloat(style.borderLeftWidth) 1148 | - parseFloat(style.paddingLeft) 1149 | - parseFloat(style.borderRightWidth) 1150 | - parseFloat(style.paddingRight); 1151 | } 1152 | else { 1153 | return element.offsetHeight 1154 | - parseFloat(style.borderTopWidth) 1155 | - parseFloat(style.paddingTop) 1156 | - parseFloat(style.borderBottomWidth) 1157 | - parseFloat(style.paddingBottom); 1158 | } 1159 | } 1160 | 1161 | /** 1162 | * Get the computed length in pixel of a CSS length value 1163 | * 1164 | * @param {string} value 1165 | * @param {Element} element 1166 | * @return {number} 1167 | */ 1168 | function getComputedLength(value, element) { 1169 | var length = value.match(LENGTH_REGEXP); 1170 | if (!length) { 1171 | return parseFloat(value); 1172 | } 1173 | value = parseFloat(length[1]); 1174 | var unit = length[2].toLowerCase(); 1175 | if (FIXED_UNIT_MAP[unit]) { 1176 | return value * FIXED_UNIT_MAP[unit]; 1177 | } 1178 | if (unit === 'vw') { 1179 | return value * window.innerWidth / 100; 1180 | } 1181 | if (unit === 'vh') { 1182 | return value * window.innerHeight / 100; 1183 | } 1184 | if (unit === 'vmin') { 1185 | return value * Math.min(window.innerWidth, window.innerHeight) / 100; 1186 | } 1187 | if (unit === 'vmax') { 1188 | return value * Math.max(window.innerWidth, window.innerHeight) / 100; 1189 | } 1190 | // em units 1191 | if (unit === 'rem') { 1192 | element = documentElement; 1193 | } 1194 | if (unit === 'ex') { 1195 | value /= 2; 1196 | } 1197 | return parseFloat(getComputedStyle(element).fontSize) * value; 1198 | } 1199 | 1200 | /** 1201 | * @param {Element} element 1202 | * @return {CSSStyleDeclaration} 1203 | */ 1204 | function getComputedStyle(element) { 1205 | 1206 | var style = window.getComputedStyle(element); 1207 | 1208 | // Fix display inline in some browsers 1209 | if (style.display === 'inline' && ( 1210 | style.position === 'absolute' 1211 | || style.position === 'fixed' 1212 | || style.cssFloat !== 'none' 1213 | )) { 1214 | var newStyle = {}; 1215 | for (var prop in style) { 1216 | if (typeof style[prop] === 'string') { 1217 | newStyle[prop] = style[prop]; 1218 | } 1219 | } 1220 | style = newStyle; 1221 | style.display = 'block'; 1222 | style.getPropertyValue = function(property) { 1223 | return this[property.replace(/-+(.)/g, function(match, char) { 1224 | return char.toUpperCase(); 1225 | })]; 1226 | }; 1227 | } 1228 | 1229 | return style; 1230 | 1231 | } 1232 | 1233 | /** 1234 | * Get the original style of an element as it was specified in CSS 1235 | * 1236 | * @param {Element} element 1237 | * @param {string} prop Property to return, e.g. `width` or `height` 1238 | * @return {string} 1239 | */ 1240 | function getOriginalStyle(element, prop) { 1241 | 1242 | var matchedRules = []; 1243 | var value; 1244 | var j; 1245 | 1246 | matchedRules = sortRulesBySpecificity( 1247 | filterRulesByElementAndProp(styleCache[prop], element, prop) 1248 | ); 1249 | 1250 | // Add style attribute 1251 | matchedRules.unshift({ 1252 | _rule: { 1253 | style: element.style, 1254 | }, 1255 | }); 1256 | 1257 | // Loop through all important styles 1258 | for (j = 0; j < matchedRules.length; j++) { 1259 | if ( 1260 | (value = matchedRules[j]._rule.style.getPropertyValue(prop)) 1261 | && matchedRules[j]._rule.style.getPropertyPriority(prop) === 'important' 1262 | ) { 1263 | return value; 1264 | } 1265 | } 1266 | 1267 | // Loop through all non-important styles 1268 | for (j = 0; j < matchedRules.length; j++) { 1269 | if ( 1270 | (value = matchedRules[j]._rule.style.getPropertyValue(prop)) 1271 | && matchedRules[j]._rule.style.getPropertyPriority(prop) !== 'important' 1272 | ) { 1273 | return value; 1274 | } 1275 | } 1276 | 1277 | return undefined; 1278 | 1279 | } 1280 | 1281 | /** 1282 | * @type {CSSStyleDeclaration} 1283 | */ 1284 | var parseColorStyle = createElement('div').style; 1285 | 1286 | /** 1287 | * Parse CSS color and return as HSLA array 1288 | * 1289 | * @param {string} color 1290 | * @return {Array.} 1291 | */ 1292 | function parseColor(color) { 1293 | 1294 | // Let the browser round the RGBA values for consistency 1295 | parseColorStyle.cssText = 'color:' + color; 1296 | color = parseColorStyle.color; 1297 | 1298 | if (!color || !color.split || !color.split('(')[1]) { 1299 | return [0, 0, 0, 0]; 1300 | } 1301 | 1302 | color = color.split('(')[1].split(',').map(parseFloat); 1303 | 1304 | if (color[3] === undefined) { 1305 | color[3] = 1; 1306 | } 1307 | else if (!color[3]) { 1308 | color[0] = color[1] = color[2] = 0; 1309 | } 1310 | 1311 | color[3] = Math.round(color[3] * 255); 1312 | 1313 | return rgbaToHsla(color); 1314 | 1315 | } 1316 | 1317 | /** 1318 | * @param {Array.} color 1319 | * @return {Array.} 1320 | */ 1321 | function rgbaToHsla(color) { 1322 | 1323 | var red = color[0] / 255; 1324 | var green = color[1] / 255; 1325 | var blue = color[2] / 255; 1326 | 1327 | var max = Math.max(red, green, blue); 1328 | var min = Math.min(red, green, blue); 1329 | 1330 | var hue; 1331 | var saturation; 1332 | var lightness = (max + min) / 2; 1333 | 1334 | hue = saturation = 0; 1335 | 1336 | if (max !== min) { 1337 | var delta = max - min; 1338 | saturation = delta / (lightness > 0.5 ? 2 - max - min : max + min); 1339 | if (max === red) { 1340 | hue = (green - blue) / delta + ((green < blue) * 6); 1341 | } 1342 | else if (max === green) { 1343 | hue = (blue - red) / delta + 2; 1344 | } 1345 | else { 1346 | hue = (red - green) / delta + 4; 1347 | } 1348 | hue /= 6; 1349 | } 1350 | 1351 | return [hue * 360, saturation * 100, lightness * 100, color[3]]; 1352 | } 1353 | 1354 | /** 1355 | * Filter rules by matching the element and at least one property 1356 | * 1357 | * @param {{: Array.<{_selector: string, _rule: CSSRule}>}} rules 1358 | * @param {Element} element 1359 | * @param {string} prop 1360 | * @return {Array.<{_selector: string, _rule: CSSRule}>} 1361 | */ 1362 | function filterRulesByElementAndProp(rules, element, prop) { 1363 | var foundRules = []; 1364 | if (element.id) { 1365 | foundRules = foundRules.concat(rules['#' + element.id] || []); 1366 | } 1367 | (element.getAttribute('class') || '').split(/\s+/).forEach(function(className) { 1368 | foundRules = foundRules.concat(rules['.' + className] || []); 1369 | }); 1370 | foundRules = foundRules 1371 | .concat(rules[element.tagName.toLowerCase()] || []) 1372 | .concat(rules['*'] || []); 1373 | return foundRules.filter(function(rule) { 1374 | return rule._rule.style.getPropertyValue(prop) 1375 | && ( 1376 | !rule._rule.parentRule 1377 | || rule._rule.parentRule.type !== 4 // @media rule 1378 | || matchesMedia(rule._rule.parentRule.media.mediaText) 1379 | ) 1380 | && elementMatchesSelector(element, rule._selector); 1381 | }); 1382 | } 1383 | 1384 | var elementMatchesSelectorMethod = (function(element) { 1385 | return element.matches 1386 | || element.mozMatchesSelector 1387 | || element.msMatchesSelector 1388 | || element.oMatchesSelector 1389 | || element.webkitMatchesSelector; 1390 | })(createElement('div')); 1391 | 1392 | /** 1393 | * @param {Element} element 1394 | * @param {string} selector 1395 | * @return {boolean} 1396 | */ 1397 | function elementMatchesSelector(element, selector) { 1398 | try { 1399 | return !!elementMatchesSelectorMethod.call(element, selector); 1400 | } 1401 | catch(e) { 1402 | return false; 1403 | } 1404 | } 1405 | 1406 | /** 1407 | * @param {Array.<{_specificity: number}>} rules 1408 | * @return {Array.<{_specificity: number}>} 1409 | */ 1410 | function sortRulesBySpecificity(rules) { 1411 | return rules.map(function(rule, i) { 1412 | return [rule, i]; 1413 | }).sort(function(a, b) { 1414 | return (b[0]._specificity - a[0]._specificity) || b[1] - a[1]; 1415 | }).map(function(rule) { 1416 | return rule[0]; 1417 | }); 1418 | } 1419 | 1420 | /** 1421 | * @param {string} selector 1422 | * @return {number} 1423 | */ 1424 | function getSpecificity(selector) { 1425 | 1426 | var idScore = 0; 1427 | var classScore = 0; 1428 | var typeScore = 0; 1429 | 1430 | selector 1431 | .replace(SELECTOR_ESCAPED_REGEXP, function() { 1432 | classScore++; 1433 | return ''; 1434 | }) 1435 | .replace(SELECTOR_REGEXP, function() { 1436 | classScore++; 1437 | return ''; 1438 | }) 1439 | .replace(ATTR_REGEXP, function() { 1440 | classScore++; 1441 | return ''; 1442 | }) 1443 | .replace(PSEUDO_NOT_REGEXP, ' ') 1444 | .replace(ID_REGEXP, function() { 1445 | idScore++; 1446 | return ''; 1447 | }) 1448 | .replace(CLASS_REGEXP, function() { 1449 | classScore++; 1450 | return ''; 1451 | }) 1452 | .replace(PSEUDO_ELEMENT_REGEXP, function() { 1453 | typeScore++; 1454 | return ''; 1455 | }) 1456 | .replace(PSEUDO_CLASS_REGEXP, function() { 1457 | classScore++; 1458 | return ''; 1459 | }) 1460 | .replace(ELEMENT_REGEXP, function() { 1461 | typeScore++; 1462 | return ''; 1463 | }); 1464 | 1465 | return ( 1466 | (idScore * 256 * 256) 1467 | + (classScore * 256) 1468 | + typeScore 1469 | ); 1470 | 1471 | } 1472 | 1473 | /** 1474 | * Create a new Map or a simple shim of it in non-supporting browsers 1475 | * 1476 | * @return {Map} 1477 | */ 1478 | function createCacheMap() { 1479 | 1480 | if (typeof Map === 'function') { 1481 | return new Map(); 1482 | } 1483 | 1484 | var keys = []; 1485 | var values = []; 1486 | 1487 | function getIndex(key) { 1488 | return keys.indexOf(key); 1489 | } 1490 | 1491 | function get(key) { 1492 | return values[getIndex(key)]; 1493 | } 1494 | 1495 | function has(key) { 1496 | return getIndex(key) !== -1; 1497 | } 1498 | 1499 | function set(key, value) { 1500 | var index = getIndex(key); 1501 | if (index === -1) { 1502 | index = keys.push(key) - 1; 1503 | } 1504 | values[index] = value; 1505 | } 1506 | 1507 | function deleteFunc(key) { 1508 | var index = getIndex(key); 1509 | if (index === -1) { 1510 | return false; 1511 | } 1512 | delete keys[index]; 1513 | delete values[index]; 1514 | return true; 1515 | } 1516 | 1517 | function forEach(callback) { 1518 | keys.forEach(function(key, index) { 1519 | if (key !== undefined) { 1520 | callback(values[index], key); 1521 | } 1522 | }); 1523 | } 1524 | 1525 | return { 1526 | set: set, 1527 | get: get, 1528 | has: has, 1529 | delete: deleteFunc, 1530 | forEach: forEach, 1531 | }; 1532 | } 1533 | 1534 | /** 1535 | * @param {Element} element 1536 | * @param {{_className: string, _attribute: string}} query 1537 | * @return {boolean} 1538 | */ 1539 | function hasQuery(element, query) { 1540 | if (query._className) { 1541 | return hasClass(element, query._className); 1542 | } 1543 | return hasAttributeValue(element, 'data-cq', query._attribute); 1544 | } 1545 | 1546 | /** 1547 | * @param {Element} element 1548 | * @param {{_className: string, _attribute: string}} query 1549 | */ 1550 | function addQuery(element, query) { 1551 | if (query._className) { 1552 | addClass(element, query._className); 1553 | } 1554 | else { 1555 | addAttributeValue(element, 'data-cq', query._attribute); 1556 | } 1557 | } 1558 | 1559 | /** 1560 | * @param {Element} element 1561 | * @param {{_className: string, _attribute: string}} query 1562 | */ 1563 | function removeQuery(element, query) { 1564 | if (query._className) { 1565 | removeClass(element, query._className); 1566 | } 1567 | else { 1568 | removeAttributeValue(element, 'data-cq', query._attribute); 1569 | } 1570 | } 1571 | 1572 | /** 1573 | * @param {Element} element 1574 | * @param {string} className 1575 | */ 1576 | function hasClass(element, className) { 1577 | if (element.classList) { 1578 | return element.classList.contains(className); 1579 | } 1580 | return hasAttributeValue(element, 'class', className); 1581 | } 1582 | 1583 | /** 1584 | * @param {Element} element 1585 | * @param {string} className 1586 | */ 1587 | function addClass(element, className) { 1588 | if (element.classList) { 1589 | element.classList.add(className); 1590 | } 1591 | else if (!hasClass(element, className)) { 1592 | addAttributeValue(element, 'class', className); 1593 | } 1594 | } 1595 | 1596 | /** 1597 | * @param {Element} element 1598 | * @param {string} className 1599 | */ 1600 | function removeClass(element, className) { 1601 | if (element.classList) { 1602 | element.classList.remove(className); 1603 | } 1604 | else { 1605 | removeAttributeValue(element, 'class', className); 1606 | } 1607 | } 1608 | 1609 | /** 1610 | * @param {Element} element 1611 | * @param {string} attr 1612 | * @param {string} value 1613 | * @return {boolean} 1614 | */ 1615 | function hasAttributeValue(element, attr, value) { 1616 | return !!(element.getAttribute(attr) || '').match(new RegExp( 1617 | '(?:^|\\s+)' 1618 | + value.replace(REGEXP_ESCAPE_REGEXP, '\\$&') 1619 | + '($|\\s+)' 1620 | )); 1621 | } 1622 | 1623 | /** 1624 | * @param {Element} element 1625 | * @param {string} attr 1626 | * @param {string} value 1627 | */ 1628 | function addAttributeValue(element, attr, value) { 1629 | if (!hasAttributeValue(element, attr, value)) { 1630 | element.setAttribute(attr, (element.getAttribute(attr) || '') + ' ' + value); 1631 | } 1632 | } 1633 | 1634 | /** 1635 | * @param {Element} element 1636 | * @param {string} attr 1637 | * @param {string} value 1638 | */ 1639 | function removeAttributeValue(element, attr, value) { 1640 | element.setAttribute(attr, (element.getAttribute(attr) || '').replace( 1641 | new RegExp( 1642 | '(?:^|\\s+)' 1643 | + value.replace(REGEXP_ESCAPE_REGEXP, '\\$&') 1644 | + '($|\\s+)' 1645 | ), 1646 | '$1' 1647 | )); 1648 | } 1649 | 1650 | /** 1651 | * @param {string} media 1652 | * @return {boolean} 1653 | */ 1654 | function matchesMedia(media) { 1655 | if (window.matchMedia) { 1656 | return window.matchMedia(media).matches; 1657 | } 1658 | return (window.styleMedia || window.media).matchMedium(media); 1659 | } 1660 | 1661 | /** 1662 | * Array.from or a simple shim for non-supporting browsers 1663 | * 1664 | * @param {{length: number}} arrayLike 1665 | * @return {array} 1666 | */ 1667 | function arrayFrom(arrayLike) { 1668 | if (Array.from) { 1669 | return Array.from(arrayLike); 1670 | } 1671 | var array = []; 1672 | for (var i = 0; i < arrayLike.length; i++) { 1673 | array[i] = arrayLike[i]; 1674 | } 1675 | return array; 1676 | } 1677 | 1678 | startObserving(); 1679 | 1680 | return api; 1681 | 1682 | })); 1683 | 1684 | })(window, document); 1685 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | QUnit.module('All', { 6 | afterEach: function(assert) { 7 | if (window.BrowserStack && QUnit.config.queue.length < 5) { 8 | var done = assert.async(); 9 | window.BrowserStack.post('/_log', 'coverage: ' + JSON.stringify(window.__coverage__), function() { 10 | done(); 11 | }); 12 | } 13 | }, 14 | }); 15 | 16 | var fixture = document.getElementById('qunit-fixture'); 17 | var TEST_FILES_URL_TIME = 'http://127.0.0.1.nip.io:8889/time'; 18 | var TEST_FILES_URL_CORS = 'http://127.0.0.1.nip.io:8889/cors/test-files/'; 19 | var TEST_FILES_URL_CROSS_ORIGIN = 'http://127.0.0.1.nip.io:8888/test-files/'; 20 | var TEST_FILES_PATH = 'test-files/'; 21 | 22 | /*global reprocess, reevaluate, getOriginalStyle, processed*/ 23 | QUnit[/Opera\/9\.80\s.*Version\/12\.16/.test(navigator.userAgent) 24 | ? 'skip' 25 | : 'test' 26 | ]('CORS', function(assert) { 27 | 28 | var done = assert.async(); 29 | 30 | var element; 31 | 32 | load('cors.css', false, true, function() { 33 | assert.equal(getOriginalStyle(element, 'width'), '10%', 'Style Stylesheet'); 34 | load('cors.css', true, true, function() { 35 | assert.equal(getOriginalStyle(element, 'width'), '10%', 'Style Stylesheet with crossOrigin'); 36 | load('cors-with-cq.css', false, true, function() { 37 | assert.equal(getOriginalStyle(element, 'width'), '20%', 'Container Query'); 38 | load('cors-with-cq.css', true, true, function() { 39 | assert.equal(getOriginalStyle(element, 'width'), '20%', 'Container Query with crossOrigin'); 40 | load('cors.css', false, false, function() { 41 | assert.ok(getOriginalStyle(element, 'width') === undefined || getOriginalStyle(element, 'width') === '10%', 'Non-CORS Style Stylesheet'); 42 | assert.equal(getComputedStyle(element).color, 'rgb(255, 0, 0)', 'Non-CORS Style Stylesheet computed style'); 43 | load('cors.css', true, false, function() { 44 | assert.ok(getOriginalStyle(element, 'width') === undefined || getOriginalStyle(element, 'width') === '10%', 'Non-CORS Style Stylesheet with crossOrigin'); 45 | if ('crossOrigin' in document.createElement('link')) { 46 | assert.equal(getComputedStyle(element).color, 'rgb(0, 0, 0)', 'Non-CORS Style Stylesheet with crossOrigin computed style (crossOrigin supported)'); 47 | } 48 | else { 49 | assert.equal(getComputedStyle(element).color, 'rgb(255, 0, 0)', 'Non-CORS Style Stylesheet with crossOrigin computed style (crossOrigin not supported)'); 50 | } 51 | load('cors-with-cq.css', false, false, function() { 52 | assert.strictEqual(getOriginalStyle(element, 'width'), undefined, 'Non-CORS Container Query'); 53 | load('cors-with-cq.css', true, false, function() { 54 | assert.strictEqual(getOriginalStyle(element, 'width'), undefined, 'Non-CORS Container Query with crossOrigin'); 55 | done(); }); }); }); }); }); }); }); }); 56 | 57 | function load(file, crossOrigin, cors, callback) { 58 | 59 | fixture.innerHTML = ''; 60 | 61 | var link = document.createElement('link'); 62 | link.rel = 'stylesheet'; 63 | if (crossOrigin) { 64 | link.crossOrigin = 'anonymous'; 65 | } 66 | link.onload = link.onerror = onLoad; 67 | link.href = (cors ? TEST_FILES_URL_CORS : TEST_FILES_URL_CROSS_ORIGIN) + file; 68 | fixture.appendChild(link); 69 | 70 | element = document.createElement('div'); 71 | element.className = 'cors-test'; 72 | fixture.appendChild(element); 73 | 74 | function onLoad() { 75 | processed ? reprocess(callback) : reevaluate(true, callback); 76 | } 77 | 78 | } 79 | 80 | }); 81 | 82 | /*global reprocess, config, startObserving, observer: true, onDomMutate*/ 83 | QUnit.test('DOM Mutations', function(assert) { 84 | 85 | var element = document.createElement('div'); 86 | element.className = 'mutations-test'; 87 | fixture.appendChild(element); 88 | 89 | var done = assert.async(); 90 | 91 | reprocess(function() { 92 | 93 | delete config.skipObserving; 94 | startObserving(); 95 | 96 | assert.equal(getComputedStyle(element).display, 'block', 'Display block'); 97 | 98 | var style = document.createElement('style'); 99 | style.type = 'text/css'; 100 | style.innerHTML = '.mutations-test:container(width > 0) { display: none }'; 101 | fixture.appendChild(style); 102 | 103 | requestAnimationFrame(function() { setTimeout(function() { 104 | 105 | assert.equal(getComputedStyle(element).display, 'none', 'Display none'); 106 | 107 | var element2 = document.createElement('div'); 108 | element2.className = 'mutations-test'; 109 | fixture.appendChild(element2); 110 | 111 | var element3 = document.createElement('div'); 112 | element3.className = 'mutations-test'; 113 | fixture.appendChild(element3); 114 | fixture.removeChild(element3); 115 | 116 | requestAnimationFrame(function() { setTimeout(function() { 117 | 118 | assert.equal(getComputedStyle(element2).display, 'none', 'Display none'); 119 | 120 | fixture.appendChild(element3); 121 | assert.equal(getComputedStyle(element3).display, 'block', 'Display block'); 122 | 123 | fixture.removeChild(style); 124 | 125 | requestAnimationFrame(function() { setTimeout(function() { 126 | 127 | assert.equal(getComputedStyle(element).display, 'block', 'Display block'); 128 | assert.equal(getComputedStyle(element2).display, 'block', 'Display block'); 129 | assert.equal(getComputedStyle(element3).display, 'block', 'Display block'); 130 | 131 | // Cleanup 132 | if (observer) { 133 | observer.disconnect(); 134 | observer = undefined; 135 | } 136 | else { 137 | window.removeEventListener('DOMNodeInserted', onDomMutate); 138 | window.removeEventListener('DOMNodeRemoved', onDomMutate); 139 | } 140 | config.skipObserving = true; 141 | done(); 142 | 143 | }, 100)}); 144 | 145 | }, 100)}); 146 | 147 | }, 100)}); 148 | 149 | }); 150 | 151 | }); 152 | 153 | /*global preprocess, processedSheets, SELECTOR_ESCAPED_REGEXP, SELECTOR_REGEXP*/ 154 | QUnit.test('preprocess', function(assert) { 155 | 156 | var style = document.createElement('style'); 157 | style.type = 'text/css'; 158 | style.innerHTML = '.first:container( width >= 100.00px ) { display: block }' 159 | + '.second:container( height <= 10em ) > child { display: block }' 160 | + '.third:container( width <= 100px ), .fourth:container( height >= 100px ) { display: block }'; 161 | fixture.appendChild(style); 162 | 163 | var style2 = document.createElement('style'); 164 | style2.type = 'text/css'; 165 | style2.innerHTML = '.foo { display: block }'; 166 | fixture.appendChild(style2); 167 | 168 | var done = assert.async(); 169 | 170 | preprocess(function () { 171 | 172 | var newStyle = style.previousSibling; 173 | var rules = newStyle.sheet.cssRules; 174 | 175 | assert.equal(style.sheet.disabled, true, 'Old stylesheet disabled'); 176 | assert.equal(newStyle.sheet.disabled, false, 'New stylesheet enabled'); 177 | assert.equal(style2.sheet.disabled, false, 'Normal stylesheet still enabled'); 178 | assert.equal(rules.length, 3, 'Three rules'); 179 | assert.equal(newStyle.innerHTML.match(SELECTOR_ESCAPED_REGEXP).length, 4, 'Four container queries'); 180 | assert.ok(rules[0].selectorText.match(SELECTOR_ESCAPED_REGEXP) || rules[0].selectorText.match(SELECTOR_REGEXP), 'Query matches either the escaped or unescaped RegExp'); 181 | 182 | style.parentNode.removeChild(style); 183 | style2.parentNode.removeChild(style2); 184 | 185 | preprocess(function () { 186 | assert.notOk(newStyle.parentNode, 'New stylesheet removed from the DOM'); 187 | assert.equal(processedSheets.has(style), false, 'Old stylesheet removed from processedSheets cache map'); 188 | done(); 189 | }); 190 | 191 | }); 192 | }); 193 | 194 | /*global preprocessSheet*/ 195 | QUnit.test('preprocessSheet', function(assert) { 196 | 197 | var allDone = assert.async(); 198 | var doneCount = 0; 199 | var done = function() { 200 | doneCount++; 201 | if (doneCount >= 4) { 202 | allDone(); 203 | } 204 | }; 205 | 206 | var link = document.createElement('link'); 207 | link.rel = 'stylesheet'; 208 | link.href = TEST_FILES_PATH + 'cors-with-cq.css'; 209 | link.onload = link.onerror = onLoad; 210 | fixture.appendChild(link); 211 | 212 | function onLoad() { 213 | 214 | link.sheet.disabled = true; 215 | var calledback = false; 216 | preprocessSheet(link.sheet, function() { 217 | calledback = true; 218 | assert.notOk(link.previousSibling, 'Disabled stylesheet not preprocessed'); 219 | }); 220 | assert.ok(calledback, 'Disabled stylesheet callback instantly'); 221 | link.sheet.disabled = false; 222 | 223 | calledback = false; 224 | preprocessSheet({}, function() { 225 | calledback = true; 226 | assert.notOk(link.previousSibling, 'Stylesheet without ownerNode not preprocessed'); 227 | }); 228 | assert.ok(calledback, 'Stylesheet without ownerNode callback instantly'); 229 | 230 | for (var i = 0; i < 4; i++) { 231 | preprocessSheet(link.sheet, function() { 232 | assert.ok(fixture.getElementsByTagName('style').length <= 1, 'Calling multiple times doesn’t duplicate the styles'); 233 | done(); 234 | }); 235 | } 236 | 237 | } 238 | 239 | }); 240 | 241 | /*global escapeSelectors*/ 242 | QUnit.test('escapeSelectors', function(assert) { 243 | assert.equal(escapeSelectors(':container( WIDTH > 100px )'), '.\\:container\\(width\\>100px\\)', 'Simple query'); 244 | assert.equal(escapeSelectors(':container(width > 100px < 200px)'), '.\\:container\\(width\\>100px\\<200px\\)', 'Double comparison'); 245 | assert.equal(escapeSelectors(':container(color-lightness < 10%)'), '.\\:container\\(color-lightness\\<10\\%\\)', 'Filter parameter'); 246 | assert.equal(escapeSelectors(':container( " width <= 100.00px")'), '.\\:container\\(width\\<\\=100\\.00px\\)', 'Query with quotes'); 247 | assert.equal(escapeSelectors(':container(min-width: 100.00px)'), '.\\:container\\(min-width\\:100\\.00px\\)', 'Min prefix'); 248 | assert.equal(escapeSelectors(':container( " MAX-WIDTH : 100.00px")'), '.\\:container\\(max-width\\:100\\.00px\\)', 'Max prefix with quotes'); 249 | assert.equal(escapeSelectors(':container(color: rgba(255, 0, 0, 1))'), '.\\:container\\(color\\:rgba\\(255\\,0\\,0\\,1\\)\\)', 'Color query rgba()'); 250 | }); 251 | 252 | /*global parseRules, queries*/ 253 | QUnit.test('parseRules', function(assert) { 254 | var style = document.createElement('style'); 255 | style.type = 'text/css'; 256 | style.innerHTML = '.foo:active:hover:focus:checked .before:container( WIDTH >= 100.00px ).after>child { display: block }' 257 | + ':container(height < 10em) .general-selector { display: block }' 258 | + '.combined-selector:container(text-align: right):container(height > 100px) { display: block }' 259 | + '.double-comparison:container(200px > width > 100px) { display: block }' 260 | + '.filter:container(color-lightness < 10%) { display: block }' 261 | + '.max-filter:container(max-background-color-lightness: 10%) { display: block }' 262 | + '.color:container(background-color: rgb(255, 0, 0)) { display: block }' 263 | + ':nth-of-type(2n+1):container(width > 100px) { display: block }' 264 | + '.pseudo-before:container(width > 100px):before { display: block }' 265 | + '.pseudo-after:container(width > 100px)::after { display: block }' 266 | + '@media screen { .inside-media-query:container(height < 10em) { display: block } }' 267 | + '.attribute[data-cq~="max-width:100.0px"] { display: block }' 268 | + '.attribute-single[data-cq~=\'color-alpha<=10%\'] { display: block }'; 269 | fixture.appendChild(style); 270 | var done = assert.async(); 271 | preprocess(function () { 272 | 273 | parseRules(); 274 | assert.equal(Object.keys(queries).length, 14, '14 queries'); 275 | 276 | assert.ok(Object.keys(queries)[0].match(/^\.foo (?:\.before|\.after){2}\.\\:container\\\(width\\>\\=100\\\.00px\\\)$/), 'Correct key'); 277 | assert.ok(queries[Object.keys(queries)[0]]._selector.match(/^\.foo (?:\.before|\.after){2}$/), 'Preceding selector'); 278 | assert.equal(queries[Object.keys(queries)[0]]._prop, 'width', 'Property'); 279 | assert.deepEqual(queries[Object.keys(queries)[0]]._types, ['>='], 'Mode'); 280 | assert.deepEqual(queries[Object.keys(queries)[0]]._values, ['100.00px'], 'Value'); 281 | assert.deepEqual(queries[Object.keys(queries)[0]]._valueType, 'l', 'Value type'); 282 | assert.equal(queries[Object.keys(queries)[0]]._className, ':container(width>=100.00px)', 'Class name'); 283 | 284 | assert.equal(Object.keys(queries)[1], '*.\\:container\\(height\\<10em\\)', 'Correct key'); 285 | assert.equal(queries[Object.keys(queries)[1]]._selector, '*', 'Preceding selector'); 286 | assert.equal(queries[Object.keys(queries)[1]]._prop, 'height', 'Property'); 287 | assert.deepEqual(queries[Object.keys(queries)[1]]._types, ['<'], 'Mode'); 288 | assert.deepEqual(queries[Object.keys(queries)[1]]._values, ['10em'], 'Value'); 289 | assert.deepEqual(queries[Object.keys(queries)[1]]._valueType, 'l', 'Value type'); 290 | assert.equal(queries[Object.keys(queries)[1]]._className, ':container(height<10em)', 'Class name'); 291 | 292 | // Fix CSS class sorting for IE/Edge 293 | var combinedKeys = [Object.keys(queries)[2], Object.keys(queries)[3]].sort().reverse(); 294 | 295 | assert.equal(combinedKeys[0], '.combined-selector.\\:container\\(text-align\\:right\\)', 'Correct key'); 296 | assert.equal(queries[combinedKeys[0]]._selector, '.combined-selector', 'Preceding selector'); 297 | assert.equal(queries[combinedKeys[0]]._prop, 'text-align', 'Property'); 298 | assert.deepEqual(queries[combinedKeys[0]]._types, ['='], 'Mode'); 299 | assert.deepEqual(queries[combinedKeys[0]]._values, ['right'], 'Value'); 300 | assert.deepEqual(queries[combinedKeys[0]]._valueType, 's', 'Value type'); 301 | assert.equal(queries[combinedKeys[0]]._className, ':container(text-align:right)', 'Class name'); 302 | 303 | assert.equal(combinedKeys[1], '.combined-selector.\\:container\\(height\\>100px\\)', 'Correct key'); 304 | assert.equal(queries[combinedKeys[1]]._selector, '.combined-selector', 'Preceding selector'); 305 | assert.equal(queries[combinedKeys[1]]._prop, 'height', 'Property'); 306 | assert.deepEqual(queries[combinedKeys[1]]._types, ['>'], 'Mode'); 307 | assert.deepEqual(queries[combinedKeys[1]]._values, ['100px'], 'Value'); 308 | assert.deepEqual(queries[combinedKeys[1]]._valueType, 'l', 'Value type'); 309 | assert.equal(queries[combinedKeys[1]]._className, ':container(height>100px)', 'Class name'); 310 | 311 | assert.equal(Object.keys(queries)[4], '.double-comparison.\\:container\\(200px\\>width\\>100px\\)', 'Correct key'); 312 | assert.equal(queries[Object.keys(queries)[4]]._selector, '.double-comparison', 'Preceding selector'); 313 | assert.equal(queries[Object.keys(queries)[4]]._prop, 'width', 'Property'); 314 | assert.deepEqual(queries[Object.keys(queries)[4]]._types, ['>', '<'], 'Mode'); 315 | assert.deepEqual(queries[Object.keys(queries)[4]]._values, ['100px', '200px'], 'Value'); 316 | assert.deepEqual(queries[Object.keys(queries)[4]]._valueType, 'l', 'Value type'); 317 | assert.equal(queries[Object.keys(queries)[4]]._className, ':container(200px>width>100px)', 'Class name'); 318 | 319 | assert.equal(Object.keys(queries)[5], '.filter.\\:container\\(color-lightness\\<10\\%\\)', 'Correct key'); 320 | assert.equal(queries[Object.keys(queries)[5]]._selector, '.filter', 'Preceding selector'); 321 | assert.equal(queries[Object.keys(queries)[5]]._prop, 'color', 'Property'); 322 | assert.deepEqual(queries[Object.keys(queries)[5]]._filter, 'lightness', 'Filter'); 323 | assert.deepEqual(queries[Object.keys(queries)[5]]._types, ['<'], 'Mode'); 324 | assert.deepEqual(queries[Object.keys(queries)[5]]._values, [10], 'Value'); 325 | assert.deepEqual(queries[Object.keys(queries)[5]]._valueType, 'n', 'Value type'); 326 | assert.equal(queries[Object.keys(queries)[5]]._className, ':container(color-lightness<10%)', 'Class name'); 327 | 328 | assert.equal(Object.keys(queries)[6], '.max-filter.\\:container\\(max-background-color-lightness\\:10\\%\\)', 'Correct key'); 329 | assert.equal(queries[Object.keys(queries)[6]]._selector, '.max-filter', 'Preceding selector'); 330 | assert.equal(queries[Object.keys(queries)[6]]._prop, 'background-color', 'Property'); 331 | assert.deepEqual(queries[Object.keys(queries)[6]]._filter, 'lightness', 'Filter'); 332 | assert.deepEqual(queries[Object.keys(queries)[6]]._types, ['<='], 'Mode'); 333 | assert.deepEqual(queries[Object.keys(queries)[6]]._values, [10], 'Value'); 334 | assert.deepEqual(queries[Object.keys(queries)[6]]._valueType, 'n', 'Value type'); 335 | assert.equal(queries[Object.keys(queries)[6]]._className, ':container(max-background-color-lightness:10%)', 'Class name'); 336 | 337 | //.color:container(background-color: rgb(255, 0, 0)) 338 | assert.equal(Object.keys(queries)[7], '.color.\\:container\\(background-color\\:rgb\\(255\\,0\\,0\\)\\)', 'Correct key'); 339 | assert.equal(queries[Object.keys(queries)[7]]._selector, '.color', 'Preceding selector'); 340 | assert.equal(queries[Object.keys(queries)[7]]._prop, 'background-color', 'Property'); 341 | assert.deepEqual(queries[Object.keys(queries)[7]]._types, ['='], 'Mode'); 342 | assert.deepEqual(queries[Object.keys(queries)[7]]._values, ['0,100,50,255'], 'Value'); 343 | assert.deepEqual(queries[Object.keys(queries)[7]]._valueType, 'c', 'Value type'); 344 | assert.equal(queries[Object.keys(queries)[7]]._className, ':container(background-color:rgb(255,0,0))', 'Class name'); 345 | 346 | assert.equal(Object.keys(queries)[8], ':nth-of-type(2n+1).\\:container\\(width\\>100px\\)', 'Correct key'); 347 | assert.equal(queries[Object.keys(queries)[8]]._selector, ':nth-of-type(2n+1)', 'Preceding selector'); 348 | assert.equal(queries[Object.keys(queries)[8]]._prop, 'width', 'Property'); 349 | assert.deepEqual(queries[Object.keys(queries)[8]]._types, ['>'], 'Mode'); 350 | assert.deepEqual(queries[Object.keys(queries)[8]]._values, ['100px'], 'Value'); 351 | assert.deepEqual(queries[Object.keys(queries)[8]]._valueType, 'l', 'Value type'); 352 | assert.equal(queries[Object.keys(queries)[8]]._className, ':container(width>100px)', 'Class name'); 353 | 354 | assert.equal(Object.keys(queries)[9], '.pseudo-before.\\:container\\(width\\>100px\\)', 'Correct key'); 355 | assert.equal(queries[Object.keys(queries)[9]]._selector, '.pseudo-before', 'Preceding selector'); 356 | assert.equal(queries[Object.keys(queries)[9]]._prop, 'width', 'Property'); 357 | assert.deepEqual(queries[Object.keys(queries)[9]]._types, ['>'], 'Mode'); 358 | assert.deepEqual(queries[Object.keys(queries)[9]]._values, ['100px'], 'Value'); 359 | assert.deepEqual(queries[Object.keys(queries)[9]]._valueType, 'l', 'Value type'); 360 | assert.equal(queries[Object.keys(queries)[9]]._className, ':container(width>100px)', 'Class name'); 361 | 362 | assert.equal(Object.keys(queries)[10], '.pseudo-after.\\:container\\(width\\>100px\\)', 'Correct key'); 363 | assert.equal(queries[Object.keys(queries)[10]]._selector, '.pseudo-after', 'Preceding selector'); 364 | assert.equal(queries[Object.keys(queries)[10]]._prop, 'width', 'Property'); 365 | assert.deepEqual(queries[Object.keys(queries)[10]]._types, ['>'], 'Mode'); 366 | assert.deepEqual(queries[Object.keys(queries)[10]]._values, ['100px'], 'Value'); 367 | assert.deepEqual(queries[Object.keys(queries)[10]]._valueType, 'l', 'Value type'); 368 | assert.equal(queries[Object.keys(queries)[10]]._className, ':container(width>100px)', 'Class name'); 369 | 370 | assert.equal(Object.keys(queries)[11], '.inside-media-query.\\:container\\(height\\<10em\\)', 'Correct key'); 371 | 372 | assert.ok(Object.keys(queries)[12].match(/^\.attribute\[data-cq~=["']max-width:100\.0px["']\]$/), 'Correct key'); 373 | assert.equal(queries[Object.keys(queries)[12]]._selector, '.attribute', 'Preceding selector'); 374 | assert.equal(queries[Object.keys(queries)[12]]._prop, 'width', 'Property'); 375 | assert.deepEqual(queries[Object.keys(queries)[12]]._types, ['<='], 'Mode'); 376 | assert.deepEqual(queries[Object.keys(queries)[12]]._values, ['100.0px'], 'Value'); 377 | assert.deepEqual(queries[Object.keys(queries)[12]]._valueType, 'l', 'Value type'); 378 | assert.equal(queries[Object.keys(queries)[12]]._attribute, 'max-width:100.0px', 'Attribute name'); 379 | 380 | assert.ok(Object.keys(queries)[13].match(/^\.attribute-single\[data-cq~=["']color-alpha<=10%["']\]$/), 'Correct key'); 381 | assert.equal(queries[Object.keys(queries)[13]]._selector, '.attribute-single', 'Preceding selector'); 382 | assert.equal(queries[Object.keys(queries)[13]]._prop, 'color', 'Property'); 383 | assert.equal(queries[Object.keys(queries)[13]]._filter, 'alpha', 'Filter'); 384 | assert.deepEqual(queries[Object.keys(queries)[13]]._types, ['<='], 'Mode'); 385 | assert.deepEqual(queries[Object.keys(queries)[13]]._values, [10], 'Value'); 386 | assert.deepEqual(queries[Object.keys(queries)[13]]._valueType, 'n', 'Value type'); 387 | assert.equal(queries[Object.keys(queries)[13]]._attribute, 'color-alpha<=10%', 'Attribute name'); 388 | 389 | done(); 390 | }); 391 | }); 392 | 393 | /*global loadExternal*/ 394 | QUnit.test('loadExternal', function(assert) { 395 | 396 | var allDone = assert.async(); 397 | var doneCount = 0; 398 | var done = function() { 399 | doneCount++; 400 | if (doneCount >= 9) { 401 | allDone(); 402 | } 403 | }; 404 | 405 | loadExternal(TEST_FILES_PATH + 'test.txt', function(response) { 406 | assert.strictEqual(response, 'test\n', 'Regular request'); 407 | done(); 408 | }); 409 | 410 | loadExternal(TEST_FILES_URL_CORS + 'test.txt', function(response) { 411 | assert.strictEqual(response, 'test\n', 'CORS request'); 412 | done(); 413 | }); 414 | 415 | loadExternal(TEST_FILES_PATH + '404', function(response) { 416 | assert.strictEqual(response, '', 'Regular 404 request'); 417 | done(); 418 | }); 419 | 420 | loadExternal(TEST_FILES_URL_CORS + '404', function(response) { 421 | assert.strictEqual(response, '', 'CORS 404 request'); 422 | done(); 423 | }); 424 | 425 | loadExternal('http://google.com/', function(response) { 426 | assert.strictEqual(response, '', 'Invalid CORS request'); 427 | done(); 428 | }); 429 | 430 | loadExternal('invalid-protocol://foo', function(response) { 431 | assert.strictEqual(response, '', 'Invalid protocol request'); 432 | done(); 433 | }); 434 | 435 | var firstResponse; 436 | for (var i = 0; i < 3; i++) { 437 | loadExternal(TEST_FILES_URL_TIME, function(response1) { 438 | if (!firstResponse) { 439 | firstResponse = response1; 440 | } 441 | else { 442 | assert.strictEqual(response1, firstResponse, 'Cached response'); 443 | } 444 | setTimeout(function() { 445 | loadExternal(TEST_FILES_URL_TIME, function(response2) { 446 | assert.strictEqual(response2, firstResponse, 'Cached response'); 447 | done(); 448 | }); 449 | }); 450 | }); 451 | } 452 | 453 | }); 454 | 455 | /*global fixRelativeUrls*/ 456 | QUnit.test('fixRelativeUrls', function(assert) { 457 | var data = { 458 | 'url()': 'url()', 459 | 'url( \t)': 'url( \t)', 460 | 'url(foo)': 'url("http://example.org/foo")', 461 | 'url("foo")': 'url("http://example.org/foo")', 462 | 'url(\'foo\')': 'url(\'http://example.org/foo\')', 463 | 'url( foo \t)': 'url("http://example.org/foo")', 464 | 'url( "foo" \t)': 'url("http://example.org/foo")', 465 | 'url( \'foo\' \t)': 'url(\'http://example.org/foo\')', 466 | 'url("http://not.example.com/foo")': 'url("http://not.example.com/foo")', 467 | }; 468 | Object.keys(data).forEach(function(css) { 469 | assert.equal(fixRelativeUrls(css, 'http://example.org/'), data[css], css + ' => ' + data[css]); 470 | }); 471 | }); 472 | 473 | /*global resolveRelativeUrl*/ 474 | QUnit.test('resolveRelativeUrl', function(assert) { 475 | var base = 'http://example.com/dir/file.ext?query#anchor'; 476 | assert.equal(resolveRelativeUrl('http://example.org', base), 'http://example.org/', 'Absolute URL'); 477 | assert.equal(resolveRelativeUrl('//example.org', base), 'http://example.org/', 'Protocol relative'); 478 | assert.equal(resolveRelativeUrl('/foo', base), 'http://example.com/foo', 'Domain relative'); 479 | assert.equal(resolveRelativeUrl('foo', base), 'http://example.com/dir/foo', 'Directory relative'); 480 | assert.equal(resolveRelativeUrl('?foo', base), 'http://example.com/dir/file.ext?foo', 'Query relative'); 481 | assert.equal(resolveRelativeUrl('#foo', base), 'http://example.com/dir/file.ext?query#foo', 'Anchor relative'); 482 | var link = document.createElement('a'); 483 | link.href = '/'; 484 | assert.equal(link.href, document.location.protocol + '//' + document.location.href.split('://')[1].split('/')[0] + '/', 'Opera tag bug'); 485 | }); 486 | 487 | /*global splitSelectors*/ 488 | QUnit.test('splitSelectors', function(assert) { 489 | assert.deepEqual(splitSelectors('foo'), ['foo'], 'Simple selector doesn’t get split'); 490 | assert.deepEqual(splitSelectors('foo,foo\t\n ,\t\n foo'), ['foo', 'foo', 'foo'], 'Simple selectors do get split'); 491 | assert.deepEqual(splitSelectors('foo:matches(foo, foo), bar'), ['foo:matches(foo, foo)', 'bar'], 'Simple selectors with :matches()'); 492 | assert.deepEqual(splitSelectors('.fo\\,o[attr="val,u\\",e"],bar'), ['.fo\\,o[attr="val,u\\",e"]', 'bar'], 'Escaped commas don’t split a selector'); 493 | assert.deepEqual(splitSelectors('.\\:container\\(font-family\\=f\\,oo\\),bar'), ['.\\:container\\(font-family\\=f\\,oo\\)', 'bar'], 'Container query with comma'); 494 | assert.deepEqual(splitSelectors('.:container(font-family=f,oo),bar'), ['.:container(font-family=f,oo)', 'bar'], 'Unescaped container query with comma'); 495 | assert.deepEqual(splitSelectors(''), [], 'Empty string'); 496 | }); 497 | 498 | /*global buildStyleCacheFromRules, styleCache: true*/ 499 | QUnit.test('buildStyleCacheFromRules', function(assert) { 500 | 501 | // Clean cache 502 | styleCache = { 503 | width: {}, 504 | height: {}, 505 | }; 506 | 507 | var style = document.createElement('style'); 508 | style.textContent = '.width { width: 100% }' 509 | + '.height { height: 100% }' 510 | + '.no-relevant-prop { color: red }' 511 | + '.pseudo-element::foo { width: 100% }' 512 | + '.pseudo-element:after { width: 100% }' 513 | + '.not-selector:not(foo) { width: 100% }' 514 | + 'foo > bar ~ baz element.class#id { width: 100% }' 515 | + 'foo > bar ~ baz element.class { width: 100% }' 516 | + 'foo > bar ~ baz element { width: 100% }' 517 | + '.star-selector * { width: 100% }' 518 | + '.implicit-star-selector :hover { width: 100% }' 519 | + '@media screen { .inside-media-query { width: 100% } }'; 520 | 521 | document.head.appendChild(style); 522 | var rules = style.sheet.cssRules; 523 | 524 | buildStyleCacheFromRules(rules); 525 | 526 | assert.equal(Object.keys(styleCache.height).length, 1, 'One height rule'); 527 | assert.equal(styleCache.height['.height'].length, 1, 'One rule for `.height`'); 528 | assert.equal(styleCache.height['.height'][0]._rule.style.height, '100%', 'Correct rule value'); 529 | 530 | assert.equal(Object.keys(styleCache.width).length, 7, 'Seven width rules'); 531 | assert.equal(styleCache.width['.width'].length, 1, 'One rule for `.width`'); 532 | assert.equal(styleCache.width['.not-selector'].length, 1, 'One rule for `.not-selector`'); 533 | assert.equal(styleCache.width['#id'].length, 1, 'One rule for `#id`'); 534 | assert.equal(styleCache.width['.class'].length, 1, 'One rule for `.class`'); 535 | assert.equal(styleCache.width.element.length, 1, 'One rule for `element`'); 536 | assert.equal(styleCache.width['*'].length, 2, 'Two rules for `*`'); 537 | assert.equal(styleCache.width['.inside-media-query'].length, 1, 'One rule for `.inside-media-query`'); 538 | 539 | document.head.removeChild(style); 540 | 541 | // Reset cache 542 | buildStyleCache(); 543 | 544 | }); 545 | 546 | /*global evaluateQuery, containerCache: true, styleCache: true*/ 547 | QUnit.test('evaluateQuery', function(assert) { 548 | 549 | // Clean caches 550 | styleCache = { 551 | width: {}, 552 | height: {}, 553 | }; 554 | containerCache = createCacheMap(); 555 | 556 | var element = document.createElement('div'); 557 | element.style.cssText = 'width: 100px; height: 100px; font-size: 10px; opacity: 0.5; background: red'; 558 | fixture.appendChild(element); 559 | 560 | var data = [ 561 | ['>', 99, 100], 562 | ['<', 101, 100], 563 | ['>=', 100, 101], 564 | ['<=', 100, 99], 565 | ['=', 100, 50], 566 | ]; 567 | data.forEach(function(item) { 568 | assert.strictEqual(evaluateQuery(element, {_prop: 'width', _types: [item[0]], _values: [item[1] + 'px'], _valueType: 'l'}), true, 'Width 100 ' + item[0] + ' ' + item[1]); 569 | assert.strictEqual(evaluateQuery(element, {_prop: 'width', _types: [item[0]], _values: [item[2] + 'px'], _valueType: 'l'}), false, 'Width 100 not ' + item[0] + ' ' + item[2]); 570 | assert.strictEqual(evaluateQuery(element, {_prop: 'height', _types: [item[0]], _values: [item[1] + 'px'], _valueType: 'l'}), true, 'Height 100 ' + item[0] + ' ' + item[1]); 571 | assert.strictEqual(evaluateQuery(element, {_prop: 'height', _types: [item[0]], _values: [item[2] + 'px'], _valueType: 'l'}), false, 'Height 100 not ' + item[0] + ' ' + item[2]); 572 | }); 573 | 574 | data = [ 575 | ['width', '=', 'l', '10em', '9.9em'], 576 | ['display', '=', 's', 'block', 'inline'], 577 | ['visibility', '=', 's', 'visible', 'hidden'], 578 | ['opacity', '=', 'n', 0.500, 1], 579 | ['opacity', '>', 'n', 0.49, 0.5], 580 | ['font-size', '=', 'l', '7.50pt', '10em'], 581 | ['background-color', '=', 'c', '0,100,50,255', '0,100,49,255'], 582 | ]; 583 | data.forEach(function(item) { 584 | assert.strictEqual(evaluateQuery(element, {_prop: item[0], _types: [item[1]], _values: [item[3]], _valueType: item[2]}), true, item[0] + ' ' + item[1] + ' ' + item[3]); 585 | assert.strictEqual(evaluateQuery(element, {_prop: item[0], _types: [item[1]], _values: [item[4]], _valueType: item[2]}), false, item[0] + ' not ' + item[1] + ' ' + item[4]); 586 | }); 587 | 588 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'hue', _types: ['='], _values: [parseFloat('0deg')], _valueType: 'n'}), true, 'Red Hue = 0deg'); 589 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'hue', _types: ['>'], _values: [parseFloat('10deg')], _valueType: 'n'}), false, 'Red Hue not > 10deg'); 590 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'saturation', _types: ['='], _values: [parseFloat('100%')], _valueType: 'n'}), true, 'Red Saturation = 100%'); 591 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'saturation', _types: ['<'], _values: [parseFloat('90%')], _valueType: 'n'}), false, 'Red Saturation not < 90%'); 592 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'lightness', _types: ['>'], _values: [parseFloat('10%')], _valueType: 'n'}), true, 'Red Lightness > 10%'); 593 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'lightness', _types: ['<'], _values: [parseFloat('10%')], _valueType: 'n'}), false, 'Red Lightness not < 10%'); 594 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'alpha', _types: ['='], _values: [parseFloat('255')], _valueType: 'n'}), true, 'Red Alpha = 255'); 595 | assert.strictEqual(evaluateQuery(element, {_prop: 'background-color', _filter: 'alpha', _types: ['<'], _values: [parseFloat('254')], _valueType: 'n'}), false, 'Red Alpha not < 254'); 596 | 597 | assert.strictEqual(evaluateQuery(element, {_prop: 'display', _types: ['<'], _values: ['10px'], _valueType: 'l'}), false, 'Invalid block < 10px'); 598 | assert.strictEqual(evaluateQuery(element, {_prop: 'display', _types: ['>'], _values: ['10px'], _valueType: 'l'}), false, 'Invalid block > 10px'); 599 | assert.strictEqual(evaluateQuery(element, {_prop: 'invalid', _types: ['<'], _values: ['10px'], _valueType: 'l'}), false, 'Invalid undefined < 10px'); 600 | assert.strictEqual(evaluateQuery(element, {_prop: 'invalid', _types: ['>'], _values: ['10px'], _valueType: 'l'}), false, 'Invalid undefined > 10px'); 601 | assert.strictEqual(evaluateQuery(element, {_prop: 'font-size', _types: ['<'], _values: ['foo'], _valueType: 's'}), false, 'Invalid 10px < foo'); 602 | assert.strictEqual(evaluateQuery(element, {_prop: 'font-size', _types: ['>'], _values: ['foo'], _valueType: 's'}), false, 'Invalid 10px > foo'); 603 | assert.strictEqual(evaluateQuery(element, {_prop: 'font-size', _types: ['<'], _values: [''], _valueType: 's'}), false, 'Invalid 10px < ""'); 604 | assert.strictEqual(evaluateQuery(element, {_prop: 'font-size', _types: ['>'], _values: [''], _valueType: 's'}), false, 'Invalid 10px > ""'); 605 | assert.strictEqual(evaluateQuery(element, {_prop: 'width', _filter: 'invalid', _types: ['>'], _values: ['0px'], _valueType: 'l'}), false, 'Invalid filter'); 606 | assert.strictEqual(evaluateQuery(element, {_prop: 'width', _types: ['='], _values: ['auto'], _valueType: 's'}), false, 'Invalid width = auto'); 607 | 608 | }); 609 | 610 | /*global getContainer, styleCache: true, containerCache: true, createCacheMap*/ 611 | QUnit.test('getContainer', function(assert) { 612 | 613 | styleCache = { 614 | width: {}, 615 | height: {}, 616 | }; 617 | containerCache = createCacheMap(); 618 | 619 | var element = document.createElement('div'); 620 | element.innerHTML = '
'; 621 | document.body.appendChild(element); 622 | var link = element.getElementsByTagName('a')[0]; 623 | var float = link.parentNode; 624 | var span = float.parentNode; 625 | 626 | assert.strictEqual(getContainer(link, 'width'), element, 'Parent
for width'); 627 | assert.strictEqual(getContainer(link, 'height'), document.documentElement, 'Document element for height'); 628 | 629 | span.style.display = 'block'; 630 | containerCache = createCacheMap(); // Clear cache 631 | assert.strictEqual(getContainer(link, 'width'), span, ' display block for width'); 632 | 633 | element.style.height = '100px'; 634 | containerCache = createCacheMap(); // Clear cache 635 | assert.strictEqual(getContainer(link, 'height'), element, '
fixed height'); 636 | 637 | span.style.cssText = 'display: block; height: 50%'; 638 | containerCache = createCacheMap(); // Clear cache 639 | assert.strictEqual(getContainer(link, 'height'), span, ' display block percentage height'); 640 | assert.ok(containerCache.has(link)); 641 | 642 | element.style.position = 'relative'; 643 | link.style.position = 'absolute'; 644 | containerCache = createCacheMap(); // Clear cache 645 | assert.strictEqual(getContainer(link, 'width'), element, '
positioned ancestor'); 646 | 647 | span.style.position = 'absolute'; 648 | containerCache = createCacheMap(); // Clear cache 649 | assert.strictEqual(getContainer(link, 'width'), element, '
positioned ancestor with non-intrinsic size'); 650 | 651 | span.style.width = '100px'; 652 | containerCache = createCacheMap(); // Clear cache 653 | assert.strictEqual(getContainer(link, 'width'), span, ' positioned ancestor with fixed size'); 654 | 655 | link.style.position = 'fixed'; 656 | containerCache = createCacheMap(); // Clear cache 657 | assert.strictEqual(getContainer(link, 'width'), document.documentElement, ' fixed ancestor'); 658 | 659 | element.style.cssText = '-webkit-transform: translateX(0); -ms-transform: translateX(0); transform: translateX(0);'; 660 | containerCache = createCacheMap(); // Clear cache 661 | assert.strictEqual(getContainer(link, 'width'), element, '
ancestor with transform applied'); 662 | 663 | document.body.removeChild(element); 664 | 665 | }); 666 | 667 | /*global isFixedSize*/ 668 | QUnit.test('isFixedSize', function(assert) { 669 | 670 | var element = document.createElement('div'); 671 | fixture.appendChild(element); 672 | 673 | assert.equal(isFixedSize(element, 'width'), false, 'Standard
width'); 674 | assert.equal(isFixedSize(element, 'height'), false, 'Standard
height'); 675 | 676 | element.style.cssText = 'width: 100%; height: 100%'; 677 | assert.equal(isFixedSize(element, 'width'), false, 'Percentage width'); 678 | assert.equal(isFixedSize(element, 'height'), false, 'Percentage height'); 679 | 680 | element.style.cssText = 'width: 100px; height: 100px'; 681 | assert.equal(isFixedSize(element, 'width'), true, 'Fixed width'); 682 | assert.equal(isFixedSize(element, 'height'), true, 'Fixed height'); 683 | 684 | element.style.cssText = 'width: 50%; width: calc(100% / 2 + 100px); height: 50%; height: calc(100% / 2 + 100px)'; 685 | assert.equal(isFixedSize(element, 'width'), false, 'Percentage calc expression width'); 686 | assert.equal(isFixedSize(element, 'height'), false, 'Percentage calc expression height'); 687 | 688 | element.style.cssText = 'width: 50px; width: calc(100px + 10em / 2); height: 50px; height: calc(100px + 10em / 2)'; 689 | assert.equal(isFixedSize(element, 'width'), true, 'Fixed calc expression width'); 690 | assert.equal(isFixedSize(element, 'height'), true, 'Fixed calc expression height'); 691 | 692 | }); 693 | 694 | /*global isIntrinsicSize*/ 695 | QUnit.test('isIntrinsicSize', function(assert) { 696 | 697 | var element = document.createElement('div'); 698 | fixture.appendChild(element); 699 | 700 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Standard
width'); 701 | assert.equal(isIntrinsicSize(element, 'height'), true, 'Standard
height'); 702 | 703 | element.style.cssText = 'display: inline'; 704 | assert.equal(isIntrinsicSize(element, 'width'), true, 'Display inline width'); 705 | assert.equal(isIntrinsicSize(element, 'height'), true, 'Display inline height'); 706 | 707 | element.style.cssText = 'display: none'; 708 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Display none width'); 709 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Display none height'); 710 | 711 | element.style.cssText = 'display: inline-block'; 712 | assert.equal(isIntrinsicSize(element, 'width'), true, 'Display inline-block width'); 713 | assert.equal(isIntrinsicSize(element, 'height'), true, 'Display inline-block height'); 714 | 715 | element.style.cssText = 'float: left'; 716 | assert.equal(isIntrinsicSize(element, 'width'), true, 'Float left width'); 717 | assert.equal(isIntrinsicSize(element, 'height'), true, 'Float left height'); 718 | 719 | element.style.cssText = 'position: absolute'; 720 | assert.equal(isIntrinsicSize(element, 'width'), true, 'Position absolute width'); 721 | assert.equal(isIntrinsicSize(element, 'height'), true, 'Position absolute height'); 722 | 723 | element.style.cssText = 'display: inline-block; width: 100%'; 724 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Percentage width'); 725 | 726 | element.style.cssText = 'display: inline-block; width: 100px'; 727 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Pixel width'); 728 | 729 | element.style.cssText = 'display: inline-block; height: 100%'; 730 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Percentage height'); 731 | 732 | element.style.cssText = 'display: inline-block; height: 100px'; 733 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Pixel height'); 734 | 735 | element.style.cssText = 'display: inline; float: left; width: 100px; height: 100px'; 736 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Display inline float left pixel width'); 737 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Display inline float left pixel height'); 738 | 739 | element.style.cssText = 'display: inline; float: left; width: 50px; width: calc(100px / 2); height: 50px; height: calc(100px / 2)'; 740 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Calc pixel width'); 741 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Calc pixel height'); 742 | 743 | element.style.cssText = 'display: inline; float: left; width: 50%; width: calc(100% / 2); height: 50%; height: calc(100% / 2)'; 744 | assert.equal(isIntrinsicSize(element, 'width'), false, 'Calc percentage width'); 745 | assert.equal(isIntrinsicSize(element, 'height'), false, 'Calc percentage height'); 746 | 747 | }); 748 | 749 | /*global getSize*/ 750 | QUnit.test('getSize', function(assert) { 751 | var element = document.createElement('div'); 752 | element.style.width = '100px'; 753 | element.style.height = '100px'; 754 | element.style.boxSizing = 'border-box'; 755 | element.style.padding = '1pc'; 756 | element.style.border = '10px solid black'; 757 | fixture.appendChild(element); 758 | assert.equal(getSize(element, 'width'), 48, 'Width'); 759 | assert.equal(getSize(element, 'height'), 48, 'Height'); 760 | }); 761 | 762 | /*global getComputedLength*/ 763 | QUnit.test('getComputedLength', function(assert) { 764 | 765 | var data = [ 766 | ['1px', 1], 767 | ['10px', 10], 768 | ['-0.123px', -0.123], 769 | ['12pt', 16], 770 | ['1pc', 16], 771 | ['1in', 96], 772 | ['2.54cm', 96], 773 | ['25.4mm', 96], 774 | ['1rem', 16], 775 | ['1em', 10], 776 | ['1ex', 5], 777 | ['100vw', window.innerWidth], 778 | ['100vh', window.innerHeight], 779 | ['100vmin', Math.min(window.innerWidth, window.innerHeight)], 780 | ['100vmax', Math.max(window.innerWidth, window.innerHeight)], 781 | ['123foobar', 123], 782 | ]; 783 | 784 | var dummy = document.createElement('div'); 785 | dummy.style.fontSize = '10px'; 786 | fixture.appendChild(dummy); 787 | 788 | data.forEach(function(item) { 789 | assert.equal(getComputedLength(item[0], dummy), item[1], item[0] + ' == ' + item[1] + 'px'); 790 | }); 791 | 792 | }); 793 | 794 | /*global getComputedStyle*/ 795 | QUnit.test('getComputedStyle', function(assert) { 796 | var element = document.createElement('div'); 797 | element.style.width = '100px'; 798 | element.style.height = '1in'; 799 | element.style.cssFloat = 'left'; 800 | fixture.appendChild(element); 801 | assert.equal(getComputedStyle(element).width, '100px', 'Normal style'); 802 | assert.equal(getComputedStyle(element).height, '96px', 'Converted to pixel'); 803 | assert.equal(getComputedStyle(element).cssFloat, 'left', 'Float left'); 804 | assert.equal(getComputedStyle(element).display, 'block', 'Default style'); 805 | element.style.cssText = 'display: inline; float: left; font-size: 10px'; 806 | assert.equal(getComputedStyle(element).display, 'block', 'Correct display value'); 807 | assert.equal(getComputedStyle(element).getPropertyValue('display'), 'block', 'Correct display value via getPropertyValue'); 808 | assert.equal(getComputedStyle(element).fontSize, '10px', 'Correct font-size value'); 809 | assert.equal(getComputedStyle(element).getPropertyValue('font-size'), '10px', 'Correct font-size value via getPropertyValue'); 810 | }); 811 | 812 | /*global getOriginalStyle, buildStyleCache*/ 813 | QUnit.test('getOriginalStyle', function(assert) { 814 | 815 | var element = document.createElement('div'); 816 | element.className = 'myel'; 817 | 818 | var style = document.createElement('style'); 819 | style.type = 'text/css'; 820 | style.innerHTML = '.myel { width: 100%; height: auto !important }'; 821 | 822 | fixture.appendChild(style); 823 | fixture.appendChild(element); 824 | 825 | buildStyleCache(); 826 | 827 | assert.equal(getOriginalStyle(element, 'width'), '100%', 'Get width from