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;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the title of the callout.
|
/// Gets or sets the title of the callout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -16,10 +16,6 @@ internal sealed class CalloutInlineParser : InlineParser
|
||||||
private static readonly MethodInfo ReplaceParentContainerMethod =
|
private static readonly MethodInfo ReplaceParentContainerMethod =
|
||||||
typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CalloutInlineParser" /> class.
|
/// Initializes a new instance of the <see cref="CalloutInlineParser" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -70,10 +66,19 @@ internal sealed class CalloutInlineParser : InlineParser
|
||||||
current = slice.NextChar(); // skip ]
|
current = slice.NextChar(); // skip ]
|
||||||
start = slice.Start;
|
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)
|
var callout = new CalloutBlock(type)
|
||||||
{
|
{
|
||||||
|
Foldable = fold,
|
||||||
Span = quoteBlock.Span,
|
Span = quoteBlock.Span,
|
||||||
TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end),
|
TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end),
|
||||||
Line = quoteBlock.Line,
|
Line = quoteBlock.Line,
|
||||||
|
@ -86,7 +91,7 @@ internal sealed class CalloutInlineParser : InlineParser
|
||||||
return true;
|
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();
|
using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder();
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,13 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
|
||||||
|
|
||||||
var typeString = type.ToString().ToLowerInvariant();
|
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("<div class=\"callout-title\"><i data-lucide=\"");
|
||||||
renderer.Write(lucideClass);
|
renderer.Write(lucideClass);
|
||||||
renderer.Write("\"></i> ");
|
renderer.Write("\"></i> ");
|
||||||
|
@ -75,9 +81,16 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
|
||||||
string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title;
|
string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title;
|
||||||
WriteTitle(renderer, pipeline, calloutTitle);
|
WriteTitle(renderer, pipeline, calloutTitle);
|
||||||
|
|
||||||
|
if (block.Foldable)
|
||||||
|
{
|
||||||
|
renderer.Write("<span class=\"callout-fold\"><i data-lucide=\"chevron-down\"></i></span>");
|
||||||
|
}
|
||||||
|
|
||||||
renderer.WriteLine("</div>");
|
renderer.WriteLine("</div>");
|
||||||
|
renderer.Write("<div class=\"callout-content\">");
|
||||||
renderer.WriteChildren(block);
|
renderer.WriteChildren(block);
|
||||||
renderer.WriteLine("</div>");
|
renderer.WriteLine("</div>");
|
||||||
|
renderer.WriteLine("</div>");
|
||||||
renderer.EnsureLine();
|
renderer.EnsureLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,45 @@ $callout-fg-grey: #9e9e9e;
|
||||||
margin-bottom: 0;
|
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"] {
|
&[data-callout="note"] {
|
||||||
background-color: $callout-bg-blue;
|
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 Input from "./Input";
|
||||||
import Author from "./Author";
|
import Author from "./Author";
|
||||||
import BlogPost from "./BlogPost";
|
import BlogPost from "./BlogPost";
|
||||||
|
import Callout from "./Callout";
|
||||||
|
|
||||||
declare const Handlebars: any;
|
declare const Handlebars: any;
|
||||||
declare const Prism: any;
|
declare const Prism: any;
|
||||||
declare const lucide: any;
|
declare const lucide: any;
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
Callout.foldAll();
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
Prism.languages.extend('markup', {});
|
Prism.languages.extend('markup', {});
|
||||||
|
|
Loading…
Reference in New Issue