breaking changes: 移除博天api

breaking changes: 移除 waline 的 avatar 和 avatar cdn 配置
feat: anchor 不再限制 post 頁開啟,可以在任何頁面開啟
feat: 文章標題支持點擊跳轉到此標題開始閲讀 closed #653
feat: toc可以設置全部展開 closed #709
feat: 增加 新的評論系統 giscus
feat: 支持新的評論名寫法,主題會處理評論名字大小寫,舊的會兼容
feat: 友情鏈接列表增加 fetch url 獲取
improvement: 鼠標移到最新評論內容,增加 title 顯示
fix: 修復 rightside 遮擋內容,導致內容無法點擊的 bug
fix: 修復 mermaid 在某些頁面(有元素 id 為 mermaid 時) 會無法加載的 bug
fix: 修復 搜索框不會自動 focus 的 bug
This commit is contained in:
Jerry 2021-11-14 17:50:11 +08:00
parent f7c50586ce
commit ffeab5e20c
30 changed files with 363 additions and 225 deletions

View File

@ -14,7 +14,7 @@
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
Demo: 👍 [Butterfly](https://butterfly.js.org/) || 🤞 [MYW](https://immyw.com/)
Demo: 👍 [Butterfly](https://butterfly.js.org/) || 🤞 [CrazyWong](https://crazywong.com/)
Docs: 📖 [Butterfly Docs](https://butterfly.js.org/posts/21cfbf15/)
@ -79,7 +79,7 @@ npm i hexo-theme-butterfly
- [x] Related articles
- [x] Displays outdated notice for a post
- [x] Share (AddThis/Sharejs/Addtoany)
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo)
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus)
- [x] Multiple Comment System Support
- [x] Online Chats (Chatra/Tidio/Daovoice/Gitter/Crisp)
- [x] Web analytics

View File

@ -14,7 +14,7 @@
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
預覽: 👍 [Butterfly](https://butterfly.js.org/) || 🤞 [MYW](https://immyw.com/)
預覽: 👍 [Butterfly](https://butterfly.js.org/) || 🤞 [CrazyWong](https://crazywong.com/)
文檔: 📖 [Butterfly Docs](https://butterfly.js.org/posts/21cfbf15/)
@ -79,7 +79,7 @@ theme: butterfly
- [x] 顯示相關文章
- [x] 過期文章提醒
- [x] 多種分享系統AddThis/Sharejs/Addtoany
- [X] 多種評論系統Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo
- [X] 多種評論系統Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus
- [x] 支持雙評論部署
- [x] 多種在線聊天Chatra/Tidio/Daovoice/Gitter/Crisp
- [x] 多種分析系統

View File

@ -171,6 +171,7 @@ toc:
post: true
page: false
number: true
expand: false
style_simple: false # for post
post_copyright:
@ -253,10 +254,7 @@ addtoany:
comments:
# Up to two comments system, the first will be shown as default
# Choose: Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo
use:
# - Valine
# - Disqus
use: # Valine,Disqus
text: true # Display the comment name next to the button
# lazyload: The comment system will be load when comment element enters the browser's viewport.
# If you set it to true, the comment count will be invalid
@ -308,8 +306,6 @@ valine:
# https://waline.js.org/
waline:
serverURL: # Waline server address url
avatar: monsterid # gravatar style https://zh-tw.gravatar.com/site/implement/images/#default-image
avatarCDN: # Gravatar CDN baseURL
bg: # waline background
visitor: false
option:
@ -341,6 +337,17 @@ twikoo:
visitor: false
option:
# Giscus
# https://giscus.app/
giscus:
repo:
repo_id:
category_id:
theme:
light: light
dark: dark
option:
# Chat Services
# --------------------------------------
@ -583,12 +590,11 @@ subtitle:
effect: true
# loop (循環打字)
loop: true
# source調用第三方服務
# source 調用第三方服務
# source: false 關閉調用
# source: 1 調用搏天 api 的隨機語錄(簡體)
# source: 2 調用一言網的一句話(簡體)
# source: 3 調用一句網(簡體)
# source: 4 調用今日詩詞(簡體)
# source: 1 調用一言網的一句話(簡體) https://hitokoto.cn/
# source: 2 調用一句網(簡體) http://yijuzhan.com/
# source: 3 調用今日詩詞(簡體) https://www.jinrishici.com/
# subtitle 會先顯示 source , 再顯示 sub 的內容
source: false
# 如果關閉打字效果subtitle 只會顯示 sub 的第一行文字
@ -836,6 +842,7 @@ CDN:
utterances:
twikoo:
waline:
giscus:
# share
addtoany:

View File

@ -119,5 +119,5 @@ script.
},
isPhotoFigcaption: !{theme.photofigcaption},
islazyload: !{theme.lazyload.enable},
isanchor: !{theme.anchor}
isAnchor: !{theme.anchor}
}

View File

@ -1,18 +1,57 @@
#article-container
.flink
if site.data.link
each i in site.data.link
if i.class_name
h2!= i.class_name
if i.class_desc
.flink-desc!=i.class_desc
.flink-list
each item in i.link_list
.flink-list-item
a(href=url_for(item.link) title=item.name target="_blank")
.flink-item-icon
img.no-lightbox(src=url_for(item.avatar) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.flink) + `'` alt=item.name )
.flink-item-name= item.name
.flink-item-desc(title=item.descr)= item.descr
if page.flink_url
script.
(()=>{
const replaceSymbol = (str) => {
return str.replace(/[\p{P}\p{S}]/gu, '-')
}
let result = ''
fetch('!{url_for(page.flink_url)}')
.then(response => response.json())
.then(str => {
for(let i = 0; i < str.length; i++){
const replaceClassName = replaceSymbol(str[i].class_name)
const className = str[i].class_name ? `<h2 id='${replaceClassName}'><a href="#${replaceClassName}" class="headerlink" title="${str[i].class_name}"></a>${str[i].class_name}</h2>` : ''
const classDesc = str[i].class_desc ? `<div class='flink-desc'>${str[i].class_desc}</div>` : ''
let listResult = ''
const lists = str[i].link_list
for(let j = 0; j < lists.length; j++){
listResult += `
<div class="flink-list-item">
<a href='${lists[j].link}' title='${lists[j].name}' target='_blank'>
<div class='flink-item-icon'>
<img class='no-lightbox' src='${lists[j].avatar}' onerror='this.onerror=null;this.src="!{url_for(theme.error_img.flink)}"' alt='${lists[j].name}' />
</div>
<div class='flink-item-name'>${lists[j].name}</div>
<div class='flink-item-desc' title='${lists[j].descr}'>${lists[j].descr}</div>
</a>
</div>`
}
result += `${className}${classDesc} <div class='flink-list'>${listResult}</div>`
}
document.querySelector('.flink').insertAdjacentHTML('afterbegin', result)
})
})()
else
if site.data.link
each i in site.data.link
if i.class_name
!=markdown(`## ${i.class_name}`)
if i.class_desc
.flink-desc!=i.class_desc
.flink-list
each item in i.link_list
.flink-list-item
a(href=url_for(item.link) title=item.name target="_blank")
.flink-item-icon
img.no-lightbox(src=url_for(item.avatar) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.flink) + `'` alt=item.name )
.flink-item-name= item.name
.flink-item-desc(title=item.descr)= item.descr
!= page.content

