diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7e45dcf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: marcj diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4f29079 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.idea +tests diff --git a/css-element-queries.d.ts b/css-element-queries.d.ts new file mode 100644 index 0000000..3fe0706 --- /dev/null +++ b/css-element-queries.d.ts @@ -0,0 +1,2 @@ +export { ResizeSensor, ResizeSensorCallback, Size } from "./src/ResizeSensor"; +export { ElementQueries } from './src/ElementQueries'; \ No newline at end of file diff --git a/package.json b/package.json index 154a842..964230f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "css-element-queries", - "version": "1.1.1", + "version": "1.2.3", "description": "CSS-Element-Queries Polyfill. Proof-of-concept for high-speed element dimension/media queries in valid css.", "main": "index.js", + "typings": "css-element-queries.d.ts", "directories": { "test": "test" }, diff --git a/src/ElementQueries.d.ts b/src/ElementQueries.d.ts new file mode 100644 index 0000000..00c5d93 --- /dev/null +++ b/src/ElementQueries.d.ts @@ -0,0 +1,14 @@ +export declare class ElementQueries { + /** + * Attaches to DOMLoadContent + */ + static listen(): void; + + /** + * Parses all available CSS and attach ResizeSensor to those elements which have rules attached. + * Make sure this is called after 'load' event, because CSS files are not ready when domReady is fired. + */ + static init(): void; +} + +export default ElementQueries; diff --git a/src/ElementQueries.js b/src/ElementQueries.js index 23038d0..4fe4298 100755 --- a/src/ElementQueries.js +++ b/src/ElementQueries.js @@ -170,13 +170,12 @@ if (!element.elementQueriesSetupInformation) { element.elementQueriesSetupInformation = new SetupInformation(element, id); } + if (!element.elementQueriesSensor) { element.elementQueriesSensor = new ResizeSensor(element, function () { element.elementQueriesSetupInformation.call(); }); } - - element.elementQueriesSetupInformation.call(); } /** @@ -441,7 +440,7 @@ for (var i = 0, j = document.styleSheets.length; i < j; i++) { try { if (document.styleSheets[i].href && 0 === document.styleSheets[i].href.indexOf('file://')) { - console.log("CssElementQueries: unable to parse local css files, " + document.styleSheets[i].href); + console.warn("CssElementQueries: unable to parse local css files, " + document.styleSheets[i].href); } readRules(document.styleSheets[i].cssRules || document.styleSheets[i].rules || document.styleSheets[i].cssText); diff --git a/src/ResizeSensor.d.ts b/src/ResizeSensor.d.ts index b0a5fdf..3fba8c6 100644 --- a/src/ResizeSensor.d.ts +++ b/src/ResizeSensor.d.ts @@ -1,11 +1,37 @@ -export declare type ResizeSensorCallback = (size: { width: number; height: number; }) => void; +export declare interface Size { + width: number; + height: number; +} + +export declare type ResizeSensorCallback = (size: Size) => void; -declare class ResizeSensor { +export declare class ResizeSensor { + /** + * Creates a new resize sensor on given elements. The provided callback is called max 1 times per requestAnimationFrame and + * is called initially. + */ constructor(element: Element | Element[], callback: ResizeSensorCallback); + + /** + * Removes the resize sensor, and stops listening to resize events. + */ detach(callback?: ResizeSensorCallback): void; + + /** + * Resets the resize sensors, so for the next element resize is correctly detected. This is rare cases necessary + * when the resize sensor isn't initialised correctly or is in a broken state due to DOM modifications. + */ reset(): void; + /** + * Removes the resize sensor, and stops listening to resize events. + */ static detach(element: Element | Element[], callback?: ResizeSensorCallback): void; + + /** + * Resets the resize sensors, so for the next element resize is correctly detected. This is rare cases necessary + * when the resize sensor isn't initialised correctly or is in a broken state due to DOM modifications. + */ static reset(element: Element | Element[]): void; } diff --git a/src/ResizeSensor.js b/src/ResizeSensor.js index 555a906..9a25503 100755 --- a/src/ResizeSensor.js +++ b/src/ResizeSensor.js @@ -19,14 +19,28 @@ if (typeof window === "undefined") { return null; } + // https://github.com/Semantic-Org/Semantic-UI/issues/3855 + // https://github.com/marcj/css-element-queries/issues/257 + var globalWindow = typeof window != 'undefined' && window.Math == Math + ? window + : typeof self != 'undefined' && self.Math == Math + ? self + : Function('return this')(); // Only used for the dirty checking, so the event callback count is limited to max 1 call per fps per sensor. // In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and // would generate too many unnecessary events. - var requestAnimationFrame = window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || + var requestAnimationFrame = globalWindow.requestAnimationFrame || + globalWindow.mozRequestAnimationFrame || + globalWindow.webkitRequestAnimationFrame || function (fn) { - return window.setTimeout(fn, 20); + return globalWindow.setTimeout(fn, 20); + }; + + var cancelAnimationFrame = globalWindow.cancelAnimationFrame || + globalWindow.mozCancelAnimationFrame || + globalWindow.webkitCancelAnimationFrame || + function (timer) { + globalWindow.clearTimeout(timer); }; /** @@ -74,6 +88,18 @@ } } + /** + * Apply CSS styles to element. + * + * @param {HTMLElement} element + * @param {Object} style + */ + function setStyle(element, style) { + Object.keys(style).forEach(function(key) { + element.style[key] = style[key]; + }); + } + /** * Class for dimension change detection. * @@ -83,6 +109,9 @@ * @constructor */ var ResizeSensor = function(element, callback) { + //Is used when checking in reset() only for invisible elements + var lastAnimationFrameForInvisibleCheck = 0; + /** * * @constructor @@ -131,35 +160,64 @@ element.resizeSensor = document.createElement('div'); element.resizeSensor.dir = 'ltr'; element.resizeSensor.className = 'resize-sensor'; - var style = 'pointer-events: none; position: absolute; left: 0px; top: 0px; right: 0; bottom: 0; ' + - 'overflow: hidden; z-index: -1; visibility: hidden; max-width: 100%;'; - var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; - - element.resizeSensor.style.cssText = style; - element.resizeSensor.innerHTML = - '
' + - '
' + - '
' + - '
' + - '
' + - '
'; + + var style = { + pointerEvents: 'none', + position: 'absolute', + left: '0px', + top: '0px', + right: '0px', + bottom: '0px', + overflow: 'hidden', + zIndex: '-1', + visibility: 'hidden', + maxWidth: '100%' + }; + var styleChild = { + position: 'absolute', + left: '0px', + top: '0px', + transition: '0s', + }; + + setStyle(element.resizeSensor, style); + + var expand = document.createElement('div'); + expand.className = 'resize-sensor-expand'; + setStyle(expand, style); + + var expandChild = document.createElement('div'); + setStyle(expandChild, styleChild); + expand.appendChild(expandChild); + + var shrink = document.createElement('div'); + shrink.className = 'resize-sensor-shrink'; + setStyle(shrink, style); + + var shrinkChild = document.createElement('div'); + setStyle(shrinkChild, styleChild); + setStyle(shrinkChild, { width: '200%', height: '200%' }); + shrink.appendChild(shrinkChild); + + element.resizeSensor.appendChild(expand); + element.resizeSensor.appendChild(shrink); element.appendChild(element.resizeSensor); var computedStyle = window.getComputedStyle(element); var position = computedStyle ? computedStyle.getPropertyValue('position') : null; - if ('absolute' !== position && 'relative' !== position && 'fixed' !== position) { + if ('absolute' !== position && 'relative' !== position && 'fixed' !== position && 'sticky' !== position) { element.style.position = 'relative'; } - var expand = element.resizeSensor.childNodes[0]; - var expandChild = expand.childNodes[0]; - var shrink = element.resizeSensor.childNodes[1]; - var dirty, rafId; + var dirty = false; + + //last request animation frame id used in onscroll event + var rafId = 0; var size = getElementSize(element); - var lastWidth = size.width; - var lastHeight = size.height; + var lastWidth = 0; + var lastHeight = 0; var initialHiddenCheck = true; - var lastAnimationFrame = 0; + lastAnimationFrameForInvisibleCheck = 0; var resetExpandShrink = function () { var width = element.offsetWidth; @@ -181,10 +239,9 @@ var invisible = element.offsetWidth === 0 && element.offsetHeight === 0; if (invisible) { // Check in next frame - if (!lastAnimationFrame){ - lastAnimationFrame = requestAnimationFrame(function(){ - lastAnimationFrame = 0; - + if (!lastAnimationFrameForInvisibleCheck){ + lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ + lastAnimationFrameForInvisibleCheck = 0; reset(); }); } @@ -235,8 +292,11 @@ addEvent(expand, 'scroll', onScroll); addEvent(shrink, 'scroll', onScroll); - // Fix for custom Elements - requestAnimationFrame(reset); + // Fix for custom Elements and invisible elements + lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ + lastAnimationFrameForInvisibleCheck = 0; + reset(); + }); } forEachElement(element, function(elem){ @@ -244,17 +304,28 @@ }); this.detach = function(ev) { + // clean up the unfinished animation frame to prevent a potential endless requestAnimationFrame of reset + if (lastAnimationFrameForInvisibleCheck) { + cancelAnimationFrame(lastAnimationFrameForInvisibleCheck); + lastAnimationFrameForInvisibleCheck = 0; + } ResizeSensor.detach(element, ev); }; this.reset = function() { - element.resizeSensor.resetSensor(); + //To prevent invoking element.resizeSensor.resetSensor if it's undefined + if (element.resizeSensor.resetSensor) { + element.resizeSensor.resetSensor(); + } }; }; ResizeSensor.reset = function(element) { forEachElement(element, function(elem){ - elem.resizeSensor.resetSensor(); + //To prevent invoking element.resizeSensor.resetSensor if it's undefined + if (element.resizeSensor.resetSensor) { + elem.resizeSensor.resetSensor(); + } }); }; diff --git a/tests/demo.html b/tests/demo.html index d8a75c2..22968f0 100644 --- a/tests/demo.html +++ b/tests/demo.html @@ -97,6 +97,15 @@ .example-2[min-width~="400px"] .example-2-box { background-color: gray; } + + #example-invisible { + display: none; + } + + #example-invisible[min-width~="100px"]{ + color: red; + font-weight: bold; + } + + +

Performance Demo

@@ -393,4 +422,4 @@

Performance Demo

- \ No newline at end of file +