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 28c459f..964230f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "css-element-queries", - "version": "1.0.2", - "description": "CSS-Element-Queries Polyfill. proof-of-concept for high-speed element dimension/media queries in valid css.", + "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 65dc3c3..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(); } /** @@ -324,7 +323,7 @@ } } - element.resizeSensor = new ResizeSensor(element, check); + element.resizeSensorInstance = new ResizeSensor(element, check); check(); } @@ -409,9 +408,11 @@ document.body.addEventListener(animationStart, function (e) { var element = e.target; - var styles = window.getComputedStyle(element, null); + var styles = element && window.getComputedStyle(element, null); + var animationName = styles && styles.getPropertyValue('animation-name'); + var requiresSetup = animationName && (-1 !== animationName.indexOf('element-queries')); - if (-1 !== styles.getPropertyValue('animation-name').indexOf('element-queries')) { + if (requiresSetup) { element.elementQueriesSensor = new ResizeSensor(element, function () { if (element.elementQueriesSetupInformation) { element.elementQueriesSetupInformation.call(); @@ -439,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); @@ -482,11 +483,11 @@ delete element.elementQueriesSetupInformation; delete element.elementQueriesSensor; - } else if (element.resizeSensor) { + } else if (element.resizeSensorInstance) { //responsive image - element.resizeSensor.detach(); - delete element.resizeSensor; + element.resizeSensorInstance.detach(); + delete element.resizeSensorInstance; } }; diff --git a/src/ResizeSensor.d.ts b/src/ResizeSensor.d.ts index fe5bb03..3fba8c6 100644 --- a/src/ResizeSensor.d.ts +++ b/src/ResizeSensor.d.ts @@ -1,6 +1,38 @@ -declare class ResizeSensor { - constructor(element: (Element | Element[]), callback: Function); - detach(callback: Function): void; +export declare interface Size { + width: number; + height: number; } -export = ResizeSensor; +export declare type ResizeSensorCallback = (size: Size) => void; + +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; +} + +export default ResizeSensor; diff --git a/src/ResizeSensor.js b/src/ResizeSensor.js index c8fcfd6..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,8 +109,8 @@ * @constructor */ var ResizeSensor = function(element, callback) { - - var observer; + //Is used when checking in reset() only for invisible elements + var lastAnimationFrameForInvisibleCheck = 0; /** * @@ -134,60 +160,88 @@ element.resizeSensor = document.createElement('div'); element.resizeSensor.dir = 'ltr'; element.resizeSensor.className = 'resize-sensor'; - var style = 'position: absolute; left: -10px; top: -10px; 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 = false; - var dirty, rafId; + //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 initialHiddenCheck = true, resetRAF_id; - + var lastWidth = 0; + var lastHeight = 0; + var initialHiddenCheck = true; + lastAnimationFrameForInvisibleCheck = 0; var resetExpandShrink = function () { - expandChild.style.width = '100000px'; - expandChild.style.height = '100000px'; + var width = element.offsetWidth; + var height = element.offsetHeight; + + expandChild.style.width = (width + 10) + 'px'; + expandChild.style.height = (height + 10) + 'px'; - expand.scrollLeft = 100000; - expand.scrollTop = 100000; + expand.scrollLeft = width + 10; + expand.scrollTop = height + 10; - shrink.scrollLeft = 100000; - shrink.scrollTop = 100000; + shrink.scrollLeft = width + 10; + shrink.scrollTop = height + 10; }; var reset = function() { // Check if element is hidden if (initialHiddenCheck) { - if (!expand.scrollTop && !expand.scrollLeft) { - - // reset - resetExpandShrink(); - + var invisible = element.offsetWidth === 0 && element.offsetHeight === 0; + if (invisible) { // Check in next frame - if (!resetRAF_id){ - resetRAF_id = requestAnimationFrame(function(){ - resetRAF_id = 0; - + if (!lastAnimationFrameForInvisibleCheck){ + lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ + lastAnimationFrameForInvisibleCheck = 0; reset(); }); } @@ -238,53 +292,40 @@ addEvent(expand, 'scroll', onScroll); addEvent(shrink, 'scroll', onScroll); - // Fix for custom Elements - requestAnimationFrame(reset); - } - - if (typeof ResizeObserver !== "undefined") { - observer = new ResizeObserver(function(element){ - forEachElement(element, function (elem) { - callback.call( - this, - { - width: elem.contentRect.width, - height: elem.contentRect.height - } - ); - }); - }); - if (element !== undefined) { - forEachElement(element, function(elem){ - observer.observe(elem); - }); - } - } - else { - forEachElement(element, function(elem){ - attachResizeEvent(elem, callback); + // Fix for custom Elements and invisible elements + lastAnimationFrameForInvisibleCheck = requestAnimationFrame(function(){ + lastAnimationFrameForInvisibleCheck = 0; + reset(); }); } + forEachElement(element, function(elem){ + attachResizeEvent(elem, callback); + }); + this.detach = function(ev) { - if (typeof ResizeObserver != "undefined") { - forEachElement(element, function(elem){ - observer.unobserve(elem); - }); - } - else { - ResizeSensor.detach(element, 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, ev) { + 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(); + } }); }; @@ -305,6 +346,28 @@ }); }; + if (typeof MutationObserver !== "undefined") { + var observer = new MutationObserver(function (mutations) { + for (var i in mutations) { + if (mutations.hasOwnProperty(i)) { + var items = mutations[i].addedNodes; + for (var j = 0; j < items.length; j++) { + if (items[j].resizeSensor) { + ResizeSensor.reset(items[j]); + } + } + } + } + }); + + document.addEventListener("DOMContentLoaded", function (event) { + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); + } + return ResizeSensor; })); 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 + diff --git a/tests/late-trigger.html b/tests/late-trigger.html new file mode 100644 index 0000000..71a2b17 --- /dev/null +++ b/tests/late-trigger.html @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tests/mutation/app.js b/tests/mutation/app.js new file mode 100644 index 0000000..2100612 --- /dev/null +++ b/tests/mutation/app.js @@ -0,0 +1,75 @@ +var __values = (this && this.__values) || function (o) { + var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + if (m) return m.call(o); + return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; +}; +var e_1, _a, e_2, _b; +var state = { + dragged: null +}; +var i = 0; +try { + for (var _c = __values(document.getElementsByClassName('drag')), _d = _c.next(); !_d.done; _d = _c.next()) { + var item = _d.value; + i++; + item.setAttribute('draggable', 'true'); + item.setAttribute('id', 'drag-' + i); + (function (element) { + var title = 'Drag me #' + i; + element.setAttribute('data-label', title); + new ResizeSensor(element, function (size) { + element.setAttribute('data-label', title + " (" + size.width + "x" + size.height + ")"); + }); + })(item); + item.addEventListener('dragstart', function (event) { + state.dragged = event.target; + event.dataTransfer.setData('text', 'thanks firefox'); + event.dataTransfer.dropEffect = 'move'; + }); + } +} +catch (e_1_1) { e_1 = { error: e_1_1 }; } +finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_1) throw e_1.error; } +} +var _loop_1 = function (item) { + (function (element) { + item.addEventListener('drop', function (event) { + event.preventDefault(); + item.classList.remove('drag-hover'); + state.dragged.parentNode.removeChild(state.dragged); + element.appendChild(state.dragged); + state.dragged = null; + }); + })(item); + item.addEventListener('dragleave', function (event) { + item.classList.remove('drag-hover'); + }); + item.addEventListener('dragover', function (event) { + item.classList.add('drag-hover'); + }); + item.addEventListener('dragover', function (event) { + event.preventDefault(); + }); +}; +try { + for (var _e = __values(document.getElementsByClassName('container')), _f = _e.next(); !_f.done; _f = _e.next()) { + var item = _f.value; + _loop_1(item); + } +} +catch (e_2_1) { e_2 = { error: e_2_1 }; } +finally { + try { + if (_f && !_f.done && (_b = _e.return)) _b.call(_e); + } + finally { if (e_2) throw e_2.error; } +} diff --git a/tests/mutation/app.ts b/tests/mutation/app.ts new file mode 100644 index 0000000..15974f7 --- /dev/null +++ b/tests/mutation/app.ts @@ -0,0 +1,56 @@ +declare const ResizeSensor; + +const state: { + dragged: Element +} = { + dragged: null +}; + +let i = 0; + +for (const item of document.getElementsByClassName('drag')) { + i++; + item.setAttribute('draggable', 'true'); + item.setAttribute('id', 'drag-' + i); + + (element => { + const title = 'Drag me #' + i; + element.setAttribute('data-label', title); + + new ResizeSensor(element, (size) => { + element.setAttribute('data-label', `${title} (${size.width}x${size.height})`); + }); + })(item); + + item.addEventListener('dragstart', (event: DragEvent) => { + state.dragged = event.target; + event.dataTransfer.setData('text', 'thanks firefox'); + event.dataTransfer.dropEffect = 'move'; + }); +} + +for (const item of document.getElementsByClassName('container')) { + (element => { + item.addEventListener('drop', (event) => { + event.preventDefault(); + item.classList.remove('drag-hover'); + + state.dragged.parentNode.removeChild(state.dragged); + element.appendChild(state.dragged); + + state.dragged = null; + }); + })(item); + + item.addEventListener('dragleave', (event) => { + item.classList.remove('drag-hover'); + }); + + item.addEventListener('dragover', (event) => { + item.classList.add('drag-hover'); + }); + + item.addEventListener('dragover', (event) => { + event.preventDefault(); + }); +} diff --git a/tests/mutation/index.html b/tests/mutation/index.html new file mode 100644 index 0000000..6d2ddce --- /dev/null +++ b/tests/mutation/index.html @@ -0,0 +1,76 @@ + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7b56f0e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es5", + "downlevelIteration": true, + "lib" : ["dom","es6","dom.iterable","scripthost", "es2015.iterable", "es2015.collection"] + } +} \ No newline at end of file