diff --git a/OliverBooth/Markdown/Callout/CalloutBlock.cs b/OliverBooth/Markdown/Callout/CalloutBlock.cs index b640fb0..5c01675 100644 --- a/OliverBooth/Markdown/Callout/CalloutBlock.cs +++ b/OliverBooth/Markdown/Callout/CalloutBlock.cs @@ -17,6 +17,12 @@ internal sealed class CalloutBlock : QuoteBlock Type = type; } + /// + /// Gets or sets a value indicating whether this callout is foldable. + /// + /// if this callout is foldable; otherwise, . + public bool Foldable { get; set; } + /// /// Gets or sets the title of the callout. /// diff --git a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs b/OliverBooth/Markdown/Callout/CalloutInlineParser.cs index 5ded38e..ce7ddc5 100644 --- a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs +++ b/OliverBooth/Markdown/Callout/CalloutInlineParser.cs @@ -16,10 +16,6 @@ internal sealed class CalloutInlineParser : InlineParser private static readonly MethodInfo ReplaceParentContainerMethod = typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!; - // but we can at least make it a bit nicer to access - private static readonly Action ReplaceParentContainer = - (processor, before, after) => ReplaceParentContainerMethod.Invoke(processor, [before, after]); - /// /// Initializes a new instance of the class. /// @@ -70,10 +66,19 @@ internal sealed class CalloutInlineParser : InlineParser current = slice.NextChar(); // skip ] start = slice.Start; - ReadTitle(current, ref slice, out StringSlice title, start, out end); + bool fold = false; + if (current == '-') + { + fold = true; + current = slice.NextChar(); // skip - + start = slice.Start; + } + + ReadTitle(current, ref slice, out StringSlice title, out end); var callout = new CalloutBlock(type) { + Foldable = fold, Span = quoteBlock.Span, TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end), Line = quoteBlock.Line, @@ -86,7 +91,7 @@ internal sealed class CalloutInlineParser : InlineParser return true; } - private static void ReadTitle(char startChar, ref StringSlice slice, out StringSlice title, int start, out int end) + private static void ReadTitle(char startChar, ref StringSlice slice, out StringSlice title, out int end) { using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); diff --git a/OliverBooth/Markdown/Callout/CalloutRenderer.cs b/OliverBooth/Markdown/Callout/CalloutRenderer.cs index 31d3a8a..f2e4f77 100644 --- a/OliverBooth/Markdown/Callout/CalloutRenderer.cs +++ b/OliverBooth/Markdown/Callout/CalloutRenderer.cs @@ -67,7 +67,13 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer var typeString = type.ToString().ToLowerInvariant(); - renderer.Write($"
"); + renderer.Write($"
'); renderer.Write("
"); @@ -75,9 +81,16 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title; WriteTitle(renderer, pipeline, calloutTitle); + if (block.Foldable) + { + renderer.Write(""); + } + renderer.WriteLine("
"); + renderer.Write("
"); renderer.WriteChildren(block); renderer.WriteLine("
"); + renderer.WriteLine("
"); renderer.EnsureLine(); } diff --git a/src/scss/markdown-callouts.scss b/src/scss/markdown-callouts.scss index a200195..558c608 100644 --- a/src/scss/markdown-callouts.scss +++ b/src/scss/markdown-callouts.scss @@ -29,6 +29,45 @@ $callout-fg-grey: #9e9e9e; margin-bottom: 0; } + &.collapsible { + .callout-fold { + transform: rotate(180deg); + transition: transform 500ms; + margin-left: 0.5em; + + svg { + transform: rotate(180deg); + transition: transform 500ms; + } + } + + .callout-title { + cursor: pointer; + transition: margin-bottom 500ms; + } + + .callout-content { + opacity: 1; + max-height: 500px; + transition: opacity 500ms, max-height 500ms; + } + + &.collapsed { + .callout-title { + margin-bottom: 0; + } + + .callout-fold svg { + transform: rotate(0deg); + } + + .callout-content { + opacity: 0; + max-height: 0; + } + } + } + &[data-callout="note"] { background-color: $callout-bg-blue; diff --git a/src/ts/Callout.ts b/src/ts/Callout.ts new file mode 100644 index 0000000..125f034 --- /dev/null +++ b/src/ts/Callout.ts @@ -0,0 +1,63 @@ +class Callout { + private readonly _callout: HTMLElement; + private readonly _title: HTMLElement; + private readonly _content: HTMLElement; + private _foldEnabled: boolean; + + constructor(element: HTMLElement) { + this._callout = element; + this._title = element.querySelector(".callout-title"); + this._content = element.querySelector(".callout-content"); + } + + public static foldAll(element?: HTMLElement): void { + element = element || document.body; + this.findAll(element).forEach(c => c.fold()); + } + + public static findAll(element?: HTMLElement): Array { + element = element || document.body; + return Array.from(element.querySelectorAll("div.callout")).map(c => { + return new Callout(c as HTMLElement); + }); + } + + public get content(): HTMLElement { + return this._content; + } + + public get element(): HTMLElement { + return this._callout; + } + + public get isFoldable(): boolean { + const fold: string = this._callout.dataset.calloutFold; + return fold !== null && fold !== undefined; + } + + public get title(): HTMLElement { + return this._title; + } + + public fold(): void { + if (this._foldEnabled || !this.isFoldable) { + return; + } + + const callout: HTMLElement = this._callout; + + if (callout === null) { + console.error("Callout element for ", this, " is null!"); + return; + } + + callout.classList.add("collapsible", "collapsed"); + this._title.addEventListener("click", () => { + callout.classList.toggle("collapsed"); + }); + + this._foldEnabled = true; + } +} + +export default Callout; \ No newline at end of file diff --git a/src/ts/app.ts b/src/ts/app.ts index 472fc86..ef56897 100644 --- a/src/ts/app.ts +++ b/src/ts/app.ts @@ -3,12 +3,14 @@ import UI from "./UI"; import Input from "./Input"; import Author from "./Author"; import BlogPost from "./BlogPost"; +import Callout from "./Callout"; declare const Handlebars: any; declare const Prism: any; declare const lucide: any; (() => { + Callout.foldAll(); lucide.createIcons(); Prism.languages.extend('markup', {});