[feat] toc

This commit is contained in:
xaoxuu 2024-02-05 10:03:55 +08:00
parent e1d1d8451d
commit 830ee9fd3b
16 changed files with 289 additions and 263 deletions

View File

@ -62,12 +62,14 @@ site_tree:
# -- 列表类页面 -- # # -- 列表类页面 -- #
# 主页配置 # 主页配置
home: home:
leftbar: welcome, recent, timeline leftbar: welcome, recent
rightbar: timeline
# 博客列表页配置 # 博客列表页配置
index_blog: index_blog:
base_dir: blog # 只影响自动生成的页面路径 base_dir: blog # 只影响自动生成的页面路径
menu_id: post # 未在 front-matter 中指定 menu_id 时layout 为 post 的页面默认使用这里配置的 menu_id menu_id: post # 未在 front-matter 中指定 menu_id 时layout 为 post 的页面默认使用这里配置的 menu_id
leftbar: welcome, recent, timeline # for categories/tags/archives leftbar: welcome, recent # for categories/tags/archives
rightbar: timeline
nav_tabs: # 近期发布 分类 标签 专栏 归档 and ... nav_tabs: # 近期发布 分类 标签 专栏 归档 and ...
# '朋友文章': /friends/rss/ # '朋友文章': /friends/rss/
# 博客专栏列表页配置 # 博客专栏列表页配置
@ -78,34 +80,40 @@ site_tree:
index_wiki: index_wiki:
base_dir: wiki # 只影响自动生成的页面路径 base_dir: wiki # 只影响自动生成的页面路径
menu_id: wiki # 未在 front-matter 中指定 menu_id 时layout 为 wiki 的页面默认使用这里配置的 menu_id menu_id: wiki # 未在 front-matter 中指定 menu_id 时layout 为 wiki 的页面默认使用这里配置的 menu_id
leftbar: toc, ghissues, related, recent # for wiki leftbar: ghissues, related, recent # for wiki
rightbar: timeline
nav_tabs: nav_tabs:
# 'more': https://github.com/xaoxuu # 'more': https://github.com/xaoxuu
# -- 内容类页面 -- # # -- 内容类页面 -- #
# 博客文章内页配置 # 博客文章内页配置
post: post:
menu_id: post # 未在 front-matter 中指定 menu_id 时layout 为 post 的页面默认使用这里配置的 menu_id menu_id: post # 未在 front-matter 中指定 menu_id 时layout 为 post 的页面默认使用这里配置的 menu_id
leftbar: toc, related, ghrepo, ghissues, recent # for pages using 'layout:post' leftbar: related, ghrepo, ghissues, recent # for pages using 'layout:post'
rightbar: toc
# 博客专栏文章内页配置 # 博客专栏文章内页配置
topic: topic:
menu_id: post menu_id: post
# 文档内页配置 # 文档内页配置
wiki: wiki:
menu_id: wiki # 未在 front-matter 中指定 menu_id 时layout 为 wiki 的页面默认使用这里配置的 menu_id menu_id: wiki # 未在 front-matter 中指定 menu_id 时layout 为 wiki 的页面默认使用这里配置的 menu_id
leftbar: toc, ghissues, related, recent # for wiki leftbar: tree, ghissues, related, recent # for wiki
rightbar: toc
# 作者信息配置 # 作者信息配置
author: author:
base_dir: author # 只影响自动生成的页面路径 base_dir: author # 只影响自动生成的页面路径
menu_id: post menu_id: post
leftbar: recent, timeline leftbar: recent, timeline
rightbar:
# 错误页配置 # 错误页配置
error_page: error_page:
menu_id: post menu_id: post
'404': '/404.html' '404': '/404.html'
leftbar: recent, timeline leftbar: recent, timeline
rightbar:
# 其它自定义页面配置 layout: page # 其它自定义页面配置 layout: page
page: page:
leftbar: toc, recent, timeline leftbar: recent
rightbar: toc, timeline

View File

@ -21,13 +21,15 @@ recent:
rss: # /atom.xml # npm i hexo-generator-feed rss: # /atom.xml # npm i hexo-generator-feed
limit: 10 # Count of posts limit: 10 # Count of posts
# TOC (valid only in layout:post/wiki) tree:
layout: tree
# TOC (valid only in rightbar)
toc: toc:
layout: toc layout: toc
list_number: false list_number: true
min_depth: 2 min_depth: 1
max_depth: 5 max_depth: 6
fallback: # recent # Use a backup widget when toc does not exist.
collapse: false # true / false / auto collapse: false # true / false / auto
# github user info # github user info

