@sticky-aside/core 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -2
- package/dist/headless.d.ts +10 -0
- package/dist/headless.js +3 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +164 -0
- package/dist/index.js.map +1 -0
- package/dist/offsets.css +7 -0
- package/dist/presenter.js +304 -0
- package/dist/presenter.js.map +1 -0
- package/dist/types.d.ts +124 -0
- package/package.json +41 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maprox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sticky-aside/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A short panel stays pinned to the top. A tall panel scrolls with the page, pins to the bottom while you scroll down, and pins to the top while you scroll up.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @sticky-aside/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<aside class="column">
|
|
11
|
+
<div class="panel">…</div>
|
|
12
|
+
</aside>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createStickyAside } from '@sticky-aside/core'
|
|
17
|
+
import '@sticky-aside/core/offsets.css'
|
|
18
|
+
|
|
19
|
+
const wrapper = document.querySelector('.column')
|
|
20
|
+
const content = document.querySelector('.panel')
|
|
21
|
+
|
|
22
|
+
const aside = createStickyAside({ wrapper, content })
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`wrapper` is the column that stays in the layout. `content` is the panel that moves and pins. The core sets `position: relative` and `height: 100%` on the wrapper.
|
|
26
|
+
|
|
27
|
+
`offsets.css` defines `sticky-aside-top` and `sticky-aside-bottom`. Set the gap with CSS variables on an ancestor (default `0px`):
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
.column {
|
|
31
|
+
--sticky-aside-top: 40px;
|
|
32
|
+
--sticky-aside-bottom: 16px;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or skip that stylesheet and pass your own `topClass` / `bottomClass` to `createStickyAside` / `setClasses`.
|
|
37
|
+
|
|
38
|
+
Vue, React, and Svelte adapters are `@sticky-aside/vue`, `@sticky-aside/react`, and `@sticky-aside/svelte`. The full guide is in the [repository README](https://github.com/maprox/sticky-aside#readme).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { _ as StickyFrame, b as StickyReason, c as StickyAsideView, d as StickyPaintFlow, f as StickyPaintLocked, g as Box, h as StickyReading, i as StickyAsideHandlers, l as StickyPaint, m as StickyPaintTop, o as StickyAsidePresenterOptions, p as StickyPaintOff, r as StickyAsideController, t as ScrollMetrics, u as StickyPaintBottom, v as StickyMode, y as StickyModel } from "./types.js";
|
|
2
|
+
//#region src/presenter/presenter.d.ts
|
|
3
|
+
export declare function createStickyAsidePresenter(view: StickyAsideView, options: StickyAsidePresenterOptions): StickyAsideController;
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/model/step.d.ts
|
|
6
|
+
export declare function getInitialStickyModel(): StickyModel;
|
|
7
|
+
export declare function getNewStickyModel(model: StickyModel, frame: StickyFrame): StickyModel;
|
|
8
|
+
//#endregion
|
|
9
|
+
export type { Box, ScrollMetrics, StickyAsideHandlers, StickyAsideView, StickyFrame, StickyMode, StickyModel, StickyPaint, StickyPaintBottom, StickyPaintFlow, StickyPaintLocked, StickyPaintOff, StickyPaintTop, StickyReading, StickyReason };
|
|
10
|
+
//# sourceMappingURL=headless.d.ts.map
|
package/dist/headless.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { a as StickyAsideOptions, g as Box, n as StickyAsideClasses, o as StickyAsidePresenterOptions, r as StickyAsideController, s as StickyAsideState, t as ScrollMetrics } from "./types.js";
|
|
2
|
+
//#region src/view/constants.d.ts
|
|
3
|
+
export declare const OFFSET_CLASS: {
|
|
4
|
+
readonly top: "sticky-aside-top";
|
|
5
|
+
readonly bottom: "sticky-aside-bottom";
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/create-sticky-aside.d.ts
|
|
9
|
+
export declare function createStickyAside(options: StickyAsideOptions): StickyAsideController;
|
|
10
|
+
//#endregion
|
|
11
|
+
export type { Box, ScrollMetrics, StickyAsideClasses, StickyAsideController, StickyAsideOptions, StickyAsidePresenterOptions, StickyAsideState };
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { t as createStickyAsidePresenter } from "./presenter.js";
|
|
2
|
+
|
|
3
|
+
//#region src/view/constants.ts
|
|
4
|
+
const OFFSET_CLASS = {
|
|
5
|
+
top: "sticky-aside-top",
|
|
6
|
+
bottom: "sticky-aside-bottom"
|
|
7
|
+
};
|
|
8
|
+
const DATASET = {
|
|
9
|
+
measure: "stickyMeasure",
|
|
10
|
+
aside: "stickyAside"
|
|
11
|
+
};
|
|
12
|
+
const ASIDE_STATE = {
|
|
13
|
+
off: "off",
|
|
14
|
+
flow: "flow",
|
|
15
|
+
bottom: "bottom",
|
|
16
|
+
locked: "locked",
|
|
17
|
+
top: "top"
|
|
18
|
+
};
|
|
19
|
+
const MEASURE_STYLE = {
|
|
20
|
+
position: "fixed",
|
|
21
|
+
visibility: "hidden",
|
|
22
|
+
pointerEvents: "none",
|
|
23
|
+
left: "0",
|
|
24
|
+
width: "0"
|
|
25
|
+
};
|
|
26
|
+
const WRAPPER_STYLE = {
|
|
27
|
+
position: "relative",
|
|
28
|
+
height: "100%"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/view/browser-view.ts
|
|
33
|
+
function createBrowserView(options) {
|
|
34
|
+
const measure = document.createElement("div");
|
|
35
|
+
measure.dataset[DATASET.measure] = "";
|
|
36
|
+
Object.assign(measure.style, MEASURE_STYLE);
|
|
37
|
+
options.wrapper.append(measure);
|
|
38
|
+
Object.assign(options.wrapper.style, WRAPPER_STYLE);
|
|
39
|
+
let resizeObserver;
|
|
40
|
+
let onScroll;
|
|
41
|
+
let onResize;
|
|
42
|
+
let topClass = options.topClass ?? OFFSET_CLASS.top;
|
|
43
|
+
let bottomClass = options.bottomClass ?? OFFSET_CLASS.bottom;
|
|
44
|
+
let contentClass = options.contentClass ?? "";
|
|
45
|
+
let appliedMeasure = [];
|
|
46
|
+
let appliedContent = [];
|
|
47
|
+
function getClassTokens(value) {
|
|
48
|
+
return value.split(/\s+/).filter((token) => token.length > 0);
|
|
49
|
+
}
|
|
50
|
+
function syncClassTokens(element, previous, next) {
|
|
51
|
+
for (const token of previous) if (!next.includes(token)) element.classList.remove(token);
|
|
52
|
+
for (const token of next) element.classList.add(token);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
function clearContentStyle() {
|
|
56
|
+
options.content.style.position = "";
|
|
57
|
+
options.content.style.top = "";
|
|
58
|
+
options.content.style.bottom = "";
|
|
59
|
+
options.content.style.width = "";
|
|
60
|
+
}
|
|
61
|
+
function getBox(element) {
|
|
62
|
+
if (options.readRect) return options.readRect(element);
|
|
63
|
+
const rect = element.getBoundingClientRect();
|
|
64
|
+
return {
|
|
65
|
+
top: rect.top,
|
|
66
|
+
bottom: rect.bottom,
|
|
67
|
+
width: rect.width,
|
|
68
|
+
height: rect.height
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function getMetrics() {
|
|
72
|
+
if (options.getScrollMetrics) return options.getScrollMetrics();
|
|
73
|
+
return {
|
|
74
|
+
scrollY: window.scrollY,
|
|
75
|
+
viewportHeight: window.innerHeight,
|
|
76
|
+
documentHeight: document.body.clientHeight
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function read() {
|
|
80
|
+
return {
|
|
81
|
+
metrics: getMetrics(),
|
|
82
|
+
wrapper: getBox(options.wrapper),
|
|
83
|
+
content: getBox(options.content),
|
|
84
|
+
measure: getBox(measure)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function setClasses(next) {
|
|
88
|
+
if (next.topClass !== void 0) topClass = next.topClass;
|
|
89
|
+
if (next.bottomClass !== void 0) bottomClass = next.bottomClass;
|
|
90
|
+
if (next.contentClass !== void 0) contentClass = next.contentClass;
|
|
91
|
+
}
|
|
92
|
+
function paintMeasure() {
|
|
93
|
+
const contentBox = getBox(options.content);
|
|
94
|
+
measure.style.top = "";
|
|
95
|
+
measure.style.bottom = "";
|
|
96
|
+
measure.style.maxHeight = `${contentBox.height}px`;
|
|
97
|
+
appliedMeasure = syncClassTokens(measure, appliedMeasure, [...getClassTokens(topClass), ...getClassTokens(bottomClass)]);
|
|
98
|
+
}
|
|
99
|
+
function paint(next) {
|
|
100
|
+
clearContentStyle();
|
|
101
|
+
options.content.dataset[DATASET.aside] = ASIDE_STATE[next.state];
|
|
102
|
+
const nextTokens = getClassTokens(contentClass);
|
|
103
|
+
if (next.state === "off") options.content.style.position = "relative";
|
|
104
|
+
else if (next.state === "flow") {
|
|
105
|
+
options.content.style.position = "relative";
|
|
106
|
+
options.content.style.top = `${next.top}px`;
|
|
107
|
+
} else if (next.state === "bottom") {
|
|
108
|
+
options.content.style.position = "fixed";
|
|
109
|
+
options.content.style.width = `${next.width}px`;
|
|
110
|
+
nextTokens.push(...getClassTokens(bottomClass));
|
|
111
|
+
} else if (next.state === "locked") {
|
|
112
|
+
options.content.style.position = "fixed";
|
|
113
|
+
options.content.style.width = `${next.width}px`;
|
|
114
|
+
options.content.style.top = `${next.top}px`;
|
|
115
|
+
} else {
|
|
116
|
+
options.content.style.position = "fixed";
|
|
117
|
+
options.content.style.width = `${next.width}px`;
|
|
118
|
+
nextTokens.push(...getClassTokens(topClass));
|
|
119
|
+
}
|
|
120
|
+
appliedContent = syncClassTokens(options.content, appliedContent, nextTokens);
|
|
121
|
+
}
|
|
122
|
+
function unsubscribe() {
|
|
123
|
+
resizeObserver?.disconnect();
|
|
124
|
+
resizeObserver = void 0;
|
|
125
|
+
if (onScroll) window.removeEventListener("scroll", onScroll);
|
|
126
|
+
if (onResize) window.removeEventListener("resize", onResize);
|
|
127
|
+
onScroll = void 0;
|
|
128
|
+
onResize = void 0;
|
|
129
|
+
}
|
|
130
|
+
function subscribe(handlers) {
|
|
131
|
+
onScroll = () => handlers.scroll();
|
|
132
|
+
onResize = () => handlers.resize();
|
|
133
|
+
resizeObserver = new ResizeObserver(() => handlers.resize());
|
|
134
|
+
resizeObserver.observe(options.content);
|
|
135
|
+
resizeObserver.observe(measure);
|
|
136
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
137
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
138
|
+
return unsubscribe;
|
|
139
|
+
}
|
|
140
|
+
function destroy() {
|
|
141
|
+
appliedContent = syncClassTokens(options.content, appliedContent, []);
|
|
142
|
+
appliedMeasure = syncClassTokens(measure, appliedMeasure, []);
|
|
143
|
+
measure.remove();
|
|
144
|
+
clearContentStyle();
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
read,
|
|
148
|
+
setClasses,
|
|
149
|
+
paintMeasure,
|
|
150
|
+
paint,
|
|
151
|
+
subscribe,
|
|
152
|
+
destroy
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/create-sticky-aside.ts
|
|
158
|
+
function createStickyAside(options) {
|
|
159
|
+
return createStickyAsidePresenter(createBrowserView(options), options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
export { OFFSET_CLASS, createStickyAside };
|
|
164
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/view/constants.ts","../src/view/browser-view.ts","../src/create-sticky-aside.ts"],"sourcesContent":["export const OFFSET_CLASS = {\n top: 'sticky-aside-top',\n bottom: 'sticky-aside-bottom',\n} as const\n\nexport const DATASET = {\n measure: 'stickyMeasure',\n aside: 'stickyAside',\n} as const\n\nexport const ASIDE_STATE = {\n off: 'off',\n flow: 'flow',\n bottom: 'bottom',\n locked: 'locked',\n top: 'top',\n} as const\n\nexport const MEASURE_STYLE = {\n position: 'fixed',\n visibility: 'hidden',\n pointerEvents: 'none',\n left: '0',\n width: '0',\n} as const\n\nexport const WRAPPER_STYLE = {\n position: 'relative',\n height: '100%',\n} as const\n","import type { Box } from '#core/model/types.ts'\nimport type {\n ScrollMetrics,\n StickyAsideClasses,\n StickyAsideHandlers,\n StickyAsideOptions,\n StickyAsideView,\n StickyPaint,\n StickyReading,\n} from '#core/types.ts'\nimport { ASIDE_STATE, DATASET, MEASURE_STYLE, OFFSET_CLASS, WRAPPER_STYLE } from '#core/view/constants.ts'\n\nexport function createBrowserView(options: StickyAsideOptions): StickyAsideView {\n const measure = document.createElement('div')\n measure.dataset[DATASET.measure] = ''\n Object.assign(measure.style, MEASURE_STYLE)\n options.wrapper.append(measure)\n Object.assign(options.wrapper.style, WRAPPER_STYLE)\n\n let resizeObserver: ResizeObserver | undefined\n let onScroll: (() => void) | undefined\n let onResize: (() => void) | undefined\n let topClass = options.topClass ?? OFFSET_CLASS.top\n let bottomClass = options.bottomClass ?? OFFSET_CLASS.bottom\n let contentClass = options.contentClass ?? ''\n let appliedMeasure: string[] = []\n let appliedContent: string[] = []\n\n function getClassTokens(value: string): string[] {\n return value.split(/\\s+/).filter((token) => token.length > 0)\n }\n\n function syncClassTokens(element: HTMLElement, previous: string[], next: string[]): string[] {\n for (const token of previous) {\n if (!next.includes(token)) {\n element.classList.remove(token)\n }\n }\n\n for (const token of next) {\n element.classList.add(token)\n }\n\n return next\n }\n\n function clearContentStyle() {\n options.content.style.position = ''\n options.content.style.top = ''\n options.content.style.bottom = ''\n options.content.style.width = ''\n }\n\n function getBox(element: HTMLElement): Box {\n if (options.readRect) {\n return options.readRect(element)\n }\n\n const rect = element.getBoundingClientRect()\n\n return {\n top: rect.top,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n }\n }\n\n function getMetrics(): ScrollMetrics {\n if (options.getScrollMetrics) {\n return options.getScrollMetrics()\n }\n\n return {\n scrollY: window.scrollY,\n viewportHeight: window.innerHeight,\n documentHeight: document.body.clientHeight,\n }\n }\n\n function read(): StickyReading {\n return {\n metrics: getMetrics(),\n wrapper: getBox(options.wrapper),\n content: getBox(options.content),\n measure: getBox(measure),\n }\n }\n\n function setClasses(next: StickyAsideClasses) {\n if (next.topClass !== undefined) {\n topClass = next.topClass\n }\n\n if (next.bottomClass !== undefined) {\n bottomClass = next.bottomClass\n }\n\n if (next.contentClass !== undefined) {\n contentClass = next.contentClass\n }\n }\n\n function paintMeasure() {\n const contentBox = getBox(options.content)\n measure.style.top = ''\n measure.style.bottom = ''\n measure.style.maxHeight = `${contentBox.height}px`\n appliedMeasure = syncClassTokens(measure, appliedMeasure, [\n ...getClassTokens(topClass),\n ...getClassTokens(bottomClass),\n ])\n }\n\n function paint(next: StickyPaint) {\n clearContentStyle()\n options.content.dataset[DATASET.aside] = ASIDE_STATE[next.state]\n const nextTokens = getClassTokens(contentClass)\n if (next.state === 'off') {\n options.content.style.position = 'relative'\n } else if (next.state === 'flow') {\n options.content.style.position = 'relative'\n options.content.style.top = `${next.top}px`\n } else if (next.state === 'bottom') {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n nextTokens.push(...getClassTokens(bottomClass))\n } else if (next.state === 'locked') {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n options.content.style.top = `${next.top}px`\n } else {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n nextTokens.push(...getClassTokens(topClass))\n }\n\n appliedContent = syncClassTokens(options.content, appliedContent, nextTokens)\n }\n\n function unsubscribe() {\n resizeObserver?.disconnect()\n resizeObserver = undefined\n if (onScroll) {\n window.removeEventListener('scroll', onScroll)\n }\n\n if (onResize) {\n window.removeEventListener('resize', onResize)\n }\n\n onScroll = undefined\n onResize = undefined\n }\n\n function subscribe(handlers: StickyAsideHandlers) {\n onScroll = () => handlers.scroll()\n onResize = () => handlers.resize()\n resizeObserver = new ResizeObserver(() => handlers.resize())\n resizeObserver.observe(options.content)\n resizeObserver.observe(measure)\n window.addEventListener('scroll', onScroll, {\n passive: true,\n })\n window.addEventListener('resize', onResize, {\n passive: true,\n })\n\n return unsubscribe\n }\n\n function destroy() {\n appliedContent = syncClassTokens(options.content, appliedContent, [])\n appliedMeasure = syncClassTokens(measure, appliedMeasure, [])\n measure.remove()\n clearContentStyle()\n }\n\n return {\n read,\n setClasses,\n paintMeasure,\n paint,\n subscribe,\n destroy,\n }\n}\n","import { createStickyAsidePresenter } from '#core/presenter/presenter.ts'\nimport type { StickyAsideController, StickyAsideOptions } from '#core/types.ts'\nimport { createBrowserView } from '#core/view/browser-view.ts'\n\nexport function createStickyAside(options: StickyAsideOptions): StickyAsideController {\n return createStickyAsidePresenter(createBrowserView(options), options)\n}\n"],"mappings":"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"}
|
package/dist/offsets.css
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
//#region src/model/constants.ts
|
|
2
|
+
const MODE = {
|
|
3
|
+
relative: "relative",
|
|
4
|
+
fixedTop: "fixed-top",
|
|
5
|
+
fixedBottom: "fixed-bottom",
|
|
6
|
+
fixedLocked: "fixed-locked"
|
|
7
|
+
};
|
|
8
|
+
const REASON = {
|
|
9
|
+
scroll: "scroll",
|
|
10
|
+
resize: "resize",
|
|
11
|
+
lock: "lock",
|
|
12
|
+
unlock: "unlock"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/model/utils.ts
|
|
17
|
+
function getClearedLockModel(model) {
|
|
18
|
+
return {
|
|
19
|
+
...model,
|
|
20
|
+
locked: false,
|
|
21
|
+
lockedTop: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function getPinnedStickyModel(model, edge) {
|
|
25
|
+
return {
|
|
26
|
+
...model,
|
|
27
|
+
mode: edge === "top" ? MODE.fixedTop : MODE.fixedBottom,
|
|
28
|
+
locked: false
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function getLockedStickyModel({ model, frame }) {
|
|
32
|
+
const lockedTop = model.lockedTop ?? frame.content.top;
|
|
33
|
+
const isPastTop = !frame.allowLockedOverflowTop && frame.wrapper.top > lockedTop;
|
|
34
|
+
const isPastBottom = !frame.allowLockedOverflowBottom && lockedTop + frame.content.height > frame.wrapper.top + frame.wrapper.height;
|
|
35
|
+
if (isPastTop || isPastBottom) return getRelativeStickyModel({
|
|
36
|
+
model: {
|
|
37
|
+
...model,
|
|
38
|
+
locked: true,
|
|
39
|
+
lockedTop
|
|
40
|
+
},
|
|
41
|
+
frame
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
...model,
|
|
45
|
+
mode: MODE.fixedLocked,
|
|
46
|
+
locked: true,
|
|
47
|
+
lockedTop
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function getRelativeStickyModel({ model, frame }) {
|
|
51
|
+
let containerTop = Math.max(frame.content.top - frame.wrapper.top, 0);
|
|
52
|
+
if (!allowsBottomGap(frame)) containerTop = getClampedContainerTop(containerTop, frame);
|
|
53
|
+
return {
|
|
54
|
+
mode: MODE.relative,
|
|
55
|
+
containerTop,
|
|
56
|
+
locked: model.locked,
|
|
57
|
+
lockedTop: model.locked ? model.lockedTop : null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function getStickyModelAtEdges({ model, frame }) {
|
|
61
|
+
const isMeasureBelow = frame.measure.bottom > frame.wrapper.bottom;
|
|
62
|
+
const isAlreadyAtBottom = Math.round(frame.content.bottom) === Math.round(frame.wrapper.bottom);
|
|
63
|
+
const isAlreadyAtTop = frame.content.top === 0;
|
|
64
|
+
if (isMeasureBelow && isAlreadyAtBottom || !isMeasureBelow && isAlreadyAtTop) return model;
|
|
65
|
+
const containerTop = getClampedContainerTop(isMeasureBelow ? frame.wrapper.height : 0, frame);
|
|
66
|
+
return {
|
|
67
|
+
mode: MODE.relative,
|
|
68
|
+
containerTop,
|
|
69
|
+
locked: false,
|
|
70
|
+
lockedTop: null
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isFullyVisible(frame) {
|
|
74
|
+
return frame.measure.height >= frame.content.height;
|
|
75
|
+
}
|
|
76
|
+
function isCoveringTopOffset(frame) {
|
|
77
|
+
return frame.content.top >= 0 && frame.content.top < frame.measure.top;
|
|
78
|
+
}
|
|
79
|
+
function isMeasureInside(frame) {
|
|
80
|
+
return frame.measure.top >= frame.wrapper.top && frame.measure.bottom <= frame.wrapper.bottom;
|
|
81
|
+
}
|
|
82
|
+
function isOutsideViewport(frame) {
|
|
83
|
+
return frame.scrollY < 0 || frame.scrollY > frame.documentHeight - frame.viewportHeight;
|
|
84
|
+
}
|
|
85
|
+
function isFixed(model) {
|
|
86
|
+
return model.mode !== MODE.relative;
|
|
87
|
+
}
|
|
88
|
+
function isScrollingOpposite({ model, frame }) {
|
|
89
|
+
const isFixedTop = model.mode === MODE.fixedTop || model.mode === MODE.fixedLocked;
|
|
90
|
+
return frame.scrollingUp && !isFixedTop || !frame.scrollingUp && isFixedTop;
|
|
91
|
+
}
|
|
92
|
+
function allowsBottomGap(frame) {
|
|
93
|
+
return frame.reason === REASON.unlock && frame.allowLockedOverflowBottom;
|
|
94
|
+
}
|
|
95
|
+
function getClampedContainerTop(containerTop, frame) {
|
|
96
|
+
const bottomGap = frame.wrapper.height - (containerTop + frame.content.height);
|
|
97
|
+
let next = containerTop;
|
|
98
|
+
if (bottomGap < 0) next += bottomGap;
|
|
99
|
+
return next < 0 ? 0 : next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/model/step.ts
|
|
104
|
+
function getInitialStickyModel() {
|
|
105
|
+
return {
|
|
106
|
+
mode: MODE.relative,
|
|
107
|
+
containerTop: 0,
|
|
108
|
+
locked: false,
|
|
109
|
+
lockedTop: null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function getNewStickyModel(model, frame) {
|
|
113
|
+
if (frame.disabled) return model;
|
|
114
|
+
if (frame.reason === REASON.unlock) return getRelativeStickyModel({
|
|
115
|
+
model: getClearedLockModel(model),
|
|
116
|
+
frame
|
|
117
|
+
});
|
|
118
|
+
if (frame.reason === REASON.lock) return getLockedStickyModel({
|
|
119
|
+
model: {
|
|
120
|
+
...model,
|
|
121
|
+
locked: true,
|
|
122
|
+
lockedTop: frame.content.top
|
|
123
|
+
},
|
|
124
|
+
frame
|
|
125
|
+
});
|
|
126
|
+
if (frame.locked) return getLockedStickyModel({
|
|
127
|
+
model,
|
|
128
|
+
frame
|
|
129
|
+
});
|
|
130
|
+
if (frame.reason === REASON.resize) {
|
|
131
|
+
if (isMeasureInside(frame)) {
|
|
132
|
+
if (isFullyVisible(frame) || isCoveringTopOffset(frame)) return getPinnedStickyModel(model, "top");
|
|
133
|
+
return model;
|
|
134
|
+
}
|
|
135
|
+
return getRelativeStickyModel({
|
|
136
|
+
model: getClearedLockModel(model),
|
|
137
|
+
frame
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (isFixed(model)) return !isMeasureInside(frame) && !isOutsideViewport(frame) || !isFullyVisible(frame) && isScrollingOpposite({
|
|
141
|
+
model,
|
|
142
|
+
frame
|
|
143
|
+
}) ? getRelativeStickyModel({
|
|
144
|
+
model: getClearedLockModel(model),
|
|
145
|
+
frame
|
|
146
|
+
}) : model;
|
|
147
|
+
if (isMeasureInside(frame)) {
|
|
148
|
+
if (isFullyVisible(frame)) return getPinnedStickyModel(model, "top");
|
|
149
|
+
if (frame.scrollingUp && frame.content.top > frame.measure.top) return getPinnedStickyModel(model, "top");
|
|
150
|
+
if (!frame.scrollingUp && frame.content.bottom < frame.measure.bottom) return getPinnedStickyModel(model, "bottom");
|
|
151
|
+
return model;
|
|
152
|
+
}
|
|
153
|
+
return getStickyModelAtEdges({
|
|
154
|
+
model,
|
|
155
|
+
frame
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/presenter/constants.ts
|
|
161
|
+
const DEFAULTS = {
|
|
162
|
+
disabled: false,
|
|
163
|
+
locked: false,
|
|
164
|
+
allowLockedOverflowTop: false,
|
|
165
|
+
allowLockedOverflowBottom: false
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/presenter/utils.ts
|
|
170
|
+
function getSession(options) {
|
|
171
|
+
return {
|
|
172
|
+
disabled: options.disabled ?? DEFAULTS.disabled,
|
|
173
|
+
locked: options.locked ?? DEFAULTS.locked,
|
|
174
|
+
allowLockedOverflowTop: options.allowLockedOverflowTop ?? DEFAULTS.allowLockedOverflowTop,
|
|
175
|
+
allowLockedOverflowBottom: options.allowLockedOverflowBottom ?? DEFAULTS.allowLockedOverflowBottom
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function getStickyFrame(session, reading, isScrollingUp, reason) {
|
|
179
|
+
return {
|
|
180
|
+
disabled: session.disabled,
|
|
181
|
+
locked: session.locked,
|
|
182
|
+
allowLockedOverflowTop: session.allowLockedOverflowTop,
|
|
183
|
+
allowLockedOverflowBottom: session.allowLockedOverflowBottom,
|
|
184
|
+
scrollingUp: isScrollingUp,
|
|
185
|
+
scrollY: reading.metrics.scrollY,
|
|
186
|
+
viewportHeight: reading.metrics.viewportHeight,
|
|
187
|
+
documentHeight: reading.metrics.documentHeight,
|
|
188
|
+
wrapper: reading.wrapper,
|
|
189
|
+
content: reading.content,
|
|
190
|
+
measure: reading.measure,
|
|
191
|
+
reason
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function getStickyPaint(model, session, wrapperWidth) {
|
|
195
|
+
if (session.disabled) return { state: "off" };
|
|
196
|
+
if (model.mode === MODE.relative) return {
|
|
197
|
+
state: "flow",
|
|
198
|
+
top: model.containerTop
|
|
199
|
+
};
|
|
200
|
+
if (model.mode === MODE.fixedBottom) return {
|
|
201
|
+
state: "bottom",
|
|
202
|
+
width: wrapperWidth
|
|
203
|
+
};
|
|
204
|
+
if (model.mode === MODE.fixedLocked) return {
|
|
205
|
+
state: "locked",
|
|
206
|
+
top: model.lockedTop ?? 0,
|
|
207
|
+
width: wrapperWidth
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
state: "top",
|
|
211
|
+
width: wrapperWidth
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/presenter/presenter.ts
|
|
217
|
+
function createStickyAsidePresenter(view, options) {
|
|
218
|
+
const session = getSession(options);
|
|
219
|
+
let model = getInitialStickyModel();
|
|
220
|
+
let isScrollingUp = false;
|
|
221
|
+
let isStopped = false;
|
|
222
|
+
let painted;
|
|
223
|
+
let lastScrollY = view.read().metrics.scrollY;
|
|
224
|
+
function report(state) {
|
|
225
|
+
if (isStopped || state === painted) return;
|
|
226
|
+
painted = state;
|
|
227
|
+
options.onStateChange?.(state);
|
|
228
|
+
}
|
|
229
|
+
function apply(reason) {
|
|
230
|
+
if (isStopped) return;
|
|
231
|
+
const previousState = painted;
|
|
232
|
+
view.paintMeasure();
|
|
233
|
+
const reading = view.read();
|
|
234
|
+
model = getNewStickyModel(model, getStickyFrame(session, reading, isScrollingUp, reason));
|
|
235
|
+
const paint = getStickyPaint(model, session, reading.wrapper.width);
|
|
236
|
+
view.paint(paint);
|
|
237
|
+
report(paint.state);
|
|
238
|
+
if (reason === REASON.scroll && previousState !== void 0 && previousState !== "flow" && previousState !== "off" && paint.state === "flow") apply(REASON.scroll);
|
|
239
|
+
}
|
|
240
|
+
function onScroll() {
|
|
241
|
+
const scrollY = view.read().metrics.scrollY;
|
|
242
|
+
isScrollingUp = scrollY < lastScrollY;
|
|
243
|
+
lastScrollY = scrollY;
|
|
244
|
+
apply(REASON.scroll);
|
|
245
|
+
}
|
|
246
|
+
function onResize() {
|
|
247
|
+
apply(REASON.resize);
|
|
248
|
+
}
|
|
249
|
+
function update(reason = REASON.resize) {
|
|
250
|
+
apply(reason);
|
|
251
|
+
}
|
|
252
|
+
function setLocked(isLocked) {
|
|
253
|
+
if (isStopped || isLocked === session.locked) return;
|
|
254
|
+
session.locked = isLocked;
|
|
255
|
+
apply(isLocked ? REASON.lock : REASON.unlock);
|
|
256
|
+
}
|
|
257
|
+
function setDisabled(isDisabled) {
|
|
258
|
+
if (isStopped || isDisabled === session.disabled) return;
|
|
259
|
+
session.disabled = isDisabled;
|
|
260
|
+
apply(REASON.resize);
|
|
261
|
+
}
|
|
262
|
+
function setClasses(next) {
|
|
263
|
+
if (isStopped) return;
|
|
264
|
+
view.setClasses(next);
|
|
265
|
+
apply(REASON.resize);
|
|
266
|
+
}
|
|
267
|
+
function setAllowLockedOverflow(isAllowTop, isAllowBottom) {
|
|
268
|
+
if (isStopped) return;
|
|
269
|
+
session.allowLockedOverflowTop = isAllowTop;
|
|
270
|
+
session.allowLockedOverflowBottom = isAllowBottom;
|
|
271
|
+
apply(REASON.resize);
|
|
272
|
+
}
|
|
273
|
+
function getState() {
|
|
274
|
+
return painted ?? "off";
|
|
275
|
+
}
|
|
276
|
+
function getModel() {
|
|
277
|
+
return model;
|
|
278
|
+
}
|
|
279
|
+
const unsubscribe = view.subscribe({
|
|
280
|
+
scroll: onScroll,
|
|
281
|
+
resize: onResize
|
|
282
|
+
});
|
|
283
|
+
function destroy() {
|
|
284
|
+
if (isStopped) return;
|
|
285
|
+
isStopped = true;
|
|
286
|
+
unsubscribe();
|
|
287
|
+
view.destroy();
|
|
288
|
+
}
|
|
289
|
+
apply(session.locked ? REASON.lock : REASON.resize);
|
|
290
|
+
return {
|
|
291
|
+
update,
|
|
292
|
+
setLocked,
|
|
293
|
+
setDisabled,
|
|
294
|
+
setClasses,
|
|
295
|
+
setAllowLockedOverflow,
|
|
296
|
+
destroy,
|
|
297
|
+
getState,
|
|
298
|
+
getModel
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
export { getInitialStickyModel as n, getNewStickyModel as r, createStickyAsidePresenter as t };
|
|
304
|
+
//# sourceMappingURL=presenter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presenter.js","names":[],"sources":["../src/model/constants.ts","../src/model/utils.ts","../src/model/step.ts","../src/presenter/constants.ts","../src/presenter/utils.ts","../src/presenter/presenter.ts"],"sourcesContent":["export const MODE = {\n relative: 'relative',\n fixedTop: 'fixed-top',\n fixedBottom: 'fixed-bottom',\n fixedLocked: 'fixed-locked',\n} as const\n\nexport const REASON = {\n scroll: 'scroll',\n resize: 'resize',\n lock: 'lock',\n unlock: 'unlock',\n} as const\n","import { MODE, REASON } from '#core/model/constants.ts'\nimport type { StickyFrame, StickyModel } from '#core/model/types.ts'\n\nexport interface StickyStep {\n model: StickyModel\n frame: StickyFrame\n}\n\nexport function getClearedLockModel(model: StickyModel): StickyModel {\n return {\n ...model,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function getPinnedStickyModel(model: StickyModel, edge: 'top' | 'bottom'): StickyModel {\n return {\n ...model,\n mode: edge === 'top' ? MODE.fixedTop : MODE.fixedBottom,\n locked: false,\n }\n}\n\nexport function getLockedStickyModel({ model, frame }: StickyStep): StickyModel {\n const lockedTop = model.lockedTop ?? frame.content.top\n const isPastTop = !frame.allowLockedOverflowTop && frame.wrapper.top > lockedTop\n const isPastBottom = !frame.allowLockedOverflowBottom &&\n lockedTop + frame.content.height > frame.wrapper.top + frame.wrapper.height\n\n if (isPastTop || isPastBottom) {\n return getRelativeStickyModel({\n model: {\n ...model,\n locked: true,\n lockedTop,\n },\n frame,\n })\n }\n\n return {\n ...model,\n mode: MODE.fixedLocked,\n locked: true,\n lockedTop,\n }\n}\n\nexport function getRelativeStickyModel({ model, frame }: StickyStep): StickyModel {\n let containerTop = Math.max(frame.content.top - frame.wrapper.top, 0)\n if (!allowsBottomGap(frame)) {\n containerTop = getClampedContainerTop(containerTop, frame)\n }\n\n return {\n mode: MODE.relative,\n containerTop,\n locked: model.locked,\n lockedTop: model.locked ? model.lockedTop : null,\n }\n}\n\nexport function getStickyModelAtEdges({ model, frame }: StickyStep): StickyModel {\n const isMeasureBelow = frame.measure.bottom > frame.wrapper.bottom\n const isAlreadyAtBottom = Math.round(frame.content.bottom) === Math.round(frame.wrapper.bottom)\n const isAlreadyAtTop = frame.content.top === 0\n\n if ((isMeasureBelow && isAlreadyAtBottom) || (!isMeasureBelow && isAlreadyAtTop)) {\n return model\n }\n\n const containerTop = getClampedContainerTop(isMeasureBelow ? frame.wrapper.height : 0, frame)\n\n return {\n mode: MODE.relative,\n containerTop,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function isFullyVisible(frame: StickyFrame): boolean {\n return frame.measure.height >= frame.content.height\n}\n\nexport function isCoveringTopOffset(frame: StickyFrame): boolean {\n return frame.content.top >= 0 && frame.content.top < frame.measure.top\n}\n\nexport function isMeasureInside(frame: StickyFrame): boolean {\n return frame.measure.top >= frame.wrapper.top && frame.measure.bottom <= frame.wrapper.bottom\n}\n\nexport function isOutsideViewport(frame: StickyFrame): boolean {\n return frame.scrollY < 0 || frame.scrollY > frame.documentHeight - frame.viewportHeight\n}\n\nexport function isFixed(model: StickyModel): boolean {\n return model.mode !== MODE.relative\n}\n\nexport function isScrollingOpposite({ model, frame }: StickyStep): boolean {\n const isFixedTop = model.mode === MODE.fixedTop || model.mode === MODE.fixedLocked\n\n return (frame.scrollingUp && !isFixedTop) || (!frame.scrollingUp && isFixedTop)\n}\n\nfunction allowsBottomGap(frame: StickyFrame): boolean {\n return frame.reason === REASON.unlock && frame.allowLockedOverflowBottom\n}\n\nfunction getClampedContainerTop(containerTop: number, frame: StickyFrame): number {\n const bottomGap = frame.wrapper.height - (containerTop + frame.content.height)\n let next = containerTop\n if (bottomGap < 0) {\n next += bottomGap\n }\n\n return next < 0 ? 0 : next\n}\n","import { MODE, REASON } from '#core/model/constants.ts'\nimport type { Box, StickyFrame, StickyModel } from '#core/model/types.ts'\nimport {\n getClearedLockModel,\n getLockedStickyModel,\n getPinnedStickyModel,\n getRelativeStickyModel,\n getStickyModelAtEdges,\n isCoveringTopOffset,\n isFixed,\n isFullyVisible,\n isMeasureInside,\n isOutsideViewport,\n isScrollingOpposite,\n} from '#core/model/utils.ts'\n\nexport type { Box, StickyFrame, StickyModel }\n\nexport function getInitialStickyModel(): StickyModel {\n return {\n mode: MODE.relative,\n containerTop: 0,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function getNewStickyModel(model: StickyModel, frame: StickyFrame): StickyModel {\n if (frame.disabled) {\n return model\n }\n\n if (frame.reason === REASON.unlock) {\n return getRelativeStickyModel({ model: getClearedLockModel(model), frame })\n }\n\n if (frame.reason === REASON.lock) {\n return getLockedStickyModel({\n model: {\n ...model,\n locked: true,\n lockedTop: frame.content.top,\n },\n frame,\n })\n }\n\n if (frame.locked) {\n return getLockedStickyModel({ model, frame })\n }\n\n if (frame.reason === REASON.resize) {\n if (isMeasureInside(frame)) {\n if (isFullyVisible(frame) || isCoveringTopOffset(frame)) {\n return getPinnedStickyModel(model, 'top')\n }\n\n return model\n }\n\n return getRelativeStickyModel({ model: getClearedLockModel(model), frame })\n }\n\n if (isFixed(model)) {\n const isReleasing =\n (!isMeasureInside(frame) && !isOutsideViewport(frame)) ||\n (!isFullyVisible(frame) && isScrollingOpposite({ model, frame }))\n\n return isReleasing ? getRelativeStickyModel({ model: getClearedLockModel(model), frame }) : model\n }\n\n if (isMeasureInside(frame)) {\n if (isFullyVisible(frame)) {\n return getPinnedStickyModel(model, 'top')\n }\n\n if (frame.scrollingUp && frame.content.top > frame.measure.top) {\n return getPinnedStickyModel(model, 'top')\n }\n\n if (!frame.scrollingUp && frame.content.bottom < frame.measure.bottom) {\n return getPinnedStickyModel(model, 'bottom')\n }\n\n return model\n }\n\n return getStickyModelAtEdges({ model, frame })\n}\n","export const DEFAULTS = {\n disabled: false,\n locked: false,\n allowLockedOverflowTop: false,\n allowLockedOverflowBottom: false,\n} as const\n","import { MODE } from '#core/model/constants.ts'\nimport type { StickyFrame, StickyModel, StickyReason } from '#core/model/types.ts'\nimport { DEFAULTS } from '#core/presenter/constants.ts'\nimport type { Session } from '#core/presenter/types.ts'\nimport type { StickyAsidePresenterOptions, StickyPaint, StickyReading } from '#core/types.ts'\n\nexport function getSession(options: StickyAsidePresenterOptions): Session {\n return {\n disabled: options.disabled ?? DEFAULTS.disabled,\n locked: options.locked ?? DEFAULTS.locked,\n allowLockedOverflowTop: options.allowLockedOverflowTop ?? DEFAULTS.allowLockedOverflowTop,\n allowLockedOverflowBottom: options.allowLockedOverflowBottom ?? DEFAULTS.allowLockedOverflowBottom,\n }\n}\n\nexport function getStickyFrame(\n session: Session,\n reading: StickyReading,\n isScrollingUp: boolean,\n reason: StickyReason,\n): StickyFrame {\n return {\n disabled: session.disabled,\n locked: session.locked,\n allowLockedOverflowTop: session.allowLockedOverflowTop,\n allowLockedOverflowBottom: session.allowLockedOverflowBottom,\n scrollingUp: isScrollingUp,\n scrollY: reading.metrics.scrollY,\n viewportHeight: reading.metrics.viewportHeight,\n documentHeight: reading.metrics.documentHeight,\n wrapper: reading.wrapper,\n content: reading.content,\n measure: reading.measure,\n reason,\n }\n}\n\nexport function getStickyPaint(model: StickyModel, session: Session, wrapperWidth: number): StickyPaint {\n if (session.disabled) {\n return {\n state: 'off',\n }\n }\n\n if (model.mode === MODE.relative) {\n return {\n state: 'flow',\n top: model.containerTop,\n }\n }\n\n if (model.mode === MODE.fixedBottom) {\n return {\n state: 'bottom',\n width: wrapperWidth,\n }\n }\n\n if (model.mode === MODE.fixedLocked) {\n return {\n state: 'locked',\n top: model.lockedTop ?? 0,\n width: wrapperWidth,\n }\n }\n\n return {\n state: 'top',\n width: wrapperWidth,\n }\n}\n","import { REASON } from '#core/model/constants.ts'\nimport { getInitialStickyModel, getNewStickyModel } from '#core/model/step.ts'\nimport type { StickyReason } from '#core/model/types.ts'\nimport { getSession, getStickyFrame, getStickyPaint } from '#core/presenter/utils.ts'\nimport type {\n StickyAsideClasses,\n StickyAsideController,\n StickyAsidePresenterOptions,\n StickyAsideState,\n StickyAsideView,\n} from '#core/types.ts'\n\nexport function createStickyAsidePresenter(\n view: StickyAsideView,\n options: StickyAsidePresenterOptions,\n): StickyAsideController {\n const session = getSession(options)\n let model = getInitialStickyModel()\n let isScrollingUp = false\n let isStopped = false\n let painted: StickyAsideState | undefined\n let lastScrollY = view.read().metrics.scrollY\n\n function report(state: StickyAsideState) {\n if (isStopped || state === painted) {\n return\n }\n\n painted = state\n options.onStateChange?.(state)\n }\n\n function apply(reason: StickyReason) {\n if (isStopped) {\n return\n }\n\n const previousState = painted\n view.paintMeasure()\n const reading = view.read()\n model = getNewStickyModel(model, getStickyFrame(session, reading, isScrollingUp, reason))\n const paint = getStickyPaint(model, session, reading.wrapper.width)\n view.paint(paint)\n report(paint.state)\n\n const didReleaseToFlow = reason === REASON.scroll &&\n previousState !== undefined &&\n previousState !== 'flow' &&\n previousState !== 'off' &&\n paint.state === 'flow'\n if (didReleaseToFlow) {\n apply(REASON.scroll)\n }\n }\n\n function onScroll() {\n const scrollY = view.read().metrics.scrollY\n isScrollingUp = scrollY < lastScrollY\n lastScrollY = scrollY\n apply(REASON.scroll)\n }\n\n function onResize() {\n apply(REASON.resize)\n }\n\n function update(reason: Extract<StickyReason, 'scroll' | 'resize'> = REASON.resize) {\n apply(reason)\n }\n\n function setLocked(isLocked: boolean) {\n if (isStopped || isLocked === session.locked) {\n return\n }\n\n session.locked = isLocked\n apply(isLocked ? REASON.lock : REASON.unlock)\n }\n\n function setDisabled(isDisabled: boolean) {\n if (isStopped || isDisabled === session.disabled) {\n return\n }\n\n session.disabled = isDisabled\n apply(REASON.resize)\n }\n\n function setClasses(next: StickyAsideClasses) {\n if (isStopped) {\n return\n }\n\n view.setClasses(next)\n apply(REASON.resize)\n }\n\n function setAllowLockedOverflow(isAllowTop: boolean, isAllowBottom: boolean) {\n if (isStopped) {\n return\n }\n\n session.allowLockedOverflowTop = isAllowTop\n session.allowLockedOverflowBottom = isAllowBottom\n apply(REASON.resize)\n }\n\n function getState() {\n return painted ?? 'off'\n }\n\n function getModel() {\n return model\n }\n\n const unsubscribe = view.subscribe({\n scroll: onScroll,\n resize: onResize,\n })\n\n function destroy() {\n if (isStopped) {\n return\n }\n\n isStopped = true\n unsubscribe()\n view.destroy()\n }\n\n apply(session.locked ? REASON.lock : REASON.resize)\n\n return {\n update,\n setLocked,\n setDisabled,\n setClasses,\n setAllowLockedOverflow,\n destroy,\n getState,\n getModel,\n }\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//#region src/model/constants.d.ts
|
|
2
|
+
declare const MODE: {
|
|
3
|
+
readonly relative: "relative";
|
|
4
|
+
readonly fixedTop: "fixed-top";
|
|
5
|
+
readonly fixedBottom: "fixed-bottom";
|
|
6
|
+
readonly fixedLocked: "fixed-locked";
|
|
7
|
+
};
|
|
8
|
+
declare const REASON: {
|
|
9
|
+
readonly scroll: "scroll";
|
|
10
|
+
readonly resize: "resize";
|
|
11
|
+
readonly lock: "lock";
|
|
12
|
+
readonly unlock: "unlock";
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/model/types.d.ts
|
|
16
|
+
type StickyMode = (typeof MODE)[keyof typeof MODE];
|
|
17
|
+
type StickyReason = (typeof REASON)[keyof typeof REASON];
|
|
18
|
+
interface Box {
|
|
19
|
+
top: number;
|
|
20
|
+
bottom: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
interface StickyModel {
|
|
25
|
+
mode: StickyMode;
|
|
26
|
+
containerTop: number;
|
|
27
|
+
locked: boolean;
|
|
28
|
+
lockedTop: number | null;
|
|
29
|
+
}
|
|
30
|
+
interface StickyFrame {
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
locked: boolean;
|
|
33
|
+
allowLockedOverflowTop: boolean;
|
|
34
|
+
allowLockedOverflowBottom: boolean;
|
|
35
|
+
scrollingUp: boolean;
|
|
36
|
+
scrollY: number;
|
|
37
|
+
viewportHeight: number;
|
|
38
|
+
documentHeight: number;
|
|
39
|
+
wrapper: Box;
|
|
40
|
+
content: Box;
|
|
41
|
+
measure: Box;
|
|
42
|
+
reason: StickyReason;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/types.d.ts
|
|
46
|
+
interface ScrollMetrics {
|
|
47
|
+
scrollY: number;
|
|
48
|
+
viewportHeight: number;
|
|
49
|
+
documentHeight: number;
|
|
50
|
+
}
|
|
51
|
+
type StickyAsideState = 'flow' | 'top' | 'bottom' | 'locked' | 'off';
|
|
52
|
+
interface StickyAsidePresenterOptions {
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
locked?: boolean;
|
|
55
|
+
allowLockedOverflowTop?: boolean;
|
|
56
|
+
allowLockedOverflowBottom?: boolean;
|
|
57
|
+
onStateChange?: (state: StickyAsideState) => void;
|
|
58
|
+
}
|
|
59
|
+
interface StickyAsideClasses {
|
|
60
|
+
topClass?: string;
|
|
61
|
+
bottomClass?: string;
|
|
62
|
+
contentClass?: string;
|
|
63
|
+
}
|
|
64
|
+
interface StickyAsideOptions extends StickyAsidePresenterOptions {
|
|
65
|
+
wrapper: HTMLElement;
|
|
66
|
+
content: HTMLElement;
|
|
67
|
+
topClass?: string;
|
|
68
|
+
bottomClass?: string;
|
|
69
|
+
contentClass?: string;
|
|
70
|
+
getScrollMetrics?: () => ScrollMetrics;
|
|
71
|
+
readRect?: (element: HTMLElement) => Box;
|
|
72
|
+
}
|
|
73
|
+
interface StickyReading {
|
|
74
|
+
metrics: ScrollMetrics;
|
|
75
|
+
wrapper: Box;
|
|
76
|
+
content: Box;
|
|
77
|
+
measure: Box;
|
|
78
|
+
}
|
|
79
|
+
interface StickyPaintOff {
|
|
80
|
+
state: 'off';
|
|
81
|
+
}
|
|
82
|
+
interface StickyPaintFlow {
|
|
83
|
+
state: 'flow';
|
|
84
|
+
top: number;
|
|
85
|
+
}
|
|
86
|
+
interface StickyPaintTop {
|
|
87
|
+
state: 'top';
|
|
88
|
+
width: number;
|
|
89
|
+
}
|
|
90
|
+
interface StickyPaintBottom {
|
|
91
|
+
state: 'bottom';
|
|
92
|
+
width: number;
|
|
93
|
+
}
|
|
94
|
+
interface StickyPaintLocked {
|
|
95
|
+
state: 'locked';
|
|
96
|
+
top: number;
|
|
97
|
+
width: number;
|
|
98
|
+
}
|
|
99
|
+
type StickyPaint = StickyPaintOff | StickyPaintFlow | StickyPaintTop | StickyPaintBottom | StickyPaintLocked;
|
|
100
|
+
interface StickyAsideHandlers {
|
|
101
|
+
scroll(): void;
|
|
102
|
+
resize(): void;
|
|
103
|
+
}
|
|
104
|
+
interface StickyAsideView {
|
|
105
|
+
read(): StickyReading;
|
|
106
|
+
setClasses(classes: StickyAsideClasses): void;
|
|
107
|
+
paintMeasure(): void;
|
|
108
|
+
paint(paint: StickyPaint): void;
|
|
109
|
+
subscribe(handlers: StickyAsideHandlers): () => void;
|
|
110
|
+
destroy(): void;
|
|
111
|
+
}
|
|
112
|
+
interface StickyAsideController {
|
|
113
|
+
update: (reason?: Extract<StickyReason, 'scroll' | 'resize'>) => void;
|
|
114
|
+
setLocked: (isLocked: boolean) => void;
|
|
115
|
+
setDisabled: (isDisabled: boolean) => void;
|
|
116
|
+
setClasses: (classes: StickyAsideClasses) => void;
|
|
117
|
+
setAllowLockedOverflow: (isAllowTop: boolean, isAllowBottom: boolean) => void;
|
|
118
|
+
destroy: () => void;
|
|
119
|
+
getState: () => StickyAsideState;
|
|
120
|
+
getModel: () => StickyModel;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { StickyFrame as _, StickyAsideOptions as a, StickyReason as b, StickyAsideView as c, StickyPaintFlow as d, StickyPaintLocked as f, Box as g, StickyReading as h, StickyAsideHandlers as i, StickyPaint as l, StickyPaintTop as m, StickyAsideClasses as n, StickyAsidePresenterOptions as o, StickyPaintOff as p, StickyAsideController as r, StickyAsideState as s, ScrollMetrics as t, StickyPaintBottom as u, StickyMode as v, StickyModel as y };
|
|
124
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sticky-aside/core",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A panel that follows page scroll and pins to the edge you are scrolling toward.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sticky",
|
|
7
|
+
"sidebar",
|
|
8
|
+
"aside",
|
|
9
|
+
"scroll"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "maprox",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/maprox/sticky-aside.git",
|
|
16
|
+
"directory": "packages/core"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/maprox/sticky-aside#readme",
|
|
19
|
+
"bugs": "https://github.com/maprox/sticky-aside/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"*.css"
|
|
23
|
+
],
|
|
24
|
+
"imports": {
|
|
25
|
+
"#core/*": "./src/*"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./headless": {
|
|
33
|
+
"types": "./dist/headless.d.ts",
|
|
34
|
+
"import": "./dist/headless.js"
|
|
35
|
+
},
|
|
36
|
+
"./offsets.css": "./dist/offsets.css"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown --config tsdown.config.mjs"
|
|
43
|
+
}
|
|
6
44
|
}
|