View File

@ -0,0 +1,49 @@
- const { repo, repo_id, category_id, option } = theme.giscus
- const themes = theme.giscus.theme
script.
function loadGiscus () {
let nowTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{themes.dark}' : '!{themes.light}'
const config = Object.assign({
src: '!{theme.CDN.giscus}',
'data-repo': '!{repo}',
'data-repo-id': '!{repo_id}',
'data-category-id': '!{category_id}',
'data-mapping': 'pathname',
'data-theme': nowTheme,
'data-reactions-enabled': '1',
crossorigin: 'anonymous',
async: true
},!{JSON.stringify(option)})
let ele = document.createElement('script')
for (let key in config) {
ele.setAttribute(key, config[key])
}
document.getElementById('giscus-wrap').insertAdjacentElement('afterbegin',ele)
}
function changeGiscusTheme () {
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{themes.dark}' : '!{themes.light}'
function sendMessage(message) {
const iframe = document.querySelector('iframe.giscus-frame');
if (!iframe) return;
iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app');
}
sendMessage({
setConfig: {
theme: theme
}
});
}
if ('!{theme.comments.use[0]}' === 'Giscus' || !!{theme.comments.lazyload}) {
if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('giscus-wrap'), loadGiscus)
else loadGiscus()
} else {
function loadOtherComment () {
loadGiscus()
}
}

