mirror of
https://github.com/bwya77/collapsible-code-blocks.git
synced 2026-07-22 12:00:25 +00:00
added feature to control per note via frontmatter
This commit is contained in:
parent
27ab04eae6
commit
59f8c4e494
5 changed files with 79 additions and 109 deletions
19
README.md
19
README.md
|
|
@ -10,8 +10,8 @@ Make code blocks collapsible in preview mode with customizable collapse/expand b
|
|||
- Customizable collapse/expand icons
|
||||
- Optional horizontal scrolling for code blocks
|
||||
- Configure number of visible lines when collapsed
|
||||
- Keyboard navigation support
|
||||
- Default collapse state setting
|
||||
- Control if a code block should be expanded or collapsed on a per-note basis by setting code-blocks to `collapsed` or `expanded` in the frontmatter
|
||||
- Smooth animations for collapse/expand transitions
|
||||
- Maintains proper layout with surrounding content
|
||||
|
||||
|
|
@ -46,6 +46,23 @@ The plugin offers several customization options:
|
|||
- **Collapse/Expand Icons**: Customize the icons shown for collapse/expand states
|
||||
- **Horizontal Scrolling**: Toggle horizontal scrolling for code blocks
|
||||
- **Collapsed Lines**: Set how many lines remain visible when collapsed
|
||||
- **Frontmatter Setting**: Set the frontmatter key to control the default state of code blocks
|
||||
|
||||
### Frontmatter Setting
|
||||
Set a note to always have code blocks expanded or collapsed by default by adding the following to the note's frontmatter (note: this will override the global default setting):
|
||||
|
||||
#### Collapsed
|
||||
```yaml
|
||||
code-blocks: collapsed
|
||||
```
|
||||

