feat: add support for collapsible callouts
This commit is contained in:
parent
29ed46eb9e
commit
7ede8b13fa
|
@ -17,6 +17,12 @@ internal sealed class CalloutBlock : QuoteBlock
|
|||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this callout is foldable.
|
||||
/// </summary>
|
||||
/// <value><see langword="true" /> if this callout is foldable; otherwise, <see langword="false" />.</value>
|
||||
public bool Foldable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the callout.
|
||||
/// </summary>
|
||||
|
|
|
@ -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<InlineProcessor, ContainerBlock, ContainerBlock> ReplaceParentContainer =
|
||||
(processor, before, after) => ReplaceParentContainerMethod.Invoke(processor, [before, after]);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CalloutInlineParser" /> class.
|
||||
/// </summary>
|
||||
|
@ -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();
|
||||
|
||||
|
|
|
@ -67,7 +67,13 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
|
|||
|
||||
var typeString = type.ToString().ToLowerInvariant();
|
||||
|
||||
renderer.Write($"<div class=\"callout\" data-callout=\"{typeString}\">");
|
||||
renderer.Write($"<div class=\"callout\" data-callout=\"{typeString}\"");
|
||||
if (block.Foldable)
|
||||
{
|
||||
renderer.Write(" data-callout-fold=\"true\"");
|
||||
}
|
||||
|
||||
renderer.Write('>');
|
||||
renderer.Write("<div class=\"callout-title\"><i data-lucide=\"");
|
||||
renderer.Write(lucideClass);
|
||||
renderer.Write("\"></i> ");
|
||||
|
@ -75,9 +81,16 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
|
|||
string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title;
|
||||
WriteTitle(renderer, pipeline, calloutTitle);
|
||||
|
||||
if (block.Foldable)
|
||||
{
|
||||
renderer.Write("<span class=\"callout-fold\"><i data-lucide=\"chevron-down\"></i></span>");
|
||||
}
|
||||
|
||||
renderer.WriteLine("</div>");
|
||||
renderer.Write("<div class=\"callout-content\">");
|
||||
renderer.WriteChildren(block);
|
||||
renderer.WriteLine("</div>");
|
||||
renderer.WriteLine("</div>");
|
||||
renderer.EnsureLine();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<Callout> {
|
||||
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;
|
|
@ -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', {});
|
||||
|
|
Loading…
Reference in New Issue