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>