From 6cbd66b5344ae37e70cb712b5ebc55547b02074a Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 6 Feb 2022 10:59:36 +0900
Subject: [PATCH] =?UTF-8?q?fix:=20v-size=E3=83=87=E3=82=A3=E3=83=AC?=
 =?UTF-8?q?=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96=E3=81=AE=E5=8B=95=E4=BD=9C?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#8249)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Fix size directive behavior not activated

* calc

* wip

* cache computed classes

* fix Vue3では使えなくなった

* 不要なIntersection Observerを削除

* comment
---
 packages/client/src/directives/get-size.ts |  65 ++++++---
 packages/client/src/directives/size.ts     | 149 +++++++++++++--------
 2 files changed, 137 insertions(+), 77 deletions(-)

diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts
index e3b5dea0f3..1fcd0718dc 100644
--- a/packages/client/src/directives/get-size.ts
+++ b/packages/client/src/directives/get-size.ts
@@ -1,34 +1,55 @@
 import { Directive } from 'vue';
 
+const mountings = new Map<Element, {
+	resize: ResizeObserver;
+	intersection?: IntersectionObserver;
+	fn: (w: number, h: number) => void;
+}>();
+
+function calc(src: Element) {
+	const info = mountings.get(src);
+	const height = src.clientHeight;
+	const width = src.clientWidth;
+
+	if (!info) return;
+
+	// アクティベート前などでsrcが描画されていない場合
+	if (!height) {
+		// IntersectionObserverで表示検出する
+		if (!info.intersection) {
+			info.intersection = new IntersectionObserver(entries => {
+				if (entries.some(entry => entry.isIntersecting)) calc(src);
+			});
+		}
+		info.intersection.observe(src);
+		return;
+	}
+	if (info.intersection) {
+		info.intersection.disconnect()
+		delete info.intersection;
+	};
+
+	info.fn(width, height);
+};
+
 export default {
 	mounted(src, binding, vn) {
-		const calc = () => {
-			const height = src.clientHeight;
-			const width = src.clientWidth;
 
-			// 要素が(一時的に)DOMに存在しないときは計算スキップ
-			if (height === 0) return;
-
-			binding.value(width, height);
-		};
-
-		calc();
-
-		// Vue3では使えなくなった
-		// 無くても大丈夫か...?
-		// TODO: ↑大丈夫じゃなかったので解決策を探す
-		//vn.context.$on('hook:activated', calc);
-
-		const ro = new ResizeObserver((entries, observer) => {
-			calc();
+		const resize = new ResizeObserver((entries, observer) => {
+			calc(src);
 		});
-		ro.observe(src);
+		resize.observe(src);
 
-		src._get_size_ro_ = ro;
+		mountings.set(src, { resize, fn: binding.value, });
+		calc(src);
 	},
 
 	unmounted(src, binding, vn) {
 		binding.value(0, 0);
-		src._get_size_ro_.unobserve(src);
+		const info = mountings.get(src);
+		if (!info) return;
+		info.resize.disconnect();
+		if (info.intersection) info.intersection.disconnect();
+		mountings.delete(src);
 	}
-} as Directive;
+} as Directive<Element, (w: number, h: number) => void>;
diff --git a/packages/client/src/directives/size.ts b/packages/client/src/directives/size.ts
index a72a97abcc..36f649f180 100644
--- a/packages/client/src/directives/size.ts
+++ b/packages/client/src/directives/size.ts
@@ -1,68 +1,107 @@
 import { Directive } from 'vue';
 
+type Value = { max?: number[]; min?: number[]; };
+
 //const observers = new Map<Element, ResizeObserver>();
+const mountings = new Map<Element, {
+	value: Value;
+	resize: ResizeObserver;
+	intersection?: IntersectionObserver;
+	previousWidth: number;
+}>();
+
+type ClassOrder = {
+	add: string[];
+	remove: string[];
+};
+
+const cache = new Map<string, ClassOrder>();
+
+function getClassOrder(width: number, queue: Value): ClassOrder {
+	const getMaxClass = (v: number) => `max-width_${v}px`;
+	const getMinClass = (v: number) => `min-width_${v}px`;
+
+	return {
+		add: [
+			...(queue.max ? queue.max.filter(v => width <= v).map(getMaxClass) : []),
+			...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []),
+		],
+		remove: [
+			...(queue.max ? queue.max.filter(v => width  > v).map(getMaxClass) : []),
+			...(queue.min ? queue.min.filter(v => width  < v).map(getMinClass) : []),
+		]
+	};
+}
+
+function applyClassOrder(el: Element, order: ClassOrder) {
+	el.classList.add(...order.add);
+	el.classList.remove(...order.remove);
+}
+
+function getOrderName(width: number, queue: Value): string {
+	return `${width}|${queue.max ? queue.max.join(',') : ''}|${queue.min ? queue.min.join(',') : ''}`;
+}
+
+function calc(el: Element) {
+	const info = mountings.get(el);
+	const width = el.clientWidth;
+
+	if (!info || info.previousWidth === width) return;
+
+	// アクティベート前などでsrcが描画されていない場合
+	if (!width) {
+		// IntersectionObserverで表示検出する
+		if (!info.intersection) {
+			info.intersection = new IntersectionObserver(entries => {
+				if (entries.some(entry => entry.isIntersecting)) calc(el);
+			});
+		}
+		info.intersection.observe(el);
+		return;
+	}
+	if (info.intersection) {
+		info.intersection.disconnect()
+		delete info.intersection;
+	};
+
+	mountings.set(el, Object.assign(info, { previousWidth: width }));
+
+	const cached = cache.get(getOrderName(width, info.value));
+	if (cached) {
+		applyClassOrder(el, cached);
+	} else {
+		const order = getClassOrder(width, info.value);
+		cache.set(getOrderName(width, info.value), order);
+		applyClassOrder(el, order);
+	}
+}
 
 export default {
 	mounted(src, binding, vn) {
-		const query = binding.value;
+		const resize = new ResizeObserver((entries, observer) => {
+			calc(src);
+		});
 
-		const addClass = (el: Element, cls: string) => {
-			el.classList.add(cls);
-		};
+		mountings.set(src, {
+			value: binding.value,
+			resize,
+			previousWidth: 0,
+		});
 
-		const removeClass = (el: Element, cls: string) => {
-			el.classList.remove(cls);
-		};
+		calc(src);
+		resize.observe(src);
+	},
 
-		const calc = () => {
-			const width = src.clientWidth;
-
-			// 要素が(一時的に)DOMに存在しないときは計算スキップ
-			if (width === 0) return;
-
-			if (query.max) {
-				for (const v of query.max) {
-					if (width <= v) {
-						addClass(src, 'max-width_' + v + 'px');
-					} else {
-						removeClass(src, 'max-width_' + v + 'px');
-					}
-				}
-			}
-			if (query.min) {
-				for (const v of query.min) {
-					if (width >= v) {
-						addClass(src, 'min-width_' + v + 'px');
-					} else {
-						removeClass(src, 'min-width_' + v + 'px');
-					}
-				}
-			}
-		};
-
-		calc();
-
-		window.addEventListener('resize', calc);
-
-		// Vue3では使えなくなった
-		// 無くても大丈夫か...?
-		// TODO: ↑大丈夫じゃなかったので解決策を探す
-		//vn.context.$on('hook:activated', calc);
-
-		//const ro = new ResizeObserver((entries, observer) => {
-		//	calc();
-		//});
-
-		//ro.observe(el);
-
-		// TODO: 新たにプロパティを作るのをやめMapを使う
-		// ただメモリ的には↓の方が省メモリかもしれないので検討中
-		//el._ro_ = ro;
-		src._calc_ = calc;
+	updated(src, binding, vn) {
+		mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value }));
+		calc(src);
 	},
 
 	unmounted(src, binding, vn) {
-		//el._ro_.unobserve(el);
-		window.removeEventListener('resize', src._calc_);
+		const info = mountings.get(src);
+		if (!info) return;
+		info.resize.disconnect();
+		if (info.intersection) info.intersection.disconnect();
+		mountings.delete(src);
 	}
-} as Directive;
+} as Directive<Element, Value>;