|
||||
|
||||
#### Expanded
|
||||
|
||||
```yaml
|
||||
code-blocks: expanded
|
||||
```
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { EditorView, Decoration, DecorationSet, WidgetType } from '@codemirror/v
|
|||
import { syntaxTree } from '@codemirror/language';
|
||||
import { StateField, StateEffect, EditorState } from '@codemirror/state';
|
||||
import { CollapsibleCodeBlockSettings } from './types';
|
||||
import { App, MarkdownView } from 'obsidian';
|
||||
|
||||
interface CodeBlockPosition {
|
||||
startPos: number;
|
||||
|
|
@ -12,18 +13,35 @@ interface CodeBlockPosition {
|
|||
export const toggleFoldEffect = StateEffect.define<{from: number, to: number, defaultState?: boolean}>();
|
||||
|
||||
class FoldWidget extends WidgetType {
|
||||
private static initializedBlocks = new Set<string>();
|
||||
private static initializedBlocks = new WeakMap<EditorState, Set<string>>();
|
||||
|
||||
constructor(
|
||||
readonly startPos: number,
|
||||
readonly endPos: number,
|
||||
readonly settings: CollapsibleCodeBlockSettings,
|
||||
readonly foldField: StateField<DecorationSet>
|
||||
readonly foldField: StateField<DecorationSet>,
|
||||
readonly app: App
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getFrontmatterCodeBlockState(): boolean | null {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!activeView?.file) return null; // Add ?.file check here
|
||||
|
||||
const cache = this.app.metadataCache.getFileCache(activeView.file);
|
||||
if (!cache?.frontmatter?.['code-blocks']) return null;
|
||||
|
||||
const value = cache.frontmatter['code-blocks'].toLowerCase();
|
||||
if (value === 'collapsed') return true;
|
||||
if (value === 'expanded') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
toDOM(view: EditorView) {
|
||||
if (!FoldWidget.initializedBlocks.has(view.state)) {
|
||||
FoldWidget.initializedBlocks.set(view.state, new Set());
|
||||
}
|
||||
const button = document.createElement('div');
|
||||
button.className = 'code-block-toggle';
|
||||
|
||||
|
|
@ -36,8 +54,12 @@ class FoldWidget extends WidgetType {
|
|||
button.setAttribute('aria-label', isFolded ? 'Expand code block' : 'Collapse code block');
|
||||
|
||||
const blockId = `${this.startPos}-${this.endPos}`;
|
||||
if (this.settings.defaultCollapsed && !isFolded && !FoldWidget.initializedBlocks.has(blockId)) {
|
||||
FoldWidget.initializedBlocks.add(blockId);
|
||||
const frontmatterState = this.getFrontmatterCodeBlockState();
|
||||
const shouldCollapse = frontmatterState !== null ? frontmatterState : this.settings.defaultCollapsed;
|
||||
const stateBlocks = FoldWidget.initializedBlocks.get(view.state)!;
|
||||
|
||||
if (shouldCollapse && !stateBlocks.has(blockId)) {
|
||||
stateBlocks.add(blockId);
|
||||
requestAnimationFrame(() => {
|
||||
view.dispatch({
|
||||
effects: toggleFoldEffect.of({
|
||||
|
|
@ -132,17 +154,6 @@ const createFoldField = (settings: CollapsibleCodeBlockSettings) => StateField.d
|
|||
provide: f => EditorView.decorations.from(f)
|
||||
});
|
||||
|
||||
const decorations = (settings: CollapsibleCodeBlockSettings, foldField: StateField<DecorationSet>) => StateField.define<DecorationSet>({
|
||||
create(state: EditorState): DecorationSet {
|
||||
return buildDecorations(state, settings, foldField);
|
||||
},
|
||||
update(value: DecorationSet, transaction): DecorationSet {
|
||||
return buildDecorations(transaction.state, settings, foldField);
|
||||
},
|
||||
provide(field: StateField<DecorationSet>): Extension {
|
||||
return EditorView.decorations.from(field);
|
||||
}
|
||||
});
|
||||
|
||||
const codeBlockPositions = StateField.define<CodeBlockPosition[]>({
|
||||
create(state: EditorState): CodeBlockPosition[] {
|
||||
|
|
@ -193,14 +204,15 @@ function findCodeBlockPositions(state: EditorState): CodeBlockPosition[] {
|
|||
function buildDecorations(
|
||||
state: EditorState,
|
||||
settings: CollapsibleCodeBlockSettings,
|
||||
foldField: StateField<DecorationSet>
|
||||
foldField: StateField<DecorationSet>,
|
||||
app: App
|
||||
): DecorationSet {
|
||||
const widgets: any[] = [];
|
||||
const positions = state.field(codeBlockPositions);
|
||||
|
||||
positions.forEach(pos => {
|
||||
const widget = Decoration.widget({
|
||||
widget: new FoldWidget(pos.startPos, pos.endPos, settings, foldField),
|
||||
widget: new FoldWidget(pos.startPos, pos.endPos, settings, foldField, app),
|
||||
side: -1
|
||||
});
|
||||
widgets.push(widget.range(pos.startPos));
|
||||
|
|
@ -209,9 +221,19 @@ function buildDecorations(
|
|||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export function setupEditView(settings: CollapsibleCodeBlockSettings): Extension[] {
|
||||
export function setupEditView(settings: CollapsibleCodeBlockSettings, app: App): Extension[] {
|
||||
const foldField = createFoldField(settings);
|
||||
const currentDecorations = decorations(settings, foldField);
|
||||
const currentDecorations = StateField.define<DecorationSet>({
|
||||
create(state: EditorState): DecorationSet {
|
||||
return buildDecorations(state, settings, foldField, app);
|
||||
},
|
||||
update(value: DecorationSet, transaction): DecorationSet {
|
||||
return buildDecorations(transaction.state, settings, foldField, app);
|
||||
},
|
||||
provide(field: StateField<DecorationSet>): Extension {
|
||||
return EditorView.decorations.from(field);
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
codeBlockPositions,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export default class CollapsibleCodeBlockPlugin extends Plugin {
|
|||
await this.loadSettings();
|
||||
this.updateScrollSetting();
|
||||
|
||||
// Set up editor view
|
||||
const editorExtensions = setupEditView(this.settings);
|
||||
// Set up editor view with app instance
|
||||
const editorExtensions = setupEditView(this.settings, this.app);
|
||||
this.registerEditorExtension(editorExtensions);
|
||||
|
||||
// Set up reading view
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@ export interface ReadViewAPI {
|
|||
}
|
||||
|
||||
export function setupReadView(app: ExtendedApp, settings: CollapsibleCodeBlockSettings): ReadViewAPI {
|
||||
function getFrontmatterCodeBlockState(): boolean | null {
|
||||
const activeView = app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!activeView?.file) return null;
|
||||
|
||||
const cache = app.metadataCache.getFileCache(activeView.file);
|
||||
if (!cache?.frontmatter?.['code-blocks']) return null;
|
||||
|
||||
const value = cache.frontmatter['code-blocks'].toLowerCase();
|
||||
if (value === 'collapsed') return true;
|
||||
if (value === 'expanded') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function createToggleButton(): HTMLElement {
|
||||
const button = document.createElement('div');
|
||||
button.className = 'code-block-toggle';
|
||||
|
|
@ -84,13 +97,16 @@ export function setupReadView(app: ExtendedApp, settings: CollapsibleCodeBlockSe
|
|||
triggerReflow();
|
||||
}
|
||||
|
||||
function setupCodeBlock(pre: HTMLElement) {
|
||||
function setupCodeBlock(pre: HTMLElement) {
|
||||
document.documentElement.style.setProperty('--collapsed-lines', settings.collapsedLines.toString());
|
||||
|
||||
const toggleButton = createToggleButton();
|
||||
pre.insertBefore(toggleButton, pre.firstChild);
|
||||
|
||||
if (settings.defaultCollapsed) {
|
||||
const frontmatterState = getFrontmatterCodeBlockState();
|
||||
const shouldCollapse = frontmatterState !== null ? frontmatterState : settings.defaultCollapsed;
|
||||
|
||||
if (shouldCollapse) {
|
||||
pre.classList.add('collapsed');
|
||||
toggleButton.textContent = settings.expandIcon;
|
||||
updateCodeBlockVisibility(pre, true);
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
/* styles.css */
|
||||
.markdown-preview-view pre {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
user-select: text;
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-preview-view pre code {
|
||||
display: block;
|
||||
line-height: 1.5em;
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-preview-view pre.collapsed code {
|
||||
max-height: calc(var(--collapsed-lines, 1) * 1.5em);
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.code-block-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-block-toggle:hover {
|
||||
color: var(--text-normal);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-block-toggle:focus-visible {
|
||||
outline: 2px solid var(--text-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.code-block-toggle::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.horizontal-scroll .markdown-preview-view pre:not(.collapsed) code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.markdown-preview-view pre code::-webkit-scrollbar:vertical {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* New styles for dynamic states */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.element-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.element-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.element-spacing {
|
||||
margin-top: var(--element-spacing);
|
||||
}
|
||||
|
||||
/* Custom properties for dynamic values */
|
||||
:root {
|
||||
--element-spacing: 0px;
|
||||
--collapsed-lines: 1;
|
||||
}
|
||||
Loading…
Reference in a new issue