This commit is contained in:
2025-10-04 16:14:27 +08:00
parent f87eeaa2f5
commit b146025c6c
24 changed files with 1531 additions and 408 deletions

View File

@@ -181,6 +181,60 @@ if hexo-config('avatar.effect') == true
.reward-main
animation: donate_effcet .3s .1s ease both
.btn-effects
position: relative
overflow: hidden
transition: all .3s cubic-bezier(.4, 0, .2, 1)
transform: translateZ(0)
&:hover
box-shadow: 0 4px 12px rgba(0, 0, 0, .15)
text-decoration: none
transform: translateY(-1px) scale(1.02)
&:active
transition-duration: .1s
transform: translateY(0) scale(.98)
i
display: inline-block
vertical-align: middle
transition: all .3s cubic-bezier(.4, 0, .2, 1)
&:hover i
animation: buttonIconBounce .6s ease-in-out
i + span
margin-left: 6px
vertical-align: middle
transition: margin-left .3s cubic-bezier(.4, 0, .2, 1)
&:hover i + span
margin-left: 8px
&::before
position: absolute
top: 0
left: -100%
z-index: 1
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .2), transparent)
content: ''
transition: left .5s cubic-bezier(.4, 0, .2, 1)
&:hover::before
left: 100%
& > *
position: relative
z-index: 2
.btn-effects-large
&:hover
box-shadow: 0 6px 16px rgba(0, 0, 0, .2)
transform: translateY(-2px) scale(1.03)
@keyframes scroll-down-effect
0%
opacity: .4
@@ -278,3 +332,17 @@ if hexo-config('avatar.effect') == true
100%
transform: translateX(0)
@keyframes buttonIconBounce
0%,
100%
transform: translateY(0) scale(1)
25%
transform: translateY(-3px) scale(1.1) rotateZ(-5deg)
50%
transform: translateY(0) scale(1.05) rotateZ(0)
75%
transform: translateY(-1px) scale(1.02) rotateZ(2deg)

View File

@@ -39,6 +39,65 @@
--zoom-bg: #fff
--mark-bg: alpha($dark-black, .3)
// tags plugin
:root
--btn-color: $btn-color
--btn-default-color: $btn-default-color
--tags-blue-color: $tagsP-blue-color
--tags-blue-color-lighten: lighten($tagsP-blue-color, 85%)
--tags-pink-color: $tagsP-pink-color
--tags-pink-color-lighten: lighten($tagsP-pink-color, 85%)
--tags-red-color: $tagsP-red-color
--tags-red-color-lighten: lighten($tagsP-red-color, 85%)
--tags-orange-color: $tagsP-orange-color
--tags-orange-color-lighten: lighten($tagsP-orange-color, 85%)
--tags-purple-color: $tagsP-purple-color
--tags-purple-color-lighten: lighten($tagsP-purple-color, 85%)
--tags-green-color: $tagsP-green-color
--tags-green-color-lighten: lighten($tagsP-green-color, 85%)
--note-default-border: $note-default-border
--note-default-bg: $note-default-bg
--note-default-text: $note-default-text
--note-modern-default-border: $note-modern-default-border
--note-modern-default-bg: $note-modern-default-bg
--note-modern-default-text: $note-modern-default-text
--note-modern-default-hover: $note-modern-default-hover
--note-primary-border: $note-primary-border
--note-primary-bg: $note-primary-bg
--note-primary-text: $note-primary-text
--note-modern-primary-border: $note-modern-primary-border
--note-modern-primary-bg: $note-modern-primary-bg
--note-modern-primary-text: $note-modern-primary-text
--note-modern-primary-hover: $note-modern-primary-hover
--note-info-border: $note-info-border
--note-info-bg: $note-info-bg
--note-info-text: $note-info-text
--note-modern-info-border: $note-modern-info-border
--note-modern-info-bg: $note-modern-info-bg
--note-modern-info-text: $note-modern-info-text
--note-modern-info-hover: $note-modern-info-hover
--note-success-border: $note-success-border
--note-success-bg: $note-success-bg
--note-success-text: $note-success-text
--note-modern-success-border: $note-modern-success-border
--note-modern-success-bg: $note-modern-success-bg
--note-modern-success-text: $note-modern-success-text
--note-modern-success-hover: $note-modern-success-hover
--note-warning-border: $note-warning-border
--note-warning-bg: $note-warning-bg
--note-warning-text: $note-warning-text
--note-modern-warning-border: $note-modern-warning-border
--note-modern-warning-bg: $note-modern-warning-bg
--note-modern-warning-text: $note-modern-warning-text
--note-modern-warning-hover: $note-modern-warning-hover
--note-danger-border: $note-danger-border
--note-danger-bg: $note-danger-bg
--note-danger-text: $note-danger-text
--note-modern-danger-border: $note-modern-danger-border
--note-modern-danger-bg: $note-modern-danger-bg
--note-modern-danger-text: $note-modern-danger-text
--note-modern-danger-hover: $note-modern-danger-hover
body
position: relative
overflow-y: scroll

View File

@@ -60,6 +60,7 @@
text-align: center
line-height: 2.4
addBorderRadius(7)
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)
@@ -180,6 +181,11 @@
.card-category-list
&.child
padding: 0 0 0 16px
overflow: hidden
max-height: 0
opacity: 0
visibility: hidden
transition: max-height .3s ease, opacity .3s ease
> .parent
> a
@@ -188,7 +194,9 @@
transform: rotate(-90deg)
& + .child
display: block
max-height: 1000px
opacity: 1
visibility: visible
.card-category-list
&-name
@@ -207,7 +215,9 @@
if hexo-config('aside.card_categories.expand') == false
> .child
display: none
max-height: 0
opacity: 0
visibility: hidden
.card-webinfo
.webinfo

View File

