From 9877eb519c308bf02b22c33491471001ae766478 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Fri, 14 Feb 2020 17:22:40 +0200 Subject: [PATCH 1/4] Use Resize/MutationObserver to detect detach/attach/resize --- docs/getting-started/integration.md | 15 - docs/getting-started/v3-migration.md | 4 +- package-lock.json | 23 +- package.json | 2 +- rollup.config.js | 21 +- rollup.plugins.js | 56 +--- samples/advanced/content-security-policy.css | 20 -- samples/advanced/content-security-policy.html | 27 -- samples/advanced/content-security-policy.js | 53 ---- samples/samples.js | 3 - src/core/core.controller.js | 36 ++- src/helpers/helpers.dom.js | 2 +- src/index.js | 2 - src/platform/platform.dom.css | 47 --- src/platform/platform.dom.js | 282 +++++++----------- src/platform/platform.js | 2 - test/specs/core.controller.tests.js | 44 --- 17 files changed, 145 insertions(+), 494 deletions(-) delete mode 100644 samples/advanced/content-security-policy.css delete mode 100644 samples/advanced/content-security-policy.html delete mode 100644 samples/advanced/content-security-policy.js delete mode 100644 src/platform/platform.dom.css delete mode 100644 src/platform/platform.js diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md index 83301191a32..049e23a56b2 100644 --- a/docs/getting-started/integration.md +++ b/docs/getting-started/integration.md @@ -64,18 +64,3 @@ require(['moment'], function() { }); }); ``` - -## Content Security Policy - -By default, Chart.js injects CSS directly into the DOM. For webpages secured using [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), this requires to allow `style-src 'unsafe-inline'`. For stricter CSP environments, where only `style-src 'self'` is allowed, the following CSS file needs to be manually added to your webpage: - -```html - -``` - -And the style injection must be turned off **before creating the first chart**: - -```javascript -// Disable automatic style injection -Chart.platform.disableCSSInjection = true; -``` diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index c8d239d02dd..0aee422ab90 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -7,6 +7,7 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * Completely rewritten animation system * Rewritten filler plugin with numerous bug fixes * API Documentation generated and verified by TypeScript +* No more CSS injection * Tons of bug fixes ## End user migration @@ -88,6 +89,7 @@ Animation system was completely rewritten in Chart.js v3. Each property can now * `Chart.chart.chart` * `Chart.Controller` * `Chart.prototype.generateLegend` +* `Chart.platform`. It only contained `disableCSSInjection`. CSS is never injected in v3. * `Chart.types` * `Chart.Tooltip` is now provided by the tooltip plugin. The positioners can be accessed from `tooltipPlugin.positioners` * `DatasetController.addElementAndReset` @@ -253,6 +255,6 @@ Animation system was completely rewritten in Chart.js v3. Each property can now #### Platform -* `Chart.platform` is no longer the platform object used by charts. It contains only a single configuration option, `disableCSSInjection`. Every chart instance now has a separate platform instance. +* `Chart.platform` is no longer the platform object used by charts. Every chart instance now has a separate platform instance. * `Chart.platforms` is an object that contains two usable platform classes, `BasicPlatform` and `DomPlatform`. It also contains `BasePlatform`, a class that all platforms must extend from. * If the canvas passed in is an instance of `OffscreenCanvas`, the `BasicPlatform` is automatically used. diff --git a/package-lock.json b/package-lock.json index 5347b531a1f..333eba827fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3640,23 +3640,6 @@ } } }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -14463,6 +14446,12 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", diff --git a/package.json b/package.json index a66cdac916b..6a0dc260954 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@babel/plugin-transform-object-assign": "^7.8.3", "@babel/preset-env": "^7.8.4", "babel-preset-es2015-rollup": "^3.0.0", - "clean-css": "^4.2.3", "coveralls": "^3.0.9", "eslint": "^6.8.0", "eslint-config-chartjs": "^0.2.0", @@ -66,6 +65,7 @@ "merge-stream": "^1.0.1", "moment": "^2.10.2", "pixelmatch": "^5.0.0", + "resize-observer-polyfill": "^1.5.1", "rollup": "^1.31.0", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-cleanup": "^3.1.1", diff --git a/rollup.config.js b/rollup.config.js index 459de3ce69d..60daa31fc5e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,6 @@ const babel = require('rollup-plugin-babel'); const cleanup = require('rollup-plugin-cleanup'); const terser = require('rollup-plugin-terser').terser; const optional = require('./rollup.plugins').optional; -const stylesheet = require('./rollup.plugins').stylesheet; const pkg = require('./package.json'); const input = 'src/index.js'; @@ -29,13 +28,12 @@ module.exports = [ resolve(), commonjs(), babel(), - stylesheet({ - extract: true - }), optional({ include: ['moment'] }), - cleanup(), + cleanup({ + sourcemap: true + }) ], output: { name: 'Chart', @@ -60,10 +58,6 @@ module.exports = [ optional({ include: ['moment'] }), - stylesheet({ - extract: true, - minify: true - }), terser({ output: { preamble: banner @@ -93,10 +87,7 @@ module.exports = [ resolve(), commonjs(), babel(), - stylesheet({ - extract: true - }), - cleanup(), + cleanup() ], output: { name: 'Chart', @@ -118,10 +109,6 @@ module.exports = [ resolve(), commonjs(), babel(), - stylesheet({ - extract: true, - minify: true - }), terser({ output: { preamble: banner diff --git a/rollup.plugins.js b/rollup.plugins.js index 8c8dd9624c1..0a8598d124d 100644 --- a/rollup.plugins.js +++ b/rollup.plugins.js @@ -1,7 +1,4 @@ -/* eslint-env es6 */ -const cleancss = require('clean-css'); -const path = require('path'); - +/* eslint-disable import/no-commonjs */ const UMD_WRAPPER_RE = /(\(function \(global, factory\) \{)((?:\s.*?)*)(\}\(this,)/; const CJS_FACTORY_RE = /(module.exports = )(factory\(.*?\))( :)/; const AMD_FACTORY_RE = /(define\()(.*?, factory)(\) :)/; @@ -24,7 +21,7 @@ function optional(config = {}) { let factory = (CJS_FACTORY_RE.exec(content) || [])[2]; let updated = false; - for (let lib of chunk.imports) { + for (const lib of chunk.imports) { if (!include || include.indexOf(lib) !== -1) { const regex = new RegExp(`require\\('${lib}'\\)`); if (!regex.test(factory)) { @@ -58,53 +55,6 @@ function optional(config = {}) { }; } -// https://github.com/chartjs/Chart.js/issues/5208 -function stylesheet(config = {}) { - const minifier = new cleancss(); - const styles = []; - - return { - name: 'stylesheet', - transform(code, id) { - // Note that 'id' can be mapped to a CJS proxy import, in which case - // 'id' will start with 'commonjs-proxy', so let's first check if we - // are importing an existing css file (i.e. startsWith()). - if (!id.startsWith(path.resolve('.')) || !id.endsWith('.css')) { - return; - } - - if (config.minify) { - code = minifier.minify(code).styles; - } - - // keep track of all imported stylesheets (already minified) - styles.push(code); - - return { - code: 'export default ' + JSON.stringify(code) - }; - }, - generateBundle(opts, bundle) { - if (!config.extract) { - return; - } - - const entry = Object.keys(bundle).find(v => bundle[v].isEntry); - const name = (entry || '').replace(/\.js$/i, '.css'); - if (!name) { - this.error('failed to guess the output file name'); - } - - this.emitFile({ - type: 'asset', - source: styles.filter(v => !!v).join(''), - fileName: name - }); - } - }; -} - module.exports = { - optional, - stylesheet + optional }; diff --git a/samples/advanced/content-security-policy.css b/samples/advanced/content-security-policy.css deleted file mode 100644 index 8e5b8fd8cbb..00000000000 --- a/samples/advanced/content-security-policy.css +++ /dev/null @@ -1,20 +0,0 @@ -.content { - max-width: 640px; - margin: auto; - padding: 1rem; -} - -.note { - font-family: sans-serif; - color: #5050a0; - line-height: 1.4; - margin-bottom: 1rem; - padding: 1rem; -} - -code { - background-color: #f5f5ff; - border: 1px solid #d0d0fa; - border-radius: 4px; - padding: 0.05rem 0.25rem; -} diff --git a/samples/advanced/content-security-policy.html b/samples/advanced/content-security-policy.html deleted file mode 100644 index fb2805cdc06..00000000000 --- a/samples/advanced/content-security-policy.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - Scriptable > Bubble | Chart.js sample - - - - - - - -
-
- In order to support a strict content security policy (default-src 'self'), - this page manually loads Chart.min.css and turns off the automatic style - injection by setting Chart.platform.disableCSSInjection = true;. -
-
- -
-
- - diff --git a/samples/advanced/content-security-policy.js b/samples/advanced/content-security-policy.js deleted file mode 100644 index a974fc6f349..00000000000 --- a/samples/advanced/content-security-policy.js +++ /dev/null @@ -1,53 +0,0 @@ -var utils = Samples.utils; - -utils.srand(110); -// CSP: disable automatic style injection -Chart.platform.disableCSSInjection = true; - -function generateData() { - var DATA_COUNT = 16; - var MIN_XY = -150; - var MAX_XY = 100; - var data = []; - var i; - - for (i = 0; i < DATA_COUNT; ++i) { - data.push({ - x: utils.rand(MIN_XY, MAX_XY), - y: utils.rand(MIN_XY, MAX_XY), - v: utils.rand(0, 1000) - }); - } - - return data; -} - -window.addEventListener('load', function() { - new Chart('chart-0', { - type: 'bubble', - data: { - datasets: [{ - backgroundColor: utils.color(0), - data: generateData() - }, { - backgroundColor: utils.color(1), - data: generateData() - }] - }, - options: { - aspectRatio: 1, - legend: false, - tooltip: false, - elements: { - point: { - radius: function(context) { - var value = context.dataset.data[context.dataIndex]; - var size = context.chart.width; - var base = Math.abs(value.v) / 1000; - return (size / 24) * base; - } - } - } - } - }); -}); diff --git a/samples/samples.js b/samples/samples.js index 7811c8690d4..1f4fc37708f 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -244,9 +244,6 @@ items: [{ title: 'Progress bar', path: 'advanced/progress-bar.html' - }, { - title: 'Content Security Policy', - path: 'advanced/content-security-policy.html' }, { title: 'Polar Area Radial Gradient', path: 'advanced/radial-gradient.html' diff --git a/src/core/core.controller.js b/src/core/core.controller.js index aca27dc759c..c535427b355 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -7,6 +7,7 @@ import layouts from './core.layouts'; import {BasicPlatform, DomPlatform} from '../platform/platforms'; import plugins from './core.plugins'; import scaleService from '../core/core.scaleService'; +import {getMaximumWidth, getMaximumHeight} from '../helpers/helpers.dom'; /** * @typedef { import("../platform/platform.base").IEvent } IEvent @@ -246,15 +247,16 @@ class Chart { // Before init plugin notification plugins.notify(me, 'beforeInit'); - helpers.dom.retinaScale(me, me.options.devicePixelRatio); - - me.bindEvents(); + // helpers.dom.retinaScale(me, me.options.devicePixelRatio); if (me.options.responsive) { - // Initial resize before chart draws (must be silent to preserve initial animations). me.resize(true); + } else { + helpers.dom.retinaScale(me, me.options.devicePixelRatio); } + me.bindEvents(); + // After init plugin notification plugins.notify(me, 'afterInit'); @@ -285,19 +287,23 @@ class Chart { return this; } - resize(silent) { + resize(silent, width, height) { const me = this; const options = me.options; const canvas = me.canvas; - const aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null; - const oldRatio = me.currentDevicePixelRatio; + const aspectRatio = options.maintainAspectRatio && me.aspectRatio; + if (height === undefined) { + width = getMaximumWidth(canvas); + height = getMaximumHeight(canvas); + } // the canvas render width and height will be casted to integers so make sure that // the canvas display style uses the same integer values to avoid blurring effect. + const newWidth = Math.max(0, Math.floor(width)); + const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : height)); - // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed - const newWidth = Math.max(0, Math.floor(helpers.dom.getMaximumWidth(canvas))); - const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.dom.getMaximumHeight(canvas))); + // detect devicePixelRation changes + const oldRatio = me.currentDevicePixelRatio; const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio(); if (me.width === newWidth && me.height === newHeight && oldRatio === newRatio) { @@ -903,7 +909,7 @@ class Chart { if (canvas) { me.unbindEvents(); helpers.canvas.clear(me); - me.platform.releaseContext(me.ctx); + me.platform.releaseContext(me, me.ctx); me.canvas = null; me.ctx = null; } @@ -932,13 +938,11 @@ class Chart { listeners[type] = listener; }); - // Elements used to detect size change should not be injected for non responsive charts. - // See https://github.com/chartjs/Chart.js/issues/2210 if (me.options.responsive) { - listener = function() { - me.resize(); + // resize + listener = function(width, height) { + me.resize(false, width, height); }; - me.platform.addEventListener(me, 'resize', listener); listeners.resize = listener; } diff --git a/src/helpers/helpers.dom.js b/src/helpers/helpers.dom.js index 86114ff503e..e71ff3352d3 100644 --- a/src/helpers/helpers.dom.js +++ b/src/helpers/helpers.dom.js @@ -9,7 +9,7 @@ function isConstrainedValue(value) { /** * @private */ -function _getParentNode(domNode) { +export function _getParentNode(domNode) { let parent = domNode.parentNode; if (parent && parent.toString() === '[object ShadowRoot]') { parent = parent.host; diff --git a/src/index.js b/src/index.js index 695ae4fe9a3..d1b19287a79 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,6 @@ import elements from './elements/index'; import Interaction from './core/core.interaction'; import layouts from './core/core.layouts'; import platforms from './platform/platforms'; -import platform from './platform/platform'; import pluginsCore from './core/core.plugins'; import Scale from './core/core.scale'; import scaleService from './core/core.scaleService'; @@ -35,7 +34,6 @@ Chart.elements = elements; Chart.Interaction = Interaction; Chart.layouts = layouts; Chart.platforms = platforms; -Chart.platform = platform; Chart.plugins = pluginsCore; Chart.Scale = Scale; Chart.scaleService = scaleService; diff --git a/src/platform/platform.dom.css b/src/platform/platform.dom.css deleted file mode 100644 index 5e749593eeb..00000000000 --- a/src/platform/platform.dom.css +++ /dev/null @@ -1,47 +0,0 @@ -/* - * DOM element rendering detection - * https://davidwalsh.name/detect-node-insertion - */ -@keyframes chartjs-render-animation { - from { opacity: 0.99; } - to { opacity: 1; } -} - -.chartjs-render-monitor { - animation: chartjs-render-animation 0.001s; -} - -/* - * DOM element resizing detection - * https://github.com/marcj/css-element-queries - */ -.chartjs-size-monitor, -.chartjs-size-monitor-expand, -.chartjs-size-monitor-shrink { - position: absolute; - direction: ltr; - left: 0; - top: 0; - right: 0; - bottom: 0; - overflow: hidden; - pointer-events: none; - visibility: hidden; - z-index: -1; -} - -.chartjs-size-monitor-expand > div { - position: absolute; - width: 1000000px; - height: 1000000px; - left: 0; - top: 0; -} - -.chartjs-size-monitor-shrink > div { - position: absolute; - width: 200%; - height: 200%; - left: 0; - top: 0; -} diff --git a/src/platform/platform.dom.js b/src/platform/platform.dom.js index 0bc5aebcc8c..ec6cdfb7bd3 100644 --- a/src/platform/platform.dom.js +++ b/src/platform/platform.dom.js @@ -4,17 +4,10 @@ import helpers from '../helpers/index'; import BasePlatform from './platform.base'; -import platform from './platform'; - -// @ts-ignore -import stylesheet from './platform.dom.css'; +import {_getParentNode} from '../helpers/helpers.dom'; +import ResizeObserver from 'resize-observer-polyfill'; const EXPANDO_KEY = '$chartjs'; -const CSS_PREFIX = 'chartjs-'; -const CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor'; -const CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor'; -const CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation'; -const ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart']; /** * DOM event types -> Chart.js event types. @@ -53,7 +46,7 @@ function readUsedSize(element, property) { * since responsiveness is handled by the controller.resize() method. The config is used * to determine the aspect ratio to apply in case no explicit height has been specified. */ -function initCanvas(canvas, config) { +function initCanvas(canvas, config) { // eslint-disable-line no-unused-vars const style = canvas.style; // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it @@ -78,6 +71,8 @@ function initCanvas(canvas, config) { // elements, which would interfere with the responsive resize process. // https://github.com/chartjs/Chart.js/issues/2538 style.display = style.display || 'block'; + // Include possible borders in the size + style.boxSizing = style.boxSizing || 'border-box'; if (renderWidth === null || renderWidth === '') { const displayWidth = readUsedSize(canvas, 'width'); @@ -169,147 +164,109 @@ function throttled(fn, thisArg) { }; } -function createDiv(cls) { - const el = document.createElement('div'); - el.className = cls || ''; - return el; -} - -// Implementation based on https://github.com/marcj/css-element-queries -function createResizer(domPlatform, handler) { - const maxSize = 1000000; - - // NOTE(SB) Don't use innerHTML because it could be considered unsafe. - // https://github.com/chartjs/Chart.js/issues/5902 - const resizer = createDiv(CSS_SIZE_MONITOR); - const expand = createDiv(CSS_SIZE_MONITOR + '-expand'); - const shrink = createDiv(CSS_SIZE_MONITOR + '-shrink'); - - expand.appendChild(createDiv()); - shrink.appendChild(createDiv()); - - resizer.appendChild(expand); - resizer.appendChild(shrink); - domPlatform._reset = function() { - expand.scrollLeft = maxSize; - expand.scrollTop = maxSize; - shrink.scrollLeft = maxSize; - shrink.scrollTop = maxSize; - }; - - const onScroll = function() { - domPlatform._reset(); - handler(); - }; - - addListener(expand, 'scroll', onScroll.bind(expand, 'expand')); - addListener(shrink, 'scroll', onScroll.bind(shrink, 'shrink')); - - return resizer; -} - -// https://davidwalsh.name/detect-node-insertion -function watchForRender(node, handler) { - const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); - const proxy = expando.renderProxy = function(e) { - if (e.animationName === CSS_RENDER_ANIMATION) { - handler(); - } - }; - - ANIMATION_START_EVENTS.forEach((type) => { - addListener(node, type, proxy); +/** + * Watch for resize of `element`. + * Calling `fn` is limited to once per animation frame + * @param {Element} element - The element to monitor + * @param {function} fn - Callback function to call when resized + * @return {ResizeObserver=} + */ +function watchForResize(element, fn) { + const observer = new ResizeObserver(entries => { + const entry = entries[0]; + helpers.requestAnimFrame.call(window, () => { + fn(entry.contentRect.width, entry.contentRect.height); + }); }); - - // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class - // is removed then added back immediately (same animation frame?). Accessing the - // `offsetParent` property will force a reflow and re-evaluate the CSS animation. - // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics - // https://github.com/chartjs/Chart.js/issues/4737 - expando.reflow = !!node.offsetParent; - - node.classList.add(CSS_RENDER_MONITOR); + observer.observe(element); + return observer; } -function unwatchForRender(node) { - const expando = node[EXPANDO_KEY] || {}; - const proxy = expando.renderProxy; - - if (proxy) { - ANIMATION_START_EVENTS.forEach((type) => { - removeListener(node, type, proxy); +/** + * Detect attachment of `element` or its direct `parent` to DOM + * @param {Element} element - The element to watch for + * @param {function} fn - Callback function to call when attachment is detected + * @return {MutationObserver=} + */ +function watchForAttachment(element, fn) { + const observer = new MutationObserver(entries => { + const parent = _getParentNode(element); + entries.forEach(entry => { + for (let i = 0; i < entry.addedNodes.length; i++) { + const added = entry.addedNodes[i]; + if (added === element || added === parent) { + fn(entry.target); + } + } }); - - delete expando.renderProxy; - } - - node.classList.remove(CSS_RENDER_MONITOR); + }); + observer.observe(document, {childList: true, subtree: true}); + return observer; } -function addResizeListener(node, listener, chart, domPlatform) { - const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); - - // Let's keep track of this added resizer and thus avoid DOM query when removing it. - const resizer = expando.resizer = createResizer(domPlatform, throttled(() => { - if (expando.resizer) { - const container = chart.options.maintainAspectRatio && node.parentNode; - const w = container ? container.clientWidth : 0; - listener(createEvent('resize', chart)); - if (container && container.clientWidth < w && chart.canvas) { - // If the container size shrank during chart resize, let's assume - // scrollbar appeared. So we resize again with the scrollbar visible - - // effectively making chart smaller and the scrollbar hidden again. - // Because we are inside `throttled`, and currently `ticking`, scroll - // events are ignored during this whole 2 resize process. - // If we assumed wrong and something else happened, we are resizing - // twice in a frame (potential performance issue) - listener(createEvent('resize', chart)); - } - } - })); - - // The resizer needs to be attached to the node parent, so we first need to be - // sure that `node` is attached to the DOM before injecting the resizer element. - watchForRender(node, () => { - if (expando.resizer) { - const container = node.parentNode; - if (container && container !== resizer.parentNode) { - container.insertBefore(resizer, container.firstChild); +/** + * Watch for detachment of `element` from its direct `parent`. + * @param {Element} element - The element to watch + * @param {function} fn - Callback function to call when detached. + * @return {MutationObserver=} + */ +function watchForDetachment(element, fn) { + const parent = _getParentNode(element); + if (!parent) { + return; + } + const observer = new MutationObserver(entries => { + entries.forEach(entry => { + for (let i = 0; i < entry.removedNodes.length; i++) { + if (entry.removedNodes[i] === element) { + fn(); + break; + } } - - // The container size might have changed, let's reset the resizer state. - domPlatform._reset(); - } + }); }); + observer.observe(parent, {childList: true}); + return observer; } -function removeResizeListener(node) { - const expando = node[EXPANDO_KEY] || {}; - const resizer = expando.resizer; - - delete expando.resizer; - unwatchForRender(node); - - if (resizer && resizer.parentNode) { - resizer.parentNode.removeChild(resizer); +function removeObserver(proxies, type) { + const observer = proxies[type]; + if (observer) { + observer.disconnect(); + proxies[type] = undefined; } } +function unlistenForResize(proxies) { + removeObserver(proxies, 'attach'); + removeObserver(proxies, 'detach'); + removeObserver(proxies, 'resize'); +} + + /** - * Injects CSS styles inline if the styles are not already present. - * @param {Node} rootNode - the HTMLDocument|ShadowRoot node to contain the