diff --git a/app/src/assets/scss/business/_av.scss b/app/src/assets/scss/business/_av.scss index 2d5ac225f..c640ee12d 100644 --- a/app/src/assets/scss/business/_av.scss +++ b/app/src/assets/scss/business/_av.scss @@ -140,10 +140,12 @@ &--header, &--footer { background-color: var(--av-background); + position: relative; + z-index: 2; } &--footer { - display: flex; + display: inline-flex; border-top: 1px solid var(--b3-theme-surface-lighter); color: var(--b3-theme-on-surface); position: relative; diff --git a/app/src/protyle/render/av/render.ts b/app/src/protyle/render/av/render.ts index 763470b93..0ec4d539e 100644 --- a/app/src/protyle/render/av/render.ts +++ b/app/src/protyle/render/av/render.ts @@ -7,6 +7,7 @@ import {unicode2Emoji} from "../../../emoji"; import {focusBlock} from "../../util/selection"; import {isMac} from "../../util/compatibility"; import {hasClosestByClassName} from "../../util/hasClosest"; +import {avScroll} from "./scroll"; export const avRender = (element: Element, protyle: IProtyle, cb?: () => void) => { let avElements: Element[] = []; @@ -41,8 +42,6 @@ export const avRender = (element: Element, protyle: IProtyle, cb?: () => void) = e.firstElementChild.innerHTML = html; } const left = e.querySelector(".av__scroll")?.scrollLeft || 0; - const headerTransform = (e.querySelector(".av__row--header") as HTMLElement)?.style.transform; - const footerTransform = (e.querySelector(".av__row--footer") as HTMLElement)?.style.transform; let selectCellId = ""; const selectCellElement = e.querySelector(".av__cell--select") as HTMLElement; if (selectCellElement) { @@ -222,7 +221,7 @@ ${cell.color ? `color:${cell.color};` : ""}">${text}`;
-
+
${tableHTML}
@@ -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); } };