@shopify/polaris
Version:
Shopify’s admin product component library
191 lines (188 loc) • 5.96 kB
JavaScript
import { themeDefault } from '@shopify/polaris-tokens';
import { debounce } from '../debounce.js';
import { scrollable, dataPolarisTopBar } from '../../components/shared.js';
import { stackedContent } from '../breakpoints.js';
import { getRectForNode } from '../geometry.js';
const SIXTY_FPS = 1000 / 60;
class StickyManager {
constructor(container) {
this.stickyItems = [];
this.stuckItems = [];
this.container = null;
this.topBarOffset = 0;
this.handleResize = debounce(() => {
this.manageStickyItems();
}, SIXTY_FPS, {
leading: true,
trailing: true,
maxWait: SIXTY_FPS
});
this.handleScroll = debounce(() => {
this.manageStickyItems();
}, SIXTY_FPS, {
leading: true,
trailing: true,
maxWait: SIXTY_FPS
});
if (container) {
this.setContainer(container);
}
}
registerStickyItem(stickyItem) {
this.stickyItems.push(stickyItem);
}
unregisterStickyItem(nodeToRemove) {
const nodeIndex = this.stickyItems.findIndex(({
stickyNode
}) => nodeToRemove === stickyNode);
this.stickyItems.splice(nodeIndex, 1);
}
getStickyItem(node) {
return this.stickyItems.find(({
stickyNode
}) => node === stickyNode);
}
setContainer(el) {
this.container = el;
if (isDocument(el)) {
this.setTopBarOffset(el);
}
this.container.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
this.manageStickyItems();
}
removeScrollListener() {
if (this.container) {
this.container.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
}
manageStickyItems() {
if (this.stickyItems.length <= 0) {
return;
}
const scrollTop = this.container ? scrollTopFor(this.container) : 0;
const containerTop = getRectForNode(this.container).top + this.topBarOffset;
this.stickyItems.forEach(stickyItem => {
const {
handlePositioning
} = stickyItem;
const {
sticky,
top,
left,
width
} = this.evaluateStickyItem(stickyItem, scrollTop, containerTop);
this.updateStuckItems(stickyItem, sticky);
handlePositioning(sticky, top, left, width);
});
}
evaluateStickyItem(stickyItem, scrollTop, containerTop) {
const {
stickyNode,
placeHolderNode,
boundingElement,
offset,
disableWhenStacked
} = stickyItem;
if (disableWhenStacked && stackedContent().matches) {
return {
sticky: false,
top: 0,
left: 0,
width: 'auto'
};
}
const stickyOffset = offset ? this.getOffset(stickyNode) + parseInt(
// Important: This will not update when the active theme changes.
// Update this to `useTheme` once converted to a function component.
themeDefault.space['space-500'], 10) : this.getOffset(stickyNode);
const scrollPosition = scrollTop + stickyOffset;
const placeHolderNodeCurrentTop = placeHolderNode.getBoundingClientRect().top - containerTop + scrollTop;
const top = containerTop + stickyOffset;
const width = placeHolderNode.getBoundingClientRect().width;
const left = placeHolderNode.getBoundingClientRect().left;
let sticky;
if (boundingElement == null) {
sticky = scrollPosition >= placeHolderNodeCurrentTop;
} else {
const stickyItemHeight = stickyNode.getBoundingClientRect().height || stickyNode.firstElementChild?.getBoundingClientRect().height || 0;
const stickyItemBottomPosition = boundingElement.getBoundingClientRect().bottom - stickyItemHeight + scrollTop - containerTop;
sticky = scrollPosition >= placeHolderNodeCurrentTop && scrollPosition < stickyItemBottomPosition;
}
return {
sticky,
top,
left,
width
};
}
updateStuckItems(item, sticky) {
const {
stickyNode
} = item;
if (sticky && !this.isNodeStuck(stickyNode)) {
this.addStuckItem(item);
} else if (!sticky && this.isNodeStuck(stickyNode)) {
this.removeStuckItem(item);
}
}
addStuckItem(stickyItem) {
this.stuckItems.push(stickyItem);
}
removeStuckItem(stickyItem) {
const {
stickyNode: nodeToRemove
} = stickyItem;
const nodeIndex = this.stuckItems.findIndex(({
stickyNode
}) => nodeToRemove === stickyNode);
this.stuckItems.splice(nodeIndex, 1);
}
getOffset(node) {
if (this.stuckItems.length === 0) {
return 0;
}
let offset = 0;
let count = 0;
const stuckNodesLength = this.stuckItems.length;
const nodeRect = getRectForNode(node);
while (count < stuckNodesLength) {
const stuckNode = this.stuckItems[count].stickyNode;
if (stuckNode !== node) {
const stuckNodeRect = getRectForNode(stuckNode);
if (!horizontallyOverlaps(nodeRect, stuckNodeRect)) {
offset += getRectForNode(stuckNode).height;
}
} else {
break;
}
count++;
}
return offset;
}
isNodeStuck(node) {
const nodeFound = this.stuckItems.findIndex(({
stickyNode
}) => node === stickyNode);
return nodeFound >= 0;
}
setTopBarOffset(container) {
const topbarElement = container.querySelector(`:not(${scrollable.selector}) ${dataPolarisTopBar.selector}`);
this.topBarOffset = topbarElement ? topbarElement.clientHeight : 0;
}
}
function isDocument(node) {
return node === document;
}
function scrollTopFor(container) {
return isDocument(container) ? document.body.scrollTop || document.documentElement.scrollTop : container.scrollTop;
}
function horizontallyOverlaps(rect1, rect2) {
const rect1Left = rect1.left;
const rect1Right = rect1.left + rect1.width;
const rect2Left = rect2.left;
const rect2Right = rect2.left + rect2.width;
return rect2Right < rect1Left || rect1Right < rect2Left;
}
export { StickyManager };