From 9b21ca1b882a8eece231c67de1e3fd8164185a24 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Mon, 2 Sep 2024 20:41:31 +0100 Subject: [PATCH 1/3] Allow detached autosize DOM elements to be garbage collected Use weak map + resize observer to make DOM + state so that an autosize instance does not prevent the DOM node from getting garbage collected. --- src/autosize.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/autosize.js b/src/autosize.js index c3f1a13..2a36de7 100644 --- a/src/autosize.js +++ b/src/autosize.js @@ -1,4 +1,24 @@ -const assignedElements = new Map(); +const modernEnv = typeof ResizeObserver === "function" && typeof WeakMap === "function"; + +const assignedElements = modernEnv ? new WeakMap() : new Map(); + +const resizeObserver = createResizeObserver(); + +function createResizeObserver() { + if (modernEnv) { + return new ResizeObserver((entries) => entries.forEach(e => onResize(e.target))); + } + // If not a modern environment, we use a Map instead of a WeakMap, which is iterable. + window.addEventListener('resize', () => assignedElements.forEach((_, el) => onResize(el))); + return { observe: () => {}, unobserve: () => {} }; +} + +function onResize(el) { + const instance = assignedElements.get(el); + if (instance !== undefined) { + instance.update(); + } +} function assign(ta) { if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || assignedElements.has(ta)) return; @@ -130,6 +150,7 @@ function assign(ta) { window.removeEventListener('resize', fullSetHeight); // future todo: consider replacing with ResizeObserver Object.keys(style).forEach(key => ta.style[key] = style[key]); assignedElements.delete(ta); + resizeObserver.unobserve(ta); }).bind(ta, { height: ta.style.height, resize: ta.style.resize, @@ -142,7 +163,7 @@ function assign(ta) { ta.addEventListener('autosize:destroy', destroy); ta.addEventListener('autosize:update', fullSetHeight); ta.addEventListener('input', handleInput); - window.addEventListener('resize', fullSetHeight); // future todo: consider replacing with ResizeObserver + resizeObserver.observe(ta); ta.style.overflowX = 'hidden'; ta.style.wordWrap = 'break-word'; From ca5d8658929a71f0c0c383abebd16b6951489220 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Mon, 2 Sep 2024 21:00:42 +0100 Subject: [PATCH 2/3] Remove removal of global resize event listener --- src/autosize.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/autosize.js b/src/autosize.js index 2a36de7..0f2f673 100644 --- a/src/autosize.js +++ b/src/autosize.js @@ -9,8 +9,9 @@ function createResizeObserver() { return new ResizeObserver((entries) => entries.forEach(e => onResize(e.target))); } // If not a modern environment, we use a Map instead of a WeakMap, which is iterable. - window.addEventListener('resize', () => assignedElements.forEach((_, el) => onResize(el))); - return { observe: () => {}, unobserve: () => {} }; + const resizeCallback = () => assignedElements.forEach((_, el) => onResize(el)); + window.addEventListener('resize', resizeCallback); + return { observe: () => {}, unobserve: () => {}, disconnect: () => window.removeEventListener('resize', resizeCallback)}; } function onResize(el) { @@ -147,7 +148,6 @@ function assign(ta) { ta.removeEventListener('autosize:destroy', destroy); ta.removeEventListener('autosize:update', fullSetHeight); ta.removeEventListener('input', handleInput); - window.removeEventListener('resize', fullSetHeight); // future todo: consider replacing with ResizeObserver Object.keys(style).forEach(key => ta.style[key] = style[key]); assignedElements.delete(ta); resizeObserver.unobserve(ta); From 6dea926b1ce18f01de1c62445d1ffc4a8bb80f6b Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Tue, 3 Sep 2024 11:26:28 +0100 Subject: [PATCH 3/3] Prevent resize listener from firing when height was changed via input event --- src/autosize.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/autosize.js b/src/autosize.js index 0f2f673..5c7c303 100644 --- a/src/autosize.js +++ b/src/autosize.js @@ -14,10 +14,17 @@ function createResizeObserver() { return { observe: () => {}, unobserve: () => {}, disconnect: () => window.removeEventListener('resize', resizeCallback)}; } +function getRelevantStyles(c) { + return `${c.width}-${c.height}-${c.padding}-${c.borderWidth}-${c.overflow}-${c.boxSizing}-${c.textAlign}`; +} + function onResize(el) { const instance = assignedElements.get(el); - if (instance !== undefined) { - instance.update(); + if (instance !== undefined && el.scrollHeight > 0) { + const styles = getRelevantStyles(instance.computed); + if (styles !== instance.previousStyles) { + instance.update(); + } } } @@ -25,6 +32,7 @@ function assign(ta) { if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || assignedElements.has(ta)) return; let previousHeight = null; + let previousStyles = null; function cacheScrollTops(el) { const arr = []; @@ -120,6 +128,8 @@ function assign(ta) { testForHeightReduction: true, }); } + + previousStyles = getRelevantStyles(computed); } function fullSetHeight() { @@ -170,6 +180,8 @@ function assign(ta) { assignedElements.set(ta, { destroy, update: fullSetHeight, + get previousStyles() { return previousStyles; }, + computed, }); fullSetHeight();