屎山雕花魔法与你:基于Wikidot原生语法的切换式按钮实现

屎山雕花魔法与你:基于Wikidot原生语法的切换式按钮实现
字数:17926

评分: +60+x

长话短说:本篇文章主要聚焦于在不使用内嵌HTML iframe的情况下,在wikidot上实现切换式按钮的技术。

本文章主要涉及一种基于常规折叠块(Collapsible Block)的切换式按钮。由于该技术的本质是利用Wikidot的内置JavaScript搞出来的花活,因此使用同样内置有JS代码的分页(Tabview)与可折叠列表容器(Foldable List Container)同样可以实现类似的效果。

注意:本文章中使用的技术强依赖于:has()伪类。这代表着在低版本浏览器(Firefox 2024年以前的版本;其余主流浏览器2022年9月以前的版本)以及部分国产手机浏览器中基于该技术的所有效果均无法生效。如果你在这一行文字的下面看到了一个警告框,则代表你所使用的浏览器不支持这一功能。

如果您看到了这段文字,那么您使用的浏览器很可能并不支持本文章的全部展示效果。

如果您希望体验完整的文章内容,请换用电脑端或升级到浏览器的最新版本后再进行阅读。

需要注意的是,本文章中提及的一部分功能可以使用后继选择器等CSS技术替代:has()伪类实现,从而规避后者可能引起的兼容性问题。若希望了解更多,请参考Dr HormressDr Hormress写作的Colmod魔法与你


1. 预备知识

1.1. :has()伪类

:has()伪类是一个用于筛选元素内部是否包含特定选择器的伪类。该伪类在使用时需要传入一个相对选择器列表

element:has(selectorA, selectorB, ...) {
  /* 指定的样式 */
}

当被:has()伪类修饰的元素element内部含有相对选择器列表中的任一选择器时,为整体指定的样式就会被应用在element上,否则则不会应用。如下例所示:

[[module css]]
/* 当一个类名为kitty的引用块(blockquote)内含有字符注释(ruby)时,其背景色会被设置为带有透明度的橙色。 */
.blockquote.kitty:has(span.ruby) {
  background-color: #fba41440;
}
[[/module]]
 
[[div class="blockquote kitty"]]
我是小猫。
[[/div]]
 
[[div class="blockquote kitty"]]
我是[[span class="ruby"]]长长长猫[[span class="rt"]]SCP-CN-20210401-J[[/span]][[/span]]。
[[/div]]

我是小猫。

我是长长长猫SCP-CN-20210401-J

此外,:has()伪类可以被用于选择两个接续选择器中的第一个元素。如下例所示:

[[module css]]
/*span.aspan.b连续时,样式修改作用于span.b */
span.a + span.b {
  color: green;
}
/*span.aspan.b连续时,样式修改作用于span.a */
span.a:has(+ span.b) {
  color: red;
}
[[/module]]
 
[[div class="blockquote"]]
[[span class="a"]]我没有颜色[[/span]]
 
[[span class="b"]]我也没有颜色[[/span]]
 
[[span class="a"]]这段文字是红色的[[/span]][[span class="b"]]而这段是绿色的[[/span]]
[[/div]]

我没有颜色

我也没有颜色

这段文字是红色的而这段是绿色的

若希望了解更多,请参考文档

1.2. 折叠块结构与作用机理

在wikidot中,你可以通过以下代码调用一个折叠块:

[[collapsible show="+ 展开" hide="- 隐藏"]]
展开后展示的内容。
[[/collapsible]]

上面这个折叠块的html代码结构是这样的:

<div class="collapsible-block">
  <div class="collapsible-block-folded">
    <a class="collapsible-block-link" href="javascript:;">+ 展开</a>
  </div>
  <div class="collapsible-block-unfolded" style="display: none;">
    <div class="collapsible-block-unfolded-link">
      <a class="collapsible-block-link" href="javascript:;">- 隐藏</a>
    </div>
    <div class="collapsible-block-content">
      <p>展开后展示的内容。</p>
    </div>
  </div>
</div>

注意到,div.collapsible-block-folded中的锚元素以及div.collapsible-block-unfolded-link中的锚元素均导向一段wikidot内置的JavaScript代码。这两段代码被用于处理折叠的打开与关闭。它们对折叠的HTML结构做出的修改主要体现在对三个div的style做出的修改。具体修改内容如下:

<div class="collapsible-block">
  <div class="collapsible-block-folded">
    <a class="collapsible-block-link" href="javascript:;">+ 展开</a>
  </div>
  <div class="collapsible-block-unfolded" style="display: none;">
    <div class="collapsible-block-unfolded-link">
      <a class="collapsible-block-link" href="javascript:;">- 隐藏</a>
    </div>
    <div class="collapsible-block-content">
      <p>展开后展示的内容。</p>
    </div>
  </div>
</div>

下表是对各种条件下折叠内部包含的三个主要div的内联style参数的整理,以供快速参考。

条件 div.collapsible-block-folded div.collapsible-block-unfolded div.collapsible-block-content
未打开折叠 无参数 "display: none;" 无参数
首次打开折叠 "display: none;" "" "display: block;"
打开后关闭折叠 "display: block;" "display: none;" "display: block;"
多次打开折叠 "display: none;" "display: block;" "display: block;"

2. 泛用切换式按钮

切换式按钮的实现方式究其本质,就是一种利用Wikidot的内置JavaScript,通过调整页面CSS,手动实现切换式按钮所必须的以下三个功能需求:

  • 存在两个不同的响应状态;
  • 可以通过点击按钮的方式在两个响应状态间切换;
  • 根据响应状态的不同,对页面上的其它页面元素产生影响。

折叠块的内置JS代码已经帮我们解决了前两个功能需求。因此,我们只需要补充一些CSS,让折叠块在切换时不仅能影响折叠块本身,还能对页面中的其他元素产生影响,就可以构建出泛用的切换式按钮。下面的这段代码是一个实现这一功能的范例:

[[module css]]
/* 隐藏用于标识触发的span元素 */
p:has(span.trigger) { display: none; }
/* .spec类默认隐藏 */
.spec { display: none; }
/* 在含有对应触发的折叠打开时,.spec类展示 */
:root:has(.collapsible-block-unfolded[style=""] span.trigger.n1, .collapsible-block-unfolded[style="display: block;"] span.trigger.n1) div.spec.n1 {
  display: block;
}
:root:has(.collapsible-block-unfolded[style=""] span.trigger.n1, .collapsible-block-unfolded[style="display: block;"] span.trigger.n1) span.spec.n1 {
  display: inline;
}
/* 在含有对应触发的折叠打开时,.anti类隐藏 */
:root:has(.collapsible-block-unfolded[style=""] span.trigger.n1, .collapsible-block-unfolded[style="display: block;"] span.trigger.n1) .anti.n1 {
  display: none;
}
 
[[/module]]
 
[[collapsible show="" hide=""]]
[[span class="trigger n1"]]@@ @@[[/span]]
[[/collapsible]]
 
[[div class="spec n1"]]
这里有一整段内容
[[/div]]
 
你的[[span class="anti n1"]]小猫[[/span]][[span class="spec n1"]]长长长猫[[/span]]正在突击!

这里有一整段内容

你的小猫长长长猫正在突击!

这段代码的变体被用于苍之预言书,第四卷,162页:“星神之树”,在这篇文章中,这一模块的引入使得我们能够根据读者选择的不同分支来自由调整页面中包含的不同内容的展示与否,实现了类似互动式小说的展现效果。这同时也是泛用切换式按钮的一个标准范例。

在这段代码中,我们在折叠内部放置了一个不展示的span.trigger.n1块。这个span块的作用是给按钮一个独特的“验证编号”,它在标明出“这是一个按钮”的同时,还能够避免在页面中存在多个同样结构的按钮时出现串线问题。当然,你也可以通过用一个带有验证类的div块包裹住整个折叠的方式实现类似的效果。这里,我们使用这样的一段代码判断选定的切换式按钮是否已经开启:

:root:has(.collapsible-block-unfolded[style=""] span.trigger.n1, .collapsible-block-unfolded[style="display: block;"] span.trigger.n1) .element {
  /* 当含有span.trigger.n1的折叠/切换式按钮被开启时,为.element元素设置的样式。 */
}

从章节1.2中我们知道,div.collapsible-block-unfolded的内联样式会在折叠打开的前后发生变化,因此,我们可以使用属性选择器(参考这里)匹配其style属性(即内联样式)的内容。根据打开折叠的次数不同,其内联样式在打开时有style=""style="display: block;"两种可能。由于在传入:has()伪类的选择器列表中,只要有一个选择器存在于该伪类修饰的元素内部,这一段选择器就会进入生效状态,因此,我们可以将两种可能出现的内联样式都用属性选择器的形式填入选择器里列表内部,并用后代选择器锁定用于验证的span元素。

需要注意的是,被:has()伪类修饰的元素需要同时是切换按钮待设置样式元素.element的父元素。为简单起见,此处使用:root伪类作为最上级元素。这一伪类与<html>元素等效,指代整个页面。也就是说,只要是页面中存在的元素都是:root伪类的子元素。当然,用满足上述条件的其他元素替换这一部分同样也没有问题。

