bibcite
Version:
Citations with Bibliography
165 lines (145 loc) • 5.38 kB
text/typescript
import { Data } from "csl-json";
import { Bibliography } from "./bibliography";
import { BibReference } from "./bibReference";
import { Citation } from "./citation";
import { CiteStyle } from "./styles/types";
import { styles, fallbackStyle } from "./styles";
export class BibController extends HTMLElement {
_scope; // only control citations which are children of this scope
_citeStyle: CiteStyle;
_bibliography: Promise<Bibliography>;
constructor() {
super();
}
listeners = {
// addEventListener used on these in connectedCallback
async CitationAdded(event: CustomEvent) {
console.log("[BibController] caught CitationAdded event");
const bib = await this.bibliography;
const citation: Citation = event.detail.element;
// so that the Citation can fire CitationRemoved on me even when disconnected
citation.myController = this;
event.stopImmediatePropagation(); // monotheism
citation.citeStyle = this.citeStyle;
bib.registerCitation(event.detail.element);
},
async CitationRemoved(event: CustomEvent) {
console.log("[BibController] caught CitationRemoved event");
const bib = await this.bibliography;
bib.unregisterCitation(event.detail.element);
},
async ReferenceAdded(event: CustomEvent) {
console.log("[BibController] caught ReferenceAdded event");
const bib: Bibliography = await this.bibliography;
const bibReference: BibReference = event.detail.element;
// so that ReferenceRemoved can fire on me even when disconnected
bibReference.myController = this;
event.stopImmediatePropagation(); // monotheism
bibReference.citeStyle = this.citeStyle;
bib.registerReferenceList(event.detail.element);
},
async ReferenceRemoved(event: CustomEvent) {
console.log("[BibController] caught ReferenceRemoved event");
const bib = await this.bibliography;
bib.unregisterReferenceList(event.detail.element);
},
};
connectedCallback() {
console.log("[BibController] connectedCallback");
this._scope =
this.parentElement === document.head ? document : this.parentElement;
for (const [functionName, callback] of Object.entries(this.listeners)) {
console.log(`|- addEventListener: ${functionName}`);
this._scope.addEventListener(functionName, callback.bind(this));
}
}
disconnectedCallback() {
console.log("[BibController] disconnectedCallback");
for (const [functionName, callback] of Object.entries(this.listeners)) {
console.log(`removeEventListener: ${functionName}`);
this._scope.removeEventListener(functionName, callback.bind(this));
}
}
static get observedAttributes() {
return ["bib", "citation-style"];
}
async attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
) {
console.log(
`[BibController] attributeChangedCallback(${name}, ${oldValue}, ${newValue})`
);
switch (name) {
case "bib":
if (!this._bibliography) {
this.bibliography; // getter for the initialization side-effect
} else {
(await this.bibliography).bib = await this.csl_from_attribute(
newValue
);
}
break;
case "citation-style":
this.citeStyle = styles.get(newValue) || fallbackStyle;
break;
default:
console.error(
`[BibController] attributeChangedCallback(${name}, ${oldValue}, ${newValue})` +
`called with unknown attribute ${name}`
);
}
}
set innerHTML(value: string) {
console.log("[BibController] set innerHTML");
super.innerHTML = value;
if (!this.getAttribute("bib")) {
// attribute has priority
this.bibliography.then((res) => (res.bib = JSON.parse(value)));
}
}
async propagateCiteStyle(citeStyle: CiteStyle) {
const bib = <Bibliography>await this.bibliography;
bib.citations.forEach((cit) => (cit.citeStyle = citeStyle));
bib._reference_lists.forEach((ref) => (ref.citeStyle = citeStyle));
}
set citeStyle(value: CiteStyle) {
this._citeStyle = value;
this.propagateCiteStyle(value);
}
get citeStyle() {
if (!this._citeStyle) {
// default: alphabetic
this._citeStyle =
styles.get(this.getAttribute("citation-style")) || fallbackStyle;
}
return this._citeStyle;
}
get bibliography() {
if (!this._bibliography) {
this._bibliography = this.csl_from_attribute(
this.getAttribute("bib")
).then((csl) => new Bibliography(csl, this.citeStyle.order.comparison));
}
return this._bibliography;
}
async csl_from_attribute(bib_attr: string): Promise<Data[]> {
// TODO: add option for bib to be callable?
if (bib_attr) {
console.log("|- bib attribute present -> fetching");
const response = await fetch(bib_attr);
return await response.json();
} else if (document.readyState == "loading") {
console.log("|- bib attribute missing -> waiting for innerHTML");
return new Promise((resolve, _) => {
document.addEventListener("DOMContentLoaded", () =>
resolve(this.innerHTML)
);
}).then(JSON.parse);
} else {
console.log("|- bib attribute missing -> using innerHTML");
return JSON.parse(this.innerHTML);
}
}
}