mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 15:28:57 +00:00
feat(code): add tabbed codeblocks support (#710)
This commit is contained in:
@@ -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.
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 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.')
|
||||
}
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,27 +18,27 @@
|
||||
{{- end -}}
|
||||
{{- if $config.linenostoggler -}}
|
||||
{{- $lineNosIcon := dict "Class" "fa-solid fa-list-ol" | partial "plugin/icon.html" -}}
|
||||
{{- $lineNosBtn = printf `<span class="line-nos-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}}
|
||||
{{- $lineNosBtn = printf `<span class="action-btn line-nos-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (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 `<span class="line-wrap-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}}
|
||||
{{- $lineWrapBtn = printf `<span class="action-btn line-wrap-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (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 `<span class="edit-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}}
|
||||
{{- $editBtn = printf `<span class="action-btn edit-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}}
|
||||
{{- end -}}
|
||||
{{- if $config.copyable -}}
|
||||
{{- $copyIcon := dict "Class" "fa-regular fa-clone" | partial "plugin/icon.html" -}}
|
||||
{{- $copyBtn = printf `<span class="copy-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}}
|
||||
{{- $copyBtn = printf `<span class="action-btn copy-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}}
|
||||
{{- end -}}
|
||||
{{- if $config.downloadable -}}
|
||||
{{- $downloadIcon := dict "Class" "fa-solid fa-download" | partial "plugin/icon.html" -}}
|
||||
{{- $downloadBtn = printf `<span class="download-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}}
|
||||
{{- $downloadBtn = printf `<span class="action-btn download-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}}
|
||||
{{- end -}}
|
||||
{{- if $config.fullscreen -}}
|
||||
{{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}}
|
||||
{{- $fullscreenBtn = printf `<span class="fullscreen-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}}
|
||||
{{- $fullscreenBtn = printf `<span class="action-btn fullscreen-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}}
|
||||
{{- end -}}
|
||||
{{- with $config.name -}}
|
||||
{{- $titleEl = replace $titleEl `<span class="code-title">` (printf `<span class="code-title" data-name="%s">` .) -}}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user