TypechoJoeTheme

阿尔法色色之屋

【Typecho】为Typecho添加文章目录树

本文最后更新于2023年08月25日。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!
为了有更好的阅读体验,阿尔法色色之屋为长篇文章提供了目录树,若没有正常显示,则需要手动缩小页面比例。
注意:本帖的演示主题为 Typecho Joe Theme

1. 添加 文章标题 锚点

打开对应的主题文件夹,找到function.php并添加如下代码:

// 添加文章标题锚点
function createAnchor($obj) {
  global $catalog;
  global $catalog_count;
  $catalog = array();
  $catalog_count = 0;
  $obj = preg_replace_callback('/<h([1-4])(.*?)>(.*?)<\/h\1>/i', function($obj) {
    global $catalog;
    global $catalog_count;
    $catalog_count ++;
    $catalog[] = array('text' => trim(strip_tags($obj[3])), 'depth' => $obj[1], 'count' => $catalog_count);
    return '<h'.$obj[1].$obj[2].'><a id="cl-'.$catalog_count.'"></a>'.$obj[3].'</h'.$obj[1].'>';
  }, $obj);
  return $obj;
}

createAnchor方法主要是通过正则表达式替换文章标题H1~H4来添加锚点,接下来我们需要调用它。

随后在core/core.php中的themeInit方法最后一行之前添加如下代码:

if ($archive->is('single')) {
     $archive->content = createAnchor($archive->content);
   }

现在可以查看一下文章详情页面的源代码。文章的H1~H2元素应该添加了<a>标签,该链接还有一个诸如cl-1、cl-2之类的id

2. 显示 文章目录

继续在functions.php中添加如下代码:

// 显示文章目录
function getCatalog() {  
  global $catalog;
  $str = '';
  if ($catalog) {
    $str = '<ul class="list">'."\n";
    $prev_depth = '';
    $to_depth = 0;
    foreach($catalog as $catalog_item) {
      $catalog_depth = $catalog_item['depth'];
      if ($prev_depth) {
        if ($catalog_depth == $prev_depth) {
          $str .= '</li>'."\n";
        } elseif ($catalog_depth > $prev_depth) {
          $to_depth++;
          $str .= '<ul class="sub-list">'."\n";
        } else {
          $to_depth2 = ($to_depth > ($prev_depth - $catalog_depth)) ? ($prev_depth - $catalog_depth) : $to_depth;
          if ($to_depth2) {
            for ($i=0; $i<$to_depth2; $i++) {
              $str .= '</li>'."\n".'</ul>'."\n";
              $to_depth--;
            }
          }
          $str .= '</li>';
        }
      }
      $str .= '<li class="item"><a class="link" href="#cl-'.$catalog_item['count'].'" title="'.$catalog_item['text'].'">'.$catalog_item['text'].'</a>';
      $prev_depth = $catalog_item['depth'];
    }
    for ($i=0; $i<=$to_depth; $i++) {
      $str .= '</li>'."\n".'</ul>'."\n"; 
    }
    $str = '<section class="navbar">'."\n".'<div class="title">文章目录</div>'."\n".$str.'</section>'."\n";
  }
  echo $str;
}

getCatalog方法通过递归$catalog数组生成文章目录,接下来我们需要调用它。

最好将放在右侧边栏中。为此在public/aside.php中根据你个人需要找到合适的位置,并添加如下代码:

<?php if ($this->is('post')) getCatalog(); ?>

这里的判断表示只有文章才使用目录,独立页面那些不需要。

现在点击右侧的文章目录,可以通过锚点滚动到相应的文章小标题位置了。

3. 添加 文章目录 样式

可以看到,当前的文章目录还比较丑陋,我们来美化一下。在post.php中或者在控制台 - 外观 - 设置外观 - 公共设置 - 自定义CSS 添加如下CSS代码:

.has_toc .joe_header {
  position: relative;
}

.navbar {
  position: -webkit-sticky;
  position: sticky;
  width: 250px;
  top: 125px;
  margin-left: 20px;
  margin-bottom: 20px;
  background: var(--background);
  border-radius: var(--radius-wrap);
  box-shadow: 1px 1px 5px var(--main);
  overflow: hidden;
}

