mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-20 03:40:36 +03:00
size observer observe appearance
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user