@@ -1,45 +1,95 @@
if hexo-config('preloader')
if hexo-config('preloader.enable') && hexo-config('preloader.source') == 1
.loading-bg
display: flex;
width: 100%;
height: 100%;
position: fixed;
background: #f0f0f2;
z-index: 1001;
opacity: 1;
transition: .3s;
position: fixed
z-index: 1000
width: 50%
height: 100%
background-color: var(--preloader-bg)
#loading-box
.loading-img
width: 100px;
height: 100px;
border-radius: 50%;
margin: auto;
border: 4px solid #f0f0f2;
animation-duration: .3s;
animation-name: loadingAction;
animation-iteration-count: infinite;
animation-direction: alternate;
.loading-image-dot
width: 30px;
height: 30px;
background: #6bdf8f;
position: absolute;
border-radius: 50%;
border: 6px solid #fff;
top: 50%;
left: 50%;
transform: translate(18px, 24px);
.loading-left-bg
@extend .loading-bg
.loading-right-bg
@extend .loading-bg
right: 0
.spinner-box
position: fixed
z-index: 1001
display: flex
justify-content: center
align-items: center
width: 100%
height: 100vh
.configure-border-1
position: absolute
padding: 3px
width: 115px
height: 115px
background: #ffab91
animation: configure-clockwise 3s ease-in-out 0s infinite alternate
.configure-border-2
left: -115px
padding: 3px
width: 115px
height: 115px
background: rgb(63, 249, 220)
transform: rotate(45deg)
animation: configure-xclockwise 3s ease-in-out 0s infinite alternate
.loading-word
position: absolute
color: var(--preloader-color)
font-size: 16px
.configure-core
width: 100%
height: 100%
background-color: var(--preloader-bg)
&.loaded
.loading-bg
opacity: 0;
z-index: -1000;
.loading-left-bg
transition: all .5s
transform: translate(-100%, 0)
@keyframes loadingAction
0% {
opacity: 1;
}
.loading-right-bg
transition: all .5s
transform: translate(100%, 0)
100% {
opacity: .4;
}
.spinner-box
display: none
@keyframes configure-clockwise
0%
transform: rotate(0)
25%
transform: rotate(90deg)
50%
transform: rotate(180deg)
75%
transform: rotate(270deg)
100%
transform: rotate(360deg)
@keyframes configure-xclockwise
0%
transform: rotate(45deg)
25%
transform: rotate(-45deg)
50%
transform: rotate(-135deg)
75%
transform: rotate(-225deg)
100%
transform: rotate(-315deg)

View File

@@ -15,9 +15,12 @@
color: var(--btn-color)
cursor: pointer
addBorderRadius()
@extend .btn-effects
@extend .btn-effects-large
i
margin-right: 5px
vertical-align: baseline
&:hover
.reward-button
@@ -28,7 +31,7 @@
.reward-main
position: absolute
bottom: 40px
bottom: 50px
left: 0
z-index: 100
display: none

View File

@@ -38,10 +38,14 @@
font-size: 16px
line-height: w
addBorderRadius(5)
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)
i
vertical-align: baseline
#mobile-toc-button
display: none
@@ -54,19 +58,52 @@
if hexo-config('rightside_scroll_percent')
#go-up
position: relative
.scroll-percent
position: absolute
top: 0
left: 0
display: none
width: 100%
height: 100%
opacity: 0
transition: all .4s cubic-bezier(.4, 0, .2, 1)
transform: scale(.8)
i
position: relative
z-index: 1
width: 100%
opacity: 1
transition: all .4s cubic-bezier(.4, 0, .2, 1)
transform: scale(1)
&.show-percent
.scroll-percent
display: block
opacity: 1
transform: scale(1)
animation: fadeInScale .4s ease-out
& + i
display: none
i
opacity: 0
transform: scale(.8)
&:hover
&:hover.show-percent
.scroll-percent
display: none
opacity: 0
transform: scale(.8)
& + i
display: block
i
opacity: 1 !important
transform: scale(1) !important
@keyframes fadeInScale
from
opacity: 0
transform: scale(.8)
to
opacity: 1
transform: scale(1)

View File

@@ -38,8 +38,7 @@
background: var(--sidebar-menu-bg)
box-shadow: 0 0 1px 1px rgba(7, 17, 27, .05)
addBorderRadius(10)
.menus_item
margin: 10px 0
.site-page
@extend .limit-one-line
position: relative
@@ -50,20 +49,17 @@
font-size: 1.15em
cursor: pointer
addBorderRadius(6)
border: var(--icat-style-border-always)
background: var(--icat-essay-card-bg)
font-size: 14px
border-radius: 12px
&:hover
background: var(--text-bg-hover)
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
color: var(--white)
transition: all .2s ease
transform: translateX(3px)
i:first-child
width: 15%
text-align: left
padding-left: 10px
&.group
& > i:last-child
@@ -77,34 +73,25 @@
transform: rotate(90deg)
& + .menus_item_child
display: none
overflow: hidden
max-height: 0
opacity: 0
transform: scaleY(0)
transform-origin: top
.menus_item_child
margin: 0
padding-left: 25px
max-height: 0
list-style: none
opacity: 0
transition: transform .3s ease, opacity .3s ease, max-height .3s ease
transform: scaleY(0)
transform-origin: top
will-change: transform, opacity, max-height
#sidebar
#sidebar-menus
.icat_menus_item
display: inline-block
width: 50%
.site-page
text-align: center
margin: 4px
display: flex
flex-direction: column
align-items: center
padding: 8px 0
border-radius: 12px
font-size: 14px
i:first-child
padding-left: 0
& > .icat-essay
font-weight: 500
span
margin-top: -8px
// .hide
.site-page.group:not(.hide) + .menus_item_child
max-height: 1000px
opacity: 1
transform: scaleY(1)

View File

