diff --git a/apps/test/content/posts/code-tabs-test.md b/apps/test/content/posts/code-tabs-test.md new file mode 100644 index 00000000..8f3c1e7a --- /dev/null +++ b/apps/test/content/posts/code-tabs-test.md @@ -0,0 +1,110 @@ +--- +title: Code Tabs Test +date: 2026-01-25T07:50:22+08:00 +collections: + - Tests +categories: + - Markdown +tags: + - Code Tabs +--- + +Code tabs test cases for grouped code blocks. + + + +## Default Name + +```python {group=tab2} +print("Python") +``` + +```go {group=tab2} +fmt.Println("Go") +``` + +## Custom Name + +```js {group=tab-actions name="[JavaScript]"} +function sum(a, b) { + return a + b +} + +console.log(sum(1, 2)) +``` + +```python {group=tab-actions name="[Python]"} +def sum(a, b): + return a + b + +print(sum(1, 2)) +``` + +## Mode + +```ts {group=tab-mode name="Classic"} +interface User { + id: number + name: string +} +``` + +```ts {group=tab-mode name="Mac" mode="mac"} +interface User { + id: number + name: string +} +``` + +```ts {group=tab-mode name="Simple" mode="simple"} +interface User { + id: number + name: string +} +``` + +## Actions + +```bash {group=tab-actions name="lineNos=true" lineNos=true} +echo "line 1" +echo "line 2" +echo "line 3" +``` + +```bash {group=tab-actions name="lineNos=false" lineNos=false} +echo "line 1" +echo "line 2" +echo "line 3" +``` + +```bash {group=tab-actions name="wrapping" .line-wrapping} +printf "this is a very very very very very very very very very very very very very very very very long line" +``` + +```js {group=tab-actions name="editable" editable=true} +function greet(name) { + return `Hello, ${name}!` +} +``` + +## Expanded/Collapsed + +```js {group=tab-expand name="Expanded"} +function longFunction() { + console.log('This is a long function.') + console.log('It has many lines of code.') + console.log('The code block should be expanded by default.') + console.log('Lorem ipsum dolor sit amet, consectetur adipiscing elit.') + console.log('Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.') + console.log('Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.') + console.log('Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.') + console.log('Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + console.log('The end of the long function.') +} +``` + +```js {group=tab-expand name="Collapsed" .is-collapsed} +function shortFunction() { + console.log('This is a short function.') +} +``` diff --git a/assets/css/_page/_single/_code.scss b/assets/css/_page/_single/_code.scss index 678cab08..0cf9b10f 100644 --- a/assets/css/_page/_single/_code.scss +++ b/assets/css/_page/_single/_code.scss @@ -359,7 +359,7 @@ pre { display: inline; } - .code-header [role='button']:not(.ellipses-btn), + .code-header .action-btn, .code-expand-btn { display: none; } @@ -559,3 +559,105 @@ pre { @import './_code-syntax/github-dark-dimmed'; } } + +// Code Tabs +.code-tabs { + margin: 0.5rem 0; + background-color: fixit-var(code-block-background-color); + border: 1px solid fixit-var(global-border-color); + @include border-radius; + + &:has(.code-block.active .code-expand-btn) { + margin-bottom: 1rem; + } + + &:has(.code-block.active.line-wrapping) .tabs-actions .line-wrap-btn, + &:has(.code-block.active:not(.line-nos-hidden)) .tabs-actions .line-nos-btn, + &:has(.code-block.active [contenteditable='true']) .tabs-actions .edit-btn { + color: fixit-var(global-link-hover-color); + } + + .tabs-header { + display: flex; + justify-content: space-between; + align-items: center; + line-height: 1.4em; + font-size: fixit-var(code-block-font-size); + color: fixit-var(code-header-color); + background-color: fixit-var(code-header-background-color); + @include border-radius(top); + + .tabs-items { + display: flex; + overflow-x: auto; + @include border-radius(top); + + .tab-item { + padding: 0.4rem 0.8rem; + cursor: pointer; + font-weight: bold; + color: fixit-var(secondary); + white-space: nowrap; + transition: all 0.2s; + user-select: none; + + &:hover { + color: fixit-var(global-font-color); + background-color: rgba(128, 128, 128, 0.05); + } + + &.active { + color: fixit-var(global-font-color); + box-shadow: inset 0 -2px 0 fixit-var(code-tab-active-border-color, fixit-var(primary)); + background-color: fixit-var(code-block-background-color); + } + } + } + + .tabs-actions { + display: flex; + flex-shrink: 0; + + .action-btn { + padding: 0.4rem; + + &:hover { + color: fixit-var(global-link-hover-color); + } + + &.copy-btn[data-copied] .fa-clone { + --fa: "\f00c"; + font-weight: 900; + color: fixit-var(global-link-hover-color); + } + + &.download-btn[data-downloaded] .fa-download { + --fa: "\f1ce"; + font-weight: 900; + color: fixit-var(global-link-hover-color); + } + + &.fullscreen-btn { + display: none; + } + } + } + } + + .tabs-content { + .code-block { + display: none; + margin: 0; + @include border-radius(bottom); + + &.active { + display: block; + } + + // hide original header + .code-header { + display: none; + } + } + } +} diff --git a/assets/js/theme.js b/assets/js/theme.js index a8b22803..46c68ceb 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -594,6 +594,104 @@ class FixIt { }); } + /** + * init code tabs + */ + initCodeTabs() { + const $codeBlocks = document.querySelectorAll('.code-block[data-tab-group]'); + const processed = new Set(); + + Util.forEach($codeBlocks, ($block) => { + if (processed.has($block)) return; + + const groupName = $block.dataset.tabGroup; + const $tabs = []; + let $curr = $block; + + // collect consecutive blocks with same group + while ($curr && $curr.classList?.contains('code-block') && $curr.dataset.tabGroup === groupName) { + $tabs.push($curr); + processed.add($curr); + $curr = $curr.nextElementSibling; + } + + if ($tabs.length < 2) return; + + // create DOM structure + const $container = document.createElement('div'); + $container.className = 'code-tabs'; + + const $header = document.createElement('div'); + $header.className = 'tabs-header'; + + const $items = document.createElement('div'); + $items.className = 'tabs-items'; + + const $actions = document.createElement('div'); + $actions.className = 'tabs-actions'; + + $header.appendChild($items); + $header.appendChild($actions); + + const $content = document.createElement('div'); + $content.className = 'tabs-content'; + + // insert container before the first block + const $firstBlock = $tabs[0]; + $firstBlock.parentNode.insertBefore($container, $firstBlock); + + $tabs.forEach(($tab, index) => { + const title = $tab.dataset.tabTitle || 'Code'; + + // tab button + const $btn = document.createElement('span'); + $btn.className = 'tab-item'; + if (index === 0) $btn.classList.add('active'); + $btn.textContent = title; + $btn.dataset.index = index; + $btn.title = title; + + $btn.addEventListener('click', () => { + // 1. restore buttons to the currently active tab + const $activeTab = $tabs.find(t => t.classList.contains('active')); + if ($activeTab) { + const $activeHeader = $activeTab.querySelector('.code-header'); + if ($activeHeader) { + Array.from($actions.children).forEach(btn => $activeHeader.appendChild(btn)); + } + } + + // 2. switch active tab UI + $items.querySelectorAll('.tab-item').forEach(b => b.classList.remove('active')); + $btn.classList.add('active'); + + // 3. switch content + $tabs.forEach(b => b.classList.remove('active')); + $tab.classList.add('active'); + + // 4. move new buttons to actions + const $codeHeader = $tab.querySelector('.code-header'); + if ($codeHeader) { + $codeHeader.querySelectorAll('.action-btn').forEach(btn => $actions.appendChild(btn)); + } + }); + $items.appendChild($btn); + + // move block to content + $tab.classList.toggle('active', index === 0); + // remove is-collapsed class + $tab.classList.remove('is-collapsed'); + $content.appendChild($tab); + }); + + $container.appendChild($header); + $container.appendChild($content); + + // initialize actions for first tab + $items.firstElementChild.click(); + }); + } + /** * init diagram copy button */ @@ -1261,6 +1359,7 @@ class FixIt { this.initDetails(target); this.initLightGallery(); this.initCodeWrapper(); + this.initCodeTabs(); this.initDiagramCopyBtn(); this.initEcharts(); this.initTypeit(target); diff --git a/layouts/_partials/function/code-header.html b/layouts/_partials/function/code-header.html index c546d426..9bf3f6ce 100644 --- a/layouts/_partials/function/code-header.html +++ b/layouts/_partials/function/code-header.html @@ -18,27 +18,27 @@ {{- end -}} {{- if $config.linenostoggler -}} {{- $lineNosIcon := dict "Class" "fa-solid fa-list-ol" | partial "plugin/icon.html" -}} - {{- $lineNosBtn = printf `%s` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}} + {{- $lineNosBtn = printf `%s` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}} {{- end -}} {{- if $config.linewraptoggler -}} {{- $lineWrapIcon := dict "Class" "fa-solid fa-right-left" | partial "plugin/icon.html" -}} - {{- $lineWrapBtn = printf `%s` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}} + {{- $lineWrapBtn = printf `%s` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}} {{- end -}} {{- if $config.editable -}} {{- $editIcon := dict "Class" "fa-solid fa-pen-to-square" | partial "plugin/icon.html" -}} - {{- $editBtn = printf `%s` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}} + {{- $editBtn = printf `%s` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}} {{- end -}} {{- if $config.copyable -}} {{- $copyIcon := dict "Class" "fa-regular fa-clone" | partial "plugin/icon.html" -}} - {{- $copyBtn = printf `%s` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}} + {{- $copyBtn = printf `%s` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}} {{- end -}} {{- if $config.downloadable -}} {{- $downloadIcon := dict "Class" "fa-solid fa-download" | partial "plugin/icon.html" -}} - {{- $downloadBtn = printf `%s` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}} + {{- $downloadBtn = printf `%s` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}} {{- end -}} {{- if $config.fullscreen -}} {{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}} - {{- $fullscreenBtn = printf `%s` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}} + {{- $fullscreenBtn = printf `%s` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}} {{- end -}} {{- with $config.name -}} {{- $titleEl = replace $titleEl `` (printf `` .) -}} diff --git a/layouts/_partials/plugin/code-block-wrapper.html b/layouts/_partials/plugin/code-block-wrapper.html index 06c5159c..dc3aa35b 100644 --- a/layouts/_partials/plugin/code-block-wrapper.html +++ b/layouts/_partials/plugin/code-block-wrapper.html @@ -80,6 +80,10 @@ $lineNosDigit ) -}} + {{- if $config.group -}} + {{- $tabTitle := $config.name | default (strings.FirstUpper .Type) | default "Code" -}} + {{- $wrapper = replace $wrapper (printf `class="%s"` $class) (printf `class="%s" data-tab-group="%s" data-tab-title="%s"` $class $config.group $tabTitle) -}} + {{- end -}} {{- $wrapper = replace $wrapper (printf `class="%s"` $class)