@@ -240,12 +239,7 @@ ${cell.color ? `color:${cell.color};` : ""}">${text}
`;
if (left) {
e.querySelector(".av__scroll").scrollLeft = left;
}
- if (headerTransform) {
- (e.querySelector(".av__row--header") as HTMLElement).style.transform = headerTransform;
- }
- if (footerTransform) {
- (e.querySelector(".av__row--footer") as HTMLElement).style.transform = footerTransform;
- }
+ avScroll(protyle.contentElement, e);
if (selectCellId) {
const newCellElement = e.querySelector(`.av__row[data-id="${selectCellId.split(Constants.ZWSP)[0]}"] .av__cell[data-col-id="${selectCellId.split(Constants.ZWSP)[1]}"]`);
if (newCellElement) {
diff --git a/app/src/protyle/render/av/scroll.ts b/app/src/protyle/render/av/scroll.ts
new file mode 100644
index 000000000..ca8a6e04b
--- /dev/null
+++ b/app/src/protyle/render/av/scroll.ts
@@ -0,0 +1,20 @@
+import {stickyScrollY} from "../../scroll/stickyScroll";
+
+export const avScroll = (
+ contentElement: HTMLElement,
+ nodeElement: HTMLElement,
+) => {
+ const bodyElement = nodeElement.querySelector(".av__body") as HTMLElement;
+
+ if (bodyElement) {
+ const headerElement = bodyElement.querySelector(".av__row--header") as HTMLElement;
+ const footerElement = bodyElement.querySelector(".av__row--footer") as HTMLElement;
+
+ stickyScrollY(
+ contentElement,
+ bodyElement,
+ headerElement ? [{element: headerElement}] : [],
+ footerElement ? [{element: footerElement}] : [],
+ );
+ }
+}
diff --git a/app/src/protyle/scroll/event.ts b/app/src/protyle/scroll/event.ts
index fb33c0836..7cd449cd9 100644
--- a/app/src/protyle/scroll/event.ts
+++ b/app/src/protyle/scroll/event.ts
@@ -4,6 +4,7 @@ import {fetchPost} from "../../util/fetch";
import {onGet} from "../util/onGet";
import {isMobile} from "../../util/functions";
import {hasClosestBlock, hasClosestByClassName} from "../util/hasClosest";
+import {avScroll} from "../render/av/scroll";
let getIndexTimeout: number;
export const scrollEvent = (protyle: IProtyle, element: HTMLElement) => {
@@ -21,32 +22,7 @@ export const scrollEvent = (protyle: IProtyle, element: HTMLElement) => {
}
protyle.wysiwyg.element.querySelectorAll(".av").forEach((item: HTMLElement) => {
- if (item.dataset.render !== "true") {
- return;
- }
- const scrollRect = item.querySelector(".av__scroll").getBoundingClientRect();
- const headerElement = item.querySelector(".av__row--header") as HTMLElement;
- if (headerElement) {
- const distance = Math.floor(elementRect.top - scrollRect.top);
- if (distance > 0 && distance < scrollRect.height) {
- headerElement.style.transform = `translateY(${distance}px)`;
- } else {
- headerElement.style.transform = "";
- }
- }
- const footerElement = item.querySelector(".av__row--footer") as HTMLElement;
- if (footerElement) {
- if (footerElement.querySelector(".av__calc--ashow")) {
- const distance = Math.floor(elementRect.bottom - footerElement.parentElement.getBoundingClientRect().bottom);
- if (distance < 0 && -distance < scrollRect.height) {
- footerElement.style.transform = `translateY(${distance}px)`;
- } else {
- footerElement.style.transform = "";
- }
- } else {
- footerElement.style.transform = "";
- }
- }
+ avScroll(element, item);
});
if (!protyle.element.classList.contains("block__edit") && !isMobile()) {
diff --git a/app/src/protyle/scroll/stickyScroll.ts b/app/src/protyle/scroll/stickyScroll.ts
new file mode 100644
index 000000000..3acc628b1
--- /dev/null
+++ b/app/src/protyle/scroll/stickyScroll.ts
@@ -0,0 +1,276 @@
+export interface IStickyPositionY {
+ top: number;
+ bottom: number;
+}
+
+export interface IStickyElementY {
+ element: HTMLElement;
+ offset?: number;
+}
+
+export interface IStickyContextY extends IStickyElementY {
+ rect: DOMRect;
+ base: number;
+ origin: IStickyPositionY;
+ current: IStickyPositionY;
+ target: IStickyPositionY;
+ style: IStickyPositionY;
+}
+
+export const stickyScrollY = (
+ view: HTMLElement, // 视口元素
+ container: HTMLElement, // 容器元素
+ topElements: IStickyElementY[] = [], // 顶部粘性元素
+ bottomElements: IStickyElementY[] = [], // 底部粘性元素
+) => {
+ if (topElements.length === 0 && bottomElements.length === 0) {
+ return;
+ }
+
+ const viewRect = view.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+
+ /**
+ * ┏---------------┓
+ * | view |
+ * ┗---------------┛ → viewRect.bottom
+ * ┌-----------┐ --→ containerRect.top
+ * | container |
+ * └-----------┘
+ * ====== OR ======
+ * ┌-----------┐
+ * | container |
+ * └-----------┘ --→ containerRect.bottom
+ * ┏---------------┓ → viewRect.top
+ * | view |
+ * ┗---------------┛
+ */
+ if (viewRect.bottom <= containerRect.top || containerRect.bottom <= viewRect.top) {
+ return;
+ }
+
+ const topContext: IStickyContextY[] = topElements.map(item => {
+ const rect = item.element.getBoundingClientRect();
+ const base = Number.parseFloat(item.element.style.top) || 0;
+ item.offset ??= 0;
+
+ return {
+ ...item,
+ rect,
+ base,
+ origin: {
+ top: rect.top - base,
+ bottom: rect.bottom - base,
+ },
+ current: {
+ top: rect.top,
+ bottom: rect.bottom,
+ },
+ target: {
+ top: null,
+ bottom: null,
+ },
+ style: {
+ top: null,
+ bottom: null,
+ }
+ };
+ });
+ const bottomContext: IStickyContextY[] = bottomElements.map(item => {
+ const rect = item.element.getBoundingClientRect();
+ const base = Number.parseFloat(item.element.style.bottom) || 0;
+ item.offset ??= 0;
+
+ return {
+ ...item,
+ rect,
+ base,
+ origin: {
+ top: rect.top + base,
+ bottom: rect.bottom + base,
+ },
+ current: {
+ top: rect.top,
+ bottom: rect.bottom,
+ },
+ target: {
+ top: null,
+ bottom: null,
+ },
+ style: {
+ top: null,
+ bottom: null,
+ }
+ };
+ });
+
+ let handleTop = false;
+ let handleBottom = false;
+
+ switch (true) {
+ /**
+ * ┏---------------┓ → viewRect.top
+ * | |
+ * | view |
+ * | ┌-----------┐ | → containerRect.top
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * | container |
+ * └-----------┘ --→ containerRect.bottom
+ */
+ case viewRect.top <= containerRect.top
+ && containerRect.top <= viewRect.bottom
+ && viewRect.top <= viewRect.bottom:
+ handleBottom = true;
+ break;
+
+ /**
+ * ┏---------------┓ → viewRect.top
+ * | view |
+ * | ┌-----------┐ | → containerRect.top
+ * | | container | |
+ * | └-----------┘ | → containerRect.bottom
+ * ┗---------------┛ → viewRect.bottom
+ */
+ case viewRect.top <= containerRect.top
+ && containerRect.bottom <= viewRect.bottom:
+ break;
+
+
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | | | |}→ view
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * | container |
+ * └-----------┘ --→ containerRect.bottom
+ */
+ case containerRect.top <= viewRect.top
+ && viewRect.bottom <= containerRect.bottom:
+ handleTop = true;
+ handleBottom = true;
+ break;
+
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * | container |
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | └-----------┘ | → containerRect.bottom
+ * | view |
+ * ┗-|-----------|-┛ → viewRect.bottom
+ */
+ case containerRect.top <= viewRect.top
+ && viewRect.top <= containerRect.bottom
+ && containerRect.bottom <= viewRect.bottom:
+ handleTop = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (handleTop) {
+ if (topContext.length > 0) {
+ topContext.reduceRight((next, current) => {
+ switch (true) {
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | | ┌┄┄┄┄┄┄┄┐ | | → current.target.top - current.offset
+ * | | ├╌╌╌╌╌╌╌┤ | | → current.origin.top
+ * | | └╌╌╌╌╌╌╌┘ | | → current.origin.bottom
+ * | | ┌╌╌╌╌╌╌╌┐ | | → next.origin.top
+ * | | └╌╌╌╌╌╌╌┘ | | → next.origin.bottom
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * └-----------┘ --→ containerRect.bottom
+ */
+ case viewRect.top <= (current.origin.top - current.offset):
+ current.target.top = current.origin.top;
+ current.target.bottom = current.origin.bottom;
+ current.style.top = null;
+ break;
+
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * | ┌╌╌╌╌╌╌╌┐ | --→ current.origin.top
+ * | └╌╌╌╌╌╌╌┘ | --→ current.origin.bottom
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | | ┌-------┐ | | → current.target.top
+ * | | └-------┘ | | → current.target.bottom
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * | container |
+ * └-----------┘ --→ containerRect.bottom
+ */
+ default:
+ current.target.top = viewRect.top + current.offset;
+ current.target.bottom = current.target.top + current.rect.height;
+ const nextTop = next
+ ? Math.min(next.target.top, next.origin.top, containerRect.bottom)
+ : containerRect.bottom;
+ if (nextTop < current.target.bottom) {
+ const diff = nextTop - current.target.bottom;
+ current.target.top += diff;
+ current.target.bottom += diff;
+ }
+ current.style.top = current.base + (current.target.top - current.current.top);
+ break;
+ }
+ return current;
+ }, null);
+ }
+ }
+ if (handleBottom) {
+ if (bottomContext.length > 0) {
+ bottomContext.reduce((last, current) => {
+ switch (true) {
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | | ┌╌╌╌╌╌╌╌┐ | | → last.origin.top
+ * | | └╌╌╌╌╌╌╌┘ | | → last.origin.bottom
+ * | | ┌╌╌╌╌╌╌╌┐ | | → current.origin.top
+ * | | ├╌╌╌╌╌╌╌┤ | | → current.origin.bottom
+ * | | └┄┄┄┄┄┄┄┘ | | → current.target.bottom + current.offset
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * └-----------┘ --→ containerRect.bottom
+ */
+ case (current.origin.bottom + current.offset) <= viewRect.bottom:
+ current.target.top = current.origin.top;
+ current.target.bottom = current.origin.bottom;
+ current.style.bottom = null;
+ break;
+
+ /**
+ * ┌-----------┐ --→ containerRect.top
+ * ┏-╊-----------╊-┓ → viewRect.top
+ * | | ┌-------┐ | | → current.target.top
+ * | | └-------┘ | | → current.target.bottom
+ * ┗-╊-----------╊-┛ → viewRect.bottom
+ * | ┌╌╌╌╌╌╌╌┐ | --→ current.origin.top
+ * | └╌╌╌╌╌╌╌┘ | --→ current.origin.bottom
+ * | container |
+ * └-----------┘ --→ containerRect.bottom
+ */
+ default:
+ current.target.bottom = viewRect.bottom - current.offset;
+ current.target.top = current.target.bottom - current.rect.height;
+ const lastBottom = last
+ ? Math.max(last.target.bottom, last.origin.bottom, containerRect.top)
+ : containerRect.top;
+ if (current.target.top < lastBottom) {
+ const diff = lastBottom - current.target.top;
+ current.target.top += diff;
+ current.target.bottom += diff;
+ }
+ current.style.bottom = current.base - (current.target.bottom - current.current.bottom);
+ break;
+ }
+ return current;
+ }, null);
+ }
+ }
+
+ [...topContext, ...bottomContext].forEach(item => {
+ item.element.style.top = item.style.top ? `${item.style.top}px` : null;
+ item.element.style.bottom = item.style.bottom ? `${item.style.bottom}px` : null;
+ });
+}
diff --git a/app/src/protyle/wysiwyg/index.ts b/app/src/protyle/wysiwyg/index.ts
index 32cfbca26..11fe36fdb 100644
--- a/app/src/protyle/wysiwyg/index.ts
+++ b/app/src/protyle/wysiwyg/index.ts
@@ -77,6 +77,7 @@ import {activeBlur, hideKeyboardToolbar} from "../../mobile/util/keyboardToolbar
import {commonClick} from "./commonClick";
import {avClick, avContextmenu, updateAVName} from "../render/av/action";
import {updateHeader} from "../render/av/row";
+import {avScroll} from "../render/av/scroll";
export class WYSIWYG {
public lastHTMLs: { [key: string]: string } = {};
@@ -389,6 +390,7 @@ export class WYSIWYG {
scrollElement.querySelectorAll(".av__row, .av__row--footer").forEach(item => {
(item.querySelector(`[data-col-id="${dragColId}"]`) as HTMLElement).style.width = newWidth;
});
+ avScroll(protyle.contentElement, nodeElement);
}
};