@@ -33,6 +33,56 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
--timeline-bg: lighten(#121212, 5)
--zoom-bg: #121212
--mark-bg: alpha($dark-black, .6)
--btn-color: darken($btn-color, 20%)
--btn-default-color: lighten($btn-default-color, 20%)
--tags-blue-color: desaturate(darken($tagsP-blue-color, 20%), 25%)
--tags-blue-color-lighten: rgba(66, 139, 202, .15)
--tags-pink-color: desaturate(darken($tagsP-pink-color, 22%), 30%)
--tags-pink-color-lighten: rgba(255, 105, 180, .15)
--tags-red-color: desaturate(darken($tagsP-red-color, 25%), 28%)
--tags-red-color-lighten: rgba(255, 0, 0, .15)
--tags-orange-color: desaturate(darken($tagsP-orange-color, 22%), 32%)
--tags-orange-color-lighten: rgba(255, 140, 0, .15)
--tags-purple-color: desaturate(darken($tagsP-purple-color, 18%), 30%)
--tags-purple-color-lighten: rgba(111, 66, 193, .15)
--tags-green-color: desaturate(darken($tagsP-green-color, 20%), 28%)
--tags-green-color-lighten: rgba(92, 184, 92, .15)
--note-default-border: $note-dark-default-border
--note-default-bg: $note-dark-default-bg
--note-default-text: $note-dark-default-text
--note-modern-default-border: $note-dark-modern-default-border
--note-modern-default-bg: $note-dark-modern-default-bg
--note-modern-default-text: $note-dark-modern-default-text
--note-primary-border: $note-dark-primary-border
--note-primary-bg: $note-dark-primary-bg
--note-primary-text: $note-dark-primary-text
--note-modern-primary-border: $note-dark-modern-primary-border
--note-modern-primary-bg: $note-dark-modern-primary-bg
--note-modern-primary-text: $note-dark-modern-primary-text
--note-info-border: $note-dark-info-border
--note-info-bg: $note-dark-info-bg
--note-info-text: $note-dark-info-text
--note-modern-info-border: $note-dark-modern-info-border
--note-modern-info-bg: $note-dark-modern-info-bg
--note-modern-info-text: $note-dark-modern-info-text
--note-success-border: $note-dark-success-border
--note-success-bg: $note-dark-success-bg
--note-success-text: $note-dark-success-text
--note-modern-success-border: $note-dark-modern-success-border
--note-modern-success-bg: $note-dark-modern-success-bg
--note-modern-success-text: $note-dark-modern-success-text
--note-warning-border: $note-dark-warning-border
--note-warning-bg: $note-dark-warning-bg
--note-warning-text: $note-dark-warning-text
--note-modern-warning-border: $note-dark-modern-warning-border
--note-modern-warning-bg: $note-dark-modern-warning-bg
--note-modern-warning-text: $note-dark-modern-warning-text
--note-danger-border: $note-dark-danger-border
--note-danger-bg: $note-dark-danger-bg
--note-danger-text: $note-dark-danger-text
--note-modern-danger-border: $note-dark-modern-danger-border
--note-modern-danger-bg: $note-dark-modern-danger-bg
--note-modern-danger-text: $note-dark-modern-danger-text
#web_bg:before
position: absolute
@@ -78,23 +128,17 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
#switch-btn
filter: brightness(.8)
// note
if hexo-config('note.style') == 'modern' || hexo-config('note.style') == 'flat'
.note
filter: brightness(.8)
// hide-tags
.hide-button,
.btn-beautify,
.hl-label,
#post-outdate-notice,
.error-img,
.container iframe,
.gist,
.ads-wrap
.ads-wrap,
.tag-cloud-list > a
filter: brightness(.8)
img
img:not(.cover)
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
filter: blur(0) brightness(.8)
else

View File

@@ -18,7 +18,7 @@
padding: 20px 5px
+minWidth2000()
max-width: 70%
max-width: 60%
& > div:first-child:not(.nc)
@extend .cardHover

View File

@@ -76,3 +76,110 @@
&.no-comment
display: none
.shuoshuo-navigation
display: flex
justify-content: center
align-items: center
margin-top: 20px
padding: 20px 0
button
display: flex
justify-content: center
align-items: center
width: 2.7em
height: 2.7em
background-color: var(--btn-bg)
color: var(--btn-color)
font-size: .9em
line-height: 2.5em
transition: all .2s ease-in-out
addBorderRadius(6)
&:not(:disabled)
@extend .btn-effects
&:hover:not(:disabled)
background-color: var(--btn-hover-color)
&:disabled
background: #f5f5f5
color: #ccc
opacity: .5
cursor: not-allowed
.shuoshuo-page-info
margin: 0 15px
color: #858585
white-space: nowrap
font-size: .9em
.shuoshuo-page-input
margin-right: 12px
padding: 0 15px
height: 2.7em
border: 1px solid var(--btn-bg)
background: var(--card-bg)
color: #858585
text-align: center
font-size: .9em
transition: all .2s ease-in-out
addBorderRadius(6)
&:focus
outline: none
border-width: 2px
&::placeholder
color: transparent
/* number */
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button
margin: 0
-webkit-appearance: none
/* Firefox */
-moz-appearance: textfield
/* */
&.shuoshuo-page-num
min-width: 40px
width: 40px
border: none
background: $light-blue
color: var(--white)
font-weight: 500
cursor: text
&:focus
border: 1px solid $light-blue
background: var(--white)
color: #333
/* */
&.invalid
border-color: #ff4757
background-color: #ffeaea
color: #ff4757
animation: shake .5s ease-in-out
/* */
@keyframes shake
0%,
100%
transform: translateX(0)
10%,
30%,
50%,
70%,
90%
transform: translateX(-2px)
20%,
40%,
60%,
80%
transform: translateX(2px)

View File

@@ -1,27 +1,91 @@
.tag-cloud
&-list
animation: tagsFadeIn .6s cubic-bezier(.4, 0, .2, 1)
&:hover a:not(:hover)
opacity: .7
transform: scale(.98)
a
position: relative
display: inline-block
margin: 2px
padding: 2px 7px
margin: 5px
padding: 3px 12px
line-height: 1.7
transition: all .3s
addBorderRadius(5)
transition: all .3s cubic-bezier(.4, 0, .2, 1)
addBorderRadius(7)
overflow: hidden
color: white
transform: translateY(0) scale(1)
will-change: transform, background-color, box-shadow
&::before
position: absolute
top: 0
left: -100%
z-index: -1
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent)
content: ''
transition: left .6s cubic-bezier(.4, 0, .2, 1)
&:hover
background: var(--btn-bg) !important
box-shadow: 2px 2px 6px rgba(0, 0, 0, .2)
background: var(--btn-hover-color) !important
box-shadow:
0 6px 20px rgba(0, 0, 0, .12),
0 4px 8px rgba(0, 0, 0, .08),
0 0 0 1px rgba(255, 255, 255, .05)
color: var(--btn-color) !important
transform: translateY(-2px) scale(1.02)
&::before
left: 100%
&:active
box-shadow:
0 3px 8px rgba(0, 0, 0, .15),
0 1px 3px rgba(0, 0, 0, .1)
transition: all .15s cubic-bezier(.4, 0, .2, 1)
transform: translateY(-1px) scale(.98)
+maxWidth768()
zoom: .85
&:hover
transform: translateY(-1px) scale(1.01)
&:active
transform: translateY(0) scale(.99)
&::before
display: none
&-title
font-size: 2.57em
animation: titleSlideIn .8s cubic-bezier(.4, 0, .2, 1)
+maxWidth768()
font-size: 2em
@keyframes tagsFadeIn
from
opacity: 0
transform: translateY(20px)
to
opacity: 1
transform: translateY(0)
@keyframes titleSlideIn
from
opacity: 0
transform: translateX(-30px)
to
opacity: 1
transform: translateX(0)
.page-title
& + .tag-cloud-list
text-align: left

