1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/src/lib/scroller/index.js

909 lines
27 KiB
JavaScript
Raw Normal View History

/**
* NOTE: This file should not be modified.
* It is a legacy library that should be replaced at some point.
2020-08-06 21:15:16 +02:00
*/
2024-08-14 16:52:29 -04:00
import browser from '../../scripts/browser';
import layoutManager from '../../components/layoutManager';
import dom from '../../scripts/dom';
import focusManager from '../../components/focusManager';
2020-08-14 08:46:34 +02:00
import ResizeObserver from 'resize-observer-polyfill';
2024-08-14 16:52:29 -04:00
import '../../styles/scrollstyles.scss';
import globalize from '../globalize';
2020-08-06 21:15:16 +02:00
/**
* Return type of the value.
*
* @param {Mixed} value
*
* @return {String}
*/
2020-08-06 21:15:16 +02:00
function type(value) {
if (value == null) {
return String(value);
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
if (typeof value === 'object' || typeof value === 'function') {
return Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase() || 'object';
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
return typeof value;
}
/**
* Disables an event it was triggered on and unbinds itself.
*
* @param {Event} event
*
* @return {Void}
*/
function disableOneEvent(event) {
/*jshint validthis:true */
event.preventDefault();
event.stopPropagation();
this.removeEventListener(event.type, disableOneEvent);
}
/**
* Make sure that number is within the limits.
*
* @param {Number} number
* @param {Number} min
* @param {Number} max
*
* @return {Number}
*/
2022-07-05 21:56:23 -04:00
function within(number, num1, num2) {
2022-07-08 12:21:35 -04:00
if (num2 === undefined && globalize.getIsRTL()) {
return number > num1 ? num1 : number;
} else if (num2 === undefined) {
2022-07-06 21:54:24 -04:00
return number < num1 ? num1 : number;
}
2022-07-05 21:56:23 -04:00
const min = Math.min(num1, num2);
const max = Math.max(num1, num2);
if (number < min) {
return min;
} else if (number > max) {
return max;
}
return number;
2020-08-06 21:15:16 +02:00
}
// Other global values
2020-10-07 21:12:14 +09:00
const dragMouseEvents = ['mousemove', 'mouseup'];
const dragTouchEvents = ['touchmove', 'touchend'];
const wheelEvent = (document.implementation.hasFeature('Event.wheel', '3.0') ? 'wheel' : 'mousewheel');
const interactiveElements = ['INPUT', 'SELECT', 'TEXTAREA'];
2020-08-06 21:15:16 +02:00
2020-10-07 21:12:14 +09:00
const scrollerFactory = function (frame, options) {
2020-08-06 21:15:16 +02:00
// Extend options
2020-10-07 21:12:14 +09:00
const o = Object.assign({}, {
2020-08-06 21:15:16 +02:00
slidee: null, // Selector, DOM element, or jQuery object with DOM element representing SLIDEE.
horizontal: false, // Switch to horizontal mode.
// Scrolling
mouseWheel: true,
scrollBy: 0, // Pixels or items to move per one mouse scroll. 0 to disable scrolling
// Dragging
dragSource: null, // Selector or DOM element for catching dragging events. Default is FRAME.
mouseDragging: 1, // Enable navigation by dragging the SLIDEE with mouse cursor.
touchDragging: 1, // Enable navigation by dragging the SLIDEE with touch events.
dragThreshold: 3, // Distance in pixels before Sly recognizes dragging.
intervactive: null, // Selector for special interactive elements.
// Mixed options
speed: 0 // Animations speed in milliseconds. 0 to disable animations.
}, options);
2020-10-07 21:12:14 +09:00
const isSmoothScrollSupported = 'scrollBehavior' in document.documentElement.style;
2020-08-06 21:15:16 +02:00
// native scroll is a must with touch input
// also use native scroll when scrolling vertically in desktop mode - excluding horizontal because the mouse wheel support is choppy at the moment
// in cases with firefox, if the smooth scroll api is supported then use that because their implementation is very good
if (options.allowNativeScroll === false) {
options.enableNativeScroll = false;
} else if (isSmoothScrollSupported && ((browser.firefox && !layoutManager.tv) || options.allowNativeSmoothScroll)) {
// native smooth scroll
options.enableNativeScroll = true;
} else if (options.requireAnimation && (browser.animate || browser.supportsCssAnimation())) {
// transform is the only way to guarantee animation
options.enableNativeScroll = false;
} else if (!layoutManager.tv || !browser.animate) {
options.enableNativeScroll = true;
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
// Need this for the magic wheel. With the animated scroll the magic wheel will run off of the screen
if (browser.web0s) {
options.enableNativeScroll = true;
}
2020-08-06 21:15:16 +02:00
// Private variables
2020-10-07 21:12:14 +09:00
const self = this;
2020-08-06 21:15:16 +02:00
self.options = o;
// Frame
2020-10-07 21:12:14 +09:00
const slideeElement = o.slidee ? o.slidee : sibling(frame.firstChild)[0];
2020-08-06 21:15:16 +02:00
self._pos = {
start: 0,
center: 0,
end: 0,
cur: 0,
dest: 0
};
2020-10-07 21:12:14 +09:00
const transform = !options.enableNativeScroll;
2020-08-06 21:15:16 +02:00
// Miscellaneous
2020-10-07 21:12:14 +09:00
const scrollSource = frame;
const dragSourceElement = o.dragSource ? o.dragSource : frame;
const dragging = {
2020-08-06 21:15:16 +02:00
released: 1
};
2020-10-07 21:12:14 +09:00
const scrolling = {
2020-08-06 21:15:16 +02:00
last: 0,
delta: 0,
resetTime: 200
};
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
// Expose properties
self.initialized = 0;
self.slidee = slideeElement;
self.options = o;
self.dragging = dragging;
2020-10-07 21:12:14 +09:00
const nativeScrollElement = frame;
2020-08-06 21:15:16 +02:00
function sibling(n, elem) {
2020-10-07 21:12:14 +09:00
const matched = [];
2020-08-06 21:15:16 +02:00
for (; n; n = n.nextSibling) {
if (n.nodeType === 1 && n !== elem) {
matched.push(n);
2018-10-23 01:05:09 +03:00
}
}
2020-08-06 21:15:16 +02:00
return matched;
}
2018-10-23 01:05:09 +03:00
2020-10-07 21:12:14 +09:00
let requiresReflow = true;
2020-10-07 21:12:14 +09:00
let frameSize = 0;
let slideeSize = 0;
2020-08-06 21:15:16 +02:00
function ensureSizeInfo() {
if (requiresReflow) {
requiresReflow = false;
2020-08-06 21:15:16 +02:00
// Reset global variables
2024-01-03 10:03:15 -05:00
frameSize = slideeElement[o.horizontal ? 'clientWidth' : 'clientHeight'];
2020-08-06 21:15:16 +02:00
slideeSize = o.scrollWidth || Math.max(slideeElement[o.horizontal ? 'offsetWidth' : 'offsetHeight'], slideeElement[o.horizontal ? 'scrollWidth' : 'scrollHeight']);
// Set position limits & relatives
2020-08-06 21:15:16 +02:00
self._pos.end = Math.max(slideeSize - frameSize, 0);
2023-09-12 17:29:03 -04:00
if (globalize.getIsRTL()) {
2022-07-05 21:56:23 -04:00
self._pos.end *= -1;
2023-09-12 17:29:03 -04:00
}
}
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
/**
* Loading function.
*
* Populate arrays, set sizes, bind events, ...
*
* @param {Boolean} [isInit] Whether load is called from within self.init().
* @return {Void}
*/
function load(isInit) {
requiresReflow = true;
2020-08-06 21:15:16 +02:00
if (!isInit) {
ensureSizeInfo();
2020-08-06 21:15:16 +02:00
// Fix possible overflowing
2020-10-07 21:12:14 +09:00
const pos = self._pos;
2020-08-06 21:15:16 +02:00
self.slideTo(within(pos.dest, pos.start, pos.end));
}
2020-08-06 21:15:16 +02:00
}
2020-08-06 21:15:16 +02:00
function initFrameResizeObserver() {
2020-10-07 21:12:14 +09:00
const observerOptions = {};
2020-08-06 21:15:16 +02:00
self.frameResizeObserver = new ResizeObserver(onResize, observerOptions);
2020-08-06 21:15:16 +02:00
self.frameResizeObserver.observe(frame);
}
2020-08-06 21:15:16 +02:00
self.reload = function () {
load();
};
2020-08-06 21:15:16 +02:00
self.getScrollEventName = function () {
return transform ? 'scrollanimate' : 'scroll';
};
2020-08-06 21:15:16 +02:00
self.getScrollSlider = function () {
return slideeElement;
};
2020-08-06 21:15:16 +02:00
self.getScrollFrame = function () {
return frame;
};
2020-08-06 21:15:16 +02:00
function nativeScrollTo(container, pos, immediate) {
if (container.scroll) {
if (o.horizontal) {
container.scroll({
left: pos,
behavior: immediate ? 'instant' : 'smooth'
});
} else {
2020-08-06 21:15:16 +02:00
container.scroll({
top: pos,
behavior: immediate ? 'instant' : 'smooth'
});
}
} else if (!immediate && container.scrollTo) {
if (o.horizontal) {
2020-08-06 22:25:14 +02:00
container.scrollTo(Math.round(pos), 0);
2020-08-06 21:15:16 +02:00
} else {
2020-08-06 22:25:14 +02:00
container.scrollTo(0, Math.round(pos));
2020-08-06 21:15:16 +02:00
}
2023-09-12 17:02:06 -04:00
} else if (o.horizontal) {
container.scrollLeft = Math.round(pos);
2020-08-06 21:15:16 +02:00
} else {
2023-09-12 17:02:06 -04:00
container.scrollTop = Math.round(pos);
}
2020-08-06 21:15:16 +02:00
}
2020-10-07 21:12:14 +09:00
let lastAnimate;
2020-08-06 21:15:16 +02:00
/**
* Animate to a position.
*
* @param {Int} newPos New position.
* @param {Bool} immediate Reposition immediately without an animation.
*
* @return {Void}
*/
self.slideTo = function (newPos, immediate, fullItemPos) {
ensureSizeInfo();
2020-10-07 21:12:14 +09:00
const pos = self._pos;
2022-07-08 22:28:43 -04:00
if (layoutManager.tv && globalize.getIsRTL()) {
2022-07-08 12:21:35 -04:00
newPos = within(-newPos, pos.start);
2022-07-08 22:28:43 -04:00
} else if (layoutManager.tv) {
newPos = within(newPos, pos.start);
} else {
newPos = within(newPos, pos.start, pos.end);
}
2020-08-06 21:15:16 +02:00
if (!transform) {
nativeScrollTo(nativeScrollElement, newPos, immediate);
return;
}
2020-08-06 21:15:16 +02:00
// Update the animation object
2020-10-07 21:12:14 +09:00
const from = pos.cur;
2020-08-06 21:15:16 +02:00
immediate = immediate || dragging.init || !o.speed;
2020-10-07 21:12:14 +09:00
const now = new Date().getTime();
2022-10-03 14:22:02 -04:00
if (o.autoImmediate && !immediate && (now - (lastAnimate || 0)) <= 50) {
immediate = true;
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
if (!immediate && o.skipSlideToWhenVisible && fullItemPos?.isVisible) {
2020-08-06 21:15:16 +02:00
return;
}
2020-08-06 21:15:16 +02:00
// Start animation rendering
// NOTE the dependency was modified here to fix a scrollbutton issue
pos.dest = newPos;
renderAnimateWithTransform(from, newPos, immediate);
lastAnimate = now;
};
2020-08-06 21:15:16 +02:00
function setStyleProperty(elem, name, value, speed, resetTransition) {
2020-10-07 21:12:14 +09:00
const style = elem.style;
2020-08-06 21:15:16 +02:00
if (resetTransition || browser.edge) {
style.transition = 'none';
void elem.offsetWidth;
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
style.transition = 'transform ' + speed + 'ms ease-out';
style[name] = value;
}
2020-08-06 21:15:16 +02:00
function dispatchScrollEventIfNeeded() {
if (o.dispatchScrollEvent) {
frame.dispatchEvent(new CustomEvent(self.getScrollEventName(), {
bubbles: true,
cancelable: false
}));
}
}
function renderAnimateWithTransform(fromPosition, toPosition, immediate) {
2020-10-07 21:12:14 +09:00
let speed = o.speed;
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
if (immediate) {
speed = o.immediateSpeed || 50;
}
2020-08-06 21:15:16 +02:00
if (o.horizontal) {
setStyleProperty(slideeElement, 'transform', 'translateX(' + (-Math.round(toPosition)) + 'px)', speed);
} else {
setStyleProperty(slideeElement, 'transform', 'translateY(' + (-Math.round(toPosition)) + 'px)', speed);
}
2020-08-06 21:15:16 +02:00
self._pos.cur = toPosition;
2020-08-06 21:15:16 +02:00
dispatchScrollEventIfNeeded();
}
2020-08-06 21:15:16 +02:00
function getBoundingClientRect(elem) {
// Support: BlackBerry 5, iOS 3 (original iPhone)
// If we don't have gBCR, just use 0,0 rather than error
if (elem.getBoundingClientRect) {
return elem.getBoundingClientRect();
} else {
return { top: 0, left: 0 };
}
}
2020-08-06 21:15:16 +02:00
/**
* Returns the position object.
*
* @param {Mixed} item
*
* @return {Object}
*/
self.getPos = function (item) {
2020-10-07 21:12:14 +09:00
const scrollElement = transform ? slideeElement : nativeScrollElement;
const slideeOffset = getBoundingClientRect(scrollElement);
const itemOffset = getBoundingClientRect(item);
2022-07-05 21:56:23 -04:00
let horizontalOffset = itemOffset.left - slideeOffset.left;
if (globalize.getIsRTL()) {
2022-07-08 12:21:35 -04:00
horizontalOffset = slideeOffset.right - itemOffset.right;
2022-07-05 21:56:23 -04:00
}
let offset = o.horizontal ? horizontalOffset : itemOffset.top - slideeOffset.top;
2020-10-07 21:12:14 +09:00
let size = o.horizontal ? itemOffset.width : itemOffset.height;
2020-08-06 21:15:16 +02:00
if (!size && size !== 0) {
size = item[o.horizontal ? 'offsetWidth' : 'offsetHeight'];
}
2020-10-07 21:12:14 +09:00
let centerOffset = o.centerOffset || 0;
2020-08-06 21:15:16 +02:00
if (!transform) {
centerOffset = 0;
if (o.horizontal) {
offset += nativeScrollElement.scrollLeft;
} else {
2020-08-06 21:15:16 +02:00
offset += nativeScrollElement.scrollTop;
}
}
2020-08-06 21:15:16 +02:00
ensureSizeInfo();
2020-10-07 21:12:14 +09:00
const currentStart = self._pos.cur;
2022-07-08 12:21:35 -04:00
let currentEnd = currentStart + frameSize;
if (globalize.getIsRTL()) {
currentEnd = currentStart - frameSize;
}
2020-08-06 21:15:16 +02:00
console.debug('offset:' + offset + ' currentStart:' + currentStart + ' currentEnd:' + currentEnd);
2022-07-05 21:56:23 -04:00
const isVisible = offset >= Math.min(currentStart, currentEnd)
&& (globalize.getIsRTL() ? (offset - size) : (offset + size)) <= Math.max(currentStart, currentEnd);
2020-08-06 21:15:16 +02:00
return {
start: offset,
center: offset + centerOffset - (frameSize / 2) + (size / 2),
2022-07-08 12:21:35 -04:00
end: offset - frameSize + size,
size,
isVisible
};
2020-08-06 21:15:16 +02:00
};
2020-08-06 21:15:16 +02:00
self.getCenterPosition = function (item) {
ensureSizeInfo();
2020-10-07 21:12:14 +09:00
const pos = self.getPos(item);
2020-08-06 21:15:16 +02:00
return within(pos.center, pos.start, pos.end);
};
2020-08-06 21:15:16 +02:00
function dragInitSlidee(event) {
2020-10-07 21:12:14 +09:00
const isTouch = event.type === 'touchstart';
2020-08-06 21:15:16 +02:00
// Ignore when already in progress, or interactive element in non-touch navivagion
if (dragging.init || !isTouch && isInteractive(event.target)) {
return;
}
2020-08-06 21:15:16 +02:00
// SLIDEE dragging conditions
if (!(isTouch ? o.touchDragging : o.mouseDragging && event.which < 2)) {
return;
}
2020-08-06 21:15:16 +02:00
if (!isTouch) {
// prevents native image dragging in Firefox
event.preventDefault();
}
2020-08-06 21:15:16 +02:00
// Reset dragging object
dragging.released = 0;
// Properties used in dragHandler
dragging.init = 0;
dragging.source = event.target;
dragging.touch = isTouch;
2020-10-07 21:12:14 +09:00
const pointer = isTouch ? event.touches[0] : event;
2020-08-06 21:15:16 +02:00
dragging.initX = pointer.pageX;
dragging.initY = pointer.pageY;
dragging.initPos = self._pos.cur;
dragging.start = +new Date();
dragging.time = 0;
dragging.path = 0;
dragging.delta = 0;
dragging.locked = 0;
dragging.pathToLock = isTouch ? 30 : 10;
// Bind dragging events
if (transform) {
if (isTouch) {
dragTouchEvents.forEach(function (eventName) {
dom.addEventListener(document, eventName, dragHandler, {
passive: true
});
2020-08-06 21:15:16 +02:00
});
} else {
dragMouseEvents.forEach(function (eventName) {
dom.addEventListener(document, eventName, dragHandler, {
passive: true
});
2020-08-06 21:15:16 +02:00
});
2018-10-23 01:05:09 +03:00
}
}
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
/**
* Handler for dragging scrollbar handle or SLIDEE.
*
* @param {Event} event
*
* @return {Void}
*/
function dragHandler(event) {
dragging.released = event.type === 'mouseup' || event.type === 'touchend';
const eventName = dragging.released ? 'changedTouches' : 'touches';
const pointer = dragging.touch ? event[eventName][0] : event;
2020-08-06 21:15:16 +02:00
dragging.pathX = pointer.pageX - dragging.initX;
dragging.pathY = pointer.pageY - dragging.initY;
dragging.path = Math.sqrt(Math.pow(dragging.pathX, 2) + Math.pow(dragging.pathY, 2));
dragging.delta = o.horizontal ? dragging.pathX : dragging.pathY;
if (!dragging.released && dragging.path < 1) {
return;
}
2020-08-06 21:15:16 +02:00
// We haven't decided whether this is a drag or not...
if (!dragging.init) {
// If the drag path was very short, maybe it's not a drag?
if (dragging.path < o.dragThreshold) {
// If the pointer was released, the path will not become longer and it's
// definitely not a drag. If not released yet, decide on next iteration
return dragging.released ? dragEnd() : undefined;
2023-09-12 17:02:06 -04:00
} else if (o.horizontal ? Math.abs(dragging.pathX) > Math.abs(dragging.pathY) : Math.abs(dragging.pathX) < Math.abs(dragging.pathY)) {
2020-08-06 21:15:16 +02:00
// If dragging path is sufficiently long we can confidently start a drag
// if drag is in different direction than scroll, ignore it
2023-09-12 17:02:06 -04:00
dragging.init = 1;
} else {
return dragEnd();
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
// Disable click on a source element, as it is unwelcome when dragging
if (!dragging.locked && dragging.path > dragging.pathToLock) {
dragging.locked = 1;
dragging.source.addEventListener('click', disableOneEvent);
}
2020-08-06 21:15:16 +02:00
// Cancel dragging on release
if (dragging.released) {
dragEnd();
}
2020-08-06 21:15:16 +02:00
self.slideTo(Math.round(dragging.initPos - dragging.delta));
}
2020-08-06 21:15:16 +02:00
/**
* Stops dragging and cleans up after it.
*
* @return {Void}
*/
function dragEnd() {
dragging.released = true;
2020-08-06 21:15:16 +02:00
dragTouchEvents.forEach(function (eventName) {
dom.removeEventListener(document, eventName, dragHandler, {
passive: true
});
2020-08-06 21:15:16 +02:00
});
2020-08-06 21:15:16 +02:00
dragMouseEvents.forEach(function (eventName) {
dom.removeEventListener(document, eventName, dragHandler, {
passive: true
});
2020-08-06 21:15:16 +02:00
});
2020-08-06 21:15:16 +02:00
// Make sure that disableOneEvent is not active in next tick.
setTimeout(function () {
dragging.source.removeEventListener('click', disableOneEvent);
});
2020-08-06 21:15:16 +02:00
dragging.init = 0;
}
2020-08-06 21:15:16 +02:00
/**
* Check whether element is interactive.
*
* @return {Boolean}
*/
function isInteractive(element) {
while (element) {
if (interactiveElements.indexOf(element.tagName) !== -1) {
return true;
}
2020-08-06 21:15:16 +02:00
element = element.parentNode;
}
return false;
}
2020-08-06 21:15:16 +02:00
/**
* Mouse wheel delta normalization.
*
* @param {Event} event
*
* @return {Int}
*/
function normalizeWheelDelta(event) {
// JELLYFIN MOD: Only use deltaX for horizontal scroll and remove IE8 support
scrolling.curDelta = o.horizontal ? event.deltaX : event.deltaY;
// END JELLYFIN MOD
if (transform) {
scrolling.curDelta /= event.deltaMode === 1 ? 3 : 100;
}
2020-08-06 21:15:16 +02:00
return scrolling.curDelta;
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
/**
* Mouse scrolling handler.
*
* @param {Event} event
*
* @return {Void}
*/
function scrollHandler(event) {
ensureSizeInfo();
2020-10-07 21:12:14 +09:00
const pos = self._pos;
2020-08-06 21:15:16 +02:00
// Ignore if there is no scrolling to be done
if (!o.scrollBy || pos.start === pos.end) {
return;
}
2020-10-07 21:12:14 +09:00
let delta = normalizeWheelDelta(event);
2020-08-06 21:15:16 +02:00
if (transform) {
self.slideBy(o.scrollBy * delta);
} else {
if (isSmoothScrollSupported) {
delta *= 12;
}
2020-08-06 21:15:16 +02:00
if (o.horizontal) {
nativeScrollElement.scrollLeft += delta;
} else {
2020-08-06 21:15:16 +02:00
nativeScrollElement.scrollTop += delta;
2018-10-23 01:05:09 +03:00
}
}
2020-08-06 21:15:16 +02:00
}
2020-08-06 21:15:16 +02:00
/**
* Destroys instance and everything it created.
*
* @return {Void}
*/
self.destroy = function () {
if (self.frameResizeObserver) {
self.frameResizeObserver.disconnect();
self.frameResizeObserver = null;
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
// Reset native FRAME element scroll
dom.removeEventListener(frame, 'scroll', resetScroll, {
passive: true
});
2020-08-06 21:15:16 +02:00
dom.removeEventListener(scrollSource, wheelEvent, scrollHandler, {
passive: true
});
2020-08-06 21:15:16 +02:00
dom.removeEventListener(dragSourceElement, 'touchstart', dragInitSlidee, {
passive: true
});
2020-08-06 21:15:16 +02:00
dom.removeEventListener(frame, 'click', onFrameClick, {
passive: true,
capture: true
});
2020-08-06 21:15:16 +02:00
dom.removeEventListener(dragSourceElement, 'mousedown', dragInitSlidee, {
//passive: true
});
2021-04-24 21:30:40 +03:00
scrollSource.removeAttribute(`data-scroll-mode-${o.horizontal ? 'x' : 'y'}`);
2020-08-06 21:15:16 +02:00
// Reset initialized status and return the instance
self.initialized = 0;
return self;
};
2020-10-07 21:12:14 +09:00
let contentRect = {};
2020-08-06 21:15:16 +02:00
function onResize(entries) {
2020-10-07 21:12:14 +09:00
const entry = entries[0];
2020-08-06 21:15:16 +02:00
if (entry) {
2020-10-07 21:12:14 +09:00
const newRect = entry.contentRect;
2020-08-06 21:15:16 +02:00
// handle element being hidden
if (newRect.width === 0 || newRect.height === 0) {
return;
}
2020-08-06 21:15:16 +02:00
if (newRect.width !== contentRect.width || newRect.height !== contentRect.height) {
contentRect = newRect;
2020-08-06 21:15:16 +02:00
load(false);
2018-10-23 01:05:09 +03:00
}
}
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
function resetScroll() {
if (o.horizontal) {
this.scrollLeft = 0;
} else {
this.scrollTop = 0;
}
2020-08-06 21:15:16 +02:00
}
2018-10-23 01:05:09 +03:00
2020-08-06 21:15:16 +02:00
function onFrameClick(e) {
if (e.which === 1) {
2020-10-07 21:12:14 +09:00
const focusableParent = focusManager.focusableParent(e.target);
2020-08-06 21:15:16 +02:00
if (focusableParent && focusableParent !== document.activeElement) {
focusableParent.focus();
2018-10-23 01:05:09 +03:00
}
}
2020-08-06 21:15:16 +02:00
}
2020-08-06 21:15:16 +02:00
self.getScrollPosition = function () {
if (transform) {
return self._pos.cur;
}
2020-08-06 21:15:16 +02:00
if (o.horizontal) {
return nativeScrollElement.scrollLeft;
} else {
return nativeScrollElement.scrollTop;
}
};
2020-08-06 21:15:16 +02:00
self.getScrollSize = function () {
if (transform) {
return slideeSize;
}
2020-08-06 21:15:16 +02:00
if (o.horizontal) {
return nativeScrollElement.scrollWidth;
} else {
return nativeScrollElement.scrollHeight;
}
};
2020-08-06 21:15:16 +02:00
/**
* Initialize.
*
* @return {Object}
*/
self.init = function () {
if (self.initialized) {
return;
}
2020-08-06 21:15:16 +02:00
if (!transform) {
if (o.horizontal) {
2020-08-06 21:15:16 +02:00
if (layoutManager.desktop && !o.hideScrollbar) {
nativeScrollElement.classList.add('scrollX');
} else {
nativeScrollElement.classList.add('scrollX');
nativeScrollElement.classList.add('hiddenScrollX');
2020-08-06 21:15:16 +02:00
if (layoutManager.tv && o.allowNativeSmoothScroll !== false) {
nativeScrollElement.classList.add('smoothScrollX');
}
2020-08-06 21:15:16 +02:00
}
2020-08-06 21:15:16 +02:00
if (o.forceHideScrollbars) {
nativeScrollElement.classList.add('hiddenScrollX-forced');
}
} else {
if (layoutManager.desktop && !o.hideScrollbar) {
nativeScrollElement.classList.add('scrollY');
} else {
2020-08-06 21:15:16 +02:00
nativeScrollElement.classList.add('scrollY');
nativeScrollElement.classList.add('hiddenScrollY');
2020-08-06 21:15:16 +02:00
if (layoutManager.tv && o.allowNativeSmoothScroll !== false) {
nativeScrollElement.classList.add('smoothScrollY');
}
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
if (o.forceHideScrollbars) {
nativeScrollElement.classList.add('hiddenScrollY-forced');
}
}
2020-08-06 21:15:16 +02:00
} else {
frame.style.overflow = 'hidden';
slideeElement.style['will-change'] = 'transform';
slideeElement.style.transition = 'transform ' + o.speed + 'ms ease-out';
2020-08-06 21:15:16 +02:00
if (o.horizontal) {
slideeElement.classList.add('animatedScrollX');
} else {
slideeElement.classList.add('animatedScrollY');
}
2020-08-06 21:15:16 +02:00
}
2021-04-24 21:30:40 +03:00
scrollSource.setAttribute(`data-scroll-mode-${o.horizontal ? 'x' : 'y'}`, 'custom');
2020-08-06 21:15:16 +02:00
if (transform || layoutManager.tv) {
// This can prevent others from being able to listen to mouse events
dom.addEventListener(dragSourceElement, 'mousedown', dragInitSlidee, {
//passive: true
});
}
2020-08-06 21:15:16 +02:00
initFrameResizeObserver();
2020-08-06 21:15:16 +02:00
if (transform) {
dom.addEventListener(dragSourceElement, 'touchstart', dragInitSlidee, {
passive: true
});
if (!o.horizontal) {
dom.addEventListener(frame, 'scroll', resetScroll, {
passive: true
});
}
2020-08-06 21:15:16 +02:00
if (o.mouseWheel) {
// Scrolling navigation
dom.addEventListener(scrollSource, wheelEvent, scrollHandler, {
passive: true
});
}
2022-10-03 14:22:02 -04:00
} else if (o.horizontal && o.mouseWheel) {
2020-08-06 21:15:16 +02:00
// Don't bind to mouse events with vertical scroll since the mouse wheel can handle this natively
2022-10-03 14:22:02 -04:00
// Scrolling navigation
dom.addEventListener(scrollSource, wheelEvent, scrollHandler, {
passive: true
});
2018-10-23 01:05:09 +03:00
}
2020-08-06 21:15:16 +02:00
dom.addEventListener(frame, 'click', onFrameClick, {
passive: true,
capture: true
});
2020-08-06 21:15:16 +02:00
// Mark instance as initialized
self.initialized = 1;
2020-08-06 21:15:16 +02:00
// Load
load(true);
2020-08-06 21:15:16 +02:00
// Return instance
return self;
};
2020-08-06 21:15:16 +02:00
};
/**
* Slide SLIDEE by amount of pixels.
*
* @param {Int} delta Pixels/Items. Positive means forward, negative means backward.
* @param {Bool} immediate Reposition immediately without an animation.
*
* @return {Void}
*/
scrollerFactory.prototype.slideBy = function (delta, immediate) {
if (!delta) {
return;
}
this.slideTo(this._pos.dest + delta, immediate);
};
/**
* Core method for handling `toLocation` methods.
*
* @param {String} location
* @param {Mixed} item
* @param {Bool} immediate
*
* @return {Void}
*/
scrollerFactory.prototype.to = function (location, item, immediate) {
// Optional arguments logic
if (type(item) === 'boolean') {
immediate = item;
item = undefined;
}
if (item === undefined) {
this.slideTo(this._pos[location], immediate);
} else {
2020-10-07 21:12:14 +09:00
const itemPos = this.getPos(item);
2020-08-06 21:15:16 +02:00
if (itemPos) {
this.slideTo(itemPos[location], immediate, itemPos);
}
}
};
/**
* Animate element or the whole SLIDEE to the start of the frame.
*
* @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
* @param {Bool} immediate Reposition immediately without an animation.
*
* @return {Void}
*/
scrollerFactory.prototype.toStart = function (item, immediate) {
this.to('start', item, immediate);
};
/**
* Animate element or the whole SLIDEE to the end of the frame.
*
* @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
* @param {Bool} immediate Reposition immediately without an animation.
*
* @return {Void}
*/
scrollerFactory.prototype.toEnd = function (item, immediate) {
this.to('end', item, immediate);
};
/**
* Animate element or the whole SLIDEE to the center of the frame.
*
* @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
* @param {Bool} immediate Reposition immediately without an animation.
*
* @return {Void}
*/
scrollerFactory.prototype.toCenter = function (item, immediate) {
this.to('center', item, immediate);
};
scrollerFactory.create = function (frame, options) {
2023-07-06 11:49:55 -04:00
// eslint-disable-next-line new-cap
2020-10-07 21:12:14 +09:00
const instance = new scrollerFactory(frame, options);
2020-08-06 21:15:16 +02:00
return Promise.resolve(instance);
};
export default scrollerFactory;