├── .gitignore ├── .npmignore ├── .github └── FUNDING.yml ├── index.js ├── css-element-queries.d.ts ├── tsconfig.json ├── src ├── ElementQueries.d.ts ├── ResizeSensor.d.ts ├── ResizeSensor.js └── ElementQueries.js ├── package.json ├── LICENSE ├── tests ├── late-trigger.html ├── mutation │ ├── app.ts │ ├── index.html │ └── app.js ├── demo.js ├── demo.css └── demo.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tests 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: marcj 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ResizeSensor: require('./src/ResizeSensor'), 3 | ElementQueries: require('./src/ElementQueries') 4 | }; 5 | -------------------------------------------------------------------------------- /css-element-queries.d.ts: -------------------------------------------------------------------------------- 1 | export { ResizeSensor, ResizeSensorCallback, Size } from "./src/ResizeSensor"; 2 | export { ElementQueries } from './src/ElementQueries'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "downlevelIteration": true, 5 | "lib" : ["dom","es6","dom.iterable","scripthost", "es2015.iterable", "es2015.collection"] 6 | } 7 | } -------------------------------------------------------------------------------- /src/ElementQueries.d.ts: -------------------------------------------------------------------------------- 1 | export declare class ElementQueries { 2 | /** 3 | * Attaches to DOMLoadContent 4 | */ 5 | static listen(): void; 6 | 7 | /** 8 | * Parses all available CSS and attach ResizeSensor to those elements which have rules attached. 9 | * Make sure this is called after 'load' event, because CSS files are not ready when domReady is fired. 10 | */ 11 | static init(): void; 12 | } 13 | 14 | export default ElementQueries; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-element-queries", 3 | "version": "1.2.3", 4 | "description": "CSS-Element-Queries Polyfill. Proof-of-concept for high-speed element dimension/media queries in valid css.", 5 | "main": "index.js", 6 | "typings": "css-element-queries.d.ts", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:marcj/css-element-queries.git" 16 | }, 17 | "author": "Marc J. Schmidt", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/marcj/css-element-queries/issues" 21 | }, 22 | "homepage": "https://github.com/marcj/css-element-queries", 23 | "devDependencies": { 24 | "grunt": "^0.4.5", 25 | "grunt-bump": "^0.3.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Marc J. Schmidt 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 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all 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 | -------------------------------------------------------------------------------- /src/ResizeSensor.d.ts: -------------------------------------------------------------------------------- 1 | export declare interface Size { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export declare type ResizeSensorCallback = (size: Size) => void; 7 | 8 | export declare class ResizeSensor { 9 | /** 10 | * Creates a new resize sensor on given elements. The provided callback is called max 1 times per requestAnimationFrame and 11 | * is called initially. 12 | */ 13 | constructor(element: Element | Element[], callback: ResizeSensorCallback); 14 | 15 | /** 16 | * Removes the resize sensor, and stops listening to resize events. 17 | */ 18 | detach(callback?: ResizeSensorCallback): void; 19 | 20 | /** 21 | * Resets the resize sensors, so for the next element resize is correctly detected. This is rare cases necessary 22 | * when the resize sensor isn't initialised correctly or is in a broken state due to DOM modifications. 23 | */ 24 | reset(): void; 25 | 26 | /** 27 | * Removes the resize sensor, and stops listening to resize events. 28 | */ 29 | static detach(element: Element | Element[], callback?: ResizeSensorCallback): void; 30 | 31 | /** 32 | * Resets the resize sensors, so for the next element resize is correctly detected. This is rare cases necessary 33 | * when the resize sensor isn't initialised correctly or is in a broken state due to DOM modifications. 34 | */ 35 | static reset(element: Element | Element[]): void; 36 | } 37 | 38 | export default ResizeSensor; 39 | -------------------------------------------------------------------------------- /tests/late-trigger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 48 | -------------------------------------------------------------------------------- /tests/mutation/app.ts: -------------------------------------------------------------------------------- 1 | declare const ResizeSensor; 2 | 3 | const state: { 4 | dragged: Element 5 | } = { 6 | dragged: null 7 | }; 8 | 9 | let i = 0; 10 | 11 | for (const item of document.getElementsByClassName('drag')) { 12 | i++; 13 | item.setAttribute('draggable', 'true'); 14 | item.setAttribute('id', 'drag-' + i); 15 | 16 | (element => { 17 | const title = 'Drag me #' + i; 18 | element.setAttribute('data-label', title); 19 | 20 | new ResizeSensor(element, (size) => { 21 | element.setAttribute('data-label', `${title} (${size.width}x${size.height})`); 22 | }); 23 | })(item); 24 | 25 | item.addEventListener('dragstart', (event: DragEvent) => { 26 | state.dragged = event.target; 27 | event.dataTransfer.setData('text', 'thanks firefox'); 28 | event.dataTransfer.dropEffect = 'move'; 29 | }); 30 | } 31 | 32 | for (const item of document.getElementsByClassName('container')) { 33 | (element => { 34 | item.addEventListener('drop', (event) => { 35 | event.preventDefault(); 36 | item.classList.remove('drag-hover'); 37 | 38 | state.dragged.parentNode.removeChild(state.dragged); 39 | element.appendChild(state.dragged); 40 | 41 | state.dragged = null; 42 | }); 43 | })(item); 44 | 45 | item.addEventListener('dragleave', (event) => { 46 | item.classList.remove('drag-hover'); 47 | }); 48 | 49 | item.addEventListener('dragover', (event) => { 50 | item.classList.add('drag-hover'); 51 | }); 52 | 53 | item.addEventListener('dragover', (event) => { 54 | event.preventDefault(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /tests/mutation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /tests/mutation/app.js: -------------------------------------------------------------------------------- 1 | var __values = (this && this.__values) || function (o) { 2 | var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; 3 | if (m) return m.call(o); 4 | return { 5 | next: function () { 6 | if (o && i >= o.length) o = void 0; 7 | return { value: o && o[i++], done: !o }; 8 | } 9 | }; 10 | }; 11 | var e_1, _a, e_2, _b; 12 | var state = { 13 | dragged: null 14 | }; 15 | var i = 0; 16 | try { 17 | for (var _c = __values(document.getElementsByClassName('drag')), _d = _c.next(); !_d.done; _d = _c.next()) { 18 | var item = _d.value; 19 | i++; 20 | item.setAttribute('draggable', 'true'); 21 | item.setAttribute('id', 'drag-' + i); 22 | (function (element) { 23 | var title = 'Drag me #' + i; 24 | element.setAttribute('data-label', title); 25 | new ResizeSensor(element, function (size) { 26 | element.setAttribute('data-label', title + " (" + size.width + "x" + size.height + ")"); 27 | }); 28 | })(item); 29 | item.addEventListener('dragstart', function (event) { 30 | state.dragged = event.target; 31 | event.dataTransfer.setData('text', 'thanks firefox'); 32 | event.dataTransfer.dropEffect = 'move'; 33 | }); 34 | } 35 | } 36 | catch (e_1_1) { e_1 = { error: e_1_1 }; } 37 | finally { 38 | try { 39 | if (_d && !_d.done && (_a = _c.return)) _a.call(_c); 40 | } 41 | finally { if (e_1) throw e_1.error; } 42 | } 43 | var _loop_1 = function (item) { 44 | (function (element) { 45 | item.addEventListener('drop', function (event) { 46 | event.preventDefault(); 47 | item.classList.remove('drag-hover'); 48 | state.dragged.parentNode.removeChild(state.dragged); 49 | element.appendChild(state.dragged); 50 | state.dragged = null; 51 | }); 52 | })(item); 53 | item.addEventListener('dragleave', function (event) { 54 | item.classList.remove('drag-hover'); 55 | }); 56 | item.addEventListener('dragover', function (event) { 57 | item.classList.add('drag-hover'); 58 | }); 59 | item.addEventListener('dragover', function (event) { 60 | event.preventDefault(); 61 | }); 62 | }; 63 | try { 64 | for (var _e = __values(document.getElementsByClassName('container')), _f = _e.next(); !_f.done; _f = _e.next()) { 65 | var item = _f.value; 66 | _loop_1(item); 67 | } 68 | } 69 | catch (e_2_1) { e_2 = { error: e_2_1 }; } 70 | finally { 71 | try { 72 | if (_f && !_f.done && (_b = _e.return)) _b.call(_e); 73 | } 74 | finally { if (e_2) throw e_2.error; } 75 | } 76 | -------------------------------------------------------------------------------- /tests/demo.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | console.log('ready'); 3 | 4 | // $("textarea.html").each(function(idx, textarea) { 5 | // CodeMirror.fromTextArea(textarea, { 6 | // lineNumbers: true, 7 | // mode: "htmlmixed", 8 | // readOnly: true 9 | // }); 10 | // }); 11 | // 12 | // $("textarea.css").each(function(idx, textarea) { 13 | // CodeMirror.fromTextArea(textarea, { 14 | // lineNumbers: true, 15 | // mode: "css", 16 | // readOnly: true 17 | // }); 18 | // }); 19 | // 20 | // $("textarea.javascript").each(function(idx, textarea) { 21 | // CodeMirror.fromTextArea(textarea, { 22 | // lineNumbers: true, 23 | // mode: "javascript", 24 | // readOnly: true 25 | // }); 26 | // }); 27 | }); 28 | 29 | function ResizerDemo(element) { 30 | element = $(element); 31 | var handler = $('
'); 32 | var info = $('
'); 33 | 34 | element.append(handler); 35 | element.append(info); 36 | 37 | var hammer = new Hammer(element[0], {recognizers: [ 38 | [Hammer.Pan, { threshold: 0}] 39 | ]}); 40 | 41 | var startWidth; 42 | element.on('mousedown', function(e){ 43 | e.preventDefault(); 44 | }); 45 | hammer.on('panstart', function(e) { 46 | startWidth = element[0].clientWidth; 47 | }); 48 | 49 | hammer.on('panmove', function(e) { 50 | element[0].style.width = (startWidth + e.deltaX) + 'px'; 51 | info.html(element[0].clientWidth + 'px x ' + element[0].clientHeight + 'px'); 52 | }) 53 | } 54 | 55 | $( document ).ready(function(){ 56 | $('.examplesResizerDemos').each(function(idx, element){ 57 | new ResizerDemo(element); 58 | }); 59 | 60 | perfTest(); 61 | example3(); 62 | example4(); 63 | example5(); 64 | }); 65 | 66 | function perfTest(){ 67 | var container = $('#dynamicContainer'); 68 | var dynamicCount = $('#dynamicCount'); 69 | var dynamicCounter = $('#dynamicCounter'); 70 | 71 | window.detachDynamic = function() { 72 | container.children().each(function(idx, element) { 73 | ResizeSensor.detach(element); 74 | }); 75 | }; 76 | 77 | window.removeDynamic = function() { 78 | container.html(''); 79 | }; 80 | 81 | window.addDynamic = function() { 82 | container.html(''); 83 | var i = 0, to = dynamicCount.val(), div, counter = 0; 84 | for (; i < to; i++) { 85 | div = $('
#'+i+'
'); 86 | container.append(div); 87 | 88 | new ResizeSensor(div, function(){ 89 | counter++; 90 | dynamicCounter.html(counter + ' changes.'); 91 | }); 92 | } 93 | } 94 | } 95 | 96 | function example3(){ 97 | var logger = $('#example-3-log'); 98 | var box = $('#example-3-box'); 99 | 100 | $('#startStop3').on('click', function(){ 101 | if (box.hasClass('example-3-box-start')) { 102 | box.removeClass('example-3-box-start'); 103 | } else { 104 | box.addClass('example-3-box-start'); 105 | } 106 | }); 107 | new ResizeSensor(box, function(el){ 108 | logger.html('Changed to ' + box[0].clientWidth+'px width.'); 109 | }); 110 | 111 | } 112 | 113 | function example4(){ 114 | var logger = $('#example-4-log'); 115 | var box = $('#example-4-box'); 116 | 117 | $('#startStop4').on('click', function(){ 118 | if (box.hasClass('example-4-box-start')) { 119 | box.removeClass('example-4-box-start'); 120 | } else { 121 | box.addClass('example-4-box-start'); 122 | } 123 | }); 124 | new ResizeSensor(box, function(){ 125 | logger.html('Changed to ' + box[0].clientHeight+'px height.'); 126 | }); 127 | } 128 | 129 | function example5(){ 130 | var box = $('#example-5'); 131 | var changed = 0; 132 | new ResizeSensor(box.parent(), function(){ 133 | box[0].innerHTML = (++changed) + ' changes. ' + box.parent()[0].clientWidth+'px/'+box.parent()[0].clientHeight+'px'; 134 | }); 135 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Element Queries 2 | 3 | 4 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/marcj/css-element-queries?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | 6 | Element Queries is a polyfill adding support for element based media-queries to all new browsers (incl. IE7+). 7 | It allows not only to define media-queries based on window-size but also adds 'media-queries' functionality depending on element (any selector supported) 8 | size while not causing performance lags due to event based implementation. 9 | 10 | It's a proof-of-concept event-based CSS element dimension query with valid CSS selector syntax. 11 | 12 | Features: 13 | 14 | - no performance issues since it listens only on size changes of elements that have element query rules defined through css. Other element query polifills only listen on `window.onresize` which causes performance issues and allows only to detect changes via window.resize event and not inside layout changes like css3 animation, :hover, DOM changes etc. 15 | - no interval/timeout detection. Truly event-based through integrated ResizeSensor class. 16 | - automatically discovers new DOM elements. No need to call javascript manually. 17 | - no CSS modifications. Valid CSS Syntax 18 | - all CSS selectors available. Uses regular attribute selector. No need to write rules in HTML/JS. 19 | - supports and tested in webkit, gecko and IE(10+) 20 | - `min-width`, `min-height`, `max-width` and `max-height` are supported so far 21 | - works with any layout modifications: HTML (innerHTML etc), inline styles, DOM mutation, CSS3 transitions, fluid layout changes (also percent changes), pseudo classes (:hover etc.), window resizes and more 22 | - no Javascript-Framework dependency (works with jQuery, Mootools, etc.) 23 | - Works beautiful for responsive images without FOUC 24 | 25 | More demos and information: http://marcj.github.io/css-element-queries/ 26 | 27 | ## Examples 28 | 29 | ### Element Query 30 | 31 | ```css 32 | .widget-name h2 { 33 | font-size: 12px; 34 | } 35 | 36 | .widget-name[min-width~="400px"] h2 { 37 | font-size: 18px; 38 | } 39 | 40 | .widget-name[min-width~="600px"] h2 { 41 | padding: 55px; 42 | text-align: center; 43 | font-size: 24px; 44 | } 45 | 46 | .widget-name[min-width~="700px"] h2 { 47 | font-size: 34px; 48 | color: red; 49 | } 50 | ``` 51 | 52 | As you can see we use the `~=` [attribute selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors). 53 | Since this css-element-queries polyfill adds new element attributes on the DOM element 54 | (`
`) depending on your actual CSS and element's dimension, 55 | you should always use this attribute selector (especially if you have several element query rules on the same element). 56 | 57 | ```html 58 |
59 |