View File

@ -33,6 +33,8 @@ hr
#twikoo-wrap
when 'Waline'
#waline-wrap
when 'Giscus'
#giscus-wrap
when 'Facebook Comments'
.fb-comments(data-colorscheme = theme.display_mode === 'dark' ? 'dark' : 'light'
data-numposts= theme.facebook_comments.pageSize || 10

View File

@ -16,5 +16,7 @@ each name in theme.comments.use
!=partial('includes/third-party/comments/twikoo', {}, {cache: true})
when 'Waline'
!=partial('includes/third-party/comments/waline', {}, {cache: true})
when 'Giscus'
!=partial('includes/third-party/comments/giscus', {}, {cache: true})
when 'Facebook Comments'
!=partial('includes/third-party/comments/facebook_comments', {}, {cache: true})

View File

@ -1,4 +1,4 @@
- const { serverURL, avatar, avatarCDN, visitor, option } = theme.waline
- const { serverURL, visitor, option } = theme.waline
script.
function loadWaline () {
@ -6,8 +6,6 @@ script.
const waline = new Waline(Object.assign({
el: '#waline-wrap',
serverURL: '!{serverURL}',
avatar: '#{avatar}',
avatarCDN: '!{avatarCDN || "https://sdn.geekzu.org/avatar/"}',
path: location.pathname,
visitor: !{visitor},
dark: 'html[data-theme="dark"]'

View File

@ -3,6 +3,7 @@ script.
const $mermaidWrap = document.querySelectorAll('#article-container .mermaid-wrap')
if ($mermaidWrap.length) {
window.runMermaid = () => {
window.loadMermaid = true
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
Array.from($mermaidWrap).forEach((item, index) => {
@ -17,7 +18,7 @@ script.
}
const loadMermaid = () => {
window.mermaid ? runMermaid() : getScript('!{theme.CDN.mermaid}').then(runMermaid)
window.loadMermaid ? runMermaid() : getScript('!{theme.CDN.mermaid}').then(runMermaid)
}
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)

View File

@ -49,7 +49,7 @@ script.
}
result += `<div class='content'>
<a class='comment' href='${array[i].url}'>${array[i].content}</a>
<a class='comment' href='${array[i].url}' title='${array[i].content}'>${array[i].content}</a>
<div class='name'><span>${array[i].nick}</span><time> / ${btf.diffDate(array[i].date, true)}</time></div>
</div></div>`
}

View File

@ -75,7 +75,7 @@ script.
}
result += `<div class='content'>
<a class='comment' href='${array[i].url}'>${array[i].content}</a>
<a class='comment' href='${array[i].url}' title='${array[i].content}'>${array[i].content}</a>
<div class='name'><span>${array[i].nick} / </span><time datetime="${array[i].date}">${btf.diffDate(array[i].date, true)}</time></div>
</div></div>`
}

View File

@ -60,7 +60,7 @@ script.
}
result += `<div class='content'>
<a class='comment' href='${array[i].url}'>${array[i].content}</a>
<a class='comment' href='${array[i].url}' title='${array[i].content}'>${array[i].content}</a>
<div class='name'><span>${array[i].nick} / </span><time datetime="${array[i].date}">${btf.diffDate(array[i].date, true)}</time></div>
</div></div>`
}

View File

@ -37,7 +37,7 @@ script.
}
result += `<div class='content'>
<a class='comment' href='${array[i].url}'>${array[i].content}</a>
<a class='comment' href='${array[i].url}' title='${array[i].content}'>${array[i].content}</a>
<div class='name'><span>${array[i].nick} / </span><time datetime="${array[i].date}">${btf.diffDate(array[i].date, true)}</time></div>
</div></div>`
}

View File

