diff --git a/packages/opendesign/src/_utils/dom.ts b/packages/opendesign/src/_utils/dom.ts index b070815d4ec109ced331fe1f441d9b1db2fe0294..dbca6473ee2ef60b4fdfd33734519eaceac050df 100644 --- a/packages/opendesign/src/_utils/dom.ts +++ b/packages/opendesign/src/_utils/dom.ts @@ -1,15 +1,20 @@ -import { isArray } from './is'; +import { easeInOutCubic } from './easing'; +import { throttleRAF } from './helper'; +import { isArray, isWindow } from './is'; import { PositionT } from './types'; +export type ScrollTarget = HTMLElement | Window | Document; + +export function isDocument(val: unknown): val is Document { + return val instanceof Document || val?.constructor.name === 'HTMLDocument'; +} + export function isHtmlElement(el: any) { return typeof HTMLElement === 'object' ? el instanceof HTMLElement : !!(el && typeof el === 'object' && (el.nodeType === 1 || el.nodeType === 9) && typeof el.nodeName === 'string'); } -export function isDocumentElement(el: HTMLElement | Window) { - return el === window || ['HTML'].includes((el as HTMLElement).tagName); -} // 获取真实相对父元素 当body没有设置position时,返回html export function getOffsetElement(el: HTMLElement) { let offsetEl = el.offsetParent; @@ -21,20 +26,32 @@ export function getOffsetElement(el: HTMLElement) { } return offsetEl; } + // 获取元素scroll值 -export function getScroll(el: HTMLElement | Window = window) { +export function getScroll(el: ScrollTarget) { + const rlt = { + scrollLeft: 0, + scrollTop: 0, + }; + if (!el) { - return { - scrollLeft: 0, - scrollTop: 0, - }; + return rlt; } - const isroot = isDocumentElement(el); - return { - scrollLeft: isroot ? window.scrollX : (el as HTMLElement).scrollLeft, - scrollTop: isroot ? window.scrollY : (el as HTMLElement).scrollTop, - }; + + if (isWindow(el)) { + rlt.scrollLeft = window.scrollX; + rlt.scrollTop = window.scrollY; + } else if (isDocument(el)) { + rlt.scrollLeft = el.documentElement.scrollLeft; + rlt.scrollTop = el.documentElement.scrollTop; + } else { + rlt.scrollLeft = el.scrollLeft; + rlt.scrollTop = el.scrollTop; + } + + return rlt; } + // 获取元素的可滚动的父元素 export function getScrollParents(el: HTMLElement) { const parents: Array = []; @@ -48,6 +65,7 @@ export function getScrollParents(el: HTMLElement) { } return parents; } + export function getRelativeBounding(e: DOMRect, c: DOMRect) { return { top: e.top, @@ -62,6 +80,7 @@ export function getRelativeBounding(e: DOMRect, c: DOMRect) { offsetBottom: e.bottom - c.top, }; } + export type RelativeRect = ReturnType; export function getElementSize(el: HTMLElement | Window) { @@ -116,3 +135,38 @@ export function mergeClass(...classList: Array) { return rlt; } + +interface ScrollTopOptions { + container?: ScrollTarget; + duration?: number; +} + +export function scrollTo(y: number, opts: ScrollTopOptions) { + const { container = window, duration = 450 } = opts; + const { scrollTop } = getScroll(container); + const startTime = Date.now(); + + return new Promise((res) => { + const frameFn = () => { + const timeStamp = Date.now(); + const time = timeStamp - startTime; + const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration); + + if (isWindow(container)) { + window.scrollTo(window.scrollX, nextScrollTop); + } else if (isDocument(container)) { + container.documentElement.scrollTop = nextScrollTop; + } else { + container.scrollTop = nextScrollTop; + } + + if (time < duration) { + throttleRAF(frameFn)(); + } else { + throttleRAF(res)(); + } + }; + + throttleRAF(frameFn)(); + }); +} diff --git a/packages/opendesign/src/_utils/easing.ts b/packages/opendesign/src/_utils/easing.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac7369cd078585908057a2e8bd835c93230865d7 --- /dev/null +++ b/packages/opendesign/src/_utils/easing.ts @@ -0,0 +1,11 @@ +export function easeInOutCubic(current: number, start: number, end: number, duration: number): number { + let elapsed = end - start; + let time = current / (duration / 2); + + if (time < 1) { + return (elapsed / 2) * time * time * time + start; + } + + time -= 2; + return (elapsed / 2) * (time * time * time + 2) + start; +} diff --git a/packages/opendesign/src/_utils/is.ts b/packages/opendesign/src/_utils/is.ts index 8bbfc2fa726b21abe3aa455f475d6d12bcb2dcd8..a956c90fac1353b619bee3f9892d00015e836c33 100644 --- a/packages/opendesign/src/_utils/is.ts +++ b/packages/opendesign/src/_utils/is.ts @@ -52,3 +52,7 @@ export const isPromise = (val: unknown): val is Promise => { export const isClient = typeof window !== 'undefined'; export const isTouchDevice = isClient ? 'ontouchstart' in document.documentElement : false; + +export function isWindow(val: unknown): val is Window { + return val === window; +} diff --git a/packages/opendesign/src/anchor/OAnchor.vue b/packages/opendesign/src/anchor/OAnchor.vue new file mode 100644 index 0000000000000000000000000000000000000000..8c56c6e1efe1443b2cfce9ca97694667d983981c --- /dev/null +++ b/packages/opendesign/src/anchor/OAnchor.vue @@ -0,0 +1,212 @@ + + + diff --git a/packages/opendesign/src/anchor/OAnchorItem.vue b/packages/opendesign/src/anchor/OAnchorItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..75ae62e243018b69cabbc93a9ea1892c7451a718 --- /dev/null +++ b/packages/opendesign/src/anchor/OAnchorItem.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/opendesign/src/anchor/__demo__/AnchorBasic.vue b/packages/opendesign/src/anchor/__demo__/AnchorBasic.vue new file mode 100644 index 0000000000000000000000000000000000000000..1a15ff5bd8b672f653a684b2564e43441bebcfcc --- /dev/null +++ b/packages/opendesign/src/anchor/__demo__/AnchorBasic.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/opendesign/src/anchor/__demo__/AnchorContainer.vue b/packages/opendesign/src/anchor/__demo__/AnchorContainer.vue new file mode 100644 index 0000000000000000000000000000000000000000..e54a942d5867ec1a3c9983adbb19fb0b6014ee4d --- /dev/null +++ b/packages/opendesign/src/anchor/__demo__/AnchorContainer.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/opendesign/src/anchor/index.ts b/packages/opendesign/src/anchor/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..52081bbb620bf78b739b224baea26df8b5295c68 --- /dev/null +++ b/packages/opendesign/src/anchor/index.ts @@ -0,0 +1,20 @@ +import type { App } from 'vue'; + +import _OAnchor from './OAnchor.vue'; +import _OAnchorItem from './OAnchorItem.vue'; + +const OAnchor = Object.assign(_OAnchor, { + install(app: App) { + app.component(_OAnchor.name, _OAnchor); + }, +}); + +const OAnchorItem = Object.assign(_OAnchorItem, { + install(app: App) { + app.component(_OAnchorItem.name, _OAnchorItem); + }, +}); + +export * from './types'; + +export { OAnchor, OAnchorItem }; diff --git a/packages/opendesign/src/anchor/provide.ts b/packages/opendesign/src/anchor/provide.ts new file mode 100644 index 0000000000000000000000000000000000000000..b28055a512260f78d9a9c1c0ce225e2e1d11de46 --- /dev/null +++ b/packages/opendesign/src/anchor/provide.ts @@ -0,0 +1,8 @@ +import { InjectionKey, Ref } from 'vue'; + +export const anchorInjectKey: InjectionKey<{ + addLink: (link: string) => void; + removeLink: (link: string) => void; + handleClick: (ev: MouseEvent, link?: string) => void; + activeLink: Ref; +}> = Symbol('provide-anchor'); diff --git a/packages/opendesign/src/anchor/style/index.scss b/packages/opendesign/src/anchor/style/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..504d59c44eea7243e3faa4e454b7675ea8b98768 --- /dev/null +++ b/packages/opendesign/src/anchor/style/index.scss @@ -0,0 +1,2 @@ +@use './style.scss' as *; +@use './media.scss' as *; diff --git a/packages/opendesign/src/anchor/style/index.ts b/packages/opendesign/src/anchor/style/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..51ae9de07216ddd657a685a8159d34bb07a61498 --- /dev/null +++ b/packages/opendesign/src/anchor/style/index.ts @@ -0,0 +1,3 @@ +import '../../_styles'; +import '../../select/style'; +import './index.scss'; diff --git a/packages/opendesign/src/anchor/style/media.scss b/packages/opendesign/src/anchor/style/media.scss new file mode 100644 index 0000000000000000000000000000000000000000..645b7e2b7a1ea1cfa8c9e7a20d1611347c9027cc --- /dev/null +++ b/packages/opendesign/src/anchor/style/media.scss @@ -0,0 +1,4 @@ +@use '../../_styles/mixin.scss' as *; + +@include respond-to('phone-pad') { +} diff --git a/packages/opendesign/src/anchor/style/style.scss b/packages/opendesign/src/anchor/style/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..8bc4d84eb621c275cb46f7a71e69b2c50e4dcf38 --- /dev/null +++ b/packages/opendesign/src/anchor/style/style.scss @@ -0,0 +1,60 @@ +@use '../../_styles/mixin.scss' as *; +@use './var.scss'; + +.o-anchor { + position: relative; + display: inline-flex; + align-items: stretch; + width: d; +} + +.o-anchor-line { + position: relative; + width: var(--anchor-line-width); + border-radius: var(--anchor-line-width); + background-color: var(--anchor-line-bg-color); + margin-right: 4px; +} + +.o-anchor-indicator { + position: absolute; + width: var(--anchor-line-width); + border-radius: var(--anchor-line-width); + background-color: var(--anchor-indicator-bg-color); + opacity: 0; + transition: all var(--o-easing-standard-in) var(--o-duration-m1); +} + +.o-anchor-item { + cursor: pointer; + width: var(--anchor-item-width); + min-width: var(--anchor-item--min-width); + color: var(--anchor-item-color); + font-size: var(--anchor-item-text-size); + line-height: var(--anchor-item-text-height); + background-color: var(--anchor-item-bg-color); + padding: var(--anchor-item-padding); + border-radius: var(--anchor-item-radius); + transition: background-color var(--o-duration-s) var(--o-easing-standard); + + @include hover { + &:hover { + background-color: var(--anchor-item-bg-color-hover); + } + } + + a { + color: inherit; + text-decoration: none; + } +} + +.o-anchor-item + .o-anchor-item { + margin-top: var(--anchor-item-gap); +} + +.o-anchor-item-active { + font-weight: 500; + color: var(--anchor-item-color-active); + background-color: var(--anchor-item-bg-color-active); +} diff --git a/packages/opendesign/src/anchor/style/var.scss b/packages/opendesign/src/anchor/style/var.scss new file mode 100644 index 0000000000000000000000000000000000000000..5fc03927f0315650e32322ec68302331b83d676a --- /dev/null +++ b/packages/opendesign/src/anchor/style/var.scss @@ -0,0 +1,26 @@ +.o-anchor { + --anchor-line-width: 2px; + --anchor-line-bg-color: var(--o-color-control4); + + --anchor-indicator-width: 2px; + --anchor-indicator-bg-color: var(--o-color-primary1); +} + +.o-anchor-item { + --anchor-item-width: 144px; + + --anchor-item-color: var(--o-color-info2); + --anchor-item-color-active: var(--o-color-info1); + + --anchor-item-text-size: var(--o-font_size-text1); + --anchor-item-text-height: var(--o-line_height-text1); + + --anchor-item-bg-color: transparent; + --anchor-item-bg-color-hover: var(--o-color-control2-light); + --anchor-item-bg-color-active: var(--o-color-control3-light); + + --anchor-item-padding: 8px; + --anchor-item-radius: var(--o-radius_control-m); + + --anchor-item-gap: 2px; +} diff --git a/packages/opendesign/src/anchor/types.ts b/packages/opendesign/src/anchor/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f9f14d9e6b8013fc5c0568f1a8abf1788301303 --- /dev/null +++ b/packages/opendesign/src/anchor/types.ts @@ -0,0 +1,40 @@ +import type { ExtractPropTypes, PropType, VNode } from 'vue'; + +export const anchorProps = { + container: { + type: [String, Object] as PropType, + }, + bounds: { + type: Number, + default: 0, + }, + targetOffset: { + type: Number, + default: 0, + }, + changeHash: { + type: Boolean, + default: true, + }, +}; + +export const anchorItemProps = { + title: { + type: String, + default: '', + }, + href: { + type: String, + default: '', + required: true, + }, + target: { + type: String as PropType<'_blank' | '_parent' | '_self' | '_top'>, + default: '_self', + }, +}; + +export type AnchorContainerT = HTMLElement | Window; + +export type AnchorPropsT = ExtractPropTypes; +export type AnchorItemPropsT = ExtractPropTypes; diff --git a/packages/opendesign/src/index.scss b/packages/opendesign/src/index.scss index a065aef6e72c9c1a633bc8e2b5def2c3c7d5b736..c741adeddf02d3af480295a9f21f98fe131a0d67 100644 --- a/packages/opendesign/src/index.scss +++ b/packages/opendesign/src/index.scss @@ -40,3 +40,4 @@ @use './scroller/style/index.scss' as *; @use './upload/style/index.scss' as *; @use './toggle/style/index.scss' as *; +@use './anchor/style/index.scss' as *; diff --git a/packages/opendesign/src/index.ts b/packages/opendesign/src/index.ts index 2bbcc66232857491522ca3b68f7b9191af15a601..b4b5f5b68ae16384d818cd51ac9aff5b06ed4d47 100644 --- a/packages/opendesign/src/index.ts +++ b/packages/opendesign/src/index.ts @@ -43,6 +43,7 @@ export * from './result'; export * from './scroller'; export * from './upload'; export * from './toggle'; +export * from './anchor'; export * from './intersection-observer'; export * from './resize-observer'; diff --git a/packages/portal/src/router.ts b/packages/portal/src/router.ts index 21a06d96dbc5085cda59810cce4ab2281f1a0b5e..a772d41d9106e425342b9e870f9f0409c4286654 100644 --- a/packages/portal/src/router.ts +++ b/packages/portal/src/router.ts @@ -1,4 +1,4 @@ -import { createRouter, createWebHashHistory } from 'vue-router'; +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'; import TheHome from './pages/TheHome.vue'; export const routes = [ @@ -249,6 +249,12 @@ export const routes = [ label: '选择块 Toggle', component: () => import('@components/toggle/__demo__/TheIndex.vue'), }, + { + path: '/anchor', + name: 'Anchor', + label: '锚点 Anchor', + component: () => import('@components/anchor/__demo__/TheIndex.vue'), + }, { path: '/resize-observer', name: 'ResizeObserver', @@ -270,7 +276,8 @@ export const routes = [ ]; export const router = createRouter({ - history: createWebHashHistory('./'), + // history: createWebHashHistory('./'), + history: createWebHistory(), routes, scrollBehavior(to, from, savePosition) { if (savePosition) {