Element responsiveness FTW!

60 |
61 | ``` 62 | 63 | ### Responsive image 64 | 65 | ```html 66 |
67 | 68 | 69 | 70 |
71 | ``` 72 | 73 | Include the javascript files at the bottom and you're good to go. No custom javascript calls needed. 74 | 75 | ```html 76 | 77 | 78 | ``` 79 | 80 | ## See it in action: 81 | 82 | Here live http://marcj.github.io/css-element-queries/. 83 | 84 | ![Demo](http://marcj.github.io/css-element-queries/images/css-element-queries-demo.gif) 85 | 86 | 87 | ## Module Loader 88 | 89 | If you're using a module loader you need to trigger the event listening or initialization yourself: 90 | 91 | ```javascript 92 | var ElementQueries = require('css-element-queries/src/ElementQueries'); 93 | 94 | //attaches to DOMLoadContent 95 | ElementQueries.listen(); 96 | 97 | //or if you want to trigger it yourself. 98 | // Parse all available CSS and attach ResizeSensor to those elements which have rules attached 99 | // (make sure this is called after 'load' event, because CSS files are not ready when domReady is fired. 100 | ElementQueries.init(); 101 | ``` 102 | 103 | ## Issues 104 | 105 | - So far does not work on `img` and other elements that can't contain other elements. Wrapping with a `div` works fine though (See demo). 106 | - Adds additional hidden elements into selected target element and forces target element to be relative or absolute. 107 | - Local stylesheets do not work (using `file://` protocol). 108 | - If you have rules on an element that has a css animation, also add `element-queries`. E.g. `.widget-name { animation: 2sec my-animation, 1s element-queries;}`. We use this to detect new added DOM elements automatically. 109 | 110 | ## License 111 | 112 | MIT license. Copyright [Marc J. Schmidt](https://twitter.com/MarcJSchmidt). 113 | -------------------------------------------------------------------------------- /tests/demo.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,300); 2 | 3 | 4 | * { 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | html, body { 11 | background-color: #f9f9f9; 12 | padding: 0; 13 | margin: 0; 14 | text-align: center; 15 | -webkit-font-smoothing: antialiased; 16 | text-rendering: geometricPrecision; 17 | font-family: 'Lato', sans-serif; 18 | font-weight: 400; 19 | font-size: 18px; 20 | line-height: 26px; 21 | color: #444; 22 | } 23 | 24 | textarea { 25 | width: 100%; 26 | height: 80px; 27 | } 28 | 29 | .wrapper { 30 | background-color: #ffffff; 31 | max-width: 1000px; 32 | margin: auto; 33 | text-align: left; 34 | padding: 30px 20px; 35 | box-shadow: 0px 0px 10px #c8cac5; 36 | } 37 | 38 | .download-buttons { 39 | padding-top: 20px; 40 | padding-bottom: 20px; 41 | text-align: right; 42 | } 43 | 44 | .wrapper h1:first-child { 45 | margin-top: 0; 46 | } 47 | 48 | footer { 49 | padding: 25px 0; 50 | } 51 | 52 | h1 { 53 | font-weight: 300; 54 | padding-bottom: 25px; 55 | } 56 | 57 | h1.sub, h2.sub { 58 | padding-top: 10px; 59 | font-size: 22px; 60 | color: #888; 61 | } 62 | 63 | h2 { 64 | padding-bottom: 20px; 65 | font-weight: 300; 66 | } 67 | 68 | p.seperator { 69 | padding-top: 30px; 70 | border-bottom: 1px solid #e6e6e6; 71 | margin-bottom: 30px; 72 | } 73 | 74 | .text-panel { 75 | margin: auto; 76 | text-align: left; 77 | max-width: 780px; 78 | 79 | font-size: 16px; 80 | color: #444; 81 | line-height: 26px; 82 | } 83 | 84 | .text-panel:first-child { 85 | padding-top: 30px; 86 | } 87 | 88 | 89 | .text-panel p.seperator { 90 | padding-top: 30px; 91 | border-bottom: 1px solid #e6e6e6; 92 | margin-bottom: 30px; 93 | } 94 | 95 | .code { 96 | width: 50%; 97 | float: left; 98 | text-align: left; 99 | margin: auto; 100 | font-size: 13px; 101 | line-height: 15px; 102 | position: relative; 103 | margin-top: 20px; 104 | padding: 0 25px; 105 | margin-bottom: 15px; 106 | } 107 | 108 | .code .CodeMirror { 109 | border: 1px solid #e8e8e8; 110 | border-radius: 5px; 111 | height: auto; 112 | } 113 | 114 | .code-full { 115 | float: none; 116 | width: 80%; 117 | margin-left: auto; 118 | margin-right: auto; 119 | padding: 0; 120 | } 121 | 122 | .example-desc { 123 | text-align: center; 124 | } 125 | 126 | .example { 127 | text-align: center; 128 | margin: 100px auto; 129 | 130 | padding-bottom: 100px; 131 | border-bottom: 1px solid #e6e6e6; 132 | } 133 | 134 | .examplesResizerDemos, 135 | .examplesResizerDemosXY { 136 | background-color: #dae3e8; 137 | width: 550px; 138 | margin: 15px auto; 139 | padding: 15px 0; 140 | border-radius: 5px; 141 | position: relative; 142 | } 143 | 144 | .examplesResizerDemosXY { 145 | height: 150px; 146 | } 147 | 148 | .examplesResizerDemos h2 { 149 | font-size: 20px; 150 | } 151 | 152 | .resizerDemo-info { 153 | position: absolute; 154 | right: 23px; 155 | top: 0; 156 | color: #333; 157 | font-size: 13px; 158 | line-height: 15px; 159 | font-family: monospace; 160 | } 161 | 162 | .bash { 163 | border-radius: 3px; 164 | border: 1px solid #eee; 165 | margin: 15px; 166 | padding: 5px; 167 | font-size: 14px; 168 | width: 80%; 169 | margin-left: auto; 170 | margin-right: auto; 171 | font-family: monospace; 172 | } 173 | 174 | 175 | .resizerDemo-handler { 176 | position: absolute; 177 | right: 0; 178 | top: 0; 179 | bottom: 0; 180 | width: 20px; 181 | padding: 0 1px; 182 | background-color: gray; 183 | cursor: ew-resize; 184 | line-height: 5px; 185 | font-size: 14px; 186 | } 187 | 188 | .resizerDemo-handler:before { 189 | content: '• • •'; 190 | color: white; 191 | width: 10px; 192 | position: absolute; 193 | top: 50%; 194 | left: 5px; 195 | font-weight: 300; 196 | line-height: 6px; 197 | font-size: 11px; 198 | text-align: center; 199 | margin-top: -15px; 200 | } 201 | 202 | .breaker { 203 | clear: both; 204 | } 205 | 206 | .example-2 { 207 | border: 1px solid silver; 208 | background-color: #eee; 209 | } 210 | 211 | .example-2-first, 212 | .example-2-second { 213 | background-color: black; 214 | color: white; 215 | padding: 2px; 216 | width: 50%; 217 | float: left; 218 | border: 5px solid #eee; 219 | } 220 | 221 | .example-2-second { 222 | float: right; 223 | } 224 | 225 | .example-2-box { 226 | color: white; 227 | margin: 5px; 228 | padding: 2px; 229 | } 230 | 231 | .example-3, 232 | .example-4 { 233 | border: 1px solid silver; 234 | margin: 25px; 235 | background-color: #eee; 236 | } 237 | 238 | #example-5 { 239 | overflow: visible; 240 | position: relative; 241 | text-align: center; 242 | } 243 | 244 | #example-3-box, 245 | #example-4-box { 246 | background-color: gray; 247 | color: white; 248 | margin: auto; 249 | width: 50%; 250 | padding: 50px; 251 | } 252 | 253 | .example-3-box-start, 254 | .example-4-box-start { 255 | animation-duration: 3s; 256 | -moz-animation-duration: 3s; 257 | -webkit-animation-duration: 3s; 258 | -moz-animation-name: anim; 259 | animation-name: anim; 260 | -webkit-animation-name: anim; 261 | animation-iteration-count: infinite; 262 | -webkit-animation-iteration-count: infinite; 263 | } 264 | 265 | .example-4-box-start { 266 | -moz-animation-name: animHeight; 267 | animation-name: animHeight; 268 | -webkit-animation-name: animHeight; 269 | } 270 | 271 | #example-3-log, 272 | #example-4-log { 273 | background-color: white; 274 | padding: 2px; 275 | margin: 5px; 276 | } 277 | 278 | @keyframes anim { 279 | 0% { 280 | padding: 50px; 281 | width: 50%; 282 | background-color: gray; 283 | } 284 | 285 | 50% { 286 | padding: 10px; 287 | width: 40%; 288 | background-color: #806522; 289 | } 290 | 291 | 100% { 292 | padding: 50px; 293 | width: 50%; 294 | background-color: gray; 295 | } 296 | } 297 | 298 | @-webkit-keyframes anim { 299 | 0% { 300 | padding: 50px; 301 | width: 50%; 302 | background-color: gray; 303 | } 304 | 305 | 50% { 306 | padding: 10px; 307 | width: 40%; 308 | background-color: #806522; 309 | } 310 | 311 | 100% { 312 | padding: 50px; 313 | width: 50%; 314 | background-color: gray; 315 | } 316 | } 317 | 318 | @-moz-keyframes anim { 319 | 0% { 320 | padding: 50px; 321 | width: 50%; 322 | background-color: gray; 323 | } 324 | 325 | 50% { 326 | padding: 10px; 327 | width: 40%; 328 | background-color: #806522; 329 | } 330 | 331 | 100% { 332 | padding: 50px; 333 | width: 50%; 334 | background-color: gray; 335 | } 336 | } 337 | 338 | @keyframes animHeight { 339 | 0% { 340 | padding: 50px; 341 | height: 50%; 342 | background-color: gray; 343 | } 344 | 345 | 50% { 346 | padding: 10px; 347 | height: 40%; 348 | background-color: #806522; 349 | } 350 | 351 | 100% { 352 | padding: 50px; 353 | height: 50%; 354 | background-color: gray; 355 | } 356 | } 357 | 358 | @-webkit-keyframes animHeight { 359 | 0% { 360 | padding: 50px; 361 | height: 50%; 362 | background-color: gray; 363 | } 364 | 365 | 50% { 366 | padding: 10px; 367 | height: 40%; 368 | background-color: #806522; 369 | } 370 | 371 | 100% { 372 | padding: 50px; 373 | height: 50%; 374 | background-color: gray; 375 | } 376 | } 377 | 378 | @-moz-keyframes animHeight { 379 | 0% { 380 | padding: 50px; 381 | height: 50%; 382 | background-color: gray; 383 | } 384 | 385 | 50% { 386 | padding: 10px; 387 | height: 40%; 388 | background-color: #806522; 389 | } 390 | 391 | 100% { 392 | padding: 50px; 393 | height: 50%; 394 | background-color: gray; 395 | } 396 | } 397 | 398 | .dynamicElement { 399 | margin: 50px; 400 | width: 50px; 401 | 402 | -moz-animation-name: animHeight; 403 | animation-name: animHeight; 404 | -webkit-animation-name: animHeight; 405 | 406 | animation-duration: 3s; 407 | -moz-animation-duration: 3s; 408 | -webkit-animation-duration: 3s; 409 | animation-iteration-count: infinite; 410 | -webkit-animation-iteration-count: infinite; 411 | 412 | display: inline-block; 413 | border: 1px solid #eee; 414 | background-color: #f9f9f9; 415 | } 416 | 417 | .dynamic { 418 | margin: 15px; 419 | text-align: center; 420 | } 421 | 422 | .dynamic .input-group { 423 | width: 500px; 424 | margin: auto; 425 | } -------------------------------------------------------------------------------- /src/ResizeSensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Copyright Marc J. Schmidt. See the LICENSE file at the top-level 5 | * directory of this distribution and at 6 | * https://github.com/marcj/css-element-queries/blob/master/LICENSE. 7 | */ 8 | (function (root, factory) { 9 | if (typeof define === "function" && define.amd) { 10 | define(factory); 11 | } else if (typeof exports === "object") { 12 | module.exports = factory(); 13 | } else { 14 | root.ResizeSensor = factory(); 15 | } 16 | }(typeof window !== 'undefined' ? window : this, function () { 17 | 18 | // Make sure it does not throw in a SSR (Server Side Rendering) situation 19 | if (typeof window === "undefined") { 20 | return null; 21 | } 22 | // https://github.com/Semantic-Org/Semantic-UI/issues/3855 23 | // https://github.com/marcj/css-element-queries/issues/257 24 | var globalWindow = typeof window != 'undefined' && window.Math == Math 25 | ? window 26 | : typeof self != 'undefined' && self.Math == Math 27 | ? self 28 | : Function('return this')(); 29 | // Only used for the dirty checking, so the event callback count is limited to max 1 call per fps per sensor. 30 | // In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and 31 | // would generate too many unnecessary events. 32 | var requestAnimationFrame = globalWindow.requestAnimationFrame || 33 | globalWindow.mozRequestAnimationFrame || 34 | globalWindow.webkitRequestAnimationFrame || 35 | function (fn) { 36 | return globalWindow.setTimeout(fn, 20); 37 | }; 38 | 39 | var cancelAnimationFrame = globalWindow.cancelAnimationFrame || 40 | globalWindow.mozCancelAnimationFrame || 41 | globalWindow.webkitCancelAnimationFrame || 42 | function (timer) { 43 | globalWindow.clearTimeout(timer); 44 | }; 45 | 46 | /** 47 | * Iterate over each of the provided element(s). 48 | * 49 | * @param {HTMLElement|HTMLElement[]} elements 50 | * @param {Function} callback 51 | */ 52 | function forEachElement(elements, callback){ 53 | var elementsType = Object.prototype.toString.call(elements); 54 | var isCollectionTyped = ('[object Array]' === elementsType 55 | || ('[object NodeList]' === elementsType) 56 | || ('[object HTMLCollection]' === elementsType) 57 | || ('[object Object]' === elementsType) 58 | || ('undefined' !== typeof jQuery && elements instanceof jQuery) //jquery 59 | || ('undefined' !== typeof Elements && elements instanceof Elements) //mootools 60 | ); 61 | var i = 0, j = elements.length; 62 | if (isCollectionTyped) { 63 | for (; i < j; i++) { 64 | callback(elements[i]); 65 | } 66 | } else { 67 | callback(elements); 68 | } 69 | } 70 | 71 | /** 72 | * Get element size 73 | * @param {HTMLElement} element 74 | * @returns {Object} {width, height} 75 | */ 76 | function getElementSize(element) { 77 | if (!element.getBoundingClientRect) { 78 | return { 79 | width: element.offsetWidth, 80 | height: element.offsetHeight 81 | } 82 | } 83 | 84 | var rect = element.getBoundingClientRect(); 85 | return { 86 | width: Math.round(rect.width), 87 | height: Math.round(rect.height) 88 | } 89 | } 90 | 91 | /** 92 | * Apply CSS styles to element. 93 | * 94 | * @param {HTMLElement} element 95 | * @param {Object} style 96 | */ 97 | function setStyle(element, style) { 98 | Object.keys(style).forEach(function(key) { 99 | element.style[key] = style[key]; 100 | }); 101 | } 102 | 103 | /** 104 | * Class for dimension change detection. 105 | * 106 | * @param {Element|Element[]|Elements|jQuery} element 107 | * @param {Function} callback 108 | * 109 | * @constructor 110 | */ 111 | var ResizeSensor = function(element, callback) { 112 | //Is used when checking in reset() only for invisible elements 113 | var lastAnimationFrameForInvisibleCheck = 0; 114 | 115 | /** 116 | * 117 | * @constructor 118 | */ 119 | function EventQueue() { 120 | var q = []; 121 | this.add = function(ev) { 122 | q.push(ev); 123 | }; 124 | 125 | var i, j; 126 | this.call = function(sizeInfo) { 127 | for (i = 0, j = q.length; i < j; i++) { 128 | q[i].call(this, sizeInfo); 129 | } 130 | }; 131 | 132 | this.remove = function(ev) { 133 | var newQueue = []; 134 | for(i = 0, j = q.length; i < j; i++) { 135 | if(q[i] !== ev) newQueue.push(q[i]); 136 | } 137 | q = newQueue; 138 | }; 139 | 140 | this.length = function() { 141 | return q.length; 142 | } 143 | } 144 | 145 | /** 146 | * 147 | * @param {HTMLElement} element 148 | * @param {Function} resized 149 | */ 150 | function attachResizeEvent(element, resized) { 151 | if (!element) return; 152 | if (element.resizedAttached) { 153 | element.resizedAttached.add(resized); 154 | return; 155 | } 156 | 157 | element.resizedAttached = new EventQueue(); 158 | element.resizedAttached.add(resized); 159 | 160 | element.resizeSensor = document.createElement('div'); 161 | element.resizeSensor.dir = 'ltr'; 162 | element.resizeSensor.className = 'resize-sensor'; 163 | 164 | var style = { 165 | pointerEvents: 'none', 166 | position: 'absolute', 167 | left: '0px', 168 | top: '0px', 169 | right: '0px', 170 | bottom: '0px', 171 | overflow: 'hidden', 172 | zIndex: '-1', 173 | visibility: 'hidden', 174 | maxWidth: '100%' 175 | }; 176 | var styleChild = { 177 | position: 'absolute', 178 | left: '0px', 179 | top: '0px', 180 | transition: '0s', 181 | }; 182 | 183 | setStyle(element.resizeSensor, style); 184 | 185 | var expand = document.createElement('div'); 186 | expand.className = 'resize-sensor-expand'; 187 | setStyle(expand, style); 188 | 189 | var expandChild = document.createElement('div'); 190 | setStyle(expandChild, styleChild); 191 | expand.appendChild(expandChild); 192 | 193 | var shrink = document.createElement('div'); 194 | shrink.className = 'resize-sensor-shrink'; 195 | setStyle(shrink, style); 196 | 197 | var shrinkChild = document.createElement('div'); 198 | setStyle(shrinkChild, styleChild); 199 | setStyle(shrinkChild, { width: '200%', height: '200%' }); 200 | shrink.appendChild(shrinkChild); 201 | 202 | element.resizeSensor.appendChild(expand); 203 | element.resizeSensor.appendChild(shrink); 204 | element.appendChild(element.resizeSensor); 205 | 206 | var computedStyle = window.getComputedStyle(element); 207 | var position = computedStyle ? computedStyle.getPropertyValue('position') : null; 208 | if ('absolute' !== position && 'relative' !== position && 'fixed' !== position && 'sticky' !== position) { 209 | element.style.position = 'relative'; 210 | } 211 | 212 | var dirty = false; 213 | 214 | //last request animation frame id used in onscroll event 215 | var rafId = 0; 216 | var size = getElementSize(element); 217 | var lastWidth = 0; 218 | var lastHeight = 0; 219 | var initialHiddenCheck = true; 220 | lastAnimationFrameForInvisibleCheck = 0; 221 | 222 | var resetExpandShrink = function () { 223 | var width = element.offsetWidth; 224 | var height = element.offsetHeight; 225 | 226 | expandChild.style.width = (width + 10) + 'px'; 227 | expandChild.style.height = (height + 10) + 'px'; 228 | 229 | expand.scrollLeft = width + 10; 230 | expand.scrollTop = height + 10; 231 | 232 | shrink.scrollLeft = width + 10; 233 | shrink.scrollTop = height + 10; 234 | }; 235 | 236 | var reset = function() { 237 | // Check if element is hidden 238 | if (initialHiddenCheck) { 239 | var invisible = element.offsetWidth === 0 && element.offsetHeight === 0; 240 | if (invisible) { 241 | // Check in next frame 242 | if (!lastAnimationFrameForInvisibleCheck){ 243 | lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ 244 | lastAnimationFrameForInvisibleCheck = 0; 245 | reset(); 246 | }); 247 | } 248 | 249 | return; 250 | } else { 251 | // Stop checking 252 | initialHiddenCheck = false; 253 | } 254 | } 255 | 256 | resetExpandShrink(); 257 | }; 258 | element.resizeSensor.resetSensor = reset; 259 | 260 | var onResized = function() { 261 | rafId = 0; 262 | 263 | if (!dirty) return; 264 | 265 | lastWidth = size.width; 266 | lastHeight = size.height; 267 | 268 | if (element.resizedAttached) { 269 | element.resizedAttached.call(size); 270 | } 271 | }; 272 | 273 | var onScroll = function() { 274 | size = getElementSize(element); 275 | dirty = size.width !== lastWidth || size.height !== lastHeight; 276 | 277 | if (dirty && !rafId) { 278 | rafId = requestAnimationFrame(onResized); 279 | } 280 | 281 | reset(); 282 | }; 283 | 284 | var addEvent = function(el, name, cb) { 285 | if (el.attachEvent) { 286 | el.attachEvent('on' + name, cb); 287 | } else { 288 | el.addEventListener(name, cb); 289 | } 290 | }; 291 | 292 | addEvent(expand, 'scroll', onScroll); 293 | addEvent(shrink, 'scroll', onScroll); 294 | 295 | // Fix for custom Elements and invisible elements 296 | lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ 297 | lastAnimationFrameForInvisibleCheck = 0; 298 | reset(); 299 | }); 300 | } 301 | 302 | forEachElement(element, function(elem){ 303 | attachResizeEvent(elem, callback); 304 | }); 305 | 306 | this.detach = function(ev) { 307 | // clean up the unfinished animation frame to prevent a potential endless requestAnimationFrame of reset 308 | if (lastAnimationFrameForInvisibleCheck) { 309 | cancelAnimationFrame(lastAnimationFrameForInvisibleCheck); 310 | lastAnimationFrameForInvisibleCheck = 0; 311 | } 312 | ResizeSensor.detach(element, ev); 313 | }; 314 | 315 | this.reset = function() { 316 | //To prevent invoking element.resizeSensor.resetSensor if it's undefined 317 | if (element.resizeSensor.resetSensor) { 318 | element.resizeSensor.resetSensor(); 319 | } 320 | }; 321 | }; 322 | 323 | ResizeSensor.reset = function(element) { 324 | forEachElement(element, function(elem){ 325 | //To prevent invoking element.resizeSensor.resetSensor if it's undefined 326 | if (element.resizeSensor.resetSensor) { 327 | elem.resizeSensor.resetSensor(); 328 | } 329 | }); 330 | }; 331 | 332 | ResizeSensor.detach = function(element, ev) { 333 | forEachElement(element, function(elem){ 334 | if (!elem) return; 335 | if(elem.resizedAttached && typeof ev === "function"){ 336 | elem.resizedAttached.remove(ev); 337 | if(elem.resizedAttached.length()) return; 338 | } 339 | if (elem.resizeSensor) { 340 | if (elem.contains(elem.resizeSensor)) { 341 | elem.removeChild(elem.resizeSensor); 342 | } 343 | delete elem.resizeSensor; 344 | delete elem.resizedAttached; 345 | } 346 | }); 347 | }; 348 | 349 | if (typeof MutationObserver !== "undefined") { 350 | var observer = new MutationObserver(function (mutations) { 351 | for (var i in mutations) { 352 | if (mutations.hasOwnProperty(i)) { 353 | var items = mutations[i].addedNodes; 354 | for (var j = 0; j < items.length; j++) { 355 | if (items[j].resizeSensor) { 356 | ResizeSensor.reset(items[j]); 357 | } 358 | } 359 | } 360 | } 361 | }); 362 | 363 | document.addEventListener("DOMContentLoaded", function (event) { 364 | observer.observe(document.body, { 365 | childList: true, 366 | subtree: true, 367 | }); 368 | }); 369 | } 370 | 371 | return ResizeSensor; 372 | 373 | })); 374 | -------------------------------------------------------------------------------- /tests/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Css-element-queries Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 110 | 111 | 114 | 115 | 116 | 117 |
118 |