在控制页面内容展示以否之外,泛用切换式按钮也可以用于调整页面元素的其他样式,例如为版式添加额外的“一键配色方案”,或是调整页面内容的排版与展示形式。下面是一个范例:

[[module css]]
/* 隐藏用于标识触发的span元素 */
p:has(span.trigger) { display: none; }
/* 在含有对应触发的折叠打开时,将整个页面中的blockquote背景色调整为(73, 117, 104)。 */
:root:has(.collapsible-block-unfolded[style=""] span.trigger.n2, .collapsible-block-unfolded[style="display: block;"] span.trigger.n2) :is(div.blockquote, blockquote) {
  background-color: rgb(73, 117, 104);
}
[[/module]]
 
[[collapsible show="切换为亮色引用框" hide="切换为暗色引用框"]]
[[span class="trigger n2"]]@@ @@[[/span]]
[[/collapsible]]

这段代码的变体被用于伯纳斯福音 > 第一章 > style.css的代码运行模拟过程。其实现原理与上面的标准范例相同,故不再加以额外解释。

3. 应用与技术拓展

3.1 翻页书

翻页书是深林 版式支持的默认组件之一。这一组件引入了切换式按钮,实现了在不影响页面链接的情况下顺次展示预先设定好的一组内容。省略装饰性代码后的实现如下:

[[module css]]
/* 初始情况下,.book类下的第3个及以后的子元素被隐藏,故只显示第一页与未展开的第一个折叠(右箭头)。 */
.book.n1 > div:nth-of-type(n+3) { display: none; }
/* 按下第一个按钮后,隐藏第1个及以前的子元素,展示第2~4个子元素,隐藏第5个及以后的子元素。 */
/* 第2~4个子元素分别为展开后的第一个折叠(左箭头),第二页与未展开的第二个折叠(右箭头)。 */
.book.n1:has(.collapsible-block-unfolded[style=""] span.page1, .collapsible-block-unfolded[style="display: block;"] span.page1) > div:nth-of-type(n):nth-of-type(-n+1) {
  display: none;
}
.book.n1:has(.collapsible-block-unfolded[style=""] span.page1, .collapsible-block-unfolded[style="display: block;"] span.page1) > div:nth-of-type(n+2):nth-of-type(-n+4) {
  display: block;
}
.book.n1:has(.collapsible-block-unfolded[style=""] span.page1, .collapsible-block-unfolded[style="display: block;"] span.page1) > div:nth-of-type(n+5) {
  display: none;
}
/* 按下第2个按钮后,隐藏第3个及以前的子元素,展示第4~6个子元素,隐藏第7个及以后的子元素。 */
.book.n1:has(.collapsible-block-unfolded[style=""] span.page2, .collapsible-block-unfolded[style="display: block;"] span.page2) > div:nth-of-type(n):nth-of-type(-n+3) {
  display: none;
}
.book.n1:has(.collapsible-block-unfolded[style=""] span.page2, .collapsible-block-unfolded[style="display: block;"] span.page2) > div:nth-of-type(n+4):nth-of-type(-n+6) {
  display: block;
}
.book.n1:has(.collapsible-block-unfolded[style=""] span.page2, .collapsible-block-unfolded[style="display: block;"] span.page2) > div:nth-of-type(n+6) {
  display: none;
}
[[/module]]
 
[[div class="blockquote wide book n1" style="height: 20em;"]]
[[div class="book-content"]]
这是第一页,是//div.book//下的第1个子元素。
[[/div]]
[[div class="paging"]]
[[collapsible show=" " hide=" "]]
[[span class="page1"]]@@ @@[[/span]]
[[/collapsible]] [!-- 这个按钮是第2个子元素 --]
[[/div]]
[[div class="book-content"]]
这是第二页,是//div.book//下的第3个子元素。
[[/div]]
[[div class="paging"]]
[[collapsible show=" " hide=" "]]
[[span class="page2"]]@@ @@[[/span]]
[[/collapsible]] [!-- 这个按钮是第4个子元素 --]
[[/div]]
[[div class="book-content"]]
这是第三页,是//div.book//下的第5个子元素。
[[/div]]
[[/div]]

这是第一页。

这是第二页。

这是第三页。

这一组件的本质是利用多个经过装饰后的泛用切换式按钮实现的翻页组件。其工作流程如下表所示:

