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:
tamaina 2022-02-06 10:59:36 +09:00 committed by GitHub
parent 58fa54a9a6
commit 6cbd66b534
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 137 additions and 77 deletions

View file

@ -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>;

View file

@ -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>;