misskey/packages/client/src/directives/size.ts
tamaina f0fe930aae
fix(client): prevent infinite resize loop (#9232)
* clientWidth?

* spacer?

* size directive?

* size directive

* use const
2022-12-26 12:55:10 +09:00

124 lines
3.1 KiB
TypeScript

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;
twoPreviousWidth: number;
}>();
type ClassOrder = {
add: string[];
remove: string[];
};
const isContainerQueriesSupported = ('container' in document.documentElement.style);
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, { ...info, ...{ previousWidth: width, twoPreviousWidth: info.previousWidth }});
// Prevent infinite resizing
// https://github.com/misskey-dev/misskey/issues/9076
if (info.twoPreviousWidth === width) {
return;
}
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) {
if (isContainerQueriesSupported) return;
const resize = new ResizeObserver((entries, observer) => {
calc(src);
});
mountings.set(src, {
value: binding.value,
resize,
previousWidth: 0,
twoPreviousWidth: 0,
});
calc(src);
resize.observe(src);
},
updated(src, binding, vn) {
if (isContainerQueriesSupported) return;
mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value }));
calc(src);
},
unmounted(src, binding, vn) {
if (isContainerQueriesSupported) return;
const info = mountings.get(src);
if (!info) return;
info.resize.disconnect();
if (info.intersection) info.intersection.disconnect();
mountings.delete(src);
},
} as Directive<Element, Value>;