.navbar .title {
  display: block;
  border-bottom: 1px solid var(--classA);
  font-size: 16px;
  font-weight: 500;
  height: 45px;
  line-height: 45px;
  text-align: center;
  color: var(--theme);
}

.navbar .list {
  padding-top: 10px;
  padding-bottom: 10px;
  max-height: calc(100vh - 60px);
  overflow: auto;
}

.navbar .list .item .link {
  display: block;
  padding: 8px 16px;
  border-left: 4px solid transparent;
  color: var(--main);
  white-space: nowrap; 
  overflow: hidden;
  text-overflow: ellipsis; 
  text-decoration: none;
}

.navbar .list .item .link:hover {
  background-color: var(--classC);
}

.navbar .list .item .link.active {
  border-left-color: var(--theme);
}

.navbar .sub-list {
  padding-left: 24px;
}

这里特意说明一下,.navbar 就是整个文章目录树的容器框样式,为了固定住目录树,将其设置为 position: sticky;。考虑到文章目录可能很多,又为 .navbar 列表添加了 overflow: auto;。考虑到每个人使用的主题版本不同,因此该样式为参考项,需要根据个人情况进行进一步修改。

4. 定位文章

要显示文章的导航目录当前选中项的状态,需要用到JavaScript给选中项添加一个active样式。在post.php 中或者在 控制台 - 外观 - 设置外观 - 公共设置 - 自定义JS 添加如下JS代码:

var titleItems = $('.joe_detail__article').find('h1, h2, h3, h4');
var navItems = $('.navbar a');
var timer = 0;
// 是否自动滚动
var autoScrolling = true;

function setItemActive(hash) {
  navItems.each(function (index, item) {
    if ($(item).attr('href') === hash) {
      $(item).addClass('active');
      if (autoScrolling) {
        var titleItem = $(titleItems.get(index));
        var top = titleItem.offset().top - 15;
        window.scrollTo({ top, behavior: 'smooth' })
      }
    } else {
      $(item).removeClass('active');
    }
  })
}

function onChange() {
  autoScrolling = true;
  setItemActive(location.hash);
}

window.addEventListener('hashchange', onChange);
// hash没有改变时手动调用一次
onChange();

由于布局和滚动动画的影响,导致锚点定位有点偏差。我们在setItemActive函数中用scrollToscrollIntoView来纠正。另外,我们希望有锚点的链接可以直接定位,因此监听了hashchange事件。点击文章目录测试一下定位,再手动键入锚点测试一下,应该都没啥问题。

5. 定位到目录

目前可以从文章导航目录定位到文章标题了,是单向定位,双向定位还需要实现滚动文章内容时定位到导航目录的当前项。正如我们马上能想到的,需要监听windowscroll事件,在post.php中或者在控制台 - 外观 - 设置外观 - 公共设置 - 自定义JS 添加如下JS代码:

function onScroll() {
  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(function () {
    var top = $(window).scrollTop();
    for (var i = 0; i < titleItems.length; i++) {
      var j = i;
      // 滚动和点击时 index 相差 1
      if (i > 0 && !autoScrolling) {
        j = i - 1;
      }
      var item = $(titleItems[i]);
      var itemTop = item.offset().top;
      // 判断滚动条滚动距离是否大于当前滚动项可滚动距离
      if (itemTop > top) {
        var id = $(titleItems[j]).children('a').attr('id');
        setItemActive("#" + id);
        break;
      } if (i === titleItems.length - 1) {
        // 特殊处理最后一个元素
        var id = item.children('a').attr('id');
        setItemActive("#" + id);
      }
    }
    autoScrolling = false;
  }, 100);
}
$(window).on('scroll', onScroll);

首先,在onScroll事件处理函数中遍历标题数组titleItems, 如果滚动条滚动距离top大于当前标题项item可滚动距离itemTop,再调用setItemActive函数,传入当前的标题项的id来判断文章目录激活状态。

现在文章目录基本上可用了,也还美观,后续可以考虑优化再封装成一个插件。

6. 原文地址

本帖改编自 Flying 老师的博客,感谢 Flying 老师的分享。

赞(0)

人生倒计时

今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月

标签云