Files
WorkNote/.obsidian/plugins/obsidian-webpage-export-master/assets/webpage.ts
2025-04-10 14:07:13 +08:00

310 lines
8.5 KiB
TypeScript

class Website
{
public static website: Website;
public static isLoaded: boolean = false;
public static loadedDocument: WebpageDocument | null = null;
public static bodyEl: HTMLElement;
public static webpageEl: HTMLElement;
public static async init()
{
Website.website = new Website();
window.addEventListener("load", () => Website.onInit());
}
private static onInit()
{
let docEl = document.querySelector(".document-container") as HTMLElement;
this.loadedDocument = new WebpageDocument(docEl, Website.website, DocumentType.Markdown);
this.bodyEl = document.body as HTMLElement;
this.webpageEl = document.querySelector(".webpage-container") as HTMLElement;
this.bodyEl.classList.toggle("loading", false);
this.bodyEl.classList.toggle("loaded", true);
this.isLoaded = true;
console.log("loaded");
}
}
Website.init();
class Header
{
public level: number;
public id: string;
public text: string;
public isCollapsed: boolean;
public isVisible: boolean = true;
public parent: Header | null;
public childHeaders: Header[];
public children: HTMLElement[];
public nextHeader: Header | null;
public previousHeader: Header | null;
public wrapperEl: HTMLElement;
public headingEl: HTMLHeadingElement;
public childrenEl: HTMLElement;
public collapseEl: HTMLElement | null;
public containingSizer: HTMLElement | null;
public document: WebpageDocument;
constructor(wrapperEl: HTMLElement, document: WebpageDocument)
{
this.wrapperEl = wrapperEl;
this.headingEl = wrapperEl.querySelector(".heading") as HTMLHeadingElement;
this.childrenEl = wrapperEl.querySelector(".heading-children") as HTMLElement;
this.collapseEl = wrapperEl.querySelector(".heading-collapse-indicator");
this.containingSizer = wrapperEl.closest(".markdown-preview-sizer") ?? wrapperEl.closest(".view-content");
if (this.headingEl == null || this.childrenEl == null) throw new Error("Invalid header element");
this.level = parseInt(this.headingEl.tagName.substring(1));
this.id = this.headingEl.id;
this.text = this.headingEl.textContent ?? "";
this.isCollapsed = this.wrapperEl.classList.contains("is-collapsed");
this.document = document;
this.document.headers.push(this);
this.childHeaders = [];
this.children = [];
this.childrenEl.childNodes.forEach((child) =>
{
if (child instanceof HTMLElement)
{
if(child.classList.contains("heading-wrapper"))
{
let header = new Header(child, document);
header.parent = this;
this.childHeaders.push(header);
}
this.children.push(child);
}
});
if (this.parent)
{
let index = this.parent.childHeaders.indexOf(this);
this.previousHeader = this.parent.childHeaders[index - 1] ?? null;
this.nextHeader = this.parent.childHeaders[index + 1] ?? null;
}
let localThis = this;
this.collapseEl?.addEventListener("click", function ()
{
localThis.toggle();
});
}
private collapseTimeout: number | null = null;
private collapseHeight: number = 0;
private forceShown: boolean = false;
public async collapse(collapse: boolean, openParents = true, instant = false)
{
if (openParents && !collapse)
{
if (this.parent) this.parent.collapse(false, true, instant);
}
let needsChange = this.isCollapsed != collapse;
if (!needsChange)
{
// if opening show the header
if (!collapse && this.document?.documentType == DocumentType.Canvas) this.show(true);
return;
}
if (this.collapseTimeout)
{
clearTimeout(this.collapseTimeout);
this.childrenEl.style.transitionDuration = "";
this.childrenEl.style.height = "";
this.wrapperEl.classList.toggle("is-animating", false);
}
if (collapse)
{
this.collapseHeight = this.childrenEl.offsetHeight + parseFloat(this.children[this.children.length - 1]?.style.marginBottom || "0");
// show all sibling headers after this one
// this is so that when the header slides down you aren't left with a blank space
let next = this.nextHeader;
while (next && this.document.documentType == DocumentType.Canvas)
{
let localNext = next;
// force show the sibling header for 500ms while this one is collapsing
localNext.show(false, true, true);
setTimeout(function()
{
localNext.forceShown = false;
}, 500);
next = next.nextHeader;
}
}
let height = this.collapseHeight;
this.childrenEl.style.height = height + "px";
// if opening show the header
if (!collapse && this.document.documentType == DocumentType.Canvas) this.show(true);
this.isCollapsed = collapse;
if (instant)
{
console.log("instant");
this.childrenEl.style.transitionDuration = "0s";
this.wrapperEl.classList.toggle("is-collapsed", collapse);
this.childrenEl.style.height = "";
this.childrenEl.style.transitionDuration = "";
let newTotalHeight = Array.from(this.containingSizer?.children ?? []).reduce((acc, cur: HTMLElement) => acc + cur.offsetHeight, 0);
if(this.containingSizer) this.containingSizer.style.minHeight = newTotalHeight + "px";
return;
}
// get the length of the height transition on heading container and wait for that time before not displaying the contents
let transitionDuration: string | number = getComputedStyle(this.childrenEl).transitionDuration;
if (transitionDuration.endsWith("s")) transitionDuration = parseFloat(transitionDuration);
else if (transitionDuration.endsWith("ms")) transitionDuration = parseFloat(transitionDuration) / 1000;
else transitionDuration = 0;
// multiply the duration by the height so that the transition is the same speed regardless of the height of the header
let transitionDurationMod = Math.min(transitionDuration * Math.sqrt(height) / 16, 0.5); // longest transition is 0.5s
this.childrenEl.style.transitionDuration = `${transitionDurationMod}s`;
if (collapse) this.childrenEl.style.height = "0px";
else this.childrenEl.style.height = height + "px";
this.wrapperEl.classList.toggle("is-animating", true);
this.wrapperEl.classList.toggle("is-collapsed", collapse);
let localThis = this;
setTimeout(function()
{
localThis.childrenEl.style.transitionDuration = "";
if(!collapse) localThis.childrenEl.style.height = "";
localThis.wrapperEl.classList.toggle("is-animating", false);
let newTotalHeight = Array.from(localThis.containingSizer?.children ?? []).reduce((acc, cur: HTMLElement) => acc + cur.offsetHeight, 0);
if(localThis.containingSizer) localThis.containingSizer.style.minHeight = newTotalHeight + "px";
}, transitionDurationMod * 1000);
}
/**Restores a hidden header back to it's normal function */
public show(showParents:boolean = false, showChildren:boolean = false, forceStay:boolean = false)
{
if (forceStay) this.forceShown = true;
if (showParents)
{
if (this.parent) this.parent.show(true, false, forceStay);
}
if (showChildren)
{
this.childHeaders.forEach((header) =>
{
header.show(false, true, forceStay);
});
}
if(this.isVisible || this.isCollapsed) return;
this.wrapperEl.classList.toggle("is-hidden", false);
this.wrapperEl.style.height = "";
this.wrapperEl.style.visibility = "";
this.isVisible = true;
}
public toggle(openParents = true)
{
this.collapse(!this.isCollapsed, openParents);
}
/**Hides everything in a header and then makes the header div take up the same space as the header element */
public hide()
{
if(this.forceShown) return;
if(!this.isVisible || this.isCollapsed) return;
if(this.wrapperEl.style.display == "none") return;
let height = this.wrapperEl.offsetHeight;
this.wrapperEl.classList.toggle("is-hidden", true);
if (height != 0) this.wrapperEl.style.height = height + "px";
this.wrapperEl.style.visibility = "hidden";
this.isVisible = false;
}
}
class Tree
{
}
class TreeItem
{
}
class Canvas
{
}
class Sidebar
{
}
class SidebarGutter
{
}
export enum DocumentType
{
Markdown,
Canvas,
Embed,
Excalidraw,
Kanban,
Other
}
class WebpageDocument
{
public headers: Header[];
public website: Website;
public documentType: DocumentType;
public documentEl: HTMLElement;
public constructor(documentEl: HTMLElement, website: Website, documentType: DocumentType)
{
this.documentEl = documentEl;
this.website = website;
this.documentType = documentType;
this.headers = [];
// only create top level headers, because headers create their own children
this.documentEl.querySelectorAll(".heading-wrapper:not(:is(.heading-children .heading-wrapper))").forEach((headerEl) =>
{
new Header(headerEl as HTMLElement, this); // headers add themselves to the document
});
}
}