View File

@@ -1,93 +1,13 @@
#algolia-search
.search-dialog
.ais-SearchBox
input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
.ais-SearchBox-loadingIndicator
position: absolute
top: 18px
left: 67px
.ais-Hits-list
margin: 0
padding: 0
@extend .list-beauty
a
color: var(--search-a-color)
&:hover
color: $search-color
mark
background: transparent
color: $search-keyword-highlight
font-weight: bold
.algolia-hits-item-title
font-weight: 600
.algolia-hit-item-content
margin: 0 0 8px
word-break: break-word
.ais-Pagination
margin: 15px 0 0
padding: 0
text-align: center
.ais-Pagination-list
margin: 0
padding: 0
list-style: none
.ais-Pagination-item
display: inline
margin: 0 4px
padding: 0
.ais-Pagination-link
display: inline-block
min-width: 24px
height: 24px
text-align: center
line-height: 24px
addBorderRadius()
.ais-Pagination-item--selected
a
background: $theme-paginator-color
color: #eee
cursor: default
.ais-Pagination-item--disabled
visibility: hidden
#algolia-hits
> div
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 220px)
+maxWidth768()
max-height: none
height: calc(var(--search-height) - 235px)
min-height: calc(var(--search-height) - 245px)
#algolia-info
div
display: inline
.algolia-poweredBy
float: right
vertical-align: text-top
padding-top: 2px
svg
height: 1.1em

View File

@@ -20,22 +20,174 @@
border-radius: 0
.search-nav
margin: 0 0 14px
display: flex
justify-content: space-between
align-items: center
margin-bottom: 14px
color: $search-color
font-size: 1.4em
line-height: 1
.search-dialog-title
margin-right: 10px
margin-right: 4px
#loading-status
&[hidden]
display: none !important
.search-close-button
float: right
flex: 1
color: $grey
transition: color .2s ease-in-out
text-align: right
transition: all .2s ease
&:hover
color: $search-color
.local-search-input,
#algolia-search-input
margin: 0 auto
max-width: 100%
width: 100%
input,
.ais-SearchBox-input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
-webkit-appearance: none
&::placeholder
color: var(--text-color)
.search-result-list,
.ais-Hits-list
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 220px)
.local-search-hit-item,
.ais-Hits-item
display: flex
align-items: flex-start
margin: 3px 0
line-height: 1.8
transition: all .2s ease-in-out
&:hover
transform: translateY(-1px)
&:not([value])::before
display: none
&[value]::before
display: inline-flex
flex-shrink: 0
justify-content: center
align-items: center
margin-right: 6px
margin-top: 3px
min-width: 24px
color: $search-color
content: attr(value) '.'
font-weight: bold
font-style: italic
font-size: .9em
&::marker
content: none
a
flex: 1
color: var(--search-a-color)
&:hover
color: $search-color
.search-result-title,
.algolia-hits-item-title
font-weight: 600
.search-result,
.algolia-hit-item-content
margin: 0 0 8px
word-break: break-all
font-size: .9em
.ais-Pagination
margin: 15px 0 0
padding: 0
text-align: center
.ais-Pagination-list
display: flex
flex-wrap: wrap
justify-content: center
align-items: center
margin: 0
padding: 0
list-style: none
gap: 6px
.ais-Pagination-item
display: flex
padding: 0
&:not(.ais-Pagination-item--selected):not(.ais-Pagination-item--ellipsis):not(.ais-Pagination-item--disabled)
.ais-Pagination-link:hover
background: var(--btn-hover-color)
transform: translateY(-1px)
.ais-Pagination-link
display: inline-flex
justify-content: center
align-items: center
padding: 4px 8px
min-width: 28px
height: 28px
border-radius: 6px
background: var(--btn-bg)
color: var(--btn-color)
transition: all .2s ease
&.ais-Pagination-link--disabled
opacity: .3
cursor: not-allowed
i
font-size: 12px
.ais-Pagination-item--selected
.ais-Pagination-link
background: $theme-paginator-color
font-weight: 600
cursor: default
.ais-Pagination-item--ellipsis
.ais-Pagination-link
padding: 4px 2px
border: none
background: transparent
color: var(--text-color)
cursor: default
&:hover
background: transparent
transform: none
.ais-Pagination-item--disabled
.ais-Pagination-link
opacity: .4
+maxWidth768()
.ais-Pagination-list
gap: 4px
hr
margin: 15px auto
@extend .custom-hr
@@ -50,6 +202,38 @@
display: none
background: rgba($dark-black, .6)
.search-result-stats,
.ais-Stats-text
margin: 15px 0 0
color: var(--text-color)
text-align: center
font-size: .9em
.search-keyword
background: transparent
color: $search-keyword-highlight
font-weight: 600
.search-loading
display: flex
justify-content: center
align-items: center
padding: 20px
color: var(--text-color)
&::before
width: 16px
height: 16px
border: 2px solid var(--text-color)
border-top-color: transparent
border-radius: 50%
content: ''
animation: spin 1s linear infinite
@keyframes spin
to
transform: rotate(360deg)
if hexo-config('search.use') == 'algolia_search'
@require 'algolia'
else if hexo-config('search.use') == 'local_search'

