add all frontend files

This commit is contained in:
2026-01-17 15:16:36 -05:00
parent ff16ae7858
commit e40287e4aa
25704 changed files with 1935289 additions and 0 deletions

21
node_modules/compute-scroll-into-view/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Cody Olsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

133
node_modules/compute-scroll-into-view/README.md generated vendored Normal file
View File

@@ -0,0 +1,133 @@
[![npm stat](https://img.shields.io/npm/dm/compute-scroll-into-view.svg?style=flat-square)](https://npm-stat.com/charts.html?package=compute-scroll-into-view)
[![npm version](https://img.shields.io/npm/v/compute-scroll-into-view.svg?style=flat-square)](https://www.npmjs.com/package/compute-scroll-into-view)
[![gzip size][gzip-badge]][unpkg-dist]
[![size][size-badge]][unpkg-dist]
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
![compute-scroll-into-view](https://user-images.githubusercontent.com/81981/43024153-a2cc212c-8c6d-11e8-913b-b4d62efcf105.png)
Lower level API that is used by the [ponyfill](https://ponyfill.com) [scroll-into-view-if-needed](https://github.com/scroll-into-view/scroll-into-view-if-needed) to compute where (if needed) elements should scroll based on [options defined in the spec](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) and the [`scrollMode: "if-needed"` draft spec proposal](https://github.com/w3c/csswg-drafts/pull/1805).
Use this if you want the smallest possible bundlesize and is ok with implementing the actual scrolling yourself.
Scrolling SVG elements are supported, as well as Shadow DOM elements. The [VisualViewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport) API is also supported, ensuring scrolling works properly on modern devices. Quirksmode is also supported as long as you polyfill [`document.scrollingElement`](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement).
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [compute(target, options)](#computetarget-options)
- [options](#options)
- [block](#block)
- [inline](#inline)
- [scrollMode](#scrollmode)
- [boundary](#boundary)
- [skipOverflowHiddenElements](#skipoverflowhiddenelements)
# Install
```bash
npm i compute-scroll-into-view
```
You can also use it from a CDN:
```js
const { compute } = await import('https://esm.sh/compute-scroll-into-view')
```
# Usage
```js
import { compute } from 'compute-scroll-into-view'
const node = document.getElementById('hero')
// same behavior as Element.scrollIntoView({block: "nearest", inline: "nearest"})
// see: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
const actions = compute(node, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
})
// same behavior as Element.scrollIntoViewIfNeeded(true)
// see: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
const actions = compute(node, {
scrollMode: 'if-needed',
block: 'center',
inline: 'center',
})
// Then perform the scrolling, use scroll-into-view-if-needed if you don't want to implement this part
actions.forEach(({ el, top, left }) => {
el.scrollTop = top
el.scrollLeft = left
})
```
# API
## compute(target, options)
## options
Type: `Object`
### [block](https://scroll-into-view.dev/#scroll-alignment)
Type: `'start' | 'center' | 'end' | 'nearest'`<br> Default: `'center'`
Control the logical scroll position on the y-axis. The spec states that the `block` direction is related to the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), but this is not implemented yet in this library.
This means that `block: 'start'` aligns to the top edge and `block: 'end'` to the bottom.
### [inline](https://scroll-into-view.dev/#scroll-alignment)
Type: `'start' | 'center' | 'end' | 'nearest'`<br> Default: `'nearest'`
Like `block` this is affected by the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode). In left-to-right pages `inline: 'start'` will align to the left edge. In right-to-left it should be flipped. This will be supported in a future release.
### [scrollMode](https://scroll-into-view.dev/#scrolling-if-needed)
Type: `'always' | 'if-needed'`<br> Default: `'always'`
This is a proposed addition to the spec that you can track here: https://github.com/w3c/csswg-drafts/pull/5677
This library will be updated to reflect any changes to the spec and will provide a migration path.
To be backwards compatible with `Element.scrollIntoViewIfNeeded` if something is not 100% visible it will count as "needs scrolling". If you need a different visibility ratio your best option would be to implement an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
### [boundary](https://scroll-into-view.dev/#limit-propagation)
Type: `Element | Function`
By default there is no boundary. All the parent elements of your target is checked until it reaches the viewport ([`document.scrollingElement`](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement)) when calculating layout and what to scroll.
By passing a boundary you can short-circuit this loop depending on your needs:
- Prevent the browser window from scrolling.
- Scroll elements into view in a list, without scrolling container elements.
You can also pass a function to do more dynamic checks to override the scroll scoping:
```js
const actions = compute(target, {
boundary: (parent) => {
// By default `overflow: hidden` elements are allowed, only `overflow: visible | clip` is skipped as
// this is required by the CSSOM spec
if (getComputedStyle(parent)['overflow'] === 'hidden') {
return false
}
return true
},
})
```
### skipOverflowHiddenElements
Type: `Boolean`<br> Default: `false`
By default the [spec](https://drafts.csswg.org/cssom-view/#scrolling-box) states that `overflow: hidden` elements should be scrollable because it has [been used to allow programatic scrolling](https://drafts.csswg.org/css-overflow-3/#valdef-overflow-hidden). This behavior can sometimes lead to [scrolling issues](https://github.com/scroll-into-view/scroll-into-view-if-needed/pull/225#issue-186419520) when you have a node that is a child of an `overflow: hidden` node.
This package follows the convention [adopted by Firefox](https://hg.mozilla.org/integration/fx-team/rev/c48c3ec05012#l7.18) of setting a boolean option to _not_ scroll all nodes with `overflow: hidden` set.
[gzip-badge]: https://img.shields.io/bundlephobia/minzip/compute-scroll-into-view?label=gzip%20size&style=flat-square
[size-badge]: https://img.shields.io/bundlephobia/min/compute-scroll-into-view?label=size&style=flat-square
[unpkg-dist]: https://unpkg.com/compute-scroll-into-view/dist/

1
node_modules/compute-scroll-into-view/dist/index.cjs generated vendored Normal file
View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const t=t=>"object"==typeof t&&null!=t&&1===t.nodeType,e=(t,e)=>(!e||"hidden"!==t)&&("visible"!==t&&"clip"!==t),o=(t,o)=>{if(t.clientHeight<t.scrollHeight||t.clientWidth<t.scrollWidth){const n=getComputedStyle(t,null);return e(n.overflowY,o)||e(n.overflowX,o)||(t=>{const e=(t=>{if(!t.ownerDocument||!t.ownerDocument.defaultView)return null;try{return t.ownerDocument.defaultView.frameElement}catch(t){return null}})(t);return!!e&&(e.clientHeight<t.scrollHeight||e.clientWidth<t.scrollWidth)})(t)}return!1},n=(t,e,o,n,l,r,i,s)=>r<t&&i>e||r>t&&i<e?0:r<=t&&s<=o||i>=e&&s>=o?r-t-n:i>e&&s<o||r<t&&s>o?i-e+l:0,l=t=>{const e=t.parentElement;return null==e?t.getRootNode().host||null:e};exports.compute=(e,r)=>{var i,s,d,c;if("undefined"==typeof document)return[];const{scrollMode:h,block:u,inline:f,boundary:a,skipOverflowHiddenElements:g}=r,p="function"==typeof a?a:t=>t!==a;if(!t(e))throw new TypeError("Invalid target");const m=document.scrollingElement||document.documentElement,w=[];let W=e;for(;t(W)&&p(W);){if(W=l(W),W===m){w.push(W);break}null!=W&&W===document.body&&o(W)&&!o(document.documentElement)||null!=W&&o(W,g)&&w.push(W)}const b=null!=(s=null==(i=window.visualViewport)?void 0:i.width)?s:innerWidth,H=null!=(c=null==(d=window.visualViewport)?void 0:d.height)?c:innerHeight,{scrollX:y,scrollY:M}=window,{height:v,width:E,top:x,right:C,bottom:I,left:R}=e.getBoundingClientRect(),{top:T,right:B,bottom:F,left:V}=(t=>{const e=window.getComputedStyle(t);return{top:parseFloat(e.scrollMarginTop)||0,right:parseFloat(e.scrollMarginRight)||0,bottom:parseFloat(e.scrollMarginBottom)||0,left:parseFloat(e.scrollMarginLeft)||0}})(e);let k="start"===u||"nearest"===u?x-T:"end"===u?I+F:x+v/2-T+F,D="center"===f?R+E/2-V+B:"end"===f?C+B:R-V;const L=[];for(let t=0;t<w.length;t++){const e=w[t],{height:l,width:r,top:i,right:s,bottom:d,left:c}=e.getBoundingClientRect();if("if-needed"===h&&x>=0&&R>=0&&I<=H&&C<=b&&(e===m&&!o(e)||x>=i&&I<=d&&R>=c&&C<=s))return L;const a=getComputedStyle(e),g=parseInt(a.borderLeftWidth,10),p=parseInt(a.borderTopWidth,10),W=parseInt(a.borderRightWidth,10),T=parseInt(a.borderBottomWidth,10);let B=0,F=0;const V="offsetWidth"in e?e.offsetWidth-e.clientWidth-g-W:0,S="offsetHeight"in e?e.offsetHeight-e.clientHeight-p-T:0,j="offsetWidth"in e?0===e.offsetWidth?0:r/e.offsetWidth:0,O="offsetHeight"in e?0===e.offsetHeight?0:l/e.offsetHeight:0;if(m===e)B="start"===u?k:"end"===u?k-H:"nearest"===u?n(M,M+H,H,p,T,M+k,M+k+v,v):k-H/2,F="start"===f?D:"center"===f?D-b/2:"end"===f?D-b:n(y,y+b,b,g,W,y+D,y+D+E,E),B=Math.max(0,B+M),F=Math.max(0,F+y);else{B="start"===u?k-i-p:"end"===u?k-d+T+S:"nearest"===u?n(i,d,l,p,T+S,k,k+v,v):k-(i+l/2)+S/2,F="start"===f?D-c-g:"center"===f?D-(c+r/2)+V/2:"end"===f?D-s+W+V:n(c,s,r,g,W+V,D,D+E,E);const{scrollLeft:t,scrollTop:o}=e;B=0===O?0:Math.max(0,Math.min(o+B/O,e.scrollHeight-l/O+S)),F=0===j?0:Math.max(0,Math.min(t+F/j,e.scrollWidth-r/j+V)),k+=o-B,D+=t-F}L.push({el:e,top:B,left:F})}return L};//# sourceMappingURL=index.cjs.map

File diff suppressed because one or more lines are too long

75
node_modules/compute-scroll-into-view/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,75 @@
/** @public */
export declare const compute: (
target: Element,
options: Options
) => ScrollAction[]
/** @public */
export declare interface Options {
/**
* Control the logical scroll position on the y-axis. The spec states that the `block` direction is related to the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), but this is not implemented yet in this library.
* This means that `block: 'start'` aligns to the top edge and `block: 'end'` to the bottom.
* @defaultValue 'center'
*/
block?: ScrollLogicalPosition
/**
* Like `block` this is affected by the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode). In left-to-right pages `inline: 'start'` will align to the left edge. In right-to-left it should be flipped. This will be supported in a future release.
* @defaultValue 'nearest'
*/
inline?: ScrollLogicalPosition
/**
* This is a proposed addition to the spec that you can track here: https://github.com/w3c/csswg-drafts/pull/5677
*
* This library will be updated to reflect any changes to the spec and will provide a migration path.
* To be backwards compatible with `Element.scrollIntoViewIfNeeded` if something is not 100% visible it will count as "needs scrolling". If you need a different visibility ratio your best option would be to implement an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
* @defaultValue 'always'
*/
scrollMode?: ScrollMode
/**
* By default there is no boundary. All the parent elements of your target is checked until it reaches the viewport ([`document.scrollingElement`](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement)) when calculating layout and what to scroll.
* By passing a boundary you can short-circuit this loop depending on your needs:
*
* - Prevent the browser window from scrolling.
* - Scroll elements into view in a list, without scrolling container elements.
*
* You can also pass a function to do more dynamic checks to override the scroll scoping:
*
* ```js
* let actions = compute(target, {
* boundary: (parent) => {
* // By default `overflow: hidden` elements are allowed, only `overflow: visible | clip` is skipped as
* // this is required by the CSSOM spec
* if (getComputedStyle(parent)['overflow'] === 'hidden') {
* return false
* }
* return true
* },
* })
* ```
* @defaultValue null
*/
boundary?: Element | ((parent: Element) => boolean) | null
/**
* New option that skips auto-scrolling all nodes with overflow: hidden set
* See FF implementation: https://hg.mozilla.org/integration/fx-team/rev/c48c3ec05012#l7.18
* @defaultValue false
* @public
*/
skipOverflowHiddenElements?: boolean
}
/** @public */
export declare interface ScrollAction {
el: Element
top: number
left: number
}
/**
* This new option is tracked in this PR, which is the most likely candidate at the time: https://github.com/w3c/csswg-drafts/pull/1805
* @public
*/
export declare type ScrollMode = 'always' | 'if-needed'
export {}

1
node_modules/compute-scroll-into-view/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1 @@
const t=t=>"object"==typeof t&&null!=t&&1===t.nodeType,e=(t,e)=>(!e||"hidden"!==t)&&("visible"!==t&&"clip"!==t),n=(t,n)=>{if(t.clientHeight<t.scrollHeight||t.clientWidth<t.scrollWidth){const o=getComputedStyle(t,null);return e(o.overflowY,n)||e(o.overflowX,n)||(t=>{const e=(t=>{if(!t.ownerDocument||!t.ownerDocument.defaultView)return null;try{return t.ownerDocument.defaultView.frameElement}catch(t){return null}})(t);return!!e&&(e.clientHeight<t.scrollHeight||e.clientWidth<t.scrollWidth)})(t)}return!1},o=(t,e,n,o,l,r,i,s)=>r<t&&i>e||r>t&&i<e?0:r<=t&&s<=n||i>=e&&s>=n?r-t-o:i>e&&s<n||r<t&&s>n?i-e+l:0,l=t=>{const e=t.parentElement;return null==e?t.getRootNode().host||null:e},r=(e,r)=>{var i,s,d,h;if("undefined"==typeof document)return[];const{scrollMode:c,block:f,inline:u,boundary:a,skipOverflowHiddenElements:g}=r,p="function"==typeof a?a:t=>t!==a;if(!t(e))throw new TypeError("Invalid target");const m=document.scrollingElement||document.documentElement,w=[];let W=e;for(;t(W)&&p(W);){if(W=l(W),W===m){w.push(W);break}null!=W&&W===document.body&&n(W)&&!n(document.documentElement)||null!=W&&n(W,g)&&w.push(W)}const b=null!=(s=null==(i=window.visualViewport)?void 0:i.width)?s:innerWidth,H=null!=(h=null==(d=window.visualViewport)?void 0:d.height)?h:innerHeight,{scrollX:y,scrollY:M}=window,{height:v,width:E,top:x,right:C,bottom:I,left:R}=e.getBoundingClientRect(),{top:T,right:B,bottom:F,left:V}=(t=>{const e=window.getComputedStyle(t);return{top:parseFloat(e.scrollMarginTop)||0,right:parseFloat(e.scrollMarginRight)||0,bottom:parseFloat(e.scrollMarginBottom)||0,left:parseFloat(e.scrollMarginLeft)||0}})(e);let k="start"===f||"nearest"===f?x-T:"end"===f?I+F:x+v/2-T+F,D="center"===u?R+E/2-V+B:"end"===u?C+B:R-V;const L=[];for(let t=0;t<w.length;t++){const e=w[t],{height:l,width:r,top:i,right:s,bottom:d,left:h}=e.getBoundingClientRect();if("if-needed"===c&&x>=0&&R>=0&&I<=H&&C<=b&&(e===m&&!n(e)||x>=i&&I<=d&&R>=h&&C<=s))return L;const a=getComputedStyle(e),g=parseInt(a.borderLeftWidth,10),p=parseInt(a.borderTopWidth,10),W=parseInt(a.borderRightWidth,10),T=parseInt(a.borderBottomWidth,10);let B=0,F=0;const V="offsetWidth"in e?e.offsetWidth-e.clientWidth-g-W:0,S="offsetHeight"in e?e.offsetHeight-e.clientHeight-p-T:0,X="offsetWidth"in e?0===e.offsetWidth?0:r/e.offsetWidth:0,Y="offsetHeight"in e?0===e.offsetHeight?0:l/e.offsetHeight:0;if(m===e)B="start"===f?k:"end"===f?k-H:"nearest"===f?o(M,M+H,H,p,T,M+k,M+k+v,v):k-H/2,F="start"===u?D:"center"===u?D-b/2:"end"===u?D-b:o(y,y+b,b,g,W,y+D,y+D+E,E),B=Math.max(0,B+M),F=Math.max(0,F+y);else{B="start"===f?k-i-p:"end"===f?k-d+T+S:"nearest"===f?o(i,d,l,p,T+S,k,k+v,v):k-(i+l/2)+S/2,F="start"===u?D-h-g:"center"===u?D-(h+r/2)+V/2:"end"===u?D-s+W+V:o(h,s,r,g,W+V,D,D+E,E);const{scrollLeft:t,scrollTop:n}=e;B=0===Y?0:Math.max(0,Math.min(n+B/Y,e.scrollHeight-l/Y+S)),F=0===X?0:Math.max(0,Math.min(t+F/X,e.scrollWidth-r/X+V)),k+=n-B,D+=t-F}L.push({el:e,top:B,left:F})}return L};export{r as compute};//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

82
node_modules/compute-scroll-into-view/package.json generated vendored Normal file
View File

@@ -0,0 +1,82 @@
{
"name": "compute-scroll-into-view",
"version": "3.1.1",
"description": "The engine that powers scroll-into-view-if-needed",
"keywords": [
"if-needed",
"scroll",
"scroll-into-view",
"scroll-into-view-if-needed",
"scrollIntoView",
"scrollIntoViewIfNeeded",
"scrollMode",
"typescript"
],
"homepage": "https://scroll-into-view.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/scroll-into-view/compute-scroll-into-view.git"
},
"license": "MIT",
"author": "Cody Olsen",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"source": "./src/index.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"source": "./src/index.ts",
"typings": "./dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"prebuild": "npx rimraf 'dist'",
"build": "pkg build --strict",
"prepublishOnly": "npm run build",
"test": "npx cross-env JEST_PUPPETEER_CONFIG='jest-puppeteer.config.cjs' jest -c integration/jest.config.cjs",
"typecheck": "tsc"
},
"browserslist": [
"> 0.2% and supports es6-module and supports es6-module-dynamic-import and not dead",
"maintained node versions"
],
"prettier": {
"semi": false,
"singleQuote": true
},
"devDependencies": {
"@sanity/pkg-utils": "^2.2.5",
"@sanity/semantic-release-preset": "^4.0.0",
"@types/expect-puppeteer": "^5.0.2",
"@types/jest": "^29.4.0",
"@types/jest-environment-puppeteer": "^5.0.3",
"@types/puppeteer": "^7.0.4",
"cross-env": "^7.0.3",
"jest": "^29.5.0",
"jest-junit": "^15.0.0",
"jest-puppeteer": "^8.0.0",
"prettier": "^2.8.4",
"prettier-plugin-packagejson": "^2.4.3",
"puppeteer": "^19.7.0",
"rimraf": "^4.1.2",
"serve": "^14.2.0",
"typescript": "^5.0.0"
},
"bundlesize": [
{
"path": "./dist/index.js",
"maxSize": "3 kB",
"compression": "none"
}
]
}

566
node_modules/compute-scroll-into-view/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,566 @@
// Compute what scrolling needs to be done on required scrolling boxes for target to be in view
// The type names here are named after the spec to make it easier to find more information around what they mean:
// To reduce churn and reduce things that need be maintained things from the official TS DOM library is used here
// https://drafts.csswg.org/cssom-view/
// For a definition on what is "block flow direction" exactly, check this: https://drafts.csswg.org/css-writing-modes-4/#block-flow-direction
/**
* This new option is tracked in this PR, which is the most likely candidate at the time: https://github.com/w3c/csswg-drafts/pull/1805
* @public
*/
export type ScrollMode = 'always' | 'if-needed'
/** @public */
export interface Options {
/**
* Control the logical scroll position on the y-axis. The spec states that the `block` direction is related to the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), but this is not implemented yet in this library.
* This means that `block: 'start'` aligns to the top edge and `block: 'end'` to the bottom.
* @defaultValue 'center'
*/
block?: ScrollLogicalPosition
/**
* Like `block` this is affected by the [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode). In left-to-right pages `inline: 'start'` will align to the left edge. In right-to-left it should be flipped. This will be supported in a future release.
* @defaultValue 'nearest'
*/
inline?: ScrollLogicalPosition
/**
* This is a proposed addition to the spec that you can track here: https://github.com/w3c/csswg-drafts/pull/5677
*
* This library will be updated to reflect any changes to the spec and will provide a migration path.
* To be backwards compatible with `Element.scrollIntoViewIfNeeded` if something is not 100% visible it will count as "needs scrolling". If you need a different visibility ratio your best option would be to implement an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
* @defaultValue 'always'
*/
scrollMode?: ScrollMode
/**
* By default there is no boundary. All the parent elements of your target is checked until it reaches the viewport ([`document.scrollingElement`](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement)) when calculating layout and what to scroll.
* By passing a boundary you can short-circuit this loop depending on your needs:
*
* - Prevent the browser window from scrolling.
* - Scroll elements into view in a list, without scrolling container elements.
*
* You can also pass a function to do more dynamic checks to override the scroll scoping:
*
* ```js
* let actions = compute(target, {
* boundary: (parent) => {
* // By default `overflow: hidden` elements are allowed, only `overflow: visible | clip` is skipped as
* // this is required by the CSSOM spec
* if (getComputedStyle(parent)['overflow'] === 'hidden') {
* return false
* }
* return true
* },
* })
* ```
* @defaultValue null
*/
boundary?: Element | ((parent: Element) => boolean) | null
/**
* New option that skips auto-scrolling all nodes with overflow: hidden set
* See FF implementation: https://hg.mozilla.org/integration/fx-team/rev/c48c3ec05012#l7.18
* @defaultValue false
* @public
*/
skipOverflowHiddenElements?: boolean
}
/** @public */
export interface ScrollAction {
el: Element
top: number
left: number
}
// @TODO better shadowdom test, 11 = document fragment
const isElement = (el: any): el is Element =>
typeof el === 'object' && el != null && el.nodeType === 1
const canOverflow = (
overflow: string | null,
skipOverflowHiddenElements?: boolean
) => {
if (skipOverflowHiddenElements && overflow === 'hidden') {
return false
}
return overflow !== 'visible' && overflow !== 'clip'
}
const getFrameElement = (el: Element) => {
if (!el.ownerDocument || !el.ownerDocument.defaultView) {
return null
}
try {
return el.ownerDocument.defaultView.frameElement
} catch (e) {
return null
}
}
const isHiddenByFrame = (el: Element): boolean => {
const frame = getFrameElement(el)
if (!frame) {
return false
}
return (
frame.clientHeight < el.scrollHeight || frame.clientWidth < el.scrollWidth
)
}
const isScrollable = (el: Element, skipOverflowHiddenElements?: boolean) => {
if (el.clientHeight < el.scrollHeight || el.clientWidth < el.scrollWidth) {
const style = getComputedStyle(el, null)
return (
canOverflow(style.overflowY, skipOverflowHiddenElements) ||
canOverflow(style.overflowX, skipOverflowHiddenElements) ||
isHiddenByFrame(el)
)
}
return false
}
/**
* Find out which edge to align against when logical scroll position is "nearest"
* Interesting fact: "nearest" works similarily to "if-needed", if the element is fully visible it will not scroll it
*
* Legends:
* ┌────────┐ ┏ ━ ━ ━ ┓
* │ target │ frame
* └────────┘ ┗ ━ ━ ━ ┛
*/
const alignNearest = (
scrollingEdgeStart: number,
scrollingEdgeEnd: number,
scrollingSize: number,
scrollingBorderStart: number,
scrollingBorderEnd: number,
elementEdgeStart: number,
elementEdgeEnd: number,
elementSize: number
) => {
/**
* If element edge A and element edge B are both outside scrolling box edge A and scrolling box edge B
*
* ┌──┐
* ┏━│━━│━┓
* │ │
* ┃ │ │ ┃ do nothing
* │ │
* ┗━│━━│━┛
* └──┘
*
* If element edge C and element edge D are both outside scrolling box edge C and scrolling box edge D
*
* ┏ ━ ━ ━ ━ ┓
* ┌───────────┐
* │┃ ┃│ do nothing
* └───────────┘
* ┗ ━ ━ ━ ━ ┛
*/
if (
(elementEdgeStart < scrollingEdgeStart &&
elementEdgeEnd > scrollingEdgeEnd) ||
(elementEdgeStart > scrollingEdgeStart && elementEdgeEnd < scrollingEdgeEnd)
) {
return 0
}
/**
* If element edge A is outside scrolling box edge A and element height is less than scrolling box height
*
* ┌──┐
* ┏━│━━│━┓ ┏━┌━━┐━┓
* └──┘ │ │
* from ┃ ┃ to ┃ └──┘ ┃
*
* ┗━ ━━ ━┛ ┗━ ━━ ━┛
*
* If element edge B is outside scrolling box edge B and element height is greater than scrolling box height
*
* ┏━ ━━ ━┓ ┏━┌━━┐━┓
* │ │
* from ┃ ┌──┐ ┃ to ┃ │ │ ┃
* │ │ │ │
* ┗━│━━│━┛ ┗━│━━│━┛
* │ │ └──┘
* │ │
* └──┘
*
* If element edge C is outside scrolling box edge C and element width is less than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───┐ ┌───┐
* │ ┃ │ ┃ ┃ │ ┃
* └───┘ └───┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
* If element edge D is outside scrolling box edge D and element width is greater than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───────────┐ ┌───────────┐
* ┃ │ ┃ │ ┃ ┃ │
* └───────────┘ └───────────┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*/
if (
(elementEdgeStart <= scrollingEdgeStart && elementSize <= scrollingSize) ||
(elementEdgeEnd >= scrollingEdgeEnd && elementSize >= scrollingSize)
) {
return elementEdgeStart - scrollingEdgeStart - scrollingBorderStart
}
/**
* If element edge B is outside scrolling box edge B and element height is less than scrolling box height
*
* ┏━ ━━ ━┓ ┏━ ━━ ━┓
*
* from ┃ ┃ to ┃ ┌──┐ ┃
* ┌──┐ │ │
* ┗━│━━│━┛ ┗━└━━┘━┛
* └──┘
*
* If element edge A is outside scrolling box edge A and element height is greater than scrolling box height
*
* ┌──┐
* │ │
* │ │ ┌──┐
* ┏━│━━│━┓ ┏━│━━│━┓
* │ │ │ │
* from ┃ └──┘ ┃ to ┃ │ │ ┃
* │ │
* ┗━ ━━ ━┛ ┗━└━━┘━┛
*
* If element edge C is outside scrolling box edge C and element width is greater than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───────────┐ ┌───────────┐
* │ ┃ │ ┃ │ ┃ ┃
* └───────────┘ └───────────┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
* If element edge D is outside scrolling box edge D and element width is less than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───┐ ┌───┐
* ┃ │ ┃ │ ┃ │ ┃
* └───┘ └───┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
*/
if (
(elementEdgeEnd > scrollingEdgeEnd && elementSize < scrollingSize) ||
(elementEdgeStart < scrollingEdgeStart && elementSize > scrollingSize)
) {
return elementEdgeEnd - scrollingEdgeEnd + scrollingBorderEnd
}
return 0
}
const getParentElement = (element: Node): Element | null => {
const parent = element.parentElement
if (parent == null) {
return (element.getRootNode() as ShadowRoot).host || null
}
return parent
}
const getScrollMargins = (target: Element) => {
const computedStyle = window.getComputedStyle(target)
return {
top: parseFloat(computedStyle.scrollMarginTop) || 0,
right: parseFloat(computedStyle.scrollMarginRight) || 0,
bottom: parseFloat(computedStyle.scrollMarginBottom) || 0,
left: parseFloat(computedStyle.scrollMarginLeft) || 0,
}
}
/** @public */
export const compute = (target: Element, options: Options): ScrollAction[] => {
if (typeof document === 'undefined') {
// If there's no DOM we assume it's not in a browser environment
return []
}
const { scrollMode, block, inline, boundary, skipOverflowHiddenElements } =
options
// Allow using a callback to check the boundary
// The default behavior is to check if the current target matches the boundary element or not
// If undefined it'll check that target is never undefined (can happen as we recurse up the tree)
const checkBoundary =
typeof boundary === 'function' ? boundary : (node: any) => node !== boundary
if (!isElement(target)) {
throw new TypeError('Invalid target')
}
// Used to handle the top most element that can be scrolled
const scrollingElement = document.scrollingElement || document.documentElement
// Collect all the scrolling boxes, as defined in the spec: https://drafts.csswg.org/cssom-view/#scrolling-box
const frames: Element[] = []
let cursor: Element | null = target
while (isElement(cursor) && checkBoundary(cursor)) {
// Move cursor to parent
cursor = getParentElement(cursor)
// Stop when we reach the viewport
if (cursor === scrollingElement) {
frames.push(cursor)
break
}
// Skip document.body if it's not the scrollingElement and documentElement isn't independently scrollable
if (
cursor != null &&
cursor === document.body &&
isScrollable(cursor) &&
!isScrollable(document.documentElement)
) {
continue
}
// Now we check if the element is scrollable, this code only runs if the loop haven't already hit the viewport or a custom boundary
if (cursor != null && isScrollable(cursor, skipOverflowHiddenElements)) {
frames.push(cursor)
}
}
// Support pinch-zooming properly, making sure elements scroll into the visual viewport
// Browsers that don't support visualViewport will report the layout viewport dimensions on document.documentElement.clientWidth/Height
// and viewport dimensions on window.innerWidth/Height
// https://www.quirksmode.org/mobile/viewports2.html
// https://bokand.github.io/viewport/index.html
const viewportWidth = window.visualViewport?.width ?? innerWidth
const viewportHeight = window.visualViewport?.height ?? innerHeight
const { scrollX, scrollY } = window
const {
height: targetHeight,
width: targetWidth,
top: targetTop,
right: targetRight,
bottom: targetBottom,
left: targetLeft,
} = target.getBoundingClientRect()
const {
top: marginTop,
right: marginRight,
bottom: marginBottom,
left: marginLeft,
} = getScrollMargins(target)
// These values mutate as we loop through and generate scroll coordinates
let targetBlock: number =
block === 'start' || block === 'nearest'
? targetTop - marginTop
: block === 'end'
? targetBottom + marginBottom
: targetTop + targetHeight / 2 - marginTop + marginBottom // block === 'center
let targetInline: number =
inline === 'center'
? targetLeft + targetWidth / 2 - marginLeft + marginRight
: inline === 'end'
? targetRight + marginRight
: targetLeft - marginLeft // inline === 'start || inline === 'nearest
// Collect new scroll positions
const computations: ScrollAction[] = []
// In chrome there's no longer a difference between caching the `frames.length` to a var or not, so we don't in this case (size > speed anyways)
for (let index = 0; index < frames.length; index++) {
const frame = frames[index]
// @TODO add a shouldScroll hook here that allows userland code to take control
const { height, width, top, right, bottom, left } =
frame.getBoundingClientRect()
// If the element is already visible we can end it here
// @TODO targetBlock and targetInline should be taken into account to be compliant with https://github.com/w3c/csswg-drafts/pull/1805/files#diff-3c17f0e43c20f8ecf89419d49e7ef5e0R1333
if (
scrollMode === 'if-needed' &&
targetTop >= 0 &&
targetLeft >= 0 &&
targetBottom <= viewportHeight &&
targetRight <= viewportWidth &&
// scrollingElement is added to the frames array even if it's not scrollable, in which case checking its bounds is not required
((frame === scrollingElement && !isScrollable(frame)) ||
(targetTop >= top &&
targetBottom <= bottom &&
targetLeft >= left &&
targetRight <= right))
) {
// Break the loop and return the computations for things that are not fully visible
return computations
}
const frameStyle = getComputedStyle(frame)
const borderLeft = parseInt(frameStyle.borderLeftWidth as string, 10)
const borderTop = parseInt(frameStyle.borderTopWidth as string, 10)
const borderRight = parseInt(frameStyle.borderRightWidth as string, 10)
const borderBottom = parseInt(frameStyle.borderBottomWidth as string, 10)
let blockScroll: number = 0
let inlineScroll: number = 0
// The property existance checks for offfset[Width|Height] is because only HTMLElement objects have them, but any Element might pass by here
// @TODO find out if the "as HTMLElement" overrides can be dropped
const scrollbarWidth =
'offsetWidth' in frame
? (frame as HTMLElement).offsetWidth -
(frame as HTMLElement).clientWidth -
borderLeft -
borderRight
: 0
const scrollbarHeight =
'offsetHeight' in frame
? (frame as HTMLElement).offsetHeight -
(frame as HTMLElement).clientHeight -
borderTop -
borderBottom
: 0
const scaleX =
'offsetWidth' in frame
? (frame as HTMLElement).offsetWidth === 0
? 0
: width / (frame as HTMLElement).offsetWidth
: 0
const scaleY =
'offsetHeight' in frame
? (frame as HTMLElement).offsetHeight === 0
? 0
: height / (frame as HTMLElement).offsetHeight
: 0
if (scrollingElement === frame) {
// Handle viewport logic (document.documentElement or document.body)
if (block === 'start') {
blockScroll = targetBlock
} else if (block === 'end') {
blockScroll = targetBlock - viewportHeight
} else if (block === 'nearest') {
blockScroll = alignNearest(
scrollY,
scrollY + viewportHeight,
viewportHeight,
borderTop,
borderBottom,
scrollY + targetBlock,
scrollY + targetBlock + targetHeight,
targetHeight
)
} else {
// block === 'center' is the default
blockScroll = targetBlock - viewportHeight / 2
}
if (inline === 'start') {
inlineScroll = targetInline
} else if (inline === 'center') {
inlineScroll = targetInline - viewportWidth / 2
} else if (inline === 'end') {
inlineScroll = targetInline - viewportWidth
} else {
// inline === 'nearest' is the default
inlineScroll = alignNearest(
scrollX,
scrollX + viewportWidth,
viewportWidth,
borderLeft,
borderRight,
scrollX + targetInline,
scrollX + targetInline + targetWidth,
targetWidth
)
}
// Apply scroll position offsets and ensure they are within bounds
// @TODO add more test cases to cover this 100%
blockScroll = Math.max(0, blockScroll + scrollY)
inlineScroll = Math.max(0, inlineScroll + scrollX)
} else {
// Handle each scrolling frame that might exist between the target and the viewport
if (block === 'start') {
blockScroll = targetBlock - top - borderTop
} else if (block === 'end') {
blockScroll = targetBlock - bottom + borderBottom + scrollbarHeight
} else if (block === 'nearest') {
blockScroll = alignNearest(
top,
bottom,
height,
borderTop,
borderBottom + scrollbarHeight,
targetBlock,
targetBlock + targetHeight,
targetHeight
)
} else {
// block === 'center' is the default
blockScroll = targetBlock - (top + height / 2) + scrollbarHeight / 2
}
if (inline === 'start') {
inlineScroll = targetInline - left - borderLeft
} else if (inline === 'center') {
inlineScroll = targetInline - (left + width / 2) + scrollbarWidth / 2
} else if (inline === 'end') {
inlineScroll = targetInline - right + borderRight + scrollbarWidth
} else {
// inline === 'nearest' is the default
inlineScroll = alignNearest(
left,
right,
width,
borderLeft,
borderRight + scrollbarWidth,
targetInline,
targetInline + targetWidth,
targetWidth
)
}
const { scrollLeft, scrollTop } = frame
// Ensure scroll coordinates are not out of bounds while applying scroll offsets
blockScroll =
scaleY === 0
? 0
: Math.max(
0,
Math.min(
scrollTop + blockScroll / scaleY,
frame.scrollHeight - height / scaleY + scrollbarHeight
)
)
inlineScroll =
scaleX === 0
? 0
: Math.max(
0,
Math.min(
scrollLeft + inlineScroll / scaleX,
frame.scrollWidth - width / scaleX + scrollbarWidth
)
)
// Cache the offset so that parent frames can scroll this into view correctly
targetBlock += scrollTop - blockScroll
targetInline += scrollLeft - inlineScroll
}
computations.push({ el: frame, top: blockScroll, left: inlineScroll })
}
return computations
}