@ -16,13 +16,6 @@ script.
return content
}
const getIcon = (ava,mail) => {
if (ava) return ava
let defaultIcon = '!{ avatar ? `?d=${avatar}` : ''}'
let iconUrl = "!{avatarCDN ? avatarCDN : 'https://gravatar.loli.net/avatar/'}" + mail + defaultIcon
return iconUrl
}
const generateHtml = array => {
let result = ''
@ -36,7 +29,7 @@ script.
}
result += `<div class='content'>
<a class='comment' href='${array[i].url}'>${array[i].content}</a>
<a class='comment' href='${array[i].url}' title='${array[i].content}'>${array[i].content}</a>
<div class='name'><span>${array[i].nick} / </span><time datetime="${array[i].date}">${btf.diffDate(array[i].date, true)}</time></div>
</div></div>`
}
@ -59,7 +52,7 @@ script.
const walineArray = res.comments.map(e => {
return {
'content': changeContent(e.comment),
'avatar': getIcon(e.QQAvatar,e.mail),
'avatar': e.avatar,
'nick': e.nick,
'url': e.url + '#' + e.objectId,
'date': e.updatedAt,

View File

@ -3,38 +3,6 @@
case source
when 1
script.
function subtitleType () {
fetch('https://api.btstu.cn/yan/api.php?charset=utf-8&encode=json')
.then(response => response.json())
.then(data => {
if (!{effect}) {
const sub = !{JSON.stringify(subContent)}
sub.unshift(data.text)
window.typed = new Typed('#subtitle', {
strings: sub,
startDelay: 300,
typeSpeed: 150,
loop: !{loop},
backSpeed: 50,
})
} else {
document.getElementById('subtitle').innerHTML = data.text
}
})
}
if (!{effect}) {
if (typeof Typed === 'function') {
subtitleType()
} else {
getScript('!{url_for(theme.CDN.typed)}').then(subtitleType)
}
} else {
subtitleType()
}
when 2
script.
function subtitleType () {
fetch('https://v1.hitokoto.cn')
@ -67,7 +35,7 @@ case source
subtitleType()
}
when 3
when 2
script.
function subtitleType () {
getScript('https://yijuzhan.com/api/word.php?m=js').then(() => {
@ -99,7 +67,7 @@ case source
subtitleType()
}
when 4
when 3
script.
function subtitleType () {
getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js').then(() => {

View File

@ -1,4 +1,6 @@
- let tocNumber = page.toc_number !== undefined ? page.toc_number : theme.toc.number
- let tocExpand = page.toc_expand !== undefined ? page.toc_expand : theme.toc.expand
- let tocExpandClass = tocExpand ? 'is-expand' : ''
#card-toc.card-widget
.item-headline
@ -7,7 +9,7 @@
span.toc-percentage
if (page.encrypt == true)
.toc-content.toc-div-class(style="display:none")!=toc(page.origin, {list_number: tocNumber})
.toc-content.toc-div-class(class=tocExpandClass style="display:none")!=toc(page.origin, {list_number: tocNumber})
else
.toc-content!=toc(page.content, {list_number: tocNumber})
.toc-content(class=tocExpandClass)!=toc(page.content, {list_number: tocNumber})

View File

@ -1,6 +1,6 @@
{
"name": "hexo-theme-butterfly",
"version": "4.0.0-b9",
"version": "4.0.0-b10",
"description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json",
"scripts": {
@ -20,13 +20,13 @@
},
"bugs": {
"url": "https://github.com/jerryc127/hexo-theme-butterfly/issues",
"email": "i@immyw.com"
"email": "my@crazywong.com"
},
"dependencies": {
"hexo-renderer-stylus": "^2.0.1",
"hexo-renderer-pug": "^2.0.0"
},
"homepage": "https://butterfly.js.org/",
"author": "Jerry <i@immyw.com>",
"author": "Jerry <my@crazywong.com>",
"license": "Apache-2.0"
}

View File

@ -1,12 +1,18 @@
/**
* Butterfly
* CDN
* 1. Merge CDN
* 2. Capitalize the first letter of comment name
*/
'use strict'
hexo.extend.filter.register('before_generate', () => {
const themeConfig = hexo.theme.config
/**
* Merge CDN
*/
const defaultCDN = {
main_css: '/css/index.css',
main: '/js/main.js',
@ -24,6 +30,7 @@ hexo.extend.filter.register('before_generate', () => {
utterances: 'https://utteranc.es/client.js',
twikoo: 'https://cdn.jsdelivr.net/npm/twikoo/dist/twikoo.all.min.js',
waline: 'https://cdn.jsdelivr.net/npm/@waline/client/dist/Waline.min.js',
giscus: 'https://giscus.app/client.js',
// share
addtoany: 'https://static.addtoany.com/menu/page.js',
@ -101,4 +108,18 @@ hexo.extend.filter.register('before_generate', () => {
}
themeConfig.CDN = Object.assign(defaultCDN, deleteNullValue(themeConfig.CDN))
/**
* Capitalize the first letter of comment name
*/
let { use } = themeConfig.comments
if (typeof use === 'string') {
use = use.split(',')
}
const newArray = use.map(item => item.toLowerCase().replace(/^\S/, s => s.toUpperCase()))
themeConfig.comments.use = newArray
})

View File

@ -22,8 +22,6 @@ function timeLineFn (args, content) {
matches.push(match[2])
}
console.log(matches)
for (let i = 0; i < matches.length; i += 2) {
const tlChildTitle = hexo.render.renderSync({ text: matches[i], engine: 'markdown' })
const tlChildContent = hexo.render.renderSync({ text: matches[i + 1], engine: 'markdown' })

View File

@ -117,7 +117,7 @@ if hexo-config('enter_transitions')
#site-title,
#site-subtitle
animation: titlescale 1s
animation: titleScale 1s
#nav.show
animation: headerNoOpacity 1s
@ -187,7 +187,7 @@ if hexo-config('avatar.effect') == true
margin-top: 0
opacity: 1
@keyframes titlescale
@keyframes titleScale
0%
opacity: 0
transform: scale(.7)

View File

@ -32,6 +32,7 @@
--timeline-bg: $timeline-content-bg
--timeline-border-color: $timeline-border-color
--pseudo-hover: $pseudo-hover
--headline-presudo: #a0a0a0
body
position: relative

View File

@ -265,16 +265,17 @@
+maxWidth900()
max-height: calc(100vh - 140px)
.toc-child
display: none
&:not(.is-expand)
.toc-child
display: none
+maxWidth900()
display: block !important
+maxWidth900()
display: block !important
.toc-item
&.active
.toc-child
display: block
.toc-item
&.active
.toc-child
display: block
ol,
li

View File

@ -7,7 +7,7 @@ beautify()
font-size: unit(fontsize, 'px')
&:hover
padding-left: unit(fontsize + 12, 'px')
padding-left: unit(fontsize + 18, 'px')
h1,
h2,
@ -102,6 +102,32 @@ beautify()
font-family: Monaco, 'Ubuntu Mono', monospace
line-height: 1em
if hexo-config('anchor')
a.headerlink
&:after
@extend .fontawesomeIcon
float: right
color: var(--headline-presudo)
content: '\f0c1'
font-size: .95em
opacity: 0
transition: all .3s
&:hover
&:after
color: var(--pseudo-hover)
h1,
h2,
h3,
h4,
h5,
h6
&:hover
a.headerlink
&:after
opacity: 1
ol,
ul
ol,

View File

@ -7,11 +7,7 @@
transition: all .5s
#rightside-config-hide
transition: transform .4s
transform: translate(48px, 0)
&.show
transform: translate(0, 0) !important
display: none
& > div
& > button,
@ -39,3 +35,19 @@
+maxWidth900()
#hide-aside-btn
display: none
@keyframes rightside-item-in
0%
transform: translate(48px, 0)
100%
transform: translate(0, 0)
@keyframes rightside-item-out
0%
display: block
transform: translate(0, 0)
100%
display: none
transform: translate(48px, 0)

View File

@ -9,7 +9,6 @@
width: 600px
border-radius: 8px
background: var(--search-bg)
animation: titlescale .5s
+maxWidth768()
top: 0

View File

@ -34,7 +34,7 @@ document.addEventListener('DOMContentLoaded', function () {
open: () => {
btf.sidebarPaddingR()
document.body.style.overflow = 'hidden'
btf.fadeIn(document.getElementById('menu-mask'), 0.5)
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
document.getElementById('sidebar-menus').classList.add('open')
mobileSidebarOpen = true
},
@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', function () {
const $body = document.body
$body.style.overflow = ''
$body.style.paddingRight = ''
btf.fadeOut(document.getElementById('menu-mask'), 0.5)
btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
document.getElementById('sidebar-menus').classList.remove('open')
mobileSidebarOpen = false
}
@ -315,82 +315,69 @@ document.addEventListener('DOMContentLoaded', function () {
}
/**
* toc
* toc,anchor
*/
const tocFn = function () {
const $cardTocLayout = document.getElementById('card-toc')
const $cardToc = $cardTocLayout.getElementsByClassName('toc-content')[0]
const $tocLink = $cardToc.querySelectorAll('.toc-link')
const scrollFnToDo = function () {
const isToc = GLOBAL_CONFIG_SITE.isToc
const isAnchor = GLOBAL_CONFIG.isAnchor
const $article = document.getElementById('article-container')
const $tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
// main of scroll
window.tocScrollFn = function () {
return btf.throttle(function () {
const currentTop = window.scrollY || document.documentElement.scrollTop
scrollPercent(currentTop)
findHeadPosition(currentTop)
}, 100)()
}
window.addEventListener('scroll', tocScrollFn)
if (!($article && (isToc || isAnchor))) return
const scrollPercent = function (currentTop) {
const docHeight = $article.clientHeight
const winHeight = document.documentElement.clientHeight
const headerHeight = $article.offsetTop
const contentMath = (docHeight > winHeight) ? (docHeight - winHeight) : (document.documentElement.scrollHeight - winHeight)
const scrollPercent = (currentTop - headerHeight) / (contentMath)
const scrollPercentRounded = Math.round(scrollPercent * 100)
const percentage = (scrollPercentRounded > 100) ? 100 : (scrollPercentRounded <= 0) ? 0 : scrollPercentRounded
$tocPercentage.textContent = percentage
}
let $tocLink, $cardToc, scrollPercent, autoScrollToc, isExpand
// anchor
const isAnchor = GLOBAL_CONFIG.isanchor
const updateAnchor = function (anchor) {
if (window.history.replaceState && anchor !== window.location.hash) {
if (!anchor) anchor = location.pathname
const title = GLOBAL_CONFIG_SITE.title
window.history.replaceState({
url: location.href,
title: title
}, title, anchor)
if (isToc) {
const $cardTocLayout = document.getElementById('card-toc')
$cardToc = $cardTocLayout.getElementsByClassName('toc-content')[0]
$tocLink = $cardToc.querySelectorAll('.toc-link')
const $tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
isExpand = $cardToc.classList.contains('is-expand')
scrollPercent = currentTop => {
const docHeight = $article.clientHeight
const winHeight = document.documentElement.clientHeight
const headerHeight = $article.offsetTop
const contentMath = (docHeight > winHeight) ? (docHeight - winHeight) : (document.documentElement.scrollHeight - winHeight)
const scrollPercent = (currentTop - headerHeight) / (contentMath)
const scrollPercentRounded = Math.round(scrollPercent * 100)
const percentage = (scrollPercentRounded > 100) ? 100 : (scrollPercentRounded <= 0) ? 0 : scrollPercentRounded
$tocPercentage.textContent = percentage
}
}
window.mobileToc = {
open: () => {
$cardTocLayout.style.cssText = 'animation: toc-open .3s; opacity: 1; right: 55px'
},
window.mobileToc = {
open: () => {
$cardTocLayout.style.cssText = 'animation: toc-open .3s; opacity: 1; right: 55px'
},
close: () => {
$cardTocLayout.style.animation = 'toc-close .2s'
setTimeout(() => {
$cardTocLayout.style.cssText = "opacity:''; animation: ''; right: ''"
}, 100)
close: () => {
$cardTocLayout.style.animation = 'toc-close .2s'
setTimeout(() => {
$cardTocLayout.style.cssText = "opacity:''; animation: ''; right: ''"
}, 100)
}
}
}
// toc元素點擊
$cardToc.addEventListener('click', (e) => {
e.preventDefault()
const $target = e.target.classList.contains('toc-link')
? e.target
: e.target.parentElement
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI($target.getAttribute('href')).replace('#', ''))), 300)
if (window.innerWidth < 900) {
window.mobileToc.close()
}
})
// toc元素點擊
$cardToc.addEventListener('click', e => {
e.preventDefault()
const $target = e.target.classList.contains('toc-link')
? e.target
: e.target.parentElement
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI($target.getAttribute('href')).replace('#', ''))), 300)
if (window.innerWidth < 900) {
window.mobileToc.close()
}
})
const autoScrollToc = function (item) {
const activePosition = item.getBoundingClientRect().top
const sidebarScrollTop = $cardToc.scrollTop
if (activePosition > (document.documentElement.clientHeight - 100)) {
$cardToc.scrollTop = sidebarScrollTop + 150
}
if (activePosition < 100) {
$cardToc.scrollTop = sidebarScrollTop - 150
autoScrollToc = item => {
const activePosition = item.getBoundingClientRect().top
const sidebarScrollTop = $cardToc.scrollTop
if (activePosition > (document.documentElement.clientHeight - 100)) {
$cardToc.scrollTop = sidebarScrollTop + 150
}
if (activePosition < 100) {
$cardToc.scrollTop = sidebarScrollTop - 150
}
}
}
@ -398,7 +385,7 @@ document.addEventListener('DOMContentLoaded', function () {
const list = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
let detectItem = ''
const findHeadPosition = function (top) {
if ($tocLink.length === 0 || top === 0) {
if (top === 0) {
return false
}
@ -407,37 +394,50 @@ document.addEventListener('DOMContentLoaded', function () {
list.forEach(function (ele, index) {
if (top > btf.getEleTop(ele) - 80) {
currentId = '#' + encodeURI(ele.getAttribute('id'))
const id = ele.id
currentId = id ? '#' + encodeURI(id) : ''
currentIndex = index
}
})
if (detectItem === currentIndex) return
if (isAnchor) updateAnchor(currentId)
if (currentId === '') {
$cardToc.querySelectorAll('.active').forEach(i => { i.classList.remove('active') })
detectItem = currentIndex
return
}
if (isAnchor) btf.updateAnchor(currentId)
detectItem = currentIndex
$cardToc.querySelectorAll('.active').forEach(item => { item.classList.remove('active') })
const currentActive = $tocLink[currentIndex]
currentActive.classList.add('active')
if (isToc) {
$cardToc.querySelectorAll('.active').forEach(i => { i.classList.remove('active') })
setTimeout(() => {
autoScrollToc(currentActive)
}, 0)
if (currentId === '') {
return
}
let parent = currentActive.parentNode
const currentActive = $tocLink[currentIndex]
currentActive.classList.add('active')
for (; !parent.matches('.toc'); parent = parent.parentNode) {
if (parent.matches('li')) parent.classList.add('active')
setTimeout(() => {
autoScrollToc(currentActive)
}, 0)
if (isExpand) return
let parent = currentActive.parentNode
for (; !parent.matches('.toc'); parent = parent.parentNode) {
if (parent.matches('li')) parent.classList.add('active')
}
}
}
// main of scroll
window.tocScrollFn = function () {
return btf.throttle(function () {
const currentTop = window.scrollY || document.documentElement.scrollTop
isToc && scrollPercent(currentTop)
findHeadPosition(currentTop)
}, 100)()
}
window.addEventListener('scroll', tocScrollFn)
}
/**
@ -473,12 +473,20 @@ document.addEventListener('DOMContentLoaded', function () {
}
// handle some cases
typeof utterancesTheme === 'function' && utterancesTheme()
typeof changeGiscusTheme === 'function' && changeGiscusTheme()
typeof FB === 'object' && window.loadFBComment()
window.DISQUS && document.getElementById('disqus_thread').children.length && setTimeout(() => window.disqusReset(), 200)
typeof runMermaid === 'function' && window.runMermaid()
},
showOrHideBtn: () => { // rightside 點擊設置 按鈕 展開
document.getElementById('rightside-config-hide').classList.toggle('show')
const target = document.getElementById('rightside-config-hide')
if (window.rightSideIn) {
window.rightSideIn = false
btf.animateOut(target, 'rightside-item-out 0.5s')
} else {
window.rightSideIn = true
btf.animateIn(target, 'rightside-item-in 0.5s')
}
},
scrollToTop: () => { // Back to top
btf.scrollToDest(0, 500)
@ -749,7 +757,7 @@ document.addEventListener('DOMContentLoaded', function () {
toggleCardCategory()
}
GLOBAL_CONFIG_SITE.isToc && tocFn()
scrollFnToDo()
GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex()
addHighlightTool()
GLOBAL_CONFIG.isPhotoFigcaption && addPhotoFigcaption()

View File

@ -3,9 +3,10 @@ window.addEventListener('load', () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
document.querySelector('#algolia-search .search-dialog').style.display = 'block'
document.querySelector('#algolia-search .ais-SearchBox-input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
btf.animateIn(document.getElementById('search-mask'), 'to_show 0.5s')
btf.animateIn(document.querySelector('#algolia-search .search-dialog'), 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
@ -19,10 +20,8 @@ window.addEventListener('load', () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
$searchDialog.style.animation = 'search_close .5s'
setTimeout(() => { $searchDialog.style.cssText = "display: none; animation: ''" }, 500)
btf.fadeOut(document.getElementById('search-mask'), 0.5)
btf.animateOut(document.querySelector('#algolia-search .search-dialog'), 'search_close .5s')
btf.animateOut(document.getElementById('search-mask'), 'to_hide 0.5s')
}
const searchClickFn = () => {

View File

@ -1,10 +1,12 @@
window.addEventListener('load', () => {
let loadFlag = false
const openSearch = function () {
document.body.style.cssText = 'width: 100%;overflow: hidden'
document.querySelector('#local-search .search-dialog').style.display = 'block'
document.querySelector('#local-search-input input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn(document.getElementById('search-mask'), 'to_show 0.5s')
btf.animateIn(document.querySelector('#local-search .search-dialog'), 'titleScale 0.5s')
setTimeout(() => { document.querySelector('#local-search-input input').focus() }, 100)
if (!loadFlag) {
search(GLOBAL_CONFIG.localSearch.path)
loadFlag = true
@ -18,12 +20,12 @@ window.addEventListener('load', () => {
})
}
const closeSearch = function () {
document.body.style.cssText = "width: '';overflow: ''"
const $searchDialog = document.querySelector('#local-search .search-dialog')
$searchDialog.style.animation = 'search_close .5s'
setTimeout(() => { $searchDialog.style.cssText = "display: none; animation: ''" }, 500)
btf.fadeOut(document.getElementById('search-mask'), 0.5)
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut(document.querySelector('#local-search .search-dialog'), 'search_close .5s')
btf.animateOut(document.getElementById('search-mask'), 'to_hide 0.5s')
}
// click function

View File

@ -58,16 +58,14 @@ const btf = {
}
},
snackbarShow: (text, showAction, duration) => {
const sa = (typeof showAction !== 'undefined') ? showAction : false
const dur = (typeof duration !== 'undefined') ? duration : 2000
const position = GLOBAL_CONFIG.Snackbar.position
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? GLOBAL_CONFIG.Snackbar.bgLight : GLOBAL_CONFIG.Snackbar.bgDark
snackbarShow: (text, showAction = false, duration = 2000) => {
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
Snackbar.show({
text: text,
backgroundColor: bg,
showAction: sa,
duration: dur,
showAction: showAction,
duration: duration,
pos: position
})
},
@ -151,16 +149,18 @@ const btf = {
})
},
fadeIn: (ele, time) => {
ele.style.cssText = `display:block;animation: to_show ${time}s`
animateIn: (ele, text) => {
ele.style.display = 'block'
ele.style.animation = text
},
fadeOut: (ele, time) => {
animateOut: (ele, text) => {
ele.addEventListener('animationend', function f () {
ele.style.cssText = "display: none; animation: '' "
ele.style.display = ''
ele.style.animation = ''
ele.removeEventListener('animationend', f)
})
ele.style.animation = `to_hide ${time}s`
ele.style.animation = text
},
getParents: (elem, selector) => {
@ -180,7 +180,6 @@ const btf = {
},
/**
*
* @param {*} selector
* @param {*} eleType the type of create element
* @param {*} options object key: value
@ -263,5 +262,16 @@ const btf = {
})
}
})
},
updateAnchor: (anchor) => {
if (anchor !== window.location.hash) {
if (!anchor) anchor = location.pathname
const title = GLOBAL_CONFIG_SITE.title
window.history.replaceState({
url: location.href,
title: title
}, title, anchor)
}
}
}