View File

@@ -1,57 +1,18 @@
#local-search
.search-dialog
.local-search-box
margin: 0 auto
max-width: 100%
width: 100%
input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
-webkit-appearance: none
.search-wrap
display: none
.local-search-hit-item
margin-left: 24px
padding-left: 3px
line-height: 1.8
&::marker
color: $search-color
font-weight: bold
font-style: italic
a
color: var(--search-a-color)
&:hover
color: $search-color
.search-result-title
font-weight: 600
.search-result
margin: 0 0 8px
word-break: break-all
font-size: .9em
.search-result-list
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 180px)
+maxWidth768()
max-height: calc(var(--search-height) - 190px) !important
if hexo-config('search.local_search.pagination.enable')
min-height: calc(var(--search-height) - 255px) !important
else
max-height: calc(var(--search-height) - 200px) !important
.search-keyword
background: transparent
color: $search-keyword-highlight
#local-search-stats
.search-result-stats
text-align: left
.search-keyword
font-weight: 600
#loading-database ~ *
visibility: hidden

View File

@@ -7,21 +7,20 @@
display: inline-block
margin: 0 4px 6px
padding: 0 15px
background-color: var(--btn-beautify-color, $btn-default-color)
color: $btn-color
background-color: var(--btn-beautify-color, var(--btn-default-color))
color: var(--btn-color, $btn-color)
vertical-align: top
line-height: 2
addBorderRadius()
@extend .btn-effects
for $type in $color-types
&.{$type}
--btn-beautify-color: lookup('$tagsP-' + $type + '-color')
--btn-beautify-color: unquote('var(--tags-' + $type + '-color)')
&:hover
background-color: var(--btn-hover-color)
i + span
margin-left: 6px
&:not(.block) + .btn-beautify:not(.block)
margin: 0 4px 20px
@@ -39,18 +38,26 @@
&.larger
padding: 6px 15px
&:hover
text-decoration: none
@extend .btn-effects-large
&.outline
border: 1px solid transparent
border-color: var(--btn-beautify-color, $btn-default-color)
border-color: var(--btn-beautify-color, var(--btn-default-color))
background-color: transparent
color: var(--btn-beautify-color, $btn-default-color)
color: var(--btn-beautify-color, var(--btn-default-color))
i,
span
transition: color .3s cubic-bezier(.4, 0, .2, 1)
&::before
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, .1), transparent)
&:hover
background-color: var(--btn-beautify-color, $btn-default-color)
border-color: var(--btn-beautify-color, var(--btn-default-color))
background-color: var(--btn-beautify-color, var(--btn-default-color))
color: var(--btn-color) !important
&:hover
color: white !important
i,
span
color: var(--btn-color)

View File

@@ -7,6 +7,7 @@
background: $tag-hide-bg
color: var(--white)
addBorderRadius()
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)

View File

@@ -1,11 +1,11 @@
.hl-label
padding: 2px 4px
color: $btn-color
color: var(--btn-color, $btn-color)
addBorderRadius(3)
&.default
background-color: $btn-default-color
background-color: var(--btn-default-color)
for $type in $color-types
&.{$type}
background-color: lookup('$tagsP-' + $type + '-color')
background-color: unquote('var(--tags-' + $type + '-color)')

View File

