diff --git a/devui/anchor/index.ts b/devui/anchor/index.ts index 091d3f965a0850722655389b0228b77dce8ad061..3df6a901d6e4e5fff5864f8a5273893ebdcc231c 100644 --- a/devui/anchor/index.ts +++ b/devui/anchor/index.ts @@ -1,10 +1,27 @@ -import type { App } from 'vue' +import { App } from 'vue' import Anchor from './src/anchor' +import dAnchorBox from './src/d-anchor-box' +import dAnchorLink from './src/d-anchor-link' +import dAnchor from './src/d-anchor' +import './src/anchor.scss'; -Anchor.install = function(app: App) { - app.component(Anchor.name, Anchor) -} +const directives = { + 'd-anchor': dAnchor, + 'd-anchor-link': dAnchorLink, + 'd-anchor-box': dAnchorBox, + +}; +Anchor.install = function(Vue: App) { + for (const key in directives) { + if (directives.hasOwnProperty(key)) { + + Vue.directive(key, directives[key]); + } + } + Vue.component(Anchor.name, Anchor) +}; + export { Anchor } export default { diff --git a/devui/anchor/src/anchor.scss b/devui/anchor/src/anchor.scss new file mode 100644 index 0000000000000000000000000000000000000000..f64724c16d44990d33fb32d49f68da4eada7c29c --- /dev/null +++ b/devui/anchor/src/anchor.scss @@ -0,0 +1,209 @@ +.mysidebar { + width: 240px; + position: absolute; + top: 0; + left: 0; + height: auto; +} +.scrollTarget { + height: 450px!important; + overflow-y: auto; +} +.mycontainer { + height: auto; + + // overflow-y: auto; +} + +.devui-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.devui-scrollbar::-webkit-scrollbar-track { + background-color: transparent; +} + +.devui-scrollbar::-webkit-scrollbar-thumb { + border-radius: 8px; + background-color: #adb0b8; + background-color: var(--devui-line, #adb0b8); +} + +.devui-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: #8a8e99; + background-color: var(--devui-placeholder, #8a8e99); +} + +body > * ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +body > * ::-webkit-scrollbar-track { + background-color: transparent; +} + +body > * ::-webkit-scrollbar-thumb { + border-radius: 8px; + background-color: #adb0b8; + background-color: var(--devui-line, #adb0b8); +} + +body > * ::-webkit-scrollbar-thumb:hover { + background-color: #8a8e99; + background-color: var(--devui-placeholder, #8a8e99); +} + +body > * ::-webkit-scrollbar-corner { + background-color: transparent; +} + +.step-nav { + padding-top: 8px; + width: 240px; +} + +.step-nav > li { + list-style: none; + counter-increment: stepli; + padding: 0; + cursor: pointer; + height: 30px; + line-height: 1.5; + font-size: 12px; + font-size: var(--devui-font-size, 12px); + color: #575d6c; + color: var(--devui-text-weak, #575d6c); + position: relative; + display: flex; + align-items: center; +} + +.step-nav > li.active, +.step-nav > li:hover { + color: #526ecc; + color: var(--devui-brand-active, #526ecc); +} + +.step-nav > li.active::before { + border-color: #526ecc; + border-color: var(--devui-brand-active, #526ecc); +} + +.step-nav > li::before { + content: ''; + display: inline-block; + width: 12px; + height: 12px; + text-align: center; + line-height: 26px; + border-radius: 50%; + background-color: #ffffff; + background-color: var(--devui-base-bg, #ffffff); + margin-right: 20px; + border: 2px solid #dfe1e6; + border: 2px solid var(--devui-dividing-line, #dfe1e6); +} + +.step-nav > li:not(:first-of-type) { + margin-top: 32px; +} + +.step-nav > li:not(:first-of-type)::after { + content: ''; + display: block; + position: absolute; + top: -32px; + left: 5px; + width: 1px; + height: 32px; + border-left: 2px solid #dfe1e6; + border-left: 2px solid var(--devui-dividing-line, #dfe1e6); +} + +.mymain { + position: relative; +} + +.mycontent { + padding: 8px; + margin-left: 240px; + border-left: 1px solid #adb0b8; + border-left: 1px solid var(--devui-line, #adb0b8); +} + +.section-block { + min-height: 200px; + border-bottom: 1px dashed #adb0b8; + border-bottom: 1px dashed var(--devui-line, #adb0b8); +} + +.section-block.active.anchor-active-by-anchor-link { + -webkit-animation: hightlight-and-disapear 3s linear 1; + animation: hightlight-and-disapear 3s linear 1; +} + +@-webkit-keyframes hightlight-and-disapear { + 0% { + outline: medium none invert; + } + + 2% { + outline: 0 none hsla(0, 0%, 100%, 0); + } + + 10% { + outline: 1px solid #5e7ce0; + outline: 1px solid var(--devui-brand, #5e7ce0); + } + + 50% { + outline: 1px solid #5e7ce0; + outline: 1px solid var(--devui-brand, #5e7ce0); + } + + 90% { + outline: 1px solid hsla(0, 0%, 100%, 0); + } + + 99% { + outline: 0 none hsla(0, 0%, 100%, 0); + } + + to { + outline: medium none invert; + } +} + +@keyframes hightlight-and-disapear { + 0% { + outline: medium none invert; + } + + 2% { + outline: 0 none hsla(0, 0%, 100%, 0); + } + + 10% { + outline: 1px solid #5e7ce0; + outline: 1px solid var(--devui-brand, #5e7ce0); + } + + 50% { + outline: 1px solid #5e7ce0; + outline: 1px solid var(--devui-brand, #5e7ce0); + } + + 90% { + outline: 1px solid hsla(0, 0%, 100%, 0); + } + + 99% { + outline: 0 none hsla(0, 0%, 100%, 0); + } + + to { + outline: medium none invert; + } +} diff --git a/devui/anchor/src/anchor.tsx b/devui/anchor/src/anchor.tsx index 36ee97ba89d1eb1b53c5832cd2dd18ee7752854f..297b31c8736cb193b9d44e45464acbebb1ade0b1 100644 --- a/devui/anchor/src/anchor.tsx +++ b/devui/anchor/src/anchor.tsx @@ -6,7 +6,9 @@ export default defineComponent({ }, setup() { return () => { - return
devui-anchor
+ return ( +
+ ) } } }) \ No newline at end of file diff --git a/devui/anchor/src/d-anchor-box.ts b/devui/anchor/src/d-anchor-box.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9396cd29ea679e467e9d868d77f3ad8344950ae --- /dev/null +++ b/devui/anchor/src/d-anchor-box.ts @@ -0,0 +1,92 @@ +import { setActiveLink, onScroll, randomId } from './util'; +export default { + // 滚动区域 + // 1.监听window滚动或滚动容器滚动,切换link+active,改变# + mounted(el: HTMLElement): void { + const timeId = 'm' + randomId(8); + el.id = timeId; + // 添加ng class名 + const classList = el.classList; + classList.add('mycontainer', 'mymain', timeId); + // 监听window + let windoScrollTop; + const div = document.querySelector(`#${timeId}`) as HTMLElement; + + const mysidebar = document.querySelector( + `#${timeId} .mysidebar` + ) as HTMLElement; + + const mysidebarHeight = mysidebar.clientHeight; + window.addEventListener('resize', () => { + cssChange(mysidebar, 'absolute', 0, 0); + }); + window.onscroll = function () { + //为了保证兼容性,这里取两个值,哪个有值取哪一个 + //scrollTop就是触发滚轮事件时滚轮的高度 + windoScrollTop = document.documentElement.scrollTop || document.body.scrollTop; + // 16为padding 8px *2 (上下边距) + if (!document.getElementsByClassName('scrollTarget').length) { + if ( windoScrollTop + mysidebarHeight - 16 >= div.offsetTop + div.clientHeight ) { + // 看不见 d-anchor-box区域 + cssChange( + mysidebar, + 'absolute', + div.clientHeight - mysidebarHeight - 8, + 0 + ); + } else if (windoScrollTop > div.offsetTop) { + // 即将隐藏部分 box + cssChange( + mysidebar, + 'fixed', + div.offsetTop, + div.getBoundingClientRect().left + ); + } else if (div.offsetTop >= windoScrollTop && windoScrollTop >= 0) { + // 刚开始滚动 + cssChange(mysidebar, 'absolute', 0, 0); + } else { + cssChange(mysidebar, 'absolute', div.clientHeight - mysidebarHeight - 8, 0); + } + } else { + // 刚开始滚动 + cssChange(mysidebar, 'absolute', div.scrollTop, 0); + } + }; + + addEvent(div, 'scroll', function () { + if (document.getElementsByClassName('scrollTarget').length) { + cssChange( + mysidebar, + 'fixed', + div.getBoundingClientRect().top, + div.getBoundingClientRect().left + ); + } + }); + + // 监听window滚动或滚动容器滚动,切换link+active,改变# + setActiveLink(timeId); + document.getElementsByClassName('scrollTarget').length + ? addEvent(div, 'scroll', onScroll) + : window.addEventListener('scroll', onScroll); + }, +}; + +const cssChange = ( + mysidebar: HTMLElement, + postion: string, + top: number, + left: number +) => { + mysidebar.style.position = postion; + mysidebar.style.top = top + 'px'; + mysidebar.style.left = left + 'px'; +}; +const addEvent = (function () { + if (window.addEventListener) { + return function (elm, type, handle) { + elm.addEventListener(type, handle, false); + }; + } +})(); diff --git a/devui/anchor/src/d-anchor-link.ts b/devui/anchor/src/d-anchor-link.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4ad15c477885a211ce6f2663ce37e47dd0c6bfb --- /dev/null +++ b/devui/anchor/src/d-anchor-link.ts @@ -0,0 +1,30 @@ +import { scrollToControl } from './util'; +interface Bind { + value: string +} + +export default { + // 当被绑定的元素挂载到 DOM 中时…… + // 1.点击滚动到对应位置,并且高亮 + // 2.到对应位置后,改变url后hash + + mounted(el: HTMLElement,binding: Bind):void { + const parent: Element = el.parentNode as Element; + if (!parent.className) { + parent.className = 'mysidebar step-nav'; + } + el.className = 'bar-link-item'; + el.innerHTML += ''; + el.setAttribute('id', binding.value); + + el.onclick = () => { + let scrollContainer: any; + const scollToDomY = document.getElementsByName(binding.value)[0]; + document.getElementsByClassName('scrollTarget').length + ? scrollContainer = document.getElementsByClassName('scrollTarget')[0] + : scrollContainer = window + scrollToControl(scollToDomY, scrollContainer); + + } + } + }; diff --git a/devui/anchor/src/d-anchor.ts b/devui/anchor/src/d-anchor.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f6d01c3aafb32103e393062686cf10a13fd1ca4 --- /dev/null +++ b/devui/anchor/src/d-anchor.ts @@ -0,0 +1,30 @@ +import {hightLightFn} from './util' +interface Bind { + value: string + } + +export default { + // 挂载事件到dom + // 1.点击对应link高亮 + // 2.href+#+bing.value + + mounted(el: HTMLElement, binding: Bind):void { + const parent: Element = el.parentNode as Element; + if (!parent.className) { + parent.className = 'mycontent' + } + el.innerHTML = '' + el.innerHTML + el.className = 'section-block'; + // anchor-active-by-scroll + el.setAttribute('name',binding.value); + el.onclick = e => { + hightLightFn(binding.value); + + const classList = document.getElementById((e.target as HTMLElement).getAttribute('name')).classList; + console.log(classList) + + } + } + }; + + \ No newline at end of file diff --git a/devui/anchor/src/util.ts b/devui/anchor/src/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec4c8ee73c04d55d374e22540dad67d8663045f9 --- /dev/null +++ b/devui/anchor/src/util.ts @@ -0,0 +1,201 @@ +let repeatCount = 0; +let cTimeout; +const timeoutIntervalSpeed = 10; +let hashName:string; +// 滚动是由于点击产生 +let scollFlag = false; +function elementPosition(obj: HTMLElement ) { + let curleft = 0, curtop = 0; + curleft = obj.offsetLeft; + curtop = obj.offsetTop; + return { x: curleft, y: curtop }; +} + +export function scrollToControl(elem: HTMLElement, container: HTMLElement):void { + hashName = elem.getAttribute('name'); + scollFlag = true; + const tops = container.scrollTop>=0 ? container.scrollTop : -(document.getElementsByClassName('mycontainer')[0] as HTMLElement).offsetTop; + let scrollPos: number = elementPosition(elem).y - tops ; + + scrollPos = scrollPos - document.documentElement.scrollTop; + const remainder: number = scrollPos % timeoutIntervalSpeed; + const repeatTimes = Math.abs((scrollPos - remainder) / timeoutIntervalSpeed); + if (scrollPos < 0 && container || elem.getBoundingClientRect().top < container.offsetTop) { + window.scrollBy(0, elem.getBoundingClientRect().top-container.offsetTop-16) + } + // 多个计时器达到平滑滚动效果 + scrollSmoothly(scrollPos, repeatTimes, container) +} + + +function scrollSmoothly(scrollPos: number, repeatTimes: number, container: HTMLElement):void { + + if (repeatCount <= repeatTimes) { + scrollPos > 0 + ? container.scrollBy(0, timeoutIntervalSpeed) + : container.scrollBy(0, -timeoutIntervalSpeed) + } + else { + repeatCount = 0; + clearTimeout(cTimeout); + history.replaceState(null, null, document.location.pathname + '#' + hashName); + + hightLightFn(hashName) + setTimeout(() => { + scollFlag = false; + }, 310) + return ; + + } + repeatCount++; + cTimeout = setTimeout(() => { + scrollSmoothly(scrollPos, repeatTimes, container) + }, 10) + +} + +// 高亮切换 +export function hightLightFn(hashName:string):void { + + const childLength = document.getElementsByClassName('mysidebar')[0].children.length; + + for (let i = 0; i < childLength; i++) { + + if (document.getElementsByClassName('mysidebar')[0].children[i].classList.value.indexOf('active') > -1) { + + document.getElementsByClassName('mysidebar')[0].children[i].classList.remove('active') + } + } + document.getElementById(hashName).classList.add('active'); + + + +} +let activeLink = null; +let rootActiveLink = null; +let rootClassName = ''; +export const setActiveLink = (timeId:string):void => { + if (scollFlag) { return } + timeId ? rootClassName = timeId : rootClassName = document.getElementsByClassName('mymain')[0].id + + const sidebarLinks = getSidebarLinks(rootClassName); + const anchors = getAnchors(sidebarLinks); + try { + anchors.forEach((index,i)=> { + + const anchor:HTMLAnchorElement = anchors[i]; + const nextAnchor:HTMLAnchorElement = anchors[i + 1]; + + const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor); + if (isActive) { + history.replaceState(null, document.title, hash ? hash as string : ' '); + activateLink(hash); + throw Error(hash+''); + } + }) + } catch (e) { + } + +} + +function throttleAndDebounce(fn:any, delay:number):any { + let timeout:any; + let called = false; + return () => { + if (timeout) { + clearTimeout(timeout); + } + if (!called) { + fn(); + called = true; + setTimeout(() => { + called = false; + }, delay); + } + else { + timeout = setTimeout(fn, delay); + } + }; +} + +export const onScroll = throttleAndDebounce(setActiveLink, 300); + +function activateLink(hash:string | boolean):void { + deactiveLink(activeLink); + deactiveLink(rootActiveLink); + hash + ? activeLink = document.querySelector(`${hash}`) + : activeLink = document.querySelector(`.${rootClassName} ul li`) + if (!activeLink) { + return; + } + + if (!scollFlag) { + hash ? hightLightFn((hash as string).split('#')[1] ) : console.log(hash) + }else { + hightLightFn(hashName) + } + // + // also add active class to parent h2 anchors + const rootLi = activeLink.closest('.mycontainer > ul > li'); + if (rootLi && rootLi !== activeLink.parentElement) { + rootActiveLink = rootLi; + rootActiveLink && rootActiveLink.classList.add('active'); + } + else { + rootActiveLink = null; + } +} +function deactiveLink(link:HTMLElement):void { + link && link.classList.remove('active'); +} +function getPageOffset():number { + return (document.querySelector('.mysidebar ') as HTMLElement).getBoundingClientRect().y; +} + +function getAnchorTop(anchor:HTMLAnchorElement):number { + const pageOffset = getPageOffset(); + return anchor.parentElement.offsetTop - pageOffset - 5; +} + +function isAnchorActive(index:number, anchor:HTMLAnchorElement, nextAnchor:HTMLAnchorElement) { + let scrollTop:number; + document.getElementsByClassName('scrollTarget').length + ? scrollTop = document.getElementsByClassName('scrollTarget')[0].scrollTop + : scrollTop = document.documentElement.scrollTop || document.body.scrollTop + if (index === 0 && scrollTop === 0) { + return [true, null]; + } + + if (scrollTop < getAnchorTop(anchor)) { + return [false, null]; + } + if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) { + + return [true, decodeURIComponent(anchor.hash)]; + } + + return [false, null]; +} + +function getSidebarLinks(rootClassName:string):Array { + return [].slice.call(document.querySelectorAll(`.${rootClassName} > .step-nav > li.bar-link-item > a`)); +} + +function getAnchors(sidebarLinks:Array):Array { + return [].slice + .call(document.querySelectorAll('.box-anchor')) + .filter((anchor:HTMLAnchorElement) => sidebarLinks.some(( sidebarLink:HTMLAnchorElement ) => sidebarLink.hash === anchor.hash )); +} + + +export const randomId = function(n=8):string { // 生成n位长度的字符串 + const str = 'abcdefghijklmnopqrstuvwxyz0123456789'; // 可以作为常量放到random外面 + let result = ''; + for(let i = 0; i < n; i++) { + result += str[parseInt((Math.random() * str.length).toString())]; + } + return result; +} + + diff --git a/sites/.vitepress/config/sidebar.ts b/sites/.vitepress/config/sidebar.ts index fbafed02c0021e878725a73807dfdd889144c59b..6ddbecd1286b20c066ff5543d89360ea1577e1ea 100644 --- a/sites/.vitepress/config/sidebar.ts +++ b/sites/.vitepress/config/sidebar.ts @@ -26,6 +26,7 @@ const sidebar = { { text: 'Pagination 分页', link: '/components/pagination/', status: '开发中' }, { text: 'StepsGuide 操作指引', link: '/components/steps-guide/' }, { text: 'Tabs 选项卡', link: '/components/tabs/', status: '已完成' }, + { text: 'Anchor 锚点', link: '/components/Anchor/' }, ] }, { diff --git a/sites/components/anchor/demo.tsx b/sites/components/anchor/demo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d87cc00cee9d87e05961c61c1a8d52b202e5a851 --- /dev/null +++ b/sites/components/anchor/demo.tsx @@ -0,0 +1,35 @@ +import { defineComponent } from 'vue' + +export default defineComponent({ + name: 'DAnchor', + props: { + }, + setup() { + return () => { + return ( +
+
    +
  • anchorlink-one
  • +
  • anchorlink-two
  • +
  • anchorlink-three
  • +
  • anchorlink-four
  • +
+
+
+ anchorlink-one +
+
+ anchorlink-two +
+
+ anchorlink-three +
+
+ anchorlink-four +
+
+
+ ) + } + } +}) \ No newline at end of file diff --git a/sites/components/anchor/index.md b/sites/components/anchor/index.md index cbd1f3c2d133f33d12b147ac372b4514cfafbf8c..f879cf4b871024d1ecaccd1c5cd30d23c5c9ec12 100644 --- a/sites/components/anchor/index.md +++ b/sites/components/anchor/index.md @@ -1,4 +1,84 @@ -# Anchor 锚点 -跳转到页面指定位置的组件。 -### 何时使用 -需要在页面的各个部分之间实现快速跳转时。 \ No newline at end of file +# anchor 锚点 + + + +# 如何使用 + + +在页面中使用: + +```html + +
+
    +
  • anchorlink-one
  • +
  • anchorlink-two
  • +
  • anchorlink-three
  • +
  • anchorlink-four
  • +
