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 的公共父节点上,用于锚点和链接之间的通信。