added feature to control per note via frontmatter

This commit is contained in:
Bradley Wyatt 2025-02-06 14:12:47 -06:00
parent 27ab04eae6
commit 59f8c4e494
5 changed files with 79 additions and 109 deletions

View file

@ -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
```
![Code Collapsed](/images/code_collapsed_frontmatter.png)
#### Expanded
```yaml
code-blocks: expanded
```
![Code Expanded](/images/code_expanded_frontmatter.png)
## Examples

View file

@ -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,

View file

@ -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

View file

@ -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);

View file

@ -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;
}