119 |

Examples 120 |
121 | Drag the gray line at the right to see it in action. 122 |
123 |

124 |
125 |
126 |
127 |

Responsive Text

128 |
129 |
130 |
131 |

Element responsiveness FTW!

132 |
133 | 136 | 137 | 138 | 139 |
140 |

Element detachable

141 |
142 | 143 | 144 | 145 |
146 |

Dynamic element {{id}}

147 |
148 | 149 | 150 |
151 |
152 |
153 | 156 |
157 |
158 | 177 |
178 |
179 |
180 |
181 |

Responsive Image

182 |
183 |
184 |
185 | 186 | 187 | 188 |
189 | 190 | The image above has a default of 700px. Shrink or expend the container too see responsive image 191 | working. 192 | Thanks @placehold.it 193 |
194 |
195 |
196 | 201 |
202 |
203 |
204 |
205 |

Responsive Widget

206 |
207 |
208 |

Demo 1

209 | This is content from the first responsive demo without media queries. It uses the element 210 | queries 211 | provided by this library. 212 |
213 |
214 | 215 |
216 | 224 |
225 |
226 | 247 |
248 |
249 |
250 | 251 |
252 |

Responsive Layout

253 |
254 |
255 |

Demo 2

256 |
257 | Box 258 |
259 |
260 | First 1/2 box 261 |
262 |
263 | Second 1/2 box 264 |
265 |
266 |
267 |
268 |
269 | 284 |
285 |
286 | 329 |
330 |
331 |
332 |
333 |

