mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-08-08 19:48:42 +08:00
Compare commits
@@ -3,17 +3,25 @@ name: npm publish
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # OIDC 必須
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm publish
|
|
||||||
|
- run: npm publish --provenance --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
+15
-5
@@ -91,7 +91,8 @@ category_per_img:
|
|||||||
footer_img: false
|
footer_img: false
|
||||||
|
|
||||||
# Website Background
|
# Website Background
|
||||||
# Can set it to color or image url
|
# Can set it to color, image URL or an array containing colors and/or image URLs
|
||||||
|
# If an array is provided, a random background will be selected from the array on each load
|
||||||
background:
|
background:
|
||||||
|
|
||||||
cover:
|
cover:
|
||||||
@@ -318,6 +319,7 @@ aside:
|
|||||||
# If set 0 will show all
|
# If set 0 will show all
|
||||||
limit: 40
|
limit: 40
|
||||||
color: false
|
color: false
|
||||||
|
custom_colors:
|
||||||
# Order of tags, random/name/length
|
# Order of tags, random/name/length
|
||||||
orderby: random
|
orderby: random
|
||||||
# Sort of order. 1, asc for ascending; -1, desc for descending
|
# Sort of order. 1, asc for ascending; -1, desc for descending
|
||||||
@@ -515,12 +517,12 @@ share:
|
|||||||
# Share.js
|
# Share.js
|
||||||
# https://github.com/overtrue/share.js
|
# https://github.com/overtrue/share.js
|
||||||
sharejs:
|
sharejs:
|
||||||
sites: facebook,twitter,wechat,weibo,qq
|
sites: facebook,x,wechat,weibo,qq
|
||||||
|
|
||||||
# AddToAny
|
# AddToAny
|
||||||
# https://www.addtoany.com/
|
# https://www.addtoany.com/
|
||||||
addtoany:
|
addtoany:
|
||||||
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link
|
item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Comments System
|
# Comments System
|
||||||
@@ -657,7 +659,7 @@ artalk:
|
|||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
|
|
||||||
chat:
|
chat:
|
||||||
# Choose: chatra/tidio/crisp
|
# Choose: chatra/tidio/crisp/knocket
|
||||||
# Leave it empty if you don't need chat
|
# Leave it empty if you don't need chat
|
||||||
use:
|
use:
|
||||||
# Chat Button [recommend]
|
# Chat Button [recommend]
|
||||||
@@ -678,6 +680,10 @@ tidio:
|
|||||||
crisp:
|
crisp:
|
||||||
website_id:
|
website_id:
|
||||||
|
|
||||||
|
# https://trtc.io/solutions/knocket
|
||||||
|
knocket:
|
||||||
|
identifier:
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Analysis
|
# Analysis
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
@@ -938,6 +944,10 @@ mermaid:
|
|||||||
theme:
|
theme:
|
||||||
light: default
|
light: default
|
||||||
dark: dark
|
dark: dark
|
||||||
|
# Enable "Open in New Tab" button to view diagram in a separate window
|
||||||
|
open_in_new_tab: true
|
||||||
|
# Enable zoom and pan interactions on diagrams
|
||||||
|
zoom_pan: true
|
||||||
|
|
||||||
# chartjs
|
# chartjs
|
||||||
# see https://www.chartjs.org/docs/latest/
|
# see https://www.chartjs.org/docs/latest/
|
||||||
@@ -1067,7 +1077,7 @@ CDN:
|
|||||||
third_party_provider: jsdelivr
|
third_party_provider: jsdelivr
|
||||||
|
|
||||||
# Add version number to url, true or false
|
# Add version number to url, true or false
|
||||||
version: false
|
version: true
|
||||||
|
|
||||||
# Custom format
|
# Custom format
|
||||||
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
|
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
page_info: '第 ${current} 页 / 共 ${total} 页'
|
||||||
|
|
||||||
comment: 评论
|
comment: 评论
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ div
|
|||||||
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
||||||
|
|
||||||
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
||||||
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
script(async data-pjax src=theme.asset.busuanzi ? url_for(theme.asset.busuanzi) : '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
||||||
|
|
||||||
!= partial('includes/third-party/search/index', {}, { cache: true })
|
!= partial('includes/third-party/search/index', {}, { cache: true })
|
||||||
|
|
||||||
|
|||||||
@@ -72,20 +72,16 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight = 'undefined'
|
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
|
||||||
let syntaxHighlighter = config.syntax_highlighter
|
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
|
||||||
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
|
let highlight = JSON.stringify({
|
||||||
if (highlightEnable) {
|
plugin: highlightProvider,
|
||||||
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
|
|
||||||
highlight = JSON.stringify({
|
|
||||||
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
|
|
||||||
highlightCopy: copy,
|
highlightCopy: copy,
|
||||||
highlightLang: language,
|
highlightLang: language,
|
||||||
highlightHeightLimit: height_limit,
|
highlightHeightLimit: height_limit,
|
||||||
highlightFullpage: fullpage,
|
highlightFullpage: fullpage,
|
||||||
highlightMacStyle: macStyle
|
highlightMacStyle: macStyle
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
script.
|
script.
|
||||||
const GLOBAL_CONFIG = {
|
const GLOBAL_CONFIG = {
|
||||||
|
|||||||
@@ -13,7 +13,29 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside
|
|||||||
!=partial('includes/loading/index', {}, {cache: true})
|
!=partial('includes/loading/index', {}, {cache: true})
|
||||||
|
|
||||||
if theme.background
|
if theme.background
|
||||||
#web_bg(style=getBgPath(theme.background))
|
if !Array.isArray(theme.background)
|
||||||
|
#web_bg.bg-animation(style=getBgPath(theme.background))
|
||||||
|
else
|
||||||
|
#web_bg.bg-animation
|
||||||
|
- const bgStyleArr = theme.background.map(getBgPath)
|
||||||
|
script.
|
||||||
|
(() => {
|
||||||
|
const arr = !{JSON.stringify(bgStyleArr)}
|
||||||
|
const webBgDiv = document.getElementById('web_bg')
|
||||||
|
|
||||||
|
const setRandomBg = () => {
|
||||||
|
webBgDiv.style = arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
requestAnimationFrame(() => webBgDiv.classList.add('bg-animation'))
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pjax:send', () => {
|
||||||
|
webBgDiv.style = ''
|
||||||
|
webBgDiv.classList.remove('bg-animation')
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('pjax:complete', setRandomBg)
|
||||||
|
document.addEventListener('DOMContentLoaded', setRandomBg)
|
||||||
|
})()
|
||||||
|
|
||||||
!=partial('includes/sidebar', {}, {cache: true})
|
!=partial('includes/sidebar', {}, {cache: true})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mixin indexPostUI()
|
mixin indexPostUI()
|
||||||
- const indexLayout = theme.index_layout
|
- const indexLayout = theme.index_layout
|
||||||
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : ''
|
- const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
|
||||||
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
||||||
.recent-post-items
|
.recent-post-items
|
||||||
each article, index in page.posts.data
|
each article, index in page.posts.data
|
||||||
@@ -8,17 +8,17 @@ mixin indexPostUI()
|
|||||||
- const link = article.link || article.path
|
- const link = article.link || article.path
|
||||||
- const title = article.title || _p('no_title')
|
- const title = article.title || _p('no_title')
|
||||||
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
||||||
- const post_cover = article.cover
|
- const postCover = article.cover
|
||||||
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
||||||
|
|
||||||
if post_cover && theme.cover.index_enable
|
if postCover && theme.cover.index_enable
|
||||||
.post_cover(class=leftOrRight)
|
.post_cover(class=leftOrRight)
|
||||||
a(href=url_for(link) title=title)
|
a(href=url_for(link) title=title)
|
||||||
if article.cover_type === 'img'
|
if article.cover_type === 'img'
|
||||||
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
||||||
else
|
else
|
||||||
div.post-bg(style=`background: ${post_cover}`)
|
div.post-bg(style=`background: ${postCover}`)
|
||||||
.recent-post-info(class=no_cover)
|
.recent-post-info(class=noCover)
|
||||||
a.article-title(href=url_for(link) title=title)
|
a.article-title(href=url_for(link) title=title)
|
||||||
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
||||||
i.fas.fa-thumbtack.sticky
|
i.fas.fa-thumbtack.sticky
|
||||||
@@ -35,13 +35,13 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label=_p('post.updated')
|
span.article-meta-label=_p('post.updated')
|
||||||
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
|
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
|
||||||
else
|
else
|
||||||
- const data_type_updated = theme.post_meta.page.date_type === 'updated'
|
- const isUpdatedType = theme.post_meta.page.date_type === 'updated'
|
||||||
- const date_type = data_type_updated ? 'updated' : 'date'
|
- const dateType = isUpdatedType ? 'updated' : 'date'
|
||||||
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
|
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
|
||||||
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
|
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
|
||||||
i(class=date_icon)
|
i(class=dateIcon)
|
||||||
span.article-meta-label= date_title
|
span.article-meta-label= dateTitle
|
||||||
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format)
|
time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format)
|
||||||
if theme.post_meta.page.categories && article.categories.data.length > 0
|
if theme.post_meta.page.categories && article.categories.data.length > 0
|
||||||
span.article-meta
|
span.article-meta
|
||||||
span.article-meta-separator |
|
span.article-meta-separator |
|
||||||
@@ -69,7 +69,10 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label= ' ' + _p('card_post_count')
|
span.article-meta-label= ' ' + _p('card_post_count')
|
||||||
|
|
||||||
if theme.comments.card_post_count && theme.comments.use
|
if theme.comments.card_post_count && theme.comments.use
|
||||||
case theme.comments.use[0]
|
- const commentSystem = theme.comments.use[0]
|
||||||
|
- const commentLink = url_for(link) + '#post-comment'
|
||||||
|
|
||||||
|
case commentSystem
|
||||||
when 'Disqus'
|
when 'Disqus'
|
||||||
when 'Disqusjs'
|
when 'Disqusjs'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
@@ -77,30 +80,30 @@ mixin indexPostUI()
|
|||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Valine'
|
when 'Valine'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.valine-comment-count(data-xid=url_for(link))
|
span.valine-comment-count(data-xid=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Waline'
|
when 'Waline'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.waline-comment-count(data-path=url_for(link))
|
span.waline-comment-count(data-path=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Twikoo'
|
when 'Twikoo'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a.twikoo-count(href=url_for(link) + '#post-comment')
|
a.twikoo-count(href=commentLink)
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Facebook Comments'
|
when 'Facebook Comments'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
||||||
when 'Remark42'
|
when 'Remark42'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Artalk'
|
when 'Artalk'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.artalk-count(data-page-key=url_for(link))
|
span.artalk-count(data-page-key=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
|
|
||||||
|
|||||||
+155
-143
@@ -46,10 +46,16 @@
|
|||||||
|
|
||||||
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
|
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
|
||||||
|
|
||||||
|
if !page.shuoshuo_url
|
||||||
|
script(type='application/json' id='shuoshuo-data')!= safeJSON(localDate)
|
||||||
|
|
||||||
|
- const { enable, native, placeholder, field } = theme.lazyload
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
||||||
|
|
||||||
|
const escapeHtml = str => String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||||
|
|
||||||
const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
|
const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||||
|
|
||||||
const filterDataByLimit = (data, limit) => {
|
const filterDataByLimit = (data, limit) => {
|
||||||
@@ -60,13 +66,10 @@
|
|||||||
return data.filter(item => new Date(item.date) >= limitDate)
|
return data.filter(item => new Date(item.date) >= limitDate)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatToTimeZone = (date) => {
|
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||||
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
timeZone: !{JSON.stringify(config.timezone || '')} || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
||||||
const options = {
|
|
||||||
timeZone: visitorTimeZone,
|
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -74,30 +77,60 @@
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}
|
})
|
||||||
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
|
|
||||||
.format(new Date(fullDate))
|
const formatToTimeZone = (date) => {
|
||||||
.match(/\d+/g)
|
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
||||||
|
const [day, month, year, hour, minute, second] = dateFormatter.format(new Date(fullDate)).match(/\d+/g)
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addLazyload = str => {
|
||||||
|
const lazyConfig = {
|
||||||
|
enable: !{Boolean(enable)},
|
||||||
|
native: !{Boolean(native)},
|
||||||
|
field: !{JSON.stringify(field || '')},
|
||||||
|
placeholder: !{JSON.stringify(url_for(placeholder))},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lazyConfig.enable || lazyConfig.field !== 'site' || str.indexOf('<img') === -1) return str
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(str, 'text/html')
|
||||||
|
const images = doc.querySelectorAll('img')
|
||||||
|
|
||||||
|
images.forEach(img => {
|
||||||
|
if (lazyConfig.native) {
|
||||||
|
img.setAttribute('loading', 'lazy')
|
||||||
|
} else {
|
||||||
|
const src = img.getAttribute('src')
|
||||||
|
img.setAttribute('data-lazy-src', src)
|
||||||
|
|
||||||
|
if (lazyConfig.placeholder) {
|
||||||
|
img.setAttribute('src', lazyConfig.placeholder)
|
||||||
|
} else {
|
||||||
|
img.removeAttribute('src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
let currentPage = 1
|
let currentPage = 1
|
||||||
const itemsPerPage = 8
|
const itemsPerPage = 8
|
||||||
let totalPages = 0
|
let totalPages = 0
|
||||||
let data = []
|
let data = []
|
||||||
let inputEventsAttached = false // Flag to mark if input event listeners have been added
|
|
||||||
|
|
||||||
const renderData = (dataSlice) => {
|
const renderData = (dataSlice) => {
|
||||||
const content = dataSlice.map(item => {
|
const content = dataSlice.map(item => {
|
||||||
const formattedDate = formatToTimeZone(item.date)
|
const formattedDate = formatToTimeZone(item.date)
|
||||||
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
|
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${escapeHtml(tag)}</span>`).join('') || ''
|
||||||
const commentButton = item.key && !{commentsJsLoad}
|
const commentButton = item.key && !{commentsJsLoad || false}
|
||||||
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
||||||
<i class="fa-solid fa-comments"></i>
|
<i class="fa-solid fa-comments"></i>
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
const commentContainer = item.key
|
const commentContainer = item.key
|
||||||
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
|
? `<div class="shuoshuo-comment no-comment" data-key="${escapeHtml(item.key)}"></div>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -105,16 +138,16 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="shuoshuo-item-header">
|
<div class="shuoshuo-item-header">
|
||||||
<div class="shuoshuo-avatar">
|
<div class="shuoshuo-avatar">
|
||||||
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
<img class="no-lightbox" src="${item.avatar ? escapeHtml(item.avatar) : !{JSON.stringify(url_for(theme.avatar.img))}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="shuoshuo-info">
|
<div class="shuoshuo-info">
|
||||||
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
<div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(config.author || '')}}</div>
|
||||||
<time class="shuoshuo-date" title="${formattedDate}">
|
<time class="shuoshuo-date" title="${formattedDate}">
|
||||||
${btf.diffDate(formattedDate, true)}
|
${btf.diffDate(formattedDate, true)}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shuoshuo-content">${item.content}</div>
|
<div class="shuoshuo-content">${addLazyload(item.content)}</div>
|
||||||
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
||||||
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
||||||
${commentButton}
|
${commentButton}
|
||||||
@@ -131,70 +164,94 @@
|
|||||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderNavigation = () => {
|
const setupNavEvents = (nav) => {
|
||||||
const container = document.getElementById('article-container')
|
nav.querySelector('.shuoshuo-prev-btn').addEventListener('click', () => {
|
||||||
const existingNav = container.nextElementSibling
|
if (currentPage > 1) { currentPage--; renderPage(currentPage) }
|
||||||
if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) {
|
})
|
||||||
existingNav.remove()
|
nav.querySelector('.shuoshuo-next-btn').addEventListener('click', () => {
|
||||||
|
if (currentPage < totalPages) { currentPage++; renderPage(currentPage) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const input = nav.querySelector('.shuoshuo-page-input')
|
||||||
|
|
||||||
|
input.addEventListener('focus', e => { e.target.placeholder = '' })
|
||||||
|
input.addEventListener('blur', e => {
|
||||||
|
if (!e.target.value.trim()) e.target.placeholder = currentPage
|
||||||
|
})
|
||||||
|
|
||||||
|
input.addEventListener('input', e => {
|
||||||
|
const value = parseInt(e.target.value) || 0
|
||||||
|
let wasInvalid = false
|
||||||
|
|
||||||
|
if (value > totalPages) { e.target.value = totalPages; wasInvalid = true }
|
||||||
|
else if (value < 1 && e.target.value !== '') { e.target.value = 1; wasInvalid = true }
|
||||||
|
|
||||||
|
if (wasInvalid) {
|
||||||
|
e.target.classList.add('invalid')
|
||||||
|
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
input.addEventListener('keydown', e => {
|
||||||
|
const value = e.target.value + e.key
|
||||||
|
|
||||||
|
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete' ||
|
||||||
|
e.key === 'ArrowLeft' || e.key === 'ArrowRight' ||
|
||||||
|
e.key === 'Tab' || e.ctrlKey || e.metaKey) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const inputValue = e.target.value.trim()
|
||||||
|
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
|
||||||
|
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
|
||||||
|
currentPage = inputPage
|
||||||
|
renderPage(currentPage)
|
||||||
|
} else if (inputValue === '') {
|
||||||
|
renderPage(currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!/^\d$/.test(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = parseInt(value) || 0
|
||||||
|
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.target.classList.add('invalid')
|
||||||
|
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNavigation = () => {
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
|
let nav = container.nextElementSibling
|
||||||
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
||||||
const pageInfoText = pageInfoTemplate
|
const pageInfoText = pageInfoTemplate
|
||||||
.replace(/\$\{current}/g, currentPage)
|
.replace(/\$\{current}/g, currentPage)
|
||||||
.replace(/\$\{total}/g, totalPages)
|
.replace(/\$\{total}/g, totalPages)
|
||||||
|
|
||||||
const navHtml = `
|
if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
|
||||||
<div class="shuoshuo-navigation">
|
nav = document.createElement('div')
|
||||||
<button onclick="window.shuoshuoPrevPage()" ${currentPage === 1 ? 'disabled' : ''}><i class="fa-solid fa-chevron-left"></i></button>
|
nav.className = 'shuoshuo-navigation'
|
||||||
<span class="shuoshuo-page-info">${pageInfoText}</span>
|
nav.innerHTML = `
|
||||||
<input type="number" class="shuoshuo-page-input" min="1" max="${totalPages}" placeholder="${currentPage}" onkeydown="window.shuoshuoHandleKeyDown(event)">
|
<button class="shuoshuo-prev-btn"><i class="fa-solid fa-chevron-left"></i></button>
|
||||||
<button onclick="window.shuoshuoNextPage()" ${currentPage === totalPages ? 'disabled' : ''}><i class="fa-solid fa-chevron-right"></i></button>
|
<span class="shuoshuo-page-info"></span>
|
||||||
</div>
|
<input type="number" class="shuoshuo-page-input" min="1">
|
||||||
|
<button class="shuoshuo-next-btn"><i class="fa-solid fa-chevron-right"></i></button>
|
||||||
`
|
`
|
||||||
container.insertAdjacentHTML('afterend', navHtml)
|
container.insertAdjacentElement('afterend', nav)
|
||||||
|
setupNavEvents(nav)
|
||||||
// Add input validation event listeners (only once)
|
|
||||||
if (!inputEventsAttached) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const input = document.querySelector('.shuoshuo-page-input')
|
|
||||||
if (input) {
|
|
||||||
// Clear placeholder when clicking the input box
|
|
||||||
input.addEventListener('focus', (event) => {
|
|
||||||
event.target.placeholder = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restore placeholder if no content when losing focus
|
|
||||||
input.addEventListener('blur', (event) => {
|
|
||||||
if (!event.target.value.trim()) {
|
|
||||||
event.target.placeholder = currentPage
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
input.addEventListener('input', (event) => {
|
|
||||||
const value = parseInt(event.target.value) || 0
|
|
||||||
let wasInvalid = false
|
|
||||||
|
|
||||||
if (value > totalPages) {
|
|
||||||
event.target.value = totalPages
|
|
||||||
wasInvalid = true
|
|
||||||
} else if (value < 1 && event.target.value !== '') {
|
|
||||||
event.target.value = 1
|
|
||||||
wasInvalid = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If value is corrected, show red and shake effect
|
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
|
||||||
if (wasInvalid) {
|
nav.querySelector('.shuoshuo-prev-btn').disabled = currentPage === 1
|
||||||
event.target.classList.add('invalid')
|
nav.querySelector('.shuoshuo-next-btn').disabled = currentPage === totalPages
|
||||||
setTimeout(() => {
|
const input = nav.querySelector('.shuoshuo-page-input')
|
||||||
event.target.classList.remove('invalid')
|
input.max = totalPages
|
||||||
}, 500)
|
input.placeholder = currentPage
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
inputEventsAttached = true // Mark that event listeners have been added
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderPage = (page) => {
|
const renderPage = (page) => {
|
||||||
@@ -205,93 +262,48 @@
|
|||||||
renderNavigation()
|
renderNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.shuoshuoPrevPage = () => {
|
|
||||||
if (currentPage > 1) {
|
|
||||||
currentPage--
|
|
||||||
renderPage(currentPage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.shuoshuoNextPage = () => {
|
|
||||||
if (currentPage < totalPages) {
|
|
||||||
currentPage++
|
|
||||||
renderPage(currentPage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.shuoshuoGoToPage = (page) => {
|
|
||||||
if (typeof page === 'number') {
|
|
||||||
// Directly jump to the specified page
|
|
||||||
if (page >= 1 && page <= totalPages && page !== currentPage) {
|
|
||||||
currentPage = page
|
|
||||||
renderPage(currentPage)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Get page from input box
|
|
||||||
const input = document.querySelector('.shuoshuo-page-input')
|
|
||||||
const inputValue = input.value.trim()
|
|
||||||
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
|
|
||||||
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
|
|
||||||
currentPage = inputPage
|
|
||||||
renderPage(currentPage)
|
|
||||||
} else if (inputValue === '') {
|
|
||||||
// If input box is empty, re-render current page (update placeholder)
|
|
||||||
renderPage(currentPage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.shuoshuoHandleKeyDown = (event) => {
|
|
||||||
const input = event.target
|
|
||||||
const value = input.value + event.key
|
|
||||||
|
|
||||||
// Allow delete, arrow keys, backspace, etc.
|
|
||||||
if (event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' ||
|
|
||||||
event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
|
|
||||||
event.key === 'Tab' || event.ctrlKey || event.metaKey) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
window.shuoshuoGoToPage()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow numbers
|
|
||||||
if (!/^\d$/.test(event.key)) {
|
|
||||||
event.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the value after input exceeds the range
|
|
||||||
const newValue = parseInt(value) || 0
|
|
||||||
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
|
||||||
event.preventDefault()
|
|
||||||
// Add red and shake effect
|
|
||||||
input.classList.add('invalid')
|
|
||||||
setTimeout(() => {
|
|
||||||
input.classList.remove('invalid')
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadShuoshuo = async () => {
|
const loadShuoshuo = async () => {
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
try {
|
try {
|
||||||
let originData = []
|
let originData = []
|
||||||
if (!{Boolean(page.shuoshuo_url)}) {
|
if (!{Boolean(page.shuoshuo_url)}) {
|
||||||
|
container.innerHTML = '<div class="shuoshuo-loading"><i class="fa-solid fa-circle-notch fa-spin"></i></div>'
|
||||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||||
originData = await response.json()
|
originData = await response.json()
|
||||||
} else {
|
} else {
|
||||||
originData = !{JSON.stringify(localDate)}
|
const dataElement = document.getElementById('shuoshuo-data')
|
||||||
|
originData = dataElement ? JSON.parse(dataElement.textContent) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
|
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
|
||||||
|
|
||||||
totalPages = Math.ceil(data.length / itemsPerPage)
|
totalPages = Math.ceil(data.length / itemsPerPage)
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
container.innerHTML = '<div class="shuoshuo-empty"></div>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
renderPage(currentPage)
|
renderPage(currentPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
container.innerHTML = '<div class="shuoshuo-error"><i class="fa-solid fa-circle-exclamation"></i></div>'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
|
const pjaxCleanup = () => {
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
|
if (container) {
|
||||||
|
const nav = container.nextElementSibling
|
||||||
|
if (nav && nav.classList.contains('shuoshuo-navigation')) nav.remove()
|
||||||
|
}
|
||||||
|
document.removeEventListener('pjax:send', pjaxCleanup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.pjax) {
|
||||||
|
document.addEventListener('pjax:send', pjaxCleanup)
|
||||||
|
loadShuoshuo()
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', loadShuoshuo)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
.tag-cloud-list.text-center
|
.tag-cloud-list.text-center
|
||||||
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'})
|
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors})
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if page.total !== 1
|
|||||||
|
|
||||||
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
||||||
if direction.cover_type === 'img'
|
if direction.cover_type === 'img'
|
||||||
img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
img.cover(src=url_for(direction.pagination_cover || direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
||||||
else
|
else
|
||||||
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -2,8 +2,11 @@ script.
|
|||||||
(() => {
|
(() => {
|
||||||
const abcjsInit = () => {
|
const abcjsInit = () => {
|
||||||
const abcjsFn = () => {
|
const abcjsFn = () => {
|
||||||
|
const $article = document.getElementById("article-container")
|
||||||
|
if (!$article || $article.querySelector(".hbe-container")) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const sheets = document.querySelectorAll(".abc-music-sheet")
|
const sheets = $article.querySelectorAll(".abc-music-sheet")
|
||||||
for (let i = 0; i < sheets.length; i++) {
|
for (let i = 0; i < sheets.length; i++) {
|
||||||
const ele = sheets[i]
|
const ele = sheets[i]
|
||||||
if (ele.children.length > 0) continue
|
if (ele.children.length > 0) continue
|
||||||
@@ -24,12 +27,12 @@ script.
|
|||||||
const options = { ...params, responsive: "resize" }
|
const options = { ...params, responsive: "resize" }
|
||||||
|
|
||||||
// Render the music score using ABCJS.renderAbc
|
// Render the music score using ABCJS.renderAbc
|
||||||
ABCJS.renderAbc(ele, ele.innerHTML, options)
|
ABCJS.renderAbc(ele, ele.textContent, options)
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ABCJS === "object") {
|
if (typeof ABCJS === "object" && ABCJS !== null) {
|
||||||
abcjsFn()
|
abcjsFn()
|
||||||
} else {
|
} else {
|
||||||
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
||||||
|
|||||||
+11
-5
@@ -6,10 +6,14 @@ script.
|
|||||||
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
|
||||||
const isChatBtn = !{theme.chat.rightside_button}
|
const isChatBtn = !{theme.chat.rightside_button}
|
||||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||||
|
|
||||||
|
if (isChatBtn) {
|
||||||
|
window.ChatraSetup = { startHidden: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||||
if (isChatBtn) {
|
if (isChatBtn) {
|
||||||
const close = () => {
|
const close = () => {
|
||||||
Chatra('minimizeWidget')
|
Chatra('minimizeWidget')
|
||||||
@@ -21,11 +25,13 @@ script.
|
|||||||
Chatra('show')
|
Chatra('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ChatraSetup = { startHidden: true }
|
window.chatBtnFn = () => {
|
||||||
|
const el = document.getElementById('chatra')
|
||||||
|
return el && el.classList.contains('chatra--expanded') ? close() : open()
|
||||||
|
}
|
||||||
|
|
||||||
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
|
const chatBtn = document.getElementById('chat-btn')
|
||||||
|
if (chatBtn) chatBtn.style.display = 'block'
|
||||||
document.getElementById('chat-btn').style.display = 'block'
|
|
||||||
} else if (isChatHideShow) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => Chatra('hide'),
|
hide: () => Chatra('hide'),
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@ script.
|
|||||||
|
|
||||||
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
|
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
|
||||||
|
|
||||||
document.getElementById('chat-btn').style.display = 'block'
|
const chatBtn = document.getElementById('chat-btn')
|
||||||
|
if (chatBtn) chatBtn.style.display = 'block'
|
||||||
} else if (isChatHideShow) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => $crisp.push(["do", "chat:hide"]),
|
hide: () => $crisp.push(["do", "chat:hide"]),
|
||||||
|
|||||||
+2
@@ -5,3 +5,5 @@ case theme.chat.use
|
|||||||
include ./tidio.pug
|
include ./tidio.pug
|
||||||
when 'crisp'
|
when 'crisp'
|
||||||
include ./crisp.pug
|
include ./crisp.pug
|
||||||
|
when 'knocket'
|
||||||
|
include ./knocket.pug
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
//- Knocket live chat — https://trtc.io/solutions/knocket
|
||||||
|
script.
|
||||||
|
(() => {
|
||||||
|
const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/</g, '\\u003c')}
|
||||||
|
if (!identifier) return
|
||||||
|
|
||||||
|
btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => {
|
||||||
|
const isChatBtn = !{theme.chat.rightside_button}
|
||||||
|
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||||
|
|
||||||
|
let widget = null
|
||||||
|
const getWidget = () => widget || (widget = document.getElementById('contact-widget-auto'))
|
||||||
|
|
||||||
|
if (isChatBtn) {
|
||||||
|
const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' }
|
||||||
|
const show = () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||||
|
|
||||||
|
// Hide the native Knocket floating button
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (getWidget()) { hide(); observer.disconnect() }
|
||||||
|
})
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true })
|
||||||
|
hide()
|
||||||
|
|
||||||
|
window.chatBtnFn = () => {
|
||||||
|
const w = getWidget()
|
||||||
|
if (!w) return
|
||||||
|
w.style.display = w.style.display === 'none' ? '' : 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatBtn = document.getElementById('chat-btn')
|
||||||
|
if (chatBtn) chatBtn.style.display = 'block'
|
||||||
|
} else if (isChatHideShow) {
|
||||||
|
window.chatBtn = {
|
||||||
|
hide: () => { const w = getWidget(); if (w) w.style.display = 'none' },
|
||||||
|
show: () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.warn('[Knocket] Failed to load the chat SDK.', error)
|
||||||
|
})
|
||||||
|
})()
|
||||||
+2
-2
@@ -32,8 +32,8 @@ script.
|
|||||||
isShow ? close() : open()
|
isShow ? close() : open()
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('chat-btn').style.display = 'block'
|
const chatBtn = document.getElementById('chat-btn')
|
||||||
|
if (chatBtn) chatBtn.style.display = 'block'
|
||||||
} else if (isChatHideShow) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
||||||
|
|||||||
+2
-2
@@ -51,8 +51,8 @@ script.
|
|||||||
const loadArtalk = async (el, pageKey) => {
|
const loadArtalk = async (el, pageKey) => {
|
||||||
if (typeof Artalk === 'object') initArtalk(el, pageKey)
|
if (typeof Artalk === 'object') initArtalk(el, pageKey)
|
||||||
else {
|
else {
|
||||||
await btf.getCSS('!{theme.asset.artalk_css}')
|
await btf.getCSS('!{url_for(theme.asset.artalk_css)}')
|
||||||
await btf.getScript('!{theme.asset.artalk_js}')
|
await btf.getScript('!{url_for(theme.asset.artalk_js)}')
|
||||||
initArtalk(el, pageKey)
|
initArtalk(el, pageKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||||
const dqOption = !{JSON.stringify(dqOption)}
|
const dqOption = !{JSON.stringify(dqOption)}
|
||||||
|
|
||||||
const destroyDisqusjs = () => {
|
const destroyDisqusjs = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
script.
|
script.
|
||||||
(()=>{
|
(()=>{
|
||||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||||
|
|
||||||
const loadFBComment = (el = document, path) => {
|
const loadFBComment = (el = document, path) => {
|
||||||
if (isShuoshuo) {
|
if (isShuoshuo) {
|
||||||
|
|||||||
+20
-17
@@ -2,7 +2,22 @@
|
|||||||
- const { tags, enableMenu } = theme.math.mathjax
|
- const { tags, enableMenu } = theme.math.mathjax
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
|
const changeScriptToMath = article => {
|
||||||
|
article.querySelectorAll('script[type^="math/tex"]').forEach(el => {
|
||||||
|
const display = /mode=display/.test(el.type)
|
||||||
|
const node = document.createElement(display ? 'div' : 'span')
|
||||||
|
node.textContent = display
|
||||||
|
? `$$${el.textContent}$$`
|
||||||
|
: `$${el.textContent}$`
|
||||||
|
el.parentNode.replaceChild(node, el)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const loadMathjax = () => {
|
const loadMathjax = () => {
|
||||||
|
const article = document.getElementById('article-container')
|
||||||
|
if (!article || article.querySelector('.hbe-container')) return
|
||||||
|
changeScriptToMath(article)
|
||||||
|
|
||||||
if (!window.MathJax) {
|
if (!window.MathJax) {
|
||||||
window.MathJax = {
|
window.MathJax = {
|
||||||
loader: {
|
loader: {
|
||||||
@@ -11,7 +26,8 @@ script.
|
|||||||
//- '[tex]/bbm',
|
//- '[tex]/bbm',
|
||||||
//- '[tex]/bboldx',
|
//- '[tex]/bboldx',
|
||||||
//- '[tex]/dsfont',
|
//- '[tex]/dsfont',
|
||||||
'[tex]/mhchem'
|
'[tex]/mhchem',
|
||||||
|
'ui/lazy'
|
||||||
],
|
],
|
||||||
paths: {
|
paths: {
|
||||||
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
|
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
|
||||||
@@ -39,25 +55,13 @@ script.
|
|||||||
scale: 1.1
|
scale: 1.1
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
lazyMargin: '200px',
|
||||||
enableMenu: !{enableMenu},
|
enableMenu: !{enableMenu},
|
||||||
menuOptions: {
|
menuOptions: {
|
||||||
settings: {
|
settings: {
|
||||||
enrich: false // Turn off Braille and voice narration text automatic generation
|
enrich: false // Turn off Braille and voice narration text automatic generation
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderActions: {
|
|
||||||
findScript: [10, doc => {
|
|
||||||
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
|
||||||
const display = !!node.type.match(/; *mode=display/)
|
|
||||||
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display)
|
|
||||||
const text = document.createTextNode('')
|
|
||||||
node.parentNode.replaceChild(text, node)
|
|
||||||
math.start = {node: text, delim: '', n: 0}
|
|
||||||
math.end = {node: text, delim: '', n: 0}
|
|
||||||
doc.math.push(math)
|
|
||||||
}
|
|
||||||
}, '']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +71,8 @@ script.
|
|||||||
script.async = true
|
script.async = true
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
} else {
|
} else {
|
||||||
MathJax.startup.document.state(0)
|
MathJax.typesetClear()
|
||||||
MathJax.texReset()
|
MathJax.typesetPromise([ article ])
|
||||||
MathJax.typesetPromise()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+321
-8
@@ -1,27 +1,337 @@
|
|||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
|
const parseViewBox = viewBox => {
|
||||||
|
if (!viewBox) return null
|
||||||
|
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
|
||||||
|
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSvgViewBox = svg => {
|
||||||
|
const attr = parseViewBox(svg.getAttribute('viewBox'))
|
||||||
|
if (attr) return attr
|
||||||
|
|
||||||
|
// Fallback: use bbox to build a viewBox
|
||||||
|
try {
|
||||||
|
const bbox = svg.getBBox()
|
||||||
|
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
|
||||||
|
} catch (e) {
|
||||||
|
// getBBox may fail on some edge cases; ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = Number(svg.getAttribute('width')) || 0
|
||||||
|
const h = Number(svg.getAttribute('height')) || 0
|
||||||
|
if (w > 0 && h > 0) return [0, 0, w, h]
|
||||||
|
return [0, 0, 100, 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSvgViewBox = (svg, vb) => {
|
||||||
|
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
|
||||||
|
|
||||||
|
const openSvgInNewTab = ({ source, initViewBox }) => {
|
||||||
|
const getClonedSvg = () => {
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
const template = document.createElement('template')
|
||||||
|
template.innerHTML = source.trim()
|
||||||
|
const svg = template.content.querySelector('svg')
|
||||||
|
return svg ? svg.cloneNode(true) : null
|
||||||
|
}
|
||||||
|
if (source && typeof source.cloneNode === 'function') {
|
||||||
|
return source.cloneNode(true)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = getClonedSvg()
|
||||||
|
if (!clone) return
|
||||||
|
if (initViewBox && initViewBox.length === 4) {
|
||||||
|
clone.setAttribute('viewBox', initViewBox.join(' '))
|
||||||
|
}
|
||||||
|
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
||||||
|
if (!clone.getAttribute('xmlns:xlink') && clone.innerHTML.includes('xlink:')) {
|
||||||
|
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
||||||
|
}
|
||||||
|
// inject background to match current theme
|
||||||
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff')
|
||||||
|
if (!clone.style.background) clone.style.background = bg
|
||||||
|
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const svgSource = serializer.serializeToString(clone)
|
||||||
|
const htmlSource = `<!doctype html><html><head><meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
|
||||||
|
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
|
||||||
|
</style>
|
||||||
|
</head><body>${svgSource}</body></html>`
|
||||||
|
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
window.open(url, '_blank', 'noopener')
|
||||||
|
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachMermaidViewerButton = wrap => {
|
||||||
|
let btn = wrap.querySelector('.mermaid-open-btn')
|
||||||
|
if (!btn) {
|
||||||
|
btn = document.createElement('button')
|
||||||
|
btn.type = 'button'
|
||||||
|
btn.className = 'mermaid-open-btn'
|
||||||
|
wrap.appendChild(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.innerHTML = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
|
||||||
|
|
||||||
|
if (!btn.__mermaidViewerBound) {
|
||||||
|
btn.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
|
||||||
|
if (!svg) return
|
||||||
|
const initViewBox = wrap.__mermaidInitViewBox
|
||||||
|
openSvgInNewTab({ source: svg, initViewBox })
|
||||||
|
})
|
||||||
|
btn.__mermaidViewerBound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates)
|
||||||
|
const zoomAtPoint = (vb, factor, px, py) => {
|
||||||
|
const w = vb[2] * factor
|
||||||
|
const h = vb[3] * factor
|
||||||
|
const nx = px - (px - vb[0]) * factor
|
||||||
|
const ny = py - (py - vb[1]) * factor
|
||||||
|
return [nx, ny, w, h]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initMermaidGestures = wrap => {
|
||||||
|
// Clean up previous event listeners and pending frames
|
||||||
|
if (wrap.__mermaidAbortController) {
|
||||||
|
wrap.__mermaidAbortController.abort()
|
||||||
|
if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId)
|
||||||
|
}
|
||||||
|
const ac = new AbortController()
|
||||||
|
wrap.__mermaidAbortController = ac
|
||||||
|
const svg = wrap.querySelector('svg')
|
||||||
|
if (!svg) return
|
||||||
|
|
||||||
|
// Ensure viewBox exists so gestures always work
|
||||||
|
const initVb = getSvgViewBox(svg)
|
||||||
|
wrap.__mermaidInitViewBox = initVb
|
||||||
|
wrap.__mermaidCurViewBox = initVb.slice()
|
||||||
|
setSvgViewBox(svg, initVb)
|
||||||
|
// Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chrome
|
||||||
|
svg.style.touchAction = 'none'
|
||||||
|
|
||||||
|
// Cache BoundingClientRect, throttled on scroll to reduce reflow
|
||||||
|
let cachedRect = svg.getBoundingClientRect()
|
||||||
|
let rectDirty = false
|
||||||
|
const markRectDirty = () => { rectDirty = true }
|
||||||
|
window.addEventListener('resize', markRectDirty, { signal: ac.signal })
|
||||||
|
window.addEventListener('scroll', markRectDirty, { signal: ac.signal, capture: true })
|
||||||
|
const getRect = () => {
|
||||||
|
if (rectDirty) {
|
||||||
|
cachedRect = svg.getBoundingClientRect()
|
||||||
|
rectDirty = false
|
||||||
|
}
|
||||||
|
return cachedRect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompute clamp bounds from initial viewBox
|
||||||
|
const minW = initVb[2] * 0.1
|
||||||
|
const maxW = initVb[2] * 10
|
||||||
|
const minH = initVb[3] * 0.1
|
||||||
|
const maxH = initVb[3] * 10
|
||||||
|
const clampVb = vb => {
|
||||||
|
const out = vb.slice()
|
||||||
|
out[2] = clamp(out[2], minW, maxW)
|
||||||
|
out[3] = clamp(out[3], minH, maxH)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle DOM updates using requestAnimationFrame
|
||||||
|
let pendingVb = null
|
||||||
|
let rafId = null
|
||||||
|
const applyVb = () => {
|
||||||
|
if (pendingVb) {
|
||||||
|
wrap.__mermaidCurViewBox = pendingVb
|
||||||
|
setSvgViewBox(svg, pendingVb)
|
||||||
|
pendingVb = null
|
||||||
|
}
|
||||||
|
rafId = null
|
||||||
|
wrap.__mermaidRafId = null
|
||||||
|
}
|
||||||
|
const setCurVb = vb => {
|
||||||
|
pendingVb = clampVb(vb)
|
||||||
|
if (!rafId) {
|
||||||
|
rafId = requestAnimationFrame(applyVb)
|
||||||
|
wrap.__mermaidRafId = rafId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
pointers: new Map(),
|
||||||
|
startVb: null,
|
||||||
|
startDist: 0,
|
||||||
|
lastPointerX: 0,
|
||||||
|
lastPointerY: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerDown = e => {
|
||||||
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||||||
|
svg.setPointerCapture(e.pointerId)
|
||||||
|
const curVb = wrap.__mermaidCurViewBox
|
||||||
|
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||||
|
if (state.pointers.size === 1) {
|
||||||
|
state.startVb = curVb.slice()
|
||||||
|
state.lastPointerX = e.clientX
|
||||||
|
state.lastPointerY = e.clientY
|
||||||
|
} else if (state.pointers.size === 2) {
|
||||||
|
const pts = [...state.pointers.values()]
|
||||||
|
const dx = pts[0].x - pts[1].x
|
||||||
|
const dy = pts[0].y - pts[1].y
|
||||||
|
state.startDist = Math.hypot(dx, dy)
|
||||||
|
state.startVb = curVb.slice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = e => {
|
||||||
|
if (!state.pointers.has(e.pointerId)) return
|
||||||
|
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||||
|
const curVb = wrap.__mermaidCurViewBox
|
||||||
|
const rect = getRect()
|
||||||
|
if (state.pointers.size === 1 && state.startVb) {
|
||||||
|
const p = state.pointers.values().next().value
|
||||||
|
const dxClient = p.x - state.lastPointerX
|
||||||
|
const dyClient = p.y - state.lastPointerY
|
||||||
|
state.lastPointerX = p.x
|
||||||
|
state.lastPointerY = p.y
|
||||||
|
const dx = dxClient * (curVb[2] / rect.width)
|
||||||
|
const dy = dyClient * (curVb[3] / rect.height)
|
||||||
|
setCurVb([curVb[0] - dx, curVb[1] - dy, curVb[2], curVb[3]])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
|
||||||
|
const pts = [...state.pointers.values()]
|
||||||
|
const dx = pts[0].x - pts[1].x
|
||||||
|
const dy = pts[0].y - pts[1].y
|
||||||
|
const dist = Math.hypot(dx, dy)
|
||||||
|
if (!dist) return
|
||||||
|
const factor = state.startDist / dist
|
||||||
|
const cx = (pts[0].x + pts[1].x) / 2
|
||||||
|
const cy = (pts[0].y + pts[1].y) / 2
|
||||||
|
const px = curVb[0] + (cx - rect.left) * (curVb[2] / rect.width)
|
||||||
|
const py = curVb[1] + (cy - rect.top) * (curVb[3] / rect.height)
|
||||||
|
setCurVb(zoomAtPoint(state.startVb, factor, px, py))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerUpOrCancel = e => {
|
||||||
|
// Release PointerCapture to avoid event capture anomalies
|
||||||
|
if (svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) {
|
||||||
|
svg.releasePointerCapture(e.pointerId)
|
||||||
|
}
|
||||||
|
state.pointers.delete(e.pointerId)
|
||||||
|
if (state.pointers.size === 0) {
|
||||||
|
state.startVb = null
|
||||||
|
state.startDist = 0
|
||||||
|
} else if (state.pointers.size === 1) {
|
||||||
|
const p = state.pointers.values().next().value
|
||||||
|
state.lastPointerX = p.x
|
||||||
|
state.lastPointerY = p.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWheel = e => {
|
||||||
|
// Prevent event bubbling from triggering external scroll
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
// Normalize deltaY across deltaMode (Chrome uses pixels, Safari uses lines)
|
||||||
|
let delta = e.deltaY
|
||||||
|
if (e.deltaMode === 1) delta *= 16
|
||||||
|
else if (e.deltaMode === 2) delta *= 400
|
||||||
|
|
||||||
|
// Continuous zoom factor: smoother than fixed 1.1/0.9 steps
|
||||||
|
const zoomFactor = Math.pow(1.001, delta)
|
||||||
|
|
||||||
|
const curVb = wrap.__mermaidCurViewBox
|
||||||
|
const rect = getRect()
|
||||||
|
const px = curVb[0] + (e.clientX - rect.left) * (curVb[2] / rect.width)
|
||||||
|
const py = curVb[1] + (e.clientY - rect.top) * (curVb[3] / rect.height)
|
||||||
|
setCurVb(zoomAtPoint(curVb, zoomFactor, px, py))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDblClick = () => {
|
||||||
|
const init = wrap.__mermaidInitViewBox
|
||||||
|
if (init) setCurVb(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.addEventListener('pointerdown', onPointerDown, { signal: ac.signal })
|
||||||
|
svg.addEventListener('pointermove', onPointerMove, { signal: ac.signal })
|
||||||
|
svg.addEventListener('pointerup', onPointerUpOrCancel, { signal: ac.signal })
|
||||||
|
svg.addEventListener('pointercancel', onPointerUpOrCancel, { signal: ac.signal })
|
||||||
|
svg.addEventListener('wheel', onWheel, { passive: false, signal: ac.signal })
|
||||||
|
svg.addEventListener('dblclick', onDblClick, { signal: ac.signal })
|
||||||
|
}
|
||||||
|
|
||||||
const runMermaid = ele => {
|
const runMermaid = ele => {
|
||||||
window.loadMermaid = true
|
window.loadMermaid = true
|
||||||
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
||||||
|
|
||||||
ele.forEach((item, index) => {
|
ele.forEach((item, index) => {
|
||||||
const mermaidSrc = item.firstElementChild
|
const mermaidSrc = item.firstElementChild
|
||||||
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
|
// Clean up event listeners before removing old SVG
|
||||||
|
if (item.__mermaidAbortController) {
|
||||||
|
item.__mermaidAbortController.abort()
|
||||||
|
}
|
||||||
|
const oldSvg = item.querySelector('svg')
|
||||||
|
if (oldSvg) oldSvg.remove()
|
||||||
|
let config = {}
|
||||||
|
try {
|
||||||
|
config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[mermaid] failed to parse dataset.config:', e)
|
||||||
|
}
|
||||||
|
if (!config.theme) {
|
||||||
|
config.theme = theme
|
||||||
|
}
|
||||||
|
const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`
|
||||||
const mermaidID = `mermaid-${index}`
|
const mermaidID = `mermaid-${index}`
|
||||||
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
||||||
|
|
||||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
|
||||||
const renderMermaid = svg => {
|
const renderMermaid = svg => {
|
||||||
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
||||||
|
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
|
||||||
|
item.__mermaidOriginalSvg = svg
|
||||||
|
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleError = err => {
|
||||||
|
console.error(`[mermaid] render failed for block #${index}:`, err)
|
||||||
|
const errorEl = document.createElement('div')
|
||||||
|
errorEl.className = 'mermaid-error'
|
||||||
|
errorEl.textContent = `Mermaid render error: ${err.message || err}`
|
||||||
|
mermaidSrc.insertAdjacentElement('afterend', errorEl)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||||
// mermaid v9 and v10 compatibility
|
// mermaid v9 and v10 compatibility
|
||||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
if (typeof renderFn === 'string') {
|
||||||
|
renderMermaid(renderFn)
|
||||||
|
} else {
|
||||||
|
renderFn.then(({ svg }) => renderMermaid(svg)).catch(handleError)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeToMermaid = () => {
|
const codeToMermaid = $article => {
|
||||||
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid')
|
const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
|
||||||
if (codeMermaidEle.length === 0) return
|
if (codeMermaidEle.length === 0) return
|
||||||
|
|
||||||
codeMermaidEle.forEach(ele => {
|
codeMermaidEle.forEach(ele => {
|
||||||
@@ -37,8 +347,11 @@ script.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadMermaid = () => {
|
const loadMermaid = () => {
|
||||||
if (!{theme.mermaid.code_write}) codeToMermaid()
|
const $article = document.getElementById('article-container')
|
||||||
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap')
|
if (!$article) return
|
||||||
|
|
||||||
|
if (!{theme.mermaid.code_write}) codeToMermaid($article)
|
||||||
|
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
|
||||||
if ($mermaid.length === 0) return
|
if ($mermaid.length === 0) return
|
||||||
|
|
||||||
const runMermaidFn = () => runMermaid($mermaid)
|
const runMermaidFn = () => runMermaid($mermaid)
|
||||||
|
|||||||
Vendored
+11
-8
@@ -14,9 +14,9 @@ if choose
|
|||||||
else
|
else
|
||||||
- pjaxSelectors.unshift('meta[name="description"]')
|
- pjaxSelectors.unshift('meta[name="description"]')
|
||||||
|
|
||||||
script(src=url_for(theme.asset.pjax))
|
script(src=url_for(theme.asset.pjax) defer)
|
||||||
script.
|
script.
|
||||||
(() => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
|
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
|
||||||
|
|
||||||
window.pjax = new Pjax({
|
window.pjax = new Pjax({
|
||||||
@@ -53,8 +53,9 @@ script.
|
|||||||
document.addEventListener('pjax:complete', () => {
|
document.addEventListener('pjax:complete', () => {
|
||||||
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
||||||
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
||||||
|
if (!item.parentNode) return
|
||||||
const newScript = document.createElement('script')
|
const newScript = document.createElement('script')
|
||||||
const content = item.text || item.textContent || item.innerHTML || ""
|
const content = item.text || item.textContent || ''
|
||||||
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
||||||
newScript.appendChild(document.createTextNode(content))
|
newScript.appendChild(document.createTextNode(content))
|
||||||
item.parentNode.replaceChild(newScript, item)
|
item.parentNode.replaceChild(newScript, item)
|
||||||
@@ -64,11 +65,13 @@ script.
|
|||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('pjax:error', e => {
|
document.addEventListener('pjax:error', e => {
|
||||||
if (e.request.status === 404) {
|
const responseURL = e.request && e.request.responseURL
|
||||||
const usePjax = !{theme.pjax && theme.pjax.enable}
|
if (e.request && e.request.status === 404) {
|
||||||
!{theme.error_404 && theme.error_404.enable}
|
!{theme.error_404 && theme.error_404.enable}
|
||||||
? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{url_for("/404.html")}')
|
? pjax.loadUrl('!{url_for("/404.html")}')
|
||||||
: window.location.href = e.request.responseURL
|
: responseURL && (window.location.href = responseURL)
|
||||||
|
} else if (responseURL) {
|
||||||
|
window.location.href = responseURL
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})()
|
})
|
||||||
+8
-4
@@ -5,11 +5,15 @@
|
|||||||
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
|
const $article = document.getElementById('article-container')
|
||||||
|
if (!$article || $article.querySelector('.hbe-container')) return
|
||||||
|
|
||||||
window.Prism = window.Prism || {}
|
window.Prism = window.Prism || {}
|
||||||
window.Prism.manual = true
|
window.Prism.manual = true
|
||||||
|
|
||||||
const highlightAll = () => {
|
const highlightAll = () => {
|
||||||
window.Prism.highlightAll()
|
// window.Prism.highlightAll()
|
||||||
|
window.Prism.highlightAllUnder($article, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', highlightAll)
|
window.addEventListener('load', highlightAll)
|
||||||
@@ -17,7 +21,7 @@ if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
|||||||
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
|
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
|
||||||
})()
|
})()
|
||||||
|
|
||||||
script(src=url_for(prismjs_js))
|
script(src=url_for(prismjs_js) defer)
|
||||||
script(src=url_for(prismjs_autoloader))
|
script(src=url_for(prismjs_autoloader) defer)
|
||||||
if (line_number)
|
if (line_number)
|
||||||
script(src=url_for(prismjs_lineNumber_js))
|
script(src=url_for(prismjs_lineNumber_js) defer)
|
||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
- const { effect, source, sub, typed_option } = theme.subtitle
|
- const { effect, source, sub, typed_option } = theme.subtitle
|
||||||
- let subContent = sub || new Array()
|
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
|
||||||
|
|
||||||
script.
|
script.
|
||||||
window.typedJSFn = {
|
window.typedJSFn = {
|
||||||
@@ -44,7 +44,7 @@ script.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
|
btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
|
||||||
|
|
||||||
case source
|
case source
|
||||||
when 1
|
when 1
|
||||||
@@ -53,8 +53,8 @@ case source
|
|||||||
fetch('https://v1.hitokoto.cn')
|
fetch('https://v1.hitokoto.cn')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const from = '出自 ' + data.from
|
const from = data.from ? '出自 ' + data.from : ''
|
||||||
typedJSFn.processSubtitle(data.hitokoto, [from])
|
typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Hitokoto API:', err)
|
console.error('Failed to get the Hitokoto API:', err)
|
||||||
@@ -79,7 +79,7 @@ case source
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Yiyan API:', err)
|
console.error('Failed to get the Yiyan API:', err)
|
||||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
typedJSFn.run(subtitleType)
|
typedJSFn.run(subtitleType)
|
||||||
@@ -99,7 +99,7 @@ case source
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Jinrishici API:', err)
|
console.error('Failed to get the Jinrishici API:', err)
|
||||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
typedJSFn.run(subtitleType)
|
typedJSFn.run(subtitleType)
|
||||||
|
|||||||
+23
-17
@@ -28,21 +28,20 @@ script.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
|
||||||
|
|
||||||
const getData = async (isPost) => {
|
const getData = async (isPost) => {
|
||||||
try {
|
try {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||||
const headerList = { 'Accept': 'application/json' }
|
const headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
if (!{isServerURL}) {
|
[!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
|
||||||
headerList['Authorization'] = `Bearer ${config.token}`
|
|
||||||
} else {
|
|
||||||
headerList['x-umami-api-key'] = config.token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
|
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: headerList
|
headers
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -58,25 +57,30 @@ script.
|
|||||||
|
|
||||||
const insertData = async () => {
|
const insertData = async () => {
|
||||||
try {
|
try {
|
||||||
|
const tasks = []
|
||||||
|
|
||||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||||
|
tasks.push((async () => {
|
||||||
const pagePV = document.getElementById('umamiPV')
|
const pagePV = document.getElementById('umamiPV')
|
||||||
if (pagePV) {
|
if (pagePV) {
|
||||||
const data = await getData(true)
|
const data = await getData(true)
|
||||||
if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
|
if (data && data.pageviews) {
|
||||||
pagePV.textContent = data.pageviews.value
|
pagePV.textContent = extractValue(data.pageviews)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Umami Analytics: Invalid page view data received')
|
console.warn('Umami Analytics: Invalid page view data received')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.site_uv || config.site_pv) {
|
if (config.site_uv || config.site_pv) {
|
||||||
|
tasks.push((async () => {
|
||||||
const data = await getData(false)
|
const data = await getData(false)
|
||||||
|
|
||||||
if (config.site_uv) {
|
if (config.site_uv) {
|
||||||
const siteUV = document.getElementById('umami-site-uv')
|
const siteUV = document.getElementById('umami-site-uv')
|
||||||
if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') {
|
if (siteUV && data && data.visitors) {
|
||||||
siteUV.textContent = data.visitors.value
|
siteUV.textContent = extractValue(data.visitors)
|
||||||
} else if (siteUV) {
|
} else if (siteUV) {
|
||||||
console.warn('Umami Analytics: Invalid site UV data received')
|
console.warn('Umami Analytics: Invalid site UV data received')
|
||||||
}
|
}
|
||||||
@@ -84,13 +88,16 @@ script.
|
|||||||
|
|
||||||
if (config.site_pv) {
|
if (config.site_pv) {
|
||||||
const sitePV = document.getElementById('umami-site-pv')
|
const sitePV = document.getElementById('umami-site-pv')
|
||||||
if (sitePV && data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
|
if (sitePV && data && data.pageviews) {
|
||||||
sitePV.textContent = data.pageviews.value
|
sitePV.textContent = extractValue(data.pageviews)
|
||||||
} else if (sitePV) {
|
} else if (sitePV) {
|
||||||
console.warn('Umami Analytics: Invalid site PV data received')
|
console.warn('Umami Analytics: Invalid site PV data received')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(tasks)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Umami Analytics: Failed to insert data', error)
|
console.error('Umami Analytics: Failed to insert data', error)
|
||||||
}
|
}
|
||||||
@@ -99,7 +106,6 @@ script.
|
|||||||
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
||||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||||
|
|
||||||
|
|
||||||
loadUmamiJS()
|
loadUmamiJS()
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
|
|||||||
i.fas.fa-tags
|
i.fas.fa-tags
|
||||||
span= _p('aside.card_tags')
|
span= _p('aside.card_tags')
|
||||||
|
|
||||||
- let { limit, orderby, order } = theme.aside.card_tags
|
- let { limit, orderby, order, custom_colors } = theme.aside.card_tags
|
||||||
- limit = limit === 0 ? 0 : limit || 40
|
- limit = limit === 0 ? 0 : limit || 40
|
||||||
|
|
||||||
if theme.aside.card_tags.color
|
if theme.aside.card_tags.color
|
||||||
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'})
|
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
|
||||||
else
|
else
|
||||||
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hexo-theme-butterfly",
|
"name": "hexo-theme-butterfly",
|
||||||
"version": "5.5.0",
|
"version": "5.6.0",
|
||||||
"description": "A Simple and Card UI Design theme for Hexo",
|
"description": "A Simple and Card UI Design theme for Hexo",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hexo-renderer-pug": "^3.0.0",
|
"hexo-renderer-pug": "^3.0.0",
|
||||||
"hexo-renderer-stylus": "^3.0.1",
|
"hexo-renderer-stylus": "^3.0.1",
|
||||||
"hexo-util": "^3.3.0",
|
"hexo-util": "^4.0.0",
|
||||||
"moment-timezone": "^0.6.0"
|
"moment-timezone": "^0.6.2"
|
||||||
},
|
},
|
||||||
"homepage": "https://butterfly.js.org/",
|
"homepage": "https://butterfly.js.org/",
|
||||||
"author": "Jerry <my@crazywong.com>",
|
"author": "Jerry <my@crazywong.com>",
|
||||||
|
|||||||
+19
-19
@@ -1,7 +1,7 @@
|
|||||||
abcjs_basic_js:
|
abcjs_basic_js:
|
||||||
name: abcjs
|
name: abcjs
|
||||||
file: dist/abcjs-basic-min.js
|
file: dist/abcjs-basic-min.js
|
||||||
version: 6.5.2
|
version: 6.6.4
|
||||||
activate_power_mode:
|
activate_power_mode:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/activate-power-mode.min.js
|
file: dist/activate-power-mode.min.js
|
||||||
@@ -9,7 +9,7 @@ activate_power_mode:
|
|||||||
algolia_search:
|
algolia_search:
|
||||||
name: algoliasearch
|
name: algoliasearch
|
||||||
file: dist/lite/builds/browser.umd.js
|
file: dist/lite/builds/browser.umd.js
|
||||||
version: 5.37.0
|
version: 5.56.0
|
||||||
aplayer_css:
|
aplayer_css:
|
||||||
name: aplayer
|
name: aplayer
|
||||||
file: dist/APlayer.min.css
|
file: dist/APlayer.min.css
|
||||||
@@ -45,7 +45,7 @@ canvas_ribbon:
|
|||||||
chartjs:
|
chartjs:
|
||||||
name: chart.js
|
name: chart.js
|
||||||
file: dist/chart.umd.js
|
file: dist/chart.umd.js
|
||||||
version: 4.5.0
|
version: 4.5.1
|
||||||
clickShowText:
|
clickShowText:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-show-text.min.js
|
file: dist/click-show-text.min.js
|
||||||
@@ -57,35 +57,35 @@ click_heart:
|
|||||||
disqusjs:
|
disqusjs:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/disqusjs.es2015.umd.min.js
|
file: dist/browser/disqusjs.es2015.umd.min.js
|
||||||
version: 3.1.0
|
version: 3.2.1
|
||||||
disqusjs_css:
|
disqusjs_css:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/styles/disqusjs.css
|
file: dist/browser/styles/disqusjs.css
|
||||||
version: 3.1.0
|
version: 3.2.1
|
||||||
docsearch_css:
|
docsearch_css:
|
||||||
name: '@docsearch/css'
|
name: '@docsearch/css'
|
||||||
other_name: docsearch-css
|
other_name: docsearch-css
|
||||||
file: dist/style.css
|
file: dist/style.css
|
||||||
version: 3.9.0
|
version: 4.6.3
|
||||||
docsearch_js:
|
docsearch_js:
|
||||||
name: '@docsearch/js'
|
name: '@docsearch/js'
|
||||||
other_name: docsearch-js
|
other_name: docsearch-js
|
||||||
file: dist/umd/index.js
|
file: dist/umd/index.js
|
||||||
version: 3.9.0
|
version: 4.6.3
|
||||||
egjs_infinitegrid:
|
egjs_infinitegrid:
|
||||||
name: '@egjs/infinitegrid'
|
name: '@egjs/infinitegrid'
|
||||||
other_name: egjs-infinitegrid
|
other_name: egjs-infinitegrid
|
||||||
file: dist/infinitegrid.min.js
|
file: dist/infinitegrid.min.js
|
||||||
version: 4.12.0
|
version: 4.13.0
|
||||||
fancybox:
|
fancybox:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.umd.js
|
file: dist/fancybox/fancybox.umd.js
|
||||||
version: 6.0.29
|
version: 6.1.14
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fancybox_css:
|
fancybox_css:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.css
|
file: dist/fancybox/fancybox.css
|
||||||
version: 6.0.29
|
version: 6.1.14
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fireworks:
|
fireworks:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
@@ -95,7 +95,7 @@ fontawesome:
|
|||||||
name: '@fortawesome/fontawesome-free'
|
name: '@fortawesome/fontawesome-free'
|
||||||
file: css/all.min.css
|
file: css/all.min.css
|
||||||
other_name: font-awesome
|
other_name: font-awesome
|
||||||
version: 7.0.1
|
version: 7.3.1
|
||||||
gitalk:
|
gitalk:
|
||||||
name: gitalk
|
name: gitalk
|
||||||
file: dist/gitalk.min.js
|
file: dist/gitalk.min.js
|
||||||
@@ -112,12 +112,12 @@ katex:
|
|||||||
name: katex
|
name: katex
|
||||||
file: dist/katex.min.css
|
file: dist/katex.min.css
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.22
|
version: 0.17.0
|
||||||
katex_copytex:
|
katex_copytex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/contrib/copy-tex.min.js
|
file: dist/contrib/copy-tex.min.js
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.22
|
version: 0.17.0
|
||||||
lazyload:
|
lazyload:
|
||||||
name: vanilla-lazyload
|
name: vanilla-lazyload
|
||||||
file: dist/lazyload.iife.min.js
|
file: dist/lazyload.iife.min.js
|
||||||
@@ -125,7 +125,7 @@ lazyload:
|
|||||||
mathjax:
|
mathjax:
|
||||||
name: mathjax
|
name: mathjax
|
||||||
file: tex-mml-chtml.js
|
file: tex-mml-chtml.js
|
||||||
version: 4.0.0
|
version: 4.1.3
|
||||||
medium_zoom:
|
medium_zoom:
|
||||||
name: medium-zoom
|
name: medium-zoom
|
||||||
file: dist/medium-zoom.min.js
|
file: dist/medium-zoom.min.js
|
||||||
@@ -133,7 +133,7 @@ medium_zoom:
|
|||||||
mermaid:
|
mermaid:
|
||||||
name: mermaid
|
name: mermaid
|
||||||
file: dist/mermaid.min.js
|
file: dist/mermaid.min.js
|
||||||
version: 11.11.0
|
version: 11.16.0
|
||||||
meting_js:
|
meting_js:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: metingjs/dist/Meting.min.js
|
file: metingjs/dist/Meting.min.js
|
||||||
@@ -186,11 +186,11 @@ snackbar_css:
|
|||||||
twikoo:
|
twikoo:
|
||||||
name: twikoo
|
name: twikoo
|
||||||
file: dist/twikoo.all.min.js
|
file: dist/twikoo.all.min.js
|
||||||
version: 1.6.44
|
version: 1.7.14
|
||||||
typed:
|
typed:
|
||||||
name: typed.js
|
name: typed.js
|
||||||
file: dist/typed.umd.js
|
file: dist/typed.umd.js
|
||||||
version: 2.1.0
|
version: 3.0.0
|
||||||
valine:
|
valine:
|
||||||
name: valine
|
name: valine
|
||||||
file: dist/Valine.min.js
|
file: dist/Valine.min.js
|
||||||
@@ -199,9 +199,9 @@ waline_css:
|
|||||||
name: '@waline/client'
|
name: '@waline/client'
|
||||||
file: dist/waline.css
|
file: dist/waline.css
|
||||||
other_name: waline
|
other_name: waline
|
||||||
version: 3.6.0
|
version: 3.15.2
|
||||||
waline_js:
|
waline_js:
|
||||||
name: '@waline/client'
|
name: '@waline/client'
|
||||||
file: dist/waline.js
|
file: dist/waline.js
|
||||||
other_name: waline
|
other_name: waline
|
||||||
version: 3.6.0
|
version: 3.15.2
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ module.exports = {
|
|||||||
enable: true,
|
enable: true,
|
||||||
limit: 40,
|
limit: 40,
|
||||||
color: false,
|
color: false,
|
||||||
|
custom_colors: null,
|
||||||
orderby: 'random',
|
orderby: 'random',
|
||||||
order: 1,
|
order: 1,
|
||||||
sort_order: null
|
sort_order: null
|
||||||
@@ -290,10 +291,10 @@ module.exports = {
|
|||||||
share: {
|
share: {
|
||||||
use: 'sharejs',
|
use: 'sharejs',
|
||||||
sharejs: {
|
sharejs: {
|
||||||
sites: 'facebook,twitter,wechat,weibo,qq'
|
sites: 'facebook,x,wechat,weibo,qq'
|
||||||
},
|
},
|
||||||
addtoany: {
|
addtoany: {
|
||||||
item: 'facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link'
|
item: 'facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
comments: {
|
comments: {
|
||||||
@@ -393,6 +394,9 @@ module.exports = {
|
|||||||
crisp: {
|
crisp: {
|
||||||
website_id: null
|
website_id: null
|
||||||
},
|
},
|
||||||
|
knocket: {
|
||||||
|
identifier: null
|
||||||
|
},
|
||||||
google_tag_manager: {
|
google_tag_manager: {
|
||||||
tag_id: null,
|
tag_id: null,
|
||||||
domain: 'https://www.googletagmanager.com'
|
domain: 'https://www.googletagmanager.com'
|
||||||
@@ -522,7 +526,9 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
light: 'default',
|
light: 'default',
|
||||||
dark: 'dark'
|
dark: 'dark'
|
||||||
}
|
},
|
||||||
|
open_in_new_tab: true,
|
||||||
|
zoom_pan: true
|
||||||
},
|
},
|
||||||
chartjs: {
|
chartjs: {
|
||||||
enable: false,
|
enable: false,
|
||||||
@@ -591,7 +597,7 @@ module.exports = {
|
|||||||
CDN: {
|
CDN: {
|
||||||
internal_provider: 'local',
|
internal_provider: 'local',
|
||||||
third_party_provider: 'jsdelivr',
|
third_party_provider: 'jsdelivr',
|
||||||
version: false,
|
version: true,
|
||||||
custom_format: null,
|
custom_format: null,
|
||||||
option: null
|
option: null
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-20
@@ -46,45 +46,43 @@ hexo.extend.filter.register('before_generate', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createCDNLink = (data, type, cond = '') => {
|
const createCDNLink = (data, type, cond = '') => {
|
||||||
Object.keys(data).forEach(key => {
|
return Object.keys(data).reduce((result, key) => {
|
||||||
let { name, version, file, other_name } = data[key]
|
let { name, version, file, other_name: otherName } = data[key]
|
||||||
const cdnjs_name = other_name || name
|
const cdnjsName = otherName || name
|
||||||
const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
||||||
const min_cdnjs_file = minFile(cdnjs_file)
|
const minCdnjsFile = minFile(cdnjsFile)
|
||||||
if (cond === 'internal') file = `source/${file}`
|
if (cond === 'internal') file = `source/${file}`
|
||||||
const min_file = minFile(file)
|
const minFilePath = minFile(file)
|
||||||
const verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
|
const verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
version,
|
version,
|
||||||
name,
|
name,
|
||||||
file,
|
file,
|
||||||
cdnjs_file,
|
cdnjs_file: cdnjsFile,
|
||||||
min_file,
|
min_file: minFilePath,
|
||||||
min_cdnjs_file,
|
min_cdnjs_file: minCdnjsFile,
|
||||||
cdnjs_name
|
cdnjs_name: cdnjsName
|
||||||
}
|
}
|
||||||
|
|
||||||
const cdnSource = {
|
const cdnSource = {
|
||||||
local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
local: cond === 'internal' ? `${cdnjsFile + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
||||||
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
|
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${minFilePath}`,
|
||||||
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
|
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
|
||||||
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjs_name}/${version}/${min_cdnjs_file}`,
|
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjsName}/${version}/${minCdnjsFile}`,
|
||||||
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
|
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
|
||||||
}
|
}
|
||||||
|
|
||||||
data[key] = cdnSource[type]
|
result[key] = cdnSource[type]
|
||||||
})
|
return result
|
||||||
|
}, cond === 'internal' ? { main_css: 'css/index.css' + (CDN.version ? `?v=${version}` : '') } : {})
|
||||||
if (cond === 'internal') data.main_css = 'css/index.css' + (CDN.version ? `?v=${version}` : '')
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete null value
|
// delete null value
|
||||||
const deleteNullValue = obj => {
|
const deleteNullValue = obj => {
|
||||||
if (!obj) return
|
if (!obj) return {}
|
||||||
for (const i in obj) {
|
for (const i in obj) {
|
||||||
obj[i] === null && delete obj[i]
|
if (obj[i] === null) delete obj[i]
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function checkHexoEnvironment (hexo) {
|
|||||||
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
|
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
|
||||||
log.error('Please update Hexo to V5.3.0 or higher!')
|
log.error('Please update Hexo to V5.3.0 or higher!')
|
||||||
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
||||||
process.exit(-1)
|
throw new Error('Hexo version too old')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for deprecated configuration file
|
// Check for deprecated configuration file
|
||||||
@@ -26,7 +26,7 @@ function checkHexoEnvironment (hexo) {
|
|||||||
if (data && data.butterfly) {
|
if (data && data.butterfly) {
|
||||||
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
||||||
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
||||||
process.exit(-1)
|
throw new Error('Deprecated configuration file')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,12 @@ const lazyload = htmlContent => {
|
|||||||
|
|
||||||
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||||
|
|
||||||
// Use more precise replacement: handle src attributes with double and single quotes, but avoid replacing content inside script tags
|
// Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
|
||||||
let result = htmlContent
|
// Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify)
|
||||||
|
return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => {
|
||||||
// Handle src attributes with double quotes
|
const src = srcDoubleQuote ?? srcSingleQuote ?? srcNoQuote
|
||||||
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc="([^"]+)")(?![^<]*<\/script>)/gi, (match, tag, src) => {
|
return `${prefix}"${bg}" data-lazy-src="${src}"`
|
||||||
return tag.replace(`src="${src}"`, `src="${bg}" data-lazy-src="${src}"`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle src attributes with single quotes
|
|
||||||
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc='([^']+)')(?![^<]*<\/script>)/gi, (match, tag, src) => {
|
|
||||||
return tag.replace(`src='${src}'`, `src='${bg}' data-lazy-src='${src}'`)
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.filter.register('after_render:html', data => {
|
hexo.extend.filter.register('after_render:html', data => {
|
||||||
|
|||||||
@@ -5,78 +5,105 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.generator.register('post', locals => {
|
hexo.extend.generator.register('post', locals => {
|
||||||
const previousIndexes = []
|
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||||
|
const remoteImgReg = /^(?:https?:)?\/\//i
|
||||||
|
const dataImgReg = /^data:image\//i
|
||||||
|
|
||||||
const getRandomCover = defaultCover => {
|
const { post_asset_folder: postAssetFolder } = hexo.config
|
||||||
if (!defaultCover) return false
|
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
||||||
if (!Array.isArray(defaultCover)) return defaultCover
|
|
||||||
|
const isImage = value => {
|
||||||
|
return typeof value === 'string' &&
|
||||||
|
(remoteImgReg.test(value) || dataImgReg.test(value) || imgTestReg.test(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function * createCoverGenerator () {
|
||||||
|
if (!defaultCover || (Array.isArray(defaultCover) && defaultCover.length === 0)) {
|
||||||
|
while (true) yield false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(defaultCover)) {
|
||||||
|
while (true) yield defaultCover
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultCover.length === 1) {
|
||||||
|
while (true) yield defaultCover[0]
|
||||||
|
}
|
||||||
|
|
||||||
const coverCount = defaultCover.length
|
const coverCount = defaultCover.length
|
||||||
|
const maxHistory = Math.min(3, coverCount - 1)
|
||||||
|
const history = []
|
||||||
|
|
||||||
if (coverCount === 1) {
|
while (true) {
|
||||||
return defaultCover[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
|
|
||||||
|
|
||||||
let index
|
let index
|
||||||
|
|
||||||
do {
|
do {
|
||||||
index = Math.floor(Math.random() * coverCount)
|
index = Math.floor(Math.random() * coverCount)
|
||||||
} while (previousIndexes.includes(index) && previousIndexes.length < coverCount)
|
} while (history.includes(index))
|
||||||
|
|
||||||
previousIndexes.push(index)
|
history.push(index)
|
||||||
if (previousIndexes.length > maxPreviousIndexes) {
|
|
||||||
previousIndexes.shift()
|
if (history.length > maxHistory) {
|
||||||
|
history.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultCover[index]
|
yield defaultCover[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverGenerator = createCoverGenerator()
|
||||||
|
|
||||||
|
const resolvePostAsset = (value, postPath) => {
|
||||||
|
if (
|
||||||
|
!postAssetFolder ||
|
||||||
|
typeof value !== 'string' ||
|
||||||
|
value.includes('/') ||
|
||||||
|
!imgTestReg.test(value)
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${postPath}${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImg = data => {
|
const handleImg = data => {
|
||||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
data.top_img = resolvePostAsset(data.top_img, data.path)
|
||||||
let { cover: coverVal, top_img: topImg } = data
|
data.cover = resolvePostAsset(data.cover, data.path)
|
||||||
|
data.pagination_cover = resolvePostAsset(data.pagination_cover, data.path)
|
||||||
|
|
||||||
// Add path to top_img and cover if post_asset_folder is enabled
|
if (data.cover === false) return data
|
||||||
if (hexo.config.post_asset_folder) {
|
|
||||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
|
if (!data.cover) {
|
||||||
data.top_img = `${data.path}${topImg}`
|
data.cover = coverGenerator.next().value
|
||||||
}
|
|
||||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
|
|
||||||
data.cover = `${data.path}${coverVal}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverVal === false) return data
|
if (isImage(data.cover)) {
|
||||||
|
|
||||||
// If cover is not set, use random cover
|
|
||||||
if (!coverVal) {
|
|
||||||
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
|
||||||
const randomCover = getRandomCover(defaultCover)
|
|
||||||
data.cover = randomCover
|
|
||||||
coverVal = randomCover // update coverVal
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
|
|
||||||
data.cover_type = 'img'
|
data.cover_type = 'img'
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
|
|
||||||
const posts = locals.posts.sort('date').toArray()
|
const posts = locals.posts.sort('date').toArray()
|
||||||
const { length } = posts
|
const { length } = posts
|
||||||
|
|
||||||
return posts.map((post, i) => {
|
return posts.map((post, index) => {
|
||||||
if (i) post.prev = posts[i - 1]
|
const data = post
|
||||||
if (i < length - 1) post.next = posts[i + 1]
|
|
||||||
|
|
||||||
post.__post = true
|
if (index > 0) {
|
||||||
|
data.prev = posts[index - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < length - 1) {
|
||||||
|
data.next = posts[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
data.__post = true
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: handleImg(post),
|
data: handleImg(data),
|
||||||
layout: 'post',
|
layout: 'post',
|
||||||
path: post.path
|
path: data.path
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||||
const { config, page, site, url_for, _p } = this
|
const { config, page, site, url_for: urlFor, _p } = this
|
||||||
const { archive_dir: archiveDir, timezone, language } = config
|
const { archive_dir: archiveDir, timezone, language } = config
|
||||||
|
|
||||||
// Destructure and set default options with object destructuring
|
// Destructure and set default options with object destructuring
|
||||||
@@ -17,56 +17,43 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
// Optimize locale handling
|
// Optimize locale handling
|
||||||
const lang = toMomentLocale(page.lang || page.language || language)
|
const lang = toMomentLocale(page.lang || page.language || language)
|
||||||
|
|
||||||
// Memoize comparison function to improve performance
|
|
||||||
const compareFunc =
|
|
||||||
type === 'monthly'
|
|
||||||
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
|
|
||||||
: (yearA, yearB) => yearA === yearB
|
|
||||||
|
|
||||||
// Early return if no posts
|
// Early return if no posts
|
||||||
if (!site.posts.length) return ''
|
if (!site.posts.length) return ''
|
||||||
|
|
||||||
// Use reduce for more efficient data processing
|
const archives = new Map()
|
||||||
const data = site.posts.sort('date', order).reduce((acc, post) => {
|
site.posts.forEach(post => {
|
||||||
let date = post.date.clone()
|
const date = post.date
|
||||||
if (timezone) date = date.tz(timezone)
|
|
||||||
|
|
||||||
const year = date.year()
|
const year = date.year()
|
||||||
const month = date.month() + 1
|
const month = date.month() + 1
|
||||||
|
const key = type === 'yearly' ? year : `${year}-${month}`
|
||||||
|
|
||||||
|
if (archives.has(key)) {
|
||||||
|
archives.get(key).count++
|
||||||
|
} else {
|
||||||
|
archives.set(key, {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
count: 1,
|
||||||
|
date // Store date object for later formatting
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = Array.from(archives.values()).sort((a, b) => {
|
||||||
|
if (order === -1) {
|
||||||
|
return b.year - a.year || b.month - a.month
|
||||||
|
}
|
||||||
|
return a.year - b.year || a.month - b.month
|
||||||
|
})
|
||||||
|
|
||||||
|
// Format names after aggregation
|
||||||
|
data.forEach(item => {
|
||||||
|
let date = item.date.clone()
|
||||||
|
if (timezone) date = date.tz(timezone)
|
||||||
if (lang) date = date.locale(lang)
|
if (lang) date = date.locale(lang)
|
||||||
|
item.name = date.format(format)
|
||||||
// Find or create archive entry
|
delete item.date // Clean up
|
||||||
const lastEntry = acc[acc.length - 1]
|
|
||||||
|
|
||||||
if (type === 'yearly') {
|
|
||||||
const existingYearIndex = acc.findIndex(entry => entry.year === year)
|
|
||||||
if (existingYearIndex !== -1) {
|
|
||||||
acc[existingYearIndex].count++
|
|
||||||
} else {
|
|
||||||
// 否則創建新條目
|
|
||||||
acc.push({
|
|
||||||
name: date.format(format),
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
count: 1
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!lastEntry || !compareFunc(lastEntry.year, lastEntry.month, year, month)) {
|
|
||||||
acc.push({
|
|
||||||
name: date.format(format),
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
count: 1
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
lastEntry.count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Create link generator function
|
// Create link generator function
|
||||||
const createArchiveLink = item => {
|
const createArchiveLink = item => {
|
||||||
@@ -74,7 +61,7 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
if (type === 'monthly') {
|
if (type === 'monthly') {
|
||||||
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
||||||
}
|
}
|
||||||
return url_for(url)
|
return urlFor(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit results efficiently
|
// Limit results efficiently
|
||||||
@@ -87,7 +74,7 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
<span>${_p('aside.card_archives')}</span>
|
<span>${_p('aside.card_archives')}</span>
|
||||||
${
|
${
|
||||||
data.length > limitedData.length
|
data.length > limitedData.length
|
||||||
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
|
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
|
||||||
title="${_p('aside.more_button')}">
|
title="${_p('aside.more_button')}">
|
||||||
<i class="fas fa-angle-right"></i>
|
<i class="fas fa-angle-right"></i>
|
||||||
</a>`
|
</a>`
|
||||||
|
|||||||
@@ -19,15 +19,33 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
const expandClass = isExpand && options.expand === true ? 'expand' : ''
|
const expandClass = isExpand && options.expand === true ? 'expand' : ''
|
||||||
const buttonLabel = this._p('aside.more_button')
|
const buttonLabel = this._p('aside.more_button')
|
||||||
|
|
||||||
const prepareQuery = parent => {
|
const categoryMap = new Map()
|
||||||
const query = parent ? { parent } : { parent: { $exists: false } }
|
categories.forEach(cat => {
|
||||||
return categories.find(query).sort(orderby, order).filter(cat => cat.length)
|
if (cat.length) {
|
||||||
|
const parentId = cat.parent || 'root'
|
||||||
|
if (!categoryMap.has(parentId)) {
|
||||||
|
categoryMap.set(parentId, [])
|
||||||
|
}
|
||||||
|
categoryMap.get(parentId).push(cat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortFn = (a, b) => {
|
||||||
|
const valA = a[orderby]
|
||||||
|
const valB = b[orderby]
|
||||||
|
if (valA < valB) return -order
|
||||||
|
if (valA > valB) return order
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const hierarchicalList = (remaining, level = 0, parent) => {
|
for (const list of categoryMap.values()) {
|
||||||
|
list.sort(sortFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hierarchicalList = (remaining, level = 0, parentId = 'root') => {
|
||||||
let result = ''
|
let result = ''
|
||||||
if (remaining > 0) {
|
if (remaining > 0 && categoryMap.has(parentId)) {
|
||||||
prepareQuery(parent).forEach(cat => {
|
categoryMap.get(parentId).forEach(cat => {
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
remaining -= 1
|
remaining -= 1
|
||||||
let child = ''
|
let child = ''
|
||||||
@@ -37,7 +55,8 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
remaining = childList.remaining
|
remaining = childList.remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentClass = isExpand && !parent && child ? 'parent' : ''
|
const isTopLevel = parentId === 'root'
|
||||||
|
const parentClass = isExpand && isTopLevel && child ? 'parent' : ''
|
||||||
result += `<li class="card-category-list-item ${parentClass}">`
|
result += `<li class="card-category-list-item ${parentClass}">`
|
||||||
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
|
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
|
||||||
result += `<span class="card-category-list-name">${cat.name}</span>`
|
result += `<span class="card-category-list-name">${cat.name}</span>`
|
||||||
@@ -46,7 +65,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
result += `<span class="card-category-list-count">${cat.length}</span>`
|
result += `<span class="card-category-list-count">${cat.length}</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpand && !parent && child) {
|
if (isExpand && isTopLevel && child) {
|
||||||
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
|
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,36 +8,36 @@ hexo.extend.helper.register('getArchiveLength', function () {
|
|||||||
// Archives Page
|
// Archives Page
|
||||||
if (!year) return posts.length
|
if (!year) return posts.length
|
||||||
|
|
||||||
// Function to generate a unique key based on the granularity
|
|
||||||
const getKey = (post, type) => {
|
|
||||||
const date = post.date.clone()
|
|
||||||
const y = date.year()
|
|
||||||
const m = date.month() + 1
|
|
||||||
const d = date.date()
|
|
||||||
if (type === 'year') return `${y}`
|
|
||||||
if (type === 'month') return `${y}-${m}`
|
|
||||||
if (type === 'day') return `${y}-${m}-${d}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map to count posts per period
|
// Create a map to count posts per period
|
||||||
const mapData = this.fragment_cache('createArchiveObj', () => {
|
const mapData = this.fragment_cache('createArchiveObj', () => {
|
||||||
const map = new Map()
|
const map = new Map()
|
||||||
posts.forEach(post => {
|
posts.forEach(post => {
|
||||||
const keyYear = getKey(post, 'year')
|
const date = post.date
|
||||||
const keyMonth = getKey(post, 'month')
|
const y = date.year()
|
||||||
const keyDay = getKey(post, 'day')
|
const m = date.month() + 1
|
||||||
|
const d = date.date()
|
||||||
|
|
||||||
if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
// Always track year so year archive pages can be counted
|
||||||
if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
const keyYear = `${y}`
|
||||||
if (daily) map.set(keyDay, (map.get(keyDay) || 0) + 1)
|
map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
||||||
|
|
||||||
|
if (monthly || daily) {
|
||||||
|
const keyMonth = `${y}-${m}`
|
||||||
|
map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daily) {
|
||||||
|
const keyDay = `${y}-${m}-${d}`
|
||||||
|
map.set(keyDay, (map.get(keyDay) || 0) + 1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
// Determine the appropriate key to fetch based on current page context
|
// Determine the appropriate key to fetch based on current page context
|
||||||
let key
|
let key
|
||||||
if (yearly && year) key = `${year}`
|
if (yearly || monthly || daily) key = `${year}`
|
||||||
if (monthly && month) key = `${year}-${month}`
|
if ((monthly || daily) && month) key = `${year}-${month}`
|
||||||
if (daily && day) key = `${year}-${month}-${day}`
|
if (daily && day) key = `${year}-${month}-${day}`
|
||||||
|
|
||||||
// Return the count for the current period or default to the total posts
|
// Return the count for the current period or default to the total posts
|
||||||
|
|||||||
@@ -4,53 +4,94 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
const { darkmode, aside, pjax } = this.theme
|
const { darkmode, aside, pjax } = this.theme
|
||||||
const start = darkmode.start || 6
|
const start = darkmode.start || 6
|
||||||
const end = darkmode.end || 18
|
const end = darkmode.end || 18
|
||||||
const { theme_color } = hexo.theme.config
|
const { theme_color: themeColor } = hexo.theme.config
|
||||||
const themeColorLight = theme_color && theme_color.enable ? theme_color.meta_theme_color_light : '#ffffff'
|
const themeColorLight = themeColor && themeColor.enable ? themeColor.meta_theme_color_light : '#ffffff'
|
||||||
const themeColorDark = theme_color && theme_color.enable ? theme_color.meta_theme_color_dark : '#0d0d0d'
|
const themeColorDark = themeColor && themeColor.enable ? themeColor.meta_theme_color_dark : '#0d0d0d'
|
||||||
|
|
||||||
const createCustomJs = () => `
|
const createCustomJs = () => `
|
||||||
const saveToLocal = {
|
const saveToLocal = {
|
||||||
set: (key, value, ttl) => {
|
set: (key, value, ttl) => {
|
||||||
if (!ttl) return
|
const data = { value }
|
||||||
const expiry = Date.now() + ttl * 86400000
|
|
||||||
localStorage.setItem(key, JSON.stringify({ value, expiry }))
|
if (ttl != null) {
|
||||||
|
data.expiry = Date.now() + ttl * 86400000
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(key, JSON.stringify(data))
|
||||||
},
|
},
|
||||||
get: key => {
|
get: key => {
|
||||||
const itemStr = localStorage.getItem(key)
|
const itemStr = localStorage.getItem(key)
|
||||||
if (!itemStr) return undefined
|
if (!itemStr) return
|
||||||
const { value, expiry } = JSON.parse(itemStr)
|
|
||||||
if (Date.now() > expiry) {
|
try {
|
||||||
|
const data = JSON.parse(itemStr)
|
||||||
|
|
||||||
|
if (data.expiry && Date.now() > data.expiry) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value
|
||||||
|
} catch {
|
||||||
localStorage.removeItem(key)
|
localStorage.removeItem(key)
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scriptCache = new Map()
|
||||||
|
const cssCache = new Map()
|
||||||
window.btf = {
|
window.btf = {
|
||||||
saveToLocal,
|
saveToLocal,
|
||||||
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
|
getScript: (url, attr = {}) => {
|
||||||
|
if (scriptCache.has(url)) {
|
||||||
|
return scriptCache.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
|
|
||||||
script.src = url
|
script.src = url
|
||||||
script.async = true
|
script.async = true
|
||||||
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
|
|
||||||
script.onload = script.onreadystatechange = () => {
|
for (const key in attr) {
|
||||||
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
|
script.setAttribute(key, attr[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
script.onload = resolve
|
||||||
script.onerror = reject
|
script.onerror = reject
|
||||||
|
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
}),
|
})
|
||||||
getCSS: (url, id) => new Promise((resolve, reject) => {
|
|
||||||
|
scriptCache.set(url, promise)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
},
|
||||||
|
getCSS: (url, id) => {
|
||||||
|
if (cssCache.has(url)) {
|
||||||
|
return cssCache.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
const link = document.createElement('link')
|
const link = document.createElement('link')
|
||||||
|
|
||||||
link.rel = 'stylesheet'
|
link.rel = 'stylesheet'
|
||||||
link.href = url
|
link.href = url
|
||||||
if (id) link.id = id
|
|
||||||
link.onload = link.onreadystatechange = () => {
|
if (id) {
|
||||||
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
link.id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link.onload = resolve
|
||||||
link.onerror = reject
|
link.onerror = reject
|
||||||
|
|
||||||
document.head.appendChild(link)
|
document.head.appendChild(link)
|
||||||
}),
|
})
|
||||||
|
|
||||||
|
cssCache.set(url, promise)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
},
|
||||||
addGlobalFn: (key, fn, name = false, parent = window) => {
|
addGlobalFn: (key, fn, name = false, parent = window) => {
|
||||||
if (!${pjax.enable} && key.startsWith('pjax')) return
|
if (!${pjax.enable} && key.startsWith('pjax')) return
|
||||||
const globalFn = parent.globalFn || {}
|
const globalFn = parent.globalFn || {}
|
||||||
@@ -65,16 +106,17 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
if (!darkmode.enable) return ''
|
if (!darkmode.enable) return ''
|
||||||
|
|
||||||
let darkmodeJs = `
|
let darkmodeJs = `
|
||||||
|
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
||||||
const activateDarkMode = () => {
|
const activateDarkMode = () => {
|
||||||
document.documentElement.setAttribute('data-theme', 'dark')
|
document.documentElement.dataset.theme = 'dark'
|
||||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
if (metaThemeColor !== null) {
|
||||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}')
|
metaThemeColor.setAttribute('content', '${themeColorDark}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const activateLightMode = () => {
|
const activateLightMode = () => {
|
||||||
document.documentElement.setAttribute('data-theme', 'light')
|
document.documentElement.dataset.theme = 'light'
|
||||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
if (metaThemeColor !== null) {
|
||||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}')
|
metaThemeColor.setAttribute('content', '${themeColorLight}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +137,15 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
else if (mediaQueryDark.matches) activateDarkMode()
|
else if (mediaQueryDark.matches) activateDarkMode()
|
||||||
else {
|
else {
|
||||||
const hour = new Date().getHours()
|
const hour = new Date().getHours()
|
||||||
const isNight = hour <= ${start} || hour >= ${end}
|
const start = ${start}
|
||||||
|
const end = ${end}
|
||||||
|
const isNight =
|
||||||
|
start < end
|
||||||
|
? hour < start || hour >= end
|
||||||
|
: hour >= start || hour < end
|
||||||
isNight ? activateDarkMode() : activateLightMode()
|
isNight ? activateDarkMode() : activateLightMode()
|
||||||
}
|
}
|
||||||
mediaQueryDark.addEventListener('change', () => {
|
mediaQueryDark.addEventListener('change', e => {
|
||||||
if (saveToLocal.get('theme') === undefined) {
|
if (saveToLocal.get('theme') === undefined) {
|
||||||
e.matches ? activateDarkMode() : activateLightMode()
|
e.matches ? activateDarkMode() : activateLightMode()
|
||||||
}
|
}
|
||||||
@@ -111,7 +158,12 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
case 2:
|
case 2:
|
||||||
darkmodeJs += `
|
darkmodeJs += `
|
||||||
const hour = new Date().getHours()
|
const hour = new Date().getHours()
|
||||||
const isNight = hour <= ${start} || hour >= ${end}
|
const start = ${start}
|
||||||
|
const end = ${end}
|
||||||
|
const isNight =
|
||||||
|
start < end
|
||||||
|
? hour < start || hour >= end
|
||||||
|
: hour >= start || hour < end
|
||||||
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
|
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
|
||||||
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
||||||
`
|
`
|
||||||
|
|||||||
+76
-43
@@ -5,6 +5,14 @@ const { prettyUrls } = require('hexo-util')
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const moment = require('moment-timezone')
|
const moment = require('moment-timezone')
|
||||||
|
|
||||||
|
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
||||||
|
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
|
||||||
|
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
||||||
|
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
|
||||||
|
const archiveRegex = /\/archives\//
|
||||||
|
|
||||||
|
const { version: themeVersion } = require('../../package.json')
|
||||||
|
|
||||||
hexo.extend.helper.register('truncate', truncateContent)
|
hexo.extend.helper.register('truncate', truncateContent)
|
||||||
|
|
||||||
hexo.extend.helper.register('postDesc', data => {
|
hexo.extend.helper.register('postDesc', data => {
|
||||||
@@ -13,39 +21,64 @@ hexo.extend.helper.register('postDesc', data => {
|
|||||||
|
|
||||||
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||||
const env = this
|
const env = this
|
||||||
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options
|
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
|
||||||
|
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
source = source.limit(limit)
|
source = source.limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
|
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
|
||||||
|
const sizeMap = new Map(sizes.map((size, index) => [size, index]))
|
||||||
|
const length = sizes.length - 1
|
||||||
|
|
||||||
const getRandomColor = () => {
|
const getRandomColor = () => {
|
||||||
const randomColor = () => Math.floor(Math.random() * 201)
|
const r = Math.floor(Math.random() * 201)
|
||||||
const r = randomColor()
|
const g = Math.floor(Math.random() * 201)
|
||||||
const g = randomColor()
|
const b = Math.floor(Math.random() * 201)
|
||||||
const b = randomColor()
|
|
||||||
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateStyle = (size, unit, page) => {
|
const normalizeColors = input => {
|
||||||
if (page === 'tags') {
|
if (!input) return null
|
||||||
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; background-color: ${getRandomColor()};`
|
if (typeof input === 'string') {
|
||||||
} else {
|
const color = input.trim()
|
||||||
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
|
return color ? [color] : null
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const value = input[i]
|
||||||
|
if (value === null || value === undefined) continue
|
||||||
|
const color = String(value).trim()
|
||||||
|
if (!color) continue
|
||||||
|
result.push(color)
|
||||||
|
}
|
||||||
|
return result.length ? result : null
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = sizes.length - 1
|
const userColors = normalizeColors(custom_colors)
|
||||||
const result = source.sort(orderby, order).map(tag => {
|
|
||||||
const ratio = length ? sizes.indexOf(tag.length) / length : 0
|
const generateStyle = (size, unit, page, color) => {
|
||||||
|
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
|
||||||
|
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.sort(orderby, order).map((tag, idx) => {
|
||||||
|
const ratio = length ? sizeMap.get(tag.length) / length : 0
|
||||||
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
||||||
const style = generateStyle(size, unit, page)
|
|
||||||
|
if (userColors && userColors.length) {
|
||||||
|
const color = userColors[idx % userColors.length]
|
||||||
|
const style = generateStyle(size, unit, page, color)
|
||||||
|
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item" style="${style}">${tag.name}</a>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = getRandomColor()
|
||||||
|
const style = generateStyle(size, unit, page, color)
|
||||||
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
|
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
|
||||||
@@ -71,13 +104,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
if (!menu) return defaultTitle
|
if (!menu) return defaultTitle
|
||||||
|
|
||||||
const loop = m => {
|
const loop = m => {
|
||||||
for (const key in m) {
|
for (const [key, value] of Object.entries(m)) {
|
||||||
if (typeof m[key] === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
const result = loop(m[key])
|
const result = loop(value)
|
||||||
if (result) return result
|
if (result) return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/archives\//.test(m[key])) {
|
if (typeof value === 'string' && archiveRegex.test(value)) {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,13 +122,9 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
hexo.extend.helper.register('getBgPath', function (path) {
|
hexo.extend.helper.register('getBgPath', function (path) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
|
||||||
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
|
|
||||||
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
|
||||||
|
|
||||||
if (colorPattern.test(path)) {
|
if (colorPattern.test(path)) {
|
||||||
return `background-color: ${path};`
|
return `background-color: ${path};`
|
||||||
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) {
|
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path) || simpleFilePattern.test(path)) {
|
||||||
return `background-image: url(${this.url_for(path)});`
|
return `background-image: url(${this.url_for(path)});`
|
||||||
} else {
|
} else {
|
||||||
return `background: ${path};`
|
return `background: ${path};`
|
||||||
@@ -104,39 +133,35 @@ hexo.extend.helper.register('getBgPath', function (path) {
|
|||||||
|
|
||||||
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
||||||
const { limit } = page
|
const { limit } = page
|
||||||
let finalResult = ''
|
|
||||||
|
// Shallow copy to avoid mutating original data
|
||||||
|
let processedData = data.map(item => ({ ...item }))
|
||||||
|
|
||||||
// Check if limit.value is a valid date
|
// Check if limit.value is a valid date
|
||||||
const isValidDate = date => !isNaN(Date.parse(date))
|
const isValidDate = date => !isNaN(Date.parse(date))
|
||||||
|
|
||||||
// order by date
|
// order by date
|
||||||
const orderByDate = data => data.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
|
processedData.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
|
||||||
|
|
||||||
// Apply number limit or time limit conditionally
|
// Apply number limit or time limit conditionally
|
||||||
const limitData = data => {
|
|
||||||
if (limit && limit.type === 'num' && limit.value > 0) {
|
if (limit && limit.type === 'num' && limit.value > 0) {
|
||||||
return data.slice(0, limit.value)
|
processedData = processedData.slice(0, limit.value)
|
||||||
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
||||||
const limitDate = Date.parse(limit.value)
|
const limitDate = Date.parse(limit.value)
|
||||||
return data.filter(item => Date.parse(item.date) >= limitDate)
|
processedData = processedData.filter(item => Date.parse(item.date) >= limitDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
orderByDate(data)
|
|
||||||
finalResult = limitData(data)
|
|
||||||
|
|
||||||
// This is a hack method, because hexo treats time as UTC time
|
// This is a hack method, because hexo treats time as UTC time
|
||||||
// so you need to manually convert the time zone
|
// so you need to manually convert the time zone
|
||||||
finalResult.forEach(item => {
|
const timezone = hexo.config.timezone
|
||||||
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
|
processedData.forEach(item => {
|
||||||
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
|
const parsed = moment.utc(item.date)
|
||||||
|
item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss')
|
||||||
// markdown
|
// markdown
|
||||||
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||||
})
|
})
|
||||||
|
|
||||||
return finalResult
|
return processedData
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||||
@@ -146,7 +171,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
|||||||
if (category) return 'category'
|
if (category) return 'category'
|
||||||
if (archive) return 'archive'
|
if (archive) return 'archive'
|
||||||
if (type) {
|
if (type) {
|
||||||
if (type === 'tags' || type === 'categories') return type
|
if (type === 'tags' || type === 'categories' || type === '404') return type
|
||||||
else return 'page'
|
else return 'page'
|
||||||
}
|
}
|
||||||
if (isHome) return 'home'
|
if (isHome) return 'home'
|
||||||
@@ -154,6 +179,14 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getVersion', () => {
|
hexo.extend.helper.register('getVersion', () => {
|
||||||
const { version } = require('../../package.json')
|
return { hexo: hexo.version, theme: themeVersion }
|
||||||
return { hexo: hexo.version, theme: version }
|
})
|
||||||
|
|
||||||
|
hexo.extend.helper.register('safeJSON', data => {
|
||||||
|
// Safely serialize JSON for embedding in <script> tags
|
||||||
|
return JSON.stringify(data)
|
||||||
|
.replace(/</g, '\\u003c')
|
||||||
|
.replace(/>/g, '\\u003e')
|
||||||
|
.replace(/\u2028/g, '\\u2028')
|
||||||
|
.replace(/\u2029/g, '\\u2029')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,14 +9,21 @@
|
|||||||
|
|
||||||
const { postDesc } = require('../common/postDesc')
|
const { postDesc } = require('../common/postDesc')
|
||||||
|
|
||||||
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||||
let relatedPosts = []
|
const relatedPosts = new Map()
|
||||||
const tagsData = currentPost.tags
|
const tagsData = currentPost.tags
|
||||||
tagsData.length && tagsData.forEach(function (tag) {
|
|
||||||
allPosts.forEach(function (post) {
|
if (!tagsData || !tagsData.length) return ''
|
||||||
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
|
|
||||||
const getPostDesc = post.postDesc || postDesc(post, hexo)
|
tagsData.forEach(tag => {
|
||||||
const relatedPost = {
|
const posts = tag.posts
|
||||||
|
posts.forEach(post => {
|
||||||
|
if (currentPost.path === post.path) return
|
||||||
|
|
||||||
|
if (relatedPosts.has(post.path)) {
|
||||||
|
relatedPosts.get(post.path).weight += 1
|
||||||
|
} else {
|
||||||
|
relatedPosts.set(post.path, {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
path: post.path,
|
path: post.path,
|
||||||
cover: post.cover,
|
cover: post.cover,
|
||||||
@@ -24,22 +31,17 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
updated: post.updated,
|
updated: post.updated,
|
||||||
created: post.date,
|
created: post.date,
|
||||||
postDesc: getPostDesc
|
post,
|
||||||
}
|
random: Math.random()
|
||||||
const index = findItem(relatedPosts, 'path', post.path)
|
})
|
||||||
if (index !== -1) {
|
|
||||||
relatedPosts[index].weight += 1
|
|
||||||
} else {
|
|
||||||
relatedPosts.push(relatedPost)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (relatedPosts.length === 0) {
|
if (relatedPosts.size === 0) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
let result = ''
|
|
||||||
const hexoConfig = hexo.config
|
const hexoConfig = hexo.config
|
||||||
const config = hexo.theme.config
|
const config = hexo.theme.config
|
||||||
|
|
||||||
@@ -47,19 +49,26 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
|||||||
const dateType = config.related_post.date_type || 'created'
|
const dateType = config.related_post.date_type || 'created'
|
||||||
const headlineLang = this._p('post.recommend')
|
const headlineLang = this._p('post.recommend')
|
||||||
|
|
||||||
relatedPosts = relatedPosts.sort(compare('weight'))
|
const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => {
|
||||||
|
if (b.weight !== a.weight) {
|
||||||
|
return b.weight - a.weight
|
||||||
|
}
|
||||||
|
return b.random - a.random
|
||||||
|
})
|
||||||
|
|
||||||
if (relatedPosts.length > 0) {
|
let result = '<div class="relatedPosts">'
|
||||||
result += '<div class="relatedPosts">'
|
|
||||||
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
||||||
result += '<div class="relatedPosts-list">'
|
result += '<div class="relatedPosts-list">'
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
|
const max = Math.min(relatedPostsList.length, limitNum)
|
||||||
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i]
|
for (let i = 0; i < max; i++) {
|
||||||
|
const item = relatedPostsList[i]
|
||||||
|
let { cover, title, path, cover_type, created, updated, post } = item
|
||||||
const { escape_html, url_for, date } = this
|
const { escape_html, url_for, date } = this
|
||||||
cover = cover || 'var(--default-bg-color)'
|
cover = cover || 'var(--default-bg-color)'
|
||||||
title = escape_html(title)
|
title = escape_html(title)
|
||||||
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
|
const desc = post.postDesc || postDesc(post, hexo)
|
||||||
|
const className = desc ? 'pagination-related' : 'pagination-related no-desc'
|
||||||
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
||||||
if (cover_type === 'img') {
|
if (cover_type === 'img') {
|
||||||
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
||||||
@@ -73,25 +82,12 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
|||||||
}
|
}
|
||||||
result += `<div class="info-item-2">${title}</div></div>`
|
result += `<div class="info-item-2">${title}</div></div>`
|
||||||
|
|
||||||
if (postDesc) {
|
if (desc) {
|
||||||
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
|
result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
|
||||||
}
|
}
|
||||||
result += '</div></a>'
|
result += '</div></a>'
|
||||||
}
|
}
|
||||||
|
|
||||||
result += '</div></div>'
|
result += '</div></div>'
|
||||||
return result
|
return result
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function isTagRelated (tagName, tags) {
|
|
||||||
return tags.some(tag => tag.name === tagName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findItem (arrayToSearch, attr, val) {
|
|
||||||
return arrayToSearch.findIndex(item => item[attr] === val)
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare (attr) {
|
|
||||||
return (a, b) => b[attr] - a[attr]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,15 +7,16 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
const { url_for, escapeHTML } = require('hexo-util')
|
||||||
|
const urlFor = url_for.bind(hexo)
|
||||||
|
|
||||||
const btn = args => {
|
const btn = args => {
|
||||||
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
||||||
|
|
||||||
const iconHTML = icon ? `<i class="${icon}"></i>` : ''
|
const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
|
||||||
const textHTML = text ? `<span>${text}</span>` : ''
|
const textHTML = text ? `<span>${escapeHTML(text)}</span>` : ''
|
||||||
|
|
||||||
return `<a class="btn-beautify ${option}" href="${urlFor(url)}" title="${text}">${iconHTML}${textHTML}</a>`
|
return `<a class="btn-beautify ${escapeHTML(option)}" href="${urlFor(url)}" title="${escapeHTML(text)}">${iconHTML}${textHTML}</a>`
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.tag.register('btn', btn, { ends: false })
|
hexo.extend.tag.register('btn', btn, { ends: false })
|
||||||
|
|||||||
@@ -4,24 +4,25 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
const { url_for, escapeHTML } = require('hexo-util')
|
||||||
|
const urlFor = url_for.bind(hexo)
|
||||||
|
|
||||||
const flinkFn = (args, content) => {
|
const flinkFn = (args, content) => {
|
||||||
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
||||||
let result = ''
|
let result = ''
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : ''
|
const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
|
||||||
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : ''
|
const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
|
||||||
|
|
||||||
const listResult = item.link_list.map(link => `
|
const listResult = item.link_list.map(link => `
|
||||||
<div class="flink-list-item">
|
<div class="flink-list-item">
|
||||||
<a href="${link.link}" title="${link.name}" target="_blank">
|
<a href="${escapeHTML(link.link)}" title="${escapeHTML(link.name)}" target="_blank">
|
||||||
<div class="flink-item-icon">
|
<div class="flink-item-icon">
|
||||||
<img class="no-lightbox" src="${link.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${link.name}" />
|
<img class="no-lightbox" src="${escapeHTML(link.avatar)}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${escapeHTML(link.name)}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flink-item-name">${link.name}</div>
|
<div class="flink-item-name">${escapeHTML(link.name)}</div>
|
||||||
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div>
|
<div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
|
|||||||
const parseImageContent = content => {
|
const parseImageContent = content => {
|
||||||
const images = []
|
const images = []
|
||||||
let match
|
let match
|
||||||
|
IMAGE_REGEX.lastIndex = 0
|
||||||
|
|
||||||
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
||||||
images.push({
|
images.push({
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const parseArgs = args => args.join(' ').split(',')
|
const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
|
||||||
|
|
||||||
const generateStyle = (bg, color) => {
|
const generateStyle = (bg, color) => {
|
||||||
let style = 'style="'
|
let style = 'style="'
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
const { escapeHTML } = require('hexo-util')
|
const { escapeHTML } = require('hexo-util')
|
||||||
|
|
||||||
const mermaid = (args, content) => {
|
const mermaid = (args, content) => {
|
||||||
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
|
const config = args[0] || '{}'
|
||||||
|
return `<div class="mermaid-wrap"><pre class="mermaid-src" data-config="${escapeHTML(config)}" hidden>
|
||||||
${escapeHTML(content)}
|
${escapeHTML(content)}
|
||||||
</pre></div>`
|
</pre></div>`
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-13
@@ -5,20 +5,10 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const { escapeHTML } = require('hexo-util')
|
||||||
|
|
||||||
const score = (args, content) => {
|
const score = (args, content) => {
|
||||||
// Escape HTML tags and some special characters, including curly braces
|
const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '{' : '}')
|
||||||
const escapeHtmlTags = s => {
|
|
||||||
const lookup = {
|
|
||||||
'&': '&',
|
|
||||||
'"': '"',
|
|
||||||
"'": ''',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'{': '{',
|
|
||||||
'}': '}'
|
|
||||||
}
|
|
||||||
return s.replace(/[&"'<>{}]/g, c => lookup[c])
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = content.trim()
|
const trimmed = content.trim()
|
||||||
// Split content using six dashes as a delimiter
|
// Split content using six dashes as a delimiter
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||||
const groups = {}
|
const groups = {}
|
||||||
|
|
||||||
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
|
Object.keys(groups).forEach(k => delete groups[k])
|
||||||
|
})
|
||||||
|
|
||||||
hexo.extend.filter.register('before_post_render', data => {
|
hexo.extend.filter.register('before_post_render', data => {
|
||||||
if (!hexo.theme.config.series.enable) return data
|
if (!hexo.theme.config.series.enable) return data
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ if hexo-config('enter_transitions')
|
|||||||
animation: titleScale 1s
|
animation: titleScale 1s
|
||||||
|
|
||||||
canvas:not(#ribbon-canvas),
|
canvas:not(#ribbon-canvas),
|
||||||
#web_bg
|
#web_bg.bg-animation
|
||||||
animation: to_show 4s
|
animation: to_show 4s
|
||||||
|
|
||||||
#ribbon-canvas
|
#ribbon-canvas
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo
|
|||||||
--hlscrollbar-bg: lighten(#121212, 5)
|
--hlscrollbar-bg: lighten(#121212, 5)
|
||||||
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
||||||
|
|
||||||
|
$scrollbar-style
|
||||||
|
// scrollbar - firefox
|
||||||
|
@-moz-document url-prefix()
|
||||||
|
scrollbar-color: var(--hlscrollbar-bg) transparent
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background: var(--hlscrollbar-bg)
|
||||||
|
|
||||||
if $highlight_enable
|
if $highlight_enable
|
||||||
@require 'highlight/index'
|
@require 'highlight/index'
|
||||||
|
|
||||||
@@ -88,6 +96,11 @@ $code-block
|
|||||||
&:hover
|
&:hover
|
||||||
border-bottom-color: var(--hl-color)
|
border-bottom-color: var(--hl-color)
|
||||||
|
|
||||||
|
&.default
|
||||||
|
pre
|
||||||
|
padding: 10px 20px
|
||||||
|
@extend $scrollbar-style
|
||||||
|
|
||||||
&.copy-true
|
&.copy-true
|
||||||
user-select: all
|
user-select: all
|
||||||
-webkit-user-select: all
|
-webkit-user-select: all
|
||||||
@@ -100,18 +113,23 @@ $code-block
|
|||||||
.highlight-tools
|
.highlight-tools
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
overflow: hidden
|
||||||
padding: 0 8px
|
padding: 0 8px
|
||||||
min-height: 24px
|
min-height: 24px
|
||||||
height: 2.15em
|
height: 2.15em
|
||||||
background: var(--hltools-bg)
|
background: var(--hltools-bg)
|
||||||
color: var(--hltools-color)
|
color: var(--hltools-color)
|
||||||
font-size: $code-font-size
|
font-size: $code-font-size
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
& > *
|
& > *
|
||||||
padding: 5px
|
flex: 0 0 auto
|
||||||
|
margin: 2px
|
||||||
|
|
||||||
i
|
i
|
||||||
|
display: inline-flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 5px
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
transition: all .3s
|
transition: all .3s
|
||||||
|
|
||||||
@@ -127,31 +145,28 @@ $code-block
|
|||||||
|
|
||||||
if !$highlight_macstyle
|
if !$highlight_macstyle
|
||||||
& > .macStyle
|
& > .macStyle
|
||||||
padding: 0
|
margin: 0
|
||||||
|
|
||||||
.code-lang
|
.code-lang
|
||||||
flex: 1
|
flex: 1 1 auto
|
||||||
|
overflow: hidden
|
||||||
|
padding-right: 10px
|
||||||
text-transform: uppercase
|
text-transform: uppercase
|
||||||
|
text-overflow: ellipsis
|
||||||
|
white-space: nowrap
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
font-size: 1.15em
|
font-size: 1.15em
|
||||||
user-select: none
|
user-select: none
|
||||||
-webkit-user-select: none
|
-webkit-user-select: none
|
||||||
padding 2px
|
|
||||||
|
|
||||||
.copy-notice
|
|
||||||
padding-right: 2px
|
|
||||||
opacity: 0
|
|
||||||
transition: opacity .4s
|
|
||||||
|
|
||||||
if hexo-config('code_blocks.language')
|
if hexo-config('code_blocks.language')
|
||||||
.code-lang
|
margin-right: auto
|
||||||
flex: 1
|
else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
|
||||||
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none')
|
& > :nth-child(2)
|
||||||
& > div:nth-child(2)
|
margin-right: auto
|
||||||
flex: 1
|
|
||||||
else
|
else
|
||||||
.macStyle
|
& > :nth-child(1)
|
||||||
flex: 1
|
margin-right: auto
|
||||||
|
|
||||||
.gutter
|
.gutter
|
||||||
user-select: none
|
user-select: none
|
||||||
@@ -163,17 +178,28 @@ $code-block
|
|||||||
td
|
td
|
||||||
border: none
|
border: none
|
||||||
|
|
||||||
|
.copy-notice
|
||||||
|
position: absolute
|
||||||
|
z-index: 99999
|
||||||
|
padding: 2px 6px
|
||||||
|
border-radius: 3px
|
||||||
|
background: var(--hltools-bg)
|
||||||
|
white-space: nowrap
|
||||||
|
font-size: 12px
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
if $highlight_macstyle
|
if $highlight_macstyle
|
||||||
.container
|
.container
|
||||||
figure.highlight
|
figure.highlight
|
||||||
margin: 0 0 24px
|
margin: 0 0 24px
|
||||||
border-radius: 7px
|
border-radius: 8px
|
||||||
box-shadow: 0 5px 10px 0 $highlight-mac-border
|
box-shadow: 0 5px 10px 0 $highlight-mac-border
|
||||||
-webkit-transform: translateZ(0)
|
-webkit-transform: translateZ(0)
|
||||||
|
|
||||||
.highlight-tools
|
.highlight-tools
|
||||||
.macStyle
|
.macStyle
|
||||||
display: flex
|
display: flex
|
||||||
|
padding: 3px
|
||||||
|
|
||||||
& > *
|
& > *
|
||||||
margin-right: 8px
|
margin-right: 8px
|
||||||
@@ -264,8 +290,8 @@ if hexo-config('code_blocks.fullpage')
|
|||||||
& ~ table
|
& ~ table
|
||||||
display: block
|
display: block
|
||||||
overflow: auto
|
overflow: auto
|
||||||
height: calc(100vh - 2.15em)
|
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
height: calc(100vh - 2.15em)
|
||||||
|
|
||||||
@keyframes code-fullpage
|
@keyframes code-fullpage
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
figure.highlight
|
figure.highlight
|
||||||
table
|
table
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
pre .deletion
|
pre .deletion
|
||||||
color: $highlight-deletion
|
color: $highlight-deletion
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ if $highlight_theme != false
|
|||||||
|
|
||||||
.container
|
.container
|
||||||
pre[class*='language-']
|
pre[class*='language-']
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
&:not(.line-numbers)
|
&:not(.line-numbers)
|
||||||
padding: 10px 20px
|
padding: 10px 20px
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
font-size: 1.85em
|
font-size: 1.85em
|
||||||
|
@extend .limit-more-line
|
||||||
|
-webkit-line-clamp: 3
|
||||||
|
|
||||||
+minWidth768()
|
+minWidth768()
|
||||||
font-size: 2.85em
|
font-size: 2.85em
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
.pagination
|
.pagination
|
||||||
& > *
|
& > *
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0 6px
|
margin: 6px
|
||||||
width: w = 2.5em
|
width: w = 2.5em
|
||||||
height: w
|
height: w
|
||||||
line-height: w
|
line-height: w
|
||||||
|
|||||||
@@ -37,13 +37,13 @@
|
|||||||
display: none
|
display: none
|
||||||
padding: 0 0 15px
|
padding: 0 0 15px
|
||||||
width: 100%
|
width: 100%
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.reward-all
|
.reward-all
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 20px 10px
|
padding: 20px 10px
|
||||||
background: var(--reward-pop)
|
background: var(--reward-pop)
|
||||||
|
addBorderRadius()
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|||||||
@@ -65,12 +65,55 @@ if hexo-config('waline.bg')
|
|||||||
|
|
||||||
if hexo-config('mermaid.enable')
|
if hexo-config('mermaid.enable')
|
||||||
.mermaid-wrap
|
.mermaid-wrap
|
||||||
|
position: relative
|
||||||
margin: 0 0 20px
|
margin: 0 0 20px
|
||||||
|
background: var(--card-bg)
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
if hexo-config('mermaid.open_in_new_tab')
|
||||||
|
.mermaid-open-btn
|
||||||
|
position: absolute
|
||||||
|
top: 8px
|
||||||
|
right: 8px
|
||||||
|
z-index: 2
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 0
|
||||||
|
width: 34px
|
||||||
|
height: 25px
|
||||||
|
border: none
|
||||||
|
border-radius: 20%
|
||||||
|
background: #D3D3D3
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, .15)
|
||||||
|
color: #fff
|
||||||
|
font-size: 0
|
||||||
|
line-height: 1
|
||||||
|
cursor: pointer
|
||||||
|
transition: background .2s ease, transform .2s ease
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: 16px
|
||||||
|
line-height: 1
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible
|
||||||
|
outline: none
|
||||||
|
background: #C0C0C0
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
& > svg
|
& > svg
|
||||||
|
max-width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
|
if hexo-config('mermaid.zoom_pan')
|
||||||
|
cursor: grab
|
||||||
|
user-select: none
|
||||||
|
touch-action: none
|
||||||
|
|
||||||
|
&:active
|
||||||
|
cursor: grabbing
|
||||||
|
|
||||||
if hexo-config('mermaid.code_write')
|
if hexo-config('mermaid.code_write')
|
||||||
pre > code.mermaid
|
pre > code.mermaid
|
||||||
display: none
|
display: none
|
||||||
@@ -183,5 +226,6 @@ if hexo-config('math.use')
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
.fancybox__toolbar__column.is-middle
|
.fancybox__toolbar__column.is-middle,
|
||||||
display: none
|
.f-carousel__toolbar__column.is-middle
|
||||||
|
visibility: hidden
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
|||||||
|
|
||||||
// hide-tags
|
// hide-tags
|
||||||
.hide-button,
|
.hide-button,
|
||||||
|
.toggle-button,
|
||||||
#post-outdate-notice,
|
#post-outdate-notice,
|
||||||
.error-img,
|
.error-img,
|
||||||
.container iframe,
|
.container iframe,
|
||||||
@@ -140,9 +141,9 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
|||||||
|
|
||||||
img:not(.cover)
|
img:not(.cover)
|
||||||
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
|
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
|
||||||
filter: blur(0) brightness(.8)
|
filter: blur(0) brightness(.88) contrast(.95)
|
||||||
else
|
else
|
||||||
filter: brightness(.8)
|
filter: brightness(.88) contrast(.95)
|
||||||
|
|
||||||
#aside-content .aside-list > .aside-list-item:not(:last-child)
|
#aside-content .aside-list > .aside-list-item:not(:last-child)
|
||||||
border-bottom: 1px dashed alpha(#FFFFFF, .1)
|
border-bottom: 1px dashed alpha(#FFFFFF, .1)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ if hexo-config('readmode')
|
|||||||
font-size: 16px
|
font-size: 16px
|
||||||
transition: background .3s
|
transition: background .3s
|
||||||
addBorderRadius(8)
|
addBorderRadius(8)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
top: initial
|
top: initial
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
justify-content: center
|
justify-content: center
|
||||||
align-items: center
|
align-items: center
|
||||||
margin-right: 6px
|
margin-right: 6px
|
||||||
|
margin-top: 3px
|
||||||
min-width: 24px
|
min-width: 24px
|
||||||
color: $search-color
|
color: $search-color
|
||||||
content: attr(value) '.'
|
content: attr(value) '.'
|
||||||
|
|||||||
@@ -39,11 +39,26 @@
|
|||||||
border: 1px solid $tag-hide-toggle-bg
|
border: 1px solid $tag-hide-toggle-bg
|
||||||
addBorderRadius(5, true)
|
addBorderRadius(5, true)
|
||||||
|
|
||||||
|
& > .toggle-content
|
||||||
|
margin: 30px 24px
|
||||||
|
|
||||||
& > .toggle-button
|
& > .toggle-button
|
||||||
padding: 6px 15px
|
padding: 6px 15px
|
||||||
background: $tag-hide-toggle-bg
|
background: $tag-hide-toggle-bg
|
||||||
color: #1F2D3D
|
color: #1F2D3D
|
||||||
|
list-style: none
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
& > .toggle-content
|
&::-webkit-details-marker
|
||||||
margin: 30px 24px
|
display: none
|
||||||
|
|
||||||
|
&::before
|
||||||
|
@extend .fontawesomeIcon
|
||||||
|
margin-right: 8px
|
||||||
|
content: '\f0d7'
|
||||||
|
transition: transform .3s ease
|
||||||
|
transform: rotate(-90deg)
|
||||||
|
transform-origin: center center
|
||||||
|
|
||||||
|
&[open] summary::before
|
||||||
|
transform: rotate(0)
|
||||||
+237
-152
@@ -2,18 +2,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let headerContentWidth, $nav
|
let headerContentWidth, $nav
|
||||||
let mobileSidebarOpen = false
|
let mobileSidebarOpen = false
|
||||||
|
|
||||||
|
// rightsideScrollPercent
|
||||||
|
let goUpElement = null
|
||||||
|
let scrollPercentElement = null
|
||||||
|
|
||||||
const adjustMenu = init => {
|
const adjustMenu = init => {
|
||||||
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
let hideMenuIndex = false
|
||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
const blogInfoWidth = Array.from(document.querySelector('#blog-info > a').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||||
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
const menusWidth = Array.from(document.getElementById('menus').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||||
headerContentWidth = blogInfoWidth + menusWidth
|
headerContentWidth = blogInfoWidth + menusWidth
|
||||||
$nav = document.getElementById('nav')
|
$nav = document.getElementById('nav')
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化header
|
// 初始化header
|
||||||
@@ -54,21 +61,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
* 代碼
|
* 代碼
|
||||||
* 只適用於Hexo默認的代碼渲染
|
* 只適用於Hexo默認的代碼渲染
|
||||||
*/
|
*/
|
||||||
const addHighlightTool = () => {
|
const addHighlightTool = $article => {
|
||||||
const highLight = GLOBAL_CONFIG.highlight
|
const highLight = GLOBAL_CONFIG.highlight
|
||||||
if (!highLight) return
|
if (!highLight) return
|
||||||
|
|
||||||
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||||
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||||
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
||||||
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
|
const isNotHighlightJs = plugin !== 'highlight.js'
|
||||||
|
const isPrismjs = plugin === 'prismjs'
|
||||||
|
const $figureHighlight = isNotHighlightJs
|
||||||
|
? Array.from($article.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
|
||||||
|
: $article.querySelectorAll('figure.highlight')
|
||||||
|
|
||||||
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||||
|
|
||||||
const isPrismjs = plugin === 'prismjs'
|
|
||||||
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||||
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
||||||
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
|
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
|
||||||
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
|
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
|
||||||
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
|
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
|
||||||
|
|
||||||
@@ -76,9 +86,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||||||
btf.snackbarShow(text)
|
btf.snackbarShow(text)
|
||||||
} else {
|
} else {
|
||||||
ele.textContent = text
|
const newEle = document.createElement('div')
|
||||||
ele.style.opacity = 1
|
newEle.className = 'copy-notice'
|
||||||
setTimeout(() => { ele.style.opacity = 0 }, 800)
|
newEle.textContent = text
|
||||||
|
document.body.appendChild(newEle)
|
||||||
|
|
||||||
|
const buttonRect = ele.getBoundingClientRect()
|
||||||
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
|
||||||
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
|
||||||
|
|
||||||
|
// X-axis boundary check
|
||||||
|
const halfWidth = newEle.offsetWidth / 2
|
||||||
|
const centerLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
|
||||||
|
const finalLeft = Math.max(halfWidth + 10, Math.min(window.innerWidth - halfWidth - 10, centerLeft))
|
||||||
|
|
||||||
|
// Show tooltip below button if too close to top
|
||||||
|
const normalTop = buttonRect.top + scrollTop - 40
|
||||||
|
const shouldShowBelow = buttonRect.top < 60 || normalTop < 10
|
||||||
|
|
||||||
|
const topValue = shouldShowBelow ? buttonRect.top + scrollTop + buttonRect.height + 10 : normalTop
|
||||||
|
|
||||||
|
newEle.style.cssText = `
|
||||||
|
top: ${topValue + 10}px;
|
||||||
|
left: ${finalLeft}px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease, top 0.3s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
newEle.style.opacity = '1'
|
||||||
|
newEle.style.top = `${topValue}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
newEle.style.opacity = '0'
|
||||||
|
newEle.style.top = `${topValue + 10}px`
|
||||||
|
setTimeout(() => {
|
||||||
|
newEle?.remove()
|
||||||
|
}, 300)
|
||||||
|
}, 800)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +143,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const highlightCopyFn = (ele, clickEle) => {
|
const highlightCopyFn = (ele, clickEle) => {
|
||||||
const $buttonParent = ele.parentNode
|
const $buttonParent = ele.parentNode
|
||||||
$buttonParent.classList.add('copy-true')
|
$buttonParent.classList.add('copy-true')
|
||||||
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
|
const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
|
||||||
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||||
if (!codeElement) return
|
if (!codeElement) return
|
||||||
copy(codeElement.innerText, clickEle.previousElementSibling)
|
copy(codeElement.innerText, clickEle)
|
||||||
$buttonParent.classList.remove('copy-true')
|
$buttonParent.classList.remove('copy-true')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,33 +173,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 獲取隱藏狀態下元素的真實高度
|
// 獲取隱藏狀態下元素的真實高度
|
||||||
const getActualHeight = item => {
|
const getActualHeight = item => {
|
||||||
const hiddenElements = new Map()
|
if (item.offsetHeight > 0) return item.offsetHeight
|
||||||
|
|
||||||
const fix = () => {
|
const clone = item.cloneNode(true)
|
||||||
let current = item
|
|
||||||
while (current !== document.body && current != null) {
|
|
||||||
if (window.getComputedStyle(current).display === 'none') {
|
|
||||||
hiddenElements.set(current, current.getAttribute('style') || '')
|
|
||||||
}
|
|
||||||
current = current.parentNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = 'visibility: hidden !important; display: block !important;'
|
clone.style.cssText = `
|
||||||
hiddenElements.forEach((originalStyle, elem) => {
|
position: absolute !important;
|
||||||
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
visibility: hidden !important;
|
||||||
})
|
display: block !important;
|
||||||
}
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
`
|
||||||
|
|
||||||
const restore = () => {
|
item.parentNode.insertBefore(clone, item)
|
||||||
hiddenElements.forEach((originalStyle, elem) => {
|
const height = clone.offsetHeight
|
||||||
if (originalStyle === '') elem.removeAttribute('style')
|
clone.remove()
|
||||||
else elem.setAttribute('style', originalStyle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fix()
|
|
||||||
const height = item.offsetHeight
|
|
||||||
restore()
|
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,20 +213,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fragment.appendChild(ele)
|
fragment.appendChild(ele)
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
$figureHighlight.forEach(item => {
|
$figureHighlight.forEach(item => {
|
||||||
let langName = ''
|
let langName = ''
|
||||||
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
|
if (isNotHighlightJs) {
|
||||||
|
const newClassName = isPrismjs ? 'prismjs' : 'default'
|
||||||
|
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
|
||||||
|
}
|
||||||
|
|
||||||
if (!highlightLang) {
|
if (!highlightLang) {
|
||||||
createEle('', item)
|
createEle('', item)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPrismjs) {
|
if (isNotHighlightJs) {
|
||||||
langName = item.getAttribute('data-language') || 'Code'
|
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
|
||||||
} else {
|
} else {
|
||||||
langName = item.getAttribute('class').split(' ')[1]
|
langName = item.getAttribute('class').split(' ')[1]
|
||||||
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||||
@@ -200,9 +241,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* PhotoFigcaption
|
* PhotoFigcaption
|
||||||
*/
|
*/
|
||||||
const addPhotoFigcaption = () => {
|
const addPhotoFigcaption = $article => {
|
||||||
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||||
document.querySelectorAll('#article-container img').forEach(item => {
|
$article.querySelectorAll('img').forEach(item => {
|
||||||
const altValue = item.title || item.alt
|
const altValue = item.title || item.alt
|
||||||
if (!altValue) return
|
if (!altValue) return
|
||||||
const ele = document.createElement('div')
|
const ele = document.createElement('div')
|
||||||
@@ -215,8 +256,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Lightbox
|
* Lightbox
|
||||||
*/
|
*/
|
||||||
const runLightbox = () => {
|
const runLightbox = $article => {
|
||||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
btf.loadLightbox($article.querySelectorAll('img:not(.no-lightbox)'))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const fetchUrl = async url => {
|
const fetchUrl = async url => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch URL:', error)
|
console.error('Failed to fetch URL:', error)
|
||||||
return []
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runJustifiedGallery = (container, data, config) => {
|
const runJustifiedGallery = (container, data, config) => {
|
||||||
const { isButton, limit, firstLimit, tabs } = config
|
const { isButton, tabs } = config
|
||||||
|
const limit = Math.max(1, Number(config.limit) || 20)
|
||||||
|
const firstLimit = Math.max(1, Number(config.firstLimit) || limit)
|
||||||
|
|
||||||
const dataLength = data.length
|
const dataLength = data.length
|
||||||
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
|
const maxGroupKey = dataLength
|
||||||
|
? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1
|
||||||
|
: 0
|
||||||
|
|
||||||
// Gallery configuration
|
// Gallery configuration
|
||||||
const igConfig = {
|
const igConfig = {
|
||||||
@@ -254,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let isLayoutHidden = false
|
let isLayoutHidden = false
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const sanitizeString = str => (str && str.replace(/"/g, '"')) || ''
|
const sanitizeString = str => String(str ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
const createImageItem = item => {
|
const createImageItem = item => {
|
||||||
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
||||||
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
||||||
|
const url = item.url ? sanitizeString(item.url) : ''
|
||||||
return `<div class="item">
|
return `<div class="item">
|
||||||
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
|
<img src="${url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
btf.setLoading.add(container)
|
btf.setLoading.add(container)
|
||||||
ig.on('renderComplete', handleRenderComplete)
|
ig.on('renderComplete', handleRenderComplete)
|
||||||
|
|
||||||
if (isButton) {
|
if (isButton && dataLength) {
|
||||||
appendItems(1, firstLimit, true)
|
appendItems(1, firstLimit, true)
|
||||||
} else {
|
} else if (dataLength) {
|
||||||
ig.on('requestAppend', handleRequestAppend)
|
ig.on('requestAppend', handleRequestAppend)
|
||||||
ig.renderItems()
|
ig.renderItems()
|
||||||
|
} else {
|
||||||
|
btf.setLoading.remove(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
||||||
@@ -356,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const container = element.firstElementChild
|
const container = element.firstElementChild
|
||||||
const content = container.textContent
|
const content = container.textContent
|
||||||
container.textContent = ''
|
container.textContent = ''
|
||||||
element.classList.add('loaded')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
||||||
|
if (!Array.isArray(data)) throw new TypeError('Gallery data must be an array')
|
||||||
runJustifiedGallery(container, data, config)
|
runJustifiedGallery(container, data, config)
|
||||||
|
element.classList.add('loaded')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gallery data parsing failed:', error)
|
console.error('Gallery data parsing failed:', error)
|
||||||
}
|
}
|
||||||
@@ -380,11 +434,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
*/
|
*/
|
||||||
const rightsideScrollPercent = currentTop => {
|
const rightsideScrollPercent = currentTop => {
|
||||||
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||||
const goUpElement = document.getElementById('go-up')
|
|
||||||
|
|
||||||
|
if (!goUpElement || !scrollPercentElement) return
|
||||||
if (scrollPercent < 95) {
|
if (scrollPercent < 95) {
|
||||||
goUpElement.classList.add('show-percent')
|
goUpElement.classList.add('show-percent')
|
||||||
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
scrollPercentElement.textContent = scrollPercent
|
||||||
} else {
|
} else {
|
||||||
goUpElement.classList.remove('show-percent')
|
goUpElement.classList.remove('show-percent')
|
||||||
}
|
}
|
||||||
@@ -395,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
*/
|
*/
|
||||||
const scrollFn = () => {
|
const scrollFn = () => {
|
||||||
const $rightside = document.getElementById('rightside')
|
const $rightside = document.getElementById('rightside')
|
||||||
const innerHeight = window.innerHeight + 56
|
|
||||||
let initTop = 0
|
let initTop = 0
|
||||||
const $header = document.getElementById('page-header')
|
const $header = document.getElementById('page-header')
|
||||||
const isChatBtn = typeof chatBtn !== 'undefined'
|
const isChatBtn = typeof window.chatBtn !== 'undefined'
|
||||||
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||||
|
|
||||||
// 檢查文檔高度是否小於視窗高度
|
// 檢查文檔高度是否小於視窗高度
|
||||||
const checkDocumentHeight = () => {
|
const checkDocumentHeight = () => {
|
||||||
if (document.body.scrollHeight <= innerHeight) {
|
if (document.body.scrollHeight <= window.innerHeight + 56) {
|
||||||
$rightside.classList.add('rightside-show')
|
$rightside.classList.add('rightside-show')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果文檔高度小於視窗高度,直接返回
|
|
||||||
if (checkDocumentHeight()) return
|
|
||||||
|
|
||||||
// find the scroll direction
|
// find the scroll direction
|
||||||
const scrollDirection = currentTop => {
|
const scrollDirection = currentTop => {
|
||||||
const result = currentTop > initTop // true is down & false is up
|
const result = currentTop > initTop // true is down & false is up
|
||||||
@@ -421,7 +471,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let flag = ''
|
let flag = ''
|
||||||
const scrollTask = btf.throttle(() => {
|
const scrollTask = btf.rafThrottle(() => {
|
||||||
|
if (checkDocumentHeight()) return
|
||||||
|
|
||||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
const isDown = scrollDirection(currentTop)
|
const isDown = scrollDirection(currentTop)
|
||||||
if (currentTop > 56) {
|
if (currentTop > 56) {
|
||||||
@@ -452,19 +504,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isShowPercent && rightsideScrollPercent(currentTop)
|
isShowPercent && rightsideScrollPercent(currentTop)
|
||||||
checkDocumentHeight()
|
})
|
||||||
}, 300)
|
|
||||||
|
|
||||||
|
checkDocumentHeight()
|
||||||
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* toc,anchor
|
* toc, anchor
|
||||||
*/
|
*/
|
||||||
const scrollFnToDo = () => {
|
const scrollFnToDo = $article => {
|
||||||
const isToc = GLOBAL_CONFIG_SITE.isToc
|
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||||
const isAnchor = GLOBAL_CONFIG.isAnchor
|
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||||
const $article = document.getElementById('article-container')
|
|
||||||
|
|
||||||
if (!($article && (isToc || isAnchor))) return
|
if (!($article && (isToc || isAnchor))) return
|
||||||
|
|
||||||
@@ -477,7 +528,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||||
isExpand = $cardToc.classList.contains('is-expand')
|
isExpand = $cardToc.classList.contains('is-expand')
|
||||||
|
|
||||||
// toc元素點擊
|
|
||||||
const tocItemClickFn = e => {
|
const tocItemClickFn = e => {
|
||||||
const target = e.target.closest('.toc-link')
|
const target = e.target.closest('.toc-link')
|
||||||
if (!target) return
|
if (!target) return
|
||||||
@@ -504,82 +554,105 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 處理 hexo-blog-encrypt 事件
|
|
||||||
$cardToc.style.display = 'block'
|
$cardToc.style.display = 'block'
|
||||||
}
|
}
|
||||||
|
|
||||||
// find head position & add active class
|
|
||||||
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||||
let detectItem = ''
|
if (!$articleList.length) return
|
||||||
|
|
||||||
const findHeadPosition = top => {
|
let activeTocItem = null
|
||||||
if (top === 0) return false
|
let activeParentItems = []
|
||||||
|
|
||||||
let currentId = ''
|
const updateTocUI = currentId => {
|
||||||
let currentIndex = ''
|
const encodedAnchor = currentId ? '#' + encodeURI(decodeURI(currentId)) : ''
|
||||||
|
if (isAnchor) btf.updateAnchor(encodedAnchor)
|
||||||
|
|
||||||
for (let i = 0; i < $articleList.length; i++) {
|
if (!isToc) return
|
||||||
const ele = $articleList[i]
|
|
||||||
if (top > btf.getEleTop(ele) - 80) {
|
if (activeTocItem) activeTocItem.classList.remove('active')
|
||||||
const id = ele.id
|
activeParentItems.forEach(i => i.classList.remove('active'))
|
||||||
currentId = id ? '#' + encodeURI(id) : ''
|
activeParentItems = []
|
||||||
currentIndex = i
|
|
||||||
} else {
|
if (!currentId) {
|
||||||
break
|
activeTocItem = null
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detectItem === currentIndex) return
|
const targetLink = Array.from($tocLink).find(link => {
|
||||||
|
const href = link.getAttribute('href')
|
||||||
|
if (!href) return false
|
||||||
|
return decodeURI(href).replace('#', '') === decodeURI(currentId)
|
||||||
|
})
|
||||||
|
|
||||||
if (isAnchor) btf.updateAnchor(currentId)
|
if (!targetLink) return
|
||||||
|
|
||||||
detectItem = currentIndex
|
targetLink.classList.add('active')
|
||||||
|
activeTocItem = targetLink
|
||||||
if (isToc) {
|
setTimeout(() => autoScrollToc(targetLink), 0)
|
||||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
|
||||||
|
|
||||||
if (currentId) {
|
|
||||||
const currentActive = $tocLink[currentIndex]
|
|
||||||
currentActive.classList.add('active')
|
|
||||||
|
|
||||||
setTimeout(() => autoScrollToc(currentActive), 0)
|
|
||||||
|
|
||||||
if (!isExpand) {
|
if (!isExpand) {
|
||||||
let parent = currentActive.parentNode
|
let parent = targetLink.parentNode
|
||||||
while (!parent.matches('.toc')) {
|
while (!parent.matches('.toc')) {
|
||||||
if (parent.matches('li')) parent.classList.add('active')
|
if (parent.matches('li')) {
|
||||||
|
parent.classList.add('active')
|
||||||
|
activeParentItems.push(parent)
|
||||||
|
}
|
||||||
parent = parent.parentNode
|
parent = parent.parentNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const observerOptions = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '-60px 0px -80% 0px',
|
||||||
|
threshold: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// main of scroll
|
const observer = new IntersectionObserver(entries => {
|
||||||
const tocScrollFn = btf.throttle(() => {
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
updateTocUI(entry.target.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, observerOptions)
|
||||||
|
|
||||||
|
$articleList.forEach(ele => observer.observe(ele))
|
||||||
|
|
||||||
|
const scrollHandler = btf.rafThrottle(() => {
|
||||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
|
|
||||||
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||||
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
||||||
}
|
}
|
||||||
findHeadPosition(currentTop)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
|
if (currentTop === 0) {
|
||||||
|
updateTocUI('')
|
||||||
|
} else if (currentTop + window.innerHeight >= document.documentElement.scrollHeight - 10) {
|
||||||
|
const lastHeader = $articleList[$articleList.length - 1]
|
||||||
|
updateTocUI(lastHeader.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
btf.addEventListenerPjax(window, 'scroll', scrollHandler, { passive: true })
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxSendOnce', () => {
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleThemeChange = mode => {
|
const handleThemeChange = mode => {
|
||||||
const globalFn = window.globalFn || {}
|
const globalFn = window.globalFn || {}
|
||||||
const themeChange = globalFn.themeChange || {}
|
const themeChange = globalFn.themeChange
|
||||||
if (!themeChange) {
|
if (!themeChange) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(themeChange).forEach(key => {
|
Object.keys(themeChange).forEach(key => {
|
||||||
const themeChangeFn = themeChange[key]
|
const fn = themeChange[key]
|
||||||
|
if (typeof fn !== 'function') return
|
||||||
|
|
||||||
if (['disqus', 'disqusjs'].includes(key)) {
|
if (['disqus', 'disqusjs'].includes(key)) {
|
||||||
setTimeout(() => themeChangeFn(mode), 300)
|
setTimeout(() => fn(mode), 300)
|
||||||
} else {
|
} else {
|
||||||
themeChangeFn(mode)
|
fn(mode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -600,7 +673,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
$body.classList.add('read-mode')
|
$body.classList.add('read-mode')
|
||||||
newEle.type = 'button'
|
newEle.type = 'button'
|
||||||
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
newEle.className = 'exit-readmode'
|
||||||
|
newEle.innerHTML = '<i class="fas fa-sign-out-alt"></i>'
|
||||||
newEle.addEventListener('click', exitReadMode)
|
newEle.addEventListener('click', exitReadMode)
|
||||||
$body.appendChild(newEle)
|
$body.appendChild(newEle)
|
||||||
},
|
},
|
||||||
@@ -740,8 +814,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* table overflow
|
* table overflow
|
||||||
*/
|
*/
|
||||||
const addTableWrap = () => {
|
const addTableWrap = $article => {
|
||||||
const $table = document.querySelectorAll('#article-container table')
|
const $table = $article.querySelectorAll('table')
|
||||||
if (!$table.length) return
|
if (!$table.length) return
|
||||||
|
|
||||||
$table.forEach(item => {
|
$table.forEach(item => {
|
||||||
@@ -751,52 +825,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const clickFnOfTagHide = $article => {
|
||||||
* tag-hide
|
const hideButtons = $article.querySelectorAll('.hide-button')
|
||||||
*/
|
|
||||||
const clickFnOfTagHide = () => {
|
|
||||||
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
|
||||||
if (!hideButtons.length) return
|
if (!hideButtons.length) return
|
||||||
hideButtons.forEach(item => item.addEventListener('click', e => {
|
|
||||||
const currentTarget = e.currentTarget
|
const handleClickOfTagHide = e => {
|
||||||
currentTarget.classList.add('open')
|
const button = e.target.closest('.hide-button')
|
||||||
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
if (!button) return
|
||||||
}, { once: true }))
|
button.classList.add('open')
|
||||||
|
addJustifiedGallery(button.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsFn = () => {
|
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
|
||||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
}
|
||||||
if (!navTabsElements.length) return
|
|
||||||
|
const tabsFn = $article => {
|
||||||
|
if (!$article.querySelector('.tabs')) return
|
||||||
|
|
||||||
const setActiveClass = (elements, activeIndex) => {
|
const setActiveClass = (elements, activeIndex) => {
|
||||||
elements.forEach((el, index) => {
|
elements.forEach((el, index) => el.classList.toggle('active', index === activeIndex))
|
||||||
el.classList.toggle('active', index === activeIndex)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNavClick = e => {
|
const handleClick = e => {
|
||||||
|
const tabsRoot = e.target.closest('.tabs')
|
||||||
|
if (!tabsRoot) return
|
||||||
|
|
||||||
|
const navContainer = tabsRoot.firstElementChild
|
||||||
|
const toTopContainer = tabsRoot.lastElementChild
|
||||||
|
|
||||||
|
if (navContainer.contains(e.target)) {
|
||||||
const target = e.target.closest('button')
|
const target = e.target.closest('button')
|
||||||
if (!target || target.classList.contains('active')) return
|
if (!target || target.classList.contains('active')) return
|
||||||
|
|
||||||
const navItems = [...e.currentTarget.children]
|
const navItems = [...navContainer.children]
|
||||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
const tabContents = [...navContainer.nextElementSibling.children]
|
||||||
const indexOfButton = navItems.indexOf(target)
|
const indexOfButton = navItems.indexOf(target)
|
||||||
setActiveClass(navItems, indexOfButton)
|
setActiveClass(navItems, indexOfButton)
|
||||||
e.currentTarget.classList.remove('no-default')
|
navContainer.classList.remove('no-default')
|
||||||
setActiveClass(tabContents, indexOfButton)
|
setActiveClass(tabContents, indexOfButton)
|
||||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToTopClick = tabElement => e => {
|
if (toTopContainer.contains(e.target) && e.target.closest('button')) {
|
||||||
if (e.target.closest('button')) {
|
btf.scrollToDest(btf.getEleTop(tabsRoot), 300)
|
||||||
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navTabsElements.forEach(tabElement => {
|
btf.addEventListenerPjax($article, 'click', handleClick)
|
||||||
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
|
||||||
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCardCategory = () => {
|
const toggleCardCategory = () => {
|
||||||
@@ -862,10 +938,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unRefreshFn = () => {
|
const unRefreshFn = () => {
|
||||||
window.addEventListener('resize', () => {
|
const resizeHandler = btf.rafThrottle(() => {
|
||||||
adjustMenu(false)
|
adjustMenu(false)
|
||||||
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
if (mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu'))) {
|
||||||
|
sidebarFn.close()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
window.addEventListener('resize', resizeHandler, { passive: true })
|
||||||
|
|
||||||
const menuMask = document.getElementById('menu-mask')
|
const menuMask = document.getElementById('menu-mask')
|
||||||
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||||
@@ -883,18 +962,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const forPostFn = () => {
|
const forPostFn = () => {
|
||||||
addHighlightTool()
|
const $article = document.getElementById('article-container')
|
||||||
addPhotoFigcaption()
|
if (!$article || $article.querySelector('.hbe-container')) return
|
||||||
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
|
||||||
runLightbox()
|
addHighlightTool($article)
|
||||||
scrollFnToDo()
|
addPhotoFigcaption($article)
|
||||||
addTableWrap()
|
addJustifiedGallery($article.querySelectorAll('.gallery-container'))
|
||||||
clickFnOfTagHide()
|
runLightbox($article)
|
||||||
tabsFn()
|
scrollFnToDo($article)
|
||||||
|
addTableWrap($article)
|
||||||
|
clickFnOfTagHide($article)
|
||||||
|
tabsFn($article)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshFn = () => {
|
const refreshFn = () => {
|
||||||
initAdjust()
|
initAdjust()
|
||||||
|
goUpElement = document.getElementById('go-up')
|
||||||
|
scrollPercentElement = goUpElement?.querySelector('.scroll-percent')
|
||||||
|
|
||||||
justifiedIndexPostUI()
|
justifiedIndexPostUI()
|
||||||
|
|
||||||
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
||||||
|
|||||||
+173
-264
@@ -6,8 +6,28 @@ window.addEventListener('load', () => {
|
|||||||
return console.error('Algolia setting is invalid!')
|
return console.error('Algolia setting is invalid!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTENT_FIELDS = ['contentStripTruncate', 'contentStrip', 'content']
|
||||||
|
const HIGHLIGHT_PARAMS = {
|
||||||
|
highlightPreTag: '<mark>',
|
||||||
|
highlightPostTag: '</mark>',
|
||||||
|
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compiled regex for tag balancing (reused across cutContent calls)
|
||||||
|
const TAG_REGEX = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*\/?>/g
|
||||||
|
|
||||||
const $searchMask = document.getElementById('search-mask')
|
const $searchMask = document.getElementById('search-mask')
|
||||||
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||||
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const $hits = document.getElementById('algolia-hits')
|
||||||
|
const $hitsEmpty = document.getElementById('algolia-hits-empty')
|
||||||
|
const $hitsList = document.querySelector('#algolia-hits .ais-Hits-list')
|
||||||
|
const $hitsWrapper = document.querySelector('#algolia-hits .ais-Hits')
|
||||||
|
const $pagination = document.getElementById('algolia-pagination')
|
||||||
|
const $paginationList = document.querySelector('#algolia-pagination .ais-Pagination-list')
|
||||||
|
const $stats = document.querySelector('#algolia-info .ais-Stats-text')
|
||||||
|
const $searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
||||||
|
const $searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
||||||
|
|
||||||
const animateElements = show => {
|
const animateElements = show => {
|
||||||
const action = show ? 'animateIn' : 'animateOut'
|
const action = show ? 'animateIn' : 'animateOut'
|
||||||
@@ -23,15 +43,12 @@ window.addEventListener('load', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSearch = () => {
|
// Debounced resize to avoid layout thrashing
|
||||||
btf.overflowPaddingR.add()
|
let resizeTimer
|
||||||
animateElements(true)
|
const onResize = () => {
|
||||||
showLoading(false)
|
clearTimeout(resizeTimer)
|
||||||
|
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||||
setTimeout(() => {
|
}
|
||||||
const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
|
||||||
if (searchInput) searchInput.focus()
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
const handleEscape = event => {
|
const handleEscape = event => {
|
||||||
if (event.code === 'Escape') {
|
if (event.code === 'Escape') {
|
||||||
@@ -40,15 +57,29 @@ window.addEventListener('load', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showLoading = show => {
|
||||||
|
if ($loadingStatus) $loadingStatus.hidden = !show
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
|
animateElements(true)
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if ($searchInput) $searchInput.focus()
|
||||||
|
}, 100)
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
document.addEventListener('keydown', handleEscape)
|
||||||
fixSafariHeight()
|
fixSafariHeight()
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
window.addEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
btf.overflowPaddingR.remove()
|
btf.overflowPaddingR.remove()
|
||||||
animateElements(false)
|
animateElements(false)
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchClickFn = () => {
|
const searchClickFn = () => {
|
||||||
@@ -60,37 +91,40 @@ window.addEventListener('load', () => {
|
|||||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutContent = content => {
|
const extractContentStr = content => {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
|
if (typeof content === 'string') return content.trim()
|
||||||
let contentStr = ''
|
if (typeof content === 'object') {
|
||||||
if (typeof content === 'string') {
|
|
||||||
contentStr = content.trim()
|
|
||||||
} else if (typeof content === 'object') {
|
|
||||||
if (content.value !== undefined) {
|
if (content.value !== undefined) {
|
||||||
contentStr = String(content.value).trim()
|
const str = String(content.value).trim()
|
||||||
if (!contentStr) return ''
|
return str || ''
|
||||||
} else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) {
|
}
|
||||||
return ''
|
if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) return ''
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
contentStr = JSON.stringify(content).trim()
|
const str = JSON.stringify(content).trim()
|
||||||
if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') {
|
return (str === '{}' || str === '[]' || str === '""') ? '' : str
|
||||||
|
} catch (e) { return '' }
|
||||||
|
}
|
||||||
|
if (content.toString && typeof content.toString === 'function') {
|
||||||
|
const str = content.toString().trim()
|
||||||
|
return (str === '[object Object]' || str === '[object Array]') ? '' : str
|
||||||
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
return ''
|
const extractHighlightValue = highlightObj => {
|
||||||
}
|
if (!highlightObj) return ''
|
||||||
}
|
if (typeof highlightObj === 'string') return highlightObj.trim()
|
||||||
} else if (content.toString && typeof content.toString === 'function') {
|
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
||||||
contentStr = content.toString().trim()
|
return String(highlightObj.value).trim()
|
||||||
if (contentStr === '[object Object]' || contentStr === '[object Array]') {
|
}
|
||||||
return ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cutContent = content => {
|
||||||
|
const contentStr = extractContentStr(content)
|
||||||
|
if (!contentStr) return ''
|
||||||
|
|
||||||
const firstOccur = contentStr.indexOf('<mark>')
|
const firstOccur = contentStr.indexOf('<mark>')
|
||||||
let start = firstOccur - 30
|
let start = firstOccur - 30
|
||||||
let end = firstOccur + 120
|
let end = firstOccur + 120
|
||||||
@@ -110,112 +144,67 @@ window.addEventListener('load', () => {
|
|||||||
post = '...'
|
post = '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we don't cut off HTML tags in the middle
|
|
||||||
let substr = contentStr.substring(start, end)
|
let substr = contentStr.substring(start, end)
|
||||||
|
|
||||||
// Handle tag completeness
|
// Remove incomplete tags at boundaries
|
||||||
// Check for incomplete opening tags at the beginning
|
|
||||||
const firstCloseBracket = substr.indexOf('>')
|
const firstCloseBracket = substr.indexOf('>')
|
||||||
const firstOpenBracket = substr.indexOf('<')
|
const firstOpenBracket = substr.indexOf('<')
|
||||||
|
|
||||||
// If there's a closing bracket but no opening bracket before it, we've cut a tag
|
|
||||||
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
||||||
substr = substr.substring(firstCloseBracket + 1)
|
substr = substr.substring(firstCloseBracket + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for incomplete closing tags at the end
|
|
||||||
const lastOpenBracket = substr.lastIndexOf('<')
|
const lastOpenBracket = substr.lastIndexOf('<')
|
||||||
const lastCloseBracket = substr.lastIndexOf('>')
|
const lastCloseBracket = substr.lastIndexOf('>')
|
||||||
|
|
||||||
// If there's an opening bracket after the last closing bracket, we've cut a tag
|
|
||||||
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
||||||
substr = substr.substring(0, lastOpenBracket)
|
substr = substr.substring(0, lastOpenBracket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Balance tags in the substring
|
// Balance tags using regex
|
||||||
const tagStack = []
|
const tagStack = []
|
||||||
let balancedStr = ''
|
let balancedStr = ''
|
||||||
let i = 0
|
let lastIndex = 0
|
||||||
|
let match
|
||||||
|
|
||||||
while (i < substr.length) {
|
TAG_REGEX.lastIndex = 0
|
||||||
if (substr[i] === '<') {
|
while ((match = TAG_REGEX.exec(substr)) !== null) {
|
||||||
// Check if it's a closing tag
|
const fullTag = match[0]
|
||||||
if (substr[i + 1] === '/') {
|
const tagName = match[1]
|
||||||
const closeTagEnd = substr.indexOf('>', i)
|
const tagStart = match.index
|
||||||
if (closeTagEnd !== -1) {
|
|
||||||
const closeTagName = substr.substring(i + 2, closeTagEnd)
|
// Append text before this tag
|
||||||
// Remove matching opening tag from stack
|
balancedStr += substr.substring(lastIndex, tagStart)
|
||||||
for (let j = tagStack.length - 1; j >= 0; j--) {
|
|
||||||
if (tagStack[j] === closeTagName) {
|
if (fullTag.startsWith('</')) {
|
||||||
tagStack.splice(j, 1)
|
// Closing tag - remove matching opening tag from stack
|
||||||
break
|
const idx = tagStack.lastIndexOf(tagName)
|
||||||
}
|
if (idx !== -1) tagStack.splice(idx, 1)
|
||||||
}
|
} else if (!fullTag.endsWith('/>') && !fullTag.startsWith('<!')) {
|
||||||
balancedStr += substr.substring(i, closeTagEnd + 1)
|
// Opening tag (not self-closing or comment)
|
||||||
i = closeTagEnd + 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if (substr.substr(i, 2) === '<!' || (substr.indexOf('/>', i) !== -1 && substr.indexOf('/>', i) < substr.indexOf('>', i))) {
|
|
||||||
const tagEnd = substr.indexOf('>', i)
|
|
||||||
if (tagEnd !== -1) {
|
|
||||||
balancedStr += substr.substring(i, tagEnd + 1)
|
|
||||||
i = tagEnd + 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const tagEnd = substr.indexOf('>', i)
|
|
||||||
if (tagEnd !== -1) {
|
|
||||||
const tagName = substr.substring(i + 1, (substr.indexOf(' ', i) > -1 && substr.indexOf(' ', i) < tagEnd)
|
|
||||||
? substr.indexOf(' ', i)
|
|
||||||
: tagEnd).split(/\s/)[0]
|
|
||||||
tagStack.push(tagName)
|
tagStack.push(tagName)
|
||||||
balancedStr += substr.substring(i, tagEnd + 1)
|
|
||||||
i = tagEnd + 1
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
balancedStr += fullTag
|
||||||
|
lastIndex = tagStart + fullTag.length
|
||||||
}
|
}
|
||||||
}
|
balancedStr += substr.substring(lastIndex)
|
||||||
balancedStr += substr[i]
|
|
||||||
i++
|
// Close unclosed tags
|
||||||
|
for (let i = tagStack.length - 1; i >= 0; i--) {
|
||||||
|
balancedStr += `</${tagStack[i]}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close any unclosed tags
|
// Check if we cut a mark tag at the beginning
|
||||||
while (tagStack.length > 0) {
|
|
||||||
const tagName = tagStack.pop()
|
|
||||||
balancedStr += `</${tagName}>`
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we removed content from the beginning, add prefix
|
|
||||||
if (start > 0 || pre) {
|
if (start > 0 || pre) {
|
||||||
const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0)
|
const checkStart = Math.max(0, start - 30)
|
||||||
const actualFirstMark = contentStr.indexOf('<mark>', start > 0 ? start - 30 : 0)
|
const actualFirstOpenBracket = contentStr.indexOf('<', checkStart)
|
||||||
|
const actualFirstMark = contentStr.indexOf('<mark>', checkStart)
|
||||||
if (actualFirstOpenBracket !== -1 &&
|
if (actualFirstOpenBracket !== -1 && (actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
|
||||||
(actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
|
|
||||||
pre = '...'
|
pre = '...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
substr = balancedStr
|
return `${pre}${balancedStr}${post}`
|
||||||
return `${pre}${substr}${post}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to handle Algolia highlight results
|
|
||||||
const extractHighlightValue = highlightObj => {
|
|
||||||
if (!highlightObj) return ''
|
|
||||||
|
|
||||||
if (typeof highlightObj === 'string') {
|
|
||||||
return highlightObj.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
|
||||||
return String(highlightObj.value).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Algolia client
|
|
||||||
let searchClient
|
let searchClient
|
||||||
|
|
||||||
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
|
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
|
||||||
@@ -230,96 +219,52 @@ window.addEventListener('load', () => {
|
|||||||
return console.error('Failed to initialize Algolia search client')
|
return console.error('Failed to initialize Algolia search client')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search state
|
|
||||||
let currentQuery = ''
|
let currentQuery = ''
|
||||||
|
let searchRequestId = 0 // Race condition guard
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
const showLoading = show => {
|
|
||||||
const loadingIndicator = document.getElementById('loading-status')
|
|
||||||
if (loadingIndicator) {
|
|
||||||
loadingIndicator.hidden = !show
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache frequently used elements
|
|
||||||
const elements = {
|
|
||||||
get searchInput () { return document.querySelector('#algolia-search-input .ais-SearchBox-input') },
|
|
||||||
get hits () { return document.getElementById('algolia-hits') },
|
|
||||||
get hitsEmpty () { return document.getElementById('algolia-hits-empty') },
|
|
||||||
get hitsList () { return document.querySelector('#algolia-hits .ais-Hits-list') },
|
|
||||||
get hitsWrapper () { return document.querySelector('#algolia-hits .ais-Hits') },
|
|
||||||
get pagination () { return document.getElementById('algolia-pagination') },
|
|
||||||
get paginationList () { return document.querySelector('#algolia-pagination .ais-Pagination-list') },
|
|
||||||
get stats () { return document.querySelector('#algolia-info .ais-Stats-text') },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show/hide search results area
|
|
||||||
const toggleResultsVisibility = hasResults => {
|
const toggleResultsVisibility = hasResults => {
|
||||||
elements.pagination.style.display = hasResults ? '' : 'none'
|
$pagination.style.display = hasResults ? '' : 'none'
|
||||||
elements.stats.style.display = hasResults ? '' : 'none'
|
$stats.style.display = hasResults ? '' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render search results
|
|
||||||
const renderHits = (hits, query, page = 0) => {
|
const renderHits = (hits, query, page = 0) => {
|
||||||
if (hits.length === 0 && query) {
|
if (hits.length === 0 && query) {
|
||||||
elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
|
$hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
|
||||||
elements.hitsEmpty.style.display = ''
|
$hitsEmpty.style.display = ''
|
||||||
elements.hitsWrapper.style.display = 'none'
|
$hitsWrapper.style.display = 'none'
|
||||||
elements.stats.style.display = 'none'
|
$stats.style.display = 'none'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.hitsEmpty.style.display = 'none'
|
$hitsEmpty.style.display = 'none'
|
||||||
|
|
||||||
const hitsHTML = hits.map((hit, index) => {
|
const hitsHTML = hits.map((hit, index) => {
|
||||||
const itemNumber = page * hitsPerPage + index + 1
|
const itemNumber = page * hitsPerPage + index + 1
|
||||||
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
|
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
|
||||||
const result = hit._highlightResult || hit
|
const result = hit._highlightResult || hit
|
||||||
|
|
||||||
// Content extraction
|
// Content extraction - try highlight result first, then raw hit
|
||||||
let content = ''
|
let content = ''
|
||||||
try {
|
for (const field of CONTENT_FIELDS) {
|
||||||
if (result.contentStripTruncate) {
|
if (result[field]) { content = cutContent(result[field]); break }
|
||||||
content = cutContent(result.contentStripTruncate)
|
if (hit[field]) { content = cutContent(hit[field]); break }
|
||||||
} else if (result.contentStrip) {
|
|
||||||
content = cutContent(result.contentStrip)
|
|
||||||
} else if (result.content) {
|
|
||||||
content = cutContent(result.content)
|
|
||||||
} else if (hit.contentStripTruncate) {
|
|
||||||
content = cutContent(hit.contentStripTruncate)
|
|
||||||
} else if (hit.contentStrip) {
|
|
||||||
content = cutContent(hit.contentStrip)
|
|
||||||
} else if (hit.content) {
|
|
||||||
content = cutContent(hit.content)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
content = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title handling
|
// Title handling - try highlight result first, then raw hit
|
||||||
let title = 'no-title'
|
let title = 'no-title'
|
||||||
try {
|
const titleSource = result.title || hit.title
|
||||||
if (result.title) {
|
if (titleSource) {
|
||||||
title = extractHighlightValue(result.title) || 'no-title'
|
title = extractHighlightValue(titleSource) || 'no-title'
|
||||||
} else if (hit.title) {
|
|
||||||
title = extractHighlightValue(hit.title) || 'no-title'
|
|
||||||
}
|
}
|
||||||
|
if (title === 'no-title') {
|
||||||
if (!title || title === 'no-title') {
|
|
||||||
if (typeof hit.title === 'string' && hit.title.trim()) {
|
if (typeof hit.title === 'string' && hit.title.trim()) {
|
||||||
title = hit.title.trim()
|
title = hit.title.trim()
|
||||||
} else if (hit.title && typeof hit.title === 'object' && hit.title.value) {
|
} else if (hit.title?.value) {
|
||||||
title = String(hit.title.value).trim() || 'no-title'
|
title = String(hit.title.value).trim() || 'no-title'
|
||||||
} else {
|
|
||||||
title = 'no-title'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
title = 'no-title'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
return `<li class="ais-Hits-item" value="${itemNumber}">
|
||||||
<li class="ais-Hits-item" value="${itemNumber}">
|
|
||||||
<a href="${link}" class="algolia-hit-item-link">
|
<a href="${link}" class="algolia-hit-item-link">
|
||||||
<span class="algolia-hits-item-title">${title}</span>
|
<span class="algolia-hits-item-title">${title}</span>
|
||||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||||
@@ -327,24 +272,21 @@ window.addEventListener('load', () => {
|
|||||||
</li>`
|
</li>`
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
elements.hitsList.innerHTML = hitsHTML
|
$hitsList.innerHTML = hitsHTML
|
||||||
elements.hitsWrapper.style.display = query ? '' : 'none'
|
$hitsWrapper.style.display = query ? '' : 'none'
|
||||||
|
|
||||||
if (hits.length > 0) {
|
if (hits.length > 0) {
|
||||||
elements.stats.style.display = ''
|
$stats.style.display = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render pagination
|
|
||||||
const renderPagination = (page, nbPages) => {
|
const renderPagination = (page, nbPages) => {
|
||||||
if (nbPages <= 1) {
|
if (nbPages <= 1) {
|
||||||
elements.pagination.style.display = 'none'
|
$pagination.style.display = 'none'
|
||||||
elements.paginationList.innerHTML = ''
|
$paginationList.innerHTML = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.pagination.style.display = 'block'
|
|
||||||
|
|
||||||
const isFirstPage = page === 0
|
const isFirstPage = page === 0
|
||||||
const isLastPage = page === nbPages - 1
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
@@ -359,90 +301,63 @@ window.addEventListener('load', () => {
|
|||||||
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pagesHTML = ''
|
const parts = []
|
||||||
|
|
||||||
// Only add ellipsis and first page when there are many pages
|
// Only add ellipsis and first page when there are many pages
|
||||||
if (nbPages > maxVisiblePages && startPage > 0) {
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
|
|
||||||
</li>`
|
|
||||||
if (startPage > 1) {
|
if (startPage > 1) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
|
||||||
<span class="ais-Pagination-link">...</span>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add middle page numbers
|
// Add middle page numbers
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
const isSelected = i === page
|
if (i === page) {
|
||||||
if (isSelected) {
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected"><span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span></li>`)
|
||||||
pagesHTML += `
|
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
|
|
||||||
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
|
|
||||||
</li>`
|
|
||||||
} else {
|
} else {
|
||||||
pagesHTML += `
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a></li>`)
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add ellipsis and last page when there are many pages
|
// Only add ellipsis and last page when there are many pages
|
||||||
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
if (endPage < nbPages - 2) {
|
if (endPage < nbPages - 2) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
|
||||||
<span class="ais-Pagination-link">...</span>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
pagesHTML += `
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a></li>`)
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nbPages > 1) {
|
// Build prev/next links
|
||||||
elements.paginationList.innerHTML = `
|
const prevLink = isFirstPage
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
|
|
||||||
${isFirstPage
|
|
||||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
}
|
const nextLink = isLastPage
|
||||||
</li>
|
|
||||||
${pagesHTML}
|
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
|
|
||||||
${isLastPage
|
|
||||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
}
|
|
||||||
</li>`
|
$paginationList.innerHTML = `<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">${prevLink}</li>${parts.join('')}<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">${nextLink}</li>`
|
||||||
elements.pagination.style.display = currentQuery ? '' : 'none'
|
$pagination.style.display = currentQuery ? '' : 'none'
|
||||||
} else {
|
|
||||||
elements.pagination.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render statistics
|
|
||||||
const renderStats = (nbHits, processingTimeMS, query) => {
|
const renderStats = (nbHits, processingTimeMS, query) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
const stats = languages.hits_stats
|
const stats = languages.hits_stats
|
||||||
.replace(/\$\{hits}/, nbHits)
|
.replace(/\$\{hits}/, nbHits)
|
||||||
.replace(/\$\{time}/, processingTimeMS)
|
.replace(/\$\{time}/, processingTimeMS)
|
||||||
elements.stats.innerHTML = `<hr>${stats}`
|
$stats.innerHTML = `<hr>${stats}`
|
||||||
elements.stats.style.display = ''
|
$stats.style.display = ''
|
||||||
} else {
|
} else {
|
||||||
elements.stats.style.display = 'none'
|
$stats.style.display = 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const performSearch = async (query, page = 0) => {
|
const performSearch = async (query, page = 0) => {
|
||||||
if (!query.trim()) {
|
const trimmedQuery = query.trim()
|
||||||
|
|
||||||
|
if (!trimmedQuery) {
|
||||||
currentQuery = ''
|
currentQuery = ''
|
||||||
|
searchRequestId++
|
||||||
renderHits([], '', 0)
|
renderHits([], '', 0)
|
||||||
renderPagination(0, 0)
|
renderPagination(0, 0)
|
||||||
renderStats(0, 0, '')
|
renderStats(0, 0, '')
|
||||||
@@ -451,7 +366,8 @@ window.addEventListener('load', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
currentQuery = query
|
currentQuery = trimmedQuery
|
||||||
|
const requestId = ++searchRequestId
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
@@ -460,82 +376,76 @@ window.addEventListener('load', () => {
|
|||||||
// v5 multi-index search
|
// v5 multi-index search
|
||||||
const searchResult = await searchClient.search([{
|
const searchResult = await searchClient.search([{
|
||||||
indexName,
|
indexName,
|
||||||
query,
|
query: trimmedQuery,
|
||||||
params: {
|
params: { page, hitsPerPage, ...HIGHLIGHT_PARAMS }
|
||||||
page,
|
|
||||||
hitsPerPage,
|
|
||||||
highlightPreTag: '<mark>',
|
|
||||||
highlightPostTag: '</mark>',
|
|
||||||
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
|
||||||
}
|
|
||||||
}])
|
}])
|
||||||
result = searchResult.results[0]
|
result = searchResult.results[0]
|
||||||
} else if (searchClient && typeof searchClient.initIndex === 'function') {
|
} else if (searchClient && typeof searchClient.initIndex === 'function') {
|
||||||
// v4 single-index search
|
// v4 single-index search
|
||||||
const index = searchClient.initIndex(indexName)
|
const index = searchClient.initIndex(indexName)
|
||||||
result = await index.search(query, {
|
result = await index.search(trimmedQuery, { page, hitsPerPage, ...HIGHLIGHT_PARAMS })
|
||||||
page,
|
|
||||||
hitsPerPage,
|
|
||||||
highlightPreTag: '<mark>',
|
|
||||||
highlightPostTag: '</mark>',
|
|
||||||
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Algolia: No compatible search method available')
|
throw new Error('Algolia: No compatible search method available')
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHits(result.hits || [], query, page)
|
// Discard stale results from superseded searches
|
||||||
|
if (requestId !== searchRequestId) return
|
||||||
|
|
||||||
|
renderHits(result.hits || [], trimmedQuery, page)
|
||||||
|
|
||||||
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
|
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
|
||||||
renderPagination(page, actualNbPages)
|
renderPagination(page, actualNbPages)
|
||||||
renderStats(result.nbHits || 0, result.processingTimeMS || 0, query)
|
renderStats(result.nbHits || 0, result.processingTimeMS || 0, trimmedQuery)
|
||||||
|
|
||||||
const hasResults = result.hits && result.hits.length > 0
|
const hasResults = result.hits && result.hits.length > 0
|
||||||
toggleResultsVisibility(hasResults)
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
// Refresh Pjax links
|
// Refresh Pjax links
|
||||||
if (window.pjax) {
|
if (window.pjax) {
|
||||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
window.pjax.refresh($hits)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestId !== searchRequestId) return
|
||||||
console.error('Algolia search error:', error)
|
console.error('Algolia search error:', error)
|
||||||
renderHits([], query, page)
|
renderHits([], trimmedQuery, page)
|
||||||
renderPagination(0, 0)
|
renderPagination(0, 0)
|
||||||
renderStats(0, 0, query)
|
renderStats(0, 0, trimmedQuery)
|
||||||
} finally {
|
} finally {
|
||||||
|
if (requestId === searchRequestId) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debounced search
|
|
||||||
let searchTimeout
|
let searchTimeout
|
||||||
const debouncedSearch = (query, delay = 300) => {
|
const debouncedSearch = (query, delay = 300) => {
|
||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
|
// Empty query: clear results immediately without debounce delay
|
||||||
|
if (!query.trim()) {
|
||||||
|
performSearch(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
searchTimeout = setTimeout(() => performSearch(query), delay)
|
searchTimeout = setTimeout(() => performSearch(query), delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize search box and events
|
|
||||||
const initializeSearch = () => {
|
const initializeSearch = () => {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
|
|
||||||
if (elements.searchInput) {
|
if ($searchInput) {
|
||||||
elements.searchInput.addEventListener('input', e => {
|
$searchInput.addEventListener('input', e => {
|
||||||
const query = e.target.value
|
debouncedSearch(e.target.value)
|
||||||
debouncedSearch(query)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
if ($searchForm) {
|
||||||
if (searchForm) {
|
$searchForm.addEventListener('submit', e => {
|
||||||
searchForm.addEventListener('submit', e => {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const query = elements.searchInput.value
|
performSearch($searchInput ? $searchInput.value : '')
|
||||||
performSearch(query)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination event delegation
|
// Pagination event delegation
|
||||||
elements.pagination.addEventListener('click', e => {
|
$pagination.addEventListener('click', e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const link = e.target.closest('a[data-page]')
|
const link = e.target.closest('a[data-page]')
|
||||||
if (link) {
|
if (link) {
|
||||||
@@ -550,7 +460,6 @@ window.addEventListener('load', () => {
|
|||||||
toggleResultsVisibility(false)
|
toggleResultsVisibility(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
|
||||||
initializeSearch()
|
initializeSearch()
|
||||||
searchClickFn()
|
searchClickFn()
|
||||||
searchFnOnce()
|
searchFnOnce()
|
||||||
|
|||||||
+110
-110
@@ -15,31 +15,39 @@ class LocalSearch {
|
|||||||
this.top_n_per_article = top_n_per_article
|
this.top_n_per_article = top_n_per_article
|
||||||
this.isfetched = false
|
this.isfetched = false
|
||||||
this.datas = null
|
this.datas = null
|
||||||
|
this._unescapeDiv = unescape ? document.createElement('div') : null
|
||||||
|
this._processedKeywords = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_processKeywords (keywords) {
|
||||||
|
if (this._processedKeywords) return this._processedKeywords
|
||||||
|
this._processedKeywords = keywords.map(word => {
|
||||||
|
if (this.unescape) {
|
||||||
|
this._unescapeDiv.innerText = word
|
||||||
|
return this._unescapeDiv.innerHTML
|
||||||
|
}
|
||||||
|
return word
|
||||||
|
})
|
||||||
|
return this._processedKeywords
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexByWord (words, text, caseSensitive = false) {
|
getIndexByWord (words, text, caseSensitive = false) {
|
||||||
const index = []
|
const index = []
|
||||||
const included = new Set()
|
const included = new Set()
|
||||||
|
const processedWords = this._processKeywords(words)
|
||||||
|
|
||||||
if (!caseSensitive) {
|
if (!caseSensitive) {
|
||||||
text = text.toLowerCase()
|
text = text.toLowerCase()
|
||||||
}
|
}
|
||||||
words.forEach(word => {
|
processedWords.forEach((word, i) => {
|
||||||
if (this.unescape) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerText = word
|
|
||||||
word = div.innerHTML
|
|
||||||
}
|
|
||||||
const wordLen = word.length
|
const wordLen = word.length
|
||||||
if (wordLen === 0) return
|
if (wordLen === 0) return
|
||||||
let startPosition = 0
|
let startPosition = 0
|
||||||
let position = -1
|
let position = -1
|
||||||
if (!caseSensitive) {
|
const searchWord = caseSensitive ? word : word.toLowerCase()
|
||||||
word = word.toLowerCase()
|
while ((position = text.indexOf(searchWord, startPosition)) > -1) {
|
||||||
}
|
|
||||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
|
||||||
index.push({ position, word })
|
index.push({ position, word })
|
||||||
included.add(word)
|
included.add(words[i])
|
||||||
startPosition = position + wordLen
|
startPosition = position + wordLen
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -90,19 +98,22 @@ class LocalSearch {
|
|||||||
|
|
||||||
// Highlight title and content
|
// Highlight title and content
|
||||||
highlightKeyword (val, slice) {
|
highlightKeyword (val, slice) {
|
||||||
let result = ''
|
const parts = []
|
||||||
let index = slice.start
|
let index = slice.start
|
||||||
for (const { position, length } of slice.hits) {
|
for (const { position, length } of slice.hits) {
|
||||||
result += val.substring(index, position)
|
parts.push(val.substring(index, position))
|
||||||
index = position + length
|
index = position + length
|
||||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
parts.push(`<mark class="search-keyword">${val.substring(position, position + length)}</mark>`)
|
||||||
}
|
}
|
||||||
result += val.substring(index, slice.end)
|
parts.push(val.substring(index, slice.end))
|
||||||
return result
|
return parts.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
getResultItems (keywords) {
|
getResultItems (keywords) {
|
||||||
const resultItems = []
|
const resultItems = []
|
||||||
|
this._processedKeywords = null
|
||||||
|
// Compute highlight param once instead of per-article
|
||||||
|
const highlightParam = keywords.join(' ')
|
||||||
this.datas.forEach(({ title, content, url }) => {
|
this.datas.forEach(({ title, content, url }) => {
|
||||||
// The number of different keywords included in the article.
|
// The number of different keywords included in the article.
|
||||||
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||||
@@ -147,7 +158,7 @@ class LocalSearch {
|
|||||||
let resultItem = ''
|
let resultItem = ''
|
||||||
|
|
||||||
url = new URL(url, location.origin)
|
url = new URL(url, location.origin)
|
||||||
url.searchParams.append('highlight', keywords.join(' '))
|
url.searchParams.append('highlight', highlightParam)
|
||||||
|
|
||||||
if (slicesOfTitle.length !== 0) {
|
if (slicesOfTitle.length !== 0) {
|
||||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||||
@@ -156,10 +167,10 @@ class LocalSearch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slicesOfContent.forEach(slice => {
|
slicesOfContent.forEach(slice => {
|
||||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
|
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
|
||||||
})
|
})
|
||||||
|
|
||||||
resultItem += '</li>'
|
resultItem += '</a></li>'
|
||||||
resultItems.push({
|
resultItems.push({
|
||||||
item: resultItem,
|
item: resultItem,
|
||||||
id: resultItems.length,
|
id: resultItems.length,
|
||||||
@@ -173,7 +184,10 @@ class LocalSearch {
|
|||||||
fetchData () {
|
fetchData () {
|
||||||
const isXml = !this.path.endsWith('json')
|
const isXml = !this.path.endsWith('json')
|
||||||
fetch(this.path)
|
fetch(this.path)
|
||||||
.then(response => response.text())
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
// Get the contents from search data
|
// Get the contents from search data
|
||||||
this.isfetched = true
|
this.isfetched = true
|
||||||
@@ -194,6 +208,12 @@ class LocalSearch {
|
|||||||
// Remove loading animation
|
// Remove loading animation
|
||||||
window.dispatchEvent(new Event('search:loaded'))
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
})
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Local search data fetch failed:', error)
|
||||||
|
this.isfetched = true
|
||||||
|
this.datas = []
|
||||||
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight by wrapping node in mark elements with the given class name
|
// Highlight by wrapping node in mark elements with the given class name
|
||||||
@@ -206,7 +226,7 @@ class LocalSearch {
|
|||||||
index = position + length
|
index = position + length
|
||||||
const mark = document.createElement('mark')
|
const mark = document.createElement('mark')
|
||||||
mark.className = className
|
mark.className = className
|
||||||
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
mark.appendChild(document.createTextNode(val.substring(position, position + length)))
|
||||||
children.push(text, mark)
|
children.push(text, mark)
|
||||||
}
|
}
|
||||||
node.nodeValue = val.substring(index, slice.end)
|
node.nodeValue = val.substring(index, slice.end)
|
||||||
@@ -235,7 +255,7 @@ class LocalSearch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Search
|
// Search
|
||||||
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||||
const enablePagination = pagination && pagination.enable
|
const enablePagination = pagination && pagination.enable
|
||||||
const localSearch = new LocalSearch({
|
const localSearch = new LocalSearch({
|
||||||
@@ -244,9 +264,14 @@ window.addEventListener('load', () => {
|
|||||||
unescape
|
unescape
|
||||||
})
|
})
|
||||||
|
|
||||||
const input = document.querySelector('.local-search-input input')
|
const $input = document.querySelector('.local-search-input input')
|
||||||
const statsItem = document.getElementById('local-search-stats')
|
const $statsItem = document.getElementById('local-search-stats')
|
||||||
const $loadingStatus = document.getElementById('loading-status')
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const $searchMask = document.getElementById('search-mask')
|
||||||
|
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||||
|
const $results = document.getElementById('local-search-results')
|
||||||
|
const $pagination = document.getElementById('local-search-pagination')
|
||||||
|
const $paginationList = document.querySelector('#local-search-pagination .ais-Pagination-list')
|
||||||
const isXml = !path.endsWith('json')
|
const isXml = !path.endsWith('json')
|
||||||
|
|
||||||
// Pagination variables (only initialize if pagination is enabled)
|
// Pagination variables (only initialize if pagination is enabled)
|
||||||
@@ -256,30 +281,17 @@ window.addEventListener('load', () => {
|
|||||||
let currentResultItems = []
|
let currentResultItems = []
|
||||||
|
|
||||||
if (!enablePagination) {
|
if (!enablePagination) {
|
||||||
// If pagination is disabled, we don't need these variables
|
|
||||||
currentPage = undefined
|
currentPage = undefined
|
||||||
currentResultItems = undefined
|
currentResultItems = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache frequently used elements
|
|
||||||
const elements = {
|
|
||||||
get pagination () { return document.getElementById('local-search-pagination') },
|
|
||||||
get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show/hide search results area
|
// Show/hide search results area
|
||||||
const toggleResultsVisibility = hasResults => {
|
const toggleResultsVisibility = hasResults => {
|
||||||
if (enablePagination) {
|
$pagination.style.display = (hasResults && enablePagination) ? '' : 'none'
|
||||||
elements.pagination.style.display = hasResults ? '' : 'none'
|
|
||||||
} else {
|
|
||||||
elements.pagination.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render search results for current page
|
// Render search results for current page
|
||||||
const renderResults = (searchText, resultItems) => {
|
const renderResults = (searchText, resultItems) => {
|
||||||
const container = document.getElementById('local-search-results')
|
|
||||||
|
|
||||||
// Determine items to display based on pagination mode
|
// Determine items to display based on pagination mode
|
||||||
const itemsToDisplay = enablePagination
|
const itemsToDisplay = enablePagination
|
||||||
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||||
@@ -303,12 +315,12 @@ window.addEventListener('load', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
$results.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||||
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
$statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
|
||||||
// Handle pagination
|
// Handle pagination
|
||||||
if (enablePagination) {
|
if (enablePagination) {
|
||||||
@@ -319,19 +331,17 @@ window.addEventListener('load', () => {
|
|||||||
const hasResults = resultItems.length > 0
|
const hasResults = resultItems.length > 0
|
||||||
toggleResultsVisibility(hasResults)
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
window.pjax && window.pjax.refresh(container)
|
window.pjax && window.pjax.refresh($results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render pagination
|
// Render pagination
|
||||||
const renderPagination = (page, nbPages, query) => {
|
const renderPagination = (page, nbPages) => {
|
||||||
if (nbPages <= 1) {
|
if (nbPages <= 1) {
|
||||||
elements.pagination.style.display = 'none'
|
$pagination.style.display = 'none'
|
||||||
elements.paginationList.innerHTML = ''
|
$paginationList.innerHTML = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.pagination.style.display = 'block'
|
|
||||||
|
|
||||||
const isFirstPage = page === 0
|
const isFirstPage = page === 0
|
||||||
const isLastPage = page === nbPages - 1
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
@@ -346,77 +356,49 @@ window.addEventListener('load', () => {
|
|||||||
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pagesHTML = ''
|
const parts = []
|
||||||
|
|
||||||
// Only add ellipsis and first page when there are many pages
|
// Only add ellipsis and first page when there are many pages
|
||||||
if (nbPages > maxVisiblePages && startPage > 0) {
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
|
|
||||||
</li>`
|
|
||||||
if (startPage > 1) {
|
if (startPage > 1) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
|
||||||
<span class="ais-Pagination-link">...</span>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add middle page numbers
|
// Add middle page numbers
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
const isSelected = i === page
|
if (i === page) {
|
||||||
if (isSelected) {
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected"><span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span></li>`)
|
||||||
pagesHTML += `
|
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
|
|
||||||
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
|
|
||||||
</li>`
|
|
||||||
} else {
|
} else {
|
||||||
pagesHTML += `
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a></li>`)
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add ellipsis and last page when there are many pages
|
// Only add ellipsis and last page when there are many pages
|
||||||
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
if (endPage < nbPages - 2) {
|
if (endPage < nbPages - 2) {
|
||||||
pagesHTML += `
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
|
||||||
<span class="ais-Pagination-link">...</span>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
pagesHTML += `
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a></li>`)
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--page">
|
|
||||||
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
|
|
||||||
</li>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nbPages > 1) {
|
// Build prev/next links
|
||||||
elements.paginationList.innerHTML = `
|
const prevLink = isFirstPage
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
|
|
||||||
${isFirstPage
|
|
||||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
}
|
const nextLink = isLastPage
|
||||||
</li>
|
|
||||||
${pagesHTML}
|
|
||||||
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
|
|
||||||
${isLastPage
|
|
||||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
}
|
|
||||||
</li>`
|
$paginationList.innerHTML = `<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">${prevLink}</li>${parts.join('')}<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">${nextLink}</li>`
|
||||||
} else {
|
$pagination.style.display = ''
|
||||||
elements.pagination.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear search results and stats
|
// Clear search results and stats
|
||||||
const clearSearchResults = () => {
|
const clearSearchResults = () => {
|
||||||
const container = document.getElementById('local-search-results')
|
$results.textContent = ''
|
||||||
container.textContent = ''
|
$statsItem.textContent = ''
|
||||||
statsItem.textContent = ''
|
|
||||||
toggleResultsVisibility(false)
|
toggleResultsVisibility(false)
|
||||||
if (enablePagination) {
|
if (enablePagination) {
|
||||||
currentResultItems = []
|
currentResultItems = []
|
||||||
@@ -426,12 +408,11 @@ window.addEventListener('load', () => {
|
|||||||
|
|
||||||
// Show no results message
|
// Show no results message
|
||||||
const showNoResults = searchText => {
|
const showNoResults = searchText => {
|
||||||
const container = document.getElementById('local-search-results')
|
$results.textContent = ''
|
||||||
container.textContent = ''
|
|
||||||
const statsDiv = document.createElement('div')
|
const statsDiv = document.createElement('div')
|
||||||
statsDiv.className = 'search-result-stats'
|
statsDiv.className = 'search-result-stats'
|
||||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||||
statsItem.innerHTML = statsDiv.outerHTML
|
$statsItem.innerHTML = statsDiv.outerHTML
|
||||||
toggleResultsVisibility(false)
|
toggleResultsVisibility(false)
|
||||||
if (enablePagination) {
|
if (enablePagination) {
|
||||||
currentResultItems = []
|
currentResultItems = []
|
||||||
@@ -441,7 +422,7 @@ window.addEventListener('load', () => {
|
|||||||
|
|
||||||
const inputEventFunction = () => {
|
const inputEventFunction = () => {
|
||||||
if (!localSearch.isfetched) return
|
if (!localSearch.isfetched) return
|
||||||
let searchText = input.value.trim().toLowerCase()
|
let searchText = $input.value.trim().toLowerCase()
|
||||||
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||||
|
|
||||||
if (searchText !== '') $loadingStatus.hidden = false
|
if (searchText !== '') $loadingStatus.hidden = false
|
||||||
@@ -478,44 +459,63 @@ window.addEventListener('load', () => {
|
|||||||
$loadingStatus.hidden = true
|
$loadingStatus.hidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadFlag = false
|
// Debounced input handler
|
||||||
const $searchMask = document.getElementById('search-mask')
|
let searchTimeout
|
||||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
const debouncedInputEvent = () => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
// Empty input: clear results immediately without debounce delay
|
||||||
|
if (!$input.value.trim()) {
|
||||||
|
inputEventFunction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(inputEventFunction, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadFlag = false
|
||||||
|
|
||||||
// fix safari
|
|
||||||
const fixSafariHeight = () => {
|
const fixSafariHeight = () => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounced resize to avoid layout thrashing
|
||||||
|
let resizeTimer
|
||||||
|
const onResize = () => {
|
||||||
|
clearTimeout(resizeTimer)
|
||||||
|
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEscape = event => {
|
||||||
|
if (event.code === 'Escape') {
|
||||||
|
closeSearch()
|
||||||
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openSearch = () => {
|
const openSearch = () => {
|
||||||
btf.overflowPaddingR.add()
|
btf.overflowPaddingR.add()
|
||||||
btf.animateIn($searchMask, 'to_show 0.5s')
|
btf.animateIn($searchMask, 'to_show 0.5s')
|
||||||
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
||||||
setTimeout(() => { input.focus() }, 300)
|
setTimeout(() => { $input.focus() }, 300)
|
||||||
if (!loadFlag) {
|
if (!loadFlag) {
|
||||||
!localSearch.isfetched && localSearch.fetchData()
|
!localSearch.isfetched && localSearch.fetchData()
|
||||||
input.addEventListener('input', inputEventFunction)
|
$input.addEventListener('input', debouncedInputEvent)
|
||||||
loadFlag = true
|
loadFlag = true
|
||||||
}
|
}
|
||||||
// shortcut: ESC
|
// shortcut: ESC
|
||||||
document.addEventListener('keydown', function f (event) {
|
document.addEventListener('keydown', handleEscape)
|
||||||
if (event.code === 'Escape') {
|
|
||||||
closeSearch()
|
|
||||||
document.removeEventListener('keydown', f)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fixSafariHeight()
|
fixSafariHeight()
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
window.addEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
btf.overflowPaddingR.remove()
|
btf.overflowPaddingR.remove()
|
||||||
btf.animateOut($searchDialog, 'search_close .5s')
|
btf.animateOut($searchDialog, 'search_close .5s')
|
||||||
btf.animateOut($searchMask, 'to_hide 0.5s')
|
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchClickFn = () => {
|
const searchClickFn = () => {
|
||||||
@@ -532,14 +532,14 @@ window.addEventListener('load', () => {
|
|||||||
|
|
||||||
// Pagination event delegation - only add if pagination is enabled
|
// Pagination event delegation - only add if pagination is enabled
|
||||||
if (enablePagination) {
|
if (enablePagination) {
|
||||||
elements.pagination.addEventListener('click', e => {
|
$pagination.addEventListener('click', e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const link = e.target.closest('a[data-page]')
|
const link = e.target.closest('a[data-page]')
|
||||||
if (link) {
|
if (link) {
|
||||||
const page = parseInt(link.dataset.page, 10)
|
const page = parseInt(link.dataset.page, 10)
|
||||||
if (!isNaN(page) && currentResultItems.length > 0) {
|
if (!isNaN(page) && currentResultItems.length > 0) {
|
||||||
currentPage = page
|
currentPage = page
|
||||||
renderResults(input.value.trim().toLowerCase(), currentResultItems)
|
renderResults($input.value.trim().toLowerCase(), currentResultItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+24
-11
File diff suppressed because one or more lines are too long
+46
-31
@@ -14,61 +14,85 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
throttle: function (func, wait, options = {}) {
|
throttle: (func, wait, options = {}) => {
|
||||||
let timeout, context, args
|
let timeout, args
|
||||||
let previous = 0
|
let previous = 0
|
||||||
|
|
||||||
const later = () => {
|
const later = () => {
|
||||||
previous = options.leading === false ? 0 : new Date().getTime()
|
previous = options.leading === false ? 0 : new Date().getTime()
|
||||||
timeout = null
|
timeout = null
|
||||||
func.apply(context, args)
|
func(...args)
|
||||||
if (!timeout) context = args = null
|
if (!timeout) args = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const throttled = (...params) => {
|
return (...params) => {
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
if (!previous && options.leading === false) previous = now
|
if (!previous && options.leading === false) previous = now
|
||||||
const remaining = wait - (now - previous)
|
const remaining = wait - (now - previous)
|
||||||
context = this
|
|
||||||
args = params
|
args = params
|
||||||
|
|
||||||
if (remaining <= 0 || remaining > wait) {
|
if (remaining <= 0 || remaining > wait) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
timeout = null
|
timeout = null
|
||||||
}
|
}
|
||||||
previous = now
|
previous = now
|
||||||
func.apply(context, args)
|
func(...args)
|
||||||
if (!timeout) context = args = null
|
if (!timeout) args = null
|
||||||
} else if (!timeout && options.trailing !== false) {
|
} else if (!timeout && options.trailing !== false) {
|
||||||
timeout = setTimeout(later, remaining)
|
timeout = setTimeout(later, remaining)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return throttled
|
|
||||||
},
|
},
|
||||||
|
|
||||||
overflowPaddingR: {
|
rafThrottle: fn => {
|
||||||
|
let rafId = null
|
||||||
|
return (...args) => {
|
||||||
|
if (rafId) return
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
fn(...args)
|
||||||
|
rafId = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
overflowPaddingR: (() => {
|
||||||
|
let headerElement = null
|
||||||
|
let menuElement = null
|
||||||
|
|
||||||
|
const getElements = () => {
|
||||||
|
if (!headerElement) {
|
||||||
|
headerElement = document.getElementById('page-header')
|
||||||
|
}
|
||||||
|
if (!menuElement) {
|
||||||
|
menuElement = document.getElementById('menus')
|
||||||
|
}
|
||||||
|
return { headerElement, menuElement }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
add: () => {
|
add: () => {
|
||||||
const paddingRight = window.innerWidth - document.body.clientWidth
|
const paddingRight = window.innerWidth - document.body.clientWidth
|
||||||
|
|
||||||
if (paddingRight > 0) {
|
if (paddingRight > 0) {
|
||||||
document.body.style.paddingRight = `${paddingRight}px`
|
document.body.style.paddingRight = `${paddingRight}px`
|
||||||
document.body.style.overflow = 'hidden'
|
document.body.style.overflow = 'hidden'
|
||||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
const { headerElement: header, menuElement: menu } = getElements()
|
||||||
if (menuElement) {
|
if (header && menu && header.classList.contains('nav-fixed')) {
|
||||||
menuElement.style.paddingRight = `${paddingRight}px`
|
menu.style.paddingRight = `${paddingRight}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove: () => {
|
remove: () => {
|
||||||
document.body.style.paddingRight = ''
|
document.body.style.paddingRight = ''
|
||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ''
|
||||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
const { headerElement: header, menuElement: menu } = getElements()
|
||||||
if (menuElement) {
|
if (header && menu && header.classList.contains('nav-fixed')) {
|
||||||
menuElement.style.paddingRight = ''
|
menu.style.paddingRight = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
})(),
|
||||||
|
|
||||||
snackbarShow: (text, showAction = false, duration = 2000) => {
|
snackbarShow: (text, showAction = false, duration = 2000) => {
|
||||||
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
|
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
|
||||||
@@ -135,7 +159,8 @@
|
|||||||
const animate = currentTime => {
|
const animate = currentTime => {
|
||||||
const timeElapsed = currentTime - startTime
|
const timeElapsed = currentTime - startTime
|
||||||
const progress = Math.min(timeElapsed / time, 1)
|
const progress = Math.min(timeElapsed / time, 1)
|
||||||
window.scrollTo(0, currentPos + (pos - currentPos) * progress)
|
const easedProgress = 1 - Math.pow(1 - progress, 4) // easeOutQuart
|
||||||
|
window.scrollTo(0, currentPos + (pos - currentPos) * easedProgress)
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
@@ -169,17 +194,7 @@
|
|||||||
|
|
||||||
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
|
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
|
||||||
|
|
||||||
getEleTop: ele => {
|
getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY,
|
||||||
let actualTop = ele.offsetTop
|
|
||||||
let current = ele.offsetParent
|
|
||||||
|
|
||||||
while (current !== null) {
|
|
||||||
actualTop += current.offsetTop
|
|
||||||
current = current.offsetParent
|
|
||||||
}
|
|
||||||
|
|
||||||
return actualTop
|
|
||||||
},
|
|
||||||
|
|
||||||
loadLightbox: ele => {
|
loadLightbox: ele => {
|
||||||
const service = GLOBAL_CONFIG.lightbox
|
const service = GLOBAL_CONFIG.lightbox
|
||||||
@@ -190,7 +205,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (service === 'fancybox') {
|
if (service === 'fancybox') {
|
||||||
Array.from(ele).forEach(i => {
|
ele.forEach(i => {
|
||||||
if (i.parentNode.tagName !== 'A') {
|
if (i.parentNode.tagName !== 'A') {
|
||||||
const dataSrc = i.dataset.lazySrc || i.src
|
const dataSrc = i.dataset.lazySrc || i.src
|
||||||
const dataCaption = i.title || i.alt || ''
|
const dataCaption = i.title || i.alt || ''
|
||||||
|
|||||||
Reference in New Issue
Block a user