fix: v-sizeディレクティブの動作を修正 (#8249)
* Fix size directive behavior not activated * calc * wip * cache computed classes * fix Vue3では使えなくなった * 不要なIntersection Observerを削除 * comment
This commit is contained in:
		
							parent
							
								
									58fa54a9a6
								
							
						
					
					
						commit
						6cbd66b534
					
				
					 2 changed files with 137 additions and 77 deletions
				
			
		|  | @ -1,34 +1,55 @@ | ||||||
| import { Directive } from 'vue'; | 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 { | export default { | ||||||
| 	mounted(src, binding, vn) { | 	mounted(src, binding, vn) { | ||||||
| 		const calc = () => { |  | ||||||
| 			const height = src.clientHeight; |  | ||||||
| 			const width = src.clientWidth; |  | ||||||
| 
 | 
 | ||||||
| 			// 要素が(一時的に)DOMに存在しないときは計算スキップ
 | 		const resize = new ResizeObserver((entries, observer) => { | ||||||
| 			if (height === 0) return; | 			calc(src); | ||||||
| 
 |  | ||||||
| 			binding.value(width, height); |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		calc(); |  | ||||||
| 
 |  | ||||||
| 		// Vue3では使えなくなった
 |  | ||||||
| 		// 無くても大丈夫か...?
 |  | ||||||
| 		// TODO: ↑大丈夫じゃなかったので解決策を探す
 |  | ||||||
| 		//vn.context.$on('hook:activated', calc);
 |  | ||||||
| 
 |  | ||||||
| 		const ro = new ResizeObserver((entries, observer) => { |  | ||||||
| 			calc(); |  | ||||||
| 		}); | 		}); | ||||||
| 		ro.observe(src); | 		resize.observe(src); | ||||||
| 
 | 
 | ||||||
| 		src._get_size_ro_ = ro; | 		mountings.set(src, { resize, fn: binding.value, }); | ||||||
|  | 		calc(src); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	unmounted(src, binding, vn) { | 	unmounted(src, binding, vn) { | ||||||
| 		binding.value(0, 0); | 		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>; | ||||||
|  |  | ||||||
|  | @ -1,68 +1,107 @@ | ||||||
| import { Directive } from 'vue'; | import { Directive } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | type Value = { max?: number[]; min?: number[]; }; | ||||||
|  | 
 | ||||||
| //const observers = new Map<Element, ResizeObserver>();
 | //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 { | export default { | ||||||
| 	mounted(src, binding, vn) { | 	mounted(src, binding, vn) { | ||||||
| 		const query = binding.value; | 		const resize = new ResizeObserver((entries, observer) => { | ||||||
|  | 			calc(src); | ||||||
|  | 		}); | ||||||
| 
 | 
 | ||||||
| 		const addClass = (el: Element, cls: string) => { | 		mountings.set(src, { | ||||||
| 			el.classList.add(cls); | 			value: binding.value, | ||||||
| 		}; | 			resize, | ||||||
|  | 			previousWidth: 0, | ||||||
|  | 		}); | ||||||
| 
 | 
 | ||||||
| 		const removeClass = (el: Element, cls: string) => { | 		calc(src); | ||||||
| 			el.classList.remove(cls); | 		resize.observe(src); | ||||||
| 		}; | 	}, | ||||||
| 
 | 
 | ||||||
| 		const calc = () => { | 	updated(src, binding, vn) { | ||||||
| 			const width = src.clientWidth; | 		mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value })); | ||||||
| 
 | 		calc(src); | ||||||
| 			// 要素が(一時的に)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; |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	unmounted(src, binding, vn) { | 	unmounted(src, binding, vn) { | ||||||
| 		//el._ro_.unobserve(el);
 | 		const info = mountings.get(src); | ||||||
| 		window.removeEventListener('resize', src._calc_); | 		if (!info) return; | ||||||
|  | 		info.resize.disconnect(); | ||||||
|  | 		if (info.intersection) info.intersection.disconnect(); | ||||||
|  | 		mountings.delete(src); | ||||||
| 	} | 	} | ||||||
| } as Directive; | } as Directive<Element, Value>; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue