master
1---
2import { articleConfig, fastActionsConfig } from '@/config';
3import { t } from '@utils/i18n';
4import { Icon } from 'astro-icon/components';
5import Button from './widgets/Button.astro';
6import DarkModeButton from './widgets/DarkModeButton.astro';
7import TocButton from './widgets/SideToolBar/TocButton.tsx';
8---
9
10<div
11 id="fast-actions"
12 class="fixed right-0 bottom-10 z-30 grid grid-cols-1 gap-2"
13 aria-label={t.info.fastActions()}
14>
15 <div
16 id="fab-hide"
17 class="grid translate-x-full grid-cols-1 gap-2 pr-4 duration-500 ease-in-out"
18 inert
19 >
20 {
21 fastActionsConfig.items.map((item) => {
22 const { icon, text } = item;
23 if ('href' in item)
24 return (
25 <Button
26 href={item.href}
27 target={item.blank ? '_blank' : undefined}
28 title={text}
29 aria-label={text}
30 {...(item.extraAttr || {})}
31 >
32 <Icon name={icon} slot="icon" />
33 </Button>
34 );
35 if ('onclick' in item) {
36 if (typeof item.onclick === 'string')
37 return (
38 <Button
39 onclick={item.onclick}
40 title={text}
41 aria-label={text}
42 {...(item.extraAttr || {})}
43 >
44 <Icon name={icon} slot="icon" />
45 </Button>
46 );
47 return (
48 <Button
49 id={`fab-${item.onclick!.id}`}
50 title={text}
51 aria-label={text}
52 {...(item.extraAttr || {})}
53 >
54 <Icon name={icon} slot="icon" />
55 </Button>
56 );
57 }
58 return (
59 <Button title={text} aria-label={text} {...(item.extraAttr || {})}>
60 <Icon name={icon} slot="icon" />
61 </Button>
62 );
63 })
64 }
65 <DarkModeButton id="fab-dark-mode" class="btn-circle btn-secondary btn-sm" />
66 </div>
67 <div
68 id="fab-show"
69 class="grid translate-x-full grid-cols-1 gap-2 pr-4 duration-500 ease-in-out"
70 >
71 <Button
72 id="fab-show-more"
73 class="btn-circle btn-secondary btn-sm"
74 aria-expanded="false"
75 aria-label={t.button.more()}
76 aria-controls="fab-hide"
77 >
78 <Icon name="material-symbols:settings-rounded" class="animate-spin" />
79 </Button>
80 {
81 articleConfig.toc && (
82 <TocButton client:media="(width <= 80rem)">
83 <Icon name="material-symbols:toc-rounded" />
84 </TocButton>
85 )
86 }
87 <Button id="fab-back-to-top" class="group btn-circle btn-secondary btn-sm">
88 <span
89 id="fab-read-percentage"
90 aria-label={t.info.readingPercentage()}
91 class="absolute text-sm opacity-0 duration-300 group-hover:opacity-0">0</span
92 >
93 <Icon
94 id="fab-back-to-top-icon"
95 name="material-symbols:arrow-upward-rounded"
96 class="duration-300 group-hover:opacity-100"
97 />
98 </Button>
99 </div>
100</div>
101
102<script>
103 import { fastActionsConfig } from '@/config';
104 import { getReadingProgress } from '@scripts/utils';
105
106 const fabItems = fastActionsConfig.items;
107
108 function setup() {
109 const fabShow = document.getElementById('fab-show');
110 const fabHide = document.getElementById('fab-hide');
111 const fabShowMore = document.getElementById('fab-show-more');
112 const fabBackToTop = document.getElementById('fab-back-to-top');
113 const fabBackToTopIcon = document.getElementById('fab-back-to-top-icon');
114 const fabReadPercent = document.getElementById('fab-read-percentage');
115
116 let isExpanded = JSON.parse(fabShowMore?.getAttribute('aria-expanded') || 'false');
117
118 fabShowMore?.addEventListener('click', () => {
119 isExpanded = !isExpanded;
120 fabShowMore.setAttribute('aria-expanded', String(isExpanded));
121 if (isExpanded) {
122 fabHide?.classList.remove('translate-x-full');
123 fabHide?.removeAttribute('inert');
124 } else {
125 fabHide?.classList.add('translate-x-full');
126 fabHide?.setAttribute('inert', 'true');
127 }
128 });
129
130 fabBackToTop?.addEventListener('click', () => {
131 window.scrollTo({
132 top: 0,
133 behavior: 'smooth',
134 });
135 });
136
137 const bottomPos =
138 (
139 document.getElementById('page-comment') ||
140 document.getElementById('page-footer') ||
141 document.getElementById('footer')
142 )?.offsetTop || document.documentElement.scrollHeight;
143
144 window.addEventListener('scroll', () => {
145 // 控制工具栏显隐
146 if (window.scrollY > 0) {
147 fabShow?.classList.remove('translate-x-full');
148 } else {
149 fabShow?.classList.add('translate-x-full');
150 fabShowMore?.setAttribute('aria-expanded', 'false');
151 isExpanded = false;
152 document.getElementById('fab-hide')?.classList.add('translate-x-full');
153 }
154 // 控制进度条
155 const scrolledPercentage = getReadingProgress(bottomPos);
156 if (fabReadPercent) fabReadPercent.textContent = `${scrolledPercentage}`;
157 const isNearEnd = scrolledPercentage >= 99 || scrolledPercentage < 0;
158 if (isNearEnd) {
159 fabReadPercent?.classList.add('opacity-0');
160 fabBackToTopIcon?.classList.remove('opacity-0');
161 } else {
162 fabReadPercent?.classList.remove('opacity-0');
163 fabBackToTopIcon?.classList.add('opacity-0');
164 }
165 });
166
167 fabItems.forEach((item) => {
168 if ('onclick' in item && item.onclick && typeof item.onclick !== 'string') {
169 const fabEl = document.getElementById('fab-' + item.onclick.id);
170 if (fabEl) fabEl.addEventListener('click', item.onclick.function);
171 }
172 });
173 }
174
175 document.addEventListener('astro:page-load', setup);
176 setup();
177</script>