Css
/* =========================================================
UNIVERSAL SINGLE POST ENHANCEMENT
Article typography + responsive images + responsive tables
+ floating left TOC (desktop) + mobile TOC drawer
Version: 1.0
========================================================= */
body.ws-single-post-enhanced {
--ws-primary: #1C6BAE;
--ws-primary-dark: #155487;
--ws-primary-soft: #eef6fb;
--ws-heading: #17354c;
--ws-text: #344757;
--ws-muted: #687986;
--ws-border: #dce5eb;
--ws-border-light: #e9eff3;
--ws-bg: #ffffff;
--ws-bg-soft: #f7fafc;
--ws-radius: 8px;
color: var(--ws-text);
}
/* =========================================================
1. ARTICLE TYPOGRAPHY
========================================================= */
body.ws-single-post-enhanced .ws-article-content {
color: var(--ws-text);
}
body.ws-single-post-enhanced .ws-article-content p {
margin: 0 0 20px !important;
color: var(--ws-text) !important;
font-size: 16px !important;
font-weight: 400 !important;
line-height: 1.95 !important;
letter-spacing: .015em !important;
}
body.ws-single-post-enhanced .ws-article-content p strong {
color: #173b59;
font-weight: 650;
}
/* =========================================================
2. H2
========================================================= */
body.ws-single-post-enhanced .ws-article-content h2 {
display: block !important;
margin: 48px 0 23px !important;
padding: 13px 17px 13px 18px !important;
color: var(--ws-heading) !important;
font-size: 24px !important;
font-weight: 700 !important;
line-height: 1.55 !important;
letter-spacing: .01em !important;
background:
linear-gradient(
90deg,
rgba(28, 107, 174, .085),
rgba(28, 107, 174, .018) 75%,
transparent
) !important;
border-top: 0 !important;
border-right: 0 !important;
border-bottom: 1px solid #e7eef3 !important;
border-left: 4px solid var(--ws-primary) !important;
border-radius: 0 6px 6px 0 !important;
scroll-margin-top: 100px;
}
body.ws-single-post-enhanced .ws-article-content h2:first-of-type {
margin-top: 38px !important;
}
/* =========================================================
3. H3
========================================================= */
body.ws-single-post-enhanced .ws-article-content h3 {
position: relative !important;
display: block !important;
margin: 33px 0 16px !important;
padding: 0 0 10px 16px !important;
color: var(--ws-heading) !important;
font-size: 19px !important;
font-weight: 700 !important;
line-height: 1.6 !important;
border: 0 !important;
border-bottom: 1px solid var(--ws-border) !important;
scroll-margin-top: 100px;
}
body.ws-single-post-enhanced .ws-article-content h3::before {
content: "" !important;
position: absolute !important;
left: 0 !important;
top: .7em !important;
width: 6px !important;
height: 6px !important;
border-radius: 50% !important;
background: var(--ws-primary) !important;
}
/* =========================================================
4. LINKS
========================================================= */
body.ws-single-post-enhanced .ws-article-content a {
color: var(--ws-primary);
font-weight: 500;
text-decoration: underline;
text-decoration-color: rgba(28, 107, 174, .28);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition:
color .18s ease,
text-decoration-color .18s ease;
}
body.ws-single-post-enhanced .ws-article-content a:hover {
color: var(--ws-primary-dark);
text-decoration-color: var(--ws-primary-dark);
}
/* =========================================================
5. LISTS
========================================================= */
body.ws-single-post-enhanced .ws-article-content ul,
body.ws-single-post-enhanced .ws-article-content ol {
margin: 18px 0 27px !important;
padding:
18px
22px
18px
43px !important;
background: #f8fafc !important;
border: 1px solid #e7edf1 !important;
border-radius: 7px !important;
}
body.ws-single-post-enhanced .ws-article-content ul li,
body.ws-single-post-enhanced .ws-article-content ol li {
margin: 7px 0 !important;
color: #3c4f5f !important;
font-size: 15px !important;
line-height: 1.8 !important;
}
body.ws-single-post-enhanced .ws-article-content ul li::marker,
body.ws-single-post-enhanced .ws-article-content ol li::marker {
color: var(--ws-primary) !important;
font-weight: 700 !important;
}
/* =========================================================
6. ARTICLE IMAGES
========================================================= */
body.ws-single-post-enhanced .ws-article-content .ws-article-image {
display: block !important;
width: min(100%, 680px) !important;
max-width: 680px !important;
margin: 30px auto 35px !important;
padding: 0 !important;
text-align: center !important;
float: none !important;
}
body.ws-single-post-enhanced .ws-article-content .ws-article-image img {
display: block !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin: 0 auto !important;
border: 0 !important;
border-radius: 8px !important;
object-fit: contain !important;
box-shadow:
0 3px 18px rgba(24, 53, 74, .075) !important;
}
body.ws-single-post-enhanced .ws-article-content img.ws-article-inline-image {
display: block !important;
width: min(100%, 680px) !important;
max-width: 680px !important;
height: auto !important;
margin: 30px auto 35px !important;
border-radius: 8px !important;
object-fit: contain !important;
box-shadow:
0 3px 18px rgba(24, 53, 74, .075) !important;
}
/* =========================================================
7. HERO / FEATURED IMAGE
========================================================= */
body.ws-single-post-enhanced .ws-article-hero {
width: min(100%, 760px) !important;
max-width: 760px !important;
margin-left: auto !important;
margin-right: auto !important;
margin-bottom: 30px !important;
}
body.ws-single-post-enhanced .ws-article-hero img,
body.ws-single-post-enhanced img.ws-article-hero {
display: block !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
border-radius: 9px !important;
box-shadow:
0 4px 20px rgba(24, 53, 74, .07) !important;
}
/* =========================================================
8. BLOCKQUOTE
========================================================= */
body.ws-single-post-enhanced .ws-article-content blockquote {
position: relative !important;
margin: 28px 0 !important;
padding:
21px
24px
21px
28px !important;
background: #f1f7fb !important;
border: 0 !important;
border-left: 4px solid var(--ws-primary) !important;
border-radius: 0 7px 7px 0 !important;
}
body.ws-single-post-enhanced .ws-article-content blockquote p {
margin: 0 !important;
color: #24455d !important;
font-size: 15.5px !important;
line-height: 1.85 !important;
}
/* =========================================================
9. RESPONSIVE TABLE WRAPPER
========================================================= */
body.ws-single-post-enhanced .ws-article-content .ws-table-scroll {
display: block !important;
position: relative !important;
width: 100% !important;
max-width: 100% !important;
margin: 27px 0 38px !important;
padding: 0 !important;
overflow-x: auto !important;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
border: 1px solid var(--ws-border) !important;
border-radius: 8px !important;
background: #fff !important;
box-shadow:
0 3px 16px rgba(24, 53, 74, .055) !important;
}
body.ws-single-post-enhanced .ws-table-scroll::-webkit-scrollbar {
height: 7px !important;
}
body.ws-single-post-enhanced .ws-table-scroll::-webkit-scrollbar-track {
background: #eef3f6 !important;
}
body.ws-single-post-enhanced .ws-table-scroll::-webkit-scrollbar-thumb {
background: #a8c3d8 !important;
border-radius: 10px !important;
}
body.ws-single-post-enhanced .ws-table-scroll::-webkit-scrollbar-thumb:hover {
background: var(--ws-primary) !important;
}
/* =========================================================
10. TABLE
========================================================= */
body.ws-single-post-enhanced .ws-article-content .ws-article-table {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-collapse: separate !important;
border-spacing: 0 !important;
table-layout: auto !important;
background: #fff !important;
color: var(--ws-text) !important;
font-size: 14px !important;
line-height: 1.65 !important;
}
/* =========================================================
11. TABLE HEADER
========================================================= */
body.ws-single-post-enhanced .ws-article-table thead,
body.ws-single-post-enhanced .ws-article-table thead tr {
background: var(--ws-primary) !important;
}
body.ws-single-post-enhanced .ws-article-table thead th {
padding: 13px 15px !important;
color: #fff !important;
background: var(--ws-primary) !important;
border: 0 !important;
border-right:
1px solid rgba(255,255,255,.18) !important;
border-bottom: 0 !important;
font-size: 13.5px !important;
font-weight: 650 !important;
line-height: 1.55 !important;
text-align: left !important;
vertical-align: middle !important;
white-space: nowrap !important;
}
body.ws-single-post-enhanced .ws-article-table thead th:last-child {
border-right: 0 !important;
}
/* =========================================================
12. TABLE CELLS
========================================================= */
body.ws-single-post-enhanced .ws-article-table tbody td {
padding: 13px 15px !important;
color: #3c4d5b !important;
background: #fff !important;
border: 0 !important;
border-right:
1px solid var(--ws-border-light) !important;
border-bottom:
1px solid var(--ws-border-light) !important;
text-align: left !important;
vertical-align: top !important;
word-break: normal !important;
}
body.ws-single-post-enhanced .ws-article-table tbody td:last-child {
border-right: 0 !important;
}
body.ws-single-post-enhanced .ws-article-table tbody tr:last-child td {
border-bottom: 0 !important;
}
body.ws-single-post-enhanced
.ws-article-table
tbody
tr:nth-child(even)
td {
background: #f7fafc !important;
}
body.ws-single-post-enhanced
.ws-article-table
tbody
td:first-child {
color: #24455e !important;
font-weight: 600 !important;
background: #f4f8fb !important;
}
body.ws-single-post-enhanced
.ws-article-table
tbody
tr:nth-child(even)
td:first-child {
background: #eef5f9 !important;
}
@media (hover:hover) {
body.ws-single-post-enhanced
.ws-article-table
tbody
tr:hover
td {
background: #edf6fc !important;
}
}
/* =========================================================
13. META / AUTHOR (OPTIONAL COMMON WORDPRESS STYLES)
========================================================= */
body.ws-single-post-enhanced .post-meta {
margin-bottom: 18px;
color: #788796;
font-size: 12.5px;
}
body.ws-single-post-enhanced .post-meta a {
color: #637889;
}
body.ws-single-post-enhanced .post-author {
margin-top: 40px;
padding: 24px;
border: 1px solid var(--ws-border);
border-radius: 10px;
background: #fafcfd;
}
/* =========================================================
14. DESKTOP FLOATING LEFT TOC
========================================================= */
#ws-floating-toc {
position: fixed;
left: 16px;
top: 50%;
z-index: 90;
width: 250px;
max-height: 64vh;
transform: translateY(-50%);
background: #fff;
border: 1px solid var(--ws-border);
border-radius: 10px;
box-shadow:
0 8px 28px rgba(23, 54, 77, .09);
overflow: hidden;
transition:
width .25s ease,
box-shadow .25s ease;
}
/* -------------------------
TOC Header
------------------------- */
.ws-floating-toc-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 50px;
padding: 0 11px 0 16px;
background: #f8fbfd;
border-bottom: 1px solid #e7eef3;
}
.ws-floating-toc-title {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
color: #18374f;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
cursor: default;
}
.ws-floating-toc-title::before {
content: "";
width: 4px;
height: 17px;
flex: 0 0 4px;
border-radius: 3px;
background: var(--ws-primary);
}
.ws-floating-toc-toggle {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 30px;
width: 30px;
height: 30px;
padding: 0;
border: 0;
border-radius: 5px;
background: transparent;
color: #617789;
cursor: pointer;
transition:
color .18s ease,
background .18s ease;
}
.ws-floating-toc-toggle:hover {
color: var(--ws-primary);
background: #eaf3f9;
}
.ws-floating-toc-toggle::before {
content: "";
width: 7px;
height: 7px;
border-left: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg);
transition: transform .2s ease;
}
/* -------------------------
Progress
------------------------- */
.ws-floating-toc-progress {
height: 2px;
background: #edf2f5;
}
.ws-floating-toc-progress-bar {
width: 0;
height: 100%;
background: var(--ws-primary);
transition: width .08s linear;
}
/* -------------------------
TOC Body
------------------------- */
.ws-floating-toc-body {
max-height: calc(64vh - 54px);
padding: 9px 8px 12px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
.ws-floating-toc-body::-webkit-scrollbar {
width: 4px;
}
.ws-floating-toc-body::-webkit-scrollbar-track {
background: transparent;
}
.ws-floating-toc-body::-webkit-scrollbar-thumb {
background: #c7d5df;
border-radius: 5px;
}
/* -------------------------
TOC List
------------------------- */
.ws-floating-toc-list {
list-style: none !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: 0 !important;
}
.ws-floating-toc-item {
margin: 1px 0 !important;
padding: 0 !important;
}
.ws-floating-toc-link {
position: relative;
display: block;
padding: 7px 9px 7px 12px;
color: #42576a !important;
border-radius: 5px;
font-size: 12.5px;
line-height: 1.55;
text-decoration: none !important;
word-break: break-word;
transition:
background .15s ease,
color .15s ease;
}
.ws-floating-toc-link:hover {
color: var(--ws-primary) !important;
background: #f0f7fb;
}
.ws-floating-toc-item.level-3 .ws-floating-toc-link {
padding-left: 27px;
color: #718191 !important;
font-size: 11.8px;
}
.ws-floating-toc-item.level-3 .ws-floating-toc-link::before {
content: "";
position: absolute;
left: 15px;
top: 15px;
width: 4px;
height: 4px;
border-radius: 50%;
background: #b8c7d2;
}
.ws-floating-toc-link.is-active {
color: var(--ws-primary) !important;
background: #edf6fc;
font-weight: 600;
}
.ws-floating-toc-link.is-active::after {
content: "";
position: absolute;
top: 7px;
bottom: 7px;
left: 0;
width: 3px;
background: var(--ws-primary);
border-radius: 3px;
}
/* =========================================================
15. COLLAPSED DESKTOP TOC
========================================================= */
#ws-floating-toc.is-collapsed {
width: 46px;
max-height: none;
border-radius: 0 8px 8px 0;
}
#ws-floating-toc.is-collapsed .ws-floating-toc-head {
height: 118px;
min-height: 118px;
padding: 9px 0;
flex-direction: column;
justify-content: space-between;
border-bottom: 0;
}
#ws-floating-toc.is-collapsed .ws-floating-toc-title {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 12px;
letter-spacing: .12em;
cursor: pointer;
}
#ws-floating-toc.is-collapsed .ws-floating-toc-title::before {
display: none;
}
#ws-floating-toc.is-collapsed .ws-floating-toc-progress,
#ws-floating-toc.is-collapsed .ws-floating-toc-body {
display: none;
}
#ws-floating-toc.is-collapsed .ws-floating-toc-toggle::before {
transform: rotate(225deg);
}
/* =========================================================
16. LAPTOP / LARGE DESKTOP
========================================================= */
@media (min-width: 992px) and (max-width: 1439px) {
#ws-floating-toc {
left: 8px;
width: 220px;
max-height: 60vh;
}
.ws-floating-toc-body {
max-height: calc(60vh - 54px);
}
.ws-floating-toc-link {
font-size: 11.8px;
}
.ws-floating-toc-item.level-3 .ws-floating-toc-link {
font-size: 11.2px;
}
#ws-floating-toc.is-collapsed {
width: 46px;
}
}
@media (min-width: 1800px) {
#ws-floating-toc {
left: 28px;
width: 270px;
max-height: 62vh;
}
.ws-floating-toc-body {
max-height: calc(62vh - 54px);
}
}
@media (max-height: 700px) and (min-width: 992px) {
#ws-floating-toc {
max-height: 72vh;
}
.ws-floating-toc-body {
max-height: calc(72vh - 54px);
}
}
/* =========================================================
17. MOBILE TOC
========================================================= */
#ws-mobile-toc-trigger,
#ws-mobile-toc-drawer,
#ws-mobile-toc-mask {
display: none;
}
@media (max-width: 991px) {
#ws-floating-toc {
display: none !important;
}
#ws-mobile-toc-trigger {
display: flex;
position: fixed;
left: 14px;
bottom: 68px;
z-index: 9990;
align-items: center;
justify-content: center;
gap: 7px;
min-width: 68px;
height: 42px;
padding: 0 15px;
border: 0;
border-radius: 22px;
background: var(--ws-primary);
color: #fff;
font-size: 12px;
font-weight: 650;
cursor: pointer;
box-shadow:
0 6px 22px rgba(14, 63, 101, .25);
transition:
transform .18s ease,
background .18s ease;
}
#ws-mobile-toc-trigger::before {
content: "☰";
font-size: 14px;
}
#ws-mobile-toc-trigger:hover {
background: var(--ws-primary-dark);
}
#ws-mobile-toc-trigger:active {
transform: scale(.96);
}
/* overlay */
#ws-mobile-toc-mask {
display: block;
position: fixed;
inset: 0;
z-index: 9991;
background: rgba(13, 30, 43, .38);
opacity: 0;
pointer-events: none;
transition: opacity .25s ease;
}
/* centered mobile drawer */
#ws-mobile-toc-drawer {
display: flex;
position: fixed;
top: 50%;
left: 0;
bottom: auto;
z-index: 9992;
width: min(86vw, 340px);
max-height: 68vh;
flex-direction: column;
background: #fff;
border-radius: 0 12px 12px 0;
box-shadow:
10px 0 32px rgba(8, 32, 49, .19);
overflow: hidden;
transform: translate(-105%, -50%);
transition:
transform .28s cubic-bezier(.22, .7, .22, 1);
}
.ws-mobile-toc-drawer-head {
display: flex;
flex: 0 0 54px;
height: 54px;
min-height: 54px;
align-items: center;
justify-content: space-between;
padding: 0 15px 0 18px;
border-bottom: 1px solid var(--ws-border);
background: #f8fbfd;
}
.ws-mobile-toc-drawer-head strong {
display: flex;
align-items: center;
gap: 9px;
color: #18374f;
font-size: 15px;
font-weight: 700;
}
.ws-mobile-toc-drawer-head strong::before {
content: "";
width: 4px;
height: 18px;
border-radius: 3px;
background: var(--ws-primary);
}
.ws-mobile-toc-close {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
border: 0;
border-radius: 50%;
background: #eaf1f5;
color: #536878;
font-size: 20px;
cursor: pointer;
}
.ws-mobile-toc-drawer-body {
flex: 1;
max-height: calc(68vh - 54px);
padding: 9px 10px 12px;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
.ws-mobile-toc-drawer-body .ws-floating-toc-link {
padding-top: 10px;
padding-bottom: 10px;
font-size: 13.5px;
}
.ws-mobile-toc-drawer-body
.ws-floating-toc-item.level-3
.ws-floating-toc-link {
font-size: 12.5px;
}
body.ws-toc-mobile-open {
overflow: hidden;
}
body.ws-toc-mobile-open #ws-mobile-toc-mask {
opacity: 1;
pointer-events: auto;
}
body.ws-toc-mobile-open #ws-mobile-toc-drawer {
transform: translate(0, -50%);
}
}
/* =========================================================
18. MOBILE ARTICLE
========================================================= */
@media (max-width: 767px) {
body.ws-single-post-enhanced .ws-article-content p {
margin-bottom: 17px !important;
font-size: 14.5px !important;
line-height: 1.9 !important;
}
body.ws-single-post-enhanced .ws-article-content h2 {
margin: 36px 0 19px !important;
padding: 10px 11px 10px 13px !important;
font-size: 19px !important;
border-left-width: 3px !important;
}
body.ws-single-post-enhanced .ws-article-content h3 {
margin: 27px 0 14px !important;
font-size: 16.5px !important;
}
body.ws-single-post-enhanced
.ws-article-content
.ws-article-image,
body.ws-single-post-enhanced
.ws-article-content
img.ws-article-inline-image {
width: 100% !important;
max-width: 100% !important;
margin: 23px auto 28px !important;
}
body.ws-single-post-enhanced
.ws-article-content
.ws-article-image
img {
width: 100% !important;
max-width: 100% !important;
border-radius: 6px !important;
}
body.ws-single-post-enhanced .ws-article-hero {
width: 100% !important;
max-width: 100% !important;
margin-bottom: 24px !important;
}
body.ws-single-post-enhanced .ws-article-content ul,
body.ws-single-post-enhanced .ws-article-content ol {
padding:
16px
15px
16px
38px !important;
margin-bottom: 23px !important;
}
body.ws-single-post-enhanced .ws-article-content ul li,
body.ws-single-post-enhanced .ws-article-content ol li {
font-size: 14.5px !important;
}
body.ws-single-post-enhanced .ws-article-content blockquote {
padding:
18px
17px
18px
22px !important;
margin: 25px 0 !important;
}
/* Mobile tables */
body.ws-single-post-enhanced
.ws-article-content
.ws-table-scroll {
width: calc(100% + 12px) !important;
max-width: calc(100% + 12px) !important;
margin-left: -6px !important;
margin-right: -6px !important;
border-radius: 6px !important;
}
body.ws-single-post-enhanced
.ws-article-content
.ws-article-table {
width: max-content !important;
min-width:
var(--ws-table-min-width, 680px) !important;
table-layout: auto !important;
font-size: 13px !important;
}
body.ws-single-post-enhanced
.ws-article-table
th,
body.ws-single-post-enhanced
.ws-article-table
td {
padding: 11px 12px !important;
min-width: 130px !important;
}
body.ws-single-post-enhanced
.ws-article-table
th:first-child,
body.ws-single-post-enhanced
.ws-article-table
td:first-child {
position: sticky !important;
left: 0 !important;
z-index: 3 !important;
min-width: 125px !important;
box-shadow:
5px 0 9px rgba(24,53,74,.06) !important;
}
body.ws-single-post-enhanced
.ws-article-table
th:first-child {
z-index: 4 !important;
color: #fff !important;
background: var(--ws-primary) !important;
}
body.ws-single-post-enhanced
.ws-article-table
tbody
tr:nth-child(odd)
td:first-child {
background: #f4f8fb !important;
}
body.ws-single-post-enhanced
.ws-article-table
tbody
tr:nth-child(even)
td:first-child {
background: #eef5f9 !important;
}
}
/* =========================================================
19. SMALL PHONE
========================================================= */
@media (max-width: 480px) {
#ws-mobile-toc-drawer {
width: 88vw;
max-height: 68vh;
}
.ws-mobile-toc-drawer-body {
max-height: calc(68vh - 54px);
}
}
/* =========================================================
20. SHORT MOBILE SCREEN
========================================================= */
@media (max-height: 650px) and (max-width: 991px) {
#ws-mobile-toc-drawer {
max-height: 76vh;
}
.ws-mobile-toc-drawer-body {
max-height: calc(76vh - 54px);
}
}
/* =========================================================
21. ACCESSIBILITY
========================================================= */
body.ws-single-post-enhanced a:focus-visible,
body.ws-single-post-enhanced button:focus-visible {
outline: 2px solid var(--ws-primary);
outline-offset: 3px;
}
/* =========================================================
OPTIONAL
Uncomment if you want to hide WordPress comments.
========================================================= */
/*
body.single-post .comments-area,
body.single-post .comment-respond,
body.single-post .meta-comments,
body.single-post .widget_recent_comments {
display: none !important;
}
*/
Js
/**
* Universal Single Post Enhancer
* ---------------------------------------------------------
* Features:
* 1. Auto-detect article content
* 2. Add professional article styles through CSS classes
* 3. Responsive images
* 4. Responsive tables with horizontal scrolling
* 5. Desktop floating left TOC, open by default
* 6. Desktop TOC can collapse
* 7. Mobile TOC closed by default
* 8. Mobile TOC opens as a centered left drawer
* 9. Active section highlight
* 10. Reading progress
* 11. Smooth scrolling
*
* Designed for WordPress / Porto / Elementor / Gutenberg,
* but can also be adapted to other CMS templates.
*/
(function () {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
/* =====================================================
CONFIG
===================================================== */
var CONFIG = {
/*
* true:
* only run on WordPress single post pages.
*
* false:
* can also run on other CMS/article pages.
*/
onlySinglePost: true,
minHeadingCount: 2,
headingSelector: 'h2, h3',
headerOffsetDesktop: 95,
headerOffsetMobile: 75,
desktopBreakpoint: 992,
/*
* Desktop TOC default:
* false = expanded
* true = collapsed
*/
desktopCollapsedByDefault: false,
articleCandidates: [
'.entry-content',
'.post-content',
'.single-post-content',
'.single-content',
'.article-content',
'.elementor-widget-theme-post-content .elementor-widget-container',
'.main-content .elementor-heading-title',
'.main-content article',
'article .entry-content',
'article'
],
heroCandidates: [
'.post-image.single',
'.post-thumbnail',
'.entry-thumbnail',
'.featured-image',
'.single-post-thumbnail'
]
};
/* =====================================================
1. PAGE CHECK
===================================================== */
if (
CONFIG.onlySinglePost &&
!document.body.classList.contains('single-post')
) {
return;
}
/* =====================================================
2. FIND BEST ARTICLE CONTAINER
===================================================== */
function uniqueElements(elements) {
var result = [];
elements.forEach(function (element) {
if (
element &&
result.indexOf(element) === -1
) {
result.push(element);
}
});
return result;
}
var candidates = [];
CONFIG.articleCandidates.forEach(function (selector) {
document
.querySelectorAll(selector)
.forEach(function (element) {
candidates.push(element);
});
});
candidates = uniqueElements(candidates);
var article = null;
var bestScore = -1;
candidates.forEach(function (candidate) {
var headings =
candidate.querySelectorAll(
CONFIG.headingSelector
).length;
var paragraphs =
candidate.querySelectorAll('p').length;
var textLength =
(candidate.textContent || '').trim().length;
/*
* Headings are weighted heavily,
* so sidebars / footers are unlikely to win.
*/
var score =
headings * 100 +
paragraphs * 4 +
Math.min(textLength / 500, 40);
if (score > bestScore) {
bestScore = score;
article = candidate;
}
});
if (!article) {
return;
}
var headings = Array.prototype.slice.call(
article.querySelectorAll(
CONFIG.headingSelector
)
);
if (headings.length < CONFIG.minHeadingCount) {
return;
}
document.body.classList.add(
'ws-single-post-enhanced'
);
article.classList.add(
'ws-article-content'
);
/* =====================================================
3. HERO IMAGE
===================================================== */
CONFIG.heroCandidates.some(function (selector) {
var hero =
document.querySelector(selector);
if (
hero &&
!article.contains(hero)
) {
hero.classList.add(
'ws-article-hero'
);
return true;
}
return false;
});
/* =====================================================
4. ARTICLE IMAGES
===================================================== */
article
.querySelectorAll('figure')
.forEach(function (figure) {
var hasImage =
figure.querySelector('img');
var hasTable =
figure.querySelector('table');
var isGallery =
figure.classList.contains('wp-block-gallery') ||
figure.closest('.wp-block-gallery') ||
figure.classList.contains('gallery');
if (
hasImage &&
!hasTable &&
!isGallery
) {
figure.classList.add(
'ws-article-image'
);
}
});
article
.querySelectorAll('img')
.forEach(function (img) {
if (
!img.closest('figure') &&
!img.closest('.gallery') &&
!img.closest('.wp-block-gallery')
) {
img.classList.add(
'ws-article-inline-image'
);
}
});
/* =====================================================
5. RESPONSIVE TABLES
===================================================== */
article
.querySelectorAll('table')
.forEach(function (table) {
table.classList.add(
'ws-article-table'
);
var wrapper =
table.closest(
'.wp-block-table, .ws-table-scroll'
);
if (!wrapper) {
wrapper =
document.createElement('div');
wrapper.className =
'ws-table-scroll';
table.parentNode.insertBefore(
wrapper,
table
);
wrapper.appendChild(table);
} else {
wrapper.classList.add(
'ws-table-scroll'
);
}
var columnCount = 0;
Array.prototype.slice
.call(table.rows)
.forEach(function (row) {
columnCount =
Math.max(
columnCount,
row.cells.length
);
});
var minWidth = 560;
if (columnCount === 3) {
minWidth = 660;
}
if (columnCount === 4) {
minWidth = 760;
}
if (columnCount >= 5) {
minWidth = 880;
}
table.style.setProperty(
'--ws-table-min-width',
minWidth + 'px'
);
wrapper.setAttribute(
'tabindex',
'0'
);
wrapper.setAttribute(
'role',
'region'
);
wrapper.setAttribute(
'aria-label',
'Horizontally scrollable table'
);
});
/* =====================================================
6. HEADING IDS
===================================================== */
headings.forEach(function (heading, index) {
if (!heading.id) {
heading.id =
'ws-article-section-' +
(index + 1);
}
});
/* =====================================================
7. BUILD TOC LIST
===================================================== */
function createTocList() {
var list =
document.createElement('ul');
list.className =
'ws-floating-toc-list';
headings.forEach(function (heading) {
var level =
heading.tagName === 'H3'
? '3'
: '2';
var li =
document.createElement('li');
li.className =
'ws-floating-toc-item level-' +
level;
var link =
document.createElement('a');
link.className =
'ws-floating-toc-link';
link.href =
'#' + heading.id;
link.dataset.target =
heading.id;
link.textContent =
heading.textContent.trim();
li.appendChild(link);
list.appendChild(li);
});
return list;
}
/* =====================================================
8. DESKTOP FLOATING TOC
===================================================== */
var toc =
document.createElement('nav');
toc.id =
'ws-floating-toc';
toc.setAttribute(
'aria-label',
'記事のContents'
);
if (CONFIG.desktopCollapsedByDefault) {
toc.classList.add(
'is-collapsed'
);
}
var tocHead =
document.createElement('div');
tocHead.className =
'ws-floating-toc-head';
var tocTitle =
document.createElement('div');
tocTitle.className =
'ws-floating-toc-title';
tocTitle.textContent =
'Contents';
var tocToggle =
document.createElement('button');
tocToggle.type =
'button';
tocToggle.className =
'ws-floating-toc-toggle';
tocToggle.setAttribute(
'aria-expanded',
CONFIG.desktopCollapsedByDefault
? 'false'
: 'true'
);
tocToggle.setAttribute(
'aria-label',
CONFIG.desktopCollapsedByDefault
? 'Contentsを開く'
: 'Contentsを折りたたむ'
);
tocHead.appendChild(
tocTitle
);
tocHead.appendChild(
tocToggle
);
var progress =
document.createElement('div');
progress.className =
'ws-floating-toc-progress';
var progressBar =
document.createElement('div');
progressBar.className =
'ws-floating-toc-progress-bar';
progress.appendChild(
progressBar
);
var tocBody =
document.createElement('div');
tocBody.className =
'ws-floating-toc-body';
tocBody.appendChild(
createTocList()
);
toc.appendChild(
tocHead
);
toc.appendChild(
progress
);
toc.appendChild(
tocBody
);
document.body.appendChild(
toc
);
/* =====================================================
9. DESKTOP COLLAPSE
===================================================== */
function setDesktopCollapsed(collapsed) {
toc.classList.toggle(
'is-collapsed',
collapsed
);
tocToggle.setAttribute(
'aria-expanded',
collapsed
? 'false'
: 'true'
);
tocToggle.setAttribute(
'aria-label',
collapsed
? 'Contentsを開く'
: 'Contentsを折りたたむ'
);
}
tocToggle.addEventListener(
'click',
function () {
setDesktopCollapsed(
!toc.classList.contains(
'is-collapsed'
)
);
}
);
tocTitle.addEventListener(
'click',
function () {
if (
toc.classList.contains(
'is-collapsed'
)
) {
setDesktopCollapsed(false);
}
}
);
/* =====================================================
10. MOBILE TOC
===================================================== */
var mobileTrigger =
document.createElement('button');
mobileTrigger.id =
'ws-mobile-toc-trigger';
mobileTrigger.type =
'button';
mobileTrigger.textContent =
'Contents';
mobileTrigger.setAttribute(
'aria-label',
'記事のContentsを開く'
);
mobileTrigger.setAttribute(
'aria-expanded',
'false'
);
var mobileMask =
document.createElement('div');
mobileMask.id =
'ws-mobile-toc-mask';
var mobileDrawer =
document.createElement('aside');
mobileDrawer.id =
'ws-mobile-toc-drawer';
mobileDrawer.setAttribute(
'role',
'dialog'
);
mobileDrawer.setAttribute(
'aria-modal',
'true'
);
mobileDrawer.setAttribute(
'aria-label',
'記事のContents'
);
var mobileHead =
document.createElement('div');
mobileHead.className =
'ws-mobile-toc-drawer-head';
var mobileTitle =
document.createElement('strong');
mobileTitle.textContent =
'この記事のContents';
var mobileClose =
document.createElement('button');
mobileClose.type =
'button';
mobileClose.className =
'ws-mobile-toc-close';
mobileClose.innerHTML =
'×';
mobileClose.setAttribute(
'aria-label',
'Contentsを閉じる'
);
mobileHead.appendChild(
mobileTitle
);
mobileHead.appendChild(
mobileClose
);
var mobileBody =
document.createElement('div');
mobileBody.className =
'ws-mobile-toc-drawer-body';
mobileBody.appendChild(
createTocList()
);
mobileDrawer.appendChild(
mobileHead
);
mobileDrawer.appendChild(
mobileBody
);
document.body.appendChild(
mobileMask
);
document.body.appendChild(
mobileDrawer
);
document.body.appendChild(
mobileTrigger
);
/* =====================================================
11. MOBILE OPEN / CLOSE
===================================================== */
function openMobileToc() {
document.body.classList.add(
'ws-toc-mobile-open'
);
mobileTrigger.setAttribute(
'aria-expanded',
'true'
);
}
function closeMobileToc() {
document.body.classList.remove(
'ws-toc-mobile-open'
);
mobileTrigger.setAttribute(
'aria-expanded',
'false'
);
}
mobileTrigger.addEventListener(
'click',
openMobileToc
);
mobileClose.addEventListener(
'click',
closeMobileToc
);
mobileMask.addEventListener(
'click',
closeMobileToc
);
document.addEventListener(
'keydown',
function (event) {
if (event.key === 'Escape') {
closeMobileToc();
}
}
);
/* =====================================================
12. HEADER OFFSET
===================================================== */
function getHeaderOffset() {
if (
window.innerWidth <= 767
) {
return CONFIG.headerOffsetMobile;
}
return CONFIG.headerOffsetDesktop;
}
/* =====================================================
13. SMOOTH SCROLL
===================================================== */
document.addEventListener(
'click',
function (event) {
var link =
event.target.closest(
'.ws-floating-toc-link'
);
if (!link) {
return;
}
var id =
link.dataset.target;
var target =
document.getElementById(id);
if (!target) {
return;
}
event.preventDefault();
var targetY =
target
.getBoundingClientRect()
.top
+
window.pageYOffset
-
getHeaderOffset();
window.scrollTo({
top: targetY,
behavior: 'smooth'
});
try {
history.replaceState(
null,
'',
'#' + id
);
} catch (error) {}
closeMobileToc();
}
);
/* =====================================================
14. ACTIVE SECTION
===================================================== */
var lastActiveId =
null;
function updateActiveSection() {
var offset =
getHeaderOffset() + 35;
var current =
headings[0];
headings.forEach(function (heading) {
var top =
heading
.getBoundingClientRect()
.top;
if (top <= offset) {
current =
heading;
}
});
if (!current) {
return;
}
var id =
current.id;
if (id === lastActiveId) {
return;
}
lastActiveId =
id;
document
.querySelectorAll(
'.ws-floating-toc-link'
)
.forEach(function (link) {
link.classList.toggle(
'is-active',
link.dataset.target === id
);
});
keepActiveLinkVisible(
tocBody
);
keepActiveLinkVisible(
mobileBody
);
}
/* =====================================================
15. KEEP ACTIVE LINK VISIBLE
===================================================== */
function keepActiveLinkVisible(container) {
if (!container) {
return;
}
var active =
container.querySelector(
'.ws-floating-toc-link.is-active'
);
if (!active) {
return;
}
var top =
active.offsetTop;
var bottom =
top + active.offsetHeight;
var visibleTop =
container.scrollTop;
var visibleBottom =
visibleTop +
container.clientHeight;
if (
top < visibleTop + 12 ||
bottom > visibleBottom - 12
) {
var targetScroll =
Math.max(
0,
top -
container.clientHeight / 2
);
container.scrollTo({
top: targetScroll,
behavior: 'smooth'
});
}
}
/* =====================================================
16. READING PROGRESS
===================================================== */
function updateProgress() {
var rect =
article.getBoundingClientRect();
var articleTop =
window.pageYOffset +
rect.top;
var articleHeight =
article.offsetHeight;
var current =
window.pageYOffset +
window.innerHeight * 0.25;
var value =
(
current - articleTop
) /
articleHeight;
value =
Math.max(
0,
Math.min(
1,
value
)
);
progressBar.style.width =
(value * 100) + '%';
}
/* =====================================================
17. SCROLL / RESIZE PERFORMANCE
===================================================== */
var ticking =
false;
function requestUpdate() {
if (ticking) {
return;
}
ticking =
true;
window.requestAnimationFrame(
function () {
updateActiveSection();
updateProgress();
ticking =
false;
}
);
}
window.addEventListener(
'scroll',
requestUpdate,
{
passive: true
}
);
window.addEventListener(
'resize',
requestUpdate,
{
passive: true
}
);
/* =====================================================
18. INITIAL STATE
===================================================== */
updateActiveSection();
updateProgress();
});
})();