@@ -19,32 +19,32 @@
for $type in $color-types
&.{$type}
&:not(.disabled)
border-left-color: lookup('$tagsP-' + $type + '-color') !important
border-left-color: unquote('var(--tags-' + $type + '-color)') !important
&.modern
border-left-color: transparent !important
color: lookup('$tagsP-' + $type + '-color')
color: unquote('var(--tags-' + $type + '-color)')
&:not(.simple)
background: lighten(lookup('$tagsP-' + $type + '-color'), 85%) !important
background: unquote('var(--tags-' + $type + '-color-lighten)')
& > .note-icon
color: lookup('$tagsP-' + $type + '-color')
color: unquote('var(--tags-' + $type + '-color)')
&.simple
border: 1px solid #EEEEEE
border: 1px solid var(--note-default-border)
border-left-width: 5px
&.modern
border: 1px solid transparent !important
background-color: #f5f5f5
color: $font-black
background-color: var(--note-modern-default-bg)
color: var(--note-modern-default-text)
&.flat
border: initial
border-left: 5px solid #EEEEEE
background-color: lighten(#EEEEEE, 65%)
color: $font-black
border-left: 5px solid var(--note-default-border)
background-color: var(--note-default-bg)
color: var(--note-default-text)
h2,
h3,
@@ -90,29 +90,30 @@
for $type in $note-types
&.{$type}
&.flat
background: lookup('$note-' + $type + '-bg')
background: unquote('var(--note-' + $type + '-bg)')
color: var(--font-color)
&.modern
border-color: lookup('$note-modern-' + $type + '-border')
background: lookup('$note-modern-' + $type + '-bg')
color: lookup('$note-modern-' + $type + '-text')
border-color: unquote('var(--note-modern-' + $type + '-border)') !important
background: unquote('var(--note-modern-' + $type + '-bg)')
color: unquote('var(--note-modern-' + $type + '-text)')
a
&:not(.btn)
color: lookup('$note-modern-' + $type + '-text')
color: unquote('var(--note-modern-' + $type + '-text)')
&:hover
color: lookup('$note-modern-' + $type + '-hover')
color: unquote('var(--note-modern-' + $type + '-hover)')
&:not(.modern)
border-left-color: lookup('$note-' + $type + '-border')
border-left-color: unquote('var(--note-' + $type + '-border)')
h2,
h3,
h4,
h5,
h6
color: lookup('$note-' + $type + '-text')
color: unquote('var(--note-' + $type + '-text)')
if $note-icons
&:not(.no-icon)
@@ -121,4 +122,4 @@
&:not(.modern)
&::before
color: lookup('$note-' + $type + '-text')
color: unquote('var(--note-' + $type + '-text)')

View File

@@ -13,11 +13,9 @@ $code-foreground = $themeColorEnable && hexo-config('theme_color.code_foreground
$code-background = $themeColorEnable && hexo-config('theme_color.code_background') ? convert(hexo-config('theme_color.code_background')) : rgba(27, 31, 35, .05)
$theme-toc-color = $themeColorEnable && hexo-config('theme_color.toc_color') ? convert(hexo-config('theme_color.toc_color')) : $strong-cyan
// font
$chineseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
$default-font-family = -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', $chineseFont, sans-serif
$default-code-font = consolas, Menlo, monospace, 'PingFang SC', $chineseFont, sans-serif
$font-family = hexo-config('font.font_family') ? unquote(hexo-config('font.font_family')) : $default-font-family
$code-font-family = hexo-config('font.code_font_family') ? unquote(hexo-config('font.code_font_family')) : $default-code-font
$site-name-font = hexo-config('blog_title_font.font_family') && unquote(hexo-config('blog_title_font.font_family'))
@@ -184,3 +182,52 @@ $tab-to-top-color = #99a9bf
$tab-to-top-hover-color = $theme-color
// Tag Plugins - timeline
$timeline-default-color = $theme-color
// note - darkmode
// Default
$note-dark-default-border = #5a5a5a
$note-dark-default-bg = #2b2b2b
$note-dark-default-text = #b3b3b3
$note-dark-modern-default-border = #9a9a9a
$note-dark-modern-default-bg = #353535
$note-dark-modern-default-text = #c4c4c4
$note-dark-modern-default-hover = #d0d0d0
// Primary
$note-dark-primary-border = #5935a1
$note-dark-primary-bg = #2e1c3e
$note-dark-primary-text = #a47dd4
$note-dark-modern-primary-border = #9985cc
$note-dark-modern-primary-bg = #3c2d4c
$note-dark-modern-primary-text = #b693e6
$note-dark-modern-primary-hover = #c9a8f0
// Info
$note-dark-info-border = #346fa2
$note-dark-info-bg = #1f2e3b
$note-dark-info-text = #7bb3db
$note-dark-modern-info-border = #7ca8b5
$note-dark-modern-info-bg = #2b3c44
$note-dark-modern-info-text = #8fc6e0
$note-dark-modern-info-hover = #a3d4ea
// Success
$note-dark-success-border = #4a944a
$note-dark-success-bg = #202e20
$note-dark-success-text = #82c682
$note-dark-modern-success-border = #8bb087
$note-dark-modern-success-bg = #2c3d2c
$note-dark-modern-success-text = #96d196
$note-dark-modern-success-hover = #a8dca8
// Warning
$note-dark-warning-border = #c08a3e
$note-dark-warning-bg = #3e301f
$note-dark-warning-text = #e6ba6b
$note-dark-modern-warning-border = #b8a285
$note-dark-modern-warning-bg = #4b3c2b
$note-dark-modern-warning-text = #d4b373
$note-dark-modern-warning-hover = #e0c080
// Danger
$note-dark-danger-border = #b34440
$note-dark-danger-bg = #3b201f
$note-dark-danger-text = #e67572
$note-dark-modern-danger-border = #c7898c
$note-dark-modern-danger-bg = #4d2b2e
$note-dark-modern-danger-text = #d98b8e
$note-dark-modern-danger-hover = #e59fa2

View File

@@ -698,7 +698,7 @@ document.addEventListener('DOMContentLoaded', () => {
const addCopyright = () => {
const { limitCount, languages } = GLOBAL_CONFIG.copyright
const handleCopy = (e) => {
const handleCopy = e => {
e.preventDefault()
const copyFont = window.getSelection(0).toString()
let textFont = copyFont
@@ -893,27 +893,6 @@ document.addEventListener('DOMContentLoaded', () => {
tabsFn()
}
/**
* 自己写的,实现功能切换类别表
*/
const setCategoryBarActive = () => {
const categoryBar = document.querySelector("#category-bar");
const currentPath = decodeURIComponent(window.location.pathname);
const isHomePage = currentPath === GLOBAL_CONFIG.root;
if (categoryBar) {
const categoryItems = categoryBar.querySelectorAll(".category-bar-item");
categoryItems.forEach(item => item.classList.remove("select"));
const activeItemId = isHomePage ? "category-bar-home" : currentPath.split("/").slice(-2, -1)[0];
const activeItem = document.getElementById(activeItemId);
if (activeItem) {
activeItem.classList.add("select");
}
}
};
const refreshFn = () => {
initAdjust()
justifiedIndexPostUI()
@@ -926,7 +905,6 @@ document.addEventListener('DOMContentLoaded', () => {
GLOBAL_CONFIG.runtime && addRuntime()
addLastPushDate()
toggleCardCategory()
setCategoryBarActive()
}
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()

View File

@@ -26,7 +26,12 @@ window.addEventListener('load', () => {
const openSearch = () => {
btf.overflowPaddingR.add()
animateElements(true)
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
showLoading(false)
setTimeout(() => {
const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
if (searchInput) searchInput.focus()
}, 100)
const handleEscape = event => {
if (event.code === 'Escape') {
@@ -55,9 +60,38 @@ window.addEventListener('load', () => {
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
}
const cutContent = (content) => {
const cutContent = content => {
if (!content) return ''
const firstOccur = content.indexOf('<mark>')
let contentStr = ''
if (typeof content === 'string') {
contentStr = content.trim()
} else if (typeof content === 'object') {
if (content.value !== undefined) {
contentStr = String(content.value).trim()
if (!contentStr) return ''
} else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) {
return ''
} else {
try {
contentStr = JSON.stringify(content).trim()
if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') {
return ''
}
} catch (e) {
return ''
}
}
} else if (content.toString && typeof content.toString === 'function') {
contentStr = content.toString().trim()
if (contentStr === '[object Object]' || contentStr === '[object Array]') {
return ''
}
} else {
return ''
}
const firstOccur = contentStr.indexOf('<mark>')
let start = firstOccur - 30
let end = firstOccur + 120
let pre = ''
@@ -70,94 +104,454 @@ window.addEventListener('load', () => {
pre = '...'
}
if (end > content.length) {
end = content.length
if (end > contentStr.length) {
end = contentStr.length
} else {
post = '...'
}
return `${pre}${content.substring(start, end)}${post}`
// Ensure we don't cut off HTML tags in the middle
let substr = contentStr.substring(start, end)
// Handle tag completeness
// Check for incomplete opening tags at the beginning
const firstCloseBracket = 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)) {
substr = substr.substring(firstCloseBracket + 1)
}
const disableDiv = [
document.getElementById('algolia-hits'),
document.getElementById('algolia-pagination'),
document.querySelector('#algolia-info .algolia-stats')
]
// Check for incomplete closing tags at the end
const lastOpenBracket = substr.lastIndexOf('<')
const lastCloseBracket = substr.lastIndexOf('>')
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
const search = instantsearch({
indexName,
searchClient: searchClient(appId, apiKey),
searchFunction (helper) {
disableDiv.forEach(item => {
item.style.display = helper.state.query ? '' : 'none'
})
if (helper.state.query) helper.search()
// If there's an opening bracket after the last closing bracket, we've cut a tag
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
substr = substr.substring(0, lastOpenBracket)
}
// Balance tags in the substring
const tagStack = []
let balancedStr = ''
let i = 0
while (i < substr.length) {
if (substr[i] === '<') {
// Check if it's a closing tag
if (substr[i + 1] === '/') {
const closeTagEnd = substr.indexOf('>', i)
if (closeTagEnd !== -1) {
const closeTagName = substr.substring(i + 2, closeTagEnd)
// Remove matching opening tag from stack
for (let j = tagStack.length - 1; j >= 0; j--) {
if (tagStack[j] === closeTagName) {
tagStack.splice(j, 1)
break
}
}
balancedStr += substr.substring(i, closeTagEnd + 1)
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)
balancedStr += substr.substring(i, tagEnd + 1)
i = tagEnd + 1
continue
}
}
}
balancedStr += substr[i]
i++
}
// Close any unclosed tags
while (tagStack.length > 0) {
const tagName = tagStack.pop()
balancedStr += `</${tagName}>`
}
// If we removed content from the beginning, add prefix
if (start > 0 || pre) {
const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0)
const actualFirstMark = contentStr.indexOf('<mark>', start > 0 ? start - 30 : 0)
if (actualFirstOpenBracket !== -1 &&
(actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
pre = '...'
}
}
substr = balancedStr
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
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
} else if (typeof window.algoliasearch === 'function') {
searchClient = window.algoliasearch(appId, apiKey)
} else {
return console.error('Algolia search client not found!')
}
if (!searchClient) {
return console.error('Failed to initialize Algolia search client')
}
// Search state
let currentQuery = ''
// 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 => {
elements.pagination.style.display = hasResults ? '' : 'none'
elements.stats.style.display = hasResults ? '' : 'none'
}
// Render search results
const renderHits = (hits, query, page = 0) => {
if (hits.length === 0 && query) {
elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
elements.hitsEmpty.style.display = ''
elements.hitsWrapper.style.display = 'none'
elements.stats.style.display = 'none'
return
}
elements.hitsEmpty.style.display = 'none'
const hitsHTML = hits.map((hit, index) => {
const itemNumber = page * hitsPerPage + index + 1
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
const result = hit._highlightResult || hit
// Content extraction
let content = ''
try {
if (result.contentStripTruncate) {
content = cutContent(result.contentStripTruncate)
} 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
let title = 'no-title'
try {
if (result.title) {
title = extractHighlightValue(result.title) || 'no-title'
} else if (hit.title) {
title = extractHighlightValue(hit.title) || 'no-title'
}
if (!title || title === 'no-title') {
if (typeof hit.title === 'string' && hit.title.trim()) {
title = hit.title.trim()
} else if (hit.title && typeof hit.title === 'object' && hit.title.value) {
title = String(hit.title.value).trim() || 'no-title'
} else {
title = 'no-title'
}
}
} catch (error) {
title = 'no-title'
}
})
const widgets = [
instantsearch.widgets.configure({ hitsPerPage }),
instantsearch.widgets.searchBox({
container: '#algolia-search-input',
showReset: false,
showSubmit: false,
placeholder: languages.input_placeholder,
showLoadingIndicator: true
}),
instantsearch.widgets.hits({
container: '#algolia-hits',
templates: {
item (data) {
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
const result = data._highlightResult
const content = result.contentStripTruncate
? cutContent(result.contentStripTruncate.value)
: result.contentStrip
? cutContent(result.contentStrip.value)
: result.content
? cutContent(result.content.value)
: ''
return `
<li class="ais-Hits-item" value="${itemNumber}">
<a href="${link}" class="algolia-hit-item-link">
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
<span class="algolia-hits-item-title">${title}</span>
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
</a>`
},
empty (data) {
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
</a>
</li>`
}).join('')
elements.hitsList.innerHTML = hitsHTML
elements.hitsWrapper.style.display = query ? '' : 'none'
if (hits.length > 0) {
elements.stats.style.display = ''
}
}
}),
instantsearch.widgets.stats({
container: '#algolia-info > .algolia-stats',
templates: {
text (data) {
// Render pagination
const renderPagination = (page, nbPages) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<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) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
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 {
pagesHTML += `
<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
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<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) {
elements.paginationList.innerHTML = `
<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>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</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>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
elements.pagination.style.display = currentQuery ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render statistics
const renderStats = (nbHits, processingTimeMS, query) => {
if (query) {
const stats = languages.hits_stats
.replace(/\$\{hits}/, data.nbHits)
.replace(/\$\{time}/, data.processingTimeMS)
return `<hr>${stats}`
.replace(/\$\{hits}/, nbHits)
.replace(/\$\{time}/, processingTimeMS)
elements.stats.innerHTML = `<hr>${stats}`
elements.stats.style.display = ''
} else {
elements.stats.style.display = 'none'
}
}
}),
instantsearch.widgets.poweredBy({
container: '#algolia-info > .algolia-poweredBy'
}),
instantsearch.widgets.pagination({
container: '#algolia-pagination',
totalPages: 5,
templates: {
first: '<i class="fas fa-angle-double-left"></i>',
last: '<i class="fas fa-angle-double-right"></i>',
previous: '<i class="fas fa-angle-left"></i>',
next: '<i class="fas fa-angle-right"></i>'
// Perform search
const performSearch = async (query, page = 0) => {
if (!query.trim()) {
currentQuery = ''
renderHits([], '', 0)
renderPagination(0, 0)
renderStats(0, 0, '')
toggleResultsVisibility(false)
return
}
showLoading(true)
currentQuery = query
try {
let result
if (searchClient && typeof searchClient.search === 'function') {
// v5 multi-index search
const searchResult = await searchClient.search([{
indexName,
query,
params: {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
}
}])
result = searchResult.results[0]
} else if (searchClient && typeof searchClient.initIndex === 'function') {
// v4 single-index search
const index = searchClient.initIndex(indexName)
result = await index.search(query, {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
})
} else {
throw new Error('Algolia: No compatible search method available')
}
renderHits(result.hits || [], query, page)
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
renderPagination(page, actualNbPages)
renderStats(result.nbHits || 0, result.processingTimeMS || 0, query)
const hasResults = result.hits && result.hits.length > 0
toggleResultsVisibility(hasResults)
// Refresh Pjax links
if (window.pjax) {
window.pjax.refresh(document.getElementById('algolia-hits'))
}
} catch (error) {
console.error('Algolia search error:', error)
renderHits([], query, page)
renderPagination(0, 0)
renderStats(0, 0, query)
} finally {
showLoading(false)
}
}
// Debounced search
let searchTimeout
const debouncedSearch = (query, delay = 300) => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => performSearch(query), delay)
}
// Initialize search box and events
const initializeSearch = () => {
showLoading(false)
if (elements.searchInput) {
elements.searchInput.addEventListener('input', e => {
const query = e.target.value
debouncedSearch(query)
})
}
const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
if (searchForm) {
searchForm.addEventListener('submit', e => {
e.preventDefault()
const query = elements.searchInput.value
performSearch(query)
})
}
// Pagination event delegation
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentQuery) {
performSearch(currentQuery, page)
}
}
})
]
search.addWidgets(widgets)
search.start()
// Initial state
toggleResultsVisibility(false)
}
// Initialize
initializeSearch()
searchClickFn()
searchFnOnce()
@@ -165,10 +559,4 @@ window.addEventListener('load', () => {
if (!btf.isHidden($searchMask)) closeSearch()
searchClickFn()
})
if (window.pjax) {
search.on('render', () => {
window.pjax.refresh(document.getElementById('algolia-hits'))
})
}
})

View File

@@ -156,10 +156,10 @@ class LocalSearch {
}
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({
item: resultItem,
id: resultItems.length,
@@ -236,40 +236,229 @@ class LocalSearch {
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
const enablePagination = pagination && pagination.enable
const localSearch = new LocalSearch({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
const input = document.querySelector('.local-search-input input')
const statsItem = document.getElementById('local-search-stats')
const $loadingStatus = document.getElementById('loading-status')
const isXml = !path.endsWith('json')
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
// Pagination variables (only initialize if pagination is enabled)
let currentPage = 0
const hitsPerPage = pagination.hitsPerPage || 10
let currentResultItems = []
if (!enablePagination) {
// If pagination is disabled, we don't need these variables
currentPage = undefined
currentResultItems = undefined
}
if (keywords.length === 1 && keywords[0] === '') {
// 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
const toggleResultsVisibility = hasResults => {
if (enablePagination) {
elements.pagination.style.display = hasResults ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render search results for current page
const renderResults = (searchText, resultItems) => {
const container = document.getElementById('local-search-results')
// Determine items to display based on pagination mode
const itemsToDisplay = enablePagination
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
: resultItems
// Handle empty page in pagination mode
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
currentPage = 0
renderResults(searchText, resultItems)
return
}
// Add numbering to items
const numberedItems = itemsToDisplay.map((result, index) => {
const itemNumber = enablePagination
? currentPage * hitsPerPage + index + 1
: index + 1
return result.item.replace(
'<li class="local-search-hit-item">',
`<li class="local-search-hit-item" value="${itemNumber}">`
)
})
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
// Update stats
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
// Handle pagination
if (enablePagination) {
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
renderPagination(currentPage, nbPages, searchText)
}
const hasResults = resultItems.length > 0
toggleResultsVisibility(hasResults)
window.pjax && window.pjax.refresh(container)
}
// Render pagination
const renderPagination = (page, nbPages, query) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<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) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
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 {
pagesHTML += `
<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
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<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) {
elements.paginationList.innerHTML = `
<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>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</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>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
} else {
elements.pagination.style.display = 'none'
}
}
// Clear search results and stats
const clearSearchResults = () => {
const container = document.getElementById('local-search-results')
container.textContent = ''
statsItem.textContent = ''
} else if (resultItems.length === 0) {
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
// Show no results message
const showNoResults = searchText => {
const container = document.getElementById('local-search-results')
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.hidden = false
const keywords = searchText.split(/[-\s]+/)
let resultItems = []
if (searchText.length > 0) {
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
clearSearchResults()
} else if (resultItems.length === 0) {
showNoResults(searchText)
} else {
// Sort results by relevance
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
@@ -279,14 +468,14 @@ window.addEventListener('load', () => {
return right.id - left.id
})
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
if (enablePagination) {
currentResultItems = resultItems
currentPage = 0
}
renderResults(searchText, resultItems)
}
$loadingStatus.textContent = ''
$loadingStatus.hidden = true
}
let loadFlag = false
@@ -340,11 +529,29 @@ window.addEventListener('load', () => {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
// Pagination event delegation - only add if pagination is enabled
if (enablePagination) {
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentResultItems.length > 0) {
currentPage = page
renderResults(input.value.trim().toLowerCase(), currentResultItems)
}
}
})
}
// Initial state
toggleResultsVisibility(false)
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.nextElementSibling.style.visibility = 'visible'
$loadDataItem.remove()
})

File diff suppressed because one or more lines are too long

View File

@@ -106,7 +106,7 @@
loadComment: (dom, callback) => {
if ('IntersectionObserver' in window) {
const observerItem = new IntersectionObserver((entries) => {
const observerItem = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
callback()
observerItem.disconnect()
@@ -249,7 +249,7 @@
'rotateCW',
'flipX',
'flipY',
"reset"
'reset'
],
right: ['autoplay', 'thumbs', 'close']
}