条件 第一页 1~2页间折叠 第二页 2~3页间折叠 第三页
未打开折叠 展示 未打开(→) 隐藏 隐藏 隐藏
打开第一个折叠 隐藏 已打开(←) 展示 未打开(→) 隐藏
打开第二个折叠 隐藏 隐藏 隐藏 已打开(←) 展示

需要注意的是,在批量展示/隐藏子div块时,本组件的代码利用了:nth-of-type()伪类(参考这里)简化页面数较多时的展示状态控制。这一代码的引入主要是用于便利页面增加时CSS控制代码的拓展流程。

3.2 “跳杀”模块

在1.2中我们没有提到的一点是,每当你打开一个折叠时,div.collapsible-block-content的内联样式都会在一小段时间内包含一个用于调整透明度的opacity参数。因此,我们可以根据这一机制做出基于折叠块的jumpscare效果:

[[module css]]
/* 这是一个大白块 */
.white {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  background-color: white;
  display: none;
}
/* 带有opacity参数的折叠内容中的大白块将会展示。 */
.collapsible-block-content[style*="opacity"] .white{
  display: block;
}
[[/module]]
 
[[collapsible show="点击此处屏幕将会短暂变白" hide="关闭"]]
[[div class="white"]]
[[/div]]
[[/collapsible]]

这一实现方式无需面对:has()伪类固有的兼容性问题,但缺点在于其只能影响放置于折叠内部的内容,且其透明度同样会随父元素的变化而变化。而使用:has()伪类实现的,具有类似功能的变体就不会受到这一问题的影响。其实现如下:

[[module css]]
/* 默认隐藏的大白块。 */
.white { display: none; }
/* 打开折叠的一瞬间,大白块将会显示。 */
:root:has(.collapsible-block-content[style*="opacity"] span.trigger.n3) .white{
  display: block;
}
[[/module]]
 
[[collapsible show="点击此处屏幕将会短暂变白" hide="关闭"]]
[[span class="trigger n3"]]@@ @@[[/span]]
[[/collapsible]]
 
[[div class="white"]]
[[/div]]

就像这样,短暂出现的白块的透明度并没有受到折叠透明度的影响。你同样可以用类似的代码修改页面中的其他元素,此处不再赘述。

3.3 适配性与无障碍制作

使用:has()伪类制作的互动性功能无疑会导致部分用户无法得到完整的阅读体验。为了避免他们在看到一团乱麻的页面之后愤怒地点下downvote,如果你可以为这些用户专门定做一个导向无障碍模式的警告信息,甚至是重新编排使用:has()部分的页面结构,那就可以避免大多数此类问题的发生。我们一般用@supports规则来检查用户浏览器是否支持某一伪类:

[[module css]]
/* 一般情况下隐藏警告。 */
.has-warning {display: none;}
/* 如果不支持涉及:has()的选择器,就展示警告部分。 */
@supports not selector(:has(.collapsible-block-unfolded[style="display: block;"] span.trigger)) {
  .has-warning {display: block;}
  /* 如果你需要做其余的兼容性适配,放在这里。 */
}
[[/module]]
 
[[div class="error-block has-warning"]]
如果您看到了这段文字,那么您使用的浏览器很可能并不支持本文章的全部展示效果。
 
如果您希望体验完整的文章内容,请换用电脑端或升级到浏览器的最新版本后再进行阅读。
[[/div]]

如果您看到了这段文字,那么您使用的浏览器很可能并不支持本文章的全部展示效果。

如果您希望体验完整的文章内容,请换用电脑端或升级到浏览器的最新版本后再进行阅读。

3.4 :has()伪类的其他应用(内嵌式沙盒加密组件)

:has()伪类可以配合Listusers模块,用来在公开沙盒中隐藏你的页面内容。其代码如下:

[[module css]]
#page-content {display: none;}
#page-content:has(div.n0000000),
#page-content:has(div.n0000001) {
  display: block;
}
[[/module]]
 
[[module Listusers users="."]]
[[div class="n%%number%%"]]
@@ @@
[[/div]]
[[/module]]

将上面这段代码复制到你需要隐藏其内容的沙盒中的任何位置(为了不影响你的编辑,建议复制到顶部或者底部),然后用你的WikidotID1替换掉module css里的七位数字,这一模块就会开始工作。这一实现方式比起直接将全部内容嵌套在Listusers模块中的优势在于其不会影响到脚注等文章内容的解析。你可以按照上面的方式复制并修改module css中的#page-content:has(div.n0000000),部分以让更多人可以阅读你的页面。

需要注意的是,如果你的浏览器不支持:has()伪类,你将无论如何也无法看到页面中的内容。

除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License