mirror of
https://github.com/bwya77/collapsible-code-blocks.git
synced 2026-07-22 12:00:25 +00:00
fixed a lot of issues
This commit is contained in:
parent
4a1655afd0
commit
c44de1fac2
8 changed files with 1040 additions and 92 deletions
327
main copy.ts
Normal file
327
main copy.ts
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import {
|
||||
Plugin,
|
||||
MarkdownView,
|
||||
PluginSettingTab,
|
||||
App,
|
||||
Setting,
|
||||
ButtonComponent
|
||||
} from 'obsidian';
|
||||
|
||||
interface CollapsibleCodeBlockSettings {
|
||||
defaultCollapsed: boolean;
|
||||
collapseIcon: string;
|
||||
expandIcon: string;
|
||||
enableHorizontalScroll: boolean;
|
||||
collapsedLines: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CollapsibleCodeBlockSettings = {
|
||||
defaultCollapsed: false,
|
||||
collapseIcon: '▼',
|
||||
expandIcon: '▶',
|
||||
enableHorizontalScroll: true,
|
||||
collapsedLines: 0
|
||||
};
|
||||
|
||||
export default class CollapsibleCodeBlockPlugin extends Plugin {
|
||||
private contentObserver: MutationObserver;
|
||||
public settings: CollapsibleCodeBlockSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.updateScrollSetting();
|
||||
|
||||
// Single content observer for the entire preview
|
||||
this.contentObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === 'childList') {
|
||||
this.processNewCodeBlocks(mutation.target as HTMLElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.registerMarkdownPostProcessor((element) => {
|
||||
this.processNewCodeBlocks(element);
|
||||
this.contentObserver.observe(element, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
this.addSettingTab(new CollapsibleCodeBlockSettingsTab(this.app, this));
|
||||
}
|
||||
|
||||
private processNewCodeBlocks(element: HTMLElement) {
|
||||
element.querySelectorAll('pre:not(.has-collapse-button)').forEach(pre => {
|
||||
if (!(pre instanceof HTMLElement)) return;
|
||||
|
||||
pre.classList.add('has-collapse-button');
|
||||
this.setupCodeBlock(pre);
|
||||
});
|
||||
}
|
||||
|
||||
private setupCodeBlock(pre: HTMLElement) {
|
||||
document.documentElement.style.setProperty('--collapsed-lines', this.settings.collapsedLines.toString());
|
||||
|
||||
const toggleButton = this.createToggleButton();
|
||||
pre.insertBefore(toggleButton, pre.firstChild);
|
||||
|
||||
if (this.settings.defaultCollapsed) {
|
||||
pre.classList.add('collapsed');
|
||||
toggleButton.textContent = this.settings.expandIcon;
|
||||
this.updateCodeBlockVisibility(pre, true);
|
||||
}
|
||||
}
|
||||
|
||||
private createToggleButton(): HTMLElement {
|
||||
const button = document.createElement('div');
|
||||
button.className = 'code-block-toggle';
|
||||
button.textContent = this.settings.collapseIcon;
|
||||
button.setAttribute('role', 'button');
|
||||
button.setAttribute('tabindex', '0');
|
||||
button.setAttribute('aria-label', 'Toggle code block visibility');
|
||||
|
||||
const toggleHandler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
const pre = (e.target as HTMLElement).closest('pre');
|
||||
if (!pre) return;
|
||||
|
||||
pre.classList.toggle('collapsed');
|
||||
this.updateCodeBlockVisibility(pre, true);
|
||||
|
||||
const isCollapsed = pre.classList.contains('collapsed');
|
||||
button.textContent = isCollapsed ? this.settings.expandIcon : this.settings.collapseIcon;
|
||||
button.setAttribute('aria-expanded', (!isCollapsed).toString());
|
||||
this.app.workspace.requestSaveLayout();
|
||||
};
|
||||
|
||||
button.addEventListener('click', toggleHandler);
|
||||
button.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleHandler(e);
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private updateCodeBlockVisibility(pre: HTMLElement, forceRefresh: boolean = false) {
|
||||
const isCollapsed = pre.classList.contains('collapsed');
|
||||
|
||||
// Update following elements' visibility and positioning
|
||||
let elements: HTMLElement[] = [];
|
||||
let curr = pre.nextElementSibling;
|
||||
|
||||
while (curr && !(curr instanceof HTMLPreElement)) {
|
||||
if (curr instanceof HTMLElement) {
|
||||
elements.push(curr);
|
||||
if (!curr.dataset.originalDisplay) {
|
||||
curr.dataset.originalDisplay = getComputedStyle(curr).display;
|
||||
}
|
||||
}
|
||||
curr = curr.nextElementSibling;
|
||||
}
|
||||
|
||||
// Only perform the layout refresh if forceRefresh is true
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView?.previewMode?.containerEl && forceRefresh) {
|
||||
const previewElement = markdownView.previewMode.containerEl;
|
||||
|
||||
// Immediately update visibility classes
|
||||
elements.forEach(el => {
|
||||
if (isCollapsed) {
|
||||
el.classList.add('element-hidden');
|
||||
el.classList.remove('element-visible', 'element-spacing');
|
||||
} else {
|
||||
el.classList.remove('element-hidden');
|
||||
el.classList.add('element-visible');
|
||||
|
||||
const preRect = pre.getBoundingClientRect();
|
||||
const elRect = el.getBoundingClientRect();
|
||||
if (elRect.top < preRect.bottom) {
|
||||
document.documentElement.style.setProperty('--element-spacing', `${preRect.bottom - elRect.top + 10}px`);
|
||||
el.classList.add('element-spacing');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Force immediate viewport recalculation
|
||||
void previewElement.offsetHeight;
|
||||
|
||||
// Get current scroll position and element position
|
||||
const currentScroll = previewElement.scrollTop;
|
||||
const preRect = pre.getBoundingClientRect();
|
||||
const isAtTop = preRect.top <= 100; // Check if code block is near the top
|
||||
|
||||
// Optimized scroll sequence
|
||||
const scrollSequence = async () => {
|
||||
// If at top, scroll down first then back up
|
||||
if (isAtTop) {
|
||||
previewElement.scrollTop = Math.min(500, previewElement.scrollHeight / 2);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
previewElement.scrollTop = 0;
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
// Quick scroll through content
|
||||
const scrollPoints = [0, previewElement.scrollHeight / 2, currentScroll];
|
||||
|
||||
for (const scrollPos of scrollPoints) {
|
||||
previewElement.scrollTop = scrollPos;
|
||||
previewElement.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
// Final layout adjustments
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
previewElement.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
};
|
||||
|
||||
// Execute scroll sequence
|
||||
scrollSequence();
|
||||
} else {
|
||||
// If not forcing refresh, just update the visibility classes
|
||||
elements.forEach(el => {
|
||||
if (isCollapsed) {
|
||||
el.classList.add('element-hidden');
|
||||
el.classList.remove('element-visible', 'element-spacing');
|
||||
} else {
|
||||
el.classList.remove('element-hidden');
|
||||
el.classList.add('element-visible');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollSetting(): void {
|
||||
document.body.classList.toggle('horizontal-scroll', this.settings.enableHorizontalScroll);
|
||||
}
|
||||
|
||||
private sanitizeIcon(icon: string): string {
|
||||
const cleaned = icon.trim();
|
||||
return cleaned.length <= 2 ? cleaned : DEFAULT_SETTINGS.collapseIcon;
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const loadedData = await this.loadData();
|
||||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...loadedData,
|
||||
// Sanitize icons when loading
|
||||
collapseIcon: this.sanitizeIcon(loadedData?.collapseIcon ?? DEFAULT_SETTINGS.collapseIcon),
|
||||
expandIcon: this.sanitizeIcon(loadedData?.expandIcon ?? DEFAULT_SETTINGS.expandIcon)
|
||||
};
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
// Sanitize before saving
|
||||
this.settings.collapseIcon = this.sanitizeIcon(this.settings.collapseIcon);
|
||||
this.settings.expandIcon = this.sanitizeIcon(this.settings.expandIcon);
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.contentObserver?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
class CollapsibleCodeBlockSettingsTab extends PluginSettingTab {
|
||||
plugin: CollapsibleCodeBlockPlugin;
|
||||
|
||||
constructor(app: App, plugin: CollapsibleCodeBlockPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Collapsible Code Block Settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Collapsed State')
|
||||
.setDesc('Should code blocks be collapsed by default?')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.defaultCollapsed)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultCollapsed = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Collapse Icon')
|
||||
.setDesc('Icon to show when code block is expanded (single character or emoji only)')
|
||||
.addText(text => text
|
||||
.setValue(this.plugin.settings.collapseIcon)
|
||||
.onChange(async (value) => {
|
||||
const sanitized = value.trim();
|
||||
if (sanitized.length <= 2) {
|
||||
this.plugin.settings.collapseIcon = sanitized || DEFAULT_SETTINGS.collapseIcon;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Expand Icon')
|
||||
.setDesc('Icon to show when code block is collapsed (single character or emoji only)')
|
||||
.addText(text => text
|
||||
.setValue(this.plugin.settings.expandIcon)
|
||||
.onChange(async (value) => {
|
||||
const sanitized = value.trim();
|
||||
if (sanitized.length <= 2) {
|
||||
this.plugin.settings.expandIcon = sanitized || DEFAULT_SETTINGS.expandIcon;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable Horizontal Scrolling')
|
||||
.setDesc('Allow code blocks to scroll horizontally instead of wrapping text.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableHorizontalScroll)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableHorizontalScroll = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateScrollSetting();
|
||||
}));
|
||||
|
||||
const collapsedLinesSetting = new Setting(containerEl)
|
||||
.setName('Collapsed Lines')
|
||||
.setDesc('Number of lines visible when code block is collapsed');
|
||||
|
||||
let reloadButton: ButtonComponent | null = null;
|
||||
|
||||
collapsedLinesSetting.addButton(btn => {
|
||||
reloadButton = btn
|
||||
.setButtonText('Apply changes (reload plugin)')
|
||||
.setCta();
|
||||
|
||||
reloadButton.buttonEl.classList.add('hidden');
|
||||
|
||||
reloadButton.onClick(async () => {
|
||||
const pluginId = this.plugin.manifest.id;
|
||||
// @ts-ignore
|
||||
await this.app.plugins.disablePlugin(pluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.enablePlugin(pluginId);
|
||||
|
||||
// Re-open the settings
|
||||
// @ts-ignore
|
||||
this.app.setting.openTabById(pluginId);
|
||||
});
|
||||
});
|
||||
|
||||
collapsedLinesSetting.addText(text => {
|
||||
text
|
||||
.setValue(this.plugin.settings.collapsedLines.toString())
|
||||
.onChange(async (value) => {
|
||||
const numericValue = parseInt(value, 10);
|
||||
this.plugin.settings.collapsedLines = isNaN(numericValue) || numericValue < 0 ? 0 : numericValue;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
if (reloadButton) {
|
||||
reloadButton.buttonEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
310
main.ts
310
main.ts
|
|
@ -4,8 +4,13 @@ import {
|
|||
PluginSettingTab,
|
||||
App,
|
||||
Setting,
|
||||
ButtonComponent
|
||||
ButtonComponent,
|
||||
Editor,
|
||||
EditorPosition
|
||||
} from 'obsidian';
|
||||
import { StateField, StateEffect, Extension, EditorState, Transaction } from '@codemirror/state';
|
||||
import { EditorView, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
interface CollapsibleCodeBlockSettings {
|
||||
defaultCollapsed: boolean;
|
||||
|
|
@ -15,6 +20,11 @@ interface CollapsibleCodeBlockSettings {
|
|||
collapsedLines: number;
|
||||
}
|
||||
|
||||
interface CodeBlockPosition {
|
||||
startPos: number;
|
||||
endPos: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CollapsibleCodeBlockSettings = {
|
||||
defaultCollapsed: false,
|
||||
collapseIcon: '▼',
|
||||
|
|
@ -23,6 +33,189 @@ const DEFAULT_SETTINGS: CollapsibleCodeBlockSettings = {
|
|||
collapsedLines: 0
|
||||
};
|
||||
|
||||
const toggleFoldEffect = StateEffect.define<{from: number, to: number}>();
|
||||
|
||||
function configureFoldField(settings: CollapsibleCodeBlockSettings) {
|
||||
return StateField.define<DecorationSet>({
|
||||
create() {
|
||||
return Decoration.none
|
||||
},
|
||||
update(folds: DecorationSet, tr: Transaction) {
|
||||
folds = folds.map(tr.changes)
|
||||
|
||||
for (let effect of tr.effects) {
|
||||
if (effect.is(toggleFoldEffect)) {
|
||||
const { from, to } = effect.value
|
||||
let hasFold = false
|
||||
|
||||
folds.between(from, to, () => { hasFold = true })
|
||||
|
||||
if (hasFold) {
|
||||
folds = folds.update({
|
||||
filter: (from, to) => from !== effect.value.from || to !== effect.value.to
|
||||
})
|
||||
} else {
|
||||
const deco = Decoration.replace({
|
||||
block: true,
|
||||
inclusive: true,
|
||||
widget: new class extends WidgetType {
|
||||
toDOM(view: EditorView) {
|
||||
const container = document.createElement('div')
|
||||
container.className = 'code-block-folded'
|
||||
container.style.setProperty('--collapsed-lines', settings.collapsedLines.toString())
|
||||
|
||||
const contentDiv = document.createElement('div')
|
||||
contentDiv.className = 'folded-content'
|
||||
const lines = view.state.doc.sliceString(from, to).split('\n')
|
||||
.slice(0, settings.collapsedLines)
|
||||
.join('\n')
|
||||
contentDiv.textContent = lines
|
||||
|
||||
const button = document.createElement('div')
|
||||
button.className = 'code-block-toggle'
|
||||
button.textContent = settings.expandIcon
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault()
|
||||
view.dispatch({
|
||||
effects: toggleFoldEffect.of({from, to})
|
||||
})
|
||||
}
|
||||
|
||||
container.appendChild(button)
|
||||
container.appendChild(contentDiv)
|
||||
return container
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
folds = folds.update({
|
||||
add: [deco.range(from, to)]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return folds
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f)
|
||||
})
|
||||
}
|
||||
|
||||
const foldField = configureFoldField(DEFAULT_SETTINGS);
|
||||
|
||||
class FoldWidget extends WidgetType {
|
||||
constructor(readonly startPos: number, readonly endPos: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(view: EditorView) {
|
||||
const button = document.createElement('div');
|
||||
button.className = 'code-block-toggle';
|
||||
|
||||
let isFolded = false;
|
||||
try {
|
||||
const field = view.state.field(foldField as StateField<DecorationSet>);
|
||||
field.between(this.startPos, this.endPos, () => {
|
||||
isFolded = true;
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Field not found:", e);
|
||||
}
|
||||
|
||||
button.innerHTML = isFolded ? '▶' : '▼';
|
||||
button.setAttribute('aria-label', isFolded ? 'Expand code block' : 'Collapse code block');
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
view.dispatch({
|
||||
effects: toggleFoldEffect.of({
|
||||
from: this.startPos,
|
||||
to: this.endPos
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
eq(other: FoldWidget) {
|
||||
return other.startPos === this.startPos && other.endPos === this.endPos;
|
||||
}
|
||||
}
|
||||
|
||||
const codeBlockPositions = StateField.define<CodeBlockPosition[]>({
|
||||
create(state: EditorState): CodeBlockPosition[] {
|
||||
return findCodeBlockPositions(state);
|
||||
},
|
||||
update(value: CodeBlockPosition[], tr) {
|
||||
return findCodeBlockPositions(tr.state);
|
||||
}
|
||||
});
|
||||
|
||||
function buildDecorations(state: EditorState): DecorationSet {
|
||||
const widgets: any[] = [];
|
||||
const positions = state.field(codeBlockPositions);
|
||||
|
||||
positions.forEach(pos => {
|
||||
const widget = Decoration.widget({
|
||||
widget: new FoldWidget(pos.startPos, pos.endPos),
|
||||
side: -1
|
||||
});
|
||||
widgets.push(widget.range(pos.startPos));
|
||||
});
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
function findCodeBlockPositions(state: EditorState): CodeBlockPosition[] {
|
||||
const positions: CodeBlockPosition[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter: (node) => {
|
||||
const nodeName = node.type.name;
|
||||
if (nodeName.includes("HyperMD-codeblock-begin")) {
|
||||
const line = state.doc.lineAt(node.from);
|
||||
if (line.text.trim().startsWith('```')) {
|
||||
let endFound = false;
|
||||
|
||||
for (let i = line.number; i <= state.doc.lines; i++) {
|
||||
const currentLine = state.doc.line(i);
|
||||
if (i !== line.number && currentLine.text.trim().startsWith('```')) {
|
||||
positions.push({
|
||||
startPos: line.from,
|
||||
endPos: currentLine.to
|
||||
});
|
||||
endFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!endFound) {
|
||||
positions.push({
|
||||
startPos: line.from,
|
||||
endPos: state.doc.line(state.doc.lines).to
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
const decorations = StateField.define<DecorationSet>({
|
||||
create(state: EditorState): DecorationSet {
|
||||
return buildDecorations(state);
|
||||
},
|
||||
update(value: DecorationSet, transaction): DecorationSet {
|
||||
return buildDecorations(transaction.state);
|
||||
},
|
||||
provide(field: StateField<DecorationSet>): Extension {
|
||||
return EditorView.decorations.from(field);
|
||||
}
|
||||
});
|
||||
|
||||
export default class CollapsibleCodeBlockPlugin extends Plugin {
|
||||
private contentObserver: MutationObserver;
|
||||
public settings: CollapsibleCodeBlockSettings;
|
||||
|
|
@ -31,7 +224,12 @@ export default class CollapsibleCodeBlockPlugin extends Plugin {
|
|||
await this.loadSettings();
|
||||
this.updateScrollSetting();
|
||||
|
||||
// Single content observer for the entire preview
|
||||
this.registerEditorExtension([
|
||||
codeBlockPositions,
|
||||
decorations,
|
||||
configureFoldField(this.settings)
|
||||
]);
|
||||
|
||||
this.contentObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === 'childList') {
|
||||
|
|
@ -104,92 +302,48 @@ export default class CollapsibleCodeBlockPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private updateCodeBlockVisibility(pre: HTMLElement, forceRefresh: boolean = false) {
|
||||
const isCollapsed = pre.classList.contains('collapsed');
|
||||
|
||||
// Update following elements' visibility and positioning
|
||||
let elements: HTMLElement[] = [];
|
||||
let curr = pre.nextElementSibling;
|
||||
|
||||
while (curr && !(curr instanceof HTMLPreElement)) {
|
||||
if (curr instanceof HTMLElement) {
|
||||
elements.push(curr);
|
||||
if (!curr.dataset.originalDisplay) {
|
||||
curr.dataset.originalDisplay = getComputedStyle(curr).display;
|
||||
}
|
||||
}
|
||||
curr = curr.nextElementSibling;
|
||||
}
|
||||
const isCollapsed = pre.classList.contains('collapsed');
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!markdownView?.previewMode?.containerEl) return;
|
||||
|
||||
// Only perform the layout refresh if forceRefresh is true
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView?.previewMode?.containerEl && forceRefresh) {
|
||||
const previewElement = markdownView.previewMode.containerEl;
|
||||
const rect = pre.getBoundingClientRect();
|
||||
const scrollTop = previewElement.scrollTop;
|
||||
const elementTop = rect.top + scrollTop;
|
||||
|
||||
// Immediately update visibility classes
|
||||
elements.forEach(el => {
|
||||
if (isCollapsed) {
|
||||
el.classList.add('element-hidden');
|
||||
el.classList.remove('element-visible', 'element-spacing');
|
||||
} else {
|
||||
el.classList.remove('element-hidden');
|
||||
el.classList.add('element-visible');
|
||||
|
||||
const preRect = pre.getBoundingClientRect();
|
||||
const elRect = el.getBoundingClientRect();
|
||||
if (elRect.top < preRect.bottom) {
|
||||
document.documentElement.style.setProperty('--element-spacing', `${preRect.bottom - elRect.top + 10}px`);
|
||||
el.classList.add('element-spacing');
|
||||
let curr = pre.nextElementSibling;
|
||||
while (curr && !(curr instanceof HTMLPreElement)) {
|
||||
if (curr instanceof HTMLElement) {
|
||||
if (isCollapsed) {
|
||||
curr.classList.add('element-hidden');
|
||||
curr.classList.remove('element-visible', 'element-spacing');
|
||||
} else {
|
||||
curr.classList.remove('element-hidden');
|
||||
curr.classList.add('element-visible');
|
||||
}
|
||||
}
|
||||
});
|
||||
curr = curr.nextElementSibling;
|
||||
}
|
||||
|
||||
// Force immediate viewport recalculation
|
||||
void previewElement.offsetHeight;
|
||||
void pre.offsetHeight;
|
||||
|
||||
// Get current scroll position and element position
|
||||
const currentScroll = previewElement.scrollTop;
|
||||
const preRect = pre.getBoundingClientRect();
|
||||
const isAtTop = preRect.top <= 100; // Check if code block is near the top
|
||||
|
||||
// Optimized scroll sequence
|
||||
const scrollSequence = async () => {
|
||||
// If at top, scroll down first then back up
|
||||
if (isAtTop) {
|
||||
previewElement.scrollTop = Math.min(500, previewElement.scrollHeight / 2);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
previewElement.scrollTop = 0;
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
// Quick scroll through content
|
||||
const scrollPoints = [0, previewElement.scrollHeight / 2, currentScroll];
|
||||
|
||||
for (const scrollPos of scrollPoints) {
|
||||
previewElement.scrollTop = scrollPos;
|
||||
previewElement.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
// Final layout adjustments
|
||||
const triggerReflow = async () => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const originalScroll = previewElement.scrollTop;
|
||||
previewElement.scrollTop = Math.max(0, previewElement.scrollHeight - previewElement.clientHeight);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
previewElement.scrollTop = originalScroll;
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
previewElement.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
};
|
||||
|
||||
// Execute scroll sequence
|
||||
scrollSequence();
|
||||
} else {
|
||||
// If not forcing refresh, just update the visibility classes
|
||||
elements.forEach(el => {
|
||||
if (isCollapsed) {
|
||||
el.classList.add('element-hidden');
|
||||
el.classList.remove('element-visible', 'element-spacing');
|
||||
} else {
|
||||
el.classList.remove('element-hidden');
|
||||
el.classList.add('element-visible');
|
||||
}
|
||||
});
|
||||
triggerReflow();
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollSetting(): void {
|
||||
document.body.classList.toggle('horizontal-scroll', this.settings.enableHorizontalScroll);
|
||||
|
|
@ -205,14 +359,12 @@ export default class CollapsibleCodeBlockPlugin extends Plugin {
|
|||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...loadedData,
|
||||
// Sanitize icons when loading
|
||||
collapseIcon: this.sanitizeIcon(loadedData?.collapseIcon ?? DEFAULT_SETTINGS.collapseIcon),
|
||||
expandIcon: this.sanitizeIcon(loadedData?.expandIcon ?? DEFAULT_SETTINGS.expandIcon)
|
||||
};
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
// Sanitize before saving
|
||||
this.settings.collapseIcon = this.sanitizeIcon(this.settings.collapseIcon);
|
||||
this.settings.expandIcon = this.sanitizeIcon(this.settings.expandIcon);
|
||||
await this.saveData(this.settings);
|
||||
|
|
|
|||
252
old/main.js.old.js
Normal file
252
old/main.js.old.js
Normal file
File diff suppressed because one or more lines are too long
14
old/manifest.json
Normal file
14
old/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "collapsible-code-blocks",
|
||||
"name": "Collapsible Code Blocks",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Makes code blocks collapsible in preview mode as well as enabling scroll-able code blocks.",
|
||||
"author": "Bradley Wyatt",
|
||||
"authorUrl": "https://github.com/bwya77",
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "buymeacoffee.com/bwya77",
|
||||
"GitHub Sponsor": "https://github.com/sponsors/bwya77"
|
||||
},
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
6
old/package-lock.json
generated
Normal file
6
old/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "collapsible-codeblock",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
85
old/styles.css
Normal file
85
old/styles.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* 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;
|
||||
}
|
||||
85
styles copy.css
Normal file
85
styles copy.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* 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;
|
||||
}
|
||||
53
styles.css
53
styles.css
|
|
@ -22,26 +22,25 @@
|
|||
|
||||
.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;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.code-block-toggle:hover {
|
||||
color: var(--text-normal);
|
||||
opacity: 1;
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.code-block-toggle:focus-visible {
|
||||
|
|
@ -82,4 +81,32 @@ body.horizontal-scroll .markdown-preview-view pre:not(.collapsed) code {
|
|||
:root {
|
||||
--element-spacing: 0px;
|
||||
--collapsed-lines: 1;
|
||||
}
|
||||
|
||||
.code-block-folded {
|
||||
min-height: 3em;
|
||||
padding: 1.5em 1em 1em 48px;
|
||||
background: var(--code-background);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--code-size);
|
||||
line-height: var(--line-height-tight);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.HyperMD-codeblock {
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.code-block-folded .folded-content {
|
||||
max-height: calc(var(--collapsed-lines, 1) * 1.5em);
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.code-block-folded .code-block-toggle {
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
}
|
||||
Loading…
Reference in a new issue