size observer observe appearance

This commit is contained in:
Rene
2020-10-24 01:14:52 +02:00
parent 24eadd5f1d
commit 29af442b38
9 changed files with 186 additions and 96 deletions
@@ -1,6 +1,6 @@
import { createDOM, style, appendChildren, offsetSize, scrollLeft, scrollTop, jsAPI, addClass, each } from 'support';
const animationStartEventName = 'animationstart mozAnimationStart webkitAnimationStart MSAnimationStart';
const animationStartEventName = 'animationstart';
const scrollEventName = 'scroll';
const scrollAmount = 3333333;
const ResizeObserverConstructor = jsAPI('ResizeObserver');
@@ -15,20 +15,20 @@ const rAF = requestAnimationFrame;
// 1. handling for event listeners (animationStartEventName.split(' '))
// 2. return not just element but also destruction function
// 3. shorthand handling for preventDefault & stopPropagation etc.
// 4. add test for appearance (display: none => display: block)
// 5. add functionality & tests for direction change
// 6. MAYBE add comparison function to offsetSize etc.
// 7. Create test utils (waitFor)
// 4. add functionality & tests for direction change
// 5. MAYBE add comparison function to offsetSize etc.
// 6. Create test utils (waitFor)
export const createSizeObserver = (onSizeChangedCallback: () => void) => {
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
const sizeObserver = baseElements[0] as HTMLElement;
const listenerElement = sizeObserver.firstChild as HTMLElement;
let appearCallback = onSizeChangedCallback;
if (ResizeObserverConstructor) {
addClass(sizeObserver, 'resize-observer');
const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallback);
resizeObserverInstance.observe(listenerElement);
} else {
addClass(sizeObserver, 'scroll-observer');
const observerElementChildren = createDOM(
`<div class="${classNameSizeObserverListenerItem}" dir="ltr"><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}"></div></div><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}" style="width: 200%; height: 200%"></div></div></div>`
);
@@ -58,7 +58,7 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => {
};
const onScroll = (scrollEvent?: Event) => {
currSize = offsetSize(listenerElement);
isDirty = currSize.w !== cacheSize.w || currSize.h !== cacheSize.h;
isDirty = !scrollEvent || currSize.w !== cacheSize.w || currSize.h !== cacheSize.h;
if (scrollEvent && isDirty && !rAFId) {
cAF(rAFId);
@@ -75,18 +75,21 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => {
expandElement.addEventListener(scrollEventName, onScroll);
shrinkElement.addEventListener(scrollEventName, onScroll);
each(animationStartEventName.split(' '), (eventName) => {
sizeObserver.addEventListener(eventName, () => {
onScroll();
});
});
// lets assume that the divs will never be that large and a constant value is enough
style(expandElementChild, {
width: scrollAmount,
height: scrollAmount,
});
reset();
appearCallback = onScroll;
}
each(animationStartEventName.split(' '), (eventName) => {
sizeObserver.addEventListener(eventName, () => {
appearCallback();
});
});
return sizeObserver;
};
@@ -7,6 +7,7 @@ $scrollbar-cushion: 100px;
pointer-events: none;
overflow: hidden;
visibility: hidden;
box-sizing: border-box;
}
.os-size-observer,
@@ -25,10 +26,9 @@ $scrollbar-cushion: 100px;
animation-duration: 0.001s;
animation-name: os-size-observer-appear-animation;
&.resize-observer {
&.scroll-observer {
.os-size-observer-listener {
position: absolute;
box-sizing: border-box;
box-sizing: content-box;
}
}
}
@@ -37,7 +37,6 @@ $scrollbar-cushion: 100px;
display: block;
height: 200%;
width: 200%;
box-sizing: content-box;
// lets assume no scrollbar is 100px wide
& > .os-size-observer-listener-item {
@@ -1,5 +1,6 @@
import { WH } from 'support/dom';
const elementHasDimensions = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length);
const zeroObj: WH = {
w: 0,
h: 0,
@@ -42,3 +43,9 @@ export const clientSize = (elm: HTMLElement | null): WH =>
* @param elm The element of which the BoundingClientRect shall be returned.
*/
export const getBoundingClientRect = (elm: HTMLElement): DOMRect => elm.getBoundingClientRect();
/**
* Determines whether the passed element has any dimensions.
* @param elm The element.
*/
export const hasDimensions = (elm: HTMLElement | null): boolean => (elm ? elementHasDimensions(elm as HTMLElement) : false);
@@ -1,7 +1,5 @@
import { each, from } from 'support/utils/array';
const elementIsVisible = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length);
/**
* Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided.
* @param selector The selector which has to be searched by.
@@ -29,20 +27,7 @@ export const findFirst = (selector: string, elm?: Element | null): Element | nul
* @param elm The element which has to be compared with the passed selector.
* @param selector The selector which has to be compared with the passed element. Additional selectors: ':visible' and ':hidden'.
*/
export const is = (elm: Element | null, selector: string): boolean => {
if (elm) {
if (selector === ':visible') {
return elementIsVisible(elm as HTMLElement);
}
if (selector === ':hidden') {
return !elementIsVisible(elm as HTMLElement);
}
if (elm.matches(selector)) {
return true;
}
}
return false;
};
export const is = (elm: Element | null, selector: string): boolean => (elm ? elm.matches(selector) : false);
/**
* Returns the children (no text-nodes or comments) of the passed element which are matching the passed selector. An empty array is returned if the passed element is null.
@@ -1,5 +1,6 @@
import { isNumber, isPlainObject } from 'support/utils/types';
import { windowSize, offsetSize, clientSize, getBoundingClientRect } from 'support/dom/dimensions';
import { createDiv } from 'support/dom/create';
import { windowSize, offsetSize, clientSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions';
describe('dom dimensions', () => {
describe('offsetSize', () => {
@@ -44,4 +45,22 @@ describe('dom dimensions', () => {
test('getBoundingClientRect', () => {
expect(getBoundingClientRect(document.body)).toEqual(document.body.getBoundingClientRect());
});
describe('hasDimensions', () => {
test('DOM element', () => {
const result = hasDimensions(document.body);
expect(result).toBe(true);
});
test('generated element', () => {
const div = createDiv();
const result = hasDimensions(div);
expect(result).toBe(false);
});
test('null', () => {
const result = hasDimensions(null);
expect(result).toBe(false);
});
});
});
@@ -119,11 +119,6 @@ describe('dom traversal', () => {
expect(is(findFirst('.div-class'), '.other-class')).toBe(false);
});
test('visibility', () => {
expect(is(findFirst('.div-class'), ':visible')).toBe(false);
expect(is(findFirst('.div-class'), ':hidden')).toBe(true);
});
test('created', () => {
const div = createDiv();
expect(div.parentNode).toBeNull();
@@ -139,9 +134,6 @@ describe('dom traversal', () => {
expect(is(div, '.div-class')).toBe(false);
expect(is(div, '.other-class')).toBe(false);
expect(is(div, ':visible')).toBe(false);
expect(is(div, ':hidden')).toBe(true);
});
test('none', () => {
@@ -154,9 +146,6 @@ describe('dom traversal', () => {
expect(is(null, '.div-class')).toBe(false);
expect(is(null, '.other-class')).toBe(false);
expect(is(null, ':visible')).toBe(false);
expect(is(null, ':hidden')).toBe(false);
});
});
@@ -16,6 +16,23 @@
<option value="padding10">10px</option>
<option value="padding50">50px</option>
</select>
<label for="border">border</label>
<select name="border" id="border">
<option value="border2">2px</option>
<option value="border10">10px</option>
<option value="border0">0</option>
</select>
<label for="boxSizing">boxSizing</label>
<select name="boxSizing" id="boxSizing">
<option value="boxSizingBorderBox">border-box</option>
<option value="boxSizingContentBox">content-box</option>
</select>
<label for="display">display</label>
<select name="display" id="display">
<option value="displayBlock">block</option>
<option value="displayNone">none</option>
</select>
<button id="start">start</button>
<span>Detected resizes: <span id="resizes">0</span></span>
<br />
@@ -1,8 +1,10 @@
#target {
border: 2px solid red;
overflow: scroll;
resize: both;
position: relative;
// prevent container from reaching 0x0 dimensions for testing purposes
min-width: 50px;
min-height: 50px;
}
.padding0 {
@@ -15,6 +17,16 @@
padding: 50px;
}
.border2 {
border: 2px solid red;
}
.border10 {
border: 10px solid red;
}
.border0 {
border: none;
}
.heightAuto {
height: auto;
}
@@ -35,3 +47,10 @@
.widthHundred {
width: 100%;
}
.displayNone {
display: none;
}
.displayBlock {
display: block;
}
@@ -1,15 +1,28 @@
import 'overlayscrollbars.scss';
import './index.scss';
import { createSizeObserver } from 'overlayscrollbars/observers/createSizeObserver';
import { from, removeClass, addClass } from 'support';
import { from, removeClass, addClass, hasDimensions, isString, isNumber, offsetSize } from 'support';
const targetElm = document.querySelector('#target');
const heightSelect: HTMLSelectElement | null = document.querySelector('#height');
const widthSelect: HTMLSelectElement | null = document.querySelector('#width');
const paddingSelect: HTMLSelectElement | null = document.querySelector('#padding');
const borderSelect: HTMLSelectElement | null = document.querySelector('#border');
const boxSizingSelect: HTMLSelectElement | null = document.querySelector('#boxSizing');
const displaySelect: HTMLSelectElement | null = document.querySelector('#display');
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
let iterations = 0;
const observerElm = createSizeObserver(() => {
iterations += 1;
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = iterations.toString();
}
});
});
const getSelectOptions = (selectElement: HTMLSelectElement) => {
const arr = from(selectElement.options).map((option) => option.value);
return arr;
@@ -24,31 +37,35 @@ const selectCallback = (event: Event) => {
addClass(targetElm, selectedOption);
};
heightSelect?.addEventListener('change', selectCallback);
widthSelect?.addEventListener('change', selectCallback);
paddingSelect?.addEventListener('change', selectCallback);
const selectOption = (select: HTMLSelectElement | null, selectedOption: string | number): boolean => {
if (!select) {
return false;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: heightSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: widthSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: paddingSelect });
const options = getSelectOptions(select);
const currValue = select.value;
let iterations = 0;
const observerElm = createSizeObserver(() => {
iterations += 1;
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = iterations.toString();
}
});
});
if (selectedOption === currValue) {
return false;
}
targetElm?.appendChild(observerElm);
if (isString(selectedOption) && options.includes(selectedOption)) {
select.value = selectedOption;
} else if (isNumber(selectedOption) && options.length < selectedOption && selectedOption > -1) {
select.selectedIndex = selectedOption;
}
let event;
if (typeof Event === 'function') {
event = new Event('change');
} else {
event = document.createEvent('Event');
event.initEvent('change', true, true);
}
select.dispatchEvent(event);
return true;
};
const waitFor = (func: () => any) => {
const start = Date.now();
@@ -77,52 +94,87 @@ const iterateSelect = async (select: HTMLSelectElement | null, afterEach?: () =>
const iterateOptions = [...selectOptions, ...selectOptionsReversed];
for (let i = 0; i < iterateOptions.length; i++) {
const option = iterateOptions[i];
const currValue = select.value;
if (option === currValue) {
continue;
}
select.value = option;
const currIterations = iterations;
const currOffsetSize = offsetSize(targetElm as HTMLElement);
if (selectOption(select, option)) {
const newOffsetSize = offsetSize(targetElm as HTMLElement);
const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h;
let event;
if (typeof Event === 'function') {
event = new Event('change');
} else {
event = document.createEvent('Event');
event.initEvent('change', true, true);
}
select.dispatchEvent(event);
if (hasDimensions(targetElm as HTMLElement) && offsetSizeChanged) {
// eslint-disable-next-line
await waitFor(() => iterations === currIterations + 1);
}
// eslint-disable-next-line
await waitFor(() => iterations === currIterations + 1);
if (typeof afterEach === 'function') {
// eslint-disable-next-line
await afterEach();
if (typeof afterEach === 'function') {
// eslint-disable-next-line
await afterEach();
}
}
}
}
};
window.iteratePadding = async (afterEach?: () => any) => {
heightSelect?.addEventListener('change', selectCallback);
widthSelect?.addEventListener('change', selectCallback);
paddingSelect?.addEventListener('change', selectCallback);
borderSelect?.addEventListener('change', selectCallback);
boxSizingSelect?.addEventListener('change', selectCallback);
displaySelect?.addEventListener('change', selectCallback);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: heightSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: widthSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: paddingSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: borderSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: boxSizingSelect });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selectCallback({ target: displaySelect });
const iteratePadding = (window.iteratePadding = async (afterEach?: () => any) => {
await iterateSelect(paddingSelect, afterEach);
};
window.iterateHeight = async (afterEach?: () => any) => {
});
const iterateBorder = (window.iterateBorder = async (afterEach?: () => any) => {
await iterateSelect(borderSelect, afterEach);
});
const iterateHeight = (window.iterateHeight = async (afterEach?: () => any) => {
await iterateSelect(heightSelect, afterEach);
};
window.iterateWidth = async (afterEach?: () => any) => {
});
const iterateWidth = (window.iterateWidth = async (afterEach?: () => any) => {
await iterateSelect(widthSelect, afterEach);
};
});
const iterateBoxSizing = (window.iterateBoxSizing = async (afterEach?: () => any) => {
await iterateSelect(boxSizingSelect, afterEach);
});
const iterateDisplay = (window.iterateDisplay = async (afterEach?: () => any) => {
await iterateSelect(displaySelect, afterEach);
});
const start = (window.iterate = async () => {
window.setTestResult(null);
targetElm?.removeAttribute('style');
await iterateHeight(async () => {
await iterateWidth(async () => {
await iteratePadding();
await iterateDisplay();
await iterateBoxSizing(async () => {
await iterateHeight(async () => {
await iterateWidth(async () => {
await iterateBorder(async () => {
await iteratePadding();
});
});
});
});
window.setTestResult(true);
});
startBtn?.addEventListener('click', start);
targetElm?.appendChild(observerElm);