G&TOC&CSS

TOC 的需求实现以及相关的思考总结。

Glass Bottle Filled With Black Straw on Brown Wooden Table
Glass Bottle Filled With Black Straw on Brown Wooden Table

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 协议,转载请注明出处!