明空Minempty
【Typecho】为Typecho添加文章目录树
注意:本帖的演示主题为 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
函数中用scrollTo
或scrollIntoView
来纠正。另外,我们希望有锚点的链接可以直接定位,因此监听了hashchange
事件。点击文章目录测试一下定位,再手动键入锚点测试一下,应该都没啥问题。
5. 定位到目录
目前可以从文章导航目录定位到文章标题了,是单向定位,双向定位还需要实现滚动文章内容时定位到导航目录的当前项。正如我们马上能想到的,需要监听window
的scroll
事件,在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 老师的分享。