Responsive Animation

334 |
335 |
336 |

Demo 3 - width 337 | 338 |

339 |
340 | This box is animated through css transitions. 341 | We attached a resize-listener to this box. See below. 342 |
343 |
344 | No changes. 345 |
346 |
347 |
348 |
349 |
350 |

Demo 4 - height 351 | 352 |

353 |
354 | This box is animated through css transitions. 355 | We attached a resize-listener to this box. See below. 356 |
357 |
358 | No changes. 359 |
360 |
361 |
362 |
363 | 364 |
365 |

ResizeSensor Demo

366 |
367 |
368 |
369 | 0 changes 370 |
371 |
372 |
373 |
374 | CSS-Element-Queries comes with a Javascript ResizeSensor class you can use in Javascript directly. 375 |
376 | 381 |
382 |
383 |
384 | 385 |
386 |

Invisible Demo

387 |
388 |
389 | Press button to show. 390 |
391 | This should be red. 392 |
393 |
394 |
395 |
396 | 397 | 402 |
403 |
404 | 405 |
406 |

Performance Demo

407 |
408 |
409 | Performance Test: 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 | 426 | -------------------------------------------------------------------------------- /src/ElementQueries.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Copyright Marc J. Schmidt. See the LICENSE file at the top-level 5 | * directory of this distribution and at 6 | * https://github.com/marcj/css-element-queries/blob/master/LICENSE. 7 | */ 8 | (function (root, factory) { 9 | if (typeof define === "function" && define.amd) { 10 | define(['./ResizeSensor.js'], factory); 11 | } else if (typeof exports === "object") { 12 | module.exports = factory(require('./ResizeSensor.js')); 13 | } else { 14 | root.ElementQueries = factory(root.ResizeSensor); 15 | root.ElementQueries.listen(); 16 | } 17 | }(typeof window !== 'undefined' ? window : this, function (ResizeSensor) { 18 | 19 | /** 20 | * 21 | * @type {Function} 22 | * @constructor 23 | */ 24 | var ElementQueries = function () { 25 | //