当前位置:首页 > CMS教程 > 其它CMS > 列表

Typecho插件实现添加文章目录的方法详解

发布:smiling 来源: PHP粉丝网  添加日期:2023-06-26 10:54:38 浏览: 评论:0 

我的长博文不少,比较影响阅读体验,有必要添加一个文章目录功能。相比 Wordpress, Typecho 的插件就比较少了。我想找一个像掘金那样为文章添加目录的插件,没一个合适的。此类教程也不是很多,而且差不多都是前台 JavaScript 来实现的,感觉这样不如后台实现来的好。

注意:我使用的是Joe主题7.3,其他主题文件路径可能不一样。

添加文章标题锚点

1.声明 createAnchor 函数

在 core/functions.php 中添加如下代码:

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

也可以在标题元素内添加 <a> 标签,然后该标签新增 id 属性。

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

2.调用函数

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

  1. if ($self->is('single')) { 
  2.   $self->content = createAnchor($self->content); 

现在可以查看一下文章详情页面的源代码。文章的 H1~H4 元素应该添加了诸如 cl-1、cl-2 之类的 id 属性值。具体啥名不是关键,好记就行。

显示文章目录

1.声明 getCatalog 函数

在 core/functions.php 中添加如下代码:

  1. // 显示文章目录 
  2. function getCatalog() {   
  3.   global $catalog
  4.   $str = ''
  5.   if ($catalog) { 
  6.     $str = '<ul class="list">'."\n"
  7.     $prev_depth = ''
  8.     $to_depth = 0; 
  9.     foreach($catalog as $catalog_item) { 
  10.       $catalog_depth = $catalog_item['depth']; 
  11.       if ($prev_depth) { 
  12.         if ($catalog_depth == $prev_depth) { 
  13.           $str .= '</li>'."\n"
  14.         } elseif ($catalog_depth > $prev_depth) { 
  15.           $to_depth++; 
  16.           $str .= '<ul class="sub-list">'."\n"
  17.         } else { 
  18.           $to_depth2 = ($to_depth > ($prev_depth - $catalog_depth)) ? ($prev_depth - $catalog_depth) : $to_depth
  19.           if ($to_depth2) { 
  20.             for ($i=0; $i<$to_depth2$i++) { 
  21.               $str .= '</li>'."\n".'</ul>'."\n"
  22.               $to_depth--; 
  23.             } 
  24.           } 
  25.           $str .= '</li>'
  26.         } 
  27.       } 
  28.       $str .= '<li class="item"><a class="link" href="#cl-'.$catalog_item['count'].'" rel="external nofollow"  title="'.$catalog_item['text'].'">'.$catalog_item['text'].'</a>'
  29.       $prev_depth = $catalog_item['depth']; 
  30.     } 
  31.     for ($i=0; $i<=$to_depth$i++) { 
  32.       $str .= '</li>'."\n".'</ul>'."\n";  
  33.     } 
  34.     $str = '<section class="toc">'."\n".'<div class="title">文章目录</div>'."\n".$str.'</section>'."\n"
  35.   } 
  36.   echo $str

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

2.函数

最好将放在右侧边栏中。为此在 public/aside.php 中添加如下代码:

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

注意:只有文章才使用目录,独立页面那些不需要,所以加了判断。Typecho 有一些神奇的 is 语法可以方便二次开发,可以访问它的官网文档了解更多。

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

添加文章目录样式

可以看到,当前的文章目录还比较丑陋,我们来美化一下。在 assets/css/joe.post.min.scss 中添加如下 SCSS 代码:

  1. .joe_aside { 
  2.   .toc { 
  3.     position: sticky; 
  4.     top: 20px
  5.     width250px
  6.     background: var(--background); 
  7.     border-radius: var(--radius-wrap); 
  8.     box-shadow: var(--box-shadow); 
  9.     overflowhidden
  10.  
  11.     .title { 
  12.       displayblock
  13.       border-bottom1px solid var(--classA); 
  14.       font-size16px
  15.       font-weight500
  16.       height45px
  17.       line-height45px
  18.       text-aligncenter
  19.       color: var(--theme); 
  20.     } 
  21.  
  22.     .list { 
  23.       padding-top10px
  24.       padding-bottom10px
  25.       max-height: calc(100vh - 80px); 
  26.       overflowauto
  27.  
  28.       .link { 
  29.         displayblock
  30.         padding8px 16px
  31.         border-left4px solid transparent
  32.         color: var(--main); 
  33.         text-decorationnone
  34.         white-spacenowrap
  35.         overflowhidden
  36.         text-overflow: ellipsis; 
  37.  
  38.         &:hover { 
  39.           background-color: var(--classC); 
  40.         } 
  41.  
  42.         &.active { 
  43.           border-left-color: var(--theme); 
  44.         } 
  45.       } 
  46.     } 
  47.   } 

为了方便操作,将 .toc 设置成 position: sticky; 实现了吸顶定位。考虑到文章目录可能很多,为 .toc 列表添加了 overflow: auto;,如代码第 3 ~ 4 行。

由于 .joe_header(主题标头)也使用了吸顶定位,导致和文章目录有遮挡,所有加了 has_toc .joe_header 来取消页面主题标头的吸顶功能,如下代码:

  1. .has_toc { 
  2.   .joe_header { 
  3.     positionrelative
  4.   } 

定位到文章

要显示文章目录当前选中项的状态,需要用到 JavaScript 给选中项添加一个 active 样式。在 assets/js/joe.post_page.js 中添加如下代码:

  1. var headings = $('.joe_detail__article').find('h1, h2, h3, h4'); 
  2. var links = $('.toc .link'); 
  3. var tocList = document.querySelector('.tocr > .list'); 
  4. var itemHeight = $('.toc .item').height(); 
  5. var distance = tocList.scrollHeight - tocList.clientHeight; 
  6. var timer = 0; 
  7. // 是否自动滚动 
  8. var autoScrolling = true; 
  9.  
  10. function setItemActive(id) { 
  11.   links.removeClass('active'); 
  12.   var link = links.filter("[href='#" + id + "']"
  13.   link.addClass('active'); 
  14.  
  15. function onChange() { 
  16.   autoScrolling = true; 
  17.   if (location.hash) { 
  18.     id = location.hash.substr(1); 
  19.     var heading = headings.filter("[id='" + id + "']"); 
  20.     var top = heading.offset().top - 15; 
  21.     window.scrollTo({ top: top }) 
  22.     setItemActive(id) 
  23.   } 
  24. window.addEventListener('hashchange', onChange); 
  25. // hash没有改变时手动调用一次 
  26. onChange(); 

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

定位到目录

目前可以从文章目录定位到文章标题了,是单向定位,双向定位还需要实现滚动文章内容时定位到文章目录的当前项。正如我们马上能想到的,需要监听 window 的 scroll 事件,如下代码:

  1. function onScroll() { 
  2.   if (timer) { 
  3.     clearTimeout(timer); 
  4.   } 
  5.   timer = setTimeout(function () { 
  6.     var top = $(window).scrollTop(); 
  7.     var count = headings.length; 
  8.     for (var i = 0; i < count; i++) { 
  9.       var j = i; 
  10.       // 滚动和点击时 index 相差 1,需要 autoScrolling 来区分 
  11.       if (i > 0 && !autoScrolling) { 
  12.         j = i - 1; 
  13.       } 
  14.       var headingTop = $(headings[i]).offset().top; 
  15.       var listTop = distance * i / count 
  16.       // 判断滚动条滚动距离是否大于当前滚动项可滚动距离 
  17.       if (headingTop > top) { 
  18.         var id = $(headings[j]).attr('id'); 
  19.         setItemActive(id); 
  20.         // 如果目录列表有滑条,使被选中的下一元素可见 
  21.         if (listTop > 0) { 
  22.           // 向上滚动 
  23.           if (listTop < itemHeight) { 
  24.             listTop -= itemHeight; 
  25.           } else { 
  26.             listTop += itemHeight; 
  27.           } 
  28.           $(tocList).scrollTop(listTop) 
  29.         } 
  30.         break
  31.       } else if (i === count - 1) { 
  32.         // 特殊处理最后一个元素 
  33.         var id = $(headings[i]).attr('id'); 
  34.         setItemActive(id); 
  35.         if (listTop > 0) { 
  36.           $(tocList).scrollTop(distance) 
  37.         } 
  38.       } 
  39.     } 
  40.     autoScrolling = false; 
  41.   }, 100); 
  42.  
  43. $(window).on('scroll', onScroll); 

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

如果目录列表有滑条,调用 jQuery 的 scrollTop 方法滚动目录列表滑条,使被选中目录项的上下元素可见,现在文章目录基本上可用了,也还美观,后续可以考虑优化再封装成一个插件。

吐槽一下:Joe 主题太依赖jQuery了,修改起来费劲 ::(汗)。

Tags: Typecho插件 Typecho添加文章目录

分享到: