G&TOC&CSS

GHOST 并没有内置 Table Of Content 的功能,且官方提供的解决方案存在不合理处,遂自行调整部分效果,本文意在记录总结实现过程。

初步实现

按照官方文档,根据文章内容中的标题生成目录需采用一款名为 Tocbot 的小型库。

将获取到的 Tocbot CSS 链接添加到 Casper 主题 default.hbs 文件中 <head> 标签体尾部。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.css">

同样地操作 Tocbot JavaScript 链接至 default.hbs</body> 标签之前。值得一提的是,随着版本更新,内容选择器不再是官方所绑定的 .post-content,而是根据源码得到的 .gh-content

<script src="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.min.js"></script>
<script>
  tocbot.init({
    tocSelector: '.toc',
    contentSelector: '.gh-content',
    hasInnerContainers: true
  });
</script>

对于选择区域进行渲染,期望是目录出现在文章内容之前,因此在 .post.hbs 文件完成如下更新。

<section class="gh-content gh-canvas">
  <aside class="toc-container">
    <div class="toc"></div>
  </aside>
  {{content}}
</section>

定制效果

期望能在浏览文章的同时,在侧边看见即时的 TOC 面板,故增加 TOC 随页面滚动更改位置的需求。

对于其他理论可行的方案来说,都或多或少与当前版本源码存在差异,最典型的是布局方式。众所周知,Ghost 后台开启服务的方式有很多种,像是 PM2、云代理,更有甚者直接用 Node 写了一个与 Ghost 模样一致的替代版

function tocHandle() {
  var tc = document.querySelector(".toc-container");
  var ah = document.querySelector(".article-header");
  
  if (tc && ah) {
    var tcch = tc.clientHeight;
    var ahch = ah.clientHeight;
    var isTocSticky = false;

    function handleScroll() {
      if (document.body.clientWidth > 1170) {
        var scrollY = window.pageYOffset || document.documentElement.scrollTop;
        var wih = window.innerHeight;

        if (scrollY >= wih + tcch + ahch && !isTocSticky) {
          tc.style.position = "sticky";
          tc.style.position = "-webkit-sticky";
          tc.style.top = "120px";
          tc.style.marginLeft = "800px";
          tc.style.minWidth = "260px";
          isTocSticky = true;
        }
        
        if (scrollY < tcch + ahch - 10 && isTocSticky) {
          tc.style.position = "";
          tc.style.top = "";
          tc.style.marginLeft = "";
          isTocSticky = false;
        }
      }
    }

    // 避免在每个滚动事件触发时都执行回调函数
    window.addEventListener("scroll", function () {
      requestAnimationFrame(handleScroll);
    });
  }
}

window.addEventListener("DOMContentLoaded", tocHandle);

去除下划线样式并解决类名为 toc-list-item 的 li 下首个子元素距顶部位置较窄。

<!-- TOC -->
<style>
.toc > .toc-list li:first-child,
.toc.active > .toc-list li:first-child {
  margin-top: 8px;
}

.toc-list a,
.toc.active .toc-list a {
  color: #000000 !important;
  text-decoration: none;
  word-break: break-word;
}

.toc > ol,
.toc > li,
.toc.active > ol,
.toc.active > li {
  font-size: 1.4rem;
}
</style>

总结反思

Ghost 现阶段源码的布局是使用 Grid 与 Flex 交叉的方式。若在改变 TOC 元素位置时,将其原有位置在 DOM 上移除,则会导致重排效果,体验感大大降低且消耗性能。鉴于页面可视区的滚动,使用者也不会关注到留白的移动区,故不考虑 DOM 元素的摘除,纯粹临时性改变坐标。

CSS

postion 的选择方式上,比较固定定位 fixed 与粘性定位 sticky。二者都可以在拖动滚动条时,固定元素于指定位置。但是在效果上,前者是直接固定于指定位置,后者在达到临界值时进行固定。在性质上,前者脱离文档流,后者不脱离文档流。在使用上,前者无需指定 top、button、left、right 中的任一值,就能以当前视口进行定位。后者必须指定其一,然后相对最近滚动祖先进行偏移。

Ghost 源码的视窗单位出现较丰富。viewport 是浏览器实际显示内容的区域,即不包括工具栏的浏览器区域。

// A viewport with width 1000px and height 800px
vw => viewport 高度的百分比 => 50vw = 500px
vh => viewport 高度的百分比 => 50vh = 400px
vmin => vw 和 vh 中较小的值作为百分比单位 => 50vim = 400px
vmax => vw 和 vh 中较大的值作为百分比单位 => 50vmax = 500px
/* 应用 */
/* 子元素的大小相对于窗口改变而不是父元素 */
.parent { width: 100px; }
.child { width: 50vw; }
/* 响应垂直居中 */
.verticalresponsecentering{ width: 50vw; height: 50vh; margin: 25vh auto; }
.verticalresponsecentering{ width: 50vw; height: 50vh; margin: 25vh 25vw; }

Javascript

在效果实现的过程中,滚动监听事件获取当前页面滚动距离,应注意页面端与到移动端的兼容;同时也应注意获取元素尺寸与视口高宽的问题。相关链接于文末。

// 兼容性
// 页面端支持 document.documentElement,移动端支持 document.body
var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
// 自动识别不同平台上的滚动容器 => document.scrollingElement
// 在桌面端 document.scrollingElement 就是 document.documentElement/在移动端 document.scrollingElement 就是 document.body
document.scrollingElement.scrollTop = 0; // 一行代码回滚顶部 => 兼容pc与移动
// 注意 document.body.xxx 情况
Element.clientWidth|clientHeight // 元素的内部宽度|高度 => 包括内边距 padding,但不包括边框 border、外边距 margin 和垂直滚动条
Element.offsetWidth|offsetHeight // 只读属性 => 返回一个元素的布局宽度|高度 => 包含元素的边框 border、水平线上的内边距 padding、竖直方向滚动条 scrollbar
Element.scrollWidth|scrollHeight // 元素内容宽度|高度的度量
Element.scrollTop|scrollLeft // 获取或设置一个元素的内容垂直滚动的像素数|读取或设置元素滚动条到元素左边的距离

window.screenTop|screenLeft // 从包括工具栏的用户浏览器上|左边界到屏幕最顶端距离
window.screen.height|width // 返回访问者屏幕的高度|宽度 => 900|1440
window.innerHeight|innerWidth // 视窗的内部高度|宽度

window.pageYOffset // 只读 => scrollTop 可设置,具有回顶部效果

window.screen.availHeight|availWidth // 返回包含工具栏的浏览器窗口在屏幕上可占用的垂直|水平空间,即最大高度|宽度 => 825|1440
Element size and scrolling
CSS Grid 网格布局教程 - 阮一峰的网络日志

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!