+
+
+ anchorlink-one1 +
+
+ anchorlink-two +
+
+ anchorlink-three +
+
+ anchorlink-four +
+
+
+``` + +# v-d-anchor-box + +定义一个锚点。 +## v-d-anchor-box 参数 + +| 参数 | 类型 | 默认 | 说明 | 基本用法 |全局配置项| +| :----------------: | :----------: | :------: | :--: | :---------------------------------------------------: | ---------------------------- | +| className | `string` | -- | 可选,className为"scrollTarget"时,为局部滚动。默认全局滚动 | className="scrollTarget" | true + +# v-d-anchor + +定义一个锚点。 +## v-d-anchor 参数 + +| 参数 | 类型 | 默认 | 说明 | 基本用法 |全局配置项| +| :----------------: | :----------: | :------: | :--: | :---------------------------------------------------: | ---------------------------- | +| v-d-anchor | `string` | -- | 必选,设置锚点对应的跳转位置 | v-d-anchor="anchorlink-one" | true + +# v-d-anchor-link + +定义一个锚点。 +## v-d-anchor-link 参数 + +| 参数 | 类型 | 默认 | 说明 | 基本用法 |全局配置项| +| :----------------: | :----------: | :------: | :--: | :---------------------------------------------------: | ---------------------------- | +| v-d-anchor-link | `string` | -- | 必选,设置一个锚点的名字 | v-d-anchor-link="anchorlink-one" | true + +## dAnchor 锚点激活事件 + +自动会给锚点加上以下类对应不同激活的对象。 + +| css 类名 | 代表意义 | +| :---------------------------: | :--------------------: | +| active | 点击锚点链接激活 | + + +# dAnchorBox + +必须有一个容器,否则功能无法使用。 + +定义一个扫描锚点的容器,放在 dAnchor 与 dAnchorLink 的公共父节点上,用于锚点和链接之间的通信。