View File

@ -1,5 +1,5 @@
<div class='float-panel mobile-only blur' style='display:none'> <div class='float-panel mobile-only blur' style='display:none'>
<button type='button' class='leftbar-toggle mobile' onclick='sidebar.toggle()'> <button type='button' class='leftbar-toggle mobile' onclick='leftbar.toggle()'>
<%- icon('default:leftbar') %> <%- icon('default:leftbar') %>
</button> </button>
</div> </div>

View File

@ -47,6 +47,8 @@ if (page.leftbar == null) {
if (typeof page.leftbar == 'string') { if (typeof page.leftbar == 'string') {
page.leftbar = page.leftbar.replace(/ /g, '').split(','); page.leftbar = page.leftbar.replace(/ /g, '').split(',');
} }
function layoutTitle(main, url, sub) { function layoutTitle(main, url, sub) {
var el = ''; var el = '';
el += '<a class="title" href="' + url_for(url || "/") + '">'; el += '<a class="title" href="' + url_for(url || "/") + '">';

View File

@ -0,0 +1,79 @@
<%
const wiki = theme.wiki.tree[page.wiki]
const topic = theme.topic.tree[page.topic]
if (page.rightbar == null) {
const { site_tree } = theme
var sidebar
if (is_home()) {
sidebar = site_tree.home.rightbar
} else if (is_category() || is_tag() || is_archive() || ['categories', 'tags', 'archives'].includes(page.layout)) {
sidebar = site_tree.index_blog.rightbar
} else if (page.layout === 'index_topic') {
// 专栏列表页等同于博客列表页
sidebar = site_tree.index_blog.rightbar
} else if (page.topic?.length > 0) {
// 专栏文章内页等同于普通文章内页
sidebar = site_tree.post.rightbar
} else if (page.layout === 'index_wiki') {
sidebar = site_tree.index_wiki.rightbar
} else if (page.wiki?.length > 0) {
sidebar = site_tree.wiki.rightbar
} else if (page.layout === '404') {
sidebar = site_tree.error_page.rightbar
} else if (page.layout === 'page') {
sidebar = site_tree.page.rightbar
} else if (page.layout === 'post') {
sidebar = site_tree.post.rightbar
} else if (page.layout == null) {
sidebar = site_tree.page.rightbar
} else {
sidebar = []
}
if (topic?.rightbar) {
sidebar = topic.rightbar
}
if (wiki?.rightbar) {
sidebar = wiki.rightbar
}
page.rightbar = sidebar
}
// parse array string
if (typeof page.rightbar == 'string') {
page.rightbar = page.rightbar.replace(/ /g, '').split(',');
}
function layoutWidgets() {
var el = '';
el += '<div class="widgets">';
if (page.rightbar) {
page.rightbar.forEach((w, i) => {
let name = ''
let widget = {}
if (typeof w == 'string') {
name = w
} else if (typeof w == 'object' && w.override) {
name = w.override
}
if (name in theme.widgets) {
Object.assign(widget, theme.widgets[name])
}
if (typeof w == 'object' && (w.override || w.layout)) {
Object.assign(widget, w)
}
if (widget && widget.layout) {
el += partial('../widgets/' + widget.layout, {item: widget})
}
});
}
el += '</div>';
return el;
}
%>
<%- layoutWidgets() %>

View File

@ -1,16 +1,13 @@
<% <%
const proj = theme.wiki.tree[page.wiki]
var hasTOC = true
function layoutTocBody() { function layoutTocBody() {
if (toc(page.content).length > 0) { if (toc(page.content).length > 0) {
hasTOC = true
return toc(page.content, { return toc(page.content, {
list_number: item.list_number, list_number: item.list_number,
min_depth: item.min_depth, min_depth: item.min_depth,
max_depth: item.max_depth max_depth: item.max_depth
}); })
} }
hasTOC = false
return '' return ''
} }
@ -22,94 +19,18 @@ function layoutTocHeader(title) {
return el return el
} }
function layoutDocTree(pages) {
var el = ''
for (let p of pages) {
if (p.title == null || p.title.length == 0) {
continue
}
let isActive = ''
if (p.path === page.path) {
isActive += ' active'
}
el += `<div class="doc-tree${isActive}">`
if (proj.pages.length > 1) {
let href = url_for(p.path);
if (p.is_homepage) {
href += '#start'
}
el += `<a class="doc-tree-link${isActive}" href="${href}">`
el += `<span class="toc-text">${p.title}</span>`
if (isActive.length > 0) {
el += icon('default:bookmark.active')
}
el += `</a>`
}
if (p.path === page.path) {
el += layoutTocBody()
}
el += `</div>`
}
return el
}
function layoutDiv(fallback) { function layoutDiv(fallback) {
var type = '' const tocBody = layoutTocBody()
if (proj?.pages) { if (tocBody.length == 0) {
type = proj.pages.length > 1 ? 'multi' : 'single' return ''
} else {
let toc_content = toc(page.content)
if (toc_content && toc_content.length > 0) {
type = 'single'
}
} }
var el = '' var el = ''
if (type.length > 0) { el += `<widget class="widget-wrapper${scrollreveal(' ')} toc" id="data-toc" collapse="${item.collapse}">`
el += `<widget class="widget-wrapper${scrollreveal(' ')} toc ${type}" id="data-toc" collapse="${item.collapse}">` el += layoutTocHeader()
if (proj) { el += `<div class="widget-body">`
// wiki 布局 el += tocBody
if (proj.sections && proj.sections.length > 0 && proj.pages.length > 1) { // 多 pages el += `</div>`
for (let sec of proj.sections) { el += `</widget>`
if (sec.pages.length == 0) {
continue
}
if (sec.title?.length > 0) {
el += layoutTocHeader(sec.title)
}
el += `<div class="widget-body fs14">`
el += layoutDocTree(sec.pages)
el += `</div>`
}
} else { // 单 page
if (proj.pages.length == 1) {
el += layoutTocHeader()
}
el += `<div class="widget-body fs14">`
el += layoutDocTree(proj.pages)
el += `</div>`
if (hasTOC == false) {
return ''
}
}
} else {
// post 布局
el += layoutTocHeader()
el += `<div class="widget-body fs14">`
el += `<div class="doc-tree active">`
el += layoutTocBody()
el += `</div>`
el += `</div>`
if (hasTOC == false) {
return ''
}
}
el += `</widget>`
} else if (item.fallback) {
const fallback = theme.widgets[item.fallback]
el += partial(fallback.layout, {item: fallback})
}
return el return el
} }

View File

@ -0,0 +1,68 @@
<%
const proj = theme.wiki.tree[page.wiki]
function layoutTocHeader(title) {
var el = ''
el += `<div class="widget-header dis-select">`
el += `<span class="name">${title || __("meta.toc")}</span>`
el += `</div>`
return el
}
function layoutDocTree(pages) {
var el = ''
for (let p of pages) {
if (p.title == null || p.title.length == 0) {
continue
}
let isActive = ''
if (p.path === page.path) {
isActive += ' active'
}
if (proj.pages.length > 1) {
let href = url_for(p.path);
if (p.is_homepage) {
href += '#start'
}
el += `<a class="link${isActive}" href="${href}">`
el += `<span class="toc-text">${p.title}</span>`
if (isActive.length > 0) {
el += icon('default:bookmark.active')
}
el += `</a>`
}
}
return el
}
function layoutDiv(fallback) {
if (proj == null) {
return ''
}
if (proj.pages == null || proj.pages.length == 0) {
return ''
}
if (proj.sections == null || proj.sections.length == 0) {
return ''
}
var el = ''
el += `<widget class="widget-wrapper${scrollreveal(' ')} post-list">`
for (let sec of proj.sections) {
if (sec.pages.length == 0) {
continue
}
if (sec.title?.length > 0) {
el += layoutTocHeader(sec.title)
}
el += `<div class="widget-body fs14">`
el += layoutDocTree(sec.pages)
el += `</div>`
}
el += `</widget>`
return el
}
%>
<%- layoutDiv() %>

View File

@ -45,7 +45,7 @@ html += `<html lang="${page.lang}">`
html += `<div class="l_body ${page_type} ${article_type}" id="start" layout="${page.layout}" ${indent ? 'text-indent' : ''}>` html += `<div class="l_body ${page_type} ${article_type}" id="start" layout="${page.layout}" ${indent ? 'text-indent' : ''}>`
html += `<aside class="l_left">` html += `<aside class="l_left">`
html += `<div class="leftbar-container${theme.style.leftbar?.blur ? ' leftbar-blur' : ''}">` html += `<div class="leftbar-container${theme.style.leftbar?.blur ? ' leftbar-blur' : ''}">`
html += partial('_partial/sidebar/index') html += partial('_partial/sidebar/index_leftbar')
html += `</div>` html += `</div>`
html += `</aside>` html += `</aside>`
html += `<div class="l_main" id="main">` html += `<div class="l_main" id="main">`
@ -55,6 +55,9 @@ html += `<html lang="${page.lang}">`
html += partial('_partial/menubtn') html += partial('_partial/menubtn')
html += `<div class="main-mask" onclick="sidebar.toggle()"></div>` html += `<div class="main-mask" onclick="sidebar.toggle()"></div>`
html += `</div>` html += `</div>`
html += `<aside class="l_right">`
html += partial('_partial/sidebar/index_rightbar')
html += `</aside>`
html += `</div>` html += `</div>`
html += `<div class="scripts">` html += `<div class="scripts">`
html += partial('_partial/scripts/index') html += partial('_partial/scripts/index')

View File

@ -5,7 +5,7 @@
margin: auto margin: auto
font-size: var(--fsp) font-size: var(--fsp)
.l_body .l_left .l_body aside
z-index: 8 z-index: 8
width: var(--width-left) width: var(--width-left)
flex-shrink: 0 flex-shrink: 0
@ -13,6 +13,12 @@
position: -webkit-sticky position: -webkit-sticky
top: 8px top: 8px
.l_body .l_right
height: 100%
.widgets
height: 100%
overflow visible
// //
@media screen and (max-width: $device-mobile-max) @media screen and (max-width: $device-mobile-max)
.mobile-only .mobile-only

View File

@ -26,3 +26,6 @@
width: auto width: auto
transform: scale(1.2) transform: scale(1.2)
.widget-wrapper.post-list
.widget-body+.widget-header
margin-top: 28px

View File

@ -0,0 +1,90 @@
//
.widget-wrapper.toc .toc
--fsp: $fsp2
padding: 0
margin: 0
position relative
list-style: none
.toc-child
padding-left: 1em
li
margin: 2px 0
list-style: none
a
padding: 4px var(--gap-l)
color: var(--text-p2)
display: block
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
position relative
&:before,&:after
position: absolute
left: 4px
width: 8px
height: 8px
top: 'calc(%s - 4px)' % 50%
border-radius: 8px
background: var(--block)
background: $color-theme
&:after
opacity 0
animation: wave 1s linear infinite
@keyframes wave
from
transform: scale(1)
opacity 1
to
transform: scale(2)
opacity 0
//
.l_right .widget-wrapper.toc
position: sticky
position: -webkit-sticky
top: 32px
max-height: 'calc(90vh - 2 * %s)' % @top
overflow: scroll
//
.widget-wrapper.toc .toc
.toc-item
font-weight: 500
--fsp: $fsp1
.toc-item .toc-item
font-weight: 400
--fsp: $fsp2
//
.widget-wrapper.toc .toc a.toc-link.active
color: var(--text-p0)
&:before,&:after
content: ''
.widget-wrapper.toc .toc a.toc-link:hover
color: $color-theme
//
.widget-wrapper.toc[collapse='true']
.toc-item a.toc-link+ol
display: none
//
.toc a.toc-link.active+ol
display: block
//
.widget-wrapper.toc[collapse='auto']
.toc-item a.toc-link+ol
display: none
//
.toc a.toc-link.active+ol
display: block
//
&:hover a.toc-link+ol
display: block
//
.widget-wrapper.toc[collapse='true'] ol:has(> .toc-item a.active)
display: block
.widget-wrapper.toc[collapse='auto'] ol:has(> .toc-item a.active)
display: block

View File

@ -1,3 +0,0 @@
.widget-wrapper.toc.single .doc-tree
&.active>.toc
border-left: 2px solid var(--alpha50)

View File

@ -1,92 +0,0 @@
.toc-item
margin-top: 2px
.toc-child
margin-top: 2px
.widget-wrapper.toc .widget-body
margin-top: 0
ul ul, ul ol
padding-left: 0
ol ul, ol ol
padding-left: 0
.toc
padding: 0
margin: 0
padding-left: 0.25rem
.toc-item .toc-link
padding: 0.5rem
font-weight: 500
font-size: $fs-13
color: var(--text-p1)
.toc-child .toc-item .toc-link
padding: 0.25rem 0.5rem 0.25rem 1.3rem
font-weight: 400
color: var(--text-p2)
.toc-child .toc-child .toc-item .toc-link
padding-left: 2.1rem
font-size: $fs-12
color: var(--text-p3)
.toc-child .toc-child .toc-child .toc-item .toc-link
padding-left: 2.9rem
.widget-wrapper.toc .toc-item
color: var(--text-p2)
font-size: $fs-12
padding: 0
list-style: none
&.active
background: var(--alpha50)
border-left-color: @color
.toc-child .toc-item
padding: 0
.widget-wrapper.toc a.toc-link
color: inherit
display: block
line-height: 1.2
border-radius: $border-bar
position: relative
trans1 background
&:before
content: ''
position: absolute
left: -6px
top: 'calc(50% - %s)' % 6px
bottom: 'calc(50% - %s)' % 6px
width: 2px
border-radius: 2px
background: var(--text)
visibility: hidden
&.active
background: var(--alpha50)
&:before
visibility: visible
&:hover
background: var(--alpha100)
//
.widget-wrapper.toc[collapse='true']
.toc-item a.toc-link+ol
display: none
//
.toc a.toc-link.active+ol
display: block
//
.widget-wrapper.toc[collapse='auto']
.toc-item a.toc-link+ol
display: none
//
.toc a.toc-link.active+ol
display: block
//
&:hover a.toc-link+ol
display: block
//
.widget-wrapper.toc[collapse='true'] ol:has(> .toc-item a.active)
display: block
.widget-wrapper.toc[collapse='auto'] ol:has(> .toc-item a.active)
display: block

View File

@ -1,62 +0,0 @@
.doc-tree+.doc-tree
margin-top: 2px
.widget-wrapper.toc.multi
// .widget-header
// color: $color-theme
// filter: brightness(75%)
.widget-body+.widget-header
margin-top: 28px
//
.widget-wrapper.toc.multi .doc-tree
border-radius: $border-bar
overflow: hidden
a.doc-tree-link
color: var(--text-p1)
padding: 0 16px
display: flex
min-height: 32px
justify-content: space-between
align-items: center
font-size: $fs-14
position: relative
trans1 background
.toc-text
txt-ellipsis()
svg,img
flex-shrink: 0
width: 1em
height: 1em
transform: scale(1.2)
&:after
position: absolute
right: .5rem
&:only-child
&.active
background: var(--alpha50)
&:hover
background: var(--alpha100)
&:hover
background: var(--alpha100)
//
.widget-wrapper.toc.multi .doc-tree.active
a.doc-tree-link
background: var(--alpha100)
font-weight: 500
&:hover:after
content: none
>.toc
padding: 4px 10px
border: 1px solid var(--alpha50)
background: var(--alpha20)
border-bottom-left-radius: $border-bar
border-bottom-right-radius: $border-bar
a.toc-link:before
left: -7px
width: 4px
background: var(--text-p2)
a.toc-link:hover
background: var(--alpha100)

View File

View File

@ -122,7 +122,8 @@ const init = {
const highlightItem = $('#data-toc a.toc-link[href="' + encodeURI(link) + '"]') const highlightItem = $('#data-toc a.toc-link[href="' + encodeURI(link) + '"]')
if (highlightItem.length > 0) { if (highlightItem.length > 0) {
highlightItem.addClass("active") highlightItem.addClass("active")
const e0 = document.querySelector('.widgets') const e0 = document.querySelector('#data-toc')
console.log('e0', e0);
const e1 = document.querySelector('#data-toc a.toc-link[href="' + encodeURI(link) + '"]') const e1 = document.querySelector('#data-toc a.toc-link[href="' + encodeURI(link) + '"]')
const offsetBottom = e1.getBoundingClientRect().bottom - e0.getBoundingClientRect().bottom + 200 const offsetBottom = e1.getBoundingClientRect().bottom - e0.getBoundingClientRect().bottom + 200
const offsetTop = e1.getBoundingClientRect().top - e0.getBoundingClientRect().top - 64 const offsetTop = e1.getBoundingClientRect().top - e0